« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/static/js')
-rw-r--r--src/static/js/client/hash-link.js58
-rw-r--r--src/static/js/client/index.js2
-rw-r--r--src/static/js/client/memorable-details.js64
-rw-r--r--src/static/js/client/sticky-heading.js4
4 files changed, 127 insertions, 1 deletions
diff --git a/src/static/js/client/hash-link.js b/src/static/js/client/hash-link.js
index 27035e29..e82e06c5 100644
--- a/src/static/js/client/hash-link.js
+++ b/src/static/js/client/hash-link.js
@@ -1,6 +1,7 @@
 /* eslint-env browser */
 
-import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js';
+import {filterMultipleArrays, stitchArrays, unique}
+  from '../../shared-util/sugar.js';
 
 import {dispatchInternalEvent} from '../client-util.js';
 
@@ -11,6 +12,9 @@ export const info = {
   hrefs: null,
   targets: null,
 
+  details: null,
+  detailsIDs: null,
+
   state: {
     highlightedTarget: null,
     scrollingAfterClick: false,
@@ -40,6 +44,19 @@ export function getPageReferences() {
     info.hrefs,
     info.targets,
     (_link, _href, target) => target);
+
+  info.details =
+    unique([
+      ...document.querySelectorAll('details[id]'),
+      ...
+        Array.from(document.querySelectorAll('summary[id]'))
+          .map(summary => summary.closest('details')),
+    ]);
+
+  info.detailsIDs =
+    info.details.map(details =>
+      details.id ||
+      details.querySelector('summary').id);
 }
 
 function processScrollingAfterHashLinkClicked() {
@@ -60,6 +77,15 @@ function processScrollingAfterHashLinkClicked() {
   }, 200);
 }
 
+export function mutatePageContent() {
+  if (location.hash.length > 1) {
+    const target = document.getElementById(location.hash.slice(1));
+    if (target) {
+      expandDetails(target);
+    }
+  }
+}
+
 export function addPageListeners() {
   // Instead of defining a scroll offset (to account for the sticky heading)
   // in JavaScript, we interface with the CSS property 'scroll-margin-top'.
@@ -94,6 +120,8 @@ export function addPageListeners() {
         return;
       }
 
+      expandDetails(target);
+
       // Hide skipper box right away, so the layout is updated on time for the
       // math operations coming up next.
       const skipper = document.getElementById('skippers');
@@ -143,4 +171,32 @@ export function addPageListeners() {
       state.highlightedTarget = null;
     });
   }
+
+  stitchArrays({
+    details: info.details,
+    id: info.detailsIDs,
+  }).forEach(({details, id}) => {
+      details.addEventListener('toggle', () => {
+        if (!details.open) {
+          detractHash(id);
+        }
+      });
+    });
+}
+
+function expandDetails(target) {
+  if (target.nodeName === 'SUMMARY') {
+    const details = target.closest('details');
+    if (details) {
+      details.open = true;
+    }
+  } else if (target.nodeName === 'DETAILS') {
+    details.open = true;
+  }
+}
+
+function detractHash(id) {
+  if (location.hash === '#' + id) {
+    history.pushState({}, undefined, location.href.replace(/#.*$/, ''));
+  }
 }
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
index 86081b5d..0f22810c 100644
--- a/src/static/js/client/index.js
+++ b/src/static/js/client/index.js
@@ -18,6 +18,7 @@ import * as hoverableTooltipModule from './hoverable-tooltip.js';
 import * as imageOverlayModule from './image-overlay.js';
 import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js';
 import * as liveMousePositionModule from './live-mouse-position.js';
+import * as memorableDetailsModule from './memorable-details.js';
 import * as quickDescriptionModule from './quick-description.js';
 import * as revealAllGridControlModule from './reveal-all-grid-control.js';
 import * as scriptedLinkModule from './scripted-link.js';
@@ -44,6 +45,7 @@ export const modules = [
   imageOverlayModule,
   intrapageDotSwitcherModule,
   liveMousePositionModule,
+  memorableDetailsModule,
   quickDescriptionModule,
   revealAllGridControlModule,
   scriptedLinkModule,
diff --git a/src/static/js/client/memorable-details.js b/src/static/js/client/memorable-details.js
new file mode 100644
index 00000000..07482b29
--- /dev/null
+++ b/src/static/js/client/memorable-details.js
@@ -0,0 +1,64 @@
+/* eslint-env browser */
+
+import {stitchArrays} from '../../shared-util/sugar.js';
+
+export const info = {
+  id: 'memorableDetailsInfo',
+
+  details: null,
+  ids: null,
+
+  session: {
+    openDetails: {
+      type: 'json',
+      maxLength: settings => settings.maxOpenDetailsStorage,
+    },
+  },
+
+  settings: {
+    maxOpenDetailsStorage: 1000,
+  },
+};
+
+export function getPageReferences() {
+  info.details =
+    Array.from(document.querySelectorAll('details.memorable'));
+
+  info.ids =
+    info.details.map(details => details.getAttribute('data-memorable-id'));
+}
+
+export function mutatePageContent() {
+  stitchArrays({
+    details: info.details,
+    id: info.ids,
+  }).forEach(({details, id}) => {
+      if (info.session.openDetails?.includes(id)) {
+        details.open = true;
+      }
+    });
+}
+
+export function addPageListeners() {
+  for (const [index, details] of info.details.entries()) {
+    details.addEventListener('toggle', () => {
+      handleDetailsToggled(index);
+    });
+  }
+}
+
+function handleDetailsToggled(index) {
+  const details = info.details[index];
+  const id = info.ids[index];
+
+  if (details.open) {
+    if (info.session.openDetails) {
+      info.session.openDetails = [...info.session.openDetails, id];
+    } else {
+      info.session.openDetails = [id];
+    }
+  } else if (info.session.openDetails?.includes(id)) {
+    info.session.openDetails =
+      info.session.openDetails.filter(item => item !== id);
+  }
+}
diff --git a/src/static/js/client/sticky-heading.js b/src/static/js/client/sticky-heading.js
index b65574d0..4660013a 100644
--- a/src/static/js/client/sticky-heading.js
+++ b/src/static/js/client/sticky-heading.js
@@ -256,6 +256,10 @@ function getContentHeadingClosestToStickySubheading(index) {
   // Iterate from bottom to top of the content area.
   const contentHeadings = info.contentHeadings[index];
   for (const heading of contentHeadings.slice().reverse()) {
+    if (heading.nodeName === 'SUMMARY' && !heading.closest('details').open) {
+      continue;
+    }
+
     const headingRect = heading.getBoundingClientRect();
     if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 40) {
       return heading;