diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 7c00e57e6ba1d0..19bb7d7c642eed 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -72,6 +72,10 @@ "type": "boolean", "default": false }, + "enableHighlightFloatingButton": { + "type": "boolean", + "default": false + }, "highlightEditorColors": { "type": "string", "default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F" diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index 110f492fc66c17..3804a3bd4d301b 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -318,6 +318,8 @@ pdfjs-editor-stamp-button-label = Add or edit images pdfjs-editor-highlight-button = .title = Highlight pdfjs-editor-highlight-button-label = Highlight +pdfjs-highlight-floating-button = + .title = Highlight ## Remove button for the various kind of editor. diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js index 135af4bc7d2ded..9916b206207d9e 100644 --- a/src/display/editor/toolbar.js +++ b/src/display/editor/toolbar.js @@ -135,4 +135,80 @@ class EditorToolbar { } } -export { EditorToolbar }; +class HighlightToolbar { + #buttons = null; + + #toolbar = null; + + #uiManager; + + constructor(uiManager) { + this.#uiManager = uiManager; + } + + #render() { + const editToolbar = (this.#toolbar = document.createElement("div")); + editToolbar.className = "editToolbar"; + editToolbar.setAttribute("role", "toolbar"); + editToolbar.addEventListener("contextmenu", noContextMenu); + + const buttons = (this.#buttons = document.createElement("div")); + buttons.className = "buttons"; + editToolbar.append(buttons); + + this.#addHighlightButton(); + + return editToolbar; + } + + #getLastPoint(boxes, isLTR) { + let lastY = 0; + let lastX = 0; + for (const box of boxes) { + const y = box.y + box.height; + if (y < lastY) { + continue; + } + const x = box.x + (isLTR ? box.width : 0); + if (y > lastY) { + lastX = x; + lastY = y; + continue; + } + if (isLTR) { + if (x > lastX) { + lastX = x; + } + } else if (x < lastX) { + lastX = x; + } + } + return [isLTR ? 1 - lastX : lastX, lastY]; + } + + show(parent, boxes, isLTR) { + const [x, y] = this.#getLastPoint(boxes, isLTR); + const { style } = (this.#toolbar ||= this.#render()); + parent.append(this.#toolbar); + style.insetInlineEnd = `${100 * x}%`; + style.top = `calc(${100 * y}% + var(--editor-toolbar-vert-offset))`; + } + + hide() { + this.#toolbar.remove(); + } + + #addHighlightButton() { + const button = document.createElement("button"); + button.className = "highlightButton"; + button.tabIndex = 0; + button.setAttribute("data-l10n-id", `pdfjs-highlight-floating-button`); + button.addEventListener("contextmenu", noContextMenu); + button.addEventListener("click", () => { + this.#uiManager.highlightSelection("floating_button"); + }); + this.#buttons.append(button); + } +} + +export { EditorToolbar, HighlightToolbar }; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 81ccd16e926812..60274e7e542bb5 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -33,6 +33,7 @@ import { getRGB, PixelsPerInch, } from "../display_utils.js"; +import { HighlightToolbar } from "./toolbar.js"; function bindEvents(obj, element, names) { for (const name of names) { @@ -555,6 +556,8 @@ class AnnotationEditorUIManager { #editorsToRescale = new Set(); + #enableHighlightFloatingButton = false; + #filterFactory = null; #focusMainContainerTimeoutId = null; @@ -563,6 +566,8 @@ class AnnotationEditorUIManager { #highlightWhenShiftUp = false; + #highlightToolbar = null; + #idManager = new IdManager(); #isEnabled = false; @@ -771,6 +776,7 @@ class AnnotationEditorUIManager { pdfDocument, pageColors, highlightColors, + enableHighlightFloatingButton, mlManager ) { this.#container = container; @@ -782,10 +788,12 @@ class AnnotationEditorUIManager { this._eventBus._on("scalechanging", this.#boundOnScaleChanging); this._eventBus._on("rotationchanging", this.#boundOnRotationChanging); this.#addSelectionListener(); + this.#addKeyboardManager(); this.#annotationStorage = pdfDocument.annotationStorage; this.#filterFactory = pdfDocument.filterFactory; this.#pageColors = pageColors; this.#highlightColors = highlightColors || null; + this.#enableHighlightFloatingButton = enableHighlightFloatingButton; this.#mlManager = mlManager || null; this.viewParameters = { realScale: PixelsPerInch.PDF_TO_CSS_UNITS, @@ -821,6 +829,8 @@ class AnnotationEditorUIManager { this.#selectedEditors.clear(); this.#commandManager.destroy(); this.#altTextManager?.destroy(); + this.#highlightToolbar?.hide(); + this.#highlightToolbar = null; if (this.#focusMainContainerTimeoutId) { clearTimeout(this.#focusMainContainerTimeoutId); this.#focusMainContainerTimeoutId = null; @@ -946,6 +956,12 @@ class AnnotationEditorUIManager { this.viewParameters.rotation = pagesRotation; } + #getAnchorElementForSelection({ anchorNode }) { + return anchorNode.nodeType === Node.TEXT_NODE + ? anchorNode.parentElement + : anchorNode; + } + highlightSelection(methodOfCreation = "") { const selection = document.getSelection(); if (!selection || selection.isCollapsed) { @@ -953,18 +969,19 @@ class AnnotationEditorUIManager { } const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; const text = selection.toString(); - const anchorElement = - anchorNode.nodeType === Node.TEXT_NODE - ? anchorNode.parentElement - : anchorNode; + const anchorElement = this.#getAnchorElementForSelection(selection); const textLayer = anchorElement.closest(".textLayer"); const boxes = this.getSelectionBoxes(textLayer); + if (!boxes) { + return; + } selection.empty(); if (this.#mode === AnnotationEditorType.NONE) { this._eventBus.dispatch("showannotationeditorui", { source: this, mode: AnnotationEditorType.HIGHLIGHT, }); + this.showAllEditors("highlight", true, /* updateButton = */ true); } for (const layer of this.#allLayers.values()) { if (layer.hasTextLayer(textLayer)) { @@ -982,6 +999,21 @@ class AnnotationEditorUIManager { } } + #displayHighlightToolbar() { + const selection = document.getSelection(); + if (!selection || selection.isCollapsed) { + return; + } + const anchorElement = this.#getAnchorElementForSelection(selection); + const textLayer = anchorElement.closest(".textLayer"); + const boxes = this.getSelectionBoxes(textLayer); + if (!boxes) { + return; + } + this.#highlightToolbar ||= new HighlightToolbar(this); + this.#highlightToolbar.show(textLayer, boxes, this.direction === "ltr"); + } + /** * Add an editor in the annotation storage. * @param {AnnotationEditor} editor @@ -1000,6 +1032,7 @@ class AnnotationEditorUIManager { const selection = document.getSelection(); if (!selection || selection.isCollapsed) { if (this.#selectedTextNode) { + this.#highlightToolbar?.hide(); this.#selectedTextNode = null; this.#dispatchUpdateStates({ hasSelectedText: false, @@ -1012,12 +1045,11 @@ class AnnotationEditorUIManager { return; } - const anchorElement = - anchorNode.nodeType === Node.TEXT_NODE - ? anchorNode.parentElement - : anchorNode; - if (!anchorElement.closest(".textLayer")) { + const anchorElement = this.#getAnchorElementForSelection(selection); + const textLayer = anchorElement.closest(".textLayer"); + if (!textLayer) { if (this.#selectedTextNode) { + this.#highlightToolbar?.hide(); this.#selectedTextNode = null; this.#dispatchUpdateStates({ hasSelectedText: false, @@ -1025,16 +1057,22 @@ class AnnotationEditorUIManager { } return; } + this.#highlightToolbar?.hide(); this.#selectedTextNode = anchorNode; this.#dispatchUpdateStates({ hasSelectedText: true, }); - if (this.#mode !== AnnotationEditorType.HIGHLIGHT) { + if ( + this.#mode !== AnnotationEditorType.HIGHLIGHT && + this.#mode !== AnnotationEditorType.NONE + ) { return; } - this.showAllEditors("highlight", true, /* updateButton = */ true); + if (this.#mode === AnnotationEditorType.HIGHLIGHT) { + this.showAllEditors("highlight", true, /* updateButton = */ true); + } this.#highlightWhenShiftUp = this.isShiftKeyDown; if (!this.isShiftKeyDown) { @@ -1046,7 +1084,7 @@ class AnnotationEditorUIManager { window.removeEventListener("pointerup", pointerup); window.removeEventListener("blur", pointerup); if (e.type === "pointerup") { - this.highlightSelection("main_toolbar"); + this.#onSelectEnd("main_toolbar"); } }; window.addEventListener("pointerup", pointerup); @@ -1054,6 +1092,14 @@ class AnnotationEditorUIManager { } } + #onSelectEnd(methodOfCreation = "") { + if (this.#mode === AnnotationEditorType.HIGHLIGHT) { + this.highlightSelection(methodOfCreation); + } else if (this.#enableHighlightFloatingButton) { + this.#displayHighlightToolbar(); + } + } + #addSelectionListener() { document.addEventListener("selectionchange", this.#boundSelectionChange); } @@ -1076,7 +1122,7 @@ class AnnotationEditorUIManager { this.isShiftKeyDown = false; if (this.#highlightWhenShiftUp) { this.#highlightWhenShiftUp = false; - this.highlightSelection("main_toolbar"); + this.#onSelectEnd("main_toolbar"); } if (!this.hasSelection) { return; @@ -1252,7 +1298,10 @@ class AnnotationEditorUIManager { if (!this.isShiftKeyDown && event.key === "Shift") { this.isShiftKeyDown = true; } - if (!this.isEditorHandlingKeyboard) { + if ( + this.#mode !== AnnotationEditorType.NONE && + !this.isEditorHandlingKeyboard + ) { AnnotationEditorUIManager._keyboardManager.exec(this, event); } } @@ -1266,7 +1315,7 @@ class AnnotationEditorUIManager { this.isShiftKeyDown = false; if (this.#highlightWhenShiftUp) { this.#highlightWhenShiftUp = false; - this.highlightSelection("main_toolbar"); + this.#onSelectEnd("main_toolbar"); } } } @@ -1335,7 +1384,6 @@ class AnnotationEditorUIManager { setEditingState(isEditing) { if (isEditing) { this.#addFocusManager(); - this.#addKeyboardManager(); this.#addCopyPasteListeners(); this.#dispatchUpdateStates({ isEditing: this.#mode !== AnnotationEditorType.NONE, @@ -1346,7 +1394,6 @@ class AnnotationEditorUIManager { }); } else { this.#removeFocusManager(); - this.#removeKeyboardManager(); this.#removeCopyPasteListeners(); this.#dispatchUpdateStates({ isEditing: false, diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 78be7bdd819834..e7de66ee837ba1 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1510,4 +1510,46 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Highlight from floating highlight button", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".annotationEditorLayer", + null, + null, + { highlightEditorColors: "red=#AB0000" } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that clicking on the highlight floating button triggers an highlight", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + + await page.waitForSelector(".textLayer .highlightButton"); + await page.click(".textLayer .highlightButton"); + + await page.waitForSelector(getEditorSelector(0)); + const usedColor = await page.evaluate(() => { + const highlight = document.querySelector( + `.page[data-page-number = "1"] .canvasWrapper > svg.highlight` + ); + return highlight.getAttribute("fill"); + }); + + expect(usedColor).withContext(`In ${browserName}`).toEqual("#AB0000"); + }) + ); + }); + }); }); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 733af85d7e1c6f..baba62b8469479 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -195,10 +195,12 @@ } .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor) { + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), +.textLayer { .editToolbar { --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); --editor-toolbar-bg-color: #f0f0f4; + --editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg); --editor-toolbar-fg-color: #2e2e56; --editor-toolbar-border-color: #8f8f9d; --editor-toolbar-hover-border-color: var(--editor-toolbar-border-color); @@ -284,6 +286,25 @@ margin-inline: 2px; } + .highlightButton { + width: var(--editor-toolbar-height); + + &::before { + content: ""; + mask-image: var(--editor-toolbar-highlight-image); + mask-repeat: no-repeat; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 100%; + height: 100%; + } + + &:hover::before { + background-color: var(--editor-toolbar-hover-fg-color); + } + } + .delete { width: var(--editor-toolbar-height); diff --git a/web/app.js b/web/app.js index a9581bbb06931e..77c4b3e1c2a05f 100644 --- a/web/app.js +++ b/web/app.js @@ -424,6 +424,9 @@ const PDFViewerApplication = { annotationMode: AppOptions.get("annotationMode"), annotationEditorMode, annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"), + enableHighlightFloatingButton: AppOptions.get( + "enableHighlightFloatingButton" + ), imageResourcesPath: AppOptions.get("imageResourcesPath"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), diff --git a/web/app_options.js b/web/app_options.js index 80fdc9c24d647a..272a3ca8f64562 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -143,6 +143,14 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableHighlightFloatingButton: { + // We'll probably want to make some experiments before enabling this + // in Firefox release, but it has to be temporary. + // TODO: remove it when unnecessary. + /** @type {boolean} */ + value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableML: { /** @type {boolean} */ value: false, diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index ad6e1712fed040..7d617592d17cd3 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -214,6 +214,8 @@ class PDFViewer { #copyCallbackBound = null; + #enableHighlightFloatingButton = false; + #enablePermissions = false; #mlManager = null; @@ -282,6 +284,8 @@ class PDFViewer { options.annotationEditorMode ?? AnnotationEditorType.NONE; this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null; + this.#enableHighlightFloatingButton = + options.enableHighlightFloatingButton === true; this.imageResourcesPath = options.imageResourcesPath || ""; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { @@ -863,6 +867,7 @@ class PDFViewer { pdfDocument, this.pageColors, this.#annotationEditorHighlightColors, + this.#enableHighlightFloatingButton, this.#mlManager ); this.eventBus.dispatch("annotationeditoruimanager", {