[IMP] web_editor: allow to use link popover everywhere

Before this commit, the link popover was only available in the website
builder. Now it is available anywhere the wysiwyg is used.

Task-2678412

closes odoo/odoo#79542

X-original-commit: 161c5fc8e742294e8d85c889b4bf8d3d8cd484f5
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Nicolas Bayet
2021-11-09 14:00:56 +00:00
parent b1e2610390
commit e68a9d37dc
11 changed files with 162 additions and 92 deletions
-1
View File
@@ -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
@@ -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 = {}) {
@@ -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)];
}
@@ -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');
@@ -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);
};
@@ -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 || $('<div class="note-editable">');
this.$root = this.$editable;
if (this.options.resizable && !device.isMobile) {
const $wrapper = $('<div class="o_wysiwyg_wrapper odoo-editor">');
this.$root = $wrapper;
$wrapper.append(this.$editable);
this.$resizer = $(`<div class="o_wysiwyg_resizer">
<div class="o_wysiwyg_resizer_hook"></div>
@@ -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');
@@ -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))));
}
@@ -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;
}
}
}
@@ -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);
},
/**
@@ -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');
@@ -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;