UNPKG

@ishitatsuyuki/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

1,375 lines (1,356 loc) 69.3 kB
'use strict'; var vue = require('vue'); var helpers = require('./helpers.js'); var config = require('./config.js'); var BaseComponentMixin = require('./BaseComponentMixin-a03c02e3.js'); var Icon = require('./Icon-172f9998.js'); var Input = require('./Input-7b380647.js'); var Button = require('./Button-3722f128.js'); var Checkbox = require('./Checkbox-3a9023a6.js'); var MatchMediaMixin = require('./MatchMediaMixin-08658b15.js'); var Field = require('./Field-b201302d.js'); var Select = require('./Select-db9aed9e.js'); var Loading = require('./Loading-c1d90132.js'); var Pagination = require('./Pagination-e5cf93db.js'); var SlotComponent = require('./SlotComponent-b10f11e8.js'); var script = vue.defineComponent({ name: 'OTableMobileSort', components: { [Button.script.name]: Button.script, [Select.script.name]: Select.script, [Icon.script.name]: Icon.script, [Field.script.name]: Field.script }, inject: ['$table'], emits: ['sort'], props: { currentSortColumn: Object, columns: Array, placeholder: String, iconPack: String, sortIcon: { type: String, default: 'arrow-up' }, sortIconSize: { type: String, default: 'small' }, isAsc: Boolean }, data() { return { mobileSort: helpers.getValueByPath(this.currentSortColumn, 'newKey'), defaultEvent: { shiftKey: true, altKey: true, ctrlKey: true }, ignoreSort: false }; }, computed: { showPlaceholder() { return !this.columns || !this.columns.some((column) => (helpers.getValueByPath(column, 'newKey') === this.mobileSort)); }, sortableColumns() { if (!this.columns) return []; return this.columns.filter(c => c.sortable); }, isCurrentSort() { return helpers.getValueByPath(this.currentSortColumn, 'newKey') === this.mobileSort; } }, watch: { mobileSort(value) { if (this.currentSortColumn.newKey === value) return; const column = this.sortableColumns.filter(c => (helpers.getValueByPath(c, 'newKey') === value))[0]; this.$emit('sort', column, this.defaultEvent); }, currentSortColumn(column) { this.mobileSort = helpers.getValueByPath(column, 'newKey'); } }, methods: { sort() { const column = this.sortableColumns.filter(c => (helpers.getValueByPath(c, 'newKey') === this.mobileSort))[0]; this.$emit('sort', column, this.defaultEvent); } } }); function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_o_select = vue.resolveComponent("o-select"); const _component_o_icon = vue.resolveComponent("o-icon"); const _component_o_button = vue.resolveComponent("o-button"); const _component_o_field = vue.resolveComponent("o-field"); return vue.openBlock(), vue.createBlock("div", { class: _ctx.$table.mobileSortClasses }, [vue.createVNode(_component_o_field, null, { default: vue.withCtx(() => [vue.createVNode(_component_o_select, { modelValue: _ctx.mobileSort, "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => _ctx.mobileSort = $event), expanded: "" }, { default: vue.withCtx(() => [_ctx.placeholder ? vue.withDirectives((vue.openBlock(), vue.createBlock("option", { key: 0, value: {}, selected: "", disabled: "", hidden: "" }, vue.toDisplayString(_ctx.placeholder), 513 /* TEXT, NEED_PATCH */ )), [[vue.vShow, _ctx.showPlaceholder]]) : vue.createCommentVNode("v-if", true), (vue.openBlock(true), vue.createBlock(vue.Fragment, null, vue.renderList(_ctx.sortableColumns, (column, index) => { return vue.openBlock(), vue.createBlock("option", { key: index, value: column.newKey }, vue.toDisplayString(column.label), 9 /* TEXT, PROPS */ , ["value"]); }), 128 /* KEYED_FRAGMENT */ ))]), _: 1 }, 8 /* PROPS */ , ["modelValue"]), vue.createVNode(_component_o_button, { onClick: _ctx.sort }, { default: vue.withCtx(() => [vue.withDirectives(vue.createVNode(_component_o_icon, { icon: _ctx.sortIcon, pack: _ctx.iconPack, size: _ctx.sortIconSize, both: "", rotation: !_ctx.isAsc ? 180 : 0 }, null, 8 /* PROPS */ , ["icon", "pack", "size", "rotation"]), [[vue.vShow, _ctx.isCurrentSort]])]), _: 1 }, 8 /* PROPS */ , ["onClick"])]), _: 1 })], 2 /* CLASS */ ); } script.render = render; script.__file = "src/components/table/TableMobileSort.vue"; /** * @displayName Table Column */ var script$1 = vue.defineComponent({ name: 'OTableColumn', inject: ['$table'], props: { label: String, customKey: [String, Number], field: String, meta: [String, Number, Boolean, Function, Object, Array], width: [Number, String], numeric: Boolean, /** * Optional, position of column content * @values centered, right */ position: { type: String, validator(value) { return [ 'left', 'centered', 'right' ].indexOf(value) > -1; } }, searchable: Boolean, sortable: Boolean, visible: { type: Boolean, default: true }, customSort: Function, customSearch: Function, sticky: Boolean, headerSelectable: Boolean, /** Adds native attributes to th :th-attrs="(column)" => ({})" */ thAttrs: { type: Function, default: () => ({}) }, /** Adds native attributes to td :td-attrs="(row, column)" => ({})" */ tdAttrs: { type: Function, default: () => ({}) }, subheading: String }, data() { return { newKey: undefined }; }, computed: { style() { return { width: helpers.toCssDimension(this.width) }; }, hasDefaultSlot() { return this.$slots.default; }, hasSearchableSlot() { return this.$slots.searchable; }, hasHeaderSlot() { return this.$slots.header; }, isHeaderUnselectable() { return !this.headerSelectable && this.sortable; } }, created() { if (!this.$table) { throw new Error('You should wrap oTableColumn on a oTable'); } this.newKey = this.$table._nextSequence(); this.$table._addColumn(this); }, beforeUnmount() { this.$table._removeColumn(this); }, render() { // renderless return vue.h('span', { 'data-id': this.newKey }, this.label); } }); script$1.__file = "src/components/table/TableColumn.vue"; var script$2 = vue.defineComponent({ name: 'OTablePagination', components: { [Pagination.script.name]: Pagination.script }, emits: ['update:currentPage', 'page-change'], props: { paginated: Boolean, total: [Number, String], perPage: [Number, String], currentPage: [Number, String], paginationSimple: Boolean, paginationSize: String, rounded: Boolean, iconPack: String, rootClass: [String, Array, Object], ariaNextLabel: String, ariaPreviousLabel: String, ariaPageLabel: String, ariaCurrentLabel: String }, data() { return { newCurrentPage: this.currentPage }; }, watch: { currentPage(newVal) { this.newCurrentPage = newVal; } }, methods: { /** * Paginator change listener. */ pageChanged(page) { this.newCurrentPage = page > 0 ? page : 1; this.$emit('update:currentPage', this.newCurrentPage); this.$emit('page-change', this.newCurrentPage); } } }); const _hoisted_1 = { key: 0 }; function render$1(_ctx, _cache, $props, $setup, $data, $options) { const _component_o_pagination = vue.resolveComponent("o-pagination"); return vue.openBlock(), vue.createBlock("div", { class: _ctx.rootClass }, [vue.createVNode("div", null, [vue.renderSlot(_ctx.$slots, "default")]), vue.createVNode("div", null, [_ctx.paginated ? (vue.openBlock(), vue.createBlock("div", _hoisted_1, [vue.createVNode(_component_o_pagination, { "icon-pack": _ctx.iconPack, total: _ctx.total, "per-page": _ctx.perPage, simple: _ctx.paginationSimple, size: _ctx.paginationSize, current: _ctx.newCurrentPage, rounded: _ctx.rounded, onChange: _ctx.pageChanged, "aria-next-label": _ctx.ariaNextLabel, "aria-previous-label": _ctx.ariaPreviousLabel, "aria-page-label": _ctx.ariaPageLabel, "aria-current-label": _ctx.ariaCurrentLabel }, null, 8 /* PROPS */ , ["icon-pack", "total", "per-page", "simple", "size", "current", "rounded", "onChange", "aria-next-label", "aria-previous-label", "aria-page-label", "aria-current-label"])])) : vue.createCommentVNode("v-if", true)])], 2 /* CLASS */ ); } script$2.render = render$1; script$2.__file = "src/components/table/TablePagination.vue"; /** * Tabulated data are sometimes needed, it's even better when it's responsive * @displayName Table * @requires ./TableColumn.vue * @style _table.scss */ var script$3 = vue.defineComponent({ name: 'OTable', components: { [Button.script.name]: Button.script, [Checkbox.script.name]: Checkbox.script, [Icon.script.name]: Icon.script, [Input.script.name]: Input.script, [Loading.script.name]: Loading.script, [SlotComponent.SlotComponent.name]: SlotComponent.SlotComponent, [script.name]: script, [script$1.name]: script$1, [script$2.name]: script$2 }, mixins: [BaseComponentMixin.BaseComponentMixin, MatchMediaMixin.MatchMediaMixin], configField: 'table', inheritAttrs: false, provide() { return { $table: this }; }, emits: [ 'page-change', 'click', 'dblclick', 'contextmenu', 'check', 'check-all', 'update:checkedRows', 'select', 'update:selected', 'filters-change', 'details-open', 'details-close', 'update:openedDetailed', 'mouseenter', 'mouseleave', 'sort', 'sorting-priority-removed', 'dragstart', 'dragend', 'drop', 'dragleave', 'dragover', 'cell-click', 'columndragstart', 'columndragend', 'columndrop', 'columndragleave', 'columndragover', 'update:currentPage' ], props: { /** Table data */ data: { type: Array, default: () => [] }, /** Table columns */ columns: { type: Array, default: () => [] }, /** Border to all cells */ bordered: Boolean, /** Whether table is striped */ striped: Boolean, /** Makes the cells narrower */ narrowed: Boolean, /** Rows are highlighted when hovering */ hoverable: Boolean, /** Loading state */ loading: Boolean, /** Allow row details */ detailed: Boolean, /** Rows can be checked (multiple), checked rows will have a .is-checked class if you want to style */ checkable: Boolean, /** Show check/uncheck all checkbox in table header when checkable */ headerCheckable: { type: Boolean, default: true }, /** * Position of the checkbox (if checkable is true) * @values left, right */ checkboxPosition: { type: String, default: 'left', validator: (value) => { return [ 'left', 'right' ].indexOf(value) >= 0; } }, /** Set which row is selected, use v-model:selected to make it two-way binding */ selected: Object, /** Custom method to verify if a row is selectable, works when is selected. */ isRowSelectable: { type: Function, default: () => true }, /** Table can be focused and user can navigate with keyboard arrows (require selected) and rows are highlighted when hovering */ focusable: Boolean, /** Custom method to verify if row is checked, works when is checkable. Useful for backend pagination */ customIsChecked: Function, /** Custom method to verify if a row is checkable, works when is checkable */ isRowCheckable: { type: Function, default: () => true }, /** Set which rows are checked, use v-model:checkedRows to make it two-way binding */ checkedRows: { type: Array, default: () => [] }, /** Rows appears as cards on mobile (collapse rows) */ mobileCards: { type: Boolean, default: () => { return helpers.getValueByPath(config.getOptions(), 'table.mobileCards', true); } }, /** Sets the default sort column and order — e.g. ['first_name', 'desc'] */ defaultSort: [String, Array], /** * Sets the default sort column direction on the first click * @values asc, desc */ defaultSortDirection: { type: String, default: 'asc' }, /** Sets the header sorting icon */ sortIcon: { type: String, default: () => { return helpers.getValueByPath(config.getOptions(), 'table.sortIcon', 'arrow-up'); } }, /** * Sets the size of the sorting icon * @values small, medium, large */ sortIconSize: { type: String, default: () => { return helpers.getValueByPath(config.getOptions(), 'table.sortIconSize', 'small'); } }, /** Adds pagination to the table */ paginated: Boolean, /** Current page of table data (if paginated), use v-model:currentPage to make it two-way binding */ currentPage: { type: Number, default: 1 }, /** How many rows per page (if paginated) */ perPage: { type: [Number, String], default: () => { return helpers.getValueByPath(config.getOptions(), 'table.perPage', 20); } }, /** Allow chevron icon and column to be visible */ showDetailIcon: { type: Boolean, default: true }, /** Icon name of detail action */ detailIcon: { type: String, default: 'chevron-right' }, /** * Pagination position (if paginated) * @values bottom, top, bot */ paginationPosition: { type: String, default: () => { return helpers.getValueByPath(config.getOptions(), 'table.paginationPosition', 'bottom'); }, validator: (value) => { return [ 'bottom', 'top', 'both' ].indexOf(value) >= 0; } }, /** Columns won't be sorted with Javascript, use with sort event to sort in your backend */ backendSorting: Boolean, /** Columns won't be filtered with Javascript, use with searchable prop to the columns to filter in your backend */ backendFiltering: Boolean, /** Add a class to row based on the return */ rowClass: { type: Function, default: () => '' }, /** Allow pre-defined opened details. Ideal to open details via vue-router. (A unique key is required; check detail-key prop) */ openedDetailed: { type: Array, default: () => [] }, /** Controls the visibility of the trigger that toggles the detailed rows. */ hasDetailedVisible: { type: Function, default: () => true }, /** Use a unique key of your data Object when use detailed or opened detailed. (id recommended) */ detailKey: { type: String, default: '' }, /** Custom style on details */ customDetailRow: { type: Boolean, default: false }, /* Transition name to use when toggling row details. */ detailTransition: { type: String, default: '' }, /** Rows won't be paginated with Javascript, use with page-change event to paginate in your backend */ backendPagination: Boolean, /** Total number of table data if backend-pagination is enabled */ total: { type: [Number, String], default: 0 }, /** Icon pack to use */ iconPack: String, /** Text when nothing is selected */ mobileSortPlaceholder: String, /** Use a unique key of your data Object for each row. Useful if your data prop has dynamic indices. (id recommended) */ customRowKey: String, /** Allows rows to be draggable */ draggable: { type: Boolean, default: false }, /** Allows columns to be draggable */ draggableColumn: { type: Boolean, default: false }, /** Add a horizontal scrollbar when table is too wide */ scrollable: Boolean, ariaNextLabel: String, ariaPreviousLabel: String, ariaPageLabel: String, ariaCurrentLabel: String, /** Show a sticky table header */ stickyHeader: Boolean, /** Table fixed height */ height: [Number, String], /** Add a native event to filter */ filtersEvent: { type: String, default: '' }, /** Filtering debounce time (in milliseconds) */ debounceSearch: Number, /** Show header */ showHeader: { type: Boolean, default: () => { return helpers.getValueByPath(config.getOptions(), 'table.showHeader', true); } }, /** Make the checkbox column sticky when checkable */ stickyCheckbox: { type: Boolean, default: false }, /** Rounded pagination if paginated */ paginationRounded: Boolean, rootClass: [String, Function, Array], tableClass: [String, Function, Array], wrapperClass: [String, Function, Array], footerClass: [String, Function, Array], emptyClass: [String, Function, Array], detailedClass: [String, Function, Array], borderedClass: [String, Function, Array], stripedClass: [String, Function, Array], narrowedClass: [String, Function, Array], hoverableClass: [String, Function, Array], thClass: [String, Function, Array], tdClass: [String, Function, Array], thPositionClass: [String, Function, Array], thStickyClass: [String, Function, Array], thCheckboxClass: [String, Function, Array], thCurrentSortClass: [String, Function, Array], thSortableClass: [String, Function, Array], thUnselectableClass: [String, Function, Array], thSortIconClass: [String, Function, Array], thDetailedClass: [String, Function, Array], tdPositionClass: [String, Function, Array], tdStickyClass: [String, Function, Array], tdCheckboxClass: [String, Function, Array], tdDetailedChevronClass: [String, Function, Array], trSelectedClass: [String, Function, Array], stickyHeaderClass: [String, Function, Array], scrollableClass: [String, Function, Array], mobileSortClass: [String, Function, Array], paginationWrapperClass: [String, Function, Array], mobileClass: [String, Function, Array], thSubheadingClass: [String, Function, Array] }, data() { return { visibleDetailRows: this.openedDetailed, newData: this.data, newDataTotal: this.backendPagination ? this.total : this.data.length, newCheckedRows: [...this.checkedRows], lastCheckedRowIndex: null, newCurrentPage: this.currentPage, currentSortColumn: {}, isAsc: true, filters: {}, defaultSlots: [], firstTimeSort: true, sequence: 1, isDraggingRow: false, isDraggingColumn: false }; }, mounted() { this.$nextTick(() => { this.checkSort(); }); }, computed: { rootClasses() { return [ this.computedClass('rootClass', 'o-table__root'), { [this.computedClass('mobileClass', 'o-table__wrapper--mobile')]: this.isMobile } ]; }, tableClasses() { return [ this.computedClass('tableClass', 'o-table'), { [this.computedClass('borderedClass', 'o-table--bordered')]: this.bordered }, { [this.computedClass('stripedClass', 'o-table--striped')]: this.striped }, { [this.computedClass('narrowedClass', 'o-table--narrowed')]: this.narrowed }, { [this.computedClass('hoverableClass', 'o-table--hoverable')]: ((this.hoverable || this.focusable) && this.visibleData.length) }, { [this.computedClass('emptyClass', 'o-table--table__empty')]: !this.visibleData.length } ]; }, tableWrapperClasses() { return [ this.computedClass('wrapperClass', 'o-table__wrapper'), { [this.computedClass('stickyHeaderClass', 'o-table__wrapper--sticky-header')]: this.stickyHeader }, { [this.computedClass('scrollableClass', 'o-table__wrapper--scrollable')]: this.isScrollable }, { [this.computedClass('mobileClass', 'o-table__wrapper--mobile')]: this.isMobile }, ]; }, footerClasses() { return [ this.computedClass('footerClass', 'o-table__footer') ]; }, thBaseClasses() { return [ this.computedClass('thClass', 'o-table__th') ]; }, tdBaseClasses() { return [ this.computedClass('tdClass', 'o-table__td') ]; }, thCheckboxClasses() { return [ ...this.thBaseClasses, this.computedClass('thCheckboxClass', 'o-table__th-checkbox') ]; }, thDetailedClasses() { return [ ...this.thBaseClasses, this.computedClass('thDetailedClass', 'o-table__th--detailed') ]; }, thSubheadingClasses() { return [ ...this.thBaseClasses, this.computedClass('thSubheadingClass', 'o-table__th') ]; }, tdCheckboxClasses() { return [ ...this.tdBaseClasses, this.computedClass('tdCheckboxClass', 'o-table__td-checkbox'), ...this.thStickyClasses({ sticky: this.stickyCheckbox }) ]; }, detailedClasses() { return [ this.computedClass('detailedClass', 'o-table__detail') ]; }, tdDetailedChevronClasses() { return [ ...this.tdBaseClasses, this.computedClass('tdDetailedChevronClass', 'o-table__td-chevron') ]; }, mobileSortClasses() { return [ this.computedClass('mobileSortClass', 'o-table__mobile-sort') ]; }, paginationWrapperClasses() { return [ this.computedClass('paginationWrapperClass', 'o-table__pagination') ]; }, tableWrapperStyle() { return { height: helpers.toCssDimension(this.height) }; }, /** * Splitted data based on the pagination. */ visibleData() { if (!this.paginated) return this.newData; const currentPage = this.newCurrentPage; const perPage = this.perPage; if (this.newData.length <= perPage) { return this.newData; } else { const start = (currentPage - 1) * perPage; const end = start + parseInt(perPage, 10); return this.newData.slice(start, end); } }, visibleColumns() { if (!this.newColumns) return this.newColumns; return this.newColumns.filter((column) => { return column.visible || column.visible === undefined; }); }, /** * Check if all rows in the page are checked. */ isAllChecked() { const validVisibleData = this.visibleData.filter((row) => this.isRowCheckable(row)); if (validVisibleData.length === 0) return false; const isAllChecked = validVisibleData.some((currentVisibleRow) => { return helpers.indexOf(this.newCheckedRows, currentVisibleRow, this.customIsChecked) < 0; }); return !isAllChecked; }, /** * Check if all rows in the page are checkable. */ isAllUncheckable() { const validVisibleData = this.visibleData.filter((row) => this.isRowCheckable(row)); return validVisibleData.length === 0; }, /** * Check if has any sortable column. */ hasSortablenewColumns() { return this.newColumns.some((column) => { return column.sortable; }); }, /** * Check if has any searchable column. */ hasSearchablenewColumns() { return this.newColumns.some((column) => { return column.searchable; }); }, /** * Return total column count based if it's checkable or expanded */ columnCount() { let count = this.visibleColumns.length; count += this.checkable ? 1 : 0; count += (this.detailed && this.showDetailIcon) ? 1 : 0; return count; }, /** * return if detailed row tabled * will be with chevron column & icon or not */ showDetailRowIcon() { return this.detailed && this.showDetailIcon; }, /** * return if scrollable table */ isScrollable() { if (this.scrollable) return true; if (!this.newColumns) return false; return this.newColumns.some((column) => { return column.sticky; }); }, newColumns() { if (this.columns && this.columns.length) { return this.columns.map((column) => { const vnode = vue.createVNode(script$1, column, (props) => { const vnode = vue.h('span', {}, helpers.getValueByPath(props.row, column.field)); return [vnode]; }); return vue.createApp(vnode) .provide('$table', this) .mount(document.createElement('div')); }); } return this.defaultSlots; }, isMobile() { return this.mobileCards && this.isMatchMedia; }, hasCustomSubheadings() { if (this.$slots.subheading) return true; return this.newColumns.some((column) => { return column.subheading || (column.$slots.subheading); }); }, canDragRow() { return this.draggable && !this.isDraggingColumn; }, canDragColumn() { return this.draggableColumn && !this.isDraggingRow; } }, watch: { /** * When data prop change: * 1. Update internal value. * 2. Filter data if it's not backend-filtered. * 3. Sort again if it's not backend-sorted. * 4. Set new total if it's not backend-paginated. */ data: { handler(value) { this.newData = value; if (!this.backendFiltering) { this.newData = value.filter((row) => this.isRowFiltered(row)); } if (!this.backendSorting) { this.sort(this.currentSortColumn, true); } if (!this.backendPagination) { this.newDataTotal = this.newData.length; } }, deep: true, }, /** * When Pagination total change, update internal total * only if it's backend-paginated. */ total(newTotal) { if (!this.backendPagination) return; this.newDataTotal = newTotal; }, currentPage(newValue) { this.newCurrentPage = newValue; }, /** * When checkedRows prop change, update internal value without * mutating original data. */ checkedRows: { handler(rows) { this.newCheckedRows = [...rows]; }, deep: true, }, debounceSearch: { handler(value) { this.debouncedHandleFiltersChange = helpers.debounce(this.handleFiltersChange, value); }, immediate: true }, filters: { handler(value) { if (this.debounceSearch) { this.debouncedHandleFiltersChange(value); } else { this.handleFiltersChange(value); } }, deep: true, }, /** * When the user wants to control the detailed rows via props. * Or wants to open the details of certain row with the router for example. */ openedDetailed(expandedRows) { this.visibleDetailRows = expandedRows; }, newCurrentPage(newVal) { this.$emit('update:currentPage', newVal); } }, methods: { thClasses(column) { return [ ...this.thBaseClasses, ...this.thStickyClasses(column), column.thAttrs && helpers.getValueByPath(column.thAttrs(column), 'class'), { [this.computedClass('thCurrentSortClass', 'o-table__th-current-sort')]: (this.currentSortColumn === column) }, { [this.computedClass('thSortableClass', 'o-table__th--sortable')]: column.sortable }, { [this.computedClass('thUnselectableClass', 'o-table__th--unselectable')]: column.isHeaderUnselectable }, { [this.computedClass('thPositionClass', 'o-table__th--', column.position)]: column.position }, ]; }, thStickyClasses(column) { return [ { [this.computedClass('thStickyClass', 'o-table__th--sticky')]: column.sticky } ]; }, rowClasses(row, index) { return [ this.rowClass(row, index), { [this.computedClass('trSelectedClass', 'o-table__tr--selected')]: this.isRowSelected(row, this.selected) } ]; }, thSortIconClasses() { return [ this.computedClass('thSortIconClass', 'o-table__th__sort-icon'), ]; }, tdClasses(row, column) { return [ ...this.tdBaseClasses, column.tdAttrs && helpers.getValueByPath(column.tdAttrs(row, column), 'class'), { [this.computedClass('tdPositionClass', 'o-table__td--', column.position)]: column.position }, { [this.computedClass('tdStickyClass', 'o-table__td--sticky')]: column.sticky } ]; }, onFiltersEvent(event) { this.$emit(`filters-event-${this.filtersEvent}`, { event, filters: this.filters }); }, handleFiltersChange(value) { if (this.backendFiltering) { this.$emit('filters-change', value); } else { this.newData = this.data.filter((row) => this.isRowFiltered(row)); if (!this.backendPagination) { this.newDataTotal = this.newData.length; } if (!this.backendSorting) { if (Object.keys(this.currentSortColumn).length > 0) { this.doSortSingleColumn(this.currentSortColumn); } } } }, /** * Sort an array by key without mutating original data. * Call the user sort function if it was passed. */ sortBy(array, key, fn, isAsc) { let sorted = []; // Sorting without mutating original data if (fn && typeof fn === 'function') { sorted = [...array].sort((a, b) => fn(a, b, isAsc)); } else { sorted = [...array].sort((a, b) => { // Get nested values from objects let newA = helpers.getValueByPath(a, key); let newB = helpers.getValueByPath(b, key); // sort boolean type if (typeof newA === 'boolean' && typeof newB === 'boolean') { return isAsc ? newA > newB ? 1 : -1 : newA > newB ? -1 : 1; } if (!newA && newA !== 0) return 1; if (!newB && newB !== 0) return -1; if (newA === newB) return 0; newA = (typeof newA === 'string') ? newA.toUpperCase() : newA; newB = (typeof newB === 'string') ? newB.toUpperCase() : newB; return isAsc ? newA > newB ? 1 : -1 : newA > newB ? -1 : 1; }); } return sorted; }, /** * Sort the column. * Toggle current direction on column if it's sortable * and not just updating the prop. */ sort(column, updatingData = false, event = null) { if (!column || !column.sortable) return; if (!updatingData) { this.isAsc = column === this.currentSortColumn ? !this.isAsc : (this.defaultSortDirection.toLowerCase() !== 'desc'); } if (!this.firstTimeSort) { /** * @property {string} field column field * @property {boolean} direction 'asc' or 'desc' * @property {Event} event native event */ this.$emit('sort', column.field, this.isAsc ? 'asc' : 'desc', event); } if (!this.backendSorting) { this.doSortSingleColumn(column); } this.currentSortColumn = column; }, doSortSingleColumn(column) { this.newData = this.sortBy(this.newData, column.field, column.customSort, this.isAsc); }, isRowSelected(row, selected) { if (!selected) { return false; } if (this.customRowKey) { return row[this.customRowKey] === selected[this.customRowKey]; } return row === selected; }, /** * Check if the row is checked (is added to the array). */ isRowChecked(row) { return helpers.indexOf(this.newCheckedRows, row, this.customIsChecked) >= 0; }, /** * Remove a checked row from the array. */ removeCheckedRow(row) { const index = helpers.indexOf(this.newCheckedRows, row, this.customIsChecked); if (index >= 0) { this.newCheckedRows.splice(index, 1); } }, /** * Header checkbox click listener. * Add or remove all rows in current page. */ checkAll() { const isAllChecked = this.isAllChecked; this.visibleData.forEach((currentRow) => { if (this.isRowCheckable(currentRow)) { this.removeCheckedRow(currentRow); } if (!isAllChecked) { if (this.isRowCheckable(currentRow)) { this.newCheckedRows.push(currentRow); } } }); /** * @property {Array<Object>} newCheckedRows checked rows */ this.$emit('check', this.newCheckedRows); this.$emit('check-all', this.newCheckedRows); // Emit checked rows to update user variable this.$emit('update:checkedRows', this.newCheckedRows); }, /** * Row checkbox click listener. */ checkRow(row, index, event) { if (!this.isRowCheckable(row)) return; const lastIndex = this.lastCheckedRowIndex; this.lastCheckedRowIndex = index; if (event.shiftKey && lastIndex !== null && index !== lastIndex) { this.shiftCheckRow(row, index, lastIndex); } else if (!this.isRowChecked(row)) { this.newCheckedRows.push(row); } else { this.removeCheckedRow(row); } this.$emit('check', this.newCheckedRows, row); // Emit checked rows to update user variable this.$emit('update:checkedRows', this.newCheckedRows); }, /** * Check row when shift is pressed. */ shiftCheckRow(row, index, lastCheckedRowIndex) { // Get the subset of the list between the two indicies const subset = this.visibleData.slice(Math.min(index, lastCheckedRowIndex), Math.max(index, lastCheckedRowIndex) + 1); // Determine the operation based on the state of the clicked checkbox const shouldCheck = !this.isRowChecked(row); subset.forEach((item) => { this.removeCheckedRow(item); if (shouldCheck && this.isRowCheckable(item)) { this.newCheckedRows.push(item); } }); }, /** * Row click listener. * Emit all necessary events. */ selectRow(row, index) { /** * @property {Object} row clicked row * @property {number} index index of clicked row */ this.$emit('click', row, index); if (this.selected === row) return; if (!this.isRowSelectable(row)) return; // Emit new and old row /** * @property {Object} row selected row * @property {Array<Object>} selected selected rows */ this.$emit('select', row, this.selected); // Emit new row to update user variable this.$emit('update:selected', row); }, /** * Toggle to show/hide details slot */ toggleDetails(obj) { const found = this.isVisibleDetailRow(obj); if (found) { this.closeDetailRow(obj); this.$emit('details-close', obj); } else { this.openDetailRow(obj); this.$emit('details-open', obj); } // Syncs the detailed rows with the parent component this.$emit('update:openedDetailed', this.visibleDetailRows); }, openDetailRow(obj) { const index = this.handleDetailKey(obj); this.visibleDetailRows.push(index); }, closeDetailRow(obj) { const index = this.handleDetailKey(obj); const i = this.visibleDetailRows.indexOf(index); if (i >= 0) { this.visibleDetailRows.splice(i, 1); } }, isVisibleDetailRow(obj) { const index = this.handleDetailKey(obj); return this.visibleDetailRows.indexOf(index) >= 0; }, isActiveDetailRow(row) { return this.detailed && !this.customDetailRow && this.isVisibleDetailRow(row); }, isActiveCustomDetailRow(row) { return this.detailed && this.customDetailRow && this.isVisibleDetailRow(row); }, isRowFiltered(row) { for (const key in this.filters) { if (!this.filters[key]) continue; const input = this.filters[key]; const column = this.newColumns.filter((c) => c.field === key)[0]; if (column && column.customSearch && typeof column.customSearch === 'function') { if (!column.customSearch(row, input)) return false; } else { const value = helpers.getValueByPath(row, key); if (value == null) return false; if (Number.isInteger(value)) { if (value !== Number(input)) return false; } else { const re = new RegExp(helpers.escapeRegExpChars(input), 'i'); if (Array.isArray(value)) { const valid = value.some((val) => re.test(helpers.removeDiacriticsFromString(val)) || re.test(val)); if (!valid) return false; } else { if (!re.test(helpers.removeDiacriticsFromString(value)) && !re.test(value)) { return false; } } } } } return true; }, /** * When the detailKey is defined we use the object[detailKey] as index. * If not, use the object reference by default. */ handleDetailKey(index) { const key = this.detailKey; return !key.length || !index ? index : index[key]; }, /** * Call initSort only first time (For example async data). */ checkSort() { if (this.newColumns.length && this.firstTimeSort) { this.initSort(); this.firstTimeSort = false; } else if (this.newColumns.length) { if (Object.keys(this.currentSortColumn).length > 0) { for (let i = 0; i < this.newColumns.length; i++) { if (this.newColumns[i].field === this.currentSortColumn.field) { this.currentSortColumn = this.newColumns[i]; break; } } } } }, /** * Check if footer slot has custom content. */ hasCustomFooterSlot() { if (this.$slots.footer) { const footer = this.$slots.footer(); if (footer.length > 1) return true; const tag = footer[0].tag; if (tag !== 'th' && tag !== 'td') return false; } return true; }, /** * Table arrow keys listener, change selection. */ pressedArrow(pos) { if (!this.visibleData.length) return; let index = this.visibleData.indexOf(this.selected) + pos; // Prevent from going up from first and down from last index = index < 0 ? 0 : index > this.visibleData.length - 1 ? this.visibleData.length - 1 : index; const row = this.visibleData[index]; if (!this.isRowSelectable(row)) { let newIndex = null; if (pos > 0) { for (let i = index; i < this.visibleData.length && newIndex === null; i++) { if (this.isRowSelectable(this.visibleData[i])) newIndex = i; } } else { for (let i = index; i >= 0 && newIndex === null; i--) { if (this.isRowSelectable(this.visibleData[i])) newIndex = i; } } if (newIndex >= 0) { this.selectRow(this.visibleData[newIndex]); } } else { this.selectRow(row); } }, /** * Focus table element if has selected prop. */ focus() { if (!this.focusable) return; this.$el.querySelector('table').focus(); }, /** * Initial sorted column based on the default-sort prop. */ initSort() { if (!this.defaultSort) return; let sortField = ''; let sortDirection = this.defaultSortDirection; if (Array.isArray(this.defaultSort)) { sortField = this.defaultSort[0]; if (this.defaultSort[1]) { sortDirection = this.defaultSort[1]; } } else { sortField = this.defaultSort; } const sortColumn = this.newColumns.filter((column) => (column.field === sortField))[0]; if (sortColumn) { this.isAsc = sortDirection.toLowerCase() !== 'desc'; this.sort(sortColumn, true); } }, /** * Emits drag start event */ handleDragStart(event, row, index) { if (!this.draggable) return; this.$emit('dragstart', { event, row, index }); }, /** * Emits drag leave event */ handleDragEnd(event, row, index) { if (!this.draggable) return; this.$emit('dragend', { event, row, index }); }, /** * Emits drop event */ handleDrop(event, row, index) { if (!this.draggable) return; this.$emit('drop', { event, row, index }); }, /** * Emits drag over event */ handleDragOver(event, row, index) { if (!this.draggable) return; this.$emit('dragover', { event, row, index }); }, /** * Emits drag leave event */ handleDragLeave(event, row, index) { if (!this.draggable) return; this.$emit('dragleave', { event, row, index }); }, /** * Emits drag start event (column) */ handleColumnDragStart(event, column, index) { if (!this.canDragColumn) return; this.isDraggingColumn = true; this.$emit('columndragstart', { event, column, index }); }, /** * Emits drag leave event (column) */ handleColumnDragEnd(event, column, index) { if (!this.canDragColumn) return; this.isDraggingColumn = fals