[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:
@@ -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%'
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user