[FIX] website: fix scrollSpy crashes on TOC

The table of content (TOC) has two mechanisms that interest us here:

The first one is scrollSpy (from Bootstrap) which allows (among other
things) to bold the menu elements according to the scroll. The second
one is a mutation observer which is implemented to regenerate the menu
when the content of the TOC changes.

This being said, when we edit the TOC and scroll at the same time, there
is a race condition that makes scrollSpy want to add a class to a menu
element while this menu has just been regenerated by the observer. The
error is only visible since the migration from Bootstrap 4 to Bootstrap
5 because to add the class, Bootstrap 4 did it with the jQuery
`addClass()` function which does not cause an error if the element on
which it is called does not exist. Now, Bootstrap 5 does the same thing
in pure JS with `classList.add()` which causes an error if the element
on which it is called is not defined. This commit fixes this error by
disposing the scrollSpy when the menu is regenerated.

Steps to reproduce the error:
- drop a table of content block
- drag a snippet around and go over the drop zones

=> traceback. It can appear directly or after some tries.

opw-3047375
opw-3035502
task-2948895

X-original-commit: a05f782871c61b2d58ca2fd4277cb7aed65a301d
Part-of: odoo/odoo#107732
This commit is contained in:
Guillaume (gdi)
2022-12-14 18:33:15 +01:00
parent 400fa9e9f4
commit 1a84970f1f
2 changed files with 41 additions and 5 deletions
@@ -25,10 +25,6 @@ const TableOfContent = publicWidget.Widget.extend({
if (indexCallback >= 0) {
extraMenuUpdateCallbacks.splice(indexCallback, 1);
}
const scrollSpyInstance = ScrollSpy.getInstance(this._scrollingElement);
if (scrollSpyInstance) {
scrollSpyInstance.dispose();
}
this.$el.css('top', '');
this.$el.find('.s_table_of_content_navbar').css('top', '');
this._super(...arguments);
@@ -42,6 +38,10 @@ const TableOfContent = publicWidget.Widget.extend({
* @private
*/
_updateTableOfContentNavbarPosition() {
if (!this.$target[0].querySelector('a.table_of_content_link')) {
// Do not start the scrollspy if the TOC is empty.
return;
}
let position = 0;
const $fixedElements = $('.o_top_fixed_element');
_.each($fixedElements, el => position += $(el).outerHeight());
@@ -20,6 +20,14 @@ options.registry.TableOfContent = options.Class.extend({
this.observer.observe(targetNode, config);
return this._super(...arguments);
},
/**
* @override
*/
onRemove() {
this._disposeScrollSpy();
const exception = (tocEl) => tocEl === this.$target[0];
this._activateScrollSpy(exception);
},
/**
* @override
*/
@@ -31,11 +39,38 @@ options.registry.TableOfContent = options.Class.extend({
// Private
//--------------------------------------------------------------------------
/**
* @param {Function} exception
*/
_activateScrollSpy(exception) {
for (const tocEl of this.ownerDocument.querySelectorAll('#wrapwrap .s_table_of_content')) {
if (exception(tocEl)) {
continue;
}
this.trigger_up('widgets_start_request', {
$target: $(tocEl),
editableMode: true,
});
}
},
/**
* @private
*/
_disposeScrollSpy() {
const scrollingEl = $().getScrollingElement(this.ownerDocument)[0];
const scrollSpyInstance =
this.$target[0].ownerDocument.defaultView.ScrollSpy.getInstance(scrollingEl);
if (scrollSpyInstance) {
scrollSpyInstance.dispose();
}
},
/**
* @private
*/
_generateNav: function (ev) {
this.options.wysiwyg && this.options.wysiwyg.odooEditor.unbreakableStepUnactive();
// We dispose the scrollSpy because the navbar will be updated.
this._disposeScrollSpy();
const $nav = this.$target.find('.s_table_of_content_navbar');
const $headings = this.$target.find(this.targetedElements);
$nav.empty();
@@ -49,7 +84,8 @@ options.registry.TableOfContent = options.Class.extend({
$el.attr('id', id);
$el[0].dataset.anchor = 'true';
});
$nav.find('a:first').addClass('active');
const exception = (tocEl) => !tocEl.querySelector('.s_table_of_content_navbar a');
this._activateScrollSpy(exception);
},
});