[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>
This commit is contained in:
Benjamin Vray
2020-01-30 16:57:43 +00:00
committed by qsm-odoo
parent 48f9060a46
commit 9fb2dad97c
9 changed files with 359 additions and 0 deletions
+1
View File
@@ -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',
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -142,6 +142,7 @@ publicWidget.registry.autohideMenu = publicWidget.Widget.extend({
dom.initAutoMoreMenu(self.$el, {unfoldable: '.divider, .divider ~ li'});
}
self.$el.removeClass('o_menu_loading');
self.$el.trigger('menu_loaded');
});
},
/**
@@ -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;
});
@@ -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;
}
}
}
}
}
}
@@ -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);
},
});
});
+1
View File
@@ -118,6 +118,7 @@
<script type="text/javascript" src="/website/static/src/snippets/s_tabs/options.js"/>
<script type="text/javascript" src="/website/static/src/snippets/s_progress_bar/options.js"/>
<script type="text/javascript" src="/website/static/src/snippets/s_blockquote/options.js"/>
<script type="text/javascript" src="/website/static/src/snippets/s_table_of_content/options.js"/>
<script type="text/javascript" src="/website/static/src/js/editor/wysiwyg_multizone.js"/>
<script type="text/javascript" src="/website/static/src/js/editor/wysiwyg_multizone_translate.js"/>
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="s_table_of_content" name="Table of Content">
<section class="s_table_of_content pt24 pb24">
<div class="container">
<div class="row s_nb_column_fixed">
<div class="col-lg-3 s_table_of_content_navbar_wrap s_table_of_content_navbar_sticky
s_table_of_content_vertical_navbar d-print-none d-none d-lg-block o_not_editable bg-white"
data-name="Navbar">
<div class="s_table_of_content_navbar list-group"/>
</div>
<div class="col-lg-9 s_table_of_content_main oe_structure oe_empty" data-name="Content">
<section class="pb16">
<h1 class="text-secondary" data-anchor="true">Intuitive system</h1>
<h4>What you see is what you get</h4>
<p>
Insert text styles like headers, bold, italic, lists and fonts with
a simple WYSIWYG editor. Flexible and easy to use.
</p>
<h4>Customization tool</h4>
<p>
Click and change content directly from the front-end: no complex back
end to deal with.
</p>
<h4>Building blocks system</h4>
<p>
Create your page from scratch by dragging and dropping pre-made,
fully customizable building blocks.
</p>
</section>
<section class="pb16">
<h1 class="text-secondary" data-anchor="true">Design features</h1>
<h4>Bootstrap based templates</h4>
<p>
Easily design your own Odoo templates thanks to clean HTML
structure and bootstrap CSS.
</p>
<h4>Professional themes</h4>
<p>
Change theme in a few clicks, and browse through Odoo's catalog of
ready-to-use themes available in our app store.
</p>
</section>
</div>
</div>
</div>
</section>
</template>
<template id="s_table_of_content_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="TableOfContent" data-selector=".s_table_of_content">
<we-checkbox string="Animate Scrolling" data-animate-scrolling="true" data-no-preview="true"/>
<we-select string="Navbar position">
<we-button data-navbar-position="left">Left</we-button>
<we-button data-navbar-position="right">Right</we-button>
<we-button data-navbar-position="top">Top</we-button>
</we-select>
<we-checkbox string="Sticky" data-select-class="s_table_of_content_navbar_sticky"
data-apply-to=".s_table_of_content_navbar_wrap" data-no-preview="true"/>
</div>
<div data-js="TableOfContentMainColumns" data-selector=".s_table_of_content_navbar_wrap, .s_table_of_content_main"/>
</xpath>
</template>
<template id="assets_snippet_s_table_of_content_css_000" inherit_id="website.assets_frontend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website/static/src/snippets/s_table_of_content/000.scss"/>
</xpath>
</template>
<template id="assets_snippet_s_table_of_content_js_000" inherit_id="website.assets_frontend">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website/static/src/snippets/s_table_of_content/000.js"/>
</xpath>
</template>
</odoo>
@@ -51,6 +51,7 @@
<t t-snippet="website.s_references" t-thumbnail="/website/static/src/img/snippets_thumbs/s_references.png"/>
<t t-snippet="website.s_faq_collapse" t-thumbnail="/website/static/src/img/snippets_thumbs/s_faq_collapse.png"/>
<t t-snippet="website.s_features_grid" t-thumbnail="/website/static/src/img/snippets_thumbs/s_features_grid.png"/>
<t t-snippet="website.s_table_of_content" t-thumbnail="/website/static/src/img/snippets_thumbs/s_table_of_content.png"/>
<t t-snippet="website.s_product_catalog" t-thumbnail="/website/static/src/img/snippets_thumbs/s_product_catalog.png"/>
<t t-snippet="website.s_product_list" t-thumbnail="/website/static/src/img/snippets_thumbs/s_product_list.png"/>
<t t-snippet="website.s_tabs" t-thumbnail="/website/static/src/img/snippets_thumbs/s_tabs.png"/>