[Editor] Add a sidebar allowing the user to navigate between the comments in a pdf (bug 1985567)
This implements what has been specified but it's still not done: we have to handle editing a comment or an annotation containing one.
This commit is contained in:
parent
13ba311e5a
commit
25990e4287
@ -328,6 +328,10 @@ pdfjs-comment-floating-button =
|
||||
.title = Comment
|
||||
.aria-label = Comment
|
||||
pdfjs-comment-floating-button-label = Comment
|
||||
pdfjs-editor-comment-button =
|
||||
.title = Comment
|
||||
.aria-label = Comment
|
||||
pdfjs-editor-comment-button-label = Comment
|
||||
pdfjs-editor-signature-button =
|
||||
.title = Add signature
|
||||
pdfjs-editor-signature-button-label = Add signature
|
||||
@ -395,6 +399,23 @@ pdfjs-free-text2 =
|
||||
.aria-label = Text Editor
|
||||
.default-content = Start typing…
|
||||
|
||||
# Used to show how many comments are present in the pdf file.
|
||||
# Variables:
|
||||
# $count (Number) - the number of comments.
|
||||
pdfjs-editor-comments-sidebar-title =
|
||||
{ $count ->
|
||||
[one] Comment
|
||||
*[other] Comments
|
||||
}
|
||||
|
||||
pdfjs-editor-comments-sidebar-close-button =
|
||||
.title = Close the sidebar
|
||||
.aria-label = Close the sidebar
|
||||
pdfjs-editor-comments-sidebar-close-button-label = Close the sidebar
|
||||
|
||||
# Instructional copy to add a comment by selecting text or an annotations.
|
||||
pdfjs-editor-comments-sidebar-no-comments = Add a comment by selecting text or an annotation.
|
||||
|
||||
## Alt-text dialog
|
||||
|
||||
# Alternative text (alt text) helps when people can't see the image.
|
||||
|
||||
@ -585,6 +585,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#activeEditor = null;
|
||||
|
||||
#allEditableAnnotations = null;
|
||||
|
||||
#allEditors = new Map();
|
||||
|
||||
#allLayers = new Map();
|
||||
@ -663,6 +665,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#showAllStates = null;
|
||||
|
||||
#pdfDocument = null;
|
||||
|
||||
#previousStates = {
|
||||
isEditing: false,
|
||||
isEmpty: true,
|
||||
@ -846,6 +850,7 @@ class AnnotationEditorUIManager {
|
||||
this.#altTextManager = altTextManager;
|
||||
this.#commentManager = commentManager;
|
||||
this.#signatureManager = signatureManager;
|
||||
this.#pdfDocument = pdfDocument;
|
||||
this._eventBus = eventBus;
|
||||
eventBus._on("editingaction", this.onEditingAction.bind(this), { signal });
|
||||
eventBus._on("pagechanging", this.onPageChanging.bind(this), { signal });
|
||||
@ -928,6 +933,7 @@ class AnnotationEditorUIManager {
|
||||
this.#floatingToolbar = null;
|
||||
this.#mainHighlightColorPicker?.destroy();
|
||||
this.#mainHighlightColorPicker = null;
|
||||
this.#allEditableAnnotations = null;
|
||||
if (this.#focusMainContainerTimeoutId) {
|
||||
clearTimeout(this.#focusMainContainerTimeoutId);
|
||||
this.#focusMainContainerTimeoutId = null;
|
||||
@ -937,6 +943,7 @@ class AnnotationEditorUIManager {
|
||||
this.#translationTimeoutId = null;
|
||||
}
|
||||
this._editorUndoBar?.destroy();
|
||||
this.#pdfDocument = null;
|
||||
}
|
||||
|
||||
combinedSignal(ac) {
|
||||
@ -1790,6 +1797,10 @@ class AnnotationEditorUIManager {
|
||||
this.#updateModeCapability = Promise.withResolvers();
|
||||
this.#currentDrawingSession?.commitOrRemove();
|
||||
|
||||
if (this.#mode === AnnotationEditorType.POPUP) {
|
||||
this.#commentManager?.hideSidebar();
|
||||
}
|
||||
|
||||
this.#mode = mode;
|
||||
if (mode === AnnotationEditorType.NONE) {
|
||||
this.setEditingState(false);
|
||||
@ -1800,9 +1811,18 @@ class AnnotationEditorUIManager {
|
||||
this.#updateModeCapability.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
this.unselectAll();
|
||||
|
||||
@ -75,6 +75,7 @@ const AnnotationEditorType = {
|
||||
HIGHLIGHT: 9,
|
||||
STAMP: 13,
|
||||
INK: 15,
|
||||
POPUP: 16,
|
||||
SIGNATURE: 101,
|
||||
COMMENT: 102,
|
||||
};
|
||||
|
||||
29
web/app.js
29
web/app.js
@ -493,7 +493,30 @@ const PDFViewerApplication = {
|
||||
: null;
|
||||
const commentManager =
|
||||
AppOptions.get("enableComment") && appConfig.editCommentDialog
|
||||
? new CommentManager(appConfig.editCommentDialog, overlayManager)
|
||||
? new CommentManager(
|
||||
appConfig.editCommentDialog,
|
||||
{
|
||||
sidebar:
|
||||
appConfig.annotationEditorParams?.editorCommentsSidebar || null,
|
||||
commentsList:
|
||||
appConfig.annotationEditorParams?.editorCommentsSidebarList ||
|
||||
null,
|
||||
commentCount:
|
||||
appConfig.annotationEditorParams?.editorCommentsSidebarCount ||
|
||||
null,
|
||||
sidebarTitle:
|
||||
appConfig.annotationEditorParams?.editorCommentsSidebarTitle ||
|
||||
null,
|
||||
closeButton:
|
||||
appConfig.annotationEditorParams
|
||||
?.editorCommentsSidebarCloseButton || null,
|
||||
commentToolbarButton:
|
||||
appConfig.toolbar?.editorCommentButton || null,
|
||||
},
|
||||
eventBus,
|
||||
linkService,
|
||||
overlayManager
|
||||
)
|
||||
: null;
|
||||
|
||||
const enableHWA = AppOptions.get("enableHWA"),
|
||||
@ -589,6 +612,10 @@ const PDFViewerApplication = {
|
||||
if (editorSignatureButton && AppOptions.get("enableSignatureEditor")) {
|
||||
editorSignatureButton.parentElement.hidden = false;
|
||||
}
|
||||
const editorCommentButton = appConfig.toolbar?.editorCommentButton;
|
||||
if (editorCommentButton && AppOptions.get("enableComment")) {
|
||||
editorCommentButton.parentElement.hidden = false;
|
||||
}
|
||||
this.annotationEditorParams = new AnnotationEditorParams(
|
||||
appConfig.annotationEditorParams,
|
||||
eventBus
|
||||
|
||||
@ -304,6 +304,10 @@
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto;
|
||||
|
||||
&:dir(rtl) {
|
||||
border-radius: 6px 6px 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
@ -315,6 +319,7 @@
|
||||
background-color: var(--comment-button-fg);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transform: scaleX(var(--dir-factor));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@ -351,3 +356,241 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebar {
|
||||
--comment-close-button-icon: url(images/comment-closeButton.svg);
|
||||
|
||||
--comment-date-fg-color: light-dark(
|
||||
rgb(21 20 26 / 0.69),
|
||||
rgb(251 251 254 / 0.69)
|
||||
);
|
||||
--comment-bg-color: light-dark(#f9f9fb, #1c1b22);
|
||||
--comment-hover-bg-color: light-dark(
|
||||
rgb(21 20 26 / 0.14),
|
||||
rgb(251 251 254 / 0.14)
|
||||
);
|
||||
--comment-active-bg-color: light-dark(
|
||||
rgb(21 20 26 / 0.21),
|
||||
rgb(251 251 254 / 0.21)
|
||||
);
|
||||
--comment-border-color: light-dark(#f0f0f4, #52525e);
|
||||
--comment-focus-outline-color: light-dark(#0062fa, #00cadb);
|
||||
--comment-fg-color: light-dark(#15141a, #fbfbfe);
|
||||
--comment-count-bg-color: light-dark(#e2f7ff, #00317e);
|
||||
--comment-indicator-active-fg-color: light-dark(#0041a4, #a6ecf4);
|
||||
--comment-indicator-focus-fg-color: light-dark(#5b5b66, #fbfbfe);
|
||||
--comment-indicator-hover-fg-color: light-dark(#0053cb, #61dce9);
|
||||
--comment-indicator-selected-fg-color: light-dark(#0062fa, #00cadb);
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--comment-date-fg-color: CanvasText;
|
||||
--comment-bg-color: Canvas;
|
||||
--comment-hover-bg-color: SelectedItemText;
|
||||
--comment-active-bg-color: SelectedItemText;
|
||||
--comment-border-color: CanvasText;
|
||||
--comment-fg-color: CanvasText;
|
||||
--comment-count-bg-color: Canvas;
|
||||
--comment-indicator-active-fg-color: SelectedItem;
|
||||
--comment-indicator-focus-fg-color: CanvasText;
|
||||
--comment-indicator-hover-fg-color: CanvasText;
|
||||
--comment-indicator-selected-fg-color: SelectedItem;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
width: 239px;
|
||||
height: auto;
|
||||
min-width: 180px;
|
||||
max-width: 632px;
|
||||
padding-bottom: 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
#editorCommentsSidebarHeader {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.commentCount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
user-select: none;
|
||||
|
||||
#editorCommentsSidebarTitle {
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 590;
|
||||
line-height: normal;
|
||||
font-size: 17px;
|
||||
color: var(--comment-fg-color);
|
||||
}
|
||||
|
||||
#editorCommentsSidebarCount {
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--comment-count-bg-color);
|
||||
|
||||
color: var(--comment-fg-color);
|
||||
text-align: center;
|
||||
|
||||
font: menu;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebarCloseButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: var(--comment-close-button-icon);
|
||||
background-color: var(--comment-fg-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--comment-hover-bg-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--comment-active-bg-color);
|
||||
}
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editorCommentsSidebarList {
|
||||
display: flex;
|
||||
width: auto;
|
||||
padding: 1px 16px 0;
|
||||
gap: 10px;
|
||||
flex: 1 0 0;
|
||||
align-self: stretch;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
|
||||
.sidebarComment {
|
||||
display: flex;
|
||||
width: auto;
|
||||
padding: 8px 16px 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
gap: 4px;
|
||||
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid var(--comment-border-color);
|
||||
background-color: var(--comment-bg-color);
|
||||
|
||||
&:not(.noComments) {
|
||||
&:hover {
|
||||
background-color: var(--comment-hover-bg-color);
|
||||
|
||||
time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-hover-fg-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--comment-active-bg-color);
|
||||
|
||||
time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-active-fg-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:focus, :focus-visible) time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-focus-fg-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--comment-focus-outline-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.sidebarCommentText {
|
||||
max-height: fit-content;
|
||||
-webkit-line-clamp: unset;
|
||||
}
|
||||
|
||||
time::after {
|
||||
display: inline-block;
|
||||
background-color: var(--comment-indicator-selected-fg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarCommentText {
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
max-height: 80px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
&.noComments .sidebarCommentText {
|
||||
max-height: fit-content;
|
||||
-webkit-line-clamp: unset;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
time {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
font: menu;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
font-size: 13px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: var(--comment-edit-button-icon);
|
||||
transform: scaleX(var(--dir-factor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getRGB, noContextMenu, shadow, stopEvent } from "pdfjs-lib";
|
||||
import {
|
||||
AnnotationEditorType,
|
||||
getRGB,
|
||||
noContextMenu,
|
||||
PDFDateString,
|
||||
shadow,
|
||||
stopEvent,
|
||||
} from "pdfjs-lib";
|
||||
import { binarySearchFirstItem } from "./ui_utils.js";
|
||||
|
||||
class CommentManager {
|
||||
#actions;
|
||||
@ -40,6 +48,8 @@ class CommentManager {
|
||||
|
||||
#saveButton;
|
||||
|
||||
#sidebar;
|
||||
|
||||
#uiManager;
|
||||
|
||||
#prevDragX = Infinity;
|
||||
@ -66,6 +76,9 @@ class CommentManager {
|
||||
cancelButton,
|
||||
saveButton,
|
||||
},
|
||||
sidebar,
|
||||
eventBus,
|
||||
linkService,
|
||||
overlayManager
|
||||
) {
|
||||
this.#actions = actions;
|
||||
@ -73,6 +86,7 @@ class CommentManager {
|
||||
this.#editMenuItem = editMenuItem;
|
||||
this.#deleteMenuItem = deleteMenuItem;
|
||||
this.#menu = menu;
|
||||
this.#sidebar = new CommentSidebar(sidebar, eventBus, linkService);
|
||||
this.#textInput = textInput;
|
||||
this.#textView = textView;
|
||||
this.#overlayManager = overlayManager;
|
||||
@ -158,6 +172,26 @@ class CommentManager {
|
||||
overlayManager.register(dialog);
|
||||
}
|
||||
|
||||
showSidebar(annotations) {
|
||||
this.#sidebar.show(annotations);
|
||||
}
|
||||
|
||||
hideSidebar() {
|
||||
this.#sidebar.hide();
|
||||
}
|
||||
|
||||
removeComments(ids) {
|
||||
this.#sidebar.removeComments(ids);
|
||||
}
|
||||
|
||||
selectComment(id) {
|
||||
this.#sidebar.selectComment(null, id);
|
||||
}
|
||||
|
||||
addComment(annotation) {
|
||||
this.#sidebar.addComment(annotation);
|
||||
}
|
||||
|
||||
#closeMenu() {
|
||||
if (!this.#menuAC) {
|
||||
return;
|
||||
@ -378,4 +412,317 @@ class CommentManager {
|
||||
}
|
||||
}
|
||||
|
||||
class CommentSidebar {
|
||||
#annotations = null;
|
||||
|
||||
#boundCommentClick = this.#commentClick.bind(this);
|
||||
|
||||
#boundCommentKeydown = this.#commentKeydown.bind(this);
|
||||
|
||||
#sidebar;
|
||||
|
||||
#closeButton;
|
||||
|
||||
#commentsList;
|
||||
|
||||
#commentCount;
|
||||
|
||||
#sidebarTitle;
|
||||
|
||||
#linkService;
|
||||
|
||||
#elementsToAnnotations = null;
|
||||
|
||||
#idsToElements = null;
|
||||
|
||||
constructor(
|
||||
{
|
||||
sidebar,
|
||||
commentsList,
|
||||
commentCount,
|
||||
sidebarTitle,
|
||||
closeButton,
|
||||
commentToolbarButton,
|
||||
},
|
||||
eventBus,
|
||||
linkService
|
||||
) {
|
||||
this.#sidebar = sidebar;
|
||||
this.#sidebarTitle = sidebarTitle;
|
||||
this.#commentsList = commentsList;
|
||||
this.#commentCount = commentCount;
|
||||
this.#linkService = linkService;
|
||||
this.#closeButton = closeButton;
|
||||
|
||||
closeButton.addEventListener("click", () => {
|
||||
eventBus.dispatch("switchannotationeditormode", {
|
||||
source: this,
|
||||
mode: AnnotationEditorType.NONE,
|
||||
});
|
||||
});
|
||||
commentToolbarButton.addEventListener("keydown", e => {
|
||||
if (e.key === "ArrowDown" || e.key === "Home" || e.key === "F6") {
|
||||
this.#commentsList.firstElementChild.focus();
|
||||
stopEvent(e);
|
||||
} else if (e.key === "ArrowUp" || e.key === "End") {
|
||||
this.#commentsList.lastElementChild.focus();
|
||||
stopEvent(e);
|
||||
}
|
||||
});
|
||||
this.#sidebar.hidden = true;
|
||||
}
|
||||
|
||||
show(annotations) {
|
||||
this.#elementsToAnnotations = new WeakMap();
|
||||
this.#idsToElements = new Map();
|
||||
this.#annotations = annotations = annotations.filter(
|
||||
a => a.popupRef && a.contentsObj?.str
|
||||
);
|
||||
annotations.sort(this.#sortComments.bind(this));
|
||||
if (annotations.length !== 0) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const annotation of annotations) {
|
||||
fragment.append(this.#createCommentElement(annotation));
|
||||
}
|
||||
|
||||
this.#setCommentsCount(fragment);
|
||||
this.#commentsList.append(fragment);
|
||||
} else {
|
||||
this.#setCommentsCount();
|
||||
}
|
||||
this.#sidebar.hidden = false;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.#sidebar.hidden = true;
|
||||
this.#commentsList.replaceChildren();
|
||||
this.#elementsToAnnotations = null;
|
||||
this.#idsToElements = null;
|
||||
this.#annotations = null;
|
||||
}
|
||||
|
||||
removeComments(ids) {
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
new Set(this.#idsToElements.keys()).difference(new Set(ids)).size === 0
|
||||
) {
|
||||
this.#removeAll();
|
||||
return;
|
||||
}
|
||||
for (const id of ids) {
|
||||
this.#removeComment(id);
|
||||
}
|
||||
}
|
||||
|
||||
focusComment(id) {
|
||||
const element = this.#idsToElements.get(id);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
this.#sidebar.scrollTop = element.offsetTop - this.#sidebar.offsetTop;
|
||||
for (const el of this.#commentsList.children) {
|
||||
el.classList.toggle("selected", el === element);
|
||||
}
|
||||
}
|
||||
|
||||
#removeComment(id) {
|
||||
const element = this.#idsToElements.get(id);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const annotation = this.#elementsToAnnotations.get(element);
|
||||
const index = binarySearchFirstItem(
|
||||
this.#annotations,
|
||||
a => this.#sortComments(a, annotation) >= 0
|
||||
);
|
||||
if (index >= this.#annotations.length) {
|
||||
return;
|
||||
}
|
||||
this.#annotations.splice(index, 1);
|
||||
element.remove();
|
||||
this.#idsToElements.delete(id);
|
||||
this.#setCommentsCount();
|
||||
}
|
||||
|
||||
#removeAll() {
|
||||
this.#commentsList.replaceChildren();
|
||||
this.#elementsToAnnotations = new WeakMap();
|
||||
this.#idsToElements.clear();
|
||||
this.#annotations.length = 0;
|
||||
this.#setCommentsCount();
|
||||
}
|
||||
|
||||
selectComment(element, id = null) {
|
||||
element ||= this.#idsToElements.get(id);
|
||||
for (const el of this.#commentsList.children) {
|
||||
el.classList.toggle("selected", el === element);
|
||||
}
|
||||
}
|
||||
|
||||
addComment(annotation) {
|
||||
if (this.#idsToElements.has(annotation.id)) {
|
||||
return;
|
||||
}
|
||||
const { popupRef, contentsObj } = annotation;
|
||||
if (!popupRef || !contentsObj?.str) {
|
||||
return;
|
||||
}
|
||||
const commentItem = this.#createCommentElement(annotation);
|
||||
if (this.#annotations.length === 0) {
|
||||
this.#commentsList.replaceChildren(commentItem);
|
||||
this.#annotations.push(annotation);
|
||||
this.#setCommentsCount();
|
||||
return;
|
||||
}
|
||||
const index = binarySearchFirstItem(
|
||||
this.#annotations,
|
||||
a => this.#sortComments(a, annotation) >= 0
|
||||
);
|
||||
this.#annotations.splice(index, 0, annotation);
|
||||
if (index >= this.#commentsList.children.length) {
|
||||
this.#commentsList.append(commentItem);
|
||||
} else {
|
||||
this.#commentsList.insertBefore(
|
||||
commentItem,
|
||||
this.#commentsList.children[index]
|
||||
);
|
||||
}
|
||||
this.#setCommentsCount();
|
||||
}
|
||||
|
||||
#setCommentsCount(container = this.#commentsList) {
|
||||
const count = this.#idsToElements.size;
|
||||
this.#sidebarTitle.setAttribute(
|
||||
"data-l10n-args",
|
||||
JSON.stringify({ count })
|
||||
);
|
||||
this.#commentCount.textContent = count;
|
||||
if (count === 0) {
|
||||
container.append(this.#createZeroCommentElement());
|
||||
}
|
||||
}
|
||||
|
||||
#createZeroCommentElement() {
|
||||
const commentItem = document.createElement("li");
|
||||
commentItem.classList.add("sidebarComment", "noComments");
|
||||
commentItem.role = "button";
|
||||
const textDiv = document.createElement("div");
|
||||
textDiv.className = "sidebarCommentText";
|
||||
textDiv.setAttribute(
|
||||
"data-l10n-id",
|
||||
"pdfjs-editor-comments-sidebar-no-comments"
|
||||
);
|
||||
commentItem.addEventListener("keydown", this.#boundCommentKeydown);
|
||||
commentItem.append(textDiv);
|
||||
return commentItem;
|
||||
}
|
||||
|
||||
#createCommentElement(annotation) {
|
||||
const {
|
||||
creationDate,
|
||||
modificationDate,
|
||||
contentsObj: { str: text },
|
||||
} = annotation;
|
||||
const commentItem = document.createElement("li");
|
||||
commentItem.role = "button";
|
||||
commentItem.className = "sidebarComment";
|
||||
commentItem.tabIndex = -1;
|
||||
|
||||
const dateDiv = document.createElement("time");
|
||||
const date = PDFDateString.toDateObject(modificationDate || creationDate);
|
||||
dateDiv.dateTime = date.toISOString();
|
||||
const dateFormat = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "long",
|
||||
});
|
||||
dateDiv.textContent = dateFormat.format(date);
|
||||
|
||||
const textDiv = document.createElement("div");
|
||||
textDiv.className = "sidebarCommentText";
|
||||
textDiv.textContent = text;
|
||||
commentItem.append(dateDiv, textDiv);
|
||||
commentItem.addEventListener("click", this.#boundCommentClick);
|
||||
commentItem.addEventListener("keydown", this.#boundCommentKeydown);
|
||||
|
||||
this.#elementsToAnnotations.set(commentItem, annotation);
|
||||
this.#idsToElements.set(annotation.id, commentItem);
|
||||
return commentItem;
|
||||
}
|
||||
|
||||
#commentClick({ currentTarget }) {
|
||||
if (currentTarget.classList.contains("selected")) {
|
||||
return;
|
||||
}
|
||||
const annotation = this.#elementsToAnnotations.get(currentTarget);
|
||||
if (!annotation) {
|
||||
return;
|
||||
}
|
||||
const { pageIndex, rect } = annotation;
|
||||
const SPACE_ABOVE_ANNOTATION = 10;
|
||||
this.#linkService?.goToXY(
|
||||
pageIndex + 1,
|
||||
rect[0],
|
||||
rect[3] + SPACE_ABOVE_ANNOTATION
|
||||
);
|
||||
this.selectComment(currentTarget);
|
||||
}
|
||||
|
||||
#commentKeydown(e) {
|
||||
const { key, currentTarget } = e;
|
||||
switch (key) {
|
||||
case "ArrowDown":
|
||||
(
|
||||
currentTarget.nextElementSibling ||
|
||||
this.#commentsList.firstElementChild
|
||||
).focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
(
|
||||
currentTarget.previousElementSibling ||
|
||||
this.#commentsList.lastElementChild
|
||||
).focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "Home":
|
||||
this.#commentsList.firstElementChild.focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "End":
|
||||
this.#commentsList.lastElementChild.focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "Enter":
|
||||
case " ":
|
||||
this.#commentClick(e);
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "ShiftTab":
|
||||
this.#closeButton.focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#sortComments(a, b) {
|
||||
if (a.pageIndex !== b.pageIndex) {
|
||||
return a.pageIndex - b.pageIndex;
|
||||
}
|
||||
if (a.rect[3] !== b.rect[3]) {
|
||||
return b.rect[3] - a.rect[3];
|
||||
}
|
||||
if (a.rect[0] !== b.rect[0]) {
|
||||
return a.rect[0] - b.rect[0];
|
||||
}
|
||||
if (a.rect[1] !== b.rect[1]) {
|
||||
return b.rect[1] - a.rect[1];
|
||||
}
|
||||
if (a.rect[2] !== b.rect[2]) {
|
||||
return a.rect[2] - b.rect[2];
|
||||
}
|
||||
return a.id.localeCompare(b.id);
|
||||
}
|
||||
}
|
||||
|
||||
export { CommentManager };
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
@import url(xfa_layer_builder.css);
|
||||
/* Ignored in GECKOVIEW builds: */
|
||||
@import url(annotation_editor_layer_builder.css);
|
||||
@import url(sidebar.css);
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
37
web/sidebar.css
Normal file
37
web/sidebar.css
Normal file
@ -0,0 +1,37 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
--sidebar-bg-color: light-dark(#fff, #23222b);
|
||||
--sidebar-border-color: light-dark(
|
||||
rgb(21 20 26 / 0.1),
|
||||
rgb(251 251 254 / 0.1)
|
||||
);
|
||||
--sidebar-box-shadow:
|
||||
0 0.25px 0.75px 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));
|
||||
|
||||
@media screen and (forced-colors: active) {
|
||||
--sidebar-bg-color: Canvas;
|
||||
--sidebar-border-color: CanvasText;
|
||||
--sidebar-box-shadow: none;
|
||||
}
|
||||
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--sidebar-box-shadow);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
background-color: var(--sidebar-bg-color);
|
||||
inset-block-start: calc(100% + var(--doorhanger-height) - 2px);
|
||||
}
|
||||
@ -67,6 +67,18 @@ class Toolbar {
|
||||
{ element: options.zoomOut, eventName: "zoomout" },
|
||||
{ element: options.print, eventName: "print" },
|
||||
{ element: options.download, eventName: "download" },
|
||||
{
|
||||
element: options.editorCommentButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
eventDetails: {
|
||||
get mode() {
|
||||
const { classList } = options.editorCommentButton;
|
||||
return classList.contains("toggled")
|
||||
? AnnotationEditorType.NONE
|
||||
: AnnotationEditorType.POPUP;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
element: options.editorFreeTextButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
@ -278,6 +290,8 @@ class Toolbar {
|
||||
|
||||
#editorModeChanged({ mode }) {
|
||||
const {
|
||||
editorCommentButton,
|
||||
editorCommentParamsToolbar,
|
||||
editorFreeTextButton,
|
||||
editorFreeTextParamsToolbar,
|
||||
editorHighlightButton,
|
||||
@ -290,6 +304,11 @@ class Toolbar {
|
||||
editorSignatureParamsToolbar,
|
||||
} = this.#opts;
|
||||
|
||||
toggleExpandedBtn(
|
||||
editorCommentButton,
|
||||
mode === AnnotationEditorType.POPUP,
|
||||
editorCommentParamsToolbar
|
||||
);
|
||||
toggleExpandedBtn(
|
||||
editorFreeTextButton,
|
||||
mode === AnnotationEditorType.FREETEXT,
|
||||
@ -316,12 +335,13 @@ class Toolbar {
|
||||
editorSignatureParamsToolbar
|
||||
);
|
||||
|
||||
const isDisable = mode === AnnotationEditorType.DISABLE;
|
||||
editorFreeTextButton.disabled = isDisable;
|
||||
editorHighlightButton.disabled = isDisable;
|
||||
editorInkButton.disabled = isDisable;
|
||||
editorStampButton.disabled = isDisable;
|
||||
editorSignatureButton.disabled = isDisable;
|
||||
editorCommentButton.disabled =
|
||||
editorFreeTextButton.disabled =
|
||||
editorHighlightButton.disabled =
|
||||
editorInkButton.disabled =
|
||||
editorStampButton.disabled =
|
||||
editorSignatureButton.disabled =
|
||||
mode === AnnotationEditorType.DISABLE;
|
||||
}
|
||||
|
||||
#updateUIState(resetNumPages = false) {
|
||||
|
||||
@ -99,6 +99,7 @@
|
||||
--loading-icon: url(images/loading.svg);
|
||||
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
|
||||
--treeitem-collapsed-icon: url(images/treeitem-collapsed.svg);
|
||||
--toolbarButton-editorComment-icon: url(images/comment-editButton.svg);
|
||||
--toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg);
|
||||
--toolbarButton-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg);
|
||||
--toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg);
|
||||
@ -541,6 +542,11 @@ body {
|
||||
mask-image: var(--toolbarButton-zoomIn-icon);
|
||||
}
|
||||
|
||||
#editorCommentButton::before {
|
||||
mask-image: var(--toolbarButton-editorComment-icon);
|
||||
transform: scaleX(var(--dir-factor));
|
||||
}
|
||||
|
||||
#editorFreeTextButton::before {
|
||||
mask-image: var(--toolbarButton-editorFreeText-icon);
|
||||
}
|
||||
|
||||
@ -245,6 +245,25 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</div>
|
||||
<div id="toolbarViewerRight" class="toolbarHorizontalGroup">
|
||||
<div id="editorModeButtons" class="toolbarHorizontalGroup" role="radiogroup">
|
||||
<div id="editorComment" class="toolbarButtonWithContainer" hidden="true">
|
||||
<button id="editorCommentButton" class="toolbarButton" type="button" tabindex="0" disabled="disabled" role="radio" aria-expanded="false" aria-haspopup="true" aria-controls="editorCommentParamsToolbar" data-l10n-id="pdfjs-editor-comment-button">
|
||||
<span data-l10n-id="pdfjs-editor-comment-button-label"></span>
|
||||
</button>
|
||||
<div class="editorParamsToolbar sidebar hidden menu" id="editorCommentParamsToolbar">
|
||||
<div id="editorCommentsSidebar" class="menuContainer" role="landmark" aria-labelledby="editorCommentsSidebarHeader">
|
||||
<div id="editorCommentsSidebarHeader" role="heading" aria-level="2">
|
||||
<span class="commentCount">
|
||||
<span id="editorCommentsSidebarTitle" data-l10n-id="pdfjs-editor-comments-sidebar-title" data-l10n-args='{ "count": 0 }'></span>
|
||||
<span id="editorCommentsSidebarCount"></span>
|
||||
</span>
|
||||
<button id="editorCommentsSidebarCloseButton" type="button" tabindex="0" data-l10n-id="pdfjs-editor-comments-sidebar-close-button">
|
||||
<span data-l10n-id="pdfjs-editor-comments-sidebar-close-button-label"></span>
|
||||
</button>
|
||||
</div>
|
||||
<ul id="editorCommentsSidebarList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editorSignature" class="toolbarButtonWithContainer" hidden="true">
|
||||
<button id="editorSignatureButton" class="toolbarButton" type="button" tabindex="0" disabled="disabled" role="radio" aria-expanded="false" aria-haspopup="true" aria-controls="editorSignatureParamsToolbar" data-l10n-id="pdfjs-editor-signature-button">
|
||||
<span data-l10n-id="pdfjs-editor-signature-button-label"></span>
|
||||
|
||||
@ -45,6 +45,10 @@ function getViewerConfiguration() {
|
||||
zoomIn: document.getElementById("zoomInButton"),
|
||||
zoomOut: document.getElementById("zoomOutButton"),
|
||||
print: document.getElementById("printButton"),
|
||||
editorCommentButton: document.getElementById("editorCommentButton"),
|
||||
editorCommentParamsToolbar: document.getElementById(
|
||||
"editorCommentParamsToolbar"
|
||||
),
|
||||
editorFreeTextButton: document.getElementById("editorFreeTextButton"),
|
||||
editorFreeTextParamsToolbar: document.getElementById(
|
||||
"editorFreeTextParamsToolbar"
|
||||
@ -241,6 +245,19 @@ function getViewerConfiguration() {
|
||||
updateButton: document.getElementById("editSignatureUpdateButton"),
|
||||
},
|
||||
annotationEditorParams: {
|
||||
editorCommentsSidebar: document.getElementById("editorCommentsSidebar"),
|
||||
editorCommentsSidebarCount: document.getElementById(
|
||||
"editorCommentsSidebarCount"
|
||||
),
|
||||
editorCommentsSidebarTitle: document.getElementById(
|
||||
"editorCommentsSidebarTitle"
|
||||
),
|
||||
editorCommentsSidebarCloseButton: document.getElementById(
|
||||
"editorCommentsSidebarCloseButton"
|
||||
),
|
||||
editorCommentsSidebarList: document.getElementById(
|
||||
"editorCommentsSidebarList"
|
||||
),
|
||||
editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"),
|
||||
editorFreeTextColor: document.getElementById("editorFreeTextColor"),
|
||||
editorInkColor: document.getElementById("editorInkColor"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user