UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

757 lines (753 loc) 36.2 kB
'use strict'; var vue = require('vue'); var index_es = require('../../node_modules/@coreui/icons-vue/dist/index.es.js'); var CElementCover = require('../element-cover/CElementCover.js'); require('../form/CForm.js'); require('../form/CFormCheck.js'); require('../form/CFormFeedback.js'); require('../form/CFormFloating.js'); var CFormInput = require('../form/CFormInput.js'); var CFormLabel = require('../form/CFormLabel.js'); require('../form/CFormRange.js'); var CFormSelect = require('../form/CFormSelect.js'); require('../form/CFormSwitch.js'); require('../form/CFormText.js'); require('../form/CFormTextarea.js'); require('../form/CInputGroup.js'); require('../form/CInputGroupText.js'); var CSmartPagination = require('../smart-pagination/CSmartPagination.js'); var CTable = require('../table/CTable.js'); require('../table/CTableBody.js'); require('../table/CTableCaption.js'); var CTableDataCell = require('../table/CTableDataCell.js'); var CTableFoot = require('../table/CTableFoot.js'); require('../table/CTableHead.js'); require('../table/CTableHeaderCell.js'); var CTableRow = require('../table/CTableRow.js'); var CSmartTableBody = require('./CSmartTableBody.js'); var CSmartTableHead = require('./CSmartTableHead.js'); var isObjectInArray = require('../../utils/isObjectInArray.js'); var consts = require('./consts.js'); var utils = require('./utils.js'); var cilFilterX = require('../../node_modules/@coreui/icons/dist/esm/free/cil-filter-x.js'); var cilSwapVertical = require('../../node_modules/@coreui/icons/dist/esm/free/cil-swap-vertical.js'); var cilArrowTop = require('../../node_modules/@coreui/icons/dist/esm/free/cil-arrow-top.js'); var cilArrowBottom = require('../../node_modules/@coreui/icons/dist/esm/free/cil-arrow-bottom.js'); const CSmartTable = vue.defineComponent({ name: 'CSmartTable', props: { /** * Sets active page. If 'pagination' prop is enabled, activePage is set only initially. */ activePage: { type: Number, default: 1, }, /** * When set, displays table cleaner above table, next to the table filter (or in place of table filter if `tableFilter` prop is not set) * Cleaner resets `tableFilterValue`, `columnFilterValue`, `sorterValue`. If clean is possible it is clickable (`tabIndex="0"` `role="button"`, `color="danger"`), otherwise it is not clickable and transparent. Cleaner can be customized through the `cleanerIcon` slot. * */ cleaner: Boolean, /** * Style table items as clickable. */ clickableRows: Boolean, /** * When set, displays additional filter row between table header and items, allowing filtering by specific column. * Column filter can be customized, by passing prop as object with additional options as keys. Available options: * - external (Boolean) - Disables automatic filtering inside component. * - lazy (Boolean) - Set to true to trigger filter updates only on change event. */ columnFilter: { type: [Boolean, Object], }, /** * Value of table filter. To set pass object where keys are column names and values are filter strings e.g.: * { user: 'John', age: 12 } */ columnFilterValue: { type: Object, }, /** * Prop for table columns configuration. If prop is not defined, table will display columns based on the first item keys, omitting keys that begins with underscore (e.g. '_props') * * In columns prop each array item represents one column. Item might be specified in two ways: * String: each item define column name equal to item value. * Object: item is object with following keys available as column configuration: * - key (required)(String) - define column name equal to item key. * - filter (Boolean) - removes filter from column when set to false. * - label (String) - define visible label of column. If not defined, label will be generated automatically based on column name, by converting kebab-case and snake_case to individual words and capitalization of each word. * - sorter (Boolean) - disables sorting of the column when set to false * - [_props](https://coreui.io/vue/docs/components/table.html#ctableheadercell) (String/Array/Object) - add props to `CTableHeaderCell`. * - _style (String/Array/Object) - adds styles to the column header (useful for defining widths) */ columns: { type: Array, }, /** * Enables table sorting by column value. Sorting will be performed corectly only if values in column are of one type: string (case insensitive) or number. * * Sorter can be customized, by passing prop as object with additional options as keys. Available options: * - external (Boolean) - Disables automatic sorting inside the component, allowing for external sorting control. * - multiple (Boolean) - Enables sorting by multiple columns simultaneously. The sorting order will be maintained across multiple columns based on the order of interaction. * - resetable (Boolean) - If set to true, clicking on the sorter cycles through three states: ascending, descending, and null (no sorting). The third click on the sorter will reset the sorting and restore the table to its original order. */ columnSorter: { type: [Boolean, Object], }, /** * If `true` Displays table footer, which mirrors table header. (without column filter). * Or Array of objects or strings, where each element represents one cell in the table footer. * * Example items: * `['FooterCell', 'FooterCell', 'FooterCell']` * or * `[{ label: 'FooterCell', _props: { color: 'success' }, ...]` */ footer: { type: [Boolean, Array], }, /** * Set to false to remove table header. */ header: { type: Boolean, default: true, }, /** * Array of objects, where each object represents one item - row in table. Additionally, you can customize each row by passing them by [_props](http://coreui.io/vue/docs/components/table.html#ctablerow) key and single cell by [_cellProps](http://coreui.io/vue/docs/components/table.html#ctabledatacell). * * Examples: * - `_props: { color: 'primary', align: 'middle'}` * - `_cellProps: { all: { class: 'fw-semibold'}, 'name': { color: 'info' }}` */ items: { type: Array, default: () => [], }, /** * The total number of items. Use if you pass a portion of data from an external source to let know component what is the total number of items. * * @since 4.8.0 */ itemsNumber: Number, /** * Number of items per site, when pagination is enabled. */ itemsPerPage: { type: Number, default: 10, }, /** * Label for items per page selector. */ itemsPerPageLabel: { type: String, default: 'Items per page:', }, /** * Items per page selector options. */ itemsPerPageOptions: { type: Array, default: () => [5, 10, 20, 50], }, /** * Adds select element over table, which is used for control items per page in pagination. If you want to customize this element, pass object with optional values: * - external (Boolean) - disables automatic 'itemsPerPage' change (use to change pages externaly by 'pagination-change' event). */ itemsPerPageSelect: { type: [Boolean, Object], }, /** * When set, table will have loading style: loading spinner and reduced opacity. When 'small' prop is enabled spinner will be also smaller. */ loading: Boolean, /** * ReactNode or string for passing custom noItemsLabel texts. */ noItemsLabel: { type: String, default: 'No items found', }, /** * Enables default pagination. Set to true for default setup or pass an object with additional CPagination props. Default pagination will always have the computed number of pages that cannot be changed. The number of pages is generated based on the number of passed items and 'itemsPerPage' prop. If this restriction is an obstacle, you can make external CPagination instead. */ pagination: { type: [Boolean, Object], }, /** * Properties to [CSmartPagination](https://coreui.io/vue/docs/components/smart-pagination#csmartpagination) component. */ paginationProps: Object, /** * Add checkboxes to make table rows selectable. */ selectable: Boolean, /** * Enables select all checkbox displayed in the header of the table. * * Can be customized, by passing prop as object with additional options as keys. Available options: * - external (Boolean) - Disables automatic selection inside the component. * * @since 4.8.0 */ selectAll: { type: [Boolean, Object], }, /** * Array of selected objects, where each object represents one item - row in table. * * Example item: `{ name: 'John' , age: 12 }` * * @since 4.8.0 */ selected: { type: Array, default: () => [], }, /** * State of the sorter. Name key is column name, direction can be 'asc' or 'desc'. eg.: * { column: 'status', state: 'asc' } */ sorterValue: { type: [Array, Object], }, /** * Properties to [CTableBody](https://coreui.io/vue/docs/components/table/#ctablebody) component. */ tableBodyProps: Object, /** * Properties to [CTableFoot](https://coreui.io/vue/docs/components/table/#ctablefoot) component. */ tableFootProps: Object, /** * When set, displays table filter above table, allowing filtering by specific column. * * Column filter can be customized, by passing prop as object with additional options as keys. Available options: * - external (Boolean) - Disables automatic filtering inside component. * - lazy (Boolean) - Set to true to trigger filter updates only on change event. */ tableFilter: { type: [Boolean, Object], }, /** * The element represents a caption for a component. */ tableFilterLabel: { type: String, default: 'Filter:', }, /** * Specifies a short hint that is visible in the search input. */ tableFilterPlaceholder: { type: String, default: 'type string...', }, /** * Value of table filter. */ tableFilterValue: String, /** * Properties to [CTableHead](https://coreui.io/vue/docs/components/table/#ctablehead) component. */ tableHeadProps: Object, /** * Properties to [CTable](https://coreui.io/vue/docs/components/table/#ctable) component. */ tableProps: Object, }, emits: [ /** * Page change callback. * * @property {number} page - active page number */ 'activePageChange', /** * Column filter change callback. * * @property {object} ColumnFilterValue {[key: string]: string | number} */ 'columnFilterChange', /** * Filtered items change callback. * * @property {array} items */ 'filteredItemsChange', /** * Pagination change callback. * * @property {number} itemsPerPageNumber - items per page number */ 'itemsPerPageChange', /** * Row click callback. * * @property {object} item * @property {number} index * @property {string} columnName * @property {event} event */ 'rowClick', /** * Select all callback. * * @since 4.8.0 */ 'selectAll', /** * Selected items change callback. * * @property {array} items */ 'selectedItemsChange', /** * Sorter value change callback. * * @property {object} SorterValue | SorterValue[] { column?: string, state?: number | string} * @property {array} SorterValue[] */ 'sorterChange', /** * Table filter change callback. * * @property {string} tableFilterValue */ 'tableFilterChange', ], setup(props, { emit, slots }) { const activePage = vue.ref(props.activePage); const columnFilterState = vue.ref(props.columnFilterValue ?? {}); const items = vue.ref(props.items.map((item, index) => { return { ...item, _id: index }; })); const itemsPerPage = vue.ref(props.itemsPerPage); const selected = vue.ref([]); const selectedAll = vue.ref(); const sorterState = vue.ref(props.sorterValue ? Array.isArray(props.sorterValue) ? props.sorterValue : [props.sorterValue] : []); const tableFilterState = vue.ref(props.tableFilterValue ?? ''); const itemsNumber = vue.computed(() => props.itemsNumber ?? props.items.length); vue.watch(() => props.activePage, () => { activePage.value = props.activePage; }); vue.watch(() => props.columnFilterValue, () => { if (props.columnFilterValue) { columnFilterState.value = props.columnFilterValue; } }); vue.watch(() => props.items, () => { if (props.items.length < itemsPerPage.value * activePage.value - itemsPerPage.value) { activePage.value = 1; } props.items.forEach((item) => { if (item._selected) { const _item = { ...item }; for (const key of consts.ITEM_INTERNAL_KEYS) { delete _item[key]; // Remove internal keys } selected.value = [...selected.value, _item]; // Add cleaned item to selected array } }); if (Array.isArray(props.items)) { items.value = props.items.map((item, index) => { return { ...item, _id: index }; }); } }, { immediate: true, }); vue.watch(() => props.itemsPerPage, () => { itemsPerPage.value = props.itemsPerPage; }); vue.watch(() => props.selected, () => { selected.value = props.selected; }); vue.watch(() => props.sorterValue, () => { if (props.sorterValue) { sorterState.value = Array.isArray(props.sorterValue) ? props.sorterValue : [props.sorterValue]; } }); vue.watch(itemsPerPage, () => { if (props.itemsPerPage !== itemsPerPage.value) { activePage.value = 1; // TODO: set proper page after _itemsPerPage update } emit('itemsPerPageChange', itemsPerPage.value); }); vue.watch([selected, itemsNumber], () => { if (props.selectable) { emit('selectedItemsChange', selected); if (selected.value.length === itemsNumber.value) { selectedAll.value = true; return; } if (selected.value.length === 0) { selectedAll.value = false; return; } if (selected.value.length > 0 && selected.value.length !== itemsNumber.value) { selectedAll.value = 'indeterminate'; } } }, { immediate: true, }); vue.onMounted(() => { if (items.value && items.value.length < itemsPerPage.value * activePage.value - itemsPerPage.value) { activePage.value = 1; } }); const handleSorterChange = (column, index, order) => { if (!utils.isSortable(index, props.columns, props.columnSorter, itemsDataColumns.value, columnNames.value)) { return; } const existingColumnState = sorterState.value.find((x) => x.column === column); const multiple = typeof props.columnSorter === 'object' && props.columnSorter.multiple; // If the column already has a sort state if (existingColumnState) { // No need to update if the order is already the same if (existingColumnState.state === order) { return; } // Remove the column from sorting if resetable and descending if (typeof props.columnSorter === 'object' && props.columnSorter.resetable && existingColumnState.state === 'desc' && order !== 'asc') { sorterState.value = multiple ? sorterState.value.filter((x) => x.column !== column) : []; } else { // Toggle between ascending and descending const newState = { column, state: order || (existingColumnState.state === 'asc' ? 'desc' : 'asc'), }; sorterState.value = multiple ? sorterState.value.map((item) => (item.column === column ? newState : item)) : [newState]; } } else { // If the column is not yet sorted, add it with the default or provided order const newSorter = { column, state: order || 'asc' }; sorterState.value = multiple ? [...sorterState.value, newSorter] : [newSorter]; } emit('sorterChange', multiple ? sorterState.value : sorterState.value[0]); }; const handleActivePageChange = (page) => { activePage.value = page; emit('activePageChange', page); }; const handleItemsPerPageChange = (event) => { if (typeof props.itemsPerPageSelect !== 'object' || (typeof props.itemsPerPageSelect === 'object' && !props.itemsPerPageSelect.external)) { itemsPerPage.value = Number(event.target.value); } }; const handleRowChecked = (item, value) => { if (value && !isObjectInArray.default(selected.value, item, consts.ITEM_INTERNAL_KEYS)) { selected.value = [...selected.value, item]; return; } selected.value = selected.value.filter((_item) => !isObjectInArray.default([_item], item, consts.ITEM_INTERNAL_KEYS)); }; const handleSelectAllChecked = () => { if (selectedAll.value === true) { selected.value = items.value.filter((item) => item._selectable === false); return; } emit('selectAll'); if (props.selectAll && typeof props.selectAll === 'object' && props.selectAll.external) { return; } const selectable = items.value.filter((item) => item._selectable !== false || item._selected === true); if (selectable.length === selected.value.length) { selected.value = items.value.filter((item) => item._selectable === false && item._selected === true); return; } const _selected = selectable.map((item) => { return { ...item }; }); selected.value = _selected.map((item) => { for (const key of consts.ITEM_INTERNAL_KEYS) { delete item[key]; } return item; }); }; const handleColumnFilterChange = (colName, value, type) => { const _isLazy = props.columnFilter && typeof props.columnFilter === 'object' && props.columnFilter.lazy === true; if ((_isLazy && type === 'input') || (!_isLazy && type === 'change')) { return; } const newState = { ...columnFilterState.value }; if (value === '') { delete newState[colName]; } else { newState[colName] = value; } activePage.value = 1; columnFilterState.value = newState; emit('columnFilterChange', columnFilterState.value); }; const handleTableFilterChange = (value, type) => { const _isLazy = props.columnFilter && typeof props.columnFilter === 'object' && props.columnFilter.lazy === true; if ((_isLazy && type === 'input') || (!_isLazy && type === 'change')) { return; } activePage.value = 1; tableFilterState.value = value; emit('tableFilterChange', tableFilterState.value); }; const handleClean = () => { tableFilterState.value = ''; columnFilterState.value = {}; sorterState.value = []; }; const columnNames = vue.computed(() => utils.getColumnNames(props.columns, items.value)); const itemsDataColumns = vue.computed(() => columnNames.value.filter((name) => utils.getColumnNamesFromItems(items.value).includes(name))); const filteredColumns = vue.computed(() => utils.filterColumns(items.value, props.columnFilter, columnFilterState.value, itemsDataColumns.value)); const filteredTable = vue.computed(() => utils.filterTable(filteredColumns.value, props.tableFilter, tableFilterState.value, itemsDataColumns.value)); const sortedItems = vue.computed(() => utils.sortItems(props.columns, props.columnSorter, filteredTable.value, itemsDataColumns.value, sorterState.value)); vue.watch(sortedItems, () => { emit('filteredItemsChange', sortedItems.value); }); const numberOfPages = vue.computed(() => itemsPerPage.value ? Math.ceil(sortedItems.value.length / itemsPerPage.value) : 1); const firstItemOnActivePageIndex = vue.computed(() => activePage.value ? (activePage.value - 1) * itemsPerPage.value : 0); const currentItems = vue.computed(() => activePage.value ? sortedItems.value.slice(firstItemOnActivePageIndex.value, firstItemOnActivePageIndex.value + itemsPerPage.value) : sortedItems.value); return () => vue.h('div', {}, [ (props.tableFilter || props.cleaner) && vue.h('div', { class: 'row my-2 mx-0', }, [ props.tableFilter && vue.h('div', { class: 'col-auto p-0', }, props.tableFilter && vue.h('div', { class: 'row mb-2', }, { default: () => [ vue.h(CFormLabel.CFormLabel, { class: 'col-sm-auto col-form-label', }, { default: () => props.tableFilterLabel, }), vue.h('div', { class: 'col-sm-auto', }, vue.h(CFormInput.CFormInput, { onInput: (e) => { handleTableFilterChange(e.target.value, 'input'); }, onChange: (e) => { handleTableFilterChange(e.target.value, 'change'); }, placeholder: props.tableFilterPlaceholder, value: tableFilterState.value, })), ], })), props.cleaner && vue.h('div', { class: 'col-auto p-0', }, vue.h('button', { type: 'button', class: 'btn btn-transparent', ...(!(tableFilterState.value || sorterState.value.length > 0 || Object.values(columnFilterState.value).join('')) && { disabled: true, tabIndex: -1 }), onClick: () => handleClean(), onKeydown: (event) => { if (event.key === 'Enter') handleClean(); }, }, slots.cleanerIcon ? slots.cleanerIcon() : vue.h(index_es.CIcon, { width: '18', content: cilFilterX.cilFilterX }))), ]), vue.h('div', { class: 'position-relative', }, { default: () => [ vue.h(CTable.CTable, { ...props.tableProps, }, { default: () => [ props.header && vue.h(CSmartTableHead.CSmartTableHead, { as: 'head', ...props.tableHeadProps, columnFilter: props.columnFilter, columnFilterValue: columnFilterState.value, columns: props.columns ?? columnNames.value, columnSorter: props.columnSorter, items: items.value, selectable: props.selectable, selectAll: props.selectAll, selectedAll: selectedAll.value, sorterState: sorterState.value, onCustomFilterChange: (key, value) => handleColumnFilterChange(key, value), onFilterInput: (key, value) => handleColumnFilterChange(key, value, 'input'), onFilterChange: (key, value) => handleColumnFilterChange(key, value, 'change'), onSelectAllChecked: () => handleSelectAllChecked(), onSortClick: (key, index, order) => handleSorterChange(key, index, order), }, { // @slot Sorter icon when items are unsorted. sortingIcon: () => slots.sortingIcon ? slots.sortingIcon() : vue.h('svg', { xmlns: 'http://www.w3.org/2000/svg', class: 'icon', viewBox: '0 0 512 512', role: 'img', innerHTML: cilSwapVertical.cilSwapVertical[1], }), // @slot Sorter icon when items are sorted ascending. sortingIconAscending: () => slots.sortingIconAscending ? slots.sortingIconAscending() : vue.h('svg', { xmlns: 'http://www.w3.org/2000/svg', class: 'icon', viewBox: '0 0 512 512', role: 'img', innerHTML: cilArrowTop.cilArrowTop[1], }), // @slot Sorter icon when items are sorted descending. sortingIconDescending: () => slots.sortingIconDescending ? slots.sortingIconDescending() : vue.h('svg', { xmlns: 'http://www.w3.org/2000/svg', class: 'icon', viewBox: '0 0 512 512', role: 'img', innerHTML: cilArrowBottom.cilArrowBottom[1], }), }), vue.h(CSmartTableBody.CSmartTableBody, { clickableRows: props.clickableRows, columnNames: columnNames.value, columns: props.columns ?? columnNames.value, currentItems: currentItems.value, firstItemOnActivePageIndex: firstItemOnActivePageIndex.value, noItemsLabel: props.noItemsLabel, onRowChecked: (item, value) => handleRowChecked(item, value), onRowClick: (item, index, columnName, event) => props.clickableRows && emit('rowClick', item, index, columnName, event), scopedSlots: slots, selectable: props.selectable, selected: selected.value, ...props.tableBodyProps, }), typeof props.footer === 'boolean' && props.footer && vue.h(CSmartTableHead.CSmartTableHead, { as: 'footer', ...props.tableFootProps, columnFilter: false, columnSorter: false, columns: props.columns ?? columnNames.value, selectable: props.selectable, selectAll: props.selectAll, selectedAll: selectedAll.value, showGroups: false, onSelectAllChecked: () => handleSelectAllChecked(), }), Array.isArray(props.footer) && vue.h(CTableFoot.CTableFoot, { ...props.tableFootProps, }, { default: () => vue.h(CTableRow.CTableRow, {}, { default: () => [ Array.isArray(props.footer) && props.footer.map((item) => vue.h(CTableDataCell.CTableDataCell, { ...(typeof item === 'object' && item._props && { ...item._props }), }, { default: () => typeof item === 'object' ? item.label : item, })), ], }), }), ], }), props.loading && vue.h(CElementCover.CElementCover, { boundaries: [ { sides: ['top'], query: 'tbody' }, { sides: ['bottom'], query: 'tbody' }, ], }, { // @slot elementCover. ...(slots.elementCover && { default: () => slots.elementCover && slots.elementCover(), }), }), ], }), (props.pagination || props.itemsPerPageSelect) && vue.h('div', { class: 'row', }, [ vue.h('div', { class: 'col', }, (props.pagination && numberOfPages.value > 1) || (props.paginationProps && props.paginationProps.pages > 1) ? vue.h(CSmartPagination.CSmartPagination, { pages: numberOfPages.value, activePage: activePage.value, ...props.paginationProps, onActivePageChange: (page) => typeof props.pagination === 'object' && props.pagination.external ? emit('activePageChange', page) : handleActivePageChange(page), }) : ''), props.itemsPerPageSelect && vue.h('div', { class: 'col-auto ms-auto', }, vue.h('div', { class: 'row', }, { default: () => [ vue.h(CFormLabel.CFormLabel, { class: 'col-auto col-form-label', }, { default: () => props.itemsPerPageLabel, }), vue.h('div', { class: 'col-auto', }, vue.h(CFormSelect.CFormSelect, { value: itemsPerPage.value, onChange: handleItemsPerPageChange, }, { default: () => props.itemsPerPageOptions && props.itemsPerPageOptions.map((number, index) => { return vue.h('option', { value: number, key: index, }, number); }), })), ], })), ]), ]); }, }); exports.CSmartTable = CSmartTable; //# sourceMappingURL=CSmartTable.js.map