Merge pull request #15237 from calixteman/annotation_a11y
[Annotations] Add some aria-owns in the text layer to link to annotations (bug 1780375)
This commit is contained in:
commit
6b4c2464ad
@ -29,6 +29,7 @@ import {
|
|||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import {
|
import {
|
||||||
|
AnnotationPrefix,
|
||||||
DOMSVGFactory,
|
DOMSVGFactory,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
PDFDateString,
|
PDFDateString,
|
||||||
@ -1901,7 +1902,8 @@ class PopupElement {
|
|||||||
}
|
}
|
||||||
if (this.hideElement.hidden) {
|
if (this.hideElement.hidden) {
|
||||||
this.hideElement.hidden = false;
|
this.hideElement.hidden = false;
|
||||||
this.container.style.zIndex += 1;
|
this.container.style.zIndex =
|
||||||
|
parseInt(this.container.style.zIndex) + 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1918,7 +1920,8 @@ class PopupElement {
|
|||||||
}
|
}
|
||||||
if (!this.hideElement.hidden && !this.pinned) {
|
if (!this.hideElement.hidden && !this.pinned) {
|
||||||
this.hideElement.hidden = true;
|
this.hideElement.hidden = true;
|
||||||
this.container.style.zIndex -= 1;
|
this.container.style.zIndex =
|
||||||
|
parseInt(this.container.style.zIndex) - 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2465,6 +2468,19 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class AnnotationLayer {
|
class AnnotationLayer {
|
||||||
|
static #appendElement(element, id, div, accessibilityManager) {
|
||||||
|
const contentElement = element.firstChild || element;
|
||||||
|
contentElement.id = `${AnnotationPrefix}${id}`;
|
||||||
|
|
||||||
|
div.append(element);
|
||||||
|
accessibilityManager?.moveElementInDOM(
|
||||||
|
div,
|
||||||
|
element,
|
||||||
|
contentElement,
|
||||||
|
/* isRemovable = */ false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a new annotation layer with all annotation elements.
|
* Render a new annotation layer with all annotation elements.
|
||||||
*
|
*
|
||||||
@ -2473,9 +2489,10 @@ class AnnotationLayer {
|
|||||||
* @memberof AnnotationLayer
|
* @memberof AnnotationLayer
|
||||||
*/
|
*/
|
||||||
static render(parameters) {
|
static render(parameters) {
|
||||||
const { annotations, div, viewport } = parameters;
|
const { annotations, div, viewport, accessibilityManager } = parameters;
|
||||||
|
|
||||||
this.#setDimensions(div, viewport);
|
this.#setDimensions(div, viewport);
|
||||||
|
let zIndex = 0;
|
||||||
|
|
||||||
for (const data of annotations) {
|
for (const data of annotations) {
|
||||||
if (data.annotationType !== AnnotationType.POPUP) {
|
if (data.annotationType !== AnnotationType.POPUP) {
|
||||||
@ -2508,15 +2525,33 @@ class AnnotationLayer {
|
|||||||
}
|
}
|
||||||
if (Array.isArray(rendered)) {
|
if (Array.isArray(rendered)) {
|
||||||
for (const renderedElement of rendered) {
|
for (const renderedElement of rendered) {
|
||||||
div.append(renderedElement);
|
renderedElement.style.zIndex = zIndex++;
|
||||||
|
AnnotationLayer.#appendElement(
|
||||||
|
renderedElement,
|
||||||
|
data.id,
|
||||||
|
div,
|
||||||
|
accessibilityManager
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// The accessibility manager will move the annotation in the DOM in
|
||||||
|
// order to match the visual ordering.
|
||||||
|
// But if an annotation is above an other one, then we must draw it
|
||||||
|
// after the other one whatever the order is in the DOM, hence the
|
||||||
|
// use of the z-index.
|
||||||
|
rendered.style.zIndex = zIndex++;
|
||||||
|
|
||||||
if (element instanceof PopupAnnotationElement) {
|
if (element instanceof PopupAnnotationElement) {
|
||||||
// Popup annotation elements should not be on top of other
|
// Popup annotation elements should not be on top of other
|
||||||
// annotation elements to prevent interfering with mouse events.
|
// annotation elements to prevent interfering with mouse events.
|
||||||
div.prepend(rendered);
|
div.prepend(rendered);
|
||||||
} else {
|
} else {
|
||||||
div.append(rendered);
|
AnnotationLayer.#appendElement(
|
||||||
|
rendered,
|
||||||
|
data.id,
|
||||||
|
div,
|
||||||
|
accessibilityManager
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ import { BaseException, stringToBytes, Util, warn } from "../shared/util.js";
|
|||||||
|
|
||||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
const AnnotationPrefix = "pdfjs_internal_id_";
|
||||||
|
|
||||||
class PixelsPerInch {
|
class PixelsPerInch {
|
||||||
static CSS = 96.0;
|
static CSS = 96.0;
|
||||||
|
|
||||||
@ -652,6 +654,7 @@ function getCurrentTransformInverse(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AnnotationPrefix,
|
||||||
binarySearchFirstItem,
|
binarySearchFirstItem,
|
||||||
deprecated,
|
deprecated,
|
||||||
DOMCanvasFactory,
|
DOMCanvasFactory,
|
||||||
|
@ -18,11 +18,12 @@
|
|||||||
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
|
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
/** @typedef {import("../../web/interfaces").IL10n} IL10n */
|
/** @typedef {import("../../web/interfaces").IL10n} IL10n */
|
||||||
|
|
||||||
import { AnnotationEditorType, shadow } from "../../shared/util.js";
|
|
||||||
import { bindEvents, KeyboardManager } from "./tools.js";
|
import { bindEvents, KeyboardManager } from "./tools.js";
|
||||||
import { binarySearchFirstItem } from "../display_utils.js";
|
import { AnnotationEditorType } from "../../shared/util.js";
|
||||||
import { FreeTextEditor } from "./freetext.js";
|
import { FreeTextEditor } from "./freetext.js";
|
||||||
import { InkEditor } from "./ink.js";
|
import { InkEditor } from "./ink.js";
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ import { InkEditor } from "./ink.js";
|
|||||||
* @property {AnnotationEditorUIManager} uiManager
|
* @property {AnnotationEditorUIManager} uiManager
|
||||||
* @property {boolean} enabled
|
* @property {boolean} enabled
|
||||||
* @property {AnnotationStorage} annotationStorage
|
* @property {AnnotationStorage} annotationStorage
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
* @property {number} pageIndex
|
* @property {number} pageIndex
|
||||||
* @property {IL10n} l10n
|
* @property {IL10n} l10n
|
||||||
*/
|
*/
|
||||||
@ -41,6 +43,8 @@ import { InkEditor } from "./ink.js";
|
|||||||
* Manage all the different editors on a page.
|
* Manage all the different editors on a page.
|
||||||
*/
|
*/
|
||||||
class AnnotationEditorLayer {
|
class AnnotationEditorLayer {
|
||||||
|
#accessibilityManager;
|
||||||
|
|
||||||
#allowClick = false;
|
#allowClick = false;
|
||||||
|
|
||||||
#boundPointerup = this.pointerup.bind(this);
|
#boundPointerup = this.pointerup.bind(this);
|
||||||
@ -53,14 +57,8 @@ class AnnotationEditorLayer {
|
|||||||
|
|
||||||
#isCleaningUp = false;
|
#isCleaningUp = false;
|
||||||
|
|
||||||
#textLayerMap = new WeakMap();
|
|
||||||
|
|
||||||
#textNodes = new Map();
|
|
||||||
|
|
||||||
#uiManager;
|
#uiManager;
|
||||||
|
|
||||||
#waitingEditors = new Set();
|
|
||||||
|
|
||||||
static _initialized = false;
|
static _initialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,43 +76,11 @@ class AnnotationEditorLayer {
|
|||||||
this.annotationStorage = options.annotationStorage;
|
this.annotationStorage = options.annotationStorage;
|
||||||
this.pageIndex = options.pageIndex;
|
this.pageIndex = options.pageIndex;
|
||||||
this.div = options.div;
|
this.div = options.div;
|
||||||
|
this.#accessibilityManager = options.accessibilityManager;
|
||||||
|
|
||||||
this.#uiManager.addLayer(this);
|
this.#uiManager.addLayer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get textLayerElements() {
|
|
||||||
// When zooming the text layer is removed from the DOM and sometimes
|
|
||||||
// it's rebuilt hence the nodes are no longer valid.
|
|
||||||
|
|
||||||
const textLayer = this.div.parentNode
|
|
||||||
.getElementsByClassName("textLayer")
|
|
||||||
.item(0);
|
|
||||||
|
|
||||||
if (!textLayer) {
|
|
||||||
return shadow(this, "textLayerElements", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
let textChildren = this.#textLayerMap.get(textLayer);
|
|
||||||
if (textChildren) {
|
|
||||||
return textChildren;
|
|
||||||
}
|
|
||||||
|
|
||||||
textChildren = textLayer.querySelectorAll(`span[role="presentation"]`);
|
|
||||||
if (textChildren.length === 0) {
|
|
||||||
return shadow(this, "textLayerElements", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
textChildren = Array.from(textChildren);
|
|
||||||
textChildren.sort(AnnotationEditorLayer.#compareElementPositions);
|
|
||||||
this.#textLayerMap.set(textLayer, textChildren);
|
|
||||||
|
|
||||||
return textChildren;
|
|
||||||
}
|
|
||||||
|
|
||||||
get #hasTextLayer() {
|
|
||||||
return !!this.div.parentNode.querySelector(".textLayer .endOfContent");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the toolbar if it's required to reflect the tool currently used.
|
* Update the toolbar if it's required to reflect the tool currently used.
|
||||||
* @param {number} mode
|
* @param {number} mode
|
||||||
@ -228,7 +194,7 @@ class AnnotationEditorLayer {
|
|||||||
|
|
||||||
detach(editor) {
|
detach(editor) {
|
||||||
this.#editors.delete(editor.id);
|
this.#editors.delete(editor.id);
|
||||||
this.removePointerInTextLayer(editor);
|
this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -281,147 +247,6 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare the positions of two elements, it must correspond to
|
|
||||||
* the visual ordering.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} e1
|
|
||||||
* @param {HTMLElement} e2
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
static #compareElementPositions(e1, e2) {
|
|
||||||
const rect1 = e1.getBoundingClientRect();
|
|
||||||
const rect2 = e2.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (rect1.y + rect1.height <= rect2.y) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect2.y + rect2.height <= rect1.y) {
|
|
||||||
return +1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const centerX1 = rect1.x + rect1.width / 2;
|
|
||||||
const centerX2 = rect2.x + rect2.width / 2;
|
|
||||||
|
|
||||||
return centerX1 - centerX2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function called when the text layer has finished rendering.
|
|
||||||
*/
|
|
||||||
onTextLayerRendered() {
|
|
||||||
this.#textNodes.clear();
|
|
||||||
for (const editor of this.#waitingEditors) {
|
|
||||||
if (editor.isAttachedToDOM) {
|
|
||||||
this.addPointerInTextLayer(editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.#waitingEditors.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an aria-owns id from a node in the text layer.
|
|
||||||
* @param {AnnotationEditor} editor
|
|
||||||
*/
|
|
||||||
removePointerInTextLayer(editor) {
|
|
||||||
if (!this.#hasTextLayer) {
|
|
||||||
this.#waitingEditors.delete(editor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = editor;
|
|
||||||
const node = this.#textNodes.get(id);
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#textNodes.delete(id);
|
|
||||||
let owns = node.getAttribute("aria-owns");
|
|
||||||
if (owns?.includes(id)) {
|
|
||||||
owns = owns
|
|
||||||
.split(" ")
|
|
||||||
.filter(x => x !== id)
|
|
||||||
.join(" ");
|
|
||||||
if (owns) {
|
|
||||||
node.setAttribute("aria-owns", owns);
|
|
||||||
} else {
|
|
||||||
node.removeAttribute("aria-owns");
|
|
||||||
node.setAttribute("role", "presentation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the text node which is the nearest and add an aria-owns attribute
|
|
||||||
* in order to correctly position this editor in the text flow.
|
|
||||||
* @param {AnnotationEditor} editor
|
|
||||||
*/
|
|
||||||
addPointerInTextLayer(editor) {
|
|
||||||
if (!this.#hasTextLayer) {
|
|
||||||
// The text layer needs to be there, so we postpone the association.
|
|
||||||
this.#waitingEditors.add(editor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removePointerInTextLayer(editor);
|
|
||||||
|
|
||||||
const children = this.textLayerElements;
|
|
||||||
if (!children) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { contentDiv } = editor;
|
|
||||||
const id = editor.getIdForTextLayer();
|
|
||||||
|
|
||||||
const index = binarySearchFirstItem(
|
|
||||||
children,
|
|
||||||
node =>
|
|
||||||
AnnotationEditorLayer.#compareElementPositions(contentDiv, node) < 0
|
|
||||||
);
|
|
||||||
const node = children[Math.max(0, index - 1)];
|
|
||||||
const owns = node.getAttribute("aria-owns");
|
|
||||||
if (!owns?.includes(id)) {
|
|
||||||
node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id);
|
|
||||||
}
|
|
||||||
node.removeAttribute("role");
|
|
||||||
|
|
||||||
this.#textNodes.set(id, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a div in the DOM in order to respect the visual order.
|
|
||||||
* @param {HTMLDivElement} div
|
|
||||||
*/
|
|
||||||
moveDivInDOM(editor) {
|
|
||||||
this.addPointerInTextLayer(editor);
|
|
||||||
|
|
||||||
const { div, contentDiv } = editor;
|
|
||||||
if (!this.div.hasChildNodes()) {
|
|
||||||
this.div.append(div);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = Array.from(this.div.childNodes).filter(
|
|
||||||
node => node !== div
|
|
||||||
);
|
|
||||||
|
|
||||||
if (children.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = binarySearchFirstItem(
|
|
||||||
children,
|
|
||||||
node =>
|
|
||||||
AnnotationEditorLayer.#compareElementPositions(contentDiv, node) < 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
children[0].before(div);
|
|
||||||
} else {
|
|
||||||
children[index - 1].after(div);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new editor in the current view.
|
* Add a new editor in the current view.
|
||||||
* @param {AnnotationEditor} editor
|
* @param {AnnotationEditor} editor
|
||||||
@ -437,11 +262,20 @@ class AnnotationEditorLayer {
|
|||||||
editor.isAttachedToDOM = true;
|
editor.isAttachedToDOM = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.moveDivInDOM(editor);
|
this.moveEditorInDOM(editor);
|
||||||
editor.onceAdded();
|
editor.onceAdded();
|
||||||
this.addToAnnotationStorage(editor);
|
this.addToAnnotationStorage(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveEditorInDOM(editor) {
|
||||||
|
this.#accessibilityManager?.moveElementInDOM(
|
||||||
|
this.div,
|
||||||
|
editor.div,
|
||||||
|
editor.contentDiv,
|
||||||
|
/* isRemovable = */ true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an editor in the annotation storage.
|
* Add an editor in the annotation storage.
|
||||||
* @param {AnnotationEditor} editor
|
* @param {AnnotationEditor} editor
|
||||||
@ -658,7 +492,7 @@ class AnnotationEditorLayer {
|
|||||||
const endY = event.clientY - rect.y;
|
const endY = event.clientY - rect.y;
|
||||||
|
|
||||||
editor.translate(endX - editor.startX, endY - editor.startY);
|
editor.translate(endX - editor.startX, endY - editor.startY);
|
||||||
this.moveDivInDOM(editor);
|
this.moveEditorInDOM(editor);
|
||||||
editor.div.focus();
|
editor.div.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,15 +513,13 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const editor of this.#editors.values()) {
|
for (const editor of this.#editors.values()) {
|
||||||
this.removePointerInTextLayer(editor);
|
this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv);
|
||||||
editor.isAttachedToDOM = false;
|
editor.isAttachedToDOM = false;
|
||||||
editor.div.remove();
|
editor.div.remove();
|
||||||
editor.parent = null;
|
editor.parent = null;
|
||||||
}
|
}
|
||||||
this.#textNodes.clear();
|
|
||||||
this.div = null;
|
this.div = null;
|
||||||
this.#editors.clear();
|
this.#editors.clear();
|
||||||
this.#waitingEditors.clear();
|
|
||||||
this.#uiManager.removeLayer(this);
|
this.#uiManager.removeLayer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,14 +489,6 @@ class AnnotationEditor {
|
|||||||
*/
|
*/
|
||||||
enableEditing() {}
|
enableEditing() {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the id to use in aria-owns when a link is done in the text layer.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getIdForTextLayer() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get some properties to update in the UI.
|
* Get some properties to update in the UI.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
|
@ -385,11 +385,6 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
this.editorDiv.setAttribute("aria-multiline", true);
|
this.editorDiv.setAttribute("aria-multiline", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
getIdForTextLayer() {
|
|
||||||
return this.editorDiv.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
render() {
|
render() {
|
||||||
if (this.div) {
|
if (this.div) {
|
||||||
|
@ -488,7 +488,7 @@ class InkEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
// When commiting, the position of this editor is changed, hence we must
|
// When commiting, the position of this editor is changed, hence we must
|
||||||
// move it to the right position in the DOM.
|
// move it to the right position in the DOM.
|
||||||
this.parent.moveDivInDOM(this);
|
this.parent.moveEditorInDOM(this);
|
||||||
// After the div has been moved in the DOM, the focus may have been stolen
|
// After the div has been moved in the DOM, the focus may have been stolen
|
||||||
// by document.body, hence we just keep it here.
|
// by document.body, hence we just keep it here.
|
||||||
this.div.focus();
|
this.div.focus();
|
||||||
|
@ -428,8 +428,6 @@ class AnnotationEditorUIManager {
|
|||||||
|
|
||||||
#boundOnPageChanging = this.onPageChanging.bind(this);
|
#boundOnPageChanging = this.onPageChanging.bind(this);
|
||||||
|
|
||||||
#boundOnTextLayerRendered = this.onTextLayerRendered.bind(this);
|
|
||||||
|
|
||||||
#previousStates = {
|
#previousStates = {
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
isEmpty: true,
|
isEmpty: true,
|
||||||
@ -474,14 +472,12 @@ class AnnotationEditorUIManager {
|
|||||||
this.#eventBus = eventBus;
|
this.#eventBus = eventBus;
|
||||||
this.#eventBus._on("editingaction", this.#boundOnEditingAction);
|
this.#eventBus._on("editingaction", this.#boundOnEditingAction);
|
||||||
this.#eventBus._on("pagechanging", this.#boundOnPageChanging);
|
this.#eventBus._on("pagechanging", this.#boundOnPageChanging);
|
||||||
this.#eventBus._on("textlayerrendered", this.#boundOnTextLayerRendered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.#removeKeyboardManager();
|
this.#removeKeyboardManager();
|
||||||
this.#eventBus._off("editingaction", this.#boundOnEditingAction);
|
this.#eventBus._off("editingaction", this.#boundOnEditingAction);
|
||||||
this.#eventBus._off("pagechanging", this.#boundOnPageChanging);
|
this.#eventBus._off("pagechanging", this.#boundOnPageChanging);
|
||||||
this.#eventBus._off("textlayerrendered", this.#boundOnTextLayerRendered);
|
|
||||||
for (const layer of this.#allLayers.values()) {
|
for (const layer of this.#allLayers.values()) {
|
||||||
layer.destroy();
|
layer.destroy();
|
||||||
}
|
}
|
||||||
@ -497,12 +493,6 @@ class AnnotationEditorUIManager {
|
|||||||
this.#currentPageIndex = pageNumber - 1;
|
this.#currentPageIndex = pageNumber - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextLayerRendered({ pageNumber }) {
|
|
||||||
const pageIndex = pageNumber - 1;
|
|
||||||
const layer = this.#allLayers.get(pageIndex);
|
|
||||||
layer?.onTextLayerRendered();
|
|
||||||
}
|
|
||||||
|
|
||||||
focusMainContainer() {
|
focusMainContainer() {
|
||||||
this.#container.focus();
|
this.#container.focus();
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ async function runTests(results) {
|
|||||||
"find_spec.js",
|
"find_spec.js",
|
||||||
"freetext_editor_spec.js",
|
"freetext_editor_spec.js",
|
||||||
"ink_editor_spec.js",
|
"ink_editor_spec.js",
|
||||||
|
"a11y_spec.js",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,4 +66,44 @@ describe("accessibility", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Annotation", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"tracemonkey_a11y.pdf",
|
||||||
|
".textLayer .endOfContent"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSpans(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const elements = document.querySelectorAll(
|
||||||
|
`.textLayer span[aria-owns]:not([role="presentation"])`
|
||||||
|
);
|
||||||
|
const results = [];
|
||||||
|
for (const element of elements) {
|
||||||
|
results.push(element.innerText);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("must check that some spans are linked to some annotations thanks to aria-owns", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const spanContents = await getSpans(page);
|
||||||
|
|
||||||
|
expect(spanContents)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(["Languages", "@intel.com", "Abstract", "Introduction"]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
closePages,
|
closePages,
|
||||||
editorPrefix,
|
getEditorSelector,
|
||||||
getSelectedEditors,
|
getSelectedEditors,
|
||||||
loadAndWait,
|
loadAndWait,
|
||||||
} = require("./test_utils.js");
|
} = require("./test_utils.js");
|
||||||
@ -51,9 +51,9 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
const data = "Hello PDF.js World !!";
|
const data = "Hello PDF.js World !!";
|
||||||
await page.mouse.click(rect.x + 100, rect.y + 100);
|
await page.mouse.click(rect.x + 100, rect.y + 100);
|
||||||
await page.type(`${editorPrefix}0 .internal`, data);
|
await page.type(`${getEditorSelector(0)} .internal`, data);
|
||||||
|
|
||||||
const editorRect = await page.$eval(`${editorPrefix}0`, el => {
|
const editorRect = await page.$eval(getEditorSelector(0), el => {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
@ -73,7 +73,7 @@ describe("Editor", () => {
|
|||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toEqual(1);
|
.toEqual(1);
|
||||||
|
|
||||||
const content = await page.$eval(`${editorPrefix}0`, el =>
|
const content = await page.$eval(getEditorSelector(0), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd()
|
||||||
);
|
);
|
||||||
expect(content).withContext(`In ${browserName}`).toEqual(data);
|
expect(content).withContext(`In ${browserName}`).toEqual(data);
|
||||||
@ -84,7 +84,7 @@ describe("Editor", () => {
|
|||||||
it("must copy/paste", async () => {
|
it("must copy/paste", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
const editorRect = await page.$eval(`${editorPrefix}0`, el => {
|
const editorRect = await page.$eval(getEditorSelector(0), el => {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
return { x, y, width, height };
|
return { x, y, width, height };
|
||||||
});
|
});
|
||||||
@ -107,11 +107,11 @@ describe("Editor", () => {
|
|||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toEqual(2);
|
.toEqual(2);
|
||||||
|
|
||||||
const content = await page.$eval(`${editorPrefix}0`, el =>
|
const content = await page.$eval(getEditorSelector(0), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd()
|
||||||
);
|
);
|
||||||
|
|
||||||
let pastedContent = await page.$eval(`${editorPrefix}1`, el =>
|
let pastedContent = await page.$eval(getEditorSelector(1), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ describe("Editor", () => {
|
|||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toEqual(3);
|
.toEqual(3);
|
||||||
|
|
||||||
pastedContent = await page.$eval(`${editorPrefix}2`, el =>
|
pastedContent = await page.$eval(getEditorSelector(2), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd()
|
||||||
);
|
);
|
||||||
expect(pastedContent)
|
expect(pastedContent)
|
||||||
@ -155,7 +155,7 @@ describe("Editor", () => {
|
|||||||
for (const n of [0, 1, 2]) {
|
for (const n of [0, 1, 2]) {
|
||||||
const hasEditor = await page.evaluate(sel => {
|
const hasEditor = await page.evaluate(sel => {
|
||||||
return !!document.querySelector(sel);
|
return !!document.querySelector(sel);
|
||||||
}, `${editorPrefix}${n}`);
|
}, getEditorSelector(n));
|
||||||
|
|
||||||
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
|
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
|
||||||
}
|
}
|
||||||
@ -177,9 +177,9 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
const data = "Hello PDF.js World !!";
|
const data = "Hello PDF.js World !!";
|
||||||
await page.mouse.click(rect.x + 100, rect.y + 100);
|
await page.mouse.click(rect.x + 100, rect.y + 100);
|
||||||
await page.type(`${editorPrefix}3 .internal`, data);
|
await page.type(`${getEditorSelector(3)} .internal`, data);
|
||||||
|
|
||||||
const editorRect = await page.$eval(`${editorPrefix}3`, el => {
|
const editorRect = await page.$eval(getEditorSelector(3), el => {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
return { x, y, width, height };
|
return { x, y, width, height };
|
||||||
});
|
});
|
||||||
@ -205,7 +205,7 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
let hasEditor = await page.evaluate(sel => {
|
let hasEditor = await page.evaluate(sel => {
|
||||||
return !!document.querySelector(sel);
|
return !!document.querySelector(sel);
|
||||||
}, `${editorPrefix}4`);
|
}, getEditorSelector(4));
|
||||||
|
|
||||||
expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
|
expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
hasEditor = await page.evaluate(sel => {
|
hasEditor = await page.evaluate(sel => {
|
||||||
return !!document.querySelector(sel);
|
return !!document.querySelector(sel);
|
||||||
}, `${editorPrefix}4`);
|
}, getEditorSelector(4));
|
||||||
|
|
||||||
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
|
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
|
||||||
|
|
||||||
@ -227,7 +227,7 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
let length = await page.evaluate(sel => {
|
let length = await page.evaluate(sel => {
|
||||||
return document.querySelectorAll(sel).length;
|
return document.querySelectorAll(sel).length;
|
||||||
}, `${editorPrefix}5, ${editorPrefix}6`);
|
}, `${getEditorSelector(5)}, ${getEditorSelector(6)}`);
|
||||||
expect(length).withContext(`In ${browserName}`).toEqual(2);
|
expect(length).withContext(`In ${browserName}`).toEqual(2);
|
||||||
|
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
@ -238,7 +238,7 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
length = await page.evaluate(sel => {
|
length = await page.evaluate(sel => {
|
||||||
return document.querySelectorAll(sel).length;
|
return document.querySelectorAll(sel).length;
|
||||||
}, `${editorPrefix}5, ${editorPrefix}6`);
|
}, `${getEditorSelector(5)}, ${getEditorSelector(6)}`);
|
||||||
expect(length).withContext(`In ${browserName}`).toEqual(0);
|
expect(length).withContext(`In ${browserName}`).toEqual(0);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -273,7 +273,7 @@ describe("Editor", () => {
|
|||||||
stacksRect.x + stacksRect.width + 1,
|
stacksRect.x + stacksRect.width + 1,
|
||||||
stacksRect.y + stacksRect.height / 2
|
stacksRect.y + stacksRect.height / 2
|
||||||
);
|
);
|
||||||
await page.type(`${editorPrefix}7 .internal`, data);
|
await page.type(`${getEditorSelector(7)} .internal`, data);
|
||||||
|
|
||||||
// Commit.
|
// Commit.
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
@ -283,9 +283,9 @@ describe("Editor", () => {
|
|||||||
return span?.getAttribute("aria-owns") || null;
|
return span?.getAttribute("aria-owns") || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ariaOwns)
|
expect(ariaOwns.endsWith("_7-editor"))
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toEqual(`${editorPrefix}7-editor`.slice(1));
|
.toEqual(true);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -308,9 +308,9 @@ describe("Editor", () => {
|
|||||||
|
|
||||||
const data = "Hello PDF.js World !!";
|
const data = "Hello PDF.js World !!";
|
||||||
await page.mouse.click(rect.x + 100, rect.y + 100);
|
await page.mouse.click(rect.x + 100, rect.y + 100);
|
||||||
await page.type(`${editorPrefix}8 .internal`, data);
|
await page.type(`${getEditorSelector(8)} .internal`, data);
|
||||||
|
|
||||||
const editorRect = await page.$eval(`${editorPrefix}8`, el => {
|
const editorRect = await page.$eval(getEditorSelector(8), el => {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
return { x, y, width, height };
|
return { x, y, width, height };
|
||||||
});
|
});
|
||||||
@ -385,9 +385,9 @@ describe("Editor", () => {
|
|||||||
rect.x + (i + 1) * 100,
|
rect.x + (i + 1) * 100,
|
||||||
rect.y + (i + 1) * 100
|
rect.y + (i + 1) * 100
|
||||||
);
|
);
|
||||||
await page.type(`${editorPrefix}${i} .internal`, data);
|
await page.type(`${getEditorSelector(i)} .internal`, data);
|
||||||
|
|
||||||
const editorRect = await page.$eval(`${editorPrefix}${i}`, el => {
|
const editorRect = await page.$eval(getEditorSelector(i), el => {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
|
@ -73,19 +73,17 @@ function getComputedStyleSelector(id) {
|
|||||||
return `getComputedStyle(${getQuerySelector(id)})`;
|
return `getComputedStyle(${getQuerySelector(id)})`;
|
||||||
}
|
}
|
||||||
exports.getComputedStyleSelector = getComputedStyleSelector;
|
exports.getComputedStyleSelector = getComputedStyleSelector;
|
||||||
|
exports.getEditorSelector = n => `#pdfjs_internal_editor_${n}`;
|
||||||
const editorPrefix = "#pdfjs_internal_editor_";
|
|
||||||
exports.editorPrefix = editorPrefix;
|
|
||||||
|
|
||||||
function getSelectedEditors(page) {
|
function getSelectedEditors(page) {
|
||||||
return page.evaluate(prefix => {
|
return page.evaluate(() => {
|
||||||
const elements = document.querySelectorAll(".selectedEditor");
|
const elements = document.querySelectorAll(".selectedEditor");
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const { id } of elements) {
|
for (const { id } of elements) {
|
||||||
results.push(parseInt(id.slice(prefix.length)));
|
results.push(parseInt(id.split("_").at(-1)));
|
||||||
}
|
}
|
||||||
results.sort();
|
results.sort();
|
||||||
return results;
|
return results;
|
||||||
}, editorPrefix.slice(1));
|
});
|
||||||
}
|
}
|
||||||
exports.getSelectedEditors = getSelectedEditors;
|
exports.getSelectedEditors = getSelectedEditors;
|
||||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -534,3 +534,4 @@
|
|||||||
!bug1675139.pdf
|
!bug1675139.pdf
|
||||||
!issue15092.pdf
|
!issue15092.pdf
|
||||||
!bug1782186.pdf
|
!bug1782186.pdf
|
||||||
|
!tracemonkey_a11y.pdf
|
||||||
|
BIN
test/pdfs/tracemonkey_a11y.pdf
Normal file
BIN
test/pdfs/tracemonkey_a11y.pdf
Normal file
Binary file not shown.
@ -21,6 +21,8 @@
|
|||||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
|
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||||
|
|
||||||
import { AnnotationEditorLayer } from "pdfjs-lib";
|
import { AnnotationEditorLayer } from "pdfjs-lib";
|
||||||
@ -31,6 +33,7 @@ import { NullL10n } from "./l10n_utils.js";
|
|||||||
* @property {number} mode - Editor mode
|
* @property {number} mode - Editor mode
|
||||||
* @property {HTMLDivElement} pageDiv
|
* @property {HTMLDivElement} pageDiv
|
||||||
* @property {PDFPageProxy} pdfPage
|
* @property {PDFPageProxy} pdfPage
|
||||||
|
* @property {TextAccessibilityManager} accessibilityManager
|
||||||
* @property {AnnotationStorage} annotationStorage
|
* @property {AnnotationStorage} annotationStorage
|
||||||
* @property {IL10n} l10n - Localization service.
|
* @property {IL10n} l10n - Localization service.
|
||||||
* @property {AnnotationEditorUIManager} uiManager
|
* @property {AnnotationEditorUIManager} uiManager
|
||||||
@ -46,6 +49,7 @@ class AnnotationEditorLayerBuilder {
|
|||||||
this.pageDiv = options.pageDiv;
|
this.pageDiv = options.pageDiv;
|
||||||
this.pdfPage = options.pdfPage;
|
this.pdfPage = options.pdfPage;
|
||||||
this.annotationStorage = options.annotationStorage || null;
|
this.annotationStorage = options.annotationStorage || null;
|
||||||
|
this.accessibilityManager = options.accessibilityManager;
|
||||||
this.l10n = options.l10n || NullL10n;
|
this.l10n = options.l10n || NullL10n;
|
||||||
this.annotationEditorLayer = null;
|
this.annotationEditorLayer = null;
|
||||||
this.div = null;
|
this.div = null;
|
||||||
@ -83,6 +87,7 @@ class AnnotationEditorLayerBuilder {
|
|||||||
uiManager: this.#uiManager,
|
uiManager: this.#uiManager,
|
||||||
div: this.div,
|
div: this.div,
|
||||||
annotationStorage: this.annotationStorage,
|
annotationStorage: this.annotationStorage,
|
||||||
|
accessibilityManager: this.accessibilityManager,
|
||||||
pageIndex: this.pdfPage._pageIndex,
|
pageIndex: this.pdfPage._pageIndex,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
viewport: clonedViewport,
|
viewport: clonedViewport,
|
||||||
|
@ -210,7 +210,6 @@
|
|||||||
|
|
||||||
.annotationLayer .popup {
|
.annotationLayer .popup {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 200;
|
|
||||||
max-width: calc(180px * var(--scale-factor));
|
max-width: calc(180px * var(--scale-factor));
|
||||||
background-color: rgba(255, 255, 153, 1);
|
background-color: rgba(255, 255, 153, 1);
|
||||||
box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor))
|
box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor))
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
|
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
|
||||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./textaccessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
|
|
||||||
import { AnnotationLayer } from "pdfjs-lib";
|
import { AnnotationLayer } from "pdfjs-lib";
|
||||||
import { NullL10n } from "./l10n_utils.js";
|
import { NullL10n } from "./l10n_utils.js";
|
||||||
@ -40,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
|
|||||||
* [fieldObjectsPromise]
|
* [fieldObjectsPromise]
|
||||||
* @property {Object} [mouseState]
|
* @property {Object} [mouseState]
|
||||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
||||||
|
* @property {TextAccessibilityManager} accessibilityManager
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AnnotationLayerBuilder {
|
class AnnotationLayerBuilder {
|
||||||
@ -60,6 +63,7 @@ class AnnotationLayerBuilder {
|
|||||||
fieldObjectsPromise = null,
|
fieldObjectsPromise = null,
|
||||||
mouseState = null,
|
mouseState = null,
|
||||||
annotationCanvasMap = null,
|
annotationCanvasMap = null,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {
|
}) {
|
||||||
this.pageDiv = pageDiv;
|
this.pageDiv = pageDiv;
|
||||||
this.pdfPage = pdfPage;
|
this.pdfPage = pdfPage;
|
||||||
@ -74,6 +78,7 @@ class AnnotationLayerBuilder {
|
|||||||
this._fieldObjectsPromise = fieldObjectsPromise;
|
this._fieldObjectsPromise = fieldObjectsPromise;
|
||||||
this._mouseState = mouseState;
|
this._mouseState = mouseState;
|
||||||
this._annotationCanvasMap = annotationCanvasMap;
|
this._annotationCanvasMap = annotationCanvasMap;
|
||||||
|
this._accessibilityManager = accessibilityManager;
|
||||||
|
|
||||||
this.div = null;
|
this.div = null;
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
@ -112,6 +117,7 @@ class AnnotationLayerBuilder {
|
|||||||
fieldObjects,
|
fieldObjects,
|
||||||
mouseState: this._mouseState,
|
mouseState: this._mouseState,
|
||||||
annotationCanvasMap: this._annotationCanvasMap,
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
|
accessibilityManager: this._accessibilityManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.div) {
|
if (this.div) {
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
||||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnnotationEditorType,
|
AnnotationEditorType,
|
||||||
@ -1637,6 +1639,7 @@ class BaseViewer {
|
|||||||
* @property {boolean} [enhanceTextSelection]
|
* @property {boolean} [enhanceTextSelection]
|
||||||
* @property {EventBus} eventBus
|
* @property {EventBus} eventBus
|
||||||
* @property {TextHighlighter} highlighter
|
* @property {TextHighlighter} highlighter
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1650,6 +1653,7 @@ class BaseViewer {
|
|||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
eventBus,
|
eventBus,
|
||||||
highlighter,
|
highlighter,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {
|
}) {
|
||||||
return new TextLayerBuilder({
|
return new TextLayerBuilder({
|
||||||
textLayerDiv,
|
textLayerDiv,
|
||||||
@ -1660,6 +1664,7 @@ class BaseViewer {
|
|||||||
? false
|
? false
|
||||||
: enhanceTextSelection,
|
: enhanceTextSelection,
|
||||||
highlighter,
|
highlighter,
|
||||||
|
accessibilityManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1698,6 +1703,7 @@ class BaseViewer {
|
|||||||
* [fieldObjectsPromise]
|
* [fieldObjectsPromise]
|
||||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
||||||
* annotation ids with canvases used to render them.
|
* annotation ids with canvases used to render them.
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1716,6 +1722,7 @@ class BaseViewer {
|
|||||||
mouseState = this._scriptingManager?.mouseState,
|
mouseState = this._scriptingManager?.mouseState,
|
||||||
fieldObjectsPromise = this.pdfDocument?.getFieldObjects(),
|
fieldObjectsPromise = this.pdfDocument?.getFieldObjects(),
|
||||||
annotationCanvasMap = null,
|
annotationCanvasMap = null,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {
|
}) {
|
||||||
return new AnnotationLayerBuilder({
|
return new AnnotationLayerBuilder({
|
||||||
pageDiv,
|
pageDiv,
|
||||||
@ -1731,6 +1738,7 @@ class BaseViewer {
|
|||||||
mouseState,
|
mouseState,
|
||||||
fieldObjectsPromise,
|
fieldObjectsPromise,
|
||||||
annotationCanvasMap,
|
annotationCanvasMap,
|
||||||
|
accessibilityManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1741,6 +1749,7 @@ class BaseViewer {
|
|||||||
* @property {PDFPageProxy} pdfPage
|
* @property {PDFPageProxy} pdfPage
|
||||||
* @property {IL10n} l10n
|
* @property {IL10n} l10n
|
||||||
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
|
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
* data in forms.
|
* data in forms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -1752,6 +1761,7 @@ class BaseViewer {
|
|||||||
uiManager = this.#annotationEditorUIManager,
|
uiManager = this.#annotationEditorUIManager,
|
||||||
pageDiv,
|
pageDiv,
|
||||||
pdfPage,
|
pdfPage,
|
||||||
|
accessibilityManager = null,
|
||||||
l10n,
|
l10n,
|
||||||
annotationStorage = this.pdfDocument?.annotationStorage,
|
annotationStorage = this.pdfDocument?.annotationStorage,
|
||||||
}) {
|
}) {
|
||||||
@ -1760,6 +1770,7 @@ class BaseViewer {
|
|||||||
pageDiv,
|
pageDiv,
|
||||||
pdfPage,
|
pdfPage,
|
||||||
annotationStorage,
|
annotationStorage,
|
||||||
|
accessibilityManager,
|
||||||
l10n,
|
l10n,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,10 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#viewer.textLayer-visible .textLayer span[aria-owns] {
|
||||||
|
background-color: rgba(255, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
#viewer.textLayer-hover .textLayer span:hover {
|
#viewer.textLayer-hover .textLayer span:hover {
|
||||||
background-color: rgba(255, 255, 255, 1);
|
background-color: rgba(255, 255, 255, 1);
|
||||||
color: rgba(0, 0, 0, 1);
|
color: rgba(0, 0, 0, 1);
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
||||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
||||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
|
|
||||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
||||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||||
@ -60,6 +62,7 @@ class DefaultAnnotationLayerFactory {
|
|||||||
* [fieldObjectsPromise]
|
* [fieldObjectsPromise]
|
||||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
||||||
* annotation ids with canvases used to render them.
|
* annotation ids with canvases used to render them.
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,6 +81,7 @@ class DefaultAnnotationLayerFactory {
|
|||||||
mouseState = null,
|
mouseState = null,
|
||||||
fieldObjectsPromise = null,
|
fieldObjectsPromise = null,
|
||||||
annotationCanvasMap = null,
|
annotationCanvasMap = null,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {
|
}) {
|
||||||
return new AnnotationLayerBuilder({
|
return new AnnotationLayerBuilder({
|
||||||
pageDiv,
|
pageDiv,
|
||||||
@ -92,6 +96,7 @@ class DefaultAnnotationLayerFactory {
|
|||||||
fieldObjectsPromise,
|
fieldObjectsPromise,
|
||||||
mouseState,
|
mouseState,
|
||||||
annotationCanvasMap,
|
annotationCanvasMap,
|
||||||
|
accessibilityManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,6 +112,7 @@ class DefaultAnnotationEditorLayerFactory {
|
|||||||
* @property {PDFPageProxy} pdfPage
|
* @property {PDFPageProxy} pdfPage
|
||||||
* @property {IL10n} l10n
|
* @property {IL10n} l10n
|
||||||
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
|
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
* data in forms.
|
* data in forms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -118,6 +124,7 @@ class DefaultAnnotationEditorLayerFactory {
|
|||||||
uiManager = null,
|
uiManager = null,
|
||||||
pageDiv,
|
pageDiv,
|
||||||
pdfPage,
|
pdfPage,
|
||||||
|
accessibilityManager = null,
|
||||||
l10n,
|
l10n,
|
||||||
annotationStorage = null,
|
annotationStorage = null,
|
||||||
}) {
|
}) {
|
||||||
@ -125,6 +132,7 @@ class DefaultAnnotationEditorLayerFactory {
|
|||||||
uiManager,
|
uiManager,
|
||||||
pageDiv,
|
pageDiv,
|
||||||
pdfPage,
|
pdfPage,
|
||||||
|
accessibilityManager,
|
||||||
l10n,
|
l10n,
|
||||||
annotationStorage,
|
annotationStorage,
|
||||||
});
|
});
|
||||||
@ -163,6 +171,7 @@ class DefaultTextLayerFactory {
|
|||||||
* @property {boolean} [enhanceTextSelection]
|
* @property {boolean} [enhanceTextSelection]
|
||||||
* @property {EventBus} eventBus
|
* @property {EventBus} eventBus
|
||||||
* @property {TextHighlighter} highlighter
|
* @property {TextHighlighter} highlighter
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,6 +185,7 @@ class DefaultTextLayerFactory {
|
|||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
eventBus,
|
eventBus,
|
||||||
highlighter,
|
highlighter,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {
|
}) {
|
||||||
return new TextLayerBuilder({
|
return new TextLayerBuilder({
|
||||||
textLayerDiv,
|
textLayerDiv,
|
||||||
@ -184,6 +194,7 @@ class DefaultTextLayerFactory {
|
|||||||
enhanceTextSelection,
|
enhanceTextSelection,
|
||||||
eventBus,
|
eventBus,
|
||||||
highlighter,
|
highlighter,
|
||||||
|
accessibilityManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
/** @typedef {import("./text_layer_builder").TextLayerBuilder} TextLayerBuilder */
|
/** @typedef {import("./text_layer_builder").TextLayerBuilder} TextLayerBuilder */
|
||||||
/** @typedef {import("./ui_utils").RenderingStates} RenderingStates */
|
/** @typedef {import("./ui_utils").RenderingStates} RenderingStates */
|
||||||
/** @typedef {import("./xfa_layer_builder").XfaLayerBuilder} XfaLayerBuilder */
|
/** @typedef {import("./xfa_layer_builder").XfaLayerBuilder} XfaLayerBuilder */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @interface
|
* @interface
|
||||||
@ -162,6 +164,7 @@ class IPDFTextLayerFactory {
|
|||||||
* @property {boolean} [enhanceTextSelection]
|
* @property {boolean} [enhanceTextSelection]
|
||||||
* @property {EventBus} eventBus
|
* @property {EventBus} eventBus
|
||||||
* @property {TextHighlighter} highlighter
|
* @property {TextHighlighter} highlighter
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,6 +178,7 @@ class IPDFTextLayerFactory {
|
|||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
eventBus,
|
eventBus,
|
||||||
highlighter,
|
highlighter,
|
||||||
|
accessibilityManager,
|
||||||
}) {}
|
}) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +203,7 @@ class IPDFAnnotationLayerFactory {
|
|||||||
* [fieldObjectsPromise]
|
* [fieldObjectsPromise]
|
||||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
||||||
* annotation ids with canvases used to render them.
|
* annotation ids with canvases used to render them.
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -217,6 +222,7 @@ class IPDFAnnotationLayerFactory {
|
|||||||
mouseState = null,
|
mouseState = null,
|
||||||
fieldObjectsPromise = null,
|
fieldObjectsPromise = null,
|
||||||
annotationCanvasMap = null,
|
annotationCanvasMap = null,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {}
|
}) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,6 +237,7 @@ class IPDFAnnotationEditorLayerFactory {
|
|||||||
* @property {PDFPageProxy} pdfPage
|
* @property {PDFPageProxy} pdfPage
|
||||||
* @property {IL10n} l10n
|
* @property {IL10n} l10n
|
||||||
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
|
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
* data in forms.
|
* data in forms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -244,6 +251,7 @@ class IPDFAnnotationEditorLayerFactory {
|
|||||||
pdfPage,
|
pdfPage,
|
||||||
l10n,
|
l10n,
|
||||||
annotationStorage = null,
|
annotationStorage = null,
|
||||||
|
accessibilityManager,
|
||||||
}) {}
|
}) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
} from "./ui_utils.js";
|
} from "./ui_utils.js";
|
||||||
import { compatibilityParams } from "./app_options.js";
|
import { compatibilityParams } from "./app_options.js";
|
||||||
import { NullL10n } from "./l10n_utils.js";
|
import { NullL10n } from "./l10n_utils.js";
|
||||||
|
import { TextAccessibilityManager } from "./text_accessibility.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PDFPageViewOptions
|
* @typedef {Object} PDFPageViewOptions
|
||||||
@ -697,6 +698,7 @@ class PDFPageView {
|
|||||||
|
|
||||||
let textLayer = null;
|
let textLayer = null;
|
||||||
if (this.textLayerMode !== TextLayerMode.DISABLE && this.textLayerFactory) {
|
if (this.textLayerMode !== TextLayerMode.DISABLE && this.textLayerFactory) {
|
||||||
|
this._accessibilityManager ||= new TextAccessibilityManager();
|
||||||
const textLayerDiv = document.createElement("div");
|
const textLayerDiv = document.createElement("div");
|
||||||
textLayerDiv.className = "textLayer";
|
textLayerDiv.className = "textLayer";
|
||||||
textLayerDiv.style.width = canvasWrapper.style.width;
|
textLayerDiv.style.width = canvasWrapper.style.width;
|
||||||
@ -716,6 +718,7 @@ class PDFPageView {
|
|||||||
this.textLayerMode === TextLayerMode.ENABLE_ENHANCE,
|
this.textLayerMode === TextLayerMode.ENABLE_ENHANCE,
|
||||||
eventBus: this.eventBus,
|
eventBus: this.eventBus,
|
||||||
highlighter: this.textHighlighter,
|
highlighter: this.textHighlighter,
|
||||||
|
accessibilityManager: this._accessibilityManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.textLayer = textLayer;
|
this.textLayer = textLayer;
|
||||||
@ -733,6 +736,7 @@ class PDFPageView {
|
|||||||
renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS,
|
renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
annotationCanvasMap: this._annotationCanvasMap,
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
|
accessibilityManager: this._accessibilityManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -824,6 +828,7 @@ class PDFPageView {
|
|||||||
pageDiv: div,
|
pageDiv: div,
|
||||||
pdfPage,
|
pdfPage,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
|
accessibilityManager: this._accessibilityManager,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this._renderAnnotationEditorLayer();
|
this._renderAnnotationEditorLayer();
|
||||||
|
246
web/text_accessibility.js
Normal file
246
web/text_accessibility.js
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
/* Copyright 2022 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { binarySearchFirstItem } from "pdfjs-lib";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class aims to provide some methods:
|
||||||
|
* - to reorder elements in the DOM with respect to the visual order;
|
||||||
|
* - to create a link, using aria-owns, between spans in the textLayer and
|
||||||
|
* annotations in the annotationLayer. The goal is to help to know
|
||||||
|
* where the annotations are in the text flow.
|
||||||
|
*/
|
||||||
|
class TextAccessibilityManager {
|
||||||
|
#enabled = false;
|
||||||
|
|
||||||
|
#textChildren = null;
|
||||||
|
|
||||||
|
#textNodes = new Map();
|
||||||
|
|
||||||
|
#waitingElements = new Map();
|
||||||
|
|
||||||
|
setTextMapping(textDivs) {
|
||||||
|
this.#textChildren = textDivs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare the positions of two elements, it must correspond to
|
||||||
|
* the visual ordering.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} e1
|
||||||
|
* @param {HTMLElement} e2
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
static #compareElementPositions(e1, e2) {
|
||||||
|
const rect1 = e1.getBoundingClientRect();
|
||||||
|
const rect2 = e2.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect1.width === 0 && rect1.height === 0) {
|
||||||
|
return +1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rect2.width === 0 && rect2.height === 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const top1 = rect1.y;
|
||||||
|
const bot1 = rect1.y + rect1.height;
|
||||||
|
const mid1 = rect1.y + rect1.height / 2;
|
||||||
|
|
||||||
|
const top2 = rect2.y;
|
||||||
|
const bot2 = rect2.y + rect2.height;
|
||||||
|
const mid2 = rect2.y + rect2.height / 2;
|
||||||
|
|
||||||
|
if (mid1 <= top2 && mid2 >= bot1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mid2 <= top1 && mid1 >= bot2) {
|
||||||
|
return +1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerX1 = rect1.x + rect1.width / 2;
|
||||||
|
const centerX2 = rect2.x + rect2.width / 2;
|
||||||
|
|
||||||
|
return centerX1 - centerX2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when the text layer has finished rendering.
|
||||||
|
*/
|
||||||
|
enable() {
|
||||||
|
if (this.#enabled) {
|
||||||
|
throw new Error("TextAccessibilityManager is already enabled.");
|
||||||
|
}
|
||||||
|
if (!this.#textChildren) {
|
||||||
|
throw new Error("Text divs and strings have not been set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#enabled = true;
|
||||||
|
this.#textChildren = this.#textChildren.slice();
|
||||||
|
this.#textChildren.sort(TextAccessibilityManager.#compareElementPositions);
|
||||||
|
|
||||||
|
if (this.#textNodes.size > 0) {
|
||||||
|
// Some links have been made before this manager has been disabled, hence
|
||||||
|
// we restore them.
|
||||||
|
const textChildren = this.#textChildren;
|
||||||
|
for (const [id, nodeIndex] of this.#textNodes) {
|
||||||
|
this.#addIdToAriaOwns(id, textChildren[nodeIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [element, isRemovable] of this.#waitingElements) {
|
||||||
|
this.addPointerInTextLayer(element, isRemovable);
|
||||||
|
}
|
||||||
|
this.#waitingElements.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (!this.#enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clear this.#textNodes which is used to rebuild the aria-owns
|
||||||
|
// in case it's re-enabled at some point.
|
||||||
|
|
||||||
|
this.#waitingElements.clear();
|
||||||
|
this.#textChildren = null;
|
||||||
|
this.#enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an aria-owns id from a node in the text layer.
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
|
removePointerInTextLayer(element) {
|
||||||
|
if (!this.#enabled) {
|
||||||
|
this.#waitingElements.delete(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = this.#textChildren;
|
||||||
|
if (!children || children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = element;
|
||||||
|
const nodeIndex = this.#textNodes.get(id);
|
||||||
|
if (nodeIndex === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = children[nodeIndex];
|
||||||
|
|
||||||
|
this.#textNodes.delete(id);
|
||||||
|
let owns = node.getAttribute("aria-owns");
|
||||||
|
if (owns?.includes(id)) {
|
||||||
|
owns = owns
|
||||||
|
.split(" ")
|
||||||
|
.filter(x => x !== id)
|
||||||
|
.join(" ");
|
||||||
|
if (owns) {
|
||||||
|
node.setAttribute("aria-owns", owns);
|
||||||
|
} else {
|
||||||
|
node.removeAttribute("aria-owns");
|
||||||
|
node.setAttribute("role", "presentation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#addIdToAriaOwns(id, node) {
|
||||||
|
const owns = node.getAttribute("aria-owns");
|
||||||
|
if (!owns?.includes(id)) {
|
||||||
|
node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id);
|
||||||
|
}
|
||||||
|
node.removeAttribute("role");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the text node which is the nearest and add an aria-owns attribute
|
||||||
|
* in order to correctly position this editor in the text flow.
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @param {boolean} isRemovable
|
||||||
|
*/
|
||||||
|
addPointerInTextLayer(element, isRemovable) {
|
||||||
|
const { id } = element;
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#enabled) {
|
||||||
|
// The text layer needs to be there, so we postpone the association.
|
||||||
|
this.#waitingElements.set(element, isRemovable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRemovable) {
|
||||||
|
this.removePointerInTextLayer(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = this.#textChildren;
|
||||||
|
if (!children || children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = binarySearchFirstItem(
|
||||||
|
children,
|
||||||
|
node =>
|
||||||
|
TextAccessibilityManager.#compareElementPositions(element, node) < 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeIndex = Math.max(0, index - 1);
|
||||||
|
this.#addIdToAriaOwns(id, children[nodeIndex]);
|
||||||
|
this.#textNodes.set(id, nodeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a div in the DOM in order to respect the visual order.
|
||||||
|
* @param {HTMLDivElement} element
|
||||||
|
*/
|
||||||
|
moveElementInDOM(container, element, contentElement, isRemovable) {
|
||||||
|
this.addPointerInTextLayer(contentElement, isRemovable);
|
||||||
|
|
||||||
|
if (!container.hasChildNodes()) {
|
||||||
|
container.append(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = Array.from(container.childNodes).filter(
|
||||||
|
node => node !== element
|
||||||
|
);
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementToCompare = contentElement || element;
|
||||||
|
const index = binarySearchFirstItem(
|
||||||
|
children,
|
||||||
|
node =>
|
||||||
|
TextAccessibilityManager.#compareElementPositions(
|
||||||
|
elementToCompare,
|
||||||
|
node
|
||||||
|
) < 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
children[0].before(element);
|
||||||
|
} else {
|
||||||
|
children[index - 1].after(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TextAccessibilityManager };
|
@ -17,6 +17,8 @@
|
|||||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
|
|
||||||
import { renderTextLayer } from "pdfjs-lib";
|
import { renderTextLayer } from "pdfjs-lib";
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ const EXPAND_DIVS_TIMEOUT = 300; // ms
|
|||||||
* highlighting text from the find controller.
|
* highlighting text from the find controller.
|
||||||
* @property {boolean} enhanceTextSelection - Option to turn on improved
|
* @property {boolean} enhanceTextSelection - Option to turn on improved
|
||||||
* text selection.
|
* text selection.
|
||||||
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,6 +50,7 @@ class TextLayerBuilder {
|
|||||||
viewport,
|
viewport,
|
||||||
highlighter = null,
|
highlighter = null,
|
||||||
enhanceTextSelection = false,
|
enhanceTextSelection = false,
|
||||||
|
accessibilityManager = null,
|
||||||
}) {
|
}) {
|
||||||
this.textLayerDiv = textLayerDiv;
|
this.textLayerDiv = textLayerDiv;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
@ -60,6 +64,7 @@ class TextLayerBuilder {
|
|||||||
this.textLayerRenderTask = null;
|
this.textLayerRenderTask = null;
|
||||||
this.highlighter = highlighter;
|
this.highlighter = highlighter;
|
||||||
this.enhanceTextSelection = enhanceTextSelection;
|
this.enhanceTextSelection = enhanceTextSelection;
|
||||||
|
this.accessibilityManager = accessibilityManager;
|
||||||
|
|
||||||
this._bindMouse();
|
this._bindMouse();
|
||||||
}
|
}
|
||||||
@ -97,6 +102,7 @@ class TextLayerBuilder {
|
|||||||
|
|
||||||
this.textDivs.length = 0;
|
this.textDivs.length = 0;
|
||||||
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
|
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
|
||||||
|
this.accessibilityManager?.setTextMapping(this.textDivs);
|
||||||
|
|
||||||
const textLayerFrag = document.createDocumentFragment();
|
const textLayerFrag = document.createDocumentFragment();
|
||||||
this.textLayerRenderTask = renderTextLayer({
|
this.textLayerRenderTask = renderTextLayer({
|
||||||
@ -114,6 +120,7 @@ class TextLayerBuilder {
|
|||||||
this.textLayerDiv.append(textLayerFrag);
|
this.textLayerDiv.append(textLayerFrag);
|
||||||
this._finishRendering();
|
this._finishRendering();
|
||||||
this.highlighter?.enable();
|
this.highlighter?.enable();
|
||||||
|
this.accessibilityManager?.enable();
|
||||||
},
|
},
|
||||||
function (reason) {
|
function (reason) {
|
||||||
// Cancelled or failed to render text layer; skipping errors.
|
// Cancelled or failed to render text layer; skipping errors.
|
||||||
@ -130,6 +137,7 @@ class TextLayerBuilder {
|
|||||||
this.textLayerRenderTask = null;
|
this.textLayerRenderTask = null;
|
||||||
}
|
}
|
||||||
this.highlighter?.disable();
|
this.highlighter?.disable();
|
||||||
|
this.accessibilityManager?.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextContentStream(readableStream) {
|
setTextContentStream(readableStream) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user