[FW][FIX] web_editor: correct delete forward and backward on non editable

Trying to delete forward a non editable was erasing the first
character of the non editable.
Trying to delete backward one character after the contenteditable false
made the cursor jump before the contenteditable.

closes odoo/odoo#74623

X-original-commit: 9b7d9ad3771579ccb60a1be526f5d7571f64e638
Signed-off-by: Antoine Guenet <Zinston@users.noreply.github.com>
This commit is contained in:
Nicolas Bayet
2021-08-03 11:32:49 +00:00
parent b006e7e8c3
commit 112aad4863
4 changed files with 62 additions and 26 deletions
@@ -30,6 +30,7 @@ import {
splitElement,
splitTextNode,
startPos,
nodeSize,
} from '../utils/utils.js';
const TEXT_CLASSES_REGEX = /\btext-[^\s]*\b/g;
@@ -11,8 +11,9 @@ import {
CTYPES,
leftPos,
isFontAwesome,
rightLeafOnlyNotBlockPath,
rightLeafOnlyPath,
rightLeafOnlyNotBlockNotEditablePath,
rightLeafOnlyPathNotBlockNotEditablePath,
isNotEditableNode,
} from '../utils/utils.js';
Text.prototype.oDeleteForward = function (offset) {
@@ -24,23 +25,27 @@ Text.prototype.oDeleteForward = function (offset) {
};
HTMLElement.prototype.oDeleteForward = function (offset) {
const filterFunc = node => isVisibleEmpty(node) || isContentTextNode(node);
const filterFunc = node =>
isVisibleEmpty(node) || isContentTextNode(node) || isNotEditableNode(node);
const firstInlineNode = findNode(rightLeafOnlyNotBlockPath(this, offset), filterFunc);
if (isFontAwesome(firstInlineNode && firstInlineNode.parentElement)) {
firstInlineNode.parentElement.remove();
const firstLeafNode = findNode(rightLeafOnlyNotBlockNotEditablePath(this, offset), filterFunc);
if (firstLeafNode && (isFontAwesome(firstLeafNode) || isNotEditableNode(firstLeafNode))) {
firstLeafNode.remove();
return;
}
if (
firstInlineNode &&
(firstInlineNode.nodeName !== 'BR' ||
getState(...rightPos(firstInlineNode), DIRECTIONS.RIGHT).cType !== CTYPES.BLOCK_INSIDE)
firstLeafNode &&
(firstLeafNode.nodeName !== 'BR' ||
getState(...rightPos(firstLeafNode), DIRECTIONS.RIGHT).cType !== CTYPES.BLOCK_INSIDE)
) {
firstInlineNode.oDeleteBackward(Math.min(1, nodeSize(firstInlineNode)));
firstLeafNode.oDeleteBackward(Math.min(1, nodeSize(firstLeafNode)));
return;
}
const firstOutNode = findNode(
rightLeafOnlyPath(...(firstInlineNode ? rightPos(firstInlineNode) : [this, offset])),
rightLeafOnlyPathNotBlockNotEditablePath(
...(firstLeafNode ? rightPos(firstLeafNode) : [this, offset]),
),
filterFunc,
);
if (firstOutNode) {
@@ -133,11 +133,11 @@ const PATH_END_REASONS = {
*
* @see leftLeafFirstPath
* @see leftLeafOnlyNotBlockPath
* @see leftLeafOnlyInScopeNotBlockPath
* @see rightLeafOnlyNotBlockPath
* @see rightLeafOnlyPath
* @see rightLeafOnlyNotBlockInScopePath
* @see leftLeafOnlyInScopeNotBlockNoEditablePath
* @see rightLeafOnlyNotBlockPath
* @see rightLeafOnlyPathNotBlockNotEditablePath
* @see rightLeafOnlyInScopeNotBlockPath
* @see rightLeafOnlyNotBlockNotEditablePath
*
* @param {number} direction
* @param {boolean} [options.leafOnly] if true, do not yield any non-leaf node
@@ -456,7 +456,8 @@ export function getNormalizedCursorPosition(node, offset, full = true) {
}
}
if (el) {
const leftInlineNode = leftLeafOnlyInScopeNotBlockPath(el, elOffset).next().value;
const leftInlineNode = leftLeafOnlyInScopeNotBlockNoEditablePath(el, elOffset).next()
.value;
let leftVisibleEmpty = false;
if (leftInlineNode) {
leftVisibleEmpty =
@@ -467,7 +468,7 @@ export function getNormalizedCursorPosition(node, offset, full = true) {
: endPos(leftInlineNode);
}
if (!leftInlineNode || leftVisibleEmpty) {
const rightInlineNode = rightLeafOnlyNotBlockInScopePath(el, elOffset).next().value;
const rightInlineNode = rightLeafOnlyInScopeNotBlockPath(el, elOffset).next().value;
if (rightInlineNode) {
const rightVisibleEmpty =
isVisibleEmpty(rightInlineNode) ||
@@ -1889,17 +1890,23 @@ export function getRangePosition(el, document, options = {}) {
return offset;
}
export const isNotEditableNode = node =>
node.getAttribute &&
node.getAttribute('contenteditable') &&
node.getAttribute('contenteditable').toLowerCase() === 'false';
export const leftLeafFirstPath = createDOMPathGenerator(DIRECTIONS.LEFT);
export const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, {
leafOnly: true,
stopTraverseFunction: isBlock,
stopFunction: isBlock,
});
export const leftLeafOnlyInScopeNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, {
export const leftLeafOnlyInScopeNotBlockNoEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, {
leafOnly: true,
inScope: true,
stopTraverseFunction: isBlock,
stopFunction: isBlock,
stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node),
stopFunction: node => isNotEditableNode(node) || isBlock(node),
});
export const rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {
@@ -1908,12 +1915,17 @@ export const rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT
stopFunction: isBlock,
});
export const rightLeafOnlyPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {
export const rightLeafOnlyPathNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {
leafOnly: true,
});
export const rightLeafOnlyNotBlockInScopePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {
export const rightLeafOnlyInScopeNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {
leafOnly: true,
inScope: true,
stopTraverseFunction: isBlock,
stopFunction: isBlock,
});
export const rightLeafOnlyNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {
leafOnly: true,
stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node),
stopFunction: node => isBlock(node) && !isNotEditableNode(node),
});
@@ -157,18 +157,18 @@ X[]
});
it('should remove contenteditable="false"', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><span contenteditable="false">abc</span>[]def</div>`,
contentBefore: `<div>[]<span contenteditable="false">abc</span>def</div>`,
stepFunction: async editor => {
await deleteBackward(editor);
await deleteForward(editor);
},
contentAfter: `<div>[]def</div>`,
});
});
it('should remove contenteditable="False"', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><span contenteditable="False">abc</span>[]def</div>`,
contentBefore: `<div>[]<span contenteditable="False">abc</span>def</div>`,
stepFunction: async editor => {
await deleteBackward(editor);
await deleteForward(editor);
},
contentAfter: `<div>[]def</div>`,
});
@@ -1043,6 +1043,24 @@ X[]
contentAfter: `<p>mollis.[]<i>Pelentesque</i></p>`,
});
});
it('should remove contenteditable="false"', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><span contenteditable="false">abc</span>[]def</div>`,
stepFunction: async editor => {
await deleteBackward(editor);
},
contentAfter: `<div>[]def</div>`,
});
});
it('should remove contenteditable="False"', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><span contenteditable="False">abc</span>[]def</div>`,
stepFunction: async editor => {
await deleteBackward(editor);
},
contentAfter: `<div>[]def</div>`,
});
});
});
describe('Line breaks', () => {
describe('Single', () => {