diff options
Diffstat (limited to 'src/static')
-rw-r--r-- | src/static/css/site.css | 559 | ||||
-rw-r--r-- | src/static/js/client-util.js | 29 | ||||
-rw-r--r-- | src/static/js/client/artist-rolling-window.js | 573 | ||||
-rw-r--r-- | src/static/js/client/expandable-gallery-section.js | 77 | ||||
-rw-r--r-- | src/static/js/client/expandable-grid-section.js | 85 | ||||
-rw-r--r-- | src/static/js/client/gallery-style-selector.js | 123 | ||||
-rw-r--r-- | src/static/js/client/hoverable-tooltip.js | 30 | ||||
-rw-r--r-- | src/static/js/client/index.js | 10 | ||||
-rw-r--r-- | src/static/js/client/quick-description.js | 2 | ||||
-rw-r--r-- | src/static/js/client/reveal-all-grid-control.js | 72 | ||||
-rw-r--r-- | src/static/js/client/sidebar-search.js | 58 | ||||
-rw-r--r-- | src/static/js/search-worker.js | 5 |
12 files changed, 1449 insertions, 174 deletions
diff --git a/src/static/css/site.css b/src/static/css/site.css index 6fa4da38..e584f918 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -61,7 +61,7 @@ body::before, .wallpaper-part { #page-container { max-width: 1100px; - margin: 0 auto 40px; + margin: 0 auto 38px; padding: 15px 0; } @@ -76,10 +76,25 @@ body::before, .wallpaper-part { height: unset; } +@property --banner-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + #banner { margin: 10px 0; width: 100%; position: relative; + + --banner-shine: 4%; + -webkit-box-reflect: below -12px linear-gradient(transparent, color-mix(in srgb, transparent, var(--banner-shine) white)); + transition: --banner-shine 0.8s; +} + +#banner:hover { + --banner-shine: 35%; + transition-delay: 0.3s; } #banner::after { @@ -261,7 +276,11 @@ body::before, .wallpaper-part { #page-container { background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); color: #ffffff; - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); + border-bottom: 2px solid #fff1; + box-shadow: + 0 0 40px #0008, + 0 2px 15px -3px #2221, + 0 2px 6px 2px #1113; } #skippers > * { @@ -899,6 +918,11 @@ summary.underline-white > span:hover a:not(:hover) { display: inline-block; } +.wiki-search-result-disambiguator { + opacity: 0.9; + display: inline-block; +} + .wiki-search-result-image-container { align-self: flex-start; flex-shrink: 0; @@ -1013,10 +1037,13 @@ a .normal-content { } .image-media-link::after { - content: ''; - display: inline-block; - width: 22px; - height: 1em; + /* Thanks to Jay Freestone for being awesome: + * https://www.jayfreestone.com/writing/wrapping-and-inline-pseudo-elements/ + */ + + pointer-events: none; + content: '\200b'; + padding-left: 22px; background-color: var(--primary-color); @@ -1027,7 +1054,6 @@ a .normal-content { mask-repeat: no-repeat; mask-position: calc(100% - 2px); - vertical-align: text-bottom; } .image-media-link:hover::after { @@ -1095,7 +1121,16 @@ a .normal-content { font-weight: 800; } +.dot-switcher > span { + color: #ffffffcc; +} + .dot-switcher > span.current { + font-weight: normal; + color: white; +} + +.dot-switcher > span.current a { font-weight: 800; } @@ -1109,6 +1144,15 @@ a .normal-content { text-decoration: none !important; } +label:hover span { + text-decoration: underline; + text-decoration-style: solid; +} + +label > input[type=checkbox]:not(:checked) + span { + opacity: 0.8; +} + #secondary-nav { text-align: center; @@ -1193,7 +1237,12 @@ a .normal-content { white-space: nowrap; } -.isolate-tooltip-z-indexing > * { +:where(.isolate-tooltip-z-indexing) { + position: relative; + z-index: 1; +} + +:where(.isolate-tooltip-z-indexing > *) { position: relative; z-index: -1; } @@ -1296,6 +1345,16 @@ li:not(:first-child:last-child) .tooltip:where(:not(.cover-artwork .tooltip)), height: 1.4em; } +.contribution-tooltip .chronology-heading { + grid-column-start: handle-start; + grid-column-end: platform-end; + min-width: 30ch; + + font-size: 0.85em; + font-style: oblique; + margin-bottom: 2px; +} + .contribution-tooltip .chronology-link { display: grid; grid-column-start: icon-start; @@ -1503,10 +1562,26 @@ s.spoiler::-moz-selection { background: white; } -span.path { - font-size: 0.9em; +span.path, code.filename { + font-size: 0.95em; font-family: "courier new", monospace; font-weight: 800; + background: #ccc3; + + padding: 0.05em 0.5ch; + border: 1px solid #ccce; + border-radius: 2px; + line-height: 1.4; +} + +.image-details code.filename { + margin-left: -0.4ch; + opacity: 0.8; +} + +.image-details code.filename:hover { + opacity: 1; + cursor: text; } span.path i { @@ -1575,9 +1650,11 @@ hr.cute, } #artwork-column .cover-artwork { + --normal-shadow: 0 0 12px 12px #00000080; + box-shadow: 0 2px 14px -6px var(--primary-color), - 0 0 12px 12px #00000080; + var(--normal-shadow); } #artwork-column .cover-artwork:not(:first-child), @@ -1586,6 +1663,17 @@ hr.cute, margin-right: 5px; } +#artwork-column .cover-artwork:not(:first-child) { + --normal-shadow: 0 0 9px 9px #00000068; +} + +#artwork-column .cover-artwork:first-child + .cover-artwork-joiner, +#artwork-column .cover-artwork.attached-artwork-is-main-artwork, +#artwork-column .cover-artwork.attached-artwork-is-main-artwork + .cover-artwork-joiner { + margin-left: 17.5px; + margin-right: 17.5px; +} + .cover-artwork:where(#artwork-column .cover-artwork:not(:first-child)) { margin-top: 20px; } @@ -1672,6 +1760,11 @@ p.image-details.origin-details { margin-bottom: 2px; } +p.image-details.origin-details .origin-details { + display: block; + margin-top: 0.25em; +} + .cover-artwork-joiner { z-index: -2; } @@ -1743,6 +1836,15 @@ p.image-details.origin-details { color: var(--primary-color); } +.inherited-commentary-section { + clear: right; + margin-top: 1em; + margin-right: min(4vw, 60px); + border: 2px solid var(--deep-color); + border-radius: 4px; + background: #ffffff07; +} + .commentary-art { float: right; width: 30%; @@ -1770,11 +1872,49 @@ p.image-details.origin-details { padding-left: 40px; } -.lyrics-entry .lyrics-details { +.lyrics-entry .lyrics-details, +.lyrics-entry .origin-details { font-size: 0.9em; font-style: oblique; } +.lyrics-entry .lyrics-details { + margin-bottom: 0; +} + +.lyrics-entry .origin-details { + margin-top: 0.25em; +} + +.lyrics-entry.long-lyrics { + clip-path: inset(-15px -20px); +} + +.lyrics-entry.long-lyrics::after { + content: ""; + pointer-events: none; + display: block; + + /* Slight stretching past the bottom of the screen seems + * to make resizing the window (and "revealing" that area) + * a bit smoother. + */ + position: fixed; + bottom: -20px; + left: 0; + right: 0; + + height: calc(20px + min(90px, 13.5vh)); + background: linear-gradient(to bottom, transparent, black 70%, black); + opacity: 0.6; +} + +.lyrics-entry sup { + vertical-align: text-top; + opacity: 0.8; + cursor: default; +} + .js-hide, .js-show-once-data, .js-hide-once-data { @@ -1782,7 +1922,8 @@ p.image-details.origin-details { } .content-image-container, -.content-video-container { +.content-video-container, +.content-audio-container { margin-top: 1em; margin-bottom: 1em; } @@ -1793,12 +1934,20 @@ p.image-details.origin-details { margin-bottom: 1.5em; } -a.align-center, img.align-center, audio.align-center { +.content-image-container.align-full { + width: 100%; +} + +a.align-center, img.align-center, audio.align-center, video.align-center { display: block; margin-left: auto; margin-right: auto; } +a.align-full, a.align-full img, img.align-full, video.align-full { + width: 100%; +} + center { margin-top: 1em; margin-bottom: 1em; @@ -1913,46 +2062,34 @@ ul.quick-info li:not(:last-child)::after { text-align: center; } -.gallery-view-switcher { +.gallery-view-switcher, +.gallery-style-selector { + margin-left: auto; + margin-right: auto; text-align: center; line-height: 1.4; } -#content.top-index section { - margin-bottom: 1.5em; +.gallery-style-selector .styles { + display: inline-flex; + justify-content: center; } -.expandable-gallery-section .section-expando { - margin-top: 1em; - margin-bottom: 2em; - - display: flex; - flex-direction: row; - justify-content: space-around; +.gallery-style-selector .styles label:not(:last-child) { + margin-right: 1.25ch; } -.expandable-gallery-section .section-expando-content { - text-align: center; - line-height: 1.5; -} +.gallery-style-selector .count { + font-size: 0.85em; -.expandable-gallery-section .section-expando-toggle { - text-decoration: underline; - text-decoration-style: dotted; -} + position: relative; + bottom: -0.25em; -.expandable-gallery-section.expanded .section-content-below-cut { - animation: expand-gallery-section 0.8s forwards; + opacity: 0.9; } -@keyframes expand-gallery-section { - from { - opacity: 0; - } - - to { - opacity: 1; - } +#content.top-index section { + margin-bottom: 1.5em; } .quick-description:not(.has-external-links-only) { @@ -2124,6 +2261,13 @@ ul > li.has-details { margin-left: -17px; } +li .origin-details { + display: block; + margin-left: 2ch; + font-size: 0.9em; + font-style: oblique; +} + .album-group-list dt, .group-series-list dt { font-style: oblique; @@ -2174,31 +2318,54 @@ ul > li.has-details { #content hr { border: 1px inset #808080; - width: 100%; +} + +#content hr.split { + color: #808080; } #content hr.split::before { content: "(split)"; - color: #808080; } -#content hr.split { +#content hr.main-separator { + color: var(--dim-color); + clear: none; + margin-top: -0.25em; + margin-bottom: 1.75em; +} + +#content hr.main-separator::before { + content: "♦"; + font-size: 1.2em; +} + +#content hr.split, +#content hr.main-separator { position: relative; overflow: hidden; border: none; } -#content hr.split::after { +#content hr.split::after, +#content hr.main-separator::after { display: inline-block; content: ""; - border: 1px inset #808080; - width: 100%; + width: calc(100% - min(calc(8vw - 35px), 45px)); position: absolute; top: 50%; - margin-top: -2px; margin-left: 10px; } +#content hr.split::after { + border: 1px inset currentColor; + margin-top: -2px; +} + +#content hr.main-separator::after { + border-bottom: 1px solid currentColor; +} + li > ul { margin-top: 5px; } @@ -2278,6 +2445,65 @@ html[data-url-key="localized.albumCommentary"] p.track-info { margin-left: 20px; } +html[data-url-key="localized.artistRollingWindow"] #content p { + text-align: center; +} + +html[data-url-key="localized.artistRollingWindow"] #content input[type=number] { + width: 3em; + margin: 0 0.25em; + background: black; + color: white; + border: 1px dotted var(--primary-color); + padding: 4px; + border-radius: 3px; +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a { + display: inline-block; + padding: 5px; + text-decoration: underline; + text-decoration-style: dotted; +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a:not([href]) { + text-decoration: none; + opacity: 0.7; +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area { + border: 1px dashed #ffffff42; + border-top-style: solid; + border-bottom-style: solid; + + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 260px); +} + +html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area .grid-listing { + width: 100%; +} + +html[data-url-key="localized.artistRollingWindow"] .grid-item.peeking { + opacity: 0.8; + background: #ffffff24; +} + +html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; +} + +html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) > *:not([style*="display: none"]) ~ *::before { + content: '\00b7'; + margin-left: 0.5ch; + margin-right: 0.5ch; +} + html[data-url-key="localized.groupInfo"] .by a { color: var(--page-primary-color); } @@ -2512,7 +2738,33 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las linear-gradient(#000000bb, #000000bb), var(--primary-color); - box-shadow: 0 -2px 6px -1px var(--dim-color) inset; + --drop-shadow: 0 -2px 6px -1px var(--dim-color) inset; + box-shadow: var(--drop-shadow); +} + +.drop.shiny { + cursor: default; +} + +@supports (box-shadow: 1px 1px 1px color-mix(in srgb, blue, 40% red)) { + @property --drop-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; + } + + .drop.shiny { + cursor: default; + transition: --drop-shine 0.2s; + } + + .drop.shiny:hover { + --drop-shine: 100%; + + box-shadow: + var(--drop-shadow), + 0 2px 4px -0.5px color-mix(in srgb, var(--primary-color), calc(100% - var(--drop-shine)) transparent); + } } .commentary-drop { @@ -2544,7 +2796,8 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las /* Videos and audios (in content) get a lite version of image-container. */ .content-video-container, .content-audio-container { - width: min-content; + width: fit-content; + max-width: 100%; background-color: var(--dark-color); border: 2px solid var(--primary-color); border-radius: 2.5px 2.5px 3px 3px; @@ -2554,6 +2807,7 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las .content-video-container video, .content-audio-container audio { display: block; + max-width: 100%; } .content-video-container.align-center, @@ -2562,6 +2816,23 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las margin-right: auto; } +.content-video-container.align-full, +.content-audio-container.align-full { + width: 100%; +} + +.content-audio-container .filename { + color: white; + font-family: monospace; + display: block; + font-size: 0.9em; + padding-left: 1ch; + padding-right: 1ch; + padding-bottom: 0.25em; + margin-bottom: 0.5em; + border-bottom: 1px solid #fff4; +} + .image-text-area { position: absolute; top: 0; @@ -2616,6 +2887,23 @@ img { object-fit: cover; } +.image { + --reveal-filter: ; + --shadow-filter: ; + + backdrop-filter: blur(0); + filter: + var(--reveal-filter) + var(--shadow-filter); +} + +p > img, li > img { + max-width: 100%; + object-fit: contain; + height: auto; + vertical-align: text-bottom; +} + .image-inner-area::after { content: ""; display: block; @@ -2675,9 +2963,9 @@ video.pixelate, .pixelate video { text-decoration-style: dotted; } -.reveal .image { +.reveal:not(.revealed) .image { opacity: 0.7; - filter: blur(20px) brightness(0.7); + --reveal-filter: blur(20px) brightness(0.7); } .reveal .image.reveal-thumbnail { @@ -2701,7 +2989,6 @@ video.pixelate, .pixelate video { } .reveal.revealed .image { - filter: none; opacity: 1; } @@ -2712,7 +2999,6 @@ video.pixelate, .pixelate video { .reveal:not(.revealed) .image-outer-area > * { --reveal-border-radius: 6px; position: relative; - overflow: hidden; border-radius: var(--reveal-border-radius); } @@ -2748,7 +3034,7 @@ video.pixelate, .pixelate video { } .reveal:not(.revealed) .image-outer-area > *:hover .image { - filter: blur(20px) brightness(0.6); + --reveal-filter: blur(20px) brightness(0.6); opacity: 0.6; } @@ -2774,20 +3060,88 @@ video.pixelate, .pixelate video { justify-content: center; align-items: flex-start; padding: 5px 15px; + box-sizing: border-box; +} + +.grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) { + padding-bottom: 140px; + background: #cccccc07; + border-radius: 10px; + border: 1px dashed #fff3; +} + +.grid-listing .reveal-all-container { + flex-basis: 100%; +} + +.grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) .reveal-all-container { + display: none; +} + +.grid-listing .reveal-all-container.has-nearby-tab { + margin-bottom: 0.6em; +} + +.grid-listing .reveal-all { + max-width: 400px; + margin: 0.20em auto 0; + text-align: center; +} + +.grid-listing .reveal-all .warnings:not(.reveal-all:hover *) { + opacity: 0.4; +} + +.grid-listing .reveal-all a { + display: inline-block; + margin-bottom: 0.15em; + + text-decoration: underline; + text-decoration-style: dotted; +} + +.grid-listing .reveal-all b { + white-space: nowrap; } .grid-item { + line-height: 1.2; font-size: 0.9em; } .grid-item { + --tab-pull: 0px; + --tabnt-offset: 0px; + display: inline-block; text-align: center; background-color: #111111; border: 1px dotted var(--primary-color); border-radius: 2px; padding: 5px; + margin: 10px; + margin-top: + calc( + 10px + - var(--tab-pull) + + var(--tabnt-offset)); +} + +.grid-item.has-tab { + border-radius: 8px 8px 3px 3px; +} + +.grid-item.has-tab:hover { + --tab-pull: 3px; +} + +.grid-item:not(.has-tab) { + --tabnt-offset: calc(1.2em - 4px); +} + +.grid-item[class*="hidden-by-"] { + display: none; } .grid-item .image-container { @@ -2804,10 +3158,16 @@ video.pixelate, .pixelate video { } .grid-item .image { + --shadow-filter: + drop-shadow(0 3px 2px #0004) + drop-shadow(0 1px 5px #0001) + drop-shadow(0 3px 4px #0001); + width: 100%; height: 100% !important; margin-top: auto; margin-bottom: auto; + object-fit: contain; } .grid-item:hover { @@ -2824,20 +3184,27 @@ video.pixelate, .pixelate video { hyphens: auto; } -.grid-item > span:not(:first-child) { - margin-top: 2px; -} +/* tab */ +.grid-item > span:first-child { + margin-bottom: calc(3px + var(--tab-pull)); -.grid-item > span:first-of-type { - margin-top: 6px; + font-style: oblique; } -.grid-item > span:not(:first-of-type) { +/* info */ +.grid-item > .image-container + span ~ span { + margin-top: 2px; + font-size: 0.9em; opacity: 0.8; } -.grid-item:hover > span:first-of-type { +/* title */ +.grid-item > .image-container + span { + margin-top: 6px; +} + +.grid-item:hover > .image-container + span { text-decoration: underline; } @@ -2846,6 +3213,10 @@ video.pixelate, .pixelate video { max-width: 200px; } +.grid-name-marker { + color: white; +} + .grid-actions { display: flex; flex-direction: row; @@ -2863,6 +3234,47 @@ video.pixelate, .pixelate video { --dim-color: inherit !important; } +.grid-caption { + flex-basis: 100%; + text-align: center; + line-height: 1.5; +} + +.grid-expando { + margin-top: 1em; + margin-bottom: 2em; + flex-basis: 100%; + + display: flex; + flex-direction: row; + justify-content: space-around; +} + +.grid-expando-content { + margin: 0; + text-align: center; + line-height: 1.5; +} + +.grid-expando-toggle { + text-decoration: underline; + text-decoration-style: dotted; +} + +.grid-item.shown-by-expandable-cut { + animation: expand-cover-grid 0.8s forwards; +} + +@keyframes expand-cover-grid { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + /* Carousel */ .carousel-container { @@ -2883,7 +3295,6 @@ video.pixelate, .pixelate video { left: 0; right: 0; bottom: 0; - z-index: -20; background-color: var(--dim-color); filter: brightness(0.6); } @@ -3273,15 +3684,11 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r grid-template-columns: 1fr min(40%, 90px); } -.content-sticky-heading-root.has-cover { - padding-right: min(40%, 400px); -} - .content-sticky-heading-row h1 { position: relative; margin: 0; padding-right: 20px; - line-height: 1.4; + overflow-x: hidden; } .content-sticky-heading-row h1 .reference-collapsed-heading { @@ -3421,7 +3828,9 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r } #content, .sidebar { - contain: paint; + /* In the year of our pizza 2025, we try commenting this out. + */ + /*contain: paint;*/ } /* Sticky sidebar */ @@ -3808,6 +4217,16 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r max-width: unset; } + #artwork-column .cover-artwork { + --normal-shadow: 0 0 transparent; + } + + #artwork-column .cover-artwork:not(:first-child), + #artwork-column .cover-artwork-joiner { + margin-left: 30px; + margin-right: 30px; + } + #additional-names-box { width: unset; max-width: unset; diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js index 71112313..396c4889 100644 --- a/src/static/js/client-util.js +++ b/src/static/js/client-util.js @@ -37,7 +37,7 @@ export function cssProp(el, ...args) { } } -export function templateContent(el) { +export function templateContent(el, slots = {}) { if (el === null) { return null; } @@ -46,7 +46,25 @@ export function templateContent(el) { throw new Error(`Expected a <template> element`); } - return el.content.cloneNode(true); + const content = el.content.cloneNode(true); + + for (const [key, value] of Object.entries(slots)) { + const slot = content.querySelector(`slot[name="${key}"]`); + + if (!slot) { + console.warn(`Slot ${key} missing in template:`, el); + continue; + } + + if (value === null || value === undefined) { + console.warn(`Valueless slot ${key} in template:`, el); + continue; + } + + slot.replaceWith(value); + } + + return content; } // Curry-style, so multiple points can more conveniently be tested at once. @@ -127,3 +145,10 @@ export function dispatchInternalEvent(event, eventName, ...args) { return results; } + +const languageCode = document.documentElement.getAttribute('lang'); + +export function formatDate(inputDate) { + const date = new Date(inputDate); + return date.toLocaleDateString(languageCode); +} diff --git a/src/static/js/client/artist-rolling-window.js b/src/static/js/client/artist-rolling-window.js new file mode 100644 index 00000000..b201e7df --- /dev/null +++ b/src/static/js/client/artist-rolling-window.js @@ -0,0 +1,573 @@ +/* eslint-env browser */ + +import {cssProp, formatDate} from '../client-util.js'; + +import {sortByDate} from '../../shared-util/sort.js'; +import {chunkByConditions, chunkByProperties, empty, stitchArrays} + from '../../shared-util/sugar.js'; + +export const info = { + id: 'artistRollingWindowInfo', + + timeframeMonthsBefore: null, + timeframeMonthsAfter: null, + timeframeMonthsPeek: null, + + contributionKind: null, + contributionGroup: null, + + timeframeSelectionSomeLine: null, + timeframeSelectionNoneLine: null, + + timeframeSelectionContributionCount: null, + timeframeSelectionTimeframeCount: null, + timeframeSelectionFirstDate: null, + timeframeSelectionLastDate: null, + + timeframeSelectionControl: null, + timeframeSelectionMenu: null, + timeframeSelectionPrevious: null, + timeframeSelectionNext: null, + + timeframeEmptyLine: null, + + sourceArea: null, + sourceGrid: null, + sources: null, +}; + +export function getPageReferences() { + if (document.documentElement.dataset.urlKey !== 'localized.artistRollingWindow') { + return; + } + + info.timeframeMonthsBefore = + document.getElementById('timeframe-months-before'); + + info.timeframeMonthsAfter = + document.getElementById('timeframe-months-after'); + + info.timeframeMonthsPeek = + document.getElementById('timeframe-months-peek'); + + info.contributionKind = + document.getElementById('contribution-kind'); + + info.contributionGroup = + document.getElementById('contribution-group'); + + info.timeframeSelectionSomeLine = + document.getElementById('timeframe-selection-some'); + + info.timeframeSelectionNoneLine = + document.getElementById('timeframe-selection-none'); + + info.timeframeSelectionContributionCount = + document.getElementById('timeframe-selection-contribution-count'); + + info.timeframeSelectionTimeframeCount = + document.getElementById('timeframe-selection-timeframe-count'); + + info.timeframeSelectionFirstDate = + document.getElementById('timeframe-selection-first-date'); + + info.timeframeSelectionLastDate = + document.getElementById('timeframe-selection-last-date'); + + info.timeframeSelectionControl = + document.getElementById('timeframe-selection-control'); + + info.timeframeSelectionMenu = + document.getElementById('timeframe-selection-menu'); + + info.timeframeSelectionPrevious = + document.getElementById('timeframe-selection-previous'); + + info.timeframeSelectionNext = + document.getElementById('timeframe-selection-next'); + + info.timeframeEmptyLine = + document.getElementById('timeframe-empty'); + + info.sourceArea = + document.getElementById('timeframe-source-area'); + + info.sourceGrid = + info.sourceArea.querySelector('.grid-listing'); + + info.sources = + info.sourceGrid.getElementsByClassName('grid-item'); +} + +export function addPageListeners() { + if (!info.sourceArea) { + return; + } + + for (const input of [ + info.timeframeMonthsBefore, + info.timeframeMonthsAfter, + info.timeframeMonthsPeek, + info.contributionKind, + info.contributionGroup, + ]) { + input.addEventListener('change', () => { + updateArtistRollingWindow() + }); + } + + info.timeframeSelectionMenu.addEventListener('change', () => { + updateRollingWindowTimeframeSelection(); + }); + + const eatClicks = (element, callback) => { + element.addEventListener('click', domEvent => { + domEvent.preventDefault(); + callback(); + }); + + element.addEventListener('mousedown', domEvent => { + if (domEvent.detail > 1) { + domEvent.preventDefault(); + } + }); + }; + + eatClicks(info.timeframeSelectionNext, nextRollingTimeframeSelection); + eatClicks(info.timeframeSelectionPrevious, previousRollingTimeframeSelection); +} + +export function mutatePageContent() { + if (!info.sourceArea) { + return; + } + + updateArtistRollingWindow(); +} + +function previousRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex > 0) { + menu.selectedIndex--; + } + + updateRollingWindowTimeframeSelection(); +} + +function nextRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex < menu.length - 1) { + menu.selectedIndex++; + } + + updateRollingWindowTimeframeSelection(); +} + +function getArtistRollingWindowSourceInfo() { + const sourceElements = + Array.from(info.sources); + + const sourceTimeElements = + sourceElements + .map(el => Array.from(el.getElementsByTagName('time'))); + + const sourceTimeClasses = + sourceTimeElements + .map(times => times + .map(time => Array.from(time.classList))); + + const sourceKinds = + sourceTimeClasses + .map(times => times + .map(classes => classes + .find(cl => cl.endsWith('-contribution-date')) + .slice(0, -'-contribution-date'.length))); + + const sourceGroups = + sourceElements + .map(el => + Array.from(el.querySelectorAll('.contribution-group')) + .map(data => data.value)); + + const sourceDates = + sourceTimeElements + .map(times => times + .map(time => new Date(time.getAttribute('datetime')))); + + return stitchArrays({ + element: sourceElements, + kinds: sourceKinds, + groups: sourceGroups, + dates: sourceDates, + }); +} + +function getArtistRollingWindowTimeframeInfo() { + const contributionKind = + info.contributionKind.value; + + const contributionGroup = + info.contributionGroup.value; + + const sourceInfo = + getArtistRollingWindowSourceInfo(); + + const principalSources = + sourceInfo.filter(source => { + if (!source.kinds.includes(contributionKind)) { + return false; + } + + if (contributionGroup !== '-') { + if (!source.groups.includes(contributionGroup)) { + return false; + } + } + + return true; + }); + + const principalSourceDates = + principalSources.map(source => + stitchArrays({ + kind: source.kinds, + date: source.dates, + }).find(({kind}) => kind === contributionKind) + .date); + + const getPeekDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value) + - parseInt(info.timeframeMonthsPeek.value))); + + return date; + }; + + const getEntranceDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value))); + + return date; + }; + + const getExitDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + + parseInt(info.timeframeMonthsAfter.value))); + + return date; + }; + + const principalSourceIndices = + Array.from({length: principalSources.length}, (_, i) => i); + + const timeframeSourceChunks = + chunkByConditions(principalSourceIndices, [ + (previous, next) => + +principalSourceDates[previous] !== + +principalSourceDates[next], + ]); + + const timeframeSourceChunkDates = + timeframeSourceChunks + .map(indices => indices[0]) + .map(index => principalSourceDates[index]); + + const timeframeSourceChunkPeekDates = + timeframeSourceChunkDates + .map(getPeekDate); + + const timeframeSourceChunkEntranceDates = + timeframeSourceChunkDates + .map(getEntranceDate); + + const timeframeSourceChunkExitDates = + timeframeSourceChunkDates + .map(getExitDate); + + const peekDateInfo = + stitchArrays({ + peek: timeframeSourceChunkPeekDates, + indices: timeframeSourceChunks, + }).map(({peek, indices}) => ({ + date: peek, + peek: indices, + })); + + const entranceDateInfo = + stitchArrays({ + entrance: timeframeSourceChunkEntranceDates, + indices: timeframeSourceChunks, + }).map(({entrance, indices}) => ({ + date: entrance, + entrance: indices, + })); + + const exitDateInfo = + stitchArrays({ + exit: timeframeSourceChunkExitDates, + indices: timeframeSourceChunks, + }).map(({exit, indices}) => ({ + date: exit, + exit: indices, + })); + + const dateInfoChunks = + chunkByProperties( + sortByDate([ + ...peekDateInfo, + ...entranceDateInfo, + ...exitDateInfo, + ]), + ['date']); + + const dateInfo = + dateInfoChunks + .map(({chunk}) => + Object.assign({ + peek: null, + entrance: null, + exit: null, + }, ...chunk)); + + const timeframeInfo = + dateInfo.reduce( + (accumulator, {date, peek, entrance, exit}) => { + const previous = accumulator.at(-1); + + // These mustn't be mutated! + let peeking = (previous ? previous.peeking : []); + let tracking = (previous ? previous.tracking : []); + + if (peek) { + peeking = + peeking.concat(peek); + } + + if (entrance) { + peeking = + peeking.filter(index => !entrance.includes(index)); + + tracking = + tracking.concat(entrance); + } + + if (exit) { + tracking = + tracking.filter(index => !exit.includes(index)); + } + + return [...accumulator, { + date, + peeking, + tracking, + peek, + entrance, + exit, + }]; + }, + []); + + const indicesToSources = indices => + (indices + ? indices.map(index => principalSources[index]) + : null); + + const finalizedTimeframeInfo = + timeframeInfo.map(({ + date, + peeking, + tracking, + peek, + entrance, + exit, + }) => ({ + date, + peeking: indicesToSources(peeking), + tracking: indicesToSources(tracking), + peek: indicesToSources(peek), + entrance: indicesToSources(entrance), + exit: indicesToSources(exit), + })); + + return finalizedTimeframeInfo; +} + +function updateArtistRollingWindow() { + const timeframeInfo = + getArtistRollingWindowTimeframeInfo(); + + if (empty(timeframeInfo)) { + cssProp(info.timeframeSelectionControl, 'display', 'none'); + cssProp(info.timeframeSelectionSomeLine, 'display', 'none'); + cssProp(info.timeframeSelectionNoneLine, 'display', null); + + updateRollingWindowTimeframeSelection(timeframeInfo); + + return; + } + + cssProp(info.timeframeSelectionControl, 'display', null); + cssProp(info.timeframeSelectionSomeLine, 'display', null); + cssProp(info.timeframeSelectionNoneLine, 'display', 'none'); + + // The last timeframe is just the exit of the final tracked sources, + // so we aren't going to display a menu option for it, and will just use + // it as the end of the final option's date range. + + const usedTimeframes = timeframeInfo.slice(0, -1); + const firstTimeframe = timeframeInfo.at(0); + const lastTimeframe = timeframeInfo.at(-1); + + const sourceCount = + timeframeInfo + .flatMap(({entrance}) => entrance ?? []) + .length; + + const timeframeCount = + usedTimeframes.length; + + info.timeframeSelectionContributionCount.innerText = sourceCount; + info.timeframeSelectionTimeframeCount.innerText = timeframeCount; + + const firstDate = firstTimeframe.date; + const lastDate = lastTimeframe.date; + + info.timeframeSelectionFirstDate.innerText = formatDate(firstDate); + info.timeframeSelectionLastDate.innerText = formatDate(lastDate); + + while (info.timeframeSelectionMenu.firstChild) { + info.timeframeSelectionMenu.firstChild.remove(); + } + + for (const [index, timeframe] of usedTimeframes.entries()) { + const nextTimeframe = timeframeInfo[index + 1]; + + const option = document.createElement('option'); + + option.appendChild(document.createTextNode( + `${formatDate(timeframe.date)} – ${formatDate(nextTimeframe.date)}`)); + + info.timeframeSelectionMenu.appendChild(option); + } + + updateRollingWindowTimeframeSelection(timeframeInfo); +} + +function updateRollingWindowTimeframeSelection(timeframeInfo) { + timeframeInfo ??= getArtistRollingWindowTimeframeInfo(); + + updateRollingWindowTimeframeSelectionControls(timeframeInfo); + updateRollingWindowTimeframeSelectionSources(timeframeInfo); +} + +function updateRollingWindowTimeframeSelectionControls(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const atFirstTimeframe = + currentIndex === 0; + + // The last actual timeframe is empty and not displayed as a menu option. + const atLastTimeframe = + currentIndex === timeframeInfo.length - 2; + + if (atFirstTimeframe) { + info.timeframeSelectionPrevious.removeAttribute('href'); + } else { + info.timeframeSelectionPrevious.setAttribute('href', '#'); + } + + if (atLastTimeframe) { + info.timeframeSelectionNext.removeAttribute('href'); + } else { + info.timeframeSelectionNext.setAttribute('href', '#'); + } +} + +function updateRollingWindowTimeframeSelectionSources(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const contributionGroup = + info.contributionGroup.value; + + cssProp(info.sourceGrid, 'display', null); + + const {peeking: peekingSources, tracking: trackingSources} = + (empty(timeframeInfo) + ? {peeking: [], tracking: []} + : timeframeInfo[currentIndex]); + + const peekingElements = + peekingSources.map(source => source.element); + + const trackingElements = + trackingSources.map(source => source.element); + + const showingElements = + [...trackingElements, ...peekingElements]; + + const hidingElements = + Array.from(info.sources) + .filter(element => + !peekingElements.includes(element) && + !trackingElements.includes(element)); + + for (const element of peekingElements) { + element.classList.add('peeking'); + element.classList.remove('tracking'); + } + + for (const element of trackingElements) { + element.classList.remove('peeking'); + element.classList.add('tracking'); + } + + for (const element of hidingElements) { + element.classList.remove('peeking'); + element.classList.remove('tracking'); + cssProp(element, 'display', 'none'); + } + + for (const element of showingElements) { + cssProp(element, 'display', null); + + for (const time of element.getElementsByTagName('time')) { + for (const className of time.classList) { + if (!className.endsWith('-contribution-date')) continue; + + const kind = className.slice(0, -'-contribution-date'.length); + if (kind === info.contributionKind.value) { + cssProp(time, 'display', null); + } else { + cssProp(time, 'display', 'none'); + } + } + } + + for (const data of element.getElementsByClassName('contribution-group')) { + if (contributionGroup === '-' || data.value !== contributionGroup) { + cssProp(data, 'display', null); + } else { + cssProp(data, 'display', 'none'); + } + } + } + + if (empty(peekingElements) && empty(trackingElements)) { + cssProp(info.timeframeEmptyLine, 'display', null); + } else { + cssProp(info.timeframeEmptyLine, 'display', 'none'); + } +} diff --git a/src/static/js/client/expandable-gallery-section.js b/src/static/js/client/expandable-gallery-section.js deleted file mode 100644 index dc83e8b7..00000000 --- a/src/static/js/client/expandable-gallery-section.js +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-env browser */ - -// TODO: Combine this and quick-description.js - -import {cssProp} from '../client-util.js'; - -import {stitchArrays} from '../../shared-util/sugar.js'; - -export const info = { - id: 'expandableGallerySectionInfo', - - sections: null, - - sectionContentBelowCut: null, - - sectionExpandoToggles: null, - - sectionExpandCues: null, - sectionCollapseCues: null, -}; - -export function getPageReferences() { - info.sections = - Array.from(document.querySelectorAll('.expandable-gallery-section')) - .filter(section => section.querySelector('.section-expando-toggle')); - - info.sectionContentBelowCut = - info.sections - .map(section => section.querySelector('.section-content-below-cut')); - - info.sectionExpandoToggles = - info.sections - .map(section => section.querySelector('.section-expando-toggle')); - - info.sectionExpandCues = - info.sections - .map(section => section.querySelector('.section-expand-cue')); - - info.sectionCollapseCues = - info.sections - .map(section => section.querySelector('.section-collapse-cue')); -} - -export function addPageListeners() { - for (const { - section, - contentBelowCut, - expandoToggle, - expandCue, - collapseCue, - } of stitchArrays({ - section: info.sections, - contentBelowCut: info.sectionContentBelowCut, - expandoToggle: info.sectionExpandoToggles, - expandCue: info.sectionExpandCues, - collapseCue: info.sectionCollapseCues, - })) { - expandoToggle.addEventListener('click', domEvent => { - domEvent.preventDefault(); - - const collapsed = - cssProp(contentBelowCut, 'display') === 'none'; - - if (collapsed) { - section.classList.add('expanded'); - cssProp(contentBelowCut, 'display', null); - cssProp(expandCue, 'display', 'none'); - cssProp(collapseCue, 'display', null); - } else { - section.classList.remove('expanded'); - cssProp(contentBelowCut, 'display', 'none'); - cssProp(expandCue, 'display', null); - cssProp(collapseCue, 'display', 'none'); - } - }); - } -} diff --git a/src/static/js/client/expandable-grid-section.js b/src/static/js/client/expandable-grid-section.js new file mode 100644 index 00000000..ce9a4c06 --- /dev/null +++ b/src/static/js/client/expandable-grid-section.js @@ -0,0 +1,85 @@ +/* eslint-env browser */ + +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'expandableGallerySectionInfo', + + items: null, + toggles: null, + expandCues: null, + collapseCues: null, +}; + +export function getPageReferences() { + const expandos = + Array.from(document.querySelectorAll('.grid-expando')); + + const grids = + expandos + .map(expando => expando.closest('.grid-listing')); + + info.items = + grids + .map(grid => grid.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.toggles = + expandos + .map(expando => expando.querySelector('.grid-expando-toggle')); + + info.expandCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-expand-cue')); + + info.collapseCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-collapse-cue')); +} + +export function addPageListeners() { + stitchArrays({ + items: info.items, + toggle: info.toggles, + expandCue: info.expandCues, + collapseCue: info.collapseCues, + }).forEach(({ + items, + toggle, + expandCue, + collapseCue, + }) => { + toggle.addEventListener('click', domEvent => { + domEvent.preventDefault(); + + const collapsed = + items.some(item => + item.classList.contains('hidden-by-expandable-cut')); + + for (const item of items) { + if ( + !item.classList.contains('hidden-by-expandable-cut') && + !item.classList.contains('shown-by-expandable-cut') + ) continue; + + if (collapsed) { + item.classList.remove('hidden-by-expandable-cut'); + item.classList.add('shown-by-expandable-cut'); + } else { + item.classList.add('hidden-by-expandable-cut'); + item.classList.remove('shown-by-expandable-cut'); + } + } + + if (collapsed) { + cssProp(expandCue, 'display', 'none'); + cssProp(collapseCue, 'display', null); + } else { + cssProp(expandCue, 'display', null); + cssProp(collapseCue, 'display', 'none'); + } + }); + }); +} diff --git a/src/static/js/client/gallery-style-selector.js b/src/static/js/client/gallery-style-selector.js new file mode 100644 index 00000000..c7086eae --- /dev/null +++ b/src/static/js/client/gallery-style-selector.js @@ -0,0 +1,123 @@ +/* eslint-env browser */ + +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'galleryStyleSelectorInfo', + + selectors: null, + sections: null, + + selectorStyleInputs: null, + selectorStyleInputStyles: null, + + selectorReleaseItems: null, + selectorReleaseItemStyles: null, + + selectorCountAll: null, + selectorCountFiltered: null, + selectorCountFilteredCount: null, + selectorCountNone: null, +}; + +export function getPageReferences() { + info.selectors = + Array.from(document.querySelectorAll('.gallery-style-selector')); + + info.sections = + info.selectors + .map(selector => selector.closest('section')); + + info.selectorStyleInputs = + info.selectors + .map(selector => selector.querySelectorAll('.styles input')) + .map(inputs => Array.from(inputs)); + + info.selectorStyleInputStyles = + info.selectorStyleInputs + .map(inputs => inputs + .map(input => input.closest('label').dataset.style)); + + info.selectorReleaseItems = + info.sections + .map(section => section.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.selectorReleaseItemStyles = + info.selectorReleaseItems + .map(items => items + .map(item => item.dataset.style)); + + info.selectorCountAll = + info.selectors + .map(selector => selector.querySelector('.count.all')); + + info.selectorCountFiltered = + info.selectors + .map(selector => selector.querySelector('.count.filtered')); + + info.selectorCountFilteredCount = + info.selectorCountFiltered + .map(selector => selector.querySelector('span')); + + info.selectorCountNone = + info.selectors + .map(selector => selector.querySelector('.count.none')); +} + +export function addPageListeners() { + for (const index of info.selectors.keys()) { + for (const input of info.selectorStyleInputs[index]) { + input.addEventListener('input', () => updateVisibleReleases(index)); + } + } +} + +function updateVisibleReleases(index) { + const inputs = info.selectorStyleInputs[index]; + const inputStyles = info.selectorStyleInputStyles[index]; + + const selectedStyles = + stitchArrays({input: inputs, style: inputStyles}) + .filter(({input}) => input.checked) + .map(({style}) => style); + + const releases = info.selectorReleaseItems[index]; + const releaseStyles = info.selectorReleaseItemStyles[index]; + + let visible = 0; + + stitchArrays({ + release: releases, + style: releaseStyles, + }).forEach(({release, style}) => { + if (selectedStyles.includes(style)) { + release.classList.remove('hidden-by-style-mismatch'); + visible++; + } else { + release.classList.add('hidden-by-style-mismatch'); + } + }); + + const countAll = info.selectorCountAll[index]; + const countFiltered = info.selectorCountFiltered[index]; + const countFilteredCount = info.selectorCountFilteredCount[index]; + const countNone = info.selectorCountNone[index]; + + if (visible === releases.length) { + cssProp(countAll, 'display', null); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', 'none'); + } else if (visible === 0) { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', null); + } else { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', null); + cssProp(countNone, 'display', 'none'); + countFilteredCount.innerHTML = visible; + } +} diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js index 9569de3e..89119a47 100644 --- a/src/static/js/client/hoverable-tooltip.js +++ b/src/static/js/client/hoverable-tooltip.js @@ -118,17 +118,17 @@ export function registerTooltipElement(tooltip) { handleTooltipMouseLeft(tooltip); }); - tooltip.addEventListener('focusin', event => { - handleTooltipReceivedFocus(tooltip, event.relatedTarget); + tooltip.addEventListener('focusin', domEvent => { + handleTooltipReceivedFocus(tooltip, domEvent.relatedTarget); }); - tooltip.addEventListener('focusout', event => { + tooltip.addEventListener('focusout', domEvent => { // This event gets activated for tabbing *between* links inside the // tooltip, which is no good and certainly doesn't represent the focus // leaving the tooltip. - if (currentlyShownTooltipHasFocus(event.relatedTarget)) return; + if (currentlyShownTooltipHasFocus(domEvent.relatedTarget)) return; - handleTooltipLostFocus(tooltip, event.relatedTarget); + handleTooltipLostFocus(tooltip, domEvent.relatedTarget); }); } @@ -158,20 +158,20 @@ export function registerTooltipHoverableElement(hoverable, tooltip) { handleTooltipHoverableMouseLeft(hoverable); }); - hoverable.addEventListener('focusin', event => { - handleTooltipHoverableReceivedFocus(hoverable, event); + hoverable.addEventListener('focusin', domEvent => { + handleTooltipHoverableReceivedFocus(hoverable, domEvent); }); - hoverable.addEventListener('focusout', event => { - handleTooltipHoverableLostFocus(hoverable, event); + hoverable.addEventListener('focusout', domEvent => { + handleTooltipHoverableLostFocus(hoverable, domEvent); }); - hoverable.addEventListener('touchend', event => { - handleTooltipHoverableTouchEnded(hoverable, event); + hoverable.addEventListener('touchend', domEvent => { + handleTooltipHoverableTouchEnded(hoverable, domEvent); }); - hoverable.addEventListener('click', event => { - handleTooltipHoverableClicked(hoverable, event); + hoverable.addEventListener('click', domEvent => { + handleTooltipHoverableClicked(hoverable, domEvent); }); } @@ -416,7 +416,7 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) { }, 1200); } -function handleTooltipHoverableClicked(hoverable) { +function handleTooltipHoverableClicked(hoverable, domEvent) { const {state} = info; // Don't navigate away from the page if the this hoverable was recently @@ -426,7 +426,7 @@ function handleTooltipHoverableClicked(hoverable) { state.currentlyActiveHoverable === hoverable && state.hoverableWasRecentlyTouched ) { - event.preventDefault(); + domEvent.preventDefault(); } } diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index aeb9264a..86081b5d 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -7,16 +7,19 @@ import * as albumCommentarySidebarModule from './album-commentary-sidebar.js'; import * as artTagGalleryFilterModule from './art-tag-gallery-filter.js'; import * as artTagNetworkModule from './art-tag-network.js'; import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip.js'; +import * as artistRollingWindowModule from './artist-rolling-window.js'; import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js'; import * as datetimestampTooltipModule from './datetimestamp-tooltip.js'; import * as draggedLinkModule from './dragged-link.js'; -import * as expandableGallerySectionModule from './expandable-gallery-section.js'; +import * as expandableGridSectionModule from './expandable-grid-section.js'; +import * as galleryStyleSelectorModule from './gallery-style-selector.js'; import * as hashLinkModule from './hash-link.js'; 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 quickDescriptionModule from './quick-description.js'; +import * as revealAllGridControlModule from './reveal-all-grid-control.js'; import * as scriptedLinkModule from './scripted-link.js'; import * as sidebarSearchModule from './sidebar-search.js'; import * as stickyHeadingModule from './sticky-heading.js'; @@ -30,16 +33,19 @@ export const modules = [ artTagGalleryFilterModule, artTagNetworkModule, artistExternalLinkTooltipModule, + artistRollingWindowModule, cssCompatibilityAssistantModule, datetimestampTooltipModule, draggedLinkModule, - expandableGallerySectionModule, + expandableGridSectionModule, + galleryStyleSelectorModule, hashLinkModule, hoverableTooltipModule, imageOverlayModule, intrapageDotSwitcherModule, liveMousePositionModule, quickDescriptionModule, + revealAllGridControlModule, scriptedLinkModule, sidebarSearchModule, stickyHeadingModule, diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js index 6a7a6023..cff82252 100644 --- a/src/static/js/client/quick-description.js +++ b/src/static/js/client/quick-description.js @@ -1,7 +1,5 @@ /* eslint-env browser */ -// TODO: Combine this and expandable-gallery-section.js - import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { diff --git a/src/static/js/client/reveal-all-grid-control.js b/src/static/js/client/reveal-all-grid-control.js new file mode 100644 index 00000000..1b362bea --- /dev/null +++ b/src/static/js/client/reveal-all-grid-control.js @@ -0,0 +1,72 @@ +/* eslint-env browser */ + +import {cssProp} from '../client-util.js'; + +export const info = { + id: 'revealAllGridControlInfo', + + revealAllLinks: null, + revealables: null, + + revealLabels: null, + concealLabels: null, +}; + +export function getPageReferences() { + info.revealAllLinks = + Array.from(document.querySelectorAll('.reveal-all a')); + + info.revealables = + info.revealAllLinks + .map(link => link.closest('.grid-listing')) + .map(listing => listing.querySelectorAll('.reveal')); + + info.revealLabels = + info.revealAllLinks + .map(link => link.querySelector('.reveal-label')); + + info.concealLabels = + info.revealAllLinks + .map(link => link.querySelector('.conceal-label')); +} + +export function addPageListeners() { + for (const [index, link] of info.revealAllLinks.entries()) { + link.addEventListener('click', domEvent => { + domEvent.preventDefault(); + handleRevealAllLinkClicked(index); + }); + } +} + +export function addInternalListeners() { + // Don't even think about it. "Reveal all artworks" is a stable control, + // meaning it only changes because the user interacted with it directly. +} + +function handleRevealAllLinkClicked(index) { + const revealables = info.revealables[index]; + const revealLabel = info.revealLabels[index]; + const concealLabel = info.concealLabels[index]; + + const shouldReveal = + (cssProp(revealLabel, 'display') === 'none' + ? false + : true); + + for (const revealable of revealables) { + if (shouldReveal) { + revealable.classList.add('revealed'); + } else { + revealable.classList.remove('revealed'); + } + } + + if (shouldReveal) { + cssProp(revealLabel, 'display', 'none'); + cssProp(concealLabel, 'display', null); + } else { + cssProp(revealLabel, 'display', null); + cssProp(concealLabel, 'display', 'none'); + } +} diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index c8f42e91..4467766c 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -73,6 +73,10 @@ export const info = { groupResultKindString: null, tagResultKindString: null, + groupResultDisambiguatorString: null, + flashResultDisambiguatorString: null, + trackResultDisambiguatorString: null, + albumResultFilterString: null, artistResultFilterString: null, flashResultFilterString: null, @@ -196,6 +200,15 @@ export function getPageReferences() { info.tagResultKindString = findString('tag-result-kind'); + info.groupResultDisambiguatorString = + findString('group-result-disambiguator'); + + info.flashResultDisambiguatorString = + findString('flash-result-disambiguator'); + + info.trackResultDisambiguatorString = + findString('track-result-disambiguator'); + info.albumResultFilterString = findString('album-result-filter'); @@ -841,7 +854,7 @@ function fillResultElements(results, { } for (const result of filteredResults) { - const el = generateSidebarSearchResult(result); + const el = generateSidebarSearchResult(result, filteredResults); if (!el) continue; info.results.appendChild(el); @@ -890,13 +903,13 @@ function showFilterElements(results) { } } -function generateSidebarSearchResult(result) { +function generateSidebarSearchResult(result, results) { const preparedSlots = { color: result.data.color ?? null, name: - result.data.name ?? result.data.primaryName ?? null, + getSearchResultName(result), imageSource: getSearchResultImageSource(result), @@ -961,9 +974,37 @@ function generateSidebarSearchResult(result) { return null; } + const compareReferenceType = otherResult => + otherResult.referenceType === result.referenceType; + + const compareName = otherResult => + getSearchResultName(otherResult) === getSearchResultName(result); + + const ambiguous = + results.some(otherResult => + otherResult !== result && + compareReferenceType(otherResult) && + compareName(otherResult)); + + if (ambiguous) { + preparedSlots.disambiguate = + result.data.disambiguator; + + preparedSlots.disambiguatorString = + info[result.referenceType + 'ResultDisambiguatorString']; + } + return generateSidebarSearchResultTemplate(preparedSlots); } +function getSearchResultName(result) { + return ( + result.data.name ?? + result.data.primaryName ?? + null + ); +} + function getSearchResultImageSource(result) { const {artwork} = result.data; if (!artwork) return null; @@ -1039,6 +1080,15 @@ function generateSidebarSearchResultTemplate(slots) { } } + if (!accentSpan && slots.disambiguate) { + accentSpan = document.createElement('span'); + accentSpan.classList.add('wiki-search-result-disambiguator'); + accentSpan.appendChild( + templateContent(slots.disambiguatorString, { + disambiguator: slots.disambiguate, + })); + } + if (!accentSpan && slots.kindString) { accentSpan = document.createElement('span'); accentSpan.classList.add('wiki-search-result-kind'); @@ -1305,7 +1355,7 @@ async function handleDroppedIntoSearchInput(domEvent) { let droppedURL; try { droppedURL = new URL(droppedText); - } catch (error) { + } catch { droppedURL = null; } diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js index c3002b18..3e9fbfca 100644 --- a/src/static/js/search-worker.js +++ b/src/static/js/search-worker.js @@ -130,7 +130,7 @@ async function loadDatabase() { try { idb = await promisifyIDBRequest(request); - } catch (error) { + } catch { console.warn(`Couldn't load search IndexedDB - won't use an internal cache.`); console.warn(request.error); idb = null; @@ -391,6 +391,8 @@ function performSearchAction({query, options}) { } const interestingFieldCombinations = [ + ['primaryName'], + ['primaryName', 'parentName', 'groups'], ['primaryName', 'parentName'], ['primaryName', 'groups', 'contributors'], @@ -412,7 +414,6 @@ const interestingFieldCombinations = [ ['contributors', 'parentName'], ['contributors', 'groups'], ['primaryName', 'contributors'], - ['primaryName'], ]; function queryGenericIndex(query, options) { |