-
Simon Malesys authoredSimon Malesys authored
AdvancedSearch.vue 8.88 KiB
<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>