From dc4d726d7a4e6a89d3f1e9e2af0d334472976ae3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 2 Aug 2023 22:14:57 -0300 Subject: content: good ol' client.js -> client2.js --- src/static/client.js | 910 --------------------------------------------------- 1 file changed, 910 deletions(-) delete mode 100644 src/static/client.js (limited to 'src/static/client.js') diff --git a/src/static/client.js b/src/static/client.js deleted file mode 100644 index dab92ffc..00000000 --- a/src/static/client.js +++ /dev/null @@ -1,910 +0,0 @@ -/* eslint-env browser */ - -// This is the JS file that gets loaded on the client! It's only really used for -// the random track feature right now - the idea is we only use it for stuff -// that cannot 8e done at static-site compile time, 8y its fundamentally -// ephemeral nature. -// -// Upd8: As of 04/02/2021, it's now used for info cards too! Nice. - -import {getColors} from '../util/colors.js'; - -import {getArtistNumContributions} from '../util/wiki-data.js'; - -let albumData, artistData; -let officialAlbumData, fandomAlbumData; - -let ready = false; - -// Localiz8tion nonsense ---------------------------------- - -const language = document.documentElement.getAttribute('lang'); - -let list; -if (typeof Intl === 'object' && typeof Intl.ListFormat === 'function') { - const getFormat = (type) => { - const formatter = new Intl.ListFormat(language, {type}); - return formatter.format.bind(formatter); - }; - - list = { - conjunction: getFormat('conjunction'), - disjunction: getFormat('disjunction'), - unit: getFormat('unit'), - }; -} else { - // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free. - // We use the same mock for every list 'cuz we don't have any of the - // necessary CLDR info to appropri8tely distinguish 8etween them. - const arbitraryMock = (array) => array.join(', '); - - list = { - conjunction: arbitraryMock, - disjunction: arbitraryMock, - unit: arbitraryMock, - }; -} - -// Miscellaneous helpers ---------------------------------- - -function rebase(href, rebaseKey = 'rebaseLocalized') { - const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/'; - if (relative) { - return relative + href; - } else { - return href; - } -} - -function pick(array) { - return array[Math.floor(Math.random() * array.length)]; -} - -function cssProp(el, key) { - return getComputedStyle(el).getPropertyValue(key).trim(); -} - -function getRefDirectory(ref) { - return ref.split(':')[1]; -} - -function getAlbum(el) { - const directory = cssProp(el, '--album-directory'); - return albumData.find((album) => album.directory === directory); -} - -// TODO: These should pro8a8ly access some shared urlSpec path. We'd need to -// separ8te the tooling around that into common-shared code too. -const getLinkHref = (type, directory) => rebase(`${type}/${directory}`); -const openAlbum = (d) => rebase(`album/${d}`); -const openTrack = (d) => rebase(`track/${d}`); -const openArtist = (d) => rebase(`artist/${d}`); - -// TODO: This should also use urlSpec. -function fetchData(type, directory) { - return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData')).then( - (res) => res.json() - ); -} - -// JS-based links ----------------------------------------- - -for (const a of document.body.querySelectorAll('[data-random]')) { - a.addEventListener('click', (evt) => { - if (!ready) { - evt.preventDefault(); - return; - } - - setTimeout(() => { - a.href = rebase('js-disabled'); - }); - switch (a.dataset.random) { - case 'album': - return (a.href = openAlbum(pick(albumData).directory)); - case 'album-in-fandom': - return (a.href = openAlbum(pick(fandomAlbumData).directory)); - case 'album-in-official': - return (a.href = openAlbum(pick(officialAlbumData).directory)); - case 'track': - return (a.href = openTrack( - getRefDirectory( - pick( - albumData.map((a) => a.tracks).reduce((a, b) => a.concat(b), []) - ) - ) - )); - case 'track-in-album': - return (a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)))); - case 'track-in-fandom': - return (a.href = openTrack( - getRefDirectory( - pick( - fandomAlbumData.reduce( - (acc, album) => acc.concat(album.tracks), - [] - ) - ) - ) - )); - case 'track-in-official': - return (a.href = openTrack( - getRefDirectory( - pick( - officialAlbumData.reduce( - (acc, album) => acc.concat(album.tracks), - [] - ) - ) - ) - )); - case 'artist': - return (a.href = openArtist(pick(artistData).directory)); - case 'artist-more-than-one-contrib': - return (a.href = openArtist( - pick( - artistData.filter((artist) => getArtistNumContributions(artist) > 1) - ).directory - )); - } - }); -} - -const next = document.getElementById('next-button'); -const previous = document.getElementById('previous-button'); -const random = document.getElementById('random-button'); - -const prependTitle = (el, prepend) => { - const existing = el.getAttribute('title'); - if (existing) { - el.setAttribute('title', prepend + ' ' + existing); - } else { - el.setAttribute('title', prepend); - } -}; - -if (next) prependTitle(next, '(Shift+N)'); -if (previous) prependTitle(previous, '(Shift+P)'); -if (random) prependTitle(random, '(Shift+R)'); - -document.addEventListener('keypress', (event) => { - if (event.shiftKey) { - if (event.charCode === 'N'.charCodeAt(0)) { - if (next) next.click(); - } else if (event.charCode === 'P'.charCodeAt(0)) { - if (previous) previous.click(); - } else if (event.charCode === 'R'.charCodeAt(0)) { - if (random && ready) random.click(); - } - } -}); - -for (const reveal of document.querySelectorAll('.reveal')) { - reveal.addEventListener('click', (event) => { - if (!reveal.classList.contains('revealed')) { - reveal.classList.add('revealed'); - event.preventDefault(); - event.stopPropagation(); - reveal.dispatchEvent(new CustomEvent('hsmusic-reveal')); - } - }); -} - -const elements1 = document.getElementsByClassName('js-hide-once-data'); -const elements2 = document.getElementsByClassName('js-show-once-data'); - -for (const element of elements1) element.style.display = 'block'; - -fetch(rebase('data.json', 'rebaseShared')) - .then((data) => data.json()) - .then((data) => { - albumData = data.albumData; - artistData = data.artistData; - - officialAlbumData = albumData.filter((album) => - album.groups.includes('group:official') - ); - fandomAlbumData = albumData.filter( - (album) => !album.groups.includes('group:official') - ); - - for (const element of elements1) element.style.display = 'none'; - for (const element of elements2) element.style.display = 'block'; - - ready = true; - }); - -// Data & info card --------------------------------------- - -/* -const NORMAL_HOVER_INFO_DELAY = 750; -const FAST_HOVER_INFO_DELAY = 250; -const END_FAST_HOVER_DELAY = 500; -const HIDE_HOVER_DELAY = 250; - -let fastHover = false; -let endFastHoverTimeout = null; - -function colorLink(a, color) { - console.warn('Info card link colors temporarily disabled: chroma.js required, no dependency linking for client.js yet'); - return; - - // eslint-disable-next-line no-unreachable - const chroma = {}; - - if (color) { - const {primary, dim} = getColors(color, {chroma}); - a.style.setProperty('--primary-color', primary); - a.style.setProperty('--dim-color', dim); - } -} - -function link(a, type, {name, directory, color}) { - colorLink(a, color); - a.innerText = name; - a.href = getLinkHref(type, directory); -} - -function joinElements(type, elements) { - // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on - // strings. So instead, we'll pass the element's outer HTML's (which means - // the entire HTML of that element). - // - // That does mean this function returns a string, so always 8e sure to - // set innerHTML when using it (not appendChild). - - return list[type](elements.map((el) => el.outerHTML)); -} - -const infoCard = (() => { - const container = document.getElementById('info-card-container'); - - let cancelShow = false; - let hideTimeout = null; - let showing = false; - - container.addEventListener('mouseenter', cancelHide); - container.addEventListener('mouseleave', readyHide); - - function show(type, target) { - cancelShow = false; - - fetchData(type, target.dataset[type]).then((data) => { - // Manual DOM 'cuz we're laaaazy. - - if (cancelShow) { - return; - } - - showing = true; - - const rect = target.getBoundingClientRect(); - - container.style.setProperty('--primary-color', data.color); - - container.style.top = window.scrollY + rect.bottom + 'px'; - container.style.left = window.scrollX + rect.left + 'px'; - - // Use a short timeout to let a currently hidden (or not yet shown) - // info card teleport to the position set a8ove. (If it's currently - // shown, it'll transition to that position.) - setTimeout(() => { - container.classList.remove('hide'); - container.classList.add('show'); - }, 50); - - // 8asic details. - - const nameLink = container.querySelector('.info-card-name a'); - link(nameLink, 'track', data); - - const albumLink = container.querySelector('.info-card-album a'); - link(albumLink, 'album', data.album); - - const artistSpan = container.querySelector('.info-card-artists span'); - artistSpan.innerHTML = joinElements( - 'conjunction', - data.artists.map(({artist}) => { - const a = document.createElement('a'); - a.href = getLinkHref('artist', artist.directory); - a.innerText = artist.name; - return a; - }) - ); - - const coverArtistParagraph = container.querySelector( - '.info-card-cover-artists' - ); - const coverArtistSpan = coverArtistParagraph.querySelector('span'); - if (data.coverArtists.length) { - coverArtistParagraph.style.display = 'block'; - coverArtistSpan.innerHTML = joinElements( - 'conjunction', - data.coverArtists.map(({artist}) => { - const a = document.createElement('a'); - a.href = getLinkHref('artist', artist.directory); - a.innerText = artist.name; - return a; - }) - ); - } else { - coverArtistParagraph.style.display = 'none'; - } - - // Cover art. - - const [containerNoReveal, containerReveal] = [ - container.querySelector('.info-card-art-container.no-reveal'), - container.querySelector('.info-card-art-container.reveal'), - ]; - - const [containerShow, containerHide] = data.cover.warnings.length - ? [containerReveal, containerNoReveal] - : [containerNoReveal, containerReveal]; - - containerHide.style.display = 'none'; - containerShow.style.display = 'block'; - - const img = containerShow.querySelector('.info-card-art'); - img.src = rebase(data.cover.paths.small, 'rebaseMedia'); - - const imgLink = containerShow.querySelector('a'); - colorLink(imgLink, data.color); - imgLink.href = rebase(data.cover.paths.original, 'rebaseMedia'); - - if (containerShow === containerReveal) { - const cw = containerShow.querySelector('.info-card-art-warnings'); - cw.innerText = list.unit(data.cover.warnings); - - const reveal = containerShow.querySelector('.reveal'); - reveal.classList.remove('revealed'); - } - }); - } - - function hide() { - container.classList.remove('show'); - container.classList.add('hide'); - cancelShow = true; - showing = false; - } - - function readyHide() { - if (!hideTimeout && showing) { - hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY); - } - } - - function cancelHide() { - if (hideTimeout) { - clearTimeout(hideTimeout); - hideTimeout = null; - } - } - - return { - show, - hide, - readyHide, - cancelHide, - }; -})(); - -function makeInfoCardLinkHandlers(type) { - let hoverTimeout = null; - - return { - mouseenter(evt) { - hoverTimeout = setTimeout( - () => { - fastHover = true; - infoCard.show(type, evt.target); - }, - fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY); - - clearTimeout(endFastHoverTimeout); - endFastHoverTimeout = null; - - infoCard.cancelHide(); - }, - - mouseleave() { - clearTimeout(hoverTimeout); - - if (fastHover && !endFastHoverTimeout) { - endFastHoverTimeout = setTimeout(() => { - endFastHoverTimeout = null; - fastHover = false; - }, END_FAST_HOVER_DELAY); - } - - infoCard.readyHide(); - }, - }; -} - -const infoCardLinkHandlers = { - track: makeInfoCardLinkHandlers('track'), -}; - -function addInfoCardLinkHandlers(type) { - for (const a of document.querySelectorAll(`a[data-${type}]`)) { - for (const [eventName, handler] of Object.entries( - infoCardLinkHandlers[type] - )) { - a.addEventListener(eventName, handler); - } - } -} - -// Info cards are disa8led for now since they aren't quite ready for release, -// 8ut you can try 'em out 8y setting this localStorage flag! -// -// localStorage.tryInfoCards = true; -// -if (localStorage.tryInfoCards) { - addInfoCardLinkHandlers('track'); -} -*/ - -// Custom hash links -------------------------------------- - -function addHashLinkHandlers() { - // Instead of defining a scroll offset (to account for the sticky heading) - // in JavaScript, we interface with the CSS property 'scroll-margin-top'. - // 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('#')) { - continue; - } - - a.addEventListener('click', handleHashLinkClicked); - } - - function handleHashLinkClicked(evt) { - if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) { - return; - } - - 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); - - const scrollY = - window.scrollY - + box.top - - style['scroll-margin-top'].replace('px', ''); - - 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; - } - }); - } - } -} - -addHashLinkHandlers(); - -// Sticky content heading --------------------------------- - -const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container')) - .map(stickyContainer => { - const {parentElement: contentContainer} = stickyContainer; - const stickySubheadingRow = stickyContainer.querySelector('.content-sticky-subheading-row'); - const stickySubheading = stickySubheadingRow.querySelector('h2'); - const stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container'); - const stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover'); - const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading')); - const contentCover = contentContainer.querySelector('#cover-art-container'); - - return { - contentContainer, - contentCover, - contentHeadings, - stickyContainer, - stickyCover, - stickyCoverContainer, - stickySubheading, - stickySubheadingRow, - state: { - displayedHeading: null, - }, - }; - }); - -const topOfViewInside = (el, scroll = window.scrollY) => ( - scroll > el.offsetTop && - scroll < el.offsetTop + el.offsetHeight -); - -function prepareStickyHeadings() { - for (const { - contentCover, - stickyCover, - } of stickyHeadingInfo) { - const coverRevealImage = contentCover?.querySelector('.reveal'); - if (coverRevealImage) { - stickyCover.classList.add('content-sticky-heading-cover-needs-reveal'); - coverRevealImage.addEventListener('hsmusic-reveal', () => { - stickyCover.classList.remove('content-sticky-heading-cover-needs-reveal'); - }); - } - } -} - -function updateStickyHeading() { - for (const { - contentContainer, - contentCover, - contentHeadings, - stickyContainer, - stickyCoverContainer, - stickySubheading, - stickySubheadingRow, - state, - } of stickyHeadingInfo) { - let closestHeading = null; - - if (contentCover && stickyCoverContainer) { - if (contentCover.getBoundingClientRect().bottom < 0) { - stickyCoverContainer.classList.add('visible'); - } else { - stickyCoverContainer.classList.remove('visible'); - } - } - - 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 + 20) { - 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)); - } - } - - stickySubheadingRow.classList.add('visible'); - } else { - stickySubheadingRow.classList.remove('visible'); - } - - state.displayedHeading = closestHeading; - } - } -} - -document.addEventListener('scroll', updateStickyHeading); -prepareStickyHeadings(); -updateStickyHeading(); - -// Image overlay ------------------------------------------ - -function addImageOverlayClickHandlers() { - const container = document.getElementById('image-overlay-container'); - - if (!container) { - console.warn(`#image-overlay-container missing, image overlay module disabled.`); - return; - } - - for (const link of document.querySelectorAll('.image-link')) { - if (link.querySelector('img').hasAttribute('data-no-image-preview')) { - continue; - } - - link.addEventListener('click', handleImageLinkClicked); - } - - const actionContainer = document.getElementById('image-overlay-action-container'); - - container.addEventListener('click', handleContainerClicked); - document.body.addEventListener('keydown', handleKeyDown); - - function handleContainerClicked(evt) { - // Only hide the image overlay if actually clicking the background. - if (evt.target !== container) { - return; - } - - // If you clicked anything close to or beneath the action bar, don't hide - // the image overlay. - const rect = actionContainer.getBoundingClientRect(); - if (evt.clientY >= rect.top - 40) { - return; - } - - container.classList.remove('visible'); - } - - function handleKeyDown(evt) { - if (evt.key === 'Escape' || evt.key === 'Esc' || evt.keyCode === 27) { - container.classList.remove('visible'); - } - } -} - -function handleImageLinkClicked(evt) { - if (evt.metaKey || evt.shiftKey || evt.altKey) { - return; - } - evt.preventDefault(); - - const container = document.getElementById('image-overlay-container'); - container.classList.add('visible'); - container.classList.remove('loaded'); - container.classList.remove('errored'); - - const allViewOriginal = document.getElementsByClassName('image-overlay-view-original'); - const mainImage = document.getElementById('image-overlay-image'); - const thumbImage = document.getElementById('image-overlay-image-thumb'); - - const mainThumbSize = getPreferredThumbSize(); - - const source = evt.target.closest('a').href; - - const mainSrc = source.replace(/\.(jpg|png)$/, `.${mainThumbSize}.jpg`); - const thumbSrc = source.replace(/\.(jpg|png)$/, '.small.jpg'); - - thumbImage.src = thumbSrc; - for (const viewOriginal of allViewOriginal) { - viewOriginal.href = source; - } - - mainImage.addEventListener('load', handleMainImageLoaded); - mainImage.addEventListener('error', handleMainImageErrored); - - const fileSize = evt.target.closest('a').querySelector('img').dataset.originalSize; - updateFileSizeInformation(fileSize); - - container.style.setProperty('--download-progress', '0%'); - loadImage(mainSrc, progress => { - container.style.setProperty('--download-progress', (20 + 0.8 * progress) + '%'); - }).then( - blobUrl => { - mainImage.src = blobUrl; - container.style.setProperty('--download-progress', '100%'); - }, - handleMainImageErrored); - - function handleMainImageLoaded() { - mainImage.removeEventListener('load', handleMainImageLoaded); - mainImage.removeEventListener('error', handleMainImageErrored); - container.classList.add('loaded'); - } - - function handleMainImageErrored() { - mainImage.removeEventListener('load', handleMainImageLoaded); - mainImage.removeEventListener('error', handleMainImageErrored); - container.classList.add('errored'); - } -} - -function getPreferredThumbSize() { - // Assuming a square, the image will be constrained to the lesser window - // dimension. Coefficient here matches CSS dimensions for image overlay. - const constrainedLength = Math.floor(Math.min( - 0.80 * window.innerWidth, - 0.80 * window.innerHeight)); - - // Match device pixel ratio, which is 2x for "retina" displays and certain - // device configurations. - const visualLength = window.devicePixelRatio * constrainedLength; - - const largeLength = 800; - const semihugeLength = 1200; - const goodEnoughThreshold = 0.90; - - if (Math.floor(visualLength * goodEnoughThreshold) <= largeLength) { - return 'large'; - } else if (Math.floor(visualLength * goodEnoughThreshold) <= semihugeLength) { - return 'semihuge'; - } else { - return 'huge'; - } -} - -function updateFileSizeInformation(fileSize) { - const fileSizeWarningThreshold = 8 * 10 ** 6; - - const actionContentWithoutSize = document.getElementById('image-overlay-action-content-without-size'); - const actionContentWithSize = document.getElementById('image-overlay-action-content-with-size'); - - if (!fileSize) { - actionContentWithSize.classList.remove('visible'); - actionContentWithoutSize.classList.add('visible'); - return; - } - - actionContentWithoutSize.classList.remove('visible'); - actionContentWithSize.classList.add('visible'); - - const megabytesContainer = document.getElementById('image-overlay-file-size-megabytes'); - const kilobytesContainer = document.getElementById('image-overlay-file-size-kilobytes'); - const megabytesContent = megabytesContainer.querySelector('.image-overlay-file-size-count'); - const kilobytesContent = kilobytesContainer.querySelector('.image-overlay-file-size-count'); - const fileSizeWarning = document.getElementById('image-overlay-file-size-warning'); - - fileSize = parseInt(fileSize); - const round = (exp) => Math.round(fileSize / 10 ** (exp - 1)) / 10; - - if (fileSize > fileSizeWarningThreshold) { - fileSizeWarning.classList.add('visible'); - } else { - fileSizeWarning.classList.remove('visible'); - } - - if (fileSize > 10 ** 6) { - megabytesContainer.classList.add('visible'); - kilobytesContainer.classList.remove('visible'); - megabytesContent.innerText = round(6); - } else { - megabytesContainer.classList.remove('visible'); - kilobytesContainer.classList.add('visible'); - kilobytesContent.innerText = round(3); - } - - void fileSizeWarning; -} - -addImageOverlayClickHandlers(); - -/** - * Credits: Parziphal, Feb 13, 2017 - * https://stackoverflow.com/a/42196770 - * - * Loads an image with progress callback. - * - * The `onprogress` callback will be called by XMLHttpRequest's onprogress - * event, and will receive the loading progress ratio as an whole number. - * However, if it's not possible to compute the progress ratio, `onprogress` - * will be called only once passing -1 as progress value. This is useful to, - * for example, change the progress animation to an undefined animation. - * - * @param {string} imageUrl The image to load - * @param {Function} onprogress - * @return {Promise} - */ -function loadImage(imageUrl, onprogress) { - return new Promise((resolve, reject) => { - var xhr = new XMLHttpRequest(); - var notifiedNotComputable = false; - - xhr.open('GET', imageUrl, true); - xhr.responseType = 'arraybuffer'; - - xhr.onprogress = function(ev) { - if (ev.lengthComputable) { - onprogress(parseInt((ev.loaded / ev.total) * 1000) / 10); - } else { - if (!notifiedNotComputable) { - notifiedNotComputable = true; - onprogress(-1); - } - } - } - - xhr.onloadend = function() { - if (!xhr.status.toString().match(/^2/)) { - reject(xhr); - } else { - if (!notifiedNotComputable) { - onprogress(100); - } - - var options = {} - var headers = xhr.getAllResponseHeaders(); - var m = headers.match(/^Content-Type:\s*(.*?)$/mi); - - if (m && m[1]) { - options.type = m[1]; - } - - var blob = new Blob([this.response], options); - - resolve(window.URL.createObjectURL(blob)); - } - } - - xhr.send(); - }); -} - -// Group contributions table ------------------------------ - -const groupContributionsTableInfo = - Array.from(document.querySelectorAll('#content dl')) - .filter(dl => dl.querySelector('a.group-contributions-sort-button')) - .map(dl => ({ - sortingByCountLink: dl.querySelector('dt.group-contributions-sorted-by-count a.group-contributions-sort-button'), - sortingByDurationLink: dl.querySelector('dt.group-contributions-sorted-by-duration a.group-contributions-sort-button'), - sortingByCountElements: dl.querySelectorAll('.group-contributions-sorted-by-count'), - sortingByDurationElements: dl.querySelectorAll('.group-contributions-sorted-by-duration'), - })); - -function sortGroupContributionsTableBy(info, sort) { - const [showThese, hideThese] = - (sort === 'count' - ? [info.sortingByCountElements, info.sortingByDurationElements] - : [info.sortingByDurationElements, info.sortingByCountElements]); - - for (const element of showThese) element.classList.add('visible'); - for (const element of hideThese) element.classList.remove('visible'); -} - -for (const info of groupContributionsTableInfo) { - info.sortingByCountLink.addEventListener('click', evt => { - evt.preventDefault(); - sortGroupContributionsTableBy(info, 'duration'); - }); - - info.sortingByDurationLink.addEventListener('click', evt => { - evt.preventDefault(); - sortGroupContributionsTableBy(info, 'count'); - }); -} -- cgit 1.3.0-6-gf8a5