toc-generate
Advanced tools
| type propsTocGenerate = { | ||
| contentWrapperSelector?: string; | ||
| tocSelector?: string | undefined; | ||
| headingLevelFrom?: number; | ||
| headingLevelTo?: number; | ||
| showsHighLight?: boolean; | ||
| showsParentHighlight?: boolean; | ||
| viewablePercentToHighlight?: number; | ||
| }; | ||
| declare function tocGenerate(props: propsTocGenerate): HTMLOListElement; | ||
| export default tocGenerate; |
| export { default, default as tocGenerate } from './dist/toc-generate.module'; |
| function createTocElement(selector, heading) { | ||
| const li = document.createElement("li"); | ||
| const a = document.createElement("a"); | ||
| heading.id ||= `${heading.textContent | ||
| .replace(/\s/g, "-") | ||
| .toLowerCase()}-${Math.random().toString(36).substring(2, 15)}`; | ||
| a.textContent = heading.textContent; | ||
| a.href = `#${heading.id}`; | ||
| // Add the link to the li item | ||
| li.appendChild(a); | ||
| selector.appendChild(li); | ||
| return li; | ||
| } | ||
| export default createTocElement; |
| function findParentScope(tocTree, level) { | ||
| for (let i = tocTree.length - 1; i >= 0; i--) { | ||
| if (tocTree[i].level === level) { | ||
| return tocTree[i].scope; | ||
| } | ||
| } | ||
| return toc; | ||
| } | ||
| export default findParentScope; |
| function queryAllHeadings(headingLevelFrom, headingLevelTo, scopeElement) { | ||
| let headingSelector = ""; | ||
| for (let i = headingLevelFrom; i <= headingLevelTo; ++i) { | ||
| headingSelector += `h${i},`; | ||
| } | ||
| return scopeElement.querySelectorAll(headingSelector.slice(0, -1)); | ||
| } | ||
| export default queryAllHeadings; |
| import setClass from "./setClass"; | ||
| function scrollHighLight(headings, tocTree, viewablePercentToHighlight, showsParentHighlight) { | ||
| for (let i = 0; i < headings.length; i++) { | ||
| const heading = headings[i]; | ||
| const safeNextIndex = (i + 1 < headings.length) ? i + 1 : i; // If the next heading is the last one, use the current heading | ||
| const safePrevIndex = (i - 1 >= 0) ? i - 1 : 1; // First heading not have parent | ||
| const nextHeading = headings[safeNextIndex]; | ||
| const parentToc = tocTree[i].parent; | ||
| const elementToc = tocTree[i].selector; | ||
| const prevElementToc = tocTree[safePrevIndex]; | ||
| function highlight() { | ||
| let { bottom, top } = heading.getBoundingClientRect(); | ||
| const scope = nextHeading.getBoundingClientRect().top - bottom; | ||
| const scopeSplit = (scope * viewablePercentToHighlight) / 100; | ||
| const scopeCanView = scope - scopeSplit; | ||
| const windowHeight = window.innerHeight; | ||
| if (bottom + scopeCanView <= windowHeight && top + scopeSplit >= 0) { | ||
| setClass("highlight",'add', [heading, elementToc]); | ||
| if (showsParentHighlight && parentToc) { | ||
| setClass("parent_highlight", 'add', [parentToc]); | ||
| } | ||
| } else if (top + scopeSplit <= 0 || bottom > windowHeight) { | ||
| setClass("highlight", 'remove', [heading, elementToc]); | ||
| if (showsParentHighlight && prevElementToc.parent) { | ||
| setClass("parent_highlight", 'remove', [heading, elementToc, prevElementToc.selector]); | ||
| } | ||
| } | ||
| } | ||
| // Add event listener to scroll | ||
| highlight(); // Run on load | ||
| document.addEventListener("scroll", highlight); | ||
| } | ||
| } | ||
| export default scrollHighLight; |
| /** | ||
| * Set the class of the elements | ||
| * | ||
| * @param {string} className - The class name to add or remove | ||
| * @param {add|remove} type - The type of action to perform | ||
| * @param {...any} elements - The elements to add or remove the class from | ||
| */ | ||
| function setClass(className = "highlight", type = "add", elements) { | ||
| elements.forEach((element) => { | ||
| if (element !== undefined) { | ||
| element.classList[type](className); | ||
| } | ||
| }); | ||
| } | ||
| export default setClass; |
| import queryAllHeadings from "./queryAllHeadings"; | ||
| import findParentScope from "./findParentScope"; | ||
| import createTocElement from "./createTocElement"; | ||
| import scrollHighLight from "./scrollHighLight"; | ||
| function tocGenerate(props) { | ||
| const contentWrapperSelector = props.contentWrapperSelector || "body"; | ||
| const tocSelector = props.tocSelector; | ||
| const headingLevelFrom = props.headingLevelFrom || 2; | ||
| const headingLevelTo = props.headingLevelTo || 6; | ||
| const viewablePercentToHighlight = props.viewablePercentToHighlight || 70; | ||
| const showsHighLight = props.showsHighLight || false; | ||
| const showsParentHighlight = props.showsParentHighlight || false; | ||
| const tocElement = tocSelector && document.querySelector(tocSelector); | ||
| const scopeElement = document.querySelector(contentWrapperSelector); | ||
| // Get all h-from - h-to elements | ||
| const headings = queryAllHeadings(headingLevelFrom, headingLevelTo, scopeElement); | ||
| // Create a list to hold the headings | ||
| const toc = document.createElement("ol"); | ||
| const tocTree = [{ level: headingLevelFrom, scope: toc }]; | ||
| headings.forEach((heading) => { | ||
| const headingLevel = Number(heading.tagName.slice(1)); | ||
| const prevToc = tocTree[tocTree.length - 1]; | ||
| if (prevToc.level < headingLevel) { | ||
| const ol = document.createElement("ol"); | ||
| const a = createTocElement(ol, heading); | ||
| prevToc.scope.appendChild(ol); | ||
| tocTree.push({ level: headingLevel, scope: ol, selector: a, parent: tocTree.at(-1).selector }); | ||
| } else if (prevToc.level > headingLevel) { | ||
| const parentToc = findParentScope(tocTree, headingLevel); | ||
| const a = createTocElement(parentToc, heading); | ||
| tocTree.push({ level: headingLevel, scope: parentToc, selector: a }); | ||
| } else { | ||
| const a = createTocElement(prevToc.scope, heading); | ||
| tocTree.push({ level: headingLevel, scope: prevToc.scope, selector: a, ...(prevToc.parent && { parent: prevToc.parent })}); | ||
| } | ||
| }); | ||
| tocTree.shift(); | ||
| // Append the list to the toc element | ||
| if (tocElement) { | ||
| tocElement.appendChild(toc); | ||
| } | ||
| if (showsHighLight) { | ||
| scrollHighLight(headings, tocTree, viewablePercentToHighlight, showsParentHighlight); | ||
| } | ||
| return toc; | ||
| } | ||
| export default tocGenerate; |
@@ -0,1 +1,2 @@ | ||
| /** @typedef { import('./toc-generate.d')} */ | ||
| function e(e,t){for(let n=e.length-1;n>=0;n--)if(e[n].level===t)return e[n].scope;return toc}function t(e,t){const n=document.createElement("li"),o=document.createElement("a");return t.id||=`${t.textContent.replace(/\s/g,"-").toLowerCase()}-${Math.random().toString(36).substring(2,15)}`,o.textContent=t.textContent,o.href=`#${t.id}`,n.appendChild(o),e.appendChild(n),n}function n(e="highlight",t="add",n){n.forEach((n=>{void 0!==n&&n.classList[t](e)}))}function o(o){const l=o.contentWrapperSelector||"body",r=o.tocSelector,c=o.headingLevelFrom||2,i=o.headingLevelTo||6,s=o.viewablePercentToHighlight||70,h=o.showsHighLight||!1,a=o.showsParentHighlight||!1,d=r&&document.querySelector(r),p=function(e,t,n){let o="";for(let n=e;n<=t;++n)o+=`h${n},`;return n.querySelectorAll(o.slice(0,-1))}(c,i,document.querySelector(l)),g=document.createElement("ol"),u=[{level:c,scope:g}];return p.forEach((n=>{const o=Number(n.tagName.slice(1)),l=u[u.length-1];if(l.level<o){const e=document.createElement("ol"),r=t(e,n);l.scope.appendChild(e),u.push({level:o,scope:e,selector:r,parent:u.at(-1).selector})}else if(l.level>o){const l=e(u,o),r=t(l,n);u.push({level:o,scope:l,selector:r})}else{const e=t(l.scope,n);u.push({level:o,scope:l.scope,selector:e,...l.parent&&{parent:l.parent}})}})),u.shift(),d&&d.appendChild(g),h&&function(e,t,o,l){for(let c=0;c<e.length;c++){const i=e[c],s=c-1>=0?c-1:1,h=e[c+1<e.length?c+1:c],a=t[c].parent,d=t[c].selector,p=t[s];function r(){let{bottom:e,top:t}=i.getBoundingClientRect();const r=h.getBoundingClientRect().top-e,c=r*o/100,s=r-c,g=window.innerHeight;e+s<=g&&t+c>=0?(n("highlight","add",[i,d]),l&&a&&n("parent_highlight","add",[a])):(t+c<=0||e>g)&&(n("highlight","remove",[i,d]),l&&p.parent&&n("parent_highlight","remove",[i,d,p.selector]))}r(),document.addEventListener("scroll",r)}}(p,u,s,a),g}export{o as default}; |
+1
-1
@@ -9,3 +9,3 @@ { | ||
| }, | ||
| "version": "1.0.1", | ||
| "version": "1.0.2", | ||
| "main": "index.js", | ||
@@ -12,0 +12,0 @@ "scripts": { |
+7
-0
| # TOC Generate | ||
| | Create a table of contents for a HTML, ESModule | ||
| [](https://www.npmjs.com/package/toc-generate) | ||
| ### Demo | ||
@@ -35,2 +37,7 @@ [Demo Link](https://dunggramer.github.io/toc-generate/public/) | ||
| #### ESModule | ||
| ```bash | ||
| npm i toc-generate | ||
| # or | ||
| yarn add toc-generate | ||
| ``` | ||
| You may not use `tocSelector` if the content is not already displayed. | ||
@@ -37,0 +44,0 @@ ```js |
+1
-1
| import { terser } from "rollup-plugin-terser"; | ||
| export default { | ||
| input: 'toc-generate/tocGenerate.js', | ||
| input: 'src/tocGenerate.js', | ||
| output: { | ||
@@ -6,0 +6,0 @@ file: 'dist/toc-generate.js', |
| function createTocElement(selector, heading) { | ||
| const li = document.createElement("li"); | ||
| const a = document.createElement("a"); | ||
| heading.id ||= `${heading.textContent | ||
| .replace(/\s/g, "-") | ||
| .toLowerCase()}-${Math.random().toString(36).substring(2, 15)}`; | ||
| a.textContent = heading.textContent; | ||
| a.href = `#${heading.id}`; | ||
| // Add the link to the li item | ||
| li.appendChild(a); | ||
| selector.appendChild(li); | ||
| return li; | ||
| } | ||
| export default createTocElement; |
| function findParentScope(tocTree, level) { | ||
| for (let i = tocTree.length - 1; i >= 0; i--) { | ||
| if (tocTree[i].level === level) { | ||
| return tocTree[i].scope; | ||
| } | ||
| } | ||
| return toc; | ||
| } | ||
| export default findParentScope; |
| function queryAllHeadings(headingLevelFrom, headingLevelTo, scopeElement) { | ||
| let headingSelector = ""; | ||
| for (let i = headingLevelFrom; i <= headingLevelTo; ++i) { | ||
| headingSelector += `h${i},`; | ||
| } | ||
| return scopeElement.querySelectorAll(headingSelector.slice(0, -1)); | ||
| } | ||
| export default queryAllHeadings; |
| import setClass from "./setClass"; | ||
| function scrollHighLight(headings, tocTree, viewablePercentToHighlight, showsParentHighlight) { | ||
| for (let i = 0; i < headings.length; i++) { | ||
| const heading = headings[i]; | ||
| const safeNextIndex = (i + 1 < headings.length) ? i + 1 : i; // If the next heading is the last one, use the current heading | ||
| const safePrevIndex = (i - 1 >= 0) ? i - 1 : 1; // First heading not have parent | ||
| const nextHeading = headings[safeNextIndex]; | ||
| const parentToc = tocTree[i].parent; | ||
| const elementToc = tocTree[i].selector; | ||
| const prevElementToc = tocTree[safePrevIndex]; | ||
| function highlight() { | ||
| let { bottom, top } = heading.getBoundingClientRect(); | ||
| const scope = nextHeading.getBoundingClientRect().top - bottom; | ||
| const scopeSplit = (scope * viewablePercentToHighlight) / 100; | ||
| const scopeCanView = scope - scopeSplit; | ||
| const windowHeight = window.innerHeight; | ||
| if (bottom + scopeCanView <= windowHeight && top + scopeSplit >= 0) { | ||
| setClass("highlight",'add', [heading, elementToc]); | ||
| if (showsParentHighlight && parentToc) { | ||
| setClass("parent_highlight", 'add', [parentToc]); | ||
| } | ||
| } else if (top + scopeSplit <= 0 || bottom > windowHeight) { | ||
| setClass("highlight", 'remove', [heading, elementToc]); | ||
| if (showsParentHighlight && prevElementToc.parent) { | ||
| setClass("parent_highlight", 'remove', [heading, elementToc, prevElementToc.selector]); | ||
| } | ||
| } | ||
| } | ||
| // Add event listener to scroll | ||
| highlight(); // Run on load | ||
| document.addEventListener("scroll", highlight); | ||
| } | ||
| } | ||
| export default scrollHighLight; |
| /** | ||
| * Set the class of the elements | ||
| * | ||
| * @param {string} className - The class name to add or remove | ||
| * @param {add|remove} type - The type of action to perform | ||
| * @param {...any} elements - The elements to add or remove the class from | ||
| */ | ||
| function setClass(className = "highlight", type = "add", elements) { | ||
| elements.forEach((element) => { | ||
| if (element !== undefined) { | ||
| element.classList[type](className); | ||
| } | ||
| }); | ||
| } | ||
| export default setClass; |
| import queryAllHeadings from "./queryAllHeadings"; | ||
| import findParentScope from "./findParentScope"; | ||
| import createTocElement from "./createTocElement"; | ||
| import scrollHighLight from "./scrollHighLight"; | ||
| function tocGenerate(props) { | ||
| const contentWrapperSelector = props.contentWrapperSelector || "body"; | ||
| const tocSelector = props.tocSelector; | ||
| const headingLevelFrom = props.headingLevelFrom || 2; | ||
| const headingLevelTo = props.headingLevelTo || 6; | ||
| const viewablePercentToHighlight = props.viewablePercentToHighlight || 70; | ||
| const showsHighLight = props.showsHighLight || false; | ||
| const showsParentHighlight = props.showsParentHighlight || false; | ||
| const tocElement = tocSelector && document.querySelector(tocSelector); | ||
| const scopeElement = document.querySelector(contentWrapperSelector); | ||
| // Get all h-from - h-to elements | ||
| const headings = queryAllHeadings(headingLevelFrom, headingLevelTo, scopeElement); | ||
| // Create a list to hold the headings | ||
| const toc = document.createElement("ol"); | ||
| const tocTree = [{ level: headingLevelFrom, scope: toc }]; | ||
| headings.forEach((heading) => { | ||
| const headingLevel = Number(heading.tagName.slice(1)); | ||
| const prevToc = tocTree[tocTree.length - 1]; | ||
| if (prevToc.level < headingLevel) { | ||
| const ol = document.createElement("ol"); | ||
| const a = createTocElement(ol, heading); | ||
| prevToc.scope.appendChild(ol); | ||
| tocTree.push({ level: headingLevel, scope: ol, selector: a, parent: tocTree.at(-1).selector }); | ||
| } else if (prevToc.level > headingLevel) { | ||
| const parentToc = findParentScope(tocTree, headingLevel); | ||
| const a = createTocElement(parentToc, heading); | ||
| tocTree.push({ level: headingLevel, scope: parentToc, selector: a }); | ||
| } else { | ||
| const a = createTocElement(prevToc.scope, heading); | ||
| tocTree.push({ level: headingLevel, scope: prevToc.scope, selector: a, ...(prevToc.parent && { parent: prevToc.parent })}); | ||
| } | ||
| }); | ||
| tocTree.shift(); | ||
| // Append the list to the toc element | ||
| if (tocElement) { | ||
| tocElement.appendChild(toc); | ||
| } | ||
| if (showsHighLight) { | ||
| scrollHighLight(headings, tocTree, viewablePercentToHighlight, showsParentHighlight); | ||
| } | ||
| return toc; | ||
| } | ||
| export default tocGenerate; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
14114
7.82%14
16.67%143
8.33%70
11.11%1
Infinity%