From 9c00d5b47d82da80605d78b762777aeb9a972e21 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 May 2024 08:50:20 -0300 Subject: url-spec, web-routes, content: static subdirectories --- src/content/dependencies/generatePageLayout.js | 6 +- src/content/dependencies/image.js | 2 +- src/content/dependencies/linkExternalAsIcon.js | 2 +- src/static/client.js | 3483 ------------------------ src/static/css/site-basic.css | 19 + src/static/css/site.css | 2404 ++++++++++++++++ src/static/icons.svg | 43 - src/static/js/client.js | 3483 ++++++++++++++++++++++++ src/static/js/lazy-loading.js | 54 + src/static/js/xhr-util.js | 64 + src/static/lazy-loading.js | 54 - src/static/misc/icons.svg | 43 + src/static/misc/warning.svg | 93 + src/static/shared-util/README.md | 11 + src/static/site-basic.css | 19 - src/static/site.css | 2404 ---------------- src/static/warning.svg | 93 - src/static/xhr-util.js | 64 - src/url-spec.js | 37 +- src/web-routes.js | 16 +- 20 files changed, 6212 insertions(+), 6182 deletions(-) delete mode 100644 src/static/client.js create mode 100644 src/static/css/site-basic.css create mode 100644 src/static/css/site.css delete mode 100644 src/static/icons.svg create mode 100644 src/static/js/client.js create mode 100644 src/static/js/lazy-loading.js create mode 100644 src/static/js/xhr-util.js delete mode 100644 src/static/lazy-loading.js create mode 100644 src/static/misc/icons.svg create mode 100644 src/static/misc/warning.svg create mode 100644 src/static/shared-util/README.md delete mode 100644 src/static/site-basic.css delete mode 100644 src/static/site.css delete mode 100644 src/static/warning.svg delete mode 100644 src/static/xhr-util.js (limited to 'src') diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 3bb8b00a..5322950f 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -596,7 +596,7 @@ export default { html.tag('link', { rel: 'stylesheet', - href: to('static.path', 'site.css'), + href: to('staticCSS.path', 'site.css'), }), html.tag('style', [ @@ -606,7 +606,7 @@ export default { ]), html.tag('script', { - src: to('static.path', 'lazy-loading.js'), + src: to('staticJS.path', 'lazy-loading.js'), }), ]), @@ -636,7 +636,7 @@ export default { html.tag('script', { type: 'module', - src: to('static.path', 'client.js'), + src: to('staticJS.path', 'client.js'), }), ]), ]) diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 617065fc..22fa2655 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -170,7 +170,7 @@ export default { if (willReveal) { reveal = [ html.tag('img', {class: 'reveal-symbol'}, - {src: to('static.path', 'warning.svg')}), + {src: to('staticMisc.icon', 'warning.svg')}), html.tag('br'), diff --git a/src/content/dependencies/linkExternalAsIcon.js b/src/content/dependencies/linkExternalAsIcon.js index 7f5ea432..e2ce4b3c 100644 --- a/src/content/dependencies/linkExternalAsIcon.js +++ b/src/content/dependencies/linkExternalAsIcon.js @@ -37,7 +37,7 @@ export default { html.tag('title', platformText), html.tag('use', { - href: to('static.icon', iconId), + href: to('staticMisc.icon', iconId), }), ]), diff --git a/src/static/client.js b/src/static/client.js deleted file mode 100644 index 729836b5..00000000 --- a/src/static/client.js +++ /dev/null @@ -1,3483 +0,0 @@ -/* eslint-env browser */ - -// This is the JS file that gets loaded on the client! It's only really used for -// the random track feature right now - the idea is we only use it for stuff -// that cannot 8e done at static-site compile time, 8y its fundamentally -// ephemeral nature. - -import {accumulateSum, empty, filterMultipleArrays, stitchArrays} - from '../util/sugar.js'; -import {fetchWithProgress} from './xhr-util.js'; - -const clientInfo = window.hsmusicClientInfo = Object.create(null); - -const clientSteps = { - getPageReferences: [], - addInternalListeners: [], - mutatePageContent: [], - initializeState: [], - addPageListeners: [], -}; - -function initInfo(infoKey, description) { - const object = {...description}; - - for (const obj of [ - object, - object.state, - object.setting, - object.event, - ]) { - if (!obj) continue; - Object.preventExtensions(obj); - } - - if (object.session) { - const sessionDefaults = object.session; - - object.session = {}; - - for (const [key, defaultValue] of Object.entries(sessionDefaults)) { - const storageKey = `hsmusic.${infoKey}.${key}`; - - let fallbackValue = defaultValue; - - Object.defineProperty(object.session, key, { - get: () => { - try { - return sessionStorage.getItem(storageKey) ?? defaultValue; - } catch (error) { - if (error instanceof DOMException) { - return fallbackValue; - } else { - throw error; - } - } - }, - - set: (value) => { - try { - sessionStorage.setItem(storageKey, value); - } catch (error) { - if (error instanceof DOMException) { - fallbackValue = value; - } else { - throw error; - } - } - }, - }); - } - - Object.preventExtensions(object.session); - } - - clientInfo[infoKey] = object; - - return object; -} - -// Localiz8tion nonsense ---------------------------------- - -/* -const language = document.documentElement.getAttribute('lang'); - -let list; -if (typeof Intl === 'object' && typeof Intl.ListFormat === 'function') { - const getFormat = (type) => { - const formatter = new Intl.ListFormat(language, {type}); - return formatter.format.bind(formatter); - }; - - list = { - conjunction: getFormat('conjunction'), - disjunction: getFormat('disjunction'), - unit: getFormat('unit'), - }; -} else { - // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free. - // We use the same mock for every list 'cuz we don't have any of the - // necessary CLDR info to appropri8tely distinguish 8etween them. - const arbitraryMock = (array) => array.join(', '); - - list = { - conjunction: arbitraryMock, - disjunction: arbitraryMock, - unit: arbitraryMock, - }; -} -*/ - -// Miscellaneous helpers ---------------------------------- - -function rebase(href, rebaseKey = 'rebaseLocalized') { - const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/'; - if (relative) { - return relative + href; - } else { - return href; - } -} - -function pick(array) { - return array[Math.floor(Math.random() * array.length)]; -} - -function cssProp(el, ...args) { - if (typeof args[0] === 'string' && args.length === 1) { - return getComputedStyle(el).getPropertyValue(args[0]).trim(); - } - - if (typeof args[0] === 'string' && args.length === 2) { - if (args[1] === null) { - el.style.removeProperty(args[0]); - } else { - el.style.setProperty(args[0], args[1]); - } - return; - } - - if (typeof args[0] === 'object') { - for (const [property, value] of Object.entries(args[0])) { - cssProp(el, property, value); - } - } -} - -// Curry-style, so multiple points can more conveniently be tested at once. -function pointIsOverAnyOf(elements) { - return (clientX, clientY) => { - const element = document.elementFromPoint(clientX, clientY); - return elements.some(el => el.contains(element)); - }; -} - -function getVisuallyContainingElement(child) { - let parent = child.parentElement; - - while (parent) { - if ( - cssProp(parent, 'overflow') === 'hidden' || - cssProp(parent, 'contain') === 'paint' - ) { - return parent; - } - - parent = parent.parentElement; - } - - return null; -} - -// TODO: These should pro8a8ly access some shared urlSpec path. We'd need to -// separ8te the tooling around that into common-shared code too. - -/* -const getLinkHref = (type, directory) => rebase(`${type}/${directory}`); -*/ - -const openAlbum = (d) => rebase(`album/${d}`); -const openTrack = (d) => rebase(`track/${d}`); -const openArtist = (d) => rebase(`artist/${d}`); - -// TODO: This should also use urlSpec. - -/* -function fetchData(type, directory) { - return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData')).then( - (res) => res.json() - ); -} -*/ - -function dispatchInternalEvent(event, eventName, ...args) { - const [infoName] = - Object.entries(clientInfo) - .find(pair => pair[1].event === event); - - if (!infoName) { - throw new Error(`Expected event to be stored on clientInfo`); - } - - const {[eventName]: listeners} = event; - - if (!listeners) { - throw new Error(`Event name "${eventName}" isn't stored on ${infoName}.event`); - } - - let results = []; - for (const listener of listeners) { - try { - results.push(listener(...args)); - } catch (error) { - console.warn(`Uncaught error in listener for ${infoName}.${eventName}`); - console.debug(error); - results.push(undefined); - } - } - - return results; -} - -// Rectangle math ----------------------------------------- - -class WikiRect extends DOMRect { - // Useful constructors - - static fromWindow() { - const {clientWidth: width, clientHeight: height} = - document.documentElement; - - return Reflect.construct(this, [0, 0, width, height]); - } - - static fromElement(element) { - return this.fromRect(element.getBoundingClientRect()); - } - - static fromMouse() { - const {clientX, clientY} = liveMousePositionInfo.state; - - return WikiRect.fromRect({ - x: clientX, - y: clientY, - width: 0, - height: 0, - }); - } - - static fromElementUnderMouse(element) { - const mouseRect = WikiRect.fromMouse(); - - const rects = - Array.from(element.getClientRects()) - .map(rect => WikiRect.fromRect(rect)); - - const rectUnderMouse = - rects.find(rect => rect.contains(mouseRect)); - - if (rectUnderMouse) { - return rectUnderMouse; - } else { - return rects[0]; - } - } - - static leftOf(origin, offset = 0) { - // Returns a rectangle representing everywhere to the left of the provided - // point or rectangle (with no top or bottom bounds), towards negative x. - // If an offset is provided, this is added onto the origin. - - return this.#past(origin, offset, { - origin: 'x', - extent: 'width', - edge: 'left', - direction: -Infinity, - construct: from => - [from, -Infinity, -Infinity, Infinity], - }); - } - - static rightOf(origin, offset = 0) { - // Returns a rectangle representing everywhere to the right of the - // provided point or rectangle (with no top or bottom bounds), towards - // positive x. If an offset is provided, this is added onto the origin. - - return this.#past(origin, offset, { - origin: 'x', - extent: 'width', - edge: 'right', - direction: Infinity, - construct: from => - [from, -Infinity, Infinity, Infinity], - }); - } - - static above(origin, offset = 0) { - // Returns a rectangle representing everywhere above the provided point - // or rectangle (with no left or right bounds), towards negative y. - // If an offset is provided, this is added onto the origin. - - return this.#past(origin, offset, { - origin: 'y', - extent: 'height', - edge: 'top', - direction: -Infinity, - construct: from => - [-Infinity, from, Infinity, -Infinity], - }); - } - - static beneath(origin, offset = 0) { - // Returns a rectangle representing everywhere beneath the provided point - // or rectangle (with no left or right bounds), towards positive y. - // If an offset is provided, this is added onto the origin. - - return this.#past(origin, offset, { - origin: 'y', - extent: 'height', - edge: 'bottom', - direction: Infinity, - construct: from => - [-Infinity, from, Infinity, Infinity], - }); - } - - // Constructor helpers - - static #past(origin, offset, opts) { - if (!isFinite(offset)) { - throw new TypeError(`Didn't expect infinite offset`); - } - - const {direction, edge} = opts; - - if (typeof origin === 'object') { - const {origin: originProperty, extent: extentProperty} = opts; - - const normalized = - WikiRect.fromRect(origin).toNormalized(); - - if (normalized[extentProperty] === direction) { - throw new TypeError(`Provided rectangle already extends to ${edge} edge`); - } - - if (normalized[extentProperty] === -direction) { - return this.#past(normalized[originProperty], offset, opts); - } - - if (normalized.y === direction) { - throw new TypeError(`Provided rectangle already starts at ${edge} edge`); - } - - return this.#past(normalized[edge], offset, opts); - } - - const {construct} = opts; - - if (origin === direction) { - throw new TypeError(`Provided point is already at ${edge} edge`); - } - - return Reflect.construct(this, construct(origin + offset)).toNormalized(); - } - - // Predicates - - static rejectInfiniteOriginNonZeroFiniteExtent({origin, extent}) { - // Indicate that, in this context, it's meaningless to provide - // a finite extent starting at an infinite origin and going towards - // or away from zero (i.e. a rectangle along a cardinal edge). - - if (!isFinite(origin) && isFinite(extent) && extent !== 0) { - throw new TypeError(`Didn't expect infinite origin paired with finite extent`); - } - } - - static rejectInfiniteOriginZeroExtent({origin, extent}) { - // Indicate that, in this context, it's meaningless to provide - // a zero extent at an infinite origin (i.e. a cardinal edge). - - if (!isFinite(origin) && extent === 0) { - throw new TypeError(`Didn't expect infinite origin paired with zero extent`); - } - } - - static rejectNonOpposingInfiniteOriginInfiniteExtent({origin, extent}) { - // Indicate that, in this context, it's meaningless to provide - // an infinite extent going in the same direction as its infinite - // origin (an area "infinitely past" a cardinal edge). - - if (!isFinite(origin) && origin === extent) { - throw new TypeError(`Didn't expect non-opposing infinite origin and extent`); - } - } - - // Transformations - - static normalizeOriginExtent({origin, extent}) { - // Varying behavior based on inputs: - // - // - For finite origin and finite extent, flip the orientation - // (if necessary) so that extent is positive. - // - For finite origin and infinite extent (i.e. an origin up to - // a cardinal edge), leave as-is. - // - For infinite origin and infinite extent, flip the orientation - // (if necessary) so origin is negative and extent is positive. - // - For infinite origin and zero extent (i.e. a cardinal edge), - // leave as-is. - // - For all other cases, error. - // - - this.rejectInfiniteOriginNonZeroFiniteExtent({origin, extent}); - this.rejectNonOpposingInfiniteOriginInfiniteExtent({origin, extent}); - - if (isFinite(origin) && isFinite(extent) && extent < 0) { - return {origin: origin + extent, extent: -extent}; - } - - if (!isFinite(origin) && !isFinite(extent)) { - return {origin: -Infinity, extent: Infinity}; - } - - return {origin, extent}; - } - - toNormalized() { - const {origin: newX, extent: newWidth} = - WikiRect.normalizeOriginExtent({ - origin: this.x, - extent: this.width, - }); - - const {origin: newY, extent: newHeight} = - WikiRect.normalizeOriginExtent({ - origin: this.y, - extent: this.height, - }); - - return Reflect.construct(this.constructor, [newX, newY, newWidth, newHeight]); - } - - static intersectionFromOriginsExtents(...entries) { - // An intersection is the common subsection across two or more regions. - - const [first, second, ...rest] = entries; - - if (entries.length >= 3) { - return this.intersection(first, this.intersection(second, ...rest)); - } - - if (entries.length === 2) { - if (first === null || second === null) { - return null; - } - - this.rejectInfiniteOriginZeroExtent(first); - this.rejectInfiniteOriginZeroExtent(second); - - const {origin: origin1, extent: extent1} = this.normalizeOriginExtent(first); - const {origin: origin2, extent: extent2} = this.normalizeOriginExtent(second); - - // After normalizing, *each* region will be one of these: - // - // - Finite origin, finite extent - // (a standard region, bounded on both sides) - // - Finite origin, infinite extent - // (everything to one direction of a given origin) - // - Infinite origin, infinite extent - // (everything everywhere) - // - // So we need to handle any *combination* of these kinds of regions. - - // If either origin is infinite, that region represents everywhere, - // so it'll never limit the region of the other. - - if (!isFinite(origin1)) { - return {origin: origin2, extent: extent2}; - } - - if (!isFinite(origin2)) { - return {origin: origin1, extent: extent1}; - } - - // If neither origin is infinite, both regions are bounded on at least - // one side, and may limit the other accordingly. Find the minimum and - // maximum points in each region, letting Infinity propagate through, - // which represents no boundary in that direction. - - const minimum1 = Math.min(origin1, origin1 + extent1); - const minimum2 = Math.min(origin2, origin2 + extent2); - const maximum1 = Math.max(origin1, origin1 + extent1); - const maximum2 = Math.max(origin2, origin2 + extent2); - - // Now get the maximum of the regions' minimums, and the minimum of the - // regions' maximums. These are the limits of the new region; computing - // with minimums and maximums in this way "polarizes" the limits, so we - // can perform specific polarized math in the following steps. - // - // Infinity will also propagate here, but with some important - // restricitons: only maxOfMinimums can be positive Infinity, and only - // minOfMaximums can be negative Infinity; and if either is Infinity, - // the other is not, since otherwise we'd be working with two everywhere - // regions, and would've just returned an everywhere region above. - - const maxOfMinimums = Math.max(minimum1, minimum2); - const minOfMaximums = Math.min(maximum1, maximum2); - - // Now check if the maximum of minimums is greater than the minimum of - // maximums. If so, the regions don't have any overlap - one region - // limits the overlap to end before the other region starts. This works - // because we've polarized the limits above! - - if (maxOfMinimums > minOfMaximums) { - return null; - } - - // Otherwise there's at least some overlap, even if it's just one point - // (i.e. one ends exactly where the other begins). We have to take care - // of infinities in particular, now. As mentioned above, only one of the - // points will be infinity (at most). So the origin is the non-infinite - // point, and the extent is in the direction of the infinite point. - - if (minOfMaximums === -Infinity) { - return {origin: maxOfMinimums, extent: -Infinity}; - } - - if (maxOfMinimums === Infinity) { - return {origin: minOfMaximums, extent: Infinity}; - } - - // If neither point is infinity, we're working with two regions that are - // both bounded on both sides, so the overlapping region is just the - // region constrained by the limits above. Since these are polarized, - // start from maxOfMinimums and extend to minOfMaximums, resulting in - // a standard, already-normalized region. - - return { - origin: maxOfMinimums, - extent: minOfMaximums - maxOfMinimums, - }; - } - - if (entries.length === 1) { - return first; - } - - throw new TypeError(`Expected at least one {origin, extent} entry`); - } - - intersectionWith(rect) { - const horizontalIntersection = - WikiRect.intersectionFromOriginsExtents( - {origin: this.x, extent: this.width}, - {origin: rect.x, extent: rect.width}); - - const verticalIntersection = - WikiRect.intersectionFromOriginsExtents( - {origin: this.y, extent: this.height}, - {origin: rect.y, extent: rect.height}); - - if (!horizontalIntersection) return null; - if (!verticalIntersection) return null; - - const {origin: x, extent: width} = horizontalIntersection; - const {origin: y, extent: height} = verticalIntersection; - - return Reflect.construct(this.constructor, [x, y, width, height]); - } - - chopExtendingOutside(rect) { - this.intersectionWith(rect).writeOnto(this); - } - - static insetOriginExtent({origin, extent, start = 0, end = 0}) { - const normalized = - this.normalizeOriginExtent({origin, extent}); - - // If this would crush the bounds past each other, just return - // the halfway point. - if (extent < start + end) { - return {origin: origin + (start + end) / 2, extent: 0}; - } - - return { - origin: normalized.origin + start, - extent: normalized.extent - start - end, - }; - } - - toInset(arg1, arg2) { - if (typeof arg1 === 'number' && typeof arg2 === 'number') { - return this.toInset({ - left: arg2, - right: arg2, - top: arg1, - bottom: arg1, - }); - } else if (typeof arg1 === 'number') { - return this.toInset({ - left: arg1, - right: arg1, - top: arg1, - bottom: arg1, - }); - } - - const {top, left, bottom, right} = arg1; - - const {origin: x, extent: width} = - WikiRect.insetOriginExtent({ - origin: this.x, - extent: this.width, - start: left, - end: right, - }); - - const {origin: y, extent: height} = - WikiRect.insetOriginExtent({ - origin: this.y, - extent: this.height, - start: top, - end: bottom, - }); - - return Reflect.construct(this.constructor, [x, y, width, height]); - } - - static extendOriginExtent({origin, extent, start = 0, end = 0}) { - const normalized = - this.normalizeOriginExtent({origin, extent}); - - return { - origin: normalized.origin - start, - extent: normalized.extent + start + end, - }; - } - - toExtended(arg1, arg2) { - if (typeof arg1 === 'number' && typeof arg2 === 'number') { - return this.toExtended({ - left: arg2, - right: arg2, - top: arg1, - bottom: arg1, - }); - } else if (typeof arg1 === 'number') { - return this.toExtended({ - left: arg1, - right: arg1, - top: arg1, - bottom: arg1, - }); - } - - const {top, left, bottom, right} = arg1; - - const {origin: x, extent: width} = - WikiRect.extendOriginExtent({ - origin: this.x, - extent: this.width, - start: left, - end: right, - }); - - const {origin: y, extent: height} = - WikiRect.extendOriginExtent({ - origin: this.y, - extent: this.height, - start: top, - end: bottom, - }); - - return Reflect.construct(this.constructor, [x, y, width, height]); - } - - // Comparisons - - equals(rect) { - const rectNormalized = WikiRect.fromRect(rect).toNormalized(); - const thisNormalized = this.toNormalized(); - - return ( - rectNormalized.x === thisNormalized.x && - rectNormalized.y === thisNormalized.y && - rectNormalized.width === thisNormalized.width && - rectNormalized.height === thisNormalized.height - ); - } - - contains(rect) { - return !!this.intersectionWith(rect)?.equals(rect); - } - - containedWithin(rect) { - return !!this.intersectionWith(rect)?.equals(this); - } - - fits(rect) { - const rectNormalized = WikiRect.fromRect(rect).toNormalized(); - const thisNormalized = this.toNormalized(); - - return ( - (!isFinite(this.width) || rectNormalized.width <= thisNormalized.width) && - (!isFinite(this.height) || rectNormalized.height <= thisNormalized.height) - ); - } - - fitsWithin(rect) { - const rectNormalized = WikiRect.fromRect(rect).toNormalized(); - const thisNormalized = this.toNormalized(); - - return ( - (!isFinite(rect.width) || thisNormalized.width <= rectNormalized.width) && - (!isFinite(rect.height) || thisNormalized.height <= rectNormalized.height) - ); - } - - // Interfacing utilities - - static fromRect(rect) { - return Reflect.construct(this, [rect.x, rect.y, rect.width, rect.height]); - } - - writeOnto(destination) { - Object.assign(destination, { - x: this.x, - y: this.y, - width: this.width, - height: this.height, - }); - } -} - -// CSS compatibility-assistant ---------------------------- - -const cssCompatibilityAssistantInfo = clientInfo.cssCompatibilityAssistantInfo = { - coverArtContainer: null, - coverArtImageDetails: null, -}; - -function getCSSCompatibilityAssistantInfoReferences() { - const info = cssCompatibilityAssistantInfo; - - info.coverArtContainer = - document.getElementById('cover-art-container'); - - info.coverArtImageDetails = - info.coverArtContainer?.querySelector('.image-details'); -} - -function mutateCSSCompatibilityContent() { - const info = cssCompatibilityAssistantInfo; - - if (info.coverArtImageDetails) { - info.coverArtContainer.classList.add('has-image-details'); - } -} - -clientSteps.getPageReferences.push(getCSSCompatibilityAssistantInfoReferences); -clientSteps.mutatePageContent.push(mutateCSSCompatibilityContent); - -// Ever-updating mouse position helper -------------------- - -const liveMousePositionInfo = initInfo('liveMousePositionInfo', { - state: { - clientX: null, - clientY: null, - }, -}); - -function addLiveMousePositionPageListeners() { - const info = liveMousePositionInfo; - const {state} = info; - - document.body.addEventListener('mousemove', domEvent => { - Object.assign(state, { - clientX: domEvent.clientX, - clientY: domEvent.clientY, - }); - }); -} - -clientSteps.addPageListeners.push(addLiveMousePositionPageListeners); - -// JS-based links ----------------------------------------- - -const scriptedLinkInfo = initInfo('scriptedLinkInfo', { - randomLinks: null, - revealLinks: null, - revealContainers: null, - - nextNavLink: null, - previousNavLink: null, - randomNavLink: null, - - state: { - albumDirectories: null, - albumTrackDirectories: null, - artistDirectories: null, - artistNumContributions: null, - }, -}); - -function getScriptedLinkReferences() { - scriptedLinkInfo.randomLinks = - document.querySelectorAll('[data-random]'); - - scriptedLinkInfo.revealLinks = - document.querySelectorAll('.reveal .image-outer-area > *'); - - scriptedLinkInfo.revealContainers = - Array.from(scriptedLinkInfo.revealLinks) - .map(link => link.closest('.reveal')); - - scriptedLinkInfo.nextNavLink = - document.getElementById('next-button'); - - scriptedLinkInfo.previousNavLink = - document.getElementById('previous-button'); - - scriptedLinkInfo.randomNavLink = - document.getElementById('random-button'); -} - -function addRandomLinkListeners() { - for (const a of scriptedLinkInfo.randomLinks ?? []) { - a.addEventListener('click', domEvent => { - handleRandomLinkClicked(a, domEvent); - }); - } -} - -function handleRandomLinkClicked(a, domEvent) { - const href = determineRandomLinkHref(a); - - if (!href) { - domEvent.preventDefault(); - return; - } - - setTimeout(() => { - a.href = '#' - }); - - a.href = href; -} - -function determineRandomLinkHref(a) { - const {state} = scriptedLinkInfo; - - const trackDirectoriesFromAlbumDirectories = albumDirectories => - albumDirectories - .map(directory => state.albumDirectories.indexOf(directory)) - .map(index => state.albumTrackDirectories[index]) - .reduce((acc, trackDirectories) => acc.concat(trackDirectories, [])); - - switch (a.dataset.random) { - case 'album': { - const {albumDirectories} = state; - if (!albumDirectories) return null; - - return openAlbum(pick(albumDirectories)); - } - - case 'track': { - const {albumDirectories} = state; - if (!albumDirectories) return null; - - const trackDirectories = - trackDirectoriesFromAlbumDirectories( - albumDirectories); - - return openTrack(pick(trackDirectories)); - } - - case 'album-in-group-dl': { - const albumLinks = - Array.from(a - .closest('dt') - .nextElementSibling - .querySelectorAll('li a')) - - const listAlbumDirectories = - albumLinks - .map(a => cssProp(a, '--album-directory')); - - return openAlbum(pick(listAlbumDirectories)); - } - - case 'track-in-group-dl': { - const {albumDirectories} = state; - if (!albumDirectories) return null; - - const albumLinks = - Array.from(a - .closest('dt') - .nextElementSibling - .querySelectorAll('li a')) - - const listAlbumDirectories = - albumLinks - .map(a => cssProp(a, '--album-directory')); - - const trackDirectories = - trackDirectoriesFromAlbumDirectories( - listAlbumDirectories); - - return openTrack(pick(trackDirectories)); - } - - case 'track-in-sidebar': { - // Note that the container for track links may be
    or