[Editor] Make sure that annotation positions in the DOM respect the visual order (bug 1992770)
This commit is contained in:
parent
d83cbb28a9
commit
41dea1e38b
@ -186,7 +186,8 @@ class AnnotationElement {
|
|||||||
this.parent = parameters.parent;
|
this.parent = parameters.parent;
|
||||||
|
|
||||||
if (isRenderable) {
|
if (isRenderable) {
|
||||||
this.container = this._createContainer(ignoreBorder);
|
this.contentElement = this.container =
|
||||||
|
this._createContainer(ignoreBorder);
|
||||||
}
|
}
|
||||||
if (createQuadrilaterals) {
|
if (createQuadrilaterals) {
|
||||||
this._createQuadrilaterals();
|
this._createQuadrilaterals();
|
||||||
@ -1008,6 +1009,7 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
|
|
||||||
this.container.classList.add("linkAnnotation");
|
this.container.classList.add("linkAnnotation");
|
||||||
if (isBound) {
|
if (isBound) {
|
||||||
|
this.contentElement = link;
|
||||||
this.container.append(link);
|
this.container.append(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1517,6 +1519,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
|
|||||||
element.hidden = true;
|
element.hidden = true;
|
||||||
}
|
}
|
||||||
GetElementsByNameSet.add(element);
|
GetElementsByNameSet.add(element);
|
||||||
|
this.contentElement = element;
|
||||||
element.setAttribute("data-element-id", id);
|
element.setAttribute("data-element-id", id);
|
||||||
|
|
||||||
element.disabled = this.data.readOnly;
|
element.disabled = this.data.readOnly;
|
||||||
@ -3070,7 +3073,7 @@ class FreeTextAnnotationElement extends AnnotationElement {
|
|||||||
this.container.classList.add("freeTextAnnotation");
|
this.container.classList.add("freeTextAnnotation");
|
||||||
|
|
||||||
if (this.textContent) {
|
if (this.textContent) {
|
||||||
const content = document.createElement("div");
|
const content = (this.contentElement = document.createElement("div"));
|
||||||
content.classList.add("annotationTextContent");
|
content.classList.add("annotationTextContent");
|
||||||
content.setAttribute("role", "comment");
|
content.setAttribute("role", "comment");
|
||||||
for (const line of this.textContent) {
|
for (const line of this.textContent) {
|
||||||
@ -3787,7 +3790,7 @@ class AnnotationLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #appendElement(element, id, popupElements) {
|
async #appendElement(element, id, popupElements) {
|
||||||
const contentElement = element.firstChild || element;
|
const { contentElement, container } = element;
|
||||||
const annotationId = (contentElement.id = `${AnnotationPrefix}${id}`);
|
const annotationId = (contentElement.id = `${AnnotationPrefix}${id}`);
|
||||||
const ariaAttributes =
|
const ariaAttributes =
|
||||||
await this.#structTreeLayer?.getAriaAttributes(annotationId);
|
await this.#structTreeLayer?.getAriaAttributes(annotationId);
|
||||||
@ -3799,18 +3802,31 @@ class AnnotationLayer {
|
|||||||
|
|
||||||
if (popupElements) {
|
if (popupElements) {
|
||||||
// Set the popup just after the first element associated with the popup.
|
// Set the popup just after the first element associated with the popup.
|
||||||
popupElements.at(-1).container.after(element);
|
popupElements.at(-1).container.after(container);
|
||||||
} else {
|
} else {
|
||||||
this.div.append(element);
|
this.#moveElementInDOM(container, contentElement);
|
||||||
this.#accessibilityManager?.moveElementInDOM(
|
|
||||||
this.div,
|
|
||||||
element,
|
|
||||||
contentElement,
|
|
||||||
/* isRemovable = */ false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#moveElementInDOM(container, contentElement) {
|
||||||
|
this.div.append(container);
|
||||||
|
this.#accessibilityManager?.moveElementInDOM(
|
||||||
|
this.div,
|
||||||
|
container,
|
||||||
|
contentElement,
|
||||||
|
/* isRemovable = */ false,
|
||||||
|
/* filter = */ node => node.nodeName === "SECTION",
|
||||||
|
/* inserter = */ (prevNode, node) => {
|
||||||
|
if (prevNode.nextElementSibling.nodeName === "BUTTON") {
|
||||||
|
// In case we have a comment button, insert after the button.
|
||||||
|
prevNode.nextElementSibling.after(node);
|
||||||
|
} else {
|
||||||
|
prevNode.after(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a new annotation layer with all annotation elements.
|
* Render a new annotation layer with all annotation elements.
|
||||||
*
|
*
|
||||||
@ -3877,7 +3893,7 @@ class AnnotationLayer {
|
|||||||
if (data.hidden) {
|
if (data.hidden) {
|
||||||
rendered.style.visibility = "hidden";
|
rendered.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
await this.#appendElement(rendered, data.id, elementParams.elements);
|
await this.#appendElement(element, data.id, elementParams.elements);
|
||||||
element.extraPopupElement?.popup?.renderCommentButton();
|
element.extraPopupElement?.popup?.renderCommentButton();
|
||||||
|
|
||||||
if (element._isEditable) {
|
if (element._isEditable) {
|
||||||
@ -3913,8 +3929,8 @@ class AnnotationLayer {
|
|||||||
if (!element.isRenderable) {
|
if (!element.isRenderable) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const rendered = element.render();
|
element.render();
|
||||||
await this.#appendElement(rendered, data.id, null);
|
await this.#appendElement(element, data.id, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3999,18 +4015,32 @@ class AnnotationLayer {
|
|||||||
linkService: this.#linkService,
|
linkService: this.#linkService,
|
||||||
annotationStorage: this.#annotationStorage,
|
annotationStorage: this.#annotationStorage,
|
||||||
});
|
});
|
||||||
const htmlElement = element.render();
|
const rendered = element.render();
|
||||||
div.append(htmlElement);
|
rendered.id = `${AnnotationPrefix}${id}`;
|
||||||
this.#accessibilityManager?.moveElementInDOM(
|
this.#moveElementInDOM(rendered, rendered);
|
||||||
div,
|
|
||||||
htmlElement,
|
|
||||||
htmlElement,
|
|
||||||
/* isRemovable = */ false
|
|
||||||
);
|
|
||||||
element.createOrUpdatePopup();
|
element.createOrUpdatePopup();
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
togglePointerEvents(enabled = false) {
|
||||||
|
this.div.classList.toggle("disabled", !enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFakeAnnotations(editors) {
|
||||||
|
if (editors.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// In order to ensure that the annotations are correctly moved in the DOM
|
||||||
|
// we need to make sure that this has been laid out.
|
||||||
|
window.requestAnimationFrame(() =>
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const editor of editors) {
|
||||||
|
editor.updateFakeAnnotationElement(this);
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -242,7 +242,7 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleAnnotationLayerPointerEvents(enabled = false) {
|
toggleAnnotationLayerPointerEvents(enabled = false) {
|
||||||
this.#annotationLayer?.div.classList.toggle("disabled", !enabled);
|
this.#annotationLayer?.togglePointerEvents(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
get #allEditorsIterator() {
|
get #allEditorsIterator() {
|
||||||
@ -354,13 +354,14 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const annotationLayer = this.#annotationLayer;
|
const annotationLayer = this.#annotationLayer;
|
||||||
|
const needFakeAnnotation = [];
|
||||||
if (annotationLayer) {
|
if (annotationLayer) {
|
||||||
const changedAnnotations = new Map();
|
const changedAnnotations = new Map();
|
||||||
const resetAnnotations = new Map();
|
const resetAnnotations = new Map();
|
||||||
for (const editor of this.#allEditorsIterator) {
|
for (const editor of this.#allEditorsIterator) {
|
||||||
editor.disableEditing();
|
editor.disableEditing();
|
||||||
if (!editor.annotationElementId) {
|
if (!editor.annotationElementId) {
|
||||||
editor.updateFakeAnnotationElement(annotationLayer);
|
needFakeAnnotation.push(editor);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (editor.serialize() !== null) {
|
if (editor.serialize() !== null) {
|
||||||
@ -411,6 +412,7 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
this.disableTextSelection();
|
this.disableTextSelection();
|
||||||
this.toggleAnnotationLayerPointerEvents(true);
|
this.toggleAnnotationLayerPointerEvents(true);
|
||||||
|
annotationLayer?.updateFakeAnnotations(needFakeAnnotation);
|
||||||
|
|
||||||
this.#isDisabling = false;
|
this.#isDisabling = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,7 +95,7 @@ describe("Annotation highlight", () => {
|
|||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
for (const i of [23, 22, 14]) {
|
for (const i of [23, 22, 14]) {
|
||||||
await page.click(getAnnotationSelector(`${i}R`));
|
await page.click(getAnnotationSelector(`${i}R`));
|
||||||
await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus`);
|
await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus-within`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -878,4 +878,46 @@ a dynamic compiler for JavaScript based on our`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Annotation order in the DOM", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"comments.pdf",
|
||||||
|
".page[data-page-number='1'] .annotationLayer #pdfjs_internal_id_661R"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that annotations are in the visual order", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const sectionIds = await page.evaluate(() =>
|
||||||
|
[
|
||||||
|
...document.querySelectorAll(
|
||||||
|
".page[data-page-number='1'] .annotationLayer > section:not(.popupAnnotation)"
|
||||||
|
),
|
||||||
|
].map(el => el.id.split("_").pop())
|
||||||
|
);
|
||||||
|
expect(sectionIds)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual([
|
||||||
|
"612R",
|
||||||
|
"693R",
|
||||||
|
"687R",
|
||||||
|
"690R",
|
||||||
|
"713R",
|
||||||
|
"673R",
|
||||||
|
"613R",
|
||||||
|
"680R",
|
||||||
|
"661R",
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -737,4 +737,65 @@ describe("Comment", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Annotations order in reading mode", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"comments.pdf",
|
||||||
|
".annotationEditorLayer",
|
||||||
|
"page-fit",
|
||||||
|
null,
|
||||||
|
{ enableComment: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that the annotations are in the right order", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await switchToHighlight(page);
|
||||||
|
await highlightSpan(
|
||||||
|
page,
|
||||||
|
1,
|
||||||
|
"method provides cheap inter-procedural type specialization, and an"
|
||||||
|
);
|
||||||
|
await editComment(page, getEditorSelector(9), "Hello world!");
|
||||||
|
|
||||||
|
await highlightSpan(page, 1, "Andreas Gal");
|
||||||
|
await editComment(page, getEditorSelector(10), "Hello world!");
|
||||||
|
|
||||||
|
await switchToHighlight(page, /* disable = */ true);
|
||||||
|
await page.waitForSelector(
|
||||||
|
".annotationLayer section:nth-child(4).editorAnnotation"
|
||||||
|
);
|
||||||
|
|
||||||
|
const sectionIds = await page.evaluate(() =>
|
||||||
|
[
|
||||||
|
...document.querySelectorAll(
|
||||||
|
".page[data-page-number='1'] .annotationLayer > section:not(.popupAnnotation)"
|
||||||
|
),
|
||||||
|
].map(el => el.id.split("_").pop())
|
||||||
|
);
|
||||||
|
expect(sectionIds).withContext(`In ${browserName}`).toEqual([
|
||||||
|
"612R",
|
||||||
|
"693R",
|
||||||
|
"10", // shortcut for pdfjs_internal_id_pdfjs_internal_editor_10
|
||||||
|
"687R",
|
||||||
|
"690R",
|
||||||
|
"713R",
|
||||||
|
"9", // shortcut for pdfjs_internal_id_pdfjs_internal_editor_9
|
||||||
|
"673R",
|
||||||
|
"613R",
|
||||||
|
"680R",
|
||||||
|
"661R",
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -222,7 +222,14 @@ class TextAccessibilityManager {
|
|||||||
* @param {HTMLDivElement} element
|
* @param {HTMLDivElement} element
|
||||||
* @returns {string|null} The id in the struct tree if any.
|
* @returns {string|null} The id in the struct tree if any.
|
||||||
*/
|
*/
|
||||||
moveElementInDOM(container, element, contentElement, isRemovable) {
|
moveElementInDOM(
|
||||||
|
container,
|
||||||
|
element,
|
||||||
|
contentElement,
|
||||||
|
isRemovable,
|
||||||
|
filter,
|
||||||
|
inserter
|
||||||
|
) {
|
||||||
const id = this.addPointerInTextLayer(contentElement, isRemovable);
|
const id = this.addPointerInTextLayer(contentElement, isRemovable);
|
||||||
|
|
||||||
if (!container.hasChildNodes()) {
|
if (!container.hasChildNodes()) {
|
||||||
@ -231,25 +238,23 @@ class TextAccessibilityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const children = Array.from(container.childNodes).filter(
|
const children = Array.from(container.childNodes).filter(
|
||||||
node => node !== element
|
node => node !== element && (!filter || filter(node))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementToCompare = contentElement || element;
|
|
||||||
const index = binarySearchFirstItem(
|
const index = binarySearchFirstItem(
|
||||||
children,
|
children,
|
||||||
node =>
|
node =>
|
||||||
TextAccessibilityManager.#compareElementPositions(
|
TextAccessibilityManager.#compareElementPositions(element, node) < 0
|
||||||
elementToCompare,
|
|
||||||
node
|
|
||||||
) < 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
children[0].before(element);
|
children[0].before(element);
|
||||||
|
} else if (inserter) {
|
||||||
|
inserter(children[index - 1], element);
|
||||||
} else {
|
} else {
|
||||||
children[index - 1].after(element);
|
children[index - 1].after(element);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user