[IMP] web_editor: deduce phone URL in Link(Tools/Dialog)
When creating or editing a link via the LinkTools or LinkDialog, http and email URLs can be deduced from incomplete user inputted URL (that is, URLs not containing the http(s):// or mailto: protocol). This commit extends such URL deduction to telephone numbers, allowing the user to create a tel:// link without the need to explicitly write "tel:" or "tel://". Obs: "tel//:" protocol is preferred over "tel:" for the reasons stated in [this] commit. [this]: https://github.com/odoo/odoo/commit/56ce29e71f75a657d4b518d86c1d7084891e898e task-3516854 Part-of: odoo/odoo#136487
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
ancestors,
|
||||
EMAIL_REGEX,
|
||||
URL_REGEX_WITH_INFOS,
|
||||
PHONE_REGEX,
|
||||
} from './utils.js';
|
||||
|
||||
const NOT_A_NUMBER = /[^\d]/g;
|
||||
@@ -76,13 +77,17 @@ export function areSimilarElements(node, node2) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URL if link's label is a valid email of http URL, null otherwise.
|
||||
* Returns a complete URL if text is a valid email address, http URL or telephone
|
||||
* number, null otherwise.
|
||||
* The optional link parameter is used to prevent protocol switching between
|
||||
* 'http' and 'https'.
|
||||
*
|
||||
* @param {HTMLAnchorElement} link
|
||||
* @param {String} text
|
||||
* @param {HTMLAnchorElement} [link]
|
||||
* @returns {String|null}
|
||||
*/
|
||||
function deduceURLfromLabel(link) {
|
||||
const label = link.innerText.trim();
|
||||
export function deduceURLfromText(text, link) {
|
||||
const label = text.replace(/\u200b/g, '').trim();
|
||||
// Check first for e-mail.
|
||||
let match = label.match(EMAIL_REGEX);
|
||||
if (match) {
|
||||
@@ -95,7 +100,7 @@ function deduceURLfromLabel(link) {
|
||||
match = URL_REGEX_WITH_INFOS.exec(label);
|
||||
URL_REGEX_WITH_INFOS.lastIndex = 0;
|
||||
if (match && match[0] === label) {
|
||||
const currentHttpProtocol = (link.href.match(/^http(s)?:\/\//gi) || [])[0];
|
||||
const currentHttpProtocol = (link?.href.match(/^http(s)?:\/\//gi) || [])[0];
|
||||
if (match[2]) {
|
||||
return match[0];
|
||||
} else if (currentHttpProtocol) {
|
||||
@@ -105,6 +110,11 @@ function deduceURLfromLabel(link) {
|
||||
return 'https://' + match[0];
|
||||
}
|
||||
}
|
||||
// Check for telephone url.
|
||||
match = label.match(PHONE_REGEX);
|
||||
if (match) {
|
||||
return match[1] ? match[0] : 'tel://' + match[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -242,7 +252,8 @@ export function sanitize(nodeToSanitize, root = nodeToSanitize) {
|
||||
// Update link URL if label is a new valid link.
|
||||
const startEl = start && closestElement(start, 'a');
|
||||
if (startEl && root.contains(startEl)) {
|
||||
const url = deduceURLfromLabel(startEl);
|
||||
const label = startEl.innerText;
|
||||
const url = deduceURLfromText(label, startEl);
|
||||
if (url) {
|
||||
startEl.setAttribute('href', url);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export const URL_REGEX_WITH_INFOS = new RegExp(`((?:(?:${httpCapturedRegex}${url
|
||||
export const YOUTUBE_URL_GET_VIDEO_ID =
|
||||
/^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube\.com|youtu\.be)(?:\/(?:[\w-]+\?v=|embed\/|v\/)?)([^\s?&#]+)(?:\S+)?$/i;
|
||||
export const EMAIL_REGEX = /^(mailto:)?[\w-.]+@(?:[\w-]+\.)+[\w-]{2,4}$/i;
|
||||
export const PHONE_REGEX = /^(tel:(?:\/\/)?)?\+?[\d\s.\-()\/]{3,25}$/;
|
||||
|
||||
export const PROTECTED_BLOCK_TAG = ['TR','TD','TABLE','TBODY','UL','OL','LI'];
|
||||
|
||||
|
||||
@@ -680,14 +680,14 @@ describe('Link', () => {
|
||||
window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false);
|
||||
await deleteBackward(editor);
|
||||
window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false);
|
||||
await insertText(editor, '1');
|
||||
await insertText(editor, 'a');
|
||||
window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false);
|
||||
await insertText(editor, '2');
|
||||
await insertText(editor, 'b');
|
||||
window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false);
|
||||
await insertText(editor, '3');
|
||||
await insertText(editor, 'c');
|
||||
window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false);
|
||||
},
|
||||
contentAfter: '<p>a<a href="#/">123[]</a>c</p>',
|
||||
contentAfter: '<p>a<a href="#/">abc[]</a>c</p>',
|
||||
});
|
||||
});
|
||||
it('should delete the content from the link when popover is active', async () => {
|
||||
|
||||
@@ -13,10 +13,9 @@ import {
|
||||
useState,
|
||||
useRef,
|
||||
} from "@odoo/owl";
|
||||
import { deduceURLfromText } from "@web_editor/js/editor/odoo-editor/src/utils/sanitize";
|
||||
|
||||
const getDeepRange = OdooEditorLib.getDeepRange;
|
||||
const getInSelection = OdooEditorLib.getInSelection;
|
||||
const EMAIL_REGEX = OdooEditorLib.EMAIL_REGEX;
|
||||
const { getDeepRange, getInSelection, EMAIL_REGEX, PHONE_REGEX } = OdooEditorLib;
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
@@ -249,6 +248,15 @@ export class Link extends Component {
|
||||
}
|
||||
return url;
|
||||
}
|
||||
_deduceUrl(text) {
|
||||
text = text.trim();
|
||||
if (/^(https?:|mailto:|tel:)/.test(text)) {
|
||||
// Text begins with a known protocol, accept it as valid URL.
|
||||
return text;
|
||||
} else {
|
||||
return deduceURLfromText(text, this.linkEl) || '';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Abstract method: return true if the URL should be stripped of its domain.
|
||||
*
|
||||
@@ -291,17 +299,14 @@ export class Link extends Component {
|
||||
(type && size ? (' btn-' + size) : '');
|
||||
var isNewWindow = this._isNewWindow(url);
|
||||
var doStripDomain = this._doStripDomain();
|
||||
const emailMatch = url.match(EMAIL_REGEX);
|
||||
if (emailMatch) {
|
||||
url = emailMatch[1] ? emailMatch[0] : 'mailto:' + emailMatch[0];
|
||||
} else if (url.indexOf(location.origin) === 0 && doStripDomain) {
|
||||
url = url.slice(location.origin.length);
|
||||
if (this.state.url.indexOf(location.origin) === 0 && doStripDomain) {
|
||||
this.state.url = this.state.url.slice(location.origin.length);
|
||||
}
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
return {
|
||||
content: content,
|
||||
url: this._correctLink(url),
|
||||
url: this._correctLink(this.state.url),
|
||||
classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, ''),
|
||||
customTextColor: customTextColor,
|
||||
customFill: customFill,
|
||||
@@ -538,13 +543,7 @@ export class Link extends Component {
|
||||
this.state.originalText = this.state.originalText ? this.state.originalText.replace(/[ \t\r\n]+/g, ' ') : '';
|
||||
}
|
||||
|
||||
|
||||
if (!this.state.url) {
|
||||
const urls = this.state.originalText.match(OdooEditorLib.URL_REGEX_WITH_INFOS);
|
||||
if (urls) {
|
||||
this.state.url = urls[0];
|
||||
}
|
||||
}
|
||||
this.state.url ||= this._deduceUrl(this.state.originalText, this.linkEl);
|
||||
|
||||
if (this.linkEl) {
|
||||
this.initialNewWindow = this.initialNewWindow || this.linkEl.target === '_blank';
|
||||
@@ -625,6 +624,8 @@ export class Link extends Component {
|
||||
* @private
|
||||
*/
|
||||
__onURLInput() {
|
||||
const inputValue = this.$el[0].querySelector('#o_link_dialog_url_input').value;
|
||||
this.state.url = this._deduceUrl(inputValue, this.linkEl) || inputValue;
|
||||
this._onURLInput(...arguments);
|
||||
}
|
||||
/**
|
||||
@@ -634,7 +635,7 @@ export class Link extends Component {
|
||||
this._savedURLInputOnDestroy = true;
|
||||
var $linkUrlInput = this.$el.find('#o_link_dialog_url_input');
|
||||
let value = $linkUrlInput.val();
|
||||
let isLink = !EMAIL_REGEX.test(value);
|
||||
let isLink = !EMAIL_REGEX.test(value) && !PHONE_REGEX.test(value);
|
||||
this._getIsNewWindowFormRow().toggleClass('d-none', !isLink);
|
||||
this.$el.find('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user