[Editor] Add the ability to make multiple selections (bug 1779582)
- several editors can be selected/unselected using ctrl+click; - and then they can be copied, pasted, their properties can be changed.
This commit is contained in:
parent
9fe4a667bd
commit
d6b9ca48a5
@ -136,7 +136,7 @@ class AnnotationEditorLayer {
|
||||
} else {
|
||||
this.enableClick();
|
||||
}
|
||||
this.setActiveEditor(null);
|
||||
this.#uiManager.unselectAll();
|
||||
}
|
||||
|
||||
addInkEditorIfNeeded(isCommitting) {
|
||||
@ -210,14 +210,6 @@ class AnnotationEditorLayer {
|
||||
}
|
||||
|
||||
this.#uiManager.setActiveEditor(editor);
|
||||
|
||||
if (currentActive && currentActive !== editor) {
|
||||
currentActive.commitOrRemove();
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
this.#uiManager.unselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
enableClick() {
|
||||
@ -250,11 +242,19 @@ class AnnotationEditorLayer {
|
||||
this.#uiManager.removeEditor(editor);
|
||||
this.detach(editor);
|
||||
this.annotationStorage.removeKey(editor.id);
|
||||
editor.div.remove();
|
||||
editor.isAttachedToDOM = false;
|
||||
if (this.#uiManager.isActive(editor) || this.#editors.size === 0) {
|
||||
this.setActiveEditor(null);
|
||||
}
|
||||
editor.div.style.display = "none";
|
||||
setTimeout(() => {
|
||||
// When the div is removed from DOM the focus can move on the
|
||||
// document.body, so we just slightly postpone the removal in
|
||||
// order to let an element potentially grab the focus before
|
||||
// the body.
|
||||
editor.div.style.display = "";
|
||||
editor.div.remove();
|
||||
editor.isAttachedToDOM = false;
|
||||
if (document.activeElement === document.body) {
|
||||
this.#uiManager.focusMainContainer();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
if (!this.#isCleaningUp) {
|
||||
this.addInkEditorIfNeeded(/* isCommitting = */ false);
|
||||
@ -271,10 +271,6 @@ class AnnotationEditorLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#uiManager.isActive(editor)) {
|
||||
editor.parent?.setActiveEditor(null);
|
||||
}
|
||||
|
||||
this.attach(editor);
|
||||
editor.pageIndex = this.pageIndex;
|
||||
editor.parent?.detach(editor);
|
||||
@ -546,6 +542,42 @@ class AnnotationEditorLayer {
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last selected editor.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
setSelected(editor) {
|
||||
this.#uiManager.setSelected(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the editor is selected.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
isSelected(editor) {
|
||||
return this.#uiManager.isSelected(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect an editor.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
unselect(editor) {
|
||||
this.#uiManager.unselect(editor);
|
||||
}
|
||||
|
||||
get isMultipleSelection() {
|
||||
return this.#uiManager.isMultipleSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* An editor just got a mousedown with ctrl key pressed.
|
||||
* @param {boolean}} isMultiple
|
||||
*/
|
||||
set isMultipleSelection(isMultiple) {
|
||||
this.#uiManager.isMultipleSelection = isMultiple;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouseclick callback.
|
||||
* @param {MouseEvent} event
|
||||
@ -662,7 +694,6 @@ class AnnotationEditorLayer {
|
||||
* @param {Object} parameters
|
||||
*/
|
||||
update(parameters) {
|
||||
this.setActiveEditor(null);
|
||||
this.viewport = parameters.viewport;
|
||||
this.setDimensions();
|
||||
this.updateMode();
|
||||
|
@ -16,12 +16,8 @@
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
|
||||
|
||||
import {
|
||||
AnnotationEditorPrefix,
|
||||
shadow,
|
||||
unreachable,
|
||||
} from "../../shared/util.js";
|
||||
import { bindEvents, ColorManager } from "./tools.js";
|
||||
import { shadow, unreachable } from "../../shared/util.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorParameters
|
||||
@ -35,8 +31,20 @@ import { bindEvents, ColorManager } from "./tools.js";
|
||||
* Base class for editors.
|
||||
*/
|
||||
class AnnotationEditor {
|
||||
#boundFocusin = this.focusin.bind(this);
|
||||
|
||||
#boundFocusout = this.focusout.bind(this);
|
||||
|
||||
#isEditing = false;
|
||||
|
||||
#isFocused = false;
|
||||
|
||||
#isInEditMode = false;
|
||||
|
||||
#wasSelected = false;
|
||||
|
||||
#wasFocused = false;
|
||||
|
||||
#zIndex = AnnotationEditor._zIndex++;
|
||||
|
||||
static _colorManager = new ColorManager();
|
||||
@ -88,17 +96,32 @@ class AnnotationEditor {
|
||||
this.div.style.zIndex = this.#zIndex;
|
||||
}
|
||||
|
||||
#select() {
|
||||
if (this.#wasSelected) {
|
||||
this.parent.unselect(this);
|
||||
this.unselect();
|
||||
this.#wasSelected = true;
|
||||
} else {
|
||||
this.parent.setSelected(this);
|
||||
this.select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onfocus callback.
|
||||
*/
|
||||
focusin(/* event */) {
|
||||
this.parent.setActiveEditor(this);
|
||||
focusin(event) {
|
||||
this.#isFocused =
|
||||
event.target === this.div ||
|
||||
!!event.relatedTarget?.closest(`#${this.id}`);
|
||||
if (event.target === this.div) {
|
||||
this.#select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onblur callback.
|
||||
* @param {FocusEvent} event
|
||||
* @returns {undefined}
|
||||
*/
|
||||
focusout(event) {
|
||||
if (!this.isAttachedToDOM) {
|
||||
@ -116,10 +139,14 @@ class AnnotationEditor {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.commitOrRemove();
|
||||
|
||||
if (!target?.id?.startsWith(AnnotationEditorPrefix)) {
|
||||
this.parent.setActiveEditor(null);
|
||||
this.#isFocused = false;
|
||||
if (!this.parent.isMultipleSelection) {
|
||||
this.commitOrRemove();
|
||||
if (target?.closest(".annotationEditorLayer")) {
|
||||
// We only unselect the element when another editor (or its parent)
|
||||
// is grabbing the focus.
|
||||
this.parent.unselect(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,15 +255,13 @@ class AnnotationEditor {
|
||||
|
||||
this.setInForeground();
|
||||
|
||||
this.div.addEventListener("focusin", this.#boundFocusin);
|
||||
this.div.addEventListener("focusout", this.#boundFocusout);
|
||||
|
||||
const [tx, ty] = this.getInitialTranslation();
|
||||
this.translate(tx, ty);
|
||||
|
||||
bindEvents(this, this.div, [
|
||||
"dragstart",
|
||||
"focusin",
|
||||
"focusout",
|
||||
"mousedown",
|
||||
]);
|
||||
bindEvents(this, this.div, ["dragstart", "mousedown", "mouseup"]);
|
||||
|
||||
return this.div;
|
||||
}
|
||||
@ -250,6 +275,23 @@ class AnnotationEditor {
|
||||
// Avoid to focus this editor because of a non-left click.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
const isMultipleSelection = (this.parent.isMultipleSelection =
|
||||
event.ctrlKey || event.shiftKey);
|
||||
this.#wasSelected = isMultipleSelection && this.parent.isSelected(this);
|
||||
this.#wasFocused = this.#isFocused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Onmouseup callback.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
mouseup(event) {
|
||||
if (this.#wasFocused) {
|
||||
this.#select();
|
||||
}
|
||||
this.parent.isMultipleSelection = false;
|
||||
this.#wasFocused = false;
|
||||
}
|
||||
|
||||
getRect(tx, ty) {
|
||||
@ -331,7 +373,6 @@ class AnnotationEditor {
|
||||
|
||||
/**
|
||||
* Enable edit mode.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
enableEditMode() {
|
||||
this.#isInEditMode = true;
|
||||
@ -339,7 +380,6 @@ class AnnotationEditor {
|
||||
|
||||
/**
|
||||
* Disable edit mode.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
disableEditMode() {
|
||||
this.#isInEditMode = false;
|
||||
@ -374,10 +414,9 @@ class AnnotationEditor {
|
||||
* Rebuild the editor in case it has been removed on undo.
|
||||
*
|
||||
* To implement in subclasses.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
rebuild() {
|
||||
unreachable("An editor must be rebuildable");
|
||||
this.div?.addEventListener("focusin", this.#boundFocusin);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -386,7 +425,6 @@ class AnnotationEditor {
|
||||
* new annotation to add to the pdf document.
|
||||
*
|
||||
* To implement in subclasses.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
serialize() {
|
||||
unreachable("An editor must be serializable");
|
||||
@ -423,10 +461,11 @@ class AnnotationEditor {
|
||||
/**
|
||||
* Remove this editor.
|
||||
* It's used on ctrl+backspace action.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
remove() {
|
||||
this.div.removeEventListener("focusin", this.#boundFocusin);
|
||||
this.div.removeEventListener("focusout", this.#boundFocusout);
|
||||
|
||||
if (!this.isEmpty()) {
|
||||
// The editor is removed but it can be back at some point thanks to
|
||||
// undo/redo so we must commit it before.
|
||||
@ -439,18 +478,14 @@ class AnnotationEditor {
|
||||
* Select this editor.
|
||||
*/
|
||||
select() {
|
||||
if (this.div) {
|
||||
this.div.classList.add("selectedEditor");
|
||||
}
|
||||
this.div?.classList.add("selectedEditor");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect this editor.
|
||||
*/
|
||||
unselect() {
|
||||
if (this.div) {
|
||||
this.div.classList.remove("selectedEditor");
|
||||
}
|
||||
this.div?.classList.remove("selectedEditor");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -494,6 +529,29 @@ class AnnotationEditor {
|
||||
get contentDiv() {
|
||||
return this.div;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true then the editor is currently edited.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isEditing() {
|
||||
return this.#isEditing;
|
||||
}
|
||||
|
||||
/**
|
||||
* When set to true, it means that this editor is currently edited.
|
||||
* @param {boolean} value
|
||||
*/
|
||||
set isEditing(value) {
|
||||
this.#isEditing = value;
|
||||
if (value) {
|
||||
this.select();
|
||||
this.parent.setSelected(this);
|
||||
this.parent.setActiveEditor(this);
|
||||
} else {
|
||||
this.parent.setActiveEditor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationEditor };
|
||||
|
@ -30,6 +30,10 @@ import { AnnotationEditor } from "./editor.js";
|
||||
* Basic text editor in order to create a FreeTex annotation.
|
||||
*/
|
||||
class FreeTextEditor extends AnnotationEditor {
|
||||
#boundEditorDivBlur = this.editorDivBlur.bind(this);
|
||||
|
||||
#boundEditorDivFocus = this.editorDivFocus.bind(this);
|
||||
|
||||
#boundEditorDivKeydown = this.editorDivKeydown.bind(this);
|
||||
|
||||
#color;
|
||||
@ -199,6 +203,7 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
|
||||
/** @inheritdoc */
|
||||
rebuild() {
|
||||
super.rebuild();
|
||||
if (this.div === null) {
|
||||
return;
|
||||
}
|
||||
@ -220,6 +225,8 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
this.div.draggable = false;
|
||||
this.div.removeAttribute("tabIndex");
|
||||
this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown);
|
||||
this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus);
|
||||
this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -231,6 +238,8 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
this.div.draggable = true;
|
||||
this.div.tabIndex = 0;
|
||||
this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown);
|
||||
this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus);
|
||||
this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -251,6 +260,7 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
|
||||
/** @inheritdoc */
|
||||
remove() {
|
||||
this.isEditing = false;
|
||||
this.parent.setEditingState(true);
|
||||
super.remove();
|
||||
}
|
||||
@ -333,6 +343,14 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
FreeTextEditor._keyboardManager.exec(this, event);
|
||||
}
|
||||
|
||||
editorDivFocus(event) {
|
||||
this.isEditing = true;
|
||||
}
|
||||
|
||||
editorDivBlur(event) {
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
disableEditing() {
|
||||
this.editorDiv.setAttribute("role", "comment");
|
||||
|
@ -123,8 +123,16 @@ class InkEditor extends AnnotationEditor {
|
||||
/** @inheritdoc */
|
||||
get propertiesToUpdate() {
|
||||
return [
|
||||
[AnnotationEditorParamsType.INK_THICKNESS, this.thickness],
|
||||
[AnnotationEditorParamsType.INK_COLOR, this.color],
|
||||
[
|
||||
AnnotationEditorParamsType.INK_THICKNESS,
|
||||
this.thickness || InkEditor._defaultThickness,
|
||||
],
|
||||
[
|
||||
AnnotationEditorParamsType.INK_COLOR,
|
||||
this.color ||
|
||||
InkEditor._defaultColor ||
|
||||
AnnotationEditor._defaultLineColor,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -174,6 +182,7 @@ class InkEditor extends AnnotationEditor {
|
||||
|
||||
/** @inheritdoc */
|
||||
rebuild() {
|
||||
super.rebuild();
|
||||
if (this.div === null) {
|
||||
return;
|
||||
}
|
||||
@ -284,6 +293,7 @@ class InkEditor extends AnnotationEditor {
|
||||
* @param {number} y
|
||||
*/
|
||||
#startDrawing(x, y) {
|
||||
this.isEditing = true;
|
||||
if (!this.#isCanvasInitialized) {
|
||||
this.#isCanvasInitialized = true;
|
||||
this.#setCanvasDims();
|
||||
@ -390,6 +400,7 @@ class InkEditor extends AnnotationEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
this.disableEditMode();
|
||||
|
||||
// This editor must be on top of the main ink editor.
|
||||
@ -408,8 +419,8 @@ class InkEditor extends AnnotationEditor {
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
focusin(/* event */) {
|
||||
super.focusin();
|
||||
focusin(event) {
|
||||
super.focusin(event);
|
||||
this.enableEditMode();
|
||||
}
|
||||
|
||||
|
@ -377,19 +377,19 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#currentPageIndex = 0;
|
||||
|
||||
#isMultipleSelection = false;
|
||||
|
||||
#editorTypes = null;
|
||||
|
||||
#eventBus = null;
|
||||
|
||||
#idManager = new IdManager();
|
||||
|
||||
#isAllSelected = false;
|
||||
|
||||
#isEnabled = false;
|
||||
|
||||
#mode = AnnotationEditorType.NONE;
|
||||
|
||||
#previousActiveEditor = null;
|
||||
#selectedEditors = new Set();
|
||||
|
||||
#boundKeydown = this.keydown.bind(this);
|
||||
|
||||
@ -435,6 +435,7 @@ class AnnotationEditorUIManager {
|
||||
],
|
||||
AnnotationEditorUIManager.prototype.delete,
|
||||
],
|
||||
[["Escape"], AnnotationEditorUIManager.prototype.unselectAll],
|
||||
]);
|
||||
|
||||
constructor(container, eventBus) {
|
||||
@ -456,6 +457,7 @@ class AnnotationEditorUIManager {
|
||||
this.#allLayers.clear();
|
||||
this.#allEditors.clear();
|
||||
this.#activeEditor = null;
|
||||
this.#selectedEditors.clear();
|
||||
this.#clipboardManager.destroy();
|
||||
this.#commandManager.destroy();
|
||||
}
|
||||
@ -470,6 +472,10 @@ class AnnotationEditorUIManager {
|
||||
layer?.onTextLayerRendered();
|
||||
}
|
||||
|
||||
focusMainContainer() {
|
||||
this.#container.focus();
|
||||
}
|
||||
|
||||
#addKeyboardManager() {
|
||||
// The keyboard events are caught at the container level in order to be able
|
||||
// to execute some callbacks even if the current page doesn't have focus.
|
||||
@ -631,10 +637,10 @@ class AnnotationEditorUIManager {
|
||||
* @param {*} value
|
||||
*/
|
||||
updateParams(type, value) {
|
||||
(this.#activeEditor || this.#previousActiveEditor)?.updateParams(
|
||||
type,
|
||||
value
|
||||
);
|
||||
for (const editor of this.#selectedEditors) {
|
||||
editor.updateParams(type, value);
|
||||
}
|
||||
|
||||
for (const editorType of this.#editorTypes) {
|
||||
editorType.updateDefaultParams(type, value);
|
||||
}
|
||||
@ -656,6 +662,7 @@ class AnnotationEditorUIManager {
|
||||
* Disable all the layers.
|
||||
*/
|
||||
#disableAll() {
|
||||
this.unselectAll();
|
||||
if (this.#isEnabled) {
|
||||
this.#isEnabled = false;
|
||||
for (const layer of this.#allLayers.values()) {
|
||||
@ -702,6 +709,9 @@ class AnnotationEditorUIManager {
|
||||
*/
|
||||
removeEditor(editor) {
|
||||
this.#allEditors.delete(editor.id);
|
||||
if (this.hasSelection) {
|
||||
this.#selectedEditors.delete(editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -726,24 +736,80 @@ class AnnotationEditorUIManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#previousActiveEditor = this.#activeEditor;
|
||||
|
||||
this.#activeEditor = editor;
|
||||
if (editor) {
|
||||
this.#dispatchUpdateUI(editor.propertiesToUpdate);
|
||||
this.#dispatchUpdateStates({ hasSelectedEditor: true });
|
||||
} else {
|
||||
this.#dispatchUpdateStates({ hasSelectedEditor: false });
|
||||
if (this.#previousActiveEditor) {
|
||||
this.#dispatchUpdateUI(this.#previousActiveEditor.propertiesToUpdate);
|
||||
} else {
|
||||
for (const editorType of this.#editorTypes) {
|
||||
this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last selected editor.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
setSelected(editor) {
|
||||
if (!this.#isMultipleSelection) {
|
||||
if (this.#selectedEditors.has(editor)) {
|
||||
if (this.#selectedEditors.size > 1) {
|
||||
for (const ed of this.#selectedEditors) {
|
||||
if (ed !== editor) {
|
||||
ed.unselect();
|
||||
}
|
||||
}
|
||||
this.#selectedEditors.clear();
|
||||
this.#selectedEditors.add(editor);
|
||||
this.#dispatchUpdateUI(editor.propertiesToUpdate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ed of this.#selectedEditors) {
|
||||
ed.unselect();
|
||||
}
|
||||
this.#selectedEditors.clear();
|
||||
}
|
||||
this.#selectedEditors.add(editor);
|
||||
this.#dispatchUpdateUI(editor.propertiesToUpdate);
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedEditor: this.hasSelection,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the editor is selected.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
isSelected(editor) {
|
||||
return this.#selectedEditors.has(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect an editor.
|
||||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
unselect(editor) {
|
||||
editor.unselect();
|
||||
this.#selectedEditors.delete(editor);
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedEditor: this.hasSelection,
|
||||
});
|
||||
}
|
||||
|
||||
get hasSelection() {
|
||||
return this.#selectedEditors.size !== 0;
|
||||
}
|
||||
|
||||
get isMultipleSelection() {
|
||||
return this.#isMultipleSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* An editor just got a mousedown with ctrl key pressed.
|
||||
* @param {boolean} isMultiple
|
||||
*/
|
||||
set isMultipleSelection(isMultiple) {
|
||||
this.#isMultipleSelection = isMultiple;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last command.
|
||||
*/
|
||||
@ -795,52 +861,26 @@ class AnnotationEditorUIManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect the current editor.
|
||||
*/
|
||||
unselect() {
|
||||
if (this.#activeEditor) {
|
||||
this.#activeEditor.parent.setActiveEditor(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current editor or all.
|
||||
*/
|
||||
delete() {
|
||||
let cmd, undo;
|
||||
if (this.#isAllSelected) {
|
||||
this.#previousActiveEditor = this.#activeEditor = null;
|
||||
const editors = Array.from(this.#allEditors.values());
|
||||
cmd = () => {
|
||||
for (const editor of editors) {
|
||||
if (!editor.isEmpty()) {
|
||||
editor.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
undo = () => {
|
||||
for (const editor of editors) {
|
||||
this.#addEditorToLayer(editor);
|
||||
}
|
||||
};
|
||||
|
||||
this.addCommands({ cmd, undo, mustExec: true });
|
||||
} else {
|
||||
if (!this.#activeEditor) {
|
||||
return;
|
||||
}
|
||||
const editor = this.#activeEditor;
|
||||
this.#previousActiveEditor = this.#activeEditor = null;
|
||||
cmd = () => {
|
||||
editor.remove();
|
||||
};
|
||||
undo = () => {
|
||||
this.#addEditorToLayer(editor);
|
||||
};
|
||||
if (!this.hasSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editors = [...this.#selectedEditors];
|
||||
const cmd = () => {
|
||||
for (const editor of editors) {
|
||||
editor.remove();
|
||||
}
|
||||
};
|
||||
const undo = () => {
|
||||
for (const editor of editors) {
|
||||
this.#addEditorToLayer(editor);
|
||||
}
|
||||
};
|
||||
|
||||
this.addCommands({ cmd, undo, mustExec: true });
|
||||
}
|
||||
|
||||
@ -848,8 +888,8 @@ class AnnotationEditorUIManager {
|
||||
* Copy the selected editor.
|
||||
*/
|
||||
copy() {
|
||||
if (this.#activeEditor) {
|
||||
this.#clipboardManager.copy(this.#activeEditor);
|
||||
if (this.hasSelection) {
|
||||
this.#clipboardManager.copy([...this.#selectedEditors]);
|
||||
this.#dispatchUpdateStates({ hasEmptyClipboard: false });
|
||||
}
|
||||
}
|
||||
@ -858,10 +898,8 @@ class AnnotationEditorUIManager {
|
||||
* Cut the selected editor.
|
||||
*/
|
||||
cut() {
|
||||
if (this.#activeEditor) {
|
||||
this.#clipboardManager.copy(this.#activeEditor);
|
||||
this.delete();
|
||||
}
|
||||
this.copy();
|
||||
this.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -873,42 +911,63 @@ class AnnotationEditorUIManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unselectAll();
|
||||
|
||||
const layer = this.#allLayers.get(this.#currentPageIndex);
|
||||
const newEditors = this.#clipboardManager
|
||||
.paste()
|
||||
.map(data => layer.deserialize(data));
|
||||
|
||||
const cmd = () => {
|
||||
newEditors.map(editor => this.#addEditorToLayer(editor));
|
||||
for (const editor of newEditors) {
|
||||
this.#addEditorToLayer(editor);
|
||||
}
|
||||
this.#selectEditors(newEditors);
|
||||
};
|
||||
const undo = () => {
|
||||
newEditors.map(editor => editor.remove());
|
||||
for (const editor of newEditors) {
|
||||
editor.remove();
|
||||
}
|
||||
};
|
||||
this.addCommands({ cmd, undo, mustExec: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all the editors.
|
||||
* Select the editors.
|
||||
* @param {Array<AnnotationEditor>} editors
|
||||
*/
|
||||
selectAll() {
|
||||
this.#isAllSelected = true;
|
||||
for (const editor of this.#allEditors.values()) {
|
||||
#selectEditors(editors) {
|
||||
this.#selectedEditors.clear();
|
||||
for (const editor of editors) {
|
||||
if (editor.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
this.#selectedEditors.add(editor);
|
||||
editor.select();
|
||||
}
|
||||
this.#dispatchUpdateStates({ hasSelectedEditor: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect all the editors.
|
||||
* Select all the editors.
|
||||
*/
|
||||
selectAll() {
|
||||
for (const editor of this.#selectedEditors) {
|
||||
editor.commit();
|
||||
}
|
||||
this.#selectEditors(this.#allEditors.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselect all the selected editors.
|
||||
*/
|
||||
unselectAll() {
|
||||
this.#isAllSelected = false;
|
||||
|
||||
for (const editor of this.#allEditors.values()) {
|
||||
for (const editor of this.#selectedEditors) {
|
||||
editor.unselect();
|
||||
}
|
||||
this.#selectedEditors.clear();
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedEditor: this.#activeEditor !== null,
|
||||
hasSelectedEditor: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -252,4 +252,176 @@ describe("Editor", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FreeText (multiselection)", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
function getSelected(page) {
|
||||
return page.evaluate(prefix => {
|
||||
const elements = document.querySelectorAll(".selectedEditor");
|
||||
const results = [];
|
||||
for (const element of elements) {
|
||||
results.push(parseInt(element.id.slice(prefix.length)));
|
||||
}
|
||||
results.sort();
|
||||
return results;
|
||||
}, editorPrefix.slice(1));
|
||||
}
|
||||
|
||||
it("must select/unselect several editors and check copy, paste and delete operations", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.click("#editorFreeText");
|
||||
|
||||
const rect = await page.$eval(".annotationEditorLayer", el => {
|
||||
// With Chrome something is wrong when serializing a DomRect,
|
||||
// hence we extract the values and just return them.
|
||||
const { x, y } = el.getBoundingClientRect();
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
const editorCenters = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const data = `FreeText ${i}`;
|
||||
await page.mouse.click(
|
||||
rect.x + (i + 1) * 100,
|
||||
rect.y + (i + 1) * 100
|
||||
);
|
||||
await page.type(`${editorPrefix}${i} .internal`, data);
|
||||
|
||||
const editorRect = await page.$eval(`${editorPrefix}${i}`, el => {
|
||||
const { x, y, width, height } = el.getBoundingClientRect();
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
});
|
||||
editorCenters.push({
|
||||
x: editorRect.x + editorRect.width / 2,
|
||||
y: editorRect.y + editorRect.height / 2,
|
||||
});
|
||||
|
||||
// Commit.
|
||||
await page.mouse.click(
|
||||
editorRect.x,
|
||||
editorRect.y + 2 * editorRect.height
|
||||
);
|
||||
}
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("a");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([0, 1, 2, 3]);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
|
||||
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([0, 2, 3]);
|
||||
|
||||
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
|
||||
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([0, 3]);
|
||||
|
||||
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([0, 1, 3]);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("c");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("v");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
// 0,1,3 are unselected and new pasted editors are selected.
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([4, 5, 6]);
|
||||
|
||||
// No ctrl here, hence all are unselected and 2 is selected.
|
||||
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([2]);
|
||||
|
||||
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([1]);
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
|
||||
await page.mouse.click(editorCenters[3].x, editorCenters[3].y);
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([1, 3]);
|
||||
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
// Delete 1 and 3.
|
||||
await page.keyboard.press("Backspace");
|
||||
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("a");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([0, 2, 4, 5, 6]);
|
||||
|
||||
// Create an empty editor.
|
||||
await page.mouse.click(rect.x + 700, rect.y + 100);
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([7]);
|
||||
|
||||
// Set the focus to 2 and check that only 2 is selected.
|
||||
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([2]);
|
||||
|
||||
// Create an empty editor.
|
||||
await page.mouse.click(rect.x + 700, rect.y + 100);
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([8]);
|
||||
// Dismiss it.
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// Select all.
|
||||
await page.keyboard.down("Control");
|
||||
await page.keyboard.press("a");
|
||||
await page.keyboard.up("Control");
|
||||
|
||||
// Check that all the editors are correctly selected (and the focus
|
||||
// didn't move to the body when the empty editor was removed).
|
||||
expect(await getSelected(page))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual([0, 2, 4, 5, 6]);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -47,6 +47,11 @@
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .selectedEditor {
|
||||
outline: var(--focus-outline);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
@ -94,21 +99,17 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor:focus-within {
|
||||
outline: var(--focus-outline);
|
||||
}
|
||||
|
||||
.annotationEditorLayer .inkEditor:not(:focus) {
|
||||
.annotationEditorLayer .inkEditor.disabled {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor:hover:not(:focus-within),
|
||||
.annotationEditorLayer .inkEditor:hover:not(:focus) {
|
||||
outline: var(--hover-outline);
|
||||
.annotationEditorLayer .inkEditor.disabled.selectedEditor {
|
||||
resize: horizontal;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .inkEditor.disabled:focus {
|
||||
resize: horizontal;
|
||||
.annotationEditorLayer .freeTextEditor:hover:not(.selectedEditor),
|
||||
.annotationEditorLayer .inkEditor:hover:not(.selectedEditor) {
|
||||
outline: var(--hover-outline);
|
||||
}
|
||||
|
||||
.annotationEditorLayer .inkEditor {
|
||||
@ -123,11 +124,6 @@
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .inkEditor:focus {
|
||||
outline: var(--focus-outline);
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .inkEditor.editing {
|
||||
resize: none;
|
||||
cursor: var(--editorInk-editing-cursor), pointer;
|
||||
@ -140,8 +136,3 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .selectedEditor {
|
||||
outline: var(--focus-outline);
|
||||
resize: none;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user