« get me outta code hell

content, test: linkContribution: tooltip icons - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-07-21 20:06:32 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-11-24 13:45:08 -0400
commitc11edada828dc734cce6988e5819630a73326085 (patch)
tree2754223a452ea3d6bc71cf12e6787f7c1ad40c37 /src
parenta6dedb0b17f514e408919240c060072df2b179dd (diff)
content, test: linkContribution: tooltip icons
Diffstat (limited to 'src')
-rw-r--r--src/content/dependencies/generateReleaseInfoContributionsLine.js1
-rw-r--r--src/content/dependencies/linkContribution.js79
-rw-r--r--src/static/client3.js150
-rw-r--r--src/static/site5.css42
4 files changed, 253 insertions, 19 deletions
diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js
index 1fa8dcca..2e6c4709 100644
--- a/src/content/dependencies/generateReleaseInfoContributionsLine.js
+++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js
@@ -35,6 +35,7 @@ export default {
             link.slots({
               showContribution: slots.showContribution,
               showIcons: slots.showIcons,
+              iconMode: 'tooltip',
             }))),
     });
   },
diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js
index 8e42f247..5bc398de 100644
--- a/src/content/dependencies/linkContribution.js
+++ b/src/content/dependencies/linkContribution.js
@@ -20,7 +20,6 @@ export default {
     if (!empty(contribution.who.urls)) {
       relations.artistIcons =
         contribution.who.urls
-          .slice(0, 4)
           .map(url => relation('linkExternalAsIcon', url));
     }
 
@@ -37,37 +36,79 @@ export default {
     showContribution: {type: 'boolean', default: false},
     showIcons: {type: 'boolean', default: false},
     preventWrapping: {type: 'boolean', default: true},
+
+    iconMode: {
+      validate: v => v.is('inline', 'tooltip'),
+      default: 'inline'
+    },
   },
 
   generate(data, relations, slots, {html, language}) {
-    const hasContributionPart = !!(slots.showContribution && data.what);
-    const hasExternalPart = !!(slots.showIcons && relations.artistIcons);
-
-    const externalLinks = hasExternalPart &&
-      html.tag('span',
-        {[html.noEdgeWhitespace]: true, class: 'icons'},
-        language.formatUnitList(relations.artistIcons));
+    const hasContribution = !!(slots.showContribution && data.what);
+    const hasExternalIcons = !!(slots.showIcons && relations.artistIcons);
 
     const parts = ['misc.artistLink'];
     const options = {artist: relations.artistLink};
 
-    if (hasContributionPart) {
+    if (hasContribution) {
       parts.push('withContribution');
       options.contrib = data.what;
     }
 
-    if (hasExternalPart) {
+    if (hasExternalIcons && slots.iconMode === 'inline') {
       parts.push('withExternalLinks');
-      options.links = externalLinks;
+      options.links =
+        html.tag('span',
+          {
+            [html.noEdgeWhitespace]: true,
+            class: ['icons', 'icons-inline'],
+          },
+          language.formatUnitList(
+            relations.artistIcons
+              .slice(0, 4)));
     }
 
-    const content = language.formatString(parts.join('.'), options);
+    let content = language.formatString(parts.join('.'), options);
 
-    return (
-      (parts.length > 1 && slots.preventWrapping
-        ? html.tag('span',
-            {[html.noEdgeWhitespace]: true, class: 'nowrap'},
-            content)
-        : content));
-    },
+    if (hasExternalIcons && slots.iconMode === 'tooltip') {
+      content = [
+        content,
+        html.tag('span',
+          {
+            [html.noEdgeWhitespace]: true,
+            class: ['icons', 'icons-tooltip'],
+            inert: true,
+          },
+          html.tag('span',
+            {
+              [html.noEdgeWhitespace]: true,
+              [html.joinChildren]: '',
+              class: 'icons-tooltip-content',
+            },
+            relations.artistIcons)),
+      ];
+    }
+
+    if (hasContribution || hasExternalIcons) {
+      content =
+        html.tag('span', {
+          [html.noEdgeWhitespace]: true,
+          [html.joinChildren]: '',
+
+          class: [
+            'contribution',
+
+            hasExternalIcons &&
+            slots.iconMode === 'tooltip' &&
+              'has-tooltip',
+
+            parts.length > 1 &&
+            slots.preventWrapping &&
+              'nowrap',
+          ],
+        }, content);
+    }
+
+    return content;
+  }
 };
diff --git a/src/static/client3.js b/src/static/client3.js
index 8372a268..091d1fcf 100644
--- a/src/static/client3.js
+++ b/src/static/client3.js
@@ -958,6 +958,8 @@ clientSteps.addPageListeners.push(addScrollListenerForStickyHeadings);
 
 // Image overlay ------------------------------------------
 
+// TODO: Update to clientSteps style.
+
 function addImageOverlayClickHandlers() {
   const container = document.getElementById('image-overlay-container');
 
@@ -1245,6 +1247,8 @@ function loadImage(imageUrl, onprogress) {
 
 // Group contributions table ------------------------------
 
+// TODO: Update to clientSteps style.
+
 const groupContributionsTableInfo =
   Array.from(document.querySelectorAll('#content dl'))
     .filter(dl => dl.querySelector('a.group-contributions-sort-button'))
@@ -1277,6 +1281,152 @@ for (const info of groupContributionsTableInfo) {
   });
 }
 
+// Artist link icon tooltips ------------------------------
+
+// TODO: Update to clientSteps style.
+
+const linkIconTooltipInfo =
+  Array.from(document.querySelectorAll('span.contribution.has-tooltip'))
+    .map(span => ({
+      mainLink: span.querySelector('a'),
+      iconsContainer: span.querySelector('span.icons-tooltip'),
+      iconLinks: span.querySelectorAll('span.icons-tooltip a'),
+    }));
+
+for (const info of linkIconTooltipInfo) {
+  const focusElements =
+    [info.mainLink, ...info.iconLinks];
+
+  const hoverElements =
+    [info.mainLink, info.iconsContainer];
+
+  let hidden = true;
+
+  const show = () => {
+    info.iconsContainer.classList.add('visible');
+    info.iconsContainer.inert = false;
+    hidden = false;
+  };
+
+  const hide = () => {
+    info.iconsContainer.classList.remove('visible');
+    info.iconsContainer.inert = true;
+    hidden = true;
+  };
+
+  const considerHiding = () => {
+    if (hoverElements.some(el => el.matches(':hover'))) {
+      return;
+    }
+
+    if (focusElements.includes(document.activeElement)) {
+      return;
+    }
+
+    if (justTouched) {
+      return;
+    }
+
+    hide();
+  };
+
+  // Hover (pointer)
+
+  let hoverTimeout;
+
+  info.mainLink.addEventListener('mouseenter', () => {
+    if (hidden) {
+      hoverTimeout = setTimeout(show, 250);
+    }
+  });
+
+  info.mainLink.addEventListener('mouseout', () => {
+    if (hidden) {
+      clearTimeout(hoverTimeout);
+    } else {
+      considerHiding();
+    }
+  });
+
+  info.iconsContainer.addEventListener('mouseout', () => {
+    if (!hidden) {
+      considerHiding();
+    }
+  });
+
+  // Focus (keyboard)
+
+  let focusTimeout;
+
+  info.mainLink.addEventListener('focus', () => {
+    focusTimeout = setTimeout(show, 750);
+  });
+
+  info.mainLink.addEventListener('blur', () => {
+    clearTimeout(focusTimeout);
+  });
+
+  info.iconsContainer.addEventListener('focusout', () => {
+    requestAnimationFrame(considerHiding);
+  });
+
+  info.mainLink.addEventListener('blur', () => {
+    requestAnimationFrame(considerHiding);
+  });
+
+  // Touch (finger)
+
+  let justTouched = false;
+  let touchTimeout;
+
+  info.mainLink.addEventListener('touchend', event => {
+    let wasTarget = false;
+
+    for (const touch of event.changedTouches) {
+      if (touch.target === info.mainLink) {
+        wasTarget = true;
+        break;
+      }
+    }
+
+    if (!wasTarget) {
+      return;
+    }
+
+    justTouched = true;
+
+    clearTimeout(touchTimeout);
+    touchTimeout = setTimeout(() => {
+      justTouched = false;
+    }, 250);
+
+    show();
+  });
+
+  info.mainLink.addEventListener('click', event => {
+    if (hidden && justTouched) {
+      event.preventDefault();
+      event.target.focus();
+      show();
+    }
+  });
+
+  document.body.addEventListener('touchend', event => {
+    const touches = [...event.changedTouches, ...event.touches];
+    for (const {clientX, clientY} of touches) {
+      const touchEl = document.elementFromPoint(clientX, clientY);
+      if (!touchEl) continue;
+
+      for (const hoverEl of hoverElements) {
+        if (touchEl === hoverEl) return;
+        if (hoverEl.contains(touchEl)) return;
+      }
+    }
+
+    hide();
+  });
+}
+
 // Sticky commentary sidebar ------------------------------
 
 const albumCommentarySidebarInfo = clientInfo.albumCommentarySidebarInfo = {
diff --git a/src/static/site5.css b/src/static/site5.css
index bb83fe67..06696799 100644
--- a/src/static/site5.css
+++ b/src/static/site5.css
@@ -427,6 +427,7 @@ a {
 
 a:hover {
   text-decoration: underline;
+  text-decoration-style: solid !important;
 }
 
 a.current {
@@ -472,11 +473,52 @@ a:not([href]):hover {
   white-space: nowrap;
 }
 
+.contribution {
+  position: relative;
+}
+
+.contribution.has-tooltip a {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
 .icons {
   font-style: normal;
   white-space: nowrap;
 }
 
+.icons-tooltip {
+  position: absolute;
+  z-index: 999;
+  left: -12px;
+  top: calc(1em - 2px);
+  padding: 4px 12px 6px 8px;
+}
+
+.icons-tooltip:not(.visible) {
+  display: none;
+}
+
+.icons-tooltip-content {
+  display: block;
+  padding: 6px 2px 2px 2px;
+  background: var(--bg-black-color);
+  border: 1px dotted var(--primary-color);
+  border-radius: 4px;
+
+  -webkit-user-select: none;
+  user-select: none;
+  cursor: default;
+}
+
+.icons a:hover {
+  filter: brightness(1.4);
+}
+
+.icons a {
+  padding: 0 3px;
+}
+
 .icon {
   display: inline-block;
   width: 24px;