Merge branch 'dev' into issue-1057

This commit is contained in:
kyyy
2024-11-12 17:17:46 +07:00
committed by GitHub
10 changed files with 1298 additions and 773 deletions

View File

@@ -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"

View File

@@ -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>

View File

@@ -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.

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
} }
} }
}) })

View File

@@ -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)) {

View File

@@ -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)

View File

@@ -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(() => {

View File

@@ -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"