[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
@ -160,6 +160,10 @@
|
||||
],
|
||||
"default": 2
|
||||
},
|
||||
"annotationEditorEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"enablePermissions": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
|
@ -249,3 +249,10 @@ password_cancel=Cancel
|
||||
printing_not_supported=Warning: Printing is not fully supported by this browser.
|
||||
printing_not_ready=Warning: The PDF is not fully loaded for printing.
|
||||
web_fonts_disabled=Web fonts are disabled: unable to use embedded PDF fonts.
|
||||
|
||||
# Editor
|
||||
editor_none.title=Disable Annotation Editing
|
||||
editor_none_label=Disable Editing
|
||||
freetext_default_content=Enter some text…
|
||||
editor_free_text.title=Add FreeText Annotation
|
||||
editor_free_text_label=FreeText Annotation
|
||||
|
@ -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,
|
||||
|
@ -30,6 +30,7 @@ async function runTests(results) {
|
||||
"annotation_spec.js",
|
||||
"accessibility_spec.js",
|
||||
"find_spec.js",
|
||||
"freetext_editor_spec.js",
|
||||
],
|
||||
});
|
||||
|
||||
|
200
test/integration/freetext_editor_spec.js
Normal file
200
test/integration/freetext_editor_spec.js
Normal file
@ -0,0 +1,200 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
const { closePages, loadAndWait } = require("./test_utils.js");
|
||||
|
||||
const editorPrefix = "#pdfjs_internal_editor_";
|
||||
|
||||
describe("Editor", () => {
|
||||
describe("FreeText", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must write a string in a FreeText editor", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.click("#editorFreeText");
|
||||
|
||||
const rect = await page.$eval(".annotationEditorLayer", el => {
|
||||
// With Chrome something is wrong when serializing a DomRect,
|
||||
// hence we extract the values and just return them.
|
||||
const { x, y } = el.getBoundingClientRect();
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
const data = "Hello PDF.js World !!";
|
||||
await page.mouse.click(rect.x + 10, rect.y + 10);
|
||||
await page.type(`${editorPrefix}0 .internal`, data);
|
||||
|
||||
const editorRect = await page.$eval(`${editorPrefix}0`, el => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
});
|
||||
|
||||
// Commit.
|
||||
await page.mouse.click(
|
||||
editorRect.x,
|
||||
editorRect.y + 2 * editorRect.height
|
||||
);
|
||||
|
||||
const content = await page.$eval(`${editorPrefix}0`, el =>
|
||||
el.innerText.trimEnd()
|
||||
);
|
||||
expect(content).withContext(`In ${browserName}`).toEqual(data);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("must copy/paste", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const editorRect = await page.$eval(`${editorPrefix}0`, el => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
return { x, y, width, height };
|
||||
});
|
||||
|
||||
// Select the editor created previously.
|
||||
await page.mouse.click(
|
||||
editorRect.x + editorRect.width / 2,
|
||||
editorRect.y + editorRect.height / 2
|
||||
);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("c");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("v");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
const content = await page.$eval(`${editorPrefix}0`, el =>
|
||||
el.innerText.trimEnd()
|
||||
);
|
||||
let pastedContent = await page.$eval(`${editorPrefix}2`, el =>
|
||||
el.innerText.trimEnd()
|
||||
);
|
||||
|
||||
expect(pastedContent)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(content);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("c");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("v");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
pastedContent = await page.$eval(`${editorPrefix}4`, el =>
|
||||
el.innerText.trimEnd()
|
||||
);
|
||||
expect(pastedContent)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(content);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("must clear all", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("a");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
for (const n of [0, 2, 4]) {
|
||||
const hasEditor = await page.evaluate(sel => {
|
||||
return !!document.querySelector(sel);
|
||||
}, `${editorPrefix}${n}`);
|
||||
|
||||
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("must check that a paste has been undone", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const rect = await page.$eval(".annotationEditorLayer", el => {
|
||||
const { x, y } = el.getBoundingClientRect();
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
const data = "Hello PDF.js World !!";
|
||||
await page.mouse.click(rect.x + 10, rect.y + 10);
|
||||
await page.type(`${editorPrefix}5 .internal`, data);
|
||||
|
||||
const editorRect = await page.$eval(`${editorPrefix}5`, el => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
return { x, y, width, height };
|
||||
});
|
||||
|
||||
// Commit.
|
||||
await page.mouse.click(
|
||||
editorRect.x,
|
||||
editorRect.y + 2 * editorRect.height
|
||||
);
|
||||
// And select it again.
|
||||
await page.mouse.click(
|
||||
editorRect.x + editorRect.width / 2,
|
||||
editorRect.y + editorRect.height / 2
|
||||
);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("c");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("v");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
let hasEditor = await page.evaluate(sel => {
|
||||
return !!document.querySelector(sel);
|
||||
}, `${editorPrefix}7`);
|
||||
|
||||
expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("z");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
hasEditor = await page.evaluate(sel => {
|
||||
return !!document.querySelector(sel);
|
||||
}, `${editorPrefix}7`);
|
||||
|
||||
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -950,6 +950,7 @@ async function startBrowser(browserName, startUrl = "") {
|
||||
// Avoid popup when saving is done
|
||||
"browser.download.always_ask_before_handling_new_types": true,
|
||||
"browser.download.panel.shown": true,
|
||||
"browser.download.alwaysOpenPanel": false,
|
||||
// Save file in output
|
||||
"browser.download.folderList": 2,
|
||||
"browser.download.dir": tempDir,
|
||||
|
85
web/annotation_editor_layer_builder.css
Normal file
85
web/annotation_editor_layer_builder.css
Normal file
@ -0,0 +1,85 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--focus-outline: solid 2px red;
|
||||
--hover-outline: dashed 2px blue;
|
||||
}
|
||||
|
||||
.annotationEditorLayer {
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
resize: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor .internal {
|
||||
background: transparent;
|
||||
border: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-height: 15px;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor .overlay {
|
||||
position: absolute;
|
||||
display: none;
|
||||
background: transparent;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor .overlay.enabled {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor .internal:empty::before {
|
||||
content: attr(default-content);
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor .internal:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor:focus-within {
|
||||
outline: var(--focus-outline);
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor:hover:not(:focus-within) {
|
||||
outline: var(--hover-outline);
|
||||
}
|
||||
|
||||
.annotationEditorLayer .selectedEditor {
|
||||
outline: var(--focus-outline);
|
||||
resize: none;
|
||||
}
|
128
web/annotation_editor_layer_builder.js
Normal file
128
web/annotation_editor_layer_builder.js
Normal file
@ -0,0 +1,128 @@
|
||||
/* 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("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
|
||||
import { AnnotationEditorLayer } from "pdfjs-lib";
|
||||
import { NullL10n } from "./l10n_utils.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorLayerBuilderOptions
|
||||
* @property {number} mode - Editor mode
|
||||
* @property {HTMLDivElement} pageDiv
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {AnnotationStorage} annotationStorage
|
||||
* @property {IL10n} l10n - Localization service.
|
||||
* @property {AnnotationEditorUIManager} uiManager
|
||||
*/
|
||||
|
||||
class AnnotationEditorLayerBuilder {
|
||||
#uiManager;
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerBuilderOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.pageDiv = options.pageDiv;
|
||||
this.pdfPage = options.pdfPage;
|
||||
this.annotationStorage = options.annotationStorage || null;
|
||||
this.l10n = options.l10n || NullL10n;
|
||||
this.annotationEditorLayer = null;
|
||||
this.div = null;
|
||||
this._cancelled = false;
|
||||
this.#uiManager = options.uiManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PageViewport} viewport
|
||||
* @param {string} intent (default value is 'display')
|
||||
*/
|
||||
async render(viewport, intent = "display") {
|
||||
if (intent !== "display") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.div) {
|
||||
this.annotationEditorLayer.update({ viewport: viewport.clone() });
|
||||
this.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an AnnotationEditor layer div
|
||||
this.div = document.createElement("div");
|
||||
this.div.className = "annotationEditorLayer";
|
||||
this.div.tabIndex = 0;
|
||||
|
||||
this.annotationEditorLayer = new AnnotationEditorLayer({
|
||||
uiManager: this.#uiManager,
|
||||
div: this.div,
|
||||
annotationStorage: this.annotationStorage,
|
||||
pageIndex: this.pdfPage._pageIndex,
|
||||
l10n: this.l10n,
|
||||
});
|
||||
|
||||
const parameters = {
|
||||
viewport: viewport.clone(),
|
||||
div: this.div,
|
||||
annotations: null,
|
||||
intent,
|
||||
};
|
||||
|
||||
this.annotationEditorLayer.render(parameters);
|
||||
|
||||
this.pageDiv.appendChild(this.div);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._cancelled = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.div.hidden = true;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.div.hidden = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.pageDiv = null;
|
||||
this.div.remove();
|
||||
this.annotationEditorLayer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditorLayerBuilder };
|
16
web/app.js
16
web/app.js
@ -525,6 +525,7 @@ const PDFViewerApplication = {
|
||||
l10n: this.l10n,
|
||||
textLayerMode: AppOptions.get("textLayerMode"),
|
||||
annotationMode: AppOptions.get("annotationMode"),
|
||||
annotationEditorEnabled: AppOptions.get("annotationEditorEnabled"),
|
||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
|
||||
@ -560,6 +561,10 @@ const PDFViewerApplication = {
|
||||
this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n);
|
||||
}
|
||||
|
||||
if (AppOptions.get("annotationEditorEnabled")) {
|
||||
document.getElementById("editorModeButtons").classList.remove("hidden");
|
||||
}
|
||||
|
||||
this.pdfDocumentProperties = new PDFDocumentProperties(
|
||||
appConfig.documentProperties,
|
||||
this.overlayManager,
|
||||
@ -1878,6 +1883,10 @@ const PDFViewerApplication = {
|
||||
eventBus._on("namedaction", webViewerNamedAction);
|
||||
eventBus._on("presentationmodechanged", webViewerPresentationModeChanged);
|
||||
eventBus._on("presentationmode", webViewerPresentationMode);
|
||||
eventBus._on(
|
||||
"switchannotationeditormode",
|
||||
webViewerSwitchAnnotationEditorMode
|
||||
);
|
||||
eventBus._on("print", webViewerPrint);
|
||||
eventBus._on("download", webViewerDownload);
|
||||
eventBus._on("firstpage", webViewerFirstPage);
|
||||
@ -2459,6 +2468,13 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
function webViewerPresentationMode() {
|
||||
PDFViewerApplication.requestPresentationMode();
|
||||
}
|
||||
function webViewerSwitchAnnotationEditorMode(evt) {
|
||||
if (evt.toggle) {
|
||||
PDFViewerApplication.pdfViewer.annotionEditorEnabled = true;
|
||||
} else {
|
||||
PDFViewerApplication.pdfViewer.annotationEditorMode = evt.mode;
|
||||
}
|
||||
}
|
||||
function webViewerPrint() {
|
||||
PDFViewerApplication.triggerPrinting();
|
||||
}
|
||||
|
@ -58,6 +58,11 @@ const defaultOptions = {
|
||||
value: 2,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
annotationEditorEnabled: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
cursorToolOnLoad: {
|
||||
/** @type {number} */
|
||||
value: 0,
|
||||
|
@ -22,6 +22,8 @@
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
||||
@ -30,6 +32,8 @@
|
||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
||||
|
||||
import {
|
||||
AnnotationEditorType,
|
||||
AnnotationEditorUIManager,
|
||||
AnnotationMode,
|
||||
createPromiseCapability,
|
||||
PermissionFlag,
|
||||
@ -61,6 +65,7 @@ import {
|
||||
VERTICAL_PADDING,
|
||||
watchScroll,
|
||||
} from "./ui_utils.js";
|
||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||
import { NullL10n } from "./l10n_utils.js";
|
||||
import { PDFPageView } from "./pdf_page_view.js";
|
||||
@ -104,6 +109,8 @@ const PagesCountLimit = {
|
||||
* being rendered. The constants from {@link AnnotationMode} should be used;
|
||||
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
||||
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
||||
* @property {boolean} [annotationEditorEnabled] - Enables the creation and
|
||||
* editing of new Annotations.
|
||||
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
||||
* mainly for annotation icons. Include trailing slash.
|
||||
* @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
|
||||
@ -194,6 +201,7 @@ class PDFPageViewBuffer {
|
||||
* Simple viewer control to display PDF content/pages.
|
||||
*
|
||||
* @implements {IPDFAnnotationLayerFactory}
|
||||
* @implements {IPDFAnnotationEditorLayerFactory}
|
||||
* @implements {IPDFStructTreeLayerFactory}
|
||||
* @implements {IPDFTextLayerFactory}
|
||||
* @implements {IPDFXfaLayerFactory}
|
||||
@ -201,6 +209,10 @@ class PDFPageViewBuffer {
|
||||
class BaseViewer {
|
||||
#buffer = null;
|
||||
|
||||
#annotationEditorMode = AnnotationEditorType.NONE;
|
||||
|
||||
#annotationEditorUIManager = null;
|
||||
|
||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||
|
||||
#previousAnnotationMode = null;
|
||||
@ -268,6 +280,10 @@ class BaseViewer {
|
||||
this.#enablePermissions = options.enablePermissions || false;
|
||||
this.pageColors = options.pageColors || null;
|
||||
|
||||
if (options.annotationEditorEnabled === true) {
|
||||
this.#annotationEditorUIManager = new AnnotationEditorUIManager();
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
if (
|
||||
this.pageColors &&
|
||||
@ -699,6 +715,9 @@ class BaseViewer {
|
||||
const annotationLayerFactory =
|
||||
this.#annotationMode !== AnnotationMode.DISABLE ? this : null;
|
||||
const xfaLayerFactory = isPureXfa ? this : null;
|
||||
const annotationEditorLayerFactory = this.#annotationEditorUIManager
|
||||
? this
|
||||
: null;
|
||||
|
||||
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
||||
const pageView = new PDFPageView({
|
||||
@ -714,6 +733,7 @@ class BaseViewer {
|
||||
annotationLayerFactory,
|
||||
annotationMode: this.#annotationMode,
|
||||
xfaLayerFactory,
|
||||
annotationEditorLayerFactory,
|
||||
textHighlighterFactory: this,
|
||||
structTreeLayerFactory: this,
|
||||
imageResourcesPath: this.imageResourcesPath,
|
||||
@ -1656,6 +1676,30 @@ class BaseViewer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLDivElement} pageDiv
|
||||
* @param {PDFPageProxy} pdfPage
|
||||
* @param {IL10n} l10n
|
||||
* @param {AnnotationStorage} [annotationStorage] - Storage for annotation
|
||||
* data in forms.
|
||||
* @returns {AnnotationEditorLayerBuilder}
|
||||
*/
|
||||
createAnnotationEditorLayerBuilder(
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
l10n,
|
||||
annotationStorage = null
|
||||
) {
|
||||
return new AnnotationEditorLayerBuilder({
|
||||
uiManager: this.#annotationEditorUIManager,
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
annotationStorage:
|
||||
annotationStorage || this.pdfDocument?.annotationStorage,
|
||||
l10n,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLDivElement} pageDiv
|
||||
* @param {PDFPageProxy} pdfPage
|
||||
@ -2072,6 +2116,36 @@ class BaseViewer {
|
||||
docStyle.setProperty("--viewer-container-height", `${height}px`);
|
||||
}
|
||||
}
|
||||
|
||||
get annotationEditorMode() {
|
||||
return this.#annotationEditorMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} mode - Annotation Editor mode (None, FreeText, Ink, ...)
|
||||
*/
|
||||
set annotationEditorMode(mode) {
|
||||
if (!this.#annotationEditorUIManager) {
|
||||
throw new Error(`The AnnotationEditor is not enabled.`);
|
||||
}
|
||||
|
||||
if (this.#annotationEditorMode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Object.values(AnnotationEditorType).includes(mode)) {
|
||||
throw new Error(`Invalid AnnotationEditor mode: ${mode}`);
|
||||
}
|
||||
|
||||
// If the mode is the same as before, it means that this mode is disabled
|
||||
// and consequently the mode is NONE.
|
||||
this.#annotationEditorMode = mode;
|
||||
this.eventBus.dispatch("annotationeditormodechanged", {
|
||||
source: this,
|
||||
mode,
|
||||
});
|
||||
this.#annotationEditorUIManager.updateMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseViewer, PagesCountLimit, PDFPageViewBuffer };
|
||||
|
@ -21,6 +21,8 @@
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
||||
@ -29,6 +31,7 @@
|
||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||
|
||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||
import { NullL10n } from "./l10n_utils.js";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
@ -87,6 +90,32 @@ class DefaultAnnotationLayerFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements IPDFAnnotationEditorLayerFactory
|
||||
*/
|
||||
class DefaultAnnotationEditorLayerFactory {
|
||||
/**
|
||||
* @param {HTMLDivElement} pageDiv
|
||||
* @param {PDFPageProxy} pdfPage
|
||||
* @param {IL10n} l10n
|
||||
* @param {AnnotationStorage} [annotationStorage]
|
||||
* @returns {AnnotationEditorLayerBuilder}
|
||||
*/
|
||||
createAnnotationEditorLayerBuilder(
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
l10n,
|
||||
annotationStorage = null
|
||||
) {
|
||||
return new AnnotationEditorLayerBuilder({
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
l10n,
|
||||
annotationStorage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements IPDFStructTreeLayerFactory
|
||||
*/
|
||||
@ -161,6 +190,7 @@ class DefaultXfaLayerFactory {
|
||||
}
|
||||
|
||||
export {
|
||||
DefaultAnnotationEditorLayerFactory,
|
||||
DefaultAnnotationLayerFactory,
|
||||
DefaultStructTreeLayerFactory,
|
||||
DefaultTextLayerFactory,
|
||||
|
24
web/images/toolbarButton-editorFreeText.svg
Normal file
24
web/images/toolbarButton-editorFreeText.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
|
||||
c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
|
||||
c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
|
||||
c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
|
||||
v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
|
||||
c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
|
||||
c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
|
||||
V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g transform="scale(0.03125)">
|
||||
<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
|
||||
c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
|
||||
h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
4
web/images/toolbarButton-editorNone.svg
Normal file
4
web/images/toolbarButton-editorNone.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>
|
After Width: | Height: | Size: 478 B |
@ -19,6 +19,8 @@
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./annotation_editor_layer_builder").AnnotationEditorLayerBuilder} AnnotationEditorLayerBuilder */
|
||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
@ -208,6 +210,26 @@ class IPDFAnnotationLayerFactory {
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
class IPDFAnnotationEditorLayerFactory {
|
||||
/**
|
||||
* @param {HTMLDivElement} pageDiv
|
||||
* @param {PDFPageProxy} pdfPage
|
||||
* @param {IL10n} l10n
|
||||
* @param {AnnotationStorage} [annotationStorage] - Storage for annotation
|
||||
* data in forms.
|
||||
* @returns {AnnotationEditorLayerBuilder}
|
||||
*/
|
||||
createAnnotationEditorLayerBuilder(
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
l10n = undefined,
|
||||
annotationStorage = null
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
@ -307,6 +329,7 @@ class IL10n {
|
||||
export {
|
||||
IDownloadManager,
|
||||
IL10n,
|
||||
IPDFAnnotationEditorLayerFactory,
|
||||
IPDFAnnotationLayerFactory,
|
||||
IPDFLinkService,
|
||||
IPDFStructTreeLayerFactory,
|
||||
|
@ -81,6 +81,7 @@ const DEFAULT_L10N_STRINGS = {
|
||||
printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
|
||||
web_fonts_disabled:
|
||||
"Web fonts are disabled: unable to use embedded PDF fonts.",
|
||||
freetext_default_content: "Enter some text…",
|
||||
};
|
||||
|
||||
function getL10nFallback(key, args) {
|
||||
|
@ -22,6 +22,8 @@
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
||||
@ -72,6 +74,7 @@ import { NullL10n } from "./l10n_utils.js";
|
||||
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
||||
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
||||
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory
|
||||
* @property {IPDFAnnotationEditorLayerFactory} annotationEditorLayerFactory
|
||||
* @property {IPDFXfaLayerFactory} xfaLayerFactory
|
||||
* @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
|
||||
* @property {Object} [textHighlighterFactory]
|
||||
@ -128,6 +131,7 @@ class PDFPageView {
|
||||
this.renderingQueue = options.renderingQueue;
|
||||
this.textLayerFactory = options.textLayerFactory;
|
||||
this.annotationLayerFactory = options.annotationLayerFactory;
|
||||
this.annotationEditorLayerFactory = options.annotationEditorLayerFactory;
|
||||
this.xfaLayerFactory = options.xfaLayerFactory;
|
||||
this.textHighlighter =
|
||||
options.textHighlighterFactory?.createTextHighlighter(
|
||||
@ -148,6 +152,7 @@ class PDFPageView {
|
||||
this._annotationCanvasMap = null;
|
||||
|
||||
this.annotationLayer = null;
|
||||
this.annotationEditorLayer = null;
|
||||
this.textLayer = null;
|
||||
this.zoomLayer = null;
|
||||
this.xfaLayer = null;
|
||||
@ -204,6 +209,24 @@ class PDFPageView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _renderAnnotationEditorLayer() {
|
||||
let error = null;
|
||||
try {
|
||||
await this.annotationEditorLayer.render(this.viewport, "display");
|
||||
} catch (ex) {
|
||||
error = ex;
|
||||
} finally {
|
||||
this.eventBus.dispatch("annotationeditorlayerrendered", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -259,9 +282,14 @@ class PDFPageView {
|
||||
reset({
|
||||
keepZoomLayer = false,
|
||||
keepAnnotationLayer = false,
|
||||
keepAnnotationEditorLayer = false,
|
||||
keepXfaLayer = false,
|
||||
} = {}) {
|
||||
this.cancelRendering({ keepAnnotationLayer, keepXfaLayer });
|
||||
this.cancelRendering({
|
||||
keepAnnotationLayer,
|
||||
keepAnnotationEditorLayer,
|
||||
keepXfaLayer,
|
||||
});
|
||||
this.renderingState = RenderingStates.INITIAL;
|
||||
|
||||
const div = this.div;
|
||||
@ -272,12 +300,15 @@ class PDFPageView {
|
||||
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
|
||||
annotationLayerNode =
|
||||
(keepAnnotationLayer && this.annotationLayer?.div) || null,
|
||||
annotationEditorLayerNode =
|
||||
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
|
||||
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
|
||||
for (let i = childNodes.length - 1; i >= 0; i--) {
|
||||
const node = childNodes[i];
|
||||
switch (node) {
|
||||
case zoomLayerNode:
|
||||
case annotationLayerNode:
|
||||
case annotationEditorLayerNode:
|
||||
case xfaLayerNode:
|
||||
continue;
|
||||
}
|
||||
@ -290,6 +321,12 @@ class PDFPageView {
|
||||
// so they are not displayed on the already resized page.
|
||||
this.annotationLayer.hide();
|
||||
}
|
||||
|
||||
if (annotationEditorLayerNode) {
|
||||
this.annotationEditorLayer.hide();
|
||||
} else {
|
||||
this.annotationEditorLayer?.destroy();
|
||||
}
|
||||
if (xfaLayerNode) {
|
||||
// Hide the XFA layer until all elements are resized
|
||||
// so they are not displayed on the already resized page.
|
||||
@ -347,6 +384,7 @@ class PDFPageView {
|
||||
this.cssTransform({
|
||||
target: this.svg,
|
||||
redrawAnnotationLayer: true,
|
||||
redrawAnnotationEditorLayer: true,
|
||||
redrawXfaLayer: true,
|
||||
});
|
||||
|
||||
@ -380,6 +418,7 @@ class PDFPageView {
|
||||
this.cssTransform({
|
||||
target: this.canvas,
|
||||
redrawAnnotationLayer: true,
|
||||
redrawAnnotationEditorLayer: true,
|
||||
redrawXfaLayer: true,
|
||||
});
|
||||
|
||||
@ -403,6 +442,7 @@ class PDFPageView {
|
||||
this.reset({
|
||||
keepZoomLayer: true,
|
||||
keepAnnotationLayer: true,
|
||||
keepAnnotationEditorLayer: true,
|
||||
keepXfaLayer: true,
|
||||
});
|
||||
}
|
||||
@ -411,7 +451,11 @@ class PDFPageView {
|
||||
* PLEASE NOTE: Most likely you want to use the `this.reset()` method,
|
||||
* rather than calling this one directly.
|
||||
*/
|
||||
cancelRendering({ keepAnnotationLayer = false, keepXfaLayer = false } = {}) {
|
||||
cancelRendering({
|
||||
keepAnnotationLayer = false,
|
||||
keepAnnotationEditorLayer = false,
|
||||
keepXfaLayer = false,
|
||||
} = {}) {
|
||||
if (this.paintTask) {
|
||||
this.paintTask.cancel();
|
||||
this.paintTask = null;
|
||||
@ -430,6 +474,13 @@ class PDFPageView {
|
||||
this.annotationLayer = null;
|
||||
this._annotationCanvasMap = null;
|
||||
}
|
||||
if (
|
||||
this.annotationEditorLayer &&
|
||||
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
|
||||
) {
|
||||
this.annotationEditorLayer.cancel();
|
||||
this.annotationEditorLayer = null;
|
||||
}
|
||||
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
|
||||
this.xfaLayer.cancel();
|
||||
this.xfaLayer = null;
|
||||
@ -444,6 +495,7 @@ class PDFPageView {
|
||||
cssTransform({
|
||||
target,
|
||||
redrawAnnotationLayer = false,
|
||||
redrawAnnotationEditorLayer = false,
|
||||
redrawXfaLayer = false,
|
||||
}) {
|
||||
// Scale target (canvas or svg), its wrapper and page container.
|
||||
@ -517,6 +569,9 @@ class PDFPageView {
|
||||
if (redrawAnnotationLayer && this.annotationLayer) {
|
||||
this._renderAnnotationLayer();
|
||||
}
|
||||
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
|
||||
this._renderAnnotationEditorLayer();
|
||||
}
|
||||
if (redrawXfaLayer && this.xfaLayer) {
|
||||
this._renderXfaLayer();
|
||||
}
|
||||
@ -567,9 +622,12 @@ class PDFPageView {
|
||||
canvasWrapper.style.height = div.style.height;
|
||||
canvasWrapper.classList.add("canvasWrapper");
|
||||
|
||||
if (this.annotationLayer?.div) {
|
||||
const lastDivBeforeTextDiv =
|
||||
this.annotationLayer?.div || this.annotationEditorLayer?.div;
|
||||
|
||||
if (lastDivBeforeTextDiv) {
|
||||
// The annotation layer needs to stay on top.
|
||||
div.insertBefore(canvasWrapper, this.annotationLayer.div);
|
||||
div.insertBefore(canvasWrapper, lastDivBeforeTextDiv);
|
||||
} else {
|
||||
div.appendChild(canvasWrapper);
|
||||
}
|
||||
@ -580,9 +638,9 @@ class PDFPageView {
|
||||
textLayerDiv.className = "textLayer";
|
||||
textLayerDiv.style.width = canvasWrapper.style.width;
|
||||
textLayerDiv.style.height = canvasWrapper.style.height;
|
||||
if (this.annotationLayer?.div) {
|
||||
if (lastDivBeforeTextDiv) {
|
||||
// The annotation layer needs to stay on top.
|
||||
div.insertBefore(textLayerDiv, this.annotationLayer.div);
|
||||
div.insertBefore(textLayerDiv, lastDivBeforeTextDiv);
|
||||
} else {
|
||||
div.appendChild(textLayerDiv);
|
||||
}
|
||||
@ -693,7 +751,18 @@ class PDFPageView {
|
||||
}
|
||||
|
||||
if (this.annotationLayer) {
|
||||
this._renderAnnotationLayer();
|
||||
this._renderAnnotationLayer().then(() => {
|
||||
if (this.annotationEditorLayerFactory) {
|
||||
this.annotationEditorLayer ||=
|
||||
this.annotationEditorLayerFactory.createAnnotationEditorLayerBuilder(
|
||||
div,
|
||||
pdfPage,
|
||||
this.l10n,
|
||||
/* annotationStorage = */ null
|
||||
);
|
||||
this._renderAnnotationEditorLayer();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -15,6 +15,7 @@
|
||||
@import url(text_layer_builder.css);
|
||||
@import url(annotation_layer_builder.css);
|
||||
@import url(xfa_layer_builder.css);
|
||||
@import url(annotation_editor_layer_builder.css);
|
||||
|
||||
:root {
|
||||
--viewer-container-height: 0;
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
MIN_SCALE,
|
||||
noContextMenuHandler,
|
||||
} from "./ui_utils.js";
|
||||
import { AnnotationEditorType } from "pdfjs-lib";
|
||||
|
||||
const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
|
||||
|
||||
@ -43,6 +44,9 @@ const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
|
||||
* @property {HTMLButtonElement} openFile - Button to open a new document.
|
||||
* @property {HTMLButtonElement} presentationModeButton - Button to switch to
|
||||
* presentation mode.
|
||||
* @property {HTMLButtonElement} editorNoneButton - Button to disable editing.
|
||||
* @property {HTMLButtonElement} editorFreeTextButton - Button to switch to Free
|
||||
* Text edition.
|
||||
* @property {HTMLButtonElement} download - Button to download the document.
|
||||
* @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
|
||||
* to the current location in the document.
|
||||
@ -70,6 +74,16 @@ class Toolbar {
|
||||
},
|
||||
{ element: options.download, eventName: "download" },
|
||||
{ element: options.viewBookmark, eventName: null },
|
||||
{
|
||||
element: options.editorNoneButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
eventDetails: { mode: AnnotationEditorType.NONE },
|
||||
},
|
||||
{
|
||||
element: options.editorFreeTextButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
eventDetails: { mode: AnnotationEditorType.FREETEXT },
|
||||
},
|
||||
];
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.buttons.push({ element: options.openFile, eventName: "openfile" });
|
||||
@ -89,7 +103,7 @@ class Toolbar {
|
||||
this.reset();
|
||||
|
||||
// Bind the event listeners for click and various other actions.
|
||||
this._bindListeners();
|
||||
this._bindListeners(options);
|
||||
}
|
||||
|
||||
setPageNumber(pageNumber, pageLabel) {
|
||||
@ -121,15 +135,21 @@ class Toolbar {
|
||||
this.updateLoadingIndicatorState();
|
||||
}
|
||||
|
||||
_bindListeners() {
|
||||
_bindListeners(options) {
|
||||
const { pageNumber, scaleSelect } = this.items;
|
||||
const self = this;
|
||||
|
||||
// The buttons within the toolbar.
|
||||
for (const { element, eventName } of this.buttons) {
|
||||
for (const { element, eventName, eventDetails } of this.buttons) {
|
||||
element.addEventListener("click", evt => {
|
||||
if (eventName !== null) {
|
||||
this.eventBus.dispatch(eventName, { source: this });
|
||||
const details = { source: this };
|
||||
if (eventDetails) {
|
||||
for (const property in eventDetails) {
|
||||
details[property] = eventDetails[property];
|
||||
}
|
||||
}
|
||||
this.eventBus.dispatch(eventName, details);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -174,6 +194,23 @@ class Toolbar {
|
||||
this.#adjustScaleWidth();
|
||||
this._updateUIState(true);
|
||||
});
|
||||
|
||||
this.#bindEditorToolsListener(options);
|
||||
}
|
||||
|
||||
#bindEditorToolsListener({ editorNoneButton, editorFreeTextButton }) {
|
||||
this.eventBus._on("annotationeditormodechanged", evt => {
|
||||
const editorButtons = [
|
||||
[AnnotationEditorType.NONE, editorNoneButton],
|
||||
[AnnotationEditorType.FREETEXT, editorFreeTextButton],
|
||||
];
|
||||
|
||||
for (const [mode, button] of editorButtons) {
|
||||
const checked = mode === evt.mode;
|
||||
button.classList.toggle("toggled", checked);
|
||||
button.setAttribute("aria-checked", checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateUIState(resetNumPages = false) {
|
||||
|
@ -71,6 +71,8 @@
|
||||
--loading-icon: url(images/loading.svg);
|
||||
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
|
||||
--treeitem-collapsed-icon: url(images/treeitem-collapsed.svg);
|
||||
--toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg);
|
||||
--toolbarButton-editorNone-icon: url(images/toolbarButton-editorNone.svg);
|
||||
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
|
||||
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
|
||||
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
|
||||
@ -824,6 +826,14 @@ select {
|
||||
mask-image: var(--toolbarButton-presentationMode-icon);
|
||||
}
|
||||
|
||||
#editorNone::before {
|
||||
mask-image: var(--toolbarButton-editorNone-icon);
|
||||
}
|
||||
|
||||
#editorFreeText::before {
|
||||
mask-image: var(--toolbarButton-editorFreeText-icon);
|
||||
}
|
||||
|
||||
#print::before,
|
||||
#secondaryPrint::before {
|
||||
mask-image: var(--toolbarButton-print-icon);
|
||||
|
@ -263,30 +263,39 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<span id="numPages" class="toolbarLabel"></span>
|
||||
</div>
|
||||
<div id="toolbarViewerRight">
|
||||
<button id="presentationMode" class="toolbarButton hiddenLargeView" title="Switch to Presentation Mode" tabindex="31" data-l10n-id="presentation_mode">
|
||||
<div id="editorModeButtons" class="splitToolbarButton hidden" role="radiogroup">
|
||||
<button id="editorNone" class="toolbarButton" title="Disable Annotation Editing" role="radio" aria-checked="false" tabindex="31" data-l10n-id="editor_none">
|
||||
<span data-l10n-id="editor_none_label">Disable Editing</span>
|
||||
</button>
|
||||
<button id="editorFreeText" class="toolbarButton" title="Add FreeText Annotation" role="radio" aria-checked="false" tabindex="32" data-l10n-id="editor_free_text">
|
||||
<span data-l10n-id="editor_free_text_label">FreeText Annotation</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button id="presentationMode" class="toolbarButton hiddenLargeView" title="Switch to Presentation Mode" tabindex="43" data-l10n-id="presentation_mode">
|
||||
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
|
||||
</button>
|
||||
|
||||
<!--#if GENERIC-->
|
||||
<button id="openFile" class="toolbarButton hiddenLargeView" title="Open File" tabindex="32" data-l10n-id="open_file">
|
||||
<button id="openFile" class="toolbarButton hiddenLargeView" title="Open File" tabindex="44" data-l10n-id="open_file">
|
||||
<span data-l10n-id="open_file_label">Open</span>
|
||||
</button>
|
||||
<!--#endif-->
|
||||
|
||||
<button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print">
|
||||
<button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="45" data-l10n-id="print">
|
||||
<span data-l10n-id="print_label">Print</span>
|
||||
</button>
|
||||
|
||||
<button id="download" class="toolbarButton hiddenMediumView" title="Download" tabindex="34" data-l10n-id="download">
|
||||
<button id="download" class="toolbarButton hiddenMediumView" title="Download" tabindex="46" data-l10n-id="download">
|
||||
<span data-l10n-id="download_label">Download</span>
|
||||
</button>
|
||||
<a href="#" id="viewBookmark" class="toolbarButton hiddenSmallView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
|
||||
<a href="#" id="viewBookmark" class="toolbarButton hiddenSmallView" title="Current view (copy or open in new window)" tabindex="47" data-l10n-id="bookmark">
|
||||
<span data-l10n-id="bookmark_label">Current View</span>
|
||||
</a>
|
||||
|
||||
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
||||
|
||||
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="36" data-l10n-id="tools" aria-expanded="false" aria-controls="secondaryToolbar">
|
||||
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="48" data-l10n-id="tools" aria-expanded="false" aria-controls="secondaryToolbar">
|
||||
<span data-l10n-id="tools_label">Tools</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -93,6 +93,8 @@ function getViewerConfiguration() {
|
||||
? document.getElementById("openFile")
|
||||
: null,
|
||||
print: document.getElementById("print"),
|
||||
editorFreeTextButton: document.getElementById("editorFreeText"),
|
||||
editorNoneButton: document.getElementById("editorNone"),
|
||||
presentationModeButton: document.getElementById("presentationMode"),
|
||||
download: document.getElementById("download"),
|
||||
viewBookmark: document.getElementById("viewBookmark"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user