summaryrefslogblamecommitdiffstats
path: root/webapp/src/components/DataTableSearch.vue
blob: aaa2cc36234cb6a55c29a84591165e49b66b0fa4 (plain) (tree)










































































































































































































                                                                                                                    
<i18n>
{
  "en": {
    "search": "Search",
    "all": "All"
  },
  "de": {
    "search": "Suche",
    "all": "Alle"
  }
}
</i18n>

<template>
  <div class="search-field-wrapper">
    <div v-for="(s, index) in search" :key="index">
      <v-text-field
        color="primary"
        solo flat
        style="padding: 0: margin: 0;"
        class="search-field"
        :placeholder="$t('search')"
        :value="s.text.raw"
        @input="prerocessSearch(s, $event)"
        hide-details
        prepend-inner-icon="search"
        clearable
      ></v-text-field>
      <v-select
        v-if="!slim"
        solo flat
        class="column-select"
        v-model="s.key"
        :items="[ { text: $t('all'), key: null }, ...searchableHeaders ]"
        item-text="text"
        item-value="key"
        color="primary"
        hide-details
        :menu-props="{
          offsetY: true,
          left: true
        }"
      ></v-select>
      <template v-if="!slim">
        <v-btn v-if="index === 0" icon @click="newSearchField"><v-icon>add</v-icon></v-btn>
        <v-btn v-else icon @click="removeSearchField(s)"><v-icon>remove</v-icon></v-btn>
      </template>
    </div>
  </div>
</template>

<script>

export default {
  name: 'DataTableSearch',
  props: {
    rows: {
      type: Array
    },
    headers: {
      type: Array
    },
    regex: {
      type: Boolean,
      default: false
    },
    caseSensitive: {
      type: Boolean,
      default: false
    },
    onlyShowSelected: {
      type: Boolean,
      default: false
    },
    slim: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      search: [{ text: { raw: '', upper: '', regex: new RegExp(), regexCI: new RegExp() }, key: null }],
      filteringLoop: { running: false, cancel: () => true }
    }
  },
  computed: {
    searchableHeaders () {
      return this.headers.filter(h => h.text !== undefined)
    },
    dataKeys () { return this.headers.map(x => x.key) },
    filterFunction () {
      if (this.regex && this.caseSensitive) return (s, str) => s.text.regex.test(str)
      else if (this.regex && !this.caseSensitive) return (s, str) => s.text.regexCI.test(str)
      else if (!this.regex && this.caseSensitive) return (s, str) => str.indexOf(s.text.raw) !== -1
      else if (!this.regex && !this.caseSensitive) return (s, str) => str.toUpperCase().indexOf(s.text.upper) !== -1
    }
  },
  watch: {
    rows () {
      this.filterRows()
    },
    regex () {
      this.filterRows()
    },
    caseSensitive () {
      this.filterRows()
    },
    onlyShowSelected () {
      this.filterRows()
    },
    search: {
      deep: true,
      handler () {
        this.filterRows()
      }
    }
  },
  methods: {
    resetData () {
      Object.assign(this.$data, this.$options.data.call(this))
    },
    prerocessSearch (s, raw) {
      if (typeof raw !== 'string') raw = ''
      const tmp = { raw, upper: raw.toUpperCase() }
      try {
        tmp.regex = new RegExp(raw)
        tmp.regexCI = new RegExp(raw, 'i')
      } catch (e) {
        tmp.regex = { test: () => false }
        tmp.regexCI = { test: () => false }
      }
      s.text = tmp
    },
    newSearchField () {
      this.search.push({ text: {
        raw: '',
        upper: '',
        regex: new RegExp(),
        regexCI: new RegExp()
      },
      key: null })
    },
    removeSearchField (s) {
      this.search.splice(this.search.indexOf(s), 1)
      window.dispatchEvent(new Event('scroll'))
    },
    filterRows () {
      // Cancel the last filtering loop
      this.filteringLoop.cancel()

      // Skip filtering if all search strings are empty
      if (this.search.every(s => s.text.raw === '') && !this.onlyShowSelected) {
        this.$emit('filter', Object.freeze(this.rows))
        return
      }

      // Filter rows using a time slicing loop
      const result = []
      this.filteringLoop = this.$loop(this.rows.length, 1000,
        i => {
          const row = this.rows[i]
          if ((!this.onlyShowSelected || (this.onlyShowSelected && row.selected)) && this.search.every(s => {
            if (s.key === null) {
              return this.dataKeys.some(key => {
                return this.filterFunction(s, String(row.data[key]))
              })
            } else {
              return this.filterFunction(s, String(row.data[s.key]))
            }
          })) result.push(row)
        },
        () => { this.$emit('filter', Object.freeze(result)) }
      )
    }
  },
  created () {
    this.filterRows()
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.search-field-wrapper {
  display: flex;
  flex-direction: column;
}

.search-field-wrapper > div {
  display: flex;
  align-items: center;
}

.search-field {
  margin: 0;
  padding: 0;
  font-family: 'Roboto Mono';
}

.column-select {
  flex: 0 1 0
}
</style>