summaryrefslogtreecommitdiffstats
path: root/webapp/src/components/DataTableSearch.vue
diff options
context:
space:
mode:
authorUdo Walter2019-02-28 20:26:08 +0100
committerUdo Walter2019-02-28 20:26:08 +0100
commit4dd3c027dc088f74c3bd6f3148b6d94eca8719d3 (patch)
treea4ecc11dcc56afa5a3e3658e12b05649506953bf /webapp/src/components/DataTableSearch.vue
parent[webapp/groups] performance improvements (diff)
downloadbas-4dd3c027dc088f74c3bd6f3148b6d94eca8719d3.tar.gz
bas-4dd3c027dc088f74c3bd6f3148b6d94eca8719d3.tar.xz
bas-4dd3c027dc088f74c3bd6f3148b6d94eca8719d3.zip
[webapp/datatable] improve search performance
Diffstat (limited to 'webapp/src/components/DataTableSearch.vue')
-rw-r--r--webapp/src/components/DataTableSearch.vue203
1 files changed, 203 insertions, 0 deletions
diff --git a/webapp/src/components/DataTableSearch.vue b/webapp/src/components/DataTableSearch.vue
new file mode 100644
index 0000000..aaa2cc3
--- /dev/null
+++ b/webapp/src/components/DataTableSearch.vue
@@ -0,0 +1,203 @@
+<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>