import { describe, expect, test } from "@odoo/hoot"; import { isInViewPort, press, queryFirst, queryOne } from "@odoo/hoot-dom"; import { animationFrame, tick } from "@odoo/hoot-mock"; import { Component, xml } from "@odoo/owl"; import { defineModels, fields, models, mountView, patchWithCleanup, } from "@web/../tests/web_test_helpers"; import { useAutofocus } from "@web/core/utils/hooks"; import { Plugin } from "../src/plugin"; import { MAIN_PLUGINS } from "../src/plugin_sets"; import { setupEditor, testEditor } from "./_helpers/editor"; import { getContent, setSelection } from "./_helpers/selection"; import { insertText, tripleClick } from "./_helpers/user_actions"; test("getEditableSelection should work, even if getSelection returns null", async () => { const { editor } = await setupEditor("
a[b]
"); let selection = editor.shared.selection.getEditableSelection(); expect(selection.startOffset).toBe(1); expect(selection.endOffset).toBe(2); // it happens sometimes in firefox that the selection is null patchWithCleanup(document, { getSelection: () => null, }); selection = editor.shared.selection.getEditableSelection(); expect(selection.startOffset).toBe(1); expect(selection.endOffset).toBe(2); }); test("plugins should be notified when ranges are removed", async () => { let count = 0; class TestPlugin extends Plugin { static id = "test"; resources = { selectionchange_handlers: () => count++, }; } const { el } = await setupEditor("a[b]
", { config: { Plugins: [...MAIN_PLUGINS, TestPlugin] }, }); const countBefore = count; document.getSelection().removeAllRanges(); await animationFrame(); expect(count).toBe(countBefore + 1); expect(getContent(el)).toBe("ab
"); }); test("triple click outside of the Editor", async () => { const { el } = await setupEditor("[]abc
d
", {}); const anchorNode = el.parentElement; await tripleClick(el.parentElement); expect(document.getSelection().anchorNode).toBe(anchorNode); expect(getContent(el)).toBe("abc
d
"); const p = el.querySelector("p"); await tripleClick(p); expect(document.getSelection().anchorNode).toBe(p.childNodes[0]); expect(getContent(el)).toBe("[abc]
d
"); }); test("correct selection after triple click with bold", async () => { const { el } = await setupEditor("[]abcd
efg
", {}); await tripleClick(queryFirst("p").firstChild); expect(getContent(el)).toBe("[abcd]
efg
"); }); test("correct selection after triple click in multi-line block (1)", async () => { const { el } = await setupEditor("[]abc
efg
[abc
efg]
block1
[]block2
block2
block3
", {}); await tripleClick(queryFirst("p").nextSibling.firstChild); // we triple click inside block2 expect(getContent(el)).toBe("block1
[block2
block2]
block3
"); }); test("fix selection P in the beggining being a direct child of the editable p after selection", async () => { const { el } = await setupEditor("b
"); expect(getContent(el)).toBe(`[]b
`); }); test("fix selection P in the beginning being a direct child of the editable p before selection", async () => { const { el } = await setupEditor("a
[]a
a[]b
"); const selectionData = editor.shared.selection.getSelectionData(); expect(selectionData.documentSelectionIsInEditable).toBe(true); }); test("documentSelectionIsInEditable should be false when it is set outside the editable", async () => { const { editor } = await setupEditor("ab
"); const selectionData = editor.shared.selection.getSelectionData(); expect(selectionData.documentSelectionIsInEditable).toBe(false); }); test("documentSelectionIsInEditable should be false when it is set outside the editable after retrieving it", async () => { const { editor } = await setupEditor("ab[]
"); const selection = document.getSelection(); let selectionData = editor.shared.selection.getSelectionData(); expect(selectionData.documentSelectionIsInEditable).toBe(true); selection.setPosition(document.body); // value is updated directly ! selectionData = editor.shared.selection.getSelectionData(); expect(selectionData.documentSelectionIsInEditable).toBe(false); }); }); test("setEditableSelection should not crash if getSelection returns null", async () => { const { editor } = await setupEditor("a[b]
"); let selection = editor.shared.selection.getEditableSelection(); expect(selection.startOffset).toBe(1); expect(selection.endOffset).toBe(2); // it happens sometimes in firefox that the selection is null patchWithCleanup(document, { getSelection: () => null, }); selection = editor.shared.selection.setSelection({ anchorNode: editor.editable.firstChild, anchorOffset: 0, }); // Selection could not be set, so it remains unchanged. expect(selection.startOffset).toBe(1); expect(selection.endOffset).toBe(2); }); test("modifySelection should not crash if getSelection returns null", async () => { const { editor } = await setupEditor("a[b]
"); let selection = editor.shared.selection.getEditableSelection(); expect(selection.startOffset).toBe(1); expect(selection.endOffset).toBe(2); // it happens sometimes in firefox that the selection is null patchWithCleanup(document, { getSelection: () => null, }); selection = editor.shared.selection.modifySelection("extend", "backward", "word"); // Selection could not be modified. expect(selection.startOffset).toBe(1); expect(selection.endOffset).toBe(2); }); test("setSelection should not set the selection outside the editable", async () => { const { editor, el } = await setupEditor("a[b]
"); editor.document.getSelection().setPosition(document.body); await tick(); const selection = editor.shared.selection.setSelection( editor.shared.selection.getEditableSelection() ); expect(el.contains(selection.anchorNode)).toBe(true); }); test("press 'ctrl+a' in 'oe_structure' child should only select his content", async () => { const { el } = await setupEditor(`a[]b
cd
[ab]
cd
a[]b
cd
[ab]
cd
te[]st
", { config: { Plugins: [...MAIN_PLUGINS, TestPlugin] }, }); await insertText(editor, "/test"); await press("enter"); await animationFrame(); expect("input.test").toBeFocused(); // Something trigger restore const cursors = editor.shared.selection.preserveSelection(); cursors.restore(); expect("input.test").toBeFocused(); }); test("set a collapse selection in a contenteditable false should move it after this node", async () => { const { el, editor } = await setupEditor(`abcdef
`); editor.shared.selection.setSelection({ anchorNode: queryOne("span[contenteditable='false']"), anchorOffset: 1, }); editor.shared.selection.focusEditable(); expect(getContent(el)).toBe(`abcd[]ef
`); }); test("preserveSelection's restore should always set the selection, even if it's the same as the current one", async () => { /** * There seems to be a bug in Chrome that renders the selection in a * different position than the one returned by document.getSelection(). * Setting the selection (even if it's the same as the current one) seems to * solve the issue. * * A concrete example: *abc some link def[]
* press shift + enter * The selection (in Chrome) is rendered at the following position if * setBaseAndExtent is skipped when setting the selection after a restore: *abc some link[] def
ab[]cd
"); patchWithCleanup(editor.document.getSelection(), { setBaseAndExtent: () => { expect.step("setBaseAndExtent"); }, }); const cursors = editor.shared.selection.preserveSelection(); cursors.restore(); expect.verifySteps(["setBaseAndExtent"]); }); /** @deprecated these are legacy functions, replaced by `getTargetedNodes` */ describe("getters", () => { describe("getTraversedNodes", () => { test("should return the anchor node of a collapsed selection", async () => { const { editor } = await setupEditor("a[]bc
a[bc
a[bc
ab[]cd
"); const abcd = editable.firstChild.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([abcd]); }); test("should find that a the range traverses the next paragraph as well", async () => { const { el: editable, editor } = await setupEditor("ab[cd
ef]gh
"); const p1 = editable.firstChild; const abcd = p1.firstChild; const p2 = editable.childNodes[1]; const efgh = p2.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([p1, abcd, p2, efgh]); }); test("should find all traversed nodes in nested range", async () => { const { el: editable, editor } = await setupEditor( 'ab[cd
ef]gh
ab
cd
ab
]
cd
ab
cd]
ef
ab
cd
]ef
ab
cd
]
ab
cd
]
ef
ab[
cd
ef
]", stepFunction: (editor) => { const editable = editor.editable; const p1 = editable.firstChild; const ab = p1.firstChild; const br = ab.nextSibling; const cd = br.nextSibling; const p2 = editable.lastChild; const ef = p2.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([p1, br, cd, p2, ef]); }, }); }); test("selection starts before a br in start of p element", async () => { await testEditor({ contentBefore: "[ab
cd
ef
]", stepFunction: (editor) => { const editable = editor.editable; const p1 = editable.firstChild; const ab = p1.firstChild; const br = ab.nextSibling; const cd = br.nextSibling; const p2 = editable.lastChild; const ef = p2.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([p1, ab, br, cd, p2, ef]); }, }); }); test("selection starts after a br at end of p element", async () => { await testEditor({ contentBefore: "ab
[
cd
]", stepFunction: (editor) => { const editable = editor.editable; const p2 = editable.lastChild; const cd = p2.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([p2, cd]); }, }); }); test("selection starts after a br in middle of p element", async () => { await testEditor({ contentBefore: "ab
[cd
ef
]", stepFunction: (editor) => { const editable = editor.editable; const p1 = editable.firstChild; const ab = p1.firstChild; const br = ab.nextSibling; const cd = br.nextSibling; const p2 = editable.lastChild; const ef = p2.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([p1, cd, p2, ef]); }, }); }); test("selection starts between 2 br elements", async () => { await testEditor({ contentBefore: "ab
[
cd
ef
]", stepFunction: (editor) => { const editable = editor.editable; const p1 = editable.firstChild; const ab = p1.firstChild; const br1 = ab.nextSibling; const br2 = br1.nextSibling; const cd = br2.nextSibling; const p2 = editable.firstChild.nextSibling; const ef = p2.firstChild; const result = editor.shared.selection.getTraversedNodes(); expect(result).toEqual([p1, br2, cd, p2, ef]); }, }); }); test("selection within table cells 1", async () => { await testEditor({ contentBefore: "| abcd[e | f]g |
| abcd [ e | f]g |
ab[]cd
", stepFunction: (editor) => { const result = editor.shared.selection.getSelectedNodes(); expect(result).toEqual([]); }, contentAfter: "ab[]cd
", }); }); test("should find that no node is fully selected", async () => { await testEditor({ contentBefore: "ab[c]d
", stepFunction: (editor) => { const result = editor.shared.selection.getSelectedNodes(); expect(result).toEqual([]); }, }); }); test("should find that no node is fully selected, across blocks", async () => { await testEditor({ contentBefore: "ab[cd
ef]gh
", stepFunction: (editor) => { const result = editor.shared.selection.getSelectedNodes(); expect(result).toEqual([]); }, }); }); test("should find that a text node is fully selected", async () => { await testEditor({ contentBefore: 'ab[cd]
', stepFunction: (editor) => { const editable = editor.editable; const result = editor.shared.selection.getSelectedNodes(); const cd = editable.firstChild.lastChild; expect(result).toEqual([cd]); }, }); }); test("should find that a block is fully selected", async () => { await testEditor({ contentBefore: "[ab
cd
ef]gh
", stepFunction: (editor) => { const editable = editor.editable; const result = editor.shared.selection.getSelectedNodes(); const ab = editable.firstChild.firstChild; const p2 = editable.childNodes[1]; const cd = p2.firstChild; expect(result).toEqual([ab, p2, cd]); }, }); }); test("should find all selected nodes in nested range", async () => { await testEditor({ contentBefore: 'ab[cd
ef]gh
abc[]def
"); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual(["abcdef"]); }); test("should return the targeted text node (collapsed, in a complex DOM)", async () => { const { editor } = await setupEditor("a[]bc
ab[cd]ef
"); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual(["abcdef"]); }); test("should return the targeted text node (full selection)", async () => { const { editor } = await setupEditor("[abcdef]
"); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual(["abcdef"]); }); test("should return the targeted text node before an inline element", async () => { const { editor } = await setupEditor(`[ab]cd
`); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual(["ab"]); }); test("should return the targeted text node after an inline element", async () => { const { editor } = await setupEditor(`ab[cd]
`); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual(["cd"]); }); }); describe("across inline elements", () => { test("should include a selected inline element (from its left outer edge)", async () => { const { editor } = await setupEditor("ab[cdef]
"); // "ab" isn't included because no part of it is selected. expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual([ "SPAN", "cd", "ef", ]); }); test("should include a selected inline element (from its left inner edge)", async () => { const { editor } = await setupEditor("ab[cdef]
"); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual([ "SPAN", "cd", "ef", ]); }); test("should include a selected inline element (until its right outer edge)", async () => { const { editor } = await setupEditor("[abcd]ef
"); // "ef" isn't included because no part of it is selected. expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual([ "ab", "SPAN", "cd", ]); }); test("should include a selected inline element (until its right inner edge)", async () => { const { editor } = await setupEditor("[abcd]ef
"); expect(nameNodes(editor.shared.selection.getTargetedNodes())).toEqual([ "ab", "SPAN", "cd", ]); }); }); }); describe("across blocks", () => { describe("basic", () => { test("should include intersected blocks", async () => { const { el: editable, editor } = await setupEditor("ab[cd
ef]gh
"); const p1 = editable.firstChild; // The selection crossed `` -> include it. const abcd = p1.firstChild; const p2 = editable.childNodes[1]; // The selection crossed `` -> include it. const efgh = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, abcd, p2, efgh]); }); test("should include intersected blocks, including an empty one", async () => { const { el: editable, editor } = await setupEditor("
ab[cd
]
` -> include it. const br = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, abcd, p2, br]); }); test("should include intersected blocks (across three blocks)", async () => { const { el: editable, editor } = await setupEditor( "
[ab
cd
ef]gh
" ); const p1 = editable.firstChild; // The selection crossed `` -> include it. const ab = p1.firstChild; const p2 = p1.nextSibling; const cd = p2.firstChild; const p3 = p2.nextSibling; // The selection crossed `` -> include it. const ef = p3.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2, cd, p3, ef]); }); test("should include intersected blocks within a common block", async () => { const { el: editable, editor } = await setupEditor( "
a[bc
a[b
ab[cd
ef]gh
abc
]ab
cd
ab
cd
ef]
"); const p1 = editable.firstChild; const ab = p1.firstChild; const br = ab.nextSibling; const cd = br.nextSibling; const p2 = editable.lastChild; // The selection crossed `` -> include it. const ef = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, br, cd, p2, ef]); }); test("should include an outwardly selected block and an intersected block (right outer edge)", async () => { const { el: editable, editor } = await setupEditor("
[ab
cd
ef
]"); const p1 = editable.firstChild; // The selection crossed `` -> include it. const ab = p1.firstChild; const br = ab.nextSibling; const cd = br.nextSibling; const p2 = editable.lastChild; const ef = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, br, cd, p2, ef]); }); }); describe("edges and brs", () => { test("should include intersected blocks, including an empty one (selection across two blocks, from/to inner right edge)", async () => { const { el: editable, editor } = await setupEditor("ab[
cd]
"); const p1 = editable.childNodes[0]; // The selection crossed `` -> include it. // "ab" isn't included because no part of it is selected. const p2 = p1.nextSibling; // The selection crossed `` -> include it. const cd = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, p2, cd]); }); test("
ab[
cd
]", async () => { const { el: editable, editor } = await setupEditor("ab[
cd
]"); const p1 = editable.childNodes[0]; // The selection crossed `` -> include it. // "ab" isn't included because no part of it is selected. const p2 = p1.nextSibling; // The selection crossed `` -> include it. const cd = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, p2, cd]); }); test.tags("former triple-click"); test("should include intersected blocks, including an empty one (selection across two blocks, from inner right to inner left edge)", async () => { const { el: editable, editor } = await setupEditor("
abcd[
]
` -> include it. // The BR is not included because the selection ended at (p2, // 0), not in the BR. const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, p2]); }); test.tags("former triple-click"); test("should include the targeted nodes until the beginning of a new block", async () => { const { editor } = await setupEditor("
ab[cd
ab
]cd
"); const p1 = editable.firstChild; const ab = p1.firstChild; const p2 = p1.nextSibling; // The selection crossed `` -> include it. // "cd" isn't included because no part of it is selected. const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2]); }); test.tags("former triple-click"); test("should include the targeted nodes until the beginning of a new block, and an outwardly selected block (2)", async () => { const { el: editable, editor } = await setupEditor("[
ab
]
cd
` -> include it. // The BR is not included because the selection ended at (p2, // 0), not in the BR. const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2]); }); test.tags("former triple-click"); test("should include the targeted nodes until the beginning of a new block, and an outwardly selected block (3)", async () => { const { el: editable, editor } = await setupEditor("[
ab
]
` -> include it. // The BR is not included because the selection ended at (p2, // 0), not in the BR. const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2]); }); test("should not include a non-selected BR just after selection", async () => { const { el: editable, editor } = await setupEditor( "[
ab
cd]
ef
` -> include it. const firstBr = p2.firstChild; const cd = firstBr.nextSibling; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2, firstBr, cd]); }); test("should not include a non-selected BR just before selection", async () => { const { el: editable, editor } = await setupEditor("
ab
[cd
ef
]"); const p1 = editable.firstChild; // The selection crossed `` -> include it. const ab = p1.firstChild; const br = ab.nextSibling; const cd = br.nextSibling; const p2 = editable.lastChild; const ef = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, cd, p2, ef]); }); test("should not include a non-selected BR just before selection (from outer right edge)", async () => { const { el: editable, editor } = await setupEditor("ab
[
cd
]"); const p1 = editable.firstChild; const p2 = editable.lastChild; const cd = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, p2, cd]); }); test("should include a selected BR just at the end of selection", async () => { const { el: editable, editor } = await setupEditor( "[ab
cd
]ef
` -> include it. const br1 = p2.firstChild; const cd = br1.nextSibling; const br2 = cd.nextSibling; // "ef" isn't included because no part of it is selected. const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2, br1, cd, br2]); }); test("should include a selected BR just at the beginning of selection", async () => { const { el: editable, editor } = await setupEditor("
ab[
cd
ef
]"); const p1 = editable.firstChild; // The selection crossed `` -> include it. // "ab" isn't included because no part of it is selected. const br = p1.childNodes[1]; const cd = br.nextSibling; const p2 = editable.lastChild; const ef = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, br, cd, p2, ef]); }); test("should include a selected BR just at the end of selection and of block", async () => { const { el: editable, editor } = await setupEditor("[ab
cd
]
` -> include it. const br1 = p2.firstChild; const cd = br1.nextSibling; const br2 = cd.nextSibling; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2, br1, cd, br2]); }); test("should include a selected BR just at the end of selection but not its non-selected BR sibling", async () => { const { el: editable, editor } = await setupEditor( "[
ab
cd
]
ef
` -> include it. const cd = p2.firstChild; const br1 = cd.nextSibling; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, ab, p2, cd, br1]); }); test("should include a selected BR just at the beginning of selection but not its non-selected BR sibling", async () => { const { el: editable, editor } = await setupEditor( "
ab
[
cd
ef
]" ); const p1 = editable.firstChild; // The selection crossed `` -> include it. const ab = p1.firstChild; const br1 = ab.nextSibling; const br2 = br1.nextSibling; const cd = br2.nextSibling; const p2 = editable.firstChild.nextSibling; const ef = p2.firstChild; const result = editor.shared.selection.getTargetedNodes(); expect(result).toEqual([p1, br2, cd, p2, ef]); }); }); test("should return an image in a parent selection", async () => { const { editor } = await setupEditor(`| abcd[e | f]g |
| abcd [ e | f]g |
abc
"); const p = el.firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: p.firstChild, anchorOffset: 0, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 0, p.firstChild, 0]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 0, p.firstChild, 0, ]); }); test("should collapse the cursor within an element", async () => { const { editor, el } = await setupEditor("abcd
"); const p = el.firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: p.firstChild, anchorOffset: 2, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 2, p.firstChild, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 2, p.firstChild, 2, ]); }); test("should collapse the cursor at the end of an element", async () => { const { editor, el } = await setupEditor("abc
"); const p = el.firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: p.firstChild, anchorOffset: 3, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 3, p.firstChild, 3]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 3, p.firstChild, 3, ]); }); test("should collapse the cursor before a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const cd = p.childNodes[1].firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: cd, anchorOffset: 2, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([cd, 2, cd, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([cd, 2, cd, 2]); }); test("should collapse the cursor at the beginning of a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const ef = p.childNodes[1].childNodes[1].firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: ef, anchorOffset: 0, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 0, ef, 0]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 0, ef, 0]); }); test("should collapse the cursor within a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghijkl
"); const p = el.firstChild; const efgh = p.childNodes[1].childNodes[1].firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: efgh, anchorOffset: 2, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([efgh, 2, efgh, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ efgh, 2, efgh, 2, ]); }); test("should collapse the cursor at the end of a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const ef = p.childNodes[1].childNodes[1].firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: ef, anchorOffset: 2, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 2, ef, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 2, ef, 2]); }); test("should collapse the cursor after a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const ef = p.childNodes[1].childNodes[1].firstChild; const gh = p.childNodes[1].lastChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: gh, anchorOffset: 0, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 2, ef, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 2, ef, 2]); const nonNormalizedResult = getProcessSelection( editor.shared.selection.setSelection( { anchorNode: gh, anchorOffset: 0 }, { normalize: false } ) ); editor.shared.selection.focusEditable(); expect(nonNormalizedResult).toEqual([gh, 0, gh, 0]); const sel = document.getSelection(); expect([sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset]).toEqual([ gh, 0, gh, 0, ]); }); }); describe("forward", () => { test("should select the contents of an element", async () => { const { editor, el } = await setupEditor("abc
"); const p = el.firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: p.firstChild, anchorOffset: 0, focusNode: p.firstChild, focusOffset: 3, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 0, p.firstChild, 3]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 0, p.firstChild, 3, ]); }); test("should make a complex selection", async () => { const { el, editor } = await setupEditor( "abcdefghij
klmnopqrst
" ); const [p1, p2] = el.childNodes; const ef = p1.childNodes[1].childNodes[1].firstChild; const qr = p2.childNodes[1].childNodes[2]; const st = p2.childNodes[2]; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: ef, anchorOffset: 1, focusNode: st, focusOffset: 0, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 1, qr, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 1, qr, 2]); const nonNormalizedResult = getProcessSelection( editor.shared.selection.setSelection( { anchorNode: ef, anchorOffset: 1, focusNode: st, focusOffset: 0, }, { normalize: false } ) ); expect(nonNormalizedResult).toEqual([ef, 1, st, 0]); const sel = document.getSelection(); expect([sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset]).toEqual([ ef, 1, st, 0, ]); }); }); describe("backward", () => { test("should select the contents of an element", async () => { const { editor, el } = await setupEditor("abc
"); const p = el.firstChild; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: p.firstChild, anchorOffset: 3, focusNode: p.firstChild, focusOffset: 0, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 3, p.firstChild, 0]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 3, p.firstChild, 0, ]); }); test("should make a complex selection", async () => { const { el, editor } = await setupEditor( "abcdefghij
klmnopqrst
" ); const [p1, p2] = el.childNodes; const ef = p1.childNodes[1].childNodes[1].firstChild; const qr = p2.childNodes[1].childNodes[2]; const st = p2.childNodes[2]; const result = getProcessSelection( editor.shared.selection.setSelection({ anchorNode: st, anchorOffset: 0, focusNode: ef, focusOffset: 1, }) ); editor.shared.selection.focusEditable(); expect(result).toEqual([qr, 2, ef, 1]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([qr, 2, ef, 1]); const nonNormalizedResult = getProcessSelection( editor.shared.selection.setSelection( { anchorNode: st, anchorOffset: 0, focusNode: ef, focusOffset: 1, }, { normalize: false } ) ); expect(nonNormalizedResult).toEqual([st, 0, ef, 1]); const sel = document.getSelection(); expect([sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset]).toEqual([ st, 0, ef, 1, ]); }); }); }); describe("setCursorStart", () => { test("should collapse the cursor at the beginning of an element", async () => { const { editor, el } = await setupEditor("abc
"); const p = el.firstChild; const result = getProcessSelection(editor.shared.selection.setCursorStart(p)); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 0, p.firstChild, 0]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 0, p.firstChild, 0, ]); }); test("should collapse the cursor at the beginning of a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const b = p.childNodes[1].childNodes[1]; const ef = b.firstChild; const result = getProcessSelection(editor.shared.selection.setCursorStart(b)); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 0, ef, 0]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 0, ef, 0]); }); test("should collapse the cursor after a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const ef = p.childNodes[1].childNodes[1].firstChild; const gh = p.childNodes[1].lastChild; const result = getProcessSelection(editor.shared.selection.setCursorStart(gh)); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 2, ef, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 2, ef, 2]); // @todo @phoenix normalize false is never use // const nonNormalizedResult = getProcessSelection(editor.shared.selection.setCursorStart(gh, false)); // expect(nonNormalizedResult).toEqual([gh, 0, gh, 0]); // const sel = document.getSelection(); // expect([sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset]).toEqual([ // gh, // 0, // gh, // 0, // ]); }); }); describe("setCursorEnd", () => { test("should collapse the cursor at the end of an element", async () => { const { editor, el } = await setupEditor("abc
"); const p = el.firstChild; const result = getProcessSelection(editor.shared.selection.setCursorEnd(p)); editor.shared.selection.focusEditable(); expect(result).toEqual([p.firstChild, 3, p.firstChild, 3]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ p.firstChild, 3, p.firstChild, 3, ]); }); test("should collapse the cursor before a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const cd = p.childNodes[1].firstChild; const result = getProcessSelection(editor.shared.selection.setCursorEnd(cd)); editor.shared.selection.focusEditable(); expect(result).toEqual([cd, 2, cd, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([cd, 2, cd, 2]); }); test("should collapse the cursor at the end of a nested inline element", async () => { const { editor, el } = await setupEditor("abcdefghij
"); const p = el.firstChild; const b = p.childNodes[1].childNodes[1]; const ef = b.firstChild; const result = getProcessSelection(editor.shared.selection.setCursorEnd(b)); editor.shared.selection.focusEditable(); expect(result).toEqual([ef, 2, ef, 2]); const { anchorNode, anchorOffset, focusNode, focusOffset } = document.getSelection(); expect([anchorNode, anchorOffset, focusNode, focusOffset]).toEqual([ef, 2, ef, 2]); }); }); }); test.tags("desktop"); test("should not autoscroll if selection is partially visible in viewport", async () => { class Test extends models.Model { name = fields.Char(); txt = fields.Html(); _records = [{ id: 1, name: "Test", txt: "This is some text
".repeat(50) }]; } defineModels([Test]); await mountView({ type: "form", resId: 1, resModel: "test", arch: ` `, }); const scrollableElement = queryOne(".o_content"); const editable = queryOne(".odoo-editor-editable"); const lastParagraph = editable.lastElementChild; const fifthLastParagraph = editable.children[45]; // Select the last five paragraphs in backward. setSelection({ anchorNode: lastParagraph, anchorOffset: 1, focusNode: fifthLastParagraph, focusOffset: 0, }); await animationFrame(); // Both ends of the selection are initially visible in the viewport. expect(isInViewPort(fifthLastParagraph)).toBe(true); expect(isInViewPort(lastParagraph)).toBe(true); // Scroll above so that last paragraph becomes invisible in viewport. scrollableElement.scrollTop -= 70; await animationFrame(); expect(isInViewPort(lastParagraph)).toBe(false); expect(isInViewPort(fifthLastParagraph)).toBe(true); const scrollTop = scrollableElement.scrollTop; // Extend the selection to include one more paragraph above. setSelection({ anchorNode: lastParagraph, anchorOffset: 1, focusNode: fifthLastParagraph.previousElementSibling, focusOffset: 0, }); await animationFrame(); // Ensure that extending selection did not trigger any auto-scrolling. expect(scrollableElement.scrollTop).toBe(scrollTop); expect(isInViewPort(lastParagraph)).toBe(false); });