mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
Merge pull request #16535 from calixteman/restore_freetext
[Editor] Allow to edit FreeText annotations
This commit is contained in:
commit
5581e22cc7
8 changed files with 459 additions and 39 deletions
|
@ -3553,7 +3553,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||
constructor(params) {
|
||||
super(params);
|
||||
|
||||
this.data.hasOwnCanvas = this.data.noRotate;
|
||||
this.data.hasOwnCanvas = true;
|
||||
|
||||
const { xref } = params;
|
||||
this.data.annotationType = AnnotationType.FREETEXT;
|
||||
|
|
|
@ -2629,7 +2629,7 @@ class AnnotationLayer {
|
|||
|
||||
#div = null;
|
||||
|
||||
#editableAnnotations = new Set();
|
||||
#editableAnnotations = new Map();
|
||||
|
||||
constructor({ div, accessibilityManager, annotationCanvasMap }) {
|
||||
this.#div = div;
|
||||
|
@ -2696,7 +2696,7 @@ class AnnotationLayer {
|
|||
}
|
||||
|
||||
if (element.annotationEditorType > 0) {
|
||||
this.#editableAnnotations.add(element);
|
||||
this.#editableAnnotations.set(element.data.id, element);
|
||||
}
|
||||
|
||||
const rendered = element.render();
|
||||
|
@ -2767,7 +2767,11 @@ class AnnotationLayer {
|
|||
}
|
||||
|
||||
getEditableAnnotations() {
|
||||
return this.#editableAnnotations;
|
||||
return Array.from(this.#editableAnnotations.values());
|
||||
}
|
||||
|
||||
getEditableAnnotation(id) {
|
||||
return this.#editableAnnotations.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./editor.js").AnnotationEditor} AnnotationEditor */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
/** @typedef {import("../display_utils.js").PageViewport} PageViewport */
|
||||
|
@ -24,6 +23,7 @@
|
|||
/** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
||||
|
||||
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { bindEvents } from "./tools.js";
|
||||
import { FreeTextEditor } from "./freetext.js";
|
||||
import { InkEditor } from "./ink.js";
|
||||
|
@ -66,6 +66,8 @@ class AnnotationEditorLayer {
|
|||
|
||||
#isCleaningUp = false;
|
||||
|
||||
#isDisabling = false;
|
||||
|
||||
#uiManager;
|
||||
|
||||
static _initialized = false;
|
||||
|
@ -86,6 +88,7 @@ class AnnotationEditorLayer {
|
|||
this.div = options.div;
|
||||
this.#accessibilityManager = options.accessibilityManager;
|
||||
this.#annotationLayer = options.annotationLayer;
|
||||
this.viewport = options.viewport;
|
||||
|
||||
this.#uiManager.addLayer(this);
|
||||
}
|
||||
|
@ -175,18 +178,33 @@ class AnnotationEditorLayer {
|
|||
*/
|
||||
enable() {
|
||||
this.div.style.pointerEvents = "auto";
|
||||
if (this.#annotationLayer) {
|
||||
const editables = this.#annotationLayer.getEditableAnnotations();
|
||||
for (const editable of editables) {
|
||||
const editor = this.deserialize(editable);
|
||||
if (!editor) {
|
||||
continue;
|
||||
}
|
||||
editable.hide();
|
||||
this.addOrRebuild(editor);
|
||||
const annotationElementIds = new Set();
|
||||
for (const editor of this.#editors.values()) {
|
||||
editor.enableEditing();
|
||||
if (editor.annotationElementId) {
|
||||
annotationElementIds.add(editor.annotationElementId);
|
||||
}
|
||||
}
|
||||
for (const editor of this.#editors.values()) {
|
||||
|
||||
if (!this.#annotationLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editables = this.#annotationLayer.getEditableAnnotations();
|
||||
for (const editable of editables) {
|
||||
// The element must be hidden whatever its state is.
|
||||
editable.hide();
|
||||
if (this.#uiManager.isDeletedAnnotationElement(editable.data.id)) {
|
||||
continue;
|
||||
}
|
||||
if (annotationElementIds.has(editable.data.id)) {
|
||||
continue;
|
||||
}
|
||||
const editor = this.deserialize(editable);
|
||||
if (!editor) {
|
||||
continue;
|
||||
}
|
||||
this.addOrRebuild(editor);
|
||||
editor.enableEditing();
|
||||
}
|
||||
}
|
||||
|
@ -195,18 +213,25 @@ class AnnotationEditorLayer {
|
|||
* Disable editor creation.
|
||||
*/
|
||||
disable() {
|
||||
this.#isDisabling = true;
|
||||
this.div.style.pointerEvents = "none";
|
||||
for (const editor of this.#editors.values()) {
|
||||
editor.disableEditing();
|
||||
if (!editor.hasElementChanged()) {
|
||||
editor.annotationElement.show();
|
||||
editor.remove();
|
||||
if (!editor.annotationElementId || editor.serialize() !== null) {
|
||||
continue;
|
||||
}
|
||||
this.getEditableAnnotation(editor.annotationElementId)?.show();
|
||||
editor.remove();
|
||||
}
|
||||
this.#cleanup();
|
||||
if (this.isEmpty) {
|
||||
this.div.hidden = true;
|
||||
}
|
||||
this.#isDisabling = false;
|
||||
}
|
||||
|
||||
getEditableAnnotation(id) {
|
||||
return this.#annotationLayer?.getEditableAnnotation(id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -234,11 +259,22 @@ class AnnotationEditorLayer {
|
|||
|
||||
attach(editor) {
|
||||
this.#editors.set(editor.id, editor);
|
||||
const { annotationElementId } = editor;
|
||||
if (
|
||||
annotationElementId &&
|
||||
this.#uiManager.isDeletedAnnotationElement(annotationElementId)
|
||||
) {
|
||||
this.#uiManager.removeDeletedAnnotationElement(editor);
|
||||
}
|
||||
}
|
||||
|
||||
detach(editor) {
|
||||
this.#editors.delete(editor.id);
|
||||
this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv);
|
||||
|
||||
if (!this.#isDisabling && editor.annotationElementId) {
|
||||
this.#uiManager.addDeletedAnnotationElement(editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -249,8 +285,8 @@ class AnnotationEditorLayer {
|
|||
// 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.#uiManager.removeEditor(editor);
|
||||
editor.div.style.display = "none";
|
||||
setTimeout(() => {
|
||||
// When the div is removed from DOM the focus can move on the
|
||||
|
@ -280,6 +316,12 @@ class AnnotationEditorLayer {
|
|||
return;
|
||||
}
|
||||
|
||||
if (editor.annotationElementId) {
|
||||
this.#uiManager.addDeletedAnnotationElement(editor.annotationElementId);
|
||||
AnnotationEditor.deleteAnnotationElement(editor);
|
||||
editor.annotationElementId = null;
|
||||
}
|
||||
|
||||
this.attach(editor);
|
||||
editor.parent?.detach(editor);
|
||||
editor.setParent(this);
|
||||
|
|
|
@ -67,7 +67,7 @@ class AnnotationEditor {
|
|||
this.name = parameters.name;
|
||||
this.div = null;
|
||||
this._uiManager = parameters.uiManager;
|
||||
this.annotationElement = null;
|
||||
this.annotationElementId = null;
|
||||
|
||||
const {
|
||||
rotation,
|
||||
|
@ -85,6 +85,7 @@ class AnnotationEditor {
|
|||
this.y = parameters.y / height;
|
||||
|
||||
this.isAttachedToDOM = false;
|
||||
this.deleted = false;
|
||||
}
|
||||
|
||||
static get _defaultLineColor() {
|
||||
|
@ -95,6 +96,17 @@ class AnnotationEditor {
|
|||
);
|
||||
}
|
||||
|
||||
static deleteAnnotationElement(editor) {
|
||||
const fakeEditor = new FakeEditor({
|
||||
id: editor.parent.getNextId(),
|
||||
parent: editor.parent,
|
||||
uiManager: editor._uiManager,
|
||||
});
|
||||
fakeEditor.annotationElementId = editor.annotationElementId;
|
||||
fakeEditor.deleted = true;
|
||||
fakeEditor._uiManager.addToAnnotationStorage(fakeEditor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some commands into the CommandManager (undo/redo stuff).
|
||||
* @param {Object} params
|
||||
|
@ -601,14 +613,22 @@ class AnnotationEditor {
|
|||
this.parent.setActiveEditor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the editor has been changed.
|
||||
* @param {Object} serialized
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasElementChanged(serialized = null) {
|
||||
return false;
|
||||
// This class is used to fake an editor which has been deleted.
|
||||
class FakeEditor extends AnnotationEditor {
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.annotationElementId = params.annotationElementId;
|
||||
this.deleted = true;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
id: this.annotationElementId,
|
||||
deleted: true,
|
||||
pageIndex: this.pageIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ class FreeTextEditor extends AnnotationEditor {
|
|||
|
||||
#fontSize;
|
||||
|
||||
#initialData = null;
|
||||
|
||||
static _freeTextDefaultContent = "";
|
||||
|
||||
static _l10nPromise;
|
||||
|
@ -285,6 +287,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||
/** @inheritdoc */
|
||||
onceAdded() {
|
||||
if (this.width) {
|
||||
this.#cheatInitialRect();
|
||||
// The editor was created in using ctrl+c.
|
||||
return;
|
||||
}
|
||||
|
@ -481,12 +484,17 @@ class FreeTextEditor extends AnnotationEditor {
|
|||
if (this.width) {
|
||||
// This editor was created in using copy (ctrl+c).
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setAt(
|
||||
baseX * parentWidth,
|
||||
baseY * parentHeight,
|
||||
this.width * parentWidth,
|
||||
this.height * parentHeight
|
||||
);
|
||||
if (this.annotationElementId) {
|
||||
const [tx] = this.getInitialTranslation();
|
||||
this.setAt(baseX * parentWidth, baseY * parentHeight, tx, tx);
|
||||
} else {
|
||||
this.setAt(
|
||||
baseX * parentWidth,
|
||||
baseY * parentHeight,
|
||||
this.width * parentWidth,
|
||||
this.height * parentHeight
|
||||
);
|
||||
}
|
||||
|
||||
this.#setContent();
|
||||
this.div.draggable = true;
|
||||
|
@ -519,14 +527,37 @@ class FreeTextEditor extends AnnotationEditor {
|
|||
|
||||
/** @inheritdoc */
|
||||
static deserialize(data, parent, uiManager) {
|
||||
let initialData = null;
|
||||
if (data instanceof FreeTextAnnotationElement) {
|
||||
return null;
|
||||
const {
|
||||
data: {
|
||||
defaultAppearanceData: { fontSize, fontColor },
|
||||
rect,
|
||||
rotation,
|
||||
id,
|
||||
},
|
||||
textContent,
|
||||
page: { pageNumber },
|
||||
} = data;
|
||||
initialData = data = {
|
||||
annotationType: AnnotationEditorType.FREETEXT,
|
||||
color: Array.from(fontColor),
|
||||
fontSize,
|
||||
value: textContent.join("\n"),
|
||||
pageIndex: pageNumber - 1,
|
||||
rect,
|
||||
rotation,
|
||||
id,
|
||||
deleted: false,
|
||||
};
|
||||
}
|
||||
const editor = super.deserialize(data, parent, uiManager);
|
||||
|
||||
editor.#fontSize = data.fontSize;
|
||||
editor.#color = Util.makeHexColor(...data.color);
|
||||
editor.#content = data.value;
|
||||
editor.annotationElementId = data.id || null;
|
||||
editor.#initialData = initialData;
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
@ -537,16 +568,23 @@ class FreeTextEditor extends AnnotationEditor {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (this.deleted) {
|
||||
return {
|
||||
pageIndex: this.pageIndex,
|
||||
id: this.annotationElementId,
|
||||
deleted: true,
|
||||
};
|
||||
}
|
||||
|
||||
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
||||
const rect = this.getRect(padding, padding);
|
||||
|
||||
const color = AnnotationEditor._colorManager.convert(
|
||||
this.isAttachedToDOM
|
||||
? getComputedStyle(this.editorDiv).color
|
||||
: this.#color
|
||||
);
|
||||
|
||||
return {
|
||||
const serialized = {
|
||||
annotationType: AnnotationEditorType.FREETEXT,
|
||||
color,
|
||||
fontSize: this.#fontSize,
|
||||
|
@ -554,7 +592,45 @@ class FreeTextEditor extends AnnotationEditor {
|
|||
pageIndex: this.pageIndex,
|
||||
rect,
|
||||
rotation: this.rotation,
|
||||
id: this.annotationElementId,
|
||||
};
|
||||
|
||||
if (this.annotationElementId && !this.#hasElementChanged(serialized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
|
||||
#hasElementChanged(serialized) {
|
||||
const { value, fontSize, color, rect, pageIndex } = this.#initialData;
|
||||
|
||||
return (
|
||||
serialized.value !== value ||
|
||||
serialized.fontSize !== fontSize ||
|
||||
serialized.rect.some((x, i) => Math.abs(x - rect[i]) >= 1) ||
|
||||
serialized.color.some((c, i) => c !== color[i]) ||
|
||||
serialized.pageIndex !== pageIndex
|
||||
);
|
||||
}
|
||||
|
||||
#cheatInitialRect(delayed = false) {
|
||||
// The annotation has a rect but the editor has an other one.
|
||||
// When we want to know if the annotation has changed (e.g. has been moved)
|
||||
// we must compare the editor initial rect with the current one.
|
||||
// So this method is a hack to have a way to compare the real rects.
|
||||
if (!this.annotationElementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#setEditorDimensions();
|
||||
if (!delayed && (this.width === 0 || this.height === 0)) {
|
||||
setTimeout(() => this.#cheatInitialRect(/* delayed = */ true), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
||||
this.#initialData.rect = this.getRect(padding, padding);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -362,6 +362,8 @@ class AnnotationEditorUIManager {
|
|||
|
||||
#currentPageIndex = 0;
|
||||
|
||||
#deletedAnnotationsElementIds = new Set();
|
||||
|
||||
#editorTypes = null;
|
||||
|
||||
#editorsToRescale = new Set();
|
||||
|
@ -554,7 +556,11 @@ class AnnotationEditorUIManager {
|
|||
const editors = [];
|
||||
for (const editor of this.#selectedEditors) {
|
||||
if (!editor.isEmpty()) {
|
||||
editors.push(editor.serialize());
|
||||
const serialized = editor.serialize();
|
||||
// Remove the id from the serialized data because it mustn't be linked
|
||||
// to an existing annotation.
|
||||
delete serialized.id;
|
||||
editors.push(serialized);
|
||||
}
|
||||
}
|
||||
if (editors.length === 0) {
|
||||
|
@ -862,7 +868,39 @@ class AnnotationEditorUIManager {
|
|||
removeEditor(editor) {
|
||||
this.#allEditors.delete(editor.id);
|
||||
this.unselect(editor);
|
||||
this.#annotationStorage?.remove(editor.id);
|
||||
if (
|
||||
!editor.annotationElementId ||
|
||||
!this.#deletedAnnotationsElementIds.has(editor.annotationElementId)
|
||||
) {
|
||||
this.#annotationStorage?.remove(editor.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The annotation element with the given id has been deleted.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
addDeletedAnnotationElement(editor) {
|
||||
this.#deletedAnnotationsElementIds.add(editor.annotationElementId);
|
||||
editor.deleted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the annotation element with the given id has been deleted.
|
||||
* @param {string} annotationElementId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isDeletedAnnotationElement(annotationElementId) {
|
||||
return this.#deletedAnnotationsElementIds.has(annotationElementId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The annotation element with the given id have been restored.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
removeDeletedAnnotationElement(editor) {
|
||||
this.#deletedAnnotationsElementIds.delete(editor.annotationElementId);
|
||||
editor.deleted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue