vuetifyjs / vuetify
1 1
import './VDataTable.sass'
2

3
// Types
4
import { VNode, VNodeChildrenArrayContents, VNodeChildren } from 'vue'
5
import { PropValidator } from 'vue/types/options'
6
import {
7
  DataTableHeader,
8
  DataTableFilterFunction,
9
  DataScopeProps,
10
  DataOptions,
11
  DataPagination,
12
  DataTableCompareFunction,
13
  DataItemsPerPageOption,
14
  ItemGroup,
15
  RowClassFunction,
16
  DataTableItemProps,
17
} from 'vuetify/types'
18

19
// Components
20 1
import { VData } from '../VData'
21 1
import { VDataFooter, VDataIterator } from '../VDataIterator'
22 1
import VBtn from '../VBtn'
23 1
import VDataTableHeader from './VDataTableHeader'
24
// import VVirtualTable from './VVirtualTable'
25 1
import VIcon from '../VIcon'
26 1
import Row from './Row'
27 1
import RowGroup from './RowGroup'
28 1
import VSimpleCheckbox from '../VCheckbox/VSimpleCheckbox'
29 1
import VSimpleTable from './VSimpleTable'
30 1
import MobileRow from './MobileRow'
31

32
// Mixins
33 1
import Loadable from '../../mixins/loadable'
34

35
// Directives
36 1
import ripple from '../../directives/ripple'
37

38
// Helpers
39 1
import mixins from '../../util/mixins'
40 1
import { deepEqual, getObjectValueByPath, getPrefixedScopedSlots, getSlot, defaultFilter, camelizeObjectKeys, getPropertyFromItem } from '../../util/helpers'
41 1
import { breaking } from '../../util/console'
42 1
import { mergeClasses } from '../../util/mergeData'
43

44 1
function filterFn (item: any, search: string | null, filter: DataTableFilterFunction) {
45 1
  return (header: DataTableHeader) => {
46 1
    const value = getObjectValueByPath(item, header.value)
47 1
    return header.filter ? header.filter(value, search, item) : filter(value, search, item)
48
  }
49
}
50

51
function searchTableItems (
52
  items: any[],
53
  search: string | null,
54
  headersWithCustomFilters: DataTableHeader[],
55
  headersWithoutCustomFilters: DataTableHeader[],
56 1
  customFilter: DataTableFilterFunction
57
) {
58 1
  search = typeof search === 'string' ? search.trim() : null
59

60 1
  return items.filter(item => {
61
    // Headers with custom filters are evaluated whether or not a search term has been provided.
62
    // We need to match every filter to be included in the results.
63 1
    const matchesColumnFilters = headersWithCustomFilters.every(filterFn(item, search, defaultFilter))
64

65
    // Headers without custom filters are only filtered by the `search` property if it is defined.
66
    // We only need a single column to match the search term to be included in the results.
67 1
    const matchesSearchTerm = !search || headersWithoutCustomFilters.some(filterFn(item, search, customFilter))
68

69 1
    return matchesColumnFilters && matchesSearchTerm
70
  })
71
}
72

73
/* @vue/component */
74 1
export default mixins(
75
  VDataIterator,
76
  Loadable,
77
).extend({
78
  name: 'v-data-table',
79

80
  // https://github.com/vuejs/vue/issues/6872
81
  directives: {
82
    ripple,
83
  },
84

85
  props: {
86
    headers: {
87
      type: Array,
88 1
      default: () => [],
89
    } as PropValidator<DataTableHeader[]>,
90
    showSelect: Boolean,
91
    showExpand: Boolean,
92
    showGroupBy: Boolean,
93
    // TODO: Fix
94
    // virtualRows: Boolean,
95
    height: [Number, String],
96
    hideDefaultHeader: Boolean,
97
    caption: String,
98
    dense: Boolean,
99
    headerProps: Object,
100
    calculateWidths: Boolean,
101
    fixedHeader: Boolean,
102
    headersLength: Number,
103
    expandIcon: {
104
      type: String,
105
      default: '$expand',
106
    },
107
    customFilter: {
108
      type: Function,
109
      default: defaultFilter,
110
    } as PropValidator<typeof defaultFilter>,
111
    itemClass: {
112
      type: [String, Function],
113 1
      default: () => '',
114
    } as PropValidator<RowClassFunction | string>,
115
    loaderHeight: {
116
      type: [Number, String],
117
      default: 4,
118
    },
119
  },
120

121 1
  data () {
122 1
    return {
123
      internalGroupBy: [] as string[],
124
      openCache: {} as { [key: string]: boolean },
125
      widths: [] as number[],
126
    }
127
  },
128

129
  computed: {
130 1
    computedHeaders (): DataTableHeader[] {
131 1
      if (!this.headers) return []
132 1
      const headers = this.headers.filter(h => h.value === undefined || !this.internalGroupBy.find(v => v === h.value))
133 1
      const defaultHeader = { text: '', sortable: false, width: '1px' }
134

135 1
      if (this.showSelect) {
136 1
        const index = headers.findIndex(h => h.value === 'data-table-select')
137 1
        if (index < 0) headers.unshift({ ...defaultHeader, value: 'data-table-select' })
138 0
        else headers.splice(index, 1, { ...defaultHeader, ...headers[index] })
139
      }
140

141 1
      if (this.showExpand) {
142 1
        const index = headers.findIndex(h => h.value === 'data-table-expand')
143 1
        if (index < 0) headers.unshift({ ...defaultHeader, value: 'data-table-expand' })
144 0
        else headers.splice(index, 1, { ...defaultHeader, ...headers[index] })
145
      }
146

147 1
      return headers
148
    },
149 1
    colspanAttrs (): object | undefined {
150 1
      return this.isMobile ? undefined : {
151 1
        colspan: this.headersLength || this.computedHeaders.length,
152
      }
153
    },
154 1
    columnSorters (): Record<string, DataTableCompareFunction> {
155 1
      return this.computedHeaders.reduce<Record<string, DataTableCompareFunction>>((acc, header) => {
156 1
        if (header.sort) acc[header.value] = header.sort
157 1
        return acc
158
      }, {})
159
    },
160 1
    headersWithCustomFilters (): DataTableHeader[] {
161 1
      return this.headers.filter(header => header.filter && (!header.hasOwnProperty('filterable') || header.filterable === true))
162
    },
163 1
    headersWithoutCustomFilters (): DataTableHeader[] {
164 1
      return this.headers.filter(header => !header.filter && (!header.hasOwnProperty('filterable') || header.filterable === true))
165
    },
166 1
    sanitizedHeaderProps (): Record<string, any> {
167 1
      return camelizeObjectKeys(this.headerProps)
168
    },
169 1
    computedItemsPerPage (): number {
170 1
      const itemsPerPage = this.options && this.options.itemsPerPage ? this.options.itemsPerPage : this.itemsPerPage
171 1
      const itemsPerPageOptions: DataItemsPerPageOption[] | undefined = this.sanitizedFooterProps.itemsPerPageOptions
172

173 1
      if (
174 1
        itemsPerPageOptions &&
175 1
        !itemsPerPageOptions.find(item => typeof item === 'number' ? item === itemsPerPage : item.value === itemsPerPage)
176
      ) {
177 1
        const firstOption = itemsPerPageOptions[0]
178 1
        return typeof firstOption === 'object' ? firstOption.value : firstOption
179
      }
180

181 1
      return itemsPerPage
182
    },
183
  },
184

185 1
  created () {
186 1
    const breakingProps = [
187
      ['sort-icon', 'header-props.sort-icon'],
188
      ['hide-headers', 'hide-default-header'],
189
      ['select-all', 'show-select'],
190
    ]
191

192
    /* istanbul ignore next */
193
    breakingProps.forEach(([original, replacement]) => {
194
      if (this.$attrs.hasOwnProperty(original)) breaking(original, replacement, this)
195
    })
196
  },
197

198 1
  mounted () {
199
    // if ((!this.sortBy || !this.sortBy.length) && (!this.options.sortBy || !this.options.sortBy.length)) {
200
    //   const firstSortable = this.headers.find(h => !('sortable' in h) || !!h.sortable)
201
    //   if (firstSortable) this.updateOptions({ sortBy: [firstSortable.value], sortDesc: [false] })
202
    // }
203

204 1
    if (this.calculateWidths) {
205 0
      window.addEventListener('resize', this.calcWidths)
206 0
      this.calcWidths()
207
    }
208
  },
209

210 0
  beforeDestroy () {
211 1
    if (this.calculateWidths) {
212 0
      window.removeEventListener('resize', this.calcWidths)
213
    }
214
  },
215

216
  methods: {
217 0
    calcWidths () {
218 0
      this.widths = Array.from(this.$el.querySelectorAll('th')).map(e => e.clientWidth)
219
    },
220 1
    customFilterWithColumns (items: any[], search: string) {
221 1
      return searchTableItems(items, search, this.headersWithCustomFilters, this.headersWithoutCustomFilters, this.customFilter)
222
    },
223 1
    customSortWithHeaders (items: any[], sortBy: string[], sortDesc: boolean[], locale: string) {
224 1
      return this.customSort(items, sortBy, sortDesc, locale, this.columnSorters)
225
    },
226 1
    createItemProps (item: any): DataTableItemProps {
227 1
      const props = VDataIterator.options.methods.createItemProps.call(this, item)
228

229 1
      return Object.assign(props, { headers: this.computedHeaders })
230
    },
231 1
    genCaption (props: DataScopeProps) {
232 1
      if (this.caption) return [this.$createElement('caption', [this.caption])]
233

234 1
      return getSlot(this, 'caption', props, true)
235
    },
236 1
    genColgroup (props: DataScopeProps) {
237 1
      return this.$createElement('colgroup', this.computedHeaders.map(header => {
238 1
        return this.$createElement('col', {
239
          class: {
240
            divider: header.divider,
241
          },
242
        })
243
      }))
244
    },
245 1
    genLoading () {
246 1
      const th = this.$createElement('th', {
247
        staticClass: 'column',
248
        attrs: this.colspanAttrs,
249
      }, [this.genProgress()])
250

251 1
      const tr = this.$createElement('tr', {
252
        staticClass: 'v-data-table__progress',
253
      }, [th])
254

255 1
      return this.$createElement('thead', [tr])
256
    },
257 1
    genHeaders (props: DataScopeProps) {
258 1
      const data = {
259
        props: {
260
          ...this.sanitizedHeaderProps,
261
          headers: this.computedHeaders,
262
          options: props.options,
263
          mobile: this.isMobile,
264
          showGroupBy: this.showGroupBy,
265
          someItems: this.someItems,
266
          everyItem: this.everyItem,
267
          singleSelect: this.singleSelect,
268
          disableSort: this.disableSort,
269
        },
270
        on: {
271
          sort: props.sort,
272
          group: props.group,
273
          'toggle-select-all': this.toggleSelectAll,
274
        },
275
      }
276

277 1
      const children: VNodeChildrenArrayContents = [getSlot(this, 'header', data)]
278

279 1
      if (!this.hideDefaultHeader) {
280 1
        const scopedSlots = getPrefixedScopedSlots('header.', this.$scopedSlots)
281 1
        children.push(this.$createElement(VDataTableHeader, {
282
          ...data,
283
          scopedSlots,
284
        }))
285
      }
286

287 1
      if (this.loading) children.push(this.genLoading())
288

289 1
      return children
290
    },
291 1
    genEmptyWrapper (content: VNodeChildrenArrayContents) {
292 1
      return this.$createElement('tr', {
293
        staticClass: 'v-data-table__empty-wrapper',
294
      }, [
295
        this.$createElement('td', {
296
          attrs: this.colspanAttrs,
297
        }, content),
298
      ])
299
    },
300 1
    genItems (items: any[], props: DataScopeProps) {
301 1
      const empty = this.genEmpty(props.originalItemsLength, props.pagination.itemsLength)
302 1
      if (empty) return [empty]
303

304 1
      return props.groupedItems
305 1
        ? this.genGroupedRows(props.groupedItems, props)
306 1
        : this.genRows(items, props)
307
    },
308 1
    genGroupedRows (groupedItems: ItemGroup<any>[], props: DataScopeProps) {
309 1
      return groupedItems.map(group => {
310 1
        if (!this.openCache.hasOwnProperty(group.name)) this.$set(this.openCache, group.name, true)
311

312 1
        if (this.$scopedSlots.group) {
313 1
          return this.$scopedSlots.group({
314
            group: group.name,
315
            options: props.options,
316
            items: group.items,
317
            headers: this.computedHeaders,
318
          })
319
        } else {
320 1
          return this.genDefaultGroupedRow(group.name, group.items, props)
321
        }
322
      })
323
    },
324 1
    genDefaultGroupedRow (group: string, items: any[], props: DataScopeProps) {
325 1
      const isOpen = !!this.openCache[group]
326 1
      const children: VNodeChildren = [
327
        this.$createElement('template', { slot: 'row.content' }, this.genRows(items, props)),
328
      ]
329 1
      const toggleFn = () => this.$set(this.openCache, group, !this.openCache[group])
330 1
      const removeFn = () => props.updateOptions({ groupBy: [], groupDesc: [] })
331

332 1
      if (this.$scopedSlots['group.header']) {
333 0
        children.unshift(this.$createElement('template', { slot: 'column.header' }, [
334
          this.$scopedSlots['group.header']!({ group, groupBy: props.options.groupBy, items, headers: this.computedHeaders, isOpen, toggle: toggleFn, remove: removeFn }),
335
        ]))
336
      } else {
337 1
        const toggle = this.$createElement(VBtn, {
338
          staticClass: 'ma-0',
339
          props: {
340
            icon: true,
341
            small: true,
342
          },
343
          on: {
344
            click: toggleFn,
345
          },
346 1
        }, [this.$createElement(VIcon, [isOpen ? '$minus' : '$plus'])])
347

348 1
        const remove = this.$createElement(VBtn, {
349
          staticClass: 'ma-0',
350
          props: {
351
            icon: true,
352
            small: true,
353
          },
354
          on: {
355
            click: removeFn,
356
          },
357
        }, [this.$createElement(VIcon, ['$close'])])
358

359 1
        const column = this.$createElement('td', {
360
          staticClass: 'text-start',
361
          attrs: this.colspanAttrs,
362
        }, [toggle, `${props.options.groupBy[0]}: ${group}`, remove])
363

364 1
        children.unshift(this.$createElement('template', { slot: 'column.header' }, [column]))
365
      }
366

367 1
      if (this.$scopedSlots['group.summary']) {
368 1
        children.push(this.$createElement('template', { slot: 'column.summary' }, [
369
          this.$scopedSlots['group.summary']!({ group, groupBy: props.options.groupBy, items, headers: this.computedHeaders, isOpen, toggle: toggleFn }),
370
        ]))
371
      }
372

373 1
      return this.$createElement(RowGroup, {
374
        key: group,
375
        props: {
376
          value: isOpen,
377
        },
378
      }, children)
379
    },
380 1
    genRows (items: any[], props: DataScopeProps) {
381 1
      return this.$scopedSlots.item ? this.genScopedRows(items, props) : this.genDefaultRows(items, props)
382
    },
383 1
    genScopedRows (items: any[], props: DataScopeProps) {
384 1
      const rows = []
385

386 1
      for (let i = 0; i < items.length; i++) {
387 1
        const item = items[i]
388 1
        rows.push(this.$scopedSlots.item!({
389
          ...this.createItemProps(item),
390
          index: i,
391
        }))
392

393 1
        if (this.isExpanded(item)) {
394 0
          rows.push(this.$scopedSlots['expanded-item']!({ item, headers: this.computedHeaders }))
395
        }
396
      }
397

398 1
      return rows
399
    },
400 1
    genDefaultRows (items: any[], props: DataScopeProps) {
401 1
      return this.$scopedSlots['expanded-item']
402 1
        ? items.map(item => this.genDefaultExpandedRow(item))
403 1
        : items.map(item => this.genDefaultSimpleRow(item))
404
    },
405 1
    genDefaultExpandedRow (item: any): VNode {
406 1
      const isExpanded = this.isExpanded(item)
407 1
      const classes = {
408
        'v-data-table__expanded v-data-table__expanded__row': isExpanded,
409
      }
410 1
      const headerRow = this.genDefaultSimpleRow(item, classes)
411 1
      const expandedRow = this.$createElement('tr', {
412
        staticClass: 'v-data-table__expanded v-data-table__expanded__content',
413
      }, [this.$scopedSlots['expanded-item']!({ item, headers: this.computedHeaders })])
414

415 1
      return this.$createElement(RowGroup, {
416
        props: {
417
          value: isExpanded,
418
        },
419
      }, [
420
        this.$createElement('template', { slot: 'row.header' }, [headerRow]),
421
        this.$createElement('template', { slot: 'row.content' }, [expandedRow]),
422
      ])
423
    },
424 1
    genDefaultSimpleRow (item: any, classes: Record<string, boolean> = {}): VNode {
425 1
      const scopedSlots = getPrefixedScopedSlots('item.', this.$scopedSlots)
426

427 1
      const data = this.createItemProps(item)
428

429 1
      if (this.showSelect) {
430 1
        const slot = scopedSlots['data-table-select']
431 1
        scopedSlots['data-table-select'] = slot ? () => slot(data) : () => this.$createElement(VSimpleCheckbox, {
432
          staticClass: 'v-data-table__checkbox',
433
          props: {
434
            value: data.isSelected,
435
            disabled: !this.isSelectable(item),
436
          },
437
          on: {
438 1
            input: (val: boolean) => data.select(val),
439
          },
440
        })
441
      }
442

443 1
      if (this.showExpand) {
444 1
        const slot = scopedSlots['data-table-expand']
445 1
        scopedSlots['data-table-expand'] = slot ? () => slot(data) : () => this.$createElement(VIcon, {
446
          staticClass: 'v-data-table__expand-icon',
447
          class: {
448
            'v-data-table__expand-icon--active': data.isExpanded,
449
          },
450
          on: {
451 1
            click: (e: MouseEvent) => {
452 1
              e.stopPropagation()
453 1
              data.expand(!data.isExpanded)
454
            },
455
          },
456
        }, [this.expandIcon])
457
      }
458

459 1
      return this.$createElement(this.isMobile ? MobileRow : Row, {
460
        key: getObjectValueByPath(item, this.itemKey),
461
        class: mergeClasses(
462
          { ...classes, 'v-data-table__selected': data.isSelected },
463
          getPropertyFromItem(item, this.itemClass)
464
        ),
465
        props: {
466
          headers: this.computedHeaders,
467
          hideDefaultHeader: this.hideDefaultHeader,
468
          item,
469
          rtl: this.$vuetify.rtl,
470
        },
471
        scopedSlots,
472
        on: {
473
          // TODO: for click, the first argument should be the event, and the second argument should be data,
474
          // but this is a breaking change so it's for v3
475 1
          click: () => this.$emit('click:row', item, data),
476 1
          contextmenu: (event: MouseEvent) => this.$emit('contextmenu:row', event, data),
477 1
          dblclick: (event: MouseEvent) => this.$emit('dblclick:row', event, data),
478
        },
479
      })
480
    },
481 1
    genBody (props: DataScopeProps): VNode | string | VNodeChildren {
482 1
      const data = {
483
        ...props,
484
        expand: this.expand,
485
        headers: this.computedHeaders,
486
        isExpanded: this.isExpanded,
487
        isMobile: this.isMobile,
488
        isSelected: this.isSelected,
489
        select: this.select,
490
      }
491

492 1
      if (this.$scopedSlots.body) {
493 1
        return this.$scopedSlots.body!(data)
494
      }
495

496 1
      return this.$createElement('tbody', [
497
        getSlot(this, 'body.prepend', data, true),
498
        this.genItems(props.items, props),
499
        getSlot(this, 'body.append', data, true),
500
      ])
501
    },
502 1
    genFooters (props: DataScopeProps) {
503 1
      const data = {
504
        props: {
505
          options: props.options,
506
          pagination: props.pagination,
507
          itemsPerPageText: '$vuetify.dataTable.itemsPerPageText',
508
          ...this.sanitizedFooterProps,
509
        },
510
        on: {
511 0
          'update:options': (value: any) => props.updateOptions(value),
512
        },
513
        widths: this.widths,
514
        headers: this.computedHeaders,
515
      }
516

517 1
      const children: VNodeChildren = [
518
        getSlot(this, 'footer', data, true),
519
      ]
520

521 1
      if (!this.hideDefaultFooter) {
522 1
        children.push(this.$createElement(VDataFooter, {
523
          ...data,
524
          scopedSlots: getPrefixedScopedSlots('footer.', this.$scopedSlots),
525
        }))
526
      }
527

528 1
      return children
529
    },
530 1
    genDefaultScopedSlot (props: DataScopeProps): VNode {
531 1
      const simpleProps = {
532
        height: this.height,
533
        fixedHeader: this.fixedHeader,
534
        dense: this.dense,
535
      }
536

537
      // if (this.virtualRows) {
538
      //   return this.$createElement(VVirtualTable, {
539
      //     props: Object.assign(simpleProps, {
540
      //       items: props.items,
541
      //       height: this.height,
542
      //       rowHeight: this.dense ? 24 : 48,
543
      //       headerHeight: this.dense ? 32 : 48,
544
      //       // TODO: expose rest of props from virtual table?
545
      //     }),
546
      //     scopedSlots: {
547
      //       items: ({ items }) => this.genItems(items, props) as any,
548
      //     },
549
      //   }, [
550
      //     this.proxySlot('body.before', [this.genCaption(props), this.genHeaders(props)]),
551
      //     this.proxySlot('bottom', this.genFooters(props)),
552
      //   ])
553
      // }
554

555 1
      return this.$createElement(VSimpleTable, {
556
        props: simpleProps,
557
      }, [
558
        this.proxySlot('top', getSlot(this, 'top', props, true)),
559
        this.genCaption(props),
560
        this.genColgroup(props),
561
        this.genHeaders(props),
562
        this.genBody(props),
563
        this.proxySlot('bottom', this.genFooters(props)),
564
      ])
565
    },
566 1
    proxySlot (slot: string, content: VNodeChildren) {
567 1
      return this.$createElement('template', { slot }, content)
568
    },
569
  },
570

571 1
  render (): VNode {
572 1
    return this.$createElement(VData, {
573
      props: {
574
        ...this.$props,
575
        customFilter: this.customFilterWithColumns,
576
        customSort: this.customSortWithHeaders,
577
        itemsPerPage: this.computedItemsPerPage,
578
      },
579
      on: {
580 1
        'update:options': (v: DataOptions, old: DataOptions) => {
581 1
          this.internalGroupBy = v.groupBy || []
582 1
          !deepEqual(v, old) && this.$emit('update:options', v)
583
        },
584 1
        'update:page': (v: number) => this.$emit('update:page', v),
585 0
        'update:items-per-page': (v: number) => this.$emit('update:items-per-page', v),
586 0
        'update:sort-by': (v: string | string[]) => this.$emit('update:sort-by', v),
587 1
        'update:sort-desc': (v: boolean | boolean[]) => this.$emit('update:sort-desc', v),
588 0
        'update:group-by': (v: string | string[]) => this.$emit('update:group-by', v),
589 0
        'update:group-desc': (v: boolean | boolean[]) => this.$emit('update:group-desc', v),
590 1
        pagination: (v: DataPagination, old: DataPagination) => !deepEqual(v, old) && this.$emit('pagination', v),
591 1
        'current-items': (v: any[]) => {
592 1
          this.internalCurrentItems = v
593 1
          this.$emit('current-items', v)
594
        },
595 1
        'page-count': (v: number) => this.$emit('page-count', v),
596
      },
597
      scopedSlots: {
598
        default: this.genDefaultScopedSlot as any,
599
      },
600
    })
601
  },
602
})

Read our documentation on viewing source code .

Loading