/** * @licstart The following is the entire license notice for the * JavaScript code in this page * * 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. * * @licend The above is the entire license notice for the * JavaScript code in this page */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KeyboardManager = exports.CommandManager = exports.ColorManager = exports.AnnotationEditorUIManager = void 0; exports.bindEvents = bindEvents; exports.opacityToHex = opacityToHex; var _util = require("../../shared/util.js"); var _display_utils = require("../display_utils.js"); function bindEvents(obj, element, names) { for (const name of names) { element.addEventListener(name, obj[name].bind(obj)); } } function opacityToHex(opacity) { return Math.round(Math.min(255, Math.max(1, 255 * opacity))).toString(16).padStart(2, "0"); } class IdManager { #id = 0; getId() { return `${_util.AnnotationEditorPrefix}${this.#id++}`; } } class CommandManager { #commands = []; #locked = false; #maxSize; #position = -1; constructor(maxSize = 128) { this.#maxSize = maxSize; } add({ cmd, undo, mustExec, type = NaN, overwriteIfSameType = false, keepUndo = false }) { if (mustExec) { cmd(); } if (this.#locked) { return; } const save = { cmd, undo, type }; if (this.#position === -1) { if (this.#commands.length > 0) { this.#commands.length = 0; } this.#position = 0; this.#commands.push(save); return; } if (overwriteIfSameType && this.#commands[this.#position].type === type) { if (keepUndo) { save.undo = this.#commands[this.#position].undo; } this.#commands[this.#position] = save; return; } const next = this.#position + 1; if (next === this.#maxSize) { this.#commands.splice(0, 1); } else { this.#position = next; if (next < this.#commands.length) { this.#commands.splice(next); } } this.#commands.push(save); } undo() { if (this.#position === -1) { return; } this.#locked = true; this.#commands[this.#position].undo(); this.#locked = false; this.#position -= 1; } redo() { if (this.#position < this.#commands.length - 1) { this.#position += 1; this.#locked = true; this.#commands[this.#position].cmd(); this.#locked = false; } } hasSomethingToUndo() { return this.#position !== -1; } hasSomethingToRedo() { return this.#position < this.#commands.length - 1; } destroy() { this.#commands = null; } } exports.CommandManager = CommandManager; class KeyboardManager { constructor(callbacks) { this.buffer = []; this.callbacks = new Map(); this.allKeys = new Set(); const isMac = KeyboardManager.platform.isMac; for (const [keys, callback] of callbacks) { for (const key of keys) { const isMacKey = key.startsWith("mac+"); if (isMac && isMacKey) { this.callbacks.set(key.slice(4), callback); this.allKeys.add(key.split("+").at(-1)); } else if (!isMac && !isMacKey) { this.callbacks.set(key, callback); this.allKeys.add(key.split("+").at(-1)); } } } } static get platform() { const platform = typeof navigator !== "undefined" ? navigator.platform : ""; return (0, _util.shadow)(this, "platform", { isWin: platform.includes("Win"), isMac: platform.includes("Mac") }); } #serialize(event) { if (event.altKey) { this.buffer.push("alt"); } if (event.ctrlKey) { this.buffer.push("ctrl"); } if (event.metaKey) { this.buffer.push("meta"); } if (event.shiftKey) { this.buffer.push("shift"); } this.buffer.push(event.key); const str = this.buffer.join("+"); this.buffer.length = 0; return str; } exec(self, event) { if (!this.allKeys.has(event.key)) { return; } const callback = this.callbacks.get(this.#serialize(event)); if (!callback) { return; } callback.bind(self)(); event.stopPropagation(); event.preventDefault(); } } exports.KeyboardManager = KeyboardManager; class ClipboardManager { #elements = null; copy(element) { if (!element) { return; } if (Array.isArray(element)) { this.#elements = element.map(el => el.serialize()); } else { this.#elements = [element.serialize()]; } this.#elements = this.#elements.filter(el => !!el); if (this.#elements.length === 0) { this.#elements = null; } } paste() { return this.#elements; } isEmpty() { return this.#elements === null; } destroy() { this.#elements = null; } } class ColorManager { static _colorsMapping = new Map([["CanvasText", [0, 0, 0]], ["Canvas", [255, 255, 255]]]); get _colors() { if (typeof document === "undefined") { return (0, _util.shadow)(this, "_colors", ColorManager._colorsMapping); } const colors = new Map([["CanvasText", null], ["Canvas", null]]); (0, _display_utils.getColorValues)(colors); return (0, _util.shadow)(this, "_colors", colors); } convert(color) { const rgb = (0, _display_utils.getRGB)(color); if (!window.matchMedia("(forced-colors: active)").matches) { return rgb; } for (const [name, RGB] of this._colors) { if (RGB.every((x, i) => x === rgb[i])) { return ColorManager._colorsMapping.get(name); } } return rgb; } getHexCode(name) { const rgb = this._colors.get(name); if (!rgb) { return name; } return _util.Util.makeHexColor(...rgb); } } exports.ColorManager = ColorManager; class AnnotationEditorUIManager { #activeEditor = null; #allEditors = new Map(); #allLayers = new Map(); #clipboardManager = new ClipboardManager(); #commandManager = new CommandManager(); #currentPageIndex = 0; #editorTypes = null; #eventBus = null; #idManager = new IdManager(); #isEnabled = false; #mode = _util.AnnotationEditorType.NONE; #selectedEditors = new Set(); #boundKeydown = this.keydown.bind(this); #boundOnEditingAction = this.onEditingAction.bind(this); #boundOnPageChanging = this.onPageChanging.bind(this); #previousStates = { isEditing: false, isEmpty: true, hasEmptyClipboard: true, hasSomethingToUndo: false, hasSomethingToRedo: false, hasSelectedEditor: false }; #container = null; static _keyboardManager = new KeyboardManager([[["ctrl+a", "mac+meta+a"], AnnotationEditorUIManager.prototype.selectAll], [["ctrl+c", "mac+meta+c"], AnnotationEditorUIManager.prototype.copy], [["ctrl+v", "mac+meta+v"], AnnotationEditorUIManager.prototype.paste], [["ctrl+x", "mac+meta+x"], AnnotationEditorUIManager.prototype.cut], [["ctrl+z", "mac+meta+z"], AnnotationEditorUIManager.prototype.undo], [["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"], AnnotationEditorUIManager.prototype.redo], [["Backspace", "alt+Backspace", "ctrl+Backspace", "shift+Backspace", "mac+Backspace", "mac+alt+Backspace", "mac+ctrl+Backspace", "Delete", "ctrl+Delete", "shift+Delete"], AnnotationEditorUIManager.prototype.delete], [["Escape", "mac+Escape"], AnnotationEditorUIManager.prototype.unselectAll]]); constructor(container, eventBus) { this.#container = container; this.#eventBus = eventBus; this.#eventBus._on("editingaction", this.#boundOnEditingAction); this.#eventBus._on("pagechanging", this.#boundOnPageChanging); } destroy() { this.#removeKeyboardManager(); this.#eventBus._off("editingaction", this.#boundOnEditingAction); this.#eventBus._off("pagechanging", this.#boundOnPageChanging); for (const layer of this.#allLayers.values()) { layer.destroy(); } this.#allLayers.clear(); this.#allEditors.clear(); this.#activeEditor = null; this.#selectedEditors.clear(); this.#clipboardManager.destroy(); this.#commandManager.destroy(); } onPageChanging({ pageNumber }) { this.#currentPageIndex = pageNumber - 1; } focusMainContainer() { this.#container.focus(); } #addKeyboardManager() { this.#container.addEventListener("keydown", this.#boundKeydown); } #removeKeyboardManager() { this.#container.removeEventListener("keydown", this.#boundKeydown); } keydown(event) { if (!this.getActive()?.shouldGetKeyboardEvents()) { AnnotationEditorUIManager._keyboardManager.exec(this, event); } } onEditingAction(details) { if (["undo", "redo", "cut", "copy", "paste", "delete", "selectAll"].includes(details.name)) { this[details.name](); } } #dispatchUpdateStates(details) { const hasChanged = Object.entries(details).some(([key, value]) => this.#previousStates[key] !== value); if (hasChanged) { this.#eventBus.dispatch("annotationeditorstateschanged", { source: this, details: Object.assign(this.#previousStates, details) }); } } #dispatchUpdateUI(details) { this.#eventBus.dispatch("annotationeditorparamschanged", { source: this, details }); } setEditingState(isEditing) { if (isEditing) { this.#addKeyboardManager(); this.#dispatchUpdateStates({ isEditing: this.#mode !== _util.AnnotationEditorType.NONE, isEmpty: this.#isEmpty(), hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(), hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(), hasSelectedEditor: false, hasEmptyClipboard: this.#clipboardManager.isEmpty() }); } else { this.#removeKeyboardManager(); this.#dispatchUpdateStates({ isEditing: false }); } } registerEditorTypes(types) { this.#editorTypes = types; for (const editorType of this.#editorTypes) { this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate); } } getId() { return this.#idManager.getId(); } addLayer(layer) { this.#allLayers.set(layer.pageIndex, layer); if (this.#isEnabled) { layer.enable(); } else { layer.disable(); } } removeLayer(layer) { this.#allLayers.delete(layer.pageIndex); } updateMode(mode) { this.#mode = mode; if (mode === _util.AnnotationEditorType.NONE) { this.setEditingState(false); this.#disableAll(); } else { this.setEditingState(true); this.#enableAll(); for (const layer of this.#allLayers.values()) { layer.updateMode(mode); } } } updateToolbar(mode) { if (mode === this.#mode) { return; } this.#eventBus.dispatch("switchannotationeditormode", { source: this, mode }); } updateParams(type, value) { for (const editor of this.#selectedEditors) { editor.updateParams(type, value); } for (const editorType of this.#editorTypes) { editorType.updateDefaultParams(type, value); } } #enableAll() { if (!this.#isEnabled) { this.#isEnabled = true; for (const layer of this.#allLayers.values()) { layer.enable(); } } } #disableAll() { this.unselectAll(); if (this.#isEnabled) { this.#isEnabled = false; for (const layer of this.#allLayers.values()) { layer.disable(); } } } getEditors(pageIndex) { const editors = []; for (const editor of this.#allEditors.values()) { if (editor.pageIndex === pageIndex) { editors.push(editor); } } return editors; } getEditor(id) { return this.#allEditors.get(id); } addEditor(editor) { this.#allEditors.set(editor.id, editor); } removeEditor(editor) { this.#allEditors.delete(editor.id); this.unselect(editor); } #addEditorToLayer(editor) { const layer = this.#allLayers.get(editor.pageIndex); if (layer) { layer.addOrRebuild(editor); } else { this.addEditor(editor); } } setActiveEditor(editor) { if (this.#activeEditor === editor) { return; } this.#activeEditor = editor; if (editor) { this.#dispatchUpdateUI(editor.propertiesToUpdate); } } toggleSelected(editor) { if (this.#selectedEditors.has(editor)) { this.#selectedEditors.delete(editor); editor.unselect(); this.#dispatchUpdateStates({ hasSelectedEditor: this.hasSelection }); return; } this.#selectedEditors.add(editor); editor.select(); this.#dispatchUpdateUI(editor.propertiesToUpdate); this.#dispatchUpdateStates({ hasSelectedEditor: true }); } setSelected(editor) { for (const ed of this.#selectedEditors) { if (ed !== editor) { ed.unselect(); } } this.#selectedEditors.clear(); this.#selectedEditors.add(editor); editor.select(); this.#dispatchUpdateUI(editor.propertiesToUpdate); this.#dispatchUpdateStates({ hasSelectedEditor: true }); } isSelected(editor) { return this.#selectedEditors.has(editor); } unselect(editor) { editor.unselect(); this.#selectedEditors.delete(editor); this.#dispatchUpdateStates({ hasSelectedEditor: this.hasSelection }); } get hasSelection() { return this.#selectedEditors.size !== 0; } undo() { this.#commandManager.undo(); this.#dispatchUpdateStates({ hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(), hasSomethingToRedo: true, isEmpty: this.#isEmpty() }); } redo() { this.#commandManager.redo(); this.#dispatchUpdateStates({ hasSomethingToUndo: true, hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(), isEmpty: this.#isEmpty() }); } addCommands(params) { this.#commandManager.add(params); this.#dispatchUpdateStates({ hasSomethingToUndo: true, hasSomethingToRedo: false, isEmpty: this.#isEmpty() }); } #isEmpty() { if (this.#allEditors.size === 0) { return true; } if (this.#allEditors.size === 1) { for (const editor of this.#allEditors.values()) { return editor.isEmpty(); } } return false; } delete() { if (this.#activeEditor) { this.#activeEditor.commitOrRemove(); } if (!this.hasSelection) { return; } const editors = [...this.#selectedEditors]; const cmd = () => { for (const editor of editors) { editor.remove(); } }; const undo = () => { for (const editor of editors) { this.#addEditorToLayer(editor); } }; this.addCommands({ cmd, undo, mustExec: true }); } copy() { if (this.#activeEditor) { this.#activeEditor.commitOrRemove(); } if (this.hasSelection) { const editors = []; for (const editor of this.#selectedEditors) { if (!editor.isEmpty()) { editors.push(editor); } } if (editors.length === 0) { return; } this.#clipboardManager.copy(editors); this.#dispatchUpdateStates({ hasEmptyClipboard: false }); } } cut() { this.copy(); this.delete(); } paste() { if (this.#clipboardManager.isEmpty()) { return; } this.unselectAll(); const layer = this.#allLayers.get(this.#currentPageIndex); const newEditors = this.#clipboardManager.paste().map(data => layer.deserialize(data)); const cmd = () => { for (const editor of newEditors) { this.#addEditorToLayer(editor); } this.#selectEditors(newEditors); }; const undo = () => { for (const editor of newEditors) { editor.remove(); } }; this.addCommands({ cmd, undo, mustExec: true }); } #selectEditors(editors) { this.#selectedEditors.clear(); for (const editor of editors) { if (editor.isEmpty()) { continue; } this.#selectedEditors.add(editor); editor.select(); } this.#dispatchUpdateStates({ hasSelectedEditor: true }); } selectAll() { for (const editor of this.#selectedEditors) { editor.commit(); } this.#selectEditors(this.#allEditors.values()); } unselectAll() { if (this.#activeEditor) { this.#activeEditor.commitOrRemove(); return; } if (this.#selectEditors.size === 0) { return; } for (const editor of this.#selectedEditors) { editor.unselect(); } this.#selectedEditors.clear(); this.#dispatchUpdateStates({ hasSelectedEditor: false }); } isActive(editor) { return this.#activeEditor === editor; } getActive() { return this.#activeEditor; } getMode() { return this.#mode; } } exports.AnnotationEditorUIManager = AnnotationEditorUIManager;