diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 4742fe0c4..62401b91b 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -643,6 +643,15 @@ class AnnotationEditorLayer { this.addCommands({ cmd, undo, mustExec: false }); } + getEditorByUID(uid) { + for (const editor of this.#editors.values()) { + if (editor.uid === uid) { + return editor; + } + } + return null; + } + /** * Get an id for an editor. * @returns {string} diff --git a/src/display/editor/comment.js b/src/display/editor/comment.js index 230ff0e55..37ed503f2 100644 --- a/src/display/editor/comment.js +++ b/src/display/editor/comment.js @@ -16,7 +16,9 @@ import { noContextMenu } from "../display_utils.js"; class Comment { - #commentButton = null; + #commentStandaloneButton = null; + + #commentToolbarButton = null; #commentWasFromKeyBoard = false; @@ -32,16 +34,40 @@ class Comment { constructor(editor) { this.#editor = editor; - this.toolbar = null; } - render() { + renderForToolbar() { + const button = (this.#commentToolbarButton = + document.createElement("button")); + button.className = "comment"; + return this.#render(button); + } + + renderForStandalone() { + const button = (this.#commentStandaloneButton = + document.createElement("button")); + button.className = "annotationCommentButton"; + + const position = this.#editor.commentButtonPosition; + if (position) { + const { style } = button; + style.insetInlineEnd = `calc(${ + 100 * + (this.#editor._uiManager.direction === "ltr" + ? 1 - position[0] + : position[0]) + }% - var(--comment-button-dim))`; + style.top = `calc(${100 * position[1]}% - var(--comment-button-dim))`; + } + + return this.#render(button); + } + + #render(comment) { if (!this.#editor._uiManager.hasCommentManager()) { return null; } - const comment = (this.#commentButton = document.createElement("button")); - comment.className = "comment"; comment.tabIndex = "0"; comment.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-button"); @@ -57,7 +83,11 @@ class Comment { const onClick = event => { event.preventDefault(); - this.edit(); + if (comment === this.#commentToolbarButton) { + this.edit(); + } else { + this.#editor._uiManager.toggleComment(this.#editor); + } }; comment.addEventListener("click", onClick, { capture: true, signal }); comment.addEventListener( @@ -86,10 +116,12 @@ class Comment { } finish() { - if (!this.#commentButton) { + if (!this.#commentToolbarButton) { return; } - this.#commentButton.focus({ focusVisible: this.#commentWasFromKeyBoard }); + this.#commentToolbarButton.focus({ + focusVisible: this.#commentWasFromKeyBoard, + }); this.#commentWasFromKeyBoard = false; } @@ -132,18 +164,13 @@ class Comment { this.data = text; } - toggle(enabled = false) { - if (!this.#commentButton) { - return; - } - this.#commentButton.disabled = !enabled; - } - shown() {} destroy() { - this.#commentButton?.remove(); - this.#commentButton = null; + this.#commentToolbarButton?.remove(); + this.#commentToolbarButton = null; + this.#commentStandaloneButton?.remove(); + this.#commentStandaloneButton = null; this.#text = ""; this.#date = null; this.#editor = null; diff --git a/src/display/editor/drawers/freedraw.js b/src/display/editor/drawers/freedraw.js index c3bd3dd5f..8300ca3b3 100644 --- a/src/display/editor/drawers/freedraw.js +++ b/src/display/editor/drawers/freedraw.js @@ -480,6 +480,7 @@ class FreeDrawOutline extends Outline { this.#scaleFactor = scaleFactor; this.#innerMargin = innerMargin; this.#isLTR = isLTR; + this.firstPoint = [NaN, NaN]; this.lastPoint = [NaN, NaN]; this.#computeMinMax(isLTR); @@ -560,9 +561,12 @@ class FreeDrawOutline extends Outline { let lastX = outline[4]; let lastY = outline[5]; const minMax = [lastX, lastY, lastX, lastY]; + let firstPointX = lastX; + let firstPointY = lastY; let lastPointX = lastX; let lastPointY = lastY; const ltrCallback = isLTR ? Math.max : Math.min; + const bezierBbox = new Float32Array(4); for (let i = 6, ii = outline.length; i < ii; i += 6) { const x = outline[i + 4], @@ -571,6 +575,12 @@ class FreeDrawOutline extends Outline { if (isNaN(outline[i])) { Util.pointBoundingBox(x, y, minMax); + if (firstPointY > y) { + firstPointX = x; + firstPointY = y; + } else if (firstPointY === y) { + firstPointX = ltrCallback(firstPointX, x); + } if (lastPointY < y) { lastPointX = x; lastPointY = y; @@ -578,16 +588,34 @@ class FreeDrawOutline extends Outline { lastPointX = ltrCallback(lastPointX, x); } } else { - const bbox = [Infinity, Infinity, -Infinity, -Infinity]; - Util.bezierBoundingBox(lastX, lastY, ...outline.slice(i, i + 6), bbox); + bezierBbox[0] = bezierBbox[1] = Infinity; + bezierBbox[2] = bezierBbox[3] = -Infinity; + Util.bezierBoundingBox( + lastX, + lastY, + ...outline.slice(i, i + 6), + bezierBbox + ); - Util.rectBoundingBox(...bbox, minMax); + Util.rectBoundingBox( + bezierBbox[0], + bezierBbox[1], + bezierBbox[2], + bezierBbox[3], + minMax + ); - if (lastPointY < bbox[3]) { - lastPointX = bbox[2]; - lastPointY = bbox[3]; - } else if (lastPointY === bbox[3]) { - lastPointX = ltrCallback(lastPointX, bbox[2]); + if (firstPointY > bezierBbox[1]) { + firstPointX = bezierBbox[0]; + firstPointY = bezierBbox[1]; + } else if (firstPointY === bezierBbox[1]) { + firstPointX = ltrCallback(firstPointX, bezierBbox[0]); + } + if (lastPointY < bezierBbox[3]) { + lastPointX = bezierBbox[2]; + lastPointY = bezierBbox[3]; + } else if (lastPointY === bezierBbox[3]) { + lastPointX = ltrCallback(lastPointX, bezierBbox[2]); } } lastX = x; @@ -599,6 +627,7 @@ class FreeDrawOutline extends Outline { bbox[1] = minMax[1] - this.#innerMargin; bbox[2] = minMax[2] - minMax[0] + 2 * this.#innerMargin; bbox[3] = minMax[3] - minMax[1] + 2 * this.#innerMargin; + this.firstPoint = [firstPointX, firstPointY]; this.lastPoint = [lastPointX, lastPointY]; } diff --git a/src/display/editor/drawers/highlight.js b/src/display/editor/drawers/highlight.js index ff0cf469a..2809dd5aa 100644 --- a/src/display/editor/drawers/highlight.js +++ b/src/display/editor/drawers/highlight.js @@ -20,6 +20,8 @@ import { Util } from "../../../shared/util.js"; class HighlightOutliner { #box; + #firstPoint; + #lastPoint; #verticalEdges = []; @@ -63,12 +65,30 @@ class HighlightOutliner { const bboxHeight = minMax[3] - minMax[1] + 2 * innerMargin; const shiftedMinX = minMax[0] - innerMargin; const shiftedMinY = minMax[1] - innerMargin; + let firstPointX = isLTR ? -Infinity : Infinity; + let firstPointY = Infinity; const lastEdge = this.#verticalEdges.at(isLTR ? -1 : -2); const lastPoint = [lastEdge[0], lastEdge[2]]; // Convert the coordinates of the edges into box coordinates. for (const edge of this.#verticalEdges) { - const [x, y1, y2] = edge; + const [x, y1, y2, left] = edge; + if (!left && isLTR) { + if (y1 < firstPointY) { + firstPointY = y1; + firstPointX = x; + } else if (y1 === firstPointY) { + firstPointX = Math.max(firstPointX, x); + } + } else if (left && !isLTR) { + if (y1 < firstPointY) { + firstPointY = y1; + firstPointX = x; + } else if (y1 === firstPointY) { + firstPointX = Math.min(firstPointX, x); + } + } + edge[0] = (x - shiftedMinX) / bboxWidth; edge[1] = (y1 - shiftedMinY) / bboxHeight; edge[2] = (y2 - shiftedMinY) / bboxHeight; @@ -80,6 +100,7 @@ class HighlightOutliner { bboxWidth, bboxHeight, ]); + this.#firstPoint = [firstPointX, firstPointY]; this.#lastPoint = lastPoint; } @@ -170,7 +191,12 @@ class HighlightOutliner { } outline.push(lastPointX, lastPointY); } - return new HighlightOutline(outlines, this.#box, this.#lastPoint); + return new HighlightOutline( + outlines, + this.#box, + this.#firstPoint, + this.#lastPoint + ); } #binarySearch(y) { @@ -264,10 +290,11 @@ class HighlightOutline extends Outline { #outlines; - constructor(outlines, box, lastPoint) { + constructor(outlines, box, firstPoint, lastPoint) { super(); this.#outlines = outlines; this.#box = box; + this.firstPoint = firstPoint; this.lastPoint = lastPoint; } diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 452028d24..ee895a905 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -55,6 +55,8 @@ class AnnotationEditor { #comment = null; + #commentStandaloneButton = null; + #disabled = false; #dragPointerId = null; @@ -184,6 +186,7 @@ class AnnotationEditor { this._initialOptions.isCentered = parameters.isCentered; this._structTreeParentId = null; this.annotationElementId = parameters.annotationElementId || null; + this.creationDate = new Date(); const { rotation, @@ -313,6 +316,10 @@ class AnnotationEditor { this.div?.classList.toggle("draggable", value); } + get uid() { + return this.annotationElementId || this.id; + } + /** * @returns {boolean} true if the editor handles the Enter key itself. */ @@ -1166,10 +1173,21 @@ class AnnotationEditor { } addCommentButton() { - if (this.#comment) { - return this.#comment; + return (this.#comment ||= new Comment(this)); + } + + addStandaloneCommentButton() { + this.#comment ||= new Comment(this); + if (this.#commentStandaloneButton) { + return; } - return (this.#comment = new Comment(this)); + this.#commentStandaloneButton = this.#comment.renderForStandalone(); + this.div.append(this.#commentStandaloneButton); + } + + removeStandaloneCommentButton() { + this.#commentStandaloneButton?.remove(); + this.#commentStandaloneButton = null; } get commentColor() { @@ -1204,6 +1222,10 @@ class AnnotationEditor { return this.#comment?.hasBeenEdited(); } + get hasComment() { + return !!this.#comment && !this.#comment.isDeleted(); + } + async editComment() { if (!this.#comment) { this.#comment = new Comment(this); @@ -1211,6 +1233,8 @@ class AnnotationEditor { this.#comment.edit(); } + showComment() {} + addComment(serialized) { if (this.hasEditedComment) { const DEFAULT_POPUP_WIDTH = 180; @@ -1581,6 +1605,17 @@ class AnnotationEditor { return this.getRect(0, 0); } + getData() { + return { + id: this.uid, + pageIndex: this.pageIndex, + rect: this.getPDFRect(), + contentsObj: { str: this.comment.text }, + creationDate: this.creationDate, + popupRef: !this.#comment.isDeleted(), + }; + } + /** * Executed once this editor has been rendered. * @param {boolean} focus - true if the editor should be focused. @@ -1814,6 +1849,14 @@ class AnnotationEditor { return null; } + /** + * Get the position of the comment button. + * @returns {Array|null} + */ + get commentButtonPosition() { + return this._uiManager.direction === "ltr" ? [1, 0] : [0, 0]; + } + /** * onkeydown callback. * @param {KeyboardEvent} event diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index a4fe5ba6a..090edf5aa 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -60,6 +60,8 @@ class HighlightEditor extends AnnotationEditor { #isFreeHighlight = false; + #firstPoint = null; + #lastPoint = null; #opacity; @@ -177,7 +179,11 @@ class HighlightEditor extends AnnotationEditor { this.#focusOutlines = outlinerForOutline.getOutlines(); // The last point is in the pages coordinate system. - const { lastPoint } = this.#focusOutlines; + const { firstPoint, lastPoint } = this.#focusOutlines; + this.#firstPoint = [ + (firstPoint[0] - this.x) / this.width, + (firstPoint[1] - this.y) / this.height, + ]; this.#lastPoint = [ (lastPoint[0] - this.x) / this.width, (lastPoint[1] - this.y) / this.height, @@ -268,7 +274,11 @@ class HighlightEditor extends AnnotationEditor { } } - const { lastPoint } = this.#focusOutlines; + const { firstPoint, lastPoint } = this.#focusOutlines; + this.#firstPoint = [ + (firstPoint[0] - x) / width, + (firstPoint[1] - y) / height, + ]; this.#lastPoint = [(lastPoint[0] - x) / width, (lastPoint[1] - y) / height]; } @@ -299,6 +309,11 @@ class HighlightEditor extends AnnotationEditor { return this.#lastPoint; } + /** @inheritdoc */ + get commentButtonPosition() { + return this.#firstPoint; + } + /** @inheritdoc */ updateParams(type, value) { switch (type) { diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js index 875a22aff..bf961fe01 100644 --- a/src/display/editor/toolbar.js +++ b/src/display/editor/toolbar.js @@ -162,7 +162,7 @@ class EditorToolbar { if (this.#comment) { return; } - const button = comment.render(); + const button = comment.renderForToolbar(); if (!button) { return; } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index b054d1889..6a1239403 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -896,6 +896,7 @@ class AnnotationEditorUIManager { this.isShiftKeyDown = false; this._editorUndoBar = editorUndoBar || null; this._supportsPinchToZoom = supportsPinchToZoom !== false; + commentManager?.setSidebarUiManager(this); if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { Object.defineProperty(this, "reset", { @@ -1071,6 +1072,27 @@ class AnnotationEditorUIManager { this.#commentManager?.open(this, editor, position); } + showComment(pageIndex, uid) { + const layer = this.#allLayers.get(pageIndex); + const editor = layer?.getEditorByUID(uid); + editor?.showComment(); + } + + async waitForPageRendered(pageNumber) { + if (this.#allLayers.has(pageNumber - 1)) { + return; + } + const { resolve, promise } = Promise.withResolvers(); + const onPageRendered = evt => { + if (evt.pageNumber === pageNumber) { + this._eventBus._off("annotationeditorlayerrendered", onPageRendered); + resolve(); + } + }; + this._eventBus.on("annotationeditorlayerrendered", onPageRendered); + await promise; + } + getSignature(editor) { this.#signatureManager?.getSignature({ uiManager: this, editor }); } @@ -1799,6 +1821,9 @@ class AnnotationEditorUIManager { if (this.#mode === AnnotationEditorType.POPUP) { this.#commentManager?.hideSidebar(); + for (const editor of this.#allEditors.values()) { + editor.removeStandaloneCommentButton(); + } } this.#mode = mode; @@ -1815,13 +1840,6 @@ class AnnotationEditorUIManager { if (mode === AnnotationEditorType.SIGNATURE) { await this.#signatureManager?.loadSignatures(); } - if (mode === AnnotationEditorType.POPUP) { - this.#allEditableAnnotations ||= - await this.#pdfDocument.getAnnotationsByType( - new Set(this.#editorTypes.map(editorClass => editorClass._editorType)) - ); - this.#commentManager?.showSidebar(this.#allEditableAnnotations); - } this.setEditingState(true); await this.#enableAll(); @@ -1829,6 +1847,40 @@ class AnnotationEditorUIManager { for (const layer of this.#allLayers.values()) { layer.updateMode(mode); } + + if (mode === AnnotationEditorType.POPUP) { + this.#allEditableAnnotations ||= + await this.#pdfDocument.getAnnotationsByType( + new Set(this.#editorTypes.map(editorClass => editorClass._editorType)) + ); + const elementIds = new Set(); + const allComments = []; + for (const editor of this.#allEditors.values()) { + const { annotationElementId, hasComment, deleted } = editor; + if (annotationElementId) { + elementIds.add(annotationElementId); + } + if (hasComment && !deleted) { + allComments.push(editor.getData()); + editor.addStandaloneCommentButton(); + } + } + for (const annotation of this.#allEditableAnnotations) { + const { id, popupRef, contentsObj } = annotation; + if ( + popupRef && + contentsObj?.str && + !elementIds.has(id) && + !this.#deletedAnnotationsElementIds.has(id) + ) { + // The annotation exists in the PDF and has a comment but there + // is no editor for it (anymore). + allComments.push(annotation); + } + } + this.#commentManager?.showSidebar(allComments); + } + if (!editId) { if (isFromKeyboard) { this.addNewEditorFromKeyboard(); diff --git a/web/comment_manager.css b/web/comment_manager.css index 59a795912..c3d7088b5 100644 --- a/web/comment_manager.css +++ b/web/comment_manager.css @@ -251,7 +251,7 @@ } } -.annotationLayer.disabled .annotationCommentButton { +.annotationLayer.disabled :is(.annotationCommentButton) { display: none; } diff --git a/web/comment_manager.js b/web/comment_manager.js index 58a503014..5d15247b5 100644 --- a/web/comment_manager.js +++ b/web/comment_manager.js @@ -173,6 +173,10 @@ class CommentManager { overlayManager.register(dialog); } + setSidebarUiManager(uiManager) { + this.#sidebar.setUIManager(uiManager); + } + showSidebar(annotations) { this.#sidebar.show(annotations); } @@ -435,6 +439,8 @@ class CommentSidebar { #idsToElements = null; + #uiManager = null; + constructor( { sidebar, @@ -472,12 +478,14 @@ class CommentSidebar { this.#sidebar.hidden = true; } + setUIManager(uiManager) { + this.#uiManager = uiManager; + } + show(annotations) { this.#elementsToAnnotations = new WeakMap(); this.#idsToElements = new Map(); - this.#annotations = annotations = annotations.filter( - a => a.popupRef && a.contentsObj?.str - ); + this.#annotations = annotations; annotations.sort(this.#sortComments.bind(this)); if (annotations.length !== 0) { const fragment = document.createDocumentFragment(); @@ -621,6 +629,7 @@ class CommentSidebar { #createCommentElement(annotation) { const { + id, creationDate, modificationDate, contentsObj: { str: text }, @@ -646,11 +655,11 @@ class CommentSidebar { commentItem.addEventListener("keydown", this.#boundCommentKeydown); this.#elementsToAnnotations.set(commentItem, annotation); - this.#idsToElements.set(annotation.id, commentItem); + this.#idsToElements.set(id, commentItem); return commentItem; } - #commentClick({ currentTarget }) { + async #commentClick({ currentTarget }) { if (currentTarget.classList.contains("selected")) { return; } @@ -658,14 +667,18 @@ class CommentSidebar { if (!annotation) { return; } - const { pageIndex, rect } = annotation; + const { id, pageIndex, rect } = annotation; const SPACE_ABOVE_ANNOTATION = 10; + const pageNumber = pageIndex + 1; + const pageVisiblePromise = this.#uiManager?.waitForPageRendered(pageNumber); this.#linkService?.goToXY( - pageIndex + 1, + pageNumber, rect[0], rect[3] + SPACE_ABOVE_ANNOTATION ); this.selectComment(currentTarget); + await pageVisiblePromise; + this.#uiManager?.showComment(pageIndex, id); } #commentKeydown(e) {