UNPKG

@anywhichway/lazui

Version:

Single page apps and lazy loading sites with minimal JavaScript or client build processes.

225 lines 11.1 kB
<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"> &nbsp;<span id="open-button">&gt;&gt;</span><span id="close-button">&lt;&lt;</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>