1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 22:58:07 +02:00

Merge pull request #17359 from calixteman/editor_highlight_color_picker

[Editor] Add a color picker with predefined colors for highlighting text (bug 1866434)
This commit is contained in:
calixteman 2023-12-06 11:06:55 +01:00 committed by GitHub
commit 8702e1bbb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 573 additions and 87 deletions

View file

@ -0,0 +1,230 @@
/* 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 ||
"#FFFF98";
}
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 dropdown = (this.#dropdown = this.#getDropdownRoot(
AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR
));
dropdown.setAttribute("aria-orientation", "horizontal");
dropdown.setAttribute("aria-labelledby", "highlightColorPickerLabel");
return dropdown;
}
#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.title = name;
button.setAttribute("data-l10n-id", `pdfjs-editor-colorpicker-${name}`);
const swatch = document.createElement("span");
button.append(swatch);
swatch.className = "swatch";
swatch.style.backgroundColor = color;
button.setAttribute("aria-selected", color === this.#defaultColor);
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.remove("hidden");
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) {
child.setAttribute("aria-selected", i.next().value === color);
}
}
destroy() {
this.#button?.remove();
this.#button = null;
this.#buttonSwatch = null;
this.#dropdown?.remove();
this.#dropdown = null;
}
}
export { ColorPicker };

View file

@ -903,15 +903,21 @@ class AnnotationEditor {
this.#altText?.finish();
}
/**
* Add a toolbar for this editor.
* @returns {Promise<EditorToolbar|null>}
*/
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() {

View file

@ -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.HIGHLIGHT_DEFAULT_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.HIGHLIGHT_DEFAULT_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 */
@ -286,7 +267,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) {
@ -424,7 +405,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;
@ -452,7 +433,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,

View file

@ -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);
}
addAltTextButton(button) {
this.#addListenersToElement(button);
this.#buttons.prepend(button, this.#divider);
}
get #divider() {
const divider = document.createElement("div");
divider.className = "divider";
return divider;
}
addAltTextButton(button) {
this.#addListenersToElement(button);
this.#buttons.prepend(button, this.#divider);
}
addColorPicker(colorPicker) {
this.#colorPicker = colorPicker;
const button = colorPicker.renderButton();
this.#addListenersToElement(button);
this.#buttons.prepend(button, this.#divider);
}
remove() {
this.#toolbar.remove();
this.#colorPicker?.destroy();
this.#colorPicker = null;
}
}

View file

@ -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.HIGHLIGHT_DEFAULT_COLOR:
this.#mainHighlightColorPicker?.updateColor(value);
break;
}
for (const editor of this.#selectedEditors) {

View file

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

View file

@ -87,7 +87,7 @@ const AnnotationEditorParamsType = {
INK_THICKNESS: 22,
INK_OPACITY: 23,
HIGHLIGHT_COLOR: 31,
HIGHLIGHT_OPACITY: 32,
HIGHLIGHT_DEFAULT_COLOR: 32,
};
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.