2023-11-19 21:06:25 +00:00
const idb = require ( "idb" )
const { solve } = require ( "yalps" )
2024-04-30 16:30:27 +01:00
const { ignorePaths } = require ( "./common" )
const prefetchHook = ( ) => {
let lastFetch = 0
const prefetch = event => {
if ( Date . now ( ) - lastFetch >= 200 ) {
2024-07-23 09:40:02 +01:00
let target = event . target
while ( ! target . hasAttribute ( "href" ) ) {
target = target . parentElement
}
const href = new URL ( target . getAttribute ( "href" ) , window . location )
2024-08-31 18:40:12 +01:00
if ( href . origin === window . location . origin && ( href . pathname !== window . location . pathname || href . search !== window . location . search ) ) {
2024-04-30 16:30:27 +01:00
for ( const ignorePath of ignorePaths ) {
if ( href . pathname . startsWith ( ignorePath ) ) return
}
console . log ( "prefetch" , href . href )
fetch ( href )
lastFetch = Date . now ( )
}
}
}
for ( const node of document . querySelectorAll ( "a[href]" ) ) {
node . addEventListener ( "touchstart" , prefetch )
node . addEventListener ( "mouseover" , prefetch )
}
}
2020-03-08 17:13:14 +00:00
// attempt to register service worker
if ( "serviceWorker" in navigator ) {
navigator . serviceWorker . register ( "/sw.js" , { scope : "/" } ) . then ( reg => {
if ( reg . installing ) {
2024-04-30 16:30:27 +01:00
console . log ( "Service worker installing" )
2020-03-08 17:13:14 +00:00
} else if ( reg . waiting ) {
2024-04-30 16:30:27 +01:00
console . log ( "Service worker installed" )
2020-03-08 17:13:14 +00:00
} else if ( reg . active ) {
2024-04-30 16:30:27 +01:00
console . log ( "Service worker active" )
prefetchHook ( )
2020-03-08 17:13:14 +00:00
}
} ) . catch ( error => {
// registration failed
2024-04-30 16:30:27 +01:00
console . log ( "Registration failed with " + error )
2020-03-08 17:13:14 +00:00
} ) ;
} else {
2024-04-30 16:30:27 +01:00
console . log ( "Service workers are not supported." )
2020-03-08 17:13:14 +00:00
}
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
2020-05-02 09:19:04 +01:00
const hashString = function ( str , seed = 0 ) {
2020-03-08 17:13:14 +00:00
let h1 = 0xdeadbeef ^ seed , h2 = 0x41c6ce57 ^ seed
for ( let i = 0 , ch ; i < str . length ; i ++ ) {
ch = str . charCodeAt ( i )
h1 = Math . imul ( h1 ^ ch , 2654435761 )
h2 = Math . imul ( h2 ^ ch , 1597334677 )
}
h1 = Math . imul ( h1 ^ h1 >>> 16 , 2246822507 ) ^ Math . imul ( h2 ^ h2 >>> 13 , 3266489909 ) ;
h2 = Math . imul ( h2 ^ h2 >>> 16 , 2246822507 ) ^ Math . imul ( h1 ^ h1 >>> 13 , 3266489909 )
return 4294967296 * ( 2097151 & h2 ) + ( h1 >>> 0 )
}
2020-05-02 09:19:04 +01:00
const colHash = ( str , saturation = 100 , lightness = 70 ) => ` hsl( ${ hashString ( str ) % 360 } , ${ saturation } %, ${ lightness } %) `
2023-11-19 21:06:25 +00:00
window . colHash = colHash
2020-03-08 17:13:14 +00:00
2024-08-31 18:36:07 +01:00
const e = ( cls , parent , content , type = "div" ) => {
const element = document . createElement ( type )
element . classList . add ( cls )
if ( content ) { element . appendChild ( document . createTextNode ( content ) ) }
if ( parent ) { parent . appendChild ( element ) }
return element
}
2020-03-08 17:13:14 +00:00
// Arbitrary Points code, wrapped in an IIFE to not pollute the global environment much more than it already is
2022-03-07 20:07:16 +00:00
window . points = ( async ( ) => {
2020-03-08 17:13:14 +00:00
const achievementInfo = {
test : {
title : "Test" ,
conditions : "testing" ,
description : "This achievement is for testing purposes." ,
points : 10
} ,
firstAchievement : {
title : "Achievement-Achieving Achievement™" ,
conditions : "unlocking another achievement" ,
description : "You achieved your first achievement, so here's an achievement to commemorate your achievement of an achievement! Enjoy the sense of achievement you get from this achievement!" ,
points : 5.5
} ,
timeSpent1Hour : {
title : "Causal Mondays" ,
conditions : "using the site for a total of 1 hour" ,
description : "Apparently you've spent an hour on this site. Weird. You get an achievement for it, though." ,
points : 9.3
} ,
visitArbitraryPoints : {
title : "Arbitrary Arbitration" ,
conditions : "visiting the Arbitrary Points management page" ,
description : "You've now visited the Arbitrary Points page, from which you can see your achievements, point count and tracked metrics." ,
points : 15
} ,
reset : {
title : "Burn It Down" ,
conditions : "resetting" ,
description : "So you wiped your Arbitrary Points data for whatever reason. Now you get this exclusive achievement!" ,
points : 11.4
} ,
pagesVisited64 : {
title : "Real Dedication" ,
conditions : "visiting 64 pages" ,
points : 15.01 ,
description : "You've visited something between 64 pages or 1 page 64 times and are thus being rewarded for your frequent use of the site."
} ,
blindLuck : {
title : "Ridiculous Blind Luck" ,
conditions : "0.001% chance of getting this every second" ,
points : 66.6 ,
description : "Through sheer chance you have obtained this achievement, which provides more points than all the other ones. This is probably a metaphor for life."
} ,
offline : {
title : "Not The Dinosaur Game" ,
conditions : "seeing the offline page" ,
points : 10.1 ,
description : "Something broke somewhere and you're seeing this. Sadly this no longer has the Chrome dinosaur game, but you can use other stuff."
} ,
attemptedXSS : {
title : "1337 h4xx0r" ,
conditions : "attempting an XSS attack" ,
points : 43.01 ,
description : "You appear to have attempted a cross-site-scripting attack. This probably hasn't worked. If it has, please tell me as this is a problem."
} ,
emuwar10 : {
title : "Emu Warrior" ,
conditions : "vanquishing 10 or more foes in Emu War" ,
points : 28.5 ,
description : "You have become a mighty Emu Warrior by defeating 10 or more monsters and/or probably things which live in Australia."
} ,
lorem400 : {
title : "quare?" ,
conditions : "seeing 400 paragraphs of Lorem Ipsum" ,
points : 42.3 ,
description : "Apparently you viewed 400 paragraphs of randomly generated Lorem Ipsum. I don't know why."
} ,
firstComment : {
title : "That's just, like, your opinion, man" ,
conditions : "posting a comment" ,
points : 30.5 ,
description : "You (probably, the detection isn't 100% accurate) posted a comment! Enjoy expressing your opinion (or random meaningless message) to random internet people!"
2020-05-02 09:19:04 +01:00
} ,
aprilFools : {
title : "April Folly" ,
conditions : "visiting on April Fools' Day" ,
description : "Enjoy being... April Fooled? Good luck getting this, speedrunners." ,
points : 16.0303
2020-07-03 21:11:23 +01:00
} ,
heavgame1 : {
title : "Pastaphysical Futility" ,
conditions : "Annoying the narrator of Heavpoot's Game" ,
description : "Irritating the narrator with control of your reality is rarely a good choice. In this case it definitely wasn't." ,
points : 34.04
} ,
heavgame2 : {
title : "(insert meaningful secret O5 councily thing here)" ,
conditions : "Solving the puzzle in Heavpoot's Game" ,
description : ` You... did something or other and somehow found the "black moon" thing and guessed/found the answer to the puzzle there, so enjoy this achievement, O6-3234234! ` ,
points : 41.5824
2022-03-07 20:07:16 +00:00
} ,
tttWinai1 : {
title : "Beginner's Luck" ,
conditions : ` Beat "AI" 1 on Tic-Tac-Toe ` ,
description : "Congratulations on beating a slightly aware random number generator." ,
points : 23
} ,
tttWinai2 : {
title : "Superior Algorithms" ,
conditions : ` Beat "AI" 2 on Tic-Tac-Toe ` ,
description : "Nonsarcastic congratulations on beating the near-optimal minimax agent." ,
points : 46
} ,
apioformGame : {
title : "Relativistic apiohydromagnetoplasmodynamic cryomemetics" ,
conditions : "Finish the Apioform Game tutorial" ,
description : "You have braved the complete lack of guidance or a description in the Apioform Game and apioformed many apioforms." ,
points : 40
} ,
rpnv4recursion : {
title : "You are doing it right" ,
conditions : "Recurse to a stack depth of 100 or more on RPNCalc v4" ,
description : "For using RPNCalcV4 as it is meant to be used - highly, highly recursively." ,
points : 18.324
2024-01-30 16:46:38 +00:00
} ,
ttt4Win : {
title : "Shape Rotator" ,
conditions : "Win 4D Tic-Tac-Toe" ,
description : "You won a game of 4D Tic-Tac-Toe against our highly advanced artificial intelligence, showing your utter comprehension of 4D space." ,
points : 37.9
2022-03-07 20:07:16 +00:00
}
}
2022-03-11 16:12:25 +00:00
let [ metrics , pointsData ] = [ { } , { } ]
try {
const [ oldMetrics , oldPoints ] = await Promise . all ( [ idb . openDB ( "arbitrary-metrics" ) , idb . openDB ( "arbitrary-points" ) ] )
const getMetrics = async ( ) => {
const metrics = { }
const tx = oldMetrics . transaction ( "metrics" , "readonly" )
for ( const key of await tx . store . getAllKeys ( ) ) {
metrics [ key ] = await tx . store . get ( key )
}
return metrics
2022-03-07 20:07:16 +00:00
}
2022-03-11 16:12:25 +00:00
const getPointsData = async ( ) => {
const data = { }
const tx = oldPoints . transaction ( "data" , "readonly" )
for ( const key of await tx . store . getAllKeys ( ) ) {
data [ key ] = await tx . store . get ( key )
}
return data
2020-03-08 17:13:14 +00:00
}
2022-03-11 16:12:25 +00:00
[ metrics , pointsData ] = await Promise . all ( [ getMetrics ( ) , getPointsData ( ) ] )
await Promise . all ( [ oldMetrics . close ( ) , oldPoints . close ( ) ] )
} catch ( e ) {
console . warn ( "old achievements not loaded due to" , e )
2020-03-08 17:13:14 +00:00
}
2022-03-07 20:07:16 +00:00
const db = await idb . openDB ( "arbitrary-data" , 1 , {
async upgrade ( db , oldVersion , newVersion , tx ) {
2022-03-11 16:12:25 +00:00
console . log ( "migrating" , oldVersion , newVersion )
2022-03-07 20:07:16 +00:00
if ( ! oldVersion || oldVersion < 1 ) {
// create metrics, KV, achievements stores
db . createObjectStore ( "kv" )
db . createObjectStore ( "metrics" )
db . createObjectStore ( "achievements" , {
keyPath : "id"
} )
for ( const [ key , value ] of Object . entries ( metrics ) ) {
await tx . objectStore ( "metrics" ) . put ( value , key )
}
for ( const achievement of ( pointsData [ "achievements" ] || [ ] ) ) {
await tx . objectStore ( "achievements" ) . put ( achievement )
}
}
await tx . done
} ,
blocked ( ) {
console . warn ( "Database error (older version open)" )
} ,
blocking ( ) {
window . location . reload ( )
} ,
terminated ( ) {
console . warn ( "Database error (unexpectedly closed)" )
} ,
} ) ;
2020-03-08 17:13:14 +00:00
const achievementsContainer = e ( "achievements" , document . body )
const displayAchievement = ( title , description , conditions , points ) => {
const elem = e ( "achievement" , achievementsContainer )
elem . title = "click to dismiss"
e ( "title" , elem , "Achievement achieved!" )
e ( "title" , elem , title )
elem . style . backgroundColor = colHash ( title )
e ( "description" , elem , description )
e ( "conditions" , elem , ` Unlocked by: ${ conditions } ` )
e ( "points" , elem , ` ${ points } points ` )
// disappear on click
elem . addEventListener ( "click" , ( ) => {
achievementsContainer . removeChild ( elem )
} )
}
const fireUpdatedEvent = ( ) => document . dispatchEvent ( new Event ( "points-update" ) )
2022-03-07 20:07:16 +00:00
let achievementsList
2020-03-08 17:13:14 +00:00
2022-03-07 20:07:16 +00:00
const getAchievements = async tx => {
if ( achievementsList ) { return achievementsList }
if ( ! tx ) { tx = db . transaction ( "achievements" , "readonly" ) }
achievementsList = await tx . objectStore ( "achievements" ) . getAll ( )
return achievementsList
2020-03-08 17:13:14 +00:00
}
2022-03-07 20:07:16 +00:00
const getPoints = async ( ) => ( await getAchievements ( ) ) . map ( a => a . points ) . reduce ( ( x , y ) => x + y , 0 )
2020-03-08 17:13:14 +00:00
const updateMetric = async ( name , fn , def ) => {
2022-03-07 20:07:16 +00:00
const tx = db . transaction ( "metrics" , "readwrite" )
const init = await tx . store . get ( name ) || def
const newValue = fn ( init )
await tx . store . put ( newValue , name )
2020-03-08 17:13:14 +00:00
switch ( name ) {
case "achievements" :
if ( newValue === 1 ) {
await unlockAchievement ( "firstAchievement" )
}
break
}
return newValue
}
// increment pages visited count, since this should be run when a page is visited
updateMetric ( "pagesVisited" , x => x + 1 , 0 )
const visitStart = Date . now ( )
window . onbeforeunload = ( ) => {
const elapsedMs = Date . now ( ) - visitStart
updateMetric ( "timeSpent" , x => x + ( elapsedMs / 1000 ) , 0 )
}
const readAllMetrics = async ( ) => {
const out = new Map ( )
2022-03-07 20:07:16 +00:00
const tx = db . transaction ( "metrics" , "readonly" )
for ( const key of await tx . store . getAllKeys ( ) ) {
out . set ( key , await tx . store . get ( key ) )
}
2020-03-08 17:13:14 +00:00
return out
}
const reset = async ( ) => {
2022-03-07 20:07:16 +00:00
const tx = db . transaction ( [ "achievements" , "metrics" ] , "readwrite" )
for ( const achievement of await tx . objectStore ( "achievements" ) . getAllKeys ( ) ) {
await tx . objectStore ( "achievements" ) . delete ( achievement )
}
for ( const metric of await tx . objectStore ( "metrics" ) . getAllKeys ( ) ) {
await tx . objectStore ( "metrics" ) . delete ( metric )
}
2020-03-08 17:13:14 +00:00
achievementsList = [ ]
2022-03-07 20:07:16 +00:00
// fireUpdatedEvent()
// called when achievement is unlocked there anyway
2020-03-08 17:13:14 +00:00
await unlockAchievement ( "reset" )
}
const unlockAchievement = async id => {
2022-03-07 20:07:16 +00:00
const tx = db . transaction ( "achievements" , "readwrite" )
const achievementsUnlocked = await getAchievements ( tx )
2020-03-08 17:13:14 +00:00
if ( achievementsUnlocked . filter ( a => a . id === id ) . length > 0 ) { return "already unlocked" }
const info = achievementInfo [ id ]
if ( ! info ) { throw new Error ( "Achievement not recognized" ) }
info . points = info . points || 10
displayAchievement ( info . title , info . description , info . conditions , info . points )
const item = {
id ,
timestamp : Date . now ( ) ,
page : window . location . pathname ,
points : info . points
}
2022-03-07 20:07:16 +00:00
achievementsList . push ( item )
2020-03-08 17:13:14 +00:00
await Promise . all ( [
updateMetric ( "achievements" , x => x + 1 , 0 ) ,
2022-03-07 20:07:16 +00:00
tx . objectStore ( "achievements" ) . put ( item ) ,
2020-03-08 17:13:14 +00:00
] )
fireUpdatedEvent ( )
}
document . addEventListener ( "DOMContentLoaded" , async ( ) => {
const metrics = await readAllMetrics ( )
if ( metrics . get ( "timeSpent" ) > 3600 ) { // one hour in seconds
unlockAchievement ( "timeSpent1Hour" )
}
if ( metrics . get ( "pagesVisited" ) > 64 ) {
unlockAchievement ( "pagesVisited64" )
}
2020-05-02 09:19:04 +01:00
const now = new Date ( )
if ( now . getUTCMonth ( ) === 3 && now . getUTCDate ( ) === 1 ) {
//if (now.getUTCMonth() === 2 && now.getUTCDate() === 22) {
unlockAchievement ( "aprilFools" )
}
2020-03-08 17:13:14 +00:00
} )
setInterval ( ( ) => {
if ( Math . random ( ) < 0.00001 ) {
unlockAchievement ( "blindLuck" )
}
} , 1000 )
window . addEventListener ( "input" , e => {
if ( e . target ) {
const text = e . target . value || e . target . textContent
// extremely advanced XSS detection algorithm
if ( text && ( text . includes ( "<script" ) || text . includes ( "onload=" ) ) ) {
unlockAchievement ( "attemptedXSS" )
}
}
} )
window . addEventListener ( "click" , e => {
// detect clicking of comment "submit" button
if ( e . target &&
e . target . value === "Submit" &&
e . target . parentElement &&
e . target . parentElement . parentElement &&
2024-04-27 23:20:43 +01:00
e . target . parentElement . parentElement . classList . contains ( "controls" ) ) {
2020-03-08 17:13:14 +00:00
unlockAchievement ( "firstComment" )
points . updateMetric ( "commentsPosted" , function ( x ) { return x + 1 } , 0 )
}
} )
return {
reset ,
updateMetric ,
readAllMetrics ,
getPoints ,
unlockAchievement ,
getAchievements ,
2022-03-07 20:07:16 +00:00
achievementInfo
2020-03-08 17:13:14 +00:00
}
2020-07-01 20:29:42 +01:00
} ) ( )
2023-11-19 21:06:25 +00:00
const footnotes = document . querySelector ( ".footnotes" )
const sidenotes = document . querySelector ( ".sidenotes" )
2024-01-02 02:23:11 +00:00
if ( sidenotes && footnotes ) {
2024-02-25 21:30:23 +00:00
const codeblocks = document . querySelectorAll ( ".wider" )
2024-01-02 02:23:11 +00:00
const article = document . querySelector ( ".content" )
2023-11-19 21:06:25 +00:00
while ( footnotes . firstChild ) {
sidenotes . appendChild ( footnotes . firstChild )
}
const footnoteItems = sidenotes . querySelectorAll ( ".footnote-item" )
const sum = xs => xs . reduce ( ( a , b ) => a + b , 0 )
const arrayOf = ( n , x ) => new Array ( n ) . fill ( x )
const BORDER = 16
const sidenotesAtSide = ( ) => getComputedStyle ( sidenotes ) . paddingLeft !== "0px"
let rendered = false
const relayout = forceRedraw => {
// sidenote column width is static: no need to redo positioning on resize unless no positions applied
if ( sidenotesAtSide ( ) ) {
if ( rendered && ! forceRedraw ) return
// sidenote vertical placement algorithm
const snRect = sidenotes . getBoundingClientRect ( )
const articleRect = article . getBoundingClientRect ( )
const exclusions = [ [ - Infinity , Math . max ( articleRect . top , snRect . top ) ] ]
for ( const codeblock of codeblocks ) {
const codeblockRect = codeblock . getBoundingClientRect ( )
if ( codeblockRect . width !== 0 ) { // collapsed
exclusions . push ( [ codeblockRect . top - BORDER , codeblockRect . top + codeblockRect . height + BORDER ] )
}
}
// convert unusable regions into list of usable regions
const inclusions = [ ]
for ( const [ start , end ] of exclusions ) {
if ( inclusions . length ) inclusions [ inclusions . length - 1 ] . end = start - snRect . top
inclusions . push ( { start : end - snRect . top , contents : [ ] } )
}
inclusions [ inclusions . length - 1 ] . end = Infinity
const notes = [ ]
// read off sidenotes to place
for ( const item of footnoteItems ) {
const link = article . querySelector ( ` # ${ item . id . replace ( /^fn/ , "fnref" ) } ` )
const linkRect = link . getBoundingClientRect ( )
item . style . position = "absolute"
item . style . left = getComputedStyle ( sidenotes ) . paddingLeft
2024-03-28 00:48:30 +00:00
item . style . paddingBottom = item . style . paddingTop = item . style . paddingRight = ` ${ BORDER / 2 } px `
2024-01-30 16:46:38 +00:00
const itemRect = item . getBoundingClientRect ( )
2023-11-19 21:06:25 +00:00
notes . push ( {
item ,
2024-01-30 16:46:38 +00:00
height : itemRect . height ,
2023-11-19 21:06:25 +00:00
target : linkRect . top - snRect . top
} )
}
// preliminary placement: place in valid regions going down
for ( const note of notes ) {
2024-03-28 00:48:30 +00:00
const index = Math . max ( inclusions . findLastIndex ( inc => inc . start < note . target ) , 0 )
2023-11-19 21:06:25 +00:00
const next = inclusions . slice ( index )
. findIndex ( inc => ( sum ( inc . contents . map ( x => x . height ) ) + note . height ) < ( inc . end - inc . start ) )
inclusions [ index + next ] . contents . push ( note )
}
// TODO: try simple moves between regions? might be useful sometimes
// place within region and apply styles
for ( const inc of inclusions ) {
const regionNotes = inc . contents
if ( regionNotes . length > 0 ) {
const variables = { }
const constraints = { }
if ( inc . end !== Infinity ) {
const heights = regionNotes . map ( note => note . height )
constraints [ "sum_gaps" ] = { max : inc . end - inc . start - sum ( heights ) }
}
regionNotes . forEach ( ( note , i ) => {
variables [ ` distbound_ ${ i } ` ] = {
"distsum" : 1 ,
[ ` distbound_ ${ i } _offset ` ] : 1 ,
[ ` distbound_ ${ i } _offset_neg ` ] : 1
}
const heightsum = sum ( regionNotes . slice ( 0 , i ) . map ( x => x . height ) )
const baseoffset = heightsum - note . target
// WANT: distbound_i >= placement_i - target_i AND distbound_i <= target_i - placement_i
// distbound_i >= gapsum_i + heightsum_i - target_i
// distbound_i_offset = distbound_i - gapsum_i
// so distbound_i_offset >= heightsum_i - target_i
// implies distbound_i - gapsum_i >= heightsum_i - target_i
// (as required)
// distbound_i + gapsum_i >= heightsum_i - target_i
constraints [ ` distbound_ ${ i } _offset ` ] = { min : baseoffset }
constraints [ ` distbound_ ${ i } _offset_neg ` ] = { min : - baseoffset }
constraints [ ` gap_ ${ i } ` ] = { min : 0 }
const G _i _var = { "sum_gaps" : 1 }
for ( let j = i ; j <= regionNotes . length ; j ++ ) G _i _var [ ` distbound_ ${ j } _offset ` ] = - 1
for ( let j = i ; j < regionNotes . length ; j ++ ) G _i _var [ ` distbound_ ${ j } _offset_neg ` ] = 1
variables [ ` gap_ ${ i } ` ] = G _i _var
} )
const model = {
direction : "minimize" ,
objective : "distsum" ,
constraints ,
variables
}
const solution = solve ( model , { includeZeroVariables : true } )
if ( solution . status !== "optimal" ) {
// implode
solution . variables = [ ]
console . warn ( "Sidenote layout failed" , solution . status )
}
const solutionVars = new Map ( solution . variables )
let position = 0
regionNotes . forEach ( ( note , i ) => {
position += solutionVars . get ( ` gap_ ${ i } ` ) || 0
note . item . style . top = position + "px"
position += note . height
} )
}
}
rendered = true
} else {
for ( const item of sidenotes . querySelectorAll ( ".footnote-item" ) ) {
item . style . position = "static"
}
rendered = false
}
2024-04-14 14:52:36 +01:00
for ( const item of sidenotes . querySelectorAll ( ".footnote-item" ) ) {
const link = article . querySelector ( ` # ${ item . id . replace ( /^fn/ , "fnref" ) } ` )
link . addEventListener ( "mouseover" , ( ) => {
item . classList . add ( "hl2" )
} )
link . addEventListener ( "mouseleave" , ( ) => {
item . classList . remove ( "hl2" )
} )
}
2023-11-19 21:06:25 +00:00
}
window . onresize = relayout
window . onload = relayout
document . querySelectorAll ( "summary" ) . forEach ( x => {
x . addEventListener ( "click" , ( ) => {
setTimeout ( ( ) => relayout ( true ) , 0 )
} )
} )
window . relayout = relayout
2024-03-28 00:48:30 +00:00
document . querySelectorAll ( "img" ) . forEach ( x => {
x . addEventListener ( "load" , ( ) => {
setTimeout ( ( ) => relayout ( true ) , 0 )
} )
} )
2023-11-19 21:06:25 +00:00
}
2024-04-14 14:52:36 +01:00
let previousHighlight
2024-01-30 16:46:38 +00:00
const fixDetailsSummary = ( ) => {
const el = document . getElementById ( window . location . hash . slice ( 1 ) )
var parent = el
if ( ! el ) return
2024-04-14 14:52:36 +01:00
if ( el . classList . contains ( "footnote-item" ) ) {
if ( previousHighlight ) {
previousHighlight . classList . remove ( "hl1" )
}
el . classList . add ( "hl1" )
previousHighlight = el
}
2024-01-30 16:46:38 +00:00
while ( parent . parentElement ) {
if ( parent . nodeName === "DETAILS" ) {
parent . setAttribute ( "open" , true )
}
parent = parent . parentElement
}
el . scrollIntoView ( )
}
window . addEventListener ( "hashchange" , fixDetailsSummary )
fixDetailsSummary ( )
2020-07-01 20:29:42 +01:00
const customStyle = localStorage . getItem ( "user-stylesheet" )
let customStyleEl = null
if ( customStyle ) {
customStyleEl = document . createElement ( "style" )
customStyleEl . appendChild ( document . createTextNode ( customStyle ) )
customStyleEl . onload = ( ) => console . log ( "Loaded custom styles" )
customStyleEl . id = "custom-style"
document . head . appendChild ( customStyleEl )
2023-11-19 21:06:25 +00:00
}
window . customStyleEl = customStyleEl
2024-08-31 18:36:07 +01:00
window . customStyle = customStyle
const nameMappings = {
"blog" : "Blog" ,
"microblog" : "Microblog" ,
"experiment" : "Experiments" ,
"mycorrhiza" : "Documentation"
}
2024-09-01 13:23:57 +01:00
const hues = {
"blog" : "210" ,
"microblog" : "290" ,
"mycorrhiza" : "30" ,
"experiment" : "120"
}
2024-08-31 18:36:07 +01:00
2024-08-31 18:48:00 +01:00
const closeOverlay = ( ) => {
if ( document . querySelector ( ".search-overlay" ) ) {
document . querySelector ( ".search-overlay" ) . remove ( )
}
document . querySelector ( "html" ) . style . overscrollBehavior = "none"
}
2024-08-31 18:36:07 +01:00
// replace login navbar option with search because whatever
const loginButton = document . querySelector ( "nav a:last-of-type" )
loginButton . href = "#"
loginButton . innerText = "Search"
loginButton . onclick = async ev => {
ev . preventDefault ( )
const query = ( await import ( "/assets/js/fts_client.js" ) ) . default
const overlay = document . createElement ( "div" )
overlay . classList . add ( "search-overlay" )
document . body . appendChild ( overlay )
2024-08-31 18:48:00 +01:00
document . querySelector ( "html" ) . style . overscrollBehavior = "none"
2024-08-31 18:36:07 +01:00
const input = document . createElement ( "input" )
input . type = "text"
input . placeholder = "Search"
let resultsEl
input . oninput = ( ) => {
if ( resultsEl ) {
resultsEl . remove ( )
}
resultsEl = document . createElement ( "div" )
resultsEl . classList . add ( "search-results" )
for ( const result of query ( input . value ) ) {
const item = e ( "search-result" , resultsEl )
const titleLine = nameMappings [ result . sourceType ] + " / " + ( result . title ? ? result . timestamp )
const link = e ( "search-result-link" , item , titleLine , "a" )
link . setAttribute ( "href" , result . url )
if ( result . title && result . timestamp ) {
e ( "deemph" , item , result . timestamp )
}
if ( result . description ) {
e ( "description" , item , result . description )
}
2024-09-01 13:23:57 +01:00
item . style . border = ` hsl( ${ hues [ result . sourceType ] } deg, 100%, 70%) solid 4px `
item . style . background = ` hsl( ${ hues [ result . sourceType ] } deg, 50%, 10%) `
2024-08-31 18:36:07 +01:00
}
overlay . appendChild ( resultsEl )
}
input . onkeydown = ev => {
if ( ev . key === "Enter" || ev . key === "Backspace" ) {
if ( input . value === "" ) {
// quit search mode
2024-08-31 18:48:00 +01:00
closeOverlay ( )
2024-08-31 18:36:07 +01:00
}
}
}
overlay . appendChild ( input )
input . focus ( )
2024-08-31 18:40:12 +01:00
}
window . addEventListener ( "keydown" , ev => {
if ( ev . key === "Escape" ) {
2024-08-31 18:48:00 +01:00
closeOverlay ( )
2024-08-31 18:40:12 +01:00
}
} )