From 1a84970f1fb01bf68ca63f74bfb6ce645470e3e0 Mon Sep 17 00:00:00 2001 From: "Guillaume (gdi)" Date: Fri, 26 Aug 2022 08:18:53 +0000 Subject: [PATCH] [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 --- .../src/snippets/s_table_of_content/000.js | 8 ++-- .../snippets/s_table_of_content/options.js | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/addons/website/static/src/snippets/s_table_of_content/000.js b/addons/website/static/src/snippets/s_table_of_content/000.js index d02d56c2137..059bc790dd1 100644 --- a/addons/website/static/src/snippets/s_table_of_content/000.js +++ b/addons/website/static/src/snippets/s_table_of_content/000.js @@ -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()); diff --git a/addons/website/static/src/snippets/s_table_of_content/options.js b/addons/website/static/src/snippets/s_table_of_content/options.js index ad4fc279ec5..caa09ff3224 100644 --- a/addons/website/static/src/snippets/s_table_of_content/options.js +++ b/addons/website/static/src/snippets/s_table_of_content/options.js @@ -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); }, });