« 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/content/dependencies/generateAlbumInfoPage.js31
-rw-r--r--src/content/dependencies/generateCollapsedContentEntrySection.js44
-rw-r--r--src/content/dependencies/generateContentContentHeading.js51
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js44
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js89
-rw-r--r--src/static/css/site.css48
-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
-rw-r--r--src/strings-default.yaml8
11 files changed, 348 insertions, 95 deletions
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 1c5be6e6..8f8b921c 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -13,9 +13,9 @@ export default {
     'generateAlbumSocialEmbed',
     'generateAlbumStyleTags',
     'generateAlbumTrackList',
+    'generateCollapsedContentEntrySection',
     'generateCommentaryContentHeading',
     'generateCommentaryEntry',
-    'generateContentContentHeading',
     'generateContentHeading',
     'generatePageLayout',
     'generateReadCommentaryLine',
@@ -58,9 +58,6 @@ export default {
     contentHeading:
       relation('generateContentHeading'),
 
-    contentContentHeading:
-      relation('generateContentContentHeading', album),
-
     releaseInfo:
       relation('generateAlbumReleaseInfo', album),
 
@@ -90,9 +87,10 @@ export default {
       album.commentary
         .map(entry => relation('generateCommentaryEntry', entry)),
 
-    creditSourceEntries:
-      album.creditingSources
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    creditingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        album.creditingSources,
+        album),
   }),
 
   data: (album) => ({
@@ -172,7 +170,7 @@ export default {
               !html.isBlank(relations.artistCommentaryEntries) &&
                 relations.readCommentaryLine,
 
-              !html.isBlank(relations.creditSourceEntries) &&
+              !html.isBlank(relations.creditingSourcesSection) &&
                 language.encapsulate(capsule, 'readCreditingSources', capsule =>
                   language.$(capsule, {
                     link:
@@ -192,9 +190,7 @@ export default {
               date: language.formatDate(data.dateAddedToWiki),
             })),
 
-          (!html.isBlank(relations.artistCommentaryEntries) ||
-           !html.isBlank(relations.creditSourceEntries))
-          &&
+          !html.isBlank(relations.artistCommentaryEntries) &&
             html.tag('hr', {class: 'main-separator'}),
 
           language.encapsulate('releaseInfo.additionalFiles', capsule =>
@@ -213,15 +209,10 @@ export default {
             relations.artistCommentaryEntries,
           ]),
 
-          html.tags([
-            relations.contentContentHeading.clone()
-              .slots({
-                attributes: {id: 'crediting-sources'},
-                string: 'misc.creditingSources',
-              }),
-
-            relations.creditSourceEntries,
-          ]),
+          relations.creditingSourcesSection.slots({
+            id: 'crediting-sources',
+            string: 'misc.creditingSources',
+          }),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/content/dependencies/generateCollapsedContentEntrySection.js b/src/content/dependencies/generateCollapsedContentEntrySection.js
new file mode 100644
index 00000000..ae3652c7
--- /dev/null
+++ b/src/content/dependencies/generateCollapsedContentEntrySection.js
@@ -0,0 +1,44 @@
+export default {
+  contentDependencies: [
+    'generateCommentaryEntry',
+    'generateContentContentHeading',
+  ],
+
+  extraDependencies: ['html'],
+
+  relations: (relation, entries, thing) => ({
+    contentContentHeading:
+      relation('generateContentContentHeading', thing),
+
+    entries:
+      entries
+        .map(entry => relation('generateCommentaryEntry', entry)),
+  }),
+
+  slots: {
+    id: {type: 'string'},
+    string: {type: 'string'},
+  },
+
+  generate: (relations, slots, {html}) =>
+    html.tag('details',
+      {[html.onlyIfContent]: true},
+
+      slots.id && [
+        {class: 'memorable'},
+        {'data-memorable-id': slots.id},
+      ],
+
+      [
+        relations.contentContentHeading.slots({
+          attributes: [
+            slots.id && {id: slots.id},
+          ],
+
+          string: slots.string,
+          summary: true,
+        }),
+
+        relations.entries,
+      ]),
+};
diff --git a/src/content/dependencies/generateContentContentHeading.js b/src/content/dependencies/generateContentContentHeading.js
index 555abb6b..54ffa205 100644
--- a/src/content/dependencies/generateContentContentHeading.js
+++ b/src/content/dependencies/generateContentContentHeading.js
@@ -9,7 +9,9 @@ export default {
 
   data: (thing) => ({
     name:
-      thing.name,
+      (thing
+        ? thing.name
+        : null),
   }),
 
   slots: {
@@ -21,6 +23,11 @@ export default {
     string: {
       type: 'string',
     },
+
+    summary: {
+      type: 'boolean',
+      default: false,
+    },
   },
 
   generate: (data, relations, slots, {html, language}) =>
@@ -28,14 +35,42 @@ export default {
       attributes: slots.attributes,
 
       title:
-        slots.string &&
-        language.$(slots.string, {
-          thing:
-            html.tag('i', data.name),
-        }),
+        (() => {
+          if (!slots.string) return html.blank();
+
+          const options = {};
+
+          if (slots.summary) {
+            options.cue =
+              html.tag('span', {class: 'cue'},
+                language.$(slots.string, 'cue'));
+          }
+
+          if (data.name) {
+            options.thing = html.tag('i', data.name);
+          }
+
+          if (slots.summary) {
+            return html.tags([
+              html.tag('span', {class: 'when-open'},
+                language.$(slots.string, options)),
+
+              html.tag('span', {class: 'when-collapsed'},
+                language.$(slots.string, 'collapsed', options)),
+            ]);
+          } else {
+            return language.$(slots.string, options);
+          }
+        })(),
 
       stickyTitle:
-        slots.string &&
-        language.$(slots.string, 'sticky'),
+        (slots.string
+          ? language.$(slots.string, 'sticky')
+          : html.blank()),
+
+      tag:
+        (slots.summary
+          ? 'summary'
+          : 'p'),
     }),
 };
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index a7c23eae..effc07ff 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -1,11 +1,24 @@
 import {empty} from '#sugar';
 
+function checkInterrupted(which, relations, {html}) {
+  if (
+    !html.isBlank(relations.contributorContributionList) ||
+    !html.isBlank(relations.featuredTracksList)
+  ) return true;
+
+  if (which === 'crediting-sources') {
+    if (!html.isBlank(relations.artistCommentaryEntries)) return true;
+  }
+
+  return false;
+}
+
 export default {
   contentDependencies: [
     'generateAdditionalNamesBox',
+    'generateCollapsedContentEntrySection',
     'generateCommentaryEntry',
     'generateCommentaryContentHeading',
-    'generateContentContentHeading',
     'generateContentHeading',
     'generateContributionList',
     'generateFlashActSidebar',
@@ -56,9 +69,6 @@ export default {
     contentHeading:
       relation('generateContentHeading'),
 
-    contentContentHeading:
-      relation('generateContentContentHeading', flash),
-
     commentaryContentHeading:
       relation('generateCommentaryContentHeading', flash),
 
@@ -81,9 +91,10 @@ export default {
       flash.commentary
         .map(entry => relation('generateCommentaryEntry', entry)),
 
-    creditSourceEntries:
-      flash.creditingSources
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    creditingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        flash.creditingSources,
+        flash),
   }),
 
   data: (_query, flash) => ({
@@ -135,11 +146,11 @@ export default {
             {[html.joinChildren]: html.tag('br')},
 
             language.encapsulate('releaseInfo', capsule => [
-              (!html.isBlank(relations.contributorContributionList) ||
-               !html.isBlank(relations.featuredTracksList)) &&
+              checkInterrupted('commentary', relations, {html}) &&
                 relations.readCommentaryLine,
 
-              !html.isBlank(relations.creditSourceEntries) &&
+              checkInterrupted('crediting-sources', relations, {html}) &&
+              !html.isBlank(relations.creditingSourcesSection) &&
                 language.encapsulate(capsule, 'readCreditingSources', capsule =>
                   language.$(capsule, {
                     link:
@@ -179,15 +190,10 @@ export default {
             relations.artistCommentaryEntries,
           ]),
 
-          html.tags([
-            relations.contentContentHeading.clone()
-              .slots({
-                attributes: {id: 'crediting-sources'},
-                string: 'misc.creditingSources',
-              }),
-
-            relations.creditSourceEntries,
-          ]),
+          relations.creditingSourcesSection.slots({
+            id: 'crediting-sources',
+            string: 'misc.creditingSources',
+          }),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index efd0ec9f..bcae9748 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,3 +1,24 @@
+function checkInterrupted(which, relations, {html}) {
+  if (
+    !html.isBlank(relations.additionalFilesList) ||
+    !html.isBlank(relations.contributorContributionList) ||
+    !html.isBlank(relations.flashesThatFeatureList) ||
+    !html.isBlank(relations.lyricsSection) ||
+    !html.isBlank(relations.midiProjectFilesList) ||
+    !html.isBlank(relations.referencedByTracksList) ||
+    !html.isBlank(relations.referencedTracksList) ||
+    !html.isBlank(relations.sampledByTracksList) ||
+    !html.isBlank(relations.sampledTracksList) ||
+    !html.isBlank(relations.sheetMusicFilesList)
+  ) return true;
+
+  if (which === 'crediting-sources' || which === 'referencing-sources') {
+    if (!html.isBlank(relations.artistCommentarySection)) return true;
+  }
+
+  return false;
+}
+
 export default {
   contentDependencies: [
     'generateAdditionalFilesList',
@@ -8,7 +29,7 @@ export default {
     'generateAlbumSidebar',
     'generateAlbumStyleTags',
     'generateCommentaryEntry',
-    'generateContentContentHeading',
+    'generateCollapsedContentEntrySection',
     'generateContentHeading',
     'generateContributionList',
     'generateLyricsSection',
@@ -81,9 +102,6 @@ export default {
     contentHeading:
       relation('generateContentHeading'),
 
-    contentContentHeading:
-      relation('generateContentContentHeading', track),
-
     releaseInfo:
       relation('generateTrackReleaseInfo', track),
 
@@ -130,13 +148,15 @@ export default {
     artistCommentarySection:
       relation('generateTrackArtistCommentarySection', track),
 
-    creditingSourceEntries:
-      track.creditingSources
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    creditingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        track.creditingSources,
+        track),
 
-    referencingSourceEntries:
-      track.referencingSources
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    referencingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        track.referencingSources,
+        track),
   }),
 
   data: (query, track) => ({
@@ -212,21 +232,11 @@ export default {
                         language.$(capsule, 'link')),
                   })),
 
-              (!html.isBlank(relations.additionalFilesList) ||
-               !html.isBlank(relations.contributorContributionList) ||
-               !html.isBlank(relations.creditingSourceEntries) ||
-               !html.isBlank(relations.flashesThatFeatureList) ||
-               !html.isBlank(relations.lyricsSection) ||
-               !html.isBlank(relations.midiProjectFilesList) ||
-               !html.isBlank(relations.referencedByTracksList) ||
-               !html.isBlank(relations.referencedTracksList) ||
-               !html.isBlank(relations.referencingSourceEntries) ||
-               !html.isBlank(relations.sampledByTracksList) ||
-               !html.isBlank(relations.sampledTracksList) ||
-               !html.isBlank(relations.sheetMusicFilesList)) &&
+              checkInterrupted('commentary', relations, {html}) &&
                 relations.readCommentaryLine,
 
-              !html.isBlank(relations.creditingSourceEntries) &&
+              !html.isBlank(relations.creditingSourcesSection) &&
+              checkInterrupted('crediting-sources', relations, {html}) &&
                 language.encapsulate(capsule, 'readCreditingSources', capsule =>
                   language.$(capsule, {
                     link:
@@ -235,7 +245,8 @@ export default {
                         language.$(capsule, 'link')),
                   })),
 
-              !html.isBlank(relations.referencingSourceEntries) &&
+              !html.isBlank(relations.referencingSourcesSection) &&
+              checkInterrupted('referencing-sources', relations, {html}) &&
                 language.encapsulate(capsule, 'readReferencingSources', capsule =>
                   language.$(capsule, {
                     link:
@@ -368,9 +379,7 @@ export default {
 
           data.firstTrackInSingle &&
           (!html.isBlank(relations.lyricsSection) ||
-           !html.isBlank(relations.artistCommentarySection) ||
-           !html.isBlank(relations.creditingSourceEntries) ||
-           !html.isBlank(relations.referencingSourceEntries)) &&
+           !html.isBlank(relations.artistCommentarySection)) &&
             html.tag('hr', {class: 'main-separator'}),
 
           data.needsLyrics &&
@@ -412,25 +421,15 @@ export default {
 
           relations.artistCommentarySection,
 
-          html.tags([
-            relations.contentContentHeading.clone()
-              .slots({
-                attributes: {id: 'crediting-sources'},
-                string: 'misc.creditingSources',
-              }),
-
-            relations.creditingSourceEntries,
-          ]),
-
-          html.tags([
-            relations.contentContentHeading.clone()
-              .slots({
-                attributes: {id: 'referencing-sources'},
-                string: 'misc.referencingSources',
-              }),
+          relations.creditingSourcesSection.slots({
+            id: 'crediting-sources',
+            string: 'misc.creditingSources',
+          }),
 
-            relations.referencingSourceEntries,
-          ]),
+          relations.referencingSourcesSection.slots({
+            id: 'referencing-sources',
+            string: 'misc.referencingSources',
+          }),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/static/css/site.css b/src/static/css/site.css
index 7bf30a7e..61803c9d 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -1860,6 +1860,7 @@ p.image-details.origin-details .filename-line {
 .inherited-commentary-section {
   clear: right;
   margin-top: 1em;
+  margin-bottom: 1.5em;
   margin-right: min(4vw, 60px);
   border: 2px solid var(--deep-color);
   border-radius: 4px;
@@ -2015,6 +2016,11 @@ h1 {
   white-space: nowrap;
 }
 
+#content details {
+  margin-top: 0.25em;
+  margin-bottom: 0.25em;
+}
+
 #content.top-index h1,
 #content.flash-index h1 {
   text-align: center;
@@ -3598,6 +3604,48 @@ h3.content-heading {
   clear: both;
 }
 
+summary.content-heading {
+  list-style-type: none;
+}
+
+summary.content-heading .cue {
+  display: inline-flex;
+  color: var(--primary-color);
+}
+
+summary.content-heading .cue::after {
+  content: "";
+  padding-left: 0.5ch;
+  display: list-item;
+  list-style-type: disclosure-closed;
+  list-style-position: inside;
+}
+
+details[open] > summary.content-heading .cue::after {
+  list-style-type: disclosure-open;
+}
+
+summary.content-heading > span:hover {
+  text-decoration: none !important;
+}
+
+summary.content-heading > span:hover .cue {
+  text-decoration: underline;
+  text-decoration-style: wavy;
+}
+
+summary.content-heading .when-open {
+  display: none;
+}
+
+details[open] > summary.content-heading .when-open {
+  display: unset;
+}
+
+details[open] > summary.content-heading .when-collapsed {
+  display: none;
+}
+
 /* This animation's name is referenced in JavaScript */
 @keyframes highlight-hash-link {
   0% {
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;
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index cd170226..5bbecbf3 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -609,7 +609,9 @@ misc:
         wallpaperArt: "wallpaper art"
 
   creditingSources:
-    _: "Crediting sources for {THING}:"
+    _: "{CUE} for {THING}:"
+    collapsed: "{CUE} for {THING}…"
+    cue: "Crediting sources"
     sticky: "Crediting sources:"
 
   # external:
@@ -847,7 +849,9 @@ misc:
       track: "Tracks"
 
   referencingSources:
-    _: "Referencing sources for {THING}:"
+    _: "{CUE} for {THING}:"
+    collapsed: "{CUE} for {THING}…"
+    cue: "Referencing sources"
     sticky: "Referencing sources:"
 
   # skippers: