From 6d4a3b3ab5c0f3361d1d681d05b974e295dcbabe Mon Sep 17 00:00:00 2001 From: Rodolpho Lima Date: Thu, 21 Sep 2023 15:57:17 +0200 Subject: [PATCH] [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 --- .../editor/odoo-editor/src/utils/sanitize.js | 23 ++++++++---- .../js/editor/odoo-editor/src/utils/utils.js | 1 + .../editor/odoo-editor/test/spec/link.test.js | 8 ++--- .../static/src/js/wysiwyg/widgets/link.js | 35 ++++++++++--------- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js index 73020c23a07..5dba980e148 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js @@ -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); } diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js index 29d98eb33e6..f42a6754f80 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js @@ -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']; diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js index 52346843fec..2dad1471ae9 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js @@ -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: '

a123[]c

', + contentAfter: '

aabc[]c

', }); }); it('should delete the content from the link when popover is active', async () => { diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/link.js b/addons/web_editor/static/src/js/wysiwyg/widgets/link.js index 7e6bc30013a..32a9d850d12 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/link.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link.js @@ -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); }