From d8f9e7f7d71fcb786908213fe0513bf200e6b4a9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 2 Apr 2021 14:07:12 -0300 Subject: basic info card layout & hover behavior --- static/client.js | 154 +++++++++++++++++++++++++++++++++++++++++++++++++------ static/site.css | 58 ++++++++++----------- 2 files changed, 165 insertions(+), 47 deletions(-) (limited to 'static') diff --git a/static/client.js b/static/client.js index 2b58ccb..b7aa27b 100644 --- a/static/client.js +++ b/static/client.js @@ -2,6 +2,8 @@ // 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'; @@ -10,8 +12,10 @@ let officialAlbumData, fandomAlbumData, artistNames; let ready = false; +// Miscellaneous helpers ---------------------------------- + function rebase(href, rebaseKey = 'rebaseLocalized') { - const relative = document.documentElement.dataset[rebaseKey]; + const relative = document.documentElement.dataset[rebaseKey] + '/'; if (relative) { return relative + href; } else { @@ -43,25 +47,12 @@ function getFlash(el) { // 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}`); -/* i implemented these functions but we dont actually use them anywhere lol -function isFlashPage() { - return !!cssProp(document.body, '--flash-directory'); -} - -function isTrackOrAlbumPage() { - return !!cssProp(document.body, '--album-directory'); -} - -function isTrackPage() { - return !!cssProp(document.body, '--track-directory'); -} -*/ - function getTrackListAndIndex() { const album = getAlbum(document.body); const directory = cssProp(document.body, '--track-directory'); @@ -85,6 +76,14 @@ function getFlashListAndIndex() { return {list, index: flashIndex}; } +// TODO: This should also use urlSpec. +function fetchData(type, directory) { + return fetch(rebase(`data/${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) { @@ -167,3 +166,128 @@ fetch(rebase('data.json', 'rebaseShared')).then(data => data.json()).then(data = 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 link(a, type, {name, directory, color}) { + if (color) { + a.style.setProperty('--primary-color', color); + } + + a.innerText = name + a.href = getLinkHref(type, directory); +} + +const infoCard = (() => { + const container = document.getElementById('info-card-container'); + + let cancelShow = false; + let hideTimeout = null; + + 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; + } + + const rect = target.getBoundingClientRect(); + + container.style.setProperty('--primary-color', data.color); + + container.classList.add('shown'); + container.style.top = window.scrollY + rect.bottom + 'px'; + container.style.left = window.scrollX + rect.left + 'px'; + + const nameLink = container.querySelector('.info-card-name a'); + link(nameLink, 'track', data); + + const albumLink = container.querySelector('.info-card-album a'); + link(albumLink, 'album', data.links.album); + }); + } + + function hide() { + container.classList.remove('shown'); + cancelShow = true; + } + + function readyHide() { + if (!hideTimeout) { + 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); + } + } +} + +addInfoCardLinkHandlers('track'); diff --git a/static/site.css b/static/site.css index 10ba090..c62b4f6 100644 --- a/static/site.css +++ b/static/site.css @@ -335,7 +335,7 @@ footer > :last-child { .sidebar article h2, .news-index h2 { - border-bottom: 1px dotted white; + border-bottom: 1px dotted; } .sidebar article h2 time, @@ -701,46 +701,40 @@ li > ul { margin-top: 5px; } -.new { - animation: new 1s infinite; +#info-card-container { + position: absolute; + display: none; + + margin-right: 10px; } -@keyframes new { - 0% { - color: #bbdd00; - } +#info-card-container.shown { + display: block; +} - 50% { - color: #eeff22; - } +.info-card { + background-color: rgba(0, 0, 0, 0.8); + border: 1px dotted var(--primary-color); + border-radius: 3px; + box-shadow: 0 5px 5px black; - 100% { - color: #bbdd00; - } + padding: 5px; + font-size: 0.9em; } -/* fake :P */ -.loading::after { - content: '.'; - animation: loading 6s infinite; +.info-card-name { + font-size: 1em; + border-bottom: 1px dotted; + margin: 0; } -@keyframes loading { - 0 { - content: '.'; - } - - 33% { - content: '..'; - } - - 66% { - content: '...'; - } +.info-card p { + margin-top: 0.25em; + margin-bottom: 0.25em; +} - 100% { - content: '.'; - } +.info-card p:last-child { + margin-bottom: 0; } @media (max-width: 900px) { -- cgit 1.3.0-6-gf8a5