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 || $('
'); + this.$root = this.$editable; if (this.options.resizable && !device.isMobile) { const $wrapper = $('
'); + this.$root = $wrapper; $wrapper.append(this.$editable); this.$resizer = $(`
@@ -1826,6 +1873,14 @@ const Wysiwyg = Widget.extend({ } return new Promise(function () {}); }, + _onSelectionChange() { + if (this.options.autohideToolbar) { + const isVisible = this.linkPopover && this.linkPopover.el.offsetParent; + if (isVisible && !this.odooEditor.document.getSelection().isCollapsed) { + this.linkPopover.hide(); + } + } + }, _onDocumentMousedown: function (e) { if (!e.target.classList.contains('o_editable_date_field_linked')) { this.$editable.find('.o_editable_date_field_linked').removeClass('o_editable_date_field_linked'); diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_utils.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_utils.js deleted file mode 100644 index e15bd82e845..00000000000 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_utils.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @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)))); -} diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index 37777350fc0..eee6ebaeef5 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -779,3 +779,33 @@ img::selection { .o_transform_removal { transform: none !important; } + +.o_edit_menu_popover { + max-width: $popover-max-width * 1.2; + // Prevent UI glitch after fetching page preview (size might change) + width: $popover-max-width * 1.2; + // Prevent the edited link from being deselected when clicking between + // buttons in the popover + user-select: none; + + .o_we_url_link { + word-break: break-all; + } + + .o_we_full_url { + word-break: break-all; + overflow: hidden; + text-overflow: ellipsis; + // clamp (`-webkit-box` display toggle in js) + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &.o_we_webkit_box { + display: -webkit-box; + } + + &:hover { + -webkit-line-clamp: unset; + } + } +} \ No newline at end of file diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 0c89a856da4..3cfef39bb7e 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -1295,7 +1295,7 @@ options.registry.menu_data = options.Class.extend({ * @override */ start: function () { - wLinkPopoverWidget.createFor(this, this.$target[0]); + wLinkPopoverWidget.createFor(this, this.$target[0], { wysiwyg: $('#wrapwrap').data('wysiwyg') }); return this._super(...arguments); }, /** diff --git a/addons/website/static/src/js/editor/wysiwyg.js b/addons/website/static/src/js/editor/wysiwyg.js index fc7e1b124b5..0f07200bb56 100644 --- a/addons/website/static/src/js/editor/wysiwyg.js +++ b/addons/website/static/src/js/editor/wysiwyg.js @@ -51,39 +51,6 @@ Wysiwyg.include({ start: function () { this.options.toolbarHandler = $('#web_editor-top-edit'); - $(document.body).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.attr('data-oe-model') && !$target.find('> [data-oe-model]').length && $target.closest('#wrapwrap').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); - } - weWidgets.LinkPopoverWidget.createFor(this, ev.target); - $target.data('popover-widget-initialized', true); - })(); - } - $target.focus(); - $('#wrapwrap').data('wysiwyg').toggleLinkTools({ - forceOpen: true, - link: $target[0], - noFocusUrl: true, - }); - } - }); - // Dropdown menu initialization: handle dropdown openings by hand var $dropdownMenuToggles = this.$('.o_mega_menu_toggle, #top_menu_container .dropdown-toggle'); $dropdownMenuToggles.removeAttr('data-toggle').dropdown('dispose'); diff --git a/addons/website/static/src/scss/website.editor.ui.scss b/addons/website/static/src/scss/website.editor.ui.scss index 954e111230c..0096c9c55b9 100644 --- a/addons/website/static/src/scss/website.editor.ui.scss +++ b/addons/website/static/src/scss/website.editor.ui.scss @@ -112,36 +112,6 @@ $o-we-switch-inactive-color: #F7F7F7 !default; } } -.o_edit_menu_popover { - max-width: $popover-max-width * 1.2; - // Prevent UI glitch after fetching page preview (size might change) - width: $popover-max-width * 1.2; - // Prevent the edited link from being deselected when clicking between - // buttons in the popover - user-select: none; - - .o_we_url_link { - word-break: break-all; - } - - .o_we_full_url { - word-break: break-all; - overflow: hidden; - text-overflow: ellipsis; - // clamp (`-webkit-box` display toggle in js) - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - - &.o_we_webkit_box { - display: -webkit-box; - } - - &:hover { - -webkit-line-clamp: unset; - } - } -} - .o_new_content_loader_container { background-color: rgba($o-shadow-color, .9); pointer-events: all;