From 7c3facb17462ea3f612d076293b6287691d1a248 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 18 Nov 2020 18:54:26 +0100 Subject: [PATCH] JS -- Add support for buttons * radio buttons * checkboxes --- src/core/annotation.js | 10 +- src/display/annotation_layer.js | 3 + src/scripting_api/event.js | 3 + src/scripting_api/field.js | 148 ++++++++++++++++++++++++++-- src/scripting_api/initialization.js | 19 +++- test/integration/scripting_spec.js | 106 ++++++++++++++++++-- test/integration/test_utils.js | 8 ++ test/pdfs/.gitignore | 1 + test/pdfs/js-buttons.pdf | Bin 0 -> 24363 bytes test/test_manifest.json | 6 ++ 10 files changed, 280 insertions(+), 24 deletions(-) create mode 100644 test/pdfs/js-buttons.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index 1dd7184136623..04c1b0d355930 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1944,18 +1944,19 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { getFieldObject() { let type = "button"; - let value = null; + let exportValues; if (this.data.checkBox) { type = "checkbox"; - value = this.data.fieldValue && this.data.fieldValue !== "Off"; + exportValues = this.data.exportValue; } else if (this.data.radioButton) { type = "radiobutton"; - value = this.data.fieldValue === this.data.buttonValue; + exportValues = this.data.buttonValue; } return { id: this.data.id, - value, + value: this.data.fieldValue || null, defaultValue: this.data.defaultFieldValue, + exportValues, editable: !this.data.readOnly, name: this.data.fieldName, rect: this.data.rect, @@ -2036,6 +2037,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { editable: !this.data.readOnly, name: this.data.fieldName, rect: this.data.rect, + numItems: this.data.fieldValue.length, multipleSelection: this.data.multiSelect, hidden: this.data.hidden, actions: this.data.actions, diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index b51d14d8dd54e..5d2462d8aa13f 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -653,6 +653,9 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { value() { elementData.userValue = detail.value || ""; storage.setValue(id, { value: elementData.userValue.toString() }); + if (!elementData.formattedValue) { + event.target.value = elementData.userValue; + } }, valueAsString() { elementData.formattedValue = detail.valueAsString || ""; diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index 92a2486775ee8..ab95eb3c86430 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -92,6 +92,9 @@ class EventDispatcher { if (source.obj._isButton()) { source.obj._id = id; event.value = source.obj._getExportValue(event.value); + if (name === "Action") { + source.obj._value = event.value; + } } if (name === "Keystroke") { diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index bd5a6e6bf8f09..d22282149a910 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -47,7 +47,7 @@ class Field extends PDFObject { this.highlight = data.highlight; this.lineWidth = data.lineWidth; this.multiline = data.multiline; - this.multipleSelection = data.multipleSelection; + this.multipleSelection = !!data.multipleSelection; this.name = data.name; this.numItems = data.numItems; this.page = data.page; @@ -66,15 +66,12 @@ class Field extends PDFObject { this.textSize = data.textSize; this.type = data.type; this.userName = data.userName; - this.value = data.value || ""; - - // Need getter/setter - this._valueAsString = data.valueAsString; // Private this._document = data.doc; + this._value = data.value || ""; + this._valueAsString = data.valueAsString; this._actions = createActionsMap(data.actions); - this._fillColor = data.fillColor || ["T"]; this._strokeColor = data.strokeColor || ["G", 0]; this._textColor = data.textColor || ["G", 0]; @@ -112,6 +109,16 @@ class Field extends PDFObject { } } + get value() { + return this._value; + } + + set value(value) { + if (!this.multipleSelection) { + this._value = value; + } + } + get valueAsString() { return this._valueAsString; } @@ -120,6 +127,16 @@ class Field extends PDFObject { this._valueAsString = val ? val.toString() : ""; } + checkThisBox(nWidget, bCheckIt = true) {} + + isBoxChecked(nWidget) { + return false; + } + + isDefaultChecked(nWidget) { + return false; + } + setAction(cTrigger, cScript) { if (typeof cTrigger !== "string" || typeof cScript !== "string") { return; @@ -159,4 +176,121 @@ class Field extends PDFObject { } } -export { Field }; +class RadioButtonField extends Field { + constructor(otherButtons, data) { + super(data); + + this.exportValues = [this.exportValues]; + this._radioIds = [this._id]; + this._radioActions = [this._actions]; + + for (const radioData of otherButtons) { + this.exportValues.push(radioData.exportValues); + this._radioIds.push(radioData.id); + this._radioActions.push(createActionsMap(radioData.actions)); + if (this._value === radioData.exportValues) { + this._id = radioData.id; + } + } + } + + get value() { + return this._value; + } + + set value(value) { + const i = this.exportValues.indexOf(value); + if (0 <= i && i < this._radioIds.length) { + this._id = this._radioIds[i]; + this._value = value; + } else if (value === "Off" && this._radioIds.length === 2) { + const nextI = (1 + this._radioIds.indexOf(this._id)) % 2; + this._id = this._radioIds[nextI]; + this._value = this.exportValues[nextI]; + } + } + + checkThisBox(nWidget, bCheckIt = true) { + if (nWidget < 0 || nWidget >= this._radioIds.length || !bCheckIt) { + return; + } + + this._id = this._radioIds[nWidget]; + this._value = this.exportValues[nWidget]; + this._send({ id: this._id, value: this._value }); + } + + isBoxChecked(nWidget) { + return ( + nWidget >= 0 && + nWidget < this._radioIds.length && + this._id === this._radioIds[nWidget] + ); + } + + isDefaultChecked(nWidget) { + return ( + nWidget >= 0 && + nWidget < this.exportValues.length && + this.defaultValue === this.exportValues[nWidget] + ); + } + + _getExportValue(state) { + const i = this._radioIds.indexOf(this._id); + return this.exportValues[i]; + } + + _runActions(event) { + const i = this._radioIds.indexOf(this._id); + this._actions = this._radioActions[i]; + return super._runActions(event); + } + + _isButton() { + return true; + } +} + +class CheckboxField extends RadioButtonField { + get value() { + return this._value; + } + + set value(value) { + if (value === "Off") { + this._value = "Off"; + } else { + super.value = value; + } + } + + _getExportValue(state) { + return state ? super._getExportValue(state) : "Off"; + } + + isBoxChecked(nWidget) { + if (this._value === "Off") { + return false; + } + return super.isBoxChecked(nWidget); + } + + isDefaultChecked(nWidget) { + if (this.defaultValue === "Off") { + return this._value === "Off"; + } + return super.isDefaultChecked(nWidget); + } + + checkThisBox(nWidget, bCheckIt = true) { + if (nWidget < 0 || nWidget >= this._radioIds.length) { + return; + } + this._id = this._radioIds[nWidget]; + this._value = bCheckIt ? this.exportValues[nWidget] : "Off"; + this._send({ id: this._id, value: this._value }); + } +} + +export { CheckboxField, Field, RadioButtonField }; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index 0ad80ab615de3..96bba1553ea00 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -26,12 +26,12 @@ import { Trans, ZoomType, } from "./constants.js"; +import { CheckboxField, Field, RadioButtonField } from "./field.js"; import { AForm } from "./aform.js"; import { App } from "./app.js"; import { Color } from "./color.js"; import { Console } from "./console.js"; import { Doc } from "./doc.js"; -import { Field } from "./field.js"; import { ProxyHandler } from "./proxy.js"; import { Util } from "./util.js"; @@ -74,10 +74,23 @@ function initSandbox(params) { obj.send = send; obj.globalEval = globalEval; obj.doc = _document.wrapped; - const field = new Field(obj); + let field; + if (obj.type === "radiobutton") { + const otherButtons = objs.slice(1); + field = new RadioButtonField(otherButtons, obj); + } else if (obj.type === "checkbox") { + const otherButtons = objs.slice(1); + field = new CheckboxField(otherButtons, obj); + } else { + field = new Field(obj); + } + const wrapped = new Proxy(field, proxyHandler); doc._addField(name, wrapped); - app._objects[obj.id] = { obj: field, wrapped }; + const _object = { obj: field, wrapped }; + for (const object of objs) { + app._objects[object.id] = _object; + } } } diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index 43fc949d173c7..923c7dded5580 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -13,7 +13,7 @@ * limitations under the License. */ -const { closePages, loadAndWait } = require("./test_utils.js"); +const { clearInput, closePages, loadAndWait } = require("./test_utils.js"); describe("Interaction", () => { describe("in 160F-2019.pdf", () => { @@ -65,11 +65,7 @@ describe("Interaction", () => { .toEqual("visible"); // Clear the textfield - await page.click("#\\34 16R"); - await page.keyboard.down("Control"); - await page.keyboard.press("A"); - await page.keyboard.up("Control"); - await page.keyboard.press("Backspace"); + await clearInput(page, "#\\34 16R"); // and leave it await page.click("#\\34 19R"); @@ -109,10 +105,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("61803"); // Clear the textfield - await page.keyboard.down("Control"); - await page.keyboard.press("A"); - await page.keyboard.up("Control"); - await page.keyboard.press("Backspace"); + await clearInput(page, "#\\34 48R"); await page.type("#\\34 48R", "1.61803", { delay: 200 }); await page.click("#\\34 19R"); @@ -194,4 +187,97 @@ describe("Interaction", () => { ); }); }); + + describe("in js-buttons.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("js-buttons.pdf", "#\\38 0R"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must show values in a text input when clicking on radio buttons", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + const expected = [ + ["#\\36 8R", "Group1=Choice1::1"], + ["#\\36 9R", "Group1=Choice2::2"], + ["#\\37 0R", "Group1=Choice3::3"], + ["#\\37 1R", "Group1=Choice4::4"], + ]; + for (const [selector, expectedText] of expected) { + // Clear the textfield + await clearInput(page, "#\\36 7R"); + + await page.click(selector); + await page.waitForFunction( + `document.querySelector("#\\\\36 7R").value !== ""` + ); + const text = await page.$eval("#\\36 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(expectedText); + } + }) + ); + }); + + it("must show values in a text input when clicking on checkboxes", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const expected = [ + ["#\\37 2R", "Check1=Yes::5"], + ["#\\37 4R", "Check2=Yes::6"], + ["#\\37 5R", "Check3=Yes::7"], + ["#\\37 6R", "Check4=Yes::8"], + ["#\\37 2R", "Check1=Off::5"], + ["#\\37 4R", "Check2=Off::6"], + ["#\\37 5R", "Check3=Off::7"], + ["#\\37 6R", "Check4=Off::8"], + ]; + for (const [selector, expectedText] of expected) { + // Clear the textfield + await clearInput(page, "#\\36 7R"); + + await page.click(selector); + await page.waitForFunction( + `document.querySelector("#\\\\36 7R").value !== ""` + ); + const text = await page.$eval("#\\36 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(expectedText); + } + }) + ); + }); + + it("must show values in a text input when clicking on checkboxes in a group", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const expected = [ + ["#\\37 7R", "Check5=Yes1::9"], + ["#\\37 8R", "Check5=Yes2::10"], + ["#\\37 9R", "Check5=Yes3::11"], + ["#\\38 0R", "Check5=Yes4::12"], + ["#\\38 0R", "Check5=Off::12"], + ]; + for (const [selector, expectedText] of expected) { + // Clear the textfield + await clearInput(page, "#\\36 7R"); + + await page.click(selector); + await page.waitForFunction( + `document.querySelector("#\\\\36 7R").value !== ""` + ); + const text = await page.$eval("#\\36 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(expectedText); + } + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.js b/test/integration/test_utils.js index bb22fb97a603b..2b730ba1e889b 100644 --- a/test/integration/test_utils.js +++ b/test/integration/test_utils.js @@ -34,3 +34,11 @@ exports.closePages = pages => await page.close(); }) ); + +exports.clearInput = async (page, selector) => { + await page.click(selector); + await page.keyboard.down("Control"); + await page.keyboard.press("A"); + await page.keyboard.up("Control"); + await page.keyboard.press("Backspace"); +}; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 027193b4ab84d..8d5894dee1320 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -365,6 +365,7 @@ !issue6108.pdf !issue6113.pdf !openoffice.pdf +!js-buttons.pdf !issue7014.pdf !issue8187.pdf !annotation-link-text-popup.pdf diff --git a/test/pdfs/js-buttons.pdf b/test/pdfs/js-buttons.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4238249b91dad9f3d4bd3496f3322e1839d15eab GIT binary patch literal 24363 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