From dbd6f8cdd4641d74ddb7a39a7911f494b7ec1c52 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 8 Jul 2025 22:40:41 +0200 Subject: [PATCH] [Editor] Add a color picker in the toolbar of Ink and Freetext annotations --- l10n/en-US/viewer.ftl | 4 ++ src/display/editor/color_picker.js | 67 ++++++++++++++++++++++- src/display/editor/draw.js | 5 ++ src/display/editor/editor.js | 2 +- src/display/editor/freetext.js | 18 ++++++ src/display/editor/ink.js | 15 +++++ src/display/editor/tools.js | 14 +++-- test/integration/freetext_editor_spec.mjs | 44 +++++++++++++++ test/integration/ink_editor_spec.mjs | 42 ++++++++++++++ web/annotation_editor_layer_builder.css | 4 ++ 10 files changed, 207 insertions(+), 8 deletions(-) diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index fa1ea61dc..3094c777d 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -306,9 +306,13 @@ pdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fo pdfjs-editor-free-text-button = .title = Text +pdfjs-editor-color-picker-free-text-input = + .title = Change text color pdfjs-editor-free-text-button-label = Text pdfjs-editor-ink-button = .title = Draw +pdfjs-editor-color-picker-ink-input = + .title = Change drawing color pdfjs-editor-ink-button-label = Draw pdfjs-editor-stamp-button = .title = Add or edit images diff --git a/src/display/editor/color_picker.js b/src/display/editor/color_picker.js index a604ff5c5..8e302e04e 100644 --- a/src/display/editor/color_picker.js +++ b/src/display/editor/color_picker.js @@ -17,6 +17,11 @@ import { AnnotationEditorParamsType, shadow } from "../../shared/util.js"; import { KeyboardManager } from "./tools.js"; import { noContextMenu } from "../display_utils.js"; +/** + * ColorPicker class provides a color picker for the annotation editor. + * It displays a dropdown with some predefined colors and allows the user + * to select a color for the annotation. + */ class ColorPicker { #button = null; @@ -304,4 +309,64 @@ class ColorPicker { } } -export { ColorPicker }; +/** + * BasicColorPicker class provides a simple color picker. + * It displays an input element (with type="color") that allows the user + * to select a color for the annotation. + */ +class BasicColorPicker { + #input = null; + + #editor = null; + + #uiManager = null; + + static #l10nColor = null; + + constructor(editor) { + this.#editor = editor; + this.#uiManager = editor._uiManager; + + BasicColorPicker.#l10nColor ||= Object.freeze({ + freetext: "pdfjs-editor-color-picker-free-text-input", + ink: "pdfjs-editor-color-picker-ink-input", + }); + } + + renderButton() { + if (this.#input) { + return this.#input; + } + const { editorType, colorType, colorValue } = this.#editor; + const input = (this.#input = document.createElement("input")); + input.type = "color"; + input.value = colorValue || "#000000"; + input.className = "basicColorPicker"; + input.tabIndex = 0; + input.setAttribute("data-l10n-id", BasicColorPicker.#l10nColor[editorType]); + input.addEventListener( + "input", + () => { + this.#uiManager.updateParams(colorType, input.value); + }, + { signal: this.#uiManager._signal } + ); + return input; + } + + update(value) { + if (!this.#input) { + return; + } + this.#input.value = value; + } + + destroy() { + this.#input?.remove(); + this.#input = null; + } + + hideDropdown() {} +} + +export { BasicColorPicker, ColorPicker }; diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index 25d05508a..faaeb516a 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -67,6 +67,8 @@ class DrawingEditor extends AnnotationEditor { #mustBeCommitted; + _colorPicker = null; + _drawId = null; static _currentDrawId = -1; @@ -240,6 +242,9 @@ class DrawingEditor extends AnnotationEditor { this._drawId, options.toSVGProperties() ); + if (type === this.colorType) { + this._colorPicker?.update(val); + } }; this.addCommands({ cmd: setter.bind(this, value), diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index a0153b63f..0ad3cf242 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1059,7 +1059,7 @@ class AnnotationEditor { /** * Get the toolbar buttons for this editor. - * @returns {Array>|null} + * @returns {Array>|null} */ get toolbarButtons() { return null; diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index b362eca35..70c6eba1c 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -26,6 +26,7 @@ import { } from "../../shared/util.js"; import { AnnotationEditorUIManager, KeyboardManager } from "./tools.js"; import { AnnotationEditor } from "./editor.js"; +import { BasicColorPicker } from "./color_picker.js"; import { FreeTextAnnotationElement } from "../annotation_layer.js"; const EOL_PATTERN = /\r\n?|\n/g; @@ -44,6 +45,8 @@ class FreeTextEditor extends AnnotationEditor { #fontSize; + _colorPicker = null; + static _freeTextDefaultContent = ""; static _internalPadding = 0; @@ -202,6 +205,20 @@ class FreeTextEditor extends AnnotationEditor { ]; } + /** @inheritdoc */ + get toolbarButtons() { + this._colorPicker ||= new BasicColorPicker(this); + return [["colorPicker", this._colorPicker]]; + } + + get colorType() { + return AnnotationEditorParamsType.FREETEXT_COLOR; + } + + get colorValue() { + return this.#color; + } + /** * Update the font size and make this action as undoable. * @param {number} fontSize @@ -232,6 +249,7 @@ class FreeTextEditor extends AnnotationEditor { #updateColor(color) { const setColor = col => { this.#color = this.editorDiv.style.color = col; + this._colorPicker?.update(col); }; const savedColor = this.#color; this.addCommands({ diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 56f6c560a..c5024ea63 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -22,6 +22,7 @@ import { import { DrawingEditor, DrawingOptions } from "./draw.js"; import { InkDrawOutline, InkDrawOutliner } from "./drawers/inkdraw.js"; import { AnnotationEditor } from "./editor.js"; +import { BasicColorPicker } from "./color_picker.js"; import { InkAnnotationElement } from "../annotation_layer.js"; class InkDrawingOptions extends DrawingOptions { @@ -177,6 +178,20 @@ class InkEditor extends DrawingEditor { return editor; } + /** @inheritdoc */ + get toolbarButtons() { + this._colorPicker ||= new BasicColorPicker(this); + return [["colorPicker", this._colorPicker]]; + } + + get colorType() { + return AnnotationEditorParamsType.INK_COLOR; + } + + get colorValue() { + return this._drawingOptions.stroke; + } + /** @inheritdoc */ onScaleChanging() { if (!this.parent) { diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index c7a78139d..37c442d31 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1805,12 +1805,14 @@ class AnnotationEditorUIManager { break; } - for (const editor of this.#selectedEditors) { - editor.updateParams(type, value); - } - - for (const editorType of this.#editorTypes) { - editorType.updateDefaultParams(type, value); + if (this.hasSelection) { + for (const editor of this.#selectedEditors) { + editor.updateParams(type, value); + } + } else { + for (const editorType of this.#editorTypes) { + editorType.updateDefaultParams(type, value); + } } } diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 311eb1306..be18987a0 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -3502,4 +3502,48 @@ describe("FreeText Editor", () => { ); }); }); + + describe("FreeText must update its color", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the text color is the one chosen from the color picker", async () => { + await Promise.all( + pages.map(async ([_, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const editorSelector = getEditorSelector(0); + const data = "Hello PDF.js World !!"; + await page.mouse.click( + rect.x + rect.width / 2, + rect.y + rect.height / 2 + ); + await page.waitForSelector(editorSelector, { visible: true }); + await page.type(`${editorSelector} .internal`, data); + await commit(page); + + const colorPickerSelector = `${editorSelector} input.basicColorPicker`; + await page.waitForSelector(colorPickerSelector, { visible: true }); + await page.locator(colorPickerSelector).fill("#ff0000"); + + await page.waitForFunction( + sel => { + const el = document.querySelector(sel); + return getComputedStyle(el).color === "rgb(255, 0, 0)"; + }, + {}, + `${editorSelector} .internal` + ); + }) + ); + }); + }); }); diff --git a/test/integration/ink_editor_spec.mjs b/test/integration/ink_editor_spec.mjs index 755b02e19..a80d9a615 100644 --- a/test/integration/ink_editor_spec.mjs +++ b/test/integration/ink_editor_spec.mjs @@ -1294,3 +1294,45 @@ describe("Should switch from an editor and mode to others by double clicking", ( ); }); }); + +describe("Ink must update its color", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the stroke color is the one chosen from the color picker", async () => { + await Promise.all( + pages.map(async ([_, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + + const x = rect.x + 20; + const y = rect.y + 20; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(x, y); + await page.mouse.down(); + await page.mouse.move(x + 50, y + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + const editorSelector = getEditorSelector(0); + const colorPickerSelector = `${editorSelector} input.basicColorPicker`; + await page.waitForSelector(colorPickerSelector, { visible: true }); + await page.locator(colorPickerSelector).fill("#ff0000"); + + await page.waitForSelector( + ".canvasWrapper svg.draw[stroke='#ff0000']", + { visible: true } + ); + }) + ); + }); +}); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 49e235b81..510dd4bd3 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -1057,6 +1057,10 @@ } } +.basicColorPicker { + width: 28px; +} + .annotationEditorLayer { &[data-main-rotation="0"] { .highlightEditor:not(.free) > .editToolbar {