mirror of
https://github.com/zen-browser/pdf.js.git
synced 2025-07-08 09:20:06 +02:00
[Editor] Provide an element to render in the annotation layer after a freetext has been edited (bug 1890535)
This commit is contained in:
parent
7290faf840
commit
71ea8499f0
11 changed files with 379 additions and 42 deletions
|
@ -20,6 +20,8 @@
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../../web/interfaces").IDownloadManager} IDownloadManager */
|
/** @typedef {import("../../web/interfaces").IDownloadManager} IDownloadManager */
|
||||||
/** @typedef {import("../../web/interfaces").IPDFLinkService} IPDFLinkService */
|
/** @typedef {import("../../web/interfaces").IPDFLinkService} IPDFLinkService */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnnotationBorderStyleType,
|
AnnotationBorderStyleType,
|
||||||
|
@ -157,6 +159,8 @@ class AnnotationElementFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnnotationElement {
|
class AnnotationElement {
|
||||||
|
#updates = null;
|
||||||
|
|
||||||
#hasBorder = false;
|
#hasBorder = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -197,6 +201,52 @@ class AnnotationElement {
|
||||||
return AnnotationElement._hasPopupData(this.data);
|
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.
|
* Create an empty container for the annotation's HTML element.
|
||||||
*
|
*
|
||||||
|
@ -216,13 +266,14 @@ class AnnotationElement {
|
||||||
if (!(this instanceof WidgetAnnotationElement)) {
|
if (!(this instanceof WidgetAnnotationElement)) {
|
||||||
container.tabIndex = DEFAULT_TAB_INDEX;
|
container.tabIndex = DEFAULT_TAB_INDEX;
|
||||||
}
|
}
|
||||||
|
const { style } = container;
|
||||||
|
|
||||||
// The accessibility manager will move the annotation in the DOM in
|
// The accessibility manager will move the annotation in the DOM in
|
||||||
// order to match the visual ordering.
|
// order to match the visual ordering.
|
||||||
// But if an annotation is above an other one, then we must draw it
|
// 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
|
// after the other one whatever the order is in the DOM, hence the
|
||||||
// use of the z-index.
|
// use of the z-index.
|
||||||
container.style.zIndex = this.parent.zIndex++;
|
style.zIndex = this.parent.zIndex++;
|
||||||
|
|
||||||
if (data.popupRef) {
|
if (data.popupRef) {
|
||||||
container.setAttribute("aria-haspopup", "dialog");
|
container.setAttribute("aria-haspopup", "dialog");
|
||||||
|
@ -236,8 +287,6 @@ class AnnotationElement {
|
||||||
container.classList.add("norotate");
|
container.classList.add("norotate");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pageWidth, pageHeight, pageX, pageY } = viewport.rawDims;
|
|
||||||
|
|
||||||
if (!data.rect || this instanceof PopupAnnotationElement) {
|
if (!data.rect || this instanceof PopupAnnotationElement) {
|
||||||
const { rotation } = data;
|
const { rotation } = data;
|
||||||
if (!data.hasOwnCanvas && rotation !== 0) {
|
if (!data.hasOwnCanvas && rotation !== 0) {
|
||||||
|
@ -248,35 +297,26 @@ class AnnotationElement {
|
||||||
|
|
||||||
const { width, height } = getRectDims(data.rect);
|
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) {
|
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 horizontalRadius = data.borderStyle.horizontalCornerRadius;
|
||||||
const verticalRadius = data.borderStyle.verticalCornerRadius;
|
const verticalRadius = data.borderStyle.verticalCornerRadius;
|
||||||
if (horizontalRadius > 0 || verticalRadius > 0) {
|
if (horizontalRadius > 0 || verticalRadius > 0) {
|
||||||
const radius = `calc(${horizontalRadius}px * var(--scale-factor)) / calc(${verticalRadius}px * var(--scale-factor))`;
|
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) {
|
} else if (this instanceof RadioButtonWidgetAnnotationElement) {
|
||||||
const radius = `calc(${width}px * var(--scale-factor)) / calc(${height}px * var(--scale-factor))`;
|
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) {
|
switch (data.borderStyle.style) {
|
||||||
case AnnotationBorderStyleType.SOLID:
|
case AnnotationBorderStyleType.SOLID:
|
||||||
container.style.borderStyle = "solid";
|
style.borderStyle = "solid";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AnnotationBorderStyleType.DASHED:
|
case AnnotationBorderStyleType.DASHED:
|
||||||
container.style.borderStyle = "dashed";
|
style.borderStyle = "dashed";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AnnotationBorderStyleType.BEVELED:
|
case AnnotationBorderStyleType.BEVELED:
|
||||||
|
@ -288,7 +328,7 @@ class AnnotationElement {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AnnotationBorderStyleType.UNDERLINE:
|
case AnnotationBorderStyleType.UNDERLINE:
|
||||||
container.style.borderBottomStyle = "solid";
|
style.borderBottomStyle = "solid";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -298,24 +338,34 @@ class AnnotationElement {
|
||||||
const borderColor = data.borderColor || null;
|
const borderColor = data.borderColor || null;
|
||||||
if (borderColor) {
|
if (borderColor) {
|
||||||
this.#hasBorder = true;
|
this.#hasBorder = true;
|
||||||
container.style.borderColor = Util.makeHexColor(
|
style.borderColor = Util.makeHexColor(
|
||||||
borderColor[0] | 0,
|
borderColor[0] | 0,
|
||||||
borderColor[1] | 0,
|
borderColor[1] | 0,
|
||||||
borderColor[2] | 0
|
borderColor[2] | 0
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Transparent (invisible) border, so do not draw it at all.
|
// 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}%`;
|
// Do *not* modify `data.rect`, since that will corrupt the annotation
|
||||||
container.style.top = `${(100 * (rect[1] - pageY)) / pageHeight}%`;
|
// 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;
|
const { rotation } = data;
|
||||||
if (data.hasOwnCanvas || rotation === 0) {
|
if (data.hasOwnCanvas || rotation === 0) {
|
||||||
container.style.width = `${(100 * width) / pageWidth}%`;
|
style.width = `${(100 * width) / pageWidth}%`;
|
||||||
container.style.height = `${(100 * height) / pageHeight}%`;
|
style.height = `${(100 * height) / pageHeight}%`;
|
||||||
} else {
|
} else {
|
||||||
this.setRotation(rotation, container);
|
this.setRotation(rotation, container);
|
||||||
}
|
}
|
||||||
|
@ -2897,6 +2947,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
|
||||||
* @property {Object<string, Array<Object>> | null} [fieldObjects]
|
* @property {Object<string, Array<Object>> | null} [fieldObjects]
|
||||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
||||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
|
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2913,6 +2964,7 @@ class AnnotationLayer {
|
||||||
div,
|
div,
|
||||||
accessibilityManager,
|
accessibilityManager,
|
||||||
annotationCanvasMap,
|
annotationCanvasMap,
|
||||||
|
annotationEditorUIManager,
|
||||||
page,
|
page,
|
||||||
viewport,
|
viewport,
|
||||||
}) {
|
}) {
|
||||||
|
@ -2922,6 +2974,7 @@ class AnnotationLayer {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
this.zIndex = 0;
|
this.zIndex = 0;
|
||||||
|
this._annotationEditorUIManager = annotationEditorUIManager;
|
||||||
|
|
||||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||||
// For testing purposes.
|
// For testing purposes.
|
||||||
|
@ -3011,15 +3064,16 @@ class AnnotationLayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.annotationEditorType > 0) {
|
|
||||||
this.#editableAnnotations.set(element.data.id, element);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rendered = element.render();
|
const rendered = element.render();
|
||||||
if (data.hidden) {
|
if (data.hidden) {
|
||||||
rendered.style.visibility = "hidden";
|
rendered.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
this.#appendElement(rendered, data.id);
|
this.#appendElement(rendered, data.id);
|
||||||
|
|
||||||
|
if (element.annotationEditorType > 0) {
|
||||||
|
this.#editableAnnotations.set(element.data.id, element);
|
||||||
|
this._annotationEditorUIManager?.renderAnnotationElement(element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#setAnnotationCanvasMap();
|
this.#setAnnotationCanvasMap();
|
||||||
|
@ -3051,13 +3105,16 @@ class AnnotationLayer {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas.className = "annotationContent";
|
||||||
const { firstChild } = element;
|
const { firstChild } = element;
|
||||||
if (!firstChild) {
|
if (!firstChild) {
|
||||||
element.append(canvas);
|
element.append(canvas);
|
||||||
} else if (firstChild.nodeName === "CANVAS") {
|
} else if (firstChild.nodeName === "CANVAS") {
|
||||||
firstChild.replaceWith(canvas);
|
firstChild.replaceWith(canvas);
|
||||||
} else {
|
} else if (!firstChild.classList.contains("annotationContent")) {
|
||||||
firstChild.before(canvas);
|
firstChild.before(canvas);
|
||||||
|
} else {
|
||||||
|
firstChild.after(canvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#annotationCanvasMap.clear();
|
this.#annotationCanvasMap.clear();
|
||||||
|
|
|
@ -248,7 +248,9 @@ class AnnotationEditorLayer {
|
||||||
const annotationElementIds = new Set();
|
const annotationElementIds = new Set();
|
||||||
for (const editor of this.#editors.values()) {
|
for (const editor of this.#editors.values()) {
|
||||||
editor.enableEditing();
|
editor.enableEditing();
|
||||||
|
editor.show(true);
|
||||||
if (editor.annotationElementId) {
|
if (editor.annotationElementId) {
|
||||||
|
this.#uiManager.removeChangedExistingAnnotation(editor);
|
||||||
annotationElementIds.add(editor.annotationElementId);
|
annotationElementIds.add(editor.annotationElementId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -283,13 +285,19 @@ class AnnotationEditorLayer {
|
||||||
this.#isDisabling = true;
|
this.#isDisabling = true;
|
||||||
this.div.tabIndex = -1;
|
this.div.tabIndex = -1;
|
||||||
this.togglePointerEvents(false);
|
this.togglePointerEvents(false);
|
||||||
const hiddenAnnotationIds = new Set();
|
const changedAnnotations = new Map();
|
||||||
|
const resetAnnotations = new Map();
|
||||||
for (const editor of this.#editors.values()) {
|
for (const editor of this.#editors.values()) {
|
||||||
editor.disableEditing();
|
editor.disableEditing();
|
||||||
if (!editor.annotationElementId || editor.serialize() !== null) {
|
if (!editor.annotationElementId) {
|
||||||
hiddenAnnotationIds.add(editor.annotationElementId);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (editor.serialize() !== null) {
|
||||||
|
changedAnnotations.set(editor.annotationElementId, editor);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
resetAnnotations.set(editor.annotationElementId, editor);
|
||||||
|
}
|
||||||
this.getEditableAnnotation(editor.annotationElementId)?.show();
|
this.getEditableAnnotation(editor.annotationElementId)?.show();
|
||||||
editor.remove();
|
editor.remove();
|
||||||
}
|
}
|
||||||
|
@ -299,12 +307,23 @@ class AnnotationEditorLayer {
|
||||||
const editables = this.#annotationLayer.getEditableAnnotations();
|
const editables = this.#annotationLayer.getEditableAnnotations();
|
||||||
for (const editable of editables) {
|
for (const editable of editables) {
|
||||||
const { id } = editable.data;
|
const { id } = editable.data;
|
||||||
if (
|
if (this.#uiManager.isDeletedAnnotationElement(id)) {
|
||||||
hiddenAnnotationIds.has(id) ||
|
|
||||||
this.#uiManager.isDeletedAnnotationElement(id)
|
|
||||||
) {
|
|
||||||
continue;
|
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();
|
editable.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -461,7 +480,7 @@ class AnnotationEditorLayer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editor.annotationElementId) {
|
if (editor.parent && editor.annotationElementId) {
|
||||||
this.#uiManager.addDeletedAnnotationElement(editor.annotationElementId);
|
this.#uiManager.addDeletedAnnotationElement(editor.annotationElementId);
|
||||||
AnnotationEditor.deleteAnnotationElement(editor);
|
AnnotationEditor.deleteAnnotationElement(editor);
|
||||||
editor.annotationElementId = null;
|
editor.annotationElementId = null;
|
||||||
|
|
|
@ -1336,6 +1336,17 @@ class AnnotationEditor {
|
||||||
return editor;
|
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.
|
* Remove this editor.
|
||||||
* It's used on ctrl+backspace action.
|
* It's used on ctrl+backspace action.
|
||||||
|
@ -1710,6 +1721,37 @@ class AnnotationEditor {
|
||||||
}
|
}
|
||||||
this.#disabled = true;
|
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.
|
// This class is used to fake an editor which has been deleted.
|
||||||
|
|
|
@ -408,11 +408,14 @@ class FreeTextEditor extends AnnotationEditor {
|
||||||
// we just insert it in the DOM, get its bounding box and then remove it.
|
// we just insert it in the DOM, get its bounding box and then remove it.
|
||||||
const { currentLayer, div } = this;
|
const { currentLayer, div } = this;
|
||||||
const savedDisplay = div.style.display;
|
const savedDisplay = div.style.display;
|
||||||
|
const savedVisibility = div.classList.contains("hidden");
|
||||||
|
div.classList.remove("hidden");
|
||||||
div.style.display = "hidden";
|
div.style.display = "hidden";
|
||||||
currentLayer.div.append(this.div);
|
currentLayer.div.append(this.div);
|
||||||
rect = div.getBoundingClientRect();
|
rect = div.getBoundingClientRect();
|
||||||
div.remove();
|
div.remove();
|
||||||
div.style.display = savedDisplay;
|
div.style.display = savedDisplay;
|
||||||
|
div.classList.toggle("hidden", savedVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dimensions are relative to the rotation of the page, hence we need to
|
// The dimensions are relative to the rotation of the page, hence we need to
|
||||||
|
@ -778,7 +781,7 @@ class FreeTextEditor extends AnnotationEditor {
|
||||||
value: textContent.join("\n"),
|
value: textContent.join("\n"),
|
||||||
position: textPosition,
|
position: textPosition,
|
||||||
pageIndex: pageNumber - 1,
|
pageIndex: pageNumber - 1,
|
||||||
rect,
|
rect: rect.slice(0),
|
||||||
rotation,
|
rotation,
|
||||||
id,
|
id,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -853,6 +856,38 @@ class FreeTextEditor extends AnnotationEditor {
|
||||||
serialized.pageIndex !== pageIndex
|
serialized.pageIndex !== pageIndex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
renderAnnotationElement(annotation) {
|
||||||
|
const content = super.renderAnnotationElement(annotation);
|
||||||
|
if (this.deleted) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
const { style } = content;
|
||||||
|
style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`;
|
||||||
|
style.color = this.#color;
|
||||||
|
|
||||||
|
content.replaceChildren();
|
||||||
|
for (const line of this.#content.split("\n")) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.append(
|
||||||
|
line ? document.createTextNode(line) : document.createElement("br")
|
||||||
|
);
|
||||||
|
content.append(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
||||||
|
annotation.updateEdited({
|
||||||
|
rect: this.getRect(padding, padding),
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAnnotationElement(annotation) {
|
||||||
|
super.resetAnnotationElement(annotation);
|
||||||
|
annotation.resetEdited();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { FreeTextEditor };
|
export { FreeTextEditor };
|
||||||
|
|
|
@ -544,6 +544,8 @@ class AnnotationEditorUIManager {
|
||||||
|
|
||||||
#annotationStorage = null;
|
#annotationStorage = null;
|
||||||
|
|
||||||
|
#changedExistingAnnotations = null;
|
||||||
|
|
||||||
#commandManager = new CommandManager();
|
#commandManager = new CommandManager();
|
||||||
|
|
||||||
#currentPageIndex = 0;
|
#currentPageIndex = 0;
|
||||||
|
@ -1682,6 +1684,7 @@ class AnnotationEditorUIManager {
|
||||||
*/
|
*/
|
||||||
addDeletedAnnotationElement(editor) {
|
addDeletedAnnotationElement(editor) {
|
||||||
this.#deletedAnnotationsElementIds.add(editor.annotationElementId);
|
this.#deletedAnnotationsElementIds.add(editor.annotationElementId);
|
||||||
|
this.addChangedExistingAnnotation(editor);
|
||||||
editor.deleted = true;
|
editor.deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1700,6 +1703,7 @@ class AnnotationEditorUIManager {
|
||||||
*/
|
*/
|
||||||
removeDeletedAnnotationElement(editor) {
|
removeDeletedAnnotationElement(editor) {
|
||||||
this.#deletedAnnotationsElementIds.delete(editor.annotationElementId);
|
this.#deletedAnnotationsElementIds.delete(editor.annotationElementId);
|
||||||
|
this.removeChangedExistingAnnotation(editor);
|
||||||
editor.deleted = false;
|
editor.deleted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2243,6 +2247,32 @@ class AnnotationEditorUIManager {
|
||||||
}
|
}
|
||||||
return boxes.length === 0 ? null : boxes;
|
return boxes.length === 0 ? null : boxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addChangedExistingAnnotation({ annotationElementId, id }) {
|
||||||
|
(this.#changedExistingAnnotations ||= new Map()).set(
|
||||||
|
annotationElementId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChangedExistingAnnotation({ annotationElementId }) {
|
||||||
|
this.#changedExistingAnnotations?.delete(annotationElementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAnnotationElement(annotation) {
|
||||||
|
const editorId = this.#changedExistingAnnotations?.get(annotation.data.id);
|
||||||
|
if (!editorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editor = this.#annotationStorage.getRawValue(editorId);
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.#mode === AnnotationEditorType.NONE && !editor.hasBeenModified) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.renderAnnotationElement(annotation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -1125,15 +1125,24 @@ describe("FreeText Editor", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// We want to check that the editor is displayed but not the original
|
// We want to check that the editor is displayed but not the original
|
||||||
// annotation.
|
// canvas.
|
||||||
editorIds = await getEditors(page, "freeText");
|
editorIds = await getEditors(page, "freeText");
|
||||||
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1);
|
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1);
|
||||||
const hidden = await page.$eval(
|
const hidden = await page.$eval(
|
||||||
"[data-annotation-id='26R']",
|
"[data-annotation-id='26R'] canvas",
|
||||||
el => el.hidden
|
el => getComputedStyle(el).display === "none"
|
||||||
);
|
);
|
||||||
expect(hidden).withContext(`In ${browserName}`).toBeTrue();
|
expect(hidden).withContext(`In ${browserName}`).toBeTrue();
|
||||||
|
|
||||||
|
// Check we've now a div containing the text.
|
||||||
|
const newDivText = await page.$eval(
|
||||||
|
"[data-annotation-id='26R'] div.annotationContent",
|
||||||
|
el => el.innerText.replaceAll("\xa0", " ")
|
||||||
|
);
|
||||||
|
expect(newDivText)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual("Hello World from Acrobat and edited in Firefox");
|
||||||
|
|
||||||
// Re-enable editing mode.
|
// Re-enable editing mode.
|
||||||
await switchToFreeText(page);
|
await switchToFreeText(page);
|
||||||
await page.focus(".annotationEditorLayer");
|
await page.focus(".annotationEditorLayer");
|
||||||
|
@ -3715,4 +3724,123 @@ describe("FreeText Editor", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Update a freetext and scroll", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"tracemonkey_freetext.pdf",
|
||||||
|
".annotationEditorLayer"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that a freetext is still there after having updated it and scroll the doc", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await switchToFreeText(page);
|
||||||
|
|
||||||
|
const editorSelector = getEditorSelector(0);
|
||||||
|
const editorRect = await page.$eval(editorSelector, el => {
|
||||||
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
|
return { x, y, width, height };
|
||||||
|
});
|
||||||
|
await page.mouse.click(
|
||||||
|
editorRect.x + editorRect.width / 2,
|
||||||
|
editorRect.y + editorRect.height / 2,
|
||||||
|
{ count: 2 }
|
||||||
|
);
|
||||||
|
await page.waitForSelector(
|
||||||
|
`${editorSelector} .overlay:not(.enabled)`
|
||||||
|
);
|
||||||
|
|
||||||
|
await kbGoToEnd(page);
|
||||||
|
await page.waitForFunction(
|
||||||
|
sel =>
|
||||||
|
document.getSelection().anchorOffset ===
|
||||||
|
document.querySelector(sel).innerText.length,
|
||||||
|
{},
|
||||||
|
`${editorSelector} .internal`
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.type(
|
||||||
|
`${editorSelector} .internal`,
|
||||||
|
" and edited in Firefox"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Disable editing mode.
|
||||||
|
await page.click("#editorFreeText");
|
||||||
|
await page.waitForSelector(
|
||||||
|
`.annotationEditorLayer:not(.freetextEditing)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const oneToOne = Array.from(new Array(13).keys(), n => n + 2).concat(
|
||||||
|
Array.from(new Array(13).keys(), n => 13 - n)
|
||||||
|
);
|
||||||
|
for (const pageNumber of oneToOne) {
|
||||||
|
await scrollIntoView(
|
||||||
|
page,
|
||||||
|
`.page[data-page-number = "${pageNumber}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForSelector("[data-annotation-id='998R'] canvas");
|
||||||
|
let hidden = await page.$eval(
|
||||||
|
"[data-annotation-id='998R'] canvas",
|
||||||
|
el => getComputedStyle(el).display === "none"
|
||||||
|
);
|
||||||
|
expect(hidden).withContext(`In ${browserName}`).toBeTrue();
|
||||||
|
|
||||||
|
// Check we've now a div containing the text.
|
||||||
|
await page.waitForSelector(
|
||||||
|
"[data-annotation-id='998R'] div.annotationContent"
|
||||||
|
);
|
||||||
|
const newDivText = await page.$eval(
|
||||||
|
"[data-annotation-id='998R'] div.annotationContent",
|
||||||
|
el => el.innerText.replaceAll("\xa0", " ")
|
||||||
|
);
|
||||||
|
expect(newDivText)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual("Hello World and edited in Firefox");
|
||||||
|
|
||||||
|
const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2);
|
||||||
|
for (const pageNumber of oneToThirteen) {
|
||||||
|
await scrollIntoView(
|
||||||
|
page,
|
||||||
|
`.page[data-page-number = "${pageNumber}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await switchToFreeText(page);
|
||||||
|
await kbUndo(page);
|
||||||
|
await waitForSerialized(page, 0);
|
||||||
|
|
||||||
|
// Disable editing mode.
|
||||||
|
await page.click("#editorFreeText");
|
||||||
|
await page.waitForSelector(
|
||||||
|
`.annotationEditorLayer:not(.freetextEditing)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
|
||||||
|
for (const pageNumber of thirteenToOne) {
|
||||||
|
await scrollIntoView(
|
||||||
|
page,
|
||||||
|
`.page[data-page-number = "${pageNumber}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForSelector("[data-annotation-id='998R'] canvas");
|
||||||
|
hidden = await page.$eval(
|
||||||
|
"[data-annotation-id='998R'] canvas",
|
||||||
|
el => getComputedStyle(el).display === "none"
|
||||||
|
);
|
||||||
|
expect(hidden).withContext(`In ${browserName}`).toBeFalse();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
|
@ -643,3 +643,4 @@
|
||||||
!bug1889122.pdf
|
!bug1889122.pdf
|
||||||
!issue17929.pdf
|
!issue17929.pdf
|
||||||
!issue12213.pdf
|
!issue12213.pdf
|
||||||
|
!tracemonkey_freetext.pdf
|
||||||
|
|
BIN
test/pdfs/tracemonkey_freetext.pdf
Executable file
BIN
test/pdfs/tracemonkey_freetext.pdf
Executable file
Binary file not shown.
|
@ -94,11 +94,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
.annotationContent {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.freetext {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
inset: 0;
|
||||||
|
overflow: visible;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: 10px sans-serif;
|
||||||
|
line-height: 1.35;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
|
@ -107,6 +118,12 @@
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
|
|
||||||
|
&:has(div.annotationContent) {
|
||||||
|
canvas.annotationContent {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
|
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||||
|
|
||||||
import { AnnotationLayer } from "pdfjs-lib";
|
import { AnnotationLayer } from "pdfjs-lib";
|
||||||
import { PresentationModeState } from "./ui_utils.js";
|
import { PresentationModeState } from "./ui_utils.js";
|
||||||
|
@ -41,6 +43,7 @@ import { PresentationModeState } from "./ui_utils.js";
|
||||||
* [fieldObjectsPromise]
|
* [fieldObjectsPromise]
|
||||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
||||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
|
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
|
||||||
* @property {function} [onAppend]
|
* @property {function} [onAppend]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -64,6 +67,7 @@ class AnnotationLayerBuilder {
|
||||||
fieldObjectsPromise = null,
|
fieldObjectsPromise = null,
|
||||||
annotationCanvasMap = null,
|
annotationCanvasMap = null,
|
||||||
accessibilityManager = null,
|
accessibilityManager = null,
|
||||||
|
annotationEditorUIManager = null,
|
||||||
onAppend = null,
|
onAppend = null,
|
||||||
}) {
|
}) {
|
||||||
this.pdfPage = pdfPage;
|
this.pdfPage = pdfPage;
|
||||||
|
@ -77,6 +81,7 @@ class AnnotationLayerBuilder {
|
||||||
this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
|
this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
|
||||||
this._annotationCanvasMap = annotationCanvasMap;
|
this._annotationCanvasMap = annotationCanvasMap;
|
||||||
this._accessibilityManager = accessibilityManager;
|
this._accessibilityManager = accessibilityManager;
|
||||||
|
this._annotationEditorUIManager = annotationEditorUIManager;
|
||||||
this.#onAppend = onAppend;
|
this.#onAppend = onAppend;
|
||||||
|
|
||||||
this.annotationLayer = null;
|
this.annotationLayer = null;
|
||||||
|
@ -128,6 +133,7 @@ class AnnotationLayerBuilder {
|
||||||
div,
|
div,
|
||||||
accessibilityManager: this._accessibilityManager,
|
accessibilityManager: this._accessibilityManager,
|
||||||
annotationCanvasMap: this._annotationCanvasMap,
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
|
annotationEditorUIManager: this._annotationEditorUIManager,
|
||||||
page: this.pdfPage,
|
page: this.pdfPage,
|
||||||
viewport: viewport.clone({ dontFlip: true }),
|
viewport: viewport.clone({ dontFlip: true }),
|
||||||
});
|
});
|
||||||
|
|
|
@ -938,6 +938,7 @@ class PDFPageView {
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
annotationStorage,
|
annotationStorage,
|
||||||
|
annotationEditorUIManager,
|
||||||
downloadManager,
|
downloadManager,
|
||||||
enableScripting,
|
enableScripting,
|
||||||
fieldObjectsPromise,
|
fieldObjectsPromise,
|
||||||
|
@ -958,6 +959,7 @@ class PDFPageView {
|
||||||
fieldObjectsPromise,
|
fieldObjectsPromise,
|
||||||
annotationCanvasMap: this._annotationCanvasMap,
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
accessibilityManager: this._accessibilityManager,
|
accessibilityManager: this._accessibilityManager,
|
||||||
|
annotationEditorUIManager,
|
||||||
onAppend: annotationLayerDiv => {
|
onAppend: annotationLayerDiv => {
|
||||||
this.#addLayer(annotationLayerDiv, "annotationLayer");
|
this.#addLayer(annotationLayerDiv, "annotationLayer");
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue