« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/data/composite/wiki-properties/commentatorArtists.js1
-rw-r--r--src/data/things/language.js2
-rw-r--r--src/static/client3.js198
-rw-r--r--src/static/site6.css14
4 files changed, 162 insertions, 53 deletions
diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js
index f400bbfc..c5c14769 100644
--- a/src/data/composite/wiki-properties/commentatorArtists.js
+++ b/src/data/composite/wiki-properties/commentatorArtists.js
@@ -2,7 +2,6 @@
 // This is mostly useful for credits and listings on artist pages.
 
 import {input, templateCompositeFrom} from '#composite';
-import {unique} from '#sugar';
 
 import {exitWithoutDependency, exposeDependency}
   from '#composite/control-flow';
diff --git a/src/data/things/language.js b/src/data/things/language.js
index 70481299..d8af9620 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -331,6 +331,8 @@ export class Language extends Thing {
       });
     }
 
+    isExternalLinkStyle(style);
+
     return getExternalLinkStringOfStyleFromDescriptors(url, style, this.externalLinkSpec, {
       language: this,
       context,
diff --git a/src/static/client3.js b/src/static/client3.js
index 866b9ba2..86b5f985 100644
--- a/src/static/client3.js
+++ b/src/static/client3.js
@@ -63,8 +63,25 @@ function pick(array) {
   return array[Math.floor(Math.random() * array.length)];
 }
 
-function cssProp(el, key) {
-  return getComputedStyle(el).getPropertyValue(key).trim();
+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);
+    }
+  }
 }
 
 // TODO: These should pro8a8ly access some shared urlSpec path. We'd need to
@@ -383,6 +400,12 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = {
     focusInfoDelay: 750,
 
     hideTooltipDelay: 500,
+
+    // If a tooltip that's transitioning to hidden is hovered during the grace
+    // period (or the corresponding hoverable is hovered at any point in the
+    // transition), it'll cancel out of this animation immediately.
+    transitionHiddenDuration: 300,
+    inertGracePeriod: 100,
   },
 
   state: {
@@ -399,8 +422,12 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = {
     focusTimeout: null,
     touchTimeout: null,
     hideTimeout: null,
+    transitionHiddenTimeout: null,
+    inertGracePeriodTimeout: null,
     currentlyShownTooltip: null,
     currentlyActiveHoverable: null,
+    currentlyTransitioningHiddenTooltip: null,
+    previouslyActiveHoverable: null,
     tooltipWasJustHidden: false,
     hoverableWasRecentlyTouched: false,
 
@@ -420,11 +447,6 @@ const hoverableTooltipInfo = clientInfo.hoverableTooltipInfo = {
     currentTouchIdentifiers: new Set(),
     touchIdentifiersBanishedByScrolling: new Set(),
   },
-
-  event: {
-    whenTooltipShouldBeShown: [],
-    whenTooltipShouldBeHidden: [],
-  },
 };
 
 // Adds DOM event listeners, so must be called during addPageListeners step.
@@ -508,9 +530,15 @@ function registerTooltipHoverableElement(hoverable, tooltip) {
 function handleTooltipMouseEntered(tooltip) {
   const {state} = hoverableTooltipInfo;
 
+  if (state.currentlyTransitioningHiddenTooltip) {
+    cancelTransitioningTooltipHidden(true);
+    return;
+  }
+
   if (state.currentlyShownTooltip !== tooltip) return;
 
   // Don't time out the current tooltip while hovering it.
+
   if (state.hideTimeout) {
     clearTimeout(state.hideTimeout);
     state.hideTimeout = null;
@@ -548,21 +576,35 @@ function handleTooltipReceivedFocus(tooltip) {
 function handleTooltipLostFocus(tooltip) {
   const {settings, state} = hoverableTooltipInfo;
 
-  // Hide the current tooltip right away when it loses focus.
-  hideCurrentlyShownTooltip();
+  // Hide the current tooltip right away when it loses focus. Specify intent
+  // to replace - while we don't strictly know if another tooltip is going to
+  // immediately replace it, the mode of navigating with tab focus (once one
+  // tooltip has been activated) is a "switch focus immediately" kind of
+  // interaction in its nature.
+  hideCurrentlyShownTooltip(true);
 }
 
 function handleTooltipHoverableMouseEntered(hoverable) {
   const {event, settings, state} = hoverableTooltipInfo;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  // If this tooltip was transitioning to hidden, hovering should cancel that
+  // animation and show it immediately.
+
+  if (tooltip === state.currentlyTransitioningHiddenTooltip) {
+    cancelTransitioningTooltipHidden(true);
+    return;
+  }
+
+  // Start a timer to show the corresponding tooltip, with the delay depending
+  // on whether fast hovering or not. This could be canceled by mousing out of
+  // the hoverable.
 
   const hoverTimeoutDelay =
     (state.fastHovering
       ? settings.fastHoveringInfoDelay
       : settings.normalHoverInfoDelay);
 
-  // Start a timer to show the corresponding tooltip, with the delay depending
-  // on whether fast hovering or not. This could be canceled by mousing out of
-  // the hoverable.
   state.hoverTimeout =
     setTimeout(() => {
       state.hoverTimeout = null;
@@ -650,9 +692,10 @@ function handleTooltipHoverableLostFocus(hoverable, domEvent) {
 
   // Unless focus is entering the tooltip itself, hide the tooltip immediately.
   // This will set the tooltipWasJustHidden flag, which is detected by a newly
-  // focused hoverable, if applicable.
+  // focused hoverable, if applicable. Always specify intent to replace when
+  // navigating via tab focus. (Check `handleTooltipLostFocus` for details.)
   if (!currentlyShownTooltipHasFocus(domEvent.relatedTarget)) {
-    hideCurrentlyShownTooltip();
+    hideCurrentlyShownTooltip(true);
   }
 }
 
@@ -743,8 +786,70 @@ function currentlyShownTooltipHasFocus(focusElement = document.activeElement) {
   return false;
 }
 
-function hideCurrentlyShownTooltip() {
-  const {event, state} = hoverableTooltipInfo;
+function beginTransitioningTooltipHidden(tooltip) {
+  const {settings, state} = hoverableTooltipInfo;
+
+  if (state.currentlyTransitioningHiddenTooltip) {
+    cancelTransitioningTooltipHidden();
+  }
+
+  cssProp(tooltip, {
+    'display': 'block',
+    'opacity': '0',
+
+    'transition-property': 'opacity',
+    'transition-timing-function':
+      `steps(${Math.ceil(settings.transitionHiddenDuration / 60)}, end)`,
+    'transition-duration':
+      `${settings.transitionHiddenDuration / 1000}s`,
+  });
+
+  state.currentlyTransitioningHiddenTooltip = tooltip;
+  state.transitionHiddenTimeout =
+    setTimeout(() => {
+      endTransitioningTooltipHidden();
+    }, settings.transitionHiddenDuration);
+}
+
+function cancelTransitioningTooltipHidden(andShow = false) {
+  const {state} = hoverableTooltipInfo;
+
+  endTransitioningTooltipHidden();
+
+  if (andShow) {
+    showTooltipFromHoverable(state.previouslyActiveHoverable);
+  }
+}
+
+function endTransitioningTooltipHidden() {
+  const {state} = hoverableTooltipInfo;
+  const {currentlyTransitioningHiddenTooltip: tooltip} = state;
+
+  if (!tooltip) return;
+
+  cssProp(tooltip, {
+    'display': null,
+    'opacity': null,
+    'transition-property': null,
+    'transition-timing-function': null,
+    'transition-duration': null,
+  });
+
+  state.currentlyTransitioningHiddenTooltip = null;
+
+  if (state.inertGracePeriodTimeout) {
+    clearTimeout(state.inertGracePeriodTimeout);
+    state.inertGracePeriodTimeout = null;
+  }
+
+  if (state.transitionHiddenTimeout) {
+    clearTimeout(state.transitionHiddenTimeout);
+    state.transitionHiddenTimeout = null;
+  }
+}
+
+function hideCurrentlyShownTooltip(intendingToReplace = false) {
+  const {event, settings, state} = hoverableTooltipInfo;
   const {currentlyShownTooltip: tooltip} = state;
 
   // If there was no tooltip to begin with, we're functionally in the desired
@@ -754,6 +859,27 @@ function hideCurrentlyShownTooltip() {
   // Never hide the tooltip if it's focused.
   if (currentlyShownTooltipHasFocus()) return false;
 
+  state.currentlyActiveHoverable.classList.remove('has-visible-tooltip');
+
+  // If there's no intent to replace this tooltip, it's the last one currently
+  // apparent in the interaction, and should be hidden with a transition.
+  if (intendingToReplace) {
+    cssProp(tooltip, 'display', 'none');
+  } else {
+    beginTransitioningTooltipHidden(state.currentlyShownTooltip);
+  }
+
+  // Wait just a moment before making the tooltip inert. You might react
+  // (to the ghosting, or just to time passing) and realize you wanted
+  // to look at the tooltip after all - this delay gives a little buffer
+  // to second guess letting it disappear.
+  state.inertGracePeriodTimeout =
+    setTimeout(() => {
+      tooltip.inert = true;
+    }, settings.inertGracePeriod);
+
+  state.previouslyActiveHoverable = state.currentlyActiveHoverable;
+
   state.currentlyShownTooltip = null;
   state.currentlyActiveHoverable = null;
 
@@ -763,8 +889,6 @@ function hideCurrentlyShownTooltip() {
     state.tooltipWasJustHidden = false;
   });
 
-  dispatchInternalEvent(event, 'whenTooltipShouldBeHidden', {tooltip});
-
   return true;
 }
 
@@ -772,15 +896,22 @@ function showTooltipFromHoverable(hoverable) {
   const {event, state} = hoverableTooltipInfo;
   const {tooltip} = state.registeredHoverables.get(hoverable);
 
-  if (!hideCurrentlyShownTooltip()) return false;
+  if (!hideCurrentlyShownTooltip(true)) return false;
+
+  // Cancel out another tooltip that's transitioning hidden, if that's going
+  // on - it's a distraction that this tooltip is now replacing.
+  cancelTransitioningTooltipHidden();
+
+  hoverable.classList.add('has-visible-tooltip');
+
+  cssProp(tooltip, 'display', 'block');
+  tooltip.inert = false;
 
   state.currentlyShownTooltip = tooltip;
   state.currentlyActiveHoverable = hoverable;
 
   state.tooltipWasJustHidden = false;
 
-  dispatchInternalEvent(event, 'whenTooltipShouldBeShown', {hoverable, tooltip});
-
   return true;
 }
 
@@ -1869,30 +2000,6 @@ function getExternalIconTooltipReferences() {
       .map(span => span.querySelector('span.icons-tooltip'));
 }
 
-function addExternalIconTooltipInternalListeners() {
-  const info = externalIconTooltipInfo;
-
-  hoverableTooltipInfo.event.whenTooltipShouldBeShown.push(({tooltip}) => {
-    if (!info.iconContainers.includes(tooltip)) return;
-    showExternalIconTooltip(tooltip);
-  });
-
-  hoverableTooltipInfo.event.whenTooltipShouldBeHidden.push(({tooltip}) => {
-    if (!info.iconContainers.includes(tooltip)) return;
-    hideExternalIconTooltip(tooltip);
-  });
-}
-
-function showExternalIconTooltip(iconContainer) {
-  iconContainer.classList.add('visible');
-  iconContainer.inert = false;
-}
-
-function hideExternalIconTooltip(iconContainer) {
-  iconContainer.classList.remove('visible');
-  iconContainer.inert = true;
-}
-
 function addExternalIconTooltipPageListeners() {
   const info = externalIconTooltipInfo;
 
@@ -1906,7 +2013,6 @@ function addExternalIconTooltipPageListeners() {
 }
 
 clientSteps.getPageReferences.push(getExternalIconTooltipReferences);
-clientSteps.addInternalListeners.push(addExternalIconTooltipInternalListeners);
 clientSteps.addPageListeners.push(addExternalIconTooltipPageListeners);
 
 /*
diff --git a/src/static/site6.css b/src/static/site6.css
index 4c083527..76b58f32 100644
--- a/src/static/site6.css
+++ b/src/static/site6.css
@@ -482,6 +482,11 @@ a:not([href]):hover {
   text-decoration-style: dotted;
 }
 
+.contribution.has-tooltip > a:hover,
+.contribution.has-tooltip > a.has-visible-tooltip {
+  text-decoration-style: wavy !important;
+}
+
 .icons {
   font-style: normal;
   white-space: nowrap;
@@ -490,12 +495,9 @@ a:not([href]):hover {
 .icons-tooltip {
   position: absolute;
   z-index: 3;
-  left: -36px;
-  top: calc(1em - 2px);
-  padding: 4px 12px 6px 8px;
-}
-
-.icons-tooltip:not(.visible) {
+  left: -34px;
+  top: calc(1em + 1px);
+  padding: 3px 6px 6px 6px;
   display: none;
 }