Merge pull request #15110 from calixteman/editing_a11y

[Editor] Improve a11y for newly added element (#15109)
This commit is contained in:
Jonas Jenwald 2022-07-19 20:02:53 +02:00 committed by GitHub
commit f46895d750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 467 additions and 96 deletions

View File

@ -265,3 +265,8 @@ editor_free_text_font_color=Font Color
editor_free_text_font_size=Font Size
editor_ink_line_color=Line Color
editor_ink_line_thickness=Line Thickness
# Editor aria
editor_free_text_aria_label=FreeText Editor
editor_ink_aria_label=Ink Editor
editor_ink_canvas_aria_label=User-created image

View File

@ -601,7 +601,40 @@ function getColorValues(colors) {
span.remove();
}
/**
* Use binary search to find the index of the first item in a given array which
* passes a given condition. The items are expected to be sorted in the sense
* that if the condition is true for one item in the array, then it is also true
* for all following items.
*
* @returns {number} Index of the first array element to pass the test,
* or |items.length| if no such element exists.
*/
function binarySearchFirstItem(items, condition, start = 0) {
let minIndex = start;
let maxIndex = items.length - 1;
if (maxIndex < 0 || !condition(items[maxIndex])) {
return items.length;
}
if (condition(items[minIndex])) {
return minIndex;
}
while (minIndex < maxIndex) {
const currentIndex = (minIndex + maxIndex) >> 1;
const currentItem = items[currentIndex];
if (condition(currentItem)) {
maxIndex = currentIndex;
} else {
minIndex = currentIndex + 1;
}
}
return minIndex; /* === maxIndex */
}
export {
binarySearchFirstItem,
deprecated,
DOMCanvasFactory,
DOMCMapReaderFactory,

View File

@ -20,8 +20,9 @@
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
/** @typedef {import("../../web/interfaces").IL10n} IL10n */
import { AnnotationEditorType, shadow } from "../../shared/util.js";
import { bindEvents, KeyboardManager } from "./tools.js";
import { AnnotationEditorType } from "../../shared/util.js";
import { binarySearchFirstItem } from "../display_utils.js";
import { FreeTextEditor } from "./freetext.js";
import { InkEditor } from "./ink.js";
@ -50,8 +51,14 @@ class AnnotationEditorLayer {
#isCleaningUp = false;
#textLayerMap = new WeakMap();
#textNodes = new Map();
#uiManager;
#waitingEditors = new Set();
static _initialized = false;
static _keyboardManager = new KeyboardManager([
@ -88,6 +95,7 @@ class AnnotationEditorLayer {
if (!AnnotationEditorLayer._initialized) {
AnnotationEditorLayer._initialized = true;
FreeTextEditor.initialize(options.l10n);
InkEditor.initialize(options.l10n);
options.uiManager.registerEditorTypes([FreeTextEditor, InkEditor]);
}
@ -98,11 +106,40 @@ class AnnotationEditorLayer {
this.#boundClick = this.click.bind(this);
this.#boundMousedown = this.mousedown.bind(this);
for (const editor of this.#uiManager.getEditors(options.pageIndex)) {
this.add(editor);
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);
}
this.#uiManager.addLayer(this);
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");
}
/**
@ -230,6 +267,9 @@ class AnnotationEditorLayer {
*/
enable() {
this.div.style.pointerEvents = "auto";
for (const editor of this.#editors.values()) {
editor.enableEditing();
}
}
/**
@ -237,6 +277,9 @@ class AnnotationEditorLayer {
*/
disable() {
this.div.style.pointerEvents = "none";
for (const editor of this.#editors.values()) {
editor.disableEditing();
}
}
/**
@ -276,6 +319,7 @@ class AnnotationEditorLayer {
detach(editor) {
this.#editors.delete(editor.id);
this.removePointerInTextLayer(editor);
}
/**
@ -311,12 +355,12 @@ class AnnotationEditorLayer {
}
if (this.#uiManager.isActive(editor)) {
editor.parent.setActiveEditor(null);
editor.parent?.setActiveEditor(null);
}
this.attach(editor);
editor.pageIndex = this.pageIndex;
editor.parent.detach(editor);
editor.parent?.detach(editor);
editor.parent = this;
if (editor.div && editor.isAttachedToDOM) {
editor.div.remove();
@ -324,6 +368,147 @@ 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.
* @param {AnnotationEditor} editor
@ -340,6 +525,7 @@ class AnnotationEditorLayer {
editor.isAttachedToDOM = true;
}
this.moveDivInDOM(editor);
editor.onceAdded();
}
@ -493,6 +679,8 @@ class AnnotationEditorLayer {
const endY = event.clientY - rect.y;
editor.translate(endX - editor.startX, endY - editor.startY);
this.moveDivInDOM(editor);
editor.div.focus();
}
/**
@ -517,13 +705,20 @@ class AnnotationEditorLayer {
* Destroy the main editor.
*/
destroy() {
if (this.#uiManager.getActive()?.parent === this) {
this.#uiManager.setActiveEditor(null);
}
for (const editor of this.#editors.values()) {
this.removePointerInTextLayer(editor);
editor.isAttachedToDOM = false;
editor.div.remove();
editor.parent = null;
this.div = null;
}
this.#textNodes.clear();
this.div = null;
this.#editors.clear();
this.#waitingEditors.clear();
this.#uiManager.removeLayer(this);
}
@ -548,6 +743,9 @@ class AnnotationEditorLayer {
this.viewport = parameters.viewport;
bindEvents(this, this.div, ["dragover", "drop", "keydown"]);
this.setDimensions();
for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
this.add(editor);
}
this.updateMode();
}

View File

@ -220,7 +220,7 @@ class AnnotationEditor {
this.div.setAttribute("data-editor-rotation", (360 - this.rotation) % 360);
this.div.className = this.name;
this.div.setAttribute("id", this.id);
this.div.tabIndex = 100;
this.div.tabIndex = 0;
const [tx, ty] = this.getInitialTranslation();
this.translate(tx, ty);
@ -454,6 +454,26 @@ class AnnotationEditor {
*/
updateParams(type, value) {}
/**
* When the user disables the editing mode some editors can change some of
* their properties.
*/
disableEditing() {}
/**
* When the user enables the editing mode some editors can change some of
* their properties.
*/
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.
* @returns {Object}
@ -461,6 +481,13 @@ class AnnotationEditor {
get propertiesToUpdate() {
return {};
}
/**
* Get the div which really contains the displayed content.
*/
get contentDiv() {
return this.div;
}
}
export { AnnotationEditor };

View File

@ -60,7 +60,13 @@ class FreeTextEditor extends AnnotationEditor {
}
static initialize(l10n) {
this._l10nPromise = l10n.get("free_text_default_content");
this._l10nPromise = new Map(
["free_text_default_content", "editor_free_text_aria_label"].map(str => [
str,
l10n.get(str),
])
);
const style = getComputedStyle(document.documentElement);
if (
@ -117,7 +123,6 @@ class FreeTextEditor extends AnnotationEditor {
];
}
/** @inheritdoc */
get propertiesToUpdate() {
return [
[AnnotationEditorParamsType.FREETEXT_SIZE, this.#fontSize],
@ -204,6 +209,7 @@ class FreeTextEditor extends AnnotationEditor {
this.overlayDiv.classList.remove("enabled");
this.editorDiv.contentEditable = true;
this.div.draggable = false;
this.div.removeAttribute("tabIndex");
}
/** @inheritdoc */
@ -213,6 +219,7 @@ class FreeTextEditor extends AnnotationEditor {
this.overlayDiv.classList.add("enabled");
this.editorDiv.contentEditable = false;
this.div.draggable = true;
this.div.tabIndex = 0;
}
/** @inheritdoc */
@ -300,6 +307,34 @@ class FreeTextEditor extends AnnotationEditor {
this.editorDiv.focus();
}
/**
* onkeydown callback.
* @param {MouseEvent} event
*/
keyup(event) {
if (event.key === "Enter") {
this.enableEditMode();
this.editorDiv.focus();
}
}
/** @inheritdoc */
disableEditing() {
this.editorDiv.setAttribute("role", "comment");
this.editorDiv.removeAttribute("aria-multiline");
}
/** @inheritdoc */
enableEditing() {
this.editorDiv.setAttribute("role", "textbox");
this.editorDiv.setAttribute("aria-multiline", true);
}
/** @inheritdoc */
getIdForTextLayer() {
return this.editorDiv.id;
}
/** @inheritdoc */
render() {
if (this.div) {
@ -314,12 +349,18 @@ class FreeTextEditor extends AnnotationEditor {
super.render();
this.editorDiv = document.createElement("div");
this.editorDiv.tabIndex = 0;
this.editorDiv.className = "internal";
FreeTextEditor._l10nPromise.then(msg =>
this.editorDiv.setAttribute("default-content", msg)
);
this.editorDiv.setAttribute("id", `${this.id}-editor`);
this.enableEditing();
FreeTextEditor._l10nPromise
.get("editor_free_text_aria_label")
.then(msg => this.editorDiv?.setAttribute("aria-label", msg));
FreeTextEditor._l10nPromise
.get("free_text_default_content")
.then(msg => this.editorDiv?.setAttribute("default-content", msg));
this.editorDiv.contentEditable = true;
const { style } = this.editorDiv;
@ -335,7 +376,7 @@ class FreeTextEditor extends AnnotationEditor {
// TODO: implement paste callback.
// The goal is to sanitize and have something suitable for this
// editor.
bindEvents(this, this.div, ["dblclick"]);
bindEvents(this, this.div, ["dblclick", "keyup"]);
if (this.width) {
// This editor was created in using copy (ctrl+c).
@ -354,6 +395,10 @@ class FreeTextEditor extends AnnotationEditor {
return this.div;
}
get contentDiv() {
return this.editorDiv;
}
/** @inheritdoc */
static deserialize(data, parent) {
const editor = super.deserialize(data, parent);

View File

@ -58,6 +58,8 @@ class InkEditor extends AnnotationEditor {
static _defaultThickness = 1;
static _l10nPromise;
constructor(params) {
super({ ...params, name: "inkEditor" });
this.color = params.color || null;
@ -76,6 +78,15 @@ class InkEditor extends AnnotationEditor {
this.#boundCanvasMousedown = this.canvasMousedown.bind(this);
}
static initialize(l10n) {
this._l10nPromise = new Map(
["editor_ink_canvas_aria_label", "editor_ink_aria_label"].map(str => [
str,
l10n.get(str),
])
);
}
static updateDefaultParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.INK_THICKNESS:
@ -390,6 +401,10 @@ class InkEditor extends AnnotationEditor {
this.#fitToContent();
this.parent.addInkEditorIfNeeded(/* isCommitting = */ true);
// When commiting, the position of this editor is changed, hence we must
// move it to the right position in the DOM.
this.parent.moveDivInDOM(this);
}
/** @inheritdoc */
@ -477,6 +492,10 @@ class InkEditor extends AnnotationEditor {
this.canvas = document.createElement("canvas");
this.canvas.width = this.canvas.height = 0;
this.canvas.className = "inkEditorCanvas";
InkEditor._l10nPromise
.get("editor_ink_canvas_aria_label")
.then(msg => this.canvas?.setAttribute("aria-label", msg));
this.div.append(this.canvas);
this.ctx = this.canvas.getContext("2d");
}
@ -507,6 +526,11 @@ class InkEditor extends AnnotationEditor {
}
super.render();
InkEditor._l10nPromise
.get("editor_ink_aria_label")
.then(msg => this.div?.setAttribute("aria-label", msg));
const [x, y, w, h] = this.#getInitialBBox();
this.setAt(x, y, 0, 0);
this.setDims(w, h);

View File

@ -426,6 +426,8 @@ class AnnotationEditorUIManager {
#boundOnPageChanging = this.onPageChanging.bind(this);
#boundOnTextLayerRendered = this.onTextLayerRendered.bind(this);
#previousStates = {
isEditing: false,
isEmpty: true,
@ -439,11 +441,13 @@ class AnnotationEditorUIManager {
this.#eventBus = eventBus;
this.#eventBus._on("editingaction", this.#boundOnEditingAction);
this.#eventBus._on("pagechanging", this.#boundOnPageChanging);
this.#eventBus._on("textlayerrendered", this.#boundOnTextLayerRendered);
}
destroy() {
this.#eventBus._off("editingaction", this.#boundOnEditingAction);
this.#eventBus._off("pagechanging", this.#boundOnPageChanging);
this.#eventBus._off("textlayerrendered", this.#boundOnTextLayerRendered);
for (const layer of this.#allLayers.values()) {
layer.destroy();
}
@ -458,6 +462,12 @@ class AnnotationEditorUIManager {
this.#currentPageIndex = pageNumber - 1;
}
onTextLayerRendered({ pageNumber }) {
const pageIndex = pageNumber - 1;
const layer = this.#allLayers.get(pageIndex);
layer?.onTextLayerRendered();
}
/**
* Execute an action for a given name.
* For example, the user can click on the "Undo" entry in the context menu

View File

@ -41,15 +41,7 @@ import {
VerbosityLevel,
} from "./shared/util.js";
import {
build,
getDocument,
LoopbackPort,
PDFDataRangeTransport,
PDFWorker,
setPDFNetworkStreamFactory,
version,
} from "./display/api.js";
import {
binarySearchFirstItem,
getFilenameFromUrl,
getPdfFilenameFromUrl,
getXfaPageViewport,
@ -60,6 +52,15 @@ import {
PixelsPerInch,
RenderingCancelledException,
} from "./display/display_utils.js";
import {
build,
getDocument,
LoopbackPort,
PDFDataRangeTransport,
PDFWorker,
setPDFNetworkStreamFactory,
version,
} from "./display/api.js";
import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js";
import { AnnotationEditorUIManager } from "./display/editor/tools.js";
import { AnnotationLayer } from "./display/annotation_layer.js";
@ -116,6 +117,7 @@ export {
AnnotationEditorUIManager,
AnnotationLayer,
AnnotationMode,
binarySearchFirstItem,
build,
CMapCompressionType,
createPromiseCapability,

View File

@ -197,5 +197,59 @@ describe("Editor", () => {
})
);
});
it("must check that aria-owns is correct", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [adobeComRect, oldAriaOwns] = await page.$eval(
".textLayer",
el => {
for (const span of el.querySelectorAll(
`span[role="presentation"]`
)) {
if (span.innerText.includes("adobe.com")) {
span.setAttribute("pdfjs", true);
const { x, y, width, height } = span.getBoundingClientRect();
return [
{ x, y, width, height },
span.getAttribute("aria-owns"),
];
}
}
return null;
}
);
expect(oldAriaOwns).withContext(`In ${browserName}`).toEqual(null);
const data = "Hello PDF.js World !!";
await page.mouse.click(
adobeComRect.x + adobeComRect.width + 10,
adobeComRect.y + adobeComRect.height / 2
);
await page.type(`${editorPrefix}5 .internal`, data);
const editorRect = await page.$eval(`${editorPrefix}5`, el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
// Commit.
await page.mouse.click(
editorRect.x,
editorRect.y + 2 * editorRect.height
);
const ariaOwns = await page.$eval(".textLayer", el => {
const span = el.querySelector(`span[pdfjs="true"]`);
return span?.getAttribute("aria-owns") || null;
});
expect(ariaOwns)
.withContext(`In ${browserName}`)
.toEqual(`${editorPrefix}5-editor`.slice(1));
})
);
});
});
});

View File

@ -14,6 +14,7 @@
*/
import {
binarySearchFirstItem,
DOMCanvasFactory,
DOMSVGFactory,
getFilenameFromUrl,
@ -25,6 +26,39 @@ import { bytesToString } from "../../src/shared/util.js";
import { isNodeJS } from "../../src/shared/is_node.js";
describe("display_utils", function () {
describe("binary search", function () {
function isTrue(boolean) {
return boolean;
}
function isGreater3(number) {
return number > 3;
}
it("empty array", function () {
expect(binarySearchFirstItem([], isTrue)).toEqual(0);
});
it("single boolean entry", function () {
expect(binarySearchFirstItem([false], isTrue)).toEqual(1);
expect(binarySearchFirstItem([true], isTrue)).toEqual(0);
});
it("three boolean entries", function () {
expect(binarySearchFirstItem([true, true, true], isTrue)).toEqual(0);
expect(binarySearchFirstItem([false, true, true], isTrue)).toEqual(1);
expect(binarySearchFirstItem([false, false, true], isTrue)).toEqual(2);
expect(binarySearchFirstItem([false, false, false], isTrue)).toEqual(3);
});
it("three numeric entries", function () {
expect(binarySearchFirstItem([0, 1, 2], isGreater3)).toEqual(3);
expect(binarySearchFirstItem([2, 3, 4], isGreater3)).toEqual(2);
expect(binarySearchFirstItem([4, 5, 6], isGreater3)).toEqual(0);
});
it("three numeric entries and a start index", function () {
expect(binarySearchFirstItem([0, 1, 2, 3, 4], isGreater3, 2)).toEqual(4);
expect(binarySearchFirstItem([2, 3, 4], isGreater3, 2)).toEqual(2);
expect(binarySearchFirstItem([4, 5, 6], isGreater3, 1)).toEqual(1);
});
});
describe("DOMCanvasFactory", function () {
let canvasFactory;

View File

@ -15,7 +15,6 @@
import {
backtrackBeforeAllVisibleElements,
binarySearchFirstItem,
getPageSizeInches,
getVisibleElements,
isPortraitOrientation,
@ -25,39 +24,6 @@ import {
} from "../../web/ui_utils.js";
describe("ui_utils", function () {
describe("binary search", function () {
function isTrue(boolean) {
return boolean;
}
function isGreater3(number) {
return number > 3;
}
it("empty array", function () {
expect(binarySearchFirstItem([], isTrue)).toEqual(0);
});
it("single boolean entry", function () {
expect(binarySearchFirstItem([false], isTrue)).toEqual(1);
expect(binarySearchFirstItem([true], isTrue)).toEqual(0);
});
it("three boolean entries", function () {
expect(binarySearchFirstItem([true, true, true], isTrue)).toEqual(0);
expect(binarySearchFirstItem([false, true, true], isTrue)).toEqual(1);
expect(binarySearchFirstItem([false, false, true], isTrue)).toEqual(2);
expect(binarySearchFirstItem([false, false, false], isTrue)).toEqual(3);
});
it("three numeric entries", function () {
expect(binarySearchFirstItem([0, 1, 2], isGreater3)).toEqual(3);
expect(binarySearchFirstItem([2, 3, 4], isGreater3)).toEqual(2);
expect(binarySearchFirstItem([4, 5, 6], isGreater3)).toEqual(0);
});
it("three numeric entries and a start index", function () {
expect(binarySearchFirstItem([0, 1, 2, 3, 4], isGreater3, 2)).toEqual(4);
expect(binarySearchFirstItem([2, 3, 4], isGreater3, 2)).toEqual(2);
expect(binarySearchFirstItem([4, 5, 6], isGreater3, 1)).toEqual(1);
});
});
describe("isValidRotation", function () {
it("should reject non-integer angles", function () {
expect(isValidRotation()).toEqual(false);

View File

@ -77,6 +77,7 @@ class AnnotationEditorLayerBuilder {
this.div = document.createElement("div");
this.div.className = "annotationEditorLayer";
this.div.tabIndex = 0;
this.pageDiv.append(this.div);
this.annotationEditorLayer = new AnnotationEditorLayer({
uiManager: this.#uiManager,
@ -84,6 +85,7 @@ class AnnotationEditorLayerBuilder {
annotationStorage: this.annotationStorage,
pageIndex: this.pdfPage._pageIndex,
l10n: this.l10n,
viewport: clonedViewport,
});
const parameters = {
@ -94,12 +96,11 @@ class AnnotationEditorLayerBuilder {
};
this.annotationEditorLayer.render(parameters);
this.pageDiv.append(this.div);
}
cancel() {
this._cancelled = true;
this.destroy();
}
hide() {
@ -121,8 +122,8 @@ class AnnotationEditorLayerBuilder {
return;
}
this.pageDiv = null;
this.div.remove();
this.annotationEditorLayer.destroy();
this.div.remove();
}
}

View File

@ -83,6 +83,9 @@ const DEFAULT_L10N_STRINGS = {
"Web fonts are disabled: unable to use embedded PDF fonts.",
free_text_default_content: "Enter text…",
editor_free_text_aria_label: "FreeText Editor",
editor_ink_aria_label: "Ink Editor",
editor_ink_canvas_aria_label: "User-created image",
};
function getL10nFallback(key, args) {

View File

@ -17,9 +17,9 @@
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js";
import { createPromiseCapability } from "pdfjs-lib";
import { binarySearchFirstItem, createPromiseCapability } from "pdfjs-lib";
import { getCharacterType } from "./pdf_find_utils.js";
import { scrollIntoView } from "./ui_utils.js";
const FindState = {
FOUND: 0,

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
import { binarySearchFirstItem } from "pdfjs-lib";
const DEFAULT_SCALE_VALUE = "auto";
const DEFAULT_SCALE = 1.0;
const DEFAULT_SCALE_DELTA = 1.1;
@ -221,38 +223,6 @@ function removeNullCharacters(str, replaceInvisible = false) {
return str.replace(NullCharactersRegExp, "");
}
/**
* Use binary search to find the index of the first item in a given array which
* passes a given condition. The items are expected to be sorted in the sense
* that if the condition is true for one item in the array, then it is also true
* for all following items.
*
* @returns {number} Index of the first array element to pass the test,
* or |items.length| if no such element exists.
*/
function binarySearchFirstItem(items, condition, start = 0) {
let minIndex = start;
let maxIndex = items.length - 1;
if (maxIndex < 0 || !condition(items[maxIndex])) {
return items.length;
}
if (condition(items[minIndex])) {
return minIndex;
}
while (minIndex < maxIndex) {
const currentIndex = (minIndex + maxIndex) >> 1;
const currentItem = items[currentIndex];
if (condition(currentItem)) {
maxIndex = currentIndex;
} else {
minIndex = currentIndex + 1;
}
}
return minIndex; /* === maxIndex */
}
/**
* Approximates float number as a fraction using Farey sequence (max order
* of 8).
@ -840,7 +810,6 @@ export {
approximateFraction,
AutoPrintRegExp,
backtrackBeforeAllVisibleElements, // only exported for testing
binarySearchFirstItem,
DEFAULT_SCALE,
DEFAULT_SCALE_DELTA,
DEFAULT_SCALE_VALUE,