|
@@ -0,0 +1,754 @@
|
|
|
+import { shadow } from "./ui_utils.js";
|
|
|
+import { AnnotationEditorType } from "../constants"
|
|
|
+/**
|
|
|
+ * Class to handle the different keyboards shortcuts we can have on mac or
|
|
|
+ * non-mac OSes.
|
|
|
+ */
|
|
|
+class KeyboardManager {
|
|
|
+ /**
|
|
|
+ * Create a new keyboard manager class.
|
|
|
+ * @param {Array<Array>} callbacks - an array containing an array of shortcuts
|
|
|
+ * and a callback to call.
|
|
|
+ * A shortcut is a string like `ctrl+c` or `mac+ctrl+c` for mac OS.
|
|
|
+ */
|
|
|
+ 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 shadow(this, "platform", {
|
|
|
+ isWin: platform.includes("Win"),
|
|
|
+ isMac: platform.includes("Mac"),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Serialize an event into a string in order to match a
|
|
|
+ * potential key for a callback.
|
|
|
+ * @param {KeyboardEvent} event
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ _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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Execute a callback, if any, for a given keyboard event.
|
|
|
+ * The self is used as `this` in the callback.
|
|
|
+ * @param {Object} self.
|
|
|
+ * @param {KeyboardEvent} event
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert a number between 0 and 100 into an hex number between 0 and 255.
|
|
|
+ * @param {number} opacity
|
|
|
+ * @return {string}
|
|
|
+ */
|
|
|
+ function opacityToHex(opacity) {
|
|
|
+ return Math.round(Math.min(255, Math.max(1, 255 * opacity)))
|
|
|
+ .toString(16)
|
|
|
+ .padStart(2, "0");
|
|
|
+}
|
|
|
+
|
|
|
+const AnnotationEditorPrefix = "pdfjs_internal_editor_";
|
|
|
+/**
|
|
|
+ * Class to create some unique ids for the different editors.
|
|
|
+ */
|
|
|
+class IdManager {
|
|
|
+ #id = 0;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get a unique id.
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ getId() {
|
|
|
+ return `${AnnotationEditorPrefix}${this.#id++}`;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * A pdf has several pages and each of them when it will rendered
|
|
|
+ * will have an AnnotationEditorLayer which will contain the some
|
|
|
+ * new Annotations associated to an editor in order to modify them.
|
|
|
+ *
|
|
|
+ * This class is used to manage all the different layers, editors and
|
|
|
+ * some action like copy/paste, undo/redo, ...
|
|
|
+ */
|
|
|
+class AnnotationManager {
|
|
|
+
|
|
|
+ constructor(container, eventBus) {
|
|
|
+
|
|
|
+ this._activeEditor = null;
|
|
|
+
|
|
|
+ this._allEditors = new Map();
|
|
|
+
|
|
|
+ this._allLayers = new Map();
|
|
|
+
|
|
|
+ this._currentPageIndex = 0;
|
|
|
+
|
|
|
+ this._editorTypes = null;
|
|
|
+
|
|
|
+ this._isEnabled = false;
|
|
|
+
|
|
|
+ this._mode = AnnotationEditorType.NONE;
|
|
|
+
|
|
|
+ this._idManager = new IdManager();
|
|
|
+ this._selectedEditors = new Set();
|
|
|
+
|
|
|
+ this._boundCopy = this.copy.bind(this);
|
|
|
+
|
|
|
+ this._boundCut = this.cut.bind(this);
|
|
|
+
|
|
|
+ this._boundPaste = this.paste.bind(this);
|
|
|
+
|
|
|
+ this._boundKeydown = this.keydown.bind(this);
|
|
|
+
|
|
|
+ this._boundOnEditingAction = this.onEditingAction.bind(this);
|
|
|
+
|
|
|
+ this._boundOnPageChanging = this.onPageChanging.bind(this);
|
|
|
+
|
|
|
+ this._previousStates = {
|
|
|
+ isEditing: false,
|
|
|
+ isEmpty: true,
|
|
|
+ hasSomethingToUndo: false,
|
|
|
+ hasSomethingToRedo: false,
|
|
|
+ hasSelectedEditor: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ this._keyboardManager = new KeyboardManager([
|
|
|
+ [
|
|
|
+ [
|
|
|
+ "Backspace",
|
|
|
+ "alt+Backspace",
|
|
|
+ "ctrl+Backspace",
|
|
|
+ "shift+Backspace",
|
|
|
+ "mac+Backspace",
|
|
|
+ "mac+alt+Backspace",
|
|
|
+ "mac+ctrl+Backspace",
|
|
|
+ "Delete",
|
|
|
+ "ctrl+Delete",
|
|
|
+ "shift+Delete",
|
|
|
+ ],
|
|
|
+ AnnotationManager.prototype.delete,
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ removeAllAnnotations() {
|
|
|
+ for (const layer of this._allLayers.values()) {
|
|
|
+ layer.removeAllAnnotations();
|
|
|
+ }
|
|
|
+ this._allEditors.clear();
|
|
|
+ this._activeEditor = null;
|
|
|
+ this._selectedEditors.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ onPageChanging({ pageNumber }) {
|
|
|
+ this._currentPageIndex = pageNumber - 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ focusMainContainer() {
|
|
|
+ this._container.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ _addKeyboardManager() {
|
|
|
+ // The keyboard events are caught at the container level in order to be able
|
|
|
+ // to execute some callbacks even if the current page doesn't have focus.
|
|
|
+ // this._container.addEventListener("keydown", this._boundKeydown);
|
|
|
+ }
|
|
|
+
|
|
|
+ _removeKeyboardManager() {
|
|
|
+ this._container.removeEventListener("keydown", this._boundKeydown);
|
|
|
+ }
|
|
|
+
|
|
|
+ _addCopyPasteListeners() {
|
|
|
+ document.addEventListener("copy", this._boundCopy);
|
|
|
+ document.addEventListener("cut", this._boundCut);
|
|
|
+ document.addEventListener("paste", this._boundPaste);
|
|
|
+ }
|
|
|
+
|
|
|
+ _removeCopyPasteListeners() {
|
|
|
+ document.removeEventListener("copy", this._boundCopy);
|
|
|
+ document.removeEventListener("cut", this._boundCut);
|
|
|
+ document.removeEventListener("paste", this._boundPaste);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Copy callback.
|
|
|
+ * @param {ClipboardEvent} event
|
|
|
+ */
|
|
|
+ copy(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ if (this._activeEditor) {
|
|
|
+ // An editor is being edited so just commit it.
|
|
|
+ this._activeEditor.commitOrRemove();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.hasSelection) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const editors = [];
|
|
|
+ for (const editor of this._selectedEditors) {
|
|
|
+ if (!editor.isEmpty()) {
|
|
|
+ editors.push(editor.serialize());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (editors.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ event.clipboardData.setData("application/pdfjs", JSON.stringify(editors));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cut callback.
|
|
|
+ * @param {ClipboardEvent} event
|
|
|
+ */
|
|
|
+ cut(event) {
|
|
|
+ this.copy(event);
|
|
|
+ this.delete();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Paste callback.
|
|
|
+ * @param {ClipboardEvent} event
|
|
|
+ */
|
|
|
+ paste(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ let data = event.clipboardData.getData("application/pdfjs");
|
|
|
+ if (!data) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ data = JSON.parse(data);
|
|
|
+ } catch (ex) {
|
|
|
+ console.warn(`paste: "${ex.message}".`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Array.isArray(data)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.unselectAll();
|
|
|
+ const layer = this._allLayers.get(this._currentPageIndex);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const newEditors = [];
|
|
|
+ for (const editor of data) {
|
|
|
+ const deserializedEditor = layer.deserialize(editor);
|
|
|
+ if (!deserializedEditor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ newEditors.push(deserializedEditor);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const editor of newEditors) {
|
|
|
+ this._addEditorToLayer(editor);
|
|
|
+ }
|
|
|
+ this._selectEditors(newEditors);
|
|
|
+ } catch (ex) {
|
|
|
+ console.warn(`paste: "${ex.message}".`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Keydown callback.
|
|
|
+ * @param {KeyboardEvent} event
|
|
|
+ */
|
|
|
+ keydown(event) {
|
|
|
+ if (!this.getActive()?.shouldGetKeyboardEvents()) {
|
|
|
+ this._keyboardManager.exec(this, event);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Execute an action for a given name.
|
|
|
+ * For example, the user can click on the "Undo" entry in the context menu
|
|
|
+ * and it'll trigger the undo action.
|
|
|
+ * @param {Object} details
|
|
|
+ */
|
|
|
+ onEditingAction(details) {
|
|
|
+ if (["delete"].includes(details.name)) {
|
|
|
+ this[details.name]();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update the different possible states of this manager, e.g. is there
|
|
|
+ * something to undo, redo, ...
|
|
|
+ * @param {Object} details
|
|
|
+ */
|
|
|
+ _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,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set the editing state.
|
|
|
+ * It can be useful to temporarily disable it when the user is editing a
|
|
|
+ * FreeText annotation.
|
|
|
+ * @param {boolean} isEditing
|
|
|
+ */
|
|
|
+ setEditingState(isEditing) {
|
|
|
+ if (isEditing) {
|
|
|
+ this._addKeyboardManager();
|
|
|
+ this._addCopyPasteListeners();
|
|
|
+ this._dispatchUpdateStates({
|
|
|
+ isEditing: this._mode !== AnnotationEditorType.NONE,
|
|
|
+ isEmpty: this._isEmpty(),
|
|
|
+ hasSelectedEditor: false,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ this._removeKeyboardManager();
|
|
|
+ this._removeCopyPasteListeners();
|
|
|
+ this._dispatchUpdateStates({
|
|
|
+ isEditing: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ registerEditorTypes(types) {
|
|
|
+ if (this._editorTypes) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this._editorTypes = types;
|
|
|
+ for (const editorType of this._editorTypes) {
|
|
|
+ this._dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get an id.
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ getId() {
|
|
|
+ return this._idManager.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add a new layer for a page which will contains the editors.
|
|
|
+ * @param {AnnotationEditorLayer} layer
|
|
|
+ */
|
|
|
+ addLayer(layer) {
|
|
|
+ this._allLayers.set(layer.pageIndex, layer);
|
|
|
+ if (this._isEnabled) {
|
|
|
+ layer.enable();
|
|
|
+ } else {
|
|
|
+ layer.disable();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Remove a layer.
|
|
|
+ * @param {AnnotationEditorLayer} layer
|
|
|
+ */
|
|
|
+ removeLayer(layer) {
|
|
|
+ this._allLayers.delete(layer.pageIndex);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Change the editor mode (None, FreeText, Ink, ...)
|
|
|
+ * @param {number} mode
|
|
|
+ */
|
|
|
+ updateMode(mode) {
|
|
|
+ this._mode = mode;
|
|
|
+ if (mode === AnnotationEditorType.NONE) {
|
|
|
+ this.setEditingState(false);
|
|
|
+ this._disableAll();
|
|
|
+ } else {
|
|
|
+ this.setEditingState(true);
|
|
|
+ this._enableAll();
|
|
|
+ for (const layer of this._allLayers.values()) {
|
|
|
+ layer.updateMode(mode);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update the toolbar if it's required to reflect the tool currently used.
|
|
|
+ * @param {number} mode
|
|
|
+ * @returns {undefined}
|
|
|
+ */
|
|
|
+ updateToolbar(mode) {
|
|
|
+ if (mode === this._mode) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this._eventBus.dispatch("switchannotationeditormode", {
|
|
|
+ source: this,
|
|
|
+ mode,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update a parameter in the current editor or globally.
|
|
|
+ * @param {number} type
|
|
|
+ * @param {*} value
|
|
|
+ */
|
|
|
+ updateParams(type, value) {
|
|
|
+ if (!this._editorTypes) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const editor of this._selectedEditors) {
|
|
|
+ editor.updateParams(type, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const editorType of this._editorTypes) {
|
|
|
+ editorType.updateDefaultParams(type, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Enable all the layers.
|
|
|
+ */
|
|
|
+ _enableAll() {
|
|
|
+ if (!this._isEnabled) {
|
|
|
+ this._isEnabled = true;
|
|
|
+ for (const layer of this._allLayers.values()) {
|
|
|
+ layer.enable();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Disable all the layers.
|
|
|
+ */
|
|
|
+ _disableAll() {
|
|
|
+ this.unselectAll();
|
|
|
+ if (this._isEnabled) {
|
|
|
+ this._isEnabled = false;
|
|
|
+ for (const layer of this._allLayers.values()) {
|
|
|
+ layer.disable();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get all the editors belonging to a give page.
|
|
|
+ * @param {number} pageIndex
|
|
|
+ * @returns {Array<AnnotationEditor>}
|
|
|
+ */
|
|
|
+ getEditors(pageIndex) {
|
|
|
+ const editors = [];
|
|
|
+ for (const editor of this._allEditors.values()) {
|
|
|
+ if (editor.pageIndex === pageIndex) {
|
|
|
+ editors.push(editor);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return editors;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get an editor with the given id.
|
|
|
+ * @param {string} id
|
|
|
+ * @returns {AnnotationEditor}
|
|
|
+ */
|
|
|
+ getEditor(id) {
|
|
|
+ return this._allEditors.get(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add a new editor.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ addEditor(editor) {
|
|
|
+ this._allEditors.set(editor.id, editor);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Remove an editor.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ removeEditor(editor) {
|
|
|
+ this._allEditors.delete(editor.id);
|
|
|
+ this.unselect(editor);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add an editor to the layer it belongs to or add it to the global map.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ _addEditorToLayer(editor) {
|
|
|
+ const layer = this._allLayers.get(editor.pageIndex);
|
|
|
+ if (layer) {
|
|
|
+ layer.addOrRebuild(editor);
|
|
|
+ } else {
|
|
|
+ this.addEditor(editor);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set the given editor as the active one.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ setActiveEditor(editor) {
|
|
|
+ if (this._activeEditor === editor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this._activeEditor = editor;
|
|
|
+ if (editor) {
|
|
|
+ this._dispatchUpdateUI(editor.propertiesToUpdate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add or remove an editor the current selection.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ 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,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set the last selected editor.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ 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,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if the editor is selected.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ isSelected(editor) {
|
|
|
+ return this._selectedEditors.has(editor);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Unselect an editor.
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ */
|
|
|
+ unselect(editor) {
|
|
|
+ editor.unselect();
|
|
|
+ this._selectedEditors.delete(editor);
|
|
|
+ this._dispatchUpdateStates({
|
|
|
+ hasSelectedEditor: this.hasSelection,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ get hasSelection() {
|
|
|
+ return this._selectedEditors.size !== 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ _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 the current editor or all.
|
|
|
+ */
|
|
|
+ delete() {
|
|
|
+ this.commitOrRemove();
|
|
|
+ const editors = [...this._selectedEditors];
|
|
|
+ for (const editor of editors) {
|
|
|
+ editor.remove();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ commitOrRemove() {
|
|
|
+ // An editor is being edited so just commit it.
|
|
|
+ this._activeEditor?.commitOrRemove();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Select the editors.
|
|
|
+ * @param {Array<AnnotationEditor>} editors
|
|
|
+ */
|
|
|
+ _selectEditors(editors) {
|
|
|
+ this._selectedEditors.clear();
|
|
|
+ for (const editor of editors) {
|
|
|
+ if (editor.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this._selectedEditors.add(editor);
|
|
|
+ editor.select();
|
|
|
+ }
|
|
|
+ this._dispatchUpdateStates({ hasSelectedEditor: true });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Select all the editors.
|
|
|
+ */
|
|
|
+ selectAll() {
|
|
|
+ for (const editor of this._selectedEditors) {
|
|
|
+ editor.commit();
|
|
|
+ }
|
|
|
+ this._selectEditors(this._allEditors.values());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Unselect all the selected editors.
|
|
|
+ */
|
|
|
+ unselectAll() {
|
|
|
+ if (this._activeEditor) {
|
|
|
+ // An editor is being edited so just commit it.
|
|
|
+ this._activeEditor.commitOrRemove();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this._selectedEditors.size === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (const editor of this._selectedEditors) {
|
|
|
+ editor.unselect();
|
|
|
+ }
|
|
|
+ this._selectedEditors.clear();
|
|
|
+ this._dispatchUpdateStates({
|
|
|
+ hasSelectedEditor: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the current editor the one passed as argument?
|
|
|
+ * @param {AnnotationEditor} editor
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+ isActive(editor) {
|
|
|
+ return this._activeEditor === editor;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the current active editor.
|
|
|
+ * @returns {AnnotationEditor|null}
|
|
|
+ */
|
|
|
+ getActive() {
|
|
|
+ return this._activeEditor;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the current editor mode.
|
|
|
+ * @returns {number}
|
|
|
+ */
|
|
|
+ getMode() {
|
|
|
+ return this._mode;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export {
|
|
|
+ AnnotationManager,
|
|
|
+ KeyboardManager,
|
|
|
+ opacityToHex
|
|
|
+};
|