[FIX] website: adapt bootstrap elements' options to iframe edition

Commit [1] moved the edition of website from the frontend to the backend
and in doing so, introduced an iframe from which the website is edited.
This broke some behaviours as the event binding for jQuery widgets (such
as the carousel) is no longer done from the same jQuery as the editor.
So bootstrap events triggered inside the iframe do not reach options
that are listening using the top window's jQuery (simple $()).

This commit adds the $bsTarget property to the SnippetOption class.
It wraps $target[0] using the iframe's jQuery instance.
This allows for options to listen to bootstrap events.
(using $bsTarget.on)

Options that were doing that previously are adapted by this commit.
(Carousel, Popup, Accordion (collapse))

Note: Popup was already adapted locally (by [2]), this commit only
changes its use of an $iframeTarget property to the $bsTarget one.

This commit also fixes "Add item" on accordion snippet to properly
assign a unique ID to the new tab.

[1]: https://github.com/odoo/odoo/commit/31cc10b91dc7762e23b4bde9b945be0c4ce3fe3b
[2]: https://github.com/odoo/odoo/commit/03c552690b15cbf2e7d6b7812386ac64042219af

task-2687506

Part-of: odoo/odoo#94564
This commit is contained in:
Arthur Detroux (ard)
2022-08-10 20:28:49 +02:00
committed by qsm-odoo
parent ddf2e74b4c
commit f7055d3dba
2 changed files with 36 additions and 26 deletions
@@ -406,6 +406,21 @@ options.Class.include({
}),
specialCheckAndReloadMethodsNames: ['customizeWebsiteViews', 'customizeWebsiteVariable', 'customizeWebsiteColor'],
/**
* @override
*/
init() {
this._super(...arguments);
// Since the website is displayed in an iframe, its jQuery
// instance is not the same as the editor. This property allows
// for easy access to bootstrap plugins (Carousel, Modal, ...).
// This is only needed because jQuery doesn't send custom events
// the same way native javascript does. So if a jQuery instance
// triggers a custom event, only that same jQuery instance will
// trigger handlers set with `.on`.
this.$bsTarget = this.ownerDocument.defaultView.$(this.$target[0]);
},
//--------------------------------------------------------------------------
// Options
//--------------------------------------------------------------------------
@@ -1494,7 +1509,7 @@ options.registry.Carousel = options.Class.extend({
* @override
*/
start: function () {
this.$target.carousel('pause');
this.$bsTarget.carousel('pause');
this.$indicators = this.$target.find('.carousel-indicators');
this.$controls = this.$target.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators');
@@ -1504,11 +1519,11 @@ options.registry.Carousel = options.Class.extend({
this.$controls.addClass('o_we_no_overlay');
let _slideTimestamp;
this.$target.on('slide.bs.carousel.carousel_option', () => {
this.$bsTarget.on('slide.bs.carousel.carousel_option', () => {
_slideTimestamp = window.performance.now();
setTimeout(() => this.trigger_up('hide_overlay'));
});
this.$target.on('slid.bs.carousel.carousel_option', () => {
this.$bsTarget.on('slid.bs.carousel.carousel_option', () => {
// slid.bs.carousel is most of the time fired too soon by bootstrap
// since it emulates the transitionEnd with a setTimeout. We wait
// here an extra 20% of the time before retargeting edition, which
@@ -1519,7 +1534,7 @@ options.registry.Carousel = options.Class.extend({
$snippet: this.$target.find('.carousel-item.active'),
ifInactiveOptions: true,
});
this.$target.trigger('active_slide_targeted');
this.$bsTarget.trigger('active_slide_targeted');
}, 0.2 * _slideDuration);
});
@@ -1530,7 +1545,7 @@ options.registry.Carousel = options.Class.extend({
*/
destroy: function () {
this._super.apply(this, arguments);
this.$target.off('.carousel_option');
this.$bsTarget.off('.carousel_option');
},
/**
* @override
@@ -1614,7 +1629,7 @@ options.registry.Carousel = options.Class.extend({
$active.clone(false)
.removeClass('active')
.insertAfter($active);
this.$target.carousel('next');
this.$bsTarget.carousel('next');
},
});
@@ -1626,7 +1641,7 @@ options.registry.CarouselItem = options.Class.extend({
* @override
*/
start: function () {
this.$carousel = this.$target.closest('.carousel');
this.$carousel = this.$bsTarget.closest('.carousel');
this.$indicators = this.$carousel.find('.carousel-indicators');
this.$controls = this.$carousel.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators');
@@ -1684,10 +1699,9 @@ options.registry.CarouselItem = options.Class.extend({
const $items = this.$carousel.find('.carousel-item');
const newLength = $items.length - 1;
if (!this.removing && newLength > 0) {
const $toDelete = $items.filter('.active');
const $toDelete = $items.filter('.active').add(this.$indicators.find('.active'));
this.$carousel.one('active_slide_targeted.carousel_item_option', () => {
$toDelete.remove();
this.$indicators.find('li:last').remove();
this.$controls.toggleClass('d-none', newLength === 1);
this.$carousel.trigger('content_changed');
this.removing = false;
@@ -1863,7 +1877,7 @@ options.registry.collapse = options.Class.extend({
*/
start: function () {
var self = this;
this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () {
this.$bsTarget.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () {
self.trigger_up('cover_update');
self.$target.trigger('content_changed');
});
@@ -1886,7 +1900,7 @@ options.registry.collapse = options.Class.extend({
*/
onMove: function () {
this._createIDs();
var $panel = this.$target.find('.collapse').removeData('bs.collapse');
var $panel = this.$bsTarget.find('.collapse').removeData('bs.collapse');
if ($panel.attr('aria-expanded') === 'true') {
$panel.closest('.accordion').find('.collapse[aria-expanded="true"]')
.filter((i, el) => (el !== $panel[0]))
@@ -1911,14 +1925,15 @@ options.registry.collapse = options.Class.extend({
const $tablist = this.$target.closest('[role="tablist"]');
const $tab = this.$target.find('[role="tab"]');
const $panel = this.$target.find('[role="tabpanel"]');
const $body = this.$target.closest('body');
const setUniqueId = ($elem, label) => {
let elemId = $elem.attr('id');
if (!elemId || $('[id="' + elemId + '"]').length > 1) {
if (!elemId || $body.find('[id="' + elemId + '"]').length > 1) {
do {
time++;
elemId = label + time;
} while ($('#' + elemId).length);
} while ($body.find('#' + elemId).length);
$elem.attr('id', elemId);
}
return elemId;
@@ -8,19 +8,14 @@ options.registry.SnippetPopup = options.Class.extend({
* @override
*/
start: function () {
// Editor jQuery and iFrame jQuery isn't the same instance. This means
// that events within the iframe (so popup events) aren't handled by
// the editor's jQuery. To apply and listen to events on the modal,
// we need to get it from the iframe's jQuery instance.
this.$iframeTarget = this.$target[0].ownerDocument.defaultView.$(this.$target[0]);
// Note: the link are excluded here so that internal modal buttons do
// not close the popup as we want to allow edition of those buttons.
this.$iframeTarget.on('click.SnippetPopup', '.js_close_popup:not(a, .btn)', ev => {
this.$bsTarget.on('click.SnippetPopup', '.js_close_popup:not(a, .btn)', ev => {
ev.stopPropagation();
this.onTargetHide();
this.trigger_up('snippet_option_visibility_update', {show: false});
});
this.$iframeTarget.on('shown.bs.modal.SnippetPopup', () => {
this.$bsTarget.on('shown.bs.modal.SnippetPopup', () => {
this.trigger_up('snippet_option_visibility_update', {show: true});
// TODO duplicated code from the popup public widget, this should
// be moved to a *video* public widget and be reviewed in master
@@ -29,7 +24,7 @@ options.registry.SnippetPopup = options.Class.extend({
iframe.src = media.dataset.oeExpression || media.dataset.src; // TODO still oeExpression to remove someday
});
});
this.$iframeTarget.on('hide.bs.modal.SnippetPopup', () => {
this.$bsTarget.on('hide.bs.modal.SnippetPopup', () => {
this.trigger_up('snippet_option_visibility_update', {show: false});
this._removeIframeSrc();
});
@@ -47,7 +42,7 @@ options.registry.SnippetPopup = options.Class.extend({
// The video should not start before the modal opens, remove it from the
// DOM. It will be added back on modal open to start the video.
this._removeIframeSrc();
this.$iframeTarget.off('.SnippetPopup');
this.$bsTarget.off('.SnippetPopup');
},
/**
* @override
@@ -65,7 +60,7 @@ options.registry.SnippetPopup = options.Class.extend({
* @override
*/
onTargetShow: async function () {
this.$iframeTarget.modal('show');
this.$bsTarget.modal('show');
$(this.$target[0].ownerDocument.body).children('.modal-backdrop:last').addClass('d-none');
},
/**
@@ -74,14 +69,14 @@ options.registry.SnippetPopup = options.Class.extend({
onTargetHide: async function () {
return new Promise(resolve => {
const timeoutID = setTimeout(() => {
this.$iframeTarget.off('hidden.bs.modal.popup_on_target_hide');
this.$bsTarget.off('hidden.bs.modal.popup_on_target_hide');
resolve();
}, 500);
this.$iframeTarget.one('hidden.bs.modal.popup_on_target_hide', () => {
this.$bsTarget.one('hidden.bs.modal.popup_on_target_hide', () => {
clearTimeout(timeoutID);
resolve();
});
this.$iframeTarget.modal('hide');
this.$bsTarget.modal('hide');
});
},
/**