From 636ff503ffcc2062176c7e4cc32cc02208302e56 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 10 Jul 2025 19:26:33 +0200 Subject: [PATCH] [Editor] Add the possibility to add Popup annotations (bug 1976724) It's a first big step. This patch contains a new modal dialog to let the user edit/update a Popup --- extensions/chromium/preferences_schema.json | 5 + l10n/en-US/viewer.ftl | 21 ++ src/display/annotation_layer.js | 57 ++- src/display/display_utils.js | 3 + src/display/editor/comment.js | 150 ++++++++ src/display/editor/editor.js | 59 +++ src/display/editor/freetext.js | 16 +- src/display/editor/highlight.js | 34 +- src/display/editor/ink.js | 15 +- src/display/editor/signature.js | 1 + src/display/editor/stamp.js | 16 +- src/display/editor/toolbar.js | 23 ++ src/display/editor/tools.js | 22 +- src/pdf.js | 3 + src/shared/util.js | 1 + test/unit/display_utils_spec.js | 2 + test/unit/pdf_spec.js | 2 + web/annotation_editor_layer_builder.css | 19 +- web/annotation_layer_builder.css | 1 + web/app.js | 6 + web/app_options.js | 5 + web/comment_manager.css | 288 +++++++++++++++ web/comment_manager.js | 375 ++++++++++++++++++++ web/images/comment-actionsButton.svg | 3 + web/images/comment-closeButton.svg | 3 + web/images/comment-editButton.svg | 3 + web/pdf_viewer.js | 10 +- web/pdfjs.js | 2 + web/viewer.html | 31 ++ web/viewer.js | 13 + 30 files changed, 1161 insertions(+), 28 deletions(-) create mode 100644 src/display/editor/comment.js create mode 100644 web/comment_manager.css create mode 100644 web/comment_manager.js create mode 100644 web/images/comment-actionsButton.svg create mode 100644 web/images/comment-closeButton.svg create mode 100644 web/images/comment-editButton.svg diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 2bfa8ba15..3b101c956 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -233,6 +233,11 @@ "description": "Enable creation of hyperlinks from text that look like URLs.", "type": "boolean", "default": true + }, + "enableComment": { + "description": "Enable creation of comment annotations.", + "type": "boolean", + "default": false } } } diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index fa1ea61dc..0f77977cf 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -638,3 +638,24 @@ pdfjs-editor-edit-signature-dialog-title = Edit description ## Dialog buttons pdfjs-editor-edit-signature-update-button = Update + +## Edit a comment dialog + +pdfjs-editor-edit-comment-actions-button-label = Actions +pdfjs-editor-edit-comment-actions-button = + .title = Actions +pdfjs-editor-edit-comment-close-button-label = Close +pdfjs-editor-edit-comment-close-button = + .title = Close +pdfjs-editor-edit-comment-actions-edit-button-label = Edit +pdfjs-editor-edit-comment-actions-delete-button-label = Delete +pdfjs-editor-edit-comment-manager-text-input = + .placeholder = Enter your comment + +pdfjs-editor-edit-comment-manager-cancel-button = Cancel +pdfjs-editor-edit-comment-manager-save-button = Save + +## Edit a comment button in the editor toolbar + +pdfjs-editor-edit-comment-button = + .title = Edit comment diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index a80da88dd..f40bf02d1 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -202,17 +202,32 @@ class AnnotationElement { return; } - this.#updates ||= { - rect: this.data.rect.slice(0), - }; + if (params.rect) { + this.#updates ||= { + rect: this.data.rect.slice(0), + }; + } - const { rect } = params; + const { rect, popup: newPopup } = params; if (rect) { this.#setRectEdited(rect); } - this.#popupElement?.popup.updateEdited(params); + let popup = this.#popupElement?.popup || this.popup; + if (!popup && newPopup.text) { + this._createPopup(newPopup); + popup = this.#popupElement.popup; + } + if (!popup) { + return; + } + popup.updateEdited(params); + if (newPopup.deleted) { + popup.remove(); + this.#popupElement = null; + this.popup = null; + } } resetEdited() { @@ -598,18 +613,30 @@ class AnnotationElement { * annotations that do not have a Popup entry in the dictionary, but * are of a type that works with popups (such as Highlight annotations). * + * @param {Object} [popupData] - The data for the popup, if any. + * * @private * @memberof AnnotationElement */ - _createPopup() { + _createPopup(popupData = null) { const { data } = this; + let contentsObj, modificationDate; + if (popupData) { + contentsObj = { + str: popupData.text, + }; + modificationDate = popupData.date; + } else { + contentsObj = data.contentsObj; + modificationDate = data.modificationDate; + } const popup = (this.#popupElement = new PopupAnnotationElement({ data: { color: data.color, titleObj: data.titleObj, - modificationDate: data.modificationDate, - contentsObj: data.contentsObj, + modificationDate, + contentsObj, richText: data.richText, parentRect: data.rect, borderStyle: 0, @@ -617,12 +644,17 @@ class AnnotationElement { rotation: data.rotation, noRotate: true, }, + linkService: this.linkService, parent: this.parent, elements: [this], })); this.parent.div.append(popup.render()); } + get hasPopupElement() { + return !!(this.#popupElement || this.popup || this.data.popupRef); + } + /** * Render the annotation's HTML element(s). * @@ -2352,8 +2384,8 @@ class PopupElement { } } - updateEdited({ rect, popupContent, deleted }) { - if (deleted) { + updateEdited({ rect, popup, deleted }) { + if (deleted || popup?.deleted) { this.remove(); return; } @@ -2365,8 +2397,9 @@ class PopupElement { if (rect) { this.#position = null; } - if (popupContent) { - this.#richText = this.#makePopupContent(popupContent); + if (popup) { + this.#richText = this.#makePopupContent(popup.text); + this.#dateObj = PDFDateString.toDateObject(popup.date); this.#contentsObj = null; } this.#popup?.remove(); diff --git a/src/display/display_utils.js b/src/display/display_utils.js index d6ed53da3..1db092a9d 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -502,6 +502,9 @@ class PDFDateString { * @returns {Date|null} */ static toDateObject(input) { + if (input instanceof Date) { + return input; + } if (!input || typeof input !== "string") { return null; } diff --git a/src/display/editor/comment.js b/src/display/editor/comment.js new file mode 100644 index 000000000..da467d444 --- /dev/null +++ b/src/display/editor/comment.js @@ -0,0 +1,150 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { noContextMenu } from "../display_utils.js"; + +class Comment { + #commentButton = null; + + #commentWasFromKeyBoard = false; + + #editor = null; + + #initialText = null; + + #text = null; + + #date = null; + + #deleted = false; + + constructor(editor) { + this.#editor = editor; + this.toolbar = null; + } + + render() { + 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"); + + const signal = this.#editor._uiManager._signal; + comment.addEventListener("contextmenu", noContextMenu, { signal }); + comment.addEventListener("pointerdown", event => event.stopPropagation(), { + signal, + }); + + const onClick = event => { + event.preventDefault(); + this.edit(); + }; + comment.addEventListener("click", onClick, { capture: true, signal }); + comment.addEventListener( + "keydown", + event => { + if (event.target === comment && event.key === "Enter") { + this.#commentWasFromKeyBoard = true; + onClick(event); + } + }, + { signal } + ); + + return comment; + } + + edit() { + const { bottom, left, right } = this.#editor.getClientDimensions(); + const position = { top: bottom }; + if (this.#editor._uiManager.direction === "ltr") { + position.right = right; + } else { + position.left = left; + } + this.#editor._uiManager.editComment(this.#editor, position); + } + + finish() { + if (!this.#commentButton) { + return; + } + this.#commentButton.focus({ focusVisible: this.#commentWasFromKeyBoard }); + this.#commentWasFromKeyBoard = false; + } + + isDeleted() { + return this.#deleted || this.#text === ""; + } + + hasBeenEdited() { + return this.isDeleted() || this.#text !== this.#initialText; + } + + serialize() { + return this.data; + } + + get data() { + return { + text: this.#text, + date: this.#date, + deleted: this.#deleted, + }; + } + + /** + * Set the comment data. + */ + set data(text) { + if (text === null) { + this.#text = ""; + this.#deleted = true; + return; + } + this.#text = text; + this.#date = new Date(); + this.#deleted = false; + } + + setInitialText(text) { + this.#initialText = text; + this.data = text; + } + + toggle(enabled = false) { + if (!this.#commentButton) { + return; + } + this.#commentButton.disabled = !enabled; + } + + shown() {} + + destroy() { + this.#commentButton?.remove(); + this.#commentButton = null; + this.#text = ""; + this.#date = null; + this.#editor = null; + this.#commentWasFromKeyBoard = false; + this.#deleted = false; + } +} + +export { Comment }; diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index a0153b63f..3bab444b5 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -30,6 +30,7 @@ import { } from "../../shared/util.js"; import { noContextMenu, stopEvent } from "../display_utils.js"; import { AltText } from "./alt_text.js"; +import { Comment } from "./comment.js"; import { EditorToolbar } from "./toolbar.js"; import { TouchManager } from "../touch_manager.js"; @@ -52,6 +53,8 @@ class AnnotationEditor { #altText = null; + #comment = null; + #disabled = false; #dragPointerId = null; @@ -1075,6 +1078,7 @@ class AnnotationEditor { } this._editToolbar = new EditorToolbar(this); this.div.append(this._editToolbar.render()); + this._editToolbar.addButton("comment", this.addCommentButton()); const { toolbarButtons } = this; if (toolbarButtons) { for (const [name, tool] of toolbarButtons) { @@ -1161,6 +1165,61 @@ class AnnotationEditor { return this.#altText?.hasData() ?? false; } + addCommentButton() { + if (this.#comment) { + return this.#comment; + } + return (this.#comment = new Comment(this)); + } + + get commentColor() { + return null; + } + + get comment() { + const comment = this.#comment; + return { + text: comment.data.text, + date: comment.data.date, + deleted: comment.isDeleted(), + color: this.commentColor, + }; + } + + set comment(text) { + if (!this.#comment) { + this.#comment = new Comment(this); + } + this.#comment.data = text; + } + + setCommentData(text) { + if (!this.#comment) { + this.#comment = new Comment(this); + } + this.#comment.setInitialText(text); + } + + get hasEditedComment() { + return this.#comment?.hasBeenEdited(); + } + + async editComment() { + if (!this.#comment) { + this.#comment = new Comment(this); + } + this.#comment.edit(); + } + + addComment(serialized) { + if (this.hasEditedComment) { + serialized.popup = { + contents: this.comment.text, + deleted: this.comment.deleted, + }; + } + } + /** * Render this editor in a div. * @returns {HTMLDivElement | null} diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index b362eca35..352110f5d 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -763,6 +763,7 @@ class FreeTextEditor extends AnnotationEditor { rotation, id, popupRef, + contentsObj, }, textContent, textPosition, @@ -789,6 +790,7 @@ class FreeTextEditor extends AnnotationEditor { id, deleted: false, popupRef, + comment: contentsObj?.str || null, }; } const editor = await super.deserialize(data, parent, uiManager); @@ -796,6 +798,9 @@ class FreeTextEditor extends AnnotationEditor { editor.#color = Util.makeHexColor(...data.color); editor.#content = FreeTextEditor.#deserializeContent(data.value); editor._initialData = initialData; + if (data.comment) { + editor.setCommentData(data.comment); + } return editor; } @@ -828,6 +833,7 @@ class FreeTextEditor extends AnnotationEditor { rotation: this.rotation, structTreeParentId: this._structTreeParentId, }; + this.addComment(serialized); if (isForCopying) { // Don't add the id when copying because the pasted editor mustn't be @@ -849,6 +855,7 @@ class FreeTextEditor extends AnnotationEditor { const { value, fontSize, color, pageIndex } = this._initialData; return ( + this.hasEditedComment || this._hasBeenMoved || serialized.value !== value || serialized.fontSize !== fontSize || @@ -874,10 +881,13 @@ class FreeTextEditor extends AnnotationEditor { } const padding = FreeTextEditor._internalPadding * this.parentScale; - annotation.updateEdited({ + const params = { rect: this.getRect(padding, padding), - popupContent: this.#content, - }); + }; + params.popup = this.hasEditedComment + ? this.comment + : { text: this.#content }; + annotation.updateEdited(params); return content; } diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index e909d91ce..3fb01d407 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -151,6 +151,10 @@ class HighlightEditor extends AnnotationEditor { }; } + get commentColor() { + return this.color; + } + static computeTelemetryFinalData(data) { // We want to know how many colors have been used. return { numberOfColors: data.get("color").size }; @@ -866,7 +870,16 @@ class HighlightEditor extends AnnotationEditor { let initialData = null; if (data instanceof HighlightAnnotationElement) { const { - data: { quadPoints, rect, rotation, id, color, opacity, popupRef }, + data: { + quadPoints, + rect, + rotation, + id, + color, + opacity, + popupRef, + contentsObj, + }, parent: { page: { pageNumber }, }, @@ -884,6 +897,7 @@ class HighlightEditor extends AnnotationEditor { id, deleted: false, popupRef, + comment: contentsObj?.str || null, }; } else if (data instanceof InkAnnotationElement) { const { @@ -895,6 +909,7 @@ class HighlightEditor extends AnnotationEditor { color, borderStyle: { rawWidth: thickness }, popupRef, + contentsObj, }, parent: { page: { pageNumber }, @@ -913,6 +928,7 @@ class HighlightEditor extends AnnotationEditor { id, deleted: false, popupRef, + comment: contentsObj?.str || null, }; } @@ -925,6 +941,9 @@ class HighlightEditor extends AnnotationEditor { editor.#thickness = data.thickness; } editor._initialData = initialData; + if (data.comment) { + editor.setCommentData(data.comment); + } const [pageWidth, pageHeight] = editor.pageDimensions; const [pageX, pageY] = editor.pageTranslation; @@ -1019,6 +1038,7 @@ class HighlightEditor extends AnnotationEditor { rotation: this.#getRotation(), structTreeParentId: this._structTreeParentId, }; + this.addComment(serialized); if (this.annotationElementId && !this.#hasElementChanged(serialized)) { return null; @@ -1030,14 +1050,20 @@ class HighlightEditor extends AnnotationEditor { #hasElementChanged(serialized) { const { color } = this._initialData; - return serialized.color.some((c, i) => c !== color[i]); + return ( + this.hasEditedComment || serialized.color.some((c, i) => c !== color[i]) + ); } /** @inheritdoc */ renderAnnotationElement(annotation) { - annotation.updateEdited({ + const params = { rect: this.getRect(0, 0), - }); + }; + if (this.hasEditedComment) { + params.popup = this.comment; + } + annotation.updateEdited(params); return null; } diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 56f6c560a..debbe282a 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -149,6 +149,7 @@ class InkEditor extends DrawingEditor { opacity, borderStyle: { rawWidth: thickness }, popupRef, + contentsObj, }, parent: { page: { pageNumber }, @@ -168,11 +169,15 @@ class InkEditor extends DrawingEditor { id, deleted: false, popupRef, + comment: contentsObj?.str || null, }; } const editor = await super.deserialize(data, parent, uiManager); editor._initialData = initialData; + if (data.comment) { + editor.setCommentData(data.comment); + } return editor; } @@ -245,6 +250,7 @@ class InkEditor extends DrawingEditor { rotation: this.rotation, structTreeParentId: this._structTreeParentId, }; + this.addComment(serialized); if (isForCopying) { serialized.isCopy = true; @@ -262,6 +268,7 @@ class InkEditor extends DrawingEditor { #hasElementChanged(serialized) { const { color, thickness, opacity, pageIndex } = this._initialData; return ( + this.hasEditedComment || this._hasBeenMoved || this._hasBeenResized || serialized.color.some((c, i) => c !== color[i]) || @@ -274,11 +281,15 @@ class InkEditor extends DrawingEditor { /** @inheritdoc */ renderAnnotationElement(annotation) { const { points, rect } = this.serializeDraw(/* isForCopying = */ false); - annotation.updateEdited({ + const params = { rect, thickness: this._drawingOptions["stroke-width"], points, - }); + }; + if (this.hasEditedComment) { + params.popup = this.comment; + } + annotation.updateEdited(params); return null; } diff --git a/src/display/editor/signature.js b/src/display/editor/signature.js index 676b97f0f..0dc38ecd1 100644 --- a/src/display/editor/signature.js +++ b/src/display/editor/signature.js @@ -389,6 +389,7 @@ class SignatureEditor extends DrawingEditor { rotation: this.rotation, structTreeParentId: this._structTreeParentId, }; + this.addComment(serialized); if (isForCopying) { serialized.paths = { lines, points }; serialized.uuid = this.#signatureUUID; diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index cd46889d4..ae4571d4f 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -750,7 +750,7 @@ class StampEditor extends AnnotationEditor { let missingCanvas = false; if (data instanceof StampAnnotationElement) { const { - data: { rect, rotation, id, structParent, popupRef }, + data: { rect, rotation, id, structParent, popupRef, contentsObj }, container, parent: { page: { pageNumber }, @@ -794,6 +794,7 @@ class StampEditor extends AnnotationEditor { isSvg: false, structParent, popupRef, + comment: contentsObj?.str || null, }; } const editor = await super.deserialize(data, parent, uiManager); @@ -820,6 +821,9 @@ class StampEditor extends AnnotationEditor { editor.altTextData = accessibilityData; } editor._initialData = initialData; + if (data.comment) { + editor.setCommentData(data.comment); + } // No need to be add in the undo stack if the editor is created from an // existing one. editor.#hasBeenAddedInUndoStack = !!initialData; @@ -846,6 +850,7 @@ class StampEditor extends AnnotationEditor { isSvg: this.#isSvg, structTreeParentId: this._structTreeParentId, }; + this.addComment(serialized); if (isForCopying) { // We don't know what's the final destination (this pdf or another one) @@ -914,6 +919,7 @@ class StampEditor extends AnnotationEditor { return { isSame: + !this.hasEditedComment && !this._hasBeenMoved && !this._hasBeenResized && isSamePageIndex && @@ -924,9 +930,13 @@ class StampEditor extends AnnotationEditor { /** @inheritdoc */ renderAnnotationElement(annotation) { - annotation.updateEdited({ + const params = { rect: this.getRect(0, 0), - }); + }; + if (this.hasEditedComment) { + params.popup = this.comment; + } + annotation.updateEdited(params); return null; } diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js index faf683856..5e428596b 100644 --- a/src/display/editor/toolbar.js +++ b/src/display/editor/toolbar.js @@ -26,6 +26,8 @@ class EditorToolbar { #altText = null; + #comment = null; + #signatureDescriptionButton = null; static #l10nRemove = null; @@ -114,6 +116,7 @@ class EditorToolbar { show() { this.#toolbar.classList.remove("hidden"); this.#altText?.shown(); + this.#comment?.shown(); } addDeleteButton() { @@ -147,7 +150,24 @@ class EditorToolbar { this.#altText = altText; } + addComment(comment) { + if (this.#comment) { + return; + } + const button = comment.render(); + if (!button) { + return; + } + this.#addListenersToElement(button); + this.#buttons.prepend(button, this.#divider); + this.#comment = comment; + comment.toolbar = this; + } + addColorPicker(colorPicker) { + if (this.#colorPicker) { + return; + } this.#colorPicker = colorPicker; const button = colorPicker.renderButton(); this.#addListenersToElement(button); @@ -175,6 +195,9 @@ class EditorToolbar { case "delete": this.addDeleteButton(); break; + case "comment": + this.addComment(tool); + break; } } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index c7a78139d..c0dc10447 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -597,6 +597,8 @@ class AnnotationEditorUIManager { #commandManager = new CommandManager(); + #commentManager = null; + #copyPasteAC = null; #currentDrawingSession = null; @@ -822,6 +824,7 @@ class AnnotationEditorUIManager { viewer, viewerAlert, altTextManager, + commentManager, signatureManager, eventBus, pdfDocument, @@ -839,6 +842,7 @@ class AnnotationEditorUIManager { this.#viewer = viewer; this.#viewerAlert = viewerAlert; this.#altTextManager = altTextManager; + this.#commentManager = commentManager; this.#signatureManager = signatureManager; this._eventBus = eventBus; eventBus._on("editingaction", this.onEditingAction.bind(this), { signal }); @@ -902,6 +906,7 @@ class AnnotationEditorUIManager { this.#selectedEditors.clear(); this.#commandManager.destroy(); this.#altTextManager?.destroy(); + this.#commentManager?.destroy(); this.#signatureManager?.destroy(); this.#highlightToolbar?.hide(); this.#highlightToolbar = null; @@ -1003,6 +1008,14 @@ class AnnotationEditorUIManager { this.#altTextManager?.editAltText(this, editor, firstTime); } + hasCommentManager() { + return !!this.#commentManager; + } + + editComment(editor, position) { + this.#commentManager?.open(this, editor, position); + } + getSignature(editor) { this.#signatureManager?.getSignature({ uiManager: this, editor }); } @@ -1686,12 +1699,15 @@ class AnnotationEditorUIManager { * keyboard action. * @param {boolean} [mustEnterInEditMode] - true if the editor must enter in * edit mode. + * @param {boolean} [editComment] - true if the mode change is due to a + * comment edit. */ async updateMode( mode, editId = null, isFromKeyboard = false, - mustEnterInEditMode = false + mustEnterInEditMode = false, + editComment = false ) { if (this.#mode === mode) { return; @@ -1739,7 +1755,9 @@ class AnnotationEditorUIManager { for (const editor of this.#allEditors.values()) { if (editor.annotationElementId === editId || editor.id === editId) { this.setSelected(editor); - if (mustEnterInEditMode) { + if (editComment) { + editor.editComment(); + } else if (mustEnterInEditMode) { editor.enterInEditMode(); } } else { diff --git a/src/pdf.js b/src/pdf.js index c8c0d82a2..d5cc9aff9 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -55,6 +55,7 @@ import { fetchData, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getXfaPageViewport, isDataScheme, isPdfFile, @@ -106,6 +107,7 @@ globalThis.pdfjsLib = { getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getUuid, getXfaPageViewport, GlobalWorkerOptions, @@ -160,6 +162,7 @@ export { getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getUuid, getXfaPageViewport, GlobalWorkerOptions, diff --git a/src/shared/util.js b/src/shared/util.js index 593b6eca7..8f2b36c67 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -76,6 +76,7 @@ const AnnotationEditorType = { STAMP: 13, INK: 15, SIGNATURE: 101, + COMMENT: 102, }; const AnnotationEditorParamsType = { diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index 8429c1129..7369021e5 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -295,6 +295,8 @@ describe("display_utils", function () { expect(result).toEqual(expectation); } } + const now = new Date(); + expect(PDFDateString.toDateObject(now)).toEqual(now); }); }); }); diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 26b8a5171..c1409b4b2 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -46,6 +46,7 @@ import { fetchData, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getXfaPageViewport, isDataScheme, isPdfFile, @@ -90,6 +91,7 @@ const expectedAPI = Object.freeze({ getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getUuid, getXfaPageViewport, GlobalWorkerOptions, diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 49e235b81..643eac2a4 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -16,8 +16,10 @@ @import url(draw_layer_builder.css); @import url(toggle_button.css); @import url(signature_manager.css); +@import url(comment_manager.css); :root { + --editor-toolbar-vert-offset: 6px; --outline-width: 2px; --outline-color: #0060df; --outline-around-width: 1px; @@ -228,6 +230,7 @@ --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); --editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33); --editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg); + --editor-toolbar-comment-edit-image: url(images/comment-editButton.svg); --editor-toolbar-fg-color: light-dark(#2e2e56, #fbfbfe); --editor-toolbar-border-color: #8f8f9d; --editor-toolbar-hover-border-color: var(--editor-toolbar-border-color); @@ -236,7 +239,6 @@ --editor-toolbar-hover-outline: none; --editor-toolbar-focus-outline-color: light-dark(#0060df, #0df); --editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2); - --editor-toolbar-vert-offset: 6px; --editor-toolbar-height: 28px; --editor-toolbar-padding: 2px; --alt-text-done-color: light-dark(#2ac3a2, #54ffbd); @@ -489,6 +491,21 @@ } } } + + .comment { + width: var(--editor-toolbar-height); + + &::before { + content: ""; + mask-image: var(--editor-toolbar-comment-edit-image); + mask-repeat: no-repeat; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 100%; + height: 100%; + } + } } } } diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index c1c2a8d54..566764d8a 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -17,6 +17,7 @@ color-scheme: only light; --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); + --comment-edit-image: url(images/comment_editButton.svg); --input-focus-border-color: Highlight; --input-focus-outline: 1px solid Canvas; --input-unfocused-border-color: transparent; diff --git a/web/app.js b/web/app.js index 588fbe52e..787473df7 100644 --- a/web/app.js +++ b/web/app.js @@ -71,6 +71,7 @@ import { LinkTarget, PDFLinkService } from "./pdf_link_service.js"; import { AltTextManager } from "web-alt_text_manager"; import { AnnotationEditorParams } from "web-annotation_editor_params"; import { CaretBrowsingMode } from "./caret_browsing.js"; +import { CommentManager } from "./comment_manager.js"; import { DownloadManager } from "web-download_manager"; import { EditorUndoBar } from "./editor_undo_bar.js"; import { OverlayManager } from "./overlay_manager.js"; @@ -484,6 +485,10 @@ const PDFViewerApplication = { eventBus ) : null; + const commentManager = + AppOptions.get("enableComment") && appConfig.editCommentDialog + ? new CommentManager(appConfig.editCommentDialog, overlayManager) + : null; const enableHWA = AppOptions.get("enableHWA"), maxCanvasPixels = AppOptions.get("maxCanvasPixels"), @@ -498,6 +503,7 @@ const PDFViewerApplication = { linkService, downloadManager, altTextManager, + commentManager, signatureManager, editorUndoBar: this.editorUndoBar, findController, diff --git a/web/app_options.js b/web/app_options.js index e933f39be..e5bfbbada 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -218,6 +218,11 @@ const defaultOptions = { value: true, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableComment: { + /** @type {boolean} */ + value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableDetailCanvas: { /** @type {boolean} */ value: true, diff --git a/web/comment_manager.css b/web/comment_manager.css new file mode 100644 index 000000000..c73a19d40 --- /dev/null +++ b/web/comment_manager.css @@ -0,0 +1,288 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#commentManagerDialog { + --comment-actions-button-icon: url(images/comment-actionsButton.svg); + --comment-close-button-icon: url(images/comment-closeButton.svg); + + --default-dialog-bg-color: #ffff98; + --dialog-base-color: var(--default-dialog-bg-color); + --dialog-bg-color: color-mix(in srgb, var(--dialog-base-color), white 30%); + --dialog-border-color: var(--dialog-base-color); + + --menuitem-bg-color: transparent; + --menuitem-fg-color: black; + --menuitem-hover-bg-color: #3383e7; + --menuitem-hover-fg-color: white; + + --comment-text-input-bg: white; + --comment-text-input-fg: black; + --comment-text-input-border: #0060df; + --comment-focus-outline-color: #0060df; + + --hover-filter: brightness(0.9); + --text-primary-color: #15141a; + + --button-secondary-bg-color: #f0f0f4; + --button-secondary-active-bg-color: color-mix( + in srgb, + var(--button-secondary-bg-color), + black 14% + ); + --button-secondary-hover-bg-color: color-mix( + in srgb, + var(--button-secondary-bg-color), + black 7% + ); + + --button-primary-bg-color: #0060df; + --button-primary-fg-color: #fbfbfe; + --button-primary-active-bg-color: #0050c0; + --button-primary-hover-bg-color: #0250bb; + + --menu-bg-color: rgb(253 250 244); + --menu-button-border-color: transparent; + --menu-button-focus-outline-color: var(--comment-text-input-border); + + @media screen and (forced-colors: active) { + --hover-filter: none; + --text-primary-color: CanvasText; + --button-secondary-bg-color: HighlightText; + --button-secondary-active-bg-color: HighlightText; + --button-secondary-hover-bg-color: HighlightText; + --button-primary-bg-color: ButtonText; + --button-primary-fg-color: HighlightText; + --button-primary-active-bg-color: SelectedItem; + --button-primary-hover-bg-color: SelectedItem; + + --menu-button-border-color: Canvas; + --menu-button-focus-outline-color: CanvasText; + } + + width: 308px; + padding: 8px 16px 16px; + overflow: visible; + position: absolute; + margin: 0; + + border-radius: 4px; + border: 1px solid var(--dialog-border-color); + background: var(--dialog-bg-color); + box-shadow: 0 2px 14px 0 rgb(58 57 68 / 0.2); + + .mainContainer { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + + *:focus-visible { + outline: 2px solid var(--comment-focus-outline-color); + outline-offset: 0; + } + + #commentManagerToolbar { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: flex-start; + gap: 8px; + align-self: stretch; + + cursor: move; + + > button { + color-scheme: light; + width: 24px; + height: 24px; + padding: 0; + border: none; + + cursor: pointer; + + &::before { + content: ""; + display: inline-block; + width: 100%; + height: 100%; + mask-repeat: no-repeat; + mask-position: center; + } + + &#commentActionsButton::before { + mask-image: var(--comment-actions-button-icon); + } + + &#commentCloseButton::before { + mask-image: var(--comment-close-button-icon); + } + + > span { + display: inline-block; + width: 0; + height: 0; + overflow: hidden; + } + } + + menu { + width: max-content; + min-width: 90px; + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; + padding: 5px 6px; + cursor: auto; + z-index: 1; + margin: 0; + + position: absolute; + top: 8px; + right: -6.5px; + + border-radius: 6px; + border: 0.5px solid #b4b4b6; + background-color: var(--menu-bg-color); + box-shadow: + 1px -1px 0 0 #fff inset, + -1px 1px 0 0 #fff inset, + -1px -1px 0 0 #fff inset, + 1px 1px 0 0 #fff inset, + 0 0 15px 0 rgb(0 0 0 / 0.25); + + button { + background-color: var(--menu-bg-color); + width: 100%; + height: 24px; + padding: 0; + box-sizing: border-box; + display: flex; + border: 2px solid var(--menu-button-border-color); + color: var(--menuitem-fg-color); + + &:hover { + background-color: var(--menuitem-hover-bg-color); + color: var(--menuitem-hover-fg-color); + } + + &:is(:focus-visible, :focus) { + outline: none; + border: 2px solid var(--menu-button-focus-outline-color); + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + + span { + align-content: center; + width: 100%; + max-width: min-content; + padding-inline: 8px; + color: inherit; + text-align: start; + font: menu; + font-size: 15px; + font-weight: 400; + line-height: normal; + } + } + } + } + + #commentManagerTextInput { + width: 100%; + min-height: 132px; + resize: none; + box-sizing: border-box; + margin-bottom: 12px; + + border-radius: 4px; + border: 2px solid var(--comment-text-input-border); + background-color: var(--comment-text-input-bg); + color: var(--comment-text-input-fg); + } + + #commentManagerTextView { + width: 100%; + height: max-content; + resize: none; + box-sizing: border-box; + margin-bottom: 12px; + + border: none; + background-color: transparent; + color: var(--comment-text-input-fg); + } + + .dialogButtonsGroup { + gap: 8px; + + #commentManagerSaveButton:disabled { + background-color: color-mix( + in srgb, + var(--button-primary-disabled-bg-color), + transparent 50% + ); + border-color: color-mix( + in srgb, + var(--button-primary-disabled-border-color), + transparent 50% + ); + opacity: 1; + } + } + } +} + +.annotationLayer { + --comment-inline-button-bg: #e0e0e6; + --comment-inline-button-fg: black; + --comment-inline-button-border-color: #8f8f9d; + --comment-button-dim: 24px; + --comment-button-offset: calc(var(--comment-button-dim) / 2); + + .annotationCommentButton { + position: absolute; + width: var(--comment-button-dim); + height: var(--comment-button-dim); + background-color: var(--comment-inline-button-bg); + cursor: auto; + z-index: 1; + border: 1px solid var(--comment-inline-button-border-color); + border-radius: 4px; + padding: 2px; + margin: 0; + box-sizing: border-box; + pointer-events: auto; + + &::before { + content: ""; + display: inline-block; + width: 100%; + height: 100%; + mask-repeat: no-repeat; + mask-size: cover; + mask-image: var(--comment-edit-image); + background-color: var(--comment-inline-button-fg); + margin: 0; + padding: 0; + } + } +} diff --git a/web/comment_manager.js b/web/comment_manager.js new file mode 100644 index 000000000..effe956f9 --- /dev/null +++ b/web/comment_manager.js @@ -0,0 +1,375 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getRGB, noContextMenu, shadow, stopEvent } from "pdfjs-lib"; + +class CommentManager { + #actions; + + #currentEditor; + + #dialog; + + #deleteMenuItem; + + #editMenuItem; + + #overlayManager; + + #previousText = ""; + + #commentText = ""; + + #menu; + + #textInput; + + #textView; + + #saveButton; + + #uiManager; + + #prevDragX = Infinity; + + #prevDragY = Infinity; + + #dialogX = 0; + + #dialogY = 0; + + #menuAC = null; + + constructor( + { + dialog, + toolbar, + actions, + menu, + editMenuItem, + deleteMenuItem, + closeButton, + textInput, + textView, + cancelButton, + saveButton, + }, + overlayManager + ) { + this.#actions = actions; + this.#dialog = dialog; + this.#editMenuItem = editMenuItem; + this.#deleteMenuItem = deleteMenuItem; + this.#menu = menu; + this.#textInput = textInput; + this.#textView = textView; + this.#overlayManager = overlayManager; + this.#saveButton = saveButton; + + const finishBound = this.#finish.bind(this); + dialog.addEventListener("close", finishBound); + dialog.addEventListener("contextmenu", e => { + if (e.target !== this.#textInput) { + e.preventDefault(); + } + }); + cancelButton.addEventListener("click", finishBound); + closeButton.addEventListener("click", finishBound); + saveButton.addEventListener("click", this.#save.bind(this)); + + this.#makeMenu(); + editMenuItem.addEventListener("click", () => { + this.#closeMenu(); + this.#edit(); + }); + deleteMenuItem.addEventListener("click", () => { + this.#closeMenu(); + this.#textInput.value = ""; + this.#currentEditor.comment = null; + this.#save(); + }); + + textInput.addEventListener("input", () => { + saveButton.disabled = textInput.value === this.#previousText; + this.#deleteMenuItem.disabled = textInput.value === ""; + }); + textView.addEventListener("dblclick", () => { + this.#edit(); + }); + + // Make the dialog draggable. + let pointerMoveAC; + const cancelDrag = () => { + this.#prevDragX = this.#prevDragY = Infinity; + this.#dialog.classList.remove("dragging"); + pointerMoveAC?.abort(); + pointerMoveAC = null; + }; + toolbar.addEventListener("pointerdown", e => { + const { target, clientX, clientY } = e; + if (target !== toolbar) { + return; + } + this.#closeMenu(); + this.#prevDragX = clientX; + this.#prevDragY = clientY; + pointerMoveAC = new AbortController(); + const { signal } = pointerMoveAC; + dialog.classList.add("dragging"); + window.addEventListener( + "pointermove", + ev => { + if (this.#prevDragX !== Infinity) { + const { clientX: x, clientY: y } = ev; + this.#setPosition( + this.#dialogX + x - this.#prevDragX, + this.#dialogY + y - this.#prevDragY + ); + this.#prevDragX = x; + this.#prevDragY = y; + stopEvent(ev); + } + }, + { signal } + ); + window.addEventListener("blur", cancelDrag, { signal }); + stopEvent(e); + }); + dialog.addEventListener("pointerup", e => { + if (this.#prevDragX === Infinity) { + return; // Not dragging. + } + cancelDrag(); + stopEvent(e); + }); + + overlayManager.register(dialog); + } + + #closeMenu() { + if (!this.#menuAC) { + return; + } + const menu = this.#menu; + menu.classList.toggle("hidden", true); + this.#actions.ariaExpanded = "false"; + this.#menuAC.abort(); + this.#menuAC = null; + if (menu.contains(document.activeElement)) { + // If the menu is closed while focused, focus the actions button. + setTimeout(() => { + if (!this.#dialog.contains(document.activeElement)) { + this.#actions.focus(); + } + }, 0); + } + } + + #makeMenu() { + this.#actions.addEventListener("click", e => { + const closeMenu = this.#closeMenu.bind(this); + if (this.#menuAC) { + closeMenu(); + return; + } + + const menu = this.#menu; + menu.classList.toggle("hidden", false); + this.#actions.ariaExpanded = "true"; + this.#menuAC = new AbortController(); + const { signal } = this.#menuAC; + window.addEventListener( + "pointerdown", + ({ target }) => { + if (target !== this.#actions && !menu.contains(target)) { + closeMenu(); + } + }, + { signal } + ); + window.addEventListener("blur", closeMenu, { signal }); + this.#actions.addEventListener( + "keydown", + ({ key }) => { + switch (key) { + case "ArrowDown": + case "Home": + menu.firstElementChild.focus(); + stopEvent(e); + break; + case "ArrowUp": + case "End": + menu.lastElementChild.focus(); + stopEvent(e); + break; + case "Escape": + closeMenu(); + stopEvent(e); + } + }, + { signal } + ); + }); + + const keyboardListener = e => { + const { key, target } = e; + const menu = this.#menu; + switch (key) { + case "Escape": + this.#closeMenu(); + stopEvent(e); + break; + case "ArrowDown": + case "Tab": + (target.nextElementSibling || menu.firstElementChild).focus(); + stopEvent(e); + break; + case "ArrowUp": + case "ShiftTab": + (target.previousElementSibling || menu.lastElementChild).focus(); + stopEvent(e); + break; + case "Home": + menu.firstElementChild.focus(); + stopEvent(e); + break; + case "End": + menu.lastElementChild.focus(); + stopEvent(e); + break; + } + }; + for (const menuItem of this.#menu.children) { + if (menuItem.classList.contains("hidden")) { + continue; // Skip hidden menu items. + } + menuItem.addEventListener("keydown", keyboardListener); + menuItem.addEventListener("contextmenu", noContextMenu); + } + this.#menu.addEventListener("contextmenu", noContextMenu); + } + + async open(uiManager, editor, position) { + if (editor) { + this.#uiManager = uiManager; + this.#currentEditor = editor; + } + const { + comment: { text, color }, + } = editor; + this.#dialog.style.setProperty( + "--dialog-base-color", + this.#lightenColor(color) || "var(--default-dialog-bg-color)" + ); + this.#commentText = text || ""; + if (!text) { + this.#edit(); + } else { + this.#setText(text); + this.#textInput.classList.toggle("hidden", true); + this.#textView.classList.toggle("hidden", false); + this.#editMenuItem.disabled = this.#deleteMenuItem.disabled = false; + } + this.#uiManager.removeEditListeners(); + this.#saveButton.disabled = true; + + const x = + position.right !== undefined + ? position.right - this._dialogWidth + : position.left; + const y = position.top; + this.#setPosition(x, y, /* isInitial */ true); + + await this.#overlayManager.open(this.#dialog); + } + + async #save() { + this.#currentEditor.comment = this.#textInput.value; + this.#finish(); + } + + get _dialogWidth() { + const dialog = this.#dialog; + const { style } = dialog; + style.opacity = "0"; + style.display = "block"; + const width = dialog.getBoundingClientRect().width; + style.opacity = style.display = ""; + return shadow(this, "_dialogWidth", width); + } + + #lightenColor(color) { + if (!color) { + return null; // No color provided. + } + const [r, g, b] = getRGB(color); + const gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; + const ratio = gray < 0.9 ? Math.round((0.9 - gray) * 100) : 0; + return `color-mix(in srgb, ${ratio}% white, ${color})`; + } + + #setText(text) { + const textView = this.#textView; + for (const line of text.split("\n")) { + const span = document.createElement("span"); + span.textContent = line; + textView.append(span, document.createElement("br")); + } + } + + #setPosition(x, y, isInitial = false) { + this.#dialogX = x; + this.#dialogY = y; + const { style } = this.#dialog; + style.left = `${x}px`; + style.top = isInitial + ? `calc(${y}px + var(--editor-toolbar-vert-offset))` + : `${y}px`; + } + + #edit() { + const textInput = this.#textInput; + const textView = this.#textView; + if (textView.childElementCount > 0) { + const height = parseFloat(getComputedStyle(textView).height); + textInput.value = this.#previousText = this.#commentText; + textInput.style.height = `${height + 20}px`; + } else { + textInput.value = this.#previousText = this.#commentText; + } + + textInput.classList.toggle("hidden", false); + textView.classList.toggle("hidden", true); + this.#editMenuItem.disabled = this.#deleteMenuItem.disabled = true; + setTimeout(() => textInput.focus(), 0); + } + + #finish() { + this.#textView.replaceChildren(); + this.#textInput.value = this.#previousText = this.#commentText = ""; + this.#overlayManager.closeIfActive(this.#dialog); + this.#textInput.style.height = ""; + this.#uiManager?.addEditListeners(); + this.#uiManager = null; + this.#currentEditor = null; + } + + destroy() { + this.#uiManager = null; + this.#finish(); + } +} + +export { CommentManager }; diff --git a/web/images/comment-actionsButton.svg b/web/images/comment-actionsButton.svg new file mode 100644 index 000000000..3161438ca --- /dev/null +++ b/web/images/comment-actionsButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/images/comment-closeButton.svg b/web/images/comment-closeButton.svg new file mode 100644 index 000000000..6fb5f6a2c --- /dev/null +++ b/web/images/comment-closeButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/images/comment-editButton.svg b/web/images/comment-editButton.svg new file mode 100644 index 000000000..fdd8f6d4e --- /dev/null +++ b/web/images/comment-editButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 2ec876467..896617cc3 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -230,6 +230,8 @@ class PDFViewer { #annotationMode = AnnotationMode.ENABLE_FORMS; + #commentManager = null; + #containerTopLeft = null; #editorUndoBar = null; @@ -314,6 +316,7 @@ class PDFViewer { this.downloadManager = options.downloadManager || null; this.findController = options.findController || null; this.#altTextManager = options.altTextManager || null; + this.#commentManager = options.commentManager || null; this.#signatureManager = options.signatureManager || null; this.#editorUndoBar = options.editorUndoBar || null; @@ -932,6 +935,7 @@ class PDFViewer { viewer, this.#viewerAlert, this.#altTextManager, + this.#commentManager, this.#signatureManager, eventBus, pdfDocument, @@ -2403,6 +2407,8 @@ class PDFViewer { * keyboard action. * @property {boolean} [mustEnterInEditMode] - True if the editor must enter * edit mode. + * @property {boolean} [editComment] - True if the editor must enter + * comment edit mode. */ /** @@ -2413,6 +2419,7 @@ class PDFViewer { editId = null, isFromKeyboard = false, mustEnterInEditMode = false, + editComment = false, }) { if (!this.#annotationEditorUIManager) { throw new Error(`The AnnotationEditor is not enabled.`); @@ -2436,7 +2443,8 @@ class PDFViewer { mode, editId, isFromKeyboard, - mustEnterInEditMode + mustEnterInEditMode, + editComment ); if ( mode !== this.#annotationEditorMode || diff --git a/web/pdfjs.js b/web/pdfjs.js index 7634d2c14..4898002fe 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -32,6 +32,7 @@ const { getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getUuid, getXfaPageViewport, GlobalWorkerOptions, @@ -86,6 +87,7 @@ export { getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, + getRGB, getUuid, getXfaPageViewport, GlobalWorkerOptions, diff --git a/web/viewer.html b/web/viewer.html index 70093d994..27764229d 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -771,6 +771,37 @@ See https://github.com/adobe-type-tools/cmap-resources + +
+
+ + + +
+ + +
+ + +
+
+
+
diff --git a/web/viewer.js b/web/viewer.js index 45bfd0ccd..0e8347934 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -260,6 +260,19 @@ function getViewerConfiguration() { undoButton: document.getElementById("editorUndoBarUndoButton"), closeButton: document.getElementById("editorUndoBarCloseButton"), }, + editCommentDialog: { + dialog: document.getElementById("commentManagerDialog"), + toolbar: document.getElementById("commentManagerToolbar"), + actions: document.getElementById("commentActionsButton"), + menu: document.getElementById("commentActionsMenu"), + editMenuItem: document.getElementById("commentActionsEditButton"), + deleteMenuItem: document.getElementById("commentActionsDeleteButton"), + closeButton: document.getElementById("commentCloseButton"), + textInput: document.getElementById("commentManagerTextInput"), + textView: document.getElementById("commentManagerTextView"), + cancelButton: document.getElementById("commentManagerCancelButton"), + saveButton: document.getElementById("commentManagerSaveButton"), + }, }; }