From a3f6882b06b3ae6b2c02d0be4a650460a84c009c Mon Sep 17 00:00:00 2001 From: calixteman Date: Mon, 25 Jan 2021 14:40:57 -0800 Subject: [PATCH] JS -- add support for choice widget (#12826) --- src/core/annotation.js | 1 + src/display/annotation_layer.js | 298 +++++++++++++++++++---------- src/scripting_api/field.js | 198 ++++++++++++++++++- test/integration/scripting_spec.js | 132 ++++++++++++- test/pdfs/.gitignore | 1 + test/pdfs/listbox_actions.pdf | Bin 0 -> 9022 bytes 6 files changed, 516 insertions(+), 114 deletions(-) create mode 100644 test/pdfs/listbox_actions.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index 1e32d56f4..4863ea93d 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -2091,6 +2091,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { multipleSelection: this.data.multiSelect, hidden: this.data.hidden, actions: this.data.actions, + items: this.data.options, type, }; } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 65919d191..8b43bb87c 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -698,25 +698,43 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { .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 + // 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; + this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { + source: this, + 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; this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, @@ -725,87 +743,67 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { name: "Keystroke", value: event.target.value, willCommit: true, - commitKey, + commitKey: 1, 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; - this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { - source: this, - 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, - ]; - }); - - 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; - } - this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { - source: this, - detail: { - id, - name: "Keystroke", - value: elementData.beforeInputValue, - change: event.data, - willCommit: false, - selStart, - selEnd, - }, - }); - }); } + _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, + ]; + }); - this._setEventListeners( - element, - [ - ["focus", "Focus"], - ["blur", "Blur"], - ["mousedown", "Mouse Down"], - ["mouseenter", "Mouse Enter"], - ["mouseleave", "Mouse Exit"], - ["mouseup", "Mouse Up"], - ], - event => event.target.value - ); + if (this.data.actions?.Keystroke) { + // 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; + } + this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { + source: this, + 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", "Mouse Up"], + ], + event => event.target.value + ); } if (blurListener) { @@ -1102,23 +1100,112 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { selectElement.appendChild(optionElement); } - function getValue(event) { + const getValue = (event, isExport) => { + const name = isExport ? "value" : "textContent"; const options = event.target.options; - return options[options.selectedIndex].value; - } + if (!event.target.multiple) { + return options.selectedIndex === -1 + ? null + : options[options.selectedIndex][name]; + } + return Array.prototype.filter + .call(options, option => option.selected) + .map(option => option[name]); + }; + + const getItems = event => { + const options = event.target.options; + return Array.prototype.map.call(options, option => { + return { displayValue: option.textContent, exportValue: option.value }; + }); + }; if (this.enableScripting && this.hasJSActions) { selectElement.addEventListener("updatefromsandbox", event => { const { detail } = event; const actions = { value() { - const options = event.target.options; + const options = selectElement.options; const value = detail.value; - const i = options.indexOf(value); - if (i !== -1) { - options.selectedIndex = i; - storage.setValue(id, { value }); + const values = new Set(Array.isArray(value) ? value : [value]); + Array.prototype.forEach.call(options, option => { + option.selected = values.has(option.value); + }); + storage.setValue(id, { + value: getValue(event, /* isExport */ true), + }); + }, + multipleSelection() { + selectElement.multiple = true; + }, + remove() { + const options = selectElement.options; + const index = detail.remove; + options[index].selected = false; + selectElement.remove(index); + if (options.length > 0) { + const i = Array.prototype.findIndex.call( + options, + option => option.selected + ); + if (i === -1) { + options[0].selected = true; + } } + storage.setValue(id, { + value: getValue(event, /* isExport */ true), + items: getItems(event), + }); + }, + clear() { + while (selectElement.length !== 0) { + selectElement.remove(0); + } + storage.setValue(id, { value: null, items: [] }); + }, + insert() { + const { index, displayValue, exportValue } = detail.insert; + const optionElement = document.createElement("option"); + optionElement.textContent = displayValue; + optionElement.value = exportValue; + selectElement.insertBefore( + optionElement, + selectElement.children[index] + ); + storage.setValue(id, { + value: getValue(event, /* isExport */ true), + items: getItems(event), + }); + }, + items() { + const { items } = detail; + while (selectElement.length !== 0) { + selectElement.remove(0); + } + for (const item of items) { + const { displayValue, exportValue } = item; + const optionElement = document.createElement("option"); + optionElement.textContent = displayValue; + optionElement.value = exportValue; + selectElement.appendChild(optionElement); + } + if (selectElement.options.length > 0) { + selectElement.options[0].selected = true; + } + storage.setValue(id, { + value: getValue(event, /* isExport */ true), + items: getItems(event), + }); + }, + indices() { + const indices = new Set(detail.indices); + const options = event.target.options; + Array.prototype.forEach.call(options, (option, i) => { + option.selected = indices.has(i); + }); + storage.setValue(id, { + value: getValue(event, /* isExport */ true), + }); }, focus() { setTimeout(() => event.target.focus({ preventScroll: false }), 0); @@ -1139,15 +1226,17 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { }); selectElement.addEventListener("input", event => { - const value = getValue(event); - storage.setValue(id, { value }); + const exportValue = getValue(event, /* isExport */ true); + const value = getValue(event, /* isExport */ false); + storage.setValue(id, { value: exportValue }); this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", - changeEx: value, + value, + changeEx: exportValue, willCommit: true, commitKey: 1, keyDown: false, @@ -1164,6 +1253,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { ["mouseenter", "Mouse Enter"], ["mouseleave", "Mouse Exit"], ["mouseup", "Mouse Up"], + ["input", "Action"], ], event => event.target.checked ); diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index d22282149..ff0e3b9e7 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -49,7 +49,6 @@ class Field extends PDFObject { this.multiline = data.multiline; this.multipleSelection = !!data.multipleSelection; this.name = data.name; - this.numItems = data.numItems; this.page = data.page; this.password = data.password; this.print = data.print; @@ -68,17 +67,64 @@ class Field extends PDFObject { this.userName = data.userName; // Private - this._document = data.doc; - this._value = data.value || ""; - this._valueAsString = data.valueAsString; this._actions = createActionsMap(data.actions); + this._currentValueIndices = data.currentValueIndices || 0; + this._document = data.doc; this._fillColor = data.fillColor || ["T"]; + this._isChoice = Array.isArray(data.items); + this._items = data.items || []; this._strokeColor = data.strokeColor || ["G", 0]; this._textColor = data.textColor || ["G", 0]; + this._value = data.value || ""; + this._valueAsString = data.valueAsString; this._globalEval = data.globalEval; } + get currentValueIndices() { + if (!this._isChoice) { + return 0; + } + return this._currentValueIndices; + } + + set currentValueIndices(indices) { + if (!this._isChoice) { + return; + } + if (!Array.isArray(indices)) { + indices = [indices]; + } + if ( + !indices.every( + i => + typeof i === "number" && + Number.isInteger(i) && + i >= 0 && + i < this.numItems + ) + ) { + return; + } + + indices.sort(); + + if (this.multipleSelection) { + this._currentValueIndices = indices; + this._value = []; + indices.forEach(i => { + this._value.push(this._items[i].displayValue); + }); + } else { + if (indices.length > 0) { + indices = indices.splice(1, indices.length - 1); + this._currentValueIndices = indices[0]; + this._value = this._items[this._currentValueIndices]; + } + } + this._send({ id: this._id, indices }); + } + get fillColor() { return this._fillColor; } @@ -89,6 +135,17 @@ class Field extends PDFObject { } } + get numItems() { + if (!this._isChoice) { + throw new Error("Not a choice widget"); + } + return this._items.length; + } + + set numItems(_) { + throw new Error("field.numItems is read-only"); + } + get strokeColor() { return this._strokeColor; } @@ -114,8 +171,21 @@ class Field extends PDFObject { } set value(value) { - if (!this.multipleSelection) { - this._value = value; + this._value = value; + if (this._isChoice) { + if (this.multipleSelection) { + const values = new Set(value); + this._currentValueIndices.length = 0; + this._items.forEach(({ displayValue }, i) => { + if (values.has(displayValue)) { + this._currentValueIndices.push(i); + } + }); + } else { + this._currentValueIndices = this._items.findIndex( + ({ displayValue }) => value === displayValue + ); + } } } @@ -129,6 +199,67 @@ class Field extends PDFObject { checkThisBox(nWidget, bCheckIt = true) {} + clearItems() { + if (!this._isChoice) { + throw new Error("Not a choice widget"); + } + this._items = []; + this._send({ id: this._id, clear: null }); + } + + deleteItemAt(nIdx = null) { + if (!this._isChoice) { + throw new Error("Not a choice widget"); + } + if (!this.numItems) { + return; + } + + if (nIdx === null) { + // Current selected item. + nIdx = Array.isArray(this._currentValueIndices) + ? this._currentValueIndices[0] + : this._currentValueIndices; + nIdx = nIdx || 0; + } + + if (nIdx < 0 || nIdx >= this.numItems) { + nIdx = this.numItems - 1; + } + + this._items.splice(nIdx, 1); + if (Array.isArray(this._currentValueIndices)) { + let index = this._currentValueIndices.findIndex(i => i >= nIdx); + if (index !== -1) { + if (this._currentValueIndices[index] === nIdx) { + this._currentValueIndices.splice(index, 1); + } + for (const ii = this._currentValueIndices.length; index < ii; index++) { + --this._currentValueIndices[index]; + } + } + } else { + if (this._currentValueIndices === nIdx) { + this._currentValueIndices = this.numItems > 0 ? 0 : -1; + } else if (this._currentValueIndices > nIdx) { + --this._currentValueIndices; + } + } + + this._send({ id: this._id, remove: nIdx }); + } + + getItemAt(nIdx = -1, bExportValue = false) { + if (!this._isChoice) { + throw new Error("Not a choice widget"); + } + if (nIdx < 0 || nIdx >= this.numItems) { + nIdx = this.numItems - 1; + } + const item = this._items[nIdx]; + return bExportValue ? item.exportValue : item.displayValue; + } + isBoxChecked(nWidget) { return false; } @@ -137,6 +268,41 @@ class Field extends PDFObject { return false; } + insertItemAt(cName, cExport = undefined, nIdx = 0) { + if (!this._isChoice) { + throw new Error("Not a choice widget"); + } + if (!cName) { + return; + } + + if (nIdx < 0 || nIdx > this.numItems) { + nIdx = this.numItems; + } + + if (this._items.some(({ displayValue }) => displayValue === cName)) { + return; + } + + if (cExport === undefined) { + cExport = cName; + } + const data = { displayValue: cName, exportValue: cExport }; + this._items.splice(nIdx, 0, data); + if (Array.isArray(this._currentValueIndices)) { + let index = this._currentValueIndices.findIndex(i => i >= nIdx); + if (index !== -1) { + for (const ii = this._currentValueIndices.length; index < ii; index++) { + ++this._currentValueIndices[index]; + } + } + } else if (this._currentValueIndices >= nIdx) { + ++this._currentValueIndices; + } + + this._send({ id: this._id, insert: { index: nIdx, ...data } }); + } + setAction(cTrigger, cScript) { if (typeof cTrigger !== "string" || typeof cScript !== "string") { return; @@ -151,6 +317,26 @@ class Field extends PDFObject { this._send({ id: this._id, focus: true }); } + setItems(oArray) { + if (!this._isChoice) { + throw new Error("Not a choice widget"); + } + this._items.length = 0; + for (const element of oArray) { + let displayValue, exportValue; + if (Array.isArray(element)) { + displayValue = element[0]?.toString() || ""; + exportValue = element[1]?.toString() || ""; + } else { + displayValue = exportValue = element?.toString() || ""; + } + this._items.push({ displayValue, exportValue }); + } + this._currentValueIndices = 0; + + this._send({ id: this._id, items: this._items }); + } + _isButton() { return false; } diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index ec3d50026..2fedfc7a3 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -489,10 +489,6 @@ describe("Interaction", () => { pages = await loadAndWait("js-authors.pdf", "#\\32 5R"); }); - afterAll(async () => { - await closePages(pages); - }); - it("must print authors in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { @@ -506,4 +502,132 @@ describe("Interaction", () => { ); }); }); + + describe("in listbox_actions.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("listbox_actions.pdf", "#\\33 3R"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must print selected value in a text field", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + for (const num of [7, 6, 4, 3, 2, 1]) { + await page.click(`option[value=Export${num}]`); + await page.waitForFunction( + `document.querySelector("#\\\\33 3R").value !== ""` + ); + const text = await page.$eval("#\\33 3R", el => el.value); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(`Item${num},Export${num}`); + } + }) + ); + }); + + it("must clear and restore list elements", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // Click on ClearItems button. + await page.click("[data-annotation-id='34R']"); + await page.waitForFunction( + `document.querySelector("#\\\\33 0R").children.length === 0` + ); + + // Click on Restore button. + await page.click("[data-annotation-id='37R']"); + await page.waitForFunction( + `document.querySelector("#\\\\33 0R").children.length !== 0` + ); + + for (const num of [7, 6, 4, 3, 2, 1]) { + await page.click(`option[value=Export${num}]`); + await page.waitForFunction( + `document.querySelector("#\\\\33 3R").value !== ""` + ); + const text = await page.$eval("#\\33 3R", el => el.value); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(`Item${num},Export${num}`); + } + }) + ); + }); + + it("must insert new elements", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + let len = 6; + for (const num of [1, 3, 5, 6, 431, -1, 0]) { + ++len; + await clearInput(page, "#\\33 9R"); + await page.type("#\\33 9R", `${num},Insert${num},Tresni${num}`, { + delay: 10, + }); + + // Click on AddItem button. + await page.click("[data-annotation-id='38R']"); + + await page.waitForFunction( + `document.querySelector("#\\\\33 0R").children.length === ${len}` + ); + + // Click on newly added option. + await page.select("#\\33 0R", `Tresni${num}`); + + await page.waitForFunction( + `document.querySelector("#\\\\33 3R").value !== ""` + ); + const text = await page.$eval("#\\33 3R", el => el.value); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(`Insert${num},Tresni${num}`); + } + }) + ); + }); + + it("must delete some element", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + let len = 6; + // Click on Restore button. + await page.click("[data-annotation-id='37R']"); + await page.waitForFunction( + `document.querySelector("#\\\\33 0R").children.length === ${len}` + ); + + for (const num of [2, 5]) { + --len; + await clearInput(page, "#\\33 9R"); + await page.type("#\\33 9R", `${num}`); + + // Click on DeleteItem button. + await page.click("[data-annotation-id='36R']"); + + await page.waitForFunction( + `document.querySelector("#\\\\33 0R").children.length === ${len}` + ); + } + + for (const num of [6, 4, 2, 1]) { + await page.click(`option[value=Export${num}]`); + await page.waitForFunction( + `document.querySelector("#\\\\33 3R").value !== ""` + ); + const text = await page.$eval("#\\33 3R", el => el.value); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(`Item${num},Export${num}`); + } + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index b53fb8b1e..24e86226d 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -403,6 +403,7 @@ !operator-in-TJ-array.pdf !issue7878.pdf !font_ascent_descent.pdf +!listbox_actions.pdf !issue11442_reduced.pdf !issue11549_reduced.pdf !issue8097_reduced.pdf diff --git a/test/pdfs/listbox_actions.pdf b/test/pdfs/listbox_actions.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cfaaadc80218fba99fc3b2de5b66d2d8134640d1 GIT binary patch literal 9022 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 QYQRxBbb+Gux5qC22k=FjQ2+n{ literal 0 HcmV?d00001