558 lines
17 KiB
JavaScript
558 lines
17 KiB
JavaScript
|
import "../../../src/components/VDataTable/VDataTable.sass"; // Components
|
||
|
|
||
|
import { VData } from '../VData';
|
||
|
import { VDataFooter, VDataIterator } from '../VDataIterator';
|
||
|
import VBtn from '../VBtn';
|
||
|
import VDataTableHeader from './VDataTableHeader'; // import VVirtualTable from './VVirtualTable'
|
||
|
|
||
|
import VIcon from '../VIcon';
|
||
|
import VProgressLinear from '../VProgressLinear';
|
||
|
import Row from './Row';
|
||
|
import RowGroup from './RowGroup';
|
||
|
import VSimpleCheckbox from '../VCheckbox/VSimpleCheckbox';
|
||
|
import VSimpleTable from './VSimpleTable';
|
||
|
import MobileRow from './MobileRow';
|
||
|
import ripple from '../../directives/ripple'; // Helpers
|
||
|
|
||
|
import { deepEqual, getObjectValueByPath, getPrefixedScopedSlots, getSlot, defaultFilter } from '../../util/helpers';
|
||
|
import { breaking } from '../../util/console';
|
||
|
|
||
|
function filterFn(item, search, filter) {
|
||
|
return header => {
|
||
|
const value = getObjectValueByPath(item, header.value);
|
||
|
return header.filter ? header.filter(value, search, item) : filter(value, search, item);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function searchTableItems(items, search, headersWithCustomFilters, headersWithoutCustomFilters, customFilter) {
|
||
|
let filtered = items;
|
||
|
search = typeof search === 'string' ? search.trim() : null;
|
||
|
|
||
|
if (search && headersWithoutCustomFilters.length) {
|
||
|
filtered = items.filter(item => headersWithoutCustomFilters.some(filterFn(item, search, customFilter)));
|
||
|
}
|
||
|
|
||
|
if (headersWithCustomFilters.length) {
|
||
|
filtered = filtered.filter(item => headersWithCustomFilters.every(filterFn(item, search, defaultFilter)));
|
||
|
}
|
||
|
|
||
|
return filtered;
|
||
|
}
|
||
|
/* @vue/component */
|
||
|
|
||
|
|
||
|
export default VDataIterator.extend({
|
||
|
name: 'v-data-table',
|
||
|
// https://github.com/vuejs/vue/issues/6872
|
||
|
directives: {
|
||
|
ripple
|
||
|
},
|
||
|
props: {
|
||
|
headers: {
|
||
|
type: Array
|
||
|
},
|
||
|
showSelect: Boolean,
|
||
|
showExpand: Boolean,
|
||
|
showGroupBy: Boolean,
|
||
|
// TODO: Fix
|
||
|
// virtualRows: Boolean,
|
||
|
mobileBreakpoint: {
|
||
|
type: Number,
|
||
|
default: 600
|
||
|
},
|
||
|
height: [Number, String],
|
||
|
hideDefaultHeader: Boolean,
|
||
|
caption: String,
|
||
|
dense: Boolean,
|
||
|
headerProps: Object,
|
||
|
calculateWidths: Boolean,
|
||
|
fixedHeader: Boolean,
|
||
|
headersLength: Number,
|
||
|
expandIcon: {
|
||
|
type: String,
|
||
|
default: '$vuetify.icons.expand'
|
||
|
},
|
||
|
customFilter: {
|
||
|
type: Function,
|
||
|
default: defaultFilter
|
||
|
}
|
||
|
},
|
||
|
|
||
|
data() {
|
||
|
return {
|
||
|
internalGroupBy: [],
|
||
|
openCache: {},
|
||
|
widths: []
|
||
|
};
|
||
|
},
|
||
|
|
||
|
computed: {
|
||
|
computedHeaders() {
|
||
|
if (!this.headers) return [];
|
||
|
const headers = this.headers.filter(h => h.value === undefined || !this.internalGroupBy.find(v => v === h.value));
|
||
|
const defaultHeader = {
|
||
|
text: '',
|
||
|
sortable: false,
|
||
|
width: '1px'
|
||
|
};
|
||
|
|
||
|
if (this.showSelect) {
|
||
|
const index = headers.findIndex(h => h.value === 'data-table-select');
|
||
|
if (index < 0) headers.unshift({ ...defaultHeader,
|
||
|
value: 'data-table-select'
|
||
|
});else headers.splice(index, 1, { ...defaultHeader,
|
||
|
...headers[index]
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.showExpand) {
|
||
|
const index = headers.findIndex(h => h.value === 'data-table-expand');
|
||
|
if (index < 0) headers.unshift({ ...defaultHeader,
|
||
|
value: 'data-table-expand'
|
||
|
});else headers.splice(index, 1, { ...defaultHeader,
|
||
|
...headers[index]
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return headers;
|
||
|
},
|
||
|
|
||
|
computedHeadersLength() {
|
||
|
return this.headersLength || this.computedHeaders.length;
|
||
|
},
|
||
|
|
||
|
isMobile() {
|
||
|
return this.$vuetify.breakpoint.width < this.mobileBreakpoint;
|
||
|
},
|
||
|
|
||
|
columnSorters() {
|
||
|
return this.computedHeaders.reduce((acc, header) => {
|
||
|
if (header.sort) acc[header.value] = header.sort;
|
||
|
return acc;
|
||
|
}, {});
|
||
|
},
|
||
|
|
||
|
headersWithCustomFilters() {
|
||
|
return this.computedHeaders.filter(header => header.filter);
|
||
|
},
|
||
|
|
||
|
headersWithoutCustomFilters() {
|
||
|
return this.computedHeaders.filter(header => !header.filter);
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
created() {
|
||
|
const breakingProps = [['sort-icon', 'header-props.sort-icon'], ['hide-headers', 'hide-default-header'], ['select-all', 'show-select']];
|
||
|
/* istanbul ignore next */
|
||
|
|
||
|
breakingProps.forEach(([original, replacement]) => {
|
||
|
if (this.$attrs.hasOwnProperty(original)) breaking(original, replacement, this);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
mounted() {
|
||
|
// if ((!this.sortBy || !this.sortBy.length) && (!this.options.sortBy || !this.options.sortBy.length)) {
|
||
|
// const firstSortable = this.headers.find(h => !('sortable' in h) || !!h.sortable)
|
||
|
// if (firstSortable) this.updateOptions({ sortBy: [firstSortable.value], sortDesc: [false] })
|
||
|
// }
|
||
|
if (this.calculateWidths) {
|
||
|
window.addEventListener('resize', this.calcWidths);
|
||
|
this.calcWidths();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
beforeDestroy() {
|
||
|
if (this.calculateWidths) {
|
||
|
window.removeEventListener('resize', this.calcWidths);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
methods: {
|
||
|
calcWidths() {
|
||
|
this.widths = Array.from(this.$el.querySelectorAll('th')).map(e => e.clientWidth);
|
||
|
},
|
||
|
|
||
|
customFilterWithColumns(items, search) {
|
||
|
return searchTableItems(items, search, this.headersWithCustomFilters, this.headersWithoutCustomFilters, this.customFilter);
|
||
|
},
|
||
|
|
||
|
customSortWithHeaders(items, sortBy, sortDesc, locale) {
|
||
|
return this.customSort(items, sortBy, sortDesc, locale, this.columnSorters);
|
||
|
},
|
||
|
|
||
|
createItemProps(item) {
|
||
|
const props = VDataIterator.options.methods.createItemProps.call(this, item);
|
||
|
return Object.assign(props, {
|
||
|
headers: this.computedHeaders
|
||
|
});
|
||
|
},
|
||
|
|
||
|
genCaption(props) {
|
||
|
if (this.caption) return [this.$createElement('caption', [this.caption])];
|
||
|
return getSlot(this, 'caption', props, true);
|
||
|
},
|
||
|
|
||
|
genColgroup(props) {
|
||
|
return this.$createElement('colgroup', this.computedHeaders.map(header => {
|
||
|
return this.$createElement('col', {
|
||
|
class: {
|
||
|
divider: header.divider
|
||
|
},
|
||
|
style: {
|
||
|
width: header.width
|
||
|
}
|
||
|
});
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
genLoading() {
|
||
|
const progress = this.$slots['progress'] ? this.$slots.progress : this.$createElement(VProgressLinear, {
|
||
|
props: {
|
||
|
color: this.loading === true ? 'primary' : this.loading,
|
||
|
height: 2,
|
||
|
indeterminate: true
|
||
|
}
|
||
|
});
|
||
|
const th = this.$createElement('th', {
|
||
|
staticClass: 'column',
|
||
|
attrs: {
|
||
|
colspan: this.computedHeadersLength
|
||
|
}
|
||
|
}, [progress]);
|
||
|
const tr = this.$createElement('tr', {
|
||
|
staticClass: 'v-data-table__progress'
|
||
|
}, [th]);
|
||
|
return this.$createElement('thead', [tr]);
|
||
|
},
|
||
|
|
||
|
genHeaders(props) {
|
||
|
const data = {
|
||
|
props: { ...this.headerProps,
|
||
|
headers: this.computedHeaders,
|
||
|
options: props.options,
|
||
|
mobile: this.isMobile,
|
||
|
showGroupBy: this.showGroupBy,
|
||
|
someItems: this.someItems,
|
||
|
everyItem: this.everyItem,
|
||
|
singleSelect: this.singleSelect,
|
||
|
disableSort: this.disableSort
|
||
|
},
|
||
|
on: {
|
||
|
sort: props.sort,
|
||
|
group: props.group,
|
||
|
'toggle-select-all': this.toggleSelectAll
|
||
|
}
|
||
|
};
|
||
|
const children = [getSlot(this, 'header', data)];
|
||
|
|
||
|
if (!this.hideDefaultHeader) {
|
||
|
const scopedSlots = getPrefixedScopedSlots('header.', this.$scopedSlots);
|
||
|
children.push(this.$createElement(VDataTableHeader, { ...data,
|
||
|
scopedSlots
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
if (this.loading) children.push(this.genLoading());
|
||
|
return children;
|
||
|
},
|
||
|
|
||
|
genEmptyWrapper(content) {
|
||
|
return this.$createElement('tr', [this.$createElement('td', {
|
||
|
attrs: {
|
||
|
colspan: this.computedHeadersLength
|
||
|
}
|
||
|
}, content)]);
|
||
|
},
|
||
|
|
||
|
genItems(items, props) {
|
||
|
const empty = this.genEmpty(props.pagination.itemsLength);
|
||
|
if (empty) return [empty];
|
||
|
return props.groupedItems ? this.genGroupedRows(props.groupedItems, props) : this.genRows(items, props);
|
||
|
},
|
||
|
|
||
|
genGroupedRows(groupedItems, props) {
|
||
|
const groups = Object.keys(groupedItems || {});
|
||
|
return groups.map(group => {
|
||
|
if (!this.openCache.hasOwnProperty(group)) this.$set(this.openCache, group, true);
|
||
|
|
||
|
if (this.$scopedSlots.group) {
|
||
|
return this.$scopedSlots.group({
|
||
|
group,
|
||
|
options: props.options,
|
||
|
items: groupedItems[group],
|
||
|
headers: this.computedHeaders
|
||
|
});
|
||
|
} else {
|
||
|
return this.genDefaultGroupedRow(group, groupedItems[group], props);
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
genDefaultGroupedRow(group, items, props) {
|
||
|
const isOpen = !!this.openCache[group];
|
||
|
const children = [this.$createElement('template', {
|
||
|
slot: 'row.content'
|
||
|
}, this.genDefaultRows(items, props))];
|
||
|
|
||
|
if (this.$scopedSlots['group.header']) {
|
||
|
children.unshift(this.$createElement('template', {
|
||
|
slot: 'column.header'
|
||
|
}, [this.$scopedSlots['group.header']({
|
||
|
group,
|
||
|
groupBy: props.options.groupBy,
|
||
|
items,
|
||
|
headers: this.computedHeaders
|
||
|
})]));
|
||
|
} else {
|
||
|
const toggle = this.$createElement(VBtn, {
|
||
|
staticClass: 'ma-0',
|
||
|
props: {
|
||
|
icon: true,
|
||
|
small: true
|
||
|
},
|
||
|
on: {
|
||
|
click: () => this.$set(this.openCache, group, !this.openCache[group])
|
||
|
}
|
||
|
}, [this.$createElement(VIcon, [isOpen ? 'remove' : 'add'])]);
|
||
|
const remove = this.$createElement(VBtn, {
|
||
|
staticClass: 'ma-0',
|
||
|
props: {
|
||
|
icon: true,
|
||
|
small: true
|
||
|
},
|
||
|
on: {
|
||
|
click: () => props.updateOptions({
|
||
|
groupBy: [],
|
||
|
groupDesc: []
|
||
|
})
|
||
|
}
|
||
|
}, [this.$createElement(VIcon, ['close'])]);
|
||
|
const column = this.$createElement('td', {
|
||
|
staticClass: 'text-start',
|
||
|
attrs: {
|
||
|
colspan: this.computedHeadersLength
|
||
|
}
|
||
|
}, [toggle, `${props.options.groupBy[0]}: ${group}`, remove]);
|
||
|
children.unshift(this.$createElement('template', {
|
||
|
slot: 'column.header'
|
||
|
}, [column]));
|
||
|
}
|
||
|
|
||
|
if (this.$scopedSlots['group.summary']) {
|
||
|
children.push(this.$createElement('template', {
|
||
|
slot: 'column.summary'
|
||
|
}, [this.$scopedSlots['group.summary']({
|
||
|
group,
|
||
|
groupBy: props.options.groupBy,
|
||
|
items,
|
||
|
headers: this.computedHeaders
|
||
|
})]));
|
||
|
}
|
||
|
|
||
|
return this.$createElement(RowGroup, {
|
||
|
key: group,
|
||
|
props: {
|
||
|
value: isOpen
|
||
|
}
|
||
|
}, children);
|
||
|
},
|
||
|
|
||
|
genRows(items, props) {
|
||
|
return this.$scopedSlots.item ? this.genScopedRows(items, props) : this.genDefaultRows(items, props);
|
||
|
},
|
||
|
|
||
|
genScopedRows(items, props) {
|
||
|
const rows = [];
|
||
|
|
||
|
for (let i = 0; i < items.length; i++) {
|
||
|
const item = items[i];
|
||
|
rows.push(this.$scopedSlots.item(this.createItemProps(item)));
|
||
|
|
||
|
if (this.isExpanded(item)) {
|
||
|
rows.push(this.$scopedSlots['expanded-item']({
|
||
|
item,
|
||
|
headers: this.computedHeaders
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return rows;
|
||
|
},
|
||
|
|
||
|
genDefaultRows(items, props) {
|
||
|
return this.$scopedSlots['expanded-item'] ? items.map(item => this.genDefaultExpandedRow(item)) : items.map(item => this.genDefaultSimpleRow(item));
|
||
|
},
|
||
|
|
||
|
genDefaultExpandedRow(item) {
|
||
|
const isExpanded = this.isExpanded(item);
|
||
|
const headerRow = this.genDefaultSimpleRow(item, isExpanded ? 'expanded expanded__row' : null);
|
||
|
const expandedRow = this.$createElement('tr', {
|
||
|
staticClass: 'expanded expanded__content'
|
||
|
}, [this.$scopedSlots['expanded-item']({
|
||
|
item,
|
||
|
headers: this.computedHeaders
|
||
|
})]);
|
||
|
return this.$createElement(RowGroup, {
|
||
|
props: {
|
||
|
value: isExpanded
|
||
|
}
|
||
|
}, [this.$createElement('template', {
|
||
|
slot: 'row.header'
|
||
|
}, [headerRow]), this.$createElement('template', {
|
||
|
slot: 'row.content'
|
||
|
}, [expandedRow])]);
|
||
|
},
|
||
|
|
||
|
genDefaultSimpleRow(item, classes = null) {
|
||
|
const scopedSlots = getPrefixedScopedSlots('item.', this.$scopedSlots);
|
||
|
const data = this.createItemProps(item);
|
||
|
|
||
|
if (this.showSelect) {
|
||
|
const slot = scopedSlots['data-table-select'];
|
||
|
scopedSlots['data-table-select'] = slot ? () => slot(data) : () => this.$createElement(VSimpleCheckbox, {
|
||
|
staticClass: 'v-data-table__checkbox',
|
||
|
props: {
|
||
|
value: data.isSelected
|
||
|
},
|
||
|
on: {
|
||
|
input: val => data.select(val)
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.showExpand) {
|
||
|
const slot = scopedSlots['data-table-expand'];
|
||
|
scopedSlots['data-table-expand'] = slot ? () => slot(data) : () => this.$createElement(VIcon, {
|
||
|
staticClass: 'v-data-table__expand-icon',
|
||
|
class: {
|
||
|
'v-data-table__expand-icon--active': data.isExpanded
|
||
|
},
|
||
|
on: {
|
||
|
click: e => {
|
||
|
e.stopPropagation();
|
||
|
data.expand(!data.isExpanded);
|
||
|
}
|
||
|
}
|
||
|
}, [this.expandIcon]);
|
||
|
}
|
||
|
|
||
|
return this.$createElement(this.isMobile ? MobileRow : Row, {
|
||
|
key: getObjectValueByPath(item, this.itemKey),
|
||
|
class: classes,
|
||
|
props: {
|
||
|
headers: this.computedHeaders,
|
||
|
item,
|
||
|
rtl: this.$vuetify.rtl
|
||
|
},
|
||
|
scopedSlots,
|
||
|
on: {
|
||
|
click: () => this.$emit('click:row', item)
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
genBody(props) {
|
||
|
const data = { ...props,
|
||
|
isMobile: this.isMobile,
|
||
|
headers: this.computedHeaders
|
||
|
};
|
||
|
|
||
|
if (this.$scopedSlots.body) {
|
||
|
return this.$scopedSlots.body(data);
|
||
|
}
|
||
|
|
||
|
return this.$createElement('tbody', [getSlot(this, 'body.prepend', data, true), this.genItems(props.items, props), getSlot(this, 'body.append', data, true)]);
|
||
|
},
|
||
|
|
||
|
genFooters(props) {
|
||
|
const data = {
|
||
|
props: {
|
||
|
options: props.options,
|
||
|
pagination: props.pagination,
|
||
|
itemsPerPageText: '$vuetify.dataTable.itemsPerPageText',
|
||
|
...this.footerProps
|
||
|
},
|
||
|
on: {
|
||
|
'update:options': value => props.updateOptions(value)
|
||
|
},
|
||
|
widths: this.widths,
|
||
|
headers: this.computedHeaders
|
||
|
};
|
||
|
const children = [getSlot(this, 'footer', data, true)];
|
||
|
|
||
|
if (!this.hideDefaultFooter) {
|
||
|
children.push(this.$createElement(VDataFooter, data));
|
||
|
}
|
||
|
|
||
|
return children;
|
||
|
},
|
||
|
|
||
|
genDefaultScopedSlot(props) {
|
||
|
const simpleProps = {
|
||
|
height: this.height,
|
||
|
fixedHeader: this.fixedHeader,
|
||
|
dense: this.dense
|
||
|
}; // if (this.virtualRows) {
|
||
|
// return this.$createElement(VVirtualTable, {
|
||
|
// props: Object.assign(simpleProps, {
|
||
|
// items: props.items,
|
||
|
// height: this.height,
|
||
|
// rowHeight: this.dense ? 24 : 48,
|
||
|
// headerHeight: this.dense ? 32 : 48,
|
||
|
// // TODO: expose rest of props from virtual table?
|
||
|
// }),
|
||
|
// scopedSlots: {
|
||
|
// items: ({ items }) => this.genItems(items, props) as any,
|
||
|
// },
|
||
|
// }, [
|
||
|
// this.proxySlot('body.before', [this.genCaption(props), this.genHeaders(props)]),
|
||
|
// this.proxySlot('bottom', this.genFooters(props)),
|
||
|
// ])
|
||
|
// }
|
||
|
|
||
|
return this.$createElement(VSimpleTable, {
|
||
|
props: simpleProps
|
||
|
}, [this.proxySlot('top', getSlot(this, 'top', props, true)), this.genCaption(props), this.genColgroup(props), this.genHeaders(props), this.genBody(props), this.proxySlot('bottom', this.genFooters(props))]);
|
||
|
},
|
||
|
|
||
|
proxySlot(slot, content) {
|
||
|
return this.$createElement('template', {
|
||
|
slot
|
||
|
}, content);
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
render() {
|
||
|
return this.$createElement(VData, {
|
||
|
props: { ...this.$props,
|
||
|
customFilter: this.customFilterWithColumns,
|
||
|
customSort: this.customSortWithHeaders
|
||
|
},
|
||
|
on: {
|
||
|
'update:options': (v, old) => {
|
||
|
this.internalGroupBy = v.groupBy || [];
|
||
|
!deepEqual(v, old) && this.$emit('update:options', v);
|
||
|
},
|
||
|
'update:page': v => this.$emit('update:page', v),
|
||
|
'update:items-per-page': v => this.$emit('update:items-per-page', v),
|
||
|
'update:sort-by': v => this.$emit('update:sort-by', v),
|
||
|
'update:sort-desc': v => this.$emit('update:sort-desc', v),
|
||
|
'update:group-by': v => this.$emit('update:group-by', v),
|
||
|
'update:group-desc': v => this.$emit('update:group-desc', v),
|
||
|
pagination: (v, old) => !deepEqual(v, old) && this.$emit('pagination', v),
|
||
|
'current-items': v => {
|
||
|
this.internalCurrentItems = v;
|
||
|
this.$emit('current-items', v);
|
||
|
},
|
||
|
'page-count': v => this.$emit('page-count', v)
|
||
|
},
|
||
|
scopedSlots: {
|
||
|
default: this.genDefaultScopedSlot
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
});
|
||
|
//# sourceMappingURL=VDataTable.js.map
|