1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-07 22:45:23 +02:00

Merge branch 'main' into feature/add-sidebar-toggler

This commit is contained in:
YeoKyung Yoon 2022-07-30 22:54:07 +09:00 committed by GitHub
commit b0b42d958c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 879 additions and 1060 deletions

View file

@ -1,2 +1,8 @@
# codex-docs password.
PASSWORD=
PASSWORD=
# Backend errors tracking Integraton Token
HAWK_TOKEN_BACKEND=
# Client errors tracking Integraton Token
HAWK_TOKEN_CLIENT=

View file

@ -17,10 +17,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@ -28,18 +28,18 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=raw,value={{branch}}-{{sha}}-{{date 'X'}}
type=raw,value={{branch}}-{{sha}}-{{date 'X'}},enable=${{ startsWith(github.ref, 'refs/heads') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push image
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
file: docker/Dockerfile.prod

13
nodemon.json Normal file
View file

@ -0,0 +1,13 @@
{
"verbose": true,
"ignore": [
".git",
"node_modules",
"public",
"src/frontend"
],
"watch": [
"**/*"
],
"ext": "js,twig"
}

119
package-lock.json generated
View file

@ -2013,6 +2013,20 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
@ -2023,6 +2037,25 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz",
@ -2031,11 +2064,10 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz",
"integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==",
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@ -6692,10 +6724,9 @@
}
},
"node_modules/got": {
"version": "11.8.3",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz",
"integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==",
"license": "MIT",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
@ -12876,15 +12907,14 @@
}
},
"node_modules/terser": {
"version": "5.12.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz",
"integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
@ -12936,16 +12966,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 8"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -15041,12 +15061,39 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
"integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==",
"dev": true
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz",
@ -15054,9 +15101,9 @@
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz",
"integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==",
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
@ -18443,9 +18490,9 @@
}
},
"got": {
"version": "11.8.3",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz",
"integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==",
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz",
"integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
@ -22755,14 +22802,14 @@
"dev": true
},
"terser": {
"version": "5.12.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz",
"integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
@ -22771,12 +22818,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
}
}
},

View file

@ -17,6 +17,8 @@
"editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest"
},
"dependencies": {
"@hawk.so/javascript": "^3.0.1",
"@hawk.so/nodejs": "^3.1.2",
"config": "^3.3.6",
"cookie-parser": "^1.4.5",
"csurf": "^1.11.0",

View file

@ -1,4 +1,4 @@
import express, { Request, Response } from 'express';
import express, { NextFunction, Request, Response } from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import morgan from 'morgan';
@ -7,18 +7,30 @@ import routes from './routes';
import HttpException from './exceptions/httpException';
import * as dotenv from 'dotenv';
import config from 'config';
import HawkCatcher from '@hawk.so/nodejs';
import os from 'os';
import appConfig from 'config';
import { downloadFavicon, FaviconData } from './utils/downloadFavicon';
dotenv.config();
const app = express();
const localConfig = rcParser.getConfiguration();
// Initialize the backend error tracking catcher.
if (process.env.HAWK_TOKEN_BACKEND) {
HawkCatcher.init(process.env.HAWK_TOKEN_BACKEND);
}
// Get url to upload favicon from config
const favicon: string = appConfig.get('favicon');
app.locals.config = localConfig;
// Set client error tracking token as app local.
if (process.env.HAWK_TOKEN_CLIENT) {
app.locals.config.hawkClientToken = process.env.HAWK_TOKEN_CLIENT;
}
// view engine setup
app.set('views', path.join(__dirname, './', 'views'));
app.set('view engine', 'twig');
@ -55,15 +67,24 @@ app.use('/favicon', express.static(downloadedFaviconFolder));
app.use('/', routes);
// error handler
app.use(function (err: HttpException, req: Request, res: Response) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
// global error handler
app.use(function (err: unknown, req: Request, res: Response, next: NextFunction) {
// send any type of error to hawk server.
if (process.env.HAWK_TOKEN_BACKEND && err instanceof Error) {
HawkCatcher.send(err);
}
// only send Http based exception to client.
if (err instanceof HttpException) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
}
next(err);
});
export default app;

View file

@ -11,7 +11,8 @@
</head>
<script>
window.config = {
misprintsChatId: "{{ config.misprintsChatId }}"
misprintsChatId: "{{ config.misprintsChatId }}",
hawkClientToken:"{{ config.hawkClientToken }}",
};
</script>
<body>
@ -26,9 +27,7 @@
{% block body %}{% endblock %}
</div>
<aside class="docs__aside-right">
<div style="width: 100%; height: 100px; background: grey"/>
</aside>
<aside class="docs__aside-right" id="layout-sidebar-right"></aside>
</div>
</div>
<script src="/dist/main.bundle.js"></script>

View file

@ -1,5 +1,4 @@
<a name="{{ text | urlify }}" style="display: inline-block; position: absolute; margin-top: -20px;"></a>
<h{{ level }} class="block-header block-header--{{ level }} block-header--anchor">
<h{{ level }} id="{{ text | urlify }}" class="block-header block-header--{{ level }} block-header--anchor">
<a href="#{{ text | urlify }}">
{{ text }}
</a>

13
src/frontend/.eslintrc Normal file
View file

@ -0,0 +1,13 @@
{
"extends": [
"codex"
],
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": true
},
"globals": {
"HTMLElement": true
}
}

View file

@ -17,6 +17,7 @@ import Writing from './modules/writing';
import Page from './modules/page';
import Extensions from './modules/extensions';
import Sidebar from './modules/sidebar';
import HawkCatcher from '@hawk.so/javascript';
/**
* Main app class
@ -30,6 +31,9 @@ class Docs {
this.page = new Page();
this.extensions = new Extensions();
this.sidebar = new Sidebar();
if (window.config.hawkClientToken) {
this.hawk = new HawkCatcher(window.config.hawkClientToken);
}
document.addEventListener('DOMContentLoaded', (event) => {
this.docReady();

View file

@ -0,0 +1,405 @@
import * as Decorators from '../utils/decorators';
import * as $ from '../utils/dom';
/**
* Generate dynamic table of content
*/
export default class TableOfContent {
/**
* Initialize table of content
*
* @param {object} options - constructor params
* @param {string} options.tagSelector - selector for tags to observe
* @param {HTMLElement} options.appendTo - element for appending of the table of content
*/
constructor({ tagSelector, appendTo }) {
/**
* Array of tags to observe
*/
this.tags = [];
this.tagsSectionsMap = [];
/**
* Selector for tags to observe
*/
this.tagSelector = tagSelector || 'h2,h3,h4';
/**
* Element to append the Table of Content
*/
this.tocParentElement = appendTo;
if (!this.tocParentElement) {
throw new Error('Table of Content wrapper not found');
}
this.nodes = {
/**
* Main Table of Content element
*/
wrapper: null,
/**
* List of Table of Content links
*/
items: [],
};
/**
* Currently highlighted element of ToC
*/
this.activeItem = null;
this.CSS = {
tocContainer: 'table-of-content',
tocHeader: 'table-of-content__header',
tocElement: 'table-of-content__list',
tocElementItem: 'table-of-content__list-item',
tocElementItemActive: 'table-of-content__list-item--active',
tocElementItemIndent: number => `table-of-content__list-item--indent-${number}x`,
};
this.init();
}
/**
* Initialize table of content
*/
init() {
this.tags = this.getSectionTagsOnThePage();
/**
* Check if no tags found then table of content is not needed
*/
if (this.tags.length === 0) {
return;
}
/**
* Initialize table of content element
*/
this.createTableOfContent();
this.addTableOfContent();
/**
* Calculate bounds for each tag and watch active section
*/
this.calculateBounds();
this.watchActiveSection();
}
/**
* Find all section tags on the page
*
* @returns {HTMLElement[]}
*/
getSectionTagsOnThePage() {
return Array.from(document.querySelectorAll(this.tagSelector));
}
/**
* Calculate top line position for each tag
*/
calculateBounds() {
this.tagsSectionsMap = this.tags.map((tag) => {
const rect = tag.getBoundingClientRect();
const top = Math.floor(rect.top + window.scrollY);
return {
top,
tag,
};
});
}
/**
* Watch active section while scrolling
*/
watchActiveSection() {
/**
* Where content zone starts in document
*/
const contentTopOffset = this.getScrollPadding();
/**
* Additional offset for correct calculation of active section
*
* Cause opening page with anchor could scroll almost
* near to the target section and we need to add 1px to calculations
*/
const activationOffset = 1;
const detectSection = () => {
/**
* Calculate scroll position
*
* @todo research how not to use magic number
*/
const scrollPosition = contentTopOffset + window.scrollY + activationOffset;
/**
* Find the nearest section above the scroll position
*/
const section = this.tagsSectionsMap.filter((tag) => {
return tag.top <= scrollPosition;
}).pop();
/**
* If section found then set it as active
*/
if (section) {
const targetLink = section.tag.querySelector('a').getAttribute('href');
this.setActiveItem(targetLink);
} else {
/**
* Otherwise no active link will be highlighted
*/
this.setActiveItem(null);
}
};
/**
* Define a flag to reduce number of calls to detectSection function
*/
const throttledDetectSectionFunction = Decorators.throttle(() => {
detectSection();
}, 400);
/**
* Scroll listener
*/
document.addEventListener('scroll', throttledDetectSectionFunction, {
passive: true,
});
}
/**
* Create table of content
*
* <section>
* <header>On this page</header>
* <ul>
* <li><a href="#"></a></li>
* ...
* </ul>
* </section>
*/
createTableOfContent() {
this.tocElement = $.make('section', this.CSS.tocElement);
this.tags.forEach((tag) => {
const linkTarget = tag.querySelector('a').getAttribute('href');
const linkWrapper = $.make('li', this.CSS.tocElementItem);
const linkBlock = $.make('a', null, {
innerText: tag.innerText,
href: `${linkTarget}`,
});
/**
* Additional indent for h3-h6 headers
*/
switch (tag.tagName.toLowerCase()) {
case 'h3':
linkWrapper.classList.add(this.CSS.tocElementItemIndent(1));
break;
case 'h4':
linkWrapper.classList.add(this.CSS.tocElementItemIndent(2));
break;
case 'h5':
linkWrapper.classList.add(this.CSS.tocElementItemIndent(3));
break;
case 'h6':
linkWrapper.classList.add(this.CSS.tocElementItemIndent(4));
break;
}
linkWrapper.appendChild(linkBlock);
this.tocElement.appendChild(linkWrapper);
this.nodes.items.push(linkWrapper);
});
}
/**
* Add table of content to the page
*/
addTableOfContent() {
this.nodes.wrapper = $.make('section', this.CSS.tocContainer);
const header = $.make('header', this.CSS.tocHeader, {
textContent: 'On this page',
});
this.nodes.wrapper.appendChild(header);
this.nodes.wrapper.appendChild(this.tocElement);
this.tocParentElement.appendChild(this.nodes.wrapper);
}
/**
* Highlight link's item with a given href
*
* @param {string|null} targetLink - href of the link. Null if we need to clear all highlights
*/
setActiveItem(targetLink) {
/**
* Clear current highlight
*/
if (this.activeItem) {
this.activeItem.classList.remove(this.CSS.tocElementItemActive);
}
/**
* If targetLink is null, that means we reached top, nothing to highlight
*/
if (targetLink === null) {
this.activeItem = null;
return;
}
/**
* Looking for a target link
*
* @todo do not fire DOM search, use saved map instead
*/
const targetElement = this.tocElement.querySelector(`a[href="${targetLink}"]`);
/**
* Getting link's wrapper
*/
const listItem = targetElement.parentNode;
/**
* Highlight and save current item
*/
listItem.classList.add(this.CSS.tocElementItemActive);
this.activeItem = listItem;
/**
* If need, scroll parent to active item
*/
this.scrollToActiveItemIfNeeded();
}
/**
* Document scroll ending callback
*
* @returns {void}
*/
scrollToActiveItemIfNeeded() {
/**
* Do nothing if the Table of Content has no internal scroll at this page
*
* @todo compute it once
*/
const hasScroll = this.nodes.wrapper.scrollHeight > this.nodes.wrapper.clientHeight;
if (!hasScroll) {
return;
}
/**
* If some item is highlighted, check whether we need to scroll to it or not
*/
if (this.activeItem) {
/**
* First, check do we need to scroll to item?
* We need to scroll in case when:
* item bottom coord is bigger than parent height + current parent scroll
*
* @todo use memoization for calculating of the itemBottomCoordWithPadding
*/
const itemOffsetTop = this.activeItem.offsetTop;
const itemHeight = this.activeItem.offsetHeight;
const itemBottomCoord = itemOffsetTop + itemHeight;
const scrollPadding = 10; // scroll offset below/above item
const itemBottomCoordWithPadding = itemBottomCoord + scrollPadding;
const itemTopCoordWithPadding = itemOffsetTop - scrollPadding;
const scrollableParentHeight = this.nodes.wrapper.offsetHeight; // @todo compute it once
const scrollableParentScrolledDistance = this.nodes.wrapper.scrollTop;
/**
* Scroll bottom required if item ends below the parent bottom boundary
*/
const isScrollDownRequired = itemBottomCoordWithPadding > scrollableParentHeight + scrollableParentScrolledDistance;
/**
* Scroll upward required when item starts above the visible parent zone
*/
const isScrollUpRequired = itemTopCoordWithPadding < scrollableParentScrolledDistance;
/**
* If item is fully visible, scroll is not required
*/
const isScrollRequired = isScrollDownRequired || isScrollUpRequired;
if (isScrollRequired === false) {
/**
* Item is visible, scroll is not needed
*/
return;
}
/**
* Now compute the scroll distance to make item visible
*/
let distanceToMakeItemFullyVisible;
if (isScrollDownRequired) {
distanceToMakeItemFullyVisible = itemBottomCoordWithPadding - scrollableParentHeight;
} else { // scrollUpRequired=true
distanceToMakeItemFullyVisible = itemTopCoordWithPadding;
}
/**
* Change the scroll
* Using RAF to prevent overloading of regular scroll animation FPS
*/
window.requestAnimationFrame(() => {
this.nodes.wrapper.scrollTop = distanceToMakeItemFullyVisible;
});
}
}
/**
* Get scroll padding top value from HTML element
*
* @returns {number}
*/
getScrollPadding() {
const defaultScrollPaddingValue = 0;
/**
* Try to get calculated value or fallback to default value
*/
try {
/**
* Getting the HTML element
*/
const htmlElement = document.documentElement;
/**
* Getting css scroll padding value
*/
const scrollPaddingTopValue = window.getComputedStyle(htmlElement)
.getPropertyValue('scroll-padding-top');
/**
* Parse value to number
*/
return parseInt(scrollPaddingTopValue, 10);
} catch (e) {}
/**
* On any errors return default value
*/
return defaultScrollPaddingValue;
}
}

View file

@ -1,27 +1,23 @@
/**
* @typedef {object} pageModuleSettings
*/
/**
* @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;
}
/**
* Called by ModuleDispatcher to initialize module from DOM
* @param {pageModuleSettings} settings - module settings
* @param {HTMLElement} moduleEl - module element
*/
init(settings = {}, moduleEl) {
init() {
this.codeStyler = this.createCodeStyling();
};
this.tableOfContent = this.createTableOfContent();
}
/**
* Init code highlighting
@ -29,8 +25,35 @@ export default class Writing {
async createCodeStyling() {
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
return new CodeStyler({
selector: '.block-code__content'
});
};
try {
// eslint-disable-next-line no-new
new CodeStyler({
selector: '.block-code__content',
});
} catch (error) {
console.error(error); // @todo send to Hawk
}
}
/**
* Init table of content
*
* @returns {Promise<TableOfContent>}
*/
async createTableOfContent() {
const { default: TableOfContent } = await import(/* webpackChunkName: "table-of-content" */ '../classes/table-of-content');
try {
// eslint-disable-next-line no-new
new TableOfContent({
tagSelector:
'h2.block-header--anchor,' +
'h3.block-header--anchor,' +
'h4.block-header--anchor',
appendTo: document.getElementById('layout-sidebar-right'),
});
} catch (error) {
console.error(error); // @todo send to Hawk
}
}
}

View file

@ -0,0 +1,62 @@
/**
* A few useful utility functions
*/
/**
* Throttle decorator function
*
* @param {Function} func - function to throttle
* @param {number} ms - milliseconds to throttle
*
* @returns {Function}
*/
export function throttle(func, ms) {
let isThrottled = false,
savedArgs,
savedThis;
// eslint-disable-next-line jsdoc/require-jsdoc
function wrapper() {
if (isThrottled) {
savedArgs = arguments;
savedThis = this;
return;
}
func.apply(this, arguments);
isThrottled = true;
setTimeout(function () {
isThrottled = false;
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
/**
* Debounce decorator function
*
* @param {Function} f - function to debounce
* @param {number} ms - milliseconds to debounce
*
* @returns {(function(): void)|*}
*/
export function debounce(f, ms) {
let timeoutId = null;
return function () {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => f.apply(this, arguments), ms);
};
}

View file

@ -0,0 +1,23 @@
/**
* Helper method for elements creation
*
* @param {string} tagName - name of tag to create
* @param {string | string[]} classNames - list of CSS classes
* @param {object} attributes - any properties to add
* @returns {HTMLElement}
*/
export function make(tagName, classNames = null, attributes = {}) {
const el = document.createElement(tagName);
if (Array.isArray(classNames)) {
el.classList.add(...classNames);
} else if (classNames) {
el.classList.add(classNames);
}
for (const attrName in attributes) {
el[attrName] = attributes[attrName];
}
return el;
}

View file

@ -1,4 +1,3 @@
/**
* Utility class to handle interaction with local storage
*/
@ -27,4 +26,4 @@ export class Storage {
get() {
return localStorage.getItem(this.key);
}
}
}

View file

@ -1,3 +1,7 @@
html {
scroll-padding-top: calc(var(--layout-height-header) + 50px);
}
.docs-header {
display: flex;
justify-content: space-between;
@ -6,7 +10,6 @@
border-bottom: 1px solid var(--color-line-gray);
font-size: 18px;
flex-wrap: wrap;
position: relative;
height: var(--layout-height-header);
box-sizing: border-box;
position: sticky;

View file

@ -0,0 +1,58 @@
.table-of-content {
border-left: 1px solid var(--color-line-gray);
padding-left: var(--layout-padding-horizontal);
height: 100%;
overflow: auto;
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
box-sizing: border-box;
&__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 {
@apply --squircle;
&:hover {
background-color: var(--color-link-hover);
cursor: pointer;
}
&--active {
background-color: var(--color-link-hover);
}
&--indent-1x { margin-left: 6px; }
&--indent-2x { margin-left: 12px; }
&--indent-3x { margin-left: 18px; }
&--indent-4x { margin-left: 24px; }
& > a {
padding: 4px 8px;
display: block;
font-size: 14px;
letter-spacing: -0.01em;
line-height: 150%;
}
}
}
}

View file

@ -20,15 +20,15 @@
margin: 0 auto;
@media (--desktop) {
margin-right: var(--layout-padding-horizontal);
margin-left: 0;
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
}
}
}
&__content {
--max-space-between-cols: 160px;
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
display: flex;
justify-content: space-between;
max-width: calc(var(--layout-width-main-col) + var(--max-space-between-cols) + var(--layout-sidebar-width));
@ -46,7 +46,10 @@
display: none;
position: sticky;
top: calc(var(--layout-height-header) + var(--layout-padding-vertical));
overflow: auto;
height: calc(100vh - var(--layout-height-header));
top: calc(var(--layout-height-header));
align-self: flex-start;
@media (--desktop) {

View file

@ -1,4 +1,5 @@
@import 'normalize.css';
@import './vars.pcss';
@import './layout.pcss';
@import './carbon.pcss';
@ -9,6 +10,7 @@
@import './components/auth.pcss';
@import './components/button.pcss';
@import './components/sidebar.pcss';
@import './components/table-of-content.pcss';
body {
font-family: system-ui, Helvetica, Arial, Verdana;

1105
yarn.lock

File diff suppressed because it is too large Load diff