pdf.js/src/display/editor/comment.js

330 lines
7.8 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 { noContextMenu, stopEvent } from "../display_utils.js";
class Comment {
#commentStandaloneButton = null;
#commentToolbarButton = null;
#commentWasFromKeyBoard = false;
#editor = null;
#initialText = null;
#richText = null;
#text = null;
#date = null;
#deleted = false;
#popupPosition = null;
constructor(editor) {
this.#editor = editor;
}
renderForToolbar() {
const button = (this.#commentToolbarButton =
document.createElement("button"));
button.className = "comment";
return this.#render(button, false);
}
renderForStandalone() {
const button = (this.#commentStandaloneButton =
document.createElement("button"));
button.className = "annotationCommentButton";
const position = this.#editor.commentButtonPosition;
if (position) {
const { style } = button;
style.insetInlineEnd = `calc(${
100 *
(this.#editor._uiManager.direction === "ltr"
? 1 - position[0]
: position[0])
}% - var(--comment-button-dim))`;
style.top = `calc(${100 * position[1]}% - var(--comment-button-dim))`;
const color = this.#editor.commentButtonColor;
if (color) {
style.backgroundColor = color;
}
}
return this.#render(button, true);
}
onUpdatedColor() {
if (!this.#commentStandaloneButton) {
return;
}
const color = this.#editor.commentButtonColor;
if (color) {
this.#commentStandaloneButton.style.backgroundColor = color;
}
this.#editor._uiManager.updatePopupColor(this.#editor);
}
get commentButtonWidth() {
return (
(this.#commentStandaloneButton?.getBoundingClientRect().width ?? 0) /
this.#editor.parent.boundingClientRect.width
);
}
get commentPopupPositionInLayer() {
if (this.#popupPosition) {
return this.#popupPosition;
}
if (!this.#commentStandaloneButton) {
return null;
}
const { x, y, height } =
this.#commentStandaloneButton.getBoundingClientRect();
const {
x: parentX,
y: parentY,
width: parentWidth,
height: parentHeight,
} = this.#editor.parent.boundingClientRect;
return [(x - parentX) / parentWidth, (y + height - parentY) / parentHeight];
}
set commentPopupPositionInLayer(pos) {
this.#popupPosition = pos;
}
removeStandaloneCommentButton() {
this.#commentStandaloneButton?.remove();
this.#commentStandaloneButton = null;
}
removeToolbarCommentButton() {
this.#commentToolbarButton?.remove();
this.#commentToolbarButton = null;
}
setCommentButtonStates({ selected, hasPopup }) {
if (!this.#commentStandaloneButton) {
return;
}
this.#commentStandaloneButton.classList.toggle("selected", selected);
this.#commentStandaloneButton.ariaExpanded = hasPopup;
}
#render(comment, isStandalone) {
if (!this.#editor._uiManager.hasCommentManager()) {
return null;
}
comment.tabIndex = "0";
comment.ariaHasPopup = "dialog";
if (isStandalone) {
comment.ariaControls = "commentPopup";
} else {
comment.ariaControlsElements = [
this.#editor._uiManager.getCommentDialogElement(),
];
comment.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-button");
}
const signal = this.#editor._uiManager._signal;
if (!(signal instanceof AbortSignal) || signal.aborted) {
return comment;
}
comment.addEventListener("contextmenu", noContextMenu, { signal });
if (isStandalone) {
comment.addEventListener(
"focusin",
e => {
this.#editor._focusEventsAllowed = false;
stopEvent(e);
},
{
capture: true,
signal,
}
);
comment.addEventListener(
"focusout",
e => {
this.#editor._focusEventsAllowed = true;
stopEvent(e);
},
{
capture: true,
signal,
}
);
}
comment.addEventListener("pointerdown", event => event.stopPropagation(), {
signal,
});
const onClick = event => {
event.preventDefault();
if (comment === this.#commentToolbarButton) {
this.edit();
} else {
this.#editor.toggleComment(/* isSelected = */ true);
}
};
comment.addEventListener("click", onClick, { capture: true, signal });
comment.addEventListener(
"keydown",
event => {
if (event.target === comment && event.key === "Enter") {
this.#commentWasFromKeyBoard = true;
onClick(event);
}
},
{ signal }
);
comment.addEventListener(
"pointerenter",
() => {
this.#editor.toggleComment(
/* isSelected = */ false,
/* visibility = */ true
);
},
{ signal }
);
comment.addEventListener(
"pointerleave",
() => {
this.#editor.toggleComment(
/* isSelected = */ false,
/* visibility = */ false
);
},
{ signal }
);
return comment;
}
edit(options) {
const position = this.commentPopupPositionInLayer;
let posX, posY;
if (position) {
[posX, posY] = position;
} else {
// The position is in the editor coordinates.
[posX, posY] = this.#editor.commentButtonPosition;
const { width, height, x, y } = this.#editor;
posX = x + posX * width;
posY = y + posY * height;
}
const parentDimensions = this.#editor.parent.boundingClientRect;
const {
x: parentX,
y: parentY,
width: parentWidth,
height: parentHeight,
} = parentDimensions;
this.#editor._uiManager.editComment(
this.#editor,
parentX + posX * parentWidth,
parentY + posY * parentHeight,
{ ...options, parentDimensions }
);
}
finish() {
if (!this.#commentToolbarButton) {
return;
}
this.#commentToolbarButton.focus({
focusVisible: this.#commentWasFromKeyBoard,
});
this.#commentWasFromKeyBoard = false;
}
isDeleted() {
return this.#deleted || this.#text === "";
}
isEmpty() {
return this.#text === null;
}
hasBeenEdited() {
return this.isDeleted() || this.#text !== this.#initialText;
}
serialize() {
return this.data;
}
get data() {
return {
text: this.#text,
richText: this.#richText,
date: this.#date,
deleted: this.isDeleted(),
};
}
/**
* Set the comment data.
*/
set data(text) {
if (text !== this.#text) {
this.#richText = null;
}
if (text === null) {
this.#text = "";
this.#deleted = true;
return;
}
this.#text = text;
this.#date = new Date();
this.#deleted = false;
}
setInitialText(text, richText = null) {
this.#initialText = text;
this.data = text;
this.#date = null;
this.#richText = richText;
}
shown() {}
destroy() {
this.#commentToolbarButton?.remove();
this.#commentToolbarButton = null;
this.#commentStandaloneButton?.remove();
this.#commentStandaloneButton = null;
this.#text = "";
this.#richText = null;
this.#date = null;
this.#editor = null;
this.#commentWasFromKeyBoard = false;
this.#deleted = false;
}
}
export { Comment };