From 6958f719e87f0e6a856e3eab08bb437002e9131e Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Wed, 7 Jun 2023 15:12:32 +0000 Subject: [PATCH] [FIX] web_editor: loop non-editable in getNormalizedCursorPosition There were two issues with the `getNormalizedCursorPosition`: 1. If the selection was in a non-editable element, the function called `rightPos` only once, without ensuring that the new position was editable. -> The current implementation fixes that by making it loop while in a non-editable element, and also allows the normalized position to use `leftPos` if the offset of a position is 0. 2. When searching for the deepest leaf to the right, the `DOMPathGenerator` was allowed to traverse non-editable elements, which is not correct since the result should always be editable for a normalized position. -> This is fixed, and the name of the `DOMPathGenerator` utils is updated for clarity. task-3280704 closes odoo/odoo#125204 X-original-commit: 3d44028a7722f61900b8669489ff929244b1a4b8 Related: odoo/enterprise#42615 Signed-off-by: David Beguin (dbe) Signed-off-by: Damien Abeloos (abd) --- .../js/editor/odoo-editor/src/utils/utils.js | 31 ++++--- .../odoo-editor/test/spec/utils.test.js | 83 ++++++++++++++++++- 2 files changed, 101 insertions(+), 13 deletions(-) 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', () => {