[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:
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user