diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js index 6f8ff84e743..06679a20dff 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js @@ -157,10 +157,10 @@ const PATH_END_REASONS = { * * @see leftLeafFirstPath * @see leftLeafOnlyNotBlockPath - * @see leftLeafOnlyInScopeNotBlockNoEditablePath + * @see leftLeafOnlyInScopeNotBlockEditablePath * @see rightLeafOnlyNotBlockPath * @see rightLeafOnlyPathNotBlockNotEditablePath - * @see rightLeafOnlyInScopeNotBlockPath + * @see rightLeafOnlyInScopeNotBlockEditablePath * @see rightLeafOnlyNotBlockNotEditablePath * * @param {number} direction @@ -469,9 +469,17 @@ export function hasValidSelection(editable) { * positions which are not possible, like the cursor inside an image). */ export function getNormalizedCursorPosition(node, offset, full = true) { - if (isSelfClosingElement(node) || !closestElement(node).isContentEditable) { - // Cannot put cursor inside those elements, put it after instead. - [node, offset] = rightPos(node); + const editable = closestElement(node, '.odoo-editor-editable'); + let closest = closestElement(node); + while ( + closest && + closest !== editable && + (isSelfClosingElement(node) || !closest.isContentEditable) + ) { + // Cannot put the cursor inside those elements, put it before if the + // offset is 0 and the node is not empty, else after instead. + [node, offset] = offset || !nodeSize(node) ? rightPos(node) : leftPos(node); + closest = closestElement(node); } // Be permissive about the received offset. @@ -495,8 +503,7 @@ export function getNormalizedCursorPosition(node, offset, full = true) { } } if (el) { - const leftInlineNode = leftLeafOnlyInScopeNotBlockNoEditablePath(el, elOffset).next() - .value; + const leftInlineNode = leftLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value; let leftVisibleEmpty = false; if (leftInlineNode) { leftVisibleEmpty = @@ -507,7 +514,7 @@ export function getNormalizedCursorPosition(node, offset, full = true) { : endPos(leftInlineNode); } if (!leftInlineNode || leftVisibleEmpty) { - const rightInlineNode = rightLeafOnlyInScopeNotBlockPath(el, elOffset).next().value; + const rightInlineNode = rightLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value; if (rightInlineNode) { const closest = closestElement(rightInlineNode); const rightVisibleEmpty = @@ -2530,7 +2537,7 @@ export const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, stopTraverseFunction: isBlock, stopFunction: isBlock, }); -export const leftLeafOnlyInScopeNotBlockNoEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, { +export const leftLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, { leafOnly: true, inScope: true, stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node), @@ -2546,11 +2553,11 @@ export const rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT export const rightLeafOnlyPathNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, { leafOnly: true, }); -export const rightLeafOnlyInScopeNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, { +export const rightLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, { leafOnly: true, inScope: true, - stopTraverseFunction: isBlock, - stopFunction: isBlock, + stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node), + stopFunction: node => isNotEditableNode(node) || isBlock(node), }); export const rightLeafOnlyNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, { leafOnly: true, diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/utils.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/utils.test.js index d3ecab53011..d43e73464c7 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/utils.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/utils.test.js @@ -824,12 +824,93 @@ describe('Utils', () => { }); }); describe('getNormalizedCursorPosition', () => { - // TODO: test more. it('should move the cursor from after a to within it', () => { const [p] = insertTestHtml('

abcdef

'); const result = getNormalizedCursorPosition(p.lastChild, 0); window.chai.expect(result).to.eql([p.firstChild.firstChild, 3]); }); + it('should move the cursor before a non-editable element with offset === 0', () => { + const [p] = insertTestHtml('

leavemealone

'); + const result = getNormalizedCursorPosition(p.lastChild, 0); + window.chai.expect(result).to.eql([p, 0]); + }); + it('should move the cursor after a non-editable element with offset > 0', () => { + const [p] = insertTestHtml('

leavemealone

'); + const result = getNormalizedCursorPosition(p.lastChild, 1); + window.chai.expect(result).to.eql([p, 1]); + }); + it('should move the cursor after a "visibleEmpty" element', () => { + const [p] = insertTestHtml('

ab
cd

'); + const result = getNormalizedCursorPosition(p.lastElementChild, 0); + window.chai.expect(result).to.eql([p.lastChild, 0]); + }); + it('should move the cursor before a "fake line break element"', () => { + const [p] = insertTestHtml('


'); + const result = getNormalizedCursorPosition(p.lastElementChild, 0); + window.chai.expect(result).to.eql([p, 0]); + }); + it('should loop outside (left) a non-editable context and then find the deepest editable leaf position', () => { + const [p] = insertTestHtml(unformat(` +

+ text + + + text + + +

+ `)); + const start = p.querySelector(".start"); + const end = p.querySelector(".end"); + const result = getNormalizedCursorPosition(start.lastChild, 0); + window.chai.expect(result).to.eql([end.firstChild, 4]); + }); + it('should loop outside (right) a non-editable context and then find the deepest editable leaf position', () => { + const [p] = insertTestHtml(unformat(` +

+ + + text + + + text +

+ `)); + const start = p.querySelector(".start"); + const end = p.querySelector(".end"); + const result = getNormalizedCursorPosition(start.lastChild, 1); + window.chai.expect(result).to.eql([end.lastChild, 0]); + }); + it('should loop outside (left) a non-editable context and not traverse a non-editable leaf position', () => { + const [p] = insertTestHtml(unformat(` +

+ leavemealone + + + text + + +

+ `)); + const start = p.querySelector(".start"); + const result = getNormalizedCursorPosition(start.lastChild, 0); + window.chai.expect(result).to.eql([p, 1]); + }); + it('should loop outside (right) a non-editable context and not traverse a non-editable leaf position', () => { + const [p] = insertTestHtml(unformat(` +

+ + + text + + + leavemealone +

+ `)); + const start = p.querySelector(".start"); + const result = getNormalizedCursorPosition(start.lastChild, 1); + window.chai.expect(result).to.eql([p, 1]); + }); }); describe('setCursor', () => { describe('collapsed', () => {