diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index 418c6278ff5..c7f4ddadaef 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -57,7 +57,6 @@ Odoo Web Editor widget. 'web_editor/static/lib/odoo-editor/src/commands/toggleList.js', # utils - 'web_editor/static/src/js/wysiwyg/wysiwyg_utils.js', 'web_editor/static/src/js/wysiwyg/PeerToPeer.js', # odoo utils diff --git a/addons/web_editor/static/src/js/backend/QWebPlugin.js b/addons/web_editor/static/src/js/backend/QWebPlugin.js index f282c990e45..9cce6b89f9b 100644 --- a/addons/web_editor/static/src/js/backend/QWebPlugin.js +++ b/addons/web_editor/static/src/js/backend/QWebPlugin.js @@ -1,16 +1,6 @@ /** @odoo-module **/ -/** - * Returns a list of all the ancestors nodes of the provided node. - * - * @param {Node} node - * @param {Node} [stopElement] include to prevent bubbling up further than the stopElement. - * @returns {HTMLElement[]} - */ -function ancestors(node, stopElement) { - if (!node || !node.parentElement || node === stopElement) return []; - return [node.parentElement, ...ancestors(node.parentElement, stopElement)]; -} +import { ancestors } from '@web_editor/js/common/wysiwyg_utils'; export class QWebPlugin { constructor(options = {}) { diff --git a/addons/web_editor/static/src/js/common/wysiwyg_utils.js b/addons/web_editor/static/src/js/common/wysiwyg_utils.js new file mode 100644 index 00000000000..c4c81dc60fc --- /dev/null +++ b/addons/web_editor/static/src/js/common/wysiwyg_utils.js @@ -0,0 +1,17 @@ +/** @odoo-module **/ + +export function isImg(node) { + return (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)(media_iframe_video|o_image|fa)(\s|$)/i)))); +} + +/** + * Returns a list of all the ancestors nodes of the provided node. + * + * @param {Node} node + * @param {Node} [stopElement] include to prevent bubbling up further than the stopElement. + * @returns {HTMLElement[]} + */ +export function ancestors(node, stopElement) { + if (!node || !node.parentElement || node === stopElement) return []; + return [node.parentElement, ...ancestors(node.parentElement, stopElement)]; +} \ No newline at end of file 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 d9f2b017d07..b671b798229 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/link.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link.js @@ -3,7 +3,6 @@ odoo.define('wysiwyg.widgets.Link', function (require) { const core = require('web.core'); const OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/OdooEditor'); -const wysiwygUtils = require('@web_editor/js/wysiwyg/wysiwyg_utils'); const Widget = require('web.Widget'); const {isColorGradient} = require('web_editor.utils'); 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 index c0eb626906e..5d9807cdf0d 100644 --- 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 @@ -3,6 +3,7 @@ import Widget from 'web.Widget'; import {_t} from 'web.core'; import {DropPrevious} from 'web.concurrency'; +import { ancestors } from '@web_editor/js/common/wysiwyg_utils'; const LinkPopoverWidget = Widget.extend({ template: 'wysiwyg.widgets.link.edit.tooltip', @@ -15,9 +16,11 @@ const LinkPopoverWidget = Widget.extend({ /** * @constructor * @param {Element} target: target Element for which we display a popover + * @param {Wysiwyg} [option.wysiwyg]: The wysiwyg editor */ - init(parent, target) { + init(parent, target, options) { this._super(...arguments); + this.options = options; this.target = target; this.$target = $(target); this.href = this.$target.attr('href'); // for template @@ -48,7 +51,12 @@ const LinkPopoverWidget = Widget.extend({ }); // init tooltips & popovers - this.$('[data-toggle="tooltip"]').tooltip({delay: 0, placement: 'bottom'}); + this.$('[data-toggle="tooltip"]').tooltip({ + delay: 0, + placement: 'bottom', + container: this.options.wysiwyg.odooEditor.document.body, + }); + let popoverShown = true; this.$target.popover({ html: true, content: this.$el, @@ -59,15 +67,45 @@ const LinkPopoverWidget = Widget.extend({ // 3. Remain open when the popover content is clicked.. // 4. ..except if it the click was on a button of the popover content // 5. Close when the user click somewhere on the page (not being the link or the popover content) - trigger: 'focus', + trigger: 'manual', boundary: 'viewport', + container: this.options.wysiwyg.odooEditor.document.body, }) .on('show.bs.popover.link_popover', () => { this._loadAsyncLinkPreview(); + popoverShown = true; + }) + .on('hide.bs.popover.link_popover', () => { + popoverShown = false; }) .popover('show') .data('bs.popover').tip.classList.add('o_edit_menu_popover'); + + this.$target.on('mousedown.link_popover', (e) => { + if (!popoverShown) { + this.$target.popover('show'); + } + }); + const onClickDocument = (e) => { + if (popoverShown) { + const hierarchy = [e.target, ...ancestors(e.target)]; + if ( + !( + hierarchy.includes(this.$target[0]) || + (hierarchy.includes(this.$el[0]) && + !hierarchy.some(x => x.tagName && x.tagName === 'A')) + ) + ) { + this.$target.popover('hide'); + } + } + } + $(document).on('mouseup.link_popover', onClickDocument); + if (document !== this.options.wysiwyg.odooEditor.document) { + $(this.options.wysiwyg.odooEditor.document).on('mouseup.link_popover', onClickDocument); + } + return this._super(...arguments); }, /** @@ -79,10 +117,19 @@ const LinkPopoverWidget = Widget.extend({ // leak. However, it is only one leak per click on a link during edit // mode so this should not be a huge problem. this.$target.off('.link_popover'); + $(document).off('.link_popover'); + $(this.options.wysiwyg.odooEditor.document).off('.link_popover'); this.$target.popover('dispose'); return this._super(...arguments); }, + /** + * Hide the popover. + */ + hide() { + this.$target.popover('hide'); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -174,7 +221,8 @@ const LinkPopoverWidget = Widget.extend({ * @param {Event} ev */ _onEditLinkClick(ev) { - $('#wrapwrap').data('wysiwyg').toggleLinkTools({ + ev.preventDefault(); + this.options.wysiwyg.toggleLinkTools({ forceOpen: true, link: this.$target[0], }); @@ -187,20 +235,20 @@ const LinkPopoverWidget = Widget.extend({ * @param {Event} ev */ _onRemoveLinkClick(ev) { - // TODO surely there is better to do than finding the editor instance in the DOM? - $('#wrapwrap').data('wysiwyg').odooEditor.execCommand('unlink'); + ev.preventDefault(); + this.options.wysiwyg.odooEditor.execCommand('unlink'); ev.stopImmediatePropagation(); }, }); -LinkPopoverWidget.createFor = async function (parent, targetEl) { +LinkPopoverWidget.createFor = async function (parent, targetEl, options) { const noLinkPopoverClass = ".o_no_link_popover, .carousel-control-prev, .carousel-control-next, .dropdown-toggle"; // Target might already have a popover, eg cart icon in navbar const alreadyPopover = $(targetEl).data('bs.popover'); if (alreadyPopover || $(targetEl).is(noLinkPopoverClass) || !!$(targetEl).parents(noLinkPopoverClass).length) { return null; } - const popoverWidget = new this(parent, targetEl); + const popoverWidget = new this(parent, targetEl, options); return popoverWidget.appendTo(targetEl).then(() => popoverWidget); }; diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 35aea4bcb3c..60cbd039f8c 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -15,7 +15,7 @@ const OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/OdooEditor'); const snippetsEditor = require('web_editor.snippet.editor'); const Toolbar = require('web_editor.toolbar'); const weWidgets = require('wysiwyg.widgets'); -const wysiwygUtils = require('@web_editor/js/wysiwyg/wysiwyg_utils'); +const wysiwygUtils = require('@web_editor/js/common/wysiwyg_utils'); const weUtils = require('web_editor.utils'); const { PeerToPeer } = require('@web_editor/js/wysiwyg/PeerToPeer'); const { Mutex } = require('web.concurrency'); @@ -41,6 +41,7 @@ const Wysiwyg = Widget.extend({ lang: 'odoo', colors: customColors, recordInfo: {context: {}}, + document: document, }, init: function (parent, options) { this._super.apply(this, arguments); @@ -122,6 +123,7 @@ const Wysiwyg = Widget.extend({ const $wrapwrap = $('#wrapwrap'); if ($wrapwrap.length) { $wrapwrap[0].addEventListener('scroll', this.odooEditor.multiselectionRefresh, { passive: true }); + this.$root = $wrapwrap; } if (this._peerToPeerLoading) { @@ -226,6 +228,44 @@ const Wysiwyg = Widget.extend({ // Ensure the Toolbar always have the correct layout in note. this._updateEditorUI(); + this.$root.on('mousedown', (ev) => { + const $target = $(ev.target); + + // 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) { + ev.preventDefault(); + } + + if ($target.is(this.customizableLinksSelector) && $target.is('a') && !$target.attr('data-oe-model') && !$target.find('> [data-oe-model]').length) { + if (!$target.data('popover-widget-initialized')) { + // TODO this code is ugly maybe the mutex should be in the + // editor root widget / the popover should not depend on + // editor panel (like originally intended but...) / ... + (async () => { + if (this.snippetsMenu) { + // Await for the editor panel to be fully updated + // as some buttons of the link popover we create + // here relies on clicking in that editor panel... + await this.snippetsMenu._mutex.exec(() => null); + } + this.linkPopover = await weWidgets.LinkPopoverWidget.createFor(this, ev.target, { wysiwyg: this }); + $target.data('popover-widget-initialized', true); + })(); + } + $target.focus(); + if ($target.closest('#wrapwrap').length) { + this.toggleLinkTools({ + forceOpen: true, + link: $target[0], + noFocusUrl: true, + }); + } + } + }); + + this._onSelectionChange = this._onSelectionChange.bind(this); + this.odooEditor.document.addEventListener('selectionchange', this._onSelectionChange); + return _super.apply(this, arguments).then(() => { if (this.options.autohideToolbar) { if (this.odooEditor.isMobile) { @@ -236,7 +276,6 @@ const Wysiwyg = Widget.extend({ } }); }, - setupCollaboration(collaborationChannel) { const modelName = collaborationChannel.collaborationModelName; const fieldName = collaborationChannel.collaborationFieldName; @@ -463,6 +502,7 @@ const Wysiwyg = Widget.extend({ } if (this.odooEditor) { + this.odooEditor.document.removeEventListener('selectionchange', this._onSelectionChange); this.odooEditor.destroy(); } // If peer to peer is initializing, wait for properly closing it. @@ -481,6 +521,11 @@ const Wysiwyg = Widget.extend({ if ($wrapwrap.length) { $('#wrapwrap')[0].removeEventListener('scroll', this.odooEditor.multiselectionRefresh, { passive: true }); } + $(this.$root).off('mousedown'); + if (this.linkPopover) { + this.linkPopover.hide(); + } + this._super(); }, /** @@ -488,9 +533,11 @@ const Wysiwyg = Widget.extend({ */ renderElement: function () { this.$editable = this.options.editable || $('