Merge pull request #15373 from calixteman/copy_paste
[Editor] Use the global clipboard for the copy/paste/cut operations
This commit is contained in:
commit
e9bdbe4574
@ -22,6 +22,7 @@ import {
|
|||||||
AnnotationEditorType,
|
AnnotationEditorType,
|
||||||
shadow,
|
shadow,
|
||||||
Util,
|
Util,
|
||||||
|
warn,
|
||||||
} from "../../shared/util.js";
|
} from "../../shared/util.js";
|
||||||
import { getColorValues, getRGB } from "../display_utils.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<AnnotationEditor>} 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 {
|
class ColorManager {
|
||||||
static _colorsMapping = new Map([
|
static _colorsMapping = new Map([
|
||||||
["CanvasText", [0, 0, 0]],
|
["CanvasText", [0, 0, 0]],
|
||||||
@ -404,8 +358,6 @@ class AnnotationEditorUIManager {
|
|||||||
|
|
||||||
#allLayers = new Map();
|
#allLayers = new Map();
|
||||||
|
|
||||||
#clipboardManager = new ClipboardManager();
|
|
||||||
|
|
||||||
#commandManager = new CommandManager();
|
#commandManager = new CommandManager();
|
||||||
|
|
||||||
#currentPageIndex = 0;
|
#currentPageIndex = 0;
|
||||||
@ -422,6 +374,12 @@ class AnnotationEditorUIManager {
|
|||||||
|
|
||||||
#selectedEditors = new Set();
|
#selectedEditors = new Set();
|
||||||
|
|
||||||
|
#boundCopy = this.copy.bind(this);
|
||||||
|
|
||||||
|
#boundCut = this.cut.bind(this);
|
||||||
|
|
||||||
|
#boundPaste = this.paste.bind(this);
|
||||||
|
|
||||||
#boundKeydown = this.keydown.bind(this);
|
#boundKeydown = this.keydown.bind(this);
|
||||||
|
|
||||||
#boundOnEditingAction = this.onEditingAction.bind(this);
|
#boundOnEditingAction = this.onEditingAction.bind(this);
|
||||||
@ -431,7 +389,6 @@ class AnnotationEditorUIManager {
|
|||||||
#previousStates = {
|
#previousStates = {
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
isEmpty: true,
|
isEmpty: true,
|
||||||
hasEmptyClipboard: true,
|
|
||||||
hasSomethingToUndo: false,
|
hasSomethingToUndo: false,
|
||||||
hasSomethingToRedo: false,
|
hasSomethingToRedo: false,
|
||||||
hasSelectedEditor: false,
|
hasSelectedEditor: false,
|
||||||
@ -441,9 +398,6 @@ class AnnotationEditorUIManager {
|
|||||||
|
|
||||||
static _keyboardManager = new KeyboardManager([
|
static _keyboardManager = new KeyboardManager([
|
||||||
[["ctrl+a", "mac+meta+a"], AnnotationEditorUIManager.prototype.selectAll],
|
[["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+z", "mac+meta+z"], AnnotationEditorUIManager.prototype.undo],
|
||||||
[
|
[
|
||||||
["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"],
|
["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"],
|
||||||
@ -485,7 +439,6 @@ class AnnotationEditorUIManager {
|
|||||||
this.#allEditors.clear();
|
this.#allEditors.clear();
|
||||||
this.#activeEditor = null;
|
this.#activeEditor = null;
|
||||||
this.#selectedEditors.clear();
|
this.#selectedEditors.clear();
|
||||||
this.#clipboardManager.destroy();
|
|
||||||
this.#commandManager.destroy();
|
this.#commandManager.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,6 +460,109 @@ class AnnotationEditorUIManager {
|
|||||||
this.#container.removeEventListener("keydown", this.#boundKeydown);
|
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.
|
* Keydown callback.
|
||||||
* @param {KeyboardEvent} event
|
* @param {KeyboardEvent} event
|
||||||
@ -534,8 +590,8 @@ class AnnotationEditorUIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the different possible states of this manager, e.g. is the clipboard
|
* Update the different possible states of this manager, e.g. is there
|
||||||
* empty or is there something to undo, ...
|
* something to undo, redo, ...
|
||||||
* @param {Object} details
|
* @param {Object} details
|
||||||
*/
|
*/
|
||||||
#dispatchUpdateStates(details) {
|
#dispatchUpdateStates(details) {
|
||||||
@ -567,16 +623,17 @@ class AnnotationEditorUIManager {
|
|||||||
setEditingState(isEditing) {
|
setEditingState(isEditing) {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
this.#addKeyboardManager();
|
this.#addKeyboardManager();
|
||||||
|
this.#addCopyPasteListeners();
|
||||||
this.#dispatchUpdateStates({
|
this.#dispatchUpdateStates({
|
||||||
isEditing: this.#mode !== AnnotationEditorType.NONE,
|
isEditing: this.#mode !== AnnotationEditorType.NONE,
|
||||||
isEmpty: this.#isEmpty(),
|
isEmpty: this.#isEmpty(),
|
||||||
hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(),
|
hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(),
|
||||||
hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(),
|
hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(),
|
||||||
hasSelectedEditor: false,
|
hasSelectedEditor: false,
|
||||||
hasEmptyClipboard: this.#clipboardManager.isEmpty(),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.#removeKeyboardManager();
|
this.#removeKeyboardManager();
|
||||||
|
this.#removeCopyPasteListeners();
|
||||||
this.#dispatchUpdateStates({
|
this.#dispatchUpdateStates({
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
});
|
});
|
||||||
@ -909,68 +966,6 @@ class AnnotationEditorUIManager {
|
|||||||
this.addCommands({ cmd, undo, mustExec: true });
|
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.
|
* Select the editors.
|
||||||
* @param {Array<AnnotationEditor>} editors
|
* @param {Array<AnnotationEditor>} editors
|
||||||
|
Loading…
x
Reference in New Issue
Block a user