[FIX] web_tour: add dom untracked modifications rules

This commit adds some untracked modifications rules that ignore
these DOM modifications:

- Ignore when the class "o_tooltip_parent" is added or removed (will
  be important with the next commit)

- Ignore when the ID of an element changes (normally never in Odoo
  codebase (at least not alongside other changes), but jQuery uses
  ID modification for some performance improvements).

This is required to avoid loops in the update system (the
o_tooltip_parent class is added, thus we update tips, which search the
DOM so jQuery changes IDs, thus we update tips, which ...). Previously,
this already occured but less frequently than it is about to be
with future tips improvements.

To be able to do that change, the system had to be reviewed. Indeed,
the DOM mutation observer was debounced thus leading to considering a
very small portion of DOM mutation. As we now have to check the addition
or removal of a specific class or ID, we have to consider them all. In
fact, it worked by chance before... this occured:

1) The user clicks on something, the UI is updated -> DOM mutation
2) The code uses jQuery so a strange ID update is done -> DOM mutation
3) As the observer is debounced, we only see (2), which was not an
   ignored DOM mutation -> we update the tips.

Now that (2) is an ignored DOM mutation, if we still debounce, (3) will
not update the tips while it should have because of (1). In fact, this
could theoratically break already: if (2) is a tip update for some
reason, (1) will be forgotten in the current codebase.

Instead of debouncing we now register all DOM mutations and process
them in a debounced way. So if I receive 3949 mutations over half a
second, I will save them all but only start processing them once I am
not receiving some anymore. And most of the time, only a couple of
processing is needed to know that a tracked DOM mutation occured, which
thus needs to lead to a tip update.

Part of https://github.com/odoo/odoo/pull/48678
task-2228694
This commit is contained in:
qsm-odoo
2020-04-14 08:29:56 +00:00
parent 4ce1340f4f
commit 88e115b480
+97 -15
View File
@@ -1,12 +1,13 @@
odoo.define('web_tour.tour', function (require) {
"use strict";
var config = require('web.config');
var rootWidget = require('root.widget');
var rpc = require('web.rpc');
var session = require('web.session');
var TourManager = require('web_tour.TourManager');
const untrackedClassnames = ["o_tooltip", "o_tooltip_content", "o_tooltip_overlay"];
/**
* @namespace
* @property {Object} active_tooltips
@@ -33,25 +34,105 @@ return session.is_bound.then(function () {
var consumed_tours = session.is_frontend ? results[0] : session.web_tours;
var tour_manager = new TourManager(rootWidget, consumed_tours);
// Use a MutationObserver to detect DOM changes
var untracked_classnames = ["o_tooltip", "o_tooltip_content", "o_tooltip_overlay"];
var check_tooltip = _.debounce(function (records) {
var update = _.some(records, function (record) {
return !(is_untracked(record.target) ||
_.some(record.addedNodes, is_untracked) ||
_.some(record.removedNodes, is_untracked));
function _isTrackedNode(node) {
if (node.classList) {
return !untrackedClassnames
.some(className => node.classList.contains(className));
}
return true;
}
function is_untracked(node) {
var record_class = node.className;
return (_.isString(record_class) &&
_.intersection(record_class.split(' '), untracked_classnames).length !== 0);
const classSplitRegex = /\s+/g;
const tooltipParentRegex = /\bo_tooltip_parent\b/;
let currentMutations = [];
function _processMutations() {
const hasTrackedMutation = currentMutations.some(mutation => {
// First check if the mutation applied on an element we do not
// track (like the tour tips themself).
if (!_isTrackedNode(mutation.target)) {
return false;
}
if (mutation.type === 'childList') {
// If it is a modification to the DOM hierarchy, only
// consider the addition/removal of tracked nodes.
for (const nodes of [mutation.addedNodes, mutation.removedNodes]) {
for (const node of nodes) {
if (!_isTrackedNode(node)) {
return false;
}
}
}
} else if (mutation.type === 'attributes') {
// Get old and new value of the attribute. Note: as we
// compute the new value after a setTimeout, this might not
// actually be the new value for that particular mutation
// record but this is the one after all mutations. This is
// normally not an issue: e.g. "a" -> "a b" -> "a" will be
// seen as "a" -> "a" (not "a b") + "a b" -> "a" but we
// only need to detect *one* tracked mutation to know we
// have to update tips anyway.
const oldV = mutation.oldValue ? mutation.oldValue.trim() : '';
const newV = (mutation.target.getAttribute(mutation.attributeName) || '').trim();
// Not sure why but this occurs, especially on ID change
// (probably some strange jQuery behavior, see below).
// Also sometimes, a class is just considered changed while
// it just loses the spaces around the class names.
if (oldV === newV) {
return false;
}
if (mutation.attributeName === 'id') {
// Check if this is not an ID change done by jQuery for
// performance reasons.
return !(oldV.includes('sizzle') || newV.includes('sizzle'));
} else if (mutation.attributeName === 'class') {
// Check if the change is *only* about receiving or
// losing the 'o_tooltip_parent' class, which is linked
// to the tour service system. We have to check the
// potential addition of another class as we compute
// the new value after a setTimeout. So this case:
// 'a' -> 'a b' -> 'a b o_tooltip_parent' produces 2
// mutation records but will be seen here as
// 1) 'a' -> 'a b o_tooltip_parent'
// 2) 'a b' -> 'a b o_tooltip_parent'
const hadClass = tooltipParentRegex.test(oldV);
const newClasses = mutation.target.classList;
const hasClass = newClasses.contains('o_tooltip_parent');
return !(hadClass !== hasClass
&& Math.abs(oldV.split(classSplitRegex).length - newClasses.length) === 1);
}
}
return true;
});
if (update) { // ignore mutations which concern the tooltips
// Either all the mutations have been ignored or one was detected as
// tracked and will trigger a tour manager update.
currentMutations = [];
// Update the tour manager if required.
if (hasTrackedMutation) {
tour_manager.update();
}
}, 500);
var observer = new MutationObserver(check_tooltip);
}
// Use a MutationObserver to detect DOM changes. When a mutation occurs,
// only add it to the list of mutations to process and delay the
// mutation processing. We have to record them all and not in a
// debounced way otherwise we may ignore tracked ones in a serie of
// 10 tracked mutations followed by an untracked one. Most of them
// will trigger a tip check anyway so, most of the time, processing the
// first ones will be enough to ensure that a tip update has to be done.
let mutationTimer;
const observer = new MutationObserver(mutations => {
clearTimeout(mutationTimer);
currentMutations.push(...mutations);
mutationTimer = setTimeout(() => _processMutations(), 500);
});
// Now that the observer is configured, we have to start it when needed.
var start_service = (function () {
return function (observe) {
return new Promise(function (resolve, reject) {
@@ -61,6 +142,7 @@ return session.is_bound.then(function () {
attributes: true,
childList: true,
subtree: true,
attributeOldValue: true,
});
}
resolve();