[FIX] web_editor: convert link only a space after it

Before this commit, when adding a space anywhere in the a text node,
a plain text link gets transformed into a proper link.

It might be an undesired behavior as we could want to manually remove
the link but it will be re-transformed when completing a paragraph.

task-2795019

X-original-commit: 76b5ecc88001ae9decaf3943550ecb4ddb874599
Part-of: odoo/odoo#92653
This commit is contained in:
Nicolas Bayet
2022-06-02 16:54:11 +02:00
parent 7db57de5d8
commit a88bb2250a
3 changed files with 147 additions and 35 deletions
@@ -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
*
@@ -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: '<p>a http://test.com b http://test.com[] c http://test.com d</p>',
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: '<p>a http://test.com b <a href="http://test.com">http://test.com</a>&nbsp;[] c http://test.com d</p>',
});
});
it('should not transform url after two space', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>a http://test.com b http://test.com [] c http://test.com d</p>',
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: '<p>a http://test.com b http://test.com &nbsp;[] c http://test.com d</p>',
});
});
})
describe('history', () => {
describe('undo', () => {
it('should undo a backspace', async () => {
@@ -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 {}