mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
Merge pull request #17062 from calixteman/resize_keyboard
[Editor] Support resizing editors with the keyboard (bug 1854340)
This commit is contained in:
commit
40d6b0e1bf
5 changed files with 469 additions and 88 deletions
|
@ -15,10 +15,13 @@
|
|||
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
|
||||
import { bindEvents, ColorManager } from "./tools.js";
|
||||
import {
|
||||
AnnotationEditorUIManager,
|
||||
bindEvents,
|
||||
ColorManager,
|
||||
KeyboardManager,
|
||||
} from "./tools.js";
|
||||
import { FeatureTest, shadow, unreachable } from "../../shared/util.js";
|
||||
import { noContextMenu } from "../display_utils.js";
|
||||
|
||||
|
@ -35,6 +38,8 @@ import { noContextMenu } from "../display_utils.js";
|
|||
* Base class for editors.
|
||||
*/
|
||||
class AnnotationEditor {
|
||||
#allResizerDivs = null;
|
||||
|
||||
#altText = "";
|
||||
|
||||
#altTextDecorative = false;
|
||||
|
@ -51,16 +56,22 @@ class AnnotationEditor {
|
|||
|
||||
#resizersDiv = null;
|
||||
|
||||
#savedDimensions = null;
|
||||
|
||||
#boundFocusin = this.focusin.bind(this);
|
||||
|
||||
#boundFocusout = this.focusout.bind(this);
|
||||
|
||||
#focusedResizerName = "";
|
||||
|
||||
#hasBeenClicked = false;
|
||||
|
||||
#isEditing = false;
|
||||
|
||||
#isInEditMode = false;
|
||||
|
||||
#isResizerEnabledForKeyboard = false;
|
||||
|
||||
#moveInDOMTimeout = null;
|
||||
|
||||
_initialOptions = Object.create(null);
|
||||
|
@ -85,6 +96,39 @@ class AnnotationEditor {
|
|||
// button to edit the alt text is visually moved outside of the editor.
|
||||
static SMALL_EDITOR_SIZE = 0;
|
||||
|
||||
static get _resizerKeyboardManager() {
|
||||
const resize = AnnotationEditor.prototype._resizeWithKeyboard;
|
||||
const small = AnnotationEditorUIManager.TRANSLATE_SMALL;
|
||||
const big = AnnotationEditorUIManager.TRANSLATE_BIG;
|
||||
|
||||
return shadow(
|
||||
this,
|
||||
"_resizerKeyboardManager",
|
||||
new KeyboardManager([
|
||||
[["ArrowLeft", "mac+ArrowLeft"], resize, { args: [-small, 0] }],
|
||||
[
|
||||
["ctrl+ArrowLeft", "mac+shift+ArrowLeft"],
|
||||
resize,
|
||||
{ args: [-big, 0] },
|
||||
],
|
||||
[["ArrowRight", "mac+ArrowRight"], resize, { args: [small, 0] }],
|
||||
[
|
||||
["ctrl+ArrowRight", "mac+shift+ArrowRight"],
|
||||
resize,
|
||||
{ args: [big, 0] },
|
||||
],
|
||||
[["ArrowUp", "mac+ArrowUp"], resize, { args: [0, -small] }],
|
||||
[["ctrl+ArrowUp", "mac+shift+ArrowUp"], resize, { args: [0, -big] }],
|
||||
[["ArrowDown", "mac+ArrowDown"], resize, { args: [0, small] }],
|
||||
[["ctrl+ArrowDown", "mac+shift+ArrowDown"], resize, { args: [0, big] }],
|
||||
[
|
||||
["Escape", "mac+Escape"],
|
||||
AnnotationEditor.prototype._stopResizingWithKeyboard,
|
||||
],
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorParameters} parameters
|
||||
*/
|
||||
|
@ -157,6 +201,14 @@ class AnnotationEditor {
|
|||
"editor_alt_text_button_label",
|
||||
"editor_alt_text_edit_button_label",
|
||||
"editor_alt_text_decorative_tooltip",
|
||||
"editor_resizer_label_topLeft",
|
||||
"editor_resizer_label_topMiddle",
|
||||
"editor_resizer_label_topRight",
|
||||
"editor_resizer_label_middleRight",
|
||||
"editor_resizer_label_bottomRight",
|
||||
"editor_resizer_label_bottomMiddle",
|
||||
"editor_resizer_label_bottomLeft",
|
||||
"editor_resizer_label_middleLeft",
|
||||
].map(str => [str, l10n.get(str)])
|
||||
);
|
||||
if (options?.strings) {
|
||||
|
@ -277,6 +329,9 @@ class AnnotationEditor {
|
|||
if (parent !== null) {
|
||||
this.pageIndex = parent.pageIndex;
|
||||
this.pageDimensions = parent.pageDimensions;
|
||||
} else {
|
||||
// The editor is being removed from the DOM, so we need to stop resizing.
|
||||
this.#stopResizing();
|
||||
}
|
||||
this.parent = parent;
|
||||
}
|
||||
|
@ -600,19 +655,32 @@ class AnnotationEditor {
|
|||
}
|
||||
this.#resizersDiv = document.createElement("div");
|
||||
this.#resizersDiv.classList.add("resizers");
|
||||
const classes = ["topLeft", "topRight", "bottomRight", "bottomLeft"];
|
||||
if (!this._willKeepAspectRatio) {
|
||||
classes.push("topMiddle", "middleRight", "bottomMiddle", "middleLeft");
|
||||
}
|
||||
// When the resizers are used with the keyboard, they're focusable, hence
|
||||
// we want to have them in this order (top left, top middle, top right, ...)
|
||||
// in the DOM to have the focus order correct.
|
||||
const classes = this._willKeepAspectRatio
|
||||
? ["topLeft", "topRight", "bottomRight", "bottomLeft"]
|
||||
: [
|
||||
"topLeft",
|
||||
"topMiddle",
|
||||
"topRight",
|
||||
"middleRight",
|
||||
"bottomRight",
|
||||
"bottomMiddle",
|
||||
"bottomLeft",
|
||||
"middleLeft",
|
||||
];
|
||||
for (const name of classes) {
|
||||
const div = document.createElement("div");
|
||||
this.#resizersDiv.append(div);
|
||||
div.classList.add("resizer", name);
|
||||
div.setAttribute("data-resizer-name", name);
|
||||
div.addEventListener(
|
||||
"pointerdown",
|
||||
this.#resizerPointerdown.bind(this, name)
|
||||
);
|
||||
div.addEventListener("contextmenu", noContextMenu);
|
||||
div.tabIndex = -1;
|
||||
}
|
||||
this.div.prepend(this.#resizersDiv);
|
||||
}
|
||||
|
@ -659,40 +727,7 @@ class AnnotationEditor {
|
|||
this.parent.div.style.cursor = savedParentCursor;
|
||||
this.div.style.cursor = savedCursor;
|
||||
|
||||
const newX = this.x;
|
||||
const newY = this.y;
|
||||
const newWidth = this.width;
|
||||
const newHeight = this.height;
|
||||
if (
|
||||
newX === savedX &&
|
||||
newY === savedY &&
|
||||
newWidth === savedWidth &&
|
||||
newHeight === savedHeight
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addCommands({
|
||||
cmd: () => {
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
this.x = newX;
|
||||
this.y = newY;
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
|
||||
this.fixAndSetPosition();
|
||||
},
|
||||
undo: () => {
|
||||
this.width = savedWidth;
|
||||
this.height = savedHeight;
|
||||
this.x = savedX;
|
||||
this.y = savedY;
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setDims(parentWidth * savedWidth, parentHeight * savedHeight);
|
||||
this.fixAndSetPosition();
|
||||
},
|
||||
mustExec: true,
|
||||
});
|
||||
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
|
||||
};
|
||||
window.addEventListener("pointerup", pointerUpCallback);
|
||||
// If the user switches to another window (with alt+tab), then we end the
|
||||
|
@ -700,6 +735,43 @@ class AnnotationEditor {
|
|||
window.addEventListener("blur", pointerUpCallback);
|
||||
}
|
||||
|
||||
#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight) {
|
||||
const newX = this.x;
|
||||
const newY = this.y;
|
||||
const newWidth = this.width;
|
||||
const newHeight = this.height;
|
||||
if (
|
||||
newX === savedX &&
|
||||
newY === savedY &&
|
||||
newWidth === savedWidth &&
|
||||
newHeight === savedHeight
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addCommands({
|
||||
cmd: () => {
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
this.x = newX;
|
||||
this.y = newY;
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
|
||||
this.fixAndSetPosition();
|
||||
},
|
||||
undo: () => {
|
||||
this.width = savedWidth;
|
||||
this.height = savedHeight;
|
||||
this.x = savedX;
|
||||
this.y = savedY;
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setDims(parentWidth * savedWidth, parentHeight * savedHeight);
|
||||
this.fixAndSetPosition();
|
||||
},
|
||||
mustExec: true,
|
||||
});
|
||||
}
|
||||
|
||||
#resizerPointermove(name, event) {
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
const savedX = this.x;
|
||||
|
@ -1205,12 +1277,12 @@ class AnnotationEditor {
|
|||
}
|
||||
|
||||
/**
|
||||
* If it returns true, then this editor handle the keyboard
|
||||
* If it returns true, then this editor handles the keyboard
|
||||
* events itself.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldGetKeyboardEvents() {
|
||||
return false;
|
||||
return this.#isResizerEnabledForKeyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1303,6 +1375,7 @@ class AnnotationEditor {
|
|||
clearTimeout(this.#moveInDOMTimeout);
|
||||
this.#moveInDOMTimeout = null;
|
||||
}
|
||||
this.#stopResizing();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1319,9 +1392,140 @@ class AnnotationEditor {
|
|||
if (this.isResizable) {
|
||||
this.#createResizers();
|
||||
this.#resizersDiv.classList.remove("hidden");
|
||||
bindEvents(this, this.div, ["keydown"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onkeydown callback.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
keydown(event) {
|
||||
if (
|
||||
!this.isResizable ||
|
||||
event.target !== this.div ||
|
||||
event.key !== "Enter"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._uiManager.setSelected(this);
|
||||
this.#savedDimensions = {
|
||||
savedX: this.x,
|
||||
savedY: this.y,
|
||||
savedWidth: this.width,
|
||||
savedHeight: this.height,
|
||||
};
|
||||
const children = this.#resizersDiv.children;
|
||||
if (!this.#allResizerDivs) {
|
||||
this.#allResizerDivs = Array.from(children);
|
||||
const boundResizerKeydown = this.#resizerKeydown.bind(this);
|
||||
const boundResizerBlur = this.#resizerBlur.bind(this);
|
||||
for (const div of this.#allResizerDivs) {
|
||||
const name = div.getAttribute("data-resizer-name");
|
||||
div.addEventListener("keydown", boundResizerKeydown);
|
||||
div.addEventListener("blur", boundResizerBlur);
|
||||
div.addEventListener("focus", this.#resizerFocus.bind(this, name));
|
||||
AnnotationEditor._l10nPromise
|
||||
.get(`editor_resizer_label_${name}`)
|
||||
.then(msg => div.setAttribute("aria-label", msg));
|
||||
}
|
||||
}
|
||||
|
||||
// We want to have the resizers in the visual order, so we move the first
|
||||
// (top-left) to the right place.
|
||||
const first = this.#allResizerDivs[0];
|
||||
let firstPosition = 0;
|
||||
for (const div of children) {
|
||||
if (div === first) {
|
||||
break;
|
||||
}
|
||||
firstPosition++;
|
||||
}
|
||||
const nextFirstPosition =
|
||||
(((360 - this.rotation + this.parentRotation) % 360) / 90) *
|
||||
(this.#allResizerDivs.length / 4);
|
||||
|
||||
if (nextFirstPosition !== firstPosition) {
|
||||
// We need to reorder the resizers in the DOM in order to have the focus
|
||||
// on the top-left one.
|
||||
if (nextFirstPosition < firstPosition) {
|
||||
for (let i = 0; i < firstPosition - nextFirstPosition; i++) {
|
||||
this.#resizersDiv.append(this.#resizersDiv.firstChild);
|
||||
}
|
||||
} else if (nextFirstPosition > firstPosition) {
|
||||
for (let i = 0; i < nextFirstPosition - firstPosition; i++) {
|
||||
this.#resizersDiv.firstChild.before(this.#resizersDiv.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (const child of children) {
|
||||
const div = this.#allResizerDivs[i++];
|
||||
const name = div.getAttribute("data-resizer-name");
|
||||
AnnotationEditor._l10nPromise
|
||||
.get(`editor_resizer_label_${name}`)
|
||||
.then(msg => child.setAttribute("aria-label", msg));
|
||||
}
|
||||
}
|
||||
|
||||
this.#setResizerTabIndex(0);
|
||||
this.#isResizerEnabledForKeyboard = true;
|
||||
this.#resizersDiv.firstChild.focus({ focusVisible: true });
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
#resizerKeydown(event) {
|
||||
AnnotationEditor._resizerKeyboardManager.exec(this, event);
|
||||
}
|
||||
|
||||
#resizerBlur(event) {
|
||||
if (
|
||||
this.#isResizerEnabledForKeyboard &&
|
||||
event.relatedTarget?.parentNode !== this.#resizersDiv
|
||||
) {
|
||||
this.#stopResizing();
|
||||
}
|
||||
}
|
||||
|
||||
#resizerFocus(name) {
|
||||
this.#focusedResizerName = this.#isResizerEnabledForKeyboard ? name : "";
|
||||
}
|
||||
|
||||
#setResizerTabIndex(value) {
|
||||
if (!this.#allResizerDivs) {
|
||||
return;
|
||||
}
|
||||
for (const div of this.#allResizerDivs) {
|
||||
div.tabIndex = value;
|
||||
}
|
||||
}
|
||||
|
||||
_resizeWithKeyboard(x, y) {
|
||||
if (!this.#isResizerEnabledForKeyboard) {
|
||||
return;
|
||||
}
|
||||
this.#resizerPointermove(this.#focusedResizerName, {
|
||||
movementX: x,
|
||||
movementY: y,
|
||||
});
|
||||
}
|
||||
|
||||
#stopResizing() {
|
||||
this.#isResizerEnabledForKeyboard = false;
|
||||
this.#setResizerTabIndex(-1);
|
||||
if (this.#savedDimensions) {
|
||||
const { savedX, savedY, savedWidth, savedHeight } = this.#savedDimensions;
|
||||
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
|
||||
this.#savedDimensions = null;
|
||||
}
|
||||
}
|
||||
|
||||
_stopResizingWithKeyboard() {
|
||||
this.#stopResizing();
|
||||
this.div.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select this editor.
|
||||
*/
|
||||
|
|
|
@ -1021,7 +1021,7 @@ class AnnotationEditorUIManager {
|
|||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
keydown(event) {
|
||||
if (!this.getActive()?.shouldGetKeyboardEvents()) {
|
||||
if (!this.isEditorHandlingKeyboard) {
|
||||
AnnotationEditorUIManager._keyboardManager.exec(this, event);
|
||||
}
|
||||
}
|
||||
|
@ -1732,6 +1732,14 @@ class AnnotationEditorUIManager {
|
|||
}
|
||||
}
|
||||
|
||||
get isEditorHandlingKeyboard() {
|
||||
return (
|
||||
this.getActive()?.shouldGetKeyboardEvents() ||
|
||||
(this.#selectedEditors.size === 1 &&
|
||||
this.#selectedEditors.values().next().value.shouldGetKeyboardEvents())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the current editor the one passed as argument?
|
||||
* @param {AnnotationEditor} editor
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue