diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js index 6258eacdb0e..ae6293a35cc 100644 --- a/addons/web_editor/static/src/js/editor/rte.js +++ b/addons/web_editor/static/src/js/editor/rte.js @@ -6,6 +6,7 @@ var concurrency = require('web.concurrency'); var core = require('web.core'); var Widget = require('web.Widget'); var weContext = require('web_editor.context'); +const weWidgets = require('wysiwyg.widgets'); var summernote = require('web_editor.summernote'); var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); @@ -676,6 +677,10 @@ var RTEWidget = Widget.extend({ if (this && this.$last && this.$last.length && this.$last[0] !== $target[0]) { $('.o_editable_date_field_linked').removeClass('o_editable_date_field_linked'); } + // Keep popover open if clicked inside it, but not on a button + if (!($target.parents('.o_edit_menu_popover').length && !$target.parent('a').addBack('a').length)) { + $('.o_edit_menu_popover').popover('hide'); + } if (!$editable.length || (!isLink && $.summernote.core.dom.isContentEditableFalse($target))) { return; } @@ -689,6 +694,11 @@ var RTEWidget = Widget.extend({ }); if (isLink) { + if (!$target.data('popover-widget-initialized')) { + weWidgets.LinkPopoverWidget.createFor(this, ev.target); + $target.data('popover-widget-initialized', true); + } + /** * Remove content editable everywhere and add it on the link only so that characters can be added * and removed at the start and at the end of it. @@ -702,7 +712,17 @@ var RTEWidget = Widget.extend({ // Once clicked outside, remove contenteditable on link and reactive all $(document).on('mousedown.reactivate_contenteditable', function (e) { - if ($target.is(e.target)) return; + // Keep popover open if clicked inside it, but not on a button + const $currTarget = $(e.target); + if (!($currTarget.parents('.o_edit_menu_popover').length && !$currTarget.parent('a').addBack('a').length)) { + $('.o_edit_menu_popover').popover('hide'); + } else { + return; + } + + if ($target.is(e.target)) { + return; + } if (!hasContentEditable) { $target.removeAttr('contenteditable'); } diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js index ef4421d0826..ee0db26929d 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -883,14 +883,6 @@ eventHandler.attach = function (oLayoutInfo, options) { eventHandler.modules.imageDialog.show(oLayoutInfo); }); - /** - * Open Link Dialog on double click on a link/button. - * Shows a tooltip on click to say to the user he can double click. - */ - create_dblclick_feature("a[href], a.btn, button.btn", function () { - eventHandler.modules.linkDialog.show(oLayoutInfo); - }); - oLayoutInfo.editable().on('mousedown', function (e) { if (dom.isImg(e.target) && dom.isContentEditable(e.target)) { range.createFromNode(e.target).select(); diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 4b87155fbdb..56fa0cbeef0 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -1076,12 +1076,16 @@ var SnippetsMenu = Widget.extend({ if (!srcElement || lastElement === srcElement) { return; } + var $target = $(srcElement); + // Keep popover open if clicked inside it, but not on a button + if ($target.parents('.o_edit_menu_popover').length && !$target.parent('a').addBack('a').length) { + return; + } lastElement = srcElement; _.defer(function () { lastElement = false; }); - var $target = $(srcElement); if (!$target.closest('we-button, we-toggler, .o_we_color_preview').length) { this._closeWidgets(); } diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js b/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js new file mode 100644 index 00000000000..13d811feb09 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js @@ -0,0 +1,188 @@ +/** @odoo-module **/ + +import Widget from 'web.Widget'; +import {_t} from 'web.core'; + +const LinkPopoverWidget = Widget.extend({ + template: 'wysiwyg.widgets.link.edit.tooltip', + xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'], + events: { + 'click .o_we_remove_link': '_onRemoveLinkClick', + 'click .o_we_edit_link': '_onEditLinkClick', + }, + + /** + * @constructor + * @param {Element} target: target Element for which we display a popover + */ + init(parent, target) { + this._super(...arguments); + this.target = target; + this.$target = $(target); + this.href = this.$target.attr('href'); // for template + }, + /** + * + * @override + */ + start() { + this.$urlLink = this.$('.o_we_url_link'); + this.$previewFaviconImg = this.$('.o_we_preview_favicon img'); + this.$previewFaviconFa = this.$('.o_we_preview_favicon .fa'); + this.$copyLink = this.$('.o_we_copy_link'); + this.$fullUrl = this.$('.o_we_full_url'); + + // Copy onclick handler + const clipboard = new ClipboardJS( + this.$copyLink[0], + {text: () => this.target.href} // Absolute href + ); + clipboard.on('success', () => { + this.$copyLink.tooltip('hide'); + this.displayNotification({ + type: 'success', + message: _t("Link copied to clipboard."), + }); + }); + + // init tooltips & popovers + this.$('[data-toggle="tooltip"]').tooltip({delay: 0, placement: 'bottom'}); + this.$target.popover({ + html: true, + content: this.$el, + placement: 'bottom', + trigger: 'click', + }) + .on('show.bs.popover', () => { + this._loadAsyncLinkPreview(); + }) + .popover('show') + .data('bs.popover').tip.classList.add('o_edit_menu_popover'); + + return this._super(...arguments); + }, + /** + * + * @override + */ + destroy() { + this.$target.popover('dispose'); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Fetches and gets the link preview data (title, description..). + * For external URL, only the favicon will be loaded. + * + * @private + */ + _loadAsyncLinkPreview() { + let url; + try { + url = new URL(this.target.href); // relative to absolute + } catch (e) { + // Invalid URL, might happen with editor unsuported protocol. eg type + // `geo:37.786971,-122.399677`, become `http://geo:37.786971,-122.399677` + this.displayNotification({ + type: 'danger', + message: _t("This URL is invalid. Preview couldn't be updated."), + }); + return; + } + + this._resetPreview(url); + const protocol = url.protocol; + if (!protocol.startsWith('http')) { + const faMap = {'mailto:': 'fa-envelope-o', 'tel:': 'fa-phone'}; + const icon = faMap[protocol]; + if (icon) { + this.$previewFaviconFa.toggleClass(`fa-globe ${icon}`); + } + } else if (window.location.hostname !== url.hostname) { + // Preview pages from current website only. External website will + // most of the time raise a CORS error. To avoid that error, we + // would need to fetch the page through the server (s2s), involving + // enduser fetching problematic pages such as illicit content. + this.$previewFaviconImg.attr({ + 'src': `https://www.google.com/s2/favicons?sz=16&domain=${url}` + }).removeClass('d-none'); + this.$previewFaviconFa.addClass('d-none'); + } else { + $.get(this.target.href).then(content => { + const parser = new window.DOMParser(); + const doc = parser.parseFromString(content, "text/html"); + + // Get + const favicon = doc.querySelector("link[rel~='icon']"); + const ogTitle = doc.querySelector("[property='og:title']"); + const title = doc.querySelector("title"); + + // Set + if (favicon) { + this.$previewFaviconImg.attr({'src': favicon.href}).removeClass('d-none'); + this.$previewFaviconFa.addClass('d-none'); + } + if (ogTitle || title) { + this.$urlLink.text(ogTitle ? ogTitle.getAttribute('content') : title.text.trim()); + } + this.$fullUrl.removeClass('d-none').addClass('o_we_webkit_box'); + this.$target.popover('update'); + }); + } + }, + /** + * Resets the preview elements visibility. Particularly useful when changing + * the link url from an internal to an external one and vice versa. + * + * @private + * @param {string} url + */ + _resetPreview(url) { + this.$previewFaviconImg.addClass('d-none'); + this.$previewFaviconFa.removeClass('d-none').addClass('fa-globe'); + this.$urlLink.text(url).attr('href', url); + this.$fullUrl.text(url).addClass('d-none').removeClass('o_we_webkit_box'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Opens the Link Dialog. + * + * TODO Call business methods once new editor is released instead of click + * + * @private + * @param {Event} ev + */ + _onEditLinkClick(ev) { + $('.note-link-popover [data-event="showLinkDialog"]').click(); + }, + /** + * Removes the link/anchor. + * + * TODO Call business methods once new editor is released instead of click + * + * @private + * @param {Event} ev + */ + _onRemoveLinkClick(ev) { + $('.note-link-popover [data-event="unlink"]').click(); + }, +}); + +LinkPopoverWidget.createFor = async function (parent, targetEl) { + // Target might already have a popover, eg cart icon in navbar + if ($(targetEl).data('bs.popover')) { + return null; + } + const popoverWidget = new this(parent, targetEl); + return popoverWidget.appendTo(targetEl).then(() => popoverWidget); +}; + +export default LinkPopoverWidget; diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js b/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js index 64a9dc062f3..5afb2b7db49 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js @@ -6,6 +6,7 @@ var AltDialog = require('wysiwyg.widgets.AltDialog'); var MediaDialog = require('wysiwyg.widgets.MediaDialog'); var LinkDialog = require('wysiwyg.widgets.LinkDialog'); var ImageCropWidget = require('wysiwyg.widgets.ImageCropWidget'); +const LinkPopoverWidget = require('@web_editor/js/wysiwyg/widgets/link_popover_widget')[Symbol.for("default")]; const {ColorpickerDialog} = require('web.Colorpicker'); var media = require('wysiwyg.widgets.media'); @@ -16,6 +17,7 @@ return { MediaDialog: MediaDialog, LinkDialog: LinkDialog, ImageCropWidget: ImageCropWidget, + LinkPopoverWidget: LinkPopoverWidget, ColorpickerDialog: ColorpickerDialog, MediaWidget: media.MediaWidget, diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml index 49f39a47bd5..92b0022645b 100644 --- a/addons/web_editor/static/src/xml/wysiwyg.xml +++ b/addons/web_editor/static/src/xml/wysiwyg.xml @@ -470,6 +470,26 @@ + +
+