From 6cba5509f26ede9986cbc4c100b037d3347d7c91 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 14 Sep 2021 12:06:28 +0200 Subject: [PATCH 1/2] Re-factor `document.getElementsByName` lookups in the AnnotationLayer (issue 14003) This replaces direct `document.getElementsByName` lookups with a helper method which: - Lets the AnnotationLayer use the data returned by the `PDFDocumentProxy.getFieldObjects` API-method, such that we can directly lookup only the necessary DOM elements. - Fallback to using `document.getElementsByName` as before, such that e.g. the standalone viewer components still work. Finally, to fix the problems reported in issue 14003, regardless of the code-path we now also enforce that the DOM elements found were actually created by the AnnotationLayer code. With these changes we'll thus be able to update form elements on all visible pages just as before, but we'll additionally update the AnnotationStorage for not-yet-rendered elements thus fixing a pre-existing bug. --- src/display/annotation_layer.js | 114 +++++++++++++++++++++++--------- src/display/api.js | 11 +-- web/annotation_layer_builder.js | 87 +++++++++++++----------- web/base_viewer.js | 7 +- web/interfaces.js | 5 +- web/pdf_page_view.js | 3 +- 6 files changed, 149 insertions(+), 78 deletions(-) diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 1a1d31776..33e9038ab 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -34,6 +34,7 @@ import { AnnotationStorage } from "./annotation_storage.js"; import { ColorConverters } from "../shared/scripting_utils.js"; const DEFAULT_TAB_INDEX = 1000; +const GetElementsByNameSet = new WeakSet(); /** * @typedef {Object} AnnotationElementParameters @@ -50,6 +51,7 @@ const DEFAULT_TAB_INDEX = 1000; * @property {Object} svgFactory * @property {boolean} [enableScripting] * @property {boolean} [hasJSActions] + * @property {Object} [fieldObjects] * @property {Object} [mouseState] */ @@ -159,6 +161,7 @@ class AnnotationElement { this.annotationStorage = parameters.annotationStorage; this.enableScripting = parameters.enableScripting; this.hasJSActions = parameters.hasJSActions; + this._fieldObjects = parameters.fieldObjects; this._mouseState = parameters.mouseState; if (isRenderable) { @@ -363,6 +366,51 @@ class AnnotationElement { unreachable("Abstract method `AnnotationElement.render` called"); } + /** + * @private + * @returns {Array} + */ + _getElementsByName(name, skipId = null) { + const fields = []; + + if (this._fieldObjects) { + const fieldObj = this._fieldObjects[name]; + if (fieldObj) { + for (const { page, id, exportValues } of fieldObj) { + if (page === -1) { + continue; + } + if (id === skipId) { + continue; + } + const exportValue = + typeof exportValues === "string" ? exportValues : null; + + const domElement = document.getElementById(id); + if (domElement && !GetElementsByNameSet.has(domElement)) { + warn(`_getElementsByName - element not allowed: ${id}`); + continue; + } + fields.push({ id, exportValue, domElement }); + } + } + return fields; + } + // Fallback to a regular DOM lookup, to ensure that the standalone + // viewer components won't break. + for (const domElement of document.getElementsByName(name)) { + const { id, exportValue } = domElement; + if (id === skipId) { + continue; + } + if (!GetElementsByNameSet.has(domElement)) { + continue; + } + fields.push({ id, exportValue, domElement }); + } + return fields; + } + static get platform() { const platform = typeof navigator !== "undefined" ? navigator.platform : ""; @@ -687,13 +735,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { setPropertyOnSiblings(base, key, value, keyInStorage) { const storage = this.annotationStorage; - for (const element of document.getElementsByName(base.name)) { - if (element !== base) { - element[key] = value; - const data = Object.create(null); - data[keyInStorage] = value; - storage.setValue(element.getAttribute("id"), data); + for (const element of this._getElementsByName( + base.name, + /* skipId = */ base.id + )) { + if (element.domElement) { + element.domElement[key] = value; } + storage.setValue(element.id, { [keyInStorage]: value }); } } @@ -728,6 +777,9 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { element.type = "text"; element.setAttribute("value", textContent); } + GetElementsByNameSet.add(element); + element.disabled = this.data.readOnly; + element.name = this.data.fieldName; element.tabIndex = DEFAULT_TAB_INDEX; elementData.userValue = textContent; @@ -900,9 +952,6 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { element.addEventListener("blur", blurListener); } - element.disabled = this.data.readOnly; - element.name = this.data.fieldName; - if (this.data.maxLen !== null) { element.maxLength = this.data.maxLen; } @@ -978,6 +1027,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { this.container.className = "buttonWidgetAnnotation checkBox"; const element = document.createElement("input"); + GetElementsByNameSet.add(element); element.disabled = data.readOnly; element.type = "checkbox"; element.name = data.fieldName; @@ -988,19 +1038,14 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { element.setAttribute("exportValue", data.exportValue); element.tabIndex = DEFAULT_TAB_INDEX; - element.addEventListener("change", function (event) { - const name = event.target.name; - const checked = event.target.checked; - for (const checkbox of document.getElementsByName(name)) { - if (checkbox !== event.target) { - checkbox.checked = - checked && - checkbox.getAttribute("exportValue") === data.exportValue; - storage.setValue( - checkbox.parentNode.getAttribute("data-annotation-id"), - { value: false } - ); + element.addEventListener("change", event => { + const { name, checked } = event.target; + for (const checkbox of this._getElementsByName(name, /* skipId = */ id)) { + const curChecked = checked && checkbox.exportValue === data.exportValue; + if (checkbox.domElement) { + checkbox.domElement.checked = curChecked; } + storage.setValue(checkbox.id, { value: curChecked }); } storage.setValue(id, { value: checked }); }); @@ -1057,6 +1102,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { } const element = document.createElement("input"); + GetElementsByNameSet.add(element); element.disabled = data.readOnly; element.type = "radio"; element.name = data.fieldName; @@ -1066,26 +1112,26 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { element.setAttribute("id", id); element.tabIndex = DEFAULT_TAB_INDEX; - element.addEventListener("change", function (event) { - const { target } = event; - for (const radio of document.getElementsByName(target.name)) { - if (radio !== target) { - storage.setValue(radio.getAttribute("id"), { value: false }); - } + element.addEventListener("change", event => { + const { name, checked } = event.target; + for (const radio of this._getElementsByName(name, /* skipId = */ id)) { + storage.setValue(radio.id, { value: false }); } - storage.setValue(id, { value: target.checked }); + storage.setValue(id, { value: checked }); }); if (this.enableScripting && this.hasJSActions) { const pdfButtonValue = data.buttonValue; element.addEventListener("updatefromsandbox", jsEvent => { const actions = { - value(event) { + value: event => { const checked = pdfButtonValue === event.detail.value; - for (const radio of document.getElementsByName(event.target.name)) { - const radioId = radio.getAttribute("id"); - radio.checked = radioId === id && checked; - storage.setValue(radioId, { value: radio.checked }); + for (const radio of this._getElementsByName(event.target.name)) { + const curChecked = checked && radio.id === id; + if (radio.domElement) { + radio.domElement.checked = curChecked; + } + storage.setValue(radio.id, { value: curChecked }); } }, }; @@ -1158,6 +1204,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { const fontSizeStyle = `calc(${fontSize}px * var(--zoom-factor))`; const selectElement = document.createElement("select"); + GetElementsByNameSet.add(selectElement); selectElement.disabled = this.data.readOnly; selectElement.name = this.data.fieldName; selectElement.setAttribute("id", id); @@ -2090,6 +2137,7 @@ class AnnotationLayer { parameters.annotationStorage || new AnnotationStorage(), enableScripting: parameters.enableScripting, hasJSActions: parameters.hasJSActions, + fieldObjects: parameters.fieldObjects, mouseState: parameters.mouseState || { isDown: false }, }); if (element.isRenderable) { diff --git a/src/display/api.js b/src/display/api.js index 0ae803035..61a1b8500 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1017,9 +1017,9 @@ class PDFDocumentProxy { } /** - * @returns {Promise | null>} A promise that is resolved with an - * {Array} containing /AcroForm field data for the JS sandbox, - * or `null` when no field data is present in the PDF file. + * @returns {Promise> | null>} A promise that is + * resolved with an {Object} containing /AcroForm field data for the JS + * sandbox, or `null` when no field data is present in the PDF file. */ getFieldObjects() { return this._transport.getFieldObjects(); @@ -2480,6 +2480,7 @@ class WorkerTransport { Promise.all(waitOn).then(() => { this.commonObjs.clear(); this.fontLoader.clear(); + this._getFieldObjectsPromise = null; this._hasJSActionsPromise = null; if (this._networkStream) { @@ -2921,7 +2922,8 @@ class WorkerTransport { } getFieldObjects() { - return this.messageHandler.sendWithPromise("GetFieldObjects", null); + return (this._getFieldObjectsPromise ||= + this.messageHandler.sendWithPromise("GetFieldObjects", null)); } hasJSActions() { @@ -3050,6 +3052,7 @@ class WorkerTransport { if (!keepLoadedFonts) { this.fontLoader.clear(); } + this._getFieldObjectsPromise = null; this._hasJSActionsPromise = null; } diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index b191c53f2..843b07b2c 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -33,6 +33,8 @@ import { SimpleLinkService } from "./pdf_link_service.js"; * @property {IL10n} l10n - Localization service. * @property {boolean} [enableScripting] * @property {Promise} [hasJSActionsPromise] + * @property {Promise> | null>} + * [fieldObjectsPromise] * @property {Object} [mouseState] */ @@ -51,6 +53,7 @@ class AnnotationLayerBuilder { l10n = NullL10n, enableScripting = false, hasJSActionsPromise = null, + fieldObjectsPromise = null, mouseState = null, }) { this.pageDiv = pageDiv; @@ -63,6 +66,7 @@ class AnnotationLayerBuilder { this.annotationStorage = annotationStorage; this.enableScripting = enableScripting; this._hasJSActionsPromise = hasJSActionsPromise; + this._fieldObjectsPromise = fieldObjectsPromise; this._mouseState = mouseState; this.div = null; @@ -75,46 +79,49 @@ class AnnotationLayerBuilder { * @returns {Promise} A promise that is resolved when rendering of the * annotations is complete. */ - render(viewport, intent = "display") { - return Promise.all([ - this.pdfPage.getAnnotations({ intent }), - this._hasJSActionsPromise, - ]).then(([annotations, hasJSActions = false]) => { - if (this._cancelled || annotations.length === 0) { - return; - } + async render(viewport, intent = "display") { + const [annotations, hasJSActions = false, fieldObjects = null] = + await Promise.all([ + this.pdfPage.getAnnotations({ intent }), + this._hasJSActionsPromise, + this._fieldObjectsPromise, + ]); - const parameters = { - viewport: viewport.clone({ dontFlip: true }), - div: this.div, - annotations, - page: this.pdfPage, - imageResourcesPath: this.imageResourcesPath, - renderForms: this.renderForms, - linkService: this.linkService, - downloadManager: this.downloadManager, - annotationStorage: this.annotationStorage, - enableScripting: this.enableScripting, - hasJSActions, - mouseState: this._mouseState, - }; + if (this._cancelled || annotations.length === 0) { + return; + } - if (this.div) { - // If an annotationLayer already exists, refresh its children's - // transformation matrices. - AnnotationLayer.update(parameters); - } else { - // Create an annotation layer div and render the annotations - // if there is at least one annotation. - this.div = document.createElement("div"); - this.div.className = "annotationLayer"; - this.pageDiv.appendChild(this.div); - parameters.div = this.div; + const parameters = { + viewport: viewport.clone({ dontFlip: true }), + div: this.div, + annotations, + page: this.pdfPage, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.renderForms, + linkService: this.linkService, + downloadManager: this.downloadManager, + annotationStorage: this.annotationStorage, + enableScripting: this.enableScripting, + hasJSActions, + fieldObjects, + mouseState: this._mouseState, + }; - AnnotationLayer.render(parameters); - this.l10n.translate(this.div); - } - }); + if (this.div) { + // If an annotationLayer already exists, refresh its children's + // transformation matrices. + AnnotationLayer.update(parameters); + } else { + // Create an annotation layer div and render the annotations + // if there is at least one annotation. + this.div = document.createElement("div"); + this.div.className = "annotationLayer"; + this.pageDiv.appendChild(this.div); + parameters.div = this.div; + + AnnotationLayer.render(parameters); + this.l10n.translate(this.div); + } } cancel() { @@ -144,6 +151,8 @@ class DefaultAnnotationLayerFactory { * @param {boolean} [enableScripting] * @param {Promise} [hasJSActionsPromise] * @param {Object} [mouseState] + * @param {Promise> | null>} + * [fieldObjectsPromise] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -155,7 +164,8 @@ class DefaultAnnotationLayerFactory { l10n = NullL10n, enableScripting = false, hasJSActionsPromise = null, - mouseState = null + mouseState = null, + fieldObjectsPromise = null ) { return new AnnotationLayerBuilder({ pageDiv, @@ -167,6 +177,7 @@ class DefaultAnnotationLayerFactory { annotationStorage, enableScripting, hasJSActionsPromise, + fieldObjectsPromise, mouseState, }); } diff --git a/web/base_viewer.js b/web/base_viewer.js index 24c0c062f..b1b1086da 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -1318,6 +1318,8 @@ class BaseViewer { * @param {boolean} [enableScripting] * @param {Promise} [hasJSActionsPromise] * @param {Object} [mouseState] + * @param {Promise> | null>} + * [fieldObjectsPromise] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -1329,7 +1331,8 @@ class BaseViewer { l10n = NullL10n, enableScripting = null, hasJSActionsPromise = null, - mouseState = null + mouseState = null, + fieldObjectsPromise = null ) { return new AnnotationLayerBuilder({ pageDiv, @@ -1344,6 +1347,8 @@ class BaseViewer { enableScripting: enableScripting ?? this.enableScripting, hasJSActionsPromise: hasJSActionsPromise || this.pdfDocument?.hasJSActions(), + fieldObjectsPromise: + fieldObjectsPromise || this.pdfDocument?.getFieldObjects(), mouseState: mouseState || this._scriptingManager?.mouseState, }); } diff --git a/web/interfaces.js b/web/interfaces.js index b3315eee4..efc66145d 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -166,6 +166,8 @@ class IPDFAnnotationLayerFactory { * @param {boolean} [enableScripting] * @param {Promise} [hasJSActionsPromise] * @param {Object} [mouseState] + * @param {Promise> | null>} + * [fieldObjectsPromise] * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -177,7 +179,8 @@ class IPDFAnnotationLayerFactory { l10n = undefined, enableScripting = false, hasJSActionsPromise = null, - mouseState = null + mouseState = null, + fieldObjectsPromise = null ) {} } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 00951f653..aac1ab729 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -675,7 +675,8 @@ class PDFPageView { this.l10n, /* enableScripting = */ null, /* hasJSActionsPromise = */ null, - /* mouseState = */ null + /* mouseState = */ null, + /* fieldObjectsPromise = */ null ); } this._renderAnnotationLayer(); From 386acf5bdd0788224fba604339be2e887e4aa225 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 17 Sep 2021 11:29:58 +0200 Subject: [PATCH 2/2] Integration test for PR #14023 --- test/integration/annotation_spec.js | 95 ++++++++++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/issue14023.pdf | Bin 0 -> 12962 bytes 3 files changed, 96 insertions(+) create mode 100644 test/pdfs/issue14023.pdf diff --git a/test/integration/annotation_spec.js b/test/integration/annotation_spec.js index a2444ea01..5fcd7901e 100644 --- a/test/integration/annotation_spec.js +++ b/test/integration/annotation_spec.js @@ -126,3 +126,98 @@ describe("Text widget", () => { }); }); }); + +describe("Annotation and storage", () => { + describe("issue14023.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue14023.pdf", "[data-annotation-id='64R']"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must let checkboxes with the same name behave like radio buttons", async () => { + const text1 = "hello world!"; + const text2 = "!dlrow olleh"; + await Promise.all( + pages.map(async ([browserName, page]) => { + // Text field. + await page.type("#\\36 4R", text1); + // Checkbox. + await page.click("[data-annotation-id='65R']"); + // Radio. + await page.click("[data-annotation-id='67R']"); + + for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [ + [2, "#\\31 8R", "#\\31 9R", "#\\32 1R", "#\\32 0R"], + [5, "#\\32 3R", "#\\32 4R", "#\\32 2R", "#\\32 5R"], + ]) { + await page.evaluate(n => { + window.document + .querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0] + .scrollIntoView(); + }, pageNumber); + + // Need to wait to have a displayed text input. + await page.waitForSelector(textId, { + timeout: 0, + }); + + const text = await page.$eval(textId, el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(text1); + + let checked = await page.$eval(checkId, el => el.checked); + expect(checked).toEqual(true); + + checked = await page.$eval(radio1Id, el => el.checked); + expect(checked).toEqual(false); + + checked = await page.$eval(radio2Id, el => el.checked); + expect(checked).toEqual(false); + } + + // Change data on page 5 and check that other pages changed. + // Text field. + await page.type("#\\32 3R", text2); + // Checkbox. + await page.click("[data-annotation-id='24R']"); + // Radio. + await page.click("[data-annotation-id='25R']"); + + for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [ + [1, "#\\36 4R", "#\\36 5R", "#\\36 7R", "#\\36 8R"], + [2, "#\\31 8R", "#\\31 9R", "#\\32 1R", "#\\32 0R"], + ]) { + await page.evaluate(n => { + window.document + .querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0] + .scrollIntoView(); + }, pageNumber); + + // Need to wait to have a displayed text input. + await page.waitForSelector(textId, { + timeout: 0, + }); + + const text = await page.$eval(textId, el => el.value); + expect(text) + .withContext(`In ${browserName}`) + .toEqual(text2 + text1); + + let checked = await page.$eval(checkId, el => el.checked); + expect(checked).toEqual(false); + + checked = await page.$eval(radio1Id, el => el.checked); + expect(checked).toEqual(false); + + checked = await page.$eval(radio2Id, el => el.checked); + expect(checked).toEqual(false); + } + }) + ); + }); + }); +}); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 3790b3514..a53250667 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -112,6 +112,7 @@ !issue11651.pdf !issue11878.pdf !issue13916.pdf +!issue14023.pdf !bad-PageLabels.pdf !decodeACSuccessive.pdf !filled-background.pdf diff --git a/test/pdfs/issue14023.pdf b/test/pdfs/issue14023.pdf new file mode 100644 index 0000000000000000000000000000000000000000..03d3ca8ec3814bc47e7f2b519d04f7f65788bd6d GIT binary patch literal 12962 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