[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:
@@ -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> [] 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 [] 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 {}
|
||||
|
||||
Reference in New Issue
Block a user