<template> <div class="advanced-search"> <div class="active-filters"> <div v-for="filter in allFilters" :key="filter.join('-')" class="filter-chip" > <span class="filter-text"> {{ filter[1] }} </span> <button class="remove-button" type="button" @click="removeFilter(filter)" > <i class="icon-close"></i> </button> </div> </div> <button class="advanced-search-button" type="button" @click="displayAdvancedSearch" > ADVANCED SEARCH </button> </div> <AppDialog v-model="advancedSearchVisible" class="advanced-search-dialog" @close="updateFilters" aria-label="Advanced search" > <header> <h2>ADVANCED SEARCH</h2> </header> <section> <h3> SEQUENCE MOTIF </h3> <MotifInput v-model="filters.motif" @valid="(valid) => motifIsValid = valid" /> </section> <section> <h3> ORGANISM <button class="select-all-button" :class="{ 'all-selected': allSelected('species', speciesList.length) }" type="button" @click="selectAll('species', speciesList)" > <i class="icon-check"></i> Select all </button> </h3> <div class="checkbox-list"> <div v-for="species in speciesList" :key="species" class="checkbox" > <AppCheckbox v-model="filters.species" :value="species" > {{ species }} </AppCheckbox> </div> </div> </section> <section> <h3> SOURCES <button class="select-all-button" :class="{ 'all-selected': allSelected('sources', sourcesList.length) }" type="button" @click="selectAll('sources', sourcesList)" > <i class="icon-check"></i> Select all </button> </h3> <div class="checkbox-list"> <div v-for="source in sourcesList" :key="source" class="checkbox" > <AppCheckbox v-model="filters.sources" :value="source" > {{ source }} </AppCheckbox> </div> </div> </section> <section> <h3> HEAVY CHAIN GENE SEGMENTS <button class="select-all-button" :class="{ 'all-selected': allSelected('heavySegments', heavySegments.length) }" type="button" @click="selectAll('heavySegments', heavySegments)" > <i class="icon-check"></i> Select all </button> </h3> <div class="checkbox-list"> <div v-for="heavySegment in heavySegments" :key="heavySegment" class="checkbox" > <AppCheckbox v-model="filters.heavySegments" :value="heavySegment" > {{ heavySegment }} </AppCheckbox> </div> </div> </section> <section> <h3> LIGHT CHAIN GENE SEGMENTS <button class="select-all-button" :class="{ 'all-selected': allSelected('lightSegments', lightSegments.length) }" type="button" @click="selectAll('lightSegments', lightSegments)" > <i class="icon-check"></i> Select all </button> </h3> <div class="checkbox-list"> <div v-for="lightSegment in lightSegments" :key="lightSegment" class="checkbox" > <AppCheckbox v-model="filters.lightSegments" :value="lightSegment" > {{ lightSegment }} </AppCheckbox> </div> </div> </section> <footer> <button class="search-button" type="button" @click="closeAdvancedSearch" > SEARCH </button> </footer> </AppDialog> </template> <script setup lang="ts"> import { computed, onMounted, ref } from 'vue' import { useStore } from '../store' import type { AdvancedFilters, FilterChip } from '../types' import AppCheckbox from './AppCheckbox.vue' import AppDialog from './AppDialog.vue' import MotifInput from './MotifInput.vue' onMounted(() => { if (store.antibodiesSources.length === 0) { store.getAntibodiesSources() store.getAntibodiesSpecies() store.getAntibodiesSegments() } }) const emit = defineEmits<{ filtersUpdate: [filters: AdvancedFilters] }>() const store = useStore() const request = computed(() => store.request) const advancedSearchVisible = ref(false) const filters = ref(request.value.advancedFilters()) const motifIsValid = ref(true) const sourcesList = computed(() => store.antibodiesSources) const speciesList = computed(() => store.antibodiesSpecies) const heavySegments = computed(() => store.antibodiesHeavySegments) const lightSegments = computed(() => store.antibodiesLightSegments) /** * The list of filter chips to display under the main * input bar. */ const allFilters = computed(() => { const filterChips: FilterChip[] = [] for (const filter of Object.entries(filters.value)) { const filterName = filter[0] as keyof AdvancedFilters const filterValue = filter[1] if (Array.isArray(filterValue)) { filterValue.forEach(value => filterChips.push([filterName, value])) } // Special handling for the motif input since its not an array // and needs to be validated. else if (filterName === 'motif' && filterValue && motifIsValid.value) { filterChips.push([filterName, filterValue]) } } return filterChips }) /** * Open the dialog box. */ function displayAdvancedSearch(): void { advancedSearchVisible.value = true } /** * Close the dialog box. */ function closeAdvancedSearch(): void { advancedSearchVisible.value = false } /** * On dialog close, emit the new filters to * the parent component. */ function updateFilters(): void { const validFilters = Object.assign({}, filters.value) if (!motifIsValid.value) validFilters.motif = '' emit('filtersUpdate', validFilters) } /** * Select all the values of a filter group. * @param filter - The name of the filter group * @param list - The list of values to select */ function selectAll(filter: keyof Omit<AdvancedFilters, "motif">, list: Readonly<Array<string>>): void { if (filters.value[filter].length === list.length) { filters.value[filter] = [] } else { filters.value[filter] = Array.from(list) } } /** * A marker to know if the values of filter group * are currently all selected. * @param filter - The name of the filter group * @param listLength - The number of values in this group * @return If all the values are currently selected */ function allSelected(filter: keyof AdvancedFilters, valuesNumber: number): boolean { return filters.value[filter].length === valuesNumber } /** * Remove one of the filter chips and re-emit * the filters immediately. * @param filterChip - The data of the filter chip to remove */ function removeFilter(filterChip: FilterChip): void { if (filterChip[0] === 'motif') { filters.value[filterChip[0]] = '' } else { const valueIndex: number = filters.value[filterChip[0]].indexOf(filterChip[1]) filters.value[filterChip[0]].splice(valueIndex, 1) } emit('filtersUpdate', filters.value) } </script> <style scoped> .active-filters { display: flex; flex-flow: row wrap; gap: calc((var(--spacing)) / 2); padding-top: calc((var(--spacing)) / 2); } .filter-chip { border-radius: var(--big-radius); border: 1px solid var(--primary); color: var(--primary); display: flex; align-items: center; padding-left: 8px; } .filter-text { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .filter-chip .remove-button { border: none; color: inherit; cursor: pointer; display: flex; font-weight: bold; padding: 6px; &:active { background-color: transparent; } } .advanced-search { display: flex; } .advanced-search-button { border: none; margin-left: auto; padding-right: 0; } .advanced-search-dialog h2 { margin-bottom: calc(var(--spacing) * 2); margin-top: 0; } .advanced-search-dialog section h3 { align-items: center; display: flex; gap: var(--spacing); } .select-all-button { border-radius: var(--big-radius); padding: 2px 8px; display: flex; align-items: center; gap: 6px; } .all-selected { background-color: var(--primary); border-color: var(--primary); color: var(--black); } .checkbox-list { display: flex; flex-flow: row wrap; gap: var(--spacing); padding-left: var(--spacing); padding-bottom: var(--spacing); } .checkbox { display: flex; transition: color 0.1s; } .advanced-search-dialog footer { align-items: center; display: flex; justify-content: flex-end; margin-top: var(--spacing); } </style>