From dc1d34caec19f942922f8d8fc17d1154ec6cefb2 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 6 Mar 2024 22:38:25 +0100 Subject: [PATCH] [Editor] Add a toggle button to show/hide all the highlights (bug 1867740) --- l10n/en-US/viewer.ftl | 7 + src/display/draw_layer.js | 4 + src/display/editor/annotation_editor_layer.js | 5 + src/display/editor/editor.js | 15 ++ src/display/editor/highlight.js | 9 + src/display/editor/tools.js | 35 +++ src/shared/util.js | 1 + test/integration/highlight_editor_spec.mjs | 69 ++++++ web/annotation_editor_layer_builder.css | 8 + web/annotation_editor_params.js | 9 + web/toggle_button.css | 219 ++++++++++++++++++ web/viewer.html | 16 +- web/viewer.js | 1 + 13 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 web/toggle_button.css diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index 6bbec786b59e90..110f492fc66c17 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -405,3 +405,10 @@ pdfjs-editor-colorpicker-pink = .title = Pink pdfjs-editor-colorpicker-red = .title = Red + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Show all +pdfjs-editor-highlight-show-all-button = + .title = Show all diff --git a/src/display/draw_layer.js b/src/display/draw_layer.js index 9e6aedfd6e53c3..c7cf6ef9fae044 100644 --- a/src/display/draw_layer.js +++ b/src/display/draw_layer.js @@ -200,6 +200,10 @@ class DrawLayer { DrawLayer.#setBox(this.#mapping.get(id), box); } + show(id, visible) { + this.#mapping.get(id).classList.toggle("hidden", !visible); + } + rotate(id, angle) { this.#mapping.get(id).setAttribute("data-main-rotation", angle); } diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index b7f7d40434e74c..367236947c8110 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -362,6 +362,11 @@ class AnnotationEditorLayer { // Do nothing on right click. return; } + this.#uiManager.showAllEditors( + "highlight", + true, + /* updateButton = */ true + ); this.#textLayer.div.classList.add("free"); HighlightEditor.startHighlighting( this, diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index cece94eebde74d..cf3ae887f24367 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -76,6 +76,8 @@ class AnnotationEditor { _initialOptions = Object.create(null); + _isVisible = true; + _uiManager = null; _focusEventsAllowed = true; @@ -1001,6 +1003,9 @@ class AnnotationEditor { this.div.className = this.name; this.div.setAttribute("id", this.id); this.div.setAttribute("tabIndex", 0); + if (!this._isVisible) { + this.div.classList.add("hidden"); + } this.setInForeground(); @@ -1263,6 +1268,7 @@ class AnnotationEditor { rebuild() { this.div?.addEventListener("focusin", this.#boundFocusin); this.div?.addEventListener("focusout", this.#boundFocusout); + this.show(this._isVisible); } /** @@ -1665,6 +1671,15 @@ class AnnotationEditor { }, }); } + + /** + * Show or hide this editor. + * @param {boolean} visible + */ + show(visible) { + this.div.classList.toggle("hidden", !visible); + this._isVisible = visible; + } } // This class is used to fake an editor which has been deleted. diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index a73df5b3574487..836748911664b9 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -453,6 +453,7 @@ class HighlightEditor extends AnnotationEditor { !this.parent && this.div?.classList.contains("selectedEditor"); } super.setParent(parent); + this.show(this._isVisible); if (mustBeSelected) { // We select it after the parent has been set. this.select(); @@ -634,6 +635,14 @@ class HighlightEditor extends AnnotationEditor { return !this.#isFreeHighlight; } + show(visible) { + super.show(visible); + if (this.parent) { + this.parent.drawLayer.show(this.#id, visible); + this.parent.drawLayer.show(this.#outlineId, visible); + } + } + #getRotation() { // Highlight annotations are always drawn horizontally but if // a free highlight annotation can be rotated. diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index c6719fbae3006a..bd1e6b5272de86 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -583,6 +583,8 @@ class AnnotationEditorUIManager { #pageColors = null; + #showAllStates = null; + #boundBlur = this.blur.bind(this); #boundFocus = this.focus.bind(this); @@ -1029,6 +1031,8 @@ class AnnotationEditorUIManager { if (this.#mode !== AnnotationEditorType.HIGHLIGHT) { return; } + + this.showAllEditors("highlight", true, /* updateButton = */ true); this.#highlightWhenShiftUp = this.isShiftKeyDown; if (!this.isShiftKeyDown) { const pointerup = e => { @@ -1477,6 +1481,20 @@ class AnnotationEditorUIManager { case AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR: this.#mainHighlightColorPicker?.updateColor(value); break; + case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL: + this._eventBus.dispatch("reporttelemetry", { + source: this, + details: { + type: "editing", + data: { + type: "highlight", + action: "toggle_visibility", + }, + }, + }); + (this.#showAllStates ||= new Map()).set(type, value); + this.showAllEditors("highlight", value); + break; } for (const editor of this.#selectedEditors) { @@ -1488,6 +1506,23 @@ class AnnotationEditorUIManager { } } + showAllEditors(type, visible, updateButton = false) { + for (const editor of this.#allEditors.values()) { + if (editor.editorType === type) { + editor.show(visible); + } + } + if ( + (this.#showAllStates?.get( + AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL + ) ?? true) !== visible + ) { + this.#dispatchUpdateUI([ + [AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL, visible], + ]); + } + } + enableWaiting(mustWait = false) { if (this.#isWaiting === mustWait) { return; diff --git a/src/shared/util.js b/src/shared/util.js index 4080818d4bb5d0..5fa1af0395ef0e 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -90,6 +90,7 @@ const AnnotationEditorParamsType = { HIGHLIGHT_DEFAULT_COLOR: 32, HIGHLIGHT_THICKNESS: 33, HIGHLIGHT_FREE: 34, + HIGHLIGHT_SHOW_ALL: 35, }; // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 59a8b19a71fccc..78be7bdd819834 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1441,4 +1441,73 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Show/hide all the highlights", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the highlights are correctly hidden/shown", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#editorHighlight"); + await page.waitForSelector(".annotationEditorLayer.highlightEditing"); + + let rect = await page.$eval(".annotationEditorLayer", el => { + const { x, y } = el.getBoundingClientRect(); + return { x, y }; + }); + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(rect.x + 20, rect.y + 20); + await page.mouse.down(); + await page.mouse.move(rect.x + 20, rect.y + 120); + await page.mouse.up(); + await awaitPromise(clickHandle); + await page.waitForSelector(getEditorSelector(0)); + + rect = await getSpanRectFromText(page, 1, "Languages"); + await page.mouse.click( + rect.x + rect.width / 2, + rect.y + rect.height / 2, + { count: 2, delay: 100 } + ); + await page.waitForSelector(getEditorSelector(1)); + + await page.click("#editorHighlightShowAll"); + await page.waitForSelector(`${getEditorSelector(0)}.hidden`); + await page.waitForSelector(`${getEditorSelector(1)}.hidden`); + + await page.click("#editorHighlightShowAll"); + await page.waitForSelector(`${getEditorSelector(0)}:not(.hidden)`); + await page.waitForSelector(`${getEditorSelector(1)}:not(.hidden)`); + + await page.click("#editorHighlightShowAll"); + await page.waitForSelector(`${getEditorSelector(0)}.hidden`); + await page.waitForSelector(`${getEditorSelector(1)}.hidden`); + + const oneToOne = Array.from(new Array(13).keys(), n => n + 2).concat( + Array.from(new Array(13).keys(), n => 13 - n) + ); + for (const pageNumber of oneToOne) { + await scrollIntoView( + page, + `.page[data-page-number = "${pageNumber}"]` + ); + if (pageNumber === 14) { + await page.click("#editorHighlightShowAll"); + } + } + + await page.waitForSelector(`${getEditorSelector(0)}:not(.hidden)`); + await page.waitForSelector(`${getEditorSelector(1)}:not(.hidden)`); + }) + ); + }); + }); }); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index d468b2df6259c8..636c8ae8e081b9 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -14,6 +14,7 @@ */ @import url(draw_layer_builder.css); +@import url(toggle_button.css); :root { --outline-width: 2px; @@ -1211,4 +1212,11 @@ } } } + + #editorHighlightVisibility { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + } } diff --git a/web/annotation_editor_params.js b/web/annotation_editor_params.js index 886341f4b11a0e..f3362de20e4ca5 100644 --- a/web/annotation_editor_params.js +++ b/web/annotation_editor_params.js @@ -33,6 +33,7 @@ class AnnotationEditorParams { editorInkOpacity, editorStampAddImage, editorFreeHighlightThickness, + editorHighlightShowAll, }) { const dispatchEvent = (typeStr, value) => { this.eventBus.dispatch("switchannotationeditorparams", { @@ -62,6 +63,11 @@ class AnnotationEditorParams { editorFreeHighlightThickness.addEventListener("input", function () { dispatchEvent("HIGHLIGHT_THICKNESS", this.valueAsNumber); }); + editorHighlightShowAll.addEventListener("click", function () { + const checked = this.getAttribute("aria-pressed") === "true"; + this.setAttribute("aria-pressed", !checked); + dispatchEvent("HIGHLIGHT_SHOW_ALL", !checked); + }); this.eventBus._on("annotationeditorparamschanged", evt => { for (const [type, value] of evt.details) { @@ -87,6 +93,9 @@ class AnnotationEditorParams { case AnnotationEditorParamsType.HIGHLIGHT_FREE: editorFreeHighlightThickness.disabled = !value; break; + case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL: + editorHighlightShowAll.setAttribute("aria-pressed", value); + break; } } }); diff --git a/web/toggle_button.css b/web/toggle_button.css new file mode 100644 index 00000000000000..a9f1379fe89ba8 --- /dev/null +++ b/web/toggle_button.css @@ -0,0 +1,219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.toggle-button { + --button-background-color: #f0f0f4; + --button-background-color-hover: #e0e0e6; + --button-background-color-active: #cfcfd8; + --color-accent-primary: #0060df; + --color-accent-primary-hover: #0250bb; + --color-accent-primary-active: #054096; + --border-interactive-color: #8f8f9d; + --border-radius-circle: 9999px; + --border-width: 1px; + --size-item-small: 16px; + --size-item-large: 32px; + --color-canvas: white; +} + +@media (prefers-color-scheme: dark) { + .toggle-button { + --button-background-color: color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover: color-mix( + in srgb, + currentColor 14%, + transparent + ); + --button-background-color-active: color-mix( + in srgb, + currentColor 21%, + transparent + ); + --color-accent-primary: #0df; + --color-accent-primary-hover: #80ebff; + --color-accent-primary-active: #aaf2ff; + --border-interactive-color: #bfbfc9; + --color-canvas: #1c1b22; + } +} + +@media (forced-colors: active) { + .toggle-button { + --color-accent-primary: ButtonText; + --color-accent-primary-hover: SelectedItem; + --color-accent-primary-active: SelectedItem; + --border-interactive-color: ButtonText; + --button-background-color: ButtonFace; + --border-interactive-color-hover: SelectedItem; + --border-interactive-color-active: SelectedItem; + --border-interactive-color-disabled: GrayText; + --color-canvas: ButtonText; + } +} + +/* The original file is located at: + http://hg.mozilla.org/mozilla-central/file/tip/toolkit/content/widgets/moz-toggle/moz-toggle.css +*/ + +.toggle-button { + --toggle-background-color: var(--button-background-color); + --toggle-background-color-hover: var(--button-background-color-hover); + --toggle-background-color-active: var(--button-background-color-active); + --toggle-background-color-pressed: var(--color-accent-primary); + --toggle-background-color-pressed-hover: var(--color-accent-primary-hover); + --toggle-background-color-pressed-active: var(--color-accent-primary-active); + --toggle-border-color: var(--border-interactive-color); + --toggle-border-color-hover: var(--toggle-border-color); + --toggle-border-color-active: var(--toggle-border-color); + --toggle-border-radius: var(--border-radius-circle); + --toggle-border-width: var(--border-width); + --toggle-height: var(--size-item-small); + --toggle-width: var(--size-item-large); + --toggle-dot-background-color: var(--toggle-border-color); + --toggle-dot-background-color-hover: var(--toggle-dot-background-color); + --toggle-dot-background-color-active: var(--toggle-dot-background-color); + --toggle-dot-background-color-on-pressed: var(--color-canvas); + --toggle-dot-margin: 1px; + --toggle-dot-height: calc( + var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * + var(--toggle-border-width) + ); + --toggle-dot-width: var(--toggle-dot-height); + --toggle-dot-transform-x: calc( + var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width) + ); +} + +.toggle-button { + appearance: none; + padding: 0; + margin: 0; + border: var(--toggle-border-width) solid var(--toggle-border-color); + height: var(--toggle-height); + width: var(--toggle-width); + border-radius: var(--toggle-border-radius); + background: var(--toggle-background-color); + box-sizing: border-box; + flex-shrink: 0; +} + +.toggle-button:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); +} + +.toggle-button:enabled:hover { + background: var(--toggle-background-color-hover); + border-color: var(--toggle-border-color); +} + +.toggle-button:enabled:active { + background: var(--toggle-background-color-active); + border-color: var(--toggle-border-color); +} + +.toggle-button[aria-pressed="true"] { + background: var(--toggle-background-color-pressed); + border-color: transparent; +} + +.toggle-button[aria-pressed="true"]:enabled:hover { + background: var(--toggle-background-color-pressed-hover); + border-color: transparent; +} + +.toggle-button[aria-pressed="true"]:enabled:active { + background: var(--toggle-background-color-pressed-active); + border-color: transparent; +} + +.toggle-button::before { + display: block; + content: ""; + background-color: var(--toggle-dot-background-color); + height: var(--toggle-dot-height); + width: var(--toggle-dot-width); + margin: var(--toggle-dot-margin); + border-radius: var(--toggle-border-radius); + translate: 0; +} + +.toggle-button[aria-pressed="true"]::before { + translate: var(--toggle-dot-transform-x); + background-color: var(--toggle-dot-background-color-on-pressed); +} + +.toggle-button[aria-pressed="true"]:enabled:hover::before, +.toggle-button[aria-pressed="true"]:enabled:active::before { + background-color: var(--toggle-dot-background-color-on-pressed); +} + +.toggle-button[aria-pressed="true"]:-moz-locale-dir(rtl)::before, +.toggle-button[aria-pressed="true"]:dir(rtl)::before { + translate: calc(-1 * var(--toggle-dot-transform-x)); +} + +@media (prefers-reduced-motion: no-preference) { + .toggle-button::before { + transition: translate 100ms; + } +} + +@media (prefers-contrast) { + .toggle-button:enabled:hover { + border-color: var(--toggle-border-color-hover); + } + + .toggle-button:enabled:active { + border-color: var(--toggle-border-color-active); + } + + .toggle-button[aria-pressed="true"]:enabled { + border-color: var(--toggle-border-color); + position: relative; + } + + .toggle-button[aria-pressed="true"]:enabled:hover, + .toggle-button[aria-pressed="true"]:enabled:hover:active { + border-color: var(--toggle-border-color-hover); + } + + .toggle-button[aria-pressed="true"]:enabled:active { + background-color: var(--toggle-dot-background-color-active); + border-color: var(--toggle-dot-background-color-hover); + } + + .toggle-button:hover::before, + .toggle-button:active::before { + background-color: var(--toggle-dot-background-color-hover); + } +} + +@media (forced-colors) { + .toggle-button { + --toggle-dot-background-color: var(--color-accent-primary); + --toggle-dot-background-color-hover: var(--color-accent-primary-hover); + --toggle-dot-background-color-active: var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed: var(--button-background-color); + --toggle-background-color-disabled: var(--button-background-color-disabled); + --toggle-border-color-hover: var(--border-interactive-color-hover); + --toggle-border-color-active: var(--border-interactive-color-active); + --toggle-border-color-disabled: var(--border-interactive-color-disabled); + } + + .toggle-button[aria-pressed="true"]:enabled::after { + border: 1px solid var(--button-background-color); + content: ""; + position: absolute; + height: var(--toggle-height); + width: var(--toggle-width); + display: block; + border-radius: var(--toggle-border-radius); + inset: -2px; + } + + .toggle-button[aria-pressed="true"]:enabled:active::after { + border-color: var(--toggle-border-color-active); + } +} diff --git a/web/viewer.html b/web/viewer.html index b81f0cc8c3ce05..002c647ef62508 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -184,6 +184,10 @@ +
+ + +
@@ -191,11 +195,11 @@
- +
- +
@@ -204,22 +208,22 @@
- +
- +
- +