[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
|
"default": 2
|
||||||
},
|
},
|
||||||
|
"annotationEditorEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"enablePermissions": {
|
"enablePermissions": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
@ -249,3 +249,10 @@ password_cancel=Cancel
|
|||||||
printing_not_supported=Warning: Printing is not fully supported by this browser.
|
printing_not_supported=Warning: Printing is not fully supported by this browser.
|
||||||
printing_not_ready=Warning: The PDF is not fully loaded for printing.
|
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.
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { AnnotationEditor } from "./editor/editor.js";
|
||||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||||
import { objectFromMap } from "../shared/util.js";
|
import { objectFromMap } from "../shared/util.js";
|
||||||
|
|
||||||
@ -62,6 +63,14 @@ class AnnotationStorage {
|
|||||||
return this._storage.get(key);
|
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
|
* Set the value for a given key
|
||||||
*
|
*
|
||||||
@ -123,7 +132,19 @@ class AnnotationStorage {
|
|||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
get serializable() {
|
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 */
|
/** @typedef {import("./display/display_utils").PageViewport} PageViewport */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AnnotationEditorType,
|
||||||
AnnotationMode,
|
AnnotationMode,
|
||||||
CMapCompressionType,
|
CMapCompressionType,
|
||||||
createPromiseCapability,
|
createPromiseCapability,
|
||||||
@ -56,6 +57,8 @@ import {
|
|||||||
PixelsPerInch,
|
PixelsPerInch,
|
||||||
RenderingCancelledException,
|
RenderingCancelledException,
|
||||||
} from "./display/display_utils.js";
|
} 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 { AnnotationLayer } from "./display/annotation_layer.js";
|
||||||
import { GlobalWorkerOptions } from "./display/worker_options.js";
|
import { GlobalWorkerOptions } from "./display/worker_options.js";
|
||||||
import { isNodeJS } from "./shared/is_node.js";
|
import { isNodeJS } from "./shared/is_node.js";
|
||||||
@ -104,6 +107,9 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AnnotationEditorLayer,
|
||||||
|
AnnotationEditorType,
|
||||||
|
AnnotationEditorUIManager,
|
||||||
AnnotationLayer,
|
AnnotationLayer,
|
||||||
AnnotationMode,
|
AnnotationMode,
|
||||||
build,
|
build,
|
||||||
|
@ -51,6 +51,13 @@ const AnnotationMode = {
|
|||||||
ENABLE_STORAGE: 3,
|
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.
|
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
|
||||||
const PermissionFlag = {
|
const PermissionFlag = {
|
||||||
PRINT: 0x04,
|
PRINT: 0x04,
|
||||||
@ -1135,6 +1142,8 @@ export {
|
|||||||
AbortException,
|
AbortException,
|
||||||
AnnotationActionEventType,
|
AnnotationActionEventType,
|
||||||
AnnotationBorderStyleType,
|
AnnotationBorderStyleType,
|
||||||
|
AnnotationEditorPrefix,
|
||||||
|
AnnotationEditorType,
|
||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
AnnotationMarkedState,
|
AnnotationMarkedState,
|
||||||
|
@ -30,6 +30,7 @@ async function runTests(results) {
|
|||||||
"annotation_spec.js",
|
"annotation_spec.js",
|
||||||
"accessibility_spec.js",
|
"accessibility_spec.js",
|
||||||
"find_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
|
// Avoid popup when saving is done
|
||||||
"browser.download.always_ask_before_handling_new_types": true,
|
"browser.download.always_ask_before_handling_new_types": true,
|
||||||
"browser.download.panel.shown": true,
|
"browser.download.panel.shown": true,
|
||||||
|
"browser.download.alwaysOpenPanel": false,
|
||||||
// Save file in output
|
// Save file in output
|
||||||
"browser.download.folderList": 2,
|
"browser.download.folderList": 2,
|
||||||
"browser.download.dir": tempDir,
|
"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,
|
l10n: this.l10n,
|
||||||
textLayerMode: AppOptions.get("textLayerMode"),
|
textLayerMode: AppOptions.get("textLayerMode"),
|
||||||
annotationMode: AppOptions.get("annotationMode"),
|
annotationMode: AppOptions.get("annotationMode"),
|
||||||
|
annotationEditorEnabled: AppOptions.get("annotationEditorEnabled"),
|
||||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||||
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
|
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
|
||||||
@ -560,6 +561,10 @@ const PDFViewerApplication = {
|
|||||||
this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n);
|
this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (AppOptions.get("annotationEditorEnabled")) {
|
||||||
|
document.getElementById("editorModeButtons").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
this.pdfDocumentProperties = new PDFDocumentProperties(
|
this.pdfDocumentProperties = new PDFDocumentProperties(
|
||||||
appConfig.documentProperties,
|
appConfig.documentProperties,
|
||||||
this.overlayManager,
|
this.overlayManager,
|
||||||
@ -1878,6 +1883,10 @@ const PDFViewerApplication = {
|
|||||||
eventBus._on("namedaction", webViewerNamedAction);
|
eventBus._on("namedaction", webViewerNamedAction);
|
||||||
eventBus._on("presentationmodechanged", webViewerPresentationModeChanged);
|
eventBus._on("presentationmodechanged", webViewerPresentationModeChanged);
|
||||||
eventBus._on("presentationmode", webViewerPresentationMode);
|
eventBus._on("presentationmode", webViewerPresentationMode);
|
||||||
|
eventBus._on(
|
||||||
|
"switchannotationeditormode",
|
||||||
|
webViewerSwitchAnnotationEditorMode
|
||||||
|
);
|
||||||
eventBus._on("print", webViewerPrint);
|
eventBus._on("print", webViewerPrint);
|
||||||
eventBus._on("download", webViewerDownload);
|
eventBus._on("download", webViewerDownload);
|
||||||
eventBus._on("firstpage", webViewerFirstPage);
|
eventBus._on("firstpage", webViewerFirstPage);
|
||||||
@ -2459,6 +2468,13 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|||||||
function webViewerPresentationMode() {
|
function webViewerPresentationMode() {
|
||||||
PDFViewerApplication.requestPresentationMode();
|
PDFViewerApplication.requestPresentationMode();
|
||||||
}
|
}
|
||||||
|
function webViewerSwitchAnnotationEditorMode(evt) {
|
||||||
|
if (evt.toggle) {
|
||||||
|
PDFViewerApplication.pdfViewer.annotionEditorEnabled = true;
|
||||||
|
} else {
|
||||||
|
PDFViewerApplication.pdfViewer.annotationEditorMode = evt.mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
function webViewerPrint() {
|
function webViewerPrint() {
|
||||||
PDFViewerApplication.triggerPrinting();
|
PDFViewerApplication.triggerPrinting();
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,11 @@ const defaultOptions = {
|
|||||||
value: 2,
|
value: 2,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
annotationEditorEnabled: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
cursorToolOnLoad: {
|
cursorToolOnLoad: {
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
value: 0,
|
value: 0,
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
|
||||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
||||||
@ -30,6 +32,8 @@
|
|||||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AnnotationEditorType,
|
||||||
|
AnnotationEditorUIManager,
|
||||||
AnnotationMode,
|
AnnotationMode,
|
||||||
createPromiseCapability,
|
createPromiseCapability,
|
||||||
PermissionFlag,
|
PermissionFlag,
|
||||||
@ -61,6 +65,7 @@ import {
|
|||||||
VERTICAL_PADDING,
|
VERTICAL_PADDING,
|
||||||
watchScroll,
|
watchScroll,
|
||||||
} from "./ui_utils.js";
|
} from "./ui_utils.js";
|
||||||
|
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
||||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||||
import { NullL10n } from "./l10n_utils.js";
|
import { NullL10n } from "./l10n_utils.js";
|
||||||
import { PDFPageView } from "./pdf_page_view.js";
|
import { PDFPageView } from "./pdf_page_view.js";
|
||||||
@ -104,6 +109,8 @@ const PagesCountLimit = {
|
|||||||
* being rendered. The constants from {@link AnnotationMode} should be used;
|
* being rendered. The constants from {@link AnnotationMode} should be used;
|
||||||
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
||||||
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
* 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
|
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
||||||
* mainly for annotation icons. Include trailing slash.
|
* mainly for annotation icons. Include trailing slash.
|
||||||
* @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
|
* @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
|
||||||
@ -194,6 +201,7 @@ class PDFPageViewBuffer {
|
|||||||
* Simple viewer control to display PDF content/pages.
|
* Simple viewer control to display PDF content/pages.
|
||||||
*
|
*
|
||||||
* @implements {IPDFAnnotationLayerFactory}
|
* @implements {IPDFAnnotationLayerFactory}
|
||||||
|
* @implements {IPDFAnnotationEditorLayerFactory}
|
||||||
* @implements {IPDFStructTreeLayerFactory}
|
* @implements {IPDFStructTreeLayerFactory}
|
||||||
* @implements {IPDFTextLayerFactory}
|
* @implements {IPDFTextLayerFactory}
|
||||||
* @implements {IPDFXfaLayerFactory}
|
* @implements {IPDFXfaLayerFactory}
|
||||||
@ -201,6 +209,10 @@ class PDFPageViewBuffer {
|
|||||||
class BaseViewer {
|
class BaseViewer {
|
||||||
#buffer = null;
|
#buffer = null;
|
||||||
|
|
||||||
|
#annotationEditorMode = AnnotationEditorType.NONE;
|
||||||
|
|
||||||
|
#annotationEditorUIManager = null;
|
||||||
|
|
||||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||||
|
|
||||||
#previousAnnotationMode = null;
|
#previousAnnotationMode = null;
|
||||||
@ -268,6 +280,10 @@ class BaseViewer {
|
|||||||
this.#enablePermissions = options.enablePermissions || false;
|
this.#enablePermissions = options.enablePermissions || false;
|
||||||
this.pageColors = options.pageColors || null;
|
this.pageColors = options.pageColors || null;
|
||||||
|
|
||||||
|
if (options.annotationEditorEnabled === true) {
|
||||||
|
this.#annotationEditorUIManager = new AnnotationEditorUIManager();
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||||
if (
|
if (
|
||||||
this.pageColors &&
|
this.pageColors &&
|
||||||
@ -699,6 +715,9 @@ class BaseViewer {
|
|||||||
const annotationLayerFactory =
|
const annotationLayerFactory =
|
||||||
this.#annotationMode !== AnnotationMode.DISABLE ? this : null;
|
this.#annotationMode !== AnnotationMode.DISABLE ? this : null;
|
||||||
const xfaLayerFactory = isPureXfa ? this : null;
|
const xfaLayerFactory = isPureXfa ? this : null;
|
||||||
|
const annotationEditorLayerFactory = this.#annotationEditorUIManager
|
||||||
|
? this
|
||||||
|
: null;
|
||||||
|
|
||||||
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
||||||
const pageView = new PDFPageView({
|
const pageView = new PDFPageView({
|
||||||
@ -714,6 +733,7 @@ class BaseViewer {
|
|||||||
annotationLayerFactory,
|
annotationLayerFactory,
|
||||||
annotationMode: this.#annotationMode,
|
annotationMode: this.#annotationMode,
|
||||||
xfaLayerFactory,
|
xfaLayerFactory,
|
||||||
|
annotationEditorLayerFactory,
|
||||||
textHighlighterFactory: this,
|
textHighlighterFactory: this,
|
||||||
structTreeLayerFactory: this,
|
structTreeLayerFactory: this,
|
||||||
imageResourcesPath: this.imageResourcesPath,
|
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 {HTMLDivElement} pageDiv
|
||||||
* @param {PDFPageProxy} pdfPage
|
* @param {PDFPageProxy} pdfPage
|
||||||
@ -2072,6 +2116,36 @@ class BaseViewer {
|
|||||||
docStyle.setProperty("--viewer-container-height", `${height}px`);
|
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 };
|
export { BaseViewer, PagesCountLimit, PDFPageViewBuffer };
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
|
||||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
||||||
@ -29,6 +31,7 @@
|
|||||||
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
|
||||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||||
|
|
||||||
|
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
||||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||||
import { NullL10n } from "./l10n_utils.js";
|
import { NullL10n } from "./l10n_utils.js";
|
||||||
import { SimpleLinkService } from "./pdf_link_service.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
|
* @implements IPDFStructTreeLayerFactory
|
||||||
*/
|
*/
|
||||||
@ -161,6 +190,7 @@ class DefaultXfaLayerFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
DefaultAnnotationEditorLayerFactory,
|
||||||
DefaultAnnotationLayerFactory,
|
DefaultAnnotationLayerFactory,
|
||||||
DefaultStructTreeLayerFactory,
|
DefaultStructTreeLayerFactory,
|
||||||
DefaultTextLayerFactory,
|
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 */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */
|
/** @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 */
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
/** @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
|
* @interface
|
||||||
*/
|
*/
|
||||||
@ -307,6 +329,7 @@ class IL10n {
|
|||||||
export {
|
export {
|
||||||
IDownloadManager,
|
IDownloadManager,
|
||||||
IL10n,
|
IL10n,
|
||||||
|
IPDFAnnotationEditorLayerFactory,
|
||||||
IPDFAnnotationLayerFactory,
|
IPDFAnnotationLayerFactory,
|
||||||
IPDFLinkService,
|
IPDFLinkService,
|
||||||
IPDFStructTreeLayerFactory,
|
IPDFStructTreeLayerFactory,
|
||||||
|
@ -81,6 +81,7 @@ const DEFAULT_L10N_STRINGS = {
|
|||||||
printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
|
printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
|
||||||
web_fonts_disabled:
|
web_fonts_disabled:
|
||||||
"Web fonts are disabled: unable to use embedded PDF fonts.",
|
"Web fonts are disabled: unable to use embedded PDF fonts.",
|
||||||
|
freetext_default_content: "Enter some text…",
|
||||||
};
|
};
|
||||||
|
|
||||||
function getL10nFallback(key, args) {
|
function getL10nFallback(key, args) {
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
|
||||||
@ -72,6 +74,7 @@ import { NullL10n } from "./l10n_utils.js";
|
|||||||
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
||||||
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
||||||
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory
|
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory
|
||||||
|
* @property {IPDFAnnotationEditorLayerFactory} annotationEditorLayerFactory
|
||||||
* @property {IPDFXfaLayerFactory} xfaLayerFactory
|
* @property {IPDFXfaLayerFactory} xfaLayerFactory
|
||||||
* @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
|
* @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
|
||||||
* @property {Object} [textHighlighterFactory]
|
* @property {Object} [textHighlighterFactory]
|
||||||
@ -128,6 +131,7 @@ class PDFPageView {
|
|||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
this.textLayerFactory = options.textLayerFactory;
|
this.textLayerFactory = options.textLayerFactory;
|
||||||
this.annotationLayerFactory = options.annotationLayerFactory;
|
this.annotationLayerFactory = options.annotationLayerFactory;
|
||||||
|
this.annotationEditorLayerFactory = options.annotationEditorLayerFactory;
|
||||||
this.xfaLayerFactory = options.xfaLayerFactory;
|
this.xfaLayerFactory = options.xfaLayerFactory;
|
||||||
this.textHighlighter =
|
this.textHighlighter =
|
||||||
options.textHighlighterFactory?.createTextHighlighter(
|
options.textHighlighterFactory?.createTextHighlighter(
|
||||||
@ -148,6 +152,7 @@ class PDFPageView {
|
|||||||
this._annotationCanvasMap = null;
|
this._annotationCanvasMap = null;
|
||||||
|
|
||||||
this.annotationLayer = null;
|
this.annotationLayer = null;
|
||||||
|
this.annotationEditorLayer = null;
|
||||||
this.textLayer = null;
|
this.textLayer = null;
|
||||||
this.zoomLayer = null;
|
this.zoomLayer = null;
|
||||||
this.xfaLayer = 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
|
* @private
|
||||||
*/
|
*/
|
||||||
@ -259,9 +282,14 @@ class PDFPageView {
|
|||||||
reset({
|
reset({
|
||||||
keepZoomLayer = false,
|
keepZoomLayer = false,
|
||||||
keepAnnotationLayer = false,
|
keepAnnotationLayer = false,
|
||||||
|
keepAnnotationEditorLayer = false,
|
||||||
keepXfaLayer = false,
|
keepXfaLayer = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.cancelRendering({ keepAnnotationLayer, keepXfaLayer });
|
this.cancelRendering({
|
||||||
|
keepAnnotationLayer,
|
||||||
|
keepAnnotationEditorLayer,
|
||||||
|
keepXfaLayer,
|
||||||
|
});
|
||||||
this.renderingState = RenderingStates.INITIAL;
|
this.renderingState = RenderingStates.INITIAL;
|
||||||
|
|
||||||
const div = this.div;
|
const div = this.div;
|
||||||
@ -272,12 +300,15 @@ class PDFPageView {
|
|||||||
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
|
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
|
||||||
annotationLayerNode =
|
annotationLayerNode =
|
||||||
(keepAnnotationLayer && this.annotationLayer?.div) || null,
|
(keepAnnotationLayer && this.annotationLayer?.div) || null,
|
||||||
|
annotationEditorLayerNode =
|
||||||
|
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
|
||||||
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
|
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
|
||||||
for (let i = childNodes.length - 1; i >= 0; i--) {
|
for (let i = childNodes.length - 1; i >= 0; i--) {
|
||||||
const node = childNodes[i];
|
const node = childNodes[i];
|
||||||
switch (node) {
|
switch (node) {
|
||||||
case zoomLayerNode:
|
case zoomLayerNode:
|
||||||
case annotationLayerNode:
|
case annotationLayerNode:
|
||||||
|
case annotationEditorLayerNode:
|
||||||
case xfaLayerNode:
|
case xfaLayerNode:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -290,6 +321,12 @@ class PDFPageView {
|
|||||||
// so they are not displayed on the already resized page.
|
// so they are not displayed on the already resized page.
|
||||||
this.annotationLayer.hide();
|
this.annotationLayer.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (annotationEditorLayerNode) {
|
||||||
|
this.annotationEditorLayer.hide();
|
||||||
|
} else {
|
||||||
|
this.annotationEditorLayer?.destroy();
|
||||||
|
}
|
||||||
if (xfaLayerNode) {
|
if (xfaLayerNode) {
|
||||||
// Hide the XFA layer until all elements are resized
|
// Hide the XFA layer until all elements are resized
|
||||||
// so they are not displayed on the already resized page.
|
// so they are not displayed on the already resized page.
|
||||||
@ -347,6 +384,7 @@ class PDFPageView {
|
|||||||
this.cssTransform({
|
this.cssTransform({
|
||||||
target: this.svg,
|
target: this.svg,
|
||||||
redrawAnnotationLayer: true,
|
redrawAnnotationLayer: true,
|
||||||
|
redrawAnnotationEditorLayer: true,
|
||||||
redrawXfaLayer: true,
|
redrawXfaLayer: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -380,6 +418,7 @@ class PDFPageView {
|
|||||||
this.cssTransform({
|
this.cssTransform({
|
||||||
target: this.canvas,
|
target: this.canvas,
|
||||||
redrawAnnotationLayer: true,
|
redrawAnnotationLayer: true,
|
||||||
|
redrawAnnotationEditorLayer: true,
|
||||||
redrawXfaLayer: true,
|
redrawXfaLayer: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -403,6 +442,7 @@ class PDFPageView {
|
|||||||
this.reset({
|
this.reset({
|
||||||
keepZoomLayer: true,
|
keepZoomLayer: true,
|
||||||
keepAnnotationLayer: true,
|
keepAnnotationLayer: true,
|
||||||
|
keepAnnotationEditorLayer: true,
|
||||||
keepXfaLayer: true,
|
keepXfaLayer: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -411,7 +451,11 @@ class PDFPageView {
|
|||||||
* PLEASE NOTE: Most likely you want to use the `this.reset()` method,
|
* PLEASE NOTE: Most likely you want to use the `this.reset()` method,
|
||||||
* rather than calling this one directly.
|
* rather than calling this one directly.
|
||||||
*/
|
*/
|
||||||
cancelRendering({ keepAnnotationLayer = false, keepXfaLayer = false } = {}) {
|
cancelRendering({
|
||||||
|
keepAnnotationLayer = false,
|
||||||
|
keepAnnotationEditorLayer = false,
|
||||||
|
keepXfaLayer = false,
|
||||||
|
} = {}) {
|
||||||
if (this.paintTask) {
|
if (this.paintTask) {
|
||||||
this.paintTask.cancel();
|
this.paintTask.cancel();
|
||||||
this.paintTask = null;
|
this.paintTask = null;
|
||||||
@ -430,6 +474,13 @@ class PDFPageView {
|
|||||||
this.annotationLayer = null;
|
this.annotationLayer = null;
|
||||||
this._annotationCanvasMap = null;
|
this._annotationCanvasMap = null;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.annotationEditorLayer &&
|
||||||
|
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
|
||||||
|
) {
|
||||||
|
this.annotationEditorLayer.cancel();
|
||||||
|
this.annotationEditorLayer = null;
|
||||||
|
}
|
||||||
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
|
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
|
||||||
this.xfaLayer.cancel();
|
this.xfaLayer.cancel();
|
||||||
this.xfaLayer = null;
|
this.xfaLayer = null;
|
||||||
@ -444,6 +495,7 @@ class PDFPageView {
|
|||||||
cssTransform({
|
cssTransform({
|
||||||
target,
|
target,
|
||||||
redrawAnnotationLayer = false,
|
redrawAnnotationLayer = false,
|
||||||
|
redrawAnnotationEditorLayer = false,
|
||||||
redrawXfaLayer = false,
|
redrawXfaLayer = false,
|
||||||
}) {
|
}) {
|
||||||
// Scale target (canvas or svg), its wrapper and page container.
|
// Scale target (canvas or svg), its wrapper and page container.
|
||||||
@ -517,6 +569,9 @@ class PDFPageView {
|
|||||||
if (redrawAnnotationLayer && this.annotationLayer) {
|
if (redrawAnnotationLayer && this.annotationLayer) {
|
||||||
this._renderAnnotationLayer();
|
this._renderAnnotationLayer();
|
||||||
}
|
}
|
||||||
|
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
|
||||||
|
this._renderAnnotationEditorLayer();
|
||||||
|
}
|
||||||
if (redrawXfaLayer && this.xfaLayer) {
|
if (redrawXfaLayer && this.xfaLayer) {
|
||||||
this._renderXfaLayer();
|
this._renderXfaLayer();
|
||||||
}
|
}
|
||||||
@ -567,9 +622,12 @@ class PDFPageView {
|
|||||||
canvasWrapper.style.height = div.style.height;
|
canvasWrapper.style.height = div.style.height;
|
||||||
canvasWrapper.classList.add("canvasWrapper");
|
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.
|
// The annotation layer needs to stay on top.
|
||||||
div.insertBefore(canvasWrapper, this.annotationLayer.div);
|
div.insertBefore(canvasWrapper, lastDivBeforeTextDiv);
|
||||||
} else {
|
} else {
|
||||||
div.appendChild(canvasWrapper);
|
div.appendChild(canvasWrapper);
|
||||||
}
|
}
|
||||||
@ -580,9 +638,9 @@ class PDFPageView {
|
|||||||
textLayerDiv.className = "textLayer";
|
textLayerDiv.className = "textLayer";
|
||||||
textLayerDiv.style.width = canvasWrapper.style.width;
|
textLayerDiv.style.width = canvasWrapper.style.width;
|
||||||
textLayerDiv.style.height = canvasWrapper.style.height;
|
textLayerDiv.style.height = canvasWrapper.style.height;
|
||||||
if (this.annotationLayer?.div) {
|
if (lastDivBeforeTextDiv) {
|
||||||
// The annotation layer needs to stay on top.
|
// The annotation layer needs to stay on top.
|
||||||
div.insertBefore(textLayerDiv, this.annotationLayer.div);
|
div.insertBefore(textLayerDiv, lastDivBeforeTextDiv);
|
||||||
} else {
|
} else {
|
||||||
div.appendChild(textLayerDiv);
|
div.appendChild(textLayerDiv);
|
||||||
}
|
}
|
||||||
@ -693,7 +751,18 @@ class PDFPageView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.annotationLayer) {
|
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(text_layer_builder.css);
|
||||||
@import url(annotation_layer_builder.css);
|
@import url(annotation_layer_builder.css);
|
||||||
@import url(xfa_layer_builder.css);
|
@import url(xfa_layer_builder.css);
|
||||||
|
@import url(annotation_editor_layer_builder.css);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--viewer-container-height: 0;
|
--viewer-container-height: 0;
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
MIN_SCALE,
|
MIN_SCALE,
|
||||||
noContextMenuHandler,
|
noContextMenuHandler,
|
||||||
} from "./ui_utils.js";
|
} from "./ui_utils.js";
|
||||||
|
import { AnnotationEditorType } from "pdfjs-lib";
|
||||||
|
|
||||||
const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
|
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} openFile - Button to open a new document.
|
||||||
* @property {HTMLButtonElement} presentationModeButton - Button to switch to
|
* @property {HTMLButtonElement} presentationModeButton - Button to switch to
|
||||||
* presentation mode.
|
* 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 {HTMLButtonElement} download - Button to download the document.
|
||||||
* @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
|
* @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
|
||||||
* to the current location in the document.
|
* to the current location in the document.
|
||||||
@ -70,6 +74,16 @@ class Toolbar {
|
|||||||
},
|
},
|
||||||
{ element: options.download, eventName: "download" },
|
{ element: options.download, eventName: "download" },
|
||||||
{ element: options.viewBookmark, eventName: null },
|
{ 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")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
this.buttons.push({ element: options.openFile, eventName: "openfile" });
|
this.buttons.push({ element: options.openFile, eventName: "openfile" });
|
||||||
@ -89,7 +103,7 @@ class Toolbar {
|
|||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
// Bind the event listeners for click and various other actions.
|
// Bind the event listeners for click and various other actions.
|
||||||
this._bindListeners();
|
this._bindListeners(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPageNumber(pageNumber, pageLabel) {
|
setPageNumber(pageNumber, pageLabel) {
|
||||||
@ -121,15 +135,21 @@ class Toolbar {
|
|||||||
this.updateLoadingIndicatorState();
|
this.updateLoadingIndicatorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
_bindListeners() {
|
_bindListeners(options) {
|
||||||
const { pageNumber, scaleSelect } = this.items;
|
const { pageNumber, scaleSelect } = this.items;
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
// The buttons within the toolbar.
|
// The buttons within the toolbar.
|
||||||
for (const { element, eventName } of this.buttons) {
|
for (const { element, eventName, eventDetails } of this.buttons) {
|
||||||
element.addEventListener("click", evt => {
|
element.addEventListener("click", evt => {
|
||||||
if (eventName !== null) {
|
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.#adjustScaleWidth();
|
||||||
this._updateUIState(true);
|
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) {
|
_updateUIState(resetNumPages = false) {
|
||||||
|
@ -71,6 +71,8 @@
|
|||||||
--loading-icon: url(images/loading.svg);
|
--loading-icon: url(images/loading.svg);
|
||||||
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
|
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
|
||||||
--treeitem-collapsed-icon: url(images/treeitem-collapsed.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-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
|
||||||
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
|
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
|
||||||
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
|
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
|
||||||
@ -824,6 +826,14 @@ select {
|
|||||||
mask-image: var(--toolbarButton-presentationMode-icon);
|
mask-image: var(--toolbarButton-presentationMode-icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editorNone::before {
|
||||||
|
mask-image: var(--toolbarButton-editorNone-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editorFreeText::before {
|
||||||
|
mask-image: var(--toolbarButton-editorFreeText-icon);
|
||||||
|
}
|
||||||
|
|
||||||
#print::before,
|
#print::before,
|
||||||
#secondaryPrint::before {
|
#secondaryPrint::before {
|
||||||
mask-image: var(--toolbarButton-print-icon);
|
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>
|
<span id="numPages" class="toolbarLabel"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbarViewerRight">
|
<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>
|
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!--#if GENERIC-->
|
<!--#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>
|
<span data-l10n-id="open_file_label">Open</span>
|
||||||
</button>
|
</button>
|
||||||
<!--#endif-->
|
<!--#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>
|
<span data-l10n-id="print_label">Print</span>
|
||||||
</button>
|
</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>
|
<span data-l10n-id="download_label">Download</span>
|
||||||
</button>
|
</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>
|
<span data-l10n-id="bookmark_label">Current View</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
<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>
|
<span data-l10n-id="tools_label">Tools</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,6 +93,8 @@ function getViewerConfiguration() {
|
|||||||
? document.getElementById("openFile")
|
? document.getElementById("openFile")
|
||||||
: null,
|
: null,
|
||||||
print: document.getElementById("print"),
|
print: document.getElementById("print"),
|
||||||
|
editorFreeTextButton: document.getElementById("editorFreeText"),
|
||||||
|
editorNoneButton: document.getElementById("editorNone"),
|
||||||
presentationModeButton: document.getElementById("presentationMode"),
|
presentationModeButton: document.getElementById("presentationMode"),
|
||||||
download: document.getElementById("download"),
|
download: document.getElementById("download"),
|
||||||
viewBookmark: document.getElementById("viewBookmark"),
|
viewBookmark: document.getElementById("viewBookmark"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user