vuetify
Version:
Vue Material Component Framework
315 lines (282 loc) • 9.98 kB
text/typescript
// Components
import { VData } from '../VData'
import VDataFooter from './VDataFooter'
// Mixins
import Mobile from '../../mixins/mobile'
import Themeable from '../../mixins/themeable'
// Helpers
import mixins from '../../util/mixins'
import { deepEqual, getObjectValueByPath, getPrefixedScopedSlots, getSlot, camelizeObjectKeys } from '../../util/helpers'
import { breaking, removed } from '../../util/console'
// Types
import { VNode, VNodeChildren } from 'vue'
import { PropValidator } from 'vue/types/options'
import { DataItemProps, DataScopeProps } from 'vuetify/types'
/* @vue/component */
export default mixins(
Mobile,
Themeable
).extend({
name: 'v-data-iterator',
props: {
...VData.options.props, // TODO: filter out props not used
itemKey: {
type: String,
default: 'id',
},
value: {
type: Array,
default: () => [],
} as PropValidator<any[]>,
singleSelect: Boolean,
expanded: {
type: Array,
default: () => [],
} as PropValidator<any[]>,
mobileBreakpoint: {
...Mobile.options.props.mobileBreakpoint,
default: 600,
},
singleExpand: Boolean,
loading: [Boolean, String],
noResultsText: {
type: String,
default: '$vuetify.dataIterator.noResultsText',
},
noDataText: {
type: String,
default: '$vuetify.noDataText',
},
loadingText: {
type: String,
default: '$vuetify.dataIterator.loadingText',
},
hideDefaultFooter: Boolean,
footerProps: Object,
selectableKey: {
type: String,
default: 'isSelectable',
},
},
data: () => ({
selection: {} as Record<string, any>,
expansion: {} as Record<string, boolean>,
internalCurrentItems: [] as any[],
}),
computed: {
everyItem (): boolean {
return !!this.selectableItems.length && this.selectableItems.every((i: any) => this.isSelected(i))
},
someItems (): boolean {
return this.selectableItems.some((i: any) => this.isSelected(i))
},
sanitizedFooterProps (): Record<string, any> {
return camelizeObjectKeys(this.footerProps)
},
selectableItems (): any[] {
return this.internalCurrentItems.filter(item => this.isSelectable(item))
},
},
watch: {
value: {
handler (value: any[]) {
this.selection = value.reduce((selection, item) => {
selection[getObjectValueByPath(item, this.itemKey)] = item
return selection
}, {})
},
immediate: true,
},
selection (value: Record<string, boolean>, old: Record<string, boolean>) {
if (deepEqual(Object.keys(value), Object.keys(old))) return
this.$emit('input', Object.values(value))
},
expanded: {
handler (value: any[]) {
this.expansion = value.reduce((expansion, item) => {
expansion[getObjectValueByPath(item, this.itemKey)] = true
return expansion
}, {})
},
immediate: true,
},
expansion (value: Record<string, boolean>, old: Record<string, boolean>) {
if (deepEqual(value, old)) return
const keys = Object.keys(value).filter(k => value[k])
const expanded = !keys.length ? [] : this.items.filter(i => keys.includes(String(getObjectValueByPath(i, this.itemKey))))
this.$emit('update:expanded', expanded)
},
},
created () {
const breakingProps = [
['disable-initial-sort', 'sort-by'],
['filter', 'custom-filter'],
['pagination', 'options'],
['total-items', 'server-items-length'],
['hide-actions', 'hide-default-footer'],
['rows-per-page-items', 'footer-props.items-per-page-options'],
['rows-per-page-text', 'footer-props.items-per-page-text'],
['prev-icon', 'footer-props.prev-icon'],
['next-icon', 'footer-props.next-icon'],
]
/* istanbul ignore next */
breakingProps.forEach(([original, replacement]) => {
if (this.$attrs.hasOwnProperty(original)) breaking(original, replacement, this)
})
const removedProps = [
'expand',
'content-class',
'content-props',
'content-tag',
]
/* istanbul ignore next */
removedProps.forEach(prop => {
if (this.$attrs.hasOwnProperty(prop)) removed(prop)
})
},
methods: {
toggleSelectAll (value: boolean): void {
const selection = Object.assign({}, this.selection)
for (let i = 0; i < this.selectableItems.length; i++) {
const item = this.selectableItems[i]
if (!this.isSelectable(item)) continue
const key = getObjectValueByPath(item, this.itemKey)
if (value) selection[key] = item
else delete selection[key]
}
this.selection = selection
this.$emit('toggle-select-all', { items: this.internalCurrentItems, value })
},
isSelectable (item: any): boolean {
return getObjectValueByPath(item, this.selectableKey) !== false
},
isSelected (item: any): boolean {
return !!this.selection[getObjectValueByPath(item, this.itemKey)] || false
},
select (item: any, value = true, emit = true): void {
if (!this.isSelectable(item)) return
const selection = this.singleSelect ? {} : Object.assign({}, this.selection)
const key = getObjectValueByPath(item, this.itemKey)
if (value) selection[key] = item
else delete selection[key]
if (this.singleSelect && emit) {
const keys = Object.keys(this.selection)
const old = keys.length && getObjectValueByPath(this.selection[keys[0]], this.itemKey)
old && old !== key && this.$emit('item-selected', { item: this.selection[old], value: false })
}
this.selection = selection
emit && this.$emit('item-selected', { item, value })
},
isExpanded (item: any): boolean {
return this.expansion[getObjectValueByPath(item, this.itemKey)] || false
},
expand (item: any, value = true): void {
const expansion = this.singleExpand ? {} : Object.assign({}, this.expansion)
const key = getObjectValueByPath(item, this.itemKey)
if (value) expansion[key] = true
else delete expansion[key]
this.expansion = expansion
this.$emit('item-expanded', { item, value })
},
createItemProps (item: any): DataItemProps {
return {
item,
select: (v: boolean) => this.select(item, v),
isSelected: this.isSelected(item),
expand: (v: boolean) => this.expand(item, v),
isExpanded: this.isExpanded(item),
isMobile: this.isMobile,
}
},
genEmptyWrapper (content: VNodeChildren) {
return this.$createElement('div', content)
},
genEmpty (originalItemsLength: number, filteredItemsLength: number) {
if (originalItemsLength === 0 && this.loading) {
const loading = this.$slots.loading || this.$vuetify.lang.t(this.loadingText)
return this.genEmptyWrapper(loading)
} else if (originalItemsLength === 0) {
const noData = this.$slots['no-data'] || this.$vuetify.lang.t(this.noDataText)
return this.genEmptyWrapper(noData)
} else if (filteredItemsLength === 0) {
const noResults = this.$slots['no-results'] || this.$vuetify.lang.t(this.noResultsText)
return this.genEmptyWrapper(noResults)
}
return null
},
genItems (props: DataScopeProps) {
const empty = this.genEmpty(props.originalItemsLength, props.pagination.itemsLength)
if (empty) return [empty]
if (this.$scopedSlots.default) {
return this.$scopedSlots.default({
...props,
isSelected: this.isSelected,
select: this.select,
isExpanded: this.isExpanded,
expand: this.expand,
})
}
if (this.$scopedSlots.item) {
return props.items.map((item: any) => this.$scopedSlots.item!(this.createItemProps(item)))
}
return []
},
genFooter (props: DataScopeProps) {
if (this.hideDefaultFooter) return null
const data = {
props: {
...this.sanitizedFooterProps,
options: props.options,
pagination: props.pagination,
},
on: {
'update:options': (value: any) => props.updateOptions(value),
},
}
const scopedSlots = getPrefixedScopedSlots('footer.', this.$scopedSlots)
return this.$createElement(VDataFooter, {
scopedSlots,
...data,
})
},
genDefaultScopedSlot (props: any) {
const outerProps = {
...props,
someItems: this.someItems,
everyItem: this.everyItem,
toggleSelectAll: this.toggleSelectAll,
}
return this.$createElement('div', {
staticClass: 'v-data-iterator',
}, [
getSlot(this, 'header', outerProps, true),
this.genItems(props),
this.genFooter(props),
getSlot(this, 'footer', outerProps, true),
])
},
},
render (): VNode {
return this.$createElement(VData, {
props: this.$props,
on: {
'update:options': (v: any, old: any) => !deepEqual(v, old) && this.$emit('update:options', v),
'update:page': (v: any) => this.$emit('update:page', v),
'update:items-per-page': (v: any) => this.$emit('update:items-per-page', v),
'update:sort-by': (v: any) => this.$emit('update:sort-by', v),
'update:sort-desc': (v: any) => this.$emit('update:sort-desc', v),
'update:group-by': (v: any) => this.$emit('update:group-by', v),
'update:group-desc': (v: any) => this.$emit('update:group-desc', v),
pagination: (v: any, old: any) => !deepEqual(v, old) && this.$emit('pagination', v),
'current-items': (v: any[]) => {
this.internalCurrentItems = v
this.$emit('current-items', v)
},
'page-count': (v: number) => this.$emit('page-count', v),
},
scopedSlots: {
default: this.genDefaultScopedSlot,
},
})
},
})