Files
PDF.js/web/comment_manager.js
Yu Cong 44db9807a1
Some checks failed
Types tests / Test (lts/*) (push) Has been cancelled
Lint / Lint (lts/*) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / Test (20) (push) Has been cancelled
CI / Test (22) (push) Has been cancelled
CI / Test (24) (push) Has been cancelled
first commit
2025-10-03 22:20:19 +08:00

1264 lines
32 KiB
JavaScript

/* 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 {
AnnotationEditorType,
applyOpacity,
CSSConstants,
findContrastColor,
MathClamp,
noContextMenu,
PDFDateString,
renderRichText,
shadow,
stopEvent,
Util,
} from "pdfjs-lib";
import { binarySearchFirstItem } from "./ui_utils.js";
class CommentManager {
#dialog;
#popup;
#sidebar;
static #hasForcedColors = null;
constructor(
commentDialog,
sidebar,
eventBus,
linkService,
overlayManager,
ltr,
hasForcedColors
) {
const dateFormat = new Intl.DateTimeFormat(undefined, {
dateStyle: "long",
});
this.dialogElement = commentDialog.dialog;
this.#dialog = new CommentDialog(
commentDialog,
overlayManager,
eventBus,
ltr
);
this.#popup = new CommentPopup(
eventBus,
dateFormat,
ltr,
this.dialogElement
);
this.#sidebar = new CommentSidebar(
sidebar,
eventBus,
linkService,
this.#popup,
dateFormat,
ltr
);
this.#popup.sidebar = this.#sidebar;
CommentManager.#hasForcedColors = hasForcedColors;
}
setSidebarUiManager(uiManager) {
this.#sidebar.setUIManager(uiManager);
}
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);
}
updateComment(annotation) {
this.#sidebar.updateComment(annotation);
}
toggleCommentPopup(editor, isSelected, visibility, isEditable) {
if (isSelected) {
this.selectComment(editor.uid);
}
this.#popup.toggle(editor, isSelected, visibility, isEditable);
}
destroyPopup() {
this.#popup.destroy();
}
updatePopupColor(editor) {
this.#popup.updateColor(editor);
}
showDialog(uiManager, editor, posX, posY, options) {
return this.#dialog.open(uiManager, editor, posX, posY, options);
}
makeCommentColor(color, opacity) {
return CommentManager._makeCommentColor(color, opacity);
}
static _makeCommentColor(color, opacity) {
return this.#hasForcedColors
? null
: findContrastColor(
applyOpacity(...color, opacity ?? 1),
CSSConstants.commentForegroundColor
);
}
destroy() {
this.#dialog.destroy();
this.#sidebar.hide();
this.#popup.destroy();
}
}
class CommentSidebar {
#annotations = null;
#eventBus;
#boundCommentClick = this.#commentClick.bind(this);
#boundCommentKeydown = this.#commentKeydown.bind(this);
#sidebar;
#closeButton;
#commentsList;
#commentCount;
#dateFormat;
#sidebarTitle;
#learnMoreUrl;
#linkService;
#popup;
#elementsToAnnotations = null;
#idsToElements = null;
#uiManager = null;
#minWidth = 0;
#maxWidth = 0;
#initialWidth = 0;
#width = 0;
#ltr;
constructor(
{
learnMoreUrl,
sidebar,
sidebarResizer,
commentsList,
commentCount,
sidebarTitle,
closeButton,
commentToolbarButton,
},
eventBus,
linkService,
popup,
dateFormat,
ltr
) {
this.#sidebar = sidebar;
this.#sidebarTitle = sidebarTitle;
this.#commentsList = commentsList;
this.#commentCount = commentCount;
this.#learnMoreUrl = learnMoreUrl;
this.#linkService = linkService;
this.#closeButton = closeButton;
this.#popup = popup;
this.#dateFormat = dateFormat;
this.#ltr = ltr;
this.#eventBus = eventBus;
const style = window.getComputedStyle(sidebar);
this.#minWidth = parseFloat(style.getPropertyValue("--sidebar-min-width"));
this.#maxWidth = parseFloat(style.getPropertyValue("--sidebar-max-width"));
this.#initialWidth = this.#width = parseFloat(
style.getPropertyValue("--sidebar-width")
);
this.#makeSidebarResizable(sidebarResizer);
closeButton.addEventListener("click", () => {
eventBus.dispatch("switchannotationeditormode", {
source: this,
mode: AnnotationEditorType.NONE,
});
});
const keyDownCallback = 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);
}
};
commentToolbarButton.addEventListener("keydown", keyDownCallback);
sidebar.addEventListener("keydown", keyDownCallback);
this.#sidebar.hidden = true;
}
#makeSidebarResizable(resizer) {
let pointerMoveAC;
const cancelResize = () => {
this.#width = MathClamp(this.#width, this.#minWidth, this.#maxWidth);
this.#sidebar.classList.remove("resizing");
pointerMoveAC?.abort();
pointerMoveAC = null;
};
resizer.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelResize();
return;
}
const { clientX } = e;
stopEvent(e);
let prevX = clientX;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
const sign = this.#ltr ? -1 : 1;
const sidebar = this.#sidebar;
const sidebarStyle = sidebar.style;
sidebar.classList.add("resizing");
const parentStyle = sidebar.parentElement.style;
parentStyle.minWidth = 0;
window.addEventListener("contextmenu", noContextMenu, { signal });
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC) {
return;
}
stopEvent(ev);
const { clientX: x } = ev;
const newWidth = (this.#width += sign * (x - prevX));
prevX = x;
if (newWidth > this.#maxWidth || newWidth < this.#minWidth) {
return;
}
sidebarStyle.width = `${newWidth.toFixed(3)}px`;
parentStyle.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`;
},
{ signal, capture: true }
);
window.addEventListener("blur", cancelResize, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelResize();
stopEvent(ev);
}
},
{ signal }
);
});
}
setUIManager(uiManager) {
this.#uiManager = uiManager;
}
show(annotations) {
this.#elementsToAnnotations = new WeakMap();
this.#idsToElements = new Map();
this.#annotations = annotations;
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;
this.#eventBus.dispatch("reporttelemetry", {
source: this,
details: {
type: "commentSidebar",
data: { numberOfAnnotations: annotations.length },
},
});
}
hide() {
this.#sidebar.hidden = true;
this.#commentsList.replaceChildren();
this.#elementsToAnnotations = null;
this.#idsToElements = null;
this.#annotations = null;
}
removeComments(ids) {
if (ids.length === 0 || !this.#idsToElements) {
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);
}
}
updateComment(annotation) {
if (!this.#idsToElements) {
return;
}
const {
id,
creationDate,
modificationDate,
richText,
contentsObj,
popupRef,
} = annotation;
if (!popupRef || (!richText && !contentsObj?.str)) {
this.#removeComment(id);
}
const element = this.#idsToElements.get(id);
if (!element) {
return;
}
const prevAnnotation = this.#elementsToAnnotations.get(element);
let index = binarySearchFirstItem(
this.#annotations,
a => this.#sortComments(a, prevAnnotation) >= 0
);
if (index >= this.#annotations.length) {
return;
}
this.#setDate(element.firstChild, modificationDate || creationDate);
this.#setText(element.lastChild, richText, contentsObj);
this.#annotations.splice(index, 1);
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(element);
} else {
this.#commentsList.insertBefore(
element,
this.#commentsList.children[index]
);
}
}
#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) {
if (!this.#idsToElements) {
return;
}
const hasNoElement = !element;
element ||= this.#idsToElements.get(id);
for (const el of this.#commentsList.children) {
el.classList.toggle("selected", el === element);
}
if (hasNoElement) {
element?.scrollIntoView({ behavior: "instant", block: "center" });
}
}
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");
const textDiv = document.createElement("div");
textDiv.className = "sidebarCommentText";
textDiv.setAttribute(
"data-l10n-id",
"pdfjs-editor-comments-sidebar-no-comments1"
);
commentItem.append(textDiv);
if (this.#learnMoreUrl) {
const a = document.createElement("a");
a.setAttribute(
"data-l10n-id",
"pdfjs-editor-comments-sidebar-no-comments-link"
);
a.href = this.#learnMoreUrl;
a.target = "_blank";
a.rel = "noopener noreferrer";
commentItem.append(a);
}
return commentItem;
}
#setDate(element, date) {
date = PDFDateString.toDateObject(date);
element.dateTime = date.toISOString();
element.textContent = this.#dateFormat.format(date);
}
#setText(element, richText, contentsObj) {
element.replaceChildren();
const html =
richText?.str && (!contentsObj?.str || richText.str === contentsObj.str)
? richText.html
: contentsObj?.str;
renderRichText(
{
html,
dir: contentsObj?.dir || "auto",
className: "richText",
},
element
);
}
#createCommentElement(annotation) {
const {
id,
creationDate,
modificationDate,
richText,
contentsObj,
color,
opacity,
} = annotation;
const commentItem = document.createElement("li");
commentItem.role = "button";
commentItem.className = "sidebarComment";
commentItem.tabIndex = -1;
commentItem.style.backgroundColor =
(color && CommentManager._makeCommentColor(color, opacity)) || "";
const dateDiv = document.createElement("time");
this.#setDate(dateDiv, modificationDate || creationDate);
const textDiv = document.createElement("div");
textDiv.className = "sidebarCommentText";
this.#setText(textDiv, richText, contentsObj);
commentItem.append(dateDiv, textDiv);
commentItem.addEventListener("click", this.#boundCommentClick);
commentItem.addEventListener("keydown", this.#boundCommentKeydown);
this.#elementsToAnnotations.set(commentItem, annotation);
this.#idsToElements.set(id, commentItem);
return commentItem;
}
async #commentClick({ currentTarget }) {
if (currentTarget.classList.contains("selected")) {
currentTarget.classList.remove("selected");
this.#popup._hide();
return;
}
const annotation = this.#elementsToAnnotations.get(currentTarget);
if (!annotation) {
return;
}
this.#popup._hide();
const { id, pageIndex, rect } = annotation;
const pageNumber = pageIndex + 1;
const pageVisiblePromise =
this.#uiManager?.waitForEditorsRendered(pageNumber);
this.#linkService?.goToXY(pageNumber, rect[0], rect[3], {
center: "both",
});
this.selectComment(currentTarget);
await pageVisiblePromise;
this.#uiManager?.selectComment(pageIndex, id);
}
#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) {
const dateA = PDFDateString.toDateObject(
a.modificationDate || a.creationDate
);
const dateB = PDFDateString.toDateObject(
b.modificationDate || b.creationDate
);
if (dateA !== dateB) {
if (dateA !== null && dateB !== null) {
return dateB - dateA;
}
return dateA !== null ? -1 : 1;
}
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);
}
}
class CommentDialog {
#dialog;
#editor;
#overlayManager;
#previousText = "";
#commentText = "";
#textInput;
#title;
#saveButton;
#uiManager;
#prevDragX = 0;
#prevDragY = 0;
#dialogX = 0;
#dialogY = 0;
#isLTR;
#eventBus;
constructor(
{ dialog, toolbar, title, textInput, cancelButton, saveButton },
overlayManager,
eventBus,
ltr
) {
this.#dialog = dialog;
this.#textInput = textInput;
this.#overlayManager = overlayManager;
this.#eventBus = eventBus;
this.#saveButton = saveButton;
this.#title = title;
this.#isLTR = ltr;
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);
saveButton.addEventListener("click", this.#save.bind(this));
textInput.addEventListener("input", () => {
saveButton.disabled = textInput.value === this.#previousText;
});
// Make the dialog draggable.
let pointerMoveAC;
const cancelDrag = () => {
dialog.classList.remove("dragging");
pointerMoveAC?.abort();
pointerMoveAC = null;
};
toolbar.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelDrag();
return;
}
const { clientX, clientY } = e;
stopEvent(e);
this.#prevDragX = clientX;
this.#prevDragY = clientY;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
const { innerHeight, innerWidth } = window;
dialog.classList.add("dragging");
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC) {
return;
}
const { clientX: x, clientY: y } = ev;
this.#setPosition(
this.#dialogX + (x - this.#prevDragX) / innerWidth,
this.#dialogY + (y - this.#prevDragY) / innerHeight
);
this.#prevDragX = x;
this.#prevDragY = y;
stopEvent(ev);
},
{ signal }
);
window.addEventListener("blur", cancelDrag, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelDrag();
stopEvent(ev);
}
},
{ signal }
);
});
overlayManager.register(dialog);
}
async open(uiManager, editor, posX, posY, options) {
if (editor) {
this.#uiManager = uiManager;
this.#editor = editor;
}
const {
contentsObj: { str },
color,
opacity,
} = editor.getData();
const { style: dialogStyle } = this.#dialog;
if (color) {
dialogStyle.backgroundColor = CommentManager._makeCommentColor(
color,
opacity
);
dialogStyle.borderColor = Util.makeHexColor(...color);
} else {
dialogStyle.backgroundColor = dialogStyle.borderColor = "";
}
this.#commentText = str || "";
const textInput = this.#textInput;
textInput.value = this.#previousText = this.#commentText;
if (str) {
this.#title.setAttribute(
"data-l10n-id",
"pdfjs-editor-edit-comment-dialog-title-when-editing"
);
this.#saveButton.setAttribute(
"data-l10n-id",
"pdfjs-editor-edit-comment-dialog-save-button-when-editing"
);
} else {
this.#title.setAttribute(
"data-l10n-id",
"pdfjs-editor-edit-comment-dialog-title-when-adding"
);
this.#saveButton.setAttribute(
"data-l10n-id",
"pdfjs-editor-edit-comment-dialog-save-button-when-adding"
);
}
if (options?.height) {
textInput.style.height = `${options.height}px`;
}
this.#uiManager?.removeEditListeners();
this.#saveButton.disabled = true;
const parentDimensions = options?.parentDimensions;
const { innerHeight, innerWidth } = window;
if (editor.hasDefaultPopupPosition()) {
const { dialogWidth, dialogHeight } = this._dialogDimensions;
if (parentDimensions) {
if (
this.#isLTR &&
posX + dialogWidth >
Math.min(parentDimensions.x + parentDimensions.width, innerWidth)
) {
const buttonWidth = this.#editor.commentButtonWidth;
posX -= dialogWidth - buttonWidth * parentDimensions.width;
} else if (!this.#isLTR) {
const buttonWidth =
this.#editor.commentButtonWidth * parentDimensions.width;
if (posX - dialogWidth < Math.max(0, parentDimensions.x)) {
posX = Math.max(0, posX);
} else {
posX -= dialogWidth - buttonWidth;
}
}
}
const height = Math.max(dialogHeight, options?.height || 0);
if (posY + height > innerHeight) {
posY = innerHeight - height;
}
if (posY < 0) {
posY = 0;
}
}
posX = MathClamp(posX / innerWidth, 0, 1);
posY = MathClamp(posY / innerHeight, 0, 1);
this.#setPosition(posX, posY);
await this.#overlayManager.open(this.#dialog);
textInput.focus();
}
async #save() {
this.#editor.comment = this.#textInput.value;
this.#finish();
}
get _dialogDimensions() {
const dialog = this.#dialog;
const { style } = dialog;
style.opacity = "0";
style.display = "block";
const { width, height } = dialog.getBoundingClientRect();
style.opacity = style.display = "";
return shadow(this, "_dialogDimensions", {
dialogWidth: width,
dialogHeight: height,
});
}
#setPosition(x, y) {
this.#dialogX = x;
this.#dialogY = y;
const { style } = this.#dialog;
style.left = `${100 * x}%`;
style.top = `${100 * y}%`;
}
#finish() {
if (!this.#editor) {
return;
}
const edited = this.#textInput.value !== this.#commentText;
this.#eventBus.dispatch("reporttelemetry", {
source: this,
details: {
type: "comment",
data: {
edited,
},
},
});
this.#editor?.focusCommentButton();
this.#editor = null;
this.#textInput.value = this.#previousText = this.#commentText = "";
this.#overlayManager.closeIfActive(this.#dialog);
this.#textInput.style.height = "";
this.#uiManager?.addEditListeners();
this.#uiManager = null;
}
destroy() {
this.#uiManager = null;
this.#editor = null;
this.#finish();
}
}
class CommentPopup {
#buttonsContainer = null;
#eventBus;
#commentDialog;
#dateFormat;
#editor = null;
#isLTR;
#container = null;
#text = null;
#time = null;
#prevDragX = 0;
#prevDragY = 0;
#posX = 0;
#posY = 0;
#previousFocusedElement = null;
#selected = false;
#visible = false;
constructor(eventBus, dateFormat, ltr, commentDialog) {
this.#eventBus = eventBus;
this.#dateFormat = dateFormat;
this.#isLTR = ltr;
this.#commentDialog = commentDialog;
this.sidebar = null;
}
get _popupWidth() {
const container = this.#createPopup();
const { style } = container;
style.opacity = "0";
style.display = "block";
document.body.append(container);
const width = container.getBoundingClientRect().width;
container.remove();
style.opacity = style.display = "";
return shadow(this, "_popupWidth", width);
}
#createPopup() {
if (this.#container) {
return this.#container;
}
const container = (this.#container = document.createElement("div"));
container.className = "commentPopup";
container.id = "commentPopup";
container.tabIndex = -1;
container.role = "dialog";
container.ariaModal = "false";
container.addEventListener("contextmenu", noContextMenu);
container.addEventListener("keydown", e => {
if (e.key === "Escape") {
this.toggle(this.#editor, true, false);
this.#previousFocusedElement?.focus();
stopEvent(e);
}
});
container.addEventListener("click", () => {
container.focus();
});
const top = document.createElement("div");
top.className = "commentPopupTop";
const time = (this.#time = document.createElement("time"));
time.className = "commentPopupTime";
const buttons = (this.#buttonsContainer = document.createElement("div"));
buttons.className = "commentPopupButtons";
const edit = document.createElement("button");
edit.classList.add("commentPopupEdit", "toolbarButton");
edit.tabIndex = 0;
edit.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-popup-button");
edit.ariaHasPopup = "dialog";
edit.ariaControlsElements = [this.#commentDialog];
const editLabel = document.createElement("span");
editLabel.setAttribute(
"data-l10n-id",
"pdfjs-editor-edit-comment-popup-button-label"
);
edit.append(editLabel);
edit.addEventListener("click", () => {
const editor = this.#editor;
const height = parseFloat(getComputedStyle(this.#text).height);
this.toggle(editor, /* isSelected */ true, /* visibility */ false);
editor.editComment({
height,
});
});
edit.addEventListener("contextmenu", noContextMenu);
const del = document.createElement("button");
del.classList.add("commentPopupDelete", "toolbarButton");
del.tabIndex = 0;
del.setAttribute(
"data-l10n-id",
"pdfjs-editor-delete-comment-popup-button"
);
const delLabel = document.createElement("span");
delLabel.setAttribute(
"data-l10n-id",
"pdfjs-editor-delete-comment-popup-button-label"
);
del.append(delLabel);
del.addEventListener("click", () => {
this.#eventBus.dispatch("reporttelemetry", {
source: this,
details: {
type: "comment",
data: {
deleted: true,
},
},
});
this.#editor.comment = null;
this.destroy();
});
del.addEventListener("contextmenu", noContextMenu);
buttons.append(edit, del);
top.append(time, buttons);
const separator = document.createElement("hr");
const text = (this.#text = document.createElement("div"));
text.className = "commentPopupText";
container.append(top, separator, text);
// Make the dialog draggable.
let pointerMoveAC;
const cancelDrag = () => {
container.classList.remove("dragging");
pointerMoveAC?.abort();
pointerMoveAC = null;
};
top.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelDrag();
return;
}
const { target, clientX, clientY } = e;
if (buttons.contains(target)) {
return;
}
stopEvent(e);
const { width: parentWidth, height: parentHeight } =
this.#editor.parentBoundingClientRect;
this.#prevDragX = clientX;
this.#prevDragY = clientY;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
container.classList.add("dragging");
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC) {
return; // Not dragging.
}
const { clientX: x, clientY: y } = ev;
this.#setPosition(
this.#posX + (x - this.#prevDragX) / parentWidth,
this.#posY + (y - this.#prevDragY) / parentHeight,
/* correctPosition = */ false
);
this.#prevDragX = x;
this.#prevDragY = y;
stopEvent(ev);
},
{ signal }
);
window.addEventListener("blur", cancelDrag, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelDrag();
stopEvent(ev);
}
},
{ signal }
);
});
return container;
}
updateColor(editor) {
if (this.#editor !== editor || !this.#visible) {
return;
}
const { color, opacity } = editor.getData();
this.#container.style.backgroundColor =
(color && CommentManager._makeCommentColor(color, opacity)) || "";
}
_hide(editor) {
const container = this.#createPopup();
container.classList.toggle("hidden", true);
container.classList.toggle("selected", false);
(editor || this.#editor)?.setCommentButtonStates({
selected: false,
hasPopup: false,
});
this.#editor = null;
this.#selected = false;
this.#visible = false;
this.#text.replaceChildren();
this.sidebar.selectComment(null);
}
toggle(editor, isSelected, visibility = undefined, isEditable = true) {
if (!editor) {
this.destroy();
return;
}
if (isSelected) {
visibility ??=
this.#editor === editor ? !this.#selected || !this.#visible : true;
} else {
if (this.#selected) {
return;
}
visibility ??= !this.#visible;
}
if (!visibility) {
this._hide(editor);
return;
}
this.#visible = true;
if (this.#editor !== editor) {
this.#editor?.setCommentButtonStates({
selected: false,
hasPopup: false,
});
}
const container = this.#createPopup();
this.#buttonsContainer.classList.toggle("hidden", !isEditable);
container.classList.toggle("hidden", false);
container.classList.toggle("selected", isSelected);
this.#selected = isSelected;
this.#editor = editor;
editor.setCommentButtonStates({
selected: isSelected,
hasPopup: true,
});
const {
contentsObj,
richText,
creationDate,
modificationDate,
color,
opacity,
} = editor.getData();
container.style.backgroundColor =
(color && CommentManager._makeCommentColor(color, opacity)) || "";
this.#text.replaceChildren();
const html =
richText?.str && (!contentsObj?.str || richText.str === contentsObj.str)
? richText.html
: contentsObj?.str;
if (html) {
renderRichText(
{
html,
dir: contentsObj?.dir || "auto",
className: "richText",
},
this.#text
);
}
this.#time.textContent = this.#dateFormat.format(
PDFDateString.toDateObject(modificationDate || creationDate)
);
this.#setPosition(
...editor.commentPopupPosition,
/* correctPosition = */ editor.hasDefaultPopupPosition()
);
editor.elementBeforePopup.after(container);
container.addEventListener(
"focus",
({ relatedTarget }) => {
this.#previousFocusedElement = relatedTarget;
},
{ once: true }
);
if (isSelected) {
setTimeout(() => container.focus(), 0);
}
}
#setPosition(x, y, correctPosition) {
if (!correctPosition) {
this.#editor.commentPopupPosition = [x, y];
} else {
const widthRatio =
this._popupWidth / this.#editor.parentBoundingClientRect.width;
if (
(this.#isLTR && x + widthRatio > 1) ||
(!this.#isLTR && x - widthRatio >= 0)
) {
const buttonWidth = this.#editor.commentButtonWidth;
x -= widthRatio - buttonWidth;
}
}
this.#posX = x;
this.#posY = y;
const { style } = this.#container;
style.left = `${100 * x}%`;
style.top = `${100 * y}%`;
}
destroy() {
this._hide();
this.#container?.remove();
this.#container = this.#text = this.#time = null;
this.#prevDragX = this.#prevDragY = Infinity;
this.#posX = this.#posY = 0;
this.#previousFocusedElement = null;
}
}
export { CommentManager };