diff options
Diffstat (limited to 'src/static/js/client/sidebar-search.js')
-rw-r--r-- | src/static/js/client/sidebar-search.js | 1351 |
1 files changed, 1351 insertions, 0 deletions
diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js new file mode 100644 index 00000000..eae1e74e --- /dev/null +++ b/src/static/js/client/sidebar-search.js @@ -0,0 +1,1351 @@ +/* eslint-env browser */ + +import {getColors} from '../../shared-util/colors.js'; +import {accumulateSum, empty, unique} from '../../shared-util/sugar.js'; + +import { + cssProp, + openAlbum, + openArtist, + openArtTag, + openFlash, + openGroup, + openTrack, + rebase, + templateContent, +} from '../client-util.js'; + +import {getLatestDraggedLink} from './dragged-link.js'; + +import { + info as wikiSearchInfo, + getSearchWorkerDownloadContext, + searchAll, +} from './wiki-search.js'; + +export const info = { + id: 'sidebarSearchInfo', + + pageContainer: null, + + searchSidebarColumn: null, + searchBox: null, + searchLabel: null, + searchInput: null, + + progressRule: null, + progressContainer: null, + progressLabel: null, + progressBar: null, + + failedRule: null, + failedContainer: null, + + filterContainer: null, + albumFilterLink: null, + artistFilterLink: null, + flashFilterLink: null, + groupFilterLink: null, + tagFilterLink: null, + trackFilterLink: null, + + resultsRule: null, + resultsContainer: null, + results: null, + + endSearchRule: null, + endSearchLine: null, + endSearchLink: null, + + standbyInputPlaceholder: null, + + preparingString: null, + loadingDataString: null, + searchingString: null, + failedString: null, + + noResultsString: null, + currentResultString: null, + endSearchString: null, + + albumResultKindString: null, + artistResultKindString: null, + groupResultKindString: null, + tagResultKindString: null, + + albumResultFilterString: null, + artistResultFilterString: null, + flashResultFilterString: null, + groupResultFilterString: null, + tagResultFilterString: null, + trackResultFilterString: null, + + state: { + sidebarColumnShownForSearch: null, + + tidiedSidebar: null, + collapsedDetailsForTidiness: null, + + recallingRecentSearch: null, + recallingRecentSearchFromMouse: null, + + currentValue: null, + + workerStatus: null, + searchStage: null, + + stoppedTypingTimeout: null, + stoppedScrollingTimeout: null, + focusFirstResultTimeout: null, + dismissChangeEventTimeout: null, + + indexDownloadStatuses: Object.create(null), + }, + + session: { + activeQuery: { + type: 'string', + }, + + activeQueryResults: { + type: 'json', + maxLength: settings => settings.maxActiveResultsStorage, + }, + + activeFilterType: { + type: 'string', + }, + + repeatQueryOnReload: { + type: 'boolean', + default: false, + }, + + resultsScrollOffset: { + type: 'number', + }, + }, + + settings: { + stoppedTypingDelay: 800, + stoppedScrollingDelay: 200, + + pressDownToFocusFirstResultLatency: 500, + dismissChangeEventAfterFocusingFirstResultLatency: 50, + + maxActiveResultsStorage: 100000, + }, +}; + +export function getPageReferences() { + info.pageContainer = + document.getElementById('page-container'); + + info.searchBox = + document.querySelector('.wiki-search-sidebar-box'); + + if (!info.searchBox) { + return; + } + + info.searchLabel = + info.searchBox.querySelector('.wiki-search-label'); + + info.searchInput = + info.searchBox.querySelector('.wiki-search-input'); + + info.searchSidebarColumn = + info.searchBox.closest('.sidebar-column'); + + info.standbyInputPlaceholder = + info.searchInput.placeholder; + + const findString = classPart => + info.searchBox.querySelector(`.wiki-search-${classPart}-string`); + + info.preparingString = + findString('preparing'); + + info.loadingDataString = + findString('loading-data'); + + info.searchingString = + findString('searching'); + + info.failedString = + findString('failed'); + + info.noResultsString = + findString('no-results'); + + info.currentResultString = + findString('current-result'); + + info.endSearchString = + findString('end-search'); + + info.albumResultKindString = + findString('album-result-kind'); + + info.artistResultKindString = + findString('artist-result-kind'); + + info.groupResultKindString = + findString('group-result-kind'); + + info.tagResultKindString = + findString('tag-result-kind'); + + info.albumResultFilterString = + findString('album-result-filter'); + + info.artistResultFilterString = + findString('artist-result-filter'); + + info.flashResultFilterString = + findString('flash-result-filter'); + + info.groupResultFilterString = + findString('group-result-filter'); + + info.tagResultFilterString = + findString('tag-result-filter'); + + info.trackResultFilterString = + findString('track-result-filter'); +} + +export function addInternalListeners() { + if (!info.searchBox) return; + + wikiSearchInfo.event.whenWorkerAlive.push( + trackSidebarSearchWorkerAlive, + updateSidebarSearchStatus); + + wikiSearchInfo.event.whenWorkerReady.push( + trackSidebarSearchWorkerReady, + updateSidebarSearchStatus); + + wikiSearchInfo.event.whenWorkerFailsToInitialize.push( + trackSidebarSearchWorkerFailsToInitialize, + updateSidebarSearchStatus); + + wikiSearchInfo.event.whenWorkerHasRuntimeError.push( + trackSidebarSearchWorkerHasRuntimeError, + updateSidebarSearchStatus); + + wikiSearchInfo.event.whenDownloadsBegin.push( + trackSidebarSearchDownloadsBegin, + updateSidebarSearchStatus); + + wikiSearchInfo.event.whenDownloadProgresses.push( + updateSidebarSearchStatus); + + wikiSearchInfo.event.whenDownloadEnds.push( + trackSidebarSearchDownloadEnds, + updateSidebarSearchStatus); +} + +export function mutatePageContent() { + if (!info.searchBox) return; + + // Progress section + + info.progressRule = + document.createElement('hr'); + + info.progressContainer = + document.createElement('div'); + + info.progressContainer.classList.add('wiki-search-progress-container'); + + cssProp(info.progressRule, 'display', 'none'); + cssProp(info.progressContainer, 'display', 'none'); + + info.progressLabel = + document.createElement('label'); + + info.progressLabel.classList.add('wiki-search-progress-label'); + info.progressLabel.htmlFor = 'wiki-search-progress-bar'; + + info.progressBar = + document.createElement('progress'); + + info.progressBar.classList.add('wiki-search-progress-bar'); + info.progressBar.id = 'wiki-search-progress-bar'; + + info.progressContainer.appendChild(info.progressLabel); + info.progressContainer.appendChild(info.progressBar); + + info.searchBox.appendChild(info.progressRule); + info.searchBox.appendChild(info.progressContainer); + + // Search failed section + + info.failedRule = + document.createElement('hr'); + + info.failedContainer = + document.createElement('div'); + + info.failedContainer.classList.add('wiki-search-failed-container'); + + { + const p = document.createElement('p'); + p.appendChild(templateContent(info.failedString)); + info.failedContainer.appendChild(p); + } + + cssProp(info.failedRule, 'display', 'none'); + cssProp(info.failedContainer, 'display', 'none'); + + info.searchBox.appendChild(info.failedRule); + info.searchBox.appendChild(info.failedContainer); + + // Filter section + + info.filterContainer = + document.createElement('div'); + + info.filterContainer.classList.add('wiki-search-filter-container'); + + cssProp(info.filterContainer, 'display', 'none'); + + forEachFilter((type, _filterLink) => { + // TODO: It's probably a sin to access `session` during this step LOL + const {session} = info; + + const filterLink = document.createElement('a'); + + filterLink.href = '#'; + filterLink.classList.add('wiki-search-filter-link'); + + if (session.activeFilterType === type) { + filterLink.classList.add('active'); + } + + const string = info[type + 'ResultFilterString']; + filterLink.appendChild(templateContent(string)); + + info[type + 'FilterLink'] = filterLink; + + info.filterContainer.appendChild(filterLink); + }); + + info.searchBox.appendChild(info.filterContainer); + + // Results section + + info.resultsRule = + document.createElement('hr'); + + info.resultsContainer = + document.createElement('div'); + + info.resultsContainer.classList.add('wiki-search-results-container'); + + cssProp(info.resultsRule, 'display', 'none'); + cssProp(info.resultsContainer, 'display', 'none'); + + info.results = + document.createElement('div'); + + info.results.classList.add('wiki-search-results'); + + info.resultsContainer.appendChild(info.results); + + info.searchBox.appendChild(info.resultsRule); + info.searchBox.appendChild(info.resultsContainer); + + // End search section + + info.endSearchRule = + document.createElement('hr'); + + info.endSearchLine = + document.createElement('p'); + + info.endSearchLink = + document.createElement('a'); + + { + const p = info.endSearchLine; + const a = info.endSearchLink; + p.classList.add('wiki-search-end-search-line'); + a.setAttribute('href', '#'); + a.appendChild(templateContent(info.endSearchString)); + p.appendChild(a); + } + + cssProp(info.endSearchRule, 'display', 'none'); + cssProp(info.endSearchLine, 'display', 'none'); + + info.searchBox.appendChild(info.endSearchRule); + info.searchBox.appendChild(info.endSearchLine); +} + +export function addPageListeners() { + if (!info.searchInput) return; + + info.searchInput.addEventListener('mousedown', _domEvent => { + const {state} = info; + + if (state.recallingRecentSearch) { + state.recallingRecentSearchFromMouse = true; + } + }); + + info.searchInput.addEventListener('focus', _domEvent => { + const {session, state} = info; + + if (state.recallingRecentSearch) { + info.searchInput.value = session.activeQuery; + info.searchInput.placeholder = info.standbyInputPlaceholder; + showSidebarSearchResults(session.activeQueryResults); + state.recallingRecentSearch = false; + } + }); + + info.searchLabel.addEventListener('click', domEvent => { + const {state} = info; + + if (state.recallingRecentSearchFromMouse) { + if (info.searchInput.selectionStart === info.searchInput.selectionEnd) { + info.searchInput.select(); + } + + state.recallingRecentSearchFromMouse = false; + return; + } + + const inputRect = info.searchInput.getBoundingClientRect(); + if (domEvent.clientX < inputRect.left - 3) { + info.searchInput.select(); + } + }); + + info.searchInput.addEventListener('change', _domEvent => { + const {state} = info; + + if (state.dismissChangeEventTimeout) { + state.dismissChangeEventTimeout = null; + clearTimeout(state.dismissChangeEventTimeout); + return; + } + + activateSidebarSearch(info.searchInput.value); + }); + + info.searchInput.addEventListener('input', _domEvent => { + const {settings, state} = info; + + if (!info.searchInput.value) { + clearSidebarSearch(); // ...but don't clear filter + return; + } + + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + } + + state.stoppedTypingTimeout = + setTimeout(() => { + state.stoppedTypingTimeout = null; + activateSidebarSearch(info.searchInput.value); + }, settings.stoppedTypingDelay); + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + } + }); + + info.searchInput.addEventListener('drop', handleDroppedIntoSearchInput); + + info.searchInput.addEventListener('keydown', domEvent => { + const {settings, state} = info; + + if (domEvent.key === 'ArrowUp' || domEvent.key === 'ArrowDown') { + domEvent.preventDefault(); + } + + if (domEvent.key === 'ArrowDown') { + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + state.stoppedTypingTimeout = null; + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + } + + state.focusFirstResultTimeout = + setTimeout(() => { + state.focusFirstResultTimeout = null; + }, settings.pressDownToFocusFirstResultLatency); + + activateSidebarSearch(info.searchInput.value); + } else { + focusFirstSidebarSearchResult(); + } + } + }); + + document.addEventListener('selectionchange', _domEvent => { + const {state} = info; + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + } + }); + + info.endSearchLink.addEventListener('click', domEvent => { + domEvent.preventDefault(); + clearSidebarSearch(); + clearSidebarFilter(); + possiblyHideSearchSidebarColumn(); + restoreSidebarSearchColumn(); + }); + + forEachFilter((type, filterLink) => { + filterLink.addEventListener('click', domEvent => { + domEvent.preventDefault(); + toggleSidebarSearchFilter(type); + }); + }); + + info.resultsContainer.addEventListener('scroll', () => { + const {settings, state} = info; + + if (state.stoppedScrollingTimeout) { + clearTimeout(state.stoppedScrollingTimeout); + } + + state.stoppedScrollingTimeout = + setTimeout(() => { + saveSidebarSearchResultsScrollOffset(); + }, settings.stoppedScrollingDelay); + }); +} + +export function initializeState() { + const {session} = info; + + if (!info.searchInput) return; + + if (session.activeQuery) { + if (session.repeatQueryOnReload) { + info.searchInput.value = session.activeQuery; + activateSidebarSearch(session.activeQuery); + } else if (session.activeQueryResults) { + considerRecallingRecentSidebarSearch(); + } + } +} + +function trackSidebarSearchWorkerAlive() { + const {state} = info; + + state.workerStatus = 'alive'; +} + +function trackSidebarSearchWorkerReady() { + const {state} = info; + + state.workerStatus = 'ready'; + state.searchStage = 'searching'; +} + +function trackSidebarSearchWorkerFailsToInitialize() { + const {state} = info; + + state.workerStatus = 'failed'; + state.searchStage = 'failed'; +} + +function trackSidebarSearchWorkerHasRuntimeError() { + const {state} = info; + + state.workerStatus = 'failed'; + state.searchStage = 'failed'; +} + +function trackSidebarSearchDownloadsBegin(event) { + const {state} = info; + + if (event.context === 'search-indexes') { + for (const key of event.keys) { + state.indexDownloadStatuses[key] = 'active'; + } + } +} + +function trackSidebarSearchDownloadEnds(event) { + const {state} = info; + + if (event.context === 'search-indexes') { + state.indexDownloadStatuses[event.key] = 'complete'; + + const statuses = Object.values(state.indexDownloadStatuses); + if (statuses.every(status => status === 'complete')) { + for (const key of Object.keys(state.indexDownloadStatuses)) { + delete state.indexDownloadStatuses[key]; + } + } + } +} + +function forEachFilter(callback) { + const filterOrder = [ + 'track', + 'album', + 'artist', + 'group', + 'flash', + 'tag', + ]; + + for (const type of filterOrder) { + callback(type, info[type + 'FilterLink']); + } +} + +async function activateSidebarSearch(query) { + const {session, state} = info; + + if (!query) { + return; + } + + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + state.stoppedTypingTimeout = null; + } + + state.searchStage = + (state.workerStatus === 'ready' + ? 'searching' + : 'preparing'); + updateSidebarSearchStatus(); + + let results; + try { + results = await searchAll(query, {enrich: true}); + } catch (error) { + console.error(`There was an error performing a sidebar search:`); + console.error(error); + showSidebarSearchFailed(); + return; + } + + state.searchStage = 'complete'; + updateSidebarSearchStatus(); + + session.activeQuery = query; + session.activeQueryResults = results; + session.resultsScrollOffset = 0; + + showSidebarSearchResults(results); + + if (state.focusFirstResultTimeout) { + clearTimeout(state.focusFirstResultTimeout); + state.focusFirstResultTimeout = null; + focusFirstSidebarSearchResult(); + } +} + +function clearSidebarSearch() { + const {session, state} = info; + + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + state.stoppedTypingTimeout = null; + } + + info.searchBox.classList.remove('showing-results'); + info.searchSidebarColumn.classList.remove('search-showing-results'); + + info.searchInput.value = ''; + + state.searchStage = null; + + session.activeQuery = null; + session.activeQueryResults = null; + session.resultsScrollOffset = null; + + hideSidebarSearchResults(); +} + +function clearSidebarFilter() { + const {session} = info; + + toggleSidebarSearchFilter(session.activeFilterType); + + forEachFilter((_type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + }); +} + +function updateSidebarSearchStatus() { + const {state} = info; + + if (state.searchStage === 'failed') { + hideSidebarSearchResults(); + showSidebarSearchFailed(); + + return; + } + + const searchIndexDownloads = + getSearchWorkerDownloadContext('search-indexes'); + + const downloadProgressValues = + Object.values(searchIndexDownloads ?? {}); + + if (downloadProgressValues.some(v => v < 1.00)) { + const total = Object.keys(state.indexDownloadStatuses).length; + const sum = accumulateSum(downloadProgressValues); + showSidebarSearchProgress( + sum / total, + templateContent(info.loadingDataString)); + + return; + } + + if (state.searchStage === 'preparing') { + showSidebarSearchProgress( + null, + templateContent(info.preparingString)); + + return; + } + + if (state.searchStage === 'searching') { + showSidebarSearchProgress( + null, + templateContent(info.searchingString)); + + return; + } + + hideSidebarSearchProgress(); +} + +function showSidebarSearchProgress(progress, label) { + cssProp(info.progressRule, 'display', null); + cssProp(info.progressContainer, 'display', null); + + if (progress === null) { + info.progressBar.removeAttribute('value'); + } else { + info.progressBar.value = progress; + } + + while (info.progressLabel.firstChild) { + info.progressLabel.firstChild.remove(); + } + + info.progressLabel.appendChild(label); +} + +function hideSidebarSearchProgress() { + cssProp(info.progressRule, 'display', 'none'); + cssProp(info.progressContainer, 'display', 'none'); +} + +function showSidebarSearchFailed() { + const {state} = info; + + hideSidebarSearchProgress(); + hideSidebarSearchResults(); + + cssProp(info.failedRule, 'display', null); + cssProp(info.failedContainer, 'display', null); + + info.searchLabel.classList.add('disabled'); + info.searchInput.disabled = true; + + if (state.stoppedTypingTimeout) { + clearTimeout(state.stoppedTypingTimeout); + state.stoppedTypingTimeout = null; + } +} + +function showSidebarSearchResults(results) { + const {session} = info; + + console.debug(`Showing search results:`, tidyResults(results)); + + showSearchSidebarColumn(); + + info.searchBox.classList.add('showing-results'); + info.searchSidebarColumn.classList.add('search-showing-results'); + + let filterType = session.activeFilterType; + let shownAnyResults = + fillResultElements(results, {filterType: session.activeFilterType}); + + showFilterElements(results); + + if (!shownAnyResults) { + shownAnyResults = toggleSidebarSearchFilter(filterType); + filterType = null; + } + + if (shownAnyResults) { + cssProp(info.endSearchRule, 'display', 'block'); + cssProp(info.endSearchLine, 'display', 'block'); + + tidySidebarSearchColumn(); + } else { + const p = document.createElement('p'); + p.classList.add('wiki-search-no-results'); + p.appendChild(templateContent(info.noResultsString)); + info.results.appendChild(p); + } + + restoreSidebarSearchResultsScrollOffset(); +} + +function tidyResults(results) { + const tidiedResults = + results.results.map(({doc, id}) => ({ + reference: id ?? null, + referenceType: (id ? id.split(':')[0] : null), + directory: (id ? id.split(':')[1] : null), + data: doc, + })); + + return tidiedResults; +} + +function fillResultElements(results, { + filterType = null, +} = {}) { + const tidiedResults = tidyResults(results); + + const filteredResults = + (filterType + ? tidiedResults.filter(result => result.referenceType === filterType) + : tidiedResults); + + while (info.results.firstChild) { + info.results.firstChild.remove(); + } + + cssProp(info.resultsRule, 'display', 'block'); + cssProp(info.resultsContainer, 'display', 'block'); + + if (empty(filteredResults)) { + return false; + } + + for (const result of filteredResults) { + const el = generateSidebarSearchResult(result); + if (!el) continue; + + info.results.appendChild(el); + } + + return true; +} + +function showFilterElements(results) { + const {queriedKind} = results; + + const tidiedResults = tidyResults(results); + + const allReferenceTypes = + unique(tidiedResults.map(result => result.referenceType)); + + let shownAny = false; + + forEachFilter((type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + + if (allReferenceTypes.includes(type)) { + shownAny = true; + cssProp(filterLink, 'display', null); + + if (queriedKind) { + filterLink.setAttribute('inert', 'inert'); + } else { + filterLink.removeAttribute('inert'); + } + + if (type === queriedKind) { + filterLink.classList.add('active-from-query'); + } else { + filterLink.classList.remove('active-from-query'); + } + } else { + cssProp(filterLink, 'display', 'none'); + } + }); + + if (shownAny) { + cssProp(info.filterContainer, 'display', null); + } else { + cssProp(info.filterContainer, 'display', 'none'); + } +} + +function generateSidebarSearchResult(result) { + const preparedSlots = { + color: + result.data.color ?? null, + + name: + result.data.name ?? result.data.primaryName ?? null, + + imageSource: + getSearchResultImageSource(result), + }; + + switch (result.referenceType) { + case 'album': { + preparedSlots.href = + openAlbum(result.directory); + + preparedSlots.kindString = + info.albumResultKindString; + + break; + } + + case 'artist': { + preparedSlots.href = + openArtist(result.directory); + + preparedSlots.kindString = + info.artistResultKindString; + + break; + } + + case 'group': { + preparedSlots.href = + openGroup(result.directory); + + preparedSlots.kindString = + info.groupResultKindString; + + break; + } + + case 'flash': { + preparedSlots.href = + openFlash(result.directory); + + break; + } + + case 'tag': { + preparedSlots.href = + openArtTag(result.directory); + + preparedSlots.kindString = + info.tagResultKindString; + + break; + } + + case 'track': { + preparedSlots.href = + openTrack(result.directory); + + break; + } + + default: + return null; + } + + return generateSidebarSearchResultTemplate(preparedSlots); +} + +function getSearchResultImageSource(result) { + const {artwork} = result.data; + if (!artwork) return null; + + return ( + rebase( + artwork.replace('<>', result.directory), + 'rebaseThumb')); +} + +function generateSidebarSearchResultTemplate(slots) { + const link = document.createElement('a'); + link.classList.add('wiki-search-result'); + + if (slots.href) { + link.setAttribute('href', slots.href); + } + + if (slots.color) { + cssProp(link, '--primary-color', slots.color); + + try { + const colors = + getColors(slots.color, { + chroma: window.chroma, + }); + cssProp(link, '--light-ghost-color', colors.lightGhost); + cssProp(link, '--deep-color', colors.deep); + } catch (error) { + console.warn(error); + } + } + + const imgContainer = document.createElement('span'); + imgContainer.classList.add('wiki-search-result-image-container'); + + if (slots.imageSource) { + const img = document.createElement('img'); + img.classList.add('wiki-search-result-image'); + img.setAttribute('src', slots.imageSource); + imgContainer.appendChild(img); + if (slots.imageSource.endsWith('.mini.jpg')) { + img.classList.add('has-warning'); + } + } else { + const placeholder = document.createElement('span'); + placeholder.classList.add('wiki-search-result-image-placeholder'); + imgContainer.appendChild(placeholder); + } + + link.appendChild(imgContainer); + + const text = document.createElement('span'); + text.classList.add('wiki-search-result-text-area'); + + if (slots.name) { + const span = document.createElement('span'); + span.classList.add('wiki-search-result-name'); + span.appendChild(document.createTextNode(slots.name)); + text.appendChild(span); + } + + let accentSpan = null; + + if (link.href) { + const here = location.href.replace(/\/$/, ''); + const there = link.href.replace(/\/$/, ''); + if (here === there) { + link.classList.add('current-result'); + accentSpan = document.createElement('span'); + accentSpan.classList.add('wiki-search-current-result-text'); + accentSpan.appendChild(templateContent(info.currentResultString)); + } + } + + if (!accentSpan && slots.kindString) { + accentSpan = document.createElement('span'); + accentSpan.classList.add('wiki-search-result-kind'); + accentSpan.appendChild(templateContent(slots.kindString)); + } + + if (accentSpan) { + text.appendChild(document.createTextNode(' ')); + text.appendChild(accentSpan); + } + + link.appendChild(text); + + link.addEventListener('click', () => { + saveSidebarSearchResultsScrollOffset(); + }); + + link.addEventListener('keydown', domEvent => { + if (domEvent.key === 'ArrowDown') { + const elem = link.nextElementSibling; + if (elem) { + domEvent.preventDefault(); + elem.focus({focusVisible: true}); + } + } else if (domEvent.key === 'ArrowUp') { + domEvent.preventDefault(); + const elem = link.previousElementSibling; + if (elem) { + elem.focus({focusVisible: true}); + } else { + info.searchInput.focus(); + } + } + }); + + return link; +} + +function hideSidebarSearchResults() { + cssProp(info.filterContainer, 'display', 'none'); + + cssProp(info.resultsRule, 'display', 'none'); + cssProp(info.resultsContainer, 'display', 'none'); + + while (info.results.firstChild) { + info.results.firstChild.remove(); + } + + cssProp(info.endSearchRule, 'display', 'none'); + cssProp(info.endSearchLine, 'display', 'none'); +} + +function focusFirstSidebarSearchResult() { + const {settings, state} = info; + + const elem = info.results.firstChild; + if (!elem?.classList.contains('wiki-search-result')) { + return; + } + + if (state.dismissChangeEventTimeout) { + clearTimeout(state.dismissChangeEventTimeout); + } + + state.dismissChangeEventTimeout = + setTimeout(() => { + state.dismissChangeEventTimeout = null; + }, settings.dismissChangeEventAfterFocusingFirstResultLatency); + + elem.focus({focusVisible: true}); +} + +function saveSidebarSearchResultsScrollOffset() { + const {session} = info; + + session.resultsScrollOffset = info.resultsContainer.scrollTop; +} + +function restoreSidebarSearchResultsScrollOffset() { + const {session} = info; + + if (session.resultsScrollOffset) { + info.resultsContainer.scrollTop = session.resultsScrollOffset; + } +} + +function showSearchSidebarColumn() { + const {state} = info; + + if (!info.searchSidebarColumn) { + return; + } + + if (!info.searchSidebarColumn.classList.contains('initially-hidden')) { + return; + } + + info.searchSidebarColumn.classList.remove('initially-hidden'); + + if (info.searchSidebarColumn.id === 'sidebar-left') { + info.pageContainer.classList.add('showing-sidebar-left'); + } else if (info.searchSidebarColumn.id === 'sidebar-right') { + info.pageContainer.classList.add('showing-sidebar-right'); + } + + state.sidebarColumnShownForSearch = true; +} + +function possiblyHideSearchSidebarColumn() { + const {state} = info; + + if (!info.searchSidebarColumn) { + return; + } + + if (!state.sidebarColumnShownForSearch) { + return; + } + + info.searchSidebarColumn.classList.add('initially-hidden'); + + if (info.searchSidebarColumn.id === 'sidebar-left') { + info.pageContainer.classList.remove('showing-sidebar-left'); + } else if (info.searchSidebarColumn.id === 'sidebar-right') { + info.pageContainer.classList.remove('showing-sidebar-right'); + } + + state.sidebarColumnShownForSearch = null; +} + +// This should be called after results are shown, since it checks the +// elements added to understand the current search state. +function tidySidebarSearchColumn() { + const {state} = info; + + // Don't *re-tidy* the sidebar if we've already tidied it to display + // some results. This flag will get cleared if the search is dismissed + // altogether (and the pre-tidy state is restored). + if (state.tidiedSidebar) { + return; + } + + const here = location.href.replace(/\/$/, ''); + const currentPageIsResult = + Array.from(info.results.querySelectorAll('a')) + .some(link => { + const there = link.href.replace(/\/$/, ''); + return here === there; + }); + + // Don't tidy the sidebar if you've navigated to some other page than + // what's in the current result list. + if (!currentPageIsResult) { + return; + } + + state.tidiedSidebar = true; + state.collapsedDetailsForTidiness = []; + + for (const box of info.searchSidebarColumn.querySelectorAll('.sidebar')) { + if (box === info.searchBox) { + continue; + } + + for (const details of box.getElementsByTagName('details')) { + if (details.open) { + details.removeAttribute('open'); + state.collapsedDetailsForTidiness.push(details); + } + } + } +} + +function toggleSidebarSearchFilter(toggleType) { + const {session} = info; + + if (!toggleType) return null; + + let shownAnyResults = null; + + forEachFilter((type, filterLink) => { + if (type === toggleType) { + const filterActive = filterLink.classList.toggle('active'); + const filterType = (filterActive ? type : null); + + if (cssProp(filterLink, 'display') !== 'none') { + filterLink.classList.add(filterActive ? 'shown' : 'hidden'); + } + + if (session.activeQueryResults) { + shownAnyResults = + fillResultElements(session.activeQueryResults, {filterType}); + } + + session.activeFilterType = filterType; + } else { + filterLink.classList.remove('active'); + } + }); + + return shownAnyResults; +} + +function restoreSidebarSearchColumn() { + const {state} = info; + + if (!state.tidiedSidebar) { + return; + } + + for (const details of state.collapsedDetailsForTidiness) { + details.setAttribute('open', ''); + } + + state.collapsedDetailsForTidiness = []; + state.tidiedSidebar = null; + + info.searchInput.placeholder = info.standbyInputPlaceholder; +} + +function considerRecallingRecentSidebarSearch() { + const {session, state} = info; + + if (document.documentElement.dataset.urlKey === 'localized.home') { + return forgetRecentSidebarSearch(); + } + + info.searchInput.placeholder = session.activeQuery; + state.recallingRecentSearch = true; +} + +function forgetRecentSidebarSearch() { + const {session} = info; + + session.activeQuery = null; + session.activeQueryResults = null; + + clearSidebarFilter(); +} + +async function handleDroppedIntoSearchInput(domEvent) { + const itemByType = type => + Array.from(domEvent.dataTransfer.items) + .find(item => item.type === type); + + const textItem = itemByType('text/plain'); + + if (!textItem) return; + + domEvent.preventDefault(); + + const getAssTring = item => + new Promise(res => item.getAsString(res)) + .then(string => string.trim()); + + const timer = Date.now(); + + let droppedText = + await getAssTring(textItem); + + if (Date.now() - timer > 500) return; + if (!droppedText) return; + + let droppedURL; + try { + droppedURL = new URL(droppedText); + } catch { + droppedURL = null; + } + + if (droppedURL) matchLink: { + const isDroppedURL = a => + a.toString() === droppedURL.toString(); + + const matchingLinks = + Array.from(document.getElementsByTagName('a')) + .filter(a => + isDroppedURL(new URL(a.href, document.documentURI))); + + const latestDraggedLink = getLatestDraggedLink(); + + if (!matchingLinks.includes(latestDraggedLink)) { + break matchLink; + } + + let matchedLink = latestDraggedLink; + + if (matchedLink.querySelector('.normal-content')) { + matchedLink = matchedLink.cloneNode(true); + for (const node of matchedLink.querySelectorAll('.normal-content')) { + node.remove(); + } + } + + droppedText = matchedLink.innerText; + } + + if (droppedText.includes('-')) splitDashes: { + if (droppedURL) break splitDashes; + if (droppedText.includes(' ')) break splitDashes; + + const parts = droppedText.split('-'); + if (parts.length === 2) break splitDashes; + + droppedText = parts.join(' '); + } + + info.searchInput.value = droppedText; + activateSidebarSearch(info.searchInput.value); +} |