@anywhichway/lazui
Version:
Single page apps and lazy loading sites with minimal JavaScript or client build processes.
225 lines • 11.1 kB
HTML
<style>
.toc {
overflow-wrap: break-word;
}
.toc ul {
margin-left: 5px;
margin-top: 0px;
margin-bottom: 0px;
list-style: none; /* This removes the list styling which are provided by default */
padding-left: 5px; /* Removes the front padding */
}
.toc ul li a {
text-decoration: none; /* Removes the underline from the link tags */
font-size: 80%
}
.toc ul li {
margin-left: 0px;
margin-top: 5px;
margin-bottom: 5px;
padding: 2px; /* Adds a little space around each <li> tag */
line-height: 80%
}
[id="close-button"] {
display: none;
float: right;
font-weight: bold;
margin-top: 0px;
}
[id="open-button"] {
display: none;
float: left;
font-weight: bold;
margin-top: 0px;
}
</style>
<div id="header" style="font-weight:bold;margin-top:0px">
<span id="open-button">>></span><span id="close-button"><<</span>
</div>
<datalist id="keyword"></datalist>
<div class="toc" style="border:1px solid grey;margin-right:5px;border-radius:5px;overflow-x:hidden;overflow-y:auto;background:whitesmoke">
</div>
<script>
function generateLinkMarkup(contentElement) {
const headings = [...contentElement.querySelectorAll('h2, h3, h4, h5')];
const parsedHeadings = headings.map(heading => {
return {
title: heading.innerText,
depth: parseInt(heading.nodeName.replace(/\D/g,'')),
id: heading.getAttribute('id')
}
});
let html = "";
for(let i=0;i<parsedHeadings.length;i++) {
const heading = parsedHeadings[i];
if(i>0) {
if(heading.depth>parsedHeadings[i-1].depth) {
let diff = heading.depth-parsedHeadings[i-1].depth;
while(diff--) html+="<ul>";
} else if(heading.depth<parsedHeadings[i-1].depth) {
let diff = parsedHeadings[i-1].depth-heading.depth;
while(diff--) html+="</ul>";
}
html += `<li><a href="#${heading.id}">${heading.title}</a></li>`;
} else {
html += `<li><a href="#${heading.id}">${heading.title}</a></li>`;
}
}
return `<ul>${html}</ul>`;
}
self.connected = function() {
this.setAttribute("style","position:fixed;top:2em;left:10px;max-height:97%;height:97%;opacity:1;background:white")
const toc = this.shadowRoot.querySelector(".toc");
if(!toc) return;
toc.innerHTML = '<input id="search" list="keyword" type="text" placeholder="Search" style="width: 150px; margin-left: 10px; margin-top: 5px; margin-bottom: 10px;"><br>'+generateLinkMarkup(document.body);
const search = toc.querySelector("#search");
document.body.style = "overflow:hidden;height:100%;max-height:100%;margin-top:0px"
const content = document.createElement("div");
content.setAttribute("style","float:right;padding-top:0px;max-height:100vh;overflow:auto;opacity:1");
const children = [...this.parentElement.children];
content.append(...children.slice(children.indexOf(this)+1));
//fixed.append(content);
this.after(content);
let touchstartX = 0,
touchendX = 0,
touchstartY = 0,
touchendY = 0,
x = 0,
y = 0;
const toggle = this.shadowRoot,
header = this.shadowRoot.getElementById("header");
function handleGesture({event,right,left}={}) {
if (left && touchendX < touchstartX && Math.abs(touchstartY-touchendY)<100 && Math.abs(touchstartX-touchendX)>75) { left(); }
else if (right && touchendX > touchstartX && touchstartX<150) { right(); }
}
let opened;
const handleTOC = (open) => {
const previous = opened;
if(open===undefined) open = opened = !opened
else opened = open;
if(opened) {
this.shadowRoot.getElementById("close-button").style.setProperty("display","inline");
this.shadowRoot.getElementById("open-button").style.setProperty("display","");
toc.style.setProperty("max-width","");
toc.style.setProperty("overflow-y","auto");
toc.style.setProperty("max-height","calc(100% - 8em)");
toc.style.setProperty("height","calc(100% - 8em)");
content.style.setProperty("margin-left",toc.clientWidth+10);
content.style.setProperty("max-width",`calc(100% - ${toc.clientWidth+40}px)`);
} else {
this.shadowRoot.getElementById("close-button").style.setProperty("display","");
this.shadowRoot.getElementById("open-button").style.setProperty("display","inline");
toc.style.setProperty("max-width","10px");
toc.style.setProperty("overflow-y","hidden");
toc.style.setProperty("max-height","97%");
toc.style.setProperty("height","97%");
content.style.setProperty("margin-left","");
content.style.setProperty("max-width",`calc(100% - ${48}px)`);
}
if(window.location.hash) {
setTimeout(() => {
const element = document.querySelector(window.location.hash);
if(element) {
element.scrollIntoView();
//content.scrollTo({top:content.scrollTop-45});
}
},250);
}
}
content.style.setProperty("margin-left",toc.clientWidth+10);
content.style.setProperty("max-width",`calc(100% - ${toc.clientWidth+40}px)`);
toc.style.setProperty("max-width","7px");
handleTOC(false);
toc.addEventListener('touchstart', event => {
touchstartX = event.changedTouches[0].screenX
});
toc.addEventListener('touchend', event => {
touchendX = event.changedTouches[0].screenX
handleGesture({left:()=>handleTOC(false),right:()=>handleTOC(true)})
});
content.addEventListener("scroll",(event) => {
y = content.scrollTop;
});
content.addEventListener('touchstart', event => {
touchstartX = event.changedTouches[0].screenX
touchstartY = event.changedTouches[0].screenY
});
content.addEventListener('touchend', event => {
touchendX = event.changedTouches[0].screenX;
touchendY = event.changedTouches[0].screenY;
handleGesture({right:()=>handleTOC(true),left:()=>event.preventDefault()});
});
toggle.addEventListener("click",(event) => {
event.stopImmediatePropagation();
if(event.target.tagName==="A") {
history.pushState({tocHREF:event.target.href}, "");
handleTOC(true);
history.replaceState({tocHREF:window.location.href}, "");
}
else if(event.target.id==="close-button") handleTOC(false);
else if(!opened) handleTOC(true);
});
content.addEventListener("click",(event) => {
if(event.target.tagName==="A") {
history.pushState({contentPosition: {scrollLeft:content.scrollLeft,scrollTop:content.scrollTop}}, "");
}
});
function xpathPrepare(xpath, searchString) {
return xpath.replace("$u", searchString.toUpperCase())
.replace("$l", searchString.toLowerCase())
.replace("$s", searchString.toLowerCase());
}
search.addEventListener("keyup",(event) => {
const datalist = this.shadowRoot.getElementById("keyword");
search.oldValue = event.target.value;
if([...datalist.children].some(option => option.value===event.target.value)) return;
datalist.innerHTML = "";
const words = new Set(event.target.value.split(" "));
for(const word of words) {
const xpathResult = document.evaluate(
xpathPrepare("//text()[contains(translate(., '$u', '$l'), '$s')]", word),
document.body,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null),
options = new Map();
for (let i = 0; i < xpathResult.snapshotLength; i++) {
const item = xpathResult.snapshotItem(i);
let textContent = item.textContent;
if(item.nextSibling) {
if(item.nextSibling?.nodeType===Node.TEXT_NODE) textContent += item.nextSibling.textContent;
else if(item.nextSibling?.nodeType===Node.ELEMENT_NODE && ["TEMPLATE","LINK","SRC"]!==item.nextSibling.tagName) textContent += item.nextSibling.innerText;
}
if (["SCRIPT", "STYLE", "TEMPLATE","DATALIST","OPTION"].includes(item.parentElement.tagName)) continue;
const target = event.target.value.endsWith(" ") ? event.target.value : event.target.value + " ";
text = (textContent.match(new RegExp(`.{0,30}${target}.{0,30}`,"gi"))||[])[0];
if(text) {
const option = options.get(item) || new Option(text.slice(text.indexOf(" "),text.lastIndexOf(" ")));
option.node = item;
option.count = (option.count || 0) + 1;
options.set(item, option);
}
//if(i===0) item.parentElement.scrollIntoView({behavior:"smooth"});
}
datalist.append(...[...options.values()].sort((a, b) => b.count - a.count));
}
});
search.addEventListener("change",(event)=> {
const datalist = this.shadowRoot.getElementById("keyword"),
option = [...datalist.children].find(option => option.value===event.target.value);
if(option) {
search.blur();
setTimeout(() => {
option.node.parentElement.scrollIntoView({behavior:"instant"});
option.node.parentElement.focus();
})
}
search.value = search.oldValue;
})
window.addEventListener("popstate", (event) => {
const contentPosition = event.state?.contentPosition;
if(contentPosition) content.scrollLeft = contentPosition.scrollLeft, content.scrollTop = contentPosition.scrollTop;
if(event.state?.tocHREF) window.location = event.state.tocHREF;
});
}
</script>