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 000000000..8f2c39858 Binary files /dev/null and b/test/pdfs/resetform.pdf differ