Skip to content

Commit 525a3f0

Browse files
committed
Re-factor document.getElementsByName lookups in 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.
1 parent 064c21d commit 525a3f0

File tree

6 files changed

+132
-65
lines changed

6 files changed

+132
-65
lines changed

src/display/annotation_layer.js

+71-23
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { AnnotationStorage } from "./annotation_storage.js";
3434
import { ColorConverters } from "../shared/scripting_utils.js";
3535

3636
const DEFAULT_TAB_INDEX = 1000;
37+
const GetElementsByNameSet = new WeakSet();
3738

3839
/**
3940
* @typedef {Object} AnnotationElementParameters
@@ -50,6 +51,7 @@ const DEFAULT_TAB_INDEX = 1000;
5051
* @property {Object} svgFactory
5152
* @property {boolean} [enableScripting]
5253
* @property {boolean} [hasJSActions]
54+
* @property {Object} [fieldObjects]
5355
* @property {Object} [mouseState]
5456
*/
5557

@@ -159,6 +161,7 @@ class AnnotationElement {
159161
this.annotationStorage = parameters.annotationStorage;
160162
this.enableScripting = parameters.enableScripting;
161163
this.hasJSActions = parameters.hasJSActions;
164+
this._fieldObjects = parameters.fieldObjects;
162165
this._mouseState = parameters.mouseState;
163166

164167
if (isRenderable) {
@@ -363,6 +366,43 @@ class AnnotationElement {
363366
unreachable("Abstract method `AnnotationElement.render` called");
364367
}
365368

369+
/**
370+
* @private
371+
* @returns {Array}
372+
*/
373+
_getElementsByName(name) {
374+
const fields = [];
375+
376+
if (this._fieldObjects) {
377+
const fieldObj = this._fieldObjects[name];
378+
if (fieldObj) {
379+
for (const { id, page } of fieldObj) {
380+
if (page === -1) {
381+
continue;
382+
}
383+
const domElement = document.getElementById(id) || null;
384+
if (domElement && !GetElementsByNameSet.has(domElement)) {
385+
warn(`_getElementsByName - element not allowed: ${id}`);
386+
continue;
387+
}
388+
fields.push({ id, domElement });
389+
}
390+
}
391+
return fields;
392+
}
393+
// Fallback to a regular DOM lookup.
394+
for (const domElement of document.getElementsByName(name)) {
395+
if (!GetElementsByNameSet.has(domElement)) {
396+
continue;
397+
}
398+
fields.push({
399+
id: domElement.getAttribute("id"),
400+
domElement,
401+
});
402+
}
403+
return fields;
404+
}
405+
366406
static get platform() {
367407
const platform = typeof navigator !== "undefined" ? navigator.platform : "";
368408

@@ -683,13 +723,14 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
683723

684724
setPropertyOnSiblings(base, key, value, keyInStorage) {
685725
const storage = this.annotationStorage;
686-
for (const element of document.getElementsByName(base.name)) {
687-
if (element !== base) {
688-
element[key] = value;
689-
const data = Object.create(null);
690-
data[keyInStorage] = value;
691-
storage.setValue(element.getAttribute("id"), data);
726+
for (const element of this._getElementsByName(base.name)) {
727+
if (element.domElement === base) {
728+
continue;
729+
}
730+
if (element.domElement) {
731+
element.domElement[key] = value;
692732
}
733+
storage.setValue(element.id, { [keyInStorage]: value });
693734
}
694735
}
695736

@@ -724,6 +765,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
724765
element.type = "text";
725766
element.setAttribute("value", textContent);
726767
}
768+
GetElementsByNameSet.add(element);
727769
element.tabIndex = DEFAULT_TAB_INDEX;
728770

729771
elementData.userValue = textContent;
@@ -974,6 +1016,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
9741016
this.container.className = "buttonWidgetAnnotation checkBox";
9751017

9761018
const element = document.createElement("input");
1019+
GetElementsByNameSet.add(element);
9771020
element.disabled = data.readOnly;
9781021
element.type = "checkbox";
9791022
element.name = this.data.fieldName;
@@ -983,16 +1026,15 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
9831026
element.setAttribute("id", id);
9841027
element.tabIndex = DEFAULT_TAB_INDEX;
9851028

986-
element.addEventListener("change", function (event) {
987-
const name = event.target.name;
988-
for (const checkbox of document.getElementsByName(name)) {
989-
if (checkbox !== event.target) {
990-
checkbox.checked = false;
991-
storage.setValue(
992-
checkbox.parentNode.getAttribute("data-annotation-id"),
993-
{ value: false }
994-
);
1029+
element.addEventListener("change", event => {
1030+
for (const checkbox of this._getElementsByName(event.target.name)) {
1031+
if (checkbox.domElement === event.target) {
1032+
continue;
9951033
}
1034+
if (checkbox.domElement) {
1035+
checkbox.domElement.checked = false;
1036+
}
1037+
storage.setValue(checkbox.id, { value: false });
9961038
}
9971039
storage.setValue(id, { value: event.target.checked });
9981040
});
@@ -1049,6 +1091,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
10491091
}
10501092

10511093
const element = document.createElement("input");
1094+
GetElementsByNameSet.add(element);
10521095
element.disabled = data.readOnly;
10531096
element.type = "radio";
10541097
element.name = data.fieldName;
@@ -1058,26 +1101,29 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
10581101
element.setAttribute("id", id);
10591102
element.tabIndex = DEFAULT_TAB_INDEX;
10601103

1061-
element.addEventListener("change", function (event) {
1104+
element.addEventListener("change", event => {
10621105
const { target } = event;
1063-
for (const radio of document.getElementsByName(target.name)) {
1064-
if (radio !== target) {
1065-
storage.setValue(radio.getAttribute("id"), { value: false });
1106+
for (const radio of this._getElementsByName(target.name)) {
1107+
if (radio.domElement !== target) {
1108+
storage.setValue(radio.id, { value: false });
10661109
}
10671110
}
10681111
storage.setValue(id, { value: target.checked });
10691112
});
10701113

10711114
if (this.enableScripting && this.hasJSActions) {
10721115
const pdfButtonValue = data.buttonValue;
1116+
const self = this;
10731117
element.addEventListener("updatefromsandbox", jsEvent => {
10741118
const actions = {
10751119
value(event) {
10761120
const checked = pdfButtonValue === event.detail.value;
1077-
for (const radio of document.getElementsByName(event.target.name)) {
1078-
const radioId = radio.getAttribute("id");
1079-
radio.checked = radioId === id && checked;
1080-
storage.setValue(radioId, { value: radio.checked });
1121+
for (const radio of self._getElementsByName(event.target.name)) {
1122+
const curChecked = radio.id === id && checked;
1123+
if (radio.domElement) {
1124+
radio.domElement.checked = curChecked;
1125+
}
1126+
storage.setValue(radio.id, { value: curChecked });
10811127
}
10821128
},
10831129
};
@@ -1150,6 +1196,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
11501196
const fontSizeStyle = `calc(${fontSize}px * var(--zoom-factor))`;
11511197

11521198
const selectElement = document.createElement("select");
1199+
GetElementsByNameSet.add(selectElement);
11531200
selectElement.disabled = this.data.readOnly;
11541201
selectElement.name = this.data.fieldName;
11551202
selectElement.setAttribute("id", id);
@@ -2082,6 +2129,7 @@ class AnnotationLayer {
20822129
parameters.annotationStorage || new AnnotationStorage(),
20832130
enableScripting: parameters.enableScripting,
20842131
hasJSActions: parameters.hasJSActions,
2132+
fieldObjects: parameters.fieldObjects,
20852133
mouseState: parameters.mouseState || { isDown: false },
20862134
});
20872135
if (element.isRenderable) {

src/display/api.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -2480,6 +2480,7 @@ class WorkerTransport {
24802480
Promise.all(waitOn).then(() => {
24812481
this.commonObjs.clear();
24822482
this.fontLoader.clear();
2483+
this._getFieldObjectsPromise = null;
24832484
this._hasJSActionsPromise = null;
24842485

24852486
if (this._networkStream) {
@@ -2921,7 +2922,8 @@ class WorkerTransport {
29212922
}
29222923

29232924
getFieldObjects() {
2924-
return this.messageHandler.sendWithPromise("GetFieldObjects", null);
2925+
return (this._getFieldObjectsPromise ||=
2926+
this.messageHandler.sendWithPromise("GetFieldObjects", null));
29252927
}
29262928

29272929
hasJSActions() {
@@ -3050,6 +3052,7 @@ class WorkerTransport {
30503052
if (!keepLoadedFonts) {
30513053
this.fontLoader.clear();
30523054
}
3055+
this._getFieldObjectsPromise = null;
30533056
this._hasJSActionsPromise = null;
30543057
}
30553058

web/annotation_layer_builder.js

+47-38
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { SimpleLinkService } from "./pdf_link_service.js";
3333
* @property {IL10n} l10n - Localization service.
3434
* @property {boolean} [enableScripting]
3535
* @property {Promise<boolean>} [hasJSActionsPromise]
36+
* @property {Promise<Array<Object> | null} [fieldObjectsPromise]
3637
* @property {Object} [mouseState]
3738
*/
3839

@@ -51,6 +52,7 @@ class AnnotationLayerBuilder {
5152
l10n = NullL10n,
5253
enableScripting = false,
5354
hasJSActionsPromise = null,
55+
fieldObjectsPromise = null,
5456
mouseState = null,
5557
}) {
5658
this.pageDiv = pageDiv;
@@ -63,6 +65,7 @@ class AnnotationLayerBuilder {
6365
this.annotationStorage = annotationStorage;
6466
this.enableScripting = enableScripting;
6567
this._hasJSActionsPromise = hasJSActionsPromise;
68+
this._fieldObjectsPromise = fieldObjectsPromise;
6669
this._mouseState = mouseState;
6770

6871
this.div = null;
@@ -75,46 +78,49 @@ class AnnotationLayerBuilder {
7578
* @returns {Promise<void>} A promise that is resolved when rendering of the
7679
* annotations is complete.
7780
*/
78-
render(viewport, intent = "display") {
79-
return Promise.all([
80-
this.pdfPage.getAnnotations({ intent }),
81-
this._hasJSActionsPromise,
82-
]).then(([annotations, hasJSActions = false]) => {
83-
if (this._cancelled || annotations.length === 0) {
84-
return;
85-
}
81+
async render(viewport, intent = "display") {
82+
const [annotations, hasJSActions = false, fieldObjects = null] =
83+
await Promise.all([
84+
this.pdfPage.getAnnotations({ intent }),
85+
this._hasJSActionsPromise,
86+
this._fieldObjectsPromise,
87+
]);
8688

87-
const parameters = {
88-
viewport: viewport.clone({ dontFlip: true }),
89-
div: this.div,
90-
annotations,
91-
page: this.pdfPage,
92-
imageResourcesPath: this.imageResourcesPath,
93-
renderForms: this.renderForms,
94-
linkService: this.linkService,
95-
downloadManager: this.downloadManager,
96-
annotationStorage: this.annotationStorage,
97-
enableScripting: this.enableScripting,
98-
hasJSActions,
99-
mouseState: this._mouseState,
100-
};
89+
if (this._cancelled || annotations.length === 0) {
90+
return;
91+
}
10192

102-
if (this.div) {
103-
// If an annotationLayer already exists, refresh its children's
104-
// transformation matrices.
105-
AnnotationLayer.update(parameters);
106-
} else {
107-
// Create an annotation layer div and render the annotations
108-
// if there is at least one annotation.
109-
this.div = document.createElement("div");
110-
this.div.className = "annotationLayer";
111-
this.pageDiv.appendChild(this.div);
112-
parameters.div = this.div;
93+
const parameters = {
94+
viewport: viewport.clone({ dontFlip: true }),
95+
div: this.div,
96+
annotations,
97+
page: this.pdfPage,
98+
imageResourcesPath: this.imageResourcesPath,
99+
renderForms: this.renderForms,
100+
linkService: this.linkService,
101+
downloadManager: this.downloadManager,
102+
annotationStorage: this.annotationStorage,
103+
enableScripting: this.enableScripting,
104+
hasJSActions,
105+
fieldObjects,
106+
mouseState: this._mouseState,
107+
};
113108

114-
AnnotationLayer.render(parameters);
115-
this.l10n.translate(this.div);
116-
}
117-
});
109+
if (this.div) {
110+
// If an annotationLayer already exists, refresh its children's
111+
// transformation matrices.
112+
AnnotationLayer.update(parameters);
113+
} else {
114+
// Create an annotation layer div and render the annotations
115+
// if there is at least one annotation.
116+
this.div = document.createElement("div");
117+
this.div.className = "annotationLayer";
118+
this.pageDiv.appendChild(this.div);
119+
parameters.div = this.div;
120+
121+
AnnotationLayer.render(parameters);
122+
this.l10n.translate(this.div);
123+
}
118124
}
119125

120126
cancel() {
@@ -144,6 +150,7 @@ class DefaultAnnotationLayerFactory {
144150
* @param {boolean} [enableScripting]
145151
* @param {Promise<boolean>} [hasJSActionsPromise]
146152
* @param {Object} [mouseState]
153+
* @param {Promise<Array<Object> | null} [fieldObjectsPromise]
147154
* @returns {AnnotationLayerBuilder}
148155
*/
149156
createAnnotationLayerBuilder(
@@ -155,7 +162,8 @@ class DefaultAnnotationLayerFactory {
155162
l10n = NullL10n,
156163
enableScripting = false,
157164
hasJSActionsPromise = null,
158-
mouseState = null
165+
mouseState = null,
166+
fieldObjectsPromise = null
159167
) {
160168
return new AnnotationLayerBuilder({
161169
pageDiv,
@@ -167,6 +175,7 @@ class DefaultAnnotationLayerFactory {
167175
annotationStorage,
168176
enableScripting,
169177
hasJSActionsPromise,
178+
fieldObjectsPromise,
170179
mouseState,
171180
});
172181
}

web/base_viewer.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,7 @@ class BaseViewer {
13051305
* @param {boolean} [enableScripting]
13061306
* @param {Promise<boolean>} [hasJSActionsPromise]
13071307
* @param {Object} [mouseState]
1308+
* @param {Promise<Array<Object> | null} [fieldObjectsPromise]
13081309
* @returns {AnnotationLayerBuilder}
13091310
*/
13101311
createAnnotationLayerBuilder(
@@ -1316,7 +1317,8 @@ class BaseViewer {
13161317
l10n = NullL10n,
13171318
enableScripting = null,
13181319
hasJSActionsPromise = null,
1319-
mouseState = null
1320+
mouseState = null,
1321+
fieldObjectsPromise = null
13201322
) {
13211323
return new AnnotationLayerBuilder({
13221324
pageDiv,
@@ -1331,6 +1333,8 @@ class BaseViewer {
13311333
enableScripting: enableScripting ?? this.enableScripting,
13321334
hasJSActionsPromise:
13331335
hasJSActionsPromise || this.pdfDocument?.hasJSActions(),
1336+
fieldObjectsPromise:
1337+
fieldObjectsPromise || this.pdfDocument?.getFieldObjects(),
13341338
mouseState: mouseState || this._scriptingManager?.mouseState,
13351339
});
13361340
}

web/interfaces.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ class IPDFAnnotationLayerFactory {
166166
* @param {boolean} [enableScripting]
167167
* @param {Promise<boolean>} [hasJSActionsPromise]
168168
* @param {Object} [mouseState]
169+
* @param {Promise<Array<Object> | null} [fieldObjectsPromise]
169170
* @returns {AnnotationLayerBuilder}
170171
*/
171172
createAnnotationLayerBuilder(
@@ -177,7 +178,8 @@ class IPDFAnnotationLayerFactory {
177178
l10n = undefined,
178179
enableScripting = false,
179180
hasJSActionsPromise = null,
180-
mouseState = null
181+
mouseState = null,
182+
fieldObjectsPromise = null
181183
) {}
182184
}
183185

0 commit comments

Comments
 (0)