[Editor] Add a color picker in the toolbar of Ink and Freetext annotations

This commit is contained in:
Calixte Denizet 2025-07-08 22:40:41 +02:00
parent e0783cd075
commit dbd6f8cdd4
10 changed files with 207 additions and 8 deletions

View File

@ -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

View File

@ -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 };

View File

@ -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),

View File

@ -1059,7 +1059,7 @@ class AnnotationEditor {
/**
* Get the toolbar buttons for this editor.
* @returns {Array<Array<string|object>>|null}
* @returns {Array<Array<string|object|null>>|null}
*/
get toolbarButtons() {
return null;

View File

@ -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({

View File

@ -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) {

View File

@ -1805,14 +1805,16 @@ class AnnotationEditorUIManager {
break;
}
if (this.hasSelection) {
for (const editor of this.#selectedEditors) {
editor.updateParams(type, value);
}
} else {
for (const editorType of this.#editorTypes) {
editorType.updateDefaultParams(type, value);
}
}
}
showAllEditors(type, visible, updateButton = false) {
for (const editor of this.#allEditors.values()) {

View File

@ -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`
);
})
);
});
});
});

View File

@ -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 }
);
})
);
});
});

View File

@ -1057,6 +1057,10 @@
}
}
.basicColorPicker {
width: 28px;
}
.annotationEditorLayer {
&[data-main-rotation="0"] {
.highlightEditor:not(.free) > .editToolbar {