diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index fb863752863ba..c272befefeeb1 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -85,6 +85,10 @@ "type": "boolean", "default": false }, + "highlightColors": { + "type": "string", + "default": "" + }, "disableRange": { "title": "Disable range requests", "description": "Whether to disable range requests (not recommended).", diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index b3aed968c52a4..65a52afbe80be 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -332,6 +332,8 @@ pdfjs-editor-remove-freetext-button = .title = Remove text pdfjs-editor-remove-stamp-button = .title = Remove image +pdfjs-editor-remove-highlight-button = + .title = Remove highlight ## @@ -384,3 +386,22 @@ pdfjs-editor-resizer-label-bottom-right = Bottom right corner — resize pdfjs-editor-resizer-label-bottom-middle = Bottom middle — resize pdfjs-editor-resizer-label-bottom-left = Bottom left corner — resize pdfjs-editor-resizer-label-middle-left = Middle left — resize + +## Color picker + +pdfjs-editor-highlight-colorpicker-label = Highlight color + +pdfjs-editor-colorpicker-button = + .title = Change color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Color choices +pdfjs-editor-colorpicker-yellow = + .title = Yellow +pdfjs-editor-colorpicker-green = + .title = Green +pdfjs-editor-colorpicker-blue = + .title = Blue +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Red diff --git a/src/display/editor/color_picker.js b/src/display/editor/color_picker.js new file mode 100644 index 0000000000000..1f50c08d996ad --- /dev/null +++ b/src/display/editor/color_picker.js @@ -0,0 +1,245 @@ +/* Copyright 2023 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 { AnnotationEditorParamsType, shadow } from "../../shared/util.js"; +import { KeyboardManager } from "./tools.js"; +import { noContextMenu } from "../display_utils.js"; + +class ColorPicker { + #boundKeyDown = this.#keyDown.bind(this); + + #button = null; + + #buttonSwatch = null; + + #defaultColor; + + #dropdown = null; + + #dropdownWasFromKeyboard = false; + + #isMainColorPicker = false; + + #eventBus; + + #uiManager = null; + + static get _keyboardManager() { + return shadow( + this, + "_keyboardManager", + new KeyboardManager([ + [ + ["Escape", "mac+Escape"], + ColorPicker.prototype._hideDropdownFromKeyboard, + ], + [[" ", "mac+ "], ColorPicker.prototype._colorSelectFromKeyboard], + [ + ["ArrowDown", "ArrowRight", "mac+ArrowDown", "mac+ArrowRight"], + ColorPicker.prototype._moveToNext, + ], + [ + ["ArrowUp", "ArrowLeft", "mac+ArrowUp", "mac+ArrowLeft"], + ColorPicker.prototype._moveToPrevious, + ], + [["Home", "mac+Home"], ColorPicker.prototype._moveToBeginning], + [["End", "mac+End"], ColorPicker.prototype._moveToEnd], + ]) + ); + } + + constructor({ editor = null, uiManager = null }) { + this.#isMainColorPicker = !editor; + this.#uiManager = editor?._uiManager || uiManager; + this.#eventBus = this.#uiManager._eventBus; + this.#defaultColor = + editor?.color || this.#uiManager?.highlightColors.values().next().value; + } + + renderButton() { + const button = (this.#button = document.createElement("button")); + button.className = "colorPicker"; + button.tabIndex = "0"; + button.setAttribute("data-l10n-id", "pdfjs-editor-colorpicker-button"); + button.setAttribute("aria-haspopup", true); + button.addEventListener("click", this.#openDropdown.bind(this)); + const swatch = (this.#buttonSwatch = document.createElement("span")); + swatch.className = "swatch"; + swatch.style.backgroundColor = this.#defaultColor; + button.append(swatch); + return button; + } + + renderMainDropdown() { + const div = document.createElement("div"); + div.className = "colorPicker"; + const span = document.createElement("span"); + div.append(span); + span.id = "highlightColorPickerLabel"; + span.setAttribute( + "data-l10n-id", + "pdfjs-editor-highlight-colorpicker-label" + ); + + const dropdown = (this.#dropdown = this.#getDropdownRoot( + AnnotationEditorParamsType.DEFAULT_HIGHLIGHT_COLOR + )); + dropdown.setAttribute("aria-orientation", "horizontal"); + dropdown.setAttribute("aria-labelledby", "highlightColorPickerLabel"); + div.append(dropdown); + + return div; + } + + #getDropdownRoot(paramType) { + const div = document.createElement("div"); + div.addEventListener("contextmenu", noContextMenu); + div.className = "dropdown"; + div.role = "listbox"; + div.setAttribute("aria-multiselectable", false); + div.setAttribute("aria-orientation", "vertical"); + div.setAttribute("data-l10n-id", "pdfjs-editor-colorpicker-dropdown"); + for (const [name, color] of this.#uiManager.highlightColors) { + const button = document.createElement("button"); + button.tabIndex = "0"; + button.role = "option"; + button.setAttribute("data-color", color); + button.setAttribute("data-l10n-id", `pdfjs-editor-colorpicker-${name}`); + const swatch = document.createElement("span"); + button.append(swatch); + swatch.className = "swatch"; + swatch.style.backgroundColor = color; + if (color === this.#defaultColor) { + button.classList.add("selected"); + button.setAttribute("aria-selected", true); + } else { + button.setAttribute("aria-selected", false); + } + button.addEventListener( + "click", + this.#colorSelect.bind(this, paramType, color) + ); + div.append(button); + } + + div.addEventListener("keydown", this.#boundKeyDown); + + return div; + } + + #colorSelect(type, color, event) { + event.stopPropagation(); + this.#eventBus.dispatch("switchannotationeditorparams", { + source: this, + type, + value: color, + }); + } + + _colorSelectFromKeyboard(event) { + const color = event.target.getAttribute("data-color"); + if (!color) { + return; + } + this.#colorSelect(color, event); + } + + _moveToNext(event) { + if (event.target === this.#button) { + this.#dropdown.firstChild?.focus(); + return; + } + event.target.nextSibling?.focus(); + } + + _moveToPrevious(event) { + event.target.previousSibling?.focus(); + } + + _moveToBeginning() { + this.#dropdown.firstChild?.focus(); + } + + _moveToEnd() { + this.#dropdown.lastChild?.focus(); + } + + #keyDown(event) { + ColorPicker._keyboardManager.exec(this, event); + } + + #openDropdown(event) { + if (this.#dropdown && !this.#dropdown.classList.contains("hidden")) { + this.hideDropdown(); + return; + } + this.#button.addEventListener("keydown", this.#boundKeyDown); + this.#dropdownWasFromKeyboard = event.detail === 0; + if (this.#dropdown) { + this.#dropdown.classList.toggle("hidden", false); + return; + } + const root = (this.#dropdown = this.#getDropdownRoot( + AnnotationEditorParamsType.HIGHLIGHT_COLOR + )); + this.#button.append(root); + } + + hideDropdown() { + this.#dropdown?.classList.add("hidden"); + } + + _hideDropdownFromKeyboard() { + if ( + this.#isMainColorPicker || + !this.#dropdown || + this.#dropdown.classList.contains("hidden") + ) { + return; + } + this.hideDropdown(); + this.#button.removeEventListener("keydown", this.#boundKeyDown); + this.#button.focus({ + preventScroll: true, + focusVisible: this.#dropdownWasFromKeyboard, + }); + } + + updateColor(color) { + if (this.#buttonSwatch) { + this.#buttonSwatch.style.backgroundColor = color; + } + if (!this.#dropdown) { + return; + } + + const i = this.#uiManager.highlightColors.values(); + for (const child of this.#dropdown.children) { + const isSelected = i.next().value === color; + child.setAttribute("aria-selected", isSelected); + child.classList.toggle("selected", isSelected); + } + } + + destroy() { + this.#button?.remove(); + this.#button = null; + this.#buttonSwatch = null; + this.#dropdown?.remove(); + this.#dropdown = null; + } +} + +export { ColorPicker }; diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index a9b6ac4cd7e05..0390228acc48f 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -903,15 +903,21 @@ class AnnotationEditor { this.#altText?.finish(); } + /** + * Add a toolbar for this editor. + * @returns {Promise} + */ async addEditToolbar() { if (this.#editToolbar || this.#isInEditMode) { - return; + return this.#editToolbar; } this.#editToolbar = new EditorToolbar(this); this.div.append(this.#editToolbar.render()); if (this.#altText) { this.#editToolbar.addAltTextButton(await this.#altText.render()); } + + return this.#editToolbar; } removeEditToolbar() { diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index c9b85a9e3c58b..24721d2961d27 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -20,6 +20,7 @@ import { } from "../../shared/util.js"; import { AnnotationEditor } from "./editor.js"; import { bindEvents } from "./tools.js"; +import { ColorPicker } from "./color_picker.js"; import { Outliner } from "./outliner.js"; /** @@ -30,7 +31,7 @@ class HighlightEditor extends AnnotationEditor { #clipPathId = null; - #color; + #colorPicker = null; #focusOutlines = null; @@ -46,9 +47,9 @@ class HighlightEditor extends AnnotationEditor { #outlineId = null; - static _defaultColor = "#FFF066"; + static _defaultColor = null; - static _defaultOpacity = 0.4; + static _defaultOpacity = 1; static _l10nPromise; @@ -58,7 +59,9 @@ class HighlightEditor extends AnnotationEditor { constructor(params) { super({ ...params, name: "highlightEditor" }); - this.#color = params.color || HighlightEditor._defaultColor; + HighlightEditor._defaultColor ||= + this._uiManager.highlightColors?.values().next().value || "#fff066"; + this.color = params.color || HighlightEditor._defaultColor; this.#opacity = params.opacity || HighlightEditor._defaultOpacity; this.#boxes = params.boxes || null; this._isDraggable = false; @@ -100,12 +103,9 @@ class HighlightEditor extends AnnotationEditor { static updateDefaultParams(type, value) { switch (type) { - case AnnotationEditorParamsType.HIGHLIGHT_COLOR: + case AnnotationEditorParamsType.DEFAULT_HIGHLIGHT_COLOR: HighlightEditor._defaultColor = value; break; - case AnnotationEditorParamsType.HIGHLIGHT_OPACITY: - HighlightEditor._defaultOpacity = value / 100; - break; } } @@ -120,22 +120,15 @@ class HighlightEditor extends AnnotationEditor { case AnnotationEditorParamsType.HIGHLIGHT_COLOR: this.#updateColor(value); break; - case AnnotationEditorParamsType.HIGHLIGHT_OPACITY: - this.#updateOpacity(value); - break; } } static get defaultPropertiesToUpdate() { return [ [ - AnnotationEditorParamsType.HIGHLIGHT_COLOR, + AnnotationEditorParamsType.DEFAULT_HIGHLIGHT_COLOR, HighlightEditor._defaultColor, ], - [ - AnnotationEditorParamsType.HIGHLIGHT_OPACITY, - Math.round(HighlightEditor._defaultOpacity * 100), - ], ]; } @@ -144,11 +137,7 @@ class HighlightEditor extends AnnotationEditor { return [ [ AnnotationEditorParamsType.HIGHLIGHT_COLOR, - this.#color || HighlightEditor._defaultColor, - ], - [ - AnnotationEditorParamsType.HIGHLIGHT_OPACITY, - Math.round(100 * (this.#opacity ?? HighlightEditor._defaultOpacity)), + this.color || HighlightEditor._defaultColor, ], ]; } @@ -161,12 +150,14 @@ class HighlightEditor extends AnnotationEditor { const savedColor = this.color; this.addCommands({ cmd: () => { - this.#color = color; + this.color = color; this.parent.drawLayer.changeColor(this.#id, color); + this.#colorPicker?.updateColor(color); }, undo: () => { - this.#color = savedColor; + this.color = savedColor; this.parent.drawLayer.changeColor(this.#id, savedColor); + this.#colorPicker?.updateColor(savedColor); }, mustExec: true, type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, @@ -175,27 +166,17 @@ class HighlightEditor extends AnnotationEditor { }); } - /** - * Update the opacity and make this action undoable. - * @param {number} opacity - */ - #updateOpacity(opacity) { - opacity /= 100; - const savedOpacity = this.#opacity; - this.addCommands({ - cmd: () => { - this.#opacity = opacity; - this.parent.drawLayer.changeOpacity(this.#id, opacity); - }, - undo: () => { - this.#opacity = savedOpacity; - this.parent.drawLayer.changeOpacity(this.#id, savedOpacity); - }, - mustExec: true, - type: AnnotationEditorParamsType.HIGHLIGHT_OPACITY, - overwriteIfSameType: true, - keepUndo: true, - }); + /** @inheritdoc */ + async addEditToolbar() { + const toolbar = await super.addEditToolbar(); + if (!toolbar) { + return null; + } + if (this._uiManager.highlightColors) { + this.#colorPicker = new ColorPicker({ editor: this }); + toolbar.addColorPicker(this.#colorPicker); + } + return toolbar; } /** @inheritdoc */ @@ -265,7 +246,7 @@ class HighlightEditor extends AnnotationEditor { ({ id: this.#id, clipPathId: this.#clipPathId } = parent.drawLayer.highlight( this.#highlightOutlines, - this.#color, + this.color, this.#opacity )); if (this.#highlightDiv) { @@ -403,7 +384,7 @@ class HighlightEditor extends AnnotationEditor { const editor = super.deserialize(data, parent, uiManager); const { rect, color, quadPoints } = data; - editor.#color = Util.makeHexColor(...color); + editor.color = Util.makeHexColor(...color); editor.#opacity = data.opacity; const [pageWidth, pageHeight] = editor.pageDimensions; @@ -431,7 +412,7 @@ class HighlightEditor extends AnnotationEditor { } const rect = this.getRect(0, 0); - const color = AnnotationEditor._colorManager.convert(this.#color); + const color = AnnotationEditor._colorManager.convert(this.color); return { annotationType: AnnotationEditorType.HIGHLIGHT, diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js index 5bb65d44a98fb..5d62f4b0a3842 100644 --- a/src/display/editor/toolbar.js +++ b/src/display/editor/toolbar.js @@ -18,6 +18,8 @@ import { noContextMenu } from "../display_utils.js"; class EditorToolbar { #toolbar = null; + #colorPicker = null; + #editor; #buttons = null; @@ -85,6 +87,7 @@ class EditorToolbar { hide() { this.#toolbar.classList.add("hidden"); + this.#colorPicker?.hideDropdown(); } show() { @@ -106,19 +109,28 @@ class EditorToolbar { this.#buttons.append(button); } + get #divider() { + const divider = document.createElement("div"); + divider.className = "divider"; + return divider; + } + addAltTextButton(button) { this.#addListenersToElement(button); this.#buttons.prepend(button, this.#divider); } - get #divider() { - const divider = document.createElement("div"); - divider.className = "divider"; - return divider; + addColorPicker(colorPicker) { + this.#colorPicker = colorPicker; + const button = colorPicker.renderButton(); + this.#addListenersToElement(button); + this.#buttons.append(this.#divider, button); } remove() { this.#toolbar.remove(); + this.#colorPicker?.destroy(); + this.#colorPicker = null; } } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index f4b490c607491..62048fb2cc0ee 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -438,7 +438,7 @@ class KeyboardManager { if (checker && !checker(self, event)) { return; } - callback.bind(self, ...args)(); + callback.bind(self, ...args, event)(); // For example, ctrl+s in a FreeText must be handled by the viewer, hence // the event must bubble. @@ -545,6 +545,8 @@ class AnnotationEditorUIManager { #focusMainContainerTimeoutId = null; + #highlightColors = null; + #idManager = new IdManager(); #isEnabled = false; @@ -553,6 +555,8 @@ class AnnotationEditorUIManager { #lastActiveElement = null; + #mainHighlightColorPicker = null; + #mode = AnnotationEditorType.NONE; #selectedEditors = new Set(); @@ -607,6 +611,7 @@ class AnnotationEditorUIManager { // For example, sliders can be controlled with the arrow keys. return ( self.#container.contains(document.activeElement) && + document.activeElement.tagName !== "BUTTON" && self.hasSomethingToControl() ); }; @@ -736,7 +741,8 @@ class AnnotationEditorUIManager { altTextManager, eventBus, pdfDocument, - pageColors + pageColors, + highlightColors ) { this.#container = container; this.#viewer = viewer; @@ -749,6 +755,7 @@ class AnnotationEditorUIManager { this.#annotationStorage = pdfDocument.annotationStorage; this.#filterFactory = pdfDocument.filterFactory; this.#pageColors = pageColors; + this.#highlightColors = highlightColors || null; this.viewParameters = { realScale: PixelsPerInch.PDF_TO_CSS_UNITS, rotation: 0, @@ -803,6 +810,24 @@ class AnnotationEditorUIManager { ); } + get highlightColors() { + return shadow( + this, + "highlightColors", + this.#highlightColors + ? new Map( + this.#highlightColors + .split(",") + .map(pair => pair.split("=").map(x => x.trim())) + ) + : null + ); + } + + setMainHighlightColorPicker(colorPicker) { + this.#mainHighlightColorPicker = colorPicker; + } + editAltText(editor) { this.#altTextManager?.editAltText(this, editor); } @@ -1246,9 +1271,14 @@ class AnnotationEditorUIManager { if (!this.#editorTypes) { return; } - if (type === AnnotationEditorParamsType.CREATE) { - this.currentLayer.addNewEditor(); - return; + + switch (type) { + case AnnotationEditorParamsType.CREATE: + this.currentLayer.addNewEditor(); + return; + case AnnotationEditorParamsType.DEFAULT_HIGHLIGHT_COLOR: + this.#mainHighlightColorPicker?.updateColor(value); + break; } for (const editor of this.#selectedEditors) { diff --git a/src/pdf.js b/src/pdf.js index 12031ac46ed41..257b39634908e 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -70,6 +70,7 @@ import { renderTextLayer, updateTextLayer } from "./display/text_layer.js"; import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "./display/editor/tools.js"; import { AnnotationLayer } from "./display/annotation_layer.js"; +import { ColorPicker } from "./display/editor/color_picker.js"; import { DrawLayer } from "./display/draw_layer.js"; import { GlobalWorkerOptions } from "./display/worker_options.js"; import { Outliner } from "./display/editor/outliner.js"; @@ -92,6 +93,7 @@ export { AnnotationMode, build, CMapCompressionType, + ColorPicker, createValidAbsoluteUrl, DOMSVGFactory, DrawLayer, diff --git a/src/shared/util.js b/src/shared/util.js index 068874cb9a97b..f3c0318fbe235 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -87,7 +87,7 @@ const AnnotationEditorParamsType = { INK_THICKNESS: 22, INK_OPACITY: 23, HIGHLIGHT_COLOR: 31, - HIGHLIGHT_OPACITY: 32, + DEFAULT_HIGHLIGHT_COLOR: 32, }; // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 4c98af5e72802..b47656cf90f23 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -63,6 +63,7 @@ import { import { AnnotationEditorLayer } from "../../src/display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "../../src/display/editor/tools.js"; import { AnnotationLayer } from "../../src/display/annotation_layer.js"; +import { ColorPicker } from "../../src/display/editor/color_picker.js"; import { DrawLayer } from "../../src/display/draw_layer.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; import { Outliner } from "../../src/display/editor/outliner.js"; @@ -78,6 +79,7 @@ const expectedAPI = Object.freeze({ AnnotationMode, build, CMapCompressionType, + ColorPicker, createValidAbsoluteUrl, DOMSVGFactory, DrawLayer, diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 206b06a6f696e..e5307aab461bd 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -907,6 +907,38 @@ } } +.colorPicker { + --hover-outline-color: #0250bb; + --selected-outline-color: #0060df; + --swatch-border-color: #cfcfd8; + + @media (prefers-color-scheme: dark) { + --hover-outline-color: #80ebff; + --selected-outline-color: #aaf2ff; + --swatch-border-color: #52525e; + } + + @media screen and (forced-colors: active) { + --hover-outline-color: Highlight; + --selected-outline-color: var(--hover-outline-color); + --swatch-border-color: ButtonText; + } + + .swatch { + width: 16px; + height: 16px; + border: 1px solid var(--swatch-border-color); + border-radius: 100%; + outline-offset: 2px; + box-sizing: border-box; + forced-color-adjust: none; + } + + button:is(:hover, .selected) > .swatch { + border: none; + } +} + .annotationEditorLayer { &[data-main-rotation="0"] { .highlightEditor > .editToolbar { @@ -961,7 +993,148 @@ } .editToolbar { + --editor-toolbar-colorpicker-arrow-image: url(images/editor-toolbar-colorpicker-arrow.svg); + transform-origin: center !important; + + .buttons { + .colorPicker { + position: relative; + width: auto; + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + padding: 4px; + + &::after { + content: ""; + mask-image: var(--editor-toolbar-colorpicker-arrow-image); + mask-repeat: no-repeat; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 12px; + height: 12px; + } + + &:hover::after { + background-color: var(--editor-toolbar-hover-fg-color); + } + + &:has(.dropdown:not(.hidden)) { + background-color: var(--editor-toolbar-hover-bg-color); + } + + .dropdown { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 11px; + padding-block: 8px; + border-radius: 6px; + background-color: var(--editor-toolbar-bg-color); + border: 1px solid var(--editor-toolbar-border-color); + box-shadow: var(--editor-toolbar-shadow); + inset-block-start: calc(100% + 3.5px); + inset-inline-end: calc(0px - var(--editor-toolbar-padding)); + width: calc(100% + 2 * var(--editor-toolbar-padding)); + + button { + width: 100%; + height: auto; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + background: none; + + &:is(:active, :focus-visible) { + outline: none; + } + + > .swatch { + outline-offset: 2px; + } + + &.selected > .swatch { + outline: 2px solid var(--selected-outline-color); + } + + &:is(:hover, :active, :focus-visible) > .swatch { + outline: 2px solid var(--hover-outline-color); + } + } + } + } + } + } + } +} + +.editorParamsToolbar:has(#highlightParamsToolbarContainer) { + padding: unset; +} + +#highlightParamsToolbarContainer { + height: auto; + padding: 16px; + display: flex; + flex-direction: column; + box-sizing: border-box; + + .colorPicker { + display: flex; + flex-direction: column; + gap: 8px; + + #highlightColorPickerLabel { + color: var(--main-color); + font: message-box; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 150%; + } + + .dropdown { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + height: auto; + + button { + width: auto; + height: auto; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + background: none; + flex: 0 0 auto; + + .swatch { + width: 24px; + height: 24px; + } + + &:is(:active, :focus-visible) { + outline: none; + } + + &.selected > .swatch { + outline: 2px solid var(--selected-outline-color); + } + + &:is(:hover, :active, :focus-visible) > .swatch { + outline: 2px solid var(--hover-outline-color); + } + } } } } diff --git a/web/annotation_editor_params.js b/web/annotation_editor_params.js index ee84fe4a76a80..ddfc17d1ab14c 100644 --- a/web/annotation_editor_params.js +++ b/web/annotation_editor_params.js @@ -28,8 +28,6 @@ class AnnotationEditorParams { #bindListeners({ editorFreeTextFontSize, editorFreeTextColor, - editorHighlightColor, - editorHighlightOpacity, editorInkColor, editorInkThickness, editorInkOpacity, @@ -48,12 +46,6 @@ class AnnotationEditorParams { editorFreeTextColor.addEventListener("input", function () { dispatchEvent("FREETEXT_COLOR", this.value); }); - editorHighlightColor.addEventListener("input", function () { - dispatchEvent("HIGHLIGHT_COLOR", this.value); - }); - editorHighlightOpacity.addEventListener("input", function () { - dispatchEvent("HIGHLIGHT_OPACITY", this.valueAsNumber); - }); editorInkColor.addEventListener("input", function () { dispatchEvent("INK_COLOR", this.value); }); @@ -76,12 +68,6 @@ class AnnotationEditorParams { case AnnotationEditorParamsType.FREETEXT_COLOR: editorFreeTextColor.value = value; break; - case AnnotationEditorParamsType.HIGHLIGHT_COLOR: - editorHighlightColor.value = value; - break; - case AnnotationEditorParamsType.HIGHLIGHT_OPACITY: - editorHighlightOpacity.value = value; - break; case AnnotationEditorParamsType.INK_COLOR: editorInkColor.value = value; break; diff --git a/web/app.js b/web/app.js index 253a6d78f40f0..8067f5196279b 100644 --- a/web/app.js +++ b/web/app.js @@ -442,6 +442,7 @@ const PDFViewerApplication = { textLayerMode: AppOptions.get("textLayerMode"), annotationMode: AppOptions.get("annotationMode"), annotationEditorMode, + annotationEditorHighlightColors: AppOptions.get("highlightColors"), imageResourcesPath: AppOptions.get("imageResourcesPath"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), isOffscreenCanvasSupported, diff --git a/web/app_options.js b/web/app_options.js index caf4dcd76aba6..2ddd7c82017c4 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -158,6 +158,14 @@ const defaultOptions = { value: 0, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + highlightColors: { + /** @type {string} */ + value: + typeof PDFJSDev === "undefined" + ? "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F" + : "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, historyUpdateUrl: { /** @type {boolean} */ value: false, diff --git a/web/draw_layer_builder.css b/web/draw_layer_builder.css index 4b59c8d962133..919ad7b2b8b0e 100644 --- a/web/draw_layer_builder.css +++ b/web/draw_layer_builder.css @@ -36,8 +36,14 @@ } &.highlight { + --blend-mode: multiply; + + @media screen and (forced-colors: active) { + --blend-mode: difference; + } + position: absolute; - mix-blend-mode: multiply; + mix-blend-mode: var(--blend-mode); fill-rule: evenodd; } diff --git a/web/images/editor-toolbar-colorpicker-arrow.svg b/web/images/editor-toolbar-colorpicker-arrow.svg new file mode 100644 index 0000000000000..be4f2e6bd8f98 --- /dev/null +++ b/web/images/editor-toolbar-colorpicker-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index b9ad52f196788..75dc069aab11d 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -109,6 +109,8 @@ function isValidAnnotationEditorMode(mode) { * @property {number} [annotationEditorMode] - Enables the creation and editing * of new Annotations. The constants from {@link AnnotationEditorType} should * be used. The default value is `AnnotationEditorType.NONE`. + * @property {string} [annotationEditorHighlightColors] - A comma separated list + * of colors to propose to highlight some text in the pdf. * @property {string} [imageResourcesPath] - Path for image resources, mainly * mainly for annotation icons. Include trailing slash. * @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of @@ -202,6 +204,8 @@ class PDFViewer { #altTextManager = null; + #annotationEditorHighlightColors = null; + #annotationEditorMode = AnnotationEditorType.NONE; #annotationEditorUIManager = null; @@ -276,6 +280,8 @@ class PDFViewer { options.annotationMode ?? AnnotationMode.ENABLE_FORMS; this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE; + this.#annotationEditorHighlightColors = + options.annotationEditorHighlightColors || null; this.imageResourcesPath = options.imageResourcesPath || ""; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { @@ -862,8 +868,13 @@ class PDFViewer { this.#altTextManager, this.eventBus, pdfDocument, - this.pageColors + this.pageColors, + this.#annotationEditorHighlightColors ); + this.eventBus.dispatch("annotationeditoruimanager", { + source: this, + uiManager: this.#annotationEditorUIManager, + }); if (mode !== AnnotationEditorType.NONE) { this.#annotationEditorUIManager.updateMode(mode); } diff --git a/web/pdfjs.js b/web/pdfjs.js index 58fcbbea4e503..aff0e95cccc4e 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -32,6 +32,7 @@ const { AnnotationMode, build, CMapCompressionType, + ColorPicker, createValidAbsoluteUrl, DOMSVGFactory, DrawLayer, @@ -80,6 +81,7 @@ export { AnnotationMode, build, CMapCompressionType, + ColorPicker, createValidAbsoluteUrl, DOMSVGFactory, DrawLayer, diff --git a/web/toolbar.js b/web/toolbar.js index 1f31a642368f9..9e8499fe51860 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { AnnotationEditorType, noContextMenu } from "pdfjs-lib"; +import { AnnotationEditorType, ColorPicker, noContextMenu } from "pdfjs-lib"; import { DEFAULT_SCALE, DEFAULT_SCALE_VALUE, @@ -120,9 +120,28 @@ class Toolbar { // Bind the event listeners for click and various other actions. this.#bindListeners(options); + if (options.editorHighlightParamsToolbar) { + this.eventBus._on("annotationeditoruimanager", ({ uiManager }) => { + this.#setAnnotationEditorUIManager( + uiManager, + options.editorHighlightParamsToolbar + ); + }); + } + this.reset(); } + #setAnnotationEditorUIManager(uiManager, parentContainer) { + const container = document.createElement("div"); + container.id = "highlightParamsToolbarContainer"; + container.className = "editorParamsToolbarContainer"; + parentContainer.append(container); + const colorPicker = new ColorPicker({ uiManager }); + uiManager.setMainHighlightColorPicker(colorPicker); + container.append(colorPicker.renderMainDropdown()); + } + setPageNumber(pageNumber, pageLabel) { this.pageNumber = pageNumber; this.pageLabel = pageLabel; diff --git a/web/viewer.html b/web/viewer.html index 4aab7094b236e..c84db693a10e5 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -172,16 +172,6 @@