From 9fb2dad97cfbd412bee3cb5d1358a9835e721f60 Mon Sep 17 00:00:00 2001 From: Benjamin Vray Date: Wed, 18 Dec 2019 12:11:41 +0000 Subject: [PATCH] [IMP] website: add a new snippet table of content New snippet table of content with a sticky navbar with links related to headings in content. Part of https://github.com/odoo/odoo/pull/40690 task-2118980 closes odoo/odoo#40690 Signed-off-by: Quentin Smetz (qsm) --- addons/website/__manifest__.py | 1 + .../snippets_thumbs/s_table_of_content.png | Bin 0 -> 1150 bytes addons/website/static/src/js/content/menu.js | 1 + .../src/snippets/s_table_of_content/000.js | 79 +++++++++++ .../src/snippets/s_table_of_content/000.scss | 64 +++++++++ .../snippets/s_table_of_content/options.js | 133 ++++++++++++++++++ addons/website/views/assets.xml | 1 + .../views/snippets/s_table_of_content.xml | 79 +++++++++++ addons/website/views/snippets/snippets.xml | 1 + 9 files changed, 359 insertions(+) create mode 100644 addons/website/static/src/img/snippets_thumbs/s_table_of_content.png create mode 100644 addons/website/static/src/snippets/s_table_of_content/000.js create mode 100644 addons/website/static/src/snippets/s_table_of_content/000.scss create mode 100644 addons/website/static/src/snippets/s_table_of_content/options.js create mode 100644 addons/website/views/snippets/s_table_of_content.xml diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index b70e143a8cc..de1e12781c9 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -54,6 +54,7 @@ 'views/snippets/s_faq_collapse.xml', 'views/snippets/s_features_grid.xml', 'views/snippets/s_tabs.xml', + 'views/snippets/s_table_of_content.xml', 'views/snippets/s_chart.xml', 'views/snippets/s_parallax.xml', 'views/snippets/s_quotes_carousel.xml', diff --git a/addons/website/static/src/img/snippets_thumbs/s_table_of_content.png b/addons/website/static/src/img/snippets_thumbs/s_table_of_content.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1cf6ae2e52e2e949998be3d948fb41fc2092b2 GIT binary patch literal 1150 zcmV-^1cCdBP)EX>4Tx04R}tkv&MmKpe$iQ>CI62Rn#5WT;LSM2k9V6^c-y)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0HI!Hs@X9PsG4P@ z;xRFkT@?eb=)n*Im_}4$raqTUCg3@~?&0I>U4&4rtTK|Hl- z>74h8L#!w%#OK6g23?T&k?XR{Z=4Gb3p_Jqq*L?6A!4!6#&R38qM;H;iNlJjQNECI zS>e3JSuIyt^Pc>L!JM|T%ypW>h+`2;NJ4~+DoQBBLWEY06cZ`hk9qh99ebbuerT7_i_3Fq^YaK4RCM> zj20++-Q(R|?Y;ebrrF;Q<5Y6gFK|jU00006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru;|m)I0}+CUfw@Wuoc zqt?Mci&`9n5^xb*Dt40K)(i@6f|H|5H{XXZz{SCL;39&fg9Qh1kQ&rB5lU&WR;}s& zO9yl2@=NsljC$N-KHfcdUQ|Zc^*4yy!UF-BhEz9rv(YNzwxRP^nCvs^A08kZUf1dOkQ7FNICKb=ly6G)O2kH=p; zIGinaKYo4w^wHS9+wCqE3mRCRPA8d6+Ju$OX3yud*#v&S{~4!Yu~2#Xsc^eY3EbIDBsUQeg4e@b@LJWsP+iF&$(Qr3bBoZMzI3vJv{m3d53aDAASrlS6 zo2ja5Nmq$PBA3fOGS+Ulv#P-By8hr;<#O4o!#~SUha87Rut=f60}EKdLb##8_j this._initializeNavbarTopPosition()); + } + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this.$target.find('.s_table_of_content_navbar').css('top', ''); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Initialize the top position of the snippet navbar according to the height + * of the headers of the page. + * + * @private + */ + _initializeNavbarTopPosition: function () { + let headerHeight = 0; + const $fixedElements = $('.o_top_fixed_element'); + const isHorizontalNavbar = this.$target.hasClass('s_table_of_content_horizontal_navbar'); + _.each($fixedElements, el => headerHeight += $(el).outerHeight()); + this.$target.css('top', isHorizontalNavbar ? headerHeight : ''); + this.$target.find('.s_table_of_content_navbar').css('top', !isHorizontalNavbar ? headerHeight : ''); + $('body').scrollspy({target: '.s_table_of_content_navbar', offset: headerHeight + 100}); + }, +}); + +publicWidget.registry.anchorSlide.include({ + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Overridden to add the height of the horizontal sticky navbar at the scroll value + * when the link is from the table of content navbar + * + * @override + * @private + */ + _computeHeaderHeight: function () { + let headerHeight = this._super(...arguments); + if (this.$el.hasClass('table_of_content_link')) { + const tableOfContentNavbarEl = this.$el.closest('.s_table_of_content_navbar_sticky.s_table_of_content_horizontal_navbar'); + if (tableOfContentNavbarEl.length > 0) { + headerHeight += $(tableOfContentNavbarEl).outerHeight(); + } + } + return headerHeight; + }, +}); + +publicWidget.registry.snippetTableOfContent = TableOfContent; + +return TableOfContent; +}); diff --git a/addons/website/static/src/snippets/s_table_of_content/000.scss b/addons/website/static/src/snippets/s_table_of_content/000.scss new file mode 100644 index 00000000000..a145dad6e4f --- /dev/null +++ b/addons/website/static/src/snippets/s_table_of_content/000.scss @@ -0,0 +1,64 @@ +.s_table_of_content:not([data-vcss]) { + .s_table_of_content_navbar_wrap { + &.s_table_of_content_navbar_sticky { + &.s_table_of_content_horizontal_navbar, &.s_table_of_content_vertical_navbar .s_table_of_content_navbar { + @include o-position-sticky($top: 0px); + } + } + &:not(.s_table_of_content_navbar_sticky) { + &, .s_table_of_content_navbar { + top: 0px !important; + } + } + &.s_table_of_content_vertical_navbar .s_table_of_content_navbar { + > a.list-group-item-action { + background: none; + color: rgba($body-color, 0.7); + font-weight: $font-weight-normal + 100; + padding-left: 3px; + transition: padding 0.1s; + + &:before { + @include o-position-absolute(10px, auto, 10px, 0); + width: 2px; + content: ""; + } + &:hover { + color: rgba($body-color, 0.9); + } + &:focus { + background: none; + } + &.active { + color: rgba($body-color, 0.9); + background: none; + padding-left: 8px; + + &:before { + background-color: theme-color('primary'); + } + } + } + } + &.s_table_of_content_horizontal_navbar { + z-index: 1; + padding-top: $navbar-padding-y; + padding-bottom: $navbar-padding-y; + margin-bottom: $spacer * 2; + + .s_table_of_content_navbar { + display: inline; + + > a { + &.list-group-item-action { + width: auto; + } + &.list-group-item { + display: inline-block; + margin-bottom: 2px; + } + } + } + } + } +} 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 new file mode 100644 index 00000000000..f33dcbafd47 --- /dev/null +++ b/addons/website/static/src/snippets/s_table_of_content/options.js @@ -0,0 +1,133 @@ +odoo.define('website.s_table_of_content_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.TableOfContent = options.Class.extend({ + /** + * @override + */ + start: function () { + this.targetedElements = 'h1, h2'; + const $headings = this.$target.find(this.targetedElements); + if ($headings.length > 0) { + this.isAnimateScrolling = this.$target.find(this.targetedElements)[0].dataset.anchor === 'true' ? true : false; + this._generateNav(); + } + // Generate the navbar if the content changes + const targetNode = this.$target.find('.s_table_of_content_main')[0]; + const config = {attributes: false, childList: true, subtree: true, characterData: true}; + this.observer = new MutationObserver(() => this._generateNav()); + this.observer.observe(targetNode, config); + return this._super(...arguments); + }, + /** + * @override + */ + onClone: function () { + this._generateNav(); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Animate (or not) scrolling. + * + * @see this.selectClass for parameters + */ + animateScrolling: function (previewMode, widgetValue, params) { + const $headings = this.$target.find(this.targetedElements); + const anchorValue = widgetValue ? 'true' : '0'; + _.each($headings, el => el.dataset.anchor = anchorValue); + this.isAnimateScrolling = !!widgetValue; + }, + /** + * Change the navbar position. + * + * @see this.selectClass for parameters + */ + navbarPosition: function (previewMode, widgetValue, params) { + const $navbar = this.$target.find('.s_table_of_content_navbar_wrap'); + const $mainContent = this.$target.find('.s_table_of_content_main'); + if (widgetValue === 'top' || widgetValue === 'left') { + $navbar.prev().before($navbar); + } + if (widgetValue === 'left' || widgetValue === 'right') { + $navbar.removeClass('s_table_of_content_horizontal_navbar col-lg-12').addClass('s_table_of_content_vertical_navbar col-lg-3'); + $mainContent.removeClass('col-lg-12').addClass('col-lg-9'); + $navbar.find('.s_table_of_content_navbar').removeClass('list-group-horizontal-md'); + } + if (widgetValue === 'right') { + $navbar.next().after($navbar); + } + if (widgetValue === 'top') { + $navbar.removeClass('s_table_of_content_vertical_navbar col-lg-3').addClass('s_table_of_content_horizontal_navbar col-lg-12'); + $navbar.find('.s_table_of_content_navbar').addClass('list-group-horizontal-md'); + $mainContent.removeClass('col-lg-9').addClass('col-lg-12'); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _generateNav: function (ev) { + const $nav = this.$target.find('.s_table_of_content_navbar'); + const $headings = this.$target.find(this.targetedElements); + $nav.empty(); + _.each($headings, el => { + const $el = $(el); + const id = 'table_of_content_heading_' + _.now() + '_' + _.uniqueId(); + $('').attr('href', "#" + id) + .addClass('table_of_content_link list-group-item list-group-item-action py-2 border-0 rounded-0') + .text($el.text()) + .appendTo($nav); + $el.attr('id', id); + $el[0].dataset.anchor = this.isAnimateScrolling === true ? 'true' : '0'; + }); + $nav.find('a:first').addClass('active'); + }, + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'animateScrolling': { + const $headings = this.$target.find(this.targetedElements); + if ($headings.length > 0) { + return $headings[0].dataset.anchor === 'true' ? 'true' : '0'; + } else { + return 'true'; + } + } + case 'navbarPosition': { + const $navbar = this.$target.find('.s_table_of_content_navbar_wrap'); + if ($navbar.hasClass('s_table_of_content_horizontal_navbar')) { + return 'top'; + } else { + const $mainContent = this.$target.find('.s_table_of_content_main'); + return $navbar.prev().is($mainContent) === true ? 'right' : 'left'; + } + } + } + return this._super(...arguments); + }, +}); + +options.registry.TableOfContentMainColumns = options.Class.extend({ + /** + * @override + */ + start: function () { + const leftPanelEl = this.$overlay.data('$optionsSection')[0]; + leftPanelEl.querySelector('.oe_snippet_remove').classList.add('d-none'); // TODO improve the way to do that + leftPanelEl.querySelector('.oe_snippet_clone').classList.add('d-none'); // TODO improve the way to do that + return this._super.apply(this, arguments); + }, +}); +}); diff --git a/addons/website/views/assets.xml b/addons/website/views/assets.xml index 3eaf292837f..56500ba06c5 100644 --- a/addons/website/views/assets.xml +++ b/addons/website/views/assets.xml @@ -118,6 +118,7 @@