diff --git a/src/lib/assets/dash.svg b/src/lib/assets/dash.svg new file mode 100644 index 00000000..c571a11b --- /dev/null +++ b/src/lib/assets/dash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/components/explorer/FacetCategory.svelte b/src/lib/components/explorer/FacetCategory.svelte index e93fb371..20f93e2e 100644 --- a/src/lib/components/explorer/FacetCategory.svelte +++ b/src/lib/components/explorer/FacetCategory.svelte @@ -2,9 +2,10 @@ import type { DictionaryFacetResult } from '$lib/models/api/DictionaryResponses'; import { AccordionItem } from '@skeletonlabs/skeleton'; import SearchStore from '$lib/stores/Search'; - import type { Facet } from '$lib/models/Search'; import { hiddenFacets } from '$lib/services/dictionary'; - let { updateFacet, selectedFacets } = SearchStore; + import FacetItem from './FacetItem.svelte'; + import type { Facet } from '$lib/models/Search'; + let { updateFacets, selectedFacets } = SearchStore; export let facetCategory: DictionaryFacetResult; export let facets = facetCategory.facets; @@ -23,37 +24,100 @@ (facet) => facet?.categoryRef?.name === facetCategory?.name, ); - $: isChecked = (facetToCheck: string) => { - return $selectedFacets.some((facet: Facet) => { - return facet.name === facetToCheck; + function isIndeterminate(facet: Facet): boolean { + const atLeastOneChildSelected = + facet.children?.some((child) => $selectedFacets.some((f) => f.name === child.name)) ?? false; + const isEveryChildSelected = facet.children?.length + ? facet.children.every((child) => $selectedFacets.some((f) => f.name === child.name)) + : false; + return atLeastOneChildSelected && !isEveryChildSelected; + } + + function isParentFullySelected(facetName: string): boolean { + const result = facets.some((parent) => { + if (!parent.children || parent.children.length === 0) return false; + return parent.children.every( + (child) => child.name === facetName || $selectedFacets.some((f) => f.name === child.name), + ); }); - }; + return result; + } function getFacetsToDisplay() { const hiddenFacetsForCategory = $hiddenFacets[facetCategory.name] || []; let facetsToDisplay = facets.filter((f) => !hiddenFacetsForCategory.includes(f.name)); - //Put selected facets at the top const selectedFacetsMap = new Map($selectedFacets.map((facet) => [facet.name, facet])); - facetsToDisplay = facetsToDisplay.filter((f) => !selectedFacetsMap.has(f.name)); + const indeterminateFacets = facetsToDisplay.filter(isIndeterminate); + const indeterminateMap = new Map(indeterminateFacets.map((facet) => [facet.name, facet])); + const isChildOfIndeterminate = (facetName: string) => { + return indeterminateFacets.some((parent) => + parent.children?.some((child) => child.name === facetName), + ); + }; + + // Remove facets that will be added to the top or are children of fully selected parents + facetsToDisplay = facetsToDisplay.filter( + (f) => + !selectedFacetsMap.has(f.name) && + !isChildOfIndeterminate(f.name) && + !indeterminateMap.has(f.name) && + !isParentFullySelected(f.name), + ); + + // Add selected facets at the top (excluding children of indeterminate parents and fully selected parents) const selectedFacetsForCategory = $selectedFacets.filter( - (facet) => facet.category === facetCategory.name, + (facet) => + facet.category === facetCategory.name && + !isChildOfIndeterminate(facet.name) && + !isParentFullySelected(facet.name), ); selectedFacetsForCategory.forEach((facet) => { facet.count = facets.find((f) => f.name === facet.name)?.count || 0; }); - facetsToDisplay.unshift(...selectedFacetsForCategory); + //Add parents with all children selected + const parentsWithAllChildrenSelected = facets.filter( + (f) => + f.children?.length && + f.children.every((child) => $selectedFacets.some((f) => f.name === child.name)), + ); + parentsWithAllChildrenSelected.forEach((facet) => { + facet.count = facets.find((f) => f.name === facet.name)?.count || 0; + }); + + // Add indeterminate facets at the top + const indeterminateFacetsForCategory = indeterminateFacets.filter( + (facet) => facet.category === facetCategory.name, + ); + indeterminateFacetsForCategory.forEach((facet) => { + facet.count = facets.find((f) => f.name === facet.name)?.count || 0; + }); + + facetsToDisplay.unshift( + ...selectedFacetsForCategory, + ...parentsWithAllChildrenSelected, + ...indeterminateFacetsForCategory, + ); + if (textFilterValue) { - //Filter Facets by searched text const lowerFilterValue = textFilterValue.toLowerCase(); - facetsToDisplay = facetsToDisplay.filter( - (facet) => + facetsToDisplay = facetsToDisplay.filter((facet) => { + const facetMatches = facet.display.toLowerCase().includes(lowerFilterValue) || facet.name.toLowerCase().includes(lowerFilterValue) || - facet.description?.toLowerCase().includes(lowerFilterValue), - ); + facet.description?.toLowerCase().includes(lowerFilterValue); + + const childrenMatch = facet.children?.some( + (child) => + child.display.toLowerCase().includes(lowerFilterValue) || + child.name.toLowerCase().includes(lowerFilterValue) || + child.description?.toLowerCase().includes(lowerFilterValue), + ); + + return facetMatches || childrenMatch; + }); } else if (moreThanTenFacets) { // Only show the first n facets facetsToDisplay = facetsToDisplay.slice(0, numFacetsToShow); @@ -77,22 +141,7 @@ /> {/if} {#each facetsToDisplay as facet} - + {/each} {#if facets.length > numFacetsToShow && !textFilterValue} diff --git a/src/lib/components/explorer/FacetItem.svelte b/src/lib/components/explorer/FacetItem.svelte new file mode 100644 index 00000000..860edba8 --- /dev/null +++ b/src/lib/components/explorer/FacetItem.svelte @@ -0,0 +1,139 @@ + + + +{#if open && facetsToDisplay !== undefined && facetsToDisplay.length > 0} +
+ {#each facetsToDisplay as child} + + {/each} +
+{/if} + + diff --git a/src/lib/models/Search.ts b/src/lib/models/Search.ts index e234fa6e..bb8c7793 100644 --- a/src/lib/models/Search.ts +++ b/src/lib/models/Search.ts @@ -8,6 +8,7 @@ export type Facet = Indexable & { children?: Facet[]; category: string; categoryRef?: ShallowFacetCategory; + parentRef?: ShallowFacetCategory; }; export type ShallowFacetCategory = Pick; diff --git a/src/lib/stores/Search.ts b/src/lib/stores/Search.ts index 9f19dda5..1819f49a 100644 --- a/src/lib/stores/Search.ts +++ b/src/lib/stores/Search.ts @@ -1,9 +1,6 @@ import { get, writable, type Writable } from 'svelte/store'; import { type Facet, type SearchResult } from '$lib/models/Search'; -import type { - DictionaryConceptResult, - DictionaryFacetResult, -} from '$lib/models/api/DictionaryResponses'; +import type { DictionaryConceptResult } from '$lib/models/api/DictionaryResponses'; import type { State } from '@vincjo/datatables/remote'; import { searchDictionary } from '$lib/services/dictionary'; @@ -38,30 +35,18 @@ async function search(searchTerm: string, facets: Facet[], state?: State): Promi } } -async function updateFacet(newFacet: Facet, facetCategory: DictionaryFacetResult | undefined) { - if (facetCategory) { - newFacet.categoryRef = { - display: facetCategory.display, - name: facetCategory.name, - description: facetCategory.description, - }; - } - try { - selectedFacets.update((facets) => { - const index = facets.findIndex((facet) => facet.name === newFacet.name); - if (index === -1) { - facets.push(newFacet); - } else { - facets.splice(index, 1); - } - //For reactivity and sorting - selectedFacets.set(get(selectedFacets).sort((a, b) => b.count - a.count)); - return facets; - }); - } catch (e) { - console.error(e); - return; - } +async function updateFacets(facetsToUpdate: Facet[]) { + const currentFacets = get(selectedFacets); + facetsToUpdate.forEach((facet) => { + const facetIndex = currentFacets.findIndex((f) => f.name === facet.name); + if (facetIndex !== -1) { + currentFacets.splice(facetIndex, 1); + } else { + currentFacets.push(facet); + } + }); + + selectedFacets.set(currentFacets.sort((a, b) => b.count - a.count)); } export function resetSearch() { @@ -75,6 +60,6 @@ export default { searchTerm, error, search, - updateFacet, + updateFacets, resetSearch, };