diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 6e25f44b633cb..b8fdc21b7a211 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -922,12 +922,17 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { const storage = this.annotationStorage; const data = this.data; const id = data.id; - const value = storage.getValue(id, { + let value = storage.getValue(id, { value: data.fieldValue && ((data.exportValue && data.exportValue === data.fieldValue) || (!data.exportValue && data.fieldValue !== "Off")), }).value; + if (typeof value === "string") { + // The value has been changed through js and set in annotationStorage. + value = value !== "Off"; + storage.setValue(id, { value }); + } this.container.className = "buttonWidgetAnnotation checkBox"; @@ -1012,9 +1017,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { const storage = this.annotationStorage; const data = this.data; const id = data.id; - const value = storage.getValue(id, { + let value = storage.getValue(id, { value: data.fieldValue === data.buttonValue, }).value; + if (typeof value === "string") { + // The value has been changed through js and set in annotationStorage. + value = value !== data.buttonValue; + storage.setValue(id, { value }); + } const element = document.createElement("input"); element.disabled = data.readOnly; diff --git a/src/pdf.sandbox.external.js b/src/pdf.sandbox.external.js index 38b9faaebe93d..3fda62553d56c 100644 --- a/src/pdf.sandbox.external.js +++ b/src/pdf.sandbox.external.js @@ -117,6 +117,12 @@ class SandboxSupportBase { } this.win.alert(cMsg); }, + confirm: cMsg => { + if (typeof cMsg !== "string") { + return false; + } + return this.win.confirm(cMsg); + }, prompt: (cQuestion, cDefault) => { if (typeof cQuestion !== "string" || typeof cDefault !== "string") { return null; diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index ddc0682ca607f..53418eb4fe07a 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -28,8 +28,6 @@ class App extends PDFObject { constructor(data) { super(data); - this.calculate = true; - this._constants = null; this._focusRect = true; this._fs = null; @@ -68,6 +66,7 @@ class App extends PDFObject { this._timeoutCallbackId = 0; this._globalEval = data.globalEval; this._externalCall = data.externalCall; + this._document = data._document; } // This function is called thanks to the proxy @@ -191,6 +190,14 @@ class App extends PDFObject { throw new Error("app.activeDocs is read-only"); } + get calculate() { + return this._document.obj.calculate; + } + + set calculate(calculate) { + this._document.obj.calculate = calculate; + } + get constants() { if (!this._constants) { this._constants = Object.freeze({ @@ -427,7 +434,21 @@ class App extends PDFObject { oDoc = null, oCheckbox = null ) { + if (typeof cMsg === "object") { + nType = cMsg.nType; + cMsg = cMsg.cMsg; + } + cMsg = (cMsg || "").toString(); + nType = + typeof nType !== "number" || isNaN(nType) || nType < 0 || nType > 3 + ? 0 + : nType; + if (nType >= 2) { + return this._externalCall("confirm", [cMsg]) ? 4 : 3; + } + this._externalCall("alert", [cMsg]); + return 1; } beep() { @@ -543,10 +564,21 @@ class App extends PDFObject { } response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { + if (typeof cQuestion === "object") { + cDefault = cQuestion.cDefault; + cQuestion = cQuestion.cQuestion; + } + cQuestion = (cQuestion || "").toString(); + cDefault = (cDefault || "").toString(); return this._externalCall("prompt", [cQuestion, cDefault || ""]); } - setInterval(cExpr, nMilliseconds) { + setInterval(cExpr, nMilliseconds = 0) { + if (typeof cExpr === "object") { + nMilliseconds = cExpr.nMilliseconds || 0; + cExpr = cExpr.cExpr; + } + if (typeof cExpr !== "string") { throw new TypeError("First argument of app.setInterval must be a string"); } @@ -560,7 +592,12 @@ class App extends PDFObject { return this._registerTimeout(callbackId, true); } - setTimeOut(cExpr, nMilliseconds) { + setTimeOut(cExpr, nMilliseconds = 0) { + if (typeof cExpr === "object") { + nMilliseconds = cExpr.nMilliseconds || 0; + cExpr = cExpr.cExpr; + } + if (typeof cExpr !== "string") { throw new TypeError("First argument of app.setTimeOut must be a string"); } diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 96cbfe4133061..c3ca6e2260720 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -820,6 +820,9 @@ class Doc extends PDFObject { } getField(cName) { + if (typeof cName === "object") { + cName = cName.cName; + } if (typeof cName !== "string") { throw new TypeError("Invalid field name: must be a string"); } @@ -852,7 +855,7 @@ class Doc extends PDFObject { } } - return undefined; + return null; } _getChildren(fieldName) { @@ -885,6 +888,9 @@ class Doc extends PDFObject { } getNthFieldName(nIndex) { + if (typeof nIndex === "object") { + nIndex = nIndex.nIndex; + } if (typeof nIndex !== "number") { throw new TypeError("Invalid field index: must be a number"); } @@ -1020,6 +1026,18 @@ class Doc extends PDFObject { bAnnotations = true, printParams = null ) { + if (typeof bUI === "object") { + nStart = bUI.nStart; + nEnd = bUI.nEnd; + bSilent = bUI.bSilent; + bShrinkToFit = bUI.bShrinkToFit; + bPrintAsImage = bUI.bPrintAsImage; + bReverse = bUI.bReverse; + bAnnotations = bUI.bAnnotations; + printParams = bUI.printParams; + bUI = bUI.bUI; + } + // TODO: for now just use nStart and nEnd // so need to see how to deal with the other params // (if possible) @@ -1084,15 +1102,22 @@ class Doc extends PDFObject { } resetForm(aFields = null) { + if (aFields && !Array.isArray(aFields) && typeof aFields === "object") { + aFields = aFields.aFields; + } let mustCalculate = false; if (aFields) { for (const fieldName of aFields) { + if (!fieldName) { + continue; + } const field = this.getField(fieldName); - if (field) { - field.value = field.defaultValue; - field.valueAsString = field.value; - mustCalculate = true; + if (!field) { + continue; } + field.value = field.defaultValue; + field.valueAsString = field.value; + mustCalculate = true; } } else { mustCalculate = this._fields.size !== 0; diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index c49c45302f915..baec46215eac1 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -96,23 +96,33 @@ class EventDispatcher { } } - if (name === "Keystroke") { - savedChange = { - value: event.value, - change: event.change, - selStart: event.selStart, - selEnd: event.selEnd, - }; - } else if (name === "Blur" || name === "Focus") { - Object.defineProperty(event, "value", { - configurable: false, - writable: false, - enumerable: true, - value: event.value, - }); - } else if (name === "Validate") { - this.runValidation(source, event); - return; + switch (name) { + case "Keystroke": + savedChange = { + value: event.value, + change: event.change, + selStart: event.selStart, + selEnd: event.selEnd, + }; + break; + case "Blur": + case "Focus": + Object.defineProperty(event, "value", { + configurable: false, + writable: false, + enumerable: true, + value: event.value, + }); + break; + case "Validate": + this.runValidation(source, event); + return; + case "Action": + this.runActions(source, source, event, name); + if (this._document.obj.calculate) { + this.runCalculate(source, event); + } + return; } this.runActions(source, source, event, name); @@ -143,8 +153,10 @@ class EventDispatcher { if (event.rc) { if (hasRan) { source.wrapped.value = event.value; + source.wrapped.valueAsString = event.value; } else { source.obj.value = event.value; + source.obj.valueAsString = event.value; } if (this._document.obj.calculate) { @@ -187,6 +199,11 @@ class EventDispatcher { continue; } + if (!this._document.obj.calculate) { + // An action may have changed calculate value. + continue; + } + event.value = null; const target = this._objects[targetId]; this.runActions(source, target, event, "Calculate"); diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index e7154dc8c3f9c..c7de3124e04e6 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -81,12 +81,14 @@ class Field extends PDFObject { this._strokeColor = data.strokeColor || ["G", 0]; this._textColor = data.textColor || ["G", 0]; this._value = data.value || ""; - this._valueAsString = data.valueAsString; this._kidIds = data.kidIds || null; this._fieldType = getFieldType(this._actions); + this._siblings = data.siblings || null; this._globalEval = data.globalEval; this._appObjects = data.appObjects; + + this.valueAsString = data.valueAsString || this._value; } get currentValueIndices() { @@ -246,6 +248,9 @@ class Field extends PDFObject { } get valueAsString() { + if (this._valueAsString === undefined) { + this._valueAsString = this._value ? this._value.toString() : ""; + } return this._valueAsString; } @@ -286,6 +291,9 @@ class Field extends PDFObject { } this._buttonCaption[nFace] = cCaption; // TODO: send to the annotation layer + // Right now the button is drawn on the canvas using its appearance so + // update the caption means redraw... + // We should probably have an html button for this annotation. } buttonSetIcon(oIcon, nFace = 0) { @@ -512,7 +520,7 @@ class RadioButtonField extends Field { } set value(value) { - if (value === null) { + if (value === null || value === undefined) { this._value = ""; } const i = this.exportValues.indexOf(value); @@ -574,7 +582,7 @@ class CheckboxField extends RadioButtonField { } set value(value) { - if (value === "Off") { + if (!value || value === "Off") { this._value = "Off"; } else { super.value = value; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index f1c4484e71619..ad33b86a3fc42 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -94,15 +94,29 @@ function initSandbox(params) { obj.doc = _document; obj.fieldPath = name; obj.appObjects = appObjects; + let field; - if (obj.type === "radiobutton") { - const otherButtons = annotations.slice(1); - field = new RadioButtonField(otherButtons, obj); - } else if (obj.type === "checkbox") { - const otherButtons = annotations.slice(1); - field = new CheckboxField(otherButtons, obj); - } else { - field = new Field(obj); + switch (obj.type) { + case "radiobutton": { + const otherButtons = annotations.slice(1); + field = new RadioButtonField(otherButtons, obj); + break; + } + case "checkbox": { + const otherButtons = annotations.slice(1); + field = new CheckboxField(otherButtons, obj); + break; + } + case "text": + if (annotations.length <= 1) { + field = new Field(obj); + break; + } + obj.siblings = annotations.map(x => x.id).slice(1); + field = new Field(obj); + break; + default: + field = new Field(obj); } const wrapped = new Proxy(field, proxyHandler); diff --git a/src/scripting_api/proxy.js b/src/scripting_api/proxy.js index 44d0ec32f78fb..41e6fb3361584 100644 --- a/src/scripting_api/proxy.js +++ b/src/scripting_api/proxy.js @@ -38,6 +38,14 @@ class ProxyHandler { } set(obj, prop, value) { + if (obj._kidIds) { + // If the field is a container for other fields then + // dispatch the kids. + obj._kidIds.forEach(id => { + obj._appObjects[id].wrapped[prop] = value; + }); + } + if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { const old = obj[prop]; obj[prop] = value; @@ -46,7 +54,12 @@ class ProxyHandler { data[prop] = obj[prop]; // send the updated value to the other side - obj._send(data); + if (!obj._siblings) { + obj._send(data); + } else { + data.siblings = obj._siblings; + obj._send(data); + } } } else { obj._expandos[prop] = value; diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index be45fee7f806f..99c31f4647f2a 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -787,7 +787,7 @@ describe("Interaction", () => { ` ['Text1', 'Text2', 'Text4', 'List Box7', 'Group6'].map(x => this.getField(x).page).join(',') - ` + ` ); // Click on execute button to eval the above code. @@ -802,4 +802,43 @@ describe("Interaction", () => { ); }); }); + + describe("in issue13269.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue13269.pdf", "#\\32 7R"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must update fields with the same name from JS", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 7R", "hello"); + await page.keyboard.press("Enter"); + + await Promise.all( + [4, 5, 6].map(async n => + page.waitForFunction( + `document.querySelector("#\\\\32 ${n}R").value !== ""` + ) + ) + ); + + const expected = "hello world"; + for (const n of [4, 5, 6]) { + const text = await page.$eval(`#\\32 ${n}R`, el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(expected); + } + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 43d39f2d33d18..12fbc208ede91 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -8,6 +8,7 @@ !franz_2.pdf !fraction-highlight.pdf !german-umlaut-r.pdf +!issue13269.pdf !xref_command_missing.pdf !issue1155r.pdf !issue2017r.pdf diff --git a/test/pdfs/issue13269.pdf b/test/pdfs/issue13269.pdf new file mode 100644 index 0000000000000..c893ea374e76e Binary files /dev/null and b/test/pdfs/issue13269.pdf differ diff --git a/web/pdf_scripting_manager.js b/web/pdf_scripting_manager.js index a1dd0339aca9c..01c6949bea1a6 100644 --- a/web/pdf_scripting_manager.js +++ b/web/pdf_scripting_manager.js @@ -271,7 +271,7 @@ class PDFScriptingManager { this._pdfViewer.isInPresentationMode || this._pdfViewer.isChangingPresentationMode; - const { id, command, value } = detail; + const { id, siblings, command, value } = detail; if (!id) { switch (command) { case "clear": @@ -309,13 +309,17 @@ class PDFScriptingManager { } } - const element = document.getElementById(id); - if (element) { - element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail })); - } else { - delete detail.id; - // The element hasn't been rendered yet, use the AnnotationStorage. - this._pdfDocument?.annotationStorage.setValue(id, detail); + delete detail.id; + + const ids = siblings ? [id, ...siblings] : [id]; + for (const elementId of ids) { + const element = document.getElementById(elementId); + if (element) { + element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail })); + } else { + // The element hasn't been rendered yet, use the AnnotationStorage. + this._pdfDocument?.annotationStorage.setValue(elementId, detail); + } } }