Merge pull request #20080 from calixteman/add_comment_1
[Editor] Add the possibility to add Popup annotations (bug 1976724)
This commit is contained in:
commit
23bd705cea
@ -233,6 +233,11 @@
|
|||||||
"description": "Enable creation of hyperlinks from text that look like URLs.",
|
"description": "Enable creation of hyperlinks from text that look like URLs.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": true
|
||||||
|
},
|
||||||
|
"enableComment": {
|
||||||
|
"description": "Enable creation of comment annotations.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -644,3 +644,24 @@ pdfjs-editor-edit-signature-dialog-title = Edit description
|
|||||||
## Dialog buttons
|
## Dialog buttons
|
||||||
|
|
||||||
pdfjs-editor-edit-signature-update-button = Update
|
pdfjs-editor-edit-signature-update-button = Update
|
||||||
|
|
||||||
|
## Edit a comment dialog
|
||||||
|
|
||||||
|
pdfjs-editor-edit-comment-actions-button-label = Actions
|
||||||
|
pdfjs-editor-edit-comment-actions-button =
|
||||||
|
.title = Actions
|
||||||
|
pdfjs-editor-edit-comment-close-button-label = Close
|
||||||
|
pdfjs-editor-edit-comment-close-button =
|
||||||
|
.title = Close
|
||||||
|
pdfjs-editor-edit-comment-actions-edit-button-label = Edit
|
||||||
|
pdfjs-editor-edit-comment-actions-delete-button-label = Delete
|
||||||
|
pdfjs-editor-edit-comment-manager-text-input =
|
||||||
|
.placeholder = Enter your comment
|
||||||
|
|
||||||
|
pdfjs-editor-edit-comment-manager-cancel-button = Cancel
|
||||||
|
pdfjs-editor-edit-comment-manager-save-button = Save
|
||||||
|
|
||||||
|
## Edit a comment button in the editor toolbar
|
||||||
|
|
||||||
|
pdfjs-editor-edit-comment-button =
|
||||||
|
.title = Edit comment
|
||||||
|
|||||||
@ -202,17 +202,32 @@ class AnnotationElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#updates ||= {
|
if (params.rect) {
|
||||||
rect: this.data.rect.slice(0),
|
this.#updates ||= {
|
||||||
};
|
rect: this.data.rect.slice(0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { rect } = params;
|
const { rect, popup: newPopup } = params;
|
||||||
|
|
||||||
if (rect) {
|
if (rect) {
|
||||||
this.#setRectEdited(rect);
|
this.#setRectEdited(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#popupElement?.popup.updateEdited(params);
|
let popup = this.#popupElement?.popup || this.popup;
|
||||||
|
if (!popup && newPopup.text) {
|
||||||
|
this._createPopup(newPopup);
|
||||||
|
popup = this.#popupElement.popup;
|
||||||
|
}
|
||||||
|
if (!popup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popup.updateEdited(params);
|
||||||
|
if (newPopup.deleted) {
|
||||||
|
popup.remove();
|
||||||
|
this.#popupElement = null;
|
||||||
|
this.popup = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetEdited() {
|
resetEdited() {
|
||||||
@ -598,18 +613,30 @@ class AnnotationElement {
|
|||||||
* annotations that do not have a Popup entry in the dictionary, but
|
* annotations that do not have a Popup entry in the dictionary, but
|
||||||
* are of a type that works with popups (such as Highlight annotations).
|
* are of a type that works with popups (such as Highlight annotations).
|
||||||
*
|
*
|
||||||
|
* @param {Object} [popupData] - The data for the popup, if any.
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
* @memberof AnnotationElement
|
* @memberof AnnotationElement
|
||||||
*/
|
*/
|
||||||
_createPopup() {
|
_createPopup(popupData = null) {
|
||||||
const { data } = this;
|
const { data } = this;
|
||||||
|
|
||||||
|
let contentsObj, modificationDate;
|
||||||
|
if (popupData) {
|
||||||
|
contentsObj = {
|
||||||
|
str: popupData.text,
|
||||||
|
};
|
||||||
|
modificationDate = popupData.date;
|
||||||
|
} else {
|
||||||
|
contentsObj = data.contentsObj;
|
||||||
|
modificationDate = data.modificationDate;
|
||||||
|
}
|
||||||
const popup = (this.#popupElement = new PopupAnnotationElement({
|
const popup = (this.#popupElement = new PopupAnnotationElement({
|
||||||
data: {
|
data: {
|
||||||
color: data.color,
|
color: data.color,
|
||||||
titleObj: data.titleObj,
|
titleObj: data.titleObj,
|
||||||
modificationDate: data.modificationDate,
|
modificationDate,
|
||||||
contentsObj: data.contentsObj,
|
contentsObj,
|
||||||
richText: data.richText,
|
richText: data.richText,
|
||||||
parentRect: data.rect,
|
parentRect: data.rect,
|
||||||
borderStyle: 0,
|
borderStyle: 0,
|
||||||
@ -617,12 +644,17 @@ class AnnotationElement {
|
|||||||
rotation: data.rotation,
|
rotation: data.rotation,
|
||||||
noRotate: true,
|
noRotate: true,
|
||||||
},
|
},
|
||||||
|
linkService: this.linkService,
|
||||||
parent: this.parent,
|
parent: this.parent,
|
||||||
elements: [this],
|
elements: [this],
|
||||||
}));
|
}));
|
||||||
this.parent.div.append(popup.render());
|
this.parent.div.append(popup.render());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasPopupElement() {
|
||||||
|
return !!(this.#popupElement || this.popup || this.data.popupRef);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the annotation's HTML element(s).
|
* Render the annotation's HTML element(s).
|
||||||
*
|
*
|
||||||
@ -2352,8 +2384,8 @@ class PopupElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEdited({ rect, popupContent, deleted }) {
|
updateEdited({ rect, popup, deleted }) {
|
||||||
if (deleted) {
|
if (deleted || popup?.deleted) {
|
||||||
this.remove();
|
this.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2365,8 +2397,9 @@ class PopupElement {
|
|||||||
if (rect) {
|
if (rect) {
|
||||||
this.#position = null;
|
this.#position = null;
|
||||||
}
|
}
|
||||||
if (popupContent) {
|
if (popup) {
|
||||||
this.#richText = this.#makePopupContent(popupContent);
|
this.#richText = this.#makePopupContent(popup.text);
|
||||||
|
this.#dateObj = PDFDateString.toDateObject(popup.date);
|
||||||
this.#contentsObj = null;
|
this.#contentsObj = null;
|
||||||
}
|
}
|
||||||
this.#popup?.remove();
|
this.#popup?.remove();
|
||||||
|
|||||||
@ -502,6 +502,9 @@ class PDFDateString {
|
|||||||
* @returns {Date|null}
|
* @returns {Date|null}
|
||||||
*/
|
*/
|
||||||
static toDateObject(input) {
|
static toDateObject(input) {
|
||||||
|
if (input instanceof Date) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
if (!input || typeof input !== "string") {
|
if (!input || typeof input !== "string") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
150
src/display/editor/comment.js
Normal file
150
src/display/editor/comment.js
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/* 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 } from "../display_utils.js";
|
||||||
|
|
||||||
|
class Comment {
|
||||||
|
#commentButton = null;
|
||||||
|
|
||||||
|
#commentWasFromKeyBoard = false;
|
||||||
|
|
||||||
|
#editor = null;
|
||||||
|
|
||||||
|
#initialText = null;
|
||||||
|
|
||||||
|
#text = null;
|
||||||
|
|
||||||
|
#date = null;
|
||||||
|
|
||||||
|
#deleted = false;
|
||||||
|
|
||||||
|
constructor(editor) {
|
||||||
|
this.#editor = editor;
|
||||||
|
this.toolbar = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.#editor._uiManager.hasCommentManager()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const comment = (this.#commentButton = document.createElement("button"));
|
||||||
|
comment.className = "comment";
|
||||||
|
comment.tabIndex = "0";
|
||||||
|
comment.setAttribute("data-l10n-id", "pdfjs-editor-edit-comment-button");
|
||||||
|
|
||||||
|
const signal = this.#editor._uiManager._signal;
|
||||||
|
comment.addEventListener("contextmenu", noContextMenu, { signal });
|
||||||
|
comment.addEventListener("pointerdown", event => event.stopPropagation(), {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClick = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.edit();
|
||||||
|
};
|
||||||
|
comment.addEventListener("click", onClick, { capture: true, signal });
|
||||||
|
comment.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
event => {
|
||||||
|
if (event.target === comment && event.key === "Enter") {
|
||||||
|
this.#commentWasFromKeyBoard = true;
|
||||||
|
onClick(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
const { bottom, left, right } = this.#editor.getClientDimensions();
|
||||||
|
const position = { top: bottom };
|
||||||
|
if (this.#editor._uiManager.direction === "ltr") {
|
||||||
|
position.right = right;
|
||||||
|
} else {
|
||||||
|
position.left = left;
|
||||||
|
}
|
||||||
|
this.#editor._uiManager.editComment(this.#editor, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
if (!this.#commentButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#commentButton.focus({ focusVisible: this.#commentWasFromKeyBoard });
|
||||||
|
this.#commentWasFromKeyBoard = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeleted() {
|
||||||
|
return this.#deleted || this.#text === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBeenEdited() {
|
||||||
|
return this.isDeleted() || this.#text !== this.#initialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return {
|
||||||
|
text: this.#text,
|
||||||
|
date: this.#date,
|
||||||
|
deleted: this.#deleted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the comment data.
|
||||||
|
*/
|
||||||
|
set data(text) {
|
||||||
|
if (text === null) {
|
||||||
|
this.#text = "";
|
||||||
|
this.#deleted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#text = text;
|
||||||
|
this.#date = new Date();
|
||||||
|
this.#deleted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialText(text) {
|
||||||
|
this.#initialText = text;
|
||||||
|
this.data = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(enabled = false) {
|
||||||
|
if (!this.#commentButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#commentButton.disabled = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
shown() {}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.#commentButton?.remove();
|
||||||
|
this.#commentButton = null;
|
||||||
|
this.#text = "";
|
||||||
|
this.#date = null;
|
||||||
|
this.#editor = null;
|
||||||
|
this.#commentWasFromKeyBoard = false;
|
||||||
|
this.#deleted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Comment };
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
} from "../../shared/util.js";
|
} from "../../shared/util.js";
|
||||||
import { noContextMenu, stopEvent } from "../display_utils.js";
|
import { noContextMenu, stopEvent } from "../display_utils.js";
|
||||||
import { AltText } from "./alt_text.js";
|
import { AltText } from "./alt_text.js";
|
||||||
|
import { Comment } from "./comment.js";
|
||||||
import { EditorToolbar } from "./toolbar.js";
|
import { EditorToolbar } from "./toolbar.js";
|
||||||
import { TouchManager } from "../touch_manager.js";
|
import { TouchManager } from "../touch_manager.js";
|
||||||
|
|
||||||
@ -52,6 +53,8 @@ class AnnotationEditor {
|
|||||||
|
|
||||||
#altText = null;
|
#altText = null;
|
||||||
|
|
||||||
|
#comment = null;
|
||||||
|
|
||||||
#disabled = false;
|
#disabled = false;
|
||||||
|
|
||||||
#dragPointerId = null;
|
#dragPointerId = null;
|
||||||
@ -1075,6 +1078,7 @@ class AnnotationEditor {
|
|||||||
}
|
}
|
||||||
this._editToolbar = new EditorToolbar(this);
|
this._editToolbar = new EditorToolbar(this);
|
||||||
this.div.append(this._editToolbar.render());
|
this.div.append(this._editToolbar.render());
|
||||||
|
this._editToolbar.addButton("comment", this.addCommentButton());
|
||||||
const { toolbarButtons } = this;
|
const { toolbarButtons } = this;
|
||||||
if (toolbarButtons) {
|
if (toolbarButtons) {
|
||||||
for (const [name, tool] of toolbarButtons) {
|
for (const [name, tool] of toolbarButtons) {
|
||||||
@ -1161,6 +1165,61 @@ class AnnotationEditor {
|
|||||||
return this.#altText?.hasData() ?? false;
|
return this.#altText?.hasData() ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addCommentButton() {
|
||||||
|
if (this.#comment) {
|
||||||
|
return this.#comment;
|
||||||
|
}
|
||||||
|
return (this.#comment = new Comment(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
get commentColor() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get comment() {
|
||||||
|
const comment = this.#comment;
|
||||||
|
return {
|
||||||
|
text: comment.data.text,
|
||||||
|
date: comment.data.date,
|
||||||
|
deleted: comment.isDeleted(),
|
||||||
|
color: this.commentColor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
set comment(text) {
|
||||||
|
if (!this.#comment) {
|
||||||
|
this.#comment = new Comment(this);
|
||||||
|
}
|
||||||
|
this.#comment.data = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommentData(text) {
|
||||||
|
if (!this.#comment) {
|
||||||
|
this.#comment = new Comment(this);
|
||||||
|
}
|
||||||
|
this.#comment.setInitialText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasEditedComment() {
|
||||||
|
return this.#comment?.hasBeenEdited();
|
||||||
|
}
|
||||||
|
|
||||||
|
async editComment() {
|
||||||
|
if (!this.#comment) {
|
||||||
|
this.#comment = new Comment(this);
|
||||||
|
}
|
||||||
|
this.#comment.edit();
|
||||||
|
}
|
||||||
|
|
||||||
|
addComment(serialized) {
|
||||||
|
if (this.hasEditedComment) {
|
||||||
|
serialized.popup = {
|
||||||
|
contents: this.comment.text,
|
||||||
|
deleted: this.comment.deleted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render this editor in a div.
|
* Render this editor in a div.
|
||||||
* @returns {HTMLDivElement | null}
|
* @returns {HTMLDivElement | null}
|
||||||
|
|||||||
@ -781,6 +781,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
rotation,
|
rotation,
|
||||||
id,
|
id,
|
||||||
popupRef,
|
popupRef,
|
||||||
|
contentsObj,
|
||||||
},
|
},
|
||||||
textContent,
|
textContent,
|
||||||
textPosition,
|
textPosition,
|
||||||
@ -807,6 +808,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
id,
|
id,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
popupRef,
|
popupRef,
|
||||||
|
comment: contentsObj?.str || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const editor = await super.deserialize(data, parent, uiManager);
|
const editor = await super.deserialize(data, parent, uiManager);
|
||||||
@ -814,6 +816,9 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
editor.#color = Util.makeHexColor(...data.color);
|
editor.#color = Util.makeHexColor(...data.color);
|
||||||
editor.#content = FreeTextEditor.#deserializeContent(data.value);
|
editor.#content = FreeTextEditor.#deserializeContent(data.value);
|
||||||
editor._initialData = initialData;
|
editor._initialData = initialData;
|
||||||
|
if (data.comment) {
|
||||||
|
editor.setCommentData(data.comment);
|
||||||
|
}
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
@ -846,6 +851,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
rotation: this.rotation,
|
rotation: this.rotation,
|
||||||
structTreeParentId: this._structTreeParentId,
|
structTreeParentId: this._structTreeParentId,
|
||||||
};
|
};
|
||||||
|
this.addComment(serialized);
|
||||||
|
|
||||||
if (isForCopying) {
|
if (isForCopying) {
|
||||||
// Don't add the id when copying because the pasted editor mustn't be
|
// Don't add the id when copying because the pasted editor mustn't be
|
||||||
@ -867,6 +873,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
const { value, fontSize, color, pageIndex } = this._initialData;
|
const { value, fontSize, color, pageIndex } = this._initialData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
this.hasEditedComment ||
|
||||||
this._hasBeenMoved ||
|
this._hasBeenMoved ||
|
||||||
serialized.value !== value ||
|
serialized.value !== value ||
|
||||||
serialized.fontSize !== fontSize ||
|
serialized.fontSize !== fontSize ||
|
||||||
@ -892,10 +899,13 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
||||||
annotation.updateEdited({
|
const params = {
|
||||||
rect: this.getRect(padding, padding),
|
rect: this.getRect(padding, padding),
|
||||||
popupContent: this.#content,
|
};
|
||||||
});
|
params.popup = this.hasEditedComment
|
||||||
|
? this.comment
|
||||||
|
: { text: this.#content };
|
||||||
|
annotation.updateEdited(params);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -151,6 +151,10 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get commentColor() {
|
||||||
|
return this.color;
|
||||||
|
}
|
||||||
|
|
||||||
static computeTelemetryFinalData(data) {
|
static computeTelemetryFinalData(data) {
|
||||||
// We want to know how many colors have been used.
|
// We want to know how many colors have been used.
|
||||||
return { numberOfColors: data.get("color").size };
|
return { numberOfColors: data.get("color").size };
|
||||||
@ -866,7 +870,16 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
let initialData = null;
|
let initialData = null;
|
||||||
if (data instanceof HighlightAnnotationElement) {
|
if (data instanceof HighlightAnnotationElement) {
|
||||||
const {
|
const {
|
||||||
data: { quadPoints, rect, rotation, id, color, opacity, popupRef },
|
data: {
|
||||||
|
quadPoints,
|
||||||
|
rect,
|
||||||
|
rotation,
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
popupRef,
|
||||||
|
contentsObj,
|
||||||
|
},
|
||||||
parent: {
|
parent: {
|
||||||
page: { pageNumber },
|
page: { pageNumber },
|
||||||
},
|
},
|
||||||
@ -884,6 +897,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
id,
|
id,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
popupRef,
|
popupRef,
|
||||||
|
comment: contentsObj?.str || null,
|
||||||
};
|
};
|
||||||
} else if (data instanceof InkAnnotationElement) {
|
} else if (data instanceof InkAnnotationElement) {
|
||||||
const {
|
const {
|
||||||
@ -895,6 +909,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
color,
|
color,
|
||||||
borderStyle: { rawWidth: thickness },
|
borderStyle: { rawWidth: thickness },
|
||||||
popupRef,
|
popupRef,
|
||||||
|
contentsObj,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
page: { pageNumber },
|
page: { pageNumber },
|
||||||
@ -913,6 +928,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
id,
|
id,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
popupRef,
|
popupRef,
|
||||||
|
comment: contentsObj?.str || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -925,6 +941,9 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
editor.#thickness = data.thickness;
|
editor.#thickness = data.thickness;
|
||||||
}
|
}
|
||||||
editor._initialData = initialData;
|
editor._initialData = initialData;
|
||||||
|
if (data.comment) {
|
||||||
|
editor.setCommentData(data.comment);
|
||||||
|
}
|
||||||
|
|
||||||
const [pageWidth, pageHeight] = editor.pageDimensions;
|
const [pageWidth, pageHeight] = editor.pageDimensions;
|
||||||
const [pageX, pageY] = editor.pageTranslation;
|
const [pageX, pageY] = editor.pageTranslation;
|
||||||
@ -1019,6 +1038,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
rotation: this.#getRotation(),
|
rotation: this.#getRotation(),
|
||||||
structTreeParentId: this._structTreeParentId,
|
structTreeParentId: this._structTreeParentId,
|
||||||
};
|
};
|
||||||
|
this.addComment(serialized);
|
||||||
|
|
||||||
if (this.annotationElementId && !this.#hasElementChanged(serialized)) {
|
if (this.annotationElementId && !this.#hasElementChanged(serialized)) {
|
||||||
return null;
|
return null;
|
||||||
@ -1030,14 +1050,20 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
#hasElementChanged(serialized) {
|
#hasElementChanged(serialized) {
|
||||||
const { color } = this._initialData;
|
const { color } = this._initialData;
|
||||||
return serialized.color.some((c, i) => c !== color[i]);
|
return (
|
||||||
|
this.hasEditedComment || serialized.color.some((c, i) => c !== color[i])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
renderAnnotationElement(annotation) {
|
renderAnnotationElement(annotation) {
|
||||||
annotation.updateEdited({
|
const params = {
|
||||||
rect: this.getRect(0, 0),
|
rect: this.getRect(0, 0),
|
||||||
});
|
};
|
||||||
|
if (this.hasEditedComment) {
|
||||||
|
params.popup = this.comment;
|
||||||
|
}
|
||||||
|
annotation.updateEdited(params);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,6 +150,7 @@ class InkEditor extends DrawingEditor {
|
|||||||
opacity,
|
opacity,
|
||||||
borderStyle: { rawWidth: thickness },
|
borderStyle: { rawWidth: thickness },
|
||||||
popupRef,
|
popupRef,
|
||||||
|
contentsObj,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
page: { pageNumber },
|
page: { pageNumber },
|
||||||
@ -169,11 +170,15 @@ class InkEditor extends DrawingEditor {
|
|||||||
id,
|
id,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
popupRef,
|
popupRef,
|
||||||
|
comment: contentsObj?.str || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = await super.deserialize(data, parent, uiManager);
|
const editor = await super.deserialize(data, parent, uiManager);
|
||||||
editor._initialData = initialData;
|
editor._initialData = initialData;
|
||||||
|
if (data.comment) {
|
||||||
|
editor.setCommentData(data.comment);
|
||||||
|
}
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
@ -260,6 +265,7 @@ class InkEditor extends DrawingEditor {
|
|||||||
rotation: this.rotation,
|
rotation: this.rotation,
|
||||||
structTreeParentId: this._structTreeParentId,
|
structTreeParentId: this._structTreeParentId,
|
||||||
};
|
};
|
||||||
|
this.addComment(serialized);
|
||||||
|
|
||||||
if (isForCopying) {
|
if (isForCopying) {
|
||||||
serialized.isCopy = true;
|
serialized.isCopy = true;
|
||||||
@ -277,6 +283,7 @@ class InkEditor extends DrawingEditor {
|
|||||||
#hasElementChanged(serialized) {
|
#hasElementChanged(serialized) {
|
||||||
const { color, thickness, opacity, pageIndex } = this._initialData;
|
const { color, thickness, opacity, pageIndex } = this._initialData;
|
||||||
return (
|
return (
|
||||||
|
this.hasEditedComment ||
|
||||||
this._hasBeenMoved ||
|
this._hasBeenMoved ||
|
||||||
this._hasBeenResized ||
|
this._hasBeenResized ||
|
||||||
serialized.color.some((c, i) => c !== color[i]) ||
|
serialized.color.some((c, i) => c !== color[i]) ||
|
||||||
@ -289,11 +296,15 @@ class InkEditor extends DrawingEditor {
|
|||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
renderAnnotationElement(annotation) {
|
renderAnnotationElement(annotation) {
|
||||||
const { points, rect } = this.serializeDraw(/* isForCopying = */ false);
|
const { points, rect } = this.serializeDraw(/* isForCopying = */ false);
|
||||||
annotation.updateEdited({
|
const params = {
|
||||||
rect,
|
rect,
|
||||||
thickness: this._drawingOptions["stroke-width"],
|
thickness: this._drawingOptions["stroke-width"],
|
||||||
points,
|
points,
|
||||||
});
|
};
|
||||||
|
if (this.hasEditedComment) {
|
||||||
|
params.popup = this.comment;
|
||||||
|
}
|
||||||
|
annotation.updateEdited(params);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -389,6 +389,7 @@ class SignatureEditor extends DrawingEditor {
|
|||||||
rotation: this.rotation,
|
rotation: this.rotation,
|
||||||
structTreeParentId: this._structTreeParentId,
|
structTreeParentId: this._structTreeParentId,
|
||||||
};
|
};
|
||||||
|
this.addComment(serialized);
|
||||||
if (isForCopying) {
|
if (isForCopying) {
|
||||||
serialized.paths = { lines, points };
|
serialized.paths = { lines, points };
|
||||||
serialized.uuid = this.#signatureUUID;
|
serialized.uuid = this.#signatureUUID;
|
||||||
|
|||||||
@ -750,7 +750,7 @@ class StampEditor extends AnnotationEditor {
|
|||||||
let missingCanvas = false;
|
let missingCanvas = false;
|
||||||
if (data instanceof StampAnnotationElement) {
|
if (data instanceof StampAnnotationElement) {
|
||||||
const {
|
const {
|
||||||
data: { rect, rotation, id, structParent, popupRef },
|
data: { rect, rotation, id, structParent, popupRef, contentsObj },
|
||||||
container,
|
container,
|
||||||
parent: {
|
parent: {
|
||||||
page: { pageNumber },
|
page: { pageNumber },
|
||||||
@ -794,6 +794,7 @@ class StampEditor extends AnnotationEditor {
|
|||||||
isSvg: false,
|
isSvg: false,
|
||||||
structParent,
|
structParent,
|
||||||
popupRef,
|
popupRef,
|
||||||
|
comment: contentsObj?.str || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const editor = await super.deserialize(data, parent, uiManager);
|
const editor = await super.deserialize(data, parent, uiManager);
|
||||||
@ -820,6 +821,9 @@ class StampEditor extends AnnotationEditor {
|
|||||||
editor.altTextData = accessibilityData;
|
editor.altTextData = accessibilityData;
|
||||||
}
|
}
|
||||||
editor._initialData = initialData;
|
editor._initialData = initialData;
|
||||||
|
if (data.comment) {
|
||||||
|
editor.setCommentData(data.comment);
|
||||||
|
}
|
||||||
// No need to be add in the undo stack if the editor is created from an
|
// No need to be add in the undo stack if the editor is created from an
|
||||||
// existing one.
|
// existing one.
|
||||||
editor.#hasBeenAddedInUndoStack = !!initialData;
|
editor.#hasBeenAddedInUndoStack = !!initialData;
|
||||||
@ -846,6 +850,7 @@ class StampEditor extends AnnotationEditor {
|
|||||||
isSvg: this.#isSvg,
|
isSvg: this.#isSvg,
|
||||||
structTreeParentId: this._structTreeParentId,
|
structTreeParentId: this._structTreeParentId,
|
||||||
};
|
};
|
||||||
|
this.addComment(serialized);
|
||||||
|
|
||||||
if (isForCopying) {
|
if (isForCopying) {
|
||||||
// We don't know what's the final destination (this pdf or another one)
|
// We don't know what's the final destination (this pdf or another one)
|
||||||
@ -914,6 +919,7 @@ class StampEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isSame:
|
isSame:
|
||||||
|
!this.hasEditedComment &&
|
||||||
!this._hasBeenMoved &&
|
!this._hasBeenMoved &&
|
||||||
!this._hasBeenResized &&
|
!this._hasBeenResized &&
|
||||||
isSamePageIndex &&
|
isSamePageIndex &&
|
||||||
@ -924,9 +930,13 @@ class StampEditor extends AnnotationEditor {
|
|||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
renderAnnotationElement(annotation) {
|
renderAnnotationElement(annotation) {
|
||||||
annotation.updateEdited({
|
const params = {
|
||||||
rect: this.getRect(0, 0),
|
rect: this.getRect(0, 0),
|
||||||
});
|
};
|
||||||
|
if (this.hasEditedComment) {
|
||||||
|
params.popup = this.comment;
|
||||||
|
}
|
||||||
|
annotation.updateEdited(params);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ class EditorToolbar {
|
|||||||
|
|
||||||
#altText = null;
|
#altText = null;
|
||||||
|
|
||||||
|
#comment = null;
|
||||||
|
|
||||||
#signatureDescriptionButton = null;
|
#signatureDescriptionButton = null;
|
||||||
|
|
||||||
static #l10nRemove = null;
|
static #l10nRemove = null;
|
||||||
@ -114,6 +116,7 @@ class EditorToolbar {
|
|||||||
show() {
|
show() {
|
||||||
this.#toolbar.classList.remove("hidden");
|
this.#toolbar.classList.remove("hidden");
|
||||||
this.#altText?.shown();
|
this.#altText?.shown();
|
||||||
|
this.#comment?.shown();
|
||||||
}
|
}
|
||||||
|
|
||||||
addDeleteButton() {
|
addDeleteButton() {
|
||||||
@ -147,7 +150,24 @@ class EditorToolbar {
|
|||||||
this.#altText = altText;
|
this.#altText = altText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addComment(comment) {
|
||||||
|
if (this.#comment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const button = comment.render();
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#addListenersToElement(button);
|
||||||
|
this.#buttons.prepend(button, this.#divider);
|
||||||
|
this.#comment = comment;
|
||||||
|
comment.toolbar = this;
|
||||||
|
}
|
||||||
|
|
||||||
addColorPicker(colorPicker) {
|
addColorPicker(colorPicker) {
|
||||||
|
if (this.#colorPicker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#colorPicker = colorPicker;
|
this.#colorPicker = colorPicker;
|
||||||
const button = colorPicker.renderButton();
|
const button = colorPicker.renderButton();
|
||||||
this.#addListenersToElement(button);
|
this.#addListenersToElement(button);
|
||||||
@ -175,6 +195,9 @@ class EditorToolbar {
|
|||||||
case "delete":
|
case "delete":
|
||||||
this.addDeleteButton();
|
this.addDeleteButton();
|
||||||
break;
|
break;
|
||||||
|
case "comment":
|
||||||
|
this.addComment(tool);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -597,6 +597,8 @@ class AnnotationEditorUIManager {
|
|||||||
|
|
||||||
#commandManager = new CommandManager();
|
#commandManager = new CommandManager();
|
||||||
|
|
||||||
|
#commentManager = null;
|
||||||
|
|
||||||
#copyPasteAC = null;
|
#copyPasteAC = null;
|
||||||
|
|
||||||
#currentDrawingSession = null;
|
#currentDrawingSession = null;
|
||||||
@ -822,6 +824,7 @@ class AnnotationEditorUIManager {
|
|||||||
viewer,
|
viewer,
|
||||||
viewerAlert,
|
viewerAlert,
|
||||||
altTextManager,
|
altTextManager,
|
||||||
|
commentManager,
|
||||||
signatureManager,
|
signatureManager,
|
||||||
eventBus,
|
eventBus,
|
||||||
pdfDocument,
|
pdfDocument,
|
||||||
@ -839,6 +842,7 @@ class AnnotationEditorUIManager {
|
|||||||
this.#viewer = viewer;
|
this.#viewer = viewer;
|
||||||
this.#viewerAlert = viewerAlert;
|
this.#viewerAlert = viewerAlert;
|
||||||
this.#altTextManager = altTextManager;
|
this.#altTextManager = altTextManager;
|
||||||
|
this.#commentManager = commentManager;
|
||||||
this.#signatureManager = signatureManager;
|
this.#signatureManager = signatureManager;
|
||||||
this._eventBus = eventBus;
|
this._eventBus = eventBus;
|
||||||
eventBus._on("editingaction", this.onEditingAction.bind(this), { signal });
|
eventBus._on("editingaction", this.onEditingAction.bind(this), { signal });
|
||||||
@ -902,6 +906,7 @@ class AnnotationEditorUIManager {
|
|||||||
this.#selectedEditors.clear();
|
this.#selectedEditors.clear();
|
||||||
this.#commandManager.destroy();
|
this.#commandManager.destroy();
|
||||||
this.#altTextManager?.destroy();
|
this.#altTextManager?.destroy();
|
||||||
|
this.#commentManager?.destroy();
|
||||||
this.#signatureManager?.destroy();
|
this.#signatureManager?.destroy();
|
||||||
this.#highlightToolbar?.hide();
|
this.#highlightToolbar?.hide();
|
||||||
this.#highlightToolbar = null;
|
this.#highlightToolbar = null;
|
||||||
@ -1003,6 +1008,14 @@ class AnnotationEditorUIManager {
|
|||||||
this.#altTextManager?.editAltText(this, editor, firstTime);
|
this.#altTextManager?.editAltText(this, editor, firstTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasCommentManager() {
|
||||||
|
return !!this.#commentManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
editComment(editor, position) {
|
||||||
|
this.#commentManager?.open(this, editor, position);
|
||||||
|
}
|
||||||
|
|
||||||
getSignature(editor) {
|
getSignature(editor) {
|
||||||
this.#signatureManager?.getSignature({ uiManager: this, editor });
|
this.#signatureManager?.getSignature({ uiManager: this, editor });
|
||||||
}
|
}
|
||||||
@ -1686,12 +1699,15 @@ class AnnotationEditorUIManager {
|
|||||||
* keyboard action.
|
* keyboard action.
|
||||||
* @param {boolean} [mustEnterInEditMode] - true if the editor must enter in
|
* @param {boolean} [mustEnterInEditMode] - true if the editor must enter in
|
||||||
* edit mode.
|
* edit mode.
|
||||||
|
* @param {boolean} [editComment] - true if the mode change is due to a
|
||||||
|
* comment edit.
|
||||||
*/
|
*/
|
||||||
async updateMode(
|
async updateMode(
|
||||||
mode,
|
mode,
|
||||||
editId = null,
|
editId = null,
|
||||||
isFromKeyboard = false,
|
isFromKeyboard = false,
|
||||||
mustEnterInEditMode = false
|
mustEnterInEditMode = false,
|
||||||
|
editComment = false
|
||||||
) {
|
) {
|
||||||
if (this.#mode === mode) {
|
if (this.#mode === mode) {
|
||||||
return;
|
return;
|
||||||
@ -1739,7 +1755,9 @@ class AnnotationEditorUIManager {
|
|||||||
for (const editor of this.#allEditors.values()) {
|
for (const editor of this.#allEditors.values()) {
|
||||||
if (editor.annotationElementId === editId || editor.id === editId) {
|
if (editor.annotationElementId === editId || editor.id === editId) {
|
||||||
this.setSelected(editor);
|
this.setSelected(editor);
|
||||||
if (mustEnterInEditMode) {
|
if (editComment) {
|
||||||
|
editor.editComment();
|
||||||
|
} else if (mustEnterInEditMode) {
|
||||||
editor.enterInEditMode();
|
editor.enterInEditMode();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -55,6 +55,7 @@ import {
|
|||||||
fetchData,
|
fetchData,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isPdfFile,
|
isPdfFile,
|
||||||
@ -106,6 +107,7 @@ globalThis.pdfjsLib = {
|
|||||||
getDocument,
|
getDocument,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getUuid,
|
getUuid,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
@ -160,6 +162,7 @@ export {
|
|||||||
getDocument,
|
getDocument,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getUuid,
|
getUuid,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
|
|||||||
@ -76,6 +76,7 @@ const AnnotationEditorType = {
|
|||||||
STAMP: 13,
|
STAMP: 13,
|
||||||
INK: 15,
|
INK: 15,
|
||||||
SIGNATURE: 101,
|
SIGNATURE: 101,
|
||||||
|
COMMENT: 102,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnnotationEditorParamsType = {
|
const AnnotationEditorParamsType = {
|
||||||
|
|||||||
@ -295,6 +295,8 @@ describe("display_utils", function () {
|
|||||||
expect(result).toEqual(expectation);
|
expect(result).toEqual(expectation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const now = new Date();
|
||||||
|
expect(PDFDateString.toDateObject(now)).toEqual(now);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import {
|
|||||||
fetchData,
|
fetchData,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isPdfFile,
|
isPdfFile,
|
||||||
@ -90,6 +91,7 @@ const expectedAPI = Object.freeze({
|
|||||||
getDocument,
|
getDocument,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getUuid,
|
getUuid,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
|
|||||||
@ -16,8 +16,10 @@
|
|||||||
@import url(draw_layer_builder.css);
|
@import url(draw_layer_builder.css);
|
||||||
@import url(toggle_button.css);
|
@import url(toggle_button.css);
|
||||||
@import url(signature_manager.css);
|
@import url(signature_manager.css);
|
||||||
|
@import url(comment_manager.css);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--editor-toolbar-vert-offset: 6px;
|
||||||
--outline-width: 2px;
|
--outline-width: 2px;
|
||||||
--outline-color: #0060df;
|
--outline-color: #0060df;
|
||||||
--outline-around-width: 1px;
|
--outline-around-width: 1px;
|
||||||
@ -228,6 +230,7 @@
|
|||||||
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
|
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
|
||||||
--editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33);
|
--editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33);
|
||||||
--editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg);
|
--editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg);
|
||||||
|
--editor-toolbar-comment-edit-image: url(images/comment-editButton.svg);
|
||||||
--editor-toolbar-fg-color: light-dark(#2e2e56, #fbfbfe);
|
--editor-toolbar-fg-color: light-dark(#2e2e56, #fbfbfe);
|
||||||
--editor-toolbar-border-color: #8f8f9d;
|
--editor-toolbar-border-color: #8f8f9d;
|
||||||
--editor-toolbar-hover-border-color: var(--editor-toolbar-border-color);
|
--editor-toolbar-hover-border-color: var(--editor-toolbar-border-color);
|
||||||
@ -236,7 +239,6 @@
|
|||||||
--editor-toolbar-hover-outline: none;
|
--editor-toolbar-hover-outline: none;
|
||||||
--editor-toolbar-focus-outline-color: light-dark(#0060df, #0df);
|
--editor-toolbar-focus-outline-color: light-dark(#0060df, #0df);
|
||||||
--editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2);
|
--editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2);
|
||||||
--editor-toolbar-vert-offset: 6px;
|
|
||||||
--editor-toolbar-height: 28px;
|
--editor-toolbar-height: 28px;
|
||||||
--editor-toolbar-padding: 2px;
|
--editor-toolbar-padding: 2px;
|
||||||
--alt-text-done-color: light-dark(#2ac3a2, #54ffbd);
|
--alt-text-done-color: light-dark(#2ac3a2, #54ffbd);
|
||||||
@ -489,6 +491,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
width: var(--editor-toolbar-height);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
mask-image: var(--editor-toolbar-comment-edit-image);
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--editor-toolbar-fg-color);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
color-scheme: only light;
|
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>");
|
--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_editButton.svg);
|
||||||
--input-focus-border-color: Highlight;
|
--input-focus-border-color: Highlight;
|
||||||
--input-focus-outline: 1px solid Canvas;
|
--input-focus-outline: 1px solid Canvas;
|
||||||
--input-unfocused-border-color: transparent;
|
--input-unfocused-border-color: transparent;
|
||||||
|
|||||||
@ -71,6 +71,7 @@ import { LinkTarget, PDFLinkService } from "./pdf_link_service.js";
|
|||||||
import { AltTextManager } from "web-alt_text_manager";
|
import { AltTextManager } from "web-alt_text_manager";
|
||||||
import { AnnotationEditorParams } from "web-annotation_editor_params";
|
import { AnnotationEditorParams } from "web-annotation_editor_params";
|
||||||
import { CaretBrowsingMode } from "./caret_browsing.js";
|
import { CaretBrowsingMode } from "./caret_browsing.js";
|
||||||
|
import { CommentManager } from "./comment_manager.js";
|
||||||
import { DownloadManager } from "web-download_manager";
|
import { DownloadManager } from "web-download_manager";
|
||||||
import { EditorUndoBar } from "./editor_undo_bar.js";
|
import { EditorUndoBar } from "./editor_undo_bar.js";
|
||||||
import { OverlayManager } from "./overlay_manager.js";
|
import { OverlayManager } from "./overlay_manager.js";
|
||||||
@ -484,6 +485,10 @@ const PDFViewerApplication = {
|
|||||||
eventBus
|
eventBus
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
const commentManager =
|
||||||
|
AppOptions.get("enableComment") && appConfig.editCommentDialog
|
||||||
|
? new CommentManager(appConfig.editCommentDialog, overlayManager)
|
||||||
|
: null;
|
||||||
|
|
||||||
const enableHWA = AppOptions.get("enableHWA"),
|
const enableHWA = AppOptions.get("enableHWA"),
|
||||||
maxCanvasPixels = AppOptions.get("maxCanvasPixels"),
|
maxCanvasPixels = AppOptions.get("maxCanvasPixels"),
|
||||||
@ -498,6 +503,7 @@ const PDFViewerApplication = {
|
|||||||
linkService,
|
linkService,
|
||||||
downloadManager,
|
downloadManager,
|
||||||
altTextManager,
|
altTextManager,
|
||||||
|
commentManager,
|
||||||
signatureManager,
|
signatureManager,
|
||||||
editorUndoBar: this.editorUndoBar,
|
editorUndoBar: this.editorUndoBar,
|
||||||
findController,
|
findController,
|
||||||
|
|||||||
@ -218,6 +218,11 @@ const defaultOptions = {
|
|||||||
value: true,
|
value: true,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
enableComment: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
enableDetailCanvas: {
|
enableDetailCanvas: {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
value: true,
|
value: true,
|
||||||
|
|||||||
288
web/comment_manager.css
Normal file
288
web/comment_manager.css
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#commentManagerDialog {
|
||||||
|
--comment-actions-button-icon: url(images/comment-actionsButton.svg);
|
||||||
|
--comment-close-button-icon: url(images/comment-closeButton.svg);
|
||||||
|
|
||||||
|
--default-dialog-bg-color: #ffff98;
|
||||||
|
--dialog-base-color: var(--default-dialog-bg-color);
|
||||||
|
--dialog-bg-color: color-mix(in srgb, var(--dialog-base-color), white 30%);
|
||||||
|
--dialog-border-color: var(--dialog-base-color);
|
||||||
|
|
||||||
|
--menuitem-bg-color: transparent;
|
||||||
|
--menuitem-fg-color: black;
|
||||||
|
--menuitem-hover-bg-color: #3383e7;
|
||||||
|
--menuitem-hover-fg-color: white;
|
||||||
|
|
||||||
|
--comment-text-input-bg: white;
|
||||||
|
--comment-text-input-fg: black;
|
||||||
|
--comment-text-input-border: #0060df;
|
||||||
|
--comment-focus-outline-color: #0060df;
|
||||||
|
|
||||||
|
--hover-filter: brightness(0.9);
|
||||||
|
--text-primary-color: #15141a;
|
||||||
|
|
||||||
|
--button-secondary-bg-color: #f0f0f4;
|
||||||
|
--button-secondary-active-bg-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--button-secondary-bg-color),
|
||||||
|
black 14%
|
||||||
|
);
|
||||||
|
--button-secondary-hover-bg-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--button-secondary-bg-color),
|
||||||
|
black 7%
|
||||||
|
);
|
||||||
|
|
||||||
|
--button-primary-bg-color: #0060df;
|
||||||
|
--button-primary-fg-color: #fbfbfe;
|
||||||
|
--button-primary-active-bg-color: #0050c0;
|
||||||
|
--button-primary-hover-bg-color: #0250bb;
|
||||||
|
|
||||||
|
--menu-bg-color: rgb(253 250 244);
|
||||||
|
--menu-button-border-color: transparent;
|
||||||
|
--menu-button-focus-outline-color: var(--comment-text-input-border);
|
||||||
|
|
||||||
|
@media screen and (forced-colors: active) {
|
||||||
|
--hover-filter: none;
|
||||||
|
--text-primary-color: CanvasText;
|
||||||
|
--button-secondary-bg-color: HighlightText;
|
||||||
|
--button-secondary-active-bg-color: HighlightText;
|
||||||
|
--button-secondary-hover-bg-color: HighlightText;
|
||||||
|
--button-primary-bg-color: ButtonText;
|
||||||
|
--button-primary-fg-color: HighlightText;
|
||||||
|
--button-primary-active-bg-color: SelectedItem;
|
||||||
|
--button-primary-hover-bg-color: SelectedItem;
|
||||||
|
|
||||||
|
--menu-button-border-color: Canvas;
|
||||||
|
--menu-button-focus-outline-color: CanvasText;
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 308px;
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
overflow: visible;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--dialog-border-color);
|
||||||
|
background: var(--dialog-bg-color);
|
||||||
|
box-shadow: 0 2px 14px 0 rgb(58 57 68 / 0.2);
|
||||||
|
|
||||||
|
.mainContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--comment-focus-outline-color);
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#commentManagerToolbar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
|
cursor: move;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
color-scheme: light;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#commentActionsButton::before {
|
||||||
|
mask-image: var(--comment-actions-button-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
&#commentCloseButton::before {
|
||||||
|
mask-image: var(--comment-close-button-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 90px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 5px 6px;
|
||||||
|
cursor: auto;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: -6.5px;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 0.5px solid #b4b4b6;
|
||||||
|
background-color: var(--menu-bg-color);
|
||||||
|
box-shadow:
|
||||||
|
1px -1px 0 0 #fff inset,
|
||||||
|
-1px 1px 0 0 #fff inset,
|
||||||
|
-1px -1px 0 0 #fff inset,
|
||||||
|
1px 1px 0 0 #fff inset,
|
||||||
|
0 0 15px 0 rgb(0 0 0 / 0.25);
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--menu-bg-color);
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
border: 2px solid var(--menu-button-border-color);
|
||||||
|
color: var(--menuitem-fg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--menuitem-hover-bg-color);
|
||||||
|
color: var(--menuitem-hover-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:focus-visible, :focus) {
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid var(--menu-button-focus-outline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
align-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: min-content;
|
||||||
|
padding-inline: 8px;
|
||||||
|
color: inherit;
|
||||||
|
text-align: start;
|
||||||
|
font: menu;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#commentManagerTextInput {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 132px;
|
||||||
|
resize: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--comment-text-input-border);
|
||||||
|
background-color: var(--comment-text-input-bg);
|
||||||
|
color: var(--comment-text-input-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#commentManagerTextView {
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
resize: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--comment-text-input-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogButtonsGroup {
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
#commentManagerSaveButton:disabled {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--button-primary-disabled-bg-color),
|
||||||
|
transparent 50%
|
||||||
|
);
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--button-primary-disabled-border-color),
|
||||||
|
transparent 50%
|
||||||
|
);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotationLayer {
|
||||||
|
--comment-inline-button-bg: #e0e0e6;
|
||||||
|
--comment-inline-button-fg: black;
|
||||||
|
--comment-inline-button-border-color: #8f8f9d;
|
||||||
|
--comment-button-dim: 24px;
|
||||||
|
--comment-button-offset: calc(var(--comment-button-dim) / 2);
|
||||||
|
|
||||||
|
.annotationCommentButton {
|
||||||
|
position: absolute;
|
||||||
|
width: var(--comment-button-dim);
|
||||||
|
height: var(--comment-button-dim);
|
||||||
|
background-color: var(--comment-inline-button-bg);
|
||||||
|
cursor: auto;
|
||||||
|
z-index: 1;
|
||||||
|
border: 1px solid var(--comment-inline-button-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
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-inline-button-fg);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
375
web/comment_manager.js
Normal file
375
web/comment_manager.js
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
/* 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 { getRGB, noContextMenu, shadow, stopEvent } from "pdfjs-lib";
|
||||||
|
|
||||||
|
class CommentManager {
|
||||||
|
#actions;
|
||||||
|
|
||||||
|
#currentEditor;
|
||||||
|
|
||||||
|
#dialog;
|
||||||
|
|
||||||
|
#deleteMenuItem;
|
||||||
|
|
||||||
|
#editMenuItem;
|
||||||
|
|
||||||
|
#overlayManager;
|
||||||
|
|
||||||
|
#previousText = "";
|
||||||
|
|
||||||
|
#commentText = "";
|
||||||
|
|
||||||
|
#menu;
|
||||||
|
|
||||||
|
#textInput;
|
||||||
|
|
||||||
|
#textView;
|
||||||
|
|
||||||
|
#saveButton;
|
||||||
|
|
||||||
|
#uiManager;
|
||||||
|
|
||||||
|
#prevDragX = Infinity;
|
||||||
|
|
||||||
|
#prevDragY = Infinity;
|
||||||
|
|
||||||
|
#dialogX = 0;
|
||||||
|
|
||||||
|
#dialogY = 0;
|
||||||
|
|
||||||
|
#menuAC = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
dialog,
|
||||||
|
toolbar,
|
||||||
|
actions,
|
||||||
|
menu,
|
||||||
|
editMenuItem,
|
||||||
|
deleteMenuItem,
|
||||||
|
closeButton,
|
||||||
|
textInput,
|
||||||
|
textView,
|
||||||
|
cancelButton,
|
||||||
|
saveButton,
|
||||||
|
},
|
||||||
|
overlayManager
|
||||||
|
) {
|
||||||
|
this.#actions = actions;
|
||||||
|
this.#dialog = dialog;
|
||||||
|
this.#editMenuItem = editMenuItem;
|
||||||
|
this.#deleteMenuItem = deleteMenuItem;
|
||||||
|
this.#menu = menu;
|
||||||
|
this.#textInput = textInput;
|
||||||
|
this.#textView = textView;
|
||||||
|
this.#overlayManager = overlayManager;
|
||||||
|
this.#saveButton = saveButton;
|
||||||
|
|
||||||
|
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);
|
||||||
|
closeButton.addEventListener("click", finishBound);
|
||||||
|
saveButton.addEventListener("click", this.#save.bind(this));
|
||||||
|
|
||||||
|
this.#makeMenu();
|
||||||
|
editMenuItem.addEventListener("click", () => {
|
||||||
|
this.#closeMenu();
|
||||||
|
this.#edit();
|
||||||
|
});
|
||||||
|
deleteMenuItem.addEventListener("click", () => {
|
||||||
|
this.#closeMenu();
|
||||||
|
this.#textInput.value = "";
|
||||||
|
this.#currentEditor.comment = null;
|
||||||
|
this.#save();
|
||||||
|
});
|
||||||
|
|
||||||
|
textInput.addEventListener("input", () => {
|
||||||
|
saveButton.disabled = textInput.value === this.#previousText;
|
||||||
|
this.#deleteMenuItem.disabled = textInput.value === "";
|
||||||
|
});
|
||||||
|
textView.addEventListener("dblclick", () => {
|
||||||
|
this.#edit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make the dialog draggable.
|
||||||
|
let pointerMoveAC;
|
||||||
|
const cancelDrag = () => {
|
||||||
|
this.#prevDragX = this.#prevDragY = Infinity;
|
||||||
|
this.#dialog.classList.remove("dragging");
|
||||||
|
pointerMoveAC?.abort();
|
||||||
|
pointerMoveAC = null;
|
||||||
|
};
|
||||||
|
toolbar.addEventListener("pointerdown", e => {
|
||||||
|
const { target, clientX, clientY } = e;
|
||||||
|
if (target !== toolbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#closeMenu();
|
||||||
|
this.#prevDragX = clientX;
|
||||||
|
this.#prevDragY = clientY;
|
||||||
|
pointerMoveAC = new AbortController();
|
||||||
|
const { signal } = pointerMoveAC;
|
||||||
|
dialog.classList.add("dragging");
|
||||||
|
window.addEventListener(
|
||||||
|
"pointermove",
|
||||||
|
ev => {
|
||||||
|
if (this.#prevDragX !== Infinity) {
|
||||||
|
const { clientX: x, clientY: y } = ev;
|
||||||
|
this.#setPosition(
|
||||||
|
this.#dialogX + x - this.#prevDragX,
|
||||||
|
this.#dialogY + y - this.#prevDragY
|
||||||
|
);
|
||||||
|
this.#prevDragX = x;
|
||||||
|
this.#prevDragY = y;
|
||||||
|
stopEvent(ev);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
window.addEventListener("blur", cancelDrag, { signal });
|
||||||
|
stopEvent(e);
|
||||||
|
});
|
||||||
|
dialog.addEventListener("pointerup", e => {
|
||||||
|
if (this.#prevDragX === Infinity) {
|
||||||
|
return; // Not dragging.
|
||||||
|
}
|
||||||
|
cancelDrag();
|
||||||
|
stopEvent(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
overlayManager.register(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
#closeMenu() {
|
||||||
|
if (!this.#menuAC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const menu = this.#menu;
|
||||||
|
menu.classList.toggle("hidden", true);
|
||||||
|
this.#actions.ariaExpanded = "false";
|
||||||
|
this.#menuAC.abort();
|
||||||
|
this.#menuAC = null;
|
||||||
|
if (menu.contains(document.activeElement)) {
|
||||||
|
// If the menu is closed while focused, focus the actions button.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.#dialog.contains(document.activeElement)) {
|
||||||
|
this.#actions.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#makeMenu() {
|
||||||
|
this.#actions.addEventListener("click", e => {
|
||||||
|
const closeMenu = this.#closeMenu.bind(this);
|
||||||
|
if (this.#menuAC) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = this.#menu;
|
||||||
|
menu.classList.toggle("hidden", false);
|
||||||
|
this.#actions.ariaExpanded = "true";
|
||||||
|
this.#menuAC = new AbortController();
|
||||||
|
const { signal } = this.#menuAC;
|
||||||
|
window.addEventListener(
|
||||||
|
"pointerdown",
|
||||||
|
({ target }) => {
|
||||||
|
if (target !== this.#actions && !menu.contains(target)) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
window.addEventListener("blur", closeMenu, { signal });
|
||||||
|
this.#actions.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
({ key }) => {
|
||||||
|
switch (key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
case "Home":
|
||||||
|
menu.firstElementChild.focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
case "End":
|
||||||
|
menu.lastElementChild.focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
closeMenu();
|
||||||
|
stopEvent(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyboardListener = e => {
|
||||||
|
const { key, target } = e;
|
||||||
|
const menu = this.#menu;
|
||||||
|
switch (key) {
|
||||||
|
case "Escape":
|
||||||
|
this.#closeMenu();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
case "Tab":
|
||||||
|
(target.nextElementSibling || menu.firstElementChild).focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
case "ShiftTab":
|
||||||
|
(target.previousElementSibling || menu.lastElementChild).focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
menu.firstElementChild.focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
menu.lastElementChild.focus();
|
||||||
|
stopEvent(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const menuItem of this.#menu.children) {
|
||||||
|
if (menuItem.classList.contains("hidden")) {
|
||||||
|
continue; // Skip hidden menu items.
|
||||||
|
}
|
||||||
|
menuItem.addEventListener("keydown", keyboardListener);
|
||||||
|
menuItem.addEventListener("contextmenu", noContextMenu);
|
||||||
|
}
|
||||||
|
this.#menu.addEventListener("contextmenu", noContextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
async open(uiManager, editor, position) {
|
||||||
|
if (editor) {
|
||||||
|
this.#uiManager = uiManager;
|
||||||
|
this.#currentEditor = editor;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
comment: { text, color },
|
||||||
|
} = editor;
|
||||||
|
this.#dialog.style.setProperty(
|
||||||
|
"--dialog-base-color",
|
||||||
|
this.#lightenColor(color) || "var(--default-dialog-bg-color)"
|
||||||
|
);
|
||||||
|
this.#commentText = text || "";
|
||||||
|
if (!text) {
|
||||||
|
this.#edit();
|
||||||
|
} else {
|
||||||
|
this.#setText(text);
|
||||||
|
this.#textInput.classList.toggle("hidden", true);
|
||||||
|
this.#textView.classList.toggle("hidden", false);
|
||||||
|
this.#editMenuItem.disabled = this.#deleteMenuItem.disabled = false;
|
||||||
|
}
|
||||||
|
this.#uiManager.removeEditListeners();
|
||||||
|
this.#saveButton.disabled = true;
|
||||||
|
|
||||||
|
const x =
|
||||||
|
position.right !== undefined
|
||||||
|
? position.right - this._dialogWidth
|
||||||
|
: position.left;
|
||||||
|
const y = position.top;
|
||||||
|
this.#setPosition(x, y, /* isInitial */ true);
|
||||||
|
|
||||||
|
await this.#overlayManager.open(this.#dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #save() {
|
||||||
|
this.#currentEditor.comment = this.#textInput.value;
|
||||||
|
this.#finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
get _dialogWidth() {
|
||||||
|
const dialog = this.#dialog;
|
||||||
|
const { style } = dialog;
|
||||||
|
style.opacity = "0";
|
||||||
|
style.display = "block";
|
||||||
|
const width = dialog.getBoundingClientRect().width;
|
||||||
|
style.opacity = style.display = "";
|
||||||
|
return shadow(this, "_dialogWidth", width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#lightenColor(color) {
|
||||||
|
if (!color) {
|
||||||
|
return null; // No color provided.
|
||||||
|
}
|
||||||
|
const [r, g, b] = getRGB(color);
|
||||||
|
const gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
const ratio = gray < 0.9 ? Math.round((0.9 - gray) * 100) : 0;
|
||||||
|
return `color-mix(in srgb, ${ratio}% white, ${color})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setText(text) {
|
||||||
|
const textView = this.#textView;
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = line;
|
||||||
|
textView.append(span, document.createElement("br"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setPosition(x, y, isInitial = false) {
|
||||||
|
this.#dialogX = x;
|
||||||
|
this.#dialogY = y;
|
||||||
|
const { style } = this.#dialog;
|
||||||
|
style.left = `${x}px`;
|
||||||
|
style.top = isInitial
|
||||||
|
? `calc(${y}px + var(--editor-toolbar-vert-offset))`
|
||||||
|
: `${y}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit() {
|
||||||
|
const textInput = this.#textInput;
|
||||||
|
const textView = this.#textView;
|
||||||
|
if (textView.childElementCount > 0) {
|
||||||
|
const height = parseFloat(getComputedStyle(textView).height);
|
||||||
|
textInput.value = this.#previousText = this.#commentText;
|
||||||
|
textInput.style.height = `${height + 20}px`;
|
||||||
|
} else {
|
||||||
|
textInput.value = this.#previousText = this.#commentText;
|
||||||
|
}
|
||||||
|
|
||||||
|
textInput.classList.toggle("hidden", false);
|
||||||
|
textView.classList.toggle("hidden", true);
|
||||||
|
this.#editMenuItem.disabled = this.#deleteMenuItem.disabled = true;
|
||||||
|
setTimeout(() => textInput.focus(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#finish() {
|
||||||
|
this.#textView.replaceChildren();
|
||||||
|
this.#textInput.value = this.#previousText = this.#commentText = "";
|
||||||
|
this.#overlayManager.closeIfActive(this.#dialog);
|
||||||
|
this.#textInput.style.height = "";
|
||||||
|
this.#uiManager?.addEditListeners();
|
||||||
|
this.#uiManager = null;
|
||||||
|
this.#currentEditor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.#uiManager = null;
|
||||||
|
this.#finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CommentManager };
|
||||||
3
web/images/comment-actionsButton.svg
Normal file
3
web/images/comment-actionsButton.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51562 11H6.01562L5.51562 11.5V13L6.01562 13.5H7.51562L8.01562 13V11.5L7.51562 11ZM13.2656 11H11.7656L11.2656 11.5V13L11.7656 13.5H13.2656L13.7656 13V11.5L13.2656 11ZM17.5156 11H19.0156L19.5156 11.5V13L19.0156 13.5H17.5156L17.0156 13V11.5L17.5156 11Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
3
web/images/comment-closeButton.svg
Normal file
3
web/images/comment-closeButton.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.6241 11.7759L18.3331 7.06694C18.4423 6.94811 18.5015 6.79167 18.4981 6.63028C18.4948 6.46889 18.4292 6.31502 18.3152 6.20081C18.2011 6.0866 18.0473 6.02088 17.8859 6.01736C17.7245 6.01384 17.568 6.0728 17.4491 6.18194L12.7601 10.8709H12.2721L7.58306 6.18294C7.52495 6.12489 7.45598 6.07886 7.38008 6.04747C7.30418 6.01609 7.22284 5.99995 7.14071 6C7.05857 6.00005 6.97725 6.01627 6.90139 6.04774C6.82553 6.07922 6.75661 6.12533 6.69856 6.18344C6.64052 6.24155 6.59449 6.31052 6.5631 6.38642C6.53171 6.46232 6.51558 6.54366 6.51563 6.62579C6.51572 6.79167 6.5817 6.95071 6.69906 7.06794L11.3861 11.7539V12.2449L6.69906 16.9319C6.5898 17.0508 6.53066 17.2072 6.53399 17.3686C6.53732 17.53 6.60288 17.6839 6.71696 17.7981C6.83104 17.9123 6.98483 17.978 7.14622 17.9815C7.3076 17.985 7.46411 17.9261 7.58306 17.8169L12.2701 13.1299H12.7611L17.4481 17.8169C17.5656 17.934 17.7247 17.9997 17.8906 17.9997C18.0564 17.9997 18.2155 17.934 18.3331 17.8169C18.4504 17.6996 18.5163 17.5404 18.5163 17.3744C18.5163 17.2085 18.4504 17.0493 18.3331 16.9319L13.6241 12.2229V11.7759Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
3
web/images/comment-editButton.svg
Normal file
3
web/images/comment-editButton.svg
Normal 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="M13.4686 2.68492L12.9276 2.14392H2.40626L1.86526 2.68492V10.1102L2.40626 10.6512H6.71147C6.92794 10.6511 7.14165 10.7265 7.31497 10.8571L10.4357 13.3978V11.2299C10.4357 11.1049 10.485 10.9851 10.5795 10.8906C10.674 10.7961 10.7938 10.7468 10.9188 10.7468H12.9276L13.4686 10.2058V2.68492ZM2.52747 0.805556H12.7181C13.2412 0.805556 13.7478 1.01961 14.1371 1.40889C14.5264 1.79817 14.7405 2.30481 14.7405 2.82792V9.82792C14.7405 10.351 14.5264 10.8577 14.1371 11.247C13.7478 11.6363 13.2412 11.8503 12.7181 11.8503H11.4686V14.2055H10.4357L6.62747 11.8503H2.52747C2.00436 11.8503 1.49772 11.6363 1.10844 11.247C0.719162 10.8577 0.505107 10.351 0.505107 9.82792V2.82792C0.505107 2.30481 0.719162 1.79817 1.10844 1.40889C1.49772 1.01961 2.00436 0.805556 2.52747 0.805556ZM10.824 5.12016H4.46259C4.30418 5.12016 4.15436 5.05707 4.04541 4.94812C3.93646 4.83917 3.87337 4.68935 3.87337 4.53094C3.87337 4.37253 3.93646 4.22271 4.04541 4.11376C4.15436 4.00481 4.30418 3.94172 4.46259 3.94172H10.824C10.9824 3.94172 11.1322 4.00481 11.2412 4.11376C11.3501 4.22271 11.4132 4.37253 11.4132 4.53094C11.4132 4.68935 11.3501 4.83917 11.2412 4.94812C11.1322 5.05707 10.9824 5.12016 10.824 5.12016ZM8.82403 8.16137H4.46259C4.30418 8.16137 4.15436 8.09828 4.04541 7.98933C3.93646 7.88038 3.87337 7.73056 3.87337 7.57215C3.87337 7.41374 3.93646 7.26392 4.04541 7.15497C4.15436 7.04602 4.30418 6.98293 4.46259 6.98293H8.82403C8.98244 6.98293 9.13226 7.04602 9.24121 7.15497C9.35016 7.26392 9.41325 7.41374 9.41325 7.57215C9.41325 7.73056 9.35016 7.88038 9.24121 7.98933C9.13226 8.09828 8.98244 8.16137 8.82403 8.16137Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -230,6 +230,8 @@ class PDFViewer {
|
|||||||
|
|
||||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||||
|
|
||||||
|
#commentManager = null;
|
||||||
|
|
||||||
#containerTopLeft = null;
|
#containerTopLeft = null;
|
||||||
|
|
||||||
#editorUndoBar = null;
|
#editorUndoBar = null;
|
||||||
@ -314,6 +316,7 @@ class PDFViewer {
|
|||||||
this.downloadManager = options.downloadManager || null;
|
this.downloadManager = options.downloadManager || null;
|
||||||
this.findController = options.findController || null;
|
this.findController = options.findController || null;
|
||||||
this.#altTextManager = options.altTextManager || null;
|
this.#altTextManager = options.altTextManager || null;
|
||||||
|
this.#commentManager = options.commentManager || null;
|
||||||
this.#signatureManager = options.signatureManager || null;
|
this.#signatureManager = options.signatureManager || null;
|
||||||
this.#editorUndoBar = options.editorUndoBar || null;
|
this.#editorUndoBar = options.editorUndoBar || null;
|
||||||
|
|
||||||
@ -932,6 +935,7 @@ class PDFViewer {
|
|||||||
viewer,
|
viewer,
|
||||||
this.#viewerAlert,
|
this.#viewerAlert,
|
||||||
this.#altTextManager,
|
this.#altTextManager,
|
||||||
|
this.#commentManager,
|
||||||
this.#signatureManager,
|
this.#signatureManager,
|
||||||
eventBus,
|
eventBus,
|
||||||
pdfDocument,
|
pdfDocument,
|
||||||
@ -2403,6 +2407,8 @@ class PDFViewer {
|
|||||||
* keyboard action.
|
* keyboard action.
|
||||||
* @property {boolean} [mustEnterInEditMode] - True if the editor must enter
|
* @property {boolean} [mustEnterInEditMode] - True if the editor must enter
|
||||||
* edit mode.
|
* edit mode.
|
||||||
|
* @property {boolean} [editComment] - True if the editor must enter
|
||||||
|
* comment edit mode.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2413,6 +2419,7 @@ class PDFViewer {
|
|||||||
editId = null,
|
editId = null,
|
||||||
isFromKeyboard = false,
|
isFromKeyboard = false,
|
||||||
mustEnterInEditMode = false,
|
mustEnterInEditMode = false,
|
||||||
|
editComment = false,
|
||||||
}) {
|
}) {
|
||||||
if (!this.#annotationEditorUIManager) {
|
if (!this.#annotationEditorUIManager) {
|
||||||
throw new Error(`The AnnotationEditor is not enabled.`);
|
throw new Error(`The AnnotationEditor is not enabled.`);
|
||||||
@ -2436,7 +2443,8 @@ class PDFViewer {
|
|||||||
mode,
|
mode,
|
||||||
editId,
|
editId,
|
||||||
isFromKeyboard,
|
isFromKeyboard,
|
||||||
mustEnterInEditMode
|
mustEnterInEditMode,
|
||||||
|
editComment
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
mode !== this.#annotationEditorMode ||
|
mode !== this.#annotationEditorMode ||
|
||||||
|
|||||||
@ -32,6 +32,7 @@ const {
|
|||||||
getDocument,
|
getDocument,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getUuid,
|
getUuid,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
@ -86,6 +87,7 @@ export {
|
|||||||
getDocument,
|
getDocument,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
|
getRGB,
|
||||||
getUuid,
|
getUuid,
|
||||||
getXfaPageViewport,
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
|
|||||||
@ -771,6 +771,37 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="dialog commentManager" id="commentManagerDialog">
|
||||||
|
<div class="mainContainer">
|
||||||
|
<div id="commentManagerToolbar">
|
||||||
|
<button id="commentActionsButton" class="toolbarButton" type="button" aria-expanded="false" aria-haspopup="true" aria-controls="commentActionsMenu" tabindex="0" data-l10n-id="pdfjs-editor-edit-comment-actions-button">
|
||||||
|
<span data-l10n-id="pdfjs-editor-edit-comment-actions-button-label"></span>
|
||||||
|
</button>
|
||||||
|
<menu class="hidden" role="menu" id="commentActionsMenu">
|
||||||
|
<button id="commentActionsEditButton" role="menuitem" type="button" tabindex="0">
|
||||||
|
<span data-l10n-id="pdfjs-editor-edit-comment-actions-edit-button-label"></span>
|
||||||
|
</button>
|
||||||
|
<button id="commentActionsDeleteButton" role="menuitem" type="button" tabindex="0">
|
||||||
|
<span data-l10n-id="pdfjs-editor-edit-comment-actions-delete-button-label"></span>
|
||||||
|
</button>
|
||||||
|
</menu>
|
||||||
|
<button id="commentCloseButton" class="toolbarButton" type="button" tabindex="0" data-l10n-id="pdfjs-editor-edit-comment-close-button">
|
||||||
|
<span data-l10n-id="pdfjs-editor-edit-comment-close-button-label"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea class="hidden" id="commentManagerTextInput" data-l10n-id="pdfjs-editor-edit-comment-manager-text-input"></textarea>
|
||||||
|
<div class="hidden" id="commentManagerTextView"></div>
|
||||||
|
<div class="dialogButtonsGroup">
|
||||||
|
<button id="commentManagerCancelButton" type="button" class="secondaryButton" tabindex="0">
|
||||||
|
<span data-l10n-id="pdfjs-editor-edit-comment-manager-cancel-button"></span>
|
||||||
|
</button>
|
||||||
|
<button id="commentManagerSaveButton" type="button" class="primaryButton" disabled tabindex="0">
|
||||||
|
<span data-l10n-id="pdfjs-editor-edit-comment-manager-save-button"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!--#if !MOZCENTRAL-->
|
<!--#if !MOZCENTRAL-->
|
||||||
<dialog id="printServiceDialog" style="min-width: 200px;">
|
<dialog id="printServiceDialog" style="min-width: 200px;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@ -262,6 +262,19 @@ function getViewerConfiguration() {
|
|||||||
undoButton: document.getElementById("editorUndoBarUndoButton"),
|
undoButton: document.getElementById("editorUndoBarUndoButton"),
|
||||||
closeButton: document.getElementById("editorUndoBarCloseButton"),
|
closeButton: document.getElementById("editorUndoBarCloseButton"),
|
||||||
},
|
},
|
||||||
|
editCommentDialog: {
|
||||||
|
dialog: document.getElementById("commentManagerDialog"),
|
||||||
|
toolbar: document.getElementById("commentManagerToolbar"),
|
||||||
|
actions: document.getElementById("commentActionsButton"),
|
||||||
|
menu: document.getElementById("commentActionsMenu"),
|
||||||
|
editMenuItem: document.getElementById("commentActionsEditButton"),
|
||||||
|
deleteMenuItem: document.getElementById("commentActionsDeleteButton"),
|
||||||
|
closeButton: document.getElementById("commentCloseButton"),
|
||||||
|
textInput: document.getElementById("commentManagerTextInput"),
|
||||||
|
textView: document.getElementById("commentManagerTextView"),
|
||||||
|
cancelButton: document.getElementById("commentManagerCancelButton"),
|
||||||
|
saveButton: document.getElementById("commentManagerSaveButton"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user