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