From 372d4d64e3472426680119540d10cd4c46509ab2 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 31 Aug 2022 14:35:28 +0200 Subject: [PATCH] [Editor] Use the global clipboard for the copy/paste/cut operations It slightly helps to reduce the code size and its complexity. But the cool thing is that it allows to copy/paste some anntations from a pdf to an other. --- src/display/editor/tools.js | 233 ++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 119 deletions(-) diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index d214d9eb32d8a..23766f11145d7 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -22,6 +22,7 @@ import { AnnotationEditorType, shadow, Util, + warn, } from "../../shared/util.js"; import { getColorValues, getRGB } from "../display_utils.js"; @@ -281,53 +282,6 @@ class KeyboardManager { } } -/** - * Basic clipboard to copy/paste some editors. - * It has to be used as a singleton. - */ -class ClipboardManager { - #elements = null; - - /** - * Copy an element. - * @param {AnnotationEditor|Array} element - */ - 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; - } - } - - /** - * Create a new element. - * @returns {AnnotationEditor|null} - */ - paste() { - return this.#elements; - } - - /** - * Check if the clipboard is empty. - * @returns {boolean} - */ - isEmpty() { - return this.#elements === null; - } - - destroy() { - this.#elements = null; - } -} - class ColorManager { static _colorsMapping = new Map([ ["CanvasText", [0, 0, 0]], @@ -404,8 +358,6 @@ class AnnotationEditorUIManager { #allLayers = new Map(); - #clipboardManager = new ClipboardManager(); - #commandManager = new CommandManager(); #currentPageIndex = 0; @@ -422,6 +374,12 @@ class AnnotationEditorUIManager { #selectedEditors = new Set(); + #boundCopy = this.copy.bind(this); + + #boundCut = this.cut.bind(this); + + #boundPaste = this.paste.bind(this); + #boundKeydown = this.keydown.bind(this); #boundOnEditingAction = this.onEditingAction.bind(this); @@ -431,7 +389,6 @@ class AnnotationEditorUIManager { #previousStates = { isEditing: false, isEmpty: true, - hasEmptyClipboard: true, hasSomethingToUndo: false, hasSomethingToRedo: false, hasSelectedEditor: false, @@ -441,9 +398,6 @@ class AnnotationEditorUIManager { 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"], @@ -485,7 +439,6 @@ class AnnotationEditorUIManager { this.#allEditors.clear(); this.#activeEditor = null; this.#selectedEditors.clear(); - this.#clipboardManager.destroy(); this.#commandManager.destroy(); } @@ -507,6 +460,109 @@ class AnnotationEditorUIManager { 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) { + 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); + } + + 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 }); + } catch (ex) { + warn(`paste: "${ex.message}".`); + } + } + /** * Keydown callback. * @param {KeyboardEvent} event @@ -534,8 +590,8 @@ class AnnotationEditorUIManager { } /** - * Update the different possible states of this manager, e.g. is the clipboard - * empty or is there something to undo, ... + * Update the different possible states of this manager, e.g. is there + * something to undo, redo, ... * @param {Object} details */ #dispatchUpdateStates(details) { @@ -567,16 +623,17 @@ class AnnotationEditorUIManager { setEditingState(isEditing) { if (isEditing) { this.#addKeyboardManager(); + this.#addCopyPasteListeners(); this.#dispatchUpdateStates({ isEditing: this.#mode !== AnnotationEditorType.NONE, isEmpty: this.#isEmpty(), hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(), hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(), hasSelectedEditor: false, - hasEmptyClipboard: this.#clipboardManager.isEmpty(), }); } else { this.#removeKeyboardManager(); + this.#removeCopyPasteListeners(); this.#dispatchUpdateStates({ isEditing: false, }); @@ -909,68 +966,6 @@ class AnnotationEditorUIManager { this.addCommands({ cmd, undo, mustExec: true }); } - /** - * Copy the selected editor. - */ - copy() { - if (this.#activeEditor) { - // An editor is being edited so just commit it. - 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 the selected editor. - */ - cut() { - this.copy(); - this.delete(); - } - - /** - * Paste a previously copied editor. - * @returns {undefined} - */ - 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 }); - } - /** * Select the editors. * @param {Array} editors