@oruga-ui/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
1 lines • 93.8 kB
Source Map (JSON)
{"version":3,"file":"table.cjs","sources":["../../src/components/table/TableMobileSort.vue","../../src/components/table/TableColumn.vue","../../src/components/table/TablePagination.vue","../../src/components/table/Table.vue","../../src/components/table/index.ts"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T\">\nimport { computed, watch, ref, type PropType } from \"vue\";\n\nimport OButton from \"@/components/button/Button.vue\";\nimport OSelect from \"@/components/select/Select.vue\";\nimport OIcon from \"@/components/icon/Icon.vue\";\nimport OField from \"@/components/field/Field.vue\";\n\nimport type { TableColumnItem } from \"./types\";\nimport type { ClassBind } from \"@/types\";\n\ndefineOptions({\n isOruga: true,\n name: \"OTableMobileSort\",\n configField: \"table\",\n});\n\nconst props = defineProps({\n currentSortColumn: {\n type: Object as PropType<TableColumnItem<T>>,\n default: undefined,\n },\n columns: {\n type: Array as PropType<TableColumnItem<T>[]>,\n default: undefined,\n },\n placeholder: { type: String, default: undefined },\n iconPack: { type: String, default: undefined },\n sortIcon: { type: String, default: \"arrow-up\" },\n sortIconSize: { type: String, default: \"small\" },\n isAsc: { type: Boolean, default: false },\n mobileSortClasses: { type: Array as PropType<ClassBind[]>, required: true },\n});\n\nconst emits = defineEmits<{\n sort: [column: TableColumnItem<T>, event: Event];\n}>();\n\nconst mobileSort = ref<string | undefined>(props.currentSortColumn?.identifier);\n\nconst showPlaceholder = computed(\n () =>\n !props.columns ||\n props.columns.every((column) => column.identifier !== mobileSort.value),\n);\n\nconst sortableColumns = computed(() =>\n props.columns ? props.columns.filter((c) => c.sortable) : [],\n);\n\nconst isCurrentSort = computed(\n () => props.currentSortColumn?.identifier === mobileSort.value,\n);\n\nwatch(mobileSort, (value) => {\n if (props.currentSortColumn?.identifier === value) return;\n sort(new Event(\"sort\"));\n});\n\nwatch(\n () => props.currentSortColumn,\n (column) => {\n mobileSort.value = column?.identifier;\n },\n);\n\nfunction sort(event: Event): void {\n const column = sortableColumns.value.find(\n (column) => column.identifier === mobileSort.value,\n );\n if (!column) return;\n emits(\"sort\", column, event);\n}\n</script>\n\n<template>\n <div :class=\"mobileSortClasses\">\n <o-field addons>\n <o-select v-model=\"mobileSort\" expanded>\n <template v-if=\"placeholder\">\n <option\n v-show=\"showPlaceholder\"\n :value=\"{}\"\n selected\n disabled\n hidden>\n {{ placeholder }}\n </option>\n </template>\n <option\n v-for=\"(column, index) in sortableColumns\"\n :key=\"column.field || index\"\n :value=\"column.identifier\">\n {{ column.label }}\n </option>\n </o-select>\n\n <o-button @click=\"sort($event)\">\n <o-icon\n v-show=\"isCurrentSort\"\n :icon=\"sortIcon\"\n :pack=\"iconPack\"\n :size=\"sortIconSize\"\n :rotation=\"!isAsc ? 180 : 0\" />\n </o-button>\n </o-field>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"T, K extends string\">\nimport { computed, getCurrentInstance } from \"vue\";\n\nimport { defineClasses, useProviderChild } from \"@/composables\";\nimport { toCssDimension } from \"@/utils/helpers\";\n\nimport type { TableColumnComponent, TableComponent } from \"./types\";\nimport type { TableColumnProps } from \"./props\";\n\n/**\n * Define a column used by the table component.\n * @displayName Table Column\n */\ndefineOptions({\n isOruga: true,\n name: \"OTableColumn\",\n configField: \"table\",\n});\n\nconst props = withDefaults(defineProps<TableColumnProps<T, K>>(), {\n label: undefined,\n field: undefined,\n formatter: undefined,\n subheading: undefined,\n width: undefined,\n numeric: false,\n position: undefined,\n searchable: false,\n sortable: false,\n hidden: false,\n sticky: false,\n headerSelectable: false,\n customSort: undefined,\n customSearch: undefined,\n thAttrs: undefined,\n tdAttrs: undefined,\n});\n\nconst style = computed(() => ({\n width: toCssDimension(props.width),\n}));\n\nconst isHeaderUnselectable = computed(\n () => !props.headerSelectable && props.sortable,\n);\n\nconst vm = getCurrentInstance();\n\n// provided data is a computed ref to ensure reactivity\nconst providedData = computed<TableColumnComponent<T>>(() => ({\n ...props,\n $el: vm!.proxy!,\n $slots: vm!.slots,\n style: style.value,\n thClasses: thClasses.value,\n tdClasses: tdClasses.value,\n}));\n\n/** inject functionalities and data from the parent component */\nconst { parent, item } = useProviderChild<\n TableComponent,\n TableColumnComponent<T>\n>({ data: providedData });\n\n// --- Computed Component Classes ---\n\nconst thClasses = defineClasses(\n [\n \"thCurrentSortClass\",\n \"o-table__th-current-sort\",\n null,\n computed(() => parent.value?.isColumnSorted(item.value)),\n ],\n [\n \"thSortableClass\",\n \"o-table__th--sortable\",\n null,\n computed(() => props.sortable),\n ],\n [\n \"thUnselectableClass\",\n \"o-table__th--unselectable\",\n null,\n isHeaderUnselectable,\n ],\n [\n \"thPositionClass\",\n \"o-table__th--\",\n computed(() => props.position),\n computed(() => !!props.position),\n ],\n [\n \"thStickyClass\",\n \"o-table__th--sticky\",\n null,\n computed(() => props.sticky),\n ],\n);\n\nconst tdClasses = defineClasses(\n [\n \"tdPositionClass\",\n \"o-table__td--\",\n computed(() => props.position),\n computed(() => !!props.position),\n ],\n [\n \"tdStickyClass\",\n \"o-table__td--sticky\",\n null,\n computed(() => props.sticky),\n ],\n);\n\n// --- SLOTS TYPED OBJECTS ---\n\n// these properties are just for type addings\n// slot props will be set in Table.vue\nconst row = {} as any;\nconst column = {} as TableColumnProps<T, K>;\nconst index = 0;\nconst toggle = () => {};\nconst filters = {} as Record<string, string>;\n</script>\n\n<template>\n <span data-oruga=\"table-column\" :data-id=\"`table-${item.identifier}`\">\n {{ label }}\n\n <!--\n Do not render these slots here.\n These are only for documentation purposes.\n Slots are defined in table component.\n -->\n <template v-if=\"false\">\n <!--\n @slot Default Slot\n @binding {T} row - row data \n @binding {TableColumn} column - column definition \n @binding {number} index - row index \n @binding {number} colindex - column index \n @binding {(): void} toggle-details - toggle details function \n -->\n <slot\n :row=\"row\"\n :column=\"column\"\n :index=\"index\"\n :colindex=\"index\"\n :toggle-details=\"toggle\" />\n <!--\n @slot Override header label \n @binding {TableColumn} column - column definition \n @binding {number} index - column index \n -->\n <slot name=\"header\" :column=\"column\" :index=\"index\" />\n <!--\n @slot Override subheading label \n @binding {TableColumn} column - column definition \n @binding {number} index - column index \n -->\n <slot name=\"subheading\" :column=\"column\" :index=\"index\" />\n\n <!--\n @slot Override searchable input \n @binding {TableColumn} column - column definition \n @binding {number} index - column index \n @binding {object} filters - active filters object\n -->\n <slot\n name=\"searchable\"\n :column=\"column\"\n :index=\"index\"\n :filters=\"filters\" />\n </template>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport type { PropType } from \"vue\";\n\nimport OPagination from \"@/components/pagination/Pagination.vue\";\n\nimport type { ComponentClass } from \"@/types\";\n\ndefineOptions({\n isOruga: true,\n name: \"OTablePagination\",\n configField: \"table\",\n inheritAttrs: false,\n});\n\ndefineProps({\n current: { type: Number, default: undefined },\n paginated: { type: Boolean, default: false },\n rootClass: {\n type: [String, Array, Object] as PropType<ComponentClass>,\n default: undefined,\n },\n});\n\nconst emits = defineEmits<{\n /**\n * current prop two-way binding\n * @param value {number} updated current prop\n */\n \"update:current\": [value: number];\n /**\n * on current change event\n * @param value {number} current value\n */\n change: [event: number];\n}>();\n\nconst currentPage = defineModel<number>(\"current\");\n\n/** paginator change listener */\nfunction pageChanged(page: number): void {\n const newPage = page > 0 ? page : 1;\n currentPage.value = newPage;\n emits(\"change\", newPage);\n}\n</script>\n\n<template>\n <div :class=\"rootClass\">\n <div>\n <slot />\n </div>\n\n <div>\n <o-pagination\n v-if=\"paginated\"\n v-bind=\"$attrs\"\n :current=\"currentPage\"\n @change=\"pageChanged\" />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"T\">\nimport {\n computed,\n ref,\n watch,\n onMounted,\n nextTick,\n useSlots,\n toValue,\n useTemplateRef,\n toRaw,\n triggerRef,\n type MaybeRefOrGetter,\n} from \"vue\";\n\nimport OCheckbox from \"@/components/checkbox/Checkbox.vue\";\nimport OIcon from \"@/components/icon/Icon.vue\";\nimport OInput from \"@/components/input/Input.vue\";\nimport OLoading from \"@/components/loading/Loading.vue\";\nimport OSlotComponent from \"@/components/utils/SlotComponent\";\n\nimport OTableMobileSort from \"./TableMobileSort.vue\";\nimport OTableColumn from \"./TableColumn.vue\";\nimport OTablePagination from \"./TablePagination.vue\";\n\nimport { getDefault } from \"@/utils/config\";\nimport {\n getValueByPath,\n toCssDimension,\n escapeRegExpChars,\n removeDiacriticsFromString,\n sortBy,\n isDefined,\n getPropertyValue,\n} from \"@/utils/helpers\";\nimport {\n defineClasses,\n getActiveClasses,\n useProviderParent,\n useMatchMedia,\n useDebounce,\n isOptionViable,\n filterOptionsItems,\n useSequentialId,\n} from \"@/composables\";\n\nimport type { ClassBind, DeepKeys } from \"@/types\";\nimport type {\n TableColumn,\n TableRow,\n TableColumnItem,\n TableColumnComponent,\n TableComponent,\n} from \"./types\";\nimport type { TableProps } from \"./props\";\n\n/**\n * Tabulated data are sometimes needed, it's even better when it's responsive.\n * @displayName Table\n * @requires ./TableColumn.vue\n * @style _table.scss\n */\ndefineOptions({\n isOruga: true,\n name: \"OTable\",\n configField: \"table\",\n inheritAttrs: false,\n});\n\nconst props = withDefaults(defineProps<TableProps<T>>(), {\n override: undefined,\n data: undefined,\n columns: undefined,\n rowKey: () => getDefault(\"table.rowKey\"),\n rowClass: getDefault(\"table.rowClass\", () => \"\"),\n thAttrs: undefined,\n tdAttrs: undefined,\n customCompare: undefined,\n bordered: () => getDefault(\"table.bordered\", false),\n striped: () => getDefault(\"table.striped\", false),\n narrowed: () => getDefault(\"table.narrowed\", false),\n hoverable: () => getDefault(\"table.hoverable\", false),\n selected: undefined,\n selectable: () => getDefault(\"table.selectable\", false),\n isRowSelectable: () => true,\n showHeader: () => getDefault(\"table.showHeader\", true),\n draggable: false,\n draggableColumn: false,\n scrollable: undefined,\n stickyHeader: false,\n height: undefined,\n checkable: false,\n stickyCheckbox: false,\n checkableHeader: true,\n checkedRows: () => [],\n checkboxPosition: () => getDefault(\"table.checkboxPosition\", \"left\"),\n checkboxVariant: () => getDefault(\"table.checkboxVariant\"),\n isRowChecked: undefined,\n isRowCheckable: getDefault(\"table.isRowCheckable\", () => true),\n backendSorting: () => getDefault(\"table.backendSorting\", false),\n defaultSort: () => getDefault(\"table.defaultSort\"),\n defaultSortDirection: () => getDefault(\"table.defaultSortDirection\", \"asc\"),\n sortIcon: () => getDefault(\"table.sortIcon\", \"arrow-up\"),\n sortIconSize: () => getDefault(\"table.sortIconSize\", \"small\"),\n iconPack: () => getDefault(\"table.iconPack\"),\n detailed: false,\n detailedRows: () => [],\n isDetailedVisible: getDefault(\"table.isDetailedVisible\", () => true),\n showDetailIcon: () => getDefault(\"table.showDetailIcon\", true),\n detailIcon: () => getDefault(\"table.detailIcon\", \"chevron-right\"),\n customDetailRow: false,\n detailTransition: () => getDefault(\"table.detailTransition\", \"slide\"),\n paginated: () => getDefault(\"table.paginated\", false),\n backendPagination: false,\n total: 0,\n currentPage: 1,\n perPage: () => getDefault(\"table.perPage\", 20),\n paginationPosition: () => getDefault(\"table.paginationPosition\", \"bottom\"),\n paginationSize: () => getDefault(\"table.paginationSize\", \"small\"),\n paginationRounded: () => getDefault(\"table.paginationRounded\", false),\n paginationSimple: () => getDefault(\"table.paginationSimple\", false),\n paginationOrder: () => getDefault(\"table.paginationOrder\"),\n paginationRangeBefore: undefined,\n paginationRangeAfter: undefined,\n backendFiltering: () => getDefault(\"table.backendFiltering\", false),\n filtersIcon: () => getDefault(\"table.filterIcon\"),\n filtersPlaceholder: () => getDefault(\"table.filterPlaceholder\"),\n filtersEvent: \"\",\n filterDebounce: () => getDefault(\"table.filterDebounce\", 300),\n emptyLabel: () => getDefault(\"table.emptyLabel\"),\n emptyIcon: () => getDefault(\"table.emptyIcon\"),\n emptyIconSize: () => getDefault(\"table.emptyIconSize\"),\n loading: false,\n loadingIcon: () => getDefault(\"table.loadingIcon\", \"loading\"),\n loadingLabel: () => getDefault(\"table.loadingLabel\"),\n mobileBreakpoint: () => getDefault(\"table.mobileBreakpoint\"),\n mobileCards: () => getDefault(\"table.mobileCards\", true),\n mobileSortPlaceholder: () => getDefault(\"table.mobileSortPlaceholder\"),\n ariaNextLabel: () => getDefault(\"table.ariaNextLabel\"),\n ariaPreviousLabel: () => getDefault(\"table.ariaPreviousLabel\"),\n ariaPageLabel: () => getDefault(\"table.ariaPageLabel\"),\n ariaCurrentLabel: () => getDefault(\"table.ariaCurrentLabel\"),\n});\n\nconst emits = defineEmits<{\n /**\n * currentPage prop two-way binding\n * @param value {number} updated currentPage prop\n */\n \"update:currentPage\": [value: number];\n /**\n * on pagination page change event\n * @param page {number} updated page\n */\n \"page-change\": [page: number];\n /**\n * select prop two-way binding\n * @param value {T} updated select prop\n */\n \"update:selected\": [value: T];\n /**\n * on row select event\n * @param newRow {T} new select value\n * @param oldRow {T} old select value\n */\n select: [newRow: T, oldRow: T];\n /**\n * on row checked event\n * @param value {T[]} all checked rows\n * @param row {T} row data\n */\n check: [value: T[], row: T];\n /**\n * on all rows checked event\n * @param value {T[]} all checked rows\n */\n \"check-all\": [value: T[]];\n /**\n * checkedRows prop two-way binding\n * @param value {T[]} updated checkedRows prop\n */\n \"update:checkedRows\": [value: T[]];\n /**\n * on column sort change event\n * @param column {TableColumn} column data\n * @param direction {string} 'asc' or 'desc'\n * @param event {Event} native event\n */\n sort: [column: TableColumn<T>, direction: \"asc\" | \"desc\", event: Event];\n /**\n * on filter change event\n * @param filters {object} filter object\n */\n \"filters-change\": [value: Record<string, string>];\n /**\n * on native filter event based on props filtersEvent\n * @param filtersEvent {string} props filtersEvent value\n * @param filters {object} filter object\n * @param event {Event} native event\n */\n \"filters-event\": [\n filtersEvent: string,\n filters: Record<string, string>,\n event: Event,\n ];\n /**\n * detailedRows prop two-way binding\n * @param value {T[]} updated detailedRows prop\n */\n \"update:detailedRows\": [value: T[]];\n /**\n * on details open event\n * @param row {T} row data\n */\n \"details-open\": [row: T];\n /**\n * on details close event\n * @param row {T} row data\n */\n \"details-close\": [row: T];\n /**\n * on row click event\n * @param row {T} row data\n * @param index {number} index of clicked row\n * @param event {Event} native click event\n */\n click: [row: T, index: number, event: Event];\n /**\n * on row double click event\n * @param row {T} row data\n * @param index {number} index of clicked row\n * @param event {Event} native click event\n */\n dblclick: [row: T, index: number, event: Event];\n /**\n * on row right click event\n * @param row {T} row data\n * @param index {number} index of clicked row\n * @param event {Event} native contextmenu event\n */\n contextmenu: [row: T, index: number, event: Event];\n /**\n * on row mouseenter event\n * @param row {T} row data\n * @param index {number} index of clicked row\n * @param event {Event} native mouseenter event\n */\n mouseenter: [row: T, index: number, event: Event];\n /**\n * on row mouseleave event\n * @param row {T} row data\n * @param index {number} index of clicked row\n * @param event {Event} native mouseleave event\n */\n mouseleave: [row: T, index: number, event: Event];\n /**\n * on cell click event\n * @param row {T} row data\n * @param column {TableColumn} column data\n * @param index {number} row index\n * @param colindex {number} column index\n * @param event {Event} native click event\n */\n \"cell-click\": [\n row: T,\n column: TableColumn<T>,\n index: number,\n colindex: number,\n event: Event,\n ];\n /**\n * on row dragstart event\n * @param row {T} row data\n * @param index {number} index of draged row\n * @param event {DragEvent} native dragstart event\n */\n dragstart: [row: T, index: number, event: DragEvent];\n /**\n * on row dragend event\n * @param row {T} row data\n * @param index {number} index of draged row\n * @param event {DragEvent} native dragend event\n */\n dragend: [row: T, index: number, event: DragEvent];\n /**\n * on row drop event\n * @param row {T} row data\n * @param index {number} index of draged row\n * @param event {DragEvent} native drop event\n */\n drop: [row: T, index: number, event: DragEvent];\n /**\n * on row dragleave event\n * @param row {T} row data\n * @param index {number} index of draged row\n * @param event {DragEvent} native dragleave event\n */\n dragleave: [row: T, index: number, event: DragEvent];\n /**\n * on row dragover event\n * @param row {T} row data\n * @param index {number} index of draged row\n * @param event {DragEvent} native dragover event\n */\n dragover: [row: T, index: number, event: DragEvent];\n /**\n * on column columndragstart event\n * @param column {TableColumn} column data\n * @param index {number} index of draged column\n * @param event {DragEvent} native columndragstart event\n */\n columndragstart: [column: TableColumn<T>, index: number, event: DragEvent];\n /**\n * on column columndragend event\n * @param column {TableColumn} column data\n * @param index {number} index of draged column\n * @param event {DragEvent} native columndragend event\n */\n columndragend: [column: TableColumn<T>, index: number, event: DragEvent];\n /**\n * on column columndrop event\n * @param column {TableColumn} column data\n * @param index {number} index of draged column\n * @param event {DragEvent} native columndrop event\n */\n columndrop: [column: TableColumn<T>, index: number, event: DragEvent];\n /**\n * on column columndragleave event\n * @param column {TableColumn} column data\n * @param index {number} index of draged column\n * @param event {DragEvent} native columndragleave event\n */\n columndragleave: [column: TableColumn<T>, index: number, event: DragEvent];\n /**\n * on column columndragover event\n * @param column {TableColumn} column data\n * @param index {number} index of draged column\n * @param event {DragEvent} native columndragover event\n */\n columndragover: [column: TableColumn<T>, index: number, event: DragEvent];\n}>();\n\nconst slots = useSlots();\n\nconst { isMobile } = useMatchMedia(props.mobileBreakpoint);\n\nconst isMobileActive = computed(() => props.mobileCards && isMobile.value);\n\nconst slotsRef = useTemplateRef(\"slotsWrapper\");\n\n// provided data is a computed ref to ensure reactivity\nconst provideData = computed<TableComponent>(() => ({\n isColumnSorted,\n}));\n\n/** provide functionalities and data to child item components */\nconst { childItems } = useProviderParent<TableColumnComponent<T>>({\n rootRef: slotsRef,\n data: provideData,\n});\n\n// #region --- TABLE COLUMNS ---\n\n/** all defined columns */\nconst tableColumns = computed<TableColumnItem<T>[]>(() => {\n if (!childItems.value.length) return [];\n return childItems.value.map((columnItem) => {\n const column = toValue(columnItem.data!);\n\n // create additional th attrs data\n let thAttrsData =\n typeof props.thAttrs === \"function\" ? props.thAttrs(column) : {};\n thAttrsData = Object.assign(thAttrsData, column.thAttrs);\n\n // create additional td attrs data\n const tdAttrsData = (props.data ?? []).map((data) => {\n const tdAttrs =\n typeof props.tdAttrs === \"function\"\n ? props.tdAttrs(data, column)\n : {};\n return Object.assign(tdAttrs, column.tdAttrs);\n });\n\n return {\n ...column,\n value: column,\n index: columnItem.index,\n identifier: columnItem.identifier,\n thAttrsData: thAttrsData,\n tdAttrsData: tdAttrsData,\n };\n });\n});\n\n/** total columns count */\nconst columnCount = computed(() => {\n let i = tableColumns.value.length;\n if (showDetailRowIcon.value) i++;\n if (props.checkable) i++;\n return i;\n});\n\n/** aria-colindex start value for ths */\nconst ariaColIndexStart = computed(() => {\n let i = 1;\n if (showDetailRowIcon.value) i++;\n if (props.checkable && props.checkboxPosition === \"left\") i++;\n return i;\n});\n\n/** check if table has subheadings */\nconst hasSubheadings = computed(() => {\n if (slots.subheading) return true;\n return tableColumns.value.some((column) => !!column.subheading);\n});\n\n/** check if table is scrollable */\nconst isScrollable = computed(() => {\n if (props.scrollable) return true;\n return tableColumns.value.some((column) => column.sticky);\n});\n\n// #endregion --- TABLE COLUMNS ---\n\n// #region --- TABLE ROWS ---\n\nconst tableCurrentPage = defineModel<number>(\"currentPage\", { default: 1 });\n\n// recompute table rows visibility on page change or data change\nwatch(\n [\n tableCurrentPage,\n () => props.perPage,\n () => props.data,\n () => props.paginated,\n ],\n () => filterTableRows(),\n);\n\n// create a unique id sequence\nconst { nextSequence } = useSequentialId();\n\n/** all defined data elements as normalized options with a unique key*/\nconst tableRows = computed<TableRow<T>[]>(() => {\n if (!props.data) return [];\n return props.data.map((value: T, idx: number) => ({\n label: \"row \" + idx, // row display label\n value: toValue(value), // normalizes wrapped ref values\n index: idx, // row index\n key:\n // if no key is given and data is object, create unique row id for each row\n String(getValueByPath(value, props.rowKey) || nextSequence()),\n }));\n});\n\n/** visible rows which are filtered by viability */\nconst availableRows = computed<TableRow<T>[]>(() =>\n tableRows.value.filter(isOptionViable),\n);\n\n/** applies visability filter of reactive tableRows */\nfunction filterTableRows(): void {\n // calculate pagination information\n const currentPage = tableCurrentPage.value;\n const perPage = Number(props.perPage);\n const pageStart = (currentPage - 1) * perPage;\n const pageEnd = pageStart + perPage;\n\n // update hidden state for each row\n filterOptionsItems(tableRows, (row, idx) => {\n // if paginated not backend paginated, paginate row\n if (props.paginated && !props.backendPagination) {\n // if not only one page and not on active page\n if (\n tableRows.value.length > perPage &&\n (idx < pageStart || idx >= pageEnd)\n )\n // return row is invisible (filtered out)\n return true;\n }\n\n // if not backend filtered, filter row\n if (!props.backendFiltering)\n // return row is visible based on filters\n return !isRowFiltered(row.value);\n\n // return row is visible (not filtered out)\n return false;\n });\n}\n\n/*\n * Total data count.\n * If backend paginated, use props total else use rows data length as pagination total.\n */\nconst tableTotal = computed(() =>\n props.backendPagination ? props.total : tableRows.value.length,\n);\n\n/** total rows count */\nconst rowCount = computed(() => {\n return tableTotal.value + ariaRowIndexStart.value;\n});\n\n/** aria-rowindex start value for tds based if it's Searchable or has subheadings */\nconst ariaRowIndexStart = computed(() => {\n let i = 1;\n if (hasSearchableColumns.value) i++;\n if (hasSubheadings.value) i++;\n return i;\n});\n\n/**\n * Check if footer slot has custom content.\n * Must be called during rendering.\n */\nfunction hasCustomFooterSlot(): boolean {\n if (!slots.footer) return false;\n\n const footer = slots.footer({\n columnCount: columnCount.value,\n rowCount: rowCount.value,\n });\n if (footer.length > 1) return true;\n\n const tag = footer[0][\"type\"];\n return tag === \"th\" || tag === \"td\";\n}\n\n/** get the formated row value for a column */\nfunction getColumnValue(row: T, column: TableColumn<T>): string {\n return getPropertyValue(row, column.field as DeepKeys<T>, column.formatter);\n}\n\n/** check if two rows are equal by a custom compare function or the rowKey attribute */\nfunction isRowEqual(\n sourceRow: MaybeRefOrGetter<T>,\n targetRow: MaybeRefOrGetter<T>,\n): boolean {\n const el1 = toRaw(toValue(sourceRow));\n const el2 = toRaw(toValue(targetRow));\n if (!isDefined(targetRow)) return false;\n if (typeof props.customCompare === \"function\")\n return props.customCompare(el1, el2);\n if (props.rowKey)\n return (\n getPropertyValue(el1, props.rowKey) ==\n getPropertyValue(el2, props.rowKey)\n );\n return el1 == el2;\n}\n\n// #endregion --- TABLE ROWS ---\n\n// #region --- Select Feature ---\n\nconst tableSelectedRow = defineModel<T>(\"selected\", { default: undefined });\n\n/** table arrow keys listener, change selection */\nfunction onArrowPressed(delta: 1 | -1, event: KeyboardEvent): void {\n if (!availableRows.value.length) return;\n\n let index =\n availableRows.value.findIndex((row) =>\n isRowEqual(row.value, tableSelectedRow.value),\n ) + delta;\n\n // check if index overflow\n index =\n index > availableRows.value.length - 1\n ? availableRows.value.length - 1\n : index;\n // check if index underflow\n index = index < 0 ? 0 : index;\n\n // get row element\n const row = availableRows.value[index];\n\n if (!props.isRowSelectable(row.value)) {\n let newIndex: number | undefined;\n if (delta > 0) {\n for (\n let i = index;\n i < availableRows.value.length && newIndex === undefined;\n i++\n ) {\n if (props.isRowSelectable(availableRows.value[i].value))\n newIndex = i;\n }\n } else {\n for (let i = index; i >= 0 && newIndex === undefined; i--) {\n if (props.isRowSelectable(availableRows.value[i].value))\n newIndex = i;\n }\n }\n if (newIndex != undefined && newIndex >= 0)\n selectRow(availableRows.value[newIndex], event);\n } else {\n selectRow(row, event);\n }\n}\n\n/**\n * Row click listener.\n * Emit all necessary events.\n */\nfunction selectRow(row: TableRow<T>, event: Event): void {\n emits(\"click\", row.value, row.index, event);\n\n if (!props.selectable) return;\n\n if (isRowEqual(tableSelectedRow, row.value)) return;\n if (!props.isRowSelectable(row.value)) return;\n\n tableSelectedRow.value = row.value;\n // emit new and old row\n emits(\"select\", row.value, tableSelectedRow.value);\n}\n\n// #endregion --- Select Feature ---\n\n// #region --- Filter Feature ---\n\n/** search filter record alias { fieldKey: filterValue } */\nconst filters = ref<Record<string, string>>({});\n\n/** check if has any searchable column */\nconst hasSearchableColumns = computed(() =>\n tableColumns.value.some((column) => column.searchable),\n);\n\nlet debouncedFilter: ReturnType<\n typeof useDebounce<Parameters<typeof handleFiltersChange>>\n>;\n\n// initialise and update debounces filter function\nwatch(\n () => props.filterDebounce,\n (debounce) =>\n (debouncedFilter = useDebounce(handleFiltersChange, debounce || 0)),\n { immediate: true },\n);\n\n// react on filters got changed\nwatch(filters, (value) => debouncedFilter(value), { deep: true });\n\nfunction handleFiltersChange(value: Record<string, string>): void {\n emits(\"filters-change\", value);\n // if not backend filtered, recompute rows visibility with updated filters\n if (!props.backendFiltering) {\n filterTableRows();\n // force tableRows reactivity to update\n triggerRef(tableRows);\n }\n}\n\nfunction onFiltersEvent(event: Event): void {\n emits(\"filters-event\", props.filtersEvent, filters.value, event);\n}\n\n/**\n * check whether a row is filtered by active filters or not\n * @param row - row element\n *\n * @returns is row filtered in\n * */\nfunction isRowFiltered(row: T): boolean {\n if (!Object.values(filters.value).filter(Boolean).length) return true;\n return Object.entries(filters.value).some(([key, filter]) => {\n if (!filter) return false;\n // get column for filter\n const column = tableColumns.value.find((c) => c.field === key);\n // if column has onSearch return result\n if (typeof column?.customSearch === \"function\")\n return column.customSearch(row, filter);\n\n const value =\n typeof row === \"object\" && !!row ? getValueByPath(row, key) : row;\n if (value == null) return false;\n // if number compare values\n if (Number.isInteger(value)) return value === Number(filter);\n const re = new RegExp(escapeRegExpChars(filter), \"i\");\n if (Array.isArray(value))\n return value.some(\n (val) =>\n re.test(removeDiacriticsFromString(val)) || re.test(val),\n );\n if (typeof value !== \"string\") return !!value;\n return re.test(removeDiacriticsFromString(value)) || re.test(value);\n });\n}\n\n// #endregion --- Filter Feature ---\n\n// #region --- Sort Feature ---\n\nconst currentSortColumn = ref<TableColumnItem<T>>();\nconst isAsc = ref(true);\n\n/** check if has any sortable column */\nconst hasSortableColumns = computed(() =>\n tableColumns.value.some((column) => column.sortable),\n);\n\n/** check if the column is the current sort column */\nfunction isColumnSorted(column: TableColumnItem<T>): boolean {\n return currentSortColumn.value?.identifier === column.identifier;\n}\n\n// call initSort only first time (for example async data)\n// initSort must be called after async TableColumns got initialised first time\nonMounted(() => nextTick(() => initSort()));\n\n/** initial sorted column based on the default-sort prop */\nfunction initSort(): void {\n if (!tableColumns.value.length || currentSortColumn.value) return;\n if (!props.defaultSort) return;\n let sortField = \"\";\n let sortDirection = props.defaultSortDirection;\n if (Array.isArray(props.defaultSort)) {\n sortField = props.defaultSort[0];\n if (props.defaultSort[1]) sortDirection = props.defaultSort[1];\n } else {\n sortField = props.defaultSort;\n }\n sortByField(sortField, sortDirection);\n}\n\nfunction sortByField(field: string, direction: \"asc\" | \"desc\"): void {\n const sortColumn = tableColumns.value.find(\n (column) => column.field === field,\n );\n if (sortColumn) {\n isAsc.value = direction.toLowerCase() === \"asc\";\n sort(sortColumn);\n }\n}\n\n/**\n * Sort the column.\n * Toggle current direction on column if it's sortable\n * and not just updating the prop.\n */\nfunction sort(\n column: TableColumnItem<T>,\n updateDirection = false,\n event?: Event,\n): void {\n if (!column?.sortable) return;\n\n if (updateDirection)\n isAsc.value = isColumnSorted(column)\n ? !isAsc.value\n : props.defaultSortDirection.toLowerCase() === \"asc\";\n\n // if not first time sort\n if (currentSortColumn.value)\n emits(\n \"sort\",\n column,\n isAsc.value ? \"asc\" : \"desc\",\n event || new Event(\"sort\"),\n );\n\n currentSortColumn.value = column;\n\n // if not backend sorted\n if (!props.backendSorting) {\n // sort rows by mutating the tableRows array\n sortByColumn(tableRows.value);\n\n // recalculate the page filter\n filterTableRows();\n }\n}\n\nfunction sortByColumn(rows: TableRow<T>[]): TableRow<T>[] {\n const column = currentSortColumn.value;\n if (!column) return rows;\n return sortBy<TableRow<T>>(\n rows,\n column?.field ? \"value.\" + column.field : \"\",\n column?.customSort\n ? (a, b, asc): number => column.customSort!(a.value, b.value, asc)\n : undefined,\n isAsc.value,\n true,\n );\n}\n\n// #endregion --- Sort Feature ---\n\n// #region --- Checkable Feature ---\n\nconst tableCheckedRows = defineModel<T[]>(\"checkedRows\", {\n default: [],\n});\n\n/** check if all rows in the page are checked */\nconst isAllChecked = computed(() => {\n const validVisibleData = availableRows.value.filter((row) =>\n props.isRowCheckable(row.value),\n );\n if (validVisibleData.length === 0) return false;\n return validVisibleData.every((currentVisibleRow) =>\n isChecked(currentVisibleRow),\n );\n});\n\n/** check if all rows in the page are checkable */\nconst isAllUncheckable = computed(\n () => !availableRows.value.some((row) => props.isRowCheckable(row.value)),\n);\n\n/** check if the row is checked (is added to the array) */\nfunction isChecked(row: TableRow<T>): boolean {\n if (typeof props.isRowChecked === \"function\")\n return props.isRowChecked(row.value);\n else return tableCheckedRows.value.some((r) => isRowEqual(r, row.value));\n}\n\n/** add a checked row to the the array */\nfunction addCheckedRow(row: TableRow<T>): void {\n tableCheckedRows.value = [...tableCheckedRows.value, row.value];\n}\n\n/** remove a checked row from the array */\nfunction removeCheckedRow(row: TableRow<T>): void {\n const idx = tableCheckedRows.value.findIndex((r) =>\n isRowEqual(r, row.value),\n );\n if (idx >= 0)\n tableCheckedRows.value = tableCheckedRows.value.toSpliced(idx, 1);\n}\n\n/**\n * Header checkbox click listener.\n * Add or remove all rows in current page.\n */\nfunction checkAll(): void {\n if (isAllChecked.value)\n // if all rows are already checked, check nothing\n tableCheckedRows.value = [];\n else {\n // else set all visible rows as checked\n tableCheckedRows.value = availableRows.value\n .filter((row) => props.isRowCheckable(row.value))\n .map((row) => row.value);\n }\n\n // emit event after the reactive checked rows list got updated\n nextTick(() => emits(\"check-all\", tableCheckedRows.value));\n}\n\n/** row checkbox click listener */\nfunction checkRow(row: TableRow<T>): void {\n if (!props.isRowCheckable(row.value)) return;\n\n if (isChecked(row)) removeCheckedRow(row);\n else addCheckedRow(row);\n\n // emit event after the reactive checked rows list got updated\n nextTick(() => emits(\"check\", tableCheckedRows.value, row.value));\n}\n\n// #endregion --- Checkable Feature ---\n\n// #region --- Detail Row Feature ---\n\nconst visibleDetailedRows = defineModel<T[]>(\"detailedRows\", {\n default: [],\n});\n\n/**\n * Return if detailed row tabled.\n * Will be with chevron column & icon or not.\n */\nconst showDetailRowIcon = computed(\n () => props.detailed && props.showDetailIcon,\n);\n\n/** toggle to show/hide details slot */\nfunction toggleDetails(row: TableRow<T>): void {\n if (isDetailRowVisible(row)) {\n closeDetailRow(row);\n emits(\"details-close\", row.value);\n } else {\n openDetailRow(row);\n emits(\"details-open\", row.value);\n }\n}\n\nfunction openDetailRow(row: TableRow<T>): void {\n visibleDetailedRows.value = [...visibleDetailedRows.value, row.value];\n}\n\nfunction closeDetailRow(row: TableRow<T>): void {\n const idx = visibleDetailedRows.value.findIndex((r) =>\n isRowEqual(r, row.value),\n );\n if (idx >= 0)\n visibleDetailedRows.value = visibleDetailedRows.value.toSpliced(idx, 1);\n}\n\nfunction isDetailRowVisible(row: TableRow<T>): boolean {\n return (\n props.detailed &&\n visibleDetailedRows.value.some((r) => isRowEqual(r, row.value))\n );\n}\n\n// #endregion --- Detail Row Feature ---\n\n// #region --- Drag&Drop Feature ---\n\nconst isDraggingRow = ref(false);\nconst isDraggingColumn = ref(false);\n\nconst canDragRow = computed(() => props.draggable && !isDraggingColumn.value);\n\nconst canDragColumn = computed(\n () => props.draggableColumn && !isDraggingRow.value,\n);\n\n/** emits drag start event */\nfunction handleDragStart(row: TableRow<T>, event: DragEvent): void {\n if (!props.draggable) return;\n emits(\"dragstart\", row.value, row.index, event);\n}\n\n/** emits drag leave event */\nfunction handleDragEnd(row: TableRow<T>, event: DragEvent): void {\n if (!props.draggable) return;\n emits(\"dragend\", row.value, row.index, event);\n}\n\n/** emits drop event */\nfunction handleDrop(row: TableRow<T>, event: DragEvent): void {\n if (!props.draggable) return;\n emits(\"drop\", row.value, row.index, event);\n}\n\n/** emits drag over event */\nfunction handleDragOver(row: TableRow<T>, event: DragEvent): void {\n if (!props.draggable) return;\n emits(\"dragover\", row.value, row.index, event);\n}\n\n/** emits drag leave event */\nfunction handleDragLeave(row: TableRow<T>, event: DragEvent): void {\n if (!props.draggable) return;\n emits(\"dragleave\", row.value, row.index, event);\n}\n\n/** emits drag start event (column) */\nfunction handleColumnDragStart(\n column: TableColumnItem<T>,\n event: DragEvent,\n): void {\n if (!canDragColumn.value) return;\n isDraggingColumn.value = true;\n emits(\"columndragstart\", column.value, column.index, event);\n}\n\n/** emits drag leave event (column) */\nfunction handleColumnDragEnd(\n column: TableColumnItem<T>,\n event: DragEvent,\n): void {\n if (!canDragColumn.value) return;\n isDraggingColumn.value = false;\n emits(\"columndragend\", column.value, column.index, event);\n}\n\n/** emits drop event (column) */\nfunction handleColumnDrop(column: TableColumnItem<T>, event: DragEvent): void {\n if (!canDragColumn.value) return;\n emits(\"columndrop\", column.value, column.index, event);\n}\n\n/** emits drag over event (column) */\nfunction handleColumnDragOver(\n column: TableColumnItem<T>,\n event: DragEvent,\n): void {\n if (!canDragColumn.value) return;\n emits(\"columndragover\", column.value, column.index, event);\n}\n\n/** emits drag leave event (column) */\nfunction handleColumnDragLeave(\n column: TableColumnItem<T>,\n event: DragEvent,\n): void {\n if (!canDragColumn.value) return;\n emits(\"columndragleave\", column.value, column.index, event);\n}\n\n// #endregion --- Drag&Drop Feature ---\n\n// #region --- Computed Component Classes ---\n\nconst rootClasses = defineClasses(\n [\"rootClass\", \"o-table__root\"],\n [\"mobileClass\", \"o-table__root--mobile\", null, isMobileActive],\n);\n\nconst tableWrapperClasses = defineClasses(\n [\"wrapperClass\", \"o-table__wrapper\"],\n [\n \"stickyHeaderClass\",\n \"o-table__wrapper--sticky-header\",\n null,\n computed(() => props.stickyHeader),\n ],\n [\"scrollableClass\", \"o-table__wrapper--scrollable\", null, isScrollable],\n [\"mobileClass\", \"o-table__wrapper--mobile\", null, isMobileActive],\n);\n\nconst tableWrapperStyle = computed(() => ({\n height: toCssDimension(props.height),\n}));\n\nconst tableClasses = defineClasses(\n [\"tableClass\", \"o-table\"],\n [\n \"borderedClass\",\n \"o-table--bordered\",\n null,\n computed(() => props.bordered),\n ],\n [\"stripedClass\", \"o-table--striped\", null, computed(() => props.striped)],\n [\n \"narrowedClass\",\n \"o-table--narrowed\",\n null,\n computed(() => props.narrowed),\n ],\n [\n \"hoverableClass\",\n \"o-table--hoverable\",\n null,\n computed(\n () =>\n (props.hoverable || props.selectable) &&\n !!availableRows.value.length,\n ),\n ],\n [\n \"emptyClass\",\n \"o-table--empty\",\n null,\n computed(() => !availableRows.value.length),\n ],\n);\n\nconst thBaseClasses = defineClasses([\"thClass\", \"o-table__th\"]);\n\nconst thCheckboxClasses = defineClasses(\n [\"thCheckboxClass\", \"o-table__th-checkbox\"],\n [\n \"thStickyClass\",\n \"o-table__th--sticky\",\n null,\n computed(() => props.stickyCheckbox),\n ],\n);\n\nconst thDetailedClasses = defineClasses([\n \"thDetailedClass\",\n \"o-table__th-detailed\",\n]);\n\nconst thSubheadingClasses = defineClasses([\n \"thSubheadingClass\",\n \"o-table__th-subheading\",\n]);\n\nconst thSortIconClasses = defineClasses([\n \"thSortIconClass\",\n \"o-table__th__sort-icon\",\n]);\n\nconst trSelectedClasses = defineClasses([\n \"trSelectedClass\",\n \"o-table__tr--selected\",\n]);\n\nconst trCheckedClasses = defineClasses([\n \"trCheckedClass\",\n \"o-table__tr--checked\",\n]);\n\nconst trEmptyClasses = defineClasses([\"trEmptyClass\", \"o-table__tr-empty\"]);\n\nconst trDetailedClasses = defineClasses([\n \"trDetailedClass\",\n \"o-table__tr-detail\",\n]);\n\nconst tdBaseClasses = defineClasses([\"tdClass\", \"o-table__td\"]);\n\nconst tdCheckboxClasses = defineClasses(\n [\"tdCheckboxClass\", \"o-table__td-checkbox\"],\n [\n \"thStickyClass\",\n \"o-table__th--sticky\",\n null,\n computed(() => props.stickyCheckbox),\n ],\n);\n\nconst tdDetailedChevronClasses = defineClasses([\n \"tdDetailedChevronClass\",\n \"o-table__td-chevron\",\n]);\n\nconst footerClasses = defineClasses([\"footerClass\", \"o-table__footer\"]);\n\nconst mobileSortClasses = defineClasses([\n \"mobileSortClass\",\n \"o-table__mobile-sort\",\n]);\n\nconst paginationWrapperClasses = defineClasses([\n \"paginationWrapperClass\",\n \"o-table__pagination\",\n]);\n\nconst paginationWrapperRootClasses = computed(() =>\n getActiveClasses(paginationWrapperClasses),\n);\n\nfunction rowClasses(row: TableRow<T>): ClassBind[] {\n const selectedClasses = isRowEqual(row.value, tableSelectedRow.value)\n ? trSelectedClasses.value\n : [];\n\n const checkedClasses = isChecked(row) ? trCheckedClasses.value : [];\n\n const rowClass =\n typeof props.rowClass === \"function\"\n ? props.rowClass(row.value, row.index) || \"\"\n : \"\";\n\n return [...selectedClasses, ...checkedClasses, { [rowClass]: true }];\n}\n\n// #endregion --- Computed Component Classes ---\n\n// compute initial row visibility\nfilterTableRows();\n\n// #region --- Expose Public Functionalities ---\n\n/** expose functionalities for programmatic usage */\ndefineExpose({ rows: tableRows, sort: sortByField });\n\n// #endregion\n</script>\n\n<template>\n <div data-oruga=\"table\" :class=\"rootClasses\">\n <div ref=\"slotsWrapper\" style=\"display: none\">\n <!--\n @slot Place o-table-column here\n -->\n <slot>\n <!--\n @slot Place extra `o-table-column` components here, even if you have some columns defined by prop\n -->\n <slot name=\"before\" />\n\n <template v-if=\"columns?.length\">\n <o-table-column\n v-for=\"(column, idx) in columns\"\n :key=\"column.field || idx\"\n v-slot=\"{ row }\"\n v-bind=\"column\">\n {{ getColumnValue(row, column) }}\n </o-table-column>\n </template>\n\n <!--\n @slot Place extra `o-table-column` components here, even if you have some columns defined by prop\n -->\n <slot name=\"after\" />\n </slot>\n </div>\n\n <o-table-mobile-sort\n v-if=\"isMobileActive && hasSortableColumns\"\n :current-sort-column=\"currentSortColumn\"\n :columns=\"tableColumns\"\n :placeholder=\"mobileSortPlaceholder\"\n :icon-pack=\"iconPack\"\n :sort-icon=\"sortIcon\"\n :sort-icon-size=\"sortIconSize\"\n :is-asc=\"isAsc\"\n :mobile-sort-classes=\"mobileSortClasses\"\n @sort=\"(column, event) => sort(column, true, event)\" />\n\n <template\n v-if=\"\n paginated &&\n (paginationPosition === 'top' || paginationPosition === 'both')\n \">\n <!--\n @slot Override pagination label\n @binding {number} current - current page\n @binding {number} per-page - rows per page\n @binding {number} total - total rows count\n @binding {(page: number): void } change - on page change event\n -->\n <slot\n name=\"pagination\"\n :current=\"tableCurrentPage\"\n :per-page=\"perPage\"\n :total=\"tableTotal\"\n :change=\"(page) => (tableCurrentPage = page)\">\n <o-table-pagination\n v-model:current=\"tableCurrentPage\"\n :paginated=\"paginated\"\n :per-page=\"perPage\"\n :total=\"tableTotal\"\n :rounded=\"paginationRounded\"\n :size=\"paginationSize\"\n :order=\"paginationOrder\"\n :simple=\"paginationSimple\"\n :range-before=\"paginationRangeBefore\"\n :range-after=\"paginationRangeAfter\"\n :icon-pack=\"iconPack\"\n :aria-next-label=\"ariaNextLabel\"\n :aria-previous-label=\"ariaPrev