From 4a1997b0edd7de4b124c17e3cdeb1a47ecea1095 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 3 Dec 2022 21:28:15 -0400 Subject: sticky subheadings --- src/data/things/language.js | 1 - src/listing-spec.js | 7 ++++ src/misc-templates.js | 20 +++++++++- src/page/album-commentary.js | 30 ++++++++++----- src/page/album.js | 11 ++++-- src/page/artist.js | 11 +++--- src/page/flash.js | 5 ++- src/page/group.js | 4 +- src/page/listing.js | 4 +- src/page/news.js | 6 ++- src/page/track.js | 60 ++++++++++++++++++++---------- src/static/client.js | 87 ++++++++++++++++++++++++++++++++++++++++++++ src/static/site2.css | 70 +++++++++++++++++++++++++++++++++-- src/upd8.js | 5 +++ 14 files changed, 272 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/data/things/language.js b/src/data/things/language.js index 21524993..3086ad2e 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -296,7 +296,6 @@ export class Language extends Thing { return this.formatString('count.fileSize.bytes', {bytes}); } } - } const countHelper = (stringKey, argName = stringKey) => diff --git a/src/listing-spec.js b/src/listing-spec.js index f773bf87..e01912cd 100644 --- a/src/listing-spec.js +++ b/src/listing-spec.js @@ -96,6 +96,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({dateAddedToWiki, chunk: albums}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listAlbums.byDateAdded.date', { date: language.formatDate(dateAddedToWiki), })), @@ -445,6 +446,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({category, groups}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listGroups.byCategory.category', { category: empty(groups) ? category.name @@ -594,6 +596,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({album, tracks}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listTracks.byAlbum.album', { album: link.album(album), })), @@ -678,6 +681,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({album, tracks}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listTracks.byDurationInAlbum.album', { album: link.album(album), })), @@ -731,6 +735,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({album, chunk: tracks}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listTracks.inFlashes.byAlbum.album', { album: link.album(album), date: language.formatDate(album.date), @@ -766,6 +771,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({flash, tracks}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listTracks.inFlashes.byFlash.flash', { flash: link.flash(flash), date: language.formatDate(flash.date), @@ -798,6 +804,7 @@ const listingSpec = [ html.tag('dl', data.flatMap(({album, tracks}) => [ html.tag('dt', + {class: ['content-heading']}, language.$('listingPage.listTracks.withLyrics.album', { album: link.album(album), date: language.formatDate(album.date), diff --git a/src/misc-templates.js b/src/misc-templates.js index 83aae190..755ad649 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -50,7 +50,10 @@ function unbound_generateAdditionalFilesList(additionalFiles, { return html.fragment([ html.tag('p', - {id: 'additional-files'}, + { + id: 'additional-files', + class: ['content-heading'], + }, language.$('releaseInfo.additionalFiles.heading', { additionalFiles: language.countAdditionalFiles(fileCount, { unit: true, @@ -717,6 +720,19 @@ function unbound_generateNavigationLinks(current, { return language.formatUnitList(links); } +// Sticky heading, ooooo + +function unbound_generateStickyHeadingContainer(headingContent, { + html, +}) { + return html.tag('div', + {class: 'content-sticky-heading-container'}, + [ + html.tag('h1', headingContent), + html.tag('h2', {class: 'content-sticky-subheading'}), + ]); +} + // Footer stuff function unbound_getFooterLocalizationLinks(pathname, { @@ -794,5 +810,7 @@ export { unbound_generateInfoGalleryLinks as generateInfoGalleryLinks, unbound_generateNavigationLinks as generateNavigationLinks, + unbound_generateStickyHeadingContainer as generateStickyHeadingContainer, + unbound_getFooterLocalizationLinks as getFooterLocalizationLinks, } diff --git a/src/page/album-commentary.js b/src/page/album-commentary.js index 50a2aa39..90040026 100644 --- a/src/page/album-commentary.js +++ b/src/page/album-commentary.js @@ -21,6 +21,7 @@ export function write(album) { type: 'page', path: ['albumCommentary', album.directory], page: ({ + generateStickyHeadingContainer, getAlbumStylesheet, getLinkThemeString, getThemeString, @@ -35,23 +36,32 @@ export function write(album) { main: { content: html.tag('div', {class: 'long-content'}, [ - html.tag('h1', language.$('albumCommentaryPage.title', { - album: link.album(album), - })), - html.tag('p', language.$('albumCommentaryPage.infoLine', { - words: html.tag('b', language.formatWordCount(words, {unit: true})), - entries: html.tag('b', language.countCommentaryEntries(entries.length, {unit: true})), - })), + generateStickyHeadingContainer( + language.$('albumCommentaryPage.title', { + album: link.album(album), + })), + + html.tag('p', + language.$('albumCommentaryPage.infoLine', { + words: html.tag('b', language.formatWordCount(words, {unit: true})), + entries: html.tag('b', language.countCommentaryEntries(entries.length, {unit: true})), + })), + ...html.fragment(album.commentary && [ - html.tag('h3', language.$('albumCommentaryPage.entry.title.albumCommentary')), - html.tag('blockquote', transformMultiline(album.commentary)), + html.tag('h3', + {class: ['content-heading']}, + language.$('albumCommentaryPage.entry.title.albumCommentary')), + html.tag('blockquote', + transformMultiline(album.commentary)), ]), + ...album.tracks.filter(t => t.commentary).flatMap(track => [ html.tag('h3', - {id: 'track.directory'}, + {id: 'track.directory', class: ['content-heading']}, language.$('albumCommentaryPage.entry.title.trackCommentary', { track: link.track(track), })), + html.tag('blockquote', {style: getLinkThemeString(track.color)}, transformMultiline(track.commentary)), diff --git a/src/page/album.js b/src/page/album.js index cb512e86..1a90a79c 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -106,6 +106,7 @@ export function write(album, {wikiData}) { generateChronologyLinks, generateCoverLink, generateNavigationLinks, + generateStickyHeadingContainer, getAlbumCover, getAlbumStylesheet, getArtistString, @@ -159,9 +160,8 @@ export function write(album, {wikiData}) { tags: album.artTags, }), - html.tag('h1', language.$('albumPage.title', { - album: album.name, - })), + generateStickyHeadingContainer( + language.$('albumPage.title', {album: album.name})), html.tag('p', { @@ -254,6 +254,7 @@ export function write(album, {wikiData}) { tracks, }) => [ html.tag('dt', + {class: ['content-heading']}, language.$('trackList.section.withDuration', { duration: language.formatDuration(getTotalDuration(tracks), { approximate: tracks.length > 1, @@ -300,7 +301,9 @@ export function write(album, {wikiData}) { ...html.fragment( album.commentary && [ - html.tag('p', language.$('releaseInfo.artistCommentary')), + html.tag('p', + {class: ['content-heading']}, + language.$('releaseInfo.artistCommentary')), html.tag('blockquote', transformMultiline(album.commentary)), ]), ], diff --git a/src/page/artist.js b/src/page/artist.js index 6dd2ef30..b62b32b5 100644 --- a/src/page/artist.js +++ b/src/page/artist.js @@ -321,6 +321,7 @@ export function write(artist, {wikiData}) { fancifyURL, generateCoverLink, generateInfoGalleryLinks, + generateStickyHeadingContainer, getArtistAvatar, getArtistString, html, @@ -346,7 +347,7 @@ export function write(artist, {wikiData}) { alt: language.$('misc.alt.artistAvatar'), }), - html.tag('h1', + generateStickyHeadingContainer( language.$('artistPage.title', { artist: name, })), @@ -408,7 +409,7 @@ export function write(artist, {wikiData}) { ...html.fragment( !empty(allTracks) && [ html.tag('h2', - {id: 'tracks'}, + {id: 'tracks', class: ['content-heading']}, language.$('artistPage.trackList.title')), totalDuration > 0 && @@ -446,7 +447,7 @@ export function write(artist, {wikiData}) { ...html.fragment( !empty(artThingsAll) && [ html.tag('h2', - {id: 'art'}, + {id: 'art', class: ['content-heading']}, language.$('artistPage.artList.title')), hasGallery && @@ -513,7 +514,7 @@ export function write(artist, {wikiData}) { wikiInfo.enableFlashesAndGames && !empty(flashes) && [ html.tag('h2', - {id: 'flashes'}, + {id: 'flashes', class: ['content-heading']}, language.$('artistPage.flashList.title')), html.tag('dl', @@ -555,7 +556,7 @@ export function write(artist, {wikiData}) { ...html.fragment( !empty(commentaryThings) && [ html.tag('h2', - {id: 'commentary'}, + {id: 'commentary', class: ['content-heading']}, language.$('artistPage.commentaryList.title')), html.tag('dl', diff --git a/src/page/flash.js b/src/page/flash.js index e5353a18..d968d00b 100644 --- a/src/page/flash.js +++ b/src/page/flash.js @@ -20,6 +20,7 @@ export function write(flash, {wikiData}) { generateChronologyLinks, generateCoverLink, generateNavigationLinks, + generateStickyHeadingContainer, getArtistString, getFlashCover, getThemeString, @@ -44,7 +45,7 @@ export function write(flash, {wikiData}) { alt: language.$('misc.alt.flashArt'), }), - html.tag('h1', + generateStickyHeadingContainer( language.$('flashPage.title', { flash: flash.name, })), @@ -68,6 +69,7 @@ export function write(flash, {wikiData}) { ...html.fragment( !empty(flash.featuredTracks) && [ html.tag('p', + {class: ['content-heading']}, language.$('releaseInfo.tracksFeatured', { flash: html.tag('i', flash.name), })), @@ -87,6 +89,7 @@ export function write(flash, {wikiData}) { ...html.fragment( !empty(flash.contributorContribs) && [ html.tag('p', + {class: ['content-heading']}, language.$('releaseInfo.contributors')), html.tag('ul', diff --git a/src/page/group.js b/src/page/group.js index 1d586cf5..c4c376b6 100644 --- a/src/page/group.js +++ b/src/page/group.js @@ -32,6 +32,7 @@ export function write(group, {wikiData}) { fancifyURL, generateInfoGalleryLinks, generateNavigationLinks, + generateStickyHeadingContainer, getLinkThemeString, getThemeString, html, @@ -46,7 +47,7 @@ export function write(group, {wikiData}) { main: { content: [ - html.tag('h1', + generateStickyHeadingContainer( language.$('groupInfoPage.title', { group: group.name })), @@ -65,6 +66,7 @@ export function write(group, {wikiData}) { ...html.fragment( group.albums && [ html.tag('h2', + {class: ['content-heading']}, language.$('groupInfoPage.albumList.title')), html.tag('p', diff --git a/src/page/listing.js b/src/page/listing.js index cb297a89..fc643b11 100644 --- a/src/page/listing.js +++ b/src/page/listing.js @@ -32,6 +32,7 @@ export function write(listing, {wikiData}) { path: ['listing', listing.directory], page: (opts) => { const { + generateStickyHeadingContainer, getLinkThemeString, html, language, @@ -45,7 +46,7 @@ export function write(listing, {wikiData}) { main: { content: [ - html.tag('h1', + generateStickyHeadingContainer( language.$(titleKey)), ...html.fragment( @@ -230,6 +231,7 @@ function generateLinkIndexForListings(currentListing, forSidebar, { : html.tag('dl', filteredByCondition.flatMap(({title, listings}) => [ html.tag('dt', + {class: ['content-heading']}, title({language})), html.tag('dd', genUL(listings)), diff --git a/src/page/news.js b/src/page/news.js index 62f94fb9..78e25f41 100644 --- a/src/page/news.js +++ b/src/page/news.js @@ -14,6 +14,7 @@ export function write(entry, {wikiData}) { path: ['newsEntry', entry.directory], page: ({ generateNavigationLinks, + generateStickyHeadingContainer, html, language, link, @@ -24,7 +25,7 @@ export function write(entry, {wikiData}) { main: { content: html.tag('div', {class: 'long-content'}, [ - html.tag('h1', + generateStickyHeadingContainer( language.$('newsEntryPage.title', { entry: entry.name, })), @@ -58,6 +59,7 @@ export function writeTargetless({wikiData}) { type: 'page', path: ['newsIndex'], page: ({ + generateStickyHeadingContainer, html, language, link, @@ -70,7 +72,7 @@ export function writeTargetless({wikiData}) { html.tag('div', {class: ['long-content', 'news-index']}, [ - html.tag('h1', + generateStickyHeadingContainer( language.$('newsIndex.title')), ...newsData.map(entry => diff --git a/src/page/track.js b/src/page/track.js index 18fd7262..09c472ac 100644 --- a/src/page/track.js +++ b/src/page/track.js @@ -162,6 +162,7 @@ export function write(track, {wikiData}) { generateChronologyLinks, generateCoverLink, generateNavigationLinks, + generateStickyHeadingContainer, generateTrackListDividedByGroups, getAlbumStylesheet, getArtistString, @@ -229,7 +230,8 @@ export function write(track, {wikiData}) { tags: track.artTags, }), - html.tag('h1', language.$('trackPage.title', {track: track.name})), + generateStickyHeadingContainer( + language.$('trackPage.title', {track: track.name})), html.tag('p', { @@ -282,7 +284,9 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(otherReleases) && [ - html.tag('p', language.$('releaseInfo.alsoReleasedAs')), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.alsoReleasedAs')), + html.tag('ul', otherReleases.map(track => html.tag('li', language.$('releaseInfo.alsoReleasedAs.item', { track: link.track(track), @@ -292,7 +296,9 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(contributorContribs) && [ - html.tag('p', language.$('releaseInfo.contributors')), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.contributors')), + html.tag('ul', contributorContribs.map(contrib => html.tag('li', getArtistString([contrib], { showContrib: true, @@ -302,17 +308,21 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(referencedTracks) && [ - html.tag('p', language.$('releaseInfo.tracksReferenced', { - track: html.tag('i', track.name), - })), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.tracksReferenced', { + track: html.tag('i', track.name), + })), + html.tag('ul', referencedTracks.map(getTrackItem)), ]), ...html.fragment( !empty(referencedByTracks) && [ - html.tag('p', language.$('releaseInfo.tracksThatReference', { - track: html.tag('i', track.name), - })), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.tracksThatReference', { + track: html.tag('i', track.name), + })), + generateTrackListDividedByGroups(referencedByTracks, { getTrackItem, wikiData, @@ -321,26 +331,32 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(sampledTracks) && [ - html.tag('p', language.$('releaseInfo.tracksSampled', { - track: html.tag('i', track.name), - })), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.tracksSampled', { + track: html.tag('i', track.name), + })), + html.tag('ul', sampledTracks.map(getTrackItem)), ]), ...html.fragment( !empty(sampledByTracks) && [ - html.tag('p', language.$('releaseInfo.tracksThatSample', { - track: html.tag('i', track.name), - })), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.tracksThatSample', { + track: html.tag('i', track.name), + })), + html.tag('ul', sampledByTracks.map(getTrackItem)), ]), ...html.fragment( wikiInfo.enableFlashesAndGames && !empty(flashesThatFeature) && [ - html.tag('p', language.$('releaseInfo.flashesThatFeature', { - track: `${track.name}`, - })), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.flashesThatFeature', { + track: html.tag('i', track.name), + })), + html.tag('ul', flashesThatFeature.map(({flash, as}) => html.tag('li', {class: as !== track && 'rerelease'}, @@ -356,13 +372,17 @@ export function write(track, {wikiData}) { ...html.fragment( track.lyrics && [ - html.tag('p', language.$('releaseInfo.lyrics')), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.lyrics')), + html.tag('blockquote', transformLyrics(track.lyrics)), ]), ...html.fragment( hasCommentary && [ - html.tag('p', language.$('releaseInfo.artistCommentary')), + html.tag('p', {class: ['content-heading']}, + language.$('releaseInfo.artistCommentary')), + html.tag('blockquote', generateCommentary({ link, language, diff --git a/src/static/client.js b/src/static/client.js index b5ed6868..ebe8604f 100644 --- a/src/static/client.js +++ b/src/static/client.js @@ -443,3 +443,90 @@ function addInfoCardLinkHandlers(type) { if (localStorage.tryInfoCards) { addInfoCardLinkHandlers('track'); } + +// Sticky content heading --------------------------------- + +const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container')) + .map(stickyContainer => { + const {parentElement: contentContainer} = stickyContainer; + const stickySubheading = stickyContainer.querySelector('.content-sticky-subheading'); + const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading')); + + return { + contentContainer, + contentHeadings, + stickyContainer, + stickySubheading, + state: { + displayedHeading: null, + }, + }; + }); + +const topOfViewInside = (el, scroll = window.scrollY) => ( + scroll > el.offsetTop && + scroll < el.offsetTop + el.offsetHeight +); + +function updateStickyHeading() { + for (const { + contentContainer, + contentHeadings, + stickyContainer, + stickySubheading, + state, + } of stickyHeadingInfo) { + let closestHeading = null; + + if (topOfViewInside(contentContainer)) { + if (stickySubheading.childNodes.length === 0) { + //   to ensure correct basic line height + stickySubheading.appendChild(document.createTextNode('\xA0')); + } + + const stickyRect = stickyContainer.getBoundingClientRect(); + const subheadingRect = stickySubheading.getBoundingClientRect(); + const stickyBottom = stickyRect.bottom + subheadingRect.height; + + // This array is reversed so that we're starting from the bottom when + // iterating over it. + for (let i = contentHeadings.length - 1; i >= 0; i--) { + const heading = contentHeadings[i]; + const headingRect = heading.getBoundingClientRect(); + if (headingRect.y + headingRect.height / 1.5 < stickyBottom) { + closestHeading = heading; + break; + } + } + } + + if (state.displayedHeading !== closestHeading) { + if (closestHeading) { + // Array.from needed to iterate over a live array with for..of + for (const child of Array.from(stickySubheading.childNodes)) { + child.remove(); + } + + for (const child of closestHeading.childNodes) { + if (child.tagName === 'A') { + for (const grandchild of child.childNodes) { + stickySubheading.appendChild(grandchild.cloneNode(true)); + } + } else { + stickySubheading.appendChild(child.cloneNode(true)); + } + } + + stickySubheading.classList.add('visible'); + } else { + stickySubheading.classList.remove('visible'); + } + + state.displayedHeading = closestHeading; + } + } +} + +document.addEventListener('scroll', updateStickyHeading); + +updateStickyHeading(); diff --git a/src/static/site2.css b/src/static/site2.css index 33553e84..070d89ee 100644 --- a/src/static/site2.css +++ b/src/static/site2.css @@ -983,11 +983,15 @@ li > ul { /* sticky headers */ -#content:not(.no-sticky-heading) h1:first-of-type, +#content:not(.no-sticky-heading) > h1:first-of-type, .sidebar:not(.no-sticky-heading) h1:first-of-type { position: sticky; top: 0; +} +#content .content-sticky-heading-container h1, +#content:not(.no-sticky-heading) > h1:first-of-type, +.sidebar:not(.no-sticky-heading) h1:first-of-type { margin: calc(-1 * var(--content-padding)); margin-bottom: calc(0.5 * var(--content-padding)); padding: @@ -997,14 +1001,74 @@ li > ul { 20px; background: var(--bg-black-color); - border-bottom: 1px dotted rgba(180, 180, 180, 0.4); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); } -#content:not(.no-sticky-heading) h1:first-of-type { +#content .content-sticky-heading-container { + position: sticky; + top: 0; + + /* Safari doesn't always play nicely with position: sticky, + * this seems to fix images sometimes displaying above the + * position: absolute subheading (h2) child + * + * See also: https://stackoverflow.com/questions/50224855/ + */ + transform: translate3d(0, 0, 0); z-index: 1; +} + +#content .content-sticky-heading-container h1 { + margin-bottom: 0; +} + +#content .content-sticky-heading-container h2 { + position: absolute; + margin: 0 calc(-1 * var(--content-padding)); + width: 100%; + padding: 10px 40px 5px 20px; + + font-size: 0.9em; + font-weight: normal; + font-style: oblique; + color: #eee; + + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); + + transition: margin-top 0.35s, opacity 0.25s; +} + +#content .content-sticky-heading-container h2:not(.visible) { + margin-top: -20px; + opacity: 0; +} + +#content .content-sticky-heading-container h2.visible { + margin-top: 0; + opacity: 1; +} + +#content:not(.no-sticky-heading) > h1:first-of-type { + z-index: 1; + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 4px rgba(0, 0, 0, 0.8); +} + +#content .content-sticky-heading-container h1 { + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 4px rgba(0, 0, 0, 0.8); +} + +#content .content-sticky-heading-container h2.visible { box-shadow: inset 0 10px 10px -5px var(--shadow-color), 0 4px 4px rgba(0, 0, 0, 0.8); diff --git a/src/upd8.js b/src/upd8.js index 89984c08..bb03c1a0 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -85,6 +85,7 @@ import { generateCoverLink, generateInfoGalleryLinks, generateNavigationLinks, + generateStickyHeadingContainer, generateTrackListDividedByGroups, getAlbumGridHTML, getAlbumStylesheet, @@ -2462,6 +2463,10 @@ async function main() { language, }); + bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, { + html, + }); + bound.generateChronologyLinks = bindOpts(generateChronologyLinks, { html, language, -- cgit 1.3.0-6-gf8a5