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