1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-31 02:59:43 +02:00

Add table of content

This commit is contained in:
Taly 2022-06-09 14:06:10 +03:00
parent f5d1a73b26
commit 20747407f7
8 changed files with 327 additions and 5 deletions

View file

@ -25,6 +25,7 @@
{% block body %}{% endblock %}
</div>
</div>
<aside class="docs__aside-right" id="layout-right-column"></aside>
</div>
<script src="/dist/main.bundle.js"></script>
{% if config.yandexMetrikaId is not empty %}

View file

@ -1,6 +1,10 @@
{% extends 'layout.twig' %}
{% block body %}
{# Remove after testing #}
<div class="page-intersection-field"></div>
<article class="page" data-module="page">
<header class="page__header">
<a href="/" class="page__header-nav">

View file

@ -16,6 +16,7 @@ import ModuleDispatcher from 'module-dispatcher';
import Writing from './modules/writing';
import Page from './modules/page';
import Extensions from './modules/extensions';
import TableOfContent from "./modules/table-of-content";
/**
* Main app class

View file

@ -6,12 +6,13 @@
* @class Page
* @classdesc Class for page module
*/
export default class Writing {
export default class Page {
/**
* Creates base properties
*/
constructor() {
this.codeStyler = null;
this.tableOfContent = null;
}
/**
@ -21,7 +22,8 @@ export default class Writing {
*/
init(settings = {}, moduleEl) {
this.codeStyler = this.createCodeStyling();
};
this.tableOfContent = this.createTableOfContent();
}
/**
* Init code highlighting
@ -30,7 +32,17 @@ export default class Writing {
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/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();
}
}

View 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');
}
}
}

View 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;
}

View file

@ -31,10 +31,21 @@
max-width: var(--layout-width-main-col);
margin: 0 auto;
}
@media not (--mobile) {
margin-bottom: 80vh;
}
}
&__aside-right {
@media (--mobile) {
display: none;
}
}
&__aside,
&__content {
&__content,
&__aside-right {
padding: var(--layout-padding-vertical) 0;
@media (--mobile) {

View file

@ -9,6 +9,7 @@
@import './components/landing.pcss';
@import './components/auth.pcss';
@import './components/button.pcss';
@import './components/table-of-content.pcss';
body {
font-family: system-ui, Helvetica, Arial, Verdana;