2022-06-01 17:38:08 +09:00
|
|
|
/* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/** @typedef {import("./editor.js").AnnotationEditor} AnnotationEditor */
|
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
|
|
|
|
/** @typedef {import("../../web/interfaces").IL10n} IL10n */
|
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
import { AnnotationEditorType, shadow } from "../../shared/util.js";
|
|
|
|
import { binarySearchFirstItem } from "../display_utils.js";
|
2022-07-20 03:54:17 +09:00
|
|
|
import { bindEvents } from "./tools.js";
|
2022-06-01 17:38:08 +09:00
|
|
|
import { FreeTextEditor } from "./freetext.js";
|
2022-06-05 06:28:19 +09:00
|
|
|
import { InkEditor } from "./ink.js";
|
2022-06-01 17:38:08 +09:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {Object} AnnotationEditorLayerOptions
|
|
|
|
* @property {Object} mode
|
|
|
|
* @property {HTMLDivElement} div
|
|
|
|
* @property {AnnotationEditorUIManager} uiManager
|
|
|
|
* @property {boolean} enabled
|
|
|
|
* @property {AnnotationStorage} annotationStorag
|
|
|
|
* @property {number} pageIndex
|
|
|
|
* @property {IL10n} l10n
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manage all the different editors on a page.
|
|
|
|
*/
|
|
|
|
class AnnotationEditorLayer {
|
2022-07-19 23:10:31 +09:00
|
|
|
#allowClick = false;
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
#boundClick;
|
|
|
|
|
2022-07-19 23:10:31 +09:00
|
|
|
#boundMousedown;
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
#editors = new Map();
|
|
|
|
|
2022-07-13 00:32:14 +09:00
|
|
|
#isCleaningUp = false;
|
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
#textLayerMap = new WeakMap();
|
|
|
|
|
|
|
|
#textNodes = new Map();
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
#uiManager;
|
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
#waitingEditors = new Set();
|
|
|
|
|
2022-06-01 22:42:46 +09:00
|
|
|
static _initialized = false;
|
2022-06-01 17:38:08 +09:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {AnnotationEditorLayerOptions} options
|
|
|
|
*/
|
|
|
|
constructor(options) {
|
2022-06-01 22:42:46 +09:00
|
|
|
if (!AnnotationEditorLayer._initialized) {
|
|
|
|
AnnotationEditorLayer._initialized = true;
|
|
|
|
FreeTextEditor.initialize(options.l10n);
|
2022-06-29 01:21:32 +09:00
|
|
|
InkEditor.initialize(options.l10n);
|
2022-06-14 01:23:10 +09:00
|
|
|
|
|
|
|
options.uiManager.registerEditorTypes([FreeTextEditor, InkEditor]);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
this.#uiManager = options.uiManager;
|
|
|
|
this.annotationStorage = options.annotationStorage;
|
|
|
|
this.pageIndex = options.pageIndex;
|
|
|
|
this.div = options.div;
|
|
|
|
this.#boundClick = this.click.bind(this);
|
2022-07-19 23:10:31 +09:00
|
|
|
this.#boundMousedown = this.mousedown.bind(this);
|
2022-06-01 17:38:08 +09:00
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
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);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
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");
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
2022-06-30 01:09:32 +09:00
|
|
|
/**
|
|
|
|
* Update the toolbar if it's required to reflect the tool currently used.
|
|
|
|
* @param {number} mode
|
|
|
|
*/
|
|
|
|
updateToolbar(mode) {
|
|
|
|
this.#uiManager.updateToolbar(mode);
|
|
|
|
}
|
|
|
|
|
2022-06-17 01:16:49 +09:00
|
|
|
/**
|
|
|
|
* The mode has changed: it must be updated.
|
|
|
|
* @param {number} mode
|
|
|
|
*/
|
2022-07-13 00:32:14 +09:00
|
|
|
updateMode(mode = this.#uiManager.getMode()) {
|
|
|
|
this.#cleanup();
|
|
|
|
if (mode === AnnotationEditorType.INK) {
|
|
|
|
// We always want to an ink editor ready to draw in.
|
|
|
|
this.addInkEditorIfNeeded(false);
|
2022-07-19 23:10:31 +09:00
|
|
|
this.disableClick();
|
|
|
|
} else {
|
|
|
|
this.enableClick();
|
2022-06-17 01:16:49 +09:00
|
|
|
}
|
2022-06-23 22:47:45 +09:00
|
|
|
this.setActiveEditor(null);
|
2022-06-17 01:16:49 +09:00
|
|
|
}
|
|
|
|
|
2022-07-13 00:32:14 +09:00
|
|
|
addInkEditorIfNeeded(isCommitting) {
|
|
|
|
if (
|
|
|
|
!isCommitting &&
|
|
|
|
this.#uiManager.getMode() !== AnnotationEditorType.INK
|
|
|
|
) {
|
|
|
|
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 });
|
|
|
|
editor.setInBackground();
|
|
|
|
}
|
|
|
|
|
2022-07-05 01:04:32 +09:00
|
|
|
/**
|
|
|
|
* Set the editing state.
|
|
|
|
* @param {boolean} isEditing
|
|
|
|
*/
|
|
|
|
setEditingState(isEditing) {
|
|
|
|
this.#uiManager.setEditingState(isEditing);
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
/**
|
|
|
|
* Add some commands into the CommandManager (undo/redo stuff).
|
2022-06-14 01:23:10 +09:00
|
|
|
* @param {Object} params
|
2022-06-01 17:38:08 +09:00
|
|
|
*/
|
2022-06-14 01:23:10 +09:00
|
|
|
addCommands(params) {
|
|
|
|
this.#uiManager.addCommands(params);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enable pointer events on the main div in order to enable
|
|
|
|
* editor creation.
|
|
|
|
*/
|
|
|
|
enable() {
|
|
|
|
this.div.style.pointerEvents = "auto";
|
2022-06-29 01:21:32 +09:00
|
|
|
for (const editor of this.#editors.values()) {
|
|
|
|
editor.enableEditing();
|
|
|
|
}
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disable editor creation.
|
|
|
|
*/
|
|
|
|
disable() {
|
|
|
|
this.div.style.pointerEvents = "none";
|
2022-06-29 01:21:32 +09:00
|
|
|
for (const editor of this.#editors.values()) {
|
|
|
|
editor.disableEditing();
|
|
|
|
}
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the current editor.
|
|
|
|
* @param {AnnotationEditor} editor
|
|
|
|
*/
|
|
|
|
setActiveEditor(editor) {
|
2022-06-17 01:16:49 +09:00
|
|
|
const currentActive = this.#uiManager.getActive();
|
|
|
|
if (currentActive === editor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#uiManager.setActiveEditor(editor);
|
|
|
|
|
|
|
|
if (currentActive && currentActive !== editor) {
|
|
|
|
currentActive.commitOrRemove();
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
if (editor) {
|
2022-07-20 03:54:17 +09:00
|
|
|
this.#uiManager.unselectAll();
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-19 23:10:31 +09:00
|
|
|
enableClick() {
|
|
|
|
this.div.addEventListener("mousedown", this.#boundMousedown);
|
|
|
|
this.div.addEventListener("click", this.#boundClick);
|
|
|
|
}
|
|
|
|
|
|
|
|
disableClick() {
|
|
|
|
this.div.removeEventListener("mousedown", this.#boundMousedown);
|
|
|
|
this.div.removeEventListener("click", this.#boundClick);
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
attach(editor) {
|
|
|
|
this.#editors.set(editor.id, editor);
|
|
|
|
}
|
|
|
|
|
|
|
|
detach(editor) {
|
|
|
|
this.#editors.delete(editor.id);
|
2022-06-29 01:21:32 +09:00
|
|
|
this.removePointerInTextLayer(editor);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.#uiManager.removeEditor(editor);
|
|
|
|
this.detach(editor);
|
|
|
|
this.annotationStorage.removeKey(editor.id);
|
|
|
|
editor.div.remove();
|
|
|
|
editor.isAttachedToDOM = false;
|
|
|
|
if (this.#uiManager.isActive(editor) || this.#editors.size === 0) {
|
|
|
|
this.setActiveEditor(null);
|
|
|
|
}
|
2022-07-13 00:32:14 +09:00
|
|
|
|
|
|
|
if (!this.#isCleaningUp) {
|
|
|
|
this.addInkEditorIfNeeded(/* isCommitting = */ false);
|
|
|
|
}
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2022-06-23 22:47:45 +09:00
|
|
|
|
|
|
|
if (this.#uiManager.isActive(editor)) {
|
2022-06-29 01:21:32 +09:00
|
|
|
editor.parent?.setActiveEditor(null);
|
2022-06-23 22:47:45 +09:00
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
this.attach(editor);
|
|
|
|
editor.pageIndex = this.pageIndex;
|
2022-06-29 01:21:32 +09:00
|
|
|
editor.parent?.detach(editor);
|
2022-06-01 17:38:08 +09:00
|
|
|
editor.parent = this;
|
|
|
|
if (editor.div && editor.isAttachedToDOM) {
|
|
|
|
editor.div.remove();
|
2022-06-12 19:20:25 +09:00
|
|
|
this.div.append(editor.div);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
/**
|
|
|
|
* Add a new editor in the current view.
|
|
|
|
* @param {AnnotationEditor} editor
|
|
|
|
*/
|
|
|
|
add(editor) {
|
|
|
|
this.#changeParent(editor);
|
|
|
|
this.annotationStorage.setValue(editor.id, editor);
|
|
|
|
this.#uiManager.addEditor(editor);
|
|
|
|
this.attach(editor);
|
|
|
|
|
|
|
|
if (!editor.isAttachedToDOM) {
|
|
|
|
const div = editor.render();
|
2022-06-12 19:20:25 +09:00
|
|
|
this.div.append(div);
|
2022-06-01 17:38:08 +09:00
|
|
|
editor.isAttachedToDOM = true;
|
|
|
|
}
|
|
|
|
|
2022-06-29 01:21:32 +09:00
|
|
|
this.moveDivInDOM(editor);
|
2022-06-01 17:38:08 +09:00
|
|
|
editor.onceAdded();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add or rebuild depending if it has been removed or not.
|
|
|
|
* @param {AnnotationEditor} editor
|
|
|
|
*/
|
|
|
|
addOrRebuild(editor) {
|
|
|
|
if (editor.needsToBeRebuilt()) {
|
|
|
|
editor.rebuild();
|
|
|
|
} else {
|
|
|
|
this.add(editor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a new editor and make this addition undoable.
|
|
|
|
* @param {AnnotationEditor} editor
|
|
|
|
*/
|
|
|
|
addANewEditor(editor) {
|
|
|
|
const cmd = () => {
|
|
|
|
this.addOrRebuild(editor);
|
|
|
|
};
|
|
|
|
const undo = () => {
|
|
|
|
editor.remove();
|
|
|
|
};
|
|
|
|
|
2022-06-14 01:23:10 +09:00
|
|
|
this.addCommands({ cmd, undo, mustExec: true });
|
2022-06-17 01:16:49 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a new editor and make this addition undoable.
|
|
|
|
* @param {AnnotationEditor} editor
|
|
|
|
*/
|
|
|
|
addUndoableEditor(editor) {
|
|
|
|
const cmd = () => {
|
|
|
|
this.addOrRebuild(editor);
|
|
|
|
};
|
|
|
|
const undo = () => {
|
|
|
|
editor.remove();
|
|
|
|
};
|
|
|
|
|
2022-06-14 01:23:10 +09:00
|
|
|
this.addCommands({ cmd, undo, mustExec: false });
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an id for an editor.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
getNextId() {
|
|
|
|
return this.#uiManager.getId();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new editor
|
|
|
|
* @param {Object} params
|
|
|
|
* @returns {AnnotationEditor}
|
|
|
|
*/
|
|
|
|
#createNewEditor(params) {
|
|
|
|
switch (this.#uiManager.getMode()) {
|
|
|
|
case AnnotationEditorType.FREETEXT:
|
|
|
|
return new FreeTextEditor(params);
|
2022-06-05 06:28:19 +09:00
|
|
|
case AnnotationEditorType.INK:
|
|
|
|
return new InkEditor(params);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-07-18 21:47:09 +09:00
|
|
|
/**
|
|
|
|
* Create a new editor
|
|
|
|
* @param {Object} data
|
|
|
|
* @returns {AnnotationEditor}
|
|
|
|
*/
|
|
|
|
deserialize(data) {
|
|
|
|
switch (data.annotationType) {
|
|
|
|
case AnnotationEditorType.FREETEXT:
|
|
|
|
return FreeTextEditor.deserialize(data, this);
|
|
|
|
case AnnotationEditorType.INK:
|
|
|
|
return InkEditor.deserialize(data, this);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
/**
|
2022-06-17 01:16:49 +09:00
|
|
|
* Create and add a new editor.
|
2022-06-01 17:38:08 +09:00
|
|
|
* @param {MouseEvent} event
|
2022-06-17 01:16:49 +09:00
|
|
|
* @returns {AnnotationEditor}
|
2022-06-01 17:38:08 +09:00
|
|
|
*/
|
2022-06-17 01:16:49 +09:00
|
|
|
#createAndAddNewEditor(event) {
|
2022-06-01 17:38:08 +09:00
|
|
|
const id = this.getNextId();
|
|
|
|
const editor = this.#createNewEditor({
|
|
|
|
parent: this,
|
|
|
|
id,
|
|
|
|
x: event.offsetX,
|
|
|
|
y: event.offsetY,
|
|
|
|
});
|
|
|
|
if (editor) {
|
2022-06-17 01:16:49 +09:00
|
|
|
this.add(editor);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
2022-06-17 01:16:49 +09:00
|
|
|
|
|
|
|
return editor;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mouseclick callback.
|
|
|
|
* @param {MouseEvent} event
|
|
|
|
*/
|
|
|
|
click(event) {
|
2022-07-19 23:10:31 +09:00
|
|
|
if (event.target !== this.div) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.#allowClick) {
|
|
|
|
this.#allowClick = true;
|
2022-06-17 01:16:49 +09:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#createAndAddNewEditor(event);
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
2022-07-19 23:10:31 +09:00
|
|
|
/**
|
|
|
|
* Mousedown callback.
|
|
|
|
* @param {MouseEvent} event
|
|
|
|
*/
|
|
|
|
mousedown(event) {
|
|
|
|
if (event.target !== this.div) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#allowClick = !this.#uiManager.hasActive();
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
/**
|
|
|
|
* Drag callback.
|
|
|
|
* @param {DragEvent} event
|
|
|
|
*/
|
|
|
|
drop(event) {
|
|
|
|
const id = event.dataTransfer.getData("text/plain");
|
|
|
|
const editor = this.#uiManager.getEditor(id);
|
|
|
|
if (!editor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.dataTransfer.dropEffect = "move";
|
|
|
|
|
|
|
|
this.#changeParent(editor);
|
|
|
|
|
|
|
|
const rect = this.div.getBoundingClientRect();
|
2022-06-23 22:47:45 +09:00
|
|
|
const endX = event.clientX - rect.x;
|
|
|
|
const endY = event.clientY - rect.y;
|
|
|
|
|
|
|
|
editor.translate(endX - editor.startX, endY - editor.startY);
|
2022-06-29 01:21:32 +09:00
|
|
|
this.moveDivInDOM(editor);
|
|
|
|
editor.div.focus();
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Dragover callback.
|
|
|
|
* @param {DragEvent} event
|
|
|
|
*/
|
|
|
|
dragover(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroy the main editor.
|
|
|
|
*/
|
|
|
|
destroy() {
|
2022-06-29 01:21:32 +09:00
|
|
|
if (this.#uiManager.getActive()?.parent === this) {
|
|
|
|
this.#uiManager.setActiveEditor(null);
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
for (const editor of this.#editors.values()) {
|
2022-06-29 01:21:32 +09:00
|
|
|
this.removePointerInTextLayer(editor);
|
2022-06-01 17:38:08 +09:00
|
|
|
editor.isAttachedToDOM = false;
|
|
|
|
editor.div.remove();
|
|
|
|
editor.parent = null;
|
|
|
|
}
|
2022-06-29 01:21:32 +09:00
|
|
|
this.#textNodes.clear();
|
|
|
|
this.div = null;
|
2022-06-01 17:38:08 +09:00
|
|
|
this.#editors.clear();
|
2022-06-29 01:21:32 +09:00
|
|
|
this.#waitingEditors.clear();
|
2022-06-01 17:38:08 +09:00
|
|
|
this.#uiManager.removeLayer(this);
|
|
|
|
}
|
|
|
|
|
2022-07-13 00:32:14 +09:00
|
|
|
#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;
|
|
|
|
}
|
|
|
|
|
2022-06-01 17:38:08 +09:00
|
|
|
/**
|
|
|
|
* Render the main editor.
|
|
|
|
* @param {Object} parameters
|
|
|
|
*/
|
|
|
|
render(parameters) {
|
|
|
|
this.viewport = parameters.viewport;
|
2022-07-20 03:54:17 +09:00
|
|
|
bindEvents(this, this.div, ["dragover", "drop"]);
|
2022-06-23 22:47:45 +09:00
|
|
|
this.setDimensions();
|
2022-06-29 01:21:32 +09:00
|
|
|
for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
|
|
|
|
this.add(editor);
|
|
|
|
}
|
2022-07-13 00:32:14 +09:00
|
|
|
this.updateMode();
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the main editor.
|
|
|
|
* @param {Object} parameters
|
|
|
|
*/
|
|
|
|
update(parameters) {
|
2022-06-23 22:47:45 +09:00
|
|
|
this.setActiveEditor(null);
|
2022-06-01 17:38:08 +09:00
|
|
|
this.viewport = parameters.viewport;
|
2022-06-23 22:47:45 +09:00
|
|
|
this.setDimensions();
|
2022-07-13 00:32:14 +09:00
|
|
|
this.updateMode();
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the scale factor from the viewport.
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
get scaleFactor() {
|
|
|
|
return this.viewport.scale;
|
|
|
|
}
|
2022-06-23 22:47:45 +09:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get page dimensions.
|
|
|
|
* @returns {Object} dimensions.
|
|
|
|
*/
|
|
|
|
get pageDimensions() {
|
|
|
|
const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
|
|
|
|
const width = pageURx - pageLLx;
|
|
|
|
const height = pageURy - pageLLy;
|
|
|
|
|
|
|
|
return [width, height];
|
|
|
|
}
|
|
|
|
|
|
|
|
get viewportBaseDimensions() {
|
|
|
|
const { width, height, rotation } = this.viewport;
|
|
|
|
return rotation % 180 === 0 ? [width, height] : [height, width];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the dimensions of the main div.
|
|
|
|
*/
|
|
|
|
setDimensions() {
|
|
|
|
const { width, height, rotation } = this.viewport;
|
|
|
|
|
|
|
|
const flipOrientation = rotation % 180 !== 0,
|
|
|
|
widthStr = Math.floor(width) + "px",
|
|
|
|
heightStr = Math.floor(height) + "px";
|
|
|
|
|
|
|
|
this.div.style.width = flipOrientation ? heightStr : widthStr;
|
|
|
|
this.div.style.height = flipOrientation ? widthStr : heightStr;
|
|
|
|
this.div.setAttribute("data-main-rotation", rotation);
|
|
|
|
}
|
2022-06-01 17:38:08 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
export { AnnotationEditorLayer };
|