Merge pull request #20250 from calixteman/new_popup_reading_mode

[Annotation] Use the new popup in reading mode (bug 1987426)
This commit is contained in:
calixteman 2025-09-15 18:00:14 +02:00 committed by GitHub
commit 394fa2c184
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 236 additions and 74 deletions

View File

@ -24,6 +24,8 @@
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../../web/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */ /** @typedef {import("../../web/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
// eslint-disable-next-line max-len
/** @typedef {import("../../web/comment_manager.js").CommentManager} CommentManager */
import { import {
AnnotationBorderStyleType, AnnotationBorderStyleType,
@ -38,9 +40,6 @@ import {
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { import {
applyOpacity,
CSSConstants,
findContrastColor,
PDFDateString, PDFDateString,
renderRichText, renderRichText,
setLayerDimensions, setLayerDimensions,
@ -206,7 +205,7 @@ class AnnotationElement {
} }
get hasCommentButton() { get hasCommentButton() {
return this.enableComment && this._isEditable && this.hasPopupElement; return this.enableComment && this.hasPopupElement;
} }
get commentButtonPosition() { get commentButtonPosition() {
@ -230,16 +229,6 @@ class AnnotationElement {
return null; return null;
} }
get commentButtonColor() {
if (!this.data.color) {
return null;
}
return findContrastColor(
applyOpacity(...this.data.color, this.data.opacity),
CSSConstants.commentForegroundColor
);
}
_normalizePoint(point) { _normalizePoint(point) {
const { const {
page: { view }, page: { view },
@ -253,6 +242,11 @@ class AnnotationElement {
return point; return point;
} }
removePopup() {
(this.#popupElement?.popup || this.popup)?.remove();
this.#popupElement = this.popup = null;
}
updateEdited(params) { updateEdited(params) {
if (!this.container) { if (!this.container) {
return; return;
@ -272,8 +266,10 @@ class AnnotationElement {
let popup = this.#popupElement?.popup || this.popup; let popup = this.#popupElement?.popup || this.popup;
if (!popup && newPopup?.text) { if (!popup && newPopup?.text) {
this._createPopup(newPopup); if (!this.parent._commentManager) {
popup = this.#popupElement.popup; this._createPopup(newPopup);
popup = this.#popupElement.popup;
}
} }
if (!popup) { if (!popup) {
return; return;
@ -680,6 +676,9 @@ class AnnotationElement {
* @memberof AnnotationElement * @memberof AnnotationElement
*/ */
_createPopup(popupData = null) { _createPopup(popupData = null) {
if (this.parent._commentManager) {
return;
}
const { data } = this; const { data } = this;
let contentsObj, modificationDate; let contentsObj, modificationDate;
@ -2228,18 +2227,24 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
class PopupAnnotationElement extends AnnotationElement { class PopupAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const { data, elements } = parameters; const { data, elements, parent } = parameters;
super(parameters, { isRenderable: AnnotationElement._hasPopupData(data) }); const hasCommentManager = !!parent._commentManager;
super(parameters, {
isRenderable: !hasCommentManager && AnnotationElement._hasPopupData(data),
});
this.elements = elements; this.elements = elements;
this.popup = null; if (hasCommentManager && AnnotationElement._hasPopupData(data)) {
const popup = (this.popup = this.#createPopup());
for (const element of elements) {
element.popup = popup;
}
} else {
this.popup = null;
}
} }
render() { #createPopup() {
const { container } = this; return new PopupElement({
container.classList.add("popupAnnotation");
container.role = "comment";
const popup = (this.popup = new PopupElement({
container: this.container, container: this.container,
color: this.data.color, color: this.data.color,
titleObj: this.data.titleObj, titleObj: this.data.titleObj,
@ -2251,8 +2256,16 @@ class PopupAnnotationElement extends AnnotationElement {
parent: this.parent, parent: this.parent,
elements: this.elements, elements: this.elements,
open: this.data.open, open: this.data.open,
eventBus: this.linkService.eventBus, commentManager: this.parent._commentManager,
})); });
}
render() {
const { container } = this;
container.classList.add("popupAnnotation");
container.role = "comment";
const popup = (this.popup = this.#createPopup());
const elementIds = []; const elementIds = [];
for (const element of this.elements) { for (const element of this.elements) {
@ -2272,6 +2285,8 @@ class PopupAnnotationElement extends AnnotationElement {
} }
class PopupElement { class PopupElement {
#commentManager = null;
#boundKeyDown = this.#keyDown.bind(this); #boundKeyDown = this.#keyDown.bind(this);
#boundHide = this.#hide.bind(this); #boundHide = this.#hide.bind(this);
@ -2290,8 +2305,6 @@ class PopupElement {
#elements = null; #elements = null;
#eventBus = null;
#parent = null; #parent = null;
#parentRect = null; #parentRect = null;
@ -2308,7 +2321,7 @@ class PopupElement {
#commentButtonPosition = null; #commentButtonPosition = null;
#commentButtonColor = null; #popupPosition = null;
#rect = null; #rect = null;
@ -2320,6 +2333,8 @@ class PopupElement {
#wasVisible = false; #wasVisible = false;
#firstElement = null;
constructor({ constructor({
container, container,
color, color,
@ -2332,7 +2347,7 @@ class PopupElement {
rect, rect,
parentRect, parentRect,
open, open,
eventBus = null, commentManager = null,
}) { }) {
this.#container = container; this.#container = container;
this.#titleObj = titleObj; this.#titleObj = titleObj;
@ -2343,29 +2358,35 @@ class PopupElement {
this.#rect = rect; this.#rect = rect;
this.#parentRect = parentRect; this.#parentRect = parentRect;
this.#elements = elements; this.#elements = elements;
this.#eventBus = eventBus; this.#commentManager = commentManager;
this.#firstElement = elements[0];
// The modification date is shown in the popup instead of the creation // The modification date is shown in the popup instead of the creation
// date if it is available and can be parsed correctly, which is // date if it is available and can be parsed correctly, which is
// consistent with other viewers such as Adobe Acrobat. // consistent with other viewers such as Adobe Acrobat.
this.#dateObj = PDFDateString.toDateObject(modificationDate); this.#dateObj = PDFDateString.toDateObject(modificationDate);
this.trigger = elements.flatMap(e => e.getElementsToTriggerPopup()); if (commentManager) {
this.#addEventListeners(); this.#popupAbortController = new AbortController();
this.#renderCommentButton();
} else {
this.trigger = elements.flatMap(e => e.getElementsToTriggerPopup());
this.#addEventListeners();
this.#container.hidden = true; this.#container.hidden = true;
if (open) { if (open) {
this.#toggle(); this.#toggle();
} }
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
// Since the popup is lazily created, we need to ensure that it'll be // Since the popup is lazily created, we need to ensure that it'll be
// created and displayed during reference tests. // created and displayed during reference tests.
this.#parent.popupShow.push(async () => { this.#parent.popupShow.push(async () => {
if (this.#container.hidden) { if (this.#container.hidden) {
this.#show(); this.#show();
} }
}); });
}
} }
} }
@ -2390,8 +2411,6 @@ class PopupElement {
signal, signal,
}); });
} }
this.#renderCommentButton();
} }
#setCommentButtonPosition() { #setCommentButtonPosition() {
@ -2402,7 +2421,6 @@ class PopupElement {
this.#commentButtonPosition = element._normalizePoint( this.#commentButtonPosition = element._normalizePoint(
element.commentButtonPosition element.commentButtonPosition
); );
this.#commentButtonColor = element.commentButtonColor;
} }
#renderCommentButton() { #renderCommentButton() {
@ -2420,40 +2438,153 @@ class PopupElement {
const button = (this.#commentButton = document.createElement("button")); const button = (this.#commentButton = document.createElement("button"));
button.className = "annotationCommentButton"; button.className = "annotationCommentButton";
const parentContainer = this.#elements[0].container; const parentContainer = this.#firstElement.container;
button.style.zIndex = parentContainer.style.zIndex + 1; button.style.zIndex = parentContainer.style.zIndex + 1;
button.tabIndex = 0; button.tabIndex = 0;
button.ariaHasPopup = "dialog";
button.ariaControls = "commentPopup";
const { signal } = this.#popupAbortController; const { signal } = this.#popupAbortController;
button.addEventListener("hover", this.#boundToggle, { signal });
button.addEventListener("keydown", this.#boundKeyDown, { signal }); button.addEventListener("keydown", this.#boundKeyDown, { signal });
button.addEventListener( button.addEventListener(
"click", "click",
() => { () => {
const [ this.#commentManager.toggleCommentPopup(this, /* isSelected = */ true);
{ },
data: { id: editId }, { signal }
annotationEditorType: mode, );
}, button.addEventListener(
] = this.#elements; "pointerenter",
this.#eventBus?.dispatch("switchannotationeditormode", { () => {
source: this, this.#commentManager.toggleCommentPopup(
editId, this,
mode, /* isSelected = */ false,
editComment: true, /* visibility = */ true
}); );
},
{ signal }
);
button.addEventListener(
"pointerleave",
() => {
this.#commentManager.toggleCommentPopup(
this,
/* isSelected = */ false,
/* visibility = */ false
);
}, },
{ signal } { signal }
); );
const { style } = button; const { style } = button;
style.left = `calc(${this.#commentButtonPosition[0]}%)`; style.left = `calc(${this.#commentButtonPosition[0]}%)`;
style.top = `calc(${this.#commentButtonPosition[1]}% - var(--comment-button-dim))`; style.top = `calc(${this.#commentButtonPosition[1]}% - var(--comment-button-dim))`;
if (this.#commentButtonColor) { if (this.commentButtonColor) {
style.backgroundColor = this.#commentButtonColor; style.backgroundColor = this.commentButtonColor;
} }
parentContainer.after(button); parentContainer.after(button);
} }
get commentButtonColor() {
const {
data: { color, opacity },
} = this.#firstElement;
if (!color) {
return null;
}
return this.#parent._commentManager.makeCommentColor(color, opacity);
}
getData() {
return this.#firstElement.data;
}
get elementBeforePopup() {
return this.#commentButton;
}
get comment() {
return this.#firstElement.data.contentsObj?.str || "";
}
set comment(text) {
const element = this.#firstElement;
if (text) {
element.data.contentsObj = { str: text };
// TODO: Support saving the text.
// element.annotationStorage.setValue(element.data.id, {
// popup: { contents: text },
// });
} else {
element.data.contentsObj = null;
element.removePopup();
}
element.data.modificationDate = new Date();
}
get parentBoundingClientRect() {
return this.#firstElement.layer.getBoundingClientRect();
}
setCommentButtonStates({ selected, hasPopup }) {
if (!this.#commentButton) {
return;
}
this.#commentButton.classList.toggle("selected", selected);
this.#commentButton.ariaExpanded = hasPopup;
}
setSelectedCommentButton(selected) {
this.#commentButton.classList.toggle("selected", selected);
}
get commentPopupPosition() {
if (this.#popupPosition) {
return this.#popupPosition;
}
const { x, y, height } = this.#commentButton.getBoundingClientRect();
const {
x: parentX,
y: parentY,
width: parentWidth,
height: parentHeight,
} = this.#firstElement.layer.getBoundingClientRect();
return [(x - parentX) / parentWidth, (y + height - parentY) / parentHeight];
}
set commentPopupPosition(pos) {
this.#popupPosition = pos;
}
get commentButtonPosition() {
return this.#commentButtonPosition;
}
get commentButtonWidth() {
return (
this.#commentButton.getBoundingClientRect().width /
this.parentBoundingClientRect.width
);
}
editComment(options) {
const [posX, posY] =
this.#popupPosition || this.commentButtonPosition.map(x => x / 100);
const parentDimensions = this.parentBoundingClientRect;
const {
x: parentX,
y: parentY,
width: parentWidth,
height: parentHeight,
} = parentDimensions;
this.#commentManager.showDialog(
null,
this,
parentX + posX * parentWidth,
parentY + posY * parentHeight,
{ ...options, parentDimensions }
);
}
render() { render() {
if (this.#popup) { if (this.#popup) {
return; return;
@ -2612,8 +2743,12 @@ class PopupElement {
this.#popup = null; this.#popup = null;
this.#wasVisible = false; this.#wasVisible = false;
this.#pinned = false; this.#pinned = false;
for (const element of this.trigger) { this.#commentButton?.remove();
element.classList.remove("popupTriggerArea"); this.#commentButton = null;
if (this.trigger) {
for (const element of this.trigger) {
element.classList.remove("popupTriggerArea");
}
} }
} }
@ -2665,6 +2800,11 @@ class PopupElement {
* Toggle the visibility of the popup. * Toggle the visibility of the popup.
*/ */
#toggle() { #toggle() {
if (this.#commentManager) {
this.#commentManager.toggleCommentPopup(this, /* isSelected = */ false);
return;
}
this.#pinned = !this.#pinned; this.#pinned = !this.#pinned;
if (this.#pinned) { if (this.#pinned) {
this.#show(); this.#show();
@ -2716,6 +2856,9 @@ class PopupElement {
} }
maybeShow() { maybeShow() {
if (this.#commentManager) {
return;
}
this.#addEventListeners(); this.#addEventListeners();
if (!this.#wasVisible) { if (!this.#wasVisible) {
return; return;
@ -2728,6 +2871,9 @@ class PopupElement {
} }
get isVisible() { get isVisible() {
if (this.#commentManager) {
return false;
}
return this.#container.hidden === false; return this.#container.hidden === false;
} }
} }
@ -3425,6 +3571,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager] * @property {AnnotationEditorUIManager} [annotationEditorUIManager]
* @property {StructTreeLayerBuilder} [structTreeLayer] * @property {StructTreeLayerBuilder} [structTreeLayer]
* @property {CommentManager} [commentManager] - The comment manager instance.
*/ */
/** /**
@ -3447,6 +3594,7 @@ class AnnotationLayer {
page, page,
viewport, viewport,
structTreeLayer, structTreeLayer,
commentManager,
}) { }) {
this.div = div; this.div = div;
this.#accessibilityManager = accessibilityManager; this.#accessibilityManager = accessibilityManager;
@ -3456,6 +3604,7 @@ class AnnotationLayer {
this.viewport = viewport; this.viewport = viewport;
this.zIndex = 0; this.zIndex = 0;
this._annotationEditorUIManager = annotationEditorUIManager; this._annotationEditorUIManager = annotationEditorUIManager;
this._commentManager = commentManager || null;
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
// For testing purposes. // For testing purposes.

View File

@ -104,11 +104,7 @@ class Comment {
width: parentWidth, width: parentWidth,
height: parentHeight, height: parentHeight,
} = this.#editor.parent.boundingClientRect; } = this.#editor.parent.boundingClientRect;
const OFFSET_UNDER_BUTTON = 2; return [(x - parentX) / parentWidth, (y + height - parentY) / parentHeight];
return [
(x - parentX) / parentWidth,
(y + height + OFFSET_UNDER_BUTTON - parentY) / parentHeight,
];
} }
set commentPopupPositionInLayer(pos) { set commentPopupPositionInLayer(pos) {

View File

@ -1237,7 +1237,10 @@ class AnnotationEditor {
} }
} }
setCommentData({ comment, richText }) { setCommentData({ comment, popupRef, richText }) {
if (!popupRef) {
return;
}
this.#comment ||= new Comment(this); this.#comment ||= new Comment(this);
this.#comment.setInitialText(comment, richText); this.#comment.setInitialText(comment, richText);
} }

View File

@ -26,6 +26,7 @@
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
/** @typedef {import("./comment_manager.js").CommentManager} CommentManager */
import { import {
AnnotationLayer, AnnotationLayer,
@ -53,6 +54,7 @@ import { PresentationModeState } from "./ui_utils.js";
* @property {TextAccessibilityManager} [accessibilityManager] * @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager] * @property {AnnotationEditorUIManager} [annotationEditorUIManager]
* @property {function} [onAppend] * @property {function} [onAppend]
* @property {CommentManager} [commentManager]
*/ */
/** /**
@ -72,6 +74,8 @@ import { PresentationModeState } from "./ui_utils.js";
class AnnotationLayerBuilder { class AnnotationLayerBuilder {
#annotations = null; #annotations = null;
#commentManager = null;
#externalHide = false; #externalHide = false;
#onAppend = null; #onAppend = null;
@ -91,6 +95,7 @@ class AnnotationLayerBuilder {
imageResourcesPath = "", imageResourcesPath = "",
renderForms = true, renderForms = true,
enableComment = false, enableComment = false,
commentManager = null,
enableScripting = false, enableScripting = false,
hasJSActionsPromise = null, hasJSActionsPromise = null,
fieldObjectsPromise = null, fieldObjectsPromise = null,
@ -106,6 +111,7 @@ class AnnotationLayerBuilder {
this.renderForms = renderForms; this.renderForms = renderForms;
this.annotationStorage = annotationStorage; this.annotationStorage = annotationStorage;
this.enableComment = enableComment; this.enableComment = enableComment;
this.#commentManager = commentManager;
this.enableScripting = enableScripting; this.enableScripting = enableScripting;
this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false); this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false);
this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null); this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
@ -204,6 +210,7 @@ class AnnotationLayerBuilder {
page: this.pdfPage, page: this.pdfPage,
viewport: viewport.clone({ dontFlip: true }), viewport: viewport.clone({ dontFlip: true }),
structTreeLayer, structTreeLayer,
commentManager: this.#commentManager,
}); });
} }

View File

@ -228,7 +228,7 @@ const defaultOptions = {
}, },
enableComment: { enableComment: {
/** @type {boolean} */ /** @type {boolean} */
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), value: typeof PDFJSDev === "undefined",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE, kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}, },
enableDetailCanvas: { enableDetailCanvas: {

View File

@ -496,6 +496,7 @@
gap: 12px; gap: 12px;
z-index: 100001; /* above selected annotation editor */ z-index: 100001; /* above selected annotation editor */
pointer-events: auto; pointer-events: auto;
margin-top: 2px;
border: 0.5px solid var(--comment-border-color); border: 0.5px solid var(--comment-border-color);
background: var(--comment-bg-color); background: var(--comment-bg-color);

View File

@ -22,6 +22,7 @@
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */ /** @typedef {import("./interfaces").IRenderableView} IRenderableView */
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */ /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
/** @typedef {import("./comment_manager.js").CommentManager} CommentManager */
import { import {
AbortException, AbortException,
@ -102,6 +103,7 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
* the necessary layer-properties. * the necessary layer-properties.
* @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from * @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from
* text that look like URLs. The default value is `true`. * text that look like URLs. The default value is `true`.
* @property {CommentManager} [commentManager] - The comment manager instance.
*/ */
const DEFAULT_LAYER_PROPERTIES = const DEFAULT_LAYER_PROPERTIES =
@ -136,6 +138,8 @@ class PDFPageView extends BasePDFPageView {
#canvasWrapper = null; #canvasWrapper = null;
#commentManager = null;
#enableAutoLinking = true; #enableAutoLinking = true;
#hasRestrictedScaling = false; #hasRestrictedScaling = false;
@ -197,6 +201,7 @@ class PDFPageView extends BasePDFPageView {
this.capCanvasAreaFactor = this.capCanvasAreaFactor =
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor"); options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
this.#enableAutoLinking = options.enableAutoLinking !== false; this.#enableAutoLinking = options.enableAutoLinking !== false;
this.#commentManager = options.commentManager || null;
this.l10n = options.l10n; this.l10n = options.l10n;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -1011,6 +1016,7 @@ class PDFPageView extends BasePDFPageView {
annotationCanvasMap: this._annotationCanvasMap, annotationCanvasMap: this._annotationCanvasMap,
accessibilityManager: this._accessibilityManager, accessibilityManager: this._accessibilityManager,
annotationEditorUIManager, annotationEditorUIManager,
commentManager: this.#commentManager,
onAppend: annotationLayerDiv => { onAppend: annotationLayerDiv => {
this.#addLayer(annotationLayerDiv, "annotationLayer"); this.#addLayer(annotationLayerDiv, "annotationLayer");
}, },