diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 6d5fea76cb4db4..6f39ddd7c3c370 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -17,8 +17,6 @@ // 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 */ -// eslint-disable-next-line max-len /** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("../../web/interfaces").IL10n} IL10n */ @@ -33,7 +31,6 @@ import { InkEditor } from "./ink.js"; * @property {HTMLDivElement} div * @property {AnnotationEditorUIManager} uiManager * @property {boolean} enabled - * @property {AnnotationStorage} annotationStorage * @property {TextAccessibilityManager} [accessibilityManager] * @property {number} pageIndex * @property {IL10n} l10n @@ -73,7 +70,6 @@ class AnnotationEditorLayer { options.uiManager.registerEditorTypes([FreeTextEditor, InkEditor]); this.#uiManager = options.uiManager; - this.annotationStorage = options.annotationStorage; this.pageIndex = options.pageIndex; this.div = options.div; this.#accessibilityManager = options.accessibilityManager; @@ -213,7 +209,6 @@ class AnnotationEditorLayer { this.#uiManager.removeEditor(editor); this.detach(editor); - this.annotationStorage.remove(editor.id); editor.div.style.display = "none"; setTimeout(() => { // When the div is removed from DOM the focus can move on the @@ -244,9 +239,8 @@ class AnnotationEditorLayer { } this.attach(editor); - editor.pageIndex = this.pageIndex; editor.parent?.detach(editor); - editor.parent = this; + editor.setParent(this); if (editor.div && editor.isAttachedToDOM) { editor.div.remove(); this.div.append(editor.div); @@ -270,7 +264,7 @@ class AnnotationEditorLayer { this.moveEditorInDOM(editor); editor.onceAdded(); - this.addToAnnotationStorage(editor); + this.#uiManager.addToAnnotationStorage(editor); } moveEditorInDOM(editor) { @@ -282,16 +276,6 @@ class AnnotationEditorLayer { ); } - /** - * Add an editor in the annotation storage. - * @param {AnnotationEditor} editor - */ - addToAnnotationStorage(editor) { - if (!editor.isEmpty() && !this.annotationStorage.has(editor.id)) { - this.annotationStorage.setValue(editor.id, editor); - } - } - /** * Add or rebuild depending if it has been removed or not. * @param {AnnotationEditor} editor @@ -365,9 +349,9 @@ class AnnotationEditorLayer { deserialize(data) { switch (data.annotationType) { case AnnotationEditorType.FREETEXT: - return FreeTextEditor.deserialize(data, this); + return FreeTextEditor.deserialize(data, this, this.#uiManager); case AnnotationEditorType.INK: - return InkEditor.deserialize(data, this); + return InkEditor.deserialize(data, this, this.#uiManager); } return null; } @@ -384,6 +368,7 @@ class AnnotationEditorLayer { id, x: event.offsetX, y: event.offsetY, + uiManager: this.#uiManager, }); if (editor) { this.add(editor); @@ -520,9 +505,9 @@ class AnnotationEditorLayer { for (const editor of this.#editors.values()) { this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv); + editor.setParent(null); editor.isAttachedToDOM = false; editor.div.remove(); - editor.parent = null; } this.div = null; this.#editors.clear(); @@ -571,14 +556,6 @@ class AnnotationEditorLayer { this.updateMode(); } - /** - * Get the scale factor from the viewport. - * @returns {number} - */ - get scaleFactor() { - return this.viewport.scale; - } - /** * Get page dimensions. * @returns {Object} dimensions. @@ -591,11 +568,6 @@ class AnnotationEditorLayer { return [width, height]; } - get viewportBaseDimensions() { - const { width, height, rotation } = this.viewport; - return rotation % 180 === 0 ? [width, height] : [height, width]; - } - /** * Set the dimensions of the main div. */ diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 60a1fcfb3b47f8..c4344e867d4ab6 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -15,12 +15,15 @@ // eslint-disable-next-line max-len /** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */ +// eslint-disable-next-line max-len +/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ import { bindEvents, ColorManager } from "./tools.js"; import { FeatureTest, shadow, unreachable } from "../../shared/util.js"; /** * @typedef {Object} AnnotationEditorParameters + * @property {AnnotationEditorUIManager} uiManager - the global manager * @property {AnnotationEditorLayer} parent - the layer containing this editor * @property {string} id - editor id * @property {number} x - x-coordinate @@ -41,6 +44,8 @@ class AnnotationEditor { #isInEditMode = false; + _uiManager = null; + #zIndex = AnnotationEditor._zIndex++; static _colorManager = new ColorManager(); @@ -61,11 +66,13 @@ class AnnotationEditor { this.pageIndex = parameters.parent.pageIndex; this.name = parameters.name; this.div = null; + this._uiManager = parameters.uiManager; - const [width, height] = this.parent.viewportBaseDimensions; + this.rotation = this.parent.viewport.rotation; + this.pageDimensions = this.parent.pageDimensions; + const [width, height] = this.parentDimensions; this.x = parameters.x / width; this.y = parameters.y / height; - this.rotation = this.parent.viewport.rotation; this.isAttachedToDOM = false; } @@ -78,6 +85,18 @@ class AnnotationEditor { ); } + /** + * Add some commands into the CommandManager (undo/redo stuff). + * @param {Object} params + */ + addCommands(params) { + this._uiManager.addCommands(params); + } + + get currentLayer() { + return this._uiManager.currentLayer; + } + /** * This editor will be behind the others. */ @@ -92,6 +111,14 @@ class AnnotationEditor { this.div.style.zIndex = this.#zIndex; } + setParent(parent) { + if (parent !== null) { + this.pageIndex = parent.pageIndex; + this.pageDimensions = parent.pageDimensions; + } + this.parent = parent; + } + /** * onfocus callback. */ @@ -123,7 +150,7 @@ class AnnotationEditor { event.preventDefault(); - if (!this.parent.isMultipleSelection) { + if (!this.parent?.isMultipleSelection) { this.commitOrRemove(); } } @@ -140,7 +167,11 @@ class AnnotationEditor { * Commit the data contained in this editor. */ commit() { - this.parent.addToAnnotationStorage(this); + this.addToAnnotationStorage(); + } + + addToAnnotationStorage() { + this._uiManager.addToAnnotationStorage(this); } /** @@ -163,7 +194,7 @@ class AnnotationEditor { * @param {number} ty - y-translation in screen coordinates. */ setAt(x, y, tx, ty) { - const [width, height] = this.parent.viewportBaseDimensions; + const [width, height] = this.parentDimensions; [tx, ty] = this.screenToPageTranslation(tx, ty); this.x = (x + tx) / width; @@ -179,7 +210,7 @@ class AnnotationEditor { * @param {number} y - y-translation in screen coordinates. */ translate(x, y) { - const [width, height] = this.parent.viewportBaseDimensions; + const [width, height] = this.parentDimensions; [x, y] = this.screenToPageTranslation(x, y); this.x += x / width; @@ -195,8 +226,7 @@ class AnnotationEditor { * @param {number} y */ screenToPageTranslation(x, y) { - const { rotation } = this.parent.viewport; - switch (rotation) { + switch (this.parentRotation) { case 90: return [y, -x]; case 180: @@ -208,13 +238,29 @@ class AnnotationEditor { } } + get parentScale() { + return this._uiManager.viewParameters.realScale; + } + + get parentRotation() { + return this._uiManager.viewParameters.rotation; + } + + get parentDimensions() { + const { realScale, rotation } = this._uiManager.viewParameters; + const [pageWidth, pageHeight] = this.pageDimensions; + return rotation % 180 === 0 + ? [pageWidth * realScale, pageHeight * realScale] + : [pageHeight * realScale, pageWidth * realScale]; + } + /** * Set the dimensions of this editor. * @param {number} width * @param {number} height */ setDims(width, height) { - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.div.style.width = `${(100 * width) / parentWidth}%`; this.div.style.height = `${(100 * height) / parentHeight}%`; } @@ -228,7 +274,7 @@ class AnnotationEditor { return; } - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; if (!widthPercent) { style.width = `${(100 * parseFloat(width)) / parentWidth}%`; } @@ -295,10 +341,10 @@ class AnnotationEditor { } getRect(tx, ty) { - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; - const [pageWidth, pageHeight] = this.parent.pageDimensions; - const shiftX = (pageWidth * tx) / parentWidth; - const shiftY = (pageHeight * ty) / parentHeight; + const scale = this.parentScale; + const [pageWidth, pageHeight] = this.pageDimensions; + const shiftX = tx / scale; + const shiftY = ty / scale; const x = this.x * pageWidth; const y = this.y * pageHeight; const width = this.width * pageWidth; @@ -436,12 +482,14 @@ class AnnotationEditor { * * @param {Object} data * @param {AnnotationEditorLayer} parent + * @param {AnnotationEditorUIManager} uiManager * @returns {AnnotationEditor} */ - static deserialize(data, parent) { + static deserialize(data, parent, uiManager) { const editor = new this.prototype.constructor({ parent, id: parent.getNextId(), + uiManager, }); editor.rotation = data.rotation; diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index fd7cc1f47c9f40..71b6be66cb0bfc 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -153,12 +153,12 @@ class FreeTextEditor extends AnnotationEditor { #updateFontSize(fontSize) { const setFontsize = size => { this.editorDiv.style.fontSize = `calc(${size}px * var(--scale-factor))`; - this.translate(0, -(size - this.#fontSize) * this.parent.scaleFactor); + this.translate(0, -(size - this.#fontSize) * this.parentScale); this.#fontSize = size; this.#setEditorDimensions(); }; const savedFontsize = this.#fontSize; - this.parent.addCommands({ + this.addCommands({ cmd: () => { setFontsize(fontSize); }, @@ -178,14 +178,12 @@ class FreeTextEditor extends AnnotationEditor { */ #updateColor(color) { const savedColor = this.#color; - this.parent.addCommands({ + this.addCommands({ cmd: () => { - this.#color = color; - this.editorDiv.style.color = color; + this.#color = this.editorDiv.style.color = color; }, undo: () => { - this.#color = savedColor; - this.editorDiv.style.color = savedColor; + this.#color = this.editorDiv.style.color = savedColor; }, mustExec: true, type: AnnotationEditorParamsType.FREETEXT_COLOR, @@ -197,10 +195,10 @@ class FreeTextEditor extends AnnotationEditor { /** @inheritdoc */ getInitialTranslation() { // The start of the base line is where the user clicked. + const scale = this.parentScale; return [ - -FreeTextEditor._internalPadding * this.parent.scaleFactor, - -(FreeTextEditor._internalPadding + this.#fontSize) * - this.parent.scaleFactor, + -FreeTextEditor._internalPadding * scale, + -(FreeTextEditor._internalPadding + this.#fontSize) * scale, ]; } @@ -254,9 +252,11 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur); this.editorDiv.removeEventListener("input", this.#boundEditorDivInput); - // On Chrome, the focus is given to when contentEditable is set to - // false, hence we focus the div. - this.div.focus(); + if (this.pageIndex === this._uiManager.currentPageIndex) { + // On Chrome, the focus is given to when contentEditable is set to + // false, hence we focus the div. + this.div.focus(); + } // In case the blur callback hasn't been called. this.isEditing = false; @@ -311,8 +311,22 @@ class FreeTextEditor extends AnnotationEditor { } #setEditorDimensions() { - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; - const rect = this.div.getBoundingClientRect(); + const [parentWidth, parentHeight] = this.parentDimensions; + + let rect; + if (this.isAttachedToDOM) { + rect = this.div.getBoundingClientRect(); + } else { + // This editor isn't on screen but we need to get its dimensions, so + // we just insert it in the DOM, get its bounding box and then remove it. + const { currentLayer, div } = this; + const savedDisplay = div.style.display; + div.style.display = "hidden"; + currentLayer.div.append(this.div); + rect = div.getBoundingClientRect(); + div.remove(); + div.style.display = savedDisplay; + } this.width = rect.width / parentWidth; this.height = rect.height / parentHeight; @@ -323,6 +337,10 @@ class FreeTextEditor extends AnnotationEditor { * @returns {undefined} */ commit() { + if (!this.isInEditMode()) { + return; + } + super.commit(); if (!this.#hasAlreadyBeenCommitted) { // This editor has something and it's the first time @@ -435,7 +453,7 @@ class FreeTextEditor extends AnnotationEditor { if (this.width) { // This editor was created in using copy (ctrl+c). - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.setAt( baseX * parentWidth, baseY * parentHeight, @@ -466,8 +484,8 @@ class FreeTextEditor extends AnnotationEditor { } /** @inheritdoc */ - static deserialize(data, parent) { - const editor = super.deserialize(data, parent); + static deserialize(data, parent, uiManager) { + const editor = super.deserialize(data, parent, uiManager); editor.#fontSize = data.fontSize; editor.#color = Util.makeHexColor(...data.color); @@ -482,11 +500,13 @@ class FreeTextEditor extends AnnotationEditor { return null; } - const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor; + const padding = FreeTextEditor._internalPadding * this.parentScale; const rect = this.getRect(padding, padding); const color = AnnotationEditor._colorManager.convert( - getComputedStyle(this.editorDiv).color + this.isAttachedToDOM + ? getComputedStyle(this.editorDiv).color + : this.#color ); return { @@ -494,7 +514,7 @@ class FreeTextEditor extends AnnotationEditor { color, fontSize: this.#fontSize, value: this.#content, - pageIndex: this.parent.pageIndex, + pageIndex: this.pageIndex, rect, rotation: this.rotation, }; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 81d4129db2287a..051bebc2561cb0 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -165,7 +165,7 @@ class InkEditor extends AnnotationEditor { */ #updateThickness(thickness) { const savedThickness = this.thickness; - this.parent.addCommands({ + this.addCommands({ cmd: () => { this.thickness = thickness; this.#fitToContent(); @@ -187,7 +187,7 @@ class InkEditor extends AnnotationEditor { */ #updateColor(color) { const savedColor = this.color; - this.parent.addCommands({ + this.addCommands({ cmd: () => { this.color = color; this.#redraw(); @@ -210,7 +210,7 @@ class InkEditor extends AnnotationEditor { #updateOpacity(opacity) { opacity /= 100; const savedOpacity = this.opacity; - this.parent.addCommands({ + this.addCommands({ cmd: () => { this.opacity = opacity; this.#redraw(); @@ -268,6 +268,22 @@ class InkEditor extends AnnotationEditor { super.remove(); } + setParent(parent) { + if (!this.parent && parent) { + this._uiManager.removeScalingListener(this); + } else if (this.parent && parent === null) { + this._uiManager.addScalingListener(this); + } + super.setParent(parent); + } + + onScaleChanging() { + const [parentWidth, parentHeight] = this.parentDimensions; + const width = this.width * parentWidth; + const height = this.height * parentHeight; + this.setDimensions(width, height); + } + /** @inheritdoc */ enableEditMode() { if (this.#disableEditing || this.canvas === null) { @@ -311,7 +327,10 @@ class InkEditor extends AnnotationEditor { } #getInitialBBox() { - const { width, height, rotation } = this.parent.viewport; + const { + rotation, + parentDimensions: [width, height], + } = this; switch (rotation) { case 90: return [0, width, width, height]; @@ -328,12 +347,12 @@ class InkEditor extends AnnotationEditor { * Set line styles. */ #setStroke() { - this.ctx.lineWidth = - (this.thickness * this.parent.scaleFactor) / this.scaleFactor; - this.ctx.lineCap = "round"; - this.ctx.lineJoin = "round"; - this.ctx.miterLimit = 10; - this.ctx.strokeStyle = `${this.color}${opacityToHex(this.opacity)}`; + const { ctx, color, opacity, thickness, parentScale, scaleFactor } = this; + ctx.lineWidth = (thickness * parentScale) / scaleFactor; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.miterLimit = 10; + ctx.strokeStyle = `${color}${opacityToHex(opacity)}`; } /** @@ -445,7 +464,7 @@ class InkEditor extends AnnotationEditor { } }; - this.parent.addCommands({ cmd, undo, mustExec: true }); + this.addCommands({ cmd, undo, mustExec: true }); } /** @@ -493,9 +512,11 @@ class InkEditor extends AnnotationEditor { // When commiting, the position of this editor is changed, hence we must // move it to the right position in the DOM. this.parent.moveEditorInDOM(this); - // After the div has been moved in the DOM, the focus may have been stolen - // by document.body, hence we just keep it here. - this.div.focus(); + if (this.pageIndex === this._uiManager.currentPageIndex) { + // After the div has been moved in the DOM, the focus may have been stolen + // by document.body, hence we just keep it here. + this.div.focus(); + } } /** @inheritdoc */ @@ -581,7 +602,7 @@ class InkEditor extends AnnotationEditor { this.#boundCanvasPointermove ); - this.parent.addToAnnotationStorage(this); + this.addToAnnotationStorage(); } /** @@ -649,7 +670,7 @@ class InkEditor extends AnnotationEditor { if (this.width) { // This editor was created in using copy (ctrl+c). - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.setAt( baseX * parentWidth, baseY * parentHeight, @@ -676,7 +697,7 @@ class InkEditor extends AnnotationEditor { if (!this.#isCanvasInitialized) { return; } - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.canvas.width = Math.ceil(this.width * parentWidth); this.canvas.height = Math.ceil(this.height * parentHeight); this.#updateTransform(); @@ -712,7 +733,7 @@ class InkEditor extends AnnotationEditor { this.setDims(width, height); } - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.width = width / parentWidth; this.height = height / parentHeight; @@ -940,7 +961,7 @@ class InkEditor extends AnnotationEditor { */ #getPadding() { return this.#disableEditing - ? Math.ceil(this.thickness * this.parent.scaleFactor) + ? Math.ceil(this.thickness * this.parentScale) : 0; } @@ -967,7 +988,7 @@ class InkEditor extends AnnotationEditor { const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor); const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor); - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.width = width / parentWidth; this.height = height / parentHeight; @@ -1005,17 +1026,17 @@ class InkEditor extends AnnotationEditor { } /** @inheritdoc */ - static deserialize(data, parent) { - const editor = super.deserialize(data, parent); + static deserialize(data, parent, uiManager) { + const editor = super.deserialize(data, parent, uiManager); editor.thickness = data.thickness; editor.color = Util.makeHexColor(...data.color); editor.opacity = data.opacity; - const [pageWidth, pageHeight] = parent.pageDimensions; + const [pageWidth, pageHeight] = editor.parentDimensions; const width = editor.width * pageWidth; const height = editor.height * pageHeight; - const scaleFactor = parent.scaleFactor; + const scaleFactor = editor.parentScale; const padding = data.thickness / 2; editor.#aspectRatio = width / height; @@ -1074,12 +1095,12 @@ class InkEditor extends AnnotationEditor { thickness: this.thickness, opacity: this.opacity, paths: this.#serializePaths( - this.scaleFactor / this.parent.scaleFactor, + this.scaleFactor / this.parentScale, this.translationX, this.translationY, height ), - pageIndex: this.parent.pageIndex, + pageIndex: this.pageIndex, rect, rotation: this.rotation, }; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index e502934b786300..9ba2d312a72ae9 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -25,7 +25,7 @@ import { Util, warn, } from "../../shared/util.js"; -import { getColorValues, getRGB } from "../display_utils.js"; +import { getColorValues, getRGB, PixelsPerInch } from "../display_utils.js"; function bindEvents(obj, element, names) { for (const name of names) { @@ -350,12 +350,16 @@ class AnnotationEditorUIManager { #allLayers = new Map(); + #annotationStorage = null; + #commandManager = new CommandManager(); #currentPageIndex = 0; #editorTypes = null; + #editorsToRescale = new Set(); + #eventBus = null; #idManager = new IdManager(); @@ -378,6 +382,10 @@ class AnnotationEditorUIManager { #boundOnPageChanging = this.onPageChanging.bind(this); + #boundOnScaleChanging = this.onScaleChanging.bind(this); + + #boundOnRotationChanging = this.onRotationChanging.bind(this); + #previousStates = { isEditing: false, isEmpty: true, @@ -388,6 +396,11 @@ class AnnotationEditorUIManager { #container = null; + #viewParameters = { + realScale: PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: 0, + }; + static _keyboardManager = new KeyboardManager([ [["ctrl+a", "mac+meta+a"], AnnotationEditorUIManager.prototype.selectAll], [["ctrl+z", "mac+meta+z"], AnnotationEditorUIManager.prototype.undo], @@ -413,22 +426,28 @@ class AnnotationEditorUIManager { [["Escape", "mac+Escape"], AnnotationEditorUIManager.prototype.unselectAll], ]); - constructor(container, eventBus) { + constructor(container, eventBus, annotationStorage) { this.#container = container; this.#eventBus = eventBus; this.#eventBus._on("editingaction", this.#boundOnEditingAction); this.#eventBus._on("pagechanging", this.#boundOnPageChanging); + this.#eventBus._on("scalechanging", this.#boundOnScaleChanging); + this.#eventBus._on("rotationchanging", this.#boundOnRotationChanging); + this.#annotationStorage = annotationStorage; } destroy() { this.#removeKeyboardManager(); this.#eventBus._off("editingaction", this.#boundOnEditingAction); this.#eventBus._off("pagechanging", this.#boundOnPageChanging); + this.#eventBus._off("scalechanging", this.#boundOnScaleChanging); + this.#eventBus._off("rotationchanging", this.#boundOnRotationChanging); for (const layer of this.#allLayers.values()) { layer.destroy(); } this.#allLayers.clear(); this.#allEditors.clear(); + this.#editorsToRescale.clear(); this.#activeEditor = null; this.#selectedEditors.clear(); this.#commandManager.destroy(); @@ -442,6 +461,39 @@ class AnnotationEditorUIManager { this.#container.focus(); } + addScalingListener(editor) { + this.#editorsToRescale.add(editor); + } + + removeScalingListener(editor) { + this.#editorsToRescale.delete(editor); + } + + onScaleChanging({ scale }) { + this.#viewParameters.realScale = scale * PixelsPerInch.PDF_TO_CSS_UNITS; + for (const editor of this.#editorsToRescale) { + editor.onScaleChanging(); + } + } + + onRotationChanging({ rotation }) { + this.#viewParameters.rotation = rotation; + } + + /** + * Add an editor in the annotation storage. + * @param {AnnotationEditor} editor + */ + addToAnnotationStorage(editor) { + if ( + !editor.isEmpty() && + this.#annotationStorage && + !this.#annotationStorage.has(editor.id) + ) { + this.#annotationStorage.setValue(editor.id, editor); + } + } + #addKeyboardManager() { // The keyboard events are caught at the container level in order to be able // to execute some callbacks even if the current page doesn't have focus. @@ -646,6 +698,18 @@ class AnnotationEditorUIManager { return this.#idManager.getId(); } + get viewParameters() { + return this.#viewParameters; + } + + get currentLayer() { + return this.#allLayers.get(this.#currentPageIndex); + } + + get currentPageIndex() { + return this.#currentPageIndex; + } + /** * Add a new layer for a page which will contains the editors. * @param {AnnotationEditorLayer} layer @@ -783,6 +847,7 @@ class AnnotationEditorUIManager { removeEditor(editor) { this.#allEditors.delete(editor.id); this.unselect(editor); + this.#annotationStorage?.remove(editor.id); } /** diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index 0fea39fbd9b85e..43a6e0c0bce31b 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -19,6 +19,8 @@ const { getSelectedEditors, loadAndWait, waitForEvent, + waitForSelectedEditor, + waitForStorageEntries, } = require("./test_utils.js"); const copyPaste = async page => { @@ -49,23 +51,6 @@ describe("Editor", () => { await closePages(pages); }); - const waitForStorageEntries = async (page, nEntries) => { - await page.waitForFunction( - n => - window.PDFViewerApplication.pdfDocument.annotationStorage.size === n, - {}, - nEntries - ); - }; - - const waitForSelected = async (page, selector) => { - await page.waitForFunction( - sel => document.querySelector(sel).classList.contains("selectedEditor"), - {}, - selector - ); - }; - it("must write a string in a FreeText editor", async () => { await Promise.all( pages.map(async ([browserName, page]) => { @@ -98,7 +83,7 @@ describe("Editor", () => { editorRect.y + 2 * editorRect.height ); - await waitForSelected(page, getEditorSelector(0)); + await waitForSelectedEditor(page, getEditorSelector(0)); await waitForStorageEntries(page, 1); const content = await page.$eval(getEditorSelector(0), el => @@ -123,7 +108,7 @@ describe("Editor", () => { editorRect.y + editorRect.height / 2 ); - await waitForSelected(page, getEditorSelector(0)); + await waitForSelectedEditor(page, getEditorSelector(0)); await copyPaste(page); await waitForStorageEntries(page, 2); @@ -199,7 +184,7 @@ describe("Editor", () => { editorRect.y + editorRect.height / 2 ); - await waitForSelected(page, getEditorSelector(3)); + await waitForSelectedEditor(page, getEditorSelector(3)); await copyPaste(page); let hasEditor = await page.evaluate(sel => { @@ -335,7 +320,7 @@ describe("Editor", () => { editorRect.y + editorRect.height / 2 ); - await waitForSelected(page, getEditorSelector(8)); + await waitForSelectedEditor(page, getEditorSelector(8)); expect(await getSelectedEditors(page)) .withContext(`In ${browserName}`) @@ -512,4 +497,153 @@ describe("Editor", () => { } }); }); + + describe("FreeText (bugs)", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must serialize invisible annotations", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#editorFreeText"); + let currentId = 0; + const expected = []; + const oneToFourteen = [...new Array(14).keys()].map(x => x + 1); + + for (const pageNumber of oneToFourteen) { + const pageSelector = `.page[data-page-number = "${pageNumber}"]`; + + await page.evaluate(selector => { + const element = window.document.querySelector(selector); + element.scrollIntoView(); + }, pageSelector); + + const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer`; + await page.waitForSelector(annotationLayerSelector, { + visible: true, + timeout: 0, + }); + await page.waitForTimeout(50); + if (![1, 14].includes(pageNumber)) { + continue; + } + + const rect = await page.$eval(annotationLayerSelector, 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 !! on page ${pageNumber}`; + expected.push(data); + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.type(`${getEditorSelector(currentId)} .internal`, data); + + const editorRect = await page.$eval( + getEditorSelector(currentId), + 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 + ); + + await waitForSelectedEditor(page, getEditorSelector(currentId)); + await waitForStorageEntries(page, currentId + 1); + + const content = await page.$eval(getEditorSelector(currentId), el => + el.innerText.trimEnd() + ); + expect(content).withContext(`In ${browserName}`).toEqual(data); + + currentId += 1; + await page.waitForTimeout(10); + } + + const serialize = proprName => + page.evaluate( + name => + [ + ...window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.values(), + ].map(x => x[name]), + proprName + ); + + expect(await serialize("value")) + .withContext(`In ${browserName}`) + .toEqual(expected); + expect(await serialize("fontSize")) + .withContext(`In ${browserName}`) + .toEqual([10, 10]); + expect(await serialize("color")) + .withContext(`In ${browserName}`) + .toEqual([ + [0, 0, 0], + [0, 0, 0], + ]); + + // Increase the font size for all the annotations. + + // Select all. + await page.keyboard.down("Control"); + await page.keyboard.press("a"); + await page.keyboard.up("Control"); + await page.waitForTimeout(10); + + page.evaluate(() => { + window.PDFViewerApplication.eventBus.dispatch( + "switchannotationeditorparams", + { + source: null, + type: /* AnnotationEditorParamsType.FREETEXT_SIZE */ 1, + value: 13, + } + ); + }); + + await page.waitForTimeout(10); + expect(await serialize("fontSize")) + .withContext(`In ${browserName}`) + .toEqual([13, 13]); + + page.evaluate(() => { + window.PDFViewerApplication.eventBus.dispatch( + "switchannotationeditorparams", + { + source: null, + type: /* AnnotationEditorParamsType.FREETEXT_COLOR */ 2, + value: "#FF0000", + } + ); + }); + + await page.waitForTimeout(10); + expect(await serialize("color")) + .withContext(`In ${browserName}`) + .toEqual([ + [255, 0, 0], + [255, 0, 0], + ]); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.js b/test/integration/test_utils.js index c50938d875d9e0..59e087164db06a 100644 --- a/test/integration/test_utils.js +++ b/test/integration/test_utils.js @@ -100,3 +100,21 @@ async function waitForEvent(page, eventName, timeout = 30000) { ]); } exports.waitForEvent = waitForEvent; + +const waitForStorageEntries = async (page, nEntries) => { + await page.waitForFunction( + n => window.PDFViewerApplication.pdfDocument.annotationStorage.size === n, + {}, + nEntries + ); +}; +exports.waitForStorageEntries = waitForStorageEntries; + +const waitForSelectedEditor = async (page, selector) => { + await page.waitForFunction( + sel => document.querySelector(sel).classList.contains("selectedEditor"), + {}, + selector + ); +}; +exports.waitForSelectedEditor = waitForSelectedEditor; diff --git a/web/default_factory.js b/web/default_factory.js index 0688eee589c288..21ab77431d7328 100644 --- a/web/default_factory.js +++ b/web/default_factory.js @@ -111,7 +111,6 @@ class DefaultAnnotationEditorLayerFactory { * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} l10n - * @property {AnnotationStorage} [annotationStorage] - Storage for annotation * @property {TextAccessibilityManager} [accessibilityManager] * data in forms. */ @@ -126,7 +125,6 @@ class DefaultAnnotationEditorLayerFactory { pdfPage, accessibilityManager = null, l10n, - annotationStorage = null, }) { return new AnnotationEditorLayerBuilder({ uiManager, @@ -134,7 +132,6 @@ class DefaultAnnotationEditorLayerFactory { pdfPage, accessibilityManager, l10n, - annotationStorage, }); } } diff --git a/web/interfaces.js b/web/interfaces.js index e558cb39b8c9a2..c57b289a6de184 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -237,7 +237,6 @@ class IPDFAnnotationEditorLayerFactory { * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} l10n - * @property {AnnotationStorage} [annotationStorage] - Storage for annotation * @property {TextAccessibilityManager} [accessibilityManager] * data in forms. */ @@ -251,7 +250,6 @@ class IPDFAnnotationEditorLayerFactory { pageDiv, pdfPage, l10n, - annotationStorage = null, accessibilityManager, }) {} } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index d6ab1ae3faad54..b0d492e6b84b63 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -730,7 +730,8 @@ class PDFViewer { } else if (isValidAnnotationEditorMode(mode)) { this.#annotationEditorUIManager = new AnnotationEditorUIManager( this.container, - this.eventBus + this.eventBus, + this.pdfDocument?.annotationStorage ); if (mode !== AnnotationEditorType.NONE) { this.#annotationEditorUIManager.updateMode(mode); @@ -1741,7 +1742,6 @@ class PDFViewer { * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} l10n - * @property {AnnotationStorage} [annotationStorage] - Storage for annotation * @property {TextAccessibilityManager} [accessibilityManager] * data in forms. */ @@ -1756,13 +1756,11 @@ class PDFViewer { pdfPage, accessibilityManager = null, l10n, - annotationStorage = this.pdfDocument?.annotationStorage, }) { return new AnnotationEditorLayerBuilder({ uiManager, pageDiv, pdfPage, - annotationStorage, accessibilityManager, l10n, });