diff --git a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js index 256fc4ec27e..8934a046a4c 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js @@ -21,8 +21,7 @@ import { isShrunkBlock, isVisible, isVisibleStr, - leftDeepFirstPath, - nodeSize, + leftLeafFirstPath, preserveCursor, rightPos, setCursor, @@ -391,9 +390,9 @@ export const editorCommands = { // List indentList: (editor, mode = 'indent') => { const [pos1, pos2] = getCursors(editor.document); - const end = leftDeepFirstPath(...pos1).next().value; + const end = leftLeafFirstPath(...pos1).next().value; const li = new Set(); - for (const node of leftDeepFirstPath(...pos2)) { + for (const node of leftLeafFirstPath(...pos2)) { const cli = closestBlock(node); if ( cli && diff --git a/addons/web_editor/static/lib/odoo-editor/src/commands/deleteForward.js b/addons/web_editor/static/lib/odoo-editor/src/commands/deleteForward.js index aeff5c6ec19..50f9206b084 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/commands/deleteForward.js +++ b/addons/web_editor/static/lib/odoo-editor/src/commands/deleteForward.js @@ -5,14 +5,14 @@ import { isContentTextNode, isVisibleEmpty, nodeSize, - rightDeepOnlyPath, - rightDeepOnlyInlinePath, rightPos, getState, DIRECTIONS, CTYPES, leftPos, isFontAwesome, + rightLeafOnlyNotBlockPath, + rightLeafOnlyPath, } from '../utils/utils.js'; Text.prototype.oDeleteForward = function (offset) { @@ -26,7 +26,7 @@ Text.prototype.oDeleteForward = function (offset) { HTMLElement.prototype.oDeleteForward = function (offset) { const filterFunc = node => isVisibleEmpty(node) || isContentTextNode(node); - const firstInlineNode = findNode(rightDeepOnlyInlinePath(this, offset), filterFunc); + const firstInlineNode = findNode(rightLeafOnlyNotBlockPath(this, offset), filterFunc); if (isFontAwesome(firstInlineNode && firstInlineNode.parentElement)) { firstInlineNode.parentElement.remove(); return; @@ -40,7 +40,7 @@ HTMLElement.prototype.oDeleteForward = function (offset) { return; } const firstOutNode = findNode( - rightDeepOnlyPath(...(firstInlineNode ? rightPos(firstInlineNode) : [this, offset])), + rightLeafOnlyPath(...(firstInlineNode ? rightPos(firstInlineNode) : [this, offset])), filterFunc, ); if (firstOutNode) { diff --git a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js index decba9fa0b1..48ce436d0a8 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js @@ -131,30 +131,86 @@ const PATH_END_REASONS = { * Creates a generator function according to the given parameters. Pre-made * generators to traverse the DOM are made using this function: * - * @see leftDeepFirstPath - * @see leftDeepOnlyPath - * @see leftDeepFirstInlinePath - * @see leftDeepOnlyInlinePath - * - * @see rightDeepFirstPath - * @see rightDeepOnlyPath - * @see rightDeepFirstInlinePath - * @see rightDeepOnlyInlinePath + * @see leftLeafFirstPath + * @see leftLeafOnlyNotBlockPath + * @see leftLeafOnlyInScopeNotBlockPath + * @see rightLeafOnlyNotBlockPath + * @see rightLeafOnlyPath + * @see rightLeafOnlyNotBlockInScopePath + * @see rightLeafOnlyNotBlockPath * * @param {number} direction - * @param {boolean} deepOnly - * @param {boolean} inline + * @param {boolean} [options.leafOnly] if true, do not yield any non-leaf node + * @param {boolean} [options.inScope] if true, stop the generator as soon as a node is not + * a descendant of `node` provided when traversing the + * generated function. + * @param {Function} [options.stopTraverseFunction] a function that takes a node + * and should return true when a node descendant should not + * be traversed. + * @param {Function} [options.stopFunction] function that makes the generator stop when a + * node is encountered. */ -export function createDOMPathGenerator(direction, deepOnly, inline, inScope = false) { +export function createDOMPathGenerator( + direction, + { leafOnly = false, inScope = false, stopTraverseFunction, stopFunction } = {}, +) { const nextDeepest = direction === DIRECTIONS.LEFT - ? node => lastLeaf(node.previousSibling, inline) - : node => firstLeaf(node.nextSibling, inline); + ? node => lastLeaf(node.previousSibling, stopTraverseFunction) + : node => firstLeaf(node.nextSibling, stopTraverseFunction); const firstNode = direction === DIRECTIONS.LEFT - ? (node, offset) => lastLeaf(node.childNodes[offset - 1], inline) - : (node, offset) => firstLeaf(node.childNodes[offset], inline); + ? (node, offset) => lastLeaf(node.childNodes[offset - 1], stopTraverseFunction) + : (node, offset) => firstLeaf(node.childNodes[offset], stopTraverseFunction); + + // Note "reasons" is a way for the caller to be able to know why the + // generator ended yielding values. + return function* (node, offset, reasons = []) { + let movedUp = false; + + let currentNode = firstNode(node, offset); + if (!currentNode) { + movedUp = true; + currentNode = node; + } + + while (currentNode) { + if (stopFunction && stopFunction(currentNode)) { + reasons.push(movedUp ? PATH_END_REASONS.BLOCK_OUT : PATH_END_REASONS.BLOCK_HIT); + break; + } + if (inScope && currentNode === node) { + reasons.push(PATH_END_REASONS.OUT_OF_SCOPE); + break; + } + if (!(leafOnly && movedUp)) { + yield currentNode; + } + + movedUp = false; + let nextNode = nextDeepest(currentNode); + if (!nextNode) { + movedUp = true; + nextNode = currentNode.parentNode; + } + currentNode = nextNode; + } + + reasons.push(PATH_END_REASONS.NO_NODE); + }; +} + +export function createDOMPathGeneratorBak(direction, deepOnly, inline, inScope = false) { + const nextDeepest = + direction === DIRECTIONS.LEFT + ? node => lastLeaf(node.previousSibling, inline ? isBlock : undefined) + : node => firstLeaf(node.nextSibling, inline ? isBlock : undefined); + + const firstNode = + direction === DIRECTIONS.LEFT + ? (node, offset) => lastLeaf(node.childNodes[offset - 1], inline ? isBlock : undefined) + : (node, offset) => firstLeaf(node.childNodes[offset], inline ? isBlock : undefined); // Note "reasons" is a way for the caller to be able to know why the // generator ended yielding values. @@ -255,11 +311,11 @@ export function closestBlock(node) { * Returns the deepest child in last position. * * @param {Node} node - * @param {boolean} [stopAtBlock=false] + * @param {Function} [stopTraverseFunction] * @returns {Node} */ -export function lastLeaf(node, stopAtBlock = false) { - while (node && node.lastChild && !(stopAtBlock && isBlock(node))) { +export function lastLeaf(node, stopTraverseFunction) { + while (node && node.lastChild && !(stopTraverseFunction && stopTraverseFunction(node))) { node = node.lastChild; } return node; @@ -268,11 +324,11 @@ export function lastLeaf(node, stopAtBlock = false) { * Returns the deepest child in first position. * * @param {Node} node - * @param {boolean} [stopAtBlock=false] + * @param {Function} [stopTraverseFunction] * @returns {Node} */ -export function firstLeaf(node, stopAtBlock = false) { - while (node && node.firstChild && !(stopAtBlock && isBlock(node))) { +export function firstLeaf(node, stopTraverseFunction) { + while (node && node.firstChild && !(stopTraverseFunction && stopTraverseFunction(node))) { node = node.firstChild; } return node; @@ -400,7 +456,7 @@ export function getNormalizedCursorPosition(node, offset, full = true) { } } if (el) { - const leftInlineNode = leftDeepOnlyInlineInScopePath(el, elOffset).next().value; + const leftInlineNode = leftLeafOnlyInScopeNotBlockPath(el, elOffset).next().value; let leftVisibleEmpty = false; if (leftInlineNode) { leftVisibleEmpty = @@ -411,7 +467,7 @@ export function getNormalizedCursorPosition(node, offset, full = true) { : endPos(leftInlineNode); } if (!leftInlineNode || leftVisibleEmpty) { - const rightInlineNode = rightDeepOnlyInlineInScopePath(el, elOffset).next().value; + const rightInlineNode = rightLeafOnlyNotBlockInScopePath(el, elOffset).next().value; if (rightInlineNode) { const rightVisibleEmpty = isVisibleEmpty(rightInlineNode) || @@ -1434,8 +1490,8 @@ export function prepareUpdate(...args) { * @returns {Object} */ export function getState(el, offset, direction, leftCType) { - const leftDOMPath = leftDeepOnlyInlinePath; - const rightDOMPath = rightDeepOnlyInlinePath; + const leftDOMPath = leftLeafOnlyNotBlockPath; + const rightDOMPath = rightLeafOnlyNotBlockPath; let domPath; let inverseDOMPath; @@ -1685,10 +1741,10 @@ export function enforceWhitespace(el, offset, direction, rule) { let domPath; let expr; if (direction === DIRECTIONS.LEFT) { - domPath = leftDeepOnlyInlinePath(el, offset); + domPath = leftLeafOnlyNotBlockPath(el, offset); expr = /[^\S\u00A0]+$/; } else { - domPath = rightDeepOnlyInlinePath(el, offset); + domPath = rightLeafOnlyNotBlockPath(el, offset); expr = /^[^\S\u00A0]+/; } @@ -1833,25 +1889,31 @@ export function getRangePosition(el, document, options = {}) { return offset; } +export const leftLeafFirstPath = createDOMPathGenerator(DIRECTIONS.LEFT); +export const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, { + leafOnly: true, + stopTraverseFunction: isBlock, + stopFunction: isBlock, +}); +export const leftLeafOnlyInScopeNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, { + leafOnly: true, + inScope: true, + stopTraverseFunction: isBlock, + stopFunction: isBlock, +}); -export const leftDeepFirstPath = createDOMPathGenerator(DIRECTIONS.LEFT, false, false); -export const leftDeepOnlyPath = createDOMPathGenerator(DIRECTIONS.LEFT, true, false); -export const leftDeepFirstInlinePath = createDOMPathGenerator(DIRECTIONS.LEFT, false, true); -export const leftDeepOnlyInlinePath = createDOMPathGenerator(DIRECTIONS.LEFT, true, true); -export const leftDeepOnlyInlineInScopePath = createDOMPathGenerator( - DIRECTIONS.LEFT, - true, - true, - true, -); +export const rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, { + leafOnly: true, + stopTraverseFunction: isBlock, + stopFunction: isBlock, +}); -export const rightDeepFirstPath = createDOMPathGenerator(DIRECTIONS.RIGHT, false, false); -export const rightDeepOnlyPath = createDOMPathGenerator(DIRECTIONS.RIGHT, true, false); -export const rightDeepFirstInlinePath = createDOMPathGenerator(DIRECTIONS.RIGHT, false, true); -export const rightDeepOnlyInlinePath = createDOMPathGenerator(DIRECTIONS.RIGHT, true, true); -export const rightDeepOnlyInlineInScopePath = createDOMPathGenerator( - DIRECTIONS.RIGHT, - true, - true, - true, -); +export const rightLeafOnlyPath = createDOMPathGenerator(DIRECTIONS.RIGHT, { + leafOnly: true, +}); +export const rightLeafOnlyNotBlockInScopePath = createDOMPathGenerator(DIRECTIONS.RIGHT, { + leafOnly: true, + inScope: true, + stopTraverseFunction: isBlock, + stopFunction: isBlock, +}); diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/utils.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/utils.test.js index e377bcced2e..10251324fd8 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/utils.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/utils.test.js @@ -28,6 +28,7 @@ import { setCursorEnd, getCursorDirection, DIRECTIONS, + isBlock, } from '../../src/utils/utils.js'; import { BasicEditor, testEditor } from '../utils.js'; @@ -310,16 +311,6 @@ describe('Utils', () => { // TODO: test path functions: // - closestPath - // - leftDeepFirstPath - // - leftDeepOnlyPath - // - leftDeepFirstInlinePath - // - leftDeepOnlyInlinePath - // - leftDeepOnlyInlineInScopePath - // - rightDeepFirstPath - // - rightDeepOnlyPath - // - rightDeepFirstInlinePath - // - rightDeepOnlyInlinePath - // - rightDeepOnlyInlineInScopePath // - findNode // - createDOMPathGenerator describe('closestElement', () => { @@ -391,7 +382,7 @@ describe('Utils', () => { const [div] = insertTestHtml( '
abcdef
abcdef