diff options
-rw-r--r-- | src/content/dependencies/generateSearchSidebarBox.js | 17 | ||||
-rw-r--r-- | src/static/css/site.css | 66 | ||||
-rw-r--r-- | src/static/js/client/sidebar-search.js | 215 | ||||
-rw-r--r-- | src/strings-default.yaml | 7 |
4 files changed, 288 insertions, 17 deletions
diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js index 188a678f..5a0e0b2e 100644 --- a/src/content/dependencies/generateSearchSidebarBox.js +++ b/src/content/dependencies/generateSearchSidebarBox.js @@ -57,6 +57,23 @@ export default { html.tag('template', {class: 'wiki-search-tag-result-kind-string'}, language.$(capsule, 'artTag')), ]), + + language.encapsulate(capsule, 'resultFilter', capsule => [ + html.tag('template', {class: 'wiki-search-album-result-filter-string'}, + language.$(capsule, 'album')), + + html.tag('template', {class: 'wiki-search-artist-result-filter-string'}, + language.$(capsule, 'artist')), + + html.tag('template', {class: 'wiki-search-group-result-filter-string'}, + language.$(capsule, 'group')), + + html.tag('template', {class: 'wiki-search-track-result-filter-string'}, + language.$(capsule, 'track')), + + html.tag('template', {class: 'wiki-search-tag-result-filter-string'}, + language.$(capsule, 'artTag')), + ]), ], })), }; diff --git a/src/static/css/site.css b/src/static/css/site.css index 0a7e36ae..f28c0c9c 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -743,6 +743,72 @@ summary.underline-white > span:hover a:not(:hover) { cursor: default; } +.wiki-search-filter-container { + padding: 4px; +} + +.wiki-search-filter-link { + margin: 2px; + padding: 2px 4px; + border: 2px solid transparent; + border-radius: 4px; +} + +.wiki-search-filter-link.active.shown { + animation: + 0.15s ease 0.00s forwards normal show-filter, + 0.60s linear 0.15s infinite alternate blink-filter; +} + +.wiki-search-filter-link.active:not(.shown) { + animation: + 0.00s linear 0.00s forwards normal show-filter, + 0.60s linear 0.00s infinite alternate blink-filter; +} + +.wiki-search-filter-link:not(.active).hidden { + /* We can't just reverse the show-filter animation, + * because that won't actually start it over again. + */ + animation: + 0.15s ease 0.00s forwards reverse show-filter-the-sequel; +} + +@keyframes show-filter { + from { + background: transparent; + border-color: transparent; + color: var(--primary-color); + } + + to { + background: var(--primary-color); + border-color: var(--primary-color); + color: black; + } +} + +/* Exactly the same as show-filter above. */ +@keyframes show-filter-the-sequel { + from { + background: transparent; + border-color: transparent; + color: var(--primary-color); + } + + to { + background: var(--primary-color); + border-color: var(--primary-color); + color: black; + } +} + +@keyframes blink-filter { + to { + background: color-mix(in srgb, var(--primary-color) 90%, transparent); + } +} + .wiki-search-result { position: relative; display: flex; diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index fb902636..67270477 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -1,7 +1,7 @@ /* eslint-env browser */ import {getColors} from '../../shared-util/colors.js'; -import {accumulateSum, empty} from '../../shared-util/sugar.js'; +import {accumulateSum, empty, unique} from '../../shared-util/sugar.js'; import { cssProp, @@ -41,6 +41,13 @@ export const info = { failedRule: null, failedContainer: null, + filterContainer: null, + albumFilterLink: null, + artistFilterLink: null, + groupFilterLink: null, + tagFilterLink: null, + trackFilterLink: null, + resultsRule: null, resultsContainer: null, results: null, @@ -65,6 +72,12 @@ export const info = { groupResultKindString: null, tagResultKindString: null, + albumResultFilterString: null, + artistResultFilterString: null, + groupResultFilterString: null, + tagResultFilterString: null, + trackResultFilterString: null, + state: { sidebarColumnShownForSearch: null, @@ -97,6 +110,10 @@ export const info = { maxLength: settings => settings.maxActiveResultsStorage, }, + activeFilterType: { + type: 'string', + }, + repeatQueryOnReload: { type: 'boolean', default: false, @@ -176,6 +193,21 @@ export function getPageReferences() { info.tagResultKindString = findString('tag-result-kind'); + + info.albumResultFilterString = + findString('album-result-filter'); + + info.artistResultFilterString = + findString('artist-result-filter'); + + info.groupResultFilterString = + findString('group-result-filter'); + + info.tagResultFilterString = + findString('tag-result-filter'); + + info.trackResultFilterString = + findString('track-result-filter'); } export function addInternalListeners() { @@ -265,6 +297,38 @@ export function mutatePageContent() { 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 = @@ -371,7 +435,7 @@ export function addPageListeners() { const {settings, state} = info; if (!info.searchInput.value) { - clearSidebarSearch(); + clearSidebarSearch(); // ...but don't clear filter return; } @@ -433,10 +497,18 @@ export function addPageListeners() { 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; @@ -518,6 +590,20 @@ function trackSidebarSearchDownloadEnds(event) { } } +function forEachFilter(callback) { + const filterOrder = [ + 'track', + 'album', + 'artist', + 'group', + 'tag', + ]; + + for (const type of filterOrder) { + callback(type, info[type + 'FilterLink']); + } +} + async function activateSidebarSearch(query) { const {session, state} = info; @@ -584,6 +670,14 @@ function clearSidebarSearch() { hideSidebarSearchResults(); } +function clearSidebarFilter() { + toggleSidebarSearchFilter(session.activeFilterType); + + forEachFilter((_type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + }); +} + function updateSidebarSearchStatus() { const {state} = info; @@ -670,10 +764,42 @@ function showSidebarSearchFailed() { } function showSidebarSearchResults(results) { - console.debug(`Showing search results:`, results); + const {session} = info; + + console.debug(`Showing search results:`, flattenResults(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 flattenResults(results) { const flatResults = Object.entries(results) .filter(([index]) => index === 'generic') @@ -686,8 +812,18 @@ function showSidebarSearchResults(results) { data: doc, }))); - info.searchBox.classList.add('showing-results'); - info.searchSidebarColumn.classList.add('search-showing-results'); + return flatResults; +} + +function fillResultElements(results, { + filterType = null, +} = {}) { + const flatResults = flattenResults(results); + + const filteredResults = + (filterType + ? flatResults.filter(result => result.referenceType === filterType) + : flatResults); while (info.results.firstChild) { info.results.firstChild.remove(); @@ -696,28 +832,43 @@ function showSidebarSearchResults(results) { cssProp(info.resultsRule, 'display', 'block'); cssProp(info.resultsContainer, 'display', 'block'); - if (empty(flatResults)) { - const p = document.createElement('p'); - p.classList.add('wiki-search-no-results'); - p.appendChild(templateContent(info.noResultsString)); - info.results.appendChild(p); + if (empty(filteredResults)) { + return false; } - for (const result of flatResults) { + for (const result of filteredResults) { const el = generateSidebarSearchResult(result); if (!el) continue; info.results.appendChild(el); } - if (!empty(flatResults)) { - cssProp(info.endSearchRule, 'display', 'block'); - cssProp(info.endSearchLine, 'display', 'block'); + return true; +} - tidySidebarSearchColumn(); - } +function showFilterElements(results) { + const flatResults = flattenResults(results); - restoreSidebarSearchResultsScrollOffset(); + const allReferenceTypes = + unique(flatResults.map(result => result.referenceType)); + + let shownAny = false; + + forEachFilter((type, filterLink) => { + if (allReferenceTypes.includes(type)) { + shownAny = true; + cssProp(filterLink, 'display', null); + } else { + cssProp(filterLink, 'display', 'none'); + filterLink.classList.remove('shown', 'hidden'); + } + }); + + if (shownAny) { + cssProp(info.filterContainer, 'display', null); + } else { + cssProp(info.filterContainer, 'display', 'none'); + } } function generateSidebarSearchResult(result) { @@ -908,6 +1059,8 @@ function generateSidebarSearchResultTemplate(slots) { } function hideSidebarSearchResults() { + cssProp(info.filterContainer, 'display', 'none'); + cssProp(info.resultsRule, 'display', 'none'); cssProp(info.resultsContainer, 'display', 'none'); @@ -1040,6 +1193,34 @@ function tidySidebarSearchColumn() { } } +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'); + } + + shownAnyResults = + fillResultElements(session.activeQueryResults, {filterType}); + + session.activeFilterType = filterType; + } else { + filterLink.classList.remove('active'); + } + }); + + return shownAnyResults; +} + function restoreSidebarSearchColumn() { const {state} = info; diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 21da885a..80771bb3 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -818,6 +818,13 @@ misc: artist: "(artist)" group: "(group)" + resultFilter: + album: "Albums" + artTag: "Art Tags" + artist: "Artists" + group: "Groups" + track: "Tracks" + # skippers: # # These are navigational links that only show up when you're |