From 32f32fa0515b753c1b762640372af89b2a9ea4be Mon Sep 17 00:00:00 2001 From: aiden Date: Tue, 25 Apr 2023 17:14:23 +0100 Subject: [PATCH] initial commit --- README.md | 11 ++ example.html | 206 +++++++++++++++++++++++++++++++++ fuck12.js | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 530 insertions(+) create mode 100644 README.md create mode 100644 example.html create mode 100644 fuck12.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd22c38 --- /dev/null +++ b/README.md @@ -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) diff --git a/example.html b/example.html new file mode 100644 index 0000000..fd397c6 --- /dev/null +++ b/example.html @@ -0,0 +1,206 @@ + + + + + + + + + + +
+

motherfuck 1

+

fucker

+

ecstatic

+ +

the computer

+

computer

+

the computer

+ +

hella tall

+

fr

+
+ +

motherfuck 2

+

welcome

+

x

+

x

+
+

y

+
+ +

motherfuck 3

+

fuck

+

fuck

+ +

h1-1

+
+ + + + + \ No newline at end of file diff --git a/fuck12.js b/fuck12.js new file mode 100644 index 0000000..a7c2b7d --- /dev/null +++ b/fuck12.js @@ -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; + } +} \ No newline at end of file