Skip to content

Commit 23bd705

Browse files
Merge pull request #20080 from calixteman/add_comment_1
[Editor] Add the possibility to add Popup annotations (bug 1976724)
2 parents d2f8e60 + 636ff50 commit 23bd705

30 files changed

Lines changed: 1161 additions & 28 deletions

extensions/chromium/preferences_schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@
233233
"description": "Enable creation of hyperlinks from text that look like URLs.",
234234
"type": "boolean",
235235
"default": true
236+
},
237+
"enableComment": {
238+
"description": "Enable creation of comment annotations.",
239+
"type": "boolean",
240+
"default": false
236241
}
237242
}
238243
}

l10n/en-US/viewer.ftl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,24 @@ pdfjs-editor-edit-signature-dialog-title = Edit description
644644
## Dialog buttons
645645

646646
pdfjs-editor-edit-signature-update-button = Update
647+
648+
## Edit a comment dialog
649+
650+
pdfjs-editor-edit-comment-actions-button-label = Actions
651+
pdfjs-editor-edit-comment-actions-button =
652+
.title = Actions
653+
pdfjs-editor-edit-comment-close-button-label = Close
654+
pdfjs-editor-edit-comment-close-button =
655+
.title = Close
656+
pdfjs-editor-edit-comment-actions-edit-button-label = Edit
657+
pdfjs-editor-edit-comment-actions-delete-button-label = Delete
658+
pdfjs-editor-edit-comment-manager-text-input =
659+
.placeholder = Enter your comment
660+
661+
pdfjs-editor-edit-comment-manager-cancel-button = Cancel
662+
pdfjs-editor-edit-comment-manager-save-button = Save
663+
664+
## Edit a comment button in the editor toolbar
665+
666+
pdfjs-editor-edit-comment-button =
667+
.title = Edit comment

src/display/annotation_layer.js

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,32 @@ class AnnotationElement {
202202
return;
203203
}
204204

205-
this.#updates ||= {
206-
rect: this.data.rect.slice(0),
207-
};
205+
if (params.rect) {
206+
this.#updates ||= {
207+
rect: this.data.rect.slice(0),
208+
};
209+
}
208210

209-
const { rect } = params;
211+
const { rect, popup: newPopup } = params;
210212

211213
if (rect) {
212214
this.#setRectEdited(rect);
213215
}
214216

215-
this.#popupElement?.popup.updateEdited(params);
217+
let popup = this.#popupElement?.popup || this.popup;
218+
if (!popup && newPopup.text) {
219+
this._createPopup(newPopup);
220+
popup = this.#popupElement.popup;
221+
}
222+
if (!popup) {
223+
return;
224+
}
225+
popup.updateEdited(params);
226+
if (newPopup.deleted) {
227+
popup.remove();
228+
this.#popupElement = null;
229+
this.popup = null;
230+
}
216231
}
217232

218233
resetEdited() {
@@ -598,31 +613,48 @@ class AnnotationElement {
598613
* annotations that do not have a Popup entry in the dictionary, but
599614
* are of a type that works with popups (such as Highlight annotations).
600615
*
616+
* @param {Object} [popupData] - The data for the popup, if any.
617+
*
601618
* @private
602619
* @memberof AnnotationElement
603620
*/
604-
_createPopup() {
621+
_createPopup(popupData = null) {
605622
const { data } = this;
606623

624+
let contentsObj, modificationDate;
625+
if (popupData) {
626+
contentsObj = {
627+
str: popupData.text,
628+
};
629+
modificationDate = popupData.date;
630+
} else {
631+
contentsObj = data.contentsObj;
632+
modificationDate = data.modificationDate;
633+
}
607634
const popup = (this.#popupElement = new PopupAnnotationElement({
608635
data: {
609636
color: data.color,
610637
titleObj: data.titleObj,
611-
modificationDate: data.modificationDate,
612-
contentsObj: data.contentsObj,
638+
modificationDate,
639+
contentsObj,
613640
richText: data.richText,
614641
parentRect: data.rect,
615642
borderStyle: 0,
616643
id: `popup_${data.id}`,
617644
rotation: data.rotation,
618645
noRotate: true,
619646
},
647+
linkService: this.linkService,
620648
parent: this.parent,
621649
elements: [this],
622650
}));
623651
this.parent.div.append(popup.render());
624652
}
625653

654+
get hasPopupElement() {
655+
return !!(this.#popupElement || this.popup || this.data.popupRef);
656+
}
657+
626658
/**
627659
* Render the annotation's HTML element(s).
628660
*
@@ -2352,8 +2384,8 @@ class PopupElement {
23522384
}
23532385
}
23542386

2355-
updateEdited({ rect, popupContent, deleted }) {
2356-
if (deleted) {
2387+
updateEdited({ rect, popup, deleted }) {
2388+
if (deleted || popup?.deleted) {
23572389
this.remove();
23582390
return;
23592391
}
@@ -2365,8 +2397,9 @@ class PopupElement {
23652397
if (rect) {
23662398
this.#position = null;
23672399
}
2368-
if (popupContent) {
2369-
this.#richText = this.#makePopupContent(popupContent);
2400+
if (popup) {
2401+
this.#richText = this.#makePopupContent(popup.text);
2402+
this.#dateObj = PDFDateString.toDateObject(popup.date);
23702403
this.#contentsObj = null;
23712404
}
23722405
this.#popup?.remove();

src/display/display_utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,9 @@ class PDFDateString {
502502
* @returns {Date|null}
503503
*/
504504
static toDateObject(input) {
505+
if (input instanceof Date) {
506+
return input;
507+
}
505508
if (!input || typeof input !== "string") {
506509
return null;
507510
}

src/display/editor/comment.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/* Copyright 2025 Mozilla Foundation
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import { noContextMenu } from "../display_utils.js";
17+
18+
class Comment {
19+
#commentButton = null;
20+
21+
#commentWasFromKeyBoard = false;
22+
23+
#editor = null;
24+
25+
#initialText = null;
26+
27+
#text = null;
28+
29+
#date = null;
30+
31+
#deleted = false;
32+
33+
constructor(editor) {
34+
this.#editor = editor;
35+
this.toolbar = null;
36+
}
37+
38+
render() {
39+
if (!this.#editor._uiManager.hasCommentManager()) {
40+
return null;
41+
}
42+
const comment = (this.#commentButton = document.createElement("button"));
43+
comment.className = "comment";
44+
comment.tabIndex = "0";
45+
comment.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-button");
46+
47+
const signal = this.#editor._uiManager._signal;
48+
comment.addEventListener("contextmenu", noContextMenu, { signal });
49+
comment.addEventListener("pointerdown", event => event.stopPropagation(), {
50+
signal,
51+
});
52+
53+
const onClick = event => {
54+
event.preventDefault();
55+
this.edit();
56+
};
57+
comment.addEventListener("click", onClick, { capture: true, signal });
58+
comment.addEventListener(
59+
"keydown",
60+
event => {
61+
if (event.target === comment && event.key === "Enter") {
62+
this.#commentWasFromKeyBoard = true;
63+
onClick(event);
64+
}
65+
},
66+
{ signal }
67+
);
68+
69+
return comment;
70+
}
71+
72+
edit() {
73+
const { bottom, left, right } = this.#editor.getClientDimensions();
74+
const position = { top: bottom };
75+
if (this.#editor._uiManager.direction === "ltr") {
76+
position.right = right;
77+
} else {
78+
position.left = left;
79+
}
80+
this.#editor._uiManager.editComment(this.#editor, position);
81+
}
82+
83+
finish() {
84+
if (!this.#commentButton) {
85+
return;
86+
}
87+
this.#commentButton.focus({ focusVisible: this.#commentWasFromKeyBoard });
88+
this.#commentWasFromKeyBoard = false;
89+
}
90+
91+
isDeleted() {
92+
return this.#deleted || this.#text === "";
93+
}
94+
95+
hasBeenEdited() {
96+
return this.isDeleted() || this.#text !== this.#initialText;
97+
}
98+
99+
serialize() {
100+
return this.data;
101+
}
102+
103+
get data() {
104+
return {
105+
text: this.#text,
106+
date: this.#date,
107+
deleted: this.#deleted,
108+
};
109+
}
110+
111+
/**
112+
* Set the comment data.
113+
*/
114+
set data(text) {
115+
if (text === null) {
116+
this.#text = "";
117+
this.#deleted = true;
118+
return;
119+
}
120+
this.#text = text;
121+
this.#date = new Date();
122+
this.#deleted = false;
123+
}
124+
125+
setInitialText(text) {
126+
this.#initialText = text;
127+
this.data = text;
128+
}
129+
130+
toggle(enabled = false) {
131+
if (!this.#commentButton) {
132+
return;
133+
}
134+
this.#commentButton.disabled = !enabled;
135+
}
136+
137+
shown() {}
138+
139+
destroy() {
140+
this.#commentButton?.remove();
141+
this.#commentButton = null;
142+
this.#text = "";
143+
this.#date = null;
144+
this.#editor = null;
145+
this.#commentWasFromKeyBoard = false;
146+
this.#deleted = false;
147+
}
148+
}
149+
150+
export { Comment };

src/display/editor/editor.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from "../../shared/util.js";
3131
import { noContextMenu, stopEvent } from "../display_utils.js";
3232
import { AltText } from "./alt_text.js";
33+
import { Comment } from "./comment.js";
3334
import { EditorToolbar } from "./toolbar.js";
3435
import { TouchManager } from "../touch_manager.js";
3536

@@ -52,6 +53,8 @@ class AnnotationEditor {
5253

5354
#altText = null;
5455

56+
#comment = null;
57+
5558
#disabled = false;
5659

5760
#dragPointerId = null;
@@ -1075,6 +1078,7 @@ class AnnotationEditor {
10751078
}
10761079
this._editToolbar = new EditorToolbar(this);
10771080
this.div.append(this._editToolbar.render());
1081+
this._editToolbar.addButton("comment", this.addCommentButton());
10781082
const { toolbarButtons } = this;
10791083
if (toolbarButtons) {
10801084
for (const [name, tool] of toolbarButtons) {
@@ -1161,6 +1165,61 @@ class AnnotationEditor {
11611165
return this.#altText?.hasData() ?? false;
11621166
}
11631167

1168+
addCommentButton() {
1169+
if (this.#comment) {
1170+
return this.#comment;
1171+
}
1172+
return (this.#comment = new Comment(this));
1173+
}
1174+
1175+
get commentColor() {
1176+
return null;
1177+
}
1178+
1179+
get comment() {
1180+
const comment = this.#comment;
1181+
return {
1182+
text: comment.data.text,
1183+
date: comment.data.date,
1184+
deleted: comment.isDeleted(),
1185+
color: this.commentColor,
1186+
};
1187+
}
1188+
1189+
set comment(text) {
1190+
if (!this.#comment) {
1191+
this.#comment = new Comment(this);
1192+
}
1193+
this.#comment.data = text;
1194+
}
1195+
1196+
setCommentData(text) {
1197+
if (!this.#comment) {
1198+
this.#comment = new Comment(this);
1199+
}
1200+
this.#comment.setInitialText(text);
1201+
}
1202+
1203+
get hasEditedComment() {
1204+
return this.#comment?.hasBeenEdited();
1205+
}
1206+
1207+
async editComment() {
1208+
if (!this.#comment) {
1209+
this.#comment = new Comment(this);
1210+
}
1211+
this.#comment.edit();
1212+
}
1213+
1214+
addComment(serialized) {
1215+
if (this.hasEditedComment) {
1216+
serialized.popup = {
1217+
contents: this.comment.text,
1218+
deleted: this.comment.deleted,
1219+
};
1220+
}
1221+
}
1222+
11641223
/**
11651224
* Render this editor in a div.
11661225
* @returns {HTMLDivElement | null}

0 commit comments

Comments
 (0)