UNPKG

@uniquedj95/vtable

Version:

An advanced datatable for Ionic vue framework

1,157 lines (1,149 loc) 46 kB
'use strict'; var vue = require('vue'); var vue$1 = require('@ionic/vue'); var icons = require('ionicons/icons'); /** * Safely gets the value at path of object. * * @param obj - The object to query. * @param path - The path of the property to get. * @returns The resolved value. */ function get(obj, path) { if (obj == null) return undefined; // Handle both dot notation and array indexing const pathArray = path.replace(/\[(\w+)\]/g, '.$1').split('.'); let result = obj; for (const key of pathArray) { if (result == null) return undefined; result = result[key]; } return result; } /** * Checks if value is an empty object, collection, or string. * @param value - The value to check. * @returns Returns true if value is empty, else false. */ function isEmpty(value) { if (value == null) return true; if (typeof value === 'string' || Array.isArray(value)) { return value.length === 0; } if (typeof value === 'object') { return Object.keys(value).length === 0; } return false; } /** * Retrieves an array of rows either from a getter function or the provided default rows. * * @param getter - An optional function to retrieve rows asynchronously. * @param defaultRows - An array of default rows (empty by default). * @param indexed - If true, adds an 'index' property to each row. * @returns An array of rows. */ async function getRows(defaultRows, indexed = false, getter) { let rows = defaultRows; if (typeof getter === 'function') rows = await getter(); return indexed ? rows.map((r, i) => ({ ...r, index: i + 1 })) : rows; } /** * Creates an array of elements, sorted in ascending/descending order by the results of running * each element through each iteratee. * * @param collection - The collection to iterate over. * @param iteratees - The iteratees to sort by. * @param orders - The sort orders of iteratees. * @returns Returns the new sorted array. */ function orderBy(collection, iteratees, orders) { if (!Array.isArray(collection)) return []; if (collection.length <= 1) return [...collection]; return [...collection].sort((a, b) => { for (let i = 0; i < iteratees.length; i++) { const iteratee = iteratees[i]; const order = orders[i]; const valueA = iteratee(a); const valueB = iteratee(b); if (valueA === valueB) continue; if (order === 'asc') { return valueA < valueB ? -1 : 1; } else { return valueA > valueB ? -1 : 1; } } return 0; }); } /** * A function that sort table rows based on specified sort queries * * @param rows An array of data * @param query an array of sort queries * @returns sorted array */ function sortRows(rows, query) { if (isEmpty(query)) return rows; const orders = query.map(({ order }) => order); const iteratees = query.map(({ column }) => (row) => { let value = get(row, column.path); if (isEmpty(value)) return ''; if (typeof column.preSort === 'function') value = column.preSort(value); if (typeof value === 'number' || column.sortCaseSensitive) return value; return value.toString().toLowerCase(); }); return orderBy(rows.slice(), iteratees, orders); } /** * Builds pagination information summary * * @param paginator The current pagination filter * @param totalRows Total filtered rows * @returns string */ function buildPaginationInfo(paginator, totalRows) { const { page, pageSize, totalPages } = paginator; const from = page * pageSize - (pageSize - 1); const to = page === totalPages ? totalRows : page * pageSize; return totalRows ? `Showing ${from} to ${to} of ${totalRows} entries` : 'No data available'; } /** * Calculates the range of visible page numbers for pagination. * * @param paginator - The pagination settings. * @param totalRows - The total number of rows. * @param pages - An array of current visible page numbers. * @returns The updated pagination settings. */ function calculatePageRange(paginator, totalRows, pages) { // Calculate the total number of pages paginator.totalPages = Math.ceil(totalRows / paginator.pageSize); // If total pages are within visibleBtns, show all pages if (paginator.totalPages <= paginator.visibleBtns) { paginator.start = 1; paginator.end = paginator.totalPages; return paginator; } // Return if start and end page numbers are already visible if ((pages.includes(paginator.page - 1) || paginator.page === 1) && (pages.includes(paginator.page + 1) || paginator.page === paginator.totalPages)) { return paginator; } // Calculate the range of visible page numbers paginator.start = paginator.page === 1 ? 1 : paginator.page - 1; paginator.end = paginator.start + paginator.visibleBtns - 5; // Adjust start and end if they go out of bounds if (paginator.start <= 3) { paginator.end += 3 - paginator.start; paginator.start = 1; } if (paginator.end >= paginator.totalPages - 2) { paginator.start -= paginator.end - (paginator.totalPages - 2); paginator.end = paginator.totalPages; } // Ensure start is within valid range paginator.start = Math.max(paginator.start, 1); return paginator; } /** * Paginates an array of rows based on the provided pagination settings. * * @param rows - The array of rows to be paginated. * @param paginator - The pagination settings. * @returns The paginated array of rows. */ function getActiveRows(rows, paginator) { if (isEmpty(rows)) return rows; const { page, pageSize } = paginator; const start = (page - 1) * pageSize; return rows.slice(start, start + pageSize); } /** * Initializes sort queries based on column configurations. * * @param columns - An array of table columns. * @returns An array of initial sort queries. */ function initializeSortQueries(columns) { return columns.reduce((acc, column) => { if (column.initialSort) acc.push({ column, order: column.initialSortOrder || 'asc' }); return acc; }, []); } /** * Updates the array of sort queries based on a specific column. * * @param sortQueries - The current array of sort queries. * @param column - The column for which to update the sort query. * @returns The updated array of sort queries. */ function updateSortQueries(sortQueries, column) { const i = sortQueries.findIndex(q => q.column.path === column.path); if (i >= 0) sortQueries[i].order = sortQueries[i].order === 'asc' ? 'desc' : 'asc'; else sortQueries = [{ column, order: 'asc' }]; return sortQueries; } /** * Filters an array of rows based on a query string. * * @param rows - The array of rows to be filtered. * @param query - The query string for filtering. * @returns The filtered array of rows. */ function filterRows(rows, query) { if (!query || isEmpty(rows)) return rows; return rows .slice() .filter(row => Object.values(row).some(v => v && JSON.stringify(v).toLowerCase().includes(query.toLowerCase()))); } /** * Determines if a table column is drillable based on the provided column configuration, value, and row. * * @param column - The table column configuration object. * @param value - The value in the table cell. * @param row - The entire row data. * @returns A boolean indicating whether the column is drillable. */ function isDrillable(column, value, row) { return typeof column.drillable === 'function' ? column.drillable(value, row) : !!column.drillable && !isEmpty(value); } /** * Creates an array of numbers within a specified range. * * @param start - The start number. * @param end - The end number (exclusive). * @returns An array of numbers. */ function range(start, end) { const result = []; for (let i = start; i < end; i++) { result.push(i); } return result; } /** * Detects if a string contains HTML content. * * @param str - The string to check for HTML content. * @returns True if the string contains HTML tags, false otherwise. */ function isHtmlString(str) { if (typeof str !== 'string') return false; // More robust pattern to match valid HTML tags const htmlPattern = /<\/?[a-z][\s\S]*>/i; return htmlPattern.test(str); } /** * Sanitizes HTML content by removing dangerous elements and attributes. * This helps prevent XSS attacks while preserving safe formatting. * * @param html - The HTML string to sanitize. * @returns The sanitized HTML string. */ function sanitizeHtml(html) { // Create a temporary div to parse HTML const temp = document.createElement('div'); temp.innerHTML = html; // Remove potentially dangerous elements and attributes const dangerousElements = ['script', 'iframe', 'object', 'embed', 'form']; const dangerousAttributes = [ 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur', ]; // Remove dangerous elements dangerousElements.forEach(tagName => { const elements = temp.querySelectorAll(tagName); elements.forEach(el => el.remove()); }); // Remove dangerous attributes from all elements const allElements = temp.querySelectorAll('*'); allElements.forEach(el => { dangerousAttributes.forEach(attr => { if (el.hasAttribute(attr)) { el.removeAttribute(attr); } }); // Remove any attribute that starts with 'on' (event handlers) Array.from(el.attributes).forEach(attr => { if (attr.name.toLowerCase().startsWith('on')) { el.removeAttribute(attr.name); } }); }); return temp.innerHTML; } /** * Renders a value as a chip component */ const renderChip = (value, config = {}, onClick) => { return vue.h(vue$1.IonChip, { color: config.color || 'primary', outline: config.outline || false, onClick, }, [vue.h(vue$1.IonLabel, value)]); }; /** * Renders a value as a badge component */ const renderBadge = (value, config = {}) => { return vue.h(vue$1.IonBadge, { color: config.color || 'primary', size: config.size || 'default', }, value); }; /** * Renders status values with predefined colors and styles */ const renderStatus = (value, statusConfig, defaultConfig = {}) => { const config = statusConfig[value] || defaultConfig; return renderChip(config.label || value, { color: config.color, outline: config.outline, }); }; /** * Renders a list of values as chips */ const renderChipList = (values, config = {}, maxVisible = 3) => { if (!Array.isArray(values)) return values; const visibleValues = values.slice(0, maxVisible); const remainingCount = values.length - maxVisible; const chips = visibleValues.map((value, _index) => renderChip(value, config)); if (remainingCount > 0) { chips.push(renderChip(`+${remainingCount}`, { ...config, color: 'medium', outline: true, })); } return vue.h('div', { style: 'display: flex; flex-wrap: wrap; gap: 4px;' }, chips); }; /** * Renders HTML content safely with sanitization * Note: Only use with trusted content or content that has been validated */ const renderHtml = (htmlContent) => { const sanitizedContent = sanitizeHtml(htmlContent); return vue.h('div', { innerHTML: sanitizedContent, }); }; /** * Renders a progress bar */ const renderProgress = (value, max = 100, color = 'primary') => { const percentage = Math.min((value / max) * 100, 100); return vue.h('div', { style: { width: '100%', height: '8px', backgroundColor: '#e0e0e0', borderRadius: '4px', overflow: 'hidden', }, }, [ vue.h('div', { style: { width: `${percentage}%`, height: '100%', backgroundColor: `var(--ion-color-${color})`, transition: 'width 0.3s ease', }, }), ]); }; /** * Renders a boolean value as a colored indicator */ const renderBoolean = (value, trueConfig = { color: 'success', label: 'Yes', }, falseConfig = { color: 'danger', label: 'No', }) => { const config = value ? trueConfig : falseConfig; return renderBadge(config.label, { color: config.color }); }; const SelectInput = vue.defineComponent({ name: 'SelectInput', props: { value: { type: Object, default: () => ({}), }, label: { type: String, default: '', }, placeholder: { type: String, default: 'Select Option', }, options: { type: Array, default: () => [], }, asyncOptions: { type: Function, required: false, }, disabled: { type: Boolean, default: false, }, multiple: { type: Boolean, default: false, }, required: { type: Boolean, default: false, }, validate: { type: Function, required: false, }, }, emits: ['select'], setup(props, { emit }) { const selectedOption = vue.ref(); const canShowOptions = vue.ref(false); const filter = vue.ref(''); const filteredOptions = vue.ref([]); const errs = vue.ref(''); const errorClass = vue.computed(() => (errs.value ? 'box-input-error' : '')); const marginTop = vue.computed(() => (props.label ? 'ion-margin-top' : '')); const tags = vue.computed(() => { if (props.multiple) return filteredOptions.value.filter(({ isChecked }) => isChecked); return selectedOption.value ? [selectedOption.value] : []; }); const showPlaceholder = vue.computed(() => { return !filter.value && isEmpty(tags.value) && !canShowOptions.value; }); const model = vue.computed({ get: () => props.value, set: value => emit('select', value), }); const setDefaults = () => { selectedOption.value = undefined; if (isEmpty(model.value)) return; if (Array.isArray(model.value) && props.multiple) { model.value.forEach(option => { const index = filteredOptions.value.findIndex(({ value }) => value === option.value); if (index === -1) { filteredOptions.value.push({ ...option, isChecked: true }); } else { filteredOptions.value[index].isChecked = true; } }); } selectedOption.value = filteredOptions.value.find(option => { if (Array.isArray(model.value)) return option.value === model.value[0].value; return option.value === model.value.value; }); if (isEmpty(selectedOption.value)) { selectedOption.value = Array.isArray(model.value) ? model.value[0] : model.value; } }; const filterOptions = async () => { const filtered = typeof props.asyncOptions === 'function' ? await props.asyncOptions(filter.value) : props.options.filter(({ label }) => label.toLowerCase().includes(filter.value.toLowerCase())); tags.value.forEach(tag => { const index = filtered.findIndex(f => f.value === tag.value); if (index === -1) filtered.push(tag); else filtered[index].isChecked = true; }); filteredOptions.value = filtered; }; const validate = async () => { if (props.required && isEmpty(model.value)) { return (errs.value = 'This field is required'); } if (typeof props.validate === 'function') { const errors = await props.validate(model.value); if (errors && errors.length) { errs.value = errors.join(', '); } } return (errs.value = ''); }; const onCloseOptions = () => { canShowOptions.value = false; model.value = props.multiple ? tags.value : !isEmpty(tags.value) ? tags.value[0] : {}; filter.value = ''; validate(); }; const showOptions = () => { if (props.disabled) return; canShowOptions.value = true; errs.value = ''; }; const selectOption = (item) => { if (!props.multiple) { selectedOption.value = item; return onCloseOptions(); } model.value = props.multiple ? tags.value : !isEmpty(tags.value) ? tags.value[0] : {}; filter.value = ''; }; const diselectOption = (tag) => { if (props.multiple) return (tag.isChecked = false); return (selectedOption.value = undefined); }; const onReset = () => { filter.value = ''; selectedOption.value = undefined; filteredOptions.value.forEach(option => (option.isChecked = false)); }; vue.watch(() => props.value, () => setDefaults()); vue.watch([filter, () => props.options, () => props.asyncOptions], async () => { await filterOptions(); setDefaults(); }); vue.onMounted(async () => { await filterOptions(); setDefaults(); addEventListener('click', (e) => { const isClosest = e.target.closest('.inner-input-box'); if (!isClosest && canShowOptions.value) { onCloseOptions(); } }); }); vue.onBeforeUnmount(() => removeEventListener('click', e => console.log(e))); return () => [ props.label && vue.h(vue$1.IonLabel, { class: 'ion-padding-bottom bold' }, props.label), vue.h('div', { class: `outer-input-box box-input ${errorClass.value} ${marginTop.value}`, }, vue.h('div', { class: 'inner-input-box' }, [ vue.h('div', { style: { display: 'flex', flexWrap: 'wrap', width: '100%' }, onClick: showOptions, }, [ ...tags.value.map(tag => vue.h(vue$1.IonChip, [ vue.h(vue$1.IonLabel, tag.label), vue.h(vue$1.IonIcon, { icon: icons.closeCircle, color: 'danger', onClick: () => diselectOption(tag), style: { zIndex: 90 }, }), ])), vue.h(vue$1.IonInput, { disabled: props.disabled, placeholder: showPlaceholder.value ? props.placeholder : '', class: 'search-input', value: filter.value, onIonInput: (e) => (filter.value = e.target.value), }), ]), canShowOptions.value && vue.h('div', { class: 'input-options' }, vue.h(vue$1.IonList, filteredOptions.value.map((option, i) => vue.h(vue$1.IonItem, { lines: i + 1 === filteredOptions.value.length ? 'none' : undefined, onClick: () => selectOption(option), }, [ props.multiple && vue.h(vue$1.IonCheckbox, { class: 'input-option-checkbox', slot: 'start', value: option.isChecked, onIonInput: (e) => (option.isChecked = e.target.checked), }), vue.h(vue$1.IonLabel, option.label), ])))), vue.h('div', { class: 'input-icon' }, [ (filter.value || tags.value.length) && vue.h(vue$1.IonIcon, { icon: icons.close, onClick: onReset }), vue.h(vue$1.IonIcon, { icon: canShowOptions.value ? icons.chevronUp : icons.chevronDown, onClick: canShowOptions.value ? onCloseOptions : showOptions, }), ].filter(Boolean)), ])), errs.value && vue.h(vue$1.IonNote, { color: 'danger' }, errs.value), ]; }, }); const DateRangePicker = vue.defineComponent({ props: { range: { type: Object, default: () => ({ startDate: '', endDate: '' }), }, }, emits: ['rangeChange'], setup(props, { emit }) { const start = vue.ref(props.range.startDate); const end = vue.ref(props.range.endDate); const cRange = vue.computed(() => ({ startDate: start.value, endDate: end.value, })); vue.watch(cRange, v => emit('rangeChange', v)); return () => vue.h(vue$1.IonGrid, { class: 'ion-no-padding ion-no-margin' }, () => vue.h(vue$1.IonRow, [ vue.h(vue$1.IonCol, { size: '6' }, () => vue.h(vue$1.IonInput, { type: 'date', class: 'box-input', value: start.value, onIonInput: (e) => (start.value = e.target.value), style: { width: '100%' }, })), vue.h(vue$1.IonCol, { size: '1', style: { display: 'flex', justifyContent: 'center ' }, }, () => vue.h(vue$1.IonIcon, { icon: icons.arrowForward, style: { fontSize: '24px', padding: '.5rem' }, })), vue.h(vue$1.IonCol, { size: '5' }, () => vue.h(vue$1.IonInput, { type: 'date', class: 'box-input', value: end.value, onIonInput: (e) => (end.value = e.target.value), style: { width: '100%' }, })), ])); }, }); const DataTable = vue.defineComponent({ name: 'DataTable', props: { rows: { type: Array, default: () => [], }, asyncRows: { type: Function, required: false, }, columns: { type: Array, default: () => [], }, actionsButtons: { type: Array, default: () => [], }, rowActionsButtons: { type: Array, default: () => [], }, customFilters: { type: Array, default: () => [], }, color: { type: String, }, config: { type: Object, default: () => ({}), }, loading: { type: Boolean, default: false, }, }, emits: ['customFilter', 'queryChange', 'drilldown'], setup(props, { emit, slots }) { const isLoading = vue.ref(false); const tableRows = vue.ref([]); const filteredRows = vue.ref([]); const totalFilteredRows = vue.computed(() => filteredRows.value.length); const totalColumns = vue.computed(() => isEmpty(props.rowActionsButtons) ? tableColumns.value.length : tableColumns.value.length + 1); const paginationPages = vue.computed(() => filters.pagination.enabled ? range(filters.pagination.start, filters.pagination.end + 1) : []); const tableColumns = vue.computed(() => props.config.showIndices ? [ { path: 'index', label: '#', initialSort: true, initialSortOrder: 'asc', }, ...props.columns, ] : props.columns); const filters = vue.reactive({ search: '', sort: [], pagination: { enabled: props.config?.pagination?.enabled ?? true, page: props.config?.pagination?.page ?? 1, pageSize: props.config?.pagination?.pageSize ?? 10, start: props.config?.pagination?.start ?? 1, end: props.config?.pagination?.end ?? 1, totalPages: props.config?.pagination?.totalPages ?? 1, visibleBtns: props.config?.pagination?.visibleBtns ?? 7, pageSizeOptions: props.config?.pagination?.pageSizeOptions ?? [ 5, 10, 20, 50, 100, ], }, }); const activeRows = vue.ref([]); const showFilterSection = vue.computed(() => { return (props.config.showSearchField !== false || props.customFilters.length > 0 || props.actionsButtons.length > 0); }); const customFiltersValues = vue.reactive(props.customFilters.reduce((acc, filter) => { acc[filter.id] = filter.value; return acc; }, {})); const init = async () => { isLoading.value = true; tableRows.value = await getRows(props.rows, props.config.showIndices || false, props.asyncRows); filters.sort = initializeSortQueries(tableColumns.value); handleFilters(filters.pagination); isLoading.value = false; }; const handleFilters = (paginator, search, sortColumn) => { filters.search = search ?? ''; if (sortColumn) filters.sort = updateSortQueries(filters.sort, sortColumn); const _filteredRows = filterRows(tableRows.value, filters.search); filteredRows.value = sortRows(_filteredRows, filters.sort); if (filters.pagination.enabled) { filters.pagination = calculatePageRange(paginator, totalFilteredRows.value, paginationPages.value); activeRows.value = getActiveRows(filteredRows.value, filters.pagination); } else { activeRows.value = filteredRows.value; } }; vue.watch(customFiltersValues, () => { if (props.config.showSubmitButton === false) { emit('customFilter', customFiltersValues); } }, { immediate: true, deep: true, }); vue.watch(() => props.rows, () => init(), { deep: true, immediate: true }); vue.onMounted(() => init()); const renderSearchbar = () => { if (props.config.showSearchField !== false) { return vue.h(vue$1.IonCol, { size: '4' }, () => [ vue.h(vue$1.IonSearchbar, { placeholder: 'search here...', class: 'box ion-no-padding', value: filters.search, onIonInput: e => handleFilters({ ...filters.pagination, page: 1 }, e.target.value), }), ]); } return null; }; const renderSelectFilter = (filter) => vue.h(vue$1.IonCol, { size: `${filter.gridSize}` || '3' }, () => vue.h(SelectInput, { options: filter.options, placeholder: filter.label || filter.placeholder || 'Select Item', value: filter.value, multiple: filter.multiple, onSelect: (v) => { if (typeof filter.onUpdate === 'function') filter.onUpdate(v); customFiltersValues[filter.id] = v; }, })); const renderDateRangeFilter = (filter) => vue.h(vue$1.IonCol, { size: `${filter.gridSize}` || '6' }, () => vue.h(DateRangePicker, { range: vue.computed(() => filter.value || { startDate: '', endDate: '' }) .value, onRangeChange: async (newRange) => { if (typeof filter.onUpdate === 'function') filter.onUpdate(newRange); customFiltersValues[filter.id] = newRange; }, })); const renderDefaultFilter = (filter) => vue.h(vue$1.IonCol, { size: '4' }, () => vue.h(vue$1.IonInput, { class: 'box', type: filter.type, placeholder: filter.placeholder, value: vue.computed(() => filter.value || '').value, onIonInput: async (e) => { const value = e.target.value; if (typeof filter.onUpdate === 'function') filter.onUpdate(value); customFiltersValues[filter.id] = value; }, })); const renderCustomFilters = () => { return props.customFilters.map(filter => { if (filter.slotName && slots[filter.slotName]) { const slotFn = slots[filter.slotName]; return vue.h(vue$1.IonCol, { size: `${filter.gridSize || '3'}` }, () => slotFn && slotFn({ filter })); } if (filter.type === 'dateRange') return renderDateRangeFilter(filter); if (filter.type === 'select') return renderSelectFilter(filter); return renderDefaultFilter(filter); }); }; const renderSubmitButton = () => { if (props.customFilters.length > 0 && props.config.showSubmitButton !== false) { return vue.h(vue$1.IonCol, { size: '2' }, () => [ vue.h(vue$1.IonButton, { color: 'primary', class: 'ion-no-margin', onClick: () => emit('customFilter', customFiltersValues), }, 'Submit'), ]); } return null; }; const renderActionsButtons = () => { return props.actionsButtons.map(btn => vue.h(vue$1.IonButton, { class: 'ion-float-right', color: btn.color || 'primary', size: btn.size ?? 'default', onClick: () => btn.action(activeRows.value, tableRows.value, filters, tableColumns.value), }, btn.icon ? vue.h(vue$1.IonIcon, { icon: btn.icon }) : btn.label)); }; const renderFilterSection = () => { return (showFilterSection.value && vue.h(vue$1.IonGrid, { style: { width: '100%', fontWeight: 500 } }, () => vue.h(vue$1.IonRow, () => [ vue.h(vue$1.IonCol, { size: '7' }, () => vue.h(vue$1.IonRow, () => [ renderSearchbar(), ...renderCustomFilters(), renderSubmitButton(), ])), vue.h(vue$1.IonCol, { size: '5', class: 'ion-padding-end' }, () => renderActionsButtons()), ]))); }; const renderPagination = () => { return (filters.pagination.enabled && filters.pagination.totalPages > 1 && vue.h(vue$1.IonGrid, { style: { width: '100%', textAlign: 'left', color: 'black' }, class: 'ion-padding', }, () => vue.h(vue$1.IonRow, [ vue.h(vue$1.IonCol, { size: '4' }, () => renderPaginationControls()), vue.h(vue$1.IonCol, { size: '5', class: 'text-center' }, () => [ renderGoToPageInput(), renderItemsPerPageSelect(), ]), vue.h(vue$1.IonCol, { size: '3' }, () => renderPaginationInfo()), ]))); }; const renderPaginationControls = () => { const { page, start, end, totalPages } = filters.pagination; const handleClick = (page) => { return handleFilters({ ...filters.pagination, page }); }; const controls = [ renderPageControlButton({ icon: icons.caretBack, disabled: page === start, onClick: () => handleClick(page - 1), }), ]; if (start > 3) { controls.push(renderPageControlButton({ label: '1', onClick: () => handleClick(1) })); controls.push(renderPageControlButton({ label: '...', disabled: true })); } paginationPages.value.forEach((label) => { controls.push(renderPageControlButton({ label, onClick: () => handleClick(label) })); }); if (end < totalPages - 2) { controls.push(renderPageControlButton({ label: '...', disabled: true })); controls.push(renderPageControlButton({ label: totalPages, onClick: () => handleClick(totalPages), })); } controls.push(renderPageControlButton({ icon: icons.caretForward, disabled: page === end || isEmpty(filteredRows.value), onClick: () => handleClick(page + 1), })); return controls; }; const renderPageControlButton = ({ disabled, label, icon, onClick, }) => { return vue.h(vue$1.IonButton, { onClick, disabled, size: 'small', color: filters.pagination.page === label ? 'primary' : 'light', }, icon ? vue.h(vue$1.IonIcon, { icon }) : label || 'Button'); }; const renderGoToPageInput = () => { return vue.h(vue$1.IonItem, { class: 'box go-to-input ion-hide-xl-down', lines: 'none' }, [ vue.h(vue$1.IonLabel, { class: 'ion-margin-end' }, 'Go to page'), vue.h(vue$1.IonInput, { type: 'number', min: 1, max: filters.pagination.totalPages, value: filters.pagination.page, style: { paddingRight: '15px' }, debounce: 500, onIonChange: e => { const page = e.target.value; if (page > 0 && page <= filters.pagination.totalPages) { handleFilters({ ...filters.pagination, page }); } }, }), ]); }; const renderItemsPerPageSelect = () => { return vue.h(vue$1.IonItem, { class: 'box per-page-input', lines: 'none' }, [ vue.h(vue$1.IonLabel, 'Items per page'), vue.h(vue$1.IonSelect, { value: filters.pagination.pageSize, interface: 'popover', onIonChange: e => handleFilters({ ...filters.pagination, pageSize: e.target.value, page: 1, }), }, [ ...filters.pagination.pageSizeOptions.map(value => vue.h(vue$1.IonSelectOption, { value, key: value }, value)), vue.h(vue$1.IonSelectOption, { value: totalFilteredRows.value }, 'All'), ]), ]); }; const renderPaginationInfo = () => { return vue.h(vue$1.IonCol, { size: '4', class: 'pagination-info' }, vue.computed(() => { return buildPaginationInfo(filters.pagination, totalFilteredRows.value); }).value); }; const renderTableHeader = () => vue.h('thead', { class: props.color || '' }, vue.h('tr', [ ...tableColumns.value.map(column => renderTableHeaderCell(column)), !isEmpty(props.rowActionsButtons) && vue.h('th', 'Actions'), ])); const renderSortIcon = (column) => { const style = { marginRight: '5px', float: 'right', cursor: 'pointer' }; const icon = vue.computed(() => { const query = filters.sort.find(s => s.column.path === column.path); return !query ? icons.swapVertical : query.order == 'asc' ? icons.arrowUp : icons.arrowDown; }); return vue.h(vue$1.IonIcon, { icon: icon.value, style }); }; const renderTableHeaderCell = (column) => { const style = { minWidth: /index/i.test(column.path) ? '80px' : '190px', ...column.thStyles, }; const onClick = () => handleFilters(filters.pagination, filters.search, column); return vue.h('th', { key: column.label, style, class: column.thClasses?.join(' '), onClick, }, [ vue.h('span', column.label), column.sortable !== false && renderSortIcon(column), ]); }; const renderTableBody = () => { return vue.h('tbody', { class: 'table-body' }, isLoading.value || props.loading ? renderLoadingRows() : isEmpty(filteredRows.value) ? renderNoDataRow() : renderDataRows()); }; const renderLoadingRows = () => { return range(0, 9).map((i) => vue.h('tr', { key: i }, vue.h('td', { colspan: totalColumns.value }, vue.h(vue$1.IonSkeletonText, { animated: true, style: { width: '100%' } })))); }; const renderNoDataRow = () => { return vue.h('tr', vue.h('td', { colspan: totalColumns.value }, vue.h('div', { class: 'no-data-table' }, 'No data available'))); }; const handleRowClick = async (row, rowIndex) => { const defaultActionBtn = props.rowActionsButtons.find(btn => btn.default); if (defaultActionBtn) await defaultActionBtn.action(row, rowIndex); }; const renderDataRows = () => { return activeRows.value.map((row, rowIndex) => vue.h('tr', { key: row, onClick: () => handleRowClick(row, rowIndex) }, [ ...renderDataRowCells(row), renderRowActionCells(row, rowIndex), ])); }; const renderDataRowCells = (row) => { return tableColumns.value.map((column, key) => { // Dynamic classes based on column configuration const dynamicClasses = typeof column.tdClasses === 'function' ? column.tdClasses(get(row, column.path), row) : (column.tdClasses ?? []); const classes = ['data-cell', ...dynamicClasses].join(' '); // Dynamic styles based on column configuration const dynamicStyles = typeof column.tdStyles === 'function' ? column.tdStyles(get(row, column.path), row) : column.tdStyles; return vue.h('td', { key, class: classes, style: dynamicStyles }, renderCellContent(row, column)); }); }; const renderCellContent = (row, column) => { let value = get(row, column.path); // Check if there's a slot for this column if (column.slotName && slots[column.slotName]) { const slotFn = slots[column.slotName]; return slotFn && slotFn({ value, row, column }); } // Check if there's a custom renderer if (typeof column.customRenderer === 'function') { return column.customRenderer(value, row, column); } // Check if there's a Vue component to render if (column.component) { const props = typeof column.componentProps === 'function' ? column.componentProps(value, row) : { value, row, column }; return vue.h(column.component, props); } // Apply formatter if provided if (typeof column.formatter === 'function' && value !== null && value !== undefined) { value = column.formatter(value, row); } // Handle drillable content if (isDrillable(column, value, row)) { const content = renderSafeContent(value); return vue.h('a', { onClick: (e) => { e.preventDefault(); e.stopPropagation(); emit('drilldown', { column, row }); }, }, content); } else { // Render content (HTML or plain text) return renderSafeContent(value); } }; // Helper function to render content (HTML or plain text) const renderSafeContent = (value) => { if (isHtmlString(value) && typeof value === 'string') { const sanitizedHtml = sanitizeHtml(value); return vue.h('span', { innerHTML: sanitizedHtml }); } return renderCellValue(value); }; const renderCellValue = (value) => { return Array.isArray(value) ? value.length : value; }; const renderRowActionCells = (row, rowIndex) => { if (!isEmpty(props.rowActionsButtons)) { return vue.h('td', props.rowActionsButtons.map(btn => { const canShowBtn = typeof btn.condition === 'function' ? btn.condition(row) : true; return canShowBtn ? renderRowActionButton(row, rowIndex, btn) : null; })); } return null; }; const renderRowActionButton = (row, rowIndex, btn) => { return vue.h(vue$1.IonButton, { key: btn.icon, size: btn.size ?? 'small', color: btn.color || 'primary', onClick: () => btn.action(row, rowIndex), }, btn.icon ? () => vue.h(vue$1.IonIcon, { icon: btn.icon }) : btn.label || 'Button'); }; const renderTable = () => { return vue.h('div', { class: 'responsive-table ion-padding-horizontal' }, vue.h('table', { class: 'table bordered-table striped-table' }, [ renderTableHeader(), renderTableBody(), ])); }; return () => [renderFilterSection(), renderTable(), renderPagination()]; }, }); // The Install function used by Vue to register the plugin const VTable = { install(app, options) { app.config.globalProperties.$globalTableOptions = options; app.provide('globalTableOptions', options); app.component('DataTable', DataTable); }, }; exports.DataTable = DataTable; exports.VTable = VTable; exports.buildPaginationInfo = buildPaginationInfo; exports.calculatePageRange = calculatePageRange; exports.filterRows = filterRows; exports.get = get; exports.getActiveRows = getActiveRows; exports.getRows = getRows; exports.initializeSortQueries = initializeSortQueries; exports.isDrillable = isDrillable; exports.isEmpty = isEmpty; exports.isHtmlString = isHtmlString; exports.orderBy = orderBy; exports.range = range; exports.renderBadge = renderBadge; exports.renderBoolean = renderBoolean; exports.renderChip = renderChip; exports.renderChipList = renderChipList; exports.renderHtml = renderHtml; exports.renderProgress = renderProgress; exports.renderStatus = renderStatus; exports.sanitizeHtml = sanitizeHtml; exports.sortRows = sortRows; exports.updateSortQueries = updateSortQueries; //# sourceMappingURL=index.js.map