Skip to content

Commit 8ba1807

Browse files
authored
Merge pull request #20344 from calixteman/bug1992770
[Editor] Make sure that annotation positions in the DOM respect the visual order (bug 1992770)
2 parents 152324f + 41dea1e commit 8ba1807

5 files changed

Lines changed: 172 additions & 32 deletions

File tree

src/display/annotation_layer.js

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ class AnnotationElement {
186186
this.parent = parameters.parent;
187187

188188
if (isRenderable) {
189-
this.container = this._createContainer(ignoreBorder);
189+
this.contentElement = this.container =
190+
this._createContainer(ignoreBorder);
190191
}
191192
if (createQuadrilaterals) {
192193
this._createQuadrilaterals();
@@ -1008,6 +1009,7 @@ class LinkAnnotationElement extends AnnotationElement {
10081009

10091010
this.container.classList.add("linkAnnotation");
10101011
if (isBound) {
1012+
this.contentElement = link;
10111013
this.container.append(link);
10121014
}
10131015

@@ -1517,6 +1519,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
15171519
element.hidden = true;
15181520
}
15191521
GetElementsByNameSet.add(element);
1522+
this.contentElement = element;
15201523
element.setAttribute("data-element-id", id);
15211524

15221525
element.disabled = this.data.readOnly;
@@ -3070,7 +3073,7 @@ class FreeTextAnnotationElement extends AnnotationElement {
30703073
this.container.classList.add("freeTextAnnotation");
30713074

30723075
if (this.textContent) {
3073-
const content = document.createElement("div");
3076+
const content = (this.contentElement = document.createElement("div"));
30743077
content.classList.add("annotationTextContent");
30753078
content.setAttribute("role", "comment");
30763079
for (const line of this.textContent) {
@@ -3787,7 +3790,7 @@ class AnnotationLayer {
37873790
}
37883791

37893792
async #appendElement(element, id, popupElements) {
3790-
const contentElement = element.firstChild || element;
3793+
const { contentElement, container } = element;
37913794
const annotationId = (contentElement.id = `${AnnotationPrefix}${id}`);
37923795
const ariaAttributes =
37933796
await this.#structTreeLayer?.getAriaAttributes(annotationId);
@@ -3799,18 +3802,31 @@ class AnnotationLayer {
37993802

38003803
if (popupElements) {
38013804
// Set the popup just after the first element associated with the popup.
3802-
popupElements.at(-1).container.after(element);
3805+
popupElements.at(-1).container.after(container);
38033806
} else {
3804-
this.div.append(element);
3805-
this.#accessibilityManager?.moveElementInDOM(
3806-
this.div,
3807-
element,
3808-
contentElement,
3809-
/* isRemovable = */ false
3810-
);
3807+
this.#moveElementInDOM(container, contentElement);
38113808
}
38123809
}
38133810

3811+
#moveElementInDOM(container, contentElement) {
3812+
this.div.append(container);
3813+
this.#accessibilityManager?.moveElementInDOM(
3814+
this.div,
3815+
container,
3816+
contentElement,
3817+
/* isRemovable = */ false,
3818+
/* filter = */ node => node.nodeName === "SECTION",
3819+
/* inserter = */ (prevNode, node) => {
3820+
if (prevNode.nextElementSibling.nodeName === "BUTTON") {
3821+
// In case we have a comment button, insert after the button.
3822+
prevNode.nextElementSibling.after(node);
3823+
} else {
3824+
prevNode.after(node);
3825+
}
3826+
}
3827+
);
3828+
}
3829+
38143830
/**
38153831
* Render a new annotation layer with all annotation elements.
38163832
*
@@ -3877,7 +3893,7 @@ class AnnotationLayer {
38773893
if (data.hidden) {
38783894
rendered.style.visibility = "hidden";
38793895
}
3880-
await this.#appendElement(rendered, data.id, elementParams.elements);
3896+
await this.#appendElement(element, data.id, elementParams.elements);
38813897
element.extraPopupElement?.popup?.renderCommentButton();
38823898

38833899
if (element._isEditable) {
@@ -3913,8 +3929,8 @@ class AnnotationLayer {
39133929
if (!element.isRenderable) {
39143930
continue;
39153931
}
3916-
const rendered = element.render();
3917-
await this.#appendElement(rendered, data.id, null);
3932+
element.render();
3933+
await this.#appendElement(element, data.id, null);
39183934
}
39193935
}
39203936

@@ -3999,18 +4015,32 @@ class AnnotationLayer {
39994015
linkService: this.#linkService,
40004016
annotationStorage: this.#annotationStorage,
40014017
});
4002-
const htmlElement = element.render();
4003-
div.append(htmlElement);
4004-
this.#accessibilityManager?.moveElementInDOM(
4005-
div,
4006-
htmlElement,
4007-
htmlElement,
4008-
/* isRemovable = */ false
4009-
);
4018+
const rendered = element.render();
4019+
rendered.id = `${AnnotationPrefix}${id}`;
4020+
this.#moveElementInDOM(rendered, rendered);
40104021
element.createOrUpdatePopup();
40114022
return element;
40124023
}
40134024

4025+
togglePointerEvents(enabled = false) {
4026+
this.div.classList.toggle("disabled", !enabled);
4027+
}
4028+
4029+
updateFakeAnnotations(editors) {
4030+
if (editors.length === 0) {
4031+
return;
4032+
}
4033+
// In order to ensure that the annotations are correctly moved in the DOM
4034+
// we need to make sure that this has been laid out.
4035+
window.requestAnimationFrame(() =>
4036+
setTimeout(() => {
4037+
for (const editor of editors) {
4038+
editor.updateFakeAnnotationElement(this);
4039+
}
4040+
}, 10)
4041+
);
4042+
}
4043+
40144044
/**
40154045
* @private
40164046
*/

src/display/editor/annotation_editor_layer.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ class AnnotationEditorLayer {
242242
}
243243

244244
toggleAnnotationLayerPointerEvents(enabled = false) {
245-
this.#annotationLayer?.div.classList.toggle("disabled", !enabled);
245+
this.#annotationLayer?.togglePointerEvents(enabled);
246246
}
247247

248248
get #allEditorsIterator() {
@@ -354,13 +354,14 @@ class AnnotationEditorLayer {
354354
}
355355

356356
const annotationLayer = this.#annotationLayer;
357+
const needFakeAnnotation = [];
357358
if (annotationLayer) {
358359
const changedAnnotations = new Map();
359360
const resetAnnotations = new Map();
360361
for (const editor of this.#allEditorsIterator) {
361362
editor.disableEditing();
362363
if (!editor.annotationElementId) {
363-
editor.updateFakeAnnotationElement(annotationLayer);
364+
needFakeAnnotation.push(editor);
364365
continue;
365366
}
366367
if (editor.serialize() !== null) {
@@ -411,6 +412,7 @@ class AnnotationEditorLayer {
411412
}
412413
this.disableTextSelection();
413414
this.toggleAnnotationLayerPointerEvents(true);
415+
annotationLayer?.updateFakeAnnotations(needFakeAnnotation);
414416

415417
this.#isDisabling = false;
416418
}

test/integration/annotation_spec.mjs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe("Annotation highlight", () => {
9595
pages.map(async ([browserName, page]) => {
9696
for (const i of [23, 22, 14]) {
9797
await page.click(getAnnotationSelector(`${i}R`));
98-
await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus`);
98+
await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus-within`);
9999
}
100100
})
101101
);
@@ -878,4 +878,46 @@ a dynamic compiler for JavaScript based on our`);
878878
});
879879
});
880880
});
881+
882+
describe("Annotation order in the DOM", () => {
883+
let pages;
884+
885+
beforeEach(async () => {
886+
pages = await loadAndWait(
887+
"comments.pdf",
888+
".page[data-page-number='1'] .annotationLayer #pdfjs_internal_id_661R"
889+
);
890+
});
891+
892+
afterEach(async () => {
893+
await closePages(pages);
894+
});
895+
896+
it("must check that annotations are in the visual order", async () => {
897+
await Promise.all(
898+
pages.map(async ([browserName, page]) => {
899+
const sectionIds = await page.evaluate(() =>
900+
[
901+
...document.querySelectorAll(
902+
".page[data-page-number='1'] .annotationLayer > section:not(.popupAnnotation)"
903+
),
904+
].map(el => el.id.split("_").pop())
905+
);
906+
expect(sectionIds)
907+
.withContext(`In ${browserName}`)
908+
.toEqual([
909+
"612R",
910+
"693R",
911+
"687R",
912+
"690R",
913+
"713R",
914+
"673R",
915+
"613R",
916+
"680R",
917+
"661R",
918+
]);
919+
})
920+
);
921+
});
922+
});
881923
});

test/integration/comment_spec.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,4 +737,65 @@ describe("Comment", () => {
737737
);
738738
});
739739
});
740+
741+
describe("Annotations order in reading mode", () => {
742+
let pages;
743+
744+
beforeEach(async () => {
745+
pages = await loadAndWait(
746+
"comments.pdf",
747+
".annotationEditorLayer",
748+
"page-fit",
749+
null,
750+
{ enableComment: true }
751+
);
752+
});
753+
754+
afterEach(async () => {
755+
await closePages(pages);
756+
});
757+
758+
it("must check that the annotations are in the right order", async () => {
759+
await Promise.all(
760+
pages.map(async ([browserName, page]) => {
761+
await switchToHighlight(page);
762+
await highlightSpan(
763+
page,
764+
1,
765+
"method provides cheap inter-procedural type specialization, and an"
766+
);
767+
await editComment(page, getEditorSelector(9), "Hello world!");
768+
769+
await highlightSpan(page, 1, "Andreas Gal");
770+
await editComment(page, getEditorSelector(10), "Hello world!");
771+
772+
await switchToHighlight(page, /* disable = */ true);
773+
await page.waitForSelector(
774+
".annotationLayer section:nth-child(4).editorAnnotation"
775+
);
776+
777+
const sectionIds = await page.evaluate(() =>
778+
[
779+
...document.querySelectorAll(
780+
".page[data-page-number='1'] .annotationLayer > section:not(.popupAnnotation)"
781+
),
782+
].map(el => el.id.split("_").pop())
783+
);
784+
expect(sectionIds).withContext(`In ${browserName}`).toEqual([
785+
"612R",
786+
"693R",
787+
"10", // shortcut for pdfjs_internal_id_pdfjs_internal_editor_10
788+
"687R",
789+
"690R",
790+
"713R",
791+
"9", // shortcut for pdfjs_internal_id_pdfjs_internal_editor_9
792+
"673R",
793+
"613R",
794+
"680R",
795+
"661R",
796+
]);
797+
})
798+
);
799+
});
800+
});
740801
});

web/text_accessibility.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,14 @@ class TextAccessibilityManager {
222222
* @param {HTMLDivElement} element
223223
* @returns {string|null} The id in the struct tree if any.
224224
*/
225-
moveElementInDOM(container, element, contentElement, isRemovable) {
225+
moveElementInDOM(
226+
container,
227+
element,
228+
contentElement,
229+
isRemovable,
230+
filter,
231+
inserter
232+
) {
226233
const id = this.addPointerInTextLayer(contentElement, isRemovable);
227234

228235
if (!container.hasChildNodes()) {
@@ -231,25 +238,23 @@ class TextAccessibilityManager {
231238
}
232239

233240
const children = Array.from(container.childNodes).filter(
234-
node => node !== element
241+
node => node !== element && (!filter || filter(node))
235242
);
236243

237244
if (children.length === 0) {
238245
return id;
239246
}
240247

241-
const elementToCompare = contentElement || element;
242248
const index = binarySearchFirstItem(
243249
children,
244250
node =>
245-
TextAccessibilityManager.#compareElementPositions(
246-
elementToCompare,
247-
node
248-
) < 0
251+
TextAccessibilityManager.#compareElementPositions(element, node) < 0
249252
);
250253

251254
if (index === 0) {
252255
children[0].before(element);
256+
} else if (inserter) {
257+
inserter(children[index - 1], element);
253258
} else {
254259
children[index - 1].after(element);
255260
}

0 commit comments

Comments
 (0)