[FIX] web_tour, web_editor: observe iframe DOM mutations in tours

Before this commit, iframes DOM mutations were not taken into account to
update the tour manager. So in the context of mass_mailing, displaying
the web editor in an iframe, the tours were updated at the right moment
by chance (no mutations for 750ms in the global document and in the
iframe document).

This commit adds an o_iframe class, used to define an iframe that will
render a part of Odoo. An observer is added in the tour service to
monitor the addition of such iframe, and sets an observer on that iframe
to correctly update the tour manager, counting the iframe DOM mutations
for the 750ms delay.

In addition to this observer, the iframe's HTML content is added to the
final document HTML log when a tour crashes (directly between the
<iframe></iframe> for convenience, copy/pasting it in a browser just
ignores what's in between).

Also, when the tour_service is initialized in an o_iframe, it will not
run the tours.

This fix targets the saas-15.3 as it is needed to avoid a race condition
introduced with [1] (introducing a tour which relies on DOM updates
inside the mass mailing iframe). This could be backported if the need
ever rise (it should really have been the case since [2]).

[1]: https://github.com/odoo/odoo/commit/52d32e26c34bef10823582ef5c1f840492b73d7a
[2]: https://github.com/odoo/odoo/commit/971629b9fe40911c3a9765c9fb746ea1c55d3560

closes odoo/odoo#91718

X-original-commit: ea35bd06c84b7101224c3b3dd24d1780e13232a9
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Younn Olivier
2022-05-19 12:28:24 +02:00
committed by qsm-odoo
parent 6a7c2840cd
commit 79867ca96f
3 changed files with 69 additions and 8 deletions
@@ -92,7 +92,7 @@ Wysiwyg.include({
*/
_loadIframe: function () {
var self = this;
this.$iframe = $('<iframe class="wysiwyg_iframe">').css({
this.$iframe = $('<iframe class="wysiwyg_iframe o_iframe">').css({
'min-height': '55vh',
width: '100%'
});
+11 -1
View File
@@ -460,7 +460,17 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
_.each(this._log, function (log) {
console.log(log);
});
console.log(document.body.parentElement.outerHTML);
let documentHTML = document.documentElement.outerHTML;
// Replace empty iframe tags with their content
const iframeEls = Array.from(document.body.querySelectorAll('iframe.o_iframe'));
if (iframeEls.length) {
const matches = documentHTML.match(/<iframe[^>]+class=["'][^"']*\bo_iframe\b[^>]+><\/iframe>/g);
for (let i = 0; i < matches.length; i++) {
const iframeWithContent = matches[i].replace(/></, `>${iframeEls[i].contentDocument.documentElement.outerHTML}<`);
documentHTML = documentHTML.replace(matches[i], `\n${iframeWithContent}\n`);
}
}
console.log(documentHTML);
console.error(error); // will be displayed as error info
} else {
console.log(_.str.sprintf("Tour %s succeeded", tour_name));
+57 -6
View File
@@ -36,6 +36,14 @@ return session.is_bound.then(function () {
const disabled = session.tour_disable || device.isMobile;
var tour_manager = new TourManager(rootWidget, consumed_tours, disabled);
// The tests can be loaded inside an iframe. The tour manager should
// not run in that context, as it will already run in its parent
// window.
const isInIframe = window.frameElement && window.frameElement.classList.contains('o_iframe');
if (isInIframe) {
return tour_manager;
}
function _isTrackedNode(node) {
if (node.classList) {
return !untrackedClassnames
@@ -140,18 +148,61 @@ return session.is_bound.then(function () {
});
// Now that the observer is configured, we have to start it when needed.
const observerOptions = {
attributes: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterData: true,
};
var start_service = (function () {
return function (observe) {
return new Promise(function (resolve, reject) {
tour_manager._register_all(observe).then(function () {
if (observe) {
observer.observe(document.body, {
attributes: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterData: true,
observer.observe(document.body, observerOptions);
// If an iframe is added during the tour, its DOM
// mutations should also be observed to update the
// tour manager.
const findIframe = mutations => {
for (const mutation of mutations) {
for (const addedNode of Array.from(mutation.addedNodes)) {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
if (addedNode.classList.contains('o_iframe')) {
return addedNode;
}
const iframeChildEl = addedNode.querySelector('.o_iframe');
if (iframeChildEl) {
return iframeChildEl;
}
}
}
}
};
const iframeObserver = new MutationObserver(mutations => {
const iframeEl = findIframe(mutations);
if (iframeEl) {
iframeEl.addEventListener('load', () => {
observer.observe(iframeEl.contentDocument.body, observerOptions);
});
// If the iframe was added without a src,
// its load event was immediately fired and
// will not fire again unless another src is
// set. Unfortunately, the case of this
// happening and the iframe content being
// altered programmaticaly may happen.
// (E.g. at the moment this was written,
// the mass mailing editor iframe is added
// without src and its content rewritten
// immediately afterwards).
if (!iframeEl.src) {
observer.observe(iframeEl.contentDocument, observerOptions);
}
}
});
iframeObserver.observe(document.body, { childList: true, subtree: true });
}
resolve();
});