diff --git a/gulpfile.mjs b/gulpfile.mjs index e1fe53f32fe89..b9e6d8fccf43a 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1152,6 +1152,7 @@ function buildComponents(defines, dir) { "web/images/messageBar_*.svg", "web/images/toolbarButton-{editorHighlight,menuArrow}.svg", "web/images/cursor-*.svg", + "web/images/secondaryToolbarButton-documentProperties.svg", ]; return ordered([ diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index fbff0d86a9c36..3e4a3510cf83d 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -503,3 +503,24 @@ pdfjs-editor-alt-text-settings-editor-title = Alt text editor pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text. pdfjs-editor-alt-text-settings-close-button = Close + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Highlight removed +pdfjs-editor-undo-bar-message-freetext = Text removed +pdfjs-editor-undo-bar-message-ink = Drawing removed +pdfjs-editor-undo-bar-message-stamp = Image removed +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removed + *[other] { $count } annotations removed + } + +pdfjs-editor-undo-bar-undo-button = + .title = Undo +pdfjs-editor-undo-bar-undo-button-label = Undo +pdfjs-editor-undo-bar-close-button = + .title = Close +pdfjs-editor-undo-bar-close-button-label = Close diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 1cce512288524..ac5241c0363d4 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -409,6 +409,9 @@ class AnnotationEditorLayer { // Do nothing on right click. return; } + + this.#uiManager._editorUndoBar?.hide(); + this.#uiManager.showAllEditors( "highlight", true, diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 6320d7a592f80..37c2bc1f285c7 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1066,6 +1066,8 @@ class AnnotationEditor { bindEvents(this, this.div, ["pointerdown"]); + this._uiManager._editorUndoBar?.hide(); + return this.div; } diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index c5d0def6c51bb..e7c672d98c778 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -694,6 +694,8 @@ class InkEditor extends AnnotationEditor { }); } + this._uiManager._editorUndoBar?.hide(); + this.#startDrawing(event.offsetX, event.offsetY); } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 40b96897babba..ea8b029f20d07 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -605,6 +605,8 @@ class AnnotationEditorUIManager { #editorsToRescale = new Set(); + _editorUndoBar = null; + #enableHighlightFloatingButton = false; #enableUpdatedAddImage = false; @@ -814,7 +816,8 @@ class AnnotationEditorUIManager { enableHighlightFloatingButton, enableUpdatedAddImage, enableNewAltTextWhenAddingImage, - mlManager + mlManager, + editorUndoBar ) { const signal = (this._signal = this.#abortController.signal); this.#container = container; @@ -849,6 +852,7 @@ class AnnotationEditorUIManager { rotation: 0, }; this.isShiftKeyDown = false; + this._editorUndoBar = editorUndoBar || null; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { Object.defineProperty(this, "reset", { @@ -889,6 +893,7 @@ class AnnotationEditorUIManager { clearTimeout(this.#translationTimeoutId); this.#translationTimeoutId = null; } + this._editorUndoBar?.destroy(); } combinedSignal(ac) { @@ -1640,6 +1645,8 @@ class AnnotationEditorUIManager { this.setEditingState(false); this.#disableAll(); + this._editorUndoBar?.hide(); + this.#updateModeCapability.resolve(); return; } @@ -2017,6 +2024,7 @@ class AnnotationEditorUIManager { hasSomethingToRedo: true, isEmpty: this.#isEmpty(), }); + this._editorUndoBar?.hide(); } /** @@ -2069,6 +2077,10 @@ class AnnotationEditorUIManager { const editors = [...this.#selectedEditors]; const cmd = () => { + this._editorUndoBar?.show( + undo, + editors.length === 1 ? editors[0].editorType : editors.length + ); for (const editor of editors) { editor.remove(); } diff --git a/web/app.js b/web/app.js index 82fa056801e7c..6ef90de641d2c 100644 --- a/web/app.js +++ b/web/app.js @@ -68,6 +68,7 @@ import { AltTextManager } from "web-alt_text_manager"; import { AnnotationEditorParams } from "web-annotation_editor_params"; import { CaretBrowsingMode } from "./caret_browsing.js"; import { DownloadManager } from "web-download_manager"; +import { EditorUndoBar } from "./editor_undo_bar.js"; import { OverlayManager } from "./overlay_manager.js"; import { PasswordPrompt } from "./password_prompt.js"; import { PDFAttachmentViewer } from "web-pdf_attachment_viewer"; @@ -182,6 +183,7 @@ const PDFViewerApplication = { _isCtrlKeyDown: false, _caretBrowsing: null, _isScrolling: false, + editorUndoBar: null, // Called once when the document is loaded. async initialize(appConfig) { @@ -451,6 +453,10 @@ const PDFViewerApplication = { : null; } + if (appConfig.editorUndoBar) { + this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus); + } + const enableHWA = AppOptions.get("enableHWA"); const pdfViewer = new PDFViewer({ container, @@ -460,6 +466,7 @@ const PDFViewerApplication = { linkService: pdfLinkService, downloadManager, altTextManager, + editorUndoBar: this.editorUndoBar, findController, scriptingManager: AppOptions.get("enableScripting") && pdfScriptingManager, @@ -2747,7 +2754,7 @@ function onTouchEnd(evt) { this._touchUnusedFactor = 1; } -function onClick(evt) { +function closeSecondaryToolbar(evt) { if (!this.secondaryToolbar?.isOpen) { return; } @@ -2764,6 +2771,20 @@ function onClick(evt) { } } +function closeEditorUndoBar(evt) { + if (!this.editorUndoBar?.isOpen) { + return; + } + if (this.appConfig.secondaryToolbar?.toolbar.contains(evt.target)) { + this.editorUndoBar.hide(); + } +} + +function onClick(evt) { + closeSecondaryToolbar.call(this, evt); + closeEditorUndoBar.call(this, evt); +} + function onKeyUp(evt) { // evt.ctrlKey is false hence we use evt.key. if (evt.key === "Control") { @@ -2774,6 +2795,11 @@ function onKeyUp(evt) { function onKeyDown(evt) { this._isCtrlKeyDown = evt.key === "Control"; + if (this.editorUndoBar?.isOpen && evt.keyCode !== 9 && evt.keyCode !== 16) { + // Hide undo bar on keypress except for Shift, Tab or Shift+Tab + this.editorUndoBar.hide(); + } + if (this.overlayManager.active) { return; } diff --git a/web/editor_undo_bar.js b/web/editor_undo_bar.js new file mode 100644 index 0000000000000..66e1027fa2a53 --- /dev/null +++ b/web/editor_undo_bar.js @@ -0,0 +1,112 @@ +/* Copyright 2024 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. + */ + +class EditorUndoBar { + #closeButton = null; + + #container; + + #eventBus = null; + + #initController = null; + + isOpen = false; + + #message; + + #showController = null; + + #undoButton; + + static #l10nMessages = Object.freeze({ + highlight: "pdfjs-editor-undo-bar-message-highlight", + freetext: "pdfjs-editor-undo-bar-message-freetext", + stamp: "pdfjs-editor-undo-bar-message-stamp", + ink: "pdfjs-editor-undo-bar-message-ink", + _multiple: "pdfjs-editor-undo-bar-message-multiple", + }); + + constructor({ container, message, undoButton, closeButton }, eventBus) { + this.#container = container; + this.#message = message; + this.#undoButton = undoButton; + this.#closeButton = closeButton; + this.#eventBus = eventBus; + } + + destroy() { + this.#initController?.abort(); + this.#initController = null; + + this.hide(); + } + + show(undoAction, messageData) { + if (!this.#initController) { + this.#initController = new AbortController(); + const opts = { signal: this.#initController.signal }; + const boundHide = this.hide.bind(this); + + this.#closeButton.addEventListener("click", boundHide, opts); + this.#eventBus._on("beforeprint", boundHide, opts); + this.#eventBus._on("download", boundHide, opts); + } + + this.hide(); + + if (typeof messageData === "string") { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages[messageData] + ); + } else { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages._multiple + ); + this.#message.setAttribute( + "data-l10n-args", + JSON.stringify({ count: messageData }) + ); + } + this.isOpen = true; + this.#container.hidden = false; + + this.#showController = new AbortController(); + + this.#undoButton.addEventListener( + "click", + () => { + undoAction(); + this.hide(); + }, + { signal: this.#showController.signal } + ); + this.#undoButton.focus(); + } + + hide() { + if (!this.isOpen) { + return; + } + this.isOpen = false; + this.#container.hidden = true; + + this.#showController?.abort(); + this.#showController = null; + } +} + +export { EditorUndoBar }; diff --git a/web/message_bar.css b/web/message_bar.css index 03667410cad3a..bcd3ba8c7504d 100644 --- a/web/message_bar.css +++ b/web/message_bar.css @@ -124,3 +124,97 @@ } } } + +#editorUndoBar { + --text-primary-color: #15141a; + + --message-bar-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color: #0060df; + --message-bar-bg-color: #deeafc; + --message-bar-fg-color: var(--text-primary-color); + --message-bar-border-color: rgb(0 0 0 / 0.08); + + --undo-button-bg-color: rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover: rgb(21 20 26 / 0.14); + --undo-button-bg-color-active: rgb(21 20 26 / 0.21); + + --undo-button-fg-color: var(--message-bar-fg-color); + --undo-button-fg-color-hover: var(--undo-button-fg-color); + --undo-button-fg-color-active: var(--undo-button-fg-color); + + --focus-ring-color: #0060df; + --focus-ring-outline: 2px solid var(--focus-ring-color); + + @media (prefers-color-scheme: dark) { + --text-primary-color: #fbfbfe; + + --message-bar-icon-color: #73a7f3; + --message-bar-bg-color: #003070; + --message-bar-border-color: rgb(255 255 255 / 0.08); + + --undo-button-bg-color: rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover: rgb(255 255 255 / 0.14); + --undo-button-bg-color-active: rgb(255 255 255 / 0.21); + } + + @media screen and (forced-colors: active) { + --text-primary-color: CanvasText; + + --message-bar-icon-color: CanvasText; + --message-bar-bg-color: Canvas; + --message-bar-border-color: CanvasText; + + --undo-button-bg-color: ButtonText; + --undo-button-bg-color-hover: SelectedItem; + --undo-button-bg-color-active: SelectedItem; + + --undo-button-fg-color: ButtonFace; + --undo-button-fg-color-hover: SelectedItemText; + --undo-button-fg-color-active: SelectedItemText; + + --focus-ring-color: CanvasText; + } + + position: fixed; + bottom: 100px; + left: 50%; + transform: translateX(-50%); + z-index: 10; + + padding-block: 8px; + padding-inline: 16px 8px; + + font-family: sans-serif; + font-size: 15px; + + cursor: default; + + button { + cursor: pointer; + } + + #editorUndoBarUndoButton { + border-radius: 4px; + font-weight: 590; + line-height: 19.5px; + color: var(--undo-button-fg-color); + border: none; + padding: 4px 16px; + margin-inline-start: 8px; + height: 32px; + + background-color: var(--undo-button-bg-color); + + &:hover { + background-color: var(--undo-button-bg-color-hover); + } + + &:active { + background-color: var(--undo-button-bg-color-active); + } + } + + > div { + align-items: center; + } +} diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 9db9c6adcfdc6..b9d3d768b05bb 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -213,6 +213,8 @@ class PDFViewer { #containerTopLeft = null; + #editorUndoBar = null; + #enableHWA = false; #enableHighlightFloatingButton = false; @@ -280,6 +282,7 @@ class PDFViewer { this.downloadManager = options.downloadManager || null; this.findController = options.findController || null; this.#altTextManager = options.altTextManager || null; + this.#editorUndoBar = options.editorUndoBar || null; if (this.findController) { this.findController.onIsPageVisible = pageNumber => @@ -908,7 +911,8 @@ class PDFViewer { this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#enableNewAltTextWhenAddingImage, - this.#mlManager + this.#mlManager, + this.#editorUndoBar ); eventBus.dispatch("annotationeditoruimanager", { source: this, diff --git a/web/viewer.html b/web/viewer.html index 1a8eb7f10dc13..d3c6268d60ae7 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -688,6 +688,20 @@ + +
diff --git a/web/viewer.js b/web/viewer.js index 30eefd0227de8..4cab203081f10 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -223,6 +223,12 @@ function getViewerConfiguration() { editorHighlightShowAll: document.getElementById("editorHighlightShowAll"), }, printContainer: document.getElementById("printContainer"), + editorUndoBar: { + container: document.getElementById("editorUndoBar"), + message: document.getElementById("editorUndoBarMessage"), + undoButton: document.getElementById("editorUndoBarUndoButton"), + closeButton: document.getElementById("editorUndoBarCloseButton"), + }, }; }