[IMP] web_editor: update Jabberwock library to commit 43a10003

Changelog:
[FIX] List: Keep lonely containers in LI if they have attribute(s)
[ADD] Core: handle triple-click delete special case
[FIX] formattingSpace: properly ignore empty text and hidden inputs.
[FIX] should use the dev mode with mobile and multi devices in same time
[FIX] DomEditable: remove flickering in browsers without auto-completion
[FIX] DomLayout,KeyMap: remove flickering on delete, backspace, enter...
[REF] Core: merge .eslintignore inside .eslintrc.js
[REF] Core: remove odoo webpack build

closes odoo/odoo#61074

Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Sébastien Geelen (sge)
2020-10-30 15:57:44 +00:00
parent 1457b75aea
commit 71b3fc7c17
@@ -5516,7 +5516,17 @@ odoo.define('web_editor.jabberwock', (function(require) {
}
}
else {
const shouldDeleteLine = !range.start.previousSibling() &&
!range.end.previousSibling() &&
range.startContainer !== range.endContainer;
const oldEndContainer = range.endContainer;
range.empty();
// This handles the special case of a triple click followed by a
// backspace: the line should be deleted with its container (no
// merging).
if (shouldDeleteLine) {
range.startContainer.replaceWith(oldEndContainer);
}
}
}
/**
@@ -5568,7 +5578,17 @@ odoo.define('web_editor.jabberwock', (function(require) {
}
}
else {
const shouldDeleteLine = !range.start.previousSibling() &&
!range.end.previousSibling() &&
range.startContainer !== range.endContainer;
const oldEndContainer = range.endContainer;
range.empty();
// This handles the special case of a triple click followed by a
// delete: the line should be deleted with its container (no
// merging).
if (shouldDeleteLine) {
range.startContainer.replaceWith(oldEndContainer);
}
}
}
async deleteWord(params) {
@@ -8191,6 +8211,7 @@ odoo.define('web_editor.jabberwock', (function(require) {
'TABLE',
'UL',
// The following elements are not in the W3C list, for some reason.
'SELECT',
'TR',
'TD',
'TBODY',
@@ -8334,43 +8355,54 @@ odoo.define('web_editor.jabberwock', (function(require) {
* @returns {boolean}
*/
function _isAtSegmentBreak(node, side) {
const siblingSide = side === 'start' ? 'previousSibling' : 'nextSibling';
const sibling = node && node[siblingSide];
const isAgainstAnotherSegment = _isAgainstAnotherSegment(node, side);
const direction = side === 'start' ? 'previous' : 'next';
const sibling = _significantSibling(node, direction);
const isAgainstAnotherSegment = sibling && _isSegment(sibling);
const isAtEdgeOfOwnSegment = _isBlockEdge(node, side);
// In the DOM, a space before a BR is rendered but a space after a BR isn't.
const isBeforeBR = side === 'end' && sibling && nodeName(sibling) === 'BR';
return (isAgainstAnotherSegment && !isBeforeBR) || isAtEdgeOfOwnSegment;
}
/**
* Return true if the given node is just before or just after another segment.
* Eg: <div>abc<div>def</div></div> -> abc is before another segment (div).
* Eg: <div><a>abc</a> <div>def</div></div> -> abc is before another segment
* (div).
* Return the next|previous "significant" node. Nodes to skip are empty text
* nodes (text nodes with no text content or only whitespace), and hidden
* inputs.
* If `searchUp` is true (which is the default), the next|previous "significant"
* node may be a cousin.
*
* @param {Node} node
* @param {'start'|'end'} side
* @returns {boolean}
* @param {'previous'|'next'} direction
* @param {boolean} [searchUp] (default: true)
* @returns {Node}
*/
function _isAgainstAnotherSegment(node, side) {
const siblingSide = side === 'start' ? 'previousSibling' : 'nextSibling';
const sibling = node && node[siblingSide];
if (sibling) {
return sibling && _isSegment(sibling);
function _significantSibling(node, direction, searchUp = true) {
const siblingSide = direction === 'previous' ? 'previousSibling' : 'nextSibling';
let sibling = node && node[siblingSide];
let lastExisting = sibling || node;
if (sibling &&
// Sibling is an empty text node.
((sibling.nodeType === Node.TEXT_NODE &&
onlyTabsSpacesAndNewLines.test(sibling.textContent)) ||
// Sibling is a hidden input.
(nodeName(sibling) === 'INPUT' &&
sibling.getAttribute('type') === 'hidden'))) {
lastExisting = sibling;
sibling = sibling[siblingSide];
}
else {
if (!sibling && searchUp) {
// Look further (eg.: `<div><a>abc</a> <div>def</div></div>`: the
// space should be removed).
let ancestor = node;
while (ancestor && !ancestor[siblingSide]) {
let ancestor = lastExisting;
while (ancestor && !_significantSibling(ancestor, direction, false)) {
ancestor = ancestor.parentNode;
}
let cousin = ancestor && !_isSegment(ancestor) && ancestor.nextSibling;
while (cousin && isInstanceOf(cousin, Text)) {
cousin = cousin.nextSibling;
sibling =
ancestor && !_isSegment(ancestor) && _significantSibling(ancestor, direction, false);
while (sibling && isInstanceOf(sibling, Text)) {
sibling = _significantSibling(sibling, direction, false);
}
return cousin && _isSegment(cousin);
}
return sibling;
}
/**
* Return true if the node is a segment according to W3 formatting model.
@@ -8408,13 +8440,13 @@ odoo.define('web_editor.jabberwock', (function(require) {
}
// Return true if no ancestor up to the first block ancestor has a
// sibling on the specified side
const siblingSide = side === 'start' ? 'previousSibling' : 'nextSibling';
const direction = side === 'start' ? 'previous' : 'next';
return ancestorsUpToBlock.every(ancestor => {
let sibling = ancestor[siblingSide];
let sibling = _significantSibling(ancestor, direction);
while (sibling &&
isInstanceOf(sibling, Text) &&
sibling.textContent.match(onlyTabsSpacesAndNewLines)) {
sibling = sibling[siblingSide];
sibling = _significantSibling(sibling, direction);
}
return !sibling;
});
@@ -9170,6 +9202,7 @@ odoo.define('web_editor.jabberwock', (function(require) {
* @param context
*/
async parse(item) {
var _a;
const children = Array.from(item.childNodes);
const nodes = [];
let inlinesContainer;
@@ -9235,6 +9268,8 @@ odoo.define('web_editor.jabberwock', (function(require) {
return [container];
}
else if ((nodes.length === 1 &&
// We can't remove container that contain Attributes
((_a = nodes[0].modifiers.find(Attributes)) === null || _a === void 0 ? void 0 : _a.keys()) === undefined &&
// TODO: we need some sort of PhrasingContainer class for this.
(nodes[0] instanceof ParagraphNode ||
nodes[0] instanceof HeadingNode ||
@@ -16349,7 +16384,9 @@ odoo.define('web_editor.jabberwock', (function(require) {
domLayoutEngine.location = this.configuration.location;
await domLayoutEngine.start();
window.addEventListener('keydown', this.processKeydown, true);
window.addEventListener('keypress', this.processKeydown, true);
window.addEventListener('keydown-iframe', this.processKeydown, true);
window.addEventListener('keypress-iframe', this.processKeydown, true);
}
async stop() {
clearTimeout(this._debounce);
@@ -16358,7 +16395,9 @@ odoo.define('web_editor.jabberwock', (function(require) {
window.removeEventListener(eventName + '-iframe', this._checkFocusChanged, true);
});
window.removeEventListener('keydown', this.processKeydown, true);
window.removeEventListener('keypress', this.processKeydown, true);
window.removeEventListener('keydown-iframe', this.processKeydown, true);
window.removeEventListener('keypress-iframe', this.processKeydown, true);
const layout = this.dependencies.get(Layout);
const domLayoutEngine = layout.engines.dom;
await domLayoutEngine.stop();
@@ -16370,6 +16409,8 @@ odoo.define('web_editor.jabberwock', (function(require) {
/**
* KeyboardEvent listener to be added to the DOM that calls `execCommand` if
* the keys pressed match one of the shortcut registered in the keymap.
* If a shortcut is detected, then a preventDefault is performed an key
* event.
*
* @param event
*/
@@ -16396,10 +16437,13 @@ odoo.define('web_editor.jabberwock', (function(require) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
await Promise.all([
this.editor.dispatcher.dispatch('@preKeydownCommand', {}),
processingContext.execCommand(command.commandId, params),
]);
// Prevent default the keypress but don't trigger the command for this.
if (event.type.includes('keydown')) {
await Promise.all([
this.editor.dispatcher.dispatch('@preKeydownCommand', {}),
processingContext.execCommand(command.commandId, params),
]);
}
return command.commandId;
}
}
@@ -17325,6 +17369,17 @@ odoo.define('web_editor.jabberwock', (function(require) {
this._isInEditable = _isInEditable;
this._isInEditor = _isInEditor;
this._triggerEventBatchOutside = _triggerEventBatchOutside;
/**
* The normalizer detects if there is a auto-completion feature and that it
* must therefore be taken into account so as not to cancel modifications
* which would alter its correct functioning.
* By default, mobile browsers are considered to have an auto-completion.
* The detection is then done on the use of the composition events.
* A device can have a spell checker without auto-completion, the chrome
* spell checker trigger a replacement inputEvent without using composition
* events.
*/
this.hasSpellchecker = /Android|Mobile|Phone|webOS|iPad|iPod|BlackBerry|Opera Mini/i.test(navigator.userAgent);
/**
* Event listeners that are bound in the DOM by the normalizer on creation
* and unbound on destroy.
@@ -17437,7 +17492,7 @@ odoo.define('web_editor.jabberwock', (function(require) {
this._bindEvent(root, 'onkeyup', this._updateModifiersKeys);
this._bindEventInEditable(root, 'keydown', this._onKeyDownOrKeyPress);
this._bindEventInEditable(root, 'keypress', this._onKeyDownOrKeyPress);
this._bindEventInEditable(root, 'compositionstart', this._registerEvent);
this._bindEventInEditable(root, 'compositionstart', this._onCompositionStart);
this._bindEventInEditable(root, 'compositionupdate', this._registerEvent);
this._bindEventInEditable(root, 'compositionend', this._registerEvent);
this._bindEventInEditable(root, 'beforeinput', this._registerEvent);
@@ -17636,7 +17691,8 @@ odoo.define('web_editor.jabberwock', (function(require) {
// events occuring during one tick is not enough so we need to delay the
// analysis after we observe events during two ticks instead.
const needSecondTickObservation = currentStackObservation._events.every(ev => {
return !MultiStackEventTypes.includes(ev.type);
return (!MultiStackEventTypes.includes(ev.type) &&
!currentStackObservation.flickeringRemoved.has(ev));
});
if (needSecondTickObservation && !secondTickObservation) {
return await new Promise((resolve) => {
@@ -17661,6 +17717,7 @@ odoo.define('web_editor.jabberwock', (function(require) {
keydownEvent.key === 'Unidentified';
const inferredKeydownEvent = keydownEvent &&
keydownEvent.key === 'Unidentified' &&
inputEvent &&
this._inferKeydownEvent(inputEvent);
//
// First pass to get the informations
@@ -17682,7 +17739,10 @@ odoo.define('web_editor.jabberwock', (function(require) {
const inputType = (cutEvent && 'deleteByCut') ||
(dropEvent && 'insertFromDrop') ||
(pasteEvent && 'insertFromPaste') ||
(key === 'Enter' && (inputEvent === null || inputEvent === void 0 ? void 0 : inputEvent.inputType) === 'insertText' && 'insertLineBreak') ||
(key === 'Enter' &&
keydownEvent.shiftKey &&
(inputEvent === null || inputEvent === void 0 ? void 0 : inputEvent.inputType) === 'insertText' &&
'insertLineBreak') ||
(inputEvent && inputEvent.inputType);
// In case of accent inserted from a Mac, check that the char before was
// one of the special accent temporarily inserted in the DOM (e.g. '^',
@@ -17708,9 +17768,12 @@ odoo.define('web_editor.jabberwock', (function(require) {
// multiples keys pushed.
if (currentStackObservation._multiKeyStack.length > 1 && possibleMultiKeydown) {
currentStackObservation._multiKeyStack.map(keydownMap => {
const keyboardAction = this._getKeyboardAction(currentStackObservation.mutations, keydownMap.keydown.key, (keydownMap.input && keydownMap.input.inputType) || '', !!mutatedElements.size);
if (keyboardAction) {
normalizedActions.push(keyboardAction);
if (mutatedElements.size ||
currentStackObservation.flickeringRemoved.has(keydownMap.keydown)) {
const keyboardAction = this._getKeyboardAction(currentStackObservation.mutations, keydownMap.keydown.key, (keydownMap.input && keydownMap.input.inputType) || '');
if (keyboardAction) {
normalizedActions.push(keyboardAction);
}
}
});
}
@@ -17736,9 +17799,12 @@ odoo.define('web_editor.jabberwock', (function(require) {
}
else if (normalizedActions.length === 0 &&
((!compositionEvent && key) || isCompositionKeyboard || isVirtualKeyboard)) {
const keyboardAction = this._getKeyboardAction(currentStackObservation.mutations, key, inputType, !!mutatedElements.size, keydownEvent);
if (keyboardAction) {
normalizedActions.push(keyboardAction);
if (mutatedElements.size ||
currentStackObservation.flickeringRemoved.has(keydownEvent)) {
const keyboardAction = this._getKeyboardAction(currentStackObservation.mutations, key, inputType, keydownEvent);
if (keyboardAction) {
normalizedActions.push(keyboardAction);
}
}
if (compositionReplaceOneChar) {
normalizedActions = compositionData.actions;
@@ -17797,6 +17863,7 @@ odoo.define('web_editor.jabberwock', (function(require) {
_multiKeyStack: [],
_eventsMap: {},
mutations: undefined,
flickeringRemoved: new Set(),
};
}
_getCompositionData(mutations, compositionEvent, inputEvent) {
@@ -17847,9 +17914,8 @@ odoo.define('web_editor.jabberwock', (function(require) {
* @param hasMutataedElements
* @param isMultiKey
*/
_getKeyboardAction(mutations, key, inputType, hasMutatedElements, keydownEvent) {
const isInsertOrRemoveAction = hasMutatedElements && !inputTypeCommands.has(inputType);
if (isInsertOrRemoveAction) {
_getKeyboardAction(mutations, key, inputType, keydownEvent) {
if (!inputTypeCommands.has(inputType)) {
if (key === 'Backspace' || key === 'Delete') {
return this._getRemoveAction(mutations, key, inputType, keydownEvent);
}
@@ -18455,6 +18521,15 @@ odoo.define('web_editor.jabberwock', (function(require) {
// menu ends up opening, the user is definitely not selecting.
this._mousedownInEditable = false;
}
/**
* Catch start composition event
*
* @param {CompositionEvent} ev
*/
_onCompositionStart(ev) {
this.hasSpellchecker = true;
this._registerEvent(ev);
}
/**
* Catch Enter, Backspace, Delete and insert actions
*
@@ -18470,6 +18545,96 @@ odoo.define('web_editor.jabberwock', (function(require) {
}
const [offsetNode, offset] = targetDeepest(selection.anchorNode, selection.anchorOffset);
this._initialCaretPosition = { offsetNode, offset };
Promise.resolve().then(() => {
// Wait the next micro task to allow the external features to
// prevent the default and cancel the normalized keypress.
if (!ev.defaultPrevented && this._isSafeToPreventKeyboardEvent(ev, selection)) {
ev.preventDefault();
this.currentStackObservation.flickeringRemoved.add(ev);
}
});
}
/**
* Check the selection to preventDefault events (Enter, Backspace, Delete)
* and remove flickering without break the browser auto completion and
* spellcheckers.
*
* @param {KeyboardEvent} ev
*/
_isSafeToPreventKeyboardEvent(ev, selection) {
const code = ev.code || ev.key;
if (this._modifierKeys.ctrlKey ||
this._modifierKeys.altKey ||
this._modifierKeys.metaKey ||
this._modifierKeys.shiftKey ||
(code !== 'Backspace' && code !== 'Delete' && code !== 'Enter')) {
// If a control key is applied, you must let the browser apply its
// own behavior because you do not know the command in advance.
return false;
}
if (!this.hasSpellchecker) {
// there is no risk of breaking the spellcheckers if there is none.
return true;
}
if (isBlock(selection.anchorNode) ||
(selection.anchorNode !== selection.focusNode && isBlock(selection.focusNode))) {
// If the range is on a block, there is no risk of altering the
// behavior of the spellcheckers.
return true;
}
const previous = this._geSiblingInlineText(selection.anchorNode, selection.anchorOffset, true);
const next = this._geSiblingInlineText(selection.focusNode, selection.focusOffset, false);
const regExpChar = /[^\s\t\r\n\u00A0\u200b]/;
if (code === 'Backspace' &&
!regExpChar.test(previous.slice(-2)) &&
!regExpChar.test(next[0] || '')) {
return true;
}
if (code === 'Delete' &&
!regExpChar.test(previous[previous.length - 1] || '') &&
!regExpChar.test(next.slice(0, 2))) {
return true;
}
if (code === 'Enter' && !regExpChar.test(previous.slice(-2))) {
return true;
}
}
/**
* Return the sibling text passing through inlines formatting.
*
* @param {KeyboardEvent} ev
*/
_geSiblingInlineText(node, offset, before = true) {
let text = '';
while (node) {
if (node.nodeType === Node.TEXT_NODE) {
if (before) {
text += node.textContent.slice(0, offset);
}
else {
text += node.textContent.slice(offset);
}
}
else if (isBlock(node)) {
return text;
}
else {
text += node.textContent;
}
let sibling = node[before ? 'previousSibling' : 'nextSibling'];
while (node && !sibling && node.parentNode) {
if (isBlock(node.parentNode)) {
return text;
}
else {
node = node.parentNode;
sibling = node[before ? 'previousSibling' : 'nextSibling'];
}
}
node = sibling;
offset = before ? nodeLength(sibling) : 0;
}
return text;
}
/**
* Set internal properties of the pointer down event to retrieve them later
@@ -20952,6 +21117,37 @@ odoo.define('web_editor.jabberwock', (function(require) {
};
return context.execCommand(domHelpersToggleClass);
}
/**
* Add (state: true) or remove (state: false) a class or a list of classes
* from a DOM node or a list of DOM nodes.
*
* @param context
* @param originalDomNode
* @param className
* @param state
*/
async setClass(context, originalDomNode, className, state) {
const domHelpersSetClass = async () => {
var _a;
const classes = Array.isArray(className) ? className : [className];
const domNodes = Array.isArray(originalDomNode) ? originalDomNode : [originalDomNode];
for (const domNode of domNodes) {
const Attributes = this._getAttributesConstructor(domNode);
for (const node of this.getNodes(domNode)) {
const classList = (_a = node.modifiers.find(Attributes)) === null || _a === void 0 ? void 0 : _a.classList;
for (const oneClass of classes) {
if (state && !(classList === null || classList === void 0 ? void 0 : classList.has(oneClass))) {
node.modifiers.get(Attributes).classList.add(oneClass);
}
else if (!state && (classList === null || classList === void 0 ? void 0 : classList.has(oneClass))) {
classList.remove(oneClass);
}
}
}
}
};
return context.execCommand(domHelpersSetClass);
}
/**
* Set an attribute on a DOM node or a list of DOM nodes.
*