[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:
Rodolpho Lima
2023-09-29 09:21:52 +00:00
parent 058e9a92d2
commit 6d4a3b3ab5
4 changed files with 40 additions and 27 deletions
@@ -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);
}