mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-08-02 03:55:23 +02:00
Add table of content
This commit is contained in:
parent
f5d1a73b26
commit
20747407f7
8 changed files with 327 additions and 5 deletions
|
@ -25,6 +25,7 @@
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<aside class="docs__aside-right" id="layout-right-column"></aside>
|
||||||
</div>
|
</div>
|
||||||
<script src="/dist/main.bundle.js"></script>
|
<script src="/dist/main.bundle.js"></script>
|
||||||
{% if config.yandexMetrikaId is not empty %}
|
{% if config.yandexMetrikaId is not empty %}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
{% extends 'layout.twig' %}
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
{# Remove after testing #}
|
||||||
|
<div class="page-intersection-field"></div>
|
||||||
|
|
||||||
<article class="page" data-module="page">
|
<article class="page" data-module="page">
|
||||||
<header class="page__header">
|
<header class="page__header">
|
||||||
<a href="/" class="page__header-nav">
|
<a href="/" class="page__header-nav">
|
||||||
|
|
|
@ -16,6 +16,7 @@ import ModuleDispatcher from 'module-dispatcher';
|
||||||
import Writing from './modules/writing';
|
import Writing from './modules/writing';
|
||||||
import Page from './modules/page';
|
import Page from './modules/page';
|
||||||
import Extensions from './modules/extensions';
|
import Extensions from './modules/extensions';
|
||||||
|
import TableOfContent from "./modules/table-of-content";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main app class
|
* Main app class
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
* @class Page
|
* @class Page
|
||||||
* @classdesc Class for page module
|
* @classdesc Class for page module
|
||||||
*/
|
*/
|
||||||
export default class Writing {
|
export default class Page {
|
||||||
/**
|
/**
|
||||||
* Creates base properties
|
* Creates base properties
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.codeStyler = null;
|
this.codeStyler = null;
|
||||||
|
this.tableOfContent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,7 +22,8 @@ export default class Writing {
|
||||||
*/
|
*/
|
||||||
init(settings = {}, moduleEl) {
|
init(settings = {}, moduleEl) {
|
||||||
this.codeStyler = this.createCodeStyling();
|
this.codeStyler = this.createCodeStyling();
|
||||||
};
|
this.tableOfContent = this.createTableOfContent();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init code highlighting
|
* Init code highlighting
|
||||||
|
@ -30,7 +32,17 @@ export default class Writing {
|
||||||
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
|
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
|
||||||
|
|
||||||
return new CodeStyler({
|
return new CodeStyler({
|
||||||
selector: '.block-code__content'
|
selector: '.block-code__content',
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init table of content
|
||||||
|
* @return {Promise<TableOfContent>}
|
||||||
|
*/
|
||||||
|
async createTableOfContent() {
|
||||||
|
const { default: TableOfContent } = await import(/* webpackChunkName: "table-of-content" */ './table-of-content');
|
||||||
|
|
||||||
|
return new TableOfContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
216
src/frontend/js/modules/table-of-content.js
Normal file
216
src/frontend/js/modules/table-of-content.js
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
/**
|
||||||
|
* Generate dynamic table of content
|
||||||
|
*/
|
||||||
|
export default class TableOfContent {
|
||||||
|
/**
|
||||||
|
* Initialize table of content
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize table of content
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.findTagsOnThePage();
|
||||||
|
this.createTableOfContent();
|
||||||
|
this.addTableOfContent();
|
||||||
|
this.initIntersectionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all tags on the page
|
||||||
|
*/
|
||||||
|
findTagsOnThePage() {
|
||||||
|
const tags = document.querySelectorAll(`.block-header--anchor`);
|
||||||
|
|
||||||
|
const allowedTags = [
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
];
|
||||||
|
|
||||||
|
this.tags = Array.prototype.filter.call(tags, (tag) => {
|
||||||
|
console.log(tag.tagName.toLowerCase());
|
||||||
|
|
||||||
|
return allowedTags.includes(tag.tagName.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create table of content
|
||||||
|
*
|
||||||
|
* <section>
|
||||||
|
* <header>On this page</header>
|
||||||
|
* <ul>
|
||||||
|
* <li><a href="#"></a></li>
|
||||||
|
* ...
|
||||||
|
* </ul>
|
||||||
|
* </section>
|
||||||
|
*/
|
||||||
|
createTableOfContent() {
|
||||||
|
this.tocElement = document.createElement('section');
|
||||||
|
|
||||||
|
this.tocElement.classList.add('table-of-content__list');
|
||||||
|
|
||||||
|
this.tags.forEach((tag) => {
|
||||||
|
const linkTarget = tag.querySelector('a').getAttribute('href');
|
||||||
|
|
||||||
|
const linkWrapper = document.createElement('li');
|
||||||
|
const linkBlock = document.createElement('a');
|
||||||
|
|
||||||
|
linkBlock.innerText = tag.innerText;
|
||||||
|
linkBlock.href = `${linkTarget}`;
|
||||||
|
|
||||||
|
linkWrapper.classList.add('table-of-content__list-item');
|
||||||
|
|
||||||
|
// additional indent for h3-h6
|
||||||
|
linkWrapper.classList.add(`table-of-content__list-item--${tag.tagName.toLowerCase()}`);
|
||||||
|
|
||||||
|
linkWrapper.appendChild(linkBlock);
|
||||||
|
this.tocElement.appendChild(linkWrapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add table of content to the page
|
||||||
|
*/
|
||||||
|
addTableOfContent() {
|
||||||
|
const header = document.createElement('header');
|
||||||
|
const container = document.createElement('section');
|
||||||
|
|
||||||
|
header.innerText = 'On this page';
|
||||||
|
header.classList.add('table-of-content__header');
|
||||||
|
container.appendChild(header);
|
||||||
|
|
||||||
|
container.classList.add('table-of-content');
|
||||||
|
container.appendChild(this.tocElement);
|
||||||
|
|
||||||
|
document.getElementById('layout-right-column')
|
||||||
|
.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init intersection observer
|
||||||
|
*/
|
||||||
|
initIntersectionObserver() {
|
||||||
|
const options = {
|
||||||
|
rootMargin: '-5% 0 -80%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const callback = (entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const target = entry.target;
|
||||||
|
const targetLink = target.querySelector('a').getAttribute('href');
|
||||||
|
|
||||||
|
// @todo remove after testing
|
||||||
|
document.querySelector('.page-intersection-field').style.top = `${entry.rootBounds.top}px`;
|
||||||
|
document.querySelector('.page-intersection-field').style.height = `${entry.rootBounds.height}px`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersection state of block
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isVisible = entry.isIntersecting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate scroll direction whith the following logic:
|
||||||
|
*
|
||||||
|
* DOWN: if block top is BELOW (coordinate value is greater) the intersection root top
|
||||||
|
* and block is NOT VISIBLE
|
||||||
|
*
|
||||||
|
* DOWN: if block top is ABOVE (coordinate value is less) the intersection root top
|
||||||
|
* and block is VISIBLE
|
||||||
|
*
|
||||||
|
* UP: if block top is ABOVE (coordinate value is less) the intersection root top
|
||||||
|
* and block is visible
|
||||||
|
*
|
||||||
|
* UP: if block top is BELOW (coordinate value is greater) the intersection root top
|
||||||
|
* and block is NOT VISIBLE
|
||||||
|
*
|
||||||
|
* Therefore we can use XOR operator for
|
||||||
|
* - is block's top is above root's top
|
||||||
|
* - is block visible
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const scrollDirection = ((entry.boundingClientRect.top < entry.rootBounds.top) ^ (entry.isIntersecting)) ? 'down' : 'up';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a header becomes VISIBLE on scroll DOWN
|
||||||
|
* then highlight its link
|
||||||
|
*
|
||||||
|
* = moving to the new chapter
|
||||||
|
*/
|
||||||
|
if (isVisible && scrollDirection === 'down') {
|
||||||
|
this.setActiveLink(targetLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a header becomes NOT VISIBLE on scroll UP
|
||||||
|
* then highlight previous link
|
||||||
|
*
|
||||||
|
* = moving to the previous chapter
|
||||||
|
*/
|
||||||
|
if (!isVisible && scrollDirection === 'up') {
|
||||||
|
this.setActiveLink(targetLink, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create intersection observer
|
||||||
|
*/
|
||||||
|
this.observer = new IntersectionObserver(callback, options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add observer to found tags
|
||||||
|
*/
|
||||||
|
this.tags.reverse().forEach((tag) => {
|
||||||
|
this.observer.observe(tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight link's item with a given href
|
||||||
|
*
|
||||||
|
* @param {string} targetLink - href of the link
|
||||||
|
* @param {boolean} [needHighlightPrevious=false] - need to highlight previous link
|
||||||
|
*/
|
||||||
|
setActiveLink(targetLink, needHighlightPrevious = false) {
|
||||||
|
/**
|
||||||
|
* Clear all links
|
||||||
|
*/
|
||||||
|
this.tocElement.querySelectorAll('li').forEach((link) => {
|
||||||
|
link.classList.remove('table-of-content__list-item--active');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looking for a target link
|
||||||
|
*/
|
||||||
|
const targetElement = this.tocElement.querySelector(`a[href="${targetLink}"]`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting link's wrapper
|
||||||
|
*/
|
||||||
|
let listItem = targetElement.parentNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change target list item if it is needed
|
||||||
|
*/
|
||||||
|
if (needHighlightPrevious) {
|
||||||
|
listItem = listItem.previousSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If target list item is found then highlight it
|
||||||
|
*/
|
||||||
|
if (listItem) {
|
||||||
|
listItem.classList.add('table-of-content__list-item--active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
src/frontend/styles/components/table-of-content.pcss
Normal file
76
src/frontend/styles/components/table-of-content.pcss
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
.table-of-content {
|
||||||
|
position: sticky;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
margin-top: 30px;
|
||||||
|
top: 30px;
|
||||||
|
width: 300px;
|
||||||
|
border-left: 1px solid #E8E8EB;
|
||||||
|
padding-left: 22px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 21px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 150%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #F3F6F8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: #F3F6F8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--h3 { margin-left: 6px; }
|
||||||
|
&--h4 { margin-left: 12px; }
|
||||||
|
&--h5 { margin-left: 18px; }
|
||||||
|
&--h6 { margin-left: 24px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-intersection-field {
|
||||||
|
//background-color: rgba(255,0,0,.1);
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3, h4, h5, h6 {
|
||||||
|
//background-color: #699a50;
|
||||||
|
}
|
|
@ -31,10 +31,21 @@
|
||||||
max-width: var(--layout-width-main-col);
|
max-width: var(--layout-width-main-col);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media not (--mobile) {
|
||||||
|
margin-bottom: 80vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__aside-right {
|
||||||
|
@media (--mobile) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__aside,
|
&__aside,
|
||||||
&__content {
|
&__content,
|
||||||
|
&__aside-right {
|
||||||
padding: var(--layout-padding-vertical) 0;
|
padding: var(--layout-padding-vertical) 0;
|
||||||
|
|
||||||
@media (--mobile) {
|
@media (--mobile) {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
@import './components/landing.pcss';
|
@import './components/landing.pcss';
|
||||||
@import './components/auth.pcss';
|
@import './components/auth.pcss';
|
||||||
@import './components/button.pcss';
|
@import './components/button.pcss';
|
||||||
|
@import './components/table-of-content.pcss';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, Helvetica, Arial, Verdana;
|
font-family: system-ui, Helvetica, Arial, Verdana;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue