[IMP] web_editor: optimize selector functions for snippetOptions

Because of cross-browser compatibility concerns, jQuery's selector
engine makes using :has selector relatively slow. This is usually
acceptable, but in the case of snippet options, some options also need
to use :has in their data-target selector, and nested :has selectors'
performance degrade exponentially with the level of nesting.

Additionaly, jQuery evaluates complex selectors right-to-left, which
makes the :not operator quite inefficient.

Because of that, this commit removes :has and :not from the conditions
that were added at the end of the data-selector, and speeds up the
selector by a factor of ~15-40

Lastly, the addDropSelector options was actually unused, and a leftover
from the editor revert, as such it has been removed.

closes odoo/odoo#50073

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Samuel Degueldre
2020-05-19 06:55:42 +00:00
committed by qsm-odoo
parent ee7e75d74b
commit a814b671ed
@@ -1386,11 +1386,11 @@ var SnippetsMenu = Widget.extend({
* See implementation for function details.
*
* @private
* @param {string} include
* @param {string} selector
* jQuery selector that DOM elements must match to be considered as
* potential snippet.
* @param {string} exclude
* jQuery selector that DOM elements must *not* match the be
* jQuery selector that DOM elements must *not* match to be
* considered as potential snippet.
* @param {string|false} target
* jQuery selector that at least one child of a DOM element must
@@ -1403,46 +1403,33 @@ var SnippetsMenu = Widget.extend({
* considered (@see noCheck), this is true if the DOM elements'
* parent must also be in an editable environment to be considered.
*/
_computeSelectorFunctions: function (include, exclude, target, noCheck, isChildren) {
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren) {
var self = this;
// Convert the selector for elements to include into a list
var selectorList = _.compact(include.split(/\s*,\s*/));
exclude += `${exclude && ', '}.o_snippet_not_selectable`;
// Convert the selector for elements to exclude into a list
var excludeList = _.compact(exclude.split(/\s*,\s*/));
excludeList.push('.o_snippet_not_selectable');
// Prepare the condition that will be added to each subselector for
// elements to include: 'not the elements to exclude and only the
// editable ones if needed'
var selectorConditions = _.map(excludeList, function (exc) {
return ':not(' + exc + ')';
}).join('');
let filterFunc = function () {
return !$(this).is(exclude);
};
if (target) {
selectorConditions += ':has(' + target + ')';
const oldFilter = filterFunc;
filterFunc = function () {
return oldFilter.apply(this) && $(this).find(target).length !== 0;
};
}
if (!noCheck) {
selectorConditions = (this.options.addDropSelector || '') + selectorConditions;
}
// (Re)join the subselectors
var selector = _.map(selectorList, function (s) {
return s + selectorConditions;
}).join(', ');
// Prepare the functions
var functions = {
is: function ($from) {
return $from.is(selector);
return $from.is(selector) && $from.filter(filterFunc).length !== 0;
},
};
if (noCheck) {
functions.closest = function ($from, parentNode) {
return $from.closest(selector, parentNode);
return $from.closest(selector, parentNode).filter(filterFunc);
};
functions.all = function ($from) {
return $from ? dom.cssFind($from, selector) : $(selector);
return ($from ? dom.cssFind($from, selector) : $(selector)).filter(filterFunc);
};
} else {
functions.closest = function ($from, parentNode) {
@@ -1456,13 +1443,13 @@ var SnippetsMenu = Widget.extend({
node = node.parentNode;
}
return false;
});
}).filter(filterFunc);
};
functions.all = isChildren ? function ($from) {
return dom.cssFind($from || self.getEditableArea(), selector);
return dom.cssFind($from || self.getEditableArea(), selector).filter(filterFunc);
} : function ($from) {
$from = $from || self.getEditableArea();
return $from.filter(selector).add(dom.cssFind($from, selector));
return $from.filter(selector).add(dom.cssFind($from, selector)).filter(filterFunc);
};
}
return functions;