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();