Skip to content
Snippets Groups Projects
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>