« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static/js/client.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/static/js/client.js')
-rw-r--r--src/static/js/client.js259
1 files changed, 130 insertions, 129 deletions
diff --git a/src/static/js/client.js b/src/static/js/client.js
index 935a9d87..21c3911a 100644
--- a/src/static/js/client.js
+++ b/src/static/js/client.js
@@ -1271,6 +1271,11 @@ const hoverableTooltipInfo = initInfo('hoverableTooltipInfo', {
     // from causing the current tooltip to be hidden.
     currentTouchIdentifiers: new Set(),
     touchIdentifiersBanishedByScrolling: new Set(),
+
+    // This is a two-item array that tracks the direction we've already
+    // dynamically placed the current tooltip. If we *reposition* the tooltip
+    // (because its dimensions changed), we'll try to follow this anchor first.
+    dynamicTooltipAnchorDirection: null,
   },
 
   event: {
@@ -1731,6 +1736,8 @@ function hideCurrentlyShownTooltip(intendingToReplace = false) {
   state.currentlyShownTooltip = null;
   state.currentlyActiveHoverable = null;
 
+  state.dynamicTooltipAnchorDirection = null;
+
   // Set this for one tick of the event cycle.
   state.tooltipWasJustHidden = true;
   setTimeout(() => {
@@ -1758,6 +1765,11 @@ function showTooltipFromHoverable(hoverable) {
 
   positionTooltipFromHoverableWithBrains(hoverable);
 
+  // After a tooltip is shown, if we *didn't* specify an anchor,
+  // assume it was shown in its default position - generally presented
+  // as down and to the right. Successive repositioning will base on this.
+  state.dynamicTooltipAnchorDirection ??= ['down', 'right'];
+
   cssProp(tooltip, 'display', 'block');
   tooltip.inert = false;
 
@@ -1792,10 +1804,23 @@ function peekTooltipClientRect(tooltip) {
   }
 }
 
+function repositionCurrentTooltip() {
+  const {state} = hoverableTooltipInfo;
+  const {currentlyActiveHoverable} = state;
+
+  if (!currentlyActiveHoverable) {
+    throw new Error(`No hoverable active to reposition tooltip from`);
+  }
+
+  positionTooltipFromHoverableWithBrains(currentlyActiveHoverable);
+}
+
 function positionTooltipFromHoverableWithBrains(hoverable) {
   const {state} = hoverableTooltipInfo;
   const {tooltip} = state.registeredHoverables.get(hoverable);
 
+  const anchorDirection = state.dynamicTooltipAnchorDirection;
+
   // Reset before doing anything else. We're going to adapt to
   // its natural placement, adjusted by CSS, which otherwise
   // could be obscured by a placement we've previously provided.
@@ -1817,23 +1842,42 @@ function positionTooltipFromHoverableWithBrains(hoverable) {
     return;
   }
 
-  let selectedRect = null;
-  for (let i = 0; i < numBaselineRects; i++) {
-    selectedRect = opportunities.right.down[i];
-    if (selectedRect) break;
+  const tryDirection = (dir1, dir2, i) => {
+    selectedRect = opportunities[dir1][dir2][i];
+    return !!selectedRect;
+  };
 
-    selectedRect = opportunities.left.down[i];
-    if (selectedRect) break;
+  let selectedRect = null;
+  selectRect: {
+    if (anchorDirection) {
+      for (let i = 0; i < numBaselineRects; i++) {
+        if (tryDirection(...anchorDirection, i)) {
+          break selectRect;
+        }
+      }
+    }
 
-    selectedRect = opportunities.right.up[i];
-    if (selectedRect) break;
+    for (let i = 0; i < numBaselineRects; i++) {
+      for (const [dir1, dir2] of [
+        ['right', 'down'],
+        ['left', 'down'],
+        ['right', 'up'],
+        ['left', 'up'],
+        ['down', 'right'],
+        ['down', 'left'],
+        ['up', 'right'],
+        ['up', 'left'],
+      ]) {
+        if (tryDirection(dir1, dir2, i)) {
+          state.dynamicTooltipAnchorDirection = [dir1, dir2];
+          break selectRect;
+        }
+      }
+    }
 
-    selectedRect = opportunities.left.up[i];
-    if (selectedRect) break;
+    selectedRect = baselineRect;
   }
 
-  selectedRect ??= baselineRect;
-
   positionTooltip(tooltip, selectedRect.x, selectedRect.y);
 }
 
@@ -1929,18 +1973,18 @@ function getTooltipFromHoverablePlacementOpportunityAreas(hoverable) {
   const neededVerticalOverlap = 30;
   const neededHorizontalOverlap = 30;
 
+  const upTopDown =
+    WikiRect.beneath(
+      hoverableRect.top + neededVerticalOverlap - tooltipRect.height);
+
+  const downBottomUp =
+    WikiRect.above(
+      hoverableRect.bottom - neededVerticalOverlap + tooltipRect.height);
+
   // Please don't ask us to make this but horizontal?
   const prepareVerticalOrientationRects = (regionRects) => {
     const orientations = {};
 
-    const upTopDown =
-      WikiRect.beneath(
-        hoverableRect.top + neededVerticalOverlap - tooltipRect.height);
-
-    const downBottomUp =
-      WikiRect.above(
-        hoverableRect.bottom - neededVerticalOverlap + tooltipRect.height);
-
     const orientHorizontally = (rect, i) => {
       if (!rect) return null;
 
@@ -1996,9 +2040,67 @@ function getTooltipFromHoverablePlacementOpportunityAreas(hoverable) {
     return orientations;
   };
 
+  const rightRightLeft =
+    WikiRect.leftOf(
+      hoverableRect.left - neededHorizontalOverlap + tooltipRect.width);
+
+  const leftLeftRight =
+    WikiRect.rightOf(
+      hoverableRect.left + neededHorizontalOverlap - tooltipRect.width);
+
+  // Oops.
+  const prepareHorizontalOrientationRects = (regionRects) => {
+    const orientations = {};
+
+    const orientVertically = (rect, i) => {
+      if (!rect) return null;
+
+      const regionRect = regionRects[i];
+
+      if (regionRect.height > 0) {
+        return rect;
+      } else {
+        return WikiRect.fromRect({
+          x: rect.x,
+          y: regionRect.bottom - tooltipRect.height,
+          width: rect.width,
+          height: rect.height,
+        });
+      }
+    };
+
+    orientations.left =
+      regionRects
+        .map(rect => rect?.intersectionWith(leftLeftRight))
+        .map(orientVertically)
+        .map(keepIfFits);
+
+    orientations.right =
+      regionRects
+        .map(rect => rect?.intersectionWith(rightRightLeft))
+        .map(rect =>
+          (rect
+            ? rect.intersectionWith(WikiRect.fromRect({
+                x: rect.right - tooltipRect.width,
+                y: rect.y,
+                width: rect.width,
+                height: tooltipRect.height,
+              }))
+            : null))
+        .map(orientVertically)
+        .map(keepIfFits);
+
+    // No analogous center because we don't actually use
+    // center alignment...
+
+    return orientations;
+  };
+
   const orientationRects = {
     left: prepareVerticalOrientationRects(regionRects.left),
     right: prepareVerticalOrientationRects(regionRects.right),
+    down: prepareHorizontalOrientationRects(regionRects.bottom),
+    up: prepareHorizontalOrientationRects(regionRects.top),
   };
 
   return {
@@ -3106,114 +3208,6 @@ clientSteps.getPageReferences.push(getAdditionalNamesBoxReferences);
 clientSteps.addInternalListeners.push(addAdditionalNamesBoxInternalListeners);
 clientSteps.addPageListeners.push(addAdditionalNamesBoxListeners);
 
-// Scoped chronology links --------------------------------
-
-const scopedChronologyLinksInfo = initInfo('scopedChronologyLinksInfo', {
-  switcher: null,
-  containers: null,
-  switcherLinks: null,
-  modes: null,
-
-  session: {
-    selectedMode: 'wiki',
-  },
-});
-
-function getScopedChronologyLinksReferences() {
-  const info = scopedChronologyLinksInfo;
-
-  info.switcher =
-    document.querySelector('.scoped-chronology-switcher');
-
-  if (!info.switcher) {
-    return;
-  }
-
-  info.containers =
-    Array.from(info.switcher.querySelectorAll(':scope > div'));
-
-  info.switcherLinks =
-    Array.from(info.switcher.querySelectorAll('.switcher-link'));
-
-  info.modes =
-    info.containers
-      .map(container =>
-        Array.from(container.classList)
-          .find(className => className.startsWith('scope-'))
-          .slice('scope-'.length));
-}
-
-function addScopedChronologyLinksPageHandlers() {
-  const info = scopedChronologyLinksInfo;
-  const {session} = scopedChronologyLinksInfo;
-
-  if (!info.switcher) {
-    return;
-  }
-
-  for (const [index, {
-    container: currentContainer,
-    switcherLink: currentSwitcherLink,
-  }] of stitchArrays({
-    container: info.containers,
-    switcherLink: info.switcherLinks,
-  }).entries()) {
-    const nextContainer =
-      atOffset(info.containers, index, +1, {wrap: true});
-
-    const nextSwitcherLink =
-      atOffset(info.switcherLinks, index, +1, {wrap: true});
-
-    const nextMode =
-      atOffset(info.modes, index, +1, {wrap: true});
-
-    currentSwitcherLink.addEventListener('click', domEvent => {
-      domEvent.preventDefault();
-
-      cssProp(currentContainer, 'display', 'none');
-      cssProp(currentSwitcherLink, 'display', 'none');
-      cssProp(nextContainer, 'display', 'block');
-      cssProp(nextSwitcherLink, 'display', 'inline');
-
-      session.selectedMode = nextMode;
-    });
-  }
-}
-
-function mutateScopedChronologyLinksContent() {
-  const info = scopedChronologyLinksInfo;
-
-  if (!info.switcher) {
-    return;
-  }
-
-  const {selectedMode} = info.session;
-
-  if (info.modes.includes(selectedMode)) {
-    const selectedIndex = info.modes.indexOf(selectedMode);
-
-    for (const [index, {
-      container,
-      switcherLink,
-    }] of stitchArrays({
-      container: info.containers,
-      switcherLink: info.switcherLinks,
-    }).entries()) {
-      if (index === selectedIndex) {
-        cssProp(container, 'display', 'block');
-        cssProp(switcherLink, 'display', 'inline');
-      } else {
-        cssProp(container, 'display', 'none');
-        cssProp(switcherLink, 'display', 'none');
-      }
-    }
-  }
-}
-
-clientSteps.getPageReferences.push(getScopedChronologyLinksReferences);
-clientSteps.mutatePageContent.push(mutateScopedChronologyLinksContent);
-clientSteps.addPageListeners.push(addScopedChronologyLinksPageHandlers);
-
 // Group contributions table ------------------------------
 
 // TODO: Update to clientSteps style.
@@ -3363,7 +3357,7 @@ function getArtistExternalLinkTooltipPageReferences() {
   const info = artistExternalLinkTooltipInfo;
 
   info.tooltips =
-    Array.from(document.getElementsByClassName('icons-tooltip'));
+    Array.from(document.getElementsByClassName('contribution-tooltip'));
 
   info.tooltipRows =
     info.tooltips.map(tooltip =>
@@ -3507,6 +3501,8 @@ function showArtistExternalLinkTooltipInfo() {
   for (const tooltip of info.tooltips) {
     tooltip.classList.add('show-info');
   }
+
+  repositionCurrentTooltip();
 }
 
 function hideArtistExternalLinkTooltipInfo() {
@@ -3831,6 +3827,7 @@ const sidebarSearchInfo = initInfo('sidebarSearchInfo', {
 
   searchSidebarColumn: null,
   searchBox: null,
+  searchLabel: null,
   searchInput: null,
 
   progressRule: null,
@@ -3919,6 +3916,9 @@ function getSidebarSearchReferences() {
     return;
   }
 
+  info.searchLabel =
+    info.searchBox.querySelector('.wiki-search-label');
+
   info.searchInput =
     info.searchBox.querySelector('.wiki-search-input');
 
@@ -4360,6 +4360,7 @@ function showSidebarSearchFailed() {
   cssProp(info.failedRule, 'display', null);
   cssProp(info.failedContainer, 'display', null);
 
+  info.searchLabel.classList.add('disabled');
   info.searchInput.disabled = true;
 
   if (state.stoppedTypingTimeout) {