diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js index 527e47411..1b4b6eca0 100644 --- a/src/content/dependencies/generateArtistNavLinks.js +++ b/src/content/dependencies/generateArtistNavLinks.js @@ -2,43 +2,44 @@ import {empty} from '#sugar'; export default { contentDependencies: [ + 'generateInterpageDotSwitcher', 'linkArtist', 'linkArtistGallery', ], extraDependencies: ['html', 'language', 'wikiData'], - sprawl({wikiInfo}) { - return { - enableListings: wikiInfo.enableListings, - }; - }, + sprawl: ({wikiInfo}) => ({ + enableListings: + wikiInfo.enableListings, + }), - relations(relation, sprawl, artist) { - const relations = {}; + query: (_sprawl, artist) => ({ + hasGallery: + !empty(artist.albumCoverArtistContributions) || + !empty(artist.trackCoverArtistContributions), + }), - relations.artistMainLink = - relation('linkArtist', artist); + relations: (relation, query, _sprawl, artist) => ({ + switcher: + relation('generateInterpageDotSwitcher'), - relations.artistInfoLink = - relation('linkArtist', artist); + artistMainLink: + relation('linkArtist', artist), - if ( - !empty(artist.albumCoverArtistContributions) || - !empty(artist.trackCoverArtistContributions) - ) { - relations.artistGalleryLink = - relation('linkArtistGallery', artist); - } + artistInfoLink: + relation('linkArtist', artist), - return relations; - }, + artistGalleryLink: + (query.hasGallery + ? relation('linkArtistGallery', artist) + : null), + }), - data(sprawl) { - return { - enableListings: sprawl.enableListings, - }; - }, + data: (_query, sprawl) => ({ + enableListings: + sprawl.enableListings, + }), slots: { showExtraLinks: {type: 'boolean', default: false}, @@ -48,53 +49,46 @@ export default { }, }, - generate(data, relations, slots, {html, language}) { - const infoLink = - relations.artistInfoLink?.slots({ - attributes: {class: slots.currentExtra === null && 'current'}, - content: language.$('misc.nav.info'), - }); - - const {content: extraLinks = []} = - slots.showExtraLinks && - {content: [ - relations.artistGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('misc.nav.gallery'), - }), - ]}; - - const mostAccentLinks = [ - ...extraLinks, - ].filter(Boolean); - - // Don't show the info accent link all on its own. - const allAccentLinks = - (empty(mostAccentLinks) - ? [] - : [infoLink, ...mostAccentLinks]); - - const accent = - (empty(allAccentLinks) - ? html.blank() - : `(${language.formatUnitList(allAccentLinks)})`); - - return [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, + generate: (data, relations, slots, {html, language}) => [ + {auto: 'home'}, + data.enableListings && { - accent, - html: - language.$('artistPage.nav.artist', { - artist: relations.artistMainLink, - }), + path: ['localized.listingIndex'], + title: language.$('listingIndex.title'), }, - ]; - }, + + { + html: + language.$('artistPage.nav.artist', { + artist: relations.artistMainLink, + }), + + accent: + relations.switcher.slots({ + links: [ + relations.artistInfoLink.slots({ + attributes: [ + slots.currentExtra === null && + {class: 'current'}, + + {[html.onlyIfSiblings]: true}, + ], + + content: language.$('misc.nav.info'), + }), + + slots.showExtraLinks && + relations.artistGalleryLink?.slots({ + attributes: [ + slots.currentExtra === 'gallery' && + {class: 'current'}, + ], + + content: language.$('misc.nav.gallery'), + }), + ], + }), + }, + ], }; diff --git a/src/content/dependencies/generateDotSwitcherTemplate.js b/src/content/dependencies/generateDotSwitcherTemplate.js index 745a14711..222059229 100644 --- a/src/content/dependencies/generateDotSwitcherTemplate.js +++ b/src/content/dependencies/generateDotSwitcherTemplate.js @@ -16,6 +16,7 @@ export default { generate: (slots, {html}) => html.tag('span', {class: 'dot-switcher'}, + {[html.onlyIfContent]: true}, {[html.noEdgeWhitespace]: true}, {[html.joinChildren]: ''}, @@ -26,8 +27,15 @@ export default { html.tag('span', {[html.onlyIfContent]: true}, + html.resolve(option, {normalize: 'tag'}) + .onlyIfSiblings && + {[html.onlyIfSiblings]: true}, + index === slots.initialOptionIndex && {class: 'current'}, - option))), + [ + html.metatag('imaginary-sibling'), + option, + ]))), }; diff --git a/src/util/html.js b/src/util/html.js index 73e088546..85464b72f 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -105,6 +105,12 @@ export const blockwrap = Symbol(); // considered wrappable units, not the entire element! export const chunkwrap = Symbol(); +// Don't pass this directly, use html.metatag('imaginary-sibling') instead. +// A tag without any content, which is completely ignored when serializing, +// but makes siblings with [onlyIfSiblings] feel less shy and show up on +// their own, even without a non-blank (and non-onlyIfSiblings) sibling. +export const imaginarySibling = Symbol(); + // Recursive helper function for isBlank, which basically flattens an array // and returns as soon as it finds any content - a non-blank case - and doesn't // traverse templates of its own accord. If it doesn't find directly non-blank @@ -322,6 +328,9 @@ export function metatag(identifier, ...args) { case 'chunkwrap': return new Tag(null, {[chunkwrap]: true, ...opts}, content); + case 'imaginary-sibling': + return new Tag(null, {[imaginarySibling]: true}, content); + default: throw new Error(`Unknown metatag "${identifier}"`); } @@ -392,16 +401,22 @@ export class Tag { } set content(value) { - if ( - this.selfClosing && - !(value === null || - value === undefined || - !value || - Array.isArray(value) && value.filter(Boolean).length === 0) - ) { + const contentful = + value !== null && + value !== undefined && + value && + (Array.isArray(value) + ? !empty(value.filter(Boolean)) + : true); + + if (this.selfClosing && contentful) { throw new Error(`Tag <${this.tagName}> is self-closing but got content`); } + if (this.imaginarySibling && contentful) { + throw new Error(`html.metatag('imaginary-sibling') can't have content`); + } + const contentArray = (Array.isArray(value) ? value.flat(Infinity).filter(Boolean) @@ -440,6 +455,10 @@ export class Tag { // something about this tag's own content or attributes. It does *not* // account for [html.onlyIfSiblings]! + if (this.imaginarySibling) { + return true; + } + if (this.onlyIfContent && isBlank(this.content)) { return true; } @@ -544,7 +563,7 @@ export class Tag { this.#setAttributeFlag(chunkwrap, value); try { - this.content = content; + this.content = this.content; } catch (error) { this.#setAttributeFlag(chunkwrap, false); throw error; @@ -555,6 +574,20 @@ export class Tag { return this.#getAttributeFlag(chunkwrap); } + set imaginarySibling(value) { + this.#setAttributeFlag(imaginarySibling, value); + + try { + this.content = this.content; + } catch (error) { + this.#setAttributeFlag(imaginarySibling, false); + } + } + + get imaginarySibling() { + return this.#getAttributeFlag(imaginarySibling); + } + toString() { if (this.onlyIfContent && isBlank(this.content)) { return ''; @@ -652,6 +685,11 @@ export class Tag { const nonTemplateItem = Template.resolve(item); + if (nonTemplateItem instanceof Tag && nonTemplateItem.imaginarySibling) { + seenSiblingIndependentContent = true; + continue; + } + let itemContent; try { itemContent = nonTemplateItem.toString();