In implementing caret browsing mode in pdf.js, I didn't notice that selectstart isn't always triggered. So this patch removes the use of selectstart and rely only on selectionchange. In order to simplify the selection management, the selection code is moved in the AnnotationUIManager: - it simplifies the code; - it allows to have only one listener for selectionchange instead of having one by visible page for selectstart. I had to add a delay in the integration tests for highlighting (there's a comment with an explanation), it isn't really nice, but it's the only way I found and in real life there always is a delay between press and release.
858 lines
22 KiB
JavaScript
858 lines
22 KiB
JavaScript
/* 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.
|
|
*/
|
|
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
|
/** @typedef {import("../display_utils.js").PageViewport} PageViewport */
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("../../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
|
/** @typedef {import("../../../web/interfaces").IL10n} IL10n */
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
|
/** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */
|
|
|
|
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
|
|
import { AnnotationEditor } from "./editor.js";
|
|
import { FreeTextEditor } from "./freetext.js";
|
|
import { HighlightEditor } from "./highlight.js";
|
|
import { InkEditor } from "./ink.js";
|
|
import { setLayerDimensions } from "../display_utils.js";
|
|
import { StampEditor } from "./stamp.js";
|
|
|
|
/**
|
|
* @typedef {Object} AnnotationEditorLayerOptions
|
|
* @property {Object} mode
|
|
* @property {HTMLDivElement} div
|
|
* @property {AnnotationEditorUIManager} uiManager
|
|
* @property {boolean} enabled
|
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
|
* @property {number} pageIndex
|
|
* @property {IL10n} l10n
|
|
* @property {AnnotationLayer} [annotationLayer]
|
|
* @property {HTMLDivElement} [textLayer]
|
|
* @property {DrawLayer} drawLayer
|
|
* @property {PageViewport} viewport
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} RenderEditorLayerOptions
|
|
* @property {PageViewport} viewport
|
|
*/
|
|
|
|
/**
|
|
* Manage all the different editors on a page.
|
|
*/
|
|
class AnnotationEditorLayer {
|
|
#accessibilityManager;
|
|
|
|
#allowClick = false;
|
|
|
|
#annotationLayer = null;
|
|
|
|
#boundPointerup = this.pointerup.bind(this);
|
|
|
|
#boundPointerdown = this.pointerdown.bind(this);
|
|
|
|
#boundTextLayerPointerDown = this.#textLayerPointerDown.bind(this);
|
|
|
|
#editorFocusTimeoutId = null;
|
|
|
|
#editors = new Map();
|
|
|
|
#hadPointerDown = false;
|
|
|
|
#isCleaningUp = false;
|
|
|
|
#isDisabling = false;
|
|
|
|
#textLayer = null;
|
|
|
|
#uiManager;
|
|
|
|
static _initialized = false;
|
|
|
|
static #editorTypes = new Map(
|
|
[FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [
|
|
type._editorType,
|
|
type,
|
|
])
|
|
);
|
|
|
|
/**
|
|
* @param {AnnotationEditorLayerOptions} options
|
|
*/
|
|
constructor({
|
|
uiManager,
|
|
pageIndex,
|
|
div,
|
|
accessibilityManager,
|
|
annotationLayer,
|
|
drawLayer,
|
|
textLayer,
|
|
viewport,
|
|
l10n,
|
|
}) {
|
|
const editorTypes = [...AnnotationEditorLayer.#editorTypes.values()];
|
|
if (!AnnotationEditorLayer._initialized) {
|
|
AnnotationEditorLayer._initialized = true;
|
|
for (const editorType of editorTypes) {
|
|
editorType.initialize(l10n, uiManager);
|
|
}
|
|
}
|
|
uiManager.registerEditorTypes(editorTypes);
|
|
|
|
this.#uiManager = uiManager;
|
|
this.pageIndex = pageIndex;
|
|
this.div = div;
|
|
this.#accessibilityManager = accessibilityManager;
|
|
this.#annotationLayer = annotationLayer;
|
|
this.viewport = viewport;
|
|
this.#textLayer = textLayer;
|
|
this.drawLayer = drawLayer;
|
|
|
|
this.#uiManager.addLayer(this);
|
|
}
|
|
|
|
get isEmpty() {
|
|
return this.#editors.size === 0;
|
|
}
|
|
|
|
/**
|
|
* Update the toolbar if it's required to reflect the tool currently used.
|
|
* @param {number} mode
|
|
*/
|
|
updateToolbar(mode) {
|
|
this.#uiManager.updateToolbar(mode);
|
|
}
|
|
|
|
/**
|
|
* The mode has changed: it must be updated.
|
|
* @param {number} mode
|
|
*/
|
|
updateMode(mode = this.#uiManager.getMode()) {
|
|
this.#cleanup();
|
|
switch (mode) {
|
|
case AnnotationEditorType.NONE:
|
|
this.disableTextSelection();
|
|
this.togglePointerEvents(false);
|
|
this.toggleAnnotationLayerPointerEvents(true);
|
|
this.disableClick();
|
|
return;
|
|
case AnnotationEditorType.INK:
|
|
// We always want to have an ink editor ready to draw in.
|
|
this.addInkEditorIfNeeded(false);
|
|
|
|
this.disableTextSelection();
|
|
this.togglePointerEvents(true);
|
|
this.disableClick();
|
|
break;
|
|
case AnnotationEditorType.HIGHLIGHT:
|
|
this.enableTextSelection();
|
|
this.togglePointerEvents(false);
|
|
this.disableClick();
|
|
break;
|
|
default:
|
|
this.disableTextSelection();
|
|
this.togglePointerEvents(true);
|
|
this.enableClick();
|
|
}
|
|
|
|
this.toggleAnnotationLayerPointerEvents(false);
|
|
const { classList } = this.div;
|
|
for (const editorType of AnnotationEditorLayer.#editorTypes.values()) {
|
|
classList.toggle(
|
|
`${editorType._type}Editing`,
|
|
mode === editorType._editorType
|
|
);
|
|
}
|
|
this.div.hidden = false;
|
|
}
|
|
|
|
hasTextLayer(textLayer) {
|
|
return textLayer === this.#textLayer?.div;
|
|
}
|
|
|
|
addInkEditorIfNeeded(isCommitting) {
|
|
if (this.#uiManager.getMode() !== AnnotationEditorType.INK) {
|
|
// We don't want to add an ink editor if we're not in ink mode!
|
|
return;
|
|
}
|
|
|
|
if (!isCommitting) {
|
|
// We're removing an editor but an empty one can already exist so in this
|
|
// case we don't need to create a new one.
|
|
for (const editor of this.#editors.values()) {
|
|
if (editor.isEmpty()) {
|
|
editor.setInBackground();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const editor = this.createAndAddNewEditor(
|
|
{ offsetX: 0, offsetY: 0 },
|
|
/* isCentered = */ false
|
|
);
|
|
editor.setInBackground();
|
|
}
|
|
|
|
/**
|
|
* Set the editing state.
|
|
* @param {boolean} isEditing
|
|
*/
|
|
setEditingState(isEditing) {
|
|
this.#uiManager.setEditingState(isEditing);
|
|
}
|
|
|
|
/**
|
|
* Add some commands into the CommandManager (undo/redo stuff).
|
|
* @param {Object} params
|
|
*/
|
|
addCommands(params) {
|
|
this.#uiManager.addCommands(params);
|
|
}
|
|
|
|
togglePointerEvents(enabled = false) {
|
|
this.div.classList.toggle("disabled", !enabled);
|
|
}
|
|
|
|
toggleAnnotationLayerPointerEvents(enabled = false) {
|
|
this.#annotationLayer?.div.classList.toggle("disabled", !enabled);
|
|
}
|
|
|
|
/**
|
|
* Enable pointer events on the main div in order to enable
|
|
* editor creation.
|
|
*/
|
|
enable() {
|
|
this.togglePointerEvents(true);
|
|
const annotationElementIds = new Set();
|
|
for (const editor of this.#editors.values()) {
|
|
editor.enableEditing();
|
|
if (editor.annotationElementId) {
|
|
annotationElementIds.add(editor.annotationElementId);
|
|
}
|
|
}
|
|
|
|
if (!this.#annotationLayer) {
|
|
return;
|
|
}
|
|
|
|
const editables = this.#annotationLayer.getEditableAnnotations();
|
|
for (const editable of editables) {
|
|
// The element must be hidden whatever its state is.
|
|
editable.hide();
|
|
if (this.#uiManager.isDeletedAnnotationElement(editable.data.id)) {
|
|
continue;
|
|
}
|
|
if (annotationElementIds.has(editable.data.id)) {
|
|
continue;
|
|
}
|
|
const editor = this.deserialize(editable);
|
|
if (!editor) {
|
|
continue;
|
|
}
|
|
this.addOrRebuild(editor);
|
|
editor.enableEditing();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable editor creation.
|
|
*/
|
|
disable() {
|
|
this.#isDisabling = true;
|
|
this.togglePointerEvents(false);
|
|
const hiddenAnnotationIds = new Set();
|
|
for (const editor of this.#editors.values()) {
|
|
editor.disableEditing();
|
|
if (!editor.annotationElementId || editor.serialize() !== null) {
|
|
hiddenAnnotationIds.add(editor.annotationElementId);
|
|
continue;
|
|
}
|
|
this.getEditableAnnotation(editor.annotationElementId)?.show();
|
|
editor.remove();
|
|
}
|
|
|
|
if (this.#annotationLayer) {
|
|
// Show the annotations that were hidden in enable().
|
|
const editables = this.#annotationLayer.getEditableAnnotations();
|
|
for (const editable of editables) {
|
|
const { id } = editable.data;
|
|
if (
|
|
hiddenAnnotationIds.has(id) ||
|
|
this.#uiManager.isDeletedAnnotationElement(id)
|
|
) {
|
|
continue;
|
|
}
|
|
editable.show();
|
|
}
|
|
}
|
|
|
|
this.#cleanup();
|
|
if (this.isEmpty) {
|
|
this.div.hidden = true;
|
|
}
|
|
const { classList } = this.div;
|
|
for (const editorType of AnnotationEditorLayer.#editorTypes.values()) {
|
|
classList.remove(`${editorType._type}Editing`);
|
|
}
|
|
this.disableTextSelection();
|
|
this.toggleAnnotationLayerPointerEvents(true);
|
|
|
|
this.#isDisabling = false;
|
|
}
|
|
|
|
getEditableAnnotation(id) {
|
|
return this.#annotationLayer?.getEditableAnnotation(id) || null;
|
|
}
|
|
|
|
/**
|
|
* Set the current editor.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
setActiveEditor(editor) {
|
|
const currentActive = this.#uiManager.getActive();
|
|
if (currentActive === editor) {
|
|
return;
|
|
}
|
|
|
|
this.#uiManager.setActiveEditor(editor);
|
|
}
|
|
|
|
enableTextSelection() {
|
|
if (this.#textLayer?.div) {
|
|
this.#textLayer.div.addEventListener(
|
|
"pointerdown",
|
|
this.#boundTextLayerPointerDown
|
|
);
|
|
this.#textLayer.div.classList.add("highlighting");
|
|
}
|
|
}
|
|
|
|
disableTextSelection() {
|
|
if (this.#textLayer?.div) {
|
|
this.#textLayer.div.removeEventListener(
|
|
"pointerdown",
|
|
this.#boundTextLayerPointerDown
|
|
);
|
|
this.#textLayer.div.classList.remove("highlighting");
|
|
}
|
|
}
|
|
|
|
#textLayerPointerDown(event) {
|
|
// Unselect all the editors in order to let the user select some text
|
|
// without being annoyed by an editor toolbar.
|
|
this.#uiManager.unselectAll();
|
|
if (event.target === this.#textLayer.div) {
|
|
const { isMac } = FeatureTest.platform;
|
|
if (event.button !== 0 || (event.ctrlKey && isMac)) {
|
|
// Do nothing on right click.
|
|
return;
|
|
}
|
|
this.#textLayer.div.classList.add("free");
|
|
HighlightEditor.startHighlighting(
|
|
this,
|
|
this.#uiManager.direction === "ltr",
|
|
event
|
|
);
|
|
this.#textLayer.div.addEventListener(
|
|
"pointerup",
|
|
() => {
|
|
this.#textLayer.div.classList.remove("free");
|
|
},
|
|
{ once: true }
|
|
);
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
enableClick() {
|
|
this.div.addEventListener("pointerdown", this.#boundPointerdown);
|
|
this.div.addEventListener("pointerup", this.#boundPointerup);
|
|
}
|
|
|
|
disableClick() {
|
|
this.div.removeEventListener("pointerdown", this.#boundPointerdown);
|
|
this.div.removeEventListener("pointerup", this.#boundPointerup);
|
|
}
|
|
|
|
attach(editor) {
|
|
this.#editors.set(editor.id, editor);
|
|
const { annotationElementId } = editor;
|
|
if (
|
|
annotationElementId &&
|
|
this.#uiManager.isDeletedAnnotationElement(annotationElementId)
|
|
) {
|
|
this.#uiManager.removeDeletedAnnotationElement(editor);
|
|
}
|
|
}
|
|
|
|
detach(editor) {
|
|
this.#editors.delete(editor.id);
|
|
this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv);
|
|
|
|
if (!this.#isDisabling && editor.annotationElementId) {
|
|
this.#uiManager.addDeletedAnnotationElement(editor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove an editor.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
remove(editor) {
|
|
// Since we can undo a removal we need to keep the
|
|
// parent property as it is, so don't null it!
|
|
|
|
this.detach(editor);
|
|
this.#uiManager.removeEditor(editor);
|
|
editor.div.remove();
|
|
editor.isAttachedToDOM = false;
|
|
|
|
if (!this.#isCleaningUp) {
|
|
this.addInkEditorIfNeeded(/* isCommitting = */ false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An editor can have a different parent, for example after having
|
|
* being dragged and droped from a page to another.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
changeParent(editor) {
|
|
if (editor.parent === this) {
|
|
return;
|
|
}
|
|
|
|
if (editor.annotationElementId) {
|
|
this.#uiManager.addDeletedAnnotationElement(editor.annotationElementId);
|
|
AnnotationEditor.deleteAnnotationElement(editor);
|
|
editor.annotationElementId = null;
|
|
}
|
|
|
|
this.attach(editor);
|
|
editor.parent?.detach(editor);
|
|
editor.setParent(this);
|
|
if (editor.div && editor.isAttachedToDOM) {
|
|
editor.div.remove();
|
|
this.div.append(editor.div);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a new editor in the current view.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
add(editor) {
|
|
this.changeParent(editor);
|
|
this.#uiManager.addEditor(editor);
|
|
this.attach(editor);
|
|
|
|
if (!editor.isAttachedToDOM) {
|
|
const div = editor.render();
|
|
this.div.append(div);
|
|
editor.isAttachedToDOM = true;
|
|
}
|
|
|
|
// The editor will be correctly moved into the DOM (see fixAndSetPosition).
|
|
editor.fixAndSetPosition();
|
|
editor.onceAdded();
|
|
this.#uiManager.addToAnnotationStorage(editor);
|
|
}
|
|
|
|
moveEditorInDOM(editor) {
|
|
if (!editor.isAttachedToDOM) {
|
|
return;
|
|
}
|
|
|
|
const { activeElement } = document;
|
|
if (editor.div.contains(activeElement) && !this.#editorFocusTimeoutId) {
|
|
// When the div is moved in the DOM the focus can move somewhere else,
|
|
// so we want to be sure that the focus will stay on the editor but we
|
|
// don't want to call any focus callbacks, hence we disable them and only
|
|
// re-enable them when the editor has the focus.
|
|
editor._focusEventsAllowed = false;
|
|
this.#editorFocusTimeoutId = setTimeout(() => {
|
|
this.#editorFocusTimeoutId = null;
|
|
if (!editor.div.contains(document.activeElement)) {
|
|
editor.div.addEventListener(
|
|
"focusin",
|
|
() => {
|
|
editor._focusEventsAllowed = true;
|
|
},
|
|
{ once: true }
|
|
);
|
|
activeElement.focus();
|
|
} else {
|
|
editor._focusEventsAllowed = true;
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
editor._structTreeParentId = this.#accessibilityManager?.moveElementInDOM(
|
|
this.div,
|
|
editor.div,
|
|
editor.contentDiv,
|
|
/* isRemovable = */ true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add or rebuild depending if it has been removed or not.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
addOrRebuild(editor) {
|
|
if (editor.needsToBeRebuilt()) {
|
|
editor.parent ||= this;
|
|
editor.rebuild();
|
|
} else {
|
|
this.add(editor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a new editor and make this addition undoable.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
addUndoableEditor(editor) {
|
|
const cmd = () => editor._uiManager.rebuild(editor);
|
|
const undo = () => {
|
|
editor.remove();
|
|
};
|
|
|
|
this.addCommands({ cmd, undo, mustExec: false });
|
|
}
|
|
|
|
/**
|
|
* Get an id for an editor.
|
|
* @returns {string}
|
|
*/
|
|
getNextId() {
|
|
return this.#uiManager.getId();
|
|
}
|
|
|
|
get #currentEditorType() {
|
|
return AnnotationEditorLayer.#editorTypes.get(this.#uiManager.getMode());
|
|
}
|
|
|
|
/**
|
|
* Create a new editor
|
|
* @param {Object} params
|
|
* @returns {AnnotationEditor}
|
|
*/
|
|
#createNewEditor(params) {
|
|
const editorType = this.#currentEditorType;
|
|
return editorType ? new editorType.prototype.constructor(params) : null;
|
|
}
|
|
|
|
canCreateNewEmptyEditor() {
|
|
return this.#currentEditorType?.canCreateNewEmptyEditor();
|
|
}
|
|
|
|
/**
|
|
* Paste some content into a new editor.
|
|
* @param {number} mode
|
|
* @param {Object} params
|
|
*/
|
|
pasteEditor(mode, params) {
|
|
this.#uiManager.updateToolbar(mode);
|
|
this.#uiManager.updateMode(mode);
|
|
|
|
const { offsetX, offsetY } = this.#getCenterPoint();
|
|
const id = this.getNextId();
|
|
const editor = this.#createNewEditor({
|
|
parent: this,
|
|
id,
|
|
x: offsetX,
|
|
y: offsetY,
|
|
uiManager: this.#uiManager,
|
|
isCentered: true,
|
|
...params,
|
|
});
|
|
if (editor) {
|
|
this.add(editor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new editor
|
|
* @param {Object} data
|
|
* @returns {AnnotationEditor | null}
|
|
*/
|
|
deserialize(data) {
|
|
return (
|
|
AnnotationEditorLayer.#editorTypes
|
|
.get(data.annotationType ?? data.annotationEditorType)
|
|
?.deserialize(data, this, this.#uiManager) || null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create and add a new editor.
|
|
* @param {PointerEvent} event
|
|
* @param {boolean} isCentered
|
|
* @param [Object] data
|
|
* @returns {AnnotationEditor}
|
|
*/
|
|
createAndAddNewEditor(event, isCentered, data = {}) {
|
|
const id = this.getNextId();
|
|
const editor = this.#createNewEditor({
|
|
parent: this,
|
|
id,
|
|
x: event.offsetX,
|
|
y: event.offsetY,
|
|
uiManager: this.#uiManager,
|
|
isCentered,
|
|
...data,
|
|
});
|
|
if (editor) {
|
|
this.add(editor);
|
|
}
|
|
|
|
return editor;
|
|
}
|
|
|
|
#getCenterPoint() {
|
|
const { x, y, width, height } = this.div.getBoundingClientRect();
|
|
const tlX = Math.max(0, x);
|
|
const tlY = Math.max(0, y);
|
|
const brX = Math.min(window.innerWidth, x + width);
|
|
const brY = Math.min(window.innerHeight, y + height);
|
|
const centerX = (tlX + brX) / 2 - x;
|
|
const centerY = (tlY + brY) / 2 - y;
|
|
const [offsetX, offsetY] =
|
|
this.viewport.rotation % 180 === 0
|
|
? [centerX, centerY]
|
|
: [centerY, centerX];
|
|
|
|
return { offsetX, offsetY };
|
|
}
|
|
|
|
/**
|
|
* Create and add a new editor.
|
|
*/
|
|
addNewEditor() {
|
|
this.createAndAddNewEditor(this.#getCenterPoint(), /* isCentered = */ true);
|
|
}
|
|
|
|
/**
|
|
* Set the last selected editor.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
setSelected(editor) {
|
|
this.#uiManager.setSelected(editor);
|
|
}
|
|
|
|
/**
|
|
* Add or remove an editor the current selection.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
toggleSelected(editor) {
|
|
this.#uiManager.toggleSelected(editor);
|
|
}
|
|
|
|
/**
|
|
* Check if the editor is selected.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
isSelected(editor) {
|
|
return this.#uiManager.isSelected(editor);
|
|
}
|
|
|
|
/**
|
|
* Unselect an editor.
|
|
* @param {AnnotationEditor} editor
|
|
*/
|
|
unselect(editor) {
|
|
this.#uiManager.unselect(editor);
|
|
}
|
|
|
|
/**
|
|
* Pointerup callback.
|
|
* @param {PointerEvent} event
|
|
*/
|
|
pointerup(event) {
|
|
const { isMac } = FeatureTest.platform;
|
|
if (event.button !== 0 || (event.ctrlKey && isMac)) {
|
|
// Don't create an editor on right click.
|
|
return;
|
|
}
|
|
|
|
if (event.target !== this.div) {
|
|
return;
|
|
}
|
|
|
|
if (!this.#hadPointerDown) {
|
|
// It can happen when the user starts a drag inside a text editor
|
|
// and then releases the mouse button outside of it. In such a case
|
|
// we don't want to create a new editor, hence we check that a pointerdown
|
|
// occurred on this div previously.
|
|
return;
|
|
}
|
|
this.#hadPointerDown = false;
|
|
|
|
if (!this.#allowClick) {
|
|
this.#allowClick = true;
|
|
return;
|
|
}
|
|
|
|
if (this.#uiManager.getMode() === AnnotationEditorType.STAMP) {
|
|
this.#uiManager.unselectAll();
|
|
return;
|
|
}
|
|
|
|
this.createAndAddNewEditor(event, /* isCentered = */ false);
|
|
}
|
|
|
|
/**
|
|
* Pointerdown callback.
|
|
* @param {PointerEvent} event
|
|
*/
|
|
pointerdown(event) {
|
|
if (this.#uiManager.getMode() === AnnotationEditorType.HIGHLIGHT) {
|
|
this.enableTextSelection();
|
|
}
|
|
if (this.#hadPointerDown) {
|
|
// It's possible to have a second pointerdown event before a pointerup one
|
|
// when the user puts a finger on a touchscreen and then add a second one
|
|
// to start a pinch-to-zoom gesture.
|
|
// That said, in case it's possible to have two pointerdown events with
|
|
// a mouse, we don't want to create a new editor in such a case either.
|
|
this.#hadPointerDown = false;
|
|
return;
|
|
}
|
|
const { isMac } = FeatureTest.platform;
|
|
if (event.button !== 0 || (event.ctrlKey && isMac)) {
|
|
// Do nothing on right click.
|
|
return;
|
|
}
|
|
|
|
if (event.target !== this.div) {
|
|
return;
|
|
}
|
|
|
|
this.#hadPointerDown = true;
|
|
|
|
const editor = this.#uiManager.getActive();
|
|
this.#allowClick = !editor || editor.isEmpty();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {AnnotationEditor} editor
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @returns
|
|
*/
|
|
findNewParent(editor, x, y) {
|
|
const layer = this.#uiManager.findParent(x, y);
|
|
if (layer === null || layer === this) {
|
|
return false;
|
|
}
|
|
layer.changeParent(editor);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Destroy the main editor.
|
|
*/
|
|
destroy() {
|
|
if (this.#uiManager.getActive()?.parent === this) {
|
|
// We need to commit the current editor before destroying the layer.
|
|
this.#uiManager.commitOrRemove();
|
|
this.#uiManager.setActiveEditor(null);
|
|
}
|
|
|
|
if (this.#editorFocusTimeoutId) {
|
|
clearTimeout(this.#editorFocusTimeoutId);
|
|
this.#editorFocusTimeoutId = null;
|
|
}
|
|
|
|
for (const editor of this.#editors.values()) {
|
|
this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv);
|
|
editor.setParent(null);
|
|
editor.isAttachedToDOM = false;
|
|
editor.div.remove();
|
|
}
|
|
this.div = null;
|
|
this.#editors.clear();
|
|
this.#uiManager.removeLayer(this);
|
|
}
|
|
|
|
#cleanup() {
|
|
// When we're cleaning up, some editors are removed but we don't want
|
|
// to add a new one which will induce an addition in this.#editors, hence
|
|
// an infinite loop.
|
|
this.#isCleaningUp = true;
|
|
for (const editor of this.#editors.values()) {
|
|
if (editor.isEmpty()) {
|
|
editor.remove();
|
|
}
|
|
}
|
|
this.#isCleaningUp = false;
|
|
}
|
|
|
|
/**
|
|
* Render the main editor.
|
|
* @param {RenderEditorLayerOptions} parameters
|
|
*/
|
|
render({ viewport }) {
|
|
this.viewport = viewport;
|
|
setLayerDimensions(this.div, viewport);
|
|
for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
|
|
this.add(editor);
|
|
}
|
|
this.updateMode();
|
|
}
|
|
|
|
/**
|
|
* Update the main editor.
|
|
* @param {RenderEditorLayerOptions} parameters
|
|
*/
|
|
update({ viewport }) {
|
|
// Editors have their dimensions/positions in percent so to avoid any
|
|
// issues (see #15582), we must commit the current one before changing
|
|
// the viewport.
|
|
this.#uiManager.commitOrRemove();
|
|
|
|
const oldRotation = this.viewport.rotation;
|
|
const rotation = viewport.rotation;
|
|
this.viewport = viewport;
|
|
setLayerDimensions(this.div, { rotation });
|
|
if (oldRotation !== rotation) {
|
|
for (const editor of this.#editors.values()) {
|
|
editor.rotate(rotation);
|
|
}
|
|
}
|
|
this.updateMode();
|
|
}
|
|
|
|
/**
|
|
* Get page dimensions.
|
|
* @returns {Object} dimensions.
|
|
*/
|
|
get pageDimensions() {
|
|
const { pageWidth, pageHeight } = this.viewport.rawDims;
|
|
return [pageWidth, pageHeight];
|
|
}
|
|
|
|
get scale() {
|
|
return this.#uiManager.viewParameters.realScale;
|
|
}
|
|
}
|
|
|
|
export { AnnotationEditorLayer };
|