Skip to content

Commit

Permalink
[Editor] Add a color picker with predefined colors for hightlighting …
Browse files Browse the repository at this point in the history
…text (bug 1866434)

The doorhanger for highlighting has a basic color picker composed of 5 predefined colors
to set the default color to use.
These colors can be changed thanks to a preference for now but it's something which could
be changed in the Firefox settings in the future.
Each highlight has in its own toolbar a color picker to just change its color.
The different color pickers are so similar (modulo few differences in their styles) that
this patch introduces a new class ColorPicker which provides a color picker component
which could be reused in future editors.
All in all, a large part of this patch is dedicated to color picker itself and its style
and the rest is almost a matter of wiring the component.
  • Loading branch information
calixteman committed Dec 1, 2023
1 parent 9ac1ac6 commit 25d1b77
Show file tree
Hide file tree
Showing 21 changed files with 570 additions and 85 deletions.
4 changes: 4 additions & 0 deletions extensions/chromium/preferences_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"type": "boolean",
"default": false
},
"highlightColors": {
"type": "string",
"default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F"
},
"disableRange": {
"title": "Disable range requests",
"description": "Whether to disable range requests (not recommended).",
Expand Down
22 changes: 22 additions & 0 deletions l10n/en-US/viewer.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
##

Expand Down Expand Up @@ -384,3 +386,23 @@ 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

# This means "Color used to highlight text"
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
228 changes: 228 additions & 0 deletions src/display/editor/color_picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/* 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 dropdown = (this.#dropdown = this.#getDropdownRoot(
AnnotationEditorParamsType.DEFAULT_HIGHLIGHT_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.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) {
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 };
8 changes: 7 additions & 1 deletion src/display/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 25d1b77

Please sign in to comment.