initial commit
This commit is contained in:
commit
32f32fa051
|
@ -0,0 +1,11 @@
|
||||||
|
# fuck12
|
||||||
|
y'know how some webpages like highlight shit on the sidebar as you scroll the main content? yeah i implemented that functionality
|
||||||
|
|
||||||
|
## name
|
||||||
|
was the twelth node named "fuck" in my home directory. i agree with the connotations, though.
|
||||||
|
|
||||||
|
## usage
|
||||||
|
see example.html
|
||||||
|
|
||||||
|
## license
|
||||||
|
isc (see fuck12.js)
|
|
@ -0,0 +1,206 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="./fuck12.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
word-wrap: break-word;
|
||||||
|
border-right: solid 1px white;
|
||||||
|
}
|
||||||
|
#nav {
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: solid 1px white;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#nav h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#links {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
#sidebar .i1 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
#sidebar .i2 {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
#sidebar a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
padding: 1em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#content h1, #content h2, #content p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#content h1::after {
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
background: red;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
#content article {
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
--sidebar-display: none;
|
||||||
|
--content-visibility: visible;
|
||||||
|
--content-padding: 1rem;
|
||||||
|
--content-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 70em) {
|
||||||
|
body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
display: var(--sidebar-display);
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
visibility: var(--content-visibility);
|
||||||
|
padding: var(--content-padding);
|
||||||
|
width: var(--content-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="sidebar">
|
||||||
|
<a id="nav" href="">
|
||||||
|
<h2>fuck12</h2>
|
||||||
|
</a>
|
||||||
|
<div id="links"></div>
|
||||||
|
</div>
|
||||||
|
<div id="content">
|
||||||
|
<h1 data-depth="0" id="motherfuck-1">motherfuck 1</h1>
|
||||||
|
<h2 data-depth="1" id="fucker">fucker</h2>
|
||||||
|
<p>ecstatic</p>
|
||||||
|
|
||||||
|
<h1 data-depth="0" id="the-computer">the computer</h1>
|
||||||
|
<h2 data-depth="1" id="computer">computer</h2>
|
||||||
|
<p>the computer</p>
|
||||||
|
|
||||||
|
<h1 data-depth="0" id="hella-tall">hella tall</h1>
|
||||||
|
<h2 data-depth="1" id="fr">fr</h2>
|
||||||
|
<div style="height: 100vh; background: green;"></div>
|
||||||
|
|
||||||
|
<h1 data-depth="0" id="motherfuck-2">motherfuck 2</h1>
|
||||||
|
<h2 data-depth="1" id="welcome">welcome</h2>
|
||||||
|
<p>x</p>
|
||||||
|
<h3 data-depth="2" id="x">x</h3>
|
||||||
|
<div style="height: 100vh; background: green;"></div>
|
||||||
|
<h2 data-depth="1" id="y">y</h2>
|
||||||
|
<div style="height: 100vh; background: green;"></div>
|
||||||
|
|
||||||
|
<h1 data-depth="0" id="motherfuck-3">motherfuck 3</h1>
|
||||||
|
<h2 data-depth="1" id="fuck">fuck</h2>
|
||||||
|
<p>fuck</p>
|
||||||
|
|
||||||
|
<h1 data-depth="0" id="h1-1">h1-1</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function doubleRAF(fn) {
|
||||||
|
return requestAnimationFrame(_ =>
|
||||||
|
requestAnimationFrame(t => fn(t))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let content = document.querySelector("#content");
|
||||||
|
let sidebar = document.querySelector("#sidebar");
|
||||||
|
|
||||||
|
// populate sidebar
|
||||||
|
for (let el of content.querySelectorAll("h1, h2, h3")) {
|
||||||
|
let a = document.createElement("a");
|
||||||
|
if (el.nodeName === "H2") {
|
||||||
|
a.classList.add("i1");
|
||||||
|
} else if (el.nodeName === "H3") {
|
||||||
|
a.classList.add("i2");
|
||||||
|
}
|
||||||
|
a.href = "#" + el.id;
|
||||||
|
a.onclick = event => doubleRAF(_ => window.onhashchange(event));
|
||||||
|
a.dataset.associated = el.id;
|
||||||
|
a.innerText = el.innerText;
|
||||||
|
sidebar.children.links.append(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
let elemMap = new WeakMap;
|
||||||
|
for (let el of sidebar.children.links.children) {
|
||||||
|
let associated = document.getElementById(el.dataset.associated);
|
||||||
|
elemMap.set(associated, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = document.querySelector(":root");
|
||||||
|
function mobile_openSidebar() {
|
||||||
|
root.style.setProperty("--sidebar-display", "block");
|
||||||
|
root.style.setProperty("--content-visibility", "hidden");
|
||||||
|
root.style.setProperty("--content-padding", "0");
|
||||||
|
root.style.setProperty("--content-width", "0px");
|
||||||
|
}
|
||||||
|
function mobile_closeSidebar() {
|
||||||
|
root.style.setProperty("--sidebar-display", "none");
|
||||||
|
root.style.setProperty("--content-visibility", "visible");
|
||||||
|
root.style.setProperty("--content-padding", "1rem");
|
||||||
|
root.style.setProperty("--content-width", "auto");
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeEl = null;
|
||||||
|
let inst = new Fuck12({
|
||||||
|
content,
|
||||||
|
activateCallback: (el, rel) => {
|
||||||
|
let associated = elemMap.get(el);
|
||||||
|
associated.style.background = "red";
|
||||||
|
if (rel === Fuck12.HEADER_BASE) {
|
||||||
|
activeEl = el;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
deactivateCallback: el => {
|
||||||
|
let associated = elemMap.get(el);
|
||||||
|
associated.style.background = "";
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
registerHashchange: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onhashchange = (function onhashchange(event) {
|
||||||
|
// ignore click event if the new
|
||||||
|
// hash element is already active
|
||||||
|
if (event && event.type === "click") {
|
||||||
|
try {
|
||||||
|
let e = Fuck12.hashEl();
|
||||||
|
if (activeEl === e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.hashchange();
|
||||||
|
return onhashchange;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,313 @@
|
||||||
|
/*
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2023, aiden (aiden@cmp.bz)
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// (horizonatal scrolling not implemented btw)
|
||||||
|
class Fuck12 {
|
||||||
|
static NO_SET_HASH = 0b00;
|
||||||
|
static CONTENT_HASH = 0b01;
|
||||||
|
static BASE_HEADER_HASH = 0b10;
|
||||||
|
static HASH_MODE_MASK = 0b11;
|
||||||
|
static FIRST_ELEMENT_NO_HASH = 1 << 2;
|
||||||
|
|
||||||
|
static NO_ACTIVATE_AFTER = 0;
|
||||||
|
static ACTIVE_TO_DEEPEST = 1;
|
||||||
|
static ACTIVATE_TO_DEEPEST_GREEDY = 2;
|
||||||
|
|
||||||
|
static HEADER_ABOVE_BASE = -1;
|
||||||
|
static HEADER_BASE = 0;
|
||||||
|
static HEADER_BENEATH_BASE = 1;
|
||||||
|
static CONTENT = null;
|
||||||
|
|
||||||
|
static setHash(el) {
|
||||||
|
if (!el.id.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
history.replaceState({}, "", "#" + encodeURIComponent(el.id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static removeHash() {
|
||||||
|
history.replaceState({}, "", location.href.substring(0, location.href.indexOf("#")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static depth(depthStr) {
|
||||||
|
let n = Number(depthStr);
|
||||||
|
if (!Number.isInteger(n) || n < 0) {
|
||||||
|
throw new Error("invalid depth");
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
static hashEl() {
|
||||||
|
if (location.hash.length <= 1) {
|
||||||
|
throw new Error("no meaningful hash");
|
||||||
|
}
|
||||||
|
let el = document.getElementById(decodeURIComponent(location.hash.substr(1)));
|
||||||
|
if (!el) {
|
||||||
|
throw new Error("no hash element");
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer = null;
|
||||||
|
observedActiveElems = [];
|
||||||
|
signalEl = document.createElement("br");
|
||||||
|
sentSignal = false;
|
||||||
|
csp = null;
|
||||||
|
activateCallback = null;
|
||||||
|
deactivateCallback = null;
|
||||||
|
activateAfterMode = null;
|
||||||
|
content = null;
|
||||||
|
|
||||||
|
signal() {
|
||||||
|
this.sentSignal = true;
|
||||||
|
this.observer.observe(this.signalEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
hashchange = _ => {
|
||||||
|
try {
|
||||||
|
var el = Fuck12.hashEl();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.signal();
|
||||||
|
|
||||||
|
this.processEl({
|
||||||
|
targetEl: el,
|
||||||
|
activateAfterMode: this.activateAfterMode,
|
||||||
|
hashMode: Fuck12.NO_SET_HASH,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activate = (el, rel, depth) => {
|
||||||
|
this.observedActiveElems.push(el);
|
||||||
|
el.dataset.observedActive = true;
|
||||||
|
this.activateCallback?.(el, rel, depth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deactivate = el => {
|
||||||
|
delete el.dataset.observedActive;
|
||||||
|
this.deactivateCallback?.(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processEl = ({ targetEl, activateAfterMode, hashMode, }) => {
|
||||||
|
let
|
||||||
|
baseEl = null,
|
||||||
|
baseDepth = null;
|
||||||
|
|
||||||
|
if (targetEl === this.content.firstElementChild && (hashMode & Fuck12.FIRST_ELEMENT_NO_HASH)) {
|
||||||
|
hashMode = Fuck12.NO_SET_HASH;
|
||||||
|
Fuck12.removeHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
// find a header
|
||||||
|
for (
|
||||||
|
let el = targetEl, contentEl = null;;
|
||||||
|
el = el.previousElementSibling
|
||||||
|
) {
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((hashMode & Fuck12.HASH_MODE_MASK) === Fuck12.CONTENT_HASH) {
|
||||||
|
if (Fuck12.setHash(el)) {
|
||||||
|
contentEl = el;
|
||||||
|
hashMode = Fuck12.NO_SET_HASH;
|
||||||
|
} else {
|
||||||
|
hashMode = (hashMode & ~Fuck12.HASH_MODE_MASK) | Fuck12.BASE_HEADER_HASH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var elDepth = Fuck12.depth(el.dataset.depth);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deactivate old elements
|
||||||
|
for (let old; old = this.observedActiveElems.pop();) {
|
||||||
|
this.deactivate(old);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((hashMode & Fuck12.HASH_MODE_MASK) === Fuck12.BASE_HEADER_HASH) {
|
||||||
|
Fuck12.setHash(el);
|
||||||
|
}
|
||||||
|
if (contentEl) {
|
||||||
|
this.activate(contentEl, Fuck12.CONTENT, null);
|
||||||
|
}
|
||||||
|
this.activate(el, Fuck12.HEADER_BASE, elDepth);
|
||||||
|
|
||||||
|
baseEl = el;
|
||||||
|
baseDepth = elDepth;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find deepest element
|
||||||
|
if (activateAfterMode !== Fuck12.NO_ACTIVATE_AFTER) {
|
||||||
|
for (
|
||||||
|
let el =
|
||||||
|
baseEl.nextElementSibling, currDepth = baseDepth;
|
||||||
|
el;
|
||||||
|
el = el.nextElementSibling
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
var elDepth = Fuck12.depth(el.dataset.depth);
|
||||||
|
} catch {
|
||||||
|
if (activateAfterMode === Fuck12.ACTIVATE_TO_DEEPEST_GREEDY) {
|
||||||
|
continue;
|
||||||
|
} else /* Fuck12.ACTIVE_TO_DEEPEST */ {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elDepth !== currDepth + 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currDepth += 1;
|
||||||
|
|
||||||
|
// to-do: maybe set hash here if setHash hasn't already succeeded
|
||||||
|
this.activate(el, Fuck12.HEADER_BENEATH_BASE, elDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find surface element
|
||||||
|
for (
|
||||||
|
let
|
||||||
|
el = baseEl.previousElementSibling,
|
||||||
|
currDepth = baseDepth;
|
||||||
|
el && currDepth > 0;
|
||||||
|
el = el.previousElementSibling
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
var elDepth = Fuck12.depth(el.dataset.depth);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elDepth !== currDepth - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
currDepth -= 1;
|
||||||
|
|
||||||
|
// to-do: maybe set hash here if setHash hasn't already succeeded
|
||||||
|
this.activate(el, Fuck12.HEADER_ABOVE_BASE, elDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
content, threshold = 0,
|
||||||
|
hashMode = Fuck12.NO_SET_HASH,
|
||||||
|
activateAfterMode = Fuck12.ACTIVATE_TO_DEEPEST_GREEDY,
|
||||||
|
activateCallback = null,
|
||||||
|
deactivateCallback = null,
|
||||||
|
registerHashchange = true,
|
||||||
|
} = {}) {
|
||||||
|
if (!(content instanceof HTMLElement)) {
|
||||||
|
throw new TypeError("content must be an instance of HTMLElement");
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = hashMode & Fuck12.HASH_MODE_MASK;
|
||||||
|
if (m < Fuck12.NO_SET_HASH || m > Fuck12.BASE_HEADER_HASH) {
|
||||||
|
throw new RangeError("invalid value for hashMode");
|
||||||
|
}
|
||||||
|
if (hashMode & ~(Fuck12.HASH_MODE_MASK | Fuck12.FIRST_ELEMENT_NO_HASH)) {
|
||||||
|
throw new Error("invalid flag set on hashMode");
|
||||||
|
}
|
||||||
|
if (activateAfterMode < Fuck12.NO_ACTIVATE_AFTER || activateAfterMode > Fuck12.ACTIVATE_TO_DEEPEST_GREEDY) {
|
||||||
|
throw new RangeError("invalid value for activateAfterMode");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.csp = content.scrollTop;
|
||||||
|
this.observer = new IntersectionObserver(entries => {
|
||||||
|
let sp = this.csp;
|
||||||
|
this.csp = content.scrollTop;
|
||||||
|
if (this.sentSignal) {
|
||||||
|
this.observer.unobserve(this.signalEl);
|
||||||
|
this.sentSignal = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let entry of entries) {
|
||||||
|
let target = entry.target;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (this.csp <= sp) {
|
||||||
|
this.processEl({
|
||||||
|
targetEl: target,
|
||||||
|
activateAfterMode,
|
||||||
|
hashMode,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (entry.boundingClientRect.bottom <= content.clientHeight) {
|
||||||
|
// element goes OFF THE TOP OF THE VIEWPORT
|
||||||
|
let el = target.nextElementSibling;
|
||||||
|
if (el) {
|
||||||
|
this.processEl({
|
||||||
|
targetEl: el,
|
||||||
|
activateAfterMode,
|
||||||
|
hashMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (target.dataset.observedActive) {
|
||||||
|
// element goes OFF THE BOTTOM OF THE VIEWPORT
|
||||||
|
let el = target.previousElementSibling;
|
||||||
|
if (el) {
|
||||||
|
this.processEl({
|
||||||
|
targetEl: el,
|
||||||
|
activateAfterMode,
|
||||||
|
hashMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, {
|
||||||
|
root: content,
|
||||||
|
threshold: threshold,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let el of content.children) {
|
||||||
|
this.observer.observe(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activateAfterMode = activateAfterMode;
|
||||||
|
this.activateCallback = activateCallback;
|
||||||
|
this.deactivateCallback = deactivateCallback;
|
||||||
|
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
if (registerHashchange) {
|
||||||
|
window.addEventListener("hashchange", this.hashchange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destructor() {
|
||||||
|
this.observer.disconnect();
|
||||||
|
this.observer = null;
|
||||||
|
window.removeEventListener("hashchange", this.hashchange);
|
||||||
|
for (let el; el = this.observedActiveElems.pop();) {
|
||||||
|
this.deactivate(el);
|
||||||
|
}
|
||||||
|
this.signalEl = null;
|
||||||
|
this.content = null;
|
||||||
|
this.activateCallback = null;
|
||||||
|
this.deactivateCallback = null;
|
||||||
|
this.observedActiveElems = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue