From aecbd7cd8982e72f26359ed9a00a77b25e6494d9 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 26 Sep 2021 21:04:11 +0200 Subject: [PATCH] AcroForm: Add support for ResetForm action - it aims to fix #12721. - Thanks to PR #14023, we've now the fieldObjects in the annotation layer so we can easily map fields names on their id if needed. - Reset values in the storage, in the JS sandbox and in the visible html elements. --- src/core/catalog.js | 20 +++- src/display/annotation_layer.js | 155 ++++++++++++++++++++++++++-- src/scripting_api/event.js | 7 ++ src/scripting_api/field.js | 4 + test/integration/annotation_spec.js | 130 +++++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/resetform.pdf | Bin 0 -> 20907 bytes 7 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 test/pdfs/resetform.pdf diff --git a/src/core/catalog.js b/src/core/catalog.js index c63e67d1b..64c214344 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -1328,6 +1328,20 @@ class Catalog { const actionName = actionType.name; switch (actionName) { + case "ResetForm": + const flags = action.get("Flags"); + const include = ((isNum(flags) ? flags : 0) & 1) === 0; + const fields = []; + const refs = []; + for (const obj of action.get("Fields") || []) { + if (isRef(obj)) { + refs.push(obj.toString()); + } else if (isString(obj)) { + fields.push(stringToPDFString(obj)); + } + } + resultObj.resetForm = { fields, refs, include }; + break; case "URI": url = action.get("URI"); if (url instanceof Name) { @@ -1405,11 +1419,7 @@ class Catalog { } /* falls through */ default: - if ( - actionName === "JavaScript" || - actionName === "ResetForm" || - actionName === "SubmitForm" - ) { + if (actionName === "JavaScript" || actionName === "SubmitForm") { // Don't bother the user with a warning for actions that require // scripting support, since those will be handled separately. break; diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index ebf794087..4b677011a 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -429,6 +429,7 @@ class LinkAnnotationElement extends AnnotationElement { parameters.data.dest || parameters.data.action || parameters.data.isTooltipOnly || + parameters.data.resetForm || (parameters.data.actions && (parameters.data.actions.Action || parameters.data.actions["Mouse Up"] || @@ -454,17 +455,25 @@ class LinkAnnotationElement extends AnnotationElement { this._bindNamedAction(link, data.action); } else if (data.dest) { this._bindLink(link, data.dest); - } else if ( - data.actions && - (data.actions.Action || - data.actions["Mouse Up"] || - data.actions["Mouse Down"]) && - this.enableScripting && - this.hasJSActions - ) { - this._bindJSAction(link, data); } else { - this._bindLink(link, ""); + let hasClickAction = false; + if ( + data.actions && + (data.actions.Action || + data.actions["Mouse Up"] || + data.actions["Mouse Down"]) && + this.enableScripting && + this.hasJSActions + ) { + hasClickAction = true; + this._bindJSAction(link, data); + } + + if (data.resetForm) { + this._bindResetFormAction(link, data.resetForm); + } else if (!hasClickAction) { + this._bindLink(link, ""); + } } if (this.quadrilaterals) { @@ -557,6 +566,106 @@ class LinkAnnotationElement extends AnnotationElement { } link.className = "internalLink"; } + + _bindResetFormAction(link, resetForm) { + const otherClickAction = link.onclick; + if (!otherClickAction) { + link.href = this.linkService.getAnchorUrl(""); + } + link.className = "internalLink"; + + if (!this._fieldObjects) { + warn( + `_bindResetFormAction - "resetForm" action not supported, ` + + "ensure that the `fieldObjects` parameter is provided." + ); + if (!otherClickAction) { + link.onclick = () => false; + } + return; + } + + link.onclick = () => { + if (otherClickAction) { + otherClickAction(); + } + + const { + fields: resetFormFields, + refs: resetFormRefs, + include, + } = resetForm; + + const allFields = []; + if (resetFormFields.length !== 0 || resetFormRefs.length !== 0) { + const fieldIds = new Set(resetFormRefs); + for (const fieldName of resetFormFields) { + const fields = this._fieldObjects[fieldName] || []; + for (const { id } of fields) { + fieldIds.add(id); + } + } + for (const fields of Object.values(this._fieldObjects)) { + for (const field of fields) { + if (fieldIds.has(field.id) === include) { + allFields.push(field); + } + } + } + } else { + for (const fields of Object.values(this._fieldObjects)) { + allFields.push(...fields); + } + } + + const storage = this.annotationStorage; + const allIds = []; + for (const field of allFields) { + const { id } = field; + allIds.push(id); + switch (field.type) { + case "text": { + const value = field.defaultValue || ""; + storage.setValue(id, { value, valueAsString: value }); + break; + } + case "checkbox": + case "radiobutton": { + const value = field.defaultValue === field.exportValues; + storage.setValue(id, { value }); + break; + } + case "combobox": + case "listbox": { + const value = field.defaultValue || ""; + storage.setValue(id, { value }); + break; + } + default: + continue; + } + const domElement = document.getElementById(id); + if (!domElement || !GetElementsByNameSet.has(domElement)) { + continue; + } + domElement.dispatchEvent(new Event("resetform")); + } + + if (this.enableScripting) { + // Update the values in the sandbox. + this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { + source: this, + detail: { + id: "app", + ids: allIds, + name: "ResetForm", + }, + }); + } + + return false; + }; + } } class TextAnnotationElement extends AnnotationElement { @@ -804,6 +913,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { ); }); + element.addEventListener("resetform", event => { + const defaultValue = this.data.defaultFieldValue || ""; + element.value = elementData.userValue = defaultValue; + delete elementData.formattedValue; + }); + let blurListener = event => { if (elementData.formattedValue) { event.target.value = elementData.formattedValue; @@ -1057,6 +1172,11 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { storage.setValue(id, { value: checked }); }); + element.addEventListener("resetform", event => { + const defaultValue = data.defaultFieldValue || "Off"; + event.target.checked = defaultValue === data.exportValue; + }); + if (this.enableScripting && this.hasJSActions) { element.addEventListener("updatefromsandbox", jsEvent => { const actions = { @@ -1129,6 +1249,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { storage.setValue(id, { value: checked }); }); + element.addEventListener("resetform", event => { + const defaultValue = data.defaultFieldValue; + event.target.checked = + defaultValue !== null && + defaultValue !== undefined && + defaultValue === data.buttonValue; + }); + if (this.enableScripting && this.hasJSActions) { const pdfButtonValue = data.buttonValue; element.addEventListener("updatefromsandbox", jsEvent => { @@ -1231,6 +1359,13 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { } } + selectElement.addEventListener("resetform", event => { + const defaultValue = this.data.defaultFieldValue; + for (const option of selectElement.options) { + option.selected = option.value === defaultValue; + } + }); + // Insert the options into the choice field. for (const option of this.data.options) { const optionElement = document.createElement("option"); diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index baec46215..a06fb0220 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -79,6 +79,13 @@ class EventDispatcher { baseEvent.actions, baseEvent.pageNumber ); + } else if (id === "app" && baseEvent.name === "ResetForm") { + for (const fieldId of baseEvent.ids) { + const obj = this._objects[fieldId]; + if (obj) { + obj.obj._reset(); + } + } } return; } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index c7de3124e..7fa2b2da9 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -476,6 +476,10 @@ class Field extends PDFObject { return false; } + _reset() { + this.value = this.valueAsString = this.defaultValue; + } + _runActions(event) { const eventName = event.name; if (!this._actions.has(eventName)) { diff --git a/test/integration/annotation_spec.js b/test/integration/annotation_spec.js index 5fcd7901e..c25487cda 100644 --- a/test/integration/annotation_spec.js +++ b/test/integration/annotation_spec.js @@ -221,3 +221,133 @@ describe("Annotation and storage", () => { }); }); }); + +describe("ResetForm action", () => { + describe("resetform.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("resetform.pdf", "[data-annotation-id='63R']"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must reset all fields", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const base = "hello world"; + for (let i = 3; i <= 7; i++) { + await page.type(`#\\36 ${i}R`, base); + } + + const selectors = [69, 71, 75].map( + n => `[data-annotation-id='${n}R']` + ); + for (const selector of selectors) { + await page.click(selector); + } + + await page.select("#\\37 8R", "b"); + await page.select("#\\38 1R", "f"); + + await page.click("[data-annotation-id='82R']"); + await page.waitForFunction( + `document.querySelector("#\\\\36 3R").value === ""` + ); + + for (let i = 3; i <= 8; i++) { + const text = await page.$eval(`#\\36 ${i}R`, el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(""); + } + + const ids = [69, 71, 72, 73, 74, 75, 76, 77]; + for (const id of ids) { + const checked = await page.$eval( + `#\\3${Math.floor(id / 10)} ${id % 10}R`, + el => el.checked + ); + expect(checked).withContext(`In ${browserName}`).toEqual(false); + } + + let selected = await page.$eval( + `#\\37 8R [value="a"]`, + el => el.selected + ); + expect(selected).withContext(`In ${browserName}`).toEqual(true); + + selected = await page.$eval( + `#\\38 1R [value="d"]`, + el => el.selected + ); + expect(selected).withContext(`In ${browserName}`).toEqual(true); + }) + ); + }); + + it("must reset some fields", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const base = "hello world"; + for (let i = 3; i <= 8; i++) { + await page.type(`#\\36 ${i}R`, base); + } + + const selectors = [69, 71, 72, 73, 75].map( + n => `[data-annotation-id='${n}R']` + ); + for (const selector of selectors) { + await page.click(selector); + } + + await page.select("#\\37 8R", "b"); + await page.select("#\\38 1R", "f"); + + await page.click("[data-annotation-id='84R']"); + await page.waitForFunction( + `document.querySelector("#\\\\36 3R").value === ""` + ); + + for (let i = 3; i <= 8; i++) { + const expected = (i - 3) % 2 === 0 ? "" : base; + const text = await page.$eval(`#\\36 ${i}R`, el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(expected); + } + + let ids = [69, 72, 73, 74, 76, 77]; + for (const id of ids) { + const checked = await page.$eval( + `#\\3${Math.floor(id / 10)} ${id % 10}R`, + el => el.checked + ); + expect(checked) + .withContext(`In ${browserName + id}`) + .toEqual(false); + } + + ids = [71, 75]; + for (const id of ids) { + const checked = await page.$eval( + `#\\3${Math.floor(id / 10)} ${id % 10}R`, + el => el.checked + ); + expect(checked).withContext(`In ${browserName}`).toEqual(true); + } + + let selected = await page.$eval( + `#\\37 8R [value="a"]`, + el => el.selected + ); + expect(selected).withContext(`In ${browserName}`).toEqual(true); + + selected = await page.$eval( + `#\\38 1R [value="f"]`, + el => el.selected + ); + expect(selected).withContext(`In ${browserName}`).toEqual(true); + }) + ); + }); + }); +}); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index f30fcadce..0c5accf17 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -385,6 +385,7 @@ !IdentityToUnicodeMap_charCodeOf.pdf !PDFJS-9279-reduced.pdf !issue5481.pdf +!resetform.pdf !issue5567.pdf !issue5701.pdf !issue6769_no_matrix.pdf diff --git a/test/pdfs/resetform.pdf b/test/pdfs/resetform.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8f2c39858017d028d50edd5ca21711826c5e4c08 GIT binary patch literal 20907 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 Bdr|-Z literal 0 HcmV?d00001