Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editor] Provide an element to render in the annotation layer after a freetext has been edited (bug 1890535) #17914

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 86 additions & 29 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
// eslint-disable-next-line max-len
/** @typedef {import("../../web/interfaces").IDownloadManager} IDownloadManager */
/** @typedef {import("../../web/interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */

import {
AnnotationBorderStyleType,
Expand Down Expand Up @@ -157,6 +159,8 @@ class AnnotationElementFactory {
}

class AnnotationElement {
#updates = null;

#hasBorder = false;

constructor(
Expand Down Expand Up @@ -197,6 +201,52 @@ class AnnotationElement {
return AnnotationElement._hasPopupData(this.data);
}

updateEdited(params) {
if (!this.container) {
return;
}

this.#updates ||= {
rect: this.data.rect.slice(0),
};

const { rect } = params;

if (rect) {
this.#setRectEdited(rect);
}
}

resetEdited() {
if (!this.#updates) {
return;
}
this.#setRectEdited(this.#updates.rect);
this.#updates = null;
}

#setRectEdited(rect) {
const {
container: { style },
data: { rect: currentRect, rotation },
parent: {
viewport: {
rawDims: { pageWidth, pageHeight, pageX, pageY },
},
},
} = this;
currentRect?.splice(0, 4, ...rect);
const { width, height } = getRectDims(rect);
style.left = `${(100 * (rect[0] - pageX)) / pageWidth}%`;
style.top = `${(100 * (pageHeight - rect[3] + pageY)) / pageHeight}%`;
if (rotation === 0) {
style.width = `${(100 * width) / pageWidth}%`;
style.height = `${(100 * height) / pageHeight}%`;
} else {
this.setRotation(rotation);
}
}

/**
* Create an empty container for the annotation's HTML element.
*
Expand All @@ -216,13 +266,14 @@ class AnnotationElement {
if (!(this instanceof WidgetAnnotationElement)) {
container.tabIndex = DEFAULT_TAB_INDEX;
}
const { style } = container;

// The accessibility manager will move the annotation in the DOM in
// order to match the visual ordering.
// But if an annotation is above an other one, then we must draw it
// after the other one whatever the order is in the DOM, hence the
// use of the z-index.
container.style.zIndex = this.parent.zIndex++;
style.zIndex = this.parent.zIndex++;

if (data.popupRef) {
container.setAttribute("aria-haspopup", "dialog");
Expand All @@ -236,8 +287,6 @@ class AnnotationElement {
container.classList.add("norotate");
}

const { pageWidth, pageHeight, pageX, pageY } = viewport.rawDims;

if (!data.rect || this instanceof PopupAnnotationElement) {
const { rotation } = data;
if (!data.hasOwnCanvas && rotation !== 0) {
Expand All @@ -248,35 +297,26 @@ class AnnotationElement {

const { width, height } = getRectDims(data.rect);

// Do *not* modify `data.rect`, since that will corrupt the annotation
// position on subsequent calls to `_createContainer` (see issue 6804).
const rect = Util.normalizeRect([
data.rect[0],
page.view[3] - data.rect[1] + page.view[1],
data.rect[2],
page.view[3] - data.rect[3] + page.view[1],
]);

if (!ignoreBorder && data.borderStyle.width > 0) {
container.style.borderWidth = `${data.borderStyle.width}px`;
style.borderWidth = `${data.borderStyle.width}px`;

const horizontalRadius = data.borderStyle.horizontalCornerRadius;
const verticalRadius = data.borderStyle.verticalCornerRadius;
if (horizontalRadius > 0 || verticalRadius > 0) {
const radius = `calc(${horizontalRadius}px * var(--scale-factor)) / calc(${verticalRadius}px * var(--scale-factor))`;
container.style.borderRadius = radius;
style.borderRadius = radius;
} else if (this instanceof RadioButtonWidgetAnnotationElement) {
const radius = `calc(${width}px * var(--scale-factor)) / calc(${height}px * var(--scale-factor))`;
container.style.borderRadius = radius;
style.borderRadius = radius;
}

switch (data.borderStyle.style) {
case AnnotationBorderStyleType.SOLID:
container.style.borderStyle = "solid";
style.borderStyle = "solid";
break;

case AnnotationBorderStyleType.DASHED:
container.style.borderStyle = "dashed";
style.borderStyle = "dashed";
break;

case AnnotationBorderStyleType.BEVELED:
Expand All @@ -288,7 +328,7 @@ class AnnotationElement {
break;

case AnnotationBorderStyleType.UNDERLINE:
container.style.borderBottomStyle = "solid";
style.borderBottomStyle = "solid";
break;

default:
Expand All @@ -298,24 +338,34 @@ class AnnotationElement {
const borderColor = data.borderColor || null;
if (borderColor) {
this.#hasBorder = true;
container.style.borderColor = Util.makeHexColor(
style.borderColor = Util.makeHexColor(
borderColor[0] | 0,
borderColor[1] | 0,
borderColor[2] | 0
);
} else {
// Transparent (invisible) border, so do not draw it at all.
container.style.borderWidth = 0;
style.borderWidth = 0;
}
}

container.style.left = `${(100 * (rect[0] - pageX)) / pageWidth}%`;
container.style.top = `${(100 * (rect[1] - pageY)) / pageHeight}%`;
// Do *not* modify `data.rect`, since that will corrupt the annotation
// position on subsequent calls to `_createContainer` (see issue 6804).
const rect = Util.normalizeRect([
data.rect[0],
page.view[3] - data.rect[1] + page.view[1],
data.rect[2],
page.view[3] - data.rect[3] + page.view[1],
]);
const { pageWidth, pageHeight, pageX, pageY } = viewport.rawDims;

style.left = `${(100 * (rect[0] - pageX)) / pageWidth}%`;
style.top = `${(100 * (rect[1] - pageY)) / pageHeight}%`;

const { rotation } = data;
if (data.hasOwnCanvas || rotation === 0) {
container.style.width = `${(100 * width) / pageWidth}%`;
container.style.height = `${(100 * height) / pageHeight}%`;
style.width = `${(100 * width) / pageWidth}%`;
style.height = `${(100 * height) / pageHeight}%`;
} else {
this.setRotation(rotation, container);
}
Expand Down Expand Up @@ -2897,6 +2947,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
* @property {Object<string, Array<Object>> | null} [fieldObjects]
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
* @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
*/

/**
Expand All @@ -2913,6 +2964,7 @@ class AnnotationLayer {
div,
accessibilityManager,
annotationCanvasMap,
annotationEditorUIManager,
page,
viewport,
}) {
Expand All @@ -2922,6 +2974,7 @@ class AnnotationLayer {
this.page = page;
this.viewport = viewport;
this.zIndex = 0;
this._annotationEditorUIManager = annotationEditorUIManager;

if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
// For testing purposes.
Expand Down Expand Up @@ -3011,15 +3064,16 @@ class AnnotationLayer {
}
}

if (element.annotationEditorType > 0) {
this.#editableAnnotations.set(element.data.id, element);
}

const rendered = element.render();
if (data.hidden) {
rendered.style.visibility = "hidden";
}
this.#appendElement(rendered, data.id);

if (element.annotationEditorType > 0) {
this.#editableAnnotations.set(element.data.id, element);
this._annotationEditorUIManager?.renderAnnotationElement(element);
}
}

this.#setAnnotationCanvasMap();
Expand Down Expand Up @@ -3051,13 +3105,16 @@ class AnnotationLayer {
continue;
}

canvas.className = "annotationContent";
const { firstChild } = element;
if (!firstChild) {
element.append(canvas);
} else if (firstChild.nodeName === "CANVAS") {
firstChild.replaceWith(canvas);
} else {
} else if (!firstChild.classList.contains("annotationContent")) {
firstChild.before(canvas);
} else {
firstChild.after(canvas);
}
}
this.#annotationCanvasMap.clear();
Expand Down
35 changes: 27 additions & 8 deletions src/display/editor/annotation_editor_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ class AnnotationEditorLayer {
const annotationElementIds = new Set();
for (const editor of this.#editors.values()) {
editor.enableEditing();
editor.show(true);
if (editor.annotationElementId) {
this.#uiManager.removeChangedExistingAnnotation(editor);
annotationElementIds.add(editor.annotationElementId);
}
}
Expand Down Expand Up @@ -283,13 +285,19 @@ class AnnotationEditorLayer {
this.#isDisabling = true;
this.div.tabIndex = -1;
this.togglePointerEvents(false);
const hiddenAnnotationIds = new Set();
const changedAnnotations = new Map();
const resetAnnotations = new Map();
for (const editor of this.#editors.values()) {
editor.disableEditing();
if (!editor.annotationElementId || editor.serialize() !== null) {
hiddenAnnotationIds.add(editor.annotationElementId);
if (!editor.annotationElementId) {
continue;
}
if (editor.serialize() !== null) {
changedAnnotations.set(editor.annotationElementId, editor);
continue;
} else {
resetAnnotations.set(editor.annotationElementId, editor);
}
this.getEditableAnnotation(editor.annotationElementId)?.show();
editor.remove();
}
Expand All @@ -299,12 +307,23 @@ class AnnotationEditorLayer {
const editables = this.#annotationLayer.getEditableAnnotations();
for (const editable of editables) {
const { id } = editable.data;
if (
hiddenAnnotationIds.has(id) ||
this.#uiManager.isDeletedAnnotationElement(id)
) {
if (this.#uiManager.isDeletedAnnotationElement(id)) {
continue;
}
let editor = resetAnnotations.get(id);
if (editor) {
editor.resetAnnotationElement(editable);
editor.show(false);
editable.show();
continue;
}

editor = changedAnnotations.get(id);
if (editor) {
this.#uiManager.addChangedExistingAnnotation(editor);
editor.renderAnnotationElement(editable);
editor.show(false);
}
editable.show();
}
}
Expand Down Expand Up @@ -461,7 +480,7 @@ class AnnotationEditorLayer {
return;
}

if (editor.annotationElementId) {
if (editor.parent && editor.annotationElementId) {
this.#uiManager.addDeletedAnnotationElement(editor.annotationElementId);
AnnotationEditor.deleteAnnotationElement(editor);
editor.annotationElementId = null;
Expand Down
42 changes: 42 additions & 0 deletions src/display/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,17 @@ class AnnotationEditor {
return editor;
}

/**
* Check if an existing annotation associated with this editor has been
* modified.
* @returns {boolean}
*/
get hasBeenModified() {
return (
!!this.annotationElementId && (this.deleted || this.serialize() !== null)
);
}

/**
* Remove this editor.
* It's used on ctrl+backspace action.
Expand Down Expand Up @@ -1710,6 +1721,37 @@ class AnnotationEditor {
}
this.#disabled = true;
}

/**
* Render an annotation in the annotation layer.
* @param {Object} annotation
* @returns {HTMLElement}
*/
renderAnnotationElement(annotation) {
let content = annotation.container.querySelector(".annotationContent");
if (!content) {
content = document.createElement("div");
content.classList.add("annotationContent", this.editorType);
annotation.container.prepend(content);
} else if (content.nodeName === "CANVAS") {
const canvas = content;
content = document.createElement("div");
content.classList.add("annotationContent", this.editorType);
canvas.before(content);
}

return content;
}

resetAnnotationElement(annotation) {
const { firstChild } = annotation.container;
if (
firstChild.nodeName === "DIV" &&
firstChild.classList.contains("annotationContent")
) {
firstChild.remove();
}
}
}

// This class is used to fake an editor which has been deleted.
Expand Down
Loading