diff --git a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js index 9e9e29bb9e7..21e1fe56519 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js @@ -46,6 +46,7 @@ import { isEmptyBlock, getUrlsInfosInString, URL_REGEX, + URL_REGEX_WITH_INFOS, isSelectionFormat, YOUTUBE_URL_GET_VIDEO_ID, unwrapContents, @@ -2402,13 +2403,32 @@ export class OdooEditor extends EventTarget { // Check for url after user insert a space so we won't transform an incomplete url. if ( ev.data && - ev.data.includes(' ') && + ev.data === ' ' && selection && - selection.anchorNode - && (!this.commandBar._active || + selection.anchorNode && + !closestElement(selection.anchorNode).closest('a') && + selection.anchorNode.nodeType === Node.TEXT_NODE && + (!this.commandBar._active || this.commandBar._currentOpenOptions.closeOnSpace !== true) ) { - this._convertUrlInElement(closestElement(selection.anchorNode)); + const textSliced = selection.anchorNode.textContent.slice(0, selection.anchorOffset); + const textNodeSplitted = textSliced.split(/\s/); + + // Remove added space + textNodeSplitted.pop(); + const potentialUrl = textNodeSplitted.pop(); + const lastWordMatch = potentialUrl.match(URL_REGEX_WITH_INFOS); + + if (lastWordMatch) { + const matches = getUrlsInfosInString(textSliced); + const match = matches[matches.length - 1]; + this._createLinkWithUrlInTextNode( + selection.anchorNode, + match.url, + match.index, + match.length, + ); + } } this.sanitize(); this.historyStep(); @@ -2999,36 +3019,6 @@ export class OdooEditor extends EventTarget { } } - /** - * 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 * diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js index b7ea65210f6..6b4953c9a60 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js @@ -15,6 +15,7 @@ import { testEditor, undo, unformat, + triggerEvent, } from '../utils.js'; async function twoDeleteForward(editor) { @@ -3317,6 +3318,47 @@ X[] }); }); + describe('automatic link creation when typing a space after an url', () => { + it('should transform url after space', async () => { + await testEditor(BasicEditor, { + contentBefore: '
a http://test.com b http://test.com[] c http://test.com d
', + stepFunction: async (editor) => { + editor.testMode = false; + const selection = document.getSelection(); + const anchorOffset = selection.anchorOffset; + const p = editor.editable.querySelector('p'); + const textNode = p.childNodes[0]; + triggerEvent(editor.editable, 'keydown', {key: ' ', code: 'Space'}); + textNode.textContent = "a http://test.com b http://test.com\u00a0 c http://test.com d"; + selection.extend(textNode, anchorOffset + 1); + selection.collapseToEnd(); + triggerEvent(editor.editable, 'input', {data: ' ', inputType: 'insertText' }); + triggerEvent(editor.editable, 'keyup', {key: ' ', code: 'Space'}); + }, + contentAfter: 'a http://test.com b http://test.com [] c http://test.com d
', + }); + }); + it('should not transform url after two space', async () => { + await testEditor(BasicEditor, { + contentBefore: 'a http://test.com b http://test.com [] c http://test.com d
', + stepFunction: async (editor) => { + editor.testMode = false; + const selection = document.getSelection(); + const anchorOffset = selection.anchorOffset; + const p = editor.editable.querySelector('p'); + const textNode = p.childNodes[0]; + triggerEvent(editor.editable, 'keydown', {key: ' ', code: 'Space'}); + textNode.textContent = "a http://test.com b http://test.com \u00a0 c http://test.com d"; + selection.extend(textNode, anchorOffset + 1); + selection.collapseToEnd(); + triggerEvent(editor.editable, 'input', {data: ' ', inputType: 'insertText' }); + triggerEvent(editor.editable, 'keyup', {key: ' ', code: 'Space'}); + }, + contentAfter: 'a http://test.com b http://test.com [] c http://test.com d
', + }); + }); + }) + describe('history', () => { describe('undo', () => { it('should undo a backspace', async () => { diff --git a/addons/web_editor/static/lib/odoo-editor/test/utils.js b/addons/web_editor/static/lib/odoo-editor/test/utils.js index c1ac2e04f96..4909ea29beb 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/test/utils.js @@ -1,6 +1,9 @@ import { OdooEditor } from '../src/OdooEditor.js'; import { sanitize } from '../src/utils/sanitize.js'; -import { insertText as insertTextSel } from '../src/utils/utils.js'; +import { + closestElement, + insertText as insertTextSel, +} from '../src/utils/utils.js'; export const Direction = { BACKWARD: 'BACKWARD', @@ -500,4 +503,81 @@ export function redo(editor) { editor.historyRedo(); } +/** + * The class exists because the original `InputEvent` does not allow to override + * its inputType property. + */ +class SimulatedInputEvent extends InputEvent { + constructor(type, eventInitDict) { + super(type, eventInitDict); + this.eventInitDict = eventInitDict; + } + get inputType() { + return this.eventInitDict.inputType; + } +} +function getEventConstructor(win, type) { + const eventTypes = { + 'pointer': win.MouseEvent, + 'contextmenu': win.MouseEvent, + 'select': win.MouseEvent, + 'wheel': win.MouseEvent, + 'click': win.MouseEvent, + 'dblclick': win.MouseEvent, + 'mousedown': win.MouseEvent, + 'mouseenter': win.MouseEvent, + 'mouseleave': win.MouseEvent, + 'mousemove': win.MouseEvent, + 'mouseout': win.MouseEvent, + 'mouseover': win.MouseEvent, + 'mouseup': win.MouseEvent, + 'compositionstart': win.CompositionEvent, + 'compositionend': win.CompositionEvent, + 'compositionupdate': win.CompositionEvent, + 'input': SimulatedInputEvent, + 'beforeinput': SimulatedInputEvent, + 'keydown': win.KeyboardEvent, + 'keypress': win.KeyboardEvent, + 'keyup': win.KeyboardEvent, + 'dragstart': win.DragEvent, + 'dragend': win.DragEvent, + 'drop': win.DragEvent, + 'beforecut': win.ClipboardEvent, + 'cut': win.ClipboardEvent, + 'paste': win.ClipboardEvent, + 'touchstart': win.TouchEvent, + 'touchend': win.TouchEvent, + }; + if (!eventTypes[type]) { + throw new Error('The event "' + type + '" is not implemented for the tests.'); + } + return eventTypes[type]; +} + +export function triggerEvent( + el, + eventName, + options, +) { + const currentElement = closestElement(el); + options = Object.assign( + { + view: el.ownerDocument.defaultView, + bubbles: true, + composed: true, + cancelable: true, + isTrusted: true, + }, + options, + ); + const EventClass = getEventConstructor(el.ownerDocument.defaultView, eventName); + if (EventClass.name === 'ClipboardEvent' && !('clipboardData' in options)) { + throw new Error('ClipboardEvent must have clipboardData in options'); + } + const ev = new EventClass(eventName, options); + + currentElement.dispatchEvent(ev); + return ev; +} + export class BasicEditor extends OdooEditor {}