[IMP] web_editor: prevent command refocus when unnecessary
Currently, the focus is forced on this.editable when using a powerbox command. Knowledge /template will introduce blocks which are contentEditable=False, but contains sub-elements with contentEditable=True, which are allowed to use powerbox commands. Those sub-elements have another editable context as this.editable. As such, forcing a refocus on this.editable will remove the caret from the sub-element. Avoid doing the refocus if the currently focused element is a child of this.editable and is contentEditable=true. Task-id: 2794990 Part-of: odoo/odoo#86489
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
containsUnremovable,
|
||||
DIRECTIONS,
|
||||
endPos,
|
||||
ensureFocus,
|
||||
getCursorDirection,
|
||||
getListMode,
|
||||
getOuid,
|
||||
@@ -1893,7 +1894,7 @@ export class OdooEditor extends EventTarget {
|
||||
this.historyStep(true);
|
||||
this._historyStepsStates.set(peek(this._historySteps).id, 'consumed');
|
||||
setTimeout(() => {
|
||||
this.editable.focus();
|
||||
ensureFocus(this.editable);
|
||||
getDeepRange(this.editable, { select: true });
|
||||
});
|
||||
},
|
||||
|
||||
@@ -462,6 +462,24 @@ export function getNormalizedCursorPosition(node, offset, full = true) {
|
||||
|
||||
return [node, offset];
|
||||
}
|
||||
/**
|
||||
* Guarantee that the focus is on element or one of its children.
|
||||
*
|
||||
* A simple call to element.focus will change the editable context
|
||||
* if one of the parents of the current activeElement is not editable,
|
||||
* and the caret position will not be preserved, even if activeElement is
|
||||
* one of the subchildren of element. This is why the (re)focus is
|
||||
* only called when the current activeElement is not one of the
|
||||
* (sub)children of element.
|
||||
*
|
||||
* @param {Element} element should have the focus or a child with the focus
|
||||
*/
|
||||
export function ensureFocus(element) {
|
||||
const activeElement = element.ownerDocument.activeElement;
|
||||
if (activeElement !== element && (!element.contains(activeElement) || !activeElement.isContentEditable)) {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Node} anchorNode
|
||||
* @param {number} anchorOffset
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { applyInlineStyle } from '../../src/commands/commands.js';
|
||||
import { OdooEditor } from '../../src/OdooEditor.js';
|
||||
import { getTraversedNodes } from '../../src/utils/utils.js';
|
||||
import { getTraversedNodes, setCursorStart } from '../../src/utils/utils.js';
|
||||
import {
|
||||
BasicEditor,
|
||||
deleteBackward,
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
insertParagraphBreak,
|
||||
insertText,
|
||||
keydown,
|
||||
keyup,
|
||||
nextTickFrame,
|
||||
redo,
|
||||
testEditor,
|
||||
undo,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
closestElement,
|
||||
descendants,
|
||||
endPos,
|
||||
ensureFocus,
|
||||
firstLeaf,
|
||||
getAdjacentPreviousSiblings,
|
||||
getAdjacentNextSiblings,
|
||||
@@ -33,7 +34,15 @@ import {
|
||||
DIRECTIONS,
|
||||
isBlock,
|
||||
} from '../../src/utils/utils.js';
|
||||
import { BasicEditor, testEditor } from '../utils.js';
|
||||
import {
|
||||
BasicEditor,
|
||||
insertText,
|
||||
keydown,
|
||||
keyup,
|
||||
nextTickFrame,
|
||||
testEditor,
|
||||
unformat,
|
||||
} from '../utils.js';
|
||||
|
||||
const cleanTestHtml = () => {
|
||||
const testElements = document.querySelectorAll('body>div[contenteditable=true]');
|
||||
@@ -722,6 +731,90 @@ describe('Utils', () => {
|
||||
// Cursor management
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe('ensureFocus', () => {
|
||||
it('should preserve the focus on the child of this.editable when executing a powerbox command even if it is enclosed in a contenteditable=false', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>
|
||||
<p><br></p>`),
|
||||
stepFunction: async editor => {
|
||||
const sel = document.getSelection();
|
||||
const element = sel.anchorNode;
|
||||
await keydown(editor.editable, '/');
|
||||
await insertText(editor, '/');
|
||||
await keyup(editor.editable, '/');
|
||||
await insertText(editor, 'h2');
|
||||
await keyup(element, '2', { bubbles: true });
|
||||
await keydown(editor.editable, 'Enter');
|
||||
const activeElement = document.activeElement;
|
||||
setCursorStart(activeElement.lastElementChild);
|
||||
await nextTickFrame();
|
||||
},
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<h2>[]<br></h2>
|
||||
</div></div>
|
||||
<p><br></p>`),
|
||||
});
|
||||
});
|
||||
it('should preserve the focus on the child of this.editable even if it is enclosed in a contenteditable=false', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>
|
||||
<p><br></p>`),
|
||||
stepFunction: async editor => {
|
||||
ensureFocus(editor.editable);
|
||||
await nextTickFrame();
|
||||
let activeElement = document.activeElement;
|
||||
setCursorStart(activeElement.lastElementChild);
|
||||
await insertText(editor, 'focusWasConserved');
|
||||
// Proof that a simple call to Element.focus would change
|
||||
// the focus in this case.
|
||||
editor.editable.focus();
|
||||
await nextTickFrame();
|
||||
activeElement = document.activeElement;
|
||||
setCursorStart(activeElement.lastElementChild);
|
||||
await nextTickFrame();
|
||||
},
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>focusWasConserved</p>
|
||||
</div></div>
|
||||
<p>[]<br></p>`),
|
||||
});
|
||||
});
|
||||
it('should update the focus when the active element is not the focus target', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>
|
||||
<div contenteditable="false"><div id="target" contenteditable="true">
|
||||
<p><br></p>
|
||||
</div></div>`),
|
||||
stepFunction: async editor => {
|
||||
const sel = document.getSelection();
|
||||
const element = editor.editable.querySelector('#target');
|
||||
ensureFocus(element);
|
||||
await nextTickFrame();
|
||||
const activeElement = document.activeElement;
|
||||
setCursorStart(activeElement.lastElementChild);
|
||||
await nextTickFrame();
|
||||
},
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p><br></p>
|
||||
</div></div>
|
||||
<div contenteditable="false"><div id="target" contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getNormalizedCursorPosition', () => {
|
||||
// TODO: test more.
|
||||
it('should move the cursor from after a <b> to within it', () => {
|
||||
|
||||
@@ -422,6 +422,16 @@ export async function keydown(editable, key, options = {}) {
|
||||
await nextTickFrame();
|
||||
}
|
||||
|
||||
export async function keyup(editable, key, options = {}) {
|
||||
options = Object.assign(
|
||||
{ key: key },
|
||||
options
|
||||
);
|
||||
const ev = new KeyboardEvent('keyup', options);
|
||||
editable.dispatchEvent(ev);
|
||||
await nextTickFrame();
|
||||
}
|
||||
|
||||
export async function deleteForward(editor) {
|
||||
editor.execCommand('oDeleteForward');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user