« get me outta code hell

basic info card layout & hover behavior - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <towerofnix@gmail.com>2021-04-02 14:07:12 -0300
committer(quasar) nebula <towerofnix@gmail.com>2021-04-02 14:07:12 -0300
commitd8f9e7f7d71fcb786908213fe0513bf200e6b4a9 (patch)
treef5a31c4617325df0d082dd43999bda28b0882c3b
parent797f4f536e2888e6003806ac6ee8278da79f34d6 (diff)
basic info card layout & hover behavior
-rw-r--r--static/client.js154
-rw-r--r--static/site.css58
-rw-r--r--strings-default.json1
-rwxr-xr-xupd8.js10
4 files changed, 176 insertions, 47 deletions
diff --git a/static/client.js b/static/client.js
index 2b58ccb6..b7aa27b0 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 10ba090f..c62b4f68 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) {
diff --git a/strings-default.json b/strings-default.json
index e9dbf293..aa98eb9a 100644
--- a/strings-default.json
+++ b/strings-default.json
@@ -71,6 +71,7 @@
     "count.duration.approximate": "~{DURATION}",
     "count.duration.missing": "_:__",
     "releaseInfo.by": "By {ARTISTS}.",
+    "releaseInfo.from": "From {ALBUM}.",
     "releaseInfo.coverArtBy": "Cover art by {ARTISTS}.",
     "releaseInfo.wallpaperArtBy": "Wallpaper art by {ARTISTS}.",
     "releaseInfo.bannerArtBy": "Banner art by {ARTISTS}.",
diff --git a/upd8.js b/upd8.js
index fe1601a5..2ca90fe3 100755
--- a/upd8.js
+++ b/upd8.js
@@ -2387,6 +2387,15 @@ writePage.html = (pageFn, {paths, strings, to}) => {
         footerHTML
     ].filter(Boolean).join('\n');
 
+    const infoCardHTML = fixWS`
+        <div id="info-card-container">
+            <div class="info-card">
+                <h1 class="info-card-name"><a></a></h1>
+                <p class="info-card-album">${strings('releaseInfo.from', {album: '<a></a>'})}</p>
+            </div>
+        </div>
+    `;
+
     return filterEmptyLines(fixWS`
         <!DOCTYPE html>
         <html ${attributes({
@@ -2429,6 +2438,7 @@ writePage.html = (pageFn, {paths, strings, to}) => {
                     `}
                     ${layoutHTML}
                 </div>
+                ${infoCardHTML}
                 <script src="${to('shared.commonFile', `common.js?${CACHEBUST}`)}"></script>
                 <script src="${to('shared.staticFile', `client.js?${CACHEBUST}`)}"></script>
             </body>