diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index a77fe3f09..45f490c18 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -173,6 +173,7 @@ class AnnotationElement { this.renderForms = parameters.renderForms; this.svgFactory = parameters.svgFactory; this.annotationStorage = parameters.annotationStorage; + this.enableComment = parameters.enableComment; this.enableScripting = parameters.enableScripting; this.hasJSActions = parameters.hasJSActions; this._fieldObjects = parameters.fieldObjects; @@ -198,6 +199,92 @@ class AnnotationElement { return AnnotationElement._hasPopupData(this.data); } + get hasCommentButton() { + return this.enableComment && this._isEditable && this.hasPopupElement; + } + + get commentButtonPosition() { + const { quadPoints, rect } = this.data; + let maxX = -Infinity; + let maxY = -Infinity; + if (quadPoints?.length >= 8) { + for (let i = 0; i < quadPoints.length; i += 8) { + if (quadPoints[i + 1] > maxY) { + maxY = quadPoints[i + 1]; + maxX = quadPoints[i + 2]; + } else if (quadPoints[i + 1] === maxY) { + maxX = Math.max(maxX, quadPoints[i + 2]); + } + } + return [maxX, maxY]; + } + if (rect) { + return [rect[2], rect[3]]; + } + return null; + } + + get commentButtonColor() { + if (!this.data.color) { + return null; + } + const [r, g, b] = this.data.color; + const opacity = this.data.opacity ?? 1; + const oppositeOpacity = 255 * (1 - opacity); + + return this.#changeLightness( + Math.min(r + oppositeOpacity, 255), + Math.min(g + oppositeOpacity, 255), + Math.min(b + oppositeOpacity, 255) + ); + } + + #changeLightness(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const l = (max + min) / 2; + const newL = (((1 + Math.sqrt(l)) / 2) * 100).toFixed(2); + + if (max === min) { + // gray + return `hsl(0, 0%, ${newL}%)`; + } + + const d = max - min; + + // hue (branch on max only; avoids mod) + let h; + if (max === r) { + h = (g - b) / d + (g < b ? 6 : 0); + } else if (max === g) { + h = (b - r) / d + 2; + } else { + // max === b + h = (r - g) / d + 4; + } + h = (h * 60).toFixed(2); + const s = ((d / (1 - Math.abs(2 * l - 1))) * 100).toFixed(2); + + return `hsl(${h}, ${s}%, ${newL}%)`; + } + + _normalizePoint(point) { + const { + page: { view }, + viewport: { + rawDims: { pageWidth, pageHeight, pageX, pageY }, + }, + } = this.parent; + point[1] = view[3] - point[1] + view[1]; + point[0] = (100 * (point[0] - pageX)) / pageWidth; + point[1] = (100 * (point[1] - pageY)) / pageHeight; + return point; + } + updateEdited(params) { if (!this.container) { return; @@ -290,7 +377,9 @@ class AnnotationElement { // But if an annotation is above an other one, then we must draw it // after the other one whatever the order is in the DOM, hence the // use of the z-index. - style.zIndex = this.parent.zIndex++; + style.zIndex = this.parent.zIndex; + // Keep zIndex + 1 for stuff we want to add on top of this annotation. + this.parent.zIndex += 2; if (data.alternativeText) { container.title = data.alternativeText; @@ -2194,6 +2283,7 @@ class PopupAnnotationElement extends AnnotationElement { parent: this.parent, elements: this.elements, open: this.data.open, + eventBus: this.linkService.eventBus, })); const elementIds = []; @@ -2232,6 +2322,8 @@ class PopupElement { #elements = null; + #eventBus = null; + #parent = null; #parentRect = null; @@ -2244,6 +2336,12 @@ class PopupElement { #position = null; + #commentButton = null; + + #commentButtonPosition = null; + + #commentButtonColor = null; + #rect = null; #richText = null; @@ -2266,6 +2364,7 @@ class PopupElement { rect, parentRect, open, + eventBus = null, }) { this.#container = container; this.#titleObj = titleObj; @@ -2276,6 +2375,7 @@ class PopupElement { this.#rect = rect; this.#parentRect = parentRect; this.#elements = elements; + this.#eventBus = eventBus; // The modification date is shown in the popup instead of the creation // date if it is available and can be parsed correctly, which is @@ -2322,6 +2422,68 @@ class PopupElement { signal, }); } + + this.#renderCommentButton(); + } + + #setCommentButtonPosition() { + const element = this.#elements.find(e => e.hasCommentButton); + if (!element) { + return; + } + this.#commentButtonPosition = element._normalizePoint( + element.commentButtonPosition + ); + this.#commentButtonColor = element.commentButtonColor; + } + + #renderCommentButton() { + if (this.#commentButton) { + return; + } + + if (!this.#commentButtonPosition) { + this.#setCommentButtonPosition(); + } + + if (!this.#commentButtonPosition) { + return; + } + + const button = (this.#commentButton = document.createElement("button")); + button.className = "annotationCommentButton"; + const parentContainer = this.#elements[0].container; + button.style.zIndex = parentContainer.style.zIndex + 1; + button.tabIndex = 0; + + const { signal } = this.#popupAbortController; + button.addEventListener("hover", this.#boundToggle, { signal }); + button.addEventListener("keydown", this.#boundKeyDown, { signal }); + button.addEventListener( + "click", + () => { + const [ + { + data: { id: editId }, + annotationEditorType: mode, + }, + ] = this.#elements; + this.#eventBus?.dispatch("switchannotationeditormode", { + source: this, + editId, + mode, + editComment: true, + }); + }, + { signal } + ); + const { style } = button; + style.left = `calc(${this.#commentButtonPosition[0]}% + var(--comment-button-offset))`; + style.top = `calc(${this.#commentButtonPosition[1]}% - var(--comment-button-dim) - var(--comment-button-offset))`; + if (this.#commentButtonColor) { + style.backgroundColor = this.#commentButtonColor; + } + parentContainer.after(button); } render() { @@ -3053,6 +3215,31 @@ class InkAnnotationElement extends AnnotationElement { addHighlightArea() { this.container.classList.add("highlightArea"); } + + get commentButtonPosition() { + const { inkLists, rect } = this.data; + if (inkLists?.length >= 1) { + let maxX = -Infinity; + let maxY = -Infinity; + for (const inkList of inkLists) { + for (let i = 0, ii = inkList.length; i < ii; i += 2) { + if (inkList[i + 1] > maxY) { + maxY = inkList[i + 1]; + maxX = inkList[i]; + } else if (inkList[i + 1] === maxY) { + maxX = Math.max(maxX, inkList[i]); + } + } + } + if (maxX !== Infinity) { + return [maxX, maxY]; + } + } + if (rect) { + return [rect[2], rect[3]]; + } + return null; + } } class HighlightAnnotationElement extends AnnotationElement { @@ -3391,6 +3578,7 @@ class AnnotationLayer { renderForms: params.renderForms !== false, svgFactory: new DOMSVGFactory(), annotationStorage: params.annotationStorage || new AnnotationStorage(), + enableComment: params.enableComment === true, enableScripting: params.enableScripting === true, hasJSActions: params.hasJSActions, fieldObjects: params.fieldObjects, diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index c1c2a8d54..95c33f4a2 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-inline-editButton.svg); --input-focus-border-color: Highlight; --input-focus-outline: 1px solid Canvas; --input-unfocused-border-color: transparent; diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index ba6ad0f9b..b7aa4c720 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -44,6 +44,7 @@ import { PresentationModeState } from "./ui_utils.js"; * @property {boolean} renderForms * @property {IPDFLinkService} linkService * @property {IDownloadManager} [downloadManager] + * @property {boolean} [enableComment] * @property {boolean} [enableScripting] * @property {Promise} [hasJSActionsPromise] * @property {Promise> | null>} @@ -89,6 +90,7 @@ class AnnotationLayerBuilder { annotationStorage = null, imageResourcesPath = "", renderForms = true, + enableComment = false, enableScripting = false, hasJSActionsPromise = null, fieldObjectsPromise = null, @@ -103,6 +105,7 @@ class AnnotationLayerBuilder { this.imageResourcesPath = imageResourcesPath; this.renderForms = renderForms; this.annotationStorage = annotationStorage; + this.enableComment = enableComment; this.enableScripting = enableScripting; this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false); this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null); @@ -166,6 +169,7 @@ class AnnotationLayerBuilder { linkService: this.linkService, downloadManager: this.downloadManager, annotationStorage: this.annotationStorage, + enableComment: this.enableComment, enableScripting: this.enableScripting, hasJSActions, fieldObjects, diff --git a/web/comment_manager.css b/web/comment_manager.css index 512f38f6b..698ae494c 100644 --- a/web/comment_manager.css +++ b/web/comment_manager.css @@ -250,3 +250,101 @@ } } } + +.annotationLayer.disabled .annotationCommentButton { + display: none; +} + +:is(.annotationLayer, .annotationEditorLayer) { + .annotationCommentButton { + --comment-button-bg: light-dark(white, #1c1b22); + --comment-button-fg: light-dark(#5b5b66, #fbfbfe); + --comment-button-active-bg: light-dark(#0041a4, #a6ecf4); + --comment-button-active-fg: light-dark(white, #15141a); + --comment-button-hover-bg: light-dark(#0053cb, #61dce9); + --comment-button-hover-fg: light-dark(white, #15141a); + --comment-button-border-color: light-dark(#8f8f9d, #bfbfc9); + --comment-button-focus-border-color: light-dark(#cfcfd8, #3a3944); + --comment-button-hover-border-color: var(--comment-button-hover-bg); + --comment-button-selected-bg: light-dark(#0062fa, #00cadb); + --comment-button-selected-fg: light-dark(white, #15141a); + --comment-button-dim: 24px; + --comment-button-offset: 1px; + --comment-button-box-shadow: + 0 0.25px 0.75px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)), + 0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4)); + --comment-button-focus-outline-color: light-dark(#0062fa, #00cadb); + + @media screen and (forced-colors: active) { + --comment-button-bg: Canvas; + --comment-button-fg: CanvasText; + --comment-button-hover-bg: Highlight; + --comment-button-hover-fg: ButtonFace; + --comment-button-active-bg: Highlight; + --comment-button-active-fg: ButtonFace; + --comment-button-border-color: ButtonBorder; + --comment-button-box-shadow: none; + --comment-button-focus-outline-color: CanvasText; + --comment-button-selected-bg: ButtonBorder; + --comment-button-selected-fg: ButtonFace; + } + + position: absolute; + width: var(--comment-button-dim); + height: var(--comment-button-dim); + background-color: var(--comment-button-bg); + border-radius: 6px 6px 6px 0; + border: 1px solid var(--comment-button-border-color); + box-shadow: var(--comment-button-box-shadow); + cursor: auto; + z-index: 1; + padding: 4px; + 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-button-fg); + margin: 0; + padding: 0; + } + + &:focus-visible { + -moz-outline-radius: 7px 7px 7px 0; + outline: 2px solid var(--comment-button-focus-outline-color); + outline-offset: 1px; + border-color: var(--comment-button-focus-border-color); + } + + &:hover { + background-color: var(--comment-button-hover-bg) !important; + + &::before { + background-color: var(--comment-button-hover-fg); + } + } + + &:active { + background-color: var(--comment-button-active-bg) !important; + + &::before { + background-color: var(--comment-button-active-fg); + } + } + + &.selected { + background-color: var(--comment-button-selected-bg) !important; + + &::before { + background-color: var(--comment-button-selected-fg); + } + } + } +} diff --git a/web/images/comment-inline-editButton.svg b/web/images/comment-inline-editButton.svg new file mode 100644 index 000000000..f33e29151 --- /dev/null +++ b/web/images/comment-inline-editButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index e98d6571c..8a983c0ef 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -984,6 +984,7 @@ class PDFPageView extends BasePDFPageView { annotationStorage, annotationEditorUIManager, downloadManager, + enableComment, enableScripting, fieldObjectsPromise, hasJSActionsPromise, @@ -998,6 +999,7 @@ class PDFPageView extends BasePDFPageView { renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS, linkService, downloadManager, + enableComment, enableScripting, hasJSActionsPromise, fieldObjectsPromise, diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 37644840f..9729f287c 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -655,6 +655,9 @@ class PDFViewer { get downloadManager() { return self.downloadManager; }, + get enableComment() { + return !!self.#commentManager; + }, get enableScripting() { return !!self._scriptingManager; },