summaryrefslogblamecommitdiffstats
path: root/webapp/src/components/DataTableSearch.vue
blob: 17e697639322d1ec2781513ffbe1fcbdf531bcc5 (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"
        :label="$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 }, ...keySelectItems ]"
        item-text="text"
        item-value="key"
        color="primary"
        hide-details
        :menu-props="{
          offsetY: true,
          left: true,
          contentClass: 'data-table-search-select-content',
          maxHeight: 310
        }"
      ></v-select>
      <template v-if="!slim">
        <v-btn v-if="index === 0" icon @click="newSearchField" class="mx-2"><v-icon>add</v-icon></v-btn>
        <v-btn v-else icon @click="removeSearchField(s)" class="mx-2"><v-icon>remove</v-icon></v-btn>
      </template>
    </div>
  </div>
</template>

<script>
import ScrollParent from 'scrollparent'

export default {
  name: 'DataTableSearch',
  props: {
    selected: {
      type: Array,
      default: () => []
    },
    items: {
      type: Array,
      required: true
    },
    dataKeys: {
      type: Array,
      required: true
    },
    nestedData: {
      type: Boolean,
      default: false
    },
    regex: {
      type: Boolean,
      default: false
    },
    caseSensitive: {
      type: Boolean,
      default: false
    },
    onlyShowSelected: {
      type: Boolean,
      default: false
    },
    slim: {
      type: Boolean,
      default: false
    },
    dispatchScroll: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      search: [{ text: { raw: '', upper: '', regex: new RegExp(), regexCI: new RegExp() }, key: null }],
      filteringLoop: { running: false, cancel: () => true }
    }
  },
  computed: {
    keySelectItems () {
      const result = []
      this.dataKeys.forEach(x => {
        let key = x.searchKey || x.key || x
        result.push({ text: x.text || x, key })
      })
      return result
    },
    dataSearchKeys () {
      return this.dataKeys.map(x => x.searchKey || x.key || x)
    },
    filterFunction () {
      var str
      if (this.nestedData) str = (item, key) => String(item.data[key])
      else str = (item, key) => String(item[key])

      if (this.regex && this.caseSensitive) return (s, item, key) => s.text.regex.test(str(item, key))
      else if (this.regex && !this.caseSensitive) return (s, item, key) => s.text.regexCI.test(str(item, key))
      else if (!this.regex && this.caseSensitive) return (s, item, key) => str(item, key).indexOf(s.text.raw) !== -1
      else if (!this.regex && !this.caseSensitive) return (s, item, key) => str(item, key).toUpperCase().indexOf(s.text.upper) !== -1
      return () => false
    }
  },
  watch: {
    selected () {
      this.filterItems()
    },
    items () {
      this.filterItems()
    },
    regex () {
      this.filterItems()
    },
    caseSensitive () {
      this.filterItems()
    },
    onlyShowSelected () {
      this.filterItems()
    },
    search: {
      deep: true,
      handler () {
        this.filterItems()
      }
    }
  },
  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)
      if (this.dispatchScroll) ScrollParent(this.$el).dispatchEvent(new Event('scroll'))
    },
    filterItems () {
      // 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.items))
        return
      }

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

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

.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>

<style>
.data-table-search-select-content .v-list__tile {
  height: 42px;
}
</style>