@uniquedj95/vtable
Version:
An advanced datatable for Ionic vue framework
1,157 lines (1,149 loc) • 46 kB
JavaScript
;
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