mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
[edition] Add a FreeText editor (#14970)
- add a basic UI to edit some text in a pdf; - an editor can be moved, suppressed, cut, copied, pasted, selected; - add an undo/redo manager.
This commit is contained in:
parent
1ac33c960d
commit
be1aa11986
28 changed files with 2321 additions and 18 deletions
|
@ -13,6 +13,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AnnotationEditor } from "./editor/editor.js";
|
||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||
import { objectFromMap } from "../shared/util.js";
|
||||
|
||||
|
@ -62,6 +63,14 @@ class AnnotationStorage {
|
|||
return this._storage.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value from the storage.
|
||||
* @param {string} key
|
||||
*/
|
||||
removeKey(key) {
|
||||
this._storage.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a given key
|
||||
*
|
||||
|
@ -123,7 +132,19 @@ class AnnotationStorage {
|
|||
* @ignore
|
||||
*/
|
||||
get serializable() {
|
||||
return this._storage.size > 0 ? this._storage : null;
|
||||
if (this._storage.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clone = new Map();
|
||||
for (const [key, value] of this._storage) {
|
||||
if (value instanceof AnnotationEditor) {
|
||||
clone.set(key, value.serialize());
|
||||
} else {
|
||||
clone.set(key, value);
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
432
src/display/editor/annotation_editor_layer.js
Normal file
432
src/display/editor/annotation_editor_layer.js
Normal file
|
@ -0,0 +1,432 @@
|
|||
/* 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 */
|
||||
|
||||
import { AnnotationEditorType, Util } from "../../shared/util.js";
|
||||
import { bindEvents, KeyboardManager } from "./tools.js";
|
||||
import { FreeTextEditor } from "./freetext.js";
|
||||
import { PixelsPerInch } from "../display_utils.js";
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
#boundClick;
|
||||
|
||||
#editors = new Map();
|
||||
|
||||
#uiManager;
|
||||
|
||||
static _l10nInitialized = false;
|
||||
|
||||
static _keyboardManager = new KeyboardManager([
|
||||
[["ctrl+a", "mac+meta+a"], AnnotationEditorLayer.prototype.selectAll],
|
||||
[["ctrl+c", "mac+meta+c"], AnnotationEditorLayer.prototype.copy],
|
||||
[["ctrl+v", "mac+meta+v"], AnnotationEditorLayer.prototype.paste],
|
||||
[["ctrl+x", "mac+meta+x"], AnnotationEditorLayer.prototype.cut],
|
||||
[["ctrl+z", "mac+meta+z"], AnnotationEditorLayer.prototype.undo],
|
||||
[
|
||||
["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"],
|
||||
AnnotationEditorLayer.prototype.redo,
|
||||
],
|
||||
[
|
||||
[
|
||||
"ctrl+Backspace",
|
||||
"mac+Backspace",
|
||||
"mac+ctrl+Backspace",
|
||||
"mac+alt+Backspace",
|
||||
],
|
||||
AnnotationEditorLayer.prototype.suppress,
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
if (!AnnotationEditorLayer._l10nInitialized) {
|
||||
AnnotationEditorLayer._l10nInitialized = true;
|
||||
FreeTextEditor.setL10n(options.l10n);
|
||||
}
|
||||
this.#uiManager = options.uiManager;
|
||||
this.annotationStorage = options.annotationStorage;
|
||||
this.pageIndex = options.pageIndex;
|
||||
this.div = options.div;
|
||||
this.#boundClick = this.click.bind(this);
|
||||
|
||||
for (const editor of this.#uiManager.getEditors(options.pageIndex)) {
|
||||
this.add(editor);
|
||||
}
|
||||
|
||||
this.#uiManager.addLayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some commands into the CommandManager (undo/redo stuff).
|
||||
* @param {function} cmd
|
||||
* @param {function} undo
|
||||
*/
|
||||
addCommands(cmd, undo) {
|
||||
this.#uiManager.addCommands(cmd, undo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last command.
|
||||
*/
|
||||
undo() {
|
||||
this.#uiManager.undo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last command.
|
||||
*/
|
||||
redo() {
|
||||
this.#uiManager.redo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress the selected editor or all editors.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
suppress() {
|
||||
this.#uiManager.suppress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the selected editor.
|
||||
*/
|
||||
copy() {
|
||||
this.#uiManager.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut the selected editor.
|
||||
*/
|
||||
cut() {
|
||||
this.#uiManager.cut(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a previously copied editor.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
paste() {
|
||||
this.#uiManager.paste(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all the editors.
|
||||
*/
|
||||
selectAll() {
|
||||
this.#uiManager.selectAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect all the editors.
|
||||
*/
|
||||
unselectAll() {
|
||||
this.#uiManager.unselectAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable pointer events on the main div in order to enable
|
||||
* editor creation.
|
||||
*/
|
||||
enable() {
|
||||
this.div.style.pointerEvents = "auto";
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable editor creation.
|
||||
*/
|
||||
disable() {
|
||||
this.div.style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current editor.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
setActiveEditor(editor) {
|
||||
if (editor) {
|
||||
this.unselectAll();
|
||||
this.div.removeEventListener("click", this.#boundClick);
|
||||
} else {
|
||||
this.#uiManager.allowClick = false;
|
||||
this.div.addEventListener("click", this.#boundClick);
|
||||
}
|
||||
this.#uiManager.setActiveEditor(editor);
|
||||
}
|
||||
|
||||
attach(editor) {
|
||||
this.#editors.set(editor.id, editor);
|
||||
}
|
||||
|
||||
detach(editor) {
|
||||
this.#editors.delete(editor.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
this.#uiManager.allowClick = true;
|
||||
this.div.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An editor can have a different parent, for example after having
|
||||
* being dragged and droped from a page to another.
|
||||
* @param {AnnotationEditor} editor
|
||||
* @returns {undefined}
|
||||
*/
|
||||
#changeParent(editor) {
|
||||
if (editor.parent === this) {
|
||||
return;
|
||||
}
|
||||
this.attach(editor);
|
||||
editor.pageIndex = this.pageIndex;
|
||||
editor.parent.detach(editor);
|
||||
editor.parent = this;
|
||||
if (editor.div && editor.isAttachedToDOM) {
|
||||
editor.div.remove();
|
||||
this.div.appendChild(editor.div);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
this.div.appendChild(div);
|
||||
editor.isAttachedToDOM = true;
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
this.addCommands(cmd, undo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouseclick callback.
|
||||
* @param {MouseEvent} event
|
||||
* @returns {undefined}
|
||||
*/
|
||||
click(event) {
|
||||
if (!this.#uiManager.allowClick) {
|
||||
this.#uiManager.allowClick = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.getNextId();
|
||||
const editor = this.#createNewEditor({
|
||||
parent: this,
|
||||
id,
|
||||
x: event.offsetX,
|
||||
y: event.offsetY,
|
||||
});
|
||||
if (editor) {
|
||||
this.addANewEditor(editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag callback.
|
||||
* @param {DragEvent} event
|
||||
* @returns {undefined}
|
||||
*/
|
||||
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();
|
||||
editor.setAt(
|
||||
event.clientX - rect.x - editor.mouseX,
|
||||
event.clientY - rect.y - editor.mouseY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dragover callback.
|
||||
* @param {DragEvent} event
|
||||
*/
|
||||
dragover(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keydown callback.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
keydown(event) {
|
||||
if (!this.#uiManager.getActive()?.shouldGetKeyboardEvents()) {
|
||||
AnnotationEditorLayer._keyboardManager.exec(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the main editor.
|
||||
*/
|
||||
destroy() {
|
||||
for (const editor of this.#editors.values()) {
|
||||
editor.isAttachedToDOM = false;
|
||||
editor.div.remove();
|
||||
editor.parent = null;
|
||||
this.div = null;
|
||||
}
|
||||
this.#editors.clear();
|
||||
this.#uiManager.removeLayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the main editor.
|
||||
* @param {Object} parameters
|
||||
*/
|
||||
render(parameters) {
|
||||
this.viewport = parameters.viewport;
|
||||
this.inverseViewportTransform = Util.inverseTransform(
|
||||
this.viewport.transform
|
||||
);
|
||||
bindEvents(this, this.div, ["dragover", "drop", "keydown"]);
|
||||
this.div.addEventListener("click", this.#boundClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the main editor.
|
||||
* @param {Object} parameters
|
||||
*/
|
||||
update(parameters) {
|
||||
const transform = Util.transform(
|
||||
parameters.viewport.transform,
|
||||
this.inverseViewportTransform
|
||||
);
|
||||
this.viewport = parameters.viewport;
|
||||
this.inverseViewportTransform = Util.inverseTransform(
|
||||
this.viewport.transform
|
||||
);
|
||||
for (const editor of this.#editors.values()) {
|
||||
editor.transform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale factor from the viewport.
|
||||
* @returns {number}
|
||||
*/
|
||||
get scaleFactor() {
|
||||
return this.viewport.scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the zoom factor.
|
||||
* @returns {number}
|
||||
*/
|
||||
get zoomFactor() {
|
||||
return this.viewport.scale / PixelsPerInch.PDF_TO_CSS_UNITS;
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditorLayer };
|
305
src/display/editor/editor.js
Normal file
305
src/display/editor/editor.js
Normal file
|
@ -0,0 +1,305 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
|
||||
|
||||
import { unreachable, Util } from "../../shared/util.js";
|
||||
import { bindEvents } from "./tools.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorParameters
|
||||
* @property {AnnotationEditorLayer} parent - the layer containing this editor
|
||||
* @property {string} id - editor id
|
||||
* @property {number} x - x-coordinate
|
||||
* @property {number} y - y-coordinate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for editors.
|
||||
*/
|
||||
class AnnotationEditor {
|
||||
#isInEditMode = false;
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorParameters} parameters
|
||||
*/
|
||||
constructor(parameters) {
|
||||
if (this.constructor === AnnotationEditor) {
|
||||
unreachable("Cannot initialize AnnotationEditor.");
|
||||
}
|
||||
|
||||
this.parent = parameters.parent;
|
||||
this.id = parameters.id;
|
||||
this.width = this.height = null;
|
||||
this.pageIndex = parameters.parent.pageIndex;
|
||||
this.name = parameters.name;
|
||||
this.div = null;
|
||||
this.x = Math.round(parameters.x);
|
||||
this.y = Math.round(parameters.y);
|
||||
|
||||
this.isAttachedToDOM = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* onfocus callback.
|
||||
*/
|
||||
focusin(/* event */) {
|
||||
this.parent.setActiveEditor(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* onblur callback.
|
||||
* @param {FocusEvent} event
|
||||
* @returns {undefined}
|
||||
*/
|
||||
focusout(event) {
|
||||
if (!this.isAttachedToDOM) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In case of focusout, the relatedTarget is the element which
|
||||
// is grabbing the focus.
|
||||
// So if the related target is an element under the div for this
|
||||
// editor, then the editor isn't unactive.
|
||||
const target = event.relatedTarget;
|
||||
if (target?.closest(`#${this.id}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (this.isEmpty()) {
|
||||
this.remove();
|
||||
} else {
|
||||
this.commit();
|
||||
}
|
||||
this.parent.setActiveEditor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pointer coordinates in order to correctly translate the
|
||||
* div in case of drag-and-drop.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
mousedown(event) {
|
||||
this.mouseX = event.offsetX;
|
||||
this.mouseY = event.offsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* We use drag-and-drop in order to move an editor on a page.
|
||||
* @param {DragEvent} event
|
||||
*/
|
||||
dragstart(event) {
|
||||
event.dataTransfer.setData("text/plain", this.id);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the editor position within its parent.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
setAt(x, y) {
|
||||
this.x = Math.round(x);
|
||||
this.y = Math.round(y);
|
||||
|
||||
this.div.style.left = `${this.x}px`;
|
||||
this.div.style.top = `${this.y}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the editor position within its parent.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
translate(x, y) {
|
||||
this.setAt(this.x + x, this.y + y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dimensions of this editor.
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
setDims(width, height) {
|
||||
this.div.style.width = `${width}px`;
|
||||
this.div.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this editor in a div.
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
render() {
|
||||
this.div = document.createElement("div");
|
||||
this.div.className = this.name;
|
||||
this.div.setAttribute("id", this.id);
|
||||
this.div.draggable = true;
|
||||
this.div.tabIndex = 100;
|
||||
this.div.style.left = `${this.x}px`;
|
||||
this.div.style.top = `${this.y}px`;
|
||||
|
||||
bindEvents(this, this.div, [
|
||||
"dragstart",
|
||||
"focusin",
|
||||
"focusout",
|
||||
"mousedown",
|
||||
]);
|
||||
|
||||
return this.div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed once this editor has been rendered.
|
||||
*/
|
||||
onceAdded() {}
|
||||
|
||||
/**
|
||||
* Apply the current transform (zoom) to this editor.
|
||||
* @param {Array<number>} transform
|
||||
*/
|
||||
transform(transform) {
|
||||
const { style } = this.div;
|
||||
const width = parseFloat(style.width);
|
||||
const height = parseFloat(style.height);
|
||||
|
||||
const [x1, y1] = Util.applyTransform([this.x, this.y], transform);
|
||||
|
||||
if (!Number.isNaN(width)) {
|
||||
const [x2] = Util.applyTransform([this.x + width, 0], transform);
|
||||
this.div.style.width = `${x2 - x1}px`;
|
||||
}
|
||||
if (!Number.isNaN(height)) {
|
||||
const [, y2] = Util.applyTransform([0, this.y + height], transform);
|
||||
this.div.style.height = `${y2 - y1}px`;
|
||||
}
|
||||
this.setAt(x1, y1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the editor contains something.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable edit mode.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
enableEditMode() {
|
||||
this.#isInEditMode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable edit mode.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
disableEditMode() {
|
||||
this.#isInEditMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the editor is edited.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInEditMode() {
|
||||
return this.#isInEditMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* If it returns true, then this editor handle the keyboard
|
||||
* events itself.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldGetKeyboardEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the elements of an editor in order to be able to build
|
||||
* a new one from these data.
|
||||
* It's used on ctrl+c action.
|
||||
*
|
||||
* To implement in subclasses.
|
||||
* @returns {AnnotationEditor}
|
||||
*/
|
||||
copy() {
|
||||
unreachable("An editor must be copyable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this editor needs to be rebuilt or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
needsToBeRebuilt() {
|
||||
return this.div && !this.isAttachedToDOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the editor in case it has been removed on undo.
|
||||
*
|
||||
* To implement in subclasses.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
rebuild() {
|
||||
unreachable("An editor must be rebuildable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the editor.
|
||||
* The result of the serialization will be used to construct a
|
||||
* new annotation to add to the pdf document.
|
||||
*
|
||||
* To implement in subclasses.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
serialize() {
|
||||
unreachable("An editor must be serializable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this editor.
|
||||
* It's used on ctrl+backspace action.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
remove() {
|
||||
this.parent.remove(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select this editor.
|
||||
*/
|
||||
select() {
|
||||
if (this.div) {
|
||||
this.div.classList.add("selectedEditor");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect this editor.
|
||||
*/
|
||||
unselect() {
|
||||
if (this.div) {
|
||||
this.div.classList.remove("selectedEditor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditor };
|
225
src/display/editor/freetext.js
Normal file
225
src/display/editor/freetext.js
Normal file
|
@ -0,0 +1,225 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
import { AnnotationEditorType, Util } from "../../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { bindEvents } from "./tools.js";
|
||||
|
||||
/**
|
||||
* Basic text editor in order to create a FreeTex annotation.
|
||||
*/
|
||||
class FreeTextEditor extends AnnotationEditor {
|
||||
#color;
|
||||
|
||||
#content = "";
|
||||
|
||||
#contentHTML = "";
|
||||
|
||||
#fontSize;
|
||||
|
||||
static _freeTextDefaultContent = "";
|
||||
|
||||
static _l10nPromise;
|
||||
|
||||
constructor(params) {
|
||||
super({ ...params, name: "freeTextEditor" });
|
||||
this.#color = params.color || "CanvasText";
|
||||
this.#fontSize = params.fontSize || 10;
|
||||
}
|
||||
|
||||
static setL10n(l10n) {
|
||||
this._l10nPromise = l10n.get("freetext_default_content");
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
copy() {
|
||||
const editor = new FreeTextEditor({
|
||||
parent: this.parent,
|
||||
id: this.parent.getNextId(),
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
});
|
||||
|
||||
editor.width = this.width;
|
||||
editor.height = this.height;
|
||||
editor.#color = this.#color;
|
||||
editor.#fontSize = this.#fontSize;
|
||||
editor.#content = this.#content;
|
||||
editor.#contentHTML = this.#contentHTML;
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
rebuild() {
|
||||
if (this.div === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isAttachedToDOM) {
|
||||
// At some point this editor has been removed and
|
||||
// we're rebuilting it, hence we must add it to its
|
||||
// parent.
|
||||
this.parent.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
enableEditMode() {
|
||||
super.enableEditMode();
|
||||
this.overlayDiv.classList.remove("enabled");
|
||||
this.div.draggable = false;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
disableEditMode() {
|
||||
super.disableEditMode();
|
||||
this.overlayDiv.classList.add("enabled");
|
||||
this.div.draggable = true;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
onceAdded() {
|
||||
if (this.width) {
|
||||
// The editor has been created in using ctrl+c.
|
||||
this.div.focus();
|
||||
return;
|
||||
}
|
||||
this.enableEditMode();
|
||||
this.editorDiv.focus();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
isEmpty() {
|
||||
return this.editorDiv.innerText.trim() === "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the text from this editor.
|
||||
* @returns {string}
|
||||
*/
|
||||
#extractText() {
|
||||
const divs = this.editorDiv.getElementsByTagName("div");
|
||||
if (divs.length === 0) {
|
||||
return this.editorDiv.innerText;
|
||||
}
|
||||
const buffer = [];
|
||||
for (let i = 0, ii = divs.length; i < ii; i++) {
|
||||
const div = divs[i];
|
||||
const first = div.firstChild;
|
||||
if (first?.nodeName === "#text") {
|
||||
buffer.push(first.data);
|
||||
} else {
|
||||
buffer.push("");
|
||||
}
|
||||
}
|
||||
return buffer.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the content we have in this editor.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
commit() {
|
||||
this.disableEditMode();
|
||||
this.#contentHTML = this.editorDiv.innerHTML;
|
||||
this.#content = this.#extractText().trimEnd();
|
||||
|
||||
const style = getComputedStyle(this.div);
|
||||
this.width = parseFloat(style.width);
|
||||
this.height = parseFloat(style.height);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
shouldGetKeyboardEvents() {
|
||||
return this.isInEditMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* ondblclick callback.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
dblclick(event) {
|
||||
this.enableEditMode();
|
||||
this.editorDiv.focus();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
render() {
|
||||
if (this.div) {
|
||||
return this.div;
|
||||
}
|
||||
|
||||
super.render();
|
||||
this.editorDiv = document.createElement("div");
|
||||
this.editorDiv.tabIndex = 0;
|
||||
this.editorDiv.className = "internal";
|
||||
|
||||
FreeTextEditor._l10nPromise.then(msg =>
|
||||
this.editorDiv.setAttribute("default-content", msg)
|
||||
);
|
||||
this.editorDiv.contentEditable = true;
|
||||
|
||||
const { style } = this.editorDiv;
|
||||
style.fontSize = `calc(${this.#fontSize}px * var(--zoom-factor))`;
|
||||
style.minHeight = `calc(${1.5 * this.#fontSize}px * var(--zoom-factor))`;
|
||||
style.color = this.#color;
|
||||
|
||||
this.div.appendChild(this.editorDiv);
|
||||
|
||||
this.overlayDiv = document.createElement("div");
|
||||
this.overlayDiv.classList.add("overlay", "enabled");
|
||||
this.div.appendChild(this.overlayDiv);
|
||||
|
||||
// TODO: implement paste callback.
|
||||
// The goal is to sanitize and have something suitable for this
|
||||
// editor.
|
||||
bindEvents(this, this.div, ["dblclick"]);
|
||||
|
||||
if (this.width) {
|
||||
// This editor has been created in using copy (ctrl+c).
|
||||
this.setAt(this.x + this.width, this.y + this.height);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.editorDiv.innerHTML = this.#contentHTML;
|
||||
}
|
||||
|
||||
return this.div;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
serialize() {
|
||||
const rect = this.div.getBoundingClientRect();
|
||||
const [x1, y1] = Util.applyTransform(
|
||||
[this.x, this.y + rect.height],
|
||||
this.parent.viewport.inverseTransform
|
||||
);
|
||||
|
||||
const [x2, y2] = Util.applyTransform(
|
||||
[this.x + rect.width, this.y],
|
||||
this.parent.viewport.inverseTransform
|
||||
);
|
||||
|
||||
return {
|
||||
annotationType: AnnotationEditorType.FREETEXT,
|
||||
color: [0, 0, 0],
|
||||
fontSize: this.#fontSize,
|
||||
value: this.#content,
|
||||
pageIndex: this.parent.pageIndex,
|
||||
rect: [x1, y1, x2, y2],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { FreeTextEditor };
|
574
src/display/editor/tools.js
Normal file
574
src/display/editor/tools.js
Normal file
|
@ -0,0 +1,574 @@
|
|||
/* 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("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
|
||||
|
||||
import {
|
||||
AnnotationEditorPrefix,
|
||||
AnnotationEditorType,
|
||||
shadow,
|
||||
} from "../../shared/util.js";
|
||||
|
||||
function bindEvents(obj, element, names) {
|
||||
for (const name of names) {
|
||||
element.addEventListener(name, obj[name].bind(obj));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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++}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle undo/redo.
|
||||
* Commands are just saved in a buffer.
|
||||
* If we hit some memory issues we could likely use a circular buffer.
|
||||
* It has to be used as a singleton.
|
||||
*/
|
||||
class CommandManager {
|
||||
#commands = [];
|
||||
|
||||
#maxSize = 100;
|
||||
|
||||
// When the position is NaN, it means the buffer is empty.
|
||||
#position = NaN;
|
||||
|
||||
#start = 0;
|
||||
|
||||
/**
|
||||
* Add a new couple of commands to be used in case of redo/undo.
|
||||
* @param {function} cmd
|
||||
* @param {function} undo
|
||||
*/
|
||||
add(cmd, undo) {
|
||||
const save = [cmd, undo];
|
||||
const next = (this.#position + 1) % this.#maxSize;
|
||||
if (next !== this.#start) {
|
||||
if (this.#start < next) {
|
||||
this.#commands = this.#commands.slice(this.#start, next);
|
||||
} else {
|
||||
this.#commands = this.#commands
|
||||
.slice(this.#start)
|
||||
.concat(this.#commands.slice(0, next));
|
||||
}
|
||||
this.#start = 0;
|
||||
this.#position = this.#commands.length - 1;
|
||||
}
|
||||
this.#setCommands(save);
|
||||
cmd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last command.
|
||||
*/
|
||||
undo() {
|
||||
if (isNaN(this.#position)) {
|
||||
// Nothing to undo.
|
||||
return;
|
||||
}
|
||||
this.#commands[this.#position][1]();
|
||||
if (this.#position === this.#start) {
|
||||
this.#position = NaN;
|
||||
} else {
|
||||
this.#position = (this.#maxSize + this.#position - 1) % this.#maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last command.
|
||||
*/
|
||||
redo() {
|
||||
if (isNaN(this.#position)) {
|
||||
if (this.#start < this.#commands.length) {
|
||||
this.#commands[this.#start][0]();
|
||||
this.#position = this.#start;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const next = (this.#position + 1) % this.#maxSize;
|
||||
if (next !== this.#start && next < this.#commands.length) {
|
||||
this.#commands[next][0]();
|
||||
this.#position = next;
|
||||
}
|
||||
}
|
||||
|
||||
#setCommands(cmds) {
|
||||
if (this.#commands.length < this.#maxSize) {
|
||||
this.#commands.push(cmds);
|
||||
this.#position = isNaN(this.#position) ? 0 : this.#position + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(this.#position)) {
|
||||
this.#position = this.#start;
|
||||
} else {
|
||||
this.#position = (this.#position + 1) % this.#maxSize;
|
||||
if (this.#position === this.#start) {
|
||||
this.#start = (this.#start + 1) % this.#maxSize;
|
||||
}
|
||||
}
|
||||
this.#commands[this.#position] = cmds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 page is used as `this` in the callback.
|
||||
* @param {AnnotationEditorLayer} page.
|
||||
* @param {KeyboardEvent} event
|
||||
* @returns
|
||||
*/
|
||||
exec(page, event) {
|
||||
if (!this.allKeys.has(event.key)) {
|
||||
return;
|
||||
}
|
||||
const callback = this.callbacks.get(this.#serialize(event));
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
callback.bind(page)();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic clipboard to copy/paste some editors.
|
||||
* It has to be used as a singleton.
|
||||
*/
|
||||
class ClipboardManager {
|
||||
constructor() {
|
||||
this.element = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy an element.
|
||||
* @param {AnnotationEditor} element
|
||||
*/
|
||||
copy(element) {
|
||||
this.element = element.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new element.
|
||||
* @returns {AnnotationEditor|null}
|
||||
*/
|
||||
paste() {
|
||||
return this.element?.copy() || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AnnotationEditorUIManager {
|
||||
#activeEditor = null;
|
||||
|
||||
#allEditors = new Map();
|
||||
|
||||
#allLayers = new Set();
|
||||
|
||||
#allowClick = true;
|
||||
|
||||
#clipboardManager = new ClipboardManager();
|
||||
|
||||
#commandManager = new CommandManager();
|
||||
|
||||
#idManager = new IdManager();
|
||||
|
||||
#isAllSelected = false;
|
||||
|
||||
#isEnabled = false;
|
||||
|
||||
#mode = AnnotationEditorType.NONE;
|
||||
|
||||
/**
|
||||
* 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.add(layer);
|
||||
if (this.#isEnabled) {
|
||||
layer.enable();
|
||||
} else {
|
||||
layer.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a layer.
|
||||
* @param {AnnotationEditorLayer} layer
|
||||
*/
|
||||
removeLayer(layer) {
|
||||
this.#allLayers.delete(layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the editor mode (None, FreeText, Ink, ...)
|
||||
* @param {number} mode
|
||||
*/
|
||||
updateMode(mode) {
|
||||
this.#mode = mode;
|
||||
if (mode === AnnotationEditorType.NONE) {
|
||||
this.#disableAll();
|
||||
} else {
|
||||
this.#enableAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable all the layers.
|
||||
*/
|
||||
#enableAll() {
|
||||
if (!this.#isEnabled) {
|
||||
this.#isEnabled = true;
|
||||
for (const layer of this.#allLayers) {
|
||||
layer.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all the layers.
|
||||
*/
|
||||
#disableAll() {
|
||||
if (this.#isEnabled) {
|
||||
this.#isEnabled = false;
|
||||
for (const layer of this.#allLayers) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given editor as the active one.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
setActiveEditor(editor) {
|
||||
this.#activeEditor = editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last command.
|
||||
*/
|
||||
undo() {
|
||||
this.#commandManager.undo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last undoed command.
|
||||
*/
|
||||
redo() {
|
||||
this.#commandManager.redo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a command to execute (cmd) and another one to undo it.
|
||||
* @param {function} cmd
|
||||
* @param {function} undo
|
||||
*/
|
||||
addCommands(cmd, undo) {
|
||||
this.#commandManager.add(cmd, undo);
|
||||
}
|
||||
|
||||
/**
|
||||
* When set to true a click on the current layer will trigger
|
||||
* an editor creation.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get allowClick() {
|
||||
return this.#allowClick;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} allow
|
||||
*/
|
||||
set allowClick(allow) {
|
||||
this.#allowClick = allow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect the current editor.
|
||||
*/
|
||||
unselect() {
|
||||
if (this.#activeEditor) {
|
||||
this.#activeEditor.parent.setActiveEditor(null);
|
||||
}
|
||||
this.#allowClick = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress some editors from the given layer.
|
||||
* @param {AnnotationEditorLayer} layer
|
||||
*/
|
||||
suppress(layer) {
|
||||
let cmd, undo;
|
||||
if (this.#isAllSelected) {
|
||||
const editors = Array.from(this.#allEditors.values());
|
||||
cmd = () => {
|
||||
for (const editor of editors) {
|
||||
editor.remove();
|
||||
}
|
||||
};
|
||||
|
||||
undo = () => {
|
||||
for (const editor of editors) {
|
||||
layer.addOrRebuild(editor);
|
||||
}
|
||||
};
|
||||
|
||||
this.addCommands(cmd, undo);
|
||||
} else {
|
||||
if (!this.#activeEditor) {
|
||||
return;
|
||||
}
|
||||
const editor = this.#activeEditor;
|
||||
cmd = () => {
|
||||
editor.remove();
|
||||
};
|
||||
undo = () => {
|
||||
layer.addOrRebuild(editor);
|
||||
};
|
||||
}
|
||||
|
||||
this.addCommands(cmd, undo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the selected editor.
|
||||
*/
|
||||
copy() {
|
||||
if (this.#activeEditor) {
|
||||
this.#clipboardManager.copy(this.#activeEditor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut the selected editor.
|
||||
* @param {AnnotationEditorLayer}
|
||||
*/
|
||||
cut(layer) {
|
||||
if (this.#activeEditor) {
|
||||
this.#clipboardManager.copy(this.#activeEditor);
|
||||
const editor = this.#activeEditor;
|
||||
const cmd = () => {
|
||||
editor.remove();
|
||||
};
|
||||
const undo = () => {
|
||||
layer.addOrRebuild(editor);
|
||||
};
|
||||
|
||||
this.addCommands(cmd, undo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a previously copied editor.
|
||||
* @param {AnnotationEditorLayer}
|
||||
* @returns {undefined}
|
||||
*/
|
||||
paste(layer) {
|
||||
const editor = this.#clipboardManager.paste();
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const cmd = () => {
|
||||
layer.addOrRebuild(editor);
|
||||
};
|
||||
const undo = () => {
|
||||
editor.remove();
|
||||
};
|
||||
|
||||
this.addCommands(cmd, undo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all the editors.
|
||||
*/
|
||||
selectAll() {
|
||||
this.#isAllSelected = true;
|
||||
for (const editor of this.#allEditors.values()) {
|
||||
editor.select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect all the editors.
|
||||
*/
|
||||
unselectAll() {
|
||||
this.#isAllSelected = false;
|
||||
for (const editor of this.#allEditors.values()) {
|
||||
editor.unselect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { AnnotationEditorUIManager, bindEvents, KeyboardManager };
|
|
@ -21,6 +21,7 @@
|
|||
/** @typedef {import("./display/display_utils").PageViewport} PageViewport */
|
||||
|
||||
import {
|
||||
AnnotationEditorType,
|
||||
AnnotationMode,
|
||||
CMapCompressionType,
|
||||
createPromiseCapability,
|
||||
|
@ -56,6 +57,8 @@ import {
|
|||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
} from "./display/display_utils.js";
|
||||
import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js";
|
||||
import { AnnotationEditorUIManager } from "./display/editor/tools.js";
|
||||
import { AnnotationLayer } from "./display/annotation_layer.js";
|
||||
import { GlobalWorkerOptions } from "./display/worker_options.js";
|
||||
import { isNodeJS } from "./shared/is_node.js";
|
||||
|
@ -104,6 +107,9 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
|
|||
}
|
||||
|
||||
export {
|
||||
AnnotationEditorLayer,
|
||||
AnnotationEditorType,
|
||||
AnnotationEditorUIManager,
|
||||
AnnotationLayer,
|
||||
AnnotationMode,
|
||||
build,
|
||||
|
|
|
@ -51,6 +51,13 @@ const AnnotationMode = {
|
|||
ENABLE_STORAGE: 3,
|
||||
};
|
||||
|
||||
const AnnotationEditorPrefix = "pdfjs_internal_editor_";
|
||||
|
||||
const AnnotationEditorType = {
|
||||
NONE: 0,
|
||||
FREETEXT: 1,
|
||||
};
|
||||
|
||||
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
|
||||
const PermissionFlag = {
|
||||
PRINT: 0x04,
|
||||
|
@ -1135,6 +1142,8 @@ export {
|
|||
AbortException,
|
||||
AnnotationActionEventType,
|
||||
AnnotationBorderStyleType,
|
||||
AnnotationEditorPrefix,
|
||||
AnnotationEditorType,
|
||||
AnnotationFieldFlag,
|
||||
AnnotationFlag,
|
||||
AnnotationMarkedState,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue