123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- // 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.
- //
- // Upd8: As of 04/02/2021, it's now used for info cards too! Nice.
- 'use strict';
- let albumData, artistData, flashData;
- let officialAlbumData, fandomAlbumData, artistNames;
- let ready = false;
- // 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, key) {
- return getComputedStyle(el).getPropertyValue(key).trim();
- }
- function getRefDirectory(ref) {
- return ref.split(':')[1];
- }
- function getAlbum(el) {
- const directory = cssProp(el, '--album-directory');
- return albumData.find(album => album.directory === directory);
- }
- function getFlash(el) {
- const directory = cssProp(el, '--flash-directory');
- return flashData.find(flash => flash.directory === directory);
- }
- // 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}`);
- const openFlash = d => rebase(`flash/${d}`);
- function getTrackListAndIndex() {
- const album = getAlbum(document.body);
- const directory = cssProp(document.body, '--track-directory');
- if (!directory && !album) return {};
- if (!directory) return {list: album.tracks};
- const trackIndex = album.tracks.findIndex(track => track.directory === directory);
- return {list: album.tracks, index: trackIndex};
- }
- function openRandomTrack() {
- const { list } = getTrackListAndIndex();
- if (!list) return;
- return openTrack(pick(list));
- }
- function getFlashListAndIndex() {
- const list = flashData.filter(flash => !flash.act8r8k)
- const flash = getFlash(document.body);
- if (!flash) return {list};
- const flashIndex = list.indexOf(flash);
- return {list, index: flashIndex};
- }
- // TODO: This should also use urlSpec.
- function fetchData(type, directory) {
- return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData'))
- .then(res => res.json());
- }
- // JS-based links -----------------------------------------
- for (const a of document.body.querySelectorAll('[data-random]')) {
- a.addEventListener('click', evt => {
- if (!ready) {
- evt.preventDefault();
- return;
- }
- setTimeout(() => {
- a.href = rebase('js-disabled');
- });
- switch (a.dataset.random) {
- case 'album': return a.href = openAlbum(pick(albumData).directory);
- case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData).directory);
- case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData).directory);
- case 'track': return a.href = openTrack(getRefDirectory(pick(albumData.map(a => a.tracks).reduce((a, b) => a.concat(b), []))));
- case 'track-in-album': return a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)));
- case 'track-in-fandom': return a.href = openTrack(getRefDirectory(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))));
- case 'track-in-official': return a.href = openTrack(getRefDirectory(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))));
- case 'artist': return a.href = openArtist(pick(artistData).directory);
- case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistData.filter(artist => C.getArtistNumContributions(artist) > 1)).directory);
- }
- });
- }
- const next = document.getElementById('next-button');
- const previous = document.getElementById('previous-button');
- const random = document.getElementById('random-button');
- const prependTitle = (el, prepend) => {
- const existing = el.getAttribute('title');
- if (existing) {
- el.setAttribute('title', prepend + ' ' + existing);
- } else {
- el.setAttribute('title', prepend);
- }
- };
- if (next) prependTitle(next, '(Shift+N)');
- if (previous) prependTitle(previous, '(Shift+P)');
- if (random) prependTitle(random, '(Shift+R)');
- document.addEventListener('keypress', event => {
- if (event.shiftKey) {
- if (event.charCode === 'N'.charCodeAt(0)) {
- if (next) next.click();
- } else if (event.charCode === 'P'.charCodeAt(0)) {
- if (previous) previous.click();
- } else if (event.charCode === 'R'.charCodeAt(0)) {
- if (random && ready) random.click();
- }
- }
- });
- for (const reveal of document.querySelectorAll('.reveal')) {
- reveal.addEventListener('click', event => {
- if (!reveal.classList.contains('revealed')) {
- reveal.classList.add('revealed');
- event.preventDefault();
- event.stopPropagation();
- }
- });
- }
- const elements1 = document.getElementsByClassName('js-hide-once-data');
- const elements2 = document.getElementsByClassName('js-show-once-data');
- for (const element of elements1) element.style.display = 'block';
- fetch(rebase('data.json', 'rebaseShared')).then(data => data.json()).then(data => {
- albumData = data.albumData;
- artistData = data.artistData;
- flashData = data.flashData;
- officialAlbumData = albumData.filter(album => album.groups.includes('group:official'));
- fandomAlbumData = albumData.filter(album => !album.groups.includes('group:official'));
- artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name);
- for (const element of elements1) element.style.display = 'none';
- for (const element of elements2) element.style.display = 'block';
- ready = true;
- });
- // Data & info card ---------------------------------------
- const NORMAL_HOVER_INFO_DELAY = 750;
- const FAST_HOVER_INFO_DELAY = 250;
- const END_FAST_HOVER_DELAY = 500;
- const HIDE_HOVER_DELAY = 250;
- let fastHover = false;
- let endFastHoverTimeout = null;
- function colorLink(a, color) {
- if (color) {
- const { primary, dim } = C.getColors(color);
- a.style.setProperty('--primary-color', primary);
- a.style.setProperty('--dim-color', dim);
- }
- }
- function link(a, type, {name, directory, color}) {
- colorLink(a, color);
- a.innerText = name
- a.href = getLinkHref(type, directory);
- }
- function joinElements(type, elements) {
- // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on
- // strings. So instead, we'll pass the element's outer HTML's (which means
- // the entire HTML of that element).
- //
- // That does mean this function returns a string, so always 8e sure to
- // set innerHTML when using it (not appendChild).
- return list[type](elements.map(el => el.outerHTML));
- }
- const infoCard = (() => {
- const container = document.getElementById('info-card-container');
- let cancelShow = false;
- let hideTimeout = null;
- let showing = false;
- container.addEventListener('mouseenter', cancelHide);
- container.addEventListener('mouseleave', readyHide);
- function show(type, target) {
- cancelShow = false;
- fetchData(type, target.dataset[type]).then(data => {
- // Manual DOM 'cuz we're laaaazy.
- if (cancelShow) {
- return;
- }
- showing = true;
- const rect = target.getBoundingClientRect();
- container.style.setProperty('--primary-color', data.color);
- container.style.top = window.scrollY + rect.bottom + 'px';
- container.style.left = window.scrollX + rect.left + 'px';
- // Use a short timeout to let a currently hidden (or not yet shown)
- // info card teleport to the position set a8ove. (If it's currently
- // shown, it'll transition to that position.)
- setTimeout(() => {
- container.classList.remove('hide');
- container.classList.add('show');
- }, 50);
- // 8asic details.
- const nameLink = container.querySelector('.info-card-name a');
- link(nameLink, 'track', data);
- const albumLink = container.querySelector('.info-card-album a');
- link(albumLink, 'album', data.album);
- const artistSpan = container.querySelector('.info-card-artists span');
- artistSpan.innerHTML = joinElements('conjunction', data.artists.map(({ artist }) => {
- const a = document.createElement('a');
- a.href = getLinkHref('artist', artist.directory);
- a.innerText = artist.name;
- return a;
- }));
- const coverArtistParagraph = container.querySelector('.info-card-cover-artists');
- const coverArtistSpan = coverArtistParagraph.querySelector('span');
- if (data.coverArtists.length) {
- coverArtistParagraph.style.display = 'block';
- coverArtistSpan.innerHTML = joinElements('conjunction', data.coverArtists.map(({ artist }) => {
- const a = document.createElement('a');
- a.href = getLinkHref('artist', artist.directory);
- a.innerText = artist.name;
- return a;
- }));
- } else {
- coverArtistParagraph.style.display = 'none';
- }
- // Cover art.
- const [ containerNoReveal, containerReveal ] = [
- container.querySelector('.info-card-art-container.no-reveal'),
- container.querySelector('.info-card-art-container.reveal')
- ];
- const [ containerShow, containerHide ] = (data.cover.warnings.length
- ? [containerReveal, containerNoReveal]
- : [containerNoReveal, containerReveal]);
- containerHide.style.display = 'none';
- containerShow.style.display = 'block';
- const img = containerShow.querySelector('.info-card-art');
- img.src = rebase(data.cover.paths.small, 'rebaseMedia');
- const imgLink = containerShow.querySelector('a');
- colorLink(imgLink, data.color);
- imgLink.href = rebase(data.cover.paths.original, 'rebaseMedia');
- if (containerShow === containerReveal) {
- const cw = containerShow.querySelector('.info-card-art-warnings');
- cw.innerText = list.unit(data.cover.warnings);
- const reveal = containerShow.querySelector('.reveal');
- reveal.classList.remove('revealed');
- }
- });
- }
- function hide() {
- container.classList.remove('show');
- container.classList.add('hide');
- cancelShow = true;
- showing = false;
- }
- function readyHide() {
- if (!hideTimeout && showing) {
- hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY);
- }
- }
- function cancelHide() {
- if (hideTimeout) {
- clearTimeout(hideTimeout);
- hideTimeout = null;
- }
- }
- return {
- show,
- hide,
- readyHide,
- cancelHide
- };
- })();
- function makeInfoCardLinkHandlers(type) {
- let hoverTimeout = null;
- return {
- mouseenter(evt) {
- hoverTimeout = setTimeout(() => {
- fastHover = true;
- infoCard.show(type, evt.target);
- }, fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY);
- clearTimeout(endFastHoverTimeout);
- endFastHoverTimeout = null;
- infoCard.cancelHide();
- },
- mouseleave(evt) {
- clearTimeout(hoverTimeout);
- if (fastHover && !endFastHoverTimeout) {
- endFastHoverTimeout = setTimeout(() => {
- endFastHoverTimeout = null;
- fastHover = false;
- }, END_FAST_HOVER_DELAY);
- }
- infoCard.readyHide();
- }
- };
- }
- const infoCardLinkHandlers = {
- track: makeInfoCardLinkHandlers('track')
- };
- function addInfoCardLinkHandlers(type) {
- for (const a of document.querySelectorAll(`a[data-${type}]`)) {
- for (const [ eventName, handler ] of Object.entries(infoCardLinkHandlers[type])) {
- a.addEventListener(eventName, handler);
- }
- }
- }
- // Info cards are disa8led for now since they aren't quite ready for release,
- // 8ut you can try 'em out 8y setting this localStorage flag!
- //
- // localStorage.tryInfoCards = true;
- //
- if (localStorage.tryInfoCards) {
- addInfoCardLinkHandlers('track');
- }
|