diff options
-rw-r--r-- | src/content/dependencies/generateAlbumInfoPage.js | 31 | ||||
-rw-r--r-- | src/content/dependencies/generateCollapsedContentEntrySection.js | 44 | ||||
-rw-r--r-- | src/content/dependencies/generateContentContentHeading.js | 51 | ||||
-rw-r--r-- | src/content/dependencies/generateFlashInfoPage.js | 44 | ||||
-rw-r--r-- | src/content/dependencies/generateTrackInfoPage.js | 89 | ||||
-rw-r--r-- | src/static/css/site.css | 48 | ||||
-rw-r--r-- | src/static/js/client/hash-link.js | 58 | ||||
-rw-r--r-- | src/static/js/client/index.js | 2 | ||||
-rw-r--r-- | src/static/js/client/memorable-details.js | 64 | ||||
-rw-r--r-- | src/static/js/client/sticky-heading.js | 4 | ||||
-rw-r--r-- | src/strings-default.yaml | 8 |
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: |