1
0
Fork 0
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:
calixteman 2023-06-15 18:10:41 +02:00 committed by GitHub
commit 5581e22cc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 459 additions and 39 deletions

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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,
};
}
}

View file

@ -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);
}
}

View file

@ -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;
}
/**