diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 497ac484dd75b..5205c82a26534 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -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(); diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index e906faa3f53c1..aaa33f9e4039e 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -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 }; diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 8fe3761f058bc..ddd1f944f1d9d 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -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"); diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index d77ef5aec321c..5f3d72cbebd0b 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -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(); } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 068861c46731e..32478508df6c2 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -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,22 +736,78 @@ 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; } /** @@ -795,51 +861,25 @@ 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); - } - }; + if (!this.hasSelection) { + return; + } - this.addCommands({ cmd, undo, mustExec: true }); - } else { - if (!this.#activeEditor) { - return; - } - const editor = this.#activeEditor; - this.#previousActiveEditor = this.#activeEditor = null; - cmd = () => { + const editors = [...this.#selectedEditors]; + const cmd = () => { + for (const editor of editors) { editor.remove(); - }; - undo = () => { + } + }; + 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} 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. */ - unselectAll() { - this.#isAllSelected = false; + selectAll() { + for (const editor of this.#selectedEditors) { + editor.commit(); + } + this.#selectEditors(this.#allEditors.values()); + } - for (const editor of this.#allEditors.values()) { + /** + * Unselect all the selected editors. + */ + unselectAll() { + for (const editor of this.#selectedEditors) { editor.unselect(); } + this.#selectedEditors.clear(); this.#dispatchUpdateStates({ - hasSelectedEditor: this.#activeEditor !== null, + hasSelectedEditor: false, }); } diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index e72c62867695d..722ba2cad7792 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -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]); + }) + ); + }); + }); }); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 4abd21cc5ad2b..44ebc00c0ae5f 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -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; -}