Skip to content
Snippets Groups Projects
Verified Commit 135d9de8 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: new toc component #189

parent 1e45953b
No related branches found
No related tags found
No related merge requests found
@import "../../style/property.pcss";
@import "../../style/color.pcss";
@import "../../style/border.pcss";
@import "../../style/theme.pcss";
:host {
box-sizing: border-box;
}
.navigation {
box-sizing: border-box;
position: absolute;
top: 0;
display: block;
cursor: pointer;
width: 20px;
transition: top 0.3s ease, visibility 0.1s ease;
& .heading-strip {
display: flex;
height: 0;
background-color: var(--monster-bg-color-primary-3);
margin-bottom: 10px;
&.level-h1 {
height: 6px;
}
&.level-h2 {
height: 4px;
}
&.level-h3 {
height: 2px;
}
&.level-h4 {
height: 1px;
}
&.level-h5 {
height: 1px;
}
&.level-h6 {
height: 1px;
}
}
}
...@@ -25,6 +25,7 @@ import {findTargetElementFromEvent} from "../../dom/events.mjs"; ...@@ -25,6 +25,7 @@ import {findTargetElementFromEvent} from "../../dom/events.mjs";
import {isFunction} from "../../types/is.mjs"; import {isFunction} from "../../types/is.mjs";
import {TableOfContentStyleSheet} from "./stylesheet/table-of-content.mjs"; import {TableOfContentStyleSheet} from "./stylesheet/table-of-content.mjs";
import {fireCustomEvent} from "../../dom/events.mjs"; import {fireCustomEvent} from "../../dom/events.mjs";
import {getWindow} from "../../dom/util.mjs";
export {TableOfContent}; export {TableOfContent};
...@@ -34,6 +35,31 @@ export {TableOfContent}; ...@@ -34,6 +35,31 @@ export {TableOfContent};
*/ */
export const tableOfContentElementSymbol = Symbol("tableOfContentElement"); export const tableOfContentElementSymbol = Symbol("tableOfContentElement");
/**
* @private
* @type {symbol}
*/
export const navigationElementSymbol = Symbol("navigation");
/**
* @private
* @type {symbol}
*/
const windowEventHandlerSymbol = Symbol("windowsEventHandler");
/**
* @private
* @type {symbol}
*/
const scrollableParentSymbol = Symbol("scrollableParent");
/**
* @private
* @type {symbol}
*/
const scrollableEventHandlerSymbol = Symbol("scrollableEventHandler");
/** /**
* A TableOfContent * A TableOfContent
* *
...@@ -77,6 +103,7 @@ class TableOfContent extends CustomElement { ...@@ -77,6 +103,7 @@ class TableOfContent extends CustomElement {
* @property {Object} actions Callbacks * @property {Object} actions Callbacks
* @property {string} actions.click="throw Error" Callback when clicked * @property {string} actions.click="throw Error" Callback when clicked
* @property {Object} features Features * @property {Object} features Features
* @property {number} offset=100 Navigation offset from top
* @property {Object} classes CSS classes * @property {Object} classes CSS classes
* @property {boolean} disabled=false Disabled state * @property {boolean} disabled=false Disabled state
*/ */
...@@ -89,6 +116,7 @@ class TableOfContent extends CustomElement { ...@@ -89,6 +116,7 @@ class TableOfContent extends CustomElement {
classes: {}, classes: {},
disabled: false, disabled: false,
features: {}, features: {},
offset: 100,
actions: { actions: {
click: () => { click: () => {
throw new Error("the click action is not defined"); throw new Error("the click action is not defined");
...@@ -97,6 +125,41 @@ class TableOfContent extends CustomElement { ...@@ -97,6 +125,41 @@ class TableOfContent extends CustomElement {
}); });
} }
/**
* @return {void}
*/
connectedCallback() {
super.connectedCallback();
initNavigation.call(this);
setTimeout(() => {
this[scrollableParentSymbol] = findScrollableParent(this);
if (this[scrollableParentSymbol] === getWindow()) {
this[scrollableParentSymbol].addEventListener('scroll', this[windowEventHandlerSymbol]);
calcAndSetNavigationTopWindowContext.call(this);
} else {
this[scrollableParentSymbol].addEventListener('scroll', this[scrollableEventHandlerSymbol]);
calcAndSetNavigationTopScrollableParentContext.call(this);
}
}, 100);
}
/**
* @return {void}
*/
disconnectedCallback() {
super.disconnectedCallback();
if (this[scrollableParentSymbol] === getWindow()) {
this[scrollableParentSymbol].removeEventListener('scroll', this[windowEventHandlerSymbol]);
} else {
this[scrollableParentSymbol].removeEventListener('scroll', this[scrollableEventHandlerSymbol]);
}
}
/** /**
* @return {string} * @return {string}
*/ */
...@@ -116,42 +179,199 @@ class TableOfContent extends CustomElement { ...@@ -116,42 +179,199 @@ class TableOfContent extends CustomElement {
/** /**
* @private * @private
* @return {initEventHandler}
* @fires monster-table-of-content-clicked
*/ */
function initEventHandler() { function calcAndSetNavigationTopWindowContext() {
const self = this; const thisTop = this.getBoundingClientRect().top;
const element = this[tableOfContentElementSymbol]; const topViewport = window.scrollY;
let top = Math.max(topViewport, thisTop);
const type = "click"; const offset = this.getOption('offset');
if (offset > 0) {
top += offset;
}
this[navigationElementSymbol].style.top = top + "px";
}
element.addEventListener(type, function (event) { /**
const callback = self.getOption("actions.click"); * @private
*/
function calcAndSetNavigationTopScrollableParentContext() {
console.log('calcAndSetNavigationTopScrollableParentContext');
fireCustomEvent(self, "monster-table-of-content-clicked", { // const parentTop = this[scrollableParentSymbol].getBoundingClientRect().top;
element: self, // const parentBottom = this[scrollableParentSymbol].getBoundingClientRect().bottom;
}); //
if (!isFunction(callback)) { const thisRect = this.getBoundingClientRect();
return; const thisTop = thisRect.top;
const thisBottom = thisRect.bottom;
// const thisHeight = thisBottom - thisTop;
const scrollTop = this[scrollableParentSymbol].scrollTop;
// const scrollHeight = this[scrollableParentSymbol].scrollHeight;
// const diff = thisHeight- parentTop-scrollTop
// console.log(parentTop, parentBottom, thisBottom,diff,scrollTop);
//
//
//
//
// // if (thisTop > 0) {
// // this[navigationElementSymbol].style.top = "0px";
// // return;
// // }
//
// //const topViewport = this[scrollableParentSymbol].scrollTop;
//
// //console.log('thisTop', thisTop,topViewport);
//
// let top = parentTop;//- topViewport;
let top = thisTop + scrollTop;
if (thisBottom < top) {
top = thisBottom;
this[navigationElementSymbol].style.visibility = "hidden";
} else {
this[navigationElementSymbol].style.visibility = "visible";
} }
const element = findTargetElementFromEvent( console.log('top', top, "bottom", thisBottom, "scrolltop", scrollTop);
event,
ATTRIBUTE_ROLE, const offset = this.getOption('offset');
"control", if (offset > 0) {
); top += offset;
}
this[navigationElementSymbol].style.top = top + "px";
}
if (!(element instanceof Node && self.hasNode(element))) { /**
* @private
*/
function initNavigation() {
const headings = getHeadings.call(this);
for (const heading of headings) {
const div = document.createElement('div');
div.classList.add('heading-strip');
div.classList.add('level-' + heading.tagName.toLowerCase());
this[navigationElementSymbol].appendChild(div);
}
}
/**
* Recursively creates a nested list (UL) from a list of heading elements.
* @param {HTMLElement[]} nodeList - The list of heading elements.
* @param {number} currentLevel - The current heading level we are processing.
* @returns {{sublist: HTMLUListElement, lastIndex: number}} An object containing the sublist and the index of the last processed element.
*/
function createListFromHeadings(nodeList, currentLevel = 1) {
let ul = document.createElement('ul');
let i = 0;
while (i < nodeList.length) {
const node = nodeList[i];
const level = parseInt(node.tagName.substring(1));
if (level === currentLevel) {
const li = document.createElement('li');
li.textContent = node.textContent;
ul.appendChild(li);
i++;
} else if (level > currentLevel) {
if (ul.lastChild) {
const {sublist, lastIndex} = createListFromHeadings(nodeList.slice(i), level);
ul.lastChild.appendChild(sublist);
i += lastIndex;
} else {
throw new Error("Heading structure error: higher level heading without a preceding lower level heading.");
}
} else {
break;
}
}
return {sublist: ul, lastIndex: i};
}
/**
* @private
* @returns {*[]}
*/
function getHeadings() {
const allHeadings = [];
const slots = this.shadowRoot.querySelectorAll('slot');
slots.forEach(slot => {
const slottedElements = slot.assignedElements();
slottedElements.forEach(element => {
if (element instanceof HTMLHeadingElement) {
allHeadings.push(element);
return; return;
} }
callback.call(self, event); const headings = element.querySelectorAll("h1, h2, h3, h4, h5, h6");
let nodeList = Array.from(headings);
allHeadings.push(...nodeList);
});
}); });
return allHeadings;
}
/**
* @private
* @return {initEventHandler}
*/
function initEventHandler() {
const self = this;
let ticking = false;
this[windowEventHandlerSymbol] = function () {
if (!ticking) {
window.requestAnimationFrame(() => {
calcAndSetNavigationTopWindowContext.call(self);
ticking = false;
});
ticking = true;
}
}
this[scrollableEventHandlerSymbol] = function () {
if (!ticking) {
window.requestAnimationFrame(() => {
calcAndSetNavigationTopScrollableParentContext.call(self);
ticking = false;
});
ticking = true;
}
}
return this; return this;
} }
/**
*
* @param {HTMLElement} element
* @return {HTMLElement|Window}
*/
function findScrollableParent(element) {
let parent = element.parentElement;
while (parent) {
const overflowY = window.getComputedStyle(parent).overflowY;
if (overflowY === 'scroll' || overflowY === 'auto') {
return parent;
}
parent = parent.parentElement;
}
return getWindow();
}
/** /**
* @private * @private
* @return {void} * @return {void}
...@@ -160,6 +380,10 @@ function initControlReferences() { ...@@ -160,6 +380,10 @@ function initControlReferences() {
this[tableOfContentElementSymbol] = this.shadowRoot.querySelector( this[tableOfContentElementSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="control"]`, `[${ATTRIBUTE_ROLE}="control"]`,
); );
this[navigationElementSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="navigation"]`,
);
} }
/** /**
...@@ -170,6 +394,8 @@ function getTemplate() { ...@@ -170,6 +394,8 @@ function getTemplate() {
// language=HTML // language=HTML
return ` return `
<div data-monster-role="control" part="control"> <div data-monster-role="control" part="control">
<div class="navigation" data-monster-role="navigation"></div>
<slot></slot>
</div>`; </div>`;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment