[Editor] Add a button to the annotation having a popup in order to edit it

This commit is contained in:
Calixte Denizet 2025-08-20 19:04:03 +02:00
parent e20ee99580
commit bd8c438428
7 changed files with 300 additions and 1 deletions

View File

@ -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,

View File

@ -17,6 +17,7 @@
color-scheme: only light;
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
--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;

View File

@ -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<boolean>} [hasJSActionsPromise]
* @property {Promise<Object<string, Array<Object>> | 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,

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.25 2.85098L2.85 2.25098H13.15L13.75 2.85098V10.151L13.15 10.751H8.913C8.68584 10.7509 8.46541 10.8281 8.288 10.97L5.25 13.4V11.251C5.25 11.1184 5.19732 10.9912 5.10355 10.8974C5.00979 10.8037 4.88261 10.751 4.75 10.751H2.85L2.25 10.151V2.85098ZM13 1.00098H3C2.46957 1.00098 1.96086 1.21169 1.58579 1.58676C1.21071 1.96184 1 2.47054 1 3.00098V10.001C1 10.5314 1.21071 11.0401 1.58579 11.4152C1.96086 11.7903 2.46957 12.001 3 12.001H4V15.001H5.25L9 12.001H13C13.5304 12.001 14.0391 11.7903 14.4142 11.4152C14.7893 11.0401 15 10.5314 15 10.001V3.00098C15 2.47054 14.7893 1.96184 14.4142 1.58676C14.0391 1.21169 13.5304 1.00098 13 1.00098ZM4.875 5.25098H11.125C11.2908 5.25098 11.4497 5.18513 11.5669 5.06792C11.6842 4.95071 11.75 4.79174 11.75 4.62598C11.75 4.46022 11.6842 4.30124 11.5669 4.18403C11.4497 4.06682 11.2908 4.00098 11.125 4.00098H4.875C4.70924 4.00098 4.55027 4.06682 4.43306 4.18403C4.31585 4.30124 4.25 4.46022 4.25 4.62598C4.25 4.79174 4.31585 4.95071 4.43306 5.06792C4.55027 5.18513 4.70924 5.25098 4.875 5.25098ZM6.875 8.25098H11.125C11.2908 8.25098 11.4497 8.18513 11.5669 8.06792C11.6842 7.95071 11.75 7.79174 11.75 7.62598C11.75 7.46022 11.6842 7.30125 11.5669 7.18403C11.4497 7.06682 11.2908 7.00098 11.125 7.00098H6.875C6.70924 7.00098 6.55027 7.06682 6.43306 7.18403C6.31585 7.30125 6.25 7.46022 6.25 7.62598C6.25 7.79174 6.31585 7.95071 6.43306 8.06792C6.55027 8.18513 6.70924 8.25098 6.875 8.25098Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -969,6 +969,7 @@ class PDFPageView extends BasePDFPageView {
annotationStorage,
annotationEditorUIManager,
downloadManager,
enableComment,
enableScripting,
fieldObjectsPromise,
hasJSActionsPromise,
@ -983,6 +984,7 @@ class PDFPageView extends BasePDFPageView {
renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS,
linkService,
downloadManager,
enableComment,
enableScripting,
hasJSActionsPromise,
fieldObjectsPromise,

View File

@ -649,6 +649,9 @@ class PDFViewer {
get downloadManager() {
return self.downloadManager;
},
get enableComment() {
return !!self.#commentManager;
},
get enableScripting() {
return !!self._scriptingManager;
},