From 6d8f75dd5873f1427a343971edd0e0ea40b015a5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 28 Feb 2023 19:50:01 -0400 Subject: hash link highlighting & additional skippers --- src/misc-templates.js | 16 +++++++ src/page/track.js | 103 ++++++++++++++++++++++++++---------------- src/static/client.js | 34 ++++++++++++++ src/static/site3.css | 47 +++++++++++++++++++- src/strings-default.json | 23 +++++++--- src/write/bind-utilities.js | 6 +++ src/write/page-template.js | 106 ++++++++++++++++++++++++++++++++++---------- 7 files changed, 266 insertions(+), 69 deletions(-) diff --git a/src/misc-templates.js b/src/misc-templates.js index 1c6dda55..db97e536 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -916,6 +916,21 @@ function unbound_generateNavigationLinks(current, { // Sticky heading, ooooo +function unbound_generateContentHeading({ + html, + + id, + title, +}) { + return html.tag('p', + { + class: 'content-heading', + id, + tabindex: '0', + }, + title); +} + function unbound_generateStickyHeadingContainer({ getRevealStringFromArtTags, html, @@ -1025,6 +1040,7 @@ export { unbound_generateInfoGalleryLinks as generateInfoGalleryLinks, unbound_generateNavigationLinks as generateNavigationLinks, + unbound_generateContentHeading as generateContentHeading, unbound_generateStickyHeadingContainer as generateStickyHeadingContainer, unbound_getFooterLocalizationLinks as getFooterLocalizationLinks, diff --git a/src/page/track.js b/src/page/track.js index b9038bac..7f0d1cf2 100644 --- a/src/page/track.js +++ b/src/page/track.js @@ -170,6 +170,7 @@ export function write(track, {wikiData}) { generateAdditionalFilesList, generateAdditionalFilesShortcut, generateChronologyLinks, + generateContentHeading, generateNavigationLinks, generateTrackListDividedByGroups, getAlbumStylesheet, @@ -324,8 +325,10 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(otherReleases) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.alsoReleasedAs')), + generateContentHeading({ + id: 'also-released-as', + title: language.$('releaseInfo.alsoReleasedAs'), + }), html.tag('ul', otherReleases.map(track => html.tag('li', language.$('releaseInfo.alsoReleasedAs.item', { @@ -336,8 +339,10 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(contributorContribs) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.contributors')), + generateContentHeading({ + id: 'contributors', + title: language.$('releaseInfo.contributors'), + }), html.tag('ul', contributorContribs.map(contrib => html.tag('li', getArtistString([contrib], { @@ -348,20 +353,26 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(referencedTracks) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.tracksReferenced', { - track: html.tag('i', track.name), - })), + generateContentHeading({ + id: 'references', + title: + language.$('releaseInfo.tracksReferenced', { + track: html.tag('i', track.name), + }), + }), html.tag('ul', referencedTracks.map(getTrackItem)), ]), ...html.fragment( !empty(referencedByTracks) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.tracksThatReference', { - track: html.tag('i', track.name), - })), + generateContentHeading({ + id: 'referenced-by', + title: + language.$('releaseInfo.tracksThatReference', { + track: html.tag('i', track.name), + }), + }), generateTrackListDividedByGroups(referencedByTracks, { getTrackItem, @@ -371,20 +382,26 @@ export function write(track, {wikiData}) { ...html.fragment( !empty(sampledTracks) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.tracksSampled', { - track: html.tag('i', track.name), - })), + generateContentHeading({ + id: 'samples', + title: + language.$('releaseInfo.tracksSampled', { + track: html.tag('i', track.name), + }), + }), html.tag('ul', sampledTracks.map(getTrackItem)), ]), ...html.fragment( !empty(sampledByTracks) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.tracksThatSample', { - track: html.tag('i', track.name), - })), + generateContentHeading({ + id: 'sampled-by', + title: + language.$('releaseInfo.tracksThatSample', { + track: html.tag('i', track.name), + }) + }), html.tag('ul', sampledByTracks.map(getTrackItem)), ]), @@ -392,10 +409,13 @@ export function write(track, {wikiData}) { ...html.fragment( wikiInfo.enableFlashesAndGames && !empty(flashesThatFeature) && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.flashesThatFeature', { - track: html.tag('i', track.name), - })), + generateContentHeading({ + id: 'featured-in', + title: + language.$('releaseInfo.flashesThatFeature', { + track: html.tag('i', track.name), + }), + }), html.tag('ul', flashesThatFeature.map(({flash, as}) => html.tag('li', @@ -412,17 +432,20 @@ export function write(track, {wikiData}) { ...html.fragment( track.lyrics && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.lyrics')), + generateContentHeading({ + id: 'lyrics', + title: language.$('releaseInfo.lyrics'), + }), html.tag('blockquote', transformLyrics(track.lyrics)), ]), ...html.fragment( hasSheetMusicFiles && [ - html.tag('p', - {id: 'sheet-music-files', class: ['content-heading']}, - language.$('releaseInfo.sheetMusicFiles.heading')), + generateContentHeading({ + id: 'sheet-music-files', + title: language.$('releaseInfo.sheetMusicFiles.heading'), + }), generateAlbumAdditionalFilesList(album, track.sheetMusicFiles, { fileSize: false, @@ -431,30 +454,34 @@ export function write(track, {wikiData}) { ...html.fragment( hasMidiProjectFiles && [ - html.tag('p', - {id: 'midi-project-files', class: ['content-heading']}, - language.$('releaseInfo.midiProjectFiles.heading')), + generateContentHeading({ + id: 'midi-project-files', + title: language.$('releaseInfo.midiProjectFiles.heading'), + }), generateAlbumAdditionalFilesList(album, track.midiProjectFiles), ]), ...html.fragment( hasAdditionalFiles && [ - html.tag('p', - {id: 'additional-files', class: ['content-heading']}, - language.$('releaseInfo.additionalFiles.heading', { + generateContentHeading({ + id: 'additional-files', + title: language.$('releaseInfo.additionalFiles.heading', { additionalFiles: language.countAdditionalFiles(numAdditionalFiles, { unit: true, }), - })), + }) + }), generateAlbumAdditionalFilesList(album, track.additionalFiles), ]), ...html.fragment( hasCommentary && [ - html.tag('p', {class: ['content-heading']}, - language.$('releaseInfo.artistCommentary')), + generateContentHeading({ + id: 'artist-commentary', + title: language.$('releaseInfo.artistCommentary'), + }), html.tag('blockquote', generateCommentary({ link, diff --git a/src/static/client.js b/src/static/client.js index 87b74004..4eb1d2ba 100644 --- a/src/static/client.js +++ b/src/static/client.js @@ -452,6 +452,8 @@ function addHashLinkHandlers() { // This lets the scroll offset be consolidated where it makes sense, and // sets an appropriate offset when (re)loading a page with hash for free! + let wasHighlighted; + for (const a of document.links) { const href = a.getAttribute('href'); if (!href || !href.startsWith('#')) { @@ -469,6 +471,17 @@ function addHashLinkHandlers() { const href = evt.target.getAttribute('href'); const id = href.slice(1); const linked = document.getElementById(id); + + if (!linked) { + return; + } + + // Hide skipper box right away, so the layout is updated on time for the + // math operations coming up next. + const skipper = document.getElementById('skippers'); + skipper.style.display = 'none'; + setTimeout(() => skipper.style.display = ''); + const box = linked.getBoundingClientRect(); const style = window.getComputedStyle(linked); @@ -480,6 +493,27 @@ function addHashLinkHandlers() { evt.preventDefault(); history.pushState({}, '', href); window.scrollTo({top: scrollY, behavior: 'smooth'}); + linked.focus({preventScroll: true}); + + const maxScroll = + document.body.scrollHeight + - window.innerHeight; + + if (scrollY > maxScroll && linked.classList.contains('content-heading')) { + if (wasHighlighted) { + wasHighlighted.classList.remove('highlight-hash-link'); + } + + wasHighlighted = linked; + linked.classList.add('highlight-hash-link'); + linked.addEventListener('animationend', function handle(evt) { + if (evt.animationName === 'highlight-hash-link') { + linked.removeEventListener('animationend', handle); + linked.classList.remove('highlight-hash-link'); + wasHighlighted = null; + } + }); + } } } diff --git a/src/static/site3.css b/src/static/site3.css index 9128bd81..c522bc9d 100644 --- a/src/static/site3.css +++ b/src/static/site3.css @@ -208,7 +208,19 @@ body::before { box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); } -#skippers > .skipper:not(:last-child)::after { +#skippers > * { + display: inline-block; +} + +#skippers > .skipper-list:not(:last-child)::after { + display: inline-block; + content: "\00a0"; + margin-left: 2px; + margin-right: -2px; + border-left: 1px dotted; +} + +#skippers .skipper-list > .skipper:not(:last-child)::after { content: " \00b7 "; font-weight: 800; } @@ -1137,6 +1149,37 @@ html[data-url-key="localized.home"] .carousel-container { margin-bottom: 0; } +/* Custom hash links */ + +.content-heading { + border-bottom: 3px double transparent; + margin-bottom: -3px; +} + +.content-heading.highlight-hash-link { + animation: highlight-hash-link 4s; + animation-delay: 125ms; +} + +/* This animation's name is referenced in JavaScript */ +@keyframes highlight-hash-link { + 0% { + border-bottom-color: transparent; + } + + 10% { + border-bottom-color: white; + } + + 25% { + border-bottom-color: white; + } + + 100% { + border-bottom-color: transparent; + } +} + /* Sticky heading */ #content [id] { @@ -1145,7 +1188,7 @@ html[data-url-key="localized.home"] .carousel-container { 74px /* Sticky heading */ + 33px /* Sticky subheading */ - 1em /* One line of text (align bottom) */ - - 4px /* Padding for hanging letters */ + - 12px /* Padding for hanging letters & focus ring */ ); } diff --git a/src/strings-default.json b/src/strings-default.json index 5d6935e1..97be1f02 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -181,11 +181,24 @@ "misc.nav.gallery": "Gallery", "misc.pageTitle": "{TITLE}", "misc.pageTitle.withWikiName": "{TITLE} | {WIKI_NAME}", - "misc.skippers.skipToContent": "Skip to content", - "misc.skippers.skipToSidebar": "Skip to sidebar", - "misc.skippers.skipToSidebar.left": "Skip to sidebar (left)", - "misc.skippers.skipToSidebar.right": "Skip to sidebar (right)", - "misc.skippers.skipToFooter": "Skip to footer", + "misc.skippers.skipTo": "Skip to:", + "misc.skippers.content": "Content", + "misc.skippers.sidebar": "Sidebar", + "misc.skippers.sidebar.left": "Sidebar (left)", + "misc.skippers.sidebar.right": "Sidebar (right)", + "misc.skippers.header": "Header", + "misc.skippers.footer": "Footer", + "misc.skippers.contributors": "Contributors", + "misc.skippers.references": "References...", + "misc.skippers.referencedBy": "Referenced by...", + "misc.skippers.samples": "Samples...", + "misc.skippers.sampledBy": "Sampled by...", + "misc.skippers.featuredIn": "Featured in...", + "misc.skippers.lyrics": "Lyrics", + "misc.skippers.sheetMusicFiles": "Sheet music files", + "misc.skippers.midiProjectFiles": "MIDI/project files", + "misc.skippers.additionalFiles": "Additional files", + "misc.skippers.artistCommentary": "Commentary", "misc.socialEmbed.heading": "{WIKI_NAME} | {HEADING}", "misc.jumpTo": "Jump to:", "misc.jumpTo.withLinks": "Jump to: {LINKS}.", diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 127afe2c..427111b4 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -19,6 +19,7 @@ import { generateAdditionalFilesList, generateAdditionalFilesShortcut, generateChronologyLinks, + generateContentHeading, generateCoverLink, generateInfoGalleryLinks, generateTrackListDividedByGroups, @@ -192,6 +193,11 @@ export function bindUtilities({ language, }); + bound.generateContentHeading = bindOpts(generateContentHeading, { + [bindOpts.bindIndex]: 0, + html, + }); + bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, { [bindOpts.bindIndex]: 0, getRevealStringFromArtTags: bound.getRevealStringFromArtTags, diff --git a/src/write/page-template.js b/src/write/page-template.js index de369018..96036df2 100644 --- a/src/write/page-template.js +++ b/src/write/page-template.js @@ -401,6 +401,87 @@ export function generateDocumentHTML(pageInfo, { footerHTML, ].filter(Boolean).join('\n'); + const processSkippers = skipperList => + skipperList + .filter(Boolean) + .map(([href, stringSubkey]) => + html.tag('span', {class: 'skipper'}, + html.tag('a', + {href}, + language.$(`misc.skippers.${stringSubkey}`)))); + + // Hilariously jank. Sorry! + const hasID = id => mainHTML.includes(`id="${id}"`); + const hasContributors = hasID('contributors'); + const hasReferences = hasID('references'); + const hasReferencedBy = hasID('referenced-by'); + const hasSamples = hasID('samples'); + const hasSampledBy = hasID('sampled-by'); + const hasFeaturedIn = hasID('featured-in'); + const hasLyrics = hasID('lyrics'); + const hasSheetMusicFiles = hasID('sheet-music-files'); + const hasMidiProjectFiles = hasID('midi-project-files'); + const hasAdditionalFiles = hasID('additional-files'); + const hasArtistCommentary = hasID('artist-commentary'); + + const skippersHTML = + mainHTML && + html.tag('div', {id: 'skippers'}, [ + html.tag('span', language.$('misc.skippers.skipTo')), + html.tag('div', {class: 'skipper-list'}, + processSkippers([ + ['#content', 'content'], + sidebarLeftHTML && + [ + '#sidebar-left', + sidebarRightHTML + ? 'sidebar.left' + : 'sidebar', + ], + sidebarRightHTML && + [ + '#sidebar-right', + sidebarLeftHTML + ? 'sidebar.right' + : 'sidebar', + ], + navHTML && + ['#header', 'header'], + footerHTML && + ['#footer', 'footer'], + ])), + + html.tag('div', + { + [html.onlyIfContent]: true, + class: 'skipper-list' + }, + processSkippers([ + hasContributors && + ['#contributors', 'contributors'], + hasReferences && + ['#references', 'references'], + hasReferencedBy && + ['#referenced-by', 'referencedBy'], + hasSamples && + ['#samples', 'samples'], + hasSampledBy && + ['#sampled-by', 'sampledBy'], + hasFeaturedIn && + ['#featured-in', 'featuredIn'], + hasLyrics && + ['#lyrics', 'lyrics'], + hasSheetMusicFiles && + ['#sheet-music-files', 'sheetMusicFiles'], + hasMidiProjectFiles && + ['#midi-project-files', 'midiProjectFiles'], + hasAdditionalFiles && + ['#additional-files', 'additionalFiles'], + hasArtistCommentary && + ['#artist-commentary', 'artistCommentary'], + ])), + ]); + const infoCardHTML = html.tag('div', {id: 'info-card-container'}, html.tag('div', {id: 'info-card-decor'}, html.tag('div', {id: 'info-card'}, [ @@ -552,30 +633,7 @@ export function generateDocumentHTML(pageInfo, { [ html.tag('div', {id: 'page-container'}, [ mainHTML && - html.tag('div', {id: 'skippers'}, - [ - ['#content', language.$('misc.skippers.skipToContent')], - sidebarLeftHTML && - [ - '#sidebar-left', - sidebarRightHTML - ? language.$('misc.skippers.skipToSidebar.left') - : language.$('misc.skippers.skipToSidebar'), - ], - sidebarRightHTML && - [ - '#sidebar-right', - sidebarLeftHTML - ? language.$('misc.skippers.skipToSidebar.right') - : language.$('misc.skippers.skipToSidebar'), - ], - footerHTML && - ['#footer', language.$('misc.skippers.skipToFooter')], - ] - .filter(Boolean) - .map(([href, title]) => - html.tag('span', {class: 'skipper'}, - html.tag('a', {href}, title)))), + skippersHTML, layoutHTML, ]), -- cgit 1.3.0-6-gf8a5