diff --git a/packages/docs/components/Loading.md b/packages/docs/components/Loading.md index a9e857d76..0a27a839e 100644 --- a/packages/docs/components/Loading.md +++ b/packages/docs/components/Loading.md @@ -45,7 +45,8 @@ | iconSpin | Enable spin effect on icon | boolean | - |
From config:
loading: {
  iconSpin: true
}
| | label | Notification label, unnecessary when default slot is used. | string | - | | | override | Override existing theme classes completely | boolean | - | | -| scroll | Use `clip` to remove the body scrollbar, `keep` to have a non scrollable scrollbar to avoid shifting background,
but will set body to position fixed, might break some layouts. | "clip" \| "keep" | `keep`, `clip` |
From config:
modal: {
  scroll: "keep"
}
| +| role | Role attribute to be passed to the div wrapper for better accessibility | string | - |
From config:
loading: {
  role: "dialog"
}
| +| scroll | Use `clip` to remove the body scrollbar, `keep` to have a non scrollable scrollbar to avoid shifting background,
but will set body to position fixed, might break some layouts. | "clip" \| "keep" | `keep`, `clip` |
From config:
loading: {
  scroll: "keep"
}
| ### Events diff --git a/packages/docs/components/Table.md b/packages/docs/components/Table.md index 6f2a79d7c..43ee6d7f0 100644 --- a/packages/docs/components/Table.md +++ b/packages/docs/components/Table.md @@ -57,7 +57,7 @@ sidebarDepth: 2 | customCompare | Define a custom comparison function to check whether two row elements are equal.
By default a `rowKey` comparison is performed if given. Otherwise a simple object comparison is done. | ((a: unknown, b: unknown) => boolean) | - | | | customDetailRow | Enable custom style on details (if detailed) | boolean | - | false | | data | Table data | unknown[] | - | | -| debounceSearch | Filtering debounce time (in milliseconds) | number | - |
From config:
table: {
  debounceSearch: undefined
}
| +| debounceSearch | Filtering debounce time (in milliseconds) | number | - |
From config:
table: {
  debounceSearch: 300
}
| | defaultSort | Sets the default sort column and order — e.g. 'first_name' or ['first_name', 'desc'] | 'desc'] \| [string, 'asc' \| string | - |
From config:
table: {
  defaultSort: undefined
}
| | defaultSortDirection | Sets the default sort column direction on the first click | 'asc'\|'desc' | `asc`, `desc` |
From config:
table: {
  defaultSortDirection: "asc"
}
| | detailIcon | Icon name of detail action (if detailed) | string | - |
From config:
table: {
  detailIcon: "chevron-right"
}
| @@ -112,38 +112,37 @@ sidebarDepth: 2 ### Events -| Event name | Properties | Description | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -| page-change | **page** `number` - updated page | on pagination page change event | -| dblclick | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native click event | on row double click event | -| mouseenter | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native mouseenter event | on row mouseenter event | -| mouseleave | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native mouseleave event | on row mouseleave event | -| contextmenu | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native contextmenu event | on row right click event | -| cell-click | **row** `T` - row data
**column** `TableColumn` - column data
**index** `number` - row index
**colindex** `number` - column index
**event** `Event` - native click event | on cell click event | -| update:currentPage | **value** `number` - updated currentPage prop | currentPage prop two-way binding | -| processed | **value** `TableRow[]` - computed table rows | is emitted each time the table data is processed into rows | -| update:selected | **value** `T` - updated select prop | select prop two-way binding | -| select | **newRow** `T` - new select value
**oldRow** `T` - old select value | on row select event | -| check | **value** `T[]` - all checked rows
**row** `T` - row data | on row checked event | -| check-all | **value** `T[]` - all checked rows | on all rows checked event | -| update:checkedRows | **value** `T[]` - updated checkedRows prop | checkedRows prop two-way binding | -| sort | **column** `TableColumn` - column data
**direction** `string` - 'asc' or 'desc'
**event** `Event` - native event | on column sort change event | -| filters-change | **filters** `object` - filter object | on filter change event | -| filters-event | **filtersEvent** `string` - props filtersEvent value
**filters** `object` - filter object
**event** `Event` - native event | on native filter event based on props filtersEvent | -| update:detailedRows | **value** `T[]` - updated detailedRows prop | detailedRows prop two-way binding | -| details-open | **row** `T` - row data | on details open event | -| details-close | **row** `T` - row data | on details close event | -| click | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native click event | on row click event | -| dragstart | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragstart event | on row dragstart event | -| dragend | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragend event | on row dragend event | -| drop | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native drop event | on row drop event | -| dragleave | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragleave event | on row dragleave event | -| dragover | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragover event | on row dragover event | -| columndragstart | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragstart event | on column columndragstart event | -| columndragend | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragend event | on column columndragend event | -| columndrop | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndrop event | on column columndrop event | -| columndragleave | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragleave event | on column columndragleave event | -| columndragover | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragover event | on column columndragover event | +| Event name | Properties | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| page-change | **page** `number` - updated page | on pagination page change event | +| dblclick | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native click event | on row double click event | +| mouseenter | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native mouseenter event | on row mouseenter event | +| mouseleave | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native mouseleave event | on row mouseleave event | +| contextmenu | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native contextmenu event | on row right click event | +| cell-click | **row** `T` - row data
**column** `TableColumn` - column data
**index** `number` - row index
**colindex** `number` - column index
**event** `Event` - native click event | on cell click event | +| update:currentPage | **value** `number` - updated currentPage prop | currentPage prop two-way binding | +| update:selected | **value** `T` - updated select prop | select prop two-way binding | +| select | **newRow** `T` - new select value
**oldRow** `T` - old select value | on row select event | +| check | **value** `T[]` - all checked rows
**row** `T` - row data | on row checked event | +| check-all | **value** `T[]` - all checked rows | on all rows checked event | +| update:checkedRows | **value** `T[]` - updated checkedRows prop | checkedRows prop two-way binding | +| sort | **column** `TableColumn` - column data
**direction** `string` - 'asc' or 'desc'
**event** `Event` - native event | on column sort change event | +| filters-change | **filters** `object` - filter object | on filter change event | +| filters-event | **filtersEvent** `string` - props filtersEvent value
**filters** `object` - filter object
**event** `Event` - native event | on native filter event based on props filtersEvent | +| update:detailedRows | **value** `T[]` - updated detailedRows prop | detailedRows prop two-way binding | +| details-open | **row** `T` - row data | on details open event | +| details-close | **row** `T` - row data | on details close event | +| click | **row** `T` - row data
**index** `number` - index of clicked row
**event** `Event` - native click event | on row click event | +| dragstart | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragstart event | on row dragstart event | +| dragend | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragend event | on row dragend event | +| drop | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native drop event | on row drop event | +| dragleave | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragleave event | on row dragleave event | +| dragover | **row** `T` - row data
**index** `number` - index of draged row
**event** `DragEvent` - native dragover event | on row dragover event | +| columndragstart | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragstart event | on column columndragstart event | +| columndragend | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragend event | on column columndragend event | +| columndrop | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndrop event | on column columndrop event | +| columndragleave | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragleave event | on column columndragleave event | +| columndragover | **column** `TableColumn` - column data
**index** `number` - index of draged column
**event** `DragEvent` - native columndragover event | on column columndragover event | ### Slots @@ -182,6 +181,7 @@ sidebarDepth: 2 | field | Define an object property key if data is an object | string | - | | | formatter | Provide a formatter function to edit the output | ((value: unknown, row: unknown) => string) | - | | | headerSelectable | Make header selectable | boolean | - | false | +| hidden | Define whether the column is visible or not | boolean | - | false | | label | Define the column label | string | - | | | numeric | Define column value as number | boolean | - | false | | position | Position of the column content | "centered" \| "left" \| "right" | `left`, `centered`, `right` | | diff --git a/packages/oruga/src/components/autocomplete/Autocomplete.vue b/packages/oruga/src/components/autocomplete/Autocomplete.vue index 6cbe62657..368d6a751 100644 --- a/packages/oruga/src/components/autocomplete/Autocomplete.vue +++ b/packages/oruga/src/components/autocomplete/Autocomplete.vue @@ -10,7 +10,9 @@ import { triggerRef, watchEffect, useTemplateRef, + toValue, type Component, + type MaybeRefOrGetter, } from "vue"; import OInput from "../input/Input.vue"; @@ -27,11 +29,11 @@ import { toOptionsList, findOption, checkOptionsEmpty, - firstValidOption, + firstViableOption, filterOptionsItems, useInputHandler, useEventListener, - isOptionValid, + isOptionViable, useSequentialId, } from "@/composables"; @@ -224,11 +226,23 @@ const groupedOptions = computed[]>(() => { */ watchEffect(() => { // filter options by input value - filterOptionsItems(groupedOptions, inputValue, props.filter); + filterOptionsItems(groupedOptions, (o) => filterItems(o, inputValue)); // trigger reactive update of groupedOptions triggerRef(groupedOptions); }); +function filterItems( + option: OptionsItem, + value: MaybeRefOrGetter, +): boolean { + if (typeof props.filter === "function") + return props.filter(option.value, toValue(value)); + else + return !String(option.label) + .toLowerCase() + .includes(toValue(value)?.toLowerCase()); +} + // set initial inputValue if selected is given if (selectedValue.value) { const selectedOption = findOption(groupedOptions, selectedValue); @@ -361,7 +375,7 @@ function setHovered(option: OptionsItem | SpecialOption | undefined): void { /** set first option as hovered */ function hoverFirstOption(): void { - const option = firstValidOption(groupedOptions); + const option = firstViableOption(groupedOptions); // set found option or undefined hovered setHovered(option); } @@ -372,7 +386,7 @@ function hoverFirstOption(): void { * Arrows keys listener. * If dropdown is active, set hovered option, or else just open. */ -function navigateItem(direction: 1 | -1): void { +function navigateItem(delta: 1 | -1): void { if (!dropdownRef.value?.$content) return; if (!isActive.value) { isActive.value = true; @@ -383,7 +397,7 @@ function navigateItem(direction: 1 | -1): void { const options: OptionsItem[] = toOptionsList(groupedOptions); // filter only avaibale options const availableOptions: (SpecialOption | OptionsItem)[] = options.filter( - (o) => isOptionValid(o), + (o) => isOptionViable(o), ); // item elements @@ -401,15 +415,14 @@ function navigateItem(direction: 1 | -1): void { // define current available options index let index: number; - if (headerHovered.value) index = 0 + direction; - else if (footerHovered.value) - index = availableOptions.length - 1 + direction; + if (headerHovered.value) index = 0 + delta; + else if (footerHovered.value) index = availableOptions.length - 1 + delta; else { index = availableOptions.findIndex( (o) => !isSpecialOption(o) && o.key === hoveredOption.value?.key, - ) + direction; + ) + delta; } // check if index overflow diff --git a/packages/oruga/src/components/loading/Loading.vue b/packages/oruga/src/components/loading/Loading.vue index 9c8949c48..854117302 100644 --- a/packages/oruga/src/components/loading/Loading.vue +++ b/packages/oruga/src/components/loading/Loading.vue @@ -35,7 +35,8 @@ const props = withDefaults(defineProps(), { icon: () => getDefault("loading.icon", "loading"), iconSpin: () => getDefault("loading.iconSpin", true), iconSize: () => getDefault("loading.iconSize", "medium"), - scroll: () => getDefault("modal.scroll", "keep"), + scroll: () => getDefault("loading.scroll", "keep"), + role: () => getDefault("loading.role", "dialog"), }); const emits = defineEmits<{ @@ -127,7 +128,7 @@ defineExpose({ close }); v-if="isActive" ref="rootElement" data-oruga="loading" - role="dialog" + :role="role" :class="rootClasses">
>(), { scrollable: undefined, stickyHeader: false, height: undefined, - debounceSearch: () => getDefault("table.debounceSearch"), + debounceSearch: () => getDefault("table.debounceSearch", 300), checkable: false, stickyCheckbox: false, headerCheckable: true, @@ -144,11 +145,6 @@ const emits = defineEmits<{ * @param value {number} updated currentPage prop */ "update:currentPage": [value: number]; - /** - * is emitted each time the table data is processed into rows - * @param value {TableRow[]} computed table rows - */ - processed: [value: TableRow[]]; /** * on pagination page change event * @param page {number} updated page @@ -341,11 +337,13 @@ const emits = defineEmits<{ columndragover: [column: TableColumn, index: number, event: DragEvent]; }>(); +const slots = useSlots(); + const { isMobile } = useMatchMedia(props.mobileBreakpoint); const isMobileActive = computed(() => props.mobileCards && isMobile.value); -const slotRef = useTemplateRef("slotElement"); +const slotsRef = useTemplateRef("slotsWrapper"); // provided data is a computed ref to enjure reactivity const provideData = computed(() => ({ @@ -354,130 +352,159 @@ const provideData = computed(() => ({ /** provide functionalities and data to child item components */ const { childItems } = useProviderParent>({ - rootRef: slotRef, + rootRef: slotsRef, data: provideData, }); +// #region --- TABLE COLUMNS --- + /** all defined columns */ const tableColumns = computed[]>(() => { - if (!childItems.value) return []; - return childItems.value.map((column) => ({ - index: column.index, - identifier: column.identifier, - ...toValue(column.data!), - thAttrsData: {}, - tdAttrsData: [], - })); + if (!childItems.value.length) return []; + return childItems.value.map((columnItem) => { + const column = toValue(columnItem.data!); + + // create additional th attrs data + let thAttrsData = + typeof props.thAttrs === "function" ? props.thAttrs(column) : {}; + thAttrsData = Object.assign(thAttrsData, column.thAttrs); + + // create additional td attrs data + const tdAttrsData = (props.data ?? []).map((data) => { + const tdAttrs = + typeof props.tdAttrs === "function" + ? props.tdAttrs(data, column) + : {}; + return Object.assign(tdAttrs, column.tdAttrs); + }); + + return { + ...column, + value: column, + index: columnItem.index, + identifier: columnItem.identifier, + thAttrsData: thAttrsData, + tdAttrsData: tdAttrsData, + }; + }); }); -// create a unique id sequence -const { nextSequence } = useSequentialId(); - -/** all defined data elements as an object map */ -const tableData = computed[]>(() => - useObjectMap(props.data, props.rowKey, nextSequence), -); - -const tableRows = ref(tableData.value) as Ref[]>; - -/** recompute table rows when table data change */ -watch(tableData, () => processTableData()); +/** total columns count */ +const columnCount = computed(() => { + let i = tableColumns.value.length; + if (showDetailRowIcon.value) i++; + if (props.checkable) i++; + return i; +}); -/** - * Compute tableRows based on: - * 1. Filter data if it's not backend-filtered. - * 2. Sort data if it's not backend-sorted. - * 3. Update internal value. - */ -function processTableData(): void { - // create new array to don't mutate the original data order - let rows = [...tableData.value]; +/** aria-colindex start value for ths */ +const ariaColIndexStart = computed(() => { + let i = 1; + if (showDetailRowIcon.value) i++; + if (props.checkable && props.checkboxPosition === "left") i++; + return i; +}); - // if not backend filtered, filter rows - if (!props.backendFiltering) rows = filterRows(rows); +/** check if table has subheadings */ +const hasSubheadings = computed(() => { + if (slots.subheading) return true; + return tableColumns.value.some((column) => !!column.subheading); +}); - // if not backend sorted, sort rows - if (!props.backendSorting) rows = sortByColumn(rows); +/** check if table is scrollable */ +const isScrollable = computed(() => { + if (props.scrollable) return true; + return tableColumns.value.some((column) => column.sticky); +}); - tableRows.value = rows; - emits("processed", rows); // emit computed rows every time they the data get changed -} +// #endregion --- TABLE COLUMNS --- -/** Shows total data. If backend paginated, use props total else use rows data length as pagination total */ -const tableTotal = computed(() => - props.backendPagination ? props.total : tableRows.value.length, -); +// #region --- TABLE ROWS --- const tableCurrentPage = defineModel("currentPage", { default: 1 }); -/** visible rows based on current page */ -const visibleRows = computed[]>((): TableRow[] => { - if (!props.paginated || props.backendPagination) return tableRows.value; - - const currentPage = tableCurrentPage.value; - const perPage = Number(props.perPage); +// recompute table rows visibility on page change or data change +watch([tableCurrentPage, () => props.perPage, () => props.data], () => + filterTableRows(), +); - if (tableRows.value.length <= perPage) return tableRows.value; +// create a unique id sequence +const { nextSequence } = useSequentialId(); - const start = (currentPage - 1) * perPage; - const end = start + perPage; - return tableRows.value.slice(start, end); +/** all defined data elements as normalized options with a unique key*/ +const tableRows = computed[]>(() => { + if (!props.data) return []; + return props.data.map((value: T, idx: number) => ({ + label: "row " + idx, // row display label + value: toValue(value), // normalizes wrapped ref values + index: idx, // row index + key: + // if no key is given and data is object, create unique row id for each row + String( + getValueByPath( + value, + props.rowKey, + nextSequence() as DeepType, + ), + ), + })); }); -const visibleColumns = computed(() => { - if (!tableColumns.value) return []; - return tableColumns.value.filter( - (column) => column.visible || column.visible === undefined, - ); -}); +/** visible rows which are filtered by viability */ +const availableRows = computed[]>(() => + tableRows.value.filter((o) => isOptionViable(o)), +); -/** process thAttrs & tdAttrs when row or columns got changed */ -watch([visibleRows, visibleColumns], () => { - if (visibleColumns.value.length && visibleRows.value.length) { - for (let i = 0; i < visibleColumns.value.length; i++) { - const col = visibleColumns.value[i]; - // create additional th attrs data - const thAttrs = - typeof props.thAttrs === "function" ? props.thAttrs(col) : {}; - col.thAttrsData = Object.assign(thAttrs, col.thAttrs); - // create additional td attrs data - col.tdAttrsData = visibleRows.value.map((data) => { - const tdAttrs = - typeof props.tdAttrs === "function" - ? props.tdAttrs(data.value, col) - : {}; - return Object.assign(tdAttrs, col.tdAttrs); - }); +/** applies visability filter of reactive tableRows */ +function filterTableRows(): void { + // calculate pagination information + const currentPage = tableCurrentPage.value; + const perPage = Number(props.perPage); + const pageStart = (currentPage - 1) * perPage; + const pageEnd = pageStart + perPage; + + // update hidden state for each row + filterOptionsItems(tableRows, (row) => { + // if paginated not backend paginated, paginate row + if (props.paginated || !props.backendPagination) { + // if not only one page and not on active page + if ( + tableRows.value.length > perPage && + (row.index < pageStart || row.index > pageEnd) + ) + // return row is invisible + return true; } - } -}); -/** total column count based if it's checkable or expanded */ -const columnCount = computed(() => { - let count = visibleColumns.value.length; - count += props.checkable ? 1 : 0; - count += props.detailed && props.showDetailIcon ? 1 : 0; - return count; -}); + // if not backend filtered, filter row + if (!props.backendFiltering) + // return row is visible based on filters + return !isRowFiltered(row.value); -/** check if has any searchable column. */ -const hasSearchableColumns = computed(() => - tableColumns.value.some((column) => column.searchable), + // return row is visible + return false; + }); +} + +/* + * Total data count. + * If backend paginated, use props total else use rows data length as pagination total. + */ +const tableTotal = computed(() => + props.backendPagination ? props.total : tableRows.value.length, ); -/** check if table is scrollable */ -const isScrollable = computed(() => { - if (props.scrollable) return true; - if (!tableColumns.value) return false; - return tableColumns.value.some((column) => column.sticky); +/** total rows count */ +const rowCount = computed(() => { + return tableTotal.value + ariaRowIndexStart.value; }); -const slots = useSlots(); - -/** check if table hast subheadings */ -const hasCustomSubheadings = computed(() => { - if (slots.subheading) return true; - return tableColumns.value.some((column) => !!column.subheading); +/** aria-rowindex start value for tds based if it's Searchable or has subheadings */ +const ariaRowIndexStart = computed(() => { + let i = 1; + if (hasSearchableColumns.value) i++; + if (hasSubheadings.value) i++; + return i; }); /** @@ -517,51 +544,53 @@ function isRowEqual( return el1 == el2; } -// --- Select Feature --- +// #endregion --- TABLE ROWS --- + +// #region --- Select Feature --- const tableSelectedRow = defineModel("selected", { default: undefined }); /** table arrow keys listener, change selection */ -function onArrowPressed(pos: number, event: KeyboardEvent): void { - if (!visibleRows.value.length) return; +function onArrowPressed(delta: 1 | -1, event: KeyboardEvent): void { + if (!availableRows.value.length) return; let index = - visibleRows.value.findIndex((row) => + availableRows.value.findIndex((row) => isRowEqual(row.value, tableSelectedRow.value), - ) + pos; + ) + delta; - // prevent from going up from first and down from last + // check if index overflow index = - index < 0 - ? 0 - : index > visibleRows.value.length - 1 - ? visibleRows.value.length - 1 - : index; + index > availableRows.value.length - 1 + ? availableRows.value.length - 1 + : index; + // check if index underflow + index = index < 0 ? 0 : index; - const row = visibleRows.value[index]; + // get row element + const row = availableRows.value[index]; if (!props.isRowSelectable(row.value)) { let newIndex: number | undefined; - if (pos > 0) { + if (delta > 0) { for ( let i = index; - i < visibleRows.value.length && newIndex === undefined; + i < availableRows.value.length && newIndex === undefined; i++ ) { - if (props.isRowSelectable(visibleRows.value[i].value)) + if (props.isRowSelectable(availableRows.value[i].value)) newIndex = i; } } else { for (let i = index; i >= 0 && newIndex === undefined; i--) { - if (props.isRowSelectable(visibleRows.value[i].value)) + if (props.isRowSelectable(availableRows.value[i].value)) newIndex = i; } } - if (newIndex != undefined && newIndex >= 0) { - selectRow(visibleRows.value[newIndex], index, event); - } + if (newIndex != undefined && newIndex >= 0) + selectRow(availableRows.value[newIndex], event); } else { - selectRow(row, index, event); + selectRow(row, event); } } @@ -569,8 +598,8 @@ function onArrowPressed(pos: number, event: KeyboardEvent): void { * Row click listener. * Emit all necessary events. */ -function selectRow(row: TableRow, index: number, event: Event): void { - emits("click", row.value, index, event); +function selectRow(row: TableRow, event: Event): void { + emits("click", row.value, row.index, event); if (!props.selectable) return; @@ -582,35 +611,53 @@ function selectRow(row: TableRow, index: number, event: Event): void { emits("select", row.value, tableSelectedRow.value); } -// --- Filter Feature --- +// #endregion --- Select Feature --- +// #region --- Filter Feature --- + +/** search filter record alias { fieldKey: filterValue } */ const filters = ref>({}); +/** check if has any searchable column */ +const hasSearchableColumns = computed(() => { + return tableColumns.value.some((column) => column.searchable); +}); + +let debouncedFilter: ReturnType< + typeof useDebounce> +>; + +// initialise and update debounces filter function watch( - filters, - (value) => { - if (props.backendFiltering) return; - if (props.debounceSearch) - useDebounce( - () => handleFiltersChange(value), - props.debounceSearch, - )(); - else handleFiltersChange(value); - }, - { deep: true }, + () => props.debounceSearch, + (debounce) => + (debouncedFilter = useDebounce(handleFiltersChange, debounce || 0)), + { immediate: true }, ); +// react on filters got changed +watch(filters, (value) => debouncedFilter(value), { deep: true }); + function handleFiltersChange(value: Record): void { emits("filters-change", value); - // recompute rows with updated filters - processTableData(); + // if not backend filtered, recompute rows visibility with updated filters + if (!props.backendFiltering) { + filterTableRows(); + // force tableRows reactivity to update + triggerRef(tableRows); + } } function onFiltersEvent(event: Event): void { emits("filters-event", props.filtersEvent, filters.value, event); } -/** check whether a row is filtered by filter or not */ +/** + * check whether a row is filtered by active filters or not + * @param row - row element + * + * @returns is row filtered in + * */ function isRowFiltered(row: T): boolean { if (!Object.values(filters.value).filter(Boolean).length) return true; return Object.entries(filters.value).some(([key, filter]) => { @@ -637,19 +684,17 @@ function isRowFiltered(row: T): boolean { }); } -function filterRows(rows: TableRow[]): TableRow[] { - return rows.filter((row) => isRowFiltered(row.value)); -} +// #endregion --- Filter Feature --- -// --- Sort Feature --- +// #region --- Sort Feature --- const currentSortColumn = ref>(); const isAsc = ref(true); /** check if has any sortable column */ -const hasSortableColumns = computed(() => - tableColumns.value.some((column) => column.sortable), -); +const hasSortableColumns = computed(() => { + return tableColumns.value.some((column) => column.sortable); +}); /** check if the column is the current sort column */ function isColumnSorted(column: TableColumnItem): boolean { @@ -657,7 +702,7 @@ function isColumnSorted(column: TableColumnItem): boolean { } // call initSort only first time (for example async data) -// initSort must be called after TableColumns got initialised first time +// initSort must be called after async TableColumns got initialised first time onMounted(() => nextTick(() => initSort())); /** initial sorted column based on the default-sort prop */ @@ -685,7 +730,7 @@ function sort( updateDirection = false, event?: Event, ): void { - if (!column || !column.sortable) return; + if (!column?.sortable) return; if (updateDirection) isAsc.value = isColumnSorted(column) @@ -702,8 +747,9 @@ function sort( ); currentSortColumn.value = column; - // recompute rows with updated currentSortColumn - processTableData(); + + // if not backend sorted, sort rows by mutating the tableRows array + if (!props.backendSorting) sortByColumn(tableRows.value); } function sortByField(field: string, direction: "asc" | "desc"): void { @@ -726,10 +772,13 @@ function sortByColumn(rows: TableRow[]): TableRow[] { ? (a, b, asc): number => column.customSort!(a.value, b.value, asc) : undefined, isAsc.value, + true, ); } -// --- Checkable Feature --- +// #endregion --- Sort Feature --- + +// #region --- Checkable Feature --- const tableCheckedRows = defineModel("checkedRows", { default: [], @@ -737,7 +786,7 @@ const tableCheckedRows = defineModel("checkedRows", { /** check if all rows in the page are checked */ const isAllChecked = computed(() => { - const validVisibleData = visibleRows.value.filter((row) => + const validVisibleData = availableRows.value.filter((row) => props.isRowCheckable(row.value), ); if (validVisibleData.length === 0) return false; @@ -748,7 +797,7 @@ const isAllChecked = computed(() => { /** check if all rows in the page are checkable */ const isAllUncheckable = computed( - () => !visibleRows.value.some((row) => props.isRowCheckable(row.value)), + () => !availableRows.value.some((row) => props.isRowCheckable(row.value)), ); /** check if the row is checked (is added to the array) */ @@ -782,7 +831,7 @@ function checkAll(): void { tableCheckedRows.value = []; else { // else set all visible rows as checked - tableCheckedRows.value = visibleRows.value + tableCheckedRows.value = availableRows.value .filter((row) => props.isRowCheckable(row.value)) .map((row) => row.value); } @@ -802,7 +851,9 @@ function checkRow(row: TableRow): void { nextTick(() => emits("check", tableCheckedRows.value, row.value)); } -// --- Detail Row Feature --- +// #endregion --- Checkable Feature --- + +// #region --- Detail Row Feature --- const visibleDetailedRows = defineModel("detailedRows", { default: [], @@ -847,7 +898,9 @@ function isActiveDetailRow(row: TableRow): boolean { return props.detailed && isVisibleDetailRow(row); } -// --- Drag&Drop Feature --- +// #endregion --- Detail Row Feature --- + +// #region --- Drag&Drop Feature --- const isDraggingRow = ref(false); const isDraggingColumn = ref(false); @@ -859,104 +912,82 @@ const canDragColumn = computed( ); /** emits drag start event */ -function handleDragStart( - row: TableRow, - index: number, - event: DragEvent, -): void { +function handleDragStart(row: TableRow, event: DragEvent): void { if (!props.draggable) return; - emits("dragstart", row.value, index, event); + emits("dragstart", row.value, row.index, event); } /** emits drag leave event */ -function handleDragEnd( - row: TableRow, - index: number, - event: DragEvent, -): void { +function handleDragEnd(row: TableRow, event: DragEvent): void { if (!props.draggable) return; - emits("dragend", row.value, index, event); + emits("dragend", row.value, row.index, event); } /** emits drop event */ -function handleDrop(row: TableRow, index: number, event: DragEvent): void { +function handleDrop(row: TableRow, event: DragEvent): void { if (!props.draggable) return; - emits("drop", row.value, index, event); + emits("drop", row.value, row.index, event); } /** emits drag over event */ -function handleDragOver( - row: TableRow, - index: number, - event: DragEvent, -): void { +function handleDragOver(row: TableRow, event: DragEvent): void { if (!props.draggable) return; - emits("dragover", row.value, index, event); + emits("dragover", row.value, row.index, event); } /** emits drag leave event */ -function handleDragLeave( - row: TableRow, - index: number, - event: DragEvent, -): void { +function handleDragLeave(row: TableRow, event: DragEvent): void { if (!props.draggable) return; - emits("dragleave", row.value, index, event); + emits("dragleave", row.value, row.index, event); } /** emits drag start event (column) */ function handleColumnDragStart( - column: TableColumn, - index: number, + column: TableColumnItem, event: DragEvent, ): void { if (!canDragColumn.value) return; isDraggingColumn.value = true; - emits("columndragstart", column, index, event); + emits("columndragstart", column.value, column.index, event); } /** emits drag leave event (column) */ function handleColumnDragEnd( - column: TableColumn, - index: number, + column: TableColumnItem, event: DragEvent, ): void { if (!canDragColumn.value) return; isDraggingColumn.value = false; - emits("columndragend", column, index, event); + emits("columndragend", column.value, column.index, event); } /** emits drop event (column) */ -function handleColumnDrop( - column: TableColumn, - index: number, - event: DragEvent, -): void { +function handleColumnDrop(column: TableColumnItem, event: DragEvent): void { if (!canDragColumn.value) return; - emits("columndrop", column, index, event); + emits("columndrop", column.value, column.index, event); } /** emits drag over event (column) */ function handleColumnDragOver( - column: TableColumn, - index: number, + column: TableColumnItem, event: DragEvent, ): void { if (!canDragColumn.value) return; - emits("columndragover", column, index, event); + emits("columndragover", column.value, column.index, event); } /** emits drag leave event (column) */ function handleColumnDragLeave( - column: TableColumn, - index: number, + column: TableColumnItem, event: DragEvent, ): void { if (!canDragColumn.value) return; - emits("columndragleave", column, index, event); + emits("columndragleave", column.value, column.index, event); } -// --- Computed Component Classes --- +// #endregion --- Drag&Drop Feature --- + +// #region --- Computed Component Classes --- const rootClasses = defineClasses( ["rootClass", "o-table__root"], @@ -985,14 +1016,14 @@ const tableClasses = defineClasses( computed( () => (props.hoverable || props.selectable) && - !!visibleRows.value.length, + !!availableRows.value.length, ), ], [ "emptyClass", "o-table--empty", null, - computed(() => !visibleRows.value.length), + computed(() => !availableRows.value.length), ], ); @@ -1016,17 +1047,25 @@ const footerClasses = defineClasses(["footerClass", "o-table__footer"]); const thBaseClasses = defineClasses(["thClass", "o-table__th"]); -const thCheckboxClasses = defineClasses([ - "thCheckboxClass", - "o-table__th-checkbox", -]); +const thCheckboxClasses = defineClasses( + ["thCheckboxClass", "o-table__th-checkbox"], + [ + "thStickyClass", + "o-table__th--sticky", + null, + computed(() => props.stickyCheckbox), + ], +); const thDetailedClasses = defineClasses([ "thDetailedClass", - "o-table__th--detailed", + "o-table__th-detailed", ]); -const thSubheadingClasses = defineClasses(["thSubheadingClass", "o-table__th"]); +const thSubheadingClasses = defineClasses([ + "thSubheadingClass", + "o-table__th-subheading", +]); const thSortIconClasses = defineClasses([ "thSortIconClass", @@ -1043,6 +1082,13 @@ const trCheckedClasses = defineClasses([ "o-table__tr--checked", ]); +const trEmptyClasses = defineClasses(["trEmptyClass", "o-table__tr-empty"]); + +const trDetailedClasses = defineClasses([ + "trDetailedClass", + "o-table__tr-detail", +]); + const tdBaseClasses = defineClasses(["tdClass", "o-table__td"]); const tdCheckboxClasses = defineClasses( @@ -1060,8 +1106,6 @@ const tdDetailedChevronClasses = defineClasses([ "o-table__td-chevron", ]); -const detailedClasses = defineClasses(["detailedClass", "o-table__detail"]); - const mobileSortClasses = defineClasses([ "mobileSortClass", "o-table__mobile-sort", @@ -1076,7 +1120,7 @@ const paginationWrapperRootClasses = computed(() => getActiveClasses(paginationWrapperClasses), ); -function rowClasses(row: TableRow, index: number): ClassBind[] { +function rowClasses(row: TableRow): ClassBind[] { const selectedClasses = isRowEqual(row.value, tableSelectedRow.value) ? trSelectedClasses.value : []; @@ -1085,21 +1129,28 @@ function rowClasses(row: TableRow, index: number): ClassBind[] { const rowClass = typeof props.rowClass === "function" - ? props.rowClass(row.value, index) || "" + ? props.rowClass(row.value, row.index) || "" : ""; return [...selectedClasses, ...checkedClasses, { [rowClass]: true }]; } -// --- Expose Public Functionalities --- +// #endregion --- Computed Component Classes --- + +// compute initial row visibility +filterTableRows(); + +// #region --- Expose Public Functionalities --- /** expose functionalities for programmatic usage */ -defineExpose({ rows: tableData, sort: sortByField }); +defineExpose({ rows: tableRows, sort: sortByField }); + +// #endregion