mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-27 18:30:35 +01:00
Merge branch 'dev' into issue-1057
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// Columns
|
// Columns
|
||||||
const columns = [{
|
const columns = [{
|
||||||
|
key: 'select',
|
||||||
|
class: 'w-2'
|
||||||
|
}, {
|
||||||
key: 'id',
|
key: 'id',
|
||||||
label: '#',
|
label: '#',
|
||||||
sortable: true
|
sortable: true
|
||||||
@@ -20,6 +23,7 @@ const columns = [{
|
|||||||
|
|
||||||
const selectedColumns = ref(columns)
|
const selectedColumns = ref(columns)
|
||||||
const columnsTable = computed(() => columns.filter(column => selectedColumns.value.includes(column)))
|
const columnsTable = computed(() => columns.filter(column => selectedColumns.value.includes(column)))
|
||||||
|
const excludeSelectColumn = computed(() => columns.filter(v => v.key !== 'select'))
|
||||||
|
|
||||||
// Selected Rows
|
// Selected Rows
|
||||||
const selectedRows = ref([])
|
const selectedRows = ref([])
|
||||||
@@ -153,7 +157,7 @@ const { data: todos, status } = await useLazyAsyncData<{
|
|||||||
</UButton>
|
</UButton>
|
||||||
</UDropdown>
|
</UDropdown>
|
||||||
|
|
||||||
<USelectMenu v-model="selectedColumns" :options="columns" multiple>
|
<USelectMenu v-model="selectedColumns" :options="excludeSelectColumn" multiple>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-view-columns"
|
icon="i-heroicons-view-columns"
|
||||||
color="gray"
|
color="gray"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const selected = ref([people[1]])
|
||||||
|
|
||||||
|
const columns = [{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID'
|
||||||
|
}, {
|
||||||
|
key: 'name',
|
||||||
|
label: 'User name'
|
||||||
|
}, {
|
||||||
|
key: 'title',
|
||||||
|
label: 'Job position'
|
||||||
|
}, {
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email'
|
||||||
|
}, {
|
||||||
|
key: 'role'
|
||||||
|
}, {
|
||||||
|
key: 'select',
|
||||||
|
class: 'w-2'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selected" :rows="people" :columns="columns" />
|
||||||
|
</template>
|
||||||
@@ -285,6 +285,65 @@ componentProps:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
|
#### Event Selectable
|
||||||
|
The `UTable` component provides two key events for handling row selection:
|
||||||
|
|
||||||
|
##### ***@select:all***
|
||||||
|
The `@select:all` event is emitted when the header checkbox in a selectable table is toggled. This event returns a boolean value indicating whether all rows are selected (true) or deselected (false).
|
||||||
|
|
||||||
|
##### ***@update:modelValue***
|
||||||
|
The `@update:modelValue` event is emitted whenever the selection state changes, including both individual row selection and bulk selection. This event returns an array containing the currently selected rows.
|
||||||
|
|
||||||
|
Here's how to implement both events:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
const selected = ref([])
|
||||||
|
|
||||||
|
const onHandleSelectAll = (isSelected: boolean) => {
|
||||||
|
console.log('All rows selected:', isSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateSelection = (selectedRows: any[]) => {
|
||||||
|
console.log('Currently selected rows:', selectedRows)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable
|
||||||
|
v-model="selected"
|
||||||
|
:rows="people"
|
||||||
|
@select:all="onHandleSelectAll"
|
||||||
|
@update:modelValue="onUpdateSelection"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Single Select Mode
|
||||||
|
Control how the select function allows only one row to be selected at a time.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- Allow only one row to be selectable at a time -->
|
||||||
|
<UTable :single-select="true" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Checkbox Placement
|
||||||
|
You can customize the checkbox column position by using the `select` key in the `columns` configuration.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
extraClass: 'overflow-hidden'
|
||||||
|
padding: false
|
||||||
|
component: 'table-example-dynamically-render-selectable'
|
||||||
|
componentProps:
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### Contextmenu
|
### Contextmenu
|
||||||
|
|
||||||
Use the `contextmenu` listener on your Table to make the rows righ-clickable. The function will receive the original event as the first argument and the row as the second argument.
|
Use the `contextmenu` listener on your Table to make the rows righ-clickable. The function will receive the original event as the first argument and the row as the second argument.
|
||||||
@@ -393,7 +452,6 @@ Controls whether multiple rows can be expanded simultaneously in the table.
|
|||||||
<!-- Or simply -->
|
<!-- Or simply -->
|
||||||
<UTable />
|
<UTable />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Disable Row Expansion
|
#### Disable Row Expansion
|
||||||
@@ -534,6 +592,82 @@ componentProps:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### `select-header`
|
||||||
|
This slot allows you to customize the checkbox appearance in the table header for selecting all rows at once while using feature [Selectable](#selectable).
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selectable">
|
||||||
|
<template #select-header="{ checked, change, indeterminate }">
|
||||||
|
<!-- Place your custom component here -->
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `checked` | `Boolean` | Indicates if all rows are selected |
|
||||||
|
| `change` | `Function` | Function to handle selection state changes. Must receive a boolean value (true/false) |
|
||||||
|
| `indeterminate` | `Boolean` | Indicates partial selection (when some rows are selected) |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UTable>
|
||||||
|
<!-- Header checkbox customization -->
|
||||||
|
<template #select-header="{ indeterminate, checked, change }">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:indeterminate="indeterminate"
|
||||||
|
:checked="checked"
|
||||||
|
@change="e => change(e.target.checked)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `select-data`
|
||||||
|
This slot allows you to customize the checkbox appearance for each row in the table while using feature [Selectable](#selectable).
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UTable v-model="selectable">
|
||||||
|
<template #select-data="{ checked, change }">
|
||||||
|
<!-- Place your custom component here -->
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `checked` | `Boolean` | Indicates if the current row is selected |
|
||||||
|
| `change` | `Function` | Function to handle selection state changes. Must receive a boolean value (true/false) |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UTable>
|
||||||
|
<!-- Row checkbox customization -->
|
||||||
|
<template #select-data="{ checked, change }">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="checked"
|
||||||
|
@change="e => change(e.target.checked)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
### `expand-action`
|
### `expand-action`
|
||||||
|
|
||||||
The `#expand-action` slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state.
|
The `#expand-action` slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"@headlessui/tailwindcss": "^0.2.1",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@iconify-json/heroicons": "^1.2.1",
|
"@iconify-json/heroicons": "^1.2.1",
|
||||||
"@nuxt/icon": "^1.6.1",
|
"@nuxt/icon": "^1.7.2",
|
||||||
"@nuxt/kit": "^3.14.159",
|
"@nuxt/kit": "^3.14.159",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||||
@@ -80,6 +80,8 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@nuxt/ui": "workspace:*",
|
"@nuxt/ui": "workspace:*",
|
||||||
"@nuxt/content": "2.13.2",
|
"@nuxt/content": "2.13.2",
|
||||||
"@nuxtjs/mdc": "0.9.0"
|
"@nuxtjs/mdc": "0.9.0",
|
||||||
|
"nuxt": "3.13.2",
|
||||||
|
"@nuxt/kit": "3.13.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1653
pnpm-lock.yaml
generated
1653
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -8,28 +8,27 @@
|
|||||||
</slot>
|
</slot>
|
||||||
<thead :class="ui.thead">
|
<thead :class="ui.thead">
|
||||||
<tr :class="ui.tr.base">
|
<tr :class="ui.tr.base">
|
||||||
<th v-if="modelValue" scope="col" :class="ui.checkbox.padding">
|
|
||||||
<UCheckbox
|
|
||||||
:model-value="isAllRowChecked"
|
|
||||||
:indeterminate="indeterminate"
|
|
||||||
v-bind="ui.default.checkbox"
|
|
||||||
aria-label="Select all"
|
|
||||||
@change="onChange"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th v-if="expand" scope="col" :class="ui.tr.base">
|
<th v-if="expand" scope="col" :class="ui.tr.base">
|
||||||
<span class="sr-only">Expand</span>
|
<span class="sr-only">Expand</span>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th
|
<th
|
||||||
v-for="(column, index) in columns"
|
v-for="(column, index) in columns"
|
||||||
:key="index"
|
:key="index"
|
||||||
scope="col"
|
scope="col"
|
||||||
:class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.class]"
|
:class="[ui.th.base, ui.th.padding, ui.th.color, ui.th.font, ui.th.size, column.key === 'select' && ui.checkbox.padding, column.class]"
|
||||||
:aria-sort="getAriaSort(column)"
|
:aria-sort="getAriaSort(column)"
|
||||||
>
|
>
|
||||||
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
|
<slot v-if="!singleSelect && modelValue && (column.key === 'select' || shouldRenderColumnInFirstPlace(index, 'select'))" name="select-header" :indeterminate="indeterminate" :checked="isAllRowChecked" :change="onChange">
|
||||||
|
<UCheckbox
|
||||||
|
:model-value="isAllRowChecked"
|
||||||
|
:indeterminate="indeterminate"
|
||||||
|
v-bind="ui.default.checkbox"
|
||||||
|
aria-label="Select all"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<slot v-else :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="column.sortable"
|
v-if="column.sortable"
|
||||||
v-bind="{ ...(ui.default.sortButton || {}), ...sortButton }"
|
v-bind="{ ...(ui.default.sortButton || {}), ...sortButton }"
|
||||||
@@ -77,16 +76,7 @@
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="(row, index) in rows" :key="index">
|
<template v-for="(row, index) in rows" :key="index">
|
||||||
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, ($attrs.onSelect || $attrs.onContextmenu) && ui.tr.active, row?.class]" @click="() => onSelect(row)" @contextmenu="(event) => onContextmenu(event, row)">
|
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, isExpanded(row) && ui.tr.expanded, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)" @contextmenu="(event) => onContextmenu(event, row)">
|
||||||
<td v-if="modelValue" :class="ui.checkbox.padding">
|
|
||||||
<UCheckbox
|
|
||||||
:model-value="isSelected(row)"
|
|
||||||
v-bind="ui.default.checkbox"
|
|
||||||
aria-label="Select row"
|
|
||||||
@change="onChangeCheckbox($event, row)"
|
|
||||||
@click.capture.stop="() => onSelect(row)"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
<td
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
|
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
|
||||||
@@ -102,8 +92,26 @@
|
|||||||
@click.capture.stop="toggleOpened(row)"
|
@click.capture.stop="toggleOpened(row)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class]">
|
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class, column.key === 'select' && ui.checkbox.padding]">
|
||||||
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
|
<slot v-if="modelValue && (column.key === 'select' || shouldRenderColumnInFirstPlace(subIndex, 'select')) " name="select-data" :checked="isSelected(row)" :change="(ev: boolean) => onChangeCheckbox(ev, row)">
|
||||||
|
<UCheckbox
|
||||||
|
:model-value="isSelected(row)"
|
||||||
|
v-bind="ui.default.checkbox"
|
||||||
|
aria-label="Select row"
|
||||||
|
@change="onChangeCheckbox($event, row)"
|
||||||
|
@click.capture.stop="() => onSelect(row)"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
:key="retriggerSlot"
|
||||||
|
:name="`${column.key}-data`"
|
||||||
|
:column="column"
|
||||||
|
:row="row"
|
||||||
|
:index="index"
|
||||||
|
:get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)"
|
||||||
|
>
|
||||||
{{ getRowData(row, column.key) }}
|
{{ getRowData(row, column.key) }}
|
||||||
</slot>
|
</slot>
|
||||||
</td>
|
</td>
|
||||||
@@ -125,11 +133,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, toRaw, toRef } from 'vue'
|
import { computed, defineComponent, ref, toRaw, toRef, watch } from 'vue'
|
||||||
import type { PropType, AriaAttributes } from 'vue'
|
import type { PropType, AriaAttributes } from 'vue'
|
||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { isEqual } from 'ohash'
|
||||||
import UIcon from '../elements/Icon.vue'
|
import UIcon from '../elements/Icon.vue'
|
||||||
import UButton from '../elements/Button.vue'
|
import UButton from '../elements/Button.vue'
|
||||||
import UProgress from '../elements/Progress.vue'
|
import UProgress from '../elements/Progress.vue'
|
||||||
@@ -144,7 +153,7 @@ import { table } from '#ui/ui.config'
|
|||||||
const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table)
|
const config = mergeConfig<typeof table>(appConfig.ui.strategy, appConfig.ui.table, table)
|
||||||
|
|
||||||
function defaultComparator<T>(a: T, z: T): boolean {
|
function defaultComparator<T>(a: T, z: T): boolean {
|
||||||
return JSON.stringify(a) === JSON.stringify(z)
|
return isEqual(a, z)
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
|
function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
|
||||||
@@ -159,6 +168,14 @@ function defaultSort(a: any, b: any, direction: 'asc' | 'desc') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStringifiedSet(arr: TableRow[]) {
|
||||||
|
return new Set(arr.map(item => JSON.stringify(item)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function accessor<T extends Record<string, any>>(key: string) {
|
||||||
|
return (obj: T) => get(obj, key)
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
UIcon,
|
UIcon,
|
||||||
@@ -221,7 +238,7 @@ export default defineComponent({
|
|||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
loadingState: {
|
loadingState: {
|
||||||
type: Object as PropType<{ icon: string, label: string }>,
|
type: Object as PropType<{ icon: string, label: string } | null>,
|
||||||
default: () => config.default.loadingState
|
default: () => config.default.loadingState
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
@@ -247,9 +264,13 @@ export default defineComponent({
|
|||||||
multipleExpand: {
|
multipleExpand: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
singleSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'update:sort', 'update:expand'],
|
emits: ['update:modelValue', 'update:sort', 'update:expand', 'select:all'],
|
||||||
setup(props, { emit, attrs: $attrs }) {
|
setup(props, { emit, attrs: $attrs }) {
|
||||||
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
|
const { ui, attrs } = useUI('table', toRef(props, 'ui'), config, toRef(props, 'class'))
|
||||||
|
|
||||||
@@ -264,6 +285,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const retriggerSlot = ref(null)
|
||||||
|
|
||||||
const savedSort = { column: sort.value.column, direction: null }
|
const savedSort = { column: sort.value.column, direction: null }
|
||||||
|
|
||||||
const rows = computed(() => {
|
const rows = computed(() => {
|
||||||
@@ -292,8 +315,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const getStringifiedSet = (arr: TableRow[]) => new Set(arr.map(item => JSON.stringify(item)))
|
|
||||||
|
|
||||||
const totalRows = computed(() => props.rows.length)
|
const totalRows = computed(() => props.rows.length)
|
||||||
|
|
||||||
const countCheckedRow = computed(() => {
|
const countCheckedRow = computed(() => {
|
||||||
@@ -328,10 +349,6 @@ export default defineComponent({
|
|||||||
return props.by(a, z)
|
return props.by(a, z)
|
||||||
}
|
}
|
||||||
|
|
||||||
function accessor<T extends Record<string, any>>(key: string) {
|
|
||||||
return (obj: T) => get(obj, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelected(row: TableRow) {
|
function isSelected(row: TableRow) {
|
||||||
if (!props.modelValue) {
|
if (!props.modelValue) {
|
||||||
return false
|
return false
|
||||||
@@ -355,6 +372,11 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSelect(row: TableRow) {
|
function onSelect(row: TableRow) {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection && selection.toString().length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!$attrs.onSelect) {
|
if (!$attrs.onSelect) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -393,11 +415,12 @@ export default defineComponent({
|
|||||||
} else {
|
} else {
|
||||||
selected.value = []
|
selected.value = []
|
||||||
}
|
}
|
||||||
|
emit('select:all', checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeCheckbox(checked: boolean, row: TableRow) {
|
function onChangeCheckbox(checked: boolean, row: TableRow) {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
selected.value.push(row)
|
selected.value = props.singleSelect ? [row] : [...selected.value, row]
|
||||||
} else {
|
} else {
|
||||||
const index = selected.value.findIndex(item => compare(item, row))
|
const index = selected.value.findIndex(item => compare(item, row))
|
||||||
selected.value.splice(index, 1)
|
selected.value.splice(index, 1)
|
||||||
@@ -412,6 +435,13 @@ export default defineComponent({
|
|||||||
return expand.value?.openedRows ? expand.value.openedRows.some(openedRow => compare(openedRow, row)) : false
|
return expand.value?.openedRows ? expand.value.openedRows.some(openedRow => compare(openedRow, row)) : false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRenderColumnInFirstPlace(index: number, key: string) {
|
||||||
|
if (!props.columns) {
|
||||||
|
return index === 0
|
||||||
|
}
|
||||||
|
return index === 0 && !props.columns.find(col => col.key === key)
|
||||||
|
}
|
||||||
|
|
||||||
function toggleOpened(row: TableRow) {
|
function toggleOpened(row: TableRow) {
|
||||||
expand.value = {
|
expand.value = {
|
||||||
openedRows: isExpanded(row) ? expand.value.openedRows.filter(v => !compare(v, row)) : props.multipleExpand ? [...expand.value.openedRows, row] : [row],
|
openedRows: isExpanded(row) ? expand.value.openedRows.filter(v => !compare(v, row)) : props.multipleExpand ? [...expand.value.openedRows, row] : [row],
|
||||||
@@ -439,6 +469,12 @@ export default defineComponent({
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(rows, () => {
|
||||||
|
retriggerSlot.value = new Date()
|
||||||
|
}, {
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
@@ -465,7 +501,9 @@ export default defineComponent({
|
|||||||
getRowData,
|
getRowData,
|
||||||
toggleOpened,
|
toggleOpened,
|
||||||
getAriaSort,
|
getAriaSort,
|
||||||
isExpanded
|
isExpanded,
|
||||||
|
shouldRenderColumnInFirstPlace,
|
||||||
|
retriggerSlot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -252,10 +252,10 @@ async function validateJoiSchema(
|
|||||||
schema: JoiSchema
|
schema: JoiSchema
|
||||||
): Promise<ValidateReturnSchema<typeof state>> {
|
): Promise<ValidateReturnSchema<typeof state>> {
|
||||||
try {
|
try {
|
||||||
await schema.validateAsync(state, { abortEarly: false })
|
const result = await schema.validateAsync(state, { abortEarly: false })
|
||||||
return {
|
return {
|
||||||
errors: null,
|
errors: null,
|
||||||
result: state
|
result
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isJoiError(error)) {
|
if (isJoiError(error)) {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||||
:key="index"
|
:key="index"
|
||||||
as="template"
|
as="template"
|
||||||
:value="valueAttribute ? option[valueAttribute] : option"
|
:value="valueAttribute ? accessor(option, valueAttribute) : option"
|
||||||
:disabled="option.disabled"
|
:disabled="option.disabled"
|
||||||
>
|
>
|
||||||
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, selected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
|
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, selected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
|
||||||
@@ -104,6 +104,7 @@ import {
|
|||||||
import { computedAsync, useDebounceFn } from '@vueuse/core'
|
import { computedAsync, useDebounceFn } from '@vueuse/core'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { twMerge, twJoin } from 'tailwind-merge'
|
import { twMerge, twJoin } from 'tailwind-merge'
|
||||||
|
import { isEqual } from 'ohash'
|
||||||
import UIcon from '../elements/Icon.vue'
|
import UIcon from '../elements/Icon.vue'
|
||||||
import UAvatar from '../elements/Avatar.vue'
|
import UAvatar from '../elements/Avatar.vue'
|
||||||
import { useUI } from '../../composables/useUI'
|
import { useUI } from '../../composables/useUI'
|
||||||
@@ -308,8 +309,28 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getValue(value: any) {
|
||||||
|
if (props.valueAttribute) {
|
||||||
|
return accessor(value, props.valueAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareValues(value1: any, value2: any) {
|
||||||
|
if (props.by && typeof value1 === 'object' && typeof value2 === 'object') {
|
||||||
|
return isEqual(value1[props.by], value2[props.by])
|
||||||
|
}
|
||||||
|
return isEqual(value1, value2)
|
||||||
|
}
|
||||||
|
|
||||||
if (props.valueAttribute) {
|
if (props.valueAttribute) {
|
||||||
const option = options.value.find(option => option[props.valueAttribute] === props.modelValue)
|
const option = options.value.find((option) => {
|
||||||
|
const optionValue = getValue(option)
|
||||||
|
|
||||||
|
return compareValues(optionValue, props.modelValue)
|
||||||
|
})
|
||||||
|
|
||||||
return option ? accessor(option, props.optionAttribute) : null
|
return option ? accessor(option, props.optionAttribute) : null
|
||||||
} else {
|
} else {
|
||||||
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
|
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
v-slot="{ active, selected: optionSelected, disabled: optionDisabled }"
|
v-slot="{ active, selected: optionSelected, disabled: optionDisabled }"
|
||||||
:key="index"
|
:key="index"
|
||||||
as="template"
|
as="template"
|
||||||
:value="valueAttribute ? option[valueAttribute] : option"
|
:value="valueAttribute ? accessor(option, valueAttribute) : option"
|
||||||
:disabled="option.disabled"
|
:disabled="option.disabled"
|
||||||
>
|
>
|
||||||
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, optionSelected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
|
<li :class="[uiMenu.option.base, uiMenu.option.rounded, uiMenu.option.padding, uiMenu.option.size, uiMenu.option.color, active ? uiMenu.option.active : uiMenu.option.inactive, optionSelected && uiMenu.option.selected, optionDisabled && uiMenu.option.disabled]">
|
||||||
@@ -143,6 +143,7 @@ import {
|
|||||||
import { computedAsync, useDebounceFn } from '@vueuse/core'
|
import { computedAsync, useDebounceFn } from '@vueuse/core'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { twMerge, twJoin } from 'tailwind-merge'
|
import { twMerge, twJoin } from 'tailwind-merge'
|
||||||
|
import { isEqual } from 'ohash'
|
||||||
import UIcon from '../elements/Icon.vue'
|
import UIcon from '../elements/Icon.vue'
|
||||||
import UAvatar from '../elements/Avatar.vue'
|
import UAvatar from '../elements/Avatar.vue'
|
||||||
import { useUI } from '../../composables/useUI'
|
import { useUI } from '../../composables/useUI'
|
||||||
@@ -379,39 +380,53 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selected = computed(() => {
|
const selected = computed(() => {
|
||||||
|
function compareValues(value1: any, value2: any) {
|
||||||
|
if (props.by && typeof value1 === 'object' && typeof value2 === 'object') {
|
||||||
|
return isEqual(value1[props.by], value2[props.by])
|
||||||
|
}
|
||||||
|
return isEqual(value1, value2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue(value: any) {
|
||||||
|
if (props.valueAttribute) {
|
||||||
|
return accessor(value, props.valueAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
if (!Array.isArray(props.modelValue) || !props.modelValue.length) {
|
const modelValue = props.modelValue
|
||||||
|
if (!Array.isArray(modelValue) || !modelValue.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.valueAttribute) {
|
return options.value.filter((option) => {
|
||||||
return options.value.filter(option => (props.modelValue as any[]).includes(option[props.valueAttribute]))
|
const optionValue = getValue(option)
|
||||||
}
|
return modelValue.some(value => compareValues(value, optionValue))
|
||||||
return options.value.filter(option => (props.modelValue as any[]).includes(option))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.valueAttribute) {
|
return options.value.find((option) => {
|
||||||
return options.value.find(option => option[props.valueAttribute] === props.modelValue)
|
const optionValue = getValue(option)
|
||||||
}
|
return compareValues(optionValue, toRaw(props.modelValue))
|
||||||
return options.value.find(option => option === props.modelValue)
|
}) ?? props.modelValue
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
if (props.multiple) {
|
if (!selected.value) return null
|
||||||
if (Array.isArray(props.modelValue) && props.modelValue.length) {
|
|
||||||
return `${selected.value.length} selected`
|
if (props.valueAttribute) {
|
||||||
} else {
|
return accessor(selected.value as Record<string, any>, props.optionAttribute)
|
||||||
return null
|
|
||||||
}
|
|
||||||
} else if (props.modelValue !== undefined && props.modelValue !== null) {
|
|
||||||
if (props.valueAttribute) {
|
|
||||||
return accessor(selected.value, props.optionAttribute) ?? null
|
|
||||||
} else {
|
|
||||||
return ['string', 'number'].includes(typeof props.modelValue) ? props.modelValue : accessor(props.modelValue as Record<string, any>, props.optionAttribute)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
if (Array.isArray(props.modelValue) && props.modelValue.length) {
|
||||||
|
return `${props.modelValue.length} selected`
|
||||||
|
} else if (['string', 'number'].includes(typeof props.modelValue)) {
|
||||||
|
return props.modelValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessor(props.modelValue as Record<string, any>, props.optionAttribute)
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectClass = computed(() => {
|
const selectClass = computed(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div :class="wrapperClass" role="region" v-bind="attrs">
|
<div v-if="notifications.length" :class="wrapperClass" role="region" v-bind="attrs">
|
||||||
<div v-if="notifications.length" :class="ui.container">
|
<div :class="ui.container">
|
||||||
<div v-for="notification of notifications" :key="notification.id">
|
<div v-for="notification of notifications" :key="notification.id">
|
||||||
<UNotification
|
<UNotification
|
||||||
v-bind="notification"
|
v-bind="notification"
|
||||||
|
|||||||
Reference in New Issue
Block a user