summaryrefslogtreecommitdiff
path: root/addons/website/static/src/snippets/s_table_of_content
diff options
context:
space:
mode:
authorBenjamin Vray <bvr@odoo.com>2019-12-18 12:11:41 +0000
committerqsm-odoo <qsm@odoo.com>2020-01-30 16:57:43 +0000
commit9fb2dad97cfbd412bee3cb5d1358a9835e721f60 (patch)
treeb1bfc35fc248a574e4fd2d428992a35a4e720a49 /addons/website/static/src/snippets/s_table_of_content
parent48f9060a4627b706dafbe9f06d61ff8dca3d945b (diff)
[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) <qsm@odoo.com>
Diffstat (limited to 'addons/website/static/src/snippets/s_table_of_content')
-rw-r--r--addons/website/static/src/snippets/s_table_of_content/000.js79
-rw-r--r--addons/website/static/src/snippets/s_table_of_content/000.scss64
-rw-r--r--addons/website/static/src/snippets/s_table_of_content/options.js133
3 files changed, 276 insertions, 0 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
new file mode 100644
index 00000000000..ca6460df641
--- /dev/null
+++ b/addons/website/static/src/snippets/s_table_of_content/000.js
@@ -0,0 +1,79 @@
+odoo.define('website.s_table_of_content', function (require) {
+'use strict';
+
+const publicWidget = require('web.public.widget');
+
+const TableOfContent = publicWidget.Widget.extend({
+ selector: 'section .s_table_of_content_navbar_sticky',
+ disabledInEditableMode: false,
+
+ /**
+ * @override
+ */
+ start: function () {
+ const $menuLoading = $('.o_menu_loading');
+ if (!$menuLoading.length) {
+ this._initializeNavbarTopPosition();
+ } else {
+ $(document.body).one('menu_loaded', () => 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();
+ $('<a>').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);
+ },
+});
+});