From 267bce176f9d0d012ec84b69c92647db0c2e949f Mon Sep 17 00:00:00 2001 From: Rodolpho Lima Date: Fri, 17 Mar 2023 14:49:40 +0000 Subject: [PATCH] [FIX] web_editor: fix issues when pasting media URL Issue # 1: pasting image/video URL as text This commit removes this command, because it was considered to be useless. The category names "Embed" and "Paste" for the other two commands ("Paste as URL" and "Embed Image/Video") were ommited as it is no longer relevant to group commands, as this would result into two categories with one command each (the powerbox does not display the category name when all the commands are under the same one - undefined in this case). Issue # 2: pasting image/video URL + other text fragments The powerbox commands are not compatible with an image/video URL + text or multiple URLs. Choosing one of its commands will lead to loss of all text fragments but the last valid URL. This commit ensures the powerbox options are only offered when a single image/video URL is pasted. Issue # 3: pasting image URL inside an exisiting link Even though the Editor's specs do not allow a URL to be transformed into a link when pasting it inside an exisiting link, before this commit, pasting an image URL inside a link would open the powerbox with 3 commands, one of them allowing to transform the URL into a link. This commit ensures the powerbox with the "Paste as URL" command is not opened in such case. When pasting an image URL inside an existing link, the image is directly embedded, and other URLs are pasted as text. Issue # 4: unnecessary regex matching Before this commit, every text fragment in "text URL text" was being prepended with "https://" and a match with the URL regex was attempted. Every fragment (text or URL) was also probed for a match with the youtube video URL regex, but such matches would be discarded anyway in modules like note and mass_mailing where the allowCommandVideo option is set to false. This commit avoids unnecessary searching with regular expressions by treating pasting of a single URL (possibly an image or video URL that would lead to powerbox commands) differently than URLs pasted among other text fragments or other URLs (see issue # 2 above). Issue # 5: `historyPauseSteps` with argument This function takes no arguments. Yet, it was called with the string 'onPaste' as argument. This commit removes it. task-3099012 X-original-commit: 3fb93d7e9047a73a8e84f165e2ff52b9887d34a4 Part-of: odoo/odoo#118204 --- addons/web_editor/i18n/web_editor.pot | 30 --- .../js/editor/odoo-editor/src/OdooEditor.js | 215 ++++++++---------- 2 files changed, 98 insertions(+), 147 deletions(-) diff --git a/addons/web_editor/i18n/web_editor.pot b/addons/web_editor/i18n/web_editor.pot index 5ba6e567caa..180e57d6ddf 100644 --- a/addons/web_editor/i18n/web_editor.pot +++ b/addons/web_editor/i18n/web_editor.pot @@ -1034,14 +1034,6 @@ msgid "" "prevent it from being updated during future App upgrades." msgstr "" -#. module: web_editor -#. odoo-javascript -#: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 -#: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 -#, python-format -msgid "Embed" -msgstr "" - #. module: web_editor #. odoo-javascript #: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 @@ -2036,14 +2028,6 @@ msgstr "" msgid "Paragraph block" msgstr "" -#. module: web_editor -#. odoo-javascript -#: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 -#: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 -#, python-format -msgid "Paste" -msgstr "" - #. module: web_editor #. odoo-javascript #: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 @@ -2051,13 +2035,6 @@ msgstr "" msgid "Paste as URL" msgstr "" -#. module: web_editor -#. odoo-javascript -#: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 -#, python-format -msgid "Paste as text" -msgstr "" - #. module: web_editor #: model_terms:ir.ui.view,arch_db:web_editor.snippet_options msgid "Patterns" @@ -2593,13 +2570,6 @@ msgstr "" msgid "Signature" msgstr "" -#. module: web_editor -#. odoo-javascript -#: code:addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js:0 -#, python-format -msgid "Simple text paste" -msgstr "" - #. module: web_editor #. odoo-javascript #: code:addons/web_editor/static/src/js/editor/snippets.options.js:0 diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js index 2abf87ef69c..aa4505abe43 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js @@ -4308,135 +4308,116 @@ export class OdooEditor extends EventTarget { if(!text.match(/\${.*}/gi)) { splitAroundUrl = text.split(URL_REGEX); } - this.historyPauseSteps("_onPaste"); - for (let i = 0; i < splitAroundUrl.length; i++) { - const url = /^https?:\/\//gi.test(splitAroundUrl[i]) - ? splitAroundUrl[i] - : 'https://' + splitAroundUrl[i]; - const youtubeUrl = YOUTUBE_URL_GET_VIDEO_ID.exec(url); + if (splitAroundUrl.length === 3 && !splitAroundUrl[0] && !splitAroundUrl[2]) { + // Pasted content is a single URL. + const url = /^https?:\/\//i.test(text) ? text : 'https://' + text; + const youtubeUrl = this.options.allowCommandVideo &&YOUTUBE_URL_GET_VIDEO_ID.exec(url); const urlFileExtention = url.split('.').pop(); - const isImageUrl = ['jpg', 'jpeg', 'png', 'gif'].includes(urlFileExtention.toLowerCase()); - // Even indexes will always be plain text, and odd indexes will always be URL. - // only allow images emebed inside an existing link. No other url or video embed. - if (i % 2 && (isImageUrl || !selectionIsInsideALink)) { - const baseEmbedCommand = [ - { - category: this.options._t('Paste'), - name: this.options._t('Paste as URL'), - description: this.options._t('Create an URL'), - fontawesome: 'fa-link', - callback: () => { - this.historyUndo(); - this._applyRawCommand('insert', this._createLink(splitAroundUrl[i], url)); - }, - }, - { - category: this.options._t('Paste'), - name: this.options._t('Paste as text'), - description: this.options._t('Simple text paste'), - fontawesome: 'fa-font', - callback: () => {}, - }, - ]; - - const execCommandAtStepIndex = (index, callback) => { - this.historyRevertUntil(index); + const isImageUrl = ['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(urlFileExtention.toLowerCase()); + // A url cannot be transformed inside an existing link. + // An image can be embedded inside an existing link, a video cannot. + if (selectionIsInsideALink) { + if (isImageUrl) { + const img = document.createElement('IMG'); + img.setAttribute('src', url); + this._applyCommand('insert', img); + } else { + this._applyCommand('insert', text); + } + } else if (isImageUrl || youtubeUrl) { + // Open powerbox with commands to embed media or paste as link. + // Insert URL as text, store history step index to revert it later. + const stepIndexBeforeInsert = this._historySteps.length - 1; + this._applyCommand('insert', text); + const revertTextInsertion = () => { + this.historyRevertUntil(stepIndexBeforeInsert); this.historyStep(true); this._historyStepsStates.set(peek(this._historySteps).id, 'consumed'); - - callback(); - - this.historyStep(true); }; - + let commands; + const pasteAsURLCommand = { + name: this.options._t('Paste as URL'), + description: this.options._t('Create an URL.'), + fontawesome: 'fa-link', + callback: () => { + revertTextInsertion(); + this._applyRawCommand('insert', this._createLink(text, url)) + }, + }; if (isImageUrl) { - const stepIndexBeforeInsert = this._historySteps.length - 1; - this.execCommand('insert', splitAroundUrl[i]); - this.powerbox.open([ - { - category: this.options._t('Embed'), - name: this.options._t('Embed Image'), - description: this.options._t('Embed the image in the document'), - fontawesome: 'fa-image', - callback: () => { - execCommandAtStepIndex(stepIndexBeforeInsert, () => { - const img = document.createElement('IMG'); - img.setAttribute('src', url); - const sel = this.document.getSelection(); - if (!sel.isCollapsed) { - this.deleteRange(sel); - } - if (sel.rangeCount) { - sel.getRangeAt(0).insertNode(img); - sel.collapseToEnd(); - } - }); - }, + const embedImageCommand = { + name: this.options._t('Embed Image'), + description: this.options._t('Embed the image in the document.'), + fontawesome: 'fa-image', + callback: () => { + revertTextInsertion(); + const img = document.createElement('IMG'); + img.setAttribute('src', url); + this._applyRawCommand('insert', img); }, - ...baseEmbedCommand, - ]); - } else if (this.options.allowCommandVideo && youtubeUrl) { - const stepIndexBeforeInsert = this._historySteps.length - 1; - this.execCommand('insert', splitAroundUrl[i]); - this.powerbox.open([ - { - category: this.options._t('Embed'), - name: this.options._t('Embed Youtube Video'), - description: this.options._t('Embed the youtube video in the document'), - fontawesome: 'fa-youtube-play', - callback: async () => { - let videoElement; - if (this.options.getYoutubeVideoElement) { - videoElement = await this.options.getYoutubeVideoElement(youtubeUrl[0]); - } else { - videoElement = document.createElement('iframe'); - videoElement.setAttribute('width', '560'); - videoElement.setAttribute('height', '315'); - videoElement.setAttribute( - 'src', - `https://www.youtube.com/embed/${youtubeUrl[1]}`, - ); - videoElement.setAttribute('title', 'YouTube video player'); - videoElement.setAttribute('frameborder', '0'); - videoElement.setAttribute( - 'allow', - 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', - ); - videoElement.setAttribute('allowfullscreen', '1'); - } - - execCommandAtStepIndex(stepIndexBeforeInsert, () => { - - const sel = this.document.getSelection(); - if (!sel.isCollapsed) { - this.deleteRange(sel); - } - if (sel.rangeCount) { - sel.getRangeAt(0).insertNode(videoElement); - sel.collapseToEnd(); - } - }); - }, - }, - ...baseEmbedCommand, - ]); + }; + commands = [embedImageCommand, pasteAsURLCommand]; } else { - this._applyCommand('insert', this._createLink(splitAroundUrl[i], url)); + // URL is a YouTube video. + const embedVideoCommand = { + name: this.options._t('Embed Youtube Video'), + description: this.options._t('Embed the youtube video in the document.'), + fontawesome: 'fa-youtube-play', + callback: () => { + revertTextInsertion(); + let videoElement; + if (this.options.getYoutubeVideoElement) { + videoElement = this.options.getYoutubeVideoElement(youtubeUrl[0]); + } else { + videoElement = document.createElement('iframe'); + videoElement.setAttribute('width', '560'); + videoElement.setAttribute('height', '315'); + videoElement.setAttribute( + 'src', + `https://www.youtube.com/embed/${youtubeUrl[1]}`, + ); + videoElement.setAttribute('title', 'YouTube video player'); + videoElement.setAttribute('frameborder', '0'); + videoElement.setAttribute( + 'allow', + 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', + ); + videoElement.setAttribute('allowfullscreen', '1'); + } + this._applyRawCommand('insert', videoElement); + }, + }; + commands = [embedVideoCommand, pasteAsURLCommand]; } - } else if (splitAroundUrl[i] !== '') { - const textFragments = splitAroundUrl[i].split(/\r?\n/); - let textIndex = 1; - for (const textFragment of textFragments) { - this._applyCommand('insert', textFragment); - if (textIndex < textFragments.length) { - this._applyCommand('oShiftEnter'); + this.powerbox.open(commands); + } else { + this._applyCommand('insert', this._createLink(text, url)); + } + } else { + this.historyPauseSteps(); + for (let i = 0; i < splitAroundUrl.length; i++) { + const url = /^https?:\/\//gi.test(splitAroundUrl[i]) + ? splitAroundUrl[i] + : 'https://' + splitAroundUrl[i]; + // Even indexes will always be plain text, and odd indexes will always be URL. + // only allow images emebed inside an existing link. No other url or video embed. + if (i % 2 && !selectionIsInsideALink) { + this._applyCommand('insert', this._createLink(splitAroundUrl[i], url)); + } else if (splitAroundUrl[i] !== '') { + const textFragments = splitAroundUrl[i].split(/\r?\n/); + let textIndex = 1; + for (const textFragment of textFragments) { + this._applyCommand('insert', textFragment); + if (textIndex < textFragments.length) { + this._applyCommand('oShiftEnter'); + } + textIndex++; } - textIndex++; } } + this.historyUnpauseSteps(); + this.historyStep(); } - this.historyUnpauseSteps("_onPaste"); - this.historyStep(); } } _onDragStart(ev) {