diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index e3afd589f..8a6f90072 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -365,7 +365,11 @@ class LinkAnnotationElement extends AnnotationElement { parameters.data.url || parameters.data.dest || parameters.data.action || - parameters.data.isTooltipOnly + parameters.data.isTooltipOnly || + (parameters.data.actions && + (parameters.data.actions.Action || + parameters.data.actions.MouseUp || + parameters.data.actions.MouseDown)) ); super(parameters, { isRenderable, createQuadrilaterals: true }); } @@ -387,6 +391,13 @@ 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.MouseUp || data.actions.MouseDown) && + this.enableScripting && + this.hasJSActions + ) { + this._bindJSAction(link); } else { this._bindLink(link, ""); } @@ -443,6 +454,42 @@ class LinkAnnotationElement extends AnnotationElement { }; link.className = "internalLink"; } + + /** + * Bind JS actions to the link element. + * + * @private + * @param {Object} link + * @param {Object} data + * @memberof LinkAnnotationElement + */ + _bindJSAction(link) { + link.href = this.linkService.getAnchorUrl("#"); + const { data } = this; + const map = new Map([ + ["Action", "onclick"], + ["MouseUp", "onmouseup"], + ["MouseDown", "onmousedown"], + ]); + for (const name of Object.keys(data.actions)) { + const jsName = map.get(name); + if (!jsName) { + continue; + } + link[jsName] = () => { + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id: data.id, + name, + }, + }) + ); + return false; + }; + } + link.className = "internalLink"; + } } class TextAnnotationElement extends AnnotationElement { @@ -488,6 +535,53 @@ class WidgetAnnotationElement extends AnnotationElement { return this.container; } + + _getKeyModifier(event) { + return ( + (navigator.platform.includes("Win") && event.ctrlKey) || + (navigator.platform.includes("Mac") && event.metaKey) + ); + } + + _setEventListener(element, baseName, eventName, valueGetter) { + if (this.data.actions && eventName.replace(" ", "") in this.data.actions) { + if (baseName.includes("mouse")) { + // Mouse events + element.addEventListener(baseName, event => { + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id: this.data.id, + name: eventName, + value: valueGetter(event), + shift: event.shiftKey, + modifier: this._getKeyModifier(event), + }, + }) + ); + }); + } else { + // Non mouse event + element.addEventListener(baseName, event => { + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id: this.data.id, + name: eventName, + value: event.target.checked, + }, + }) + ); + }); + } + } + } + + _setEventListeners(element, names, getter) { + for (const [baseName, eventName] of names) { + this._setEventListener(element, baseName, eventName, getter); + } + } } class TextWidgetAnnotationElement extends WidgetAnnotationElement { @@ -496,6 +590,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { parameters.renderInteractiveForms || (!parameters.data.hasAppearance && !!parameters.data.fieldValue); super(parameters, { isRenderable }); + this.mouseState = parameters.mouseState; } render() { @@ -513,6 +608,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { const textContent = storage.getOrCreateValue(id, { value: this.data.fieldValue, }).value; + const elementData = { + userValue: null, + formattedValue: null, + beforeInputSelectionRange: null, + beforeInputValue: null, + }; if (this.data.multiLine) { element = document.createElement("textarea"); @@ -523,104 +624,196 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { element.setAttribute("value", textContent); } - element.userValue = textContent; + elementData.userValue = textContent; element.setAttribute("id", id); element.addEventListener("input", function (event) { storage.setValue(id, { value: event.target.value }); }); - element.addEventListener("blur", function (event) { + let blurListener = event => { + if (elementData.formattedValue) { + event.target.value = elementData.formattedValue; + } event.target.setSelectionRange(0, 0); - }); + elementData.beforeInputSelectionRange = null; + }; if (this.enableScripting && this.hasJSActions) { element.addEventListener("focus", event => { - if (event.target.userValue) { - event.target.value = event.target.userValue; + if (elementData.userValue) { + event.target.value = elementData.userValue; } }); - if (this.data.actions) { - element.addEventListener("updateFromSandbox", function (event) { - const detail = event.detail; - const actions = { - value() { - const value = detail.value; - if (value === undefined || value === null) { - // remove data - event.target.userValue = ""; - } else { - event.target.userValue = value; - } - }, - valueAsString() { - const value = detail.valueAsString; - if (value === undefined || value === null) { - // remove data - event.target.value = ""; - } else { - event.target.value = value; - } - storage.setValue(id, event.target.value); - }, - focus() { - event.target.focus({ preventScroll: false }); - }, - userName() { - const tooltip = detail.userName; - event.target.title = tooltip; - }, - hidden() { - event.target.style.display = detail.hidden ? "none" : "block"; - }, - editable() { - event.target.disabled = !detail.editable; - }, - selRange() { - const [selStart, selEnd] = detail.selRange; - if (selStart >= 0 && selEnd < event.target.value.length) { - event.target.setSelectionRange(selStart, selEnd); - } - }, - strokeColor() { - const color = detail.strokeColor; - event.target.style.color = ColorConverters[`${color[0]}_HTML`]( - color.slice(1) - ); - }, - }; - for (const name of Object.keys(detail)) { - if (name in actions) { - actions[name](); + element.addEventListener("updateFromSandbox", function (event) { + const { detail } = event; + const actions = { + value() { + elementData.userValue = detail.value || ""; + storage.setValue(id, { value: elementData.userValue.toString() }); + }, + valueAsString() { + elementData.formattedValue = detail.valueAsString || ""; + if (event.target !== document.activeElement) { + // Input hasn't the focus so display formatted string + event.target.value = elementData.formattedValue; } + storage.setValue(id, { + formattedValue: elementData.formattedValue, + }); + }, + focus() { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + userName() { + // tooltip + event.target.title = detail.userName; + }, + hidden() { + event.target.style.visibility = detail.hidden + ? "hidden" + : "visible"; + storage.setValue(id, { hidden: detail.hidden }); + }, + editable() { + event.target.disabled = !detail.editable; + }, + selRange() { + const [selStart, selEnd] = detail.selRange; + if (selStart >= 0 && selEnd < event.target.value.length) { + event.target.setSelectionRange(selStart, selEnd); + } + }, + strokeColor() { + const color = detail.strokeColor; + event.target.style.color = ColorConverters[`${color[0]}_HTML`]( + color.slice(1) + ); + }, + }; + Object.keys(detail) + .filter(name => name in actions) + .forEach(name => actions[name]()); + }); + + if (this.data.actions) { + // Even if the field hasn't any actions + // leaving it can still trigger some actions with Calculate + element.addEventListener("keydown", event => { + elementData.beforeInputValue = event.target.value; + // if the key is one of Escape, Enter or Tab + // then the data are committed + let commitKey = -1; + if (event.key === "Escape") { + commitKey = 0; + } else if (event.key === "Enter") { + commitKey = 2; + } else if (event.key === "Tab") { + commitKey = 3; + } + if (commitKey === -1) { + return; + } + // Save the entered value + elementData.userValue = event.target.value; + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id, + name: "Keystroke", + value: event.target.value, + willCommit: true, + commitKey, + selStart: event.target.selectionStart, + selEnd: event.target.selectionEnd, + }, + }) + ); + }); + const _blurListener = blurListener; + blurListener = null; + element.addEventListener("blur", event => { + if (this.mouseState.isDown) { + // Focus out using the mouse: data are committed + elementData.userValue = event.target.value; + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id, + name: "Keystroke", + value: event.target.value, + willCommit: true, + commitKey: 1, + selStart: event.target.selectionStart, + selEnd: event.target.selectionEnd, + }, + }) + ); + } + _blurListener(event); + }); + element.addEventListener("mousedown", event => { + elementData.beforeInputValue = event.target.value; + elementData.beforeInputSelectionRange = null; + }); + element.addEventListener("keyup", event => { + // keyup is triggered after input + if (event.target.selectionStart === event.target.selectionEnd) { + elementData.beforeInputSelectionRange = null; } }); + element.addEventListener("select", event => { + elementData.beforeInputSelectionRange = [ + event.target.selectionStart, + event.target.selectionEnd, + ]; + }); - for (const eventType of Object.keys(this.data.actions)) { - switch (eventType) { - case "Format": - element.addEventListener("change", function (event) { - window.dispatchEvent( - new CustomEvent("dispatchEventInSandbox", { - detail: { - id, - name: "Keystroke", - value: event.target.value, - willCommit: true, - commitKey: 1, - selStart: event.target.selectionStart, - selEnd: event.target.selectionEnd, - }, - }) - ); - }); - break; - } + if ("Keystroke" in this.data.actions) { + // We should use beforeinput but this + // event isn't available in Firefox + element.addEventListener("input", event => { + let selStart = -1; + let selEnd = -1; + if (elementData.beforeInputSelectionRange) { + [selStart, selEnd] = elementData.beforeInputSelectionRange; + } + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id, + name: "Keystroke", + value: elementData.beforeInputValue, + change: event.data, + willCommit: false, + selStart, + selEnd, + }, + }) + ); + }); } + + this._setEventListeners( + element, + [ + ["focus", "Focus"], + ["blur", "Blur"], + ["mousedown", "Mouse Down"], + ["mouseenter", "Mouse Enter"], + ["mouseleave", "Mouse Exit"], + ["mouseup", "MouseUp"], + ], + event => event.target.value + ); } } + if (blurListener) { + element.addEventListener("blur", blurListener); + } + element.disabled = this.data.readOnly; element.name = this.data.fieldName; @@ -715,6 +908,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { if (value) { element.setAttribute("checked", true); } + element.setAttribute("id", id); element.addEventListener("change", function (event) { const name = event.target.name; @@ -730,6 +924,48 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { storage.setValue(id, { value: event.target.checked }); }); + if (this.enableScripting && this.hasJSActions) { + element.addEventListener("updateFromSandbox", event => { + const { detail } = event; + const actions = { + value() { + event.target.checked = detail.value !== "Off"; + storage.setValue(id, { value: event.target.checked }); + }, + focus() { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + hidden() { + event.target.style.visibility = detail.hidden + ? "hidden" + : "visible"; + storage.setValue(id, { hidden: detail.hidden }); + }, + editable() { + event.target.disabled = !detail.editable; + }, + }; + Object.keys(detail) + .filter(name => name in actions) + .forEach(name => actions[name]()); + }); + + this._setEventListeners( + element, + [ + ["change", "Validate"], + ["change", "Action"], + ["focus", "Focus"], + ["blur", "Blur"], + ["mousedown", "Mouse Down"], + ["mouseenter", "Mouse Enter"], + ["mouseleave", "Mouse Exit"], + ["mouseup", "MouseUp"], + ], + event => event.target.checked + ); + } + this.container.appendChild(element); return this.container; } @@ -756,20 +992,69 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { if (value) { element.setAttribute("checked", true); } + element.setAttribute("pdfButtonValue", data.buttonValue); + element.setAttribute("id", id); element.addEventListener("change", function (event) { - const name = event.target.name; - for (const radio of document.getElementsByName(name)) { - if (radio !== event.target) { - storage.setValue( - radio.parentNode.getAttribute("data-annotation-id"), - { value: false } - ); + const target = event.target; + for (const radio of document.getElementsByName(event.target.name)) { + if (radio !== target) { + storage.setValue(radio.getAttribute("id"), { value: false }); } } - storage.setValue(id, { value: event.target.checked }); + storage.setValue(id, { value: target.checked }); }); + if (this.enableScripting && this.hasJSActions) { + element.addEventListener("updateFromSandbox", event => { + const { detail } = event; + const actions = { + value() { + const fieldValue = detail.value; + for (const radio of document.getElementsByName(event.target.name)) { + const radioId = radio.getAttribute("id"); + if (fieldValue === radio.getAttribute("pdfButtonValue")) { + radio.setAttribute("checked", true); + storage.setValue(radioId, { value: true }); + } else { + storage.setValue(radioId, { value: false }); + } + } + }, + focus() { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + hidden() { + event.target.style.visibility = detail.hidden + ? "hidden" + : "visible"; + storage.setValue(id, { hidden: detail.hidden }); + }, + editable() { + event.target.disabled = !detail.editable; + }, + }; + Object.keys(detail) + .filter(name => name in actions) + .forEach(name => actions[name]()); + }); + + this._setEventListeners( + element, + [ + ["change", "Validate"], + ["change", "Action"], + ["focus", "Focus"], + ["blur", "Blur"], + ["mousedown", "Mouse Down"], + ["mouseenter", "Mouse Enter"], + ["mouseleave", "Mouse Exit"], + ["mouseup", "MouseUp"], + ], + event => event.target.checked + ); + } + this.container.appendChild(element); return this.container; } @@ -816,6 +1101,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { const selectElement = document.createElement("select"); selectElement.disabled = this.data.readOnly; selectElement.name = this.data.fieldName; + selectElement.setAttribute("id", id); if (!this.data.combo) { // List boxes have a size and (optionally) multiple selection. @@ -836,11 +1122,77 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { selectElement.appendChild(optionElement); } - selectElement.addEventListener("input", function (event) { + function getValue(event) { const options = event.target.options; - const value = options[options.selectedIndex].value; - storage.setValue(id, { value }); - }); + return options[options.selectedIndex].value; + } + + if (this.enableScripting && this.hasJSActions) { + selectElement.addEventListener("updateFromSandbox", event => { + const { detail } = event; + const actions = { + value() { + const options = event.target.options; + const value = detail.value; + const i = options.indexOf(value); + if (i !== -1) { + options.selectedIndex = i; + storage.setValue(id, { value }); + } + }, + focus() { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + hidden() { + event.target.style.visibility = detail.hidden + ? "hidden" + : "visible"; + storage.setValue(id, { hidden: detail.hidden }); + }, + editable() { + event.target.disabled = !detail.editable; + }, + }; + Object.keys(detail) + .filter(name => name in actions) + .forEach(name => actions[name]()); + }); + + selectElement.addEventListener("input", function (event) { + const value = getValue(event); + storage.setValue(id, { value }); + + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id, + name: "Keystroke", + changeEx: value, + willCommit: true, + commitKey: 1, + keyDown: false, + }, + }) + ); + }); + + this._setEventListeners( + selectElement, + [ + ["focus", "Focus"], + ["blur", "Blur"], + ["mousedown", "Mouse Down"], + ["mouseenter", "Mouse Enter"], + ["mouseleave", "Mouse Exit"], + ["mouseup", "MouseUp"], + ], + event => event.target.checked + ); + } else { + selectElement.addEventListener("input", function (event) { + storage.setValue(id, { value: getValue(event) }); + }); + } this.container.appendChild(selectElement); return this.container; @@ -1599,6 +1951,7 @@ class AnnotationLayer { parameters.annotationStorage || new AnnotationStorage(), enableScripting: parameters.enableScripting, hasJSActions: parameters.hasJSActions, + mouseState: parameters.mouseState, }); if (element.isRenderable) { const rendered = element.render(); diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 715a852ea..5b25191a2 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -913,6 +913,7 @@ class Doc extends PDFObject { const field = this.getField(fieldName); if (field) { field.value = field.defaultValue; + field.valueAsString = field.value; mustCalculate = true; } } @@ -920,6 +921,7 @@ class Doc extends PDFObject { mustCalculate = this._fields.size !== 0; for (const field of this._fields.values()) { field.value = field.defaultValue; + field.valueAsString = field.value; } } if (mustCalculate) { diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 95344211b..8d6053126 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -66,7 +66,9 @@ class Field extends PDFObject { this.type = data.type; this.userName = data.userName; this.value = data.value || ""; - this.valueAsString = data.valueAsString; + + // Need getter/setter + this._valueAsString = data.valueAsString; // Private this._document = data.doc; @@ -107,6 +109,28 @@ class Field extends PDFObject { } } + get valueAsString() { + return this._valueAsString; + } + + set valueAsString(val) { + this._valueAsString = val ? val.toString() : ""; + } + + _getFunction(code, actionName) { + try { + // This eval is running in a sandbox so it's safe to use Function + // eslint-disable-next-line no-new-func + return Function("event", `with (this) {${code}}`).bind(this._document); + } catch (error) { + const value = + `"${error.toString()}" for action ` + + `"${actionName}" in object ${this._id}.`; + this._send({ command: "error", value }); + } + return null; + } + setAction(cTrigger, cScript) { if (typeof cTrigger !== "string" || typeof cScript !== "string") { return; @@ -114,10 +138,10 @@ class Field extends PDFObject { if (!(cTrigger in this._actions)) { this._actions[cTrigger] = []; } - this._actions[cTrigger].push( - // eslint-disable-next-line no-new-func - Function("event", `with (this) {${cScript}}`).bind(this._document) - ); + const fun = this._getFunction(cScript, cTrigger); + if (fun) { + this._actions[cTrigger].push(fun); + } } setFocus() { @@ -127,16 +151,13 @@ class Field extends PDFObject { _createActionsMap(actions) { const actionsMap = new Map(); if (actions) { - const doc = this._document; for (const [eventType, actionsForEvent] of Object.entries(actions)) { - // This stuff is running in a sandbox so it's safe to use Function - actionsMap.set( - eventType, - actionsForEvent.map(action => - // eslint-disable-next-line no-new-func - Function("event", `with (this) {${action}}`).bind(doc) - ) - ); + const functions = actionsForEvent + .map(action => this._getFunction(action, eventType)) + .filter(fun => !!fun); + if (functions.length > 0) { + actionsMap.set(eventType, functions); + } } } return actionsMap; diff --git a/src/scripting_api/proxy.js b/src/scripting_api/proxy.js index 4c10d334b..c20ef49bf 100644 --- a/src/scripting_api/proxy.js +++ b/src/scripting_api/proxy.js @@ -54,7 +54,7 @@ class ProxyHandler { obj[prop] = value; if (obj._send && obj._id !== null && typeof old !== "function") { const data = { id: obj._id }; - data[prop] = value; + data[prop] = obj[prop]; // send the updated value to the other side obj._send(data); diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index ae9999837..2760517ae 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -27,6 +27,44 @@ describe("Interaction", () => { await closePages(pages); }); + it("must show a text field and then make in invisible when content is removed", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + let visibility = await page.$eval( + "#\\34 27R", + el => getComputedStyle(el).visibility + ); + expect(visibility).withContext(`In ${browserName}`).toEqual("hidden"); + + await page.type("#\\34 16R", "3.14159", { delay: 200 }); + await page.click("#\\34 19R"); + + visibility = await page.$eval( + "#\\34 27R", + el => getComputedStyle(el).visibility + ); + expect(visibility) + .withContext(`In ${browserName}`) + .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"); + // and leave it + await page.click("#\\34 19R"); + + visibility = await page.$eval( + "#\\34 27R", + el => getComputedStyle(el).visibility + ); + expect(visibility).withContext(`In ${browserName}`).toEqual("hidden"); + }) + ); + }); + it("must format the field with 2 digits and leave field with a click", async () => { await Promise.all( pages.map(async ([browserName, page]) => { @@ -34,6 +72,35 @@ describe("Interaction", () => { await page.click("#\\34 19R"); const text = await page.$eval("#\\34 16R", el => el.value); expect(text).withContext(`In ${browserName}`).toEqual("3,14"); + + const sum = await page.$eval("#\\34 27R", el => el.value); + expect(sum).withContext(`In ${browserName}`).toEqual("3,14"); + }) + ); + }); + + it("must format the field with 2 digits, leave field with a click and again", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.type("#\\34 48R", "61803", { delay: 200 }); + await page.click("#\\34 19R"); + let text = await page.$eval("#\\34 48R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("61.803,00"); + + await page.click("#\\34 48R"); + text = await page.$eval("#\\34 48R", el => el.value); + 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 page.type("#\\34 48R", "1.61803", { delay: 200 }); + await page.click("#\\34 19R"); + text = await page.$eval("#\\34 48R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("1,62"); }) ); }); @@ -45,6 +112,67 @@ describe("Interaction", () => { await page.keyboard.press("Tab"); const text = await page.$eval("#\\34 22R", el => el.value); expect(text).withContext(`In ${browserName}`).toEqual("2,72"); + + const sum = await page.$eval("#\\34 27R", el => el.value); + expect(sum).withContext(`In ${browserName}`).toEqual("5,86"); + }) + ); + }); + + it("must format the field with 2 digits and hit ESC", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + let sum = await page.$eval("#\\34 71R", el => el.value); + expect(sum).withContext(`In ${browserName}`).toEqual("4,24"); + + await page.type("#\\34 36R", "0.69314", { delay: 200 }); + await page.keyboard.press("Escape"); + const text = await page.$eval("#\\34 36R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("0.69314"); + + sum = await page.$eval("#\\34 71R", el => el.value); + expect(sum).withContext(`In ${browserName}`).toEqual("3,55"); + }) + ); + }); + + it("must format the field with 2 digits on key ENTER", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.type("#\\34 19R", "0.577215", { delay: 200 }); + await page.keyboard.press("Enter"); + const text = await page.$eval("#\\34 19R", el => el.value); + expect(text).toEqual("0.577215"); + + const sum = await page.$eval("#\\34 27R", el => el.value); + expect(sum).toEqual("6,44"); + }) + ); + }); + + it("must reset all", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // this field has no actions but it must be cleared on reset + await page.type("#\\34 05R", "employee", { delay: 200 }); + + // click on reset button + await page.click("[data-annotation-id='402R']"); + + let text = await page.$eval("#\\34 16R", el => el.value); + expect(text).toEqual(""); + + text = await page.$eval("#\\34 22R", el => el.value); + expect(text).toEqual(""); + + text = await page.$eval("#\\34 19R", el => el.value); + expect(text).toEqual(""); + + text = await page.$eval("#\\34 05R", el => el.value); + expect(text).toEqual(""); + + const sum = await page.$eval("#\\34 27R", el => el.value); + expect(sum).toEqual(""); }) ); }); diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index c999b28c0..f84294bcf 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -84,7 +84,7 @@ describe("Scripting", function () { return s; } const number = 123; - const expected = ((number - 1) * number) / 2; + const expected = (((number - 1) * number) / 2).toString(); const refId = getId(); const data = { @@ -1094,7 +1094,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 1, - valueAsString: 1, + valueAsString: "1", }); await sandbox.dispatchEventInSandbox({ @@ -1107,7 +1107,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 3, - valueAsString: 3, + valueAsString: "3", }); await sandbox.dispatchEventInSandbox({ @@ -1120,7 +1120,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 6, - valueAsString: 6, + valueAsString: "6", }); done(); diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 194139fa8..e81b7a554 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -47,6 +47,7 @@ class AnnotationLayerBuilder { l10n = NullL10n, enableScripting = false, hasJSActionsPromise = null, + mouseState = null, }) { this.pageDiv = pageDiv; this.pdfPage = pdfPage; @@ -58,6 +59,7 @@ class AnnotationLayerBuilder { this.annotationStorage = annotationStorage; this.enableScripting = enableScripting; this._hasJSActionsPromise = hasJSActionsPromise; + this._mouseState = mouseState; this.div = null; this._cancelled = false; @@ -93,6 +95,7 @@ class AnnotationLayerBuilder { annotationStorage: this.annotationStorage, enableScripting: this.enableScripting, hasJSActions, + mouseState: this._mouseState, }; if (this.div) { @@ -139,6 +142,7 @@ class DefaultAnnotationLayerFactory { * @param {IL10n} l10n * @param {boolean} [enableScripting] * @param {Promise} [hasJSActionsPromise] + * @param {Object} [mouseState] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -149,7 +153,8 @@ class DefaultAnnotationLayerFactory { renderInteractiveForms = true, l10n = NullL10n, enableScripting = false, - hasJSActionsPromise = null + hasJSActionsPromise = null, + mouseState = null ) { return new AnnotationLayerBuilder({ pageDiv, @@ -161,6 +166,7 @@ class DefaultAnnotationLayerFactory { annotationStorage, enableScripting, hasJSActionsPromise, + mouseState, }); } } diff --git a/web/app.js b/web/app.js index e67d7bc07..b30eb7edc 100644 --- a/web/app.js +++ b/web/app.js @@ -258,6 +258,7 @@ const PDFViewerApplication = { _wheelUnusedTicks: 0, _idleCallbacks: new Set(), _scriptingInstance: null, + _mouseState: Object.create(null), // Called once when the document is loaded. async initialize(appConfig) { @@ -501,6 +502,7 @@ const PDFViewerApplication = { useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), enableScripting: AppOptions.get("enableScripting"), + mouseState: this._mouseState, }); pdfRenderingQueue.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer); @@ -1538,6 +1540,17 @@ const PDFViewerApplication = { dispatchEventInSandbox ); + const mouseDown = event => { + this._mouseState.isDown = true; + }; + const mouseUp = event => { + this._mouseState.isDown = false; + }; + window.addEventListener("mousedown", mouseDown); + this._scriptingInstance.events.set("mousedown", mouseDown); + window.addEventListener("mouseup", mouseUp); + this._scriptingInstance.events.set("mouseup", mouseUp); + const dispatchEventName = generateRandomStringForSandbox(objects); if (!this._contentLength) { diff --git a/web/base_viewer.js b/web/base_viewer.js index 240d6b75a..f0a172b37 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -79,6 +79,7 @@ const DEFAULT_CACHE_SIZE = 10; * @property {IL10n} l10n - Localization service. * @property {boolean} [enableScripting] - Enable embedded script execution. * The default value is `false`. + * @property {Object} [mouseState] - The mouse button state. */ function PDFPageViewBuffer(size) { @@ -194,6 +195,7 @@ class BaseViewer { this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; this.enableScripting = options.enableScripting || false; + this.mouseState = options.mouseState || null; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -533,6 +535,7 @@ class BaseViewer { maxCanvasPixels: this.maxCanvasPixels, l10n: this.l10n, enableScripting: this.enableScripting, + mouseState: this.mouseState, }); this._pages.push(pageView); } @@ -1265,6 +1268,7 @@ class BaseViewer { * @param {IL10n} l10n * @param {boolean} [enableScripting] * @param {Promise} [hasJSActionsPromise] + * @param {Object} [mouseState] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -1275,7 +1279,8 @@ class BaseViewer { renderInteractiveForms = false, l10n = NullL10n, enableScripting = false, - hasJSActionsPromise = null + hasJSActionsPromise = null, + mouseState = null ) { return new AnnotationLayerBuilder({ pageDiv, @@ -1290,6 +1295,7 @@ class BaseViewer { enableScripting, hasJSActionsPromise: hasJSActionsPromise || this.pdfDocument?.hasJSActions(), + mouseState, }); } diff --git a/web/interfaces.js b/web/interfaces.js index 0925417a2..2ab22a6d2 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -188,6 +188,7 @@ class IPDFAnnotationLayerFactory { * @param {IL10n} l10n * @param {boolean} [enableScripting] * @param {Promise} [hasJSActionsPromise] + * @param {Object} [mouseState] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -198,7 +199,8 @@ class IPDFAnnotationLayerFactory { renderInteractiveForms = true, l10n = undefined, enableScripting = false, - hasJSActionsPromise = null + hasJSActionsPromise = null, + mouseState = null ) {} } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index c32259641..0aa427178 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -63,6 +63,7 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js"; * @property {IL10n} l10n - Localization service. * @property {boolean} [enableScripting] - Enable embedded script execution. * The default value is `false`. + * @property {Object} [mouseState] - The mouse button state. */ const MAX_CANVAS_PIXELS = viewerCompatibilityParams.maxCanvasPixels || 16777216; @@ -109,6 +110,7 @@ class PDFPageView { this.enableWebGL = options.enableWebGL || false; this.l10n = options.l10n || NullL10n; this.enableScripting = options.enableScripting || false; + this.mouseState = options.mouseState || null; this.paintTask = null; this.paintedViewportMap = new WeakMap(); @@ -551,7 +553,8 @@ class PDFPageView { this.renderInteractiveForms, this.l10n, this.enableScripting, - /* hasJSActionsPromise = */ null + /* hasJSActionsPromise = */ null, + this.mouseState ); } this._renderAnnotationLayer();