[IMP] web_editor: update odoo-editor lib to commit 5fea955f
X-original-commit: 2b0bfd4152db6b20631b0c99c0288197485bebb1
This commit is contained in:
@@ -28,6 +28,9 @@ var exportVariable = (function (exports) {
|
||||
BR: CTYPES.BR,
|
||||
};
|
||||
|
||||
const URL_REGEX = /((?:https?:\/\/)?(?:[a-z0-9-]{1,63}\.){1,2}[a-z]{2,15}(?:\/[^\s]*)?)/gi;
|
||||
const URL_REGEX_WITH_INFOS = /((https?:\/\/)?([a-z0-9-]{1,63}\.){1,2}[a-z]{2,15}(\/[^\s]*)?)/gi;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Position and sizes
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -640,7 +643,12 @@ var exportVariable = (function (exports) {
|
||||
// A selection spanning multiple nodes and ending at position 0 of a
|
||||
// node, like the one resulting from a triple click, are corrected so
|
||||
// that it ends at the last position of the previous node instead.
|
||||
if (correctTripleClick && !endOffset && !end.previousSibling) {
|
||||
const beforeEnd = end.previousSibling;
|
||||
if (
|
||||
correctTripleClick &&
|
||||
!endOffset &&
|
||||
(!beforeEnd || (beforeEnd.nodeType === Node.TEXT_NODE && !isVisibleStr(beforeEnd)))
|
||||
) {
|
||||
const previous = previousLeaf(end, editable, true);
|
||||
if (previous && closestElement(previous).isContentEditable) {
|
||||
[end, endOffset] = [previous, nodeSize(previous)];
|
||||
@@ -915,6 +923,26 @@ var exportVariable = (function (exports) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of url infos for url matched in the given string.
|
||||
*
|
||||
* @param {String} string
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getUrlsInfosInString(string) {
|
||||
let infos = [],
|
||||
match;
|
||||
while ((match = URL_REGEX_WITH_INFOS.exec(string))) {
|
||||
infos.push({
|
||||
url: match[2] ? match[0] : 'https://' + match[0],
|
||||
label: match[0],
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
});
|
||||
}
|
||||
return infos;
|
||||
}
|
||||
|
||||
// optimize: use the parent Oid to speed up detection
|
||||
function getOuid(node, optimize = false) {
|
||||
while (node && !isUnbreakable(node)) {
|
||||
@@ -1232,15 +1260,27 @@ var exportVariable = (function (exports) {
|
||||
|
||||
/**
|
||||
* Add a BR in the given node if its closest ancestor block has nothing to make
|
||||
* it visible.
|
||||
* it visible, and/or add a zero-width space in the given node if it's an empty
|
||||
* inline unremovable so the cursor can stay in it.
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* @returns {Object} { br: the inserted <br> if any,
|
||||
* zws: the inserted zero-width space if any }
|
||||
*/
|
||||
function fillEmpty(el) {
|
||||
const fillers = {};
|
||||
const blockEl = closestBlock(el);
|
||||
if (isShrunkBlock(blockEl)) {
|
||||
blockEl.appendChild(document.createElement('br'));
|
||||
const br = document.createElement('br');
|
||||
blockEl.appendChild(br);
|
||||
fillers.br = br;
|
||||
}
|
||||
if (!el.textContent.length && isUnremovable(el) && !isBlock(el)) {
|
||||
const zws = document.createTextNode('\u200B');
|
||||
el.appendChild(zws);
|
||||
fillers.zws = zws;
|
||||
}
|
||||
return fillers;
|
||||
}
|
||||
/**
|
||||
* Removes the given node if invisible and all its invisible ancestors.
|
||||
@@ -1745,7 +1785,7 @@ var exportVariable = (function (exports) {
|
||||
);
|
||||
}
|
||||
|
||||
function getRangePosition(el, options = {}) {
|
||||
function getRangePosition(el, document, options = {}) {
|
||||
const selection = document.getSelection();
|
||||
if (!selection.isCollapsed || !selection.rangeCount) return;
|
||||
const range = selection.getRangeAt(0);
|
||||
@@ -2818,6 +2858,9 @@ var exportVariable = (function (exports) {
|
||||
if (res) {
|
||||
setCursor(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
|
||||
const node = findNode(closestPath(sel.focusNode), node => node.tagName === 'A');
|
||||
for (const [param, value] of Object.entries(editor.options.defaultLinkAttributes)) {
|
||||
node.setAttribute(param, `${value}`);
|
||||
}
|
||||
const pos = [node.parentElement, childNodeIndex(node) + 1];
|
||||
setCursor(...pos, ...pos, false);
|
||||
}
|
||||
@@ -3316,6 +3359,9 @@ var exportVariable = (function (exports) {
|
||||
this._mainWrapperElement = document.createElement('div');
|
||||
this._mainWrapperElement.className = 'oe-commandbar-mainWrapper';
|
||||
this.el.append(this._mainWrapperElement);
|
||||
this.el.addEventListener('mousedown', event => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -3407,6 +3453,7 @@ var exportVariable = (function (exports) {
|
||||
event => {
|
||||
this._currentValidate();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
true,
|
||||
);
|
||||
@@ -3556,7 +3603,7 @@ var exportVariable = (function (exports) {
|
||||
}
|
||||
|
||||
_resetPosition() {
|
||||
const position = getRangePosition(this.el);
|
||||
const position = getRangePosition(this.el, this.options.document);
|
||||
if (!position) {
|
||||
this.hide();
|
||||
return;
|
||||
@@ -3701,7 +3748,7 @@ var exportVariable = (function (exports) {
|
||||
}
|
||||
};
|
||||
|
||||
const offset = getRangePosition(this.el);
|
||||
const offset = getRangePosition(this.el, this.options.document);
|
||||
this.el.style.left = `${offset.left}px`;
|
||||
this.el.style.top = `${offset.top}px`;
|
||||
|
||||
@@ -3754,6 +3801,7 @@ var exportVariable = (function (exports) {
|
||||
},
|
||||
toSanitize: true,
|
||||
isRootEditable: true,
|
||||
defaultLinkAttributes: {},
|
||||
getContentEditableAreas: () => [],
|
||||
_t: string => string,
|
||||
},
|
||||
@@ -4547,8 +4595,11 @@ var exportVariable = (function (exports) {
|
||||
fillEmpty(closestBlock(range.endContainer));
|
||||
// Ensure trailing space remains visible.
|
||||
const joinWith = range.endContainer;
|
||||
const joinSibling = joinWith && joinWith.nextSibling;
|
||||
const oldText = joinWith.textContent;
|
||||
if (joinWith && oldText.endsWith(' ')) {
|
||||
const hasSpaceAfter = joinSibling && joinSibling.textContent.startsWith(' ');
|
||||
const shouldPreserveSpace = (doJoin || hasSpaceAfter) && joinWith && oldText.endsWith(' ');
|
||||
if (shouldPreserveSpace) {
|
||||
joinWith.textContent = oldText.replace(/ $/, '\u00A0');
|
||||
setCursor(joinWith, nodeSize(joinWith));
|
||||
}
|
||||
@@ -4576,14 +4627,20 @@ var exportVariable = (function (exports) {
|
||||
}
|
||||
next = joinWith && joinWith.nextSibling;
|
||||
if (
|
||||
joinWith &&
|
||||
oldText.endsWith(' ') &&
|
||||
shouldPreserveSpace &&
|
||||
!(next && next.nodeType === Node.TEXT_NODE && next.textContent.startsWith(' '))
|
||||
) {
|
||||
// Restore the text we modified in order to preserve trailing space.
|
||||
joinWith.textContent = oldText;
|
||||
setCursor(joinWith, nodeSize(joinWith));
|
||||
}
|
||||
if (joinWith) {
|
||||
const el = closestElement(joinWith);
|
||||
const { zws } = fillEmpty(el);
|
||||
if (zws) {
|
||||
setCursor(zws, 0, zws, nodeSize(zws));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateColorpickerLabels(params = {}) {
|
||||
@@ -4628,6 +4685,13 @@ var exportVariable = (function (exports) {
|
||||
*/
|
||||
_applyRawCommand(method, ...args) {
|
||||
const sel = this.document.getSelection();
|
||||
if (
|
||||
!this.editable.contains(sel.anchorNode) ||
|
||||
(sel.anchorNode !== sel.focusNode && !this.editable.contains(sel.focusNode))
|
||||
) {
|
||||
// Do not apply commands out of the editable area.
|
||||
return false;
|
||||
}
|
||||
if (!sel.isCollapsed && BACKSPACE_FIRST_COMMANDS.includes(method)) {
|
||||
this.deleteRange(sel);
|
||||
if (BACKSPACE_ONLY_COMMANDS.includes(method)) {
|
||||
@@ -4866,15 +4930,15 @@ var exportVariable = (function (exports) {
|
||||
groupName: 'Basic blocks',
|
||||
title: 'Checklist',
|
||||
description: 'Track tasks with a checklist.',
|
||||
fontawesome: 'fa-tasks',
|
||||
fontawesome: 'fa-check-square-o',
|
||||
callback: () => {
|
||||
this.execCommand('toggleList', 'CL');
|
||||
},
|
||||
},
|
||||
{
|
||||
groupName: 'Basic blocks',
|
||||
title: 'Horizontal rule',
|
||||
description: 'Insert an horizantal rule.',
|
||||
title: 'Separator',
|
||||
description: 'Insert an horizontal rule separator.',
|
||||
fontawesome: 'fa-minus',
|
||||
callback: () => {
|
||||
this.execCommand('insertHorizontalRule');
|
||||
@@ -4898,6 +4962,7 @@ var exportVariable = (function (exports) {
|
||||
}
|
||||
this.commandBar = new CommandBar({
|
||||
editable: this.editable,
|
||||
document: this.document,
|
||||
_t: this.options._t,
|
||||
onShow: () => {
|
||||
this.commandbarTablePicker.hide();
|
||||
@@ -5146,6 +5211,12 @@ var exportVariable = (function (exports) {
|
||||
const range = selection.getRangeAt(0);
|
||||
setCursor(range.endContainer, range.endOffset);
|
||||
}
|
||||
// Check for url after user insert a space so we won't transform an incomplete url.
|
||||
if (ev.data.includes(' ') && selection && selection.anchorNode) {
|
||||
ev.preventDefault();
|
||||
this._convertUrlInElement(closestElement(selection.anchorNode));
|
||||
this.execCommand('insertText', ev.data);
|
||||
}
|
||||
this.sanitize();
|
||||
this.historyStep();
|
||||
} else if (ev.inputType === 'insertLineBreak') {
|
||||
@@ -5192,10 +5263,10 @@ var exportVariable = (function (exports) {
|
||||
|
||||
if (closestTag === 'LI') {
|
||||
this._applyCommand('indentList', ev.shiftKey ? 'outdent' : 'indent');
|
||||
ev.preventDefault();
|
||||
} else if (closestTag === 'TABLE') {
|
||||
this._onTabulationInTable(ev);
|
||||
ev.preventDefault();
|
||||
} else if (!ev.shiftKey) {
|
||||
this.execCommand('insertText', '\u00A0 \u00A0\u00A0');
|
||||
}
|
||||
ev.preventDefault();
|
||||
} else if (IS_KEYBOARD_EVENT_UNDO(ev)) {
|
||||
@@ -5403,13 +5474,81 @@ var exportVariable = (function (exports) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert valid url text into links inside the given element.
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
_convertUrlInElement(el) {
|
||||
// We will not replace url inside already existing Link element.
|
||||
if (el.tagName === 'A') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let child of el.childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE && child.length > 3) {
|
||||
const childStr = child.nodeValue;
|
||||
const matches = getUrlsInfosInString(childStr);
|
||||
if (matches.length) {
|
||||
// We only to take care of the first match.
|
||||
// The method `_createLinkWithUrlInTextNode` will split the text node,
|
||||
// the other url matches will then be matched again in the nexts loops of el.childnodes.
|
||||
this._createLinkWithUrlInTextNode(
|
||||
child,
|
||||
matches[0].url,
|
||||
matches[0].index,
|
||||
matches[0].length,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Link in the node text based on the given data
|
||||
*
|
||||
* @param {Node} textNode
|
||||
* @param {String} url
|
||||
* @param {int} index
|
||||
* @param {int} length
|
||||
*/
|
||||
_createLinkWithUrlInTextNode(textNode, url, index, length) {
|
||||
setCursor(textNode, index, textNode, index + length);
|
||||
this.document.execCommand('createLink', false, url);
|
||||
const sel = this.document.getSelection();
|
||||
const link = closestElement(sel.anchorNode, 'a');
|
||||
for (const [param, value] of Object.entries(this.options.defaultLinkAttributes)) {
|
||||
link.setAttribute(param, `${value}`);
|
||||
}
|
||||
sel.collapseToEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the pasting of HTML and paste text only instead.
|
||||
*/
|
||||
_onPaste(ev) {
|
||||
ev.preventDefault();
|
||||
const pastedText = (ev.originalEvent || ev).clipboardData.getData('text/plain');
|
||||
this.execCommand('insertText', pastedText);
|
||||
const splitAroundUrl = pastedText.split(URL_REGEX);
|
||||
const linkAttrs =
|
||||
Object.entries(this.options.defaultLinkAttributes)
|
||||
.map(entry => entry.join('="'))
|
||||
.join('" ') + '" ';
|
||||
|
||||
for (let i = 0; i < splitAroundUrl.length; i++) {
|
||||
// Even indexes will always be plain text, and odd indexes will always be URL.
|
||||
if (i % 2) {
|
||||
const url = /^https?:\/\//gi.test(splitAroundUrl[i])
|
||||
? splitAroundUrl[i]
|
||||
: 'https://' + splitAroundUrl[i];
|
||||
this.execCommand(
|
||||
'insertHTML',
|
||||
`<a href="${url}" ${linkAttrs}>${splitAroundUrl[i]}</a>`,
|
||||
);
|
||||
} else if (splitAroundUrl[i] !== '') {
|
||||
this.execCommand('insertText', splitAroundUrl[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5552,6 +5691,8 @@ var exportVariable = (function (exports) {
|
||||
exports.OdooEditor = OdooEditor;
|
||||
exports.UNBREAKABLE_ROLLBACK_CODE = UNBREAKABLE_ROLLBACK_CODE;
|
||||
exports.UNREMOVABLE_ROLLBACK_CODE = UNREMOVABLE_ROLLBACK_CODE;
|
||||
exports.URL_REGEX = URL_REGEX;
|
||||
exports.URL_REGEX_WITH_INFOS = URL_REGEX_WITH_INFOS;
|
||||
exports.ancestors = ancestors;
|
||||
exports.boundariesIn = boundariesIn;
|
||||
exports.boundariesOut = boundariesOut;
|
||||
@@ -5585,6 +5726,7 @@ var exportVariable = (function (exports) {
|
||||
exports.getSelectedNodes = getSelectedNodes;
|
||||
exports.getState = getState;
|
||||
exports.getTraversedNodes = getTraversedNodes;
|
||||
exports.getUrlsInfosInString = getUrlsInfosInString;
|
||||
exports.insertListAfter = insertListAfter;
|
||||
exports.insertText = insertText;
|
||||
exports.isBlock = isBlock;
|
||||
|
||||
Reference in New Issue
Block a user