[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) <dbe@odoo.com> Signed-off-by: Damien Abeloos (abd) <abd@odoo.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -824,12 +824,93 @@ describe('Utils', () => {
|
||||
});
|
||||
});
|
||||
describe('getNormalizedCursorPosition', () => {
|
||||
// TODO: test more.
|
||||
it('should move the cursor from after a <b> to within it', () => {
|
||||
const [p] = insertTestHtml('<p><b>abc</b>def</p>');
|
||||
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('<p><span contenteditable="false">leavemealone</span></p>');
|
||||
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('<p><span contenteditable="false">leavemealone</span></p>');
|
||||
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('<p>ab<br>cd</p>');
|
||||
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('<p><br></p>');
|
||||
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(`
|
||||
<p>
|
||||
<a class="end">text</a>
|
||||
<span contenteditable="false">
|
||||
<b class="start">
|
||||
text
|
||||
</b>
|
||||
</span>
|
||||
</p>
|
||||
`));
|
||||
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(`
|
||||
<p>
|
||||
<span contenteditable="false">
|
||||
<b class="start">
|
||||
text
|
||||
</b>
|
||||
</span>
|
||||
<a class="end">text</a>
|
||||
</p>
|
||||
`));
|
||||
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(`
|
||||
<p>
|
||||
<a contenteditable="false">leavemealone</a>
|
||||
<span contenteditable="false">
|
||||
<b class="start">
|
||||
text
|
||||
</b>
|
||||
</span>
|
||||
</p>
|
||||
`));
|
||||
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(`
|
||||
<p>
|
||||
<span contenteditable="false">
|
||||
<b class="start">
|
||||
text
|
||||
</b>
|
||||
</span>
|
||||
<a contenteditable="false">leavemealone</a>
|
||||
</p>
|
||||
`));
|
||||
const start = p.querySelector(".start");
|
||||
const result = getNormalizedCursorPosition(start.lastChild, 1);
|
||||
window.chai.expect(result).to.eql([p, 1]);
|
||||
});
|
||||
});
|
||||
describe('setCursor', () => {
|
||||
describe('collapsed', () => {
|
||||
|
||||
Reference in New Issue
Block a user