@indielayer/ui 1.14.5 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/pages/component/avatar/usage.vue +1 -1
- package/docs/pages/component/input/usage.vue +22 -8
- package/docs/pages/component/table/selectable.vue +1 -1
- package/docs/pages/component/table/virtual.vue +2 -1
- package/docs/pages/component/tag/usage.vue +1 -1
- package/docs/pages/component/textarea/usage.vue +22 -8
- package/lib/components/avatar/Avatar.vue2.js +20 -19
- package/lib/components/avatar/theme/Avatar.base.theme.js +9 -12
- package/lib/components/datepicker/Datepicker.vue.js +1 -1
- package/lib/components/drawer/Drawer.vue.js +66 -60
- package/lib/components/input/Input.vue.d.ts +8 -0
- package/lib/components/input/Input.vue.js +84 -69
- package/lib/components/inputFooter/InputFooter.vue.d.ts +13 -2
- package/lib/components/inputFooter/InputFooter.vue.js +35 -19
- package/lib/components/inputFooter/theme/InputFooter.base.theme.js +3 -1
- package/lib/components/inputFooter/theme/InputFooter.carbon.theme.js +3 -1
- package/lib/components/popover/Popover.vue.d.ts +1 -1
- package/lib/components/select/Select.vue.d.ts +38 -10
- package/lib/components/select/Select.vue.js +210 -200
- package/lib/components/table/Table.vue.d.ts +55 -19
- package/lib/components/table/Table.vue.js +256 -214
- package/lib/components/table/TableCell.vue.d.ts +9 -0
- package/lib/components/table/TableCell.vue.js +45 -21
- package/lib/components/table/TableHeader.vue.js +14 -14
- package/lib/components/table/theme/TableCell.base.theme.js +3 -3
- package/lib/components/tag/Tag.vue.d.ts +3 -0
- package/lib/components/tag/Tag.vue.js +37 -35
- package/lib/components/textarea/Textarea.vue.d.ts +19 -3
- package/lib/components/textarea/Textarea.vue.js +98 -76
- package/lib/components/textarea/theme/Textarea.base.theme.js +2 -1
- package/lib/components/textarea/theme/Textarea.carbon.theme.js +2 -1
- package/lib/components/upload/Upload.vue.js +91 -86
- package/lib/index.js +1 -1
- package/lib/index.umd.js +4 -4
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/components/avatar/Avatar.vue +2 -2
- package/src/components/avatar/theme/Avatar.base.theme.ts +0 -5
- package/src/components/datepicker/Datepicker.vue +6 -1
- package/src/components/drawer/Drawer.vue +13 -2
- package/src/components/input/Input.vue +27 -2
- package/src/components/inputFooter/InputFooter.vue +35 -3
- package/src/components/inputFooter/theme/InputFooter.base.theme.ts +2 -0
- package/src/components/inputFooter/theme/InputFooter.carbon.theme.ts +2 -0
- package/src/components/select/Select.vue +21 -8
- package/src/components/table/Table.vue +170 -48
- package/src/components/table/TableCell.vue +23 -0
- package/src/components/table/TableHeader.vue +2 -2
- package/src/components/table/theme/TableCell.base.theme.ts +20 -11
- package/src/components/tag/Tag.vue +8 -3
- package/src/components/textarea/Textarea.vue +63 -30
- package/src/components/textarea/theme/Textarea.base.theme.ts +2 -0
- package/src/components/textarea/theme/Textarea.carbon.theme.ts +2 -0
- package/src/components/upload/Upload.vue +12 -2
- package/src/version.ts +1 -1
package/lib/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "1.
|
|
1
|
+
declare const _default: "1.15.0";
|
|
2
2
|
export default _default;
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -78,11 +78,11 @@ const { styles, classes, className } = useTheme('Avatar', {}, props, { source })
|
|
|
78
78
|
v-if="source"
|
|
79
79
|
:alt="alt"
|
|
80
80
|
:src="image"
|
|
81
|
-
class="h-full w-full"
|
|
81
|
+
class="absolute top-0 left-0 h-full w-full"
|
|
82
82
|
/>
|
|
83
83
|
|
|
84
84
|
<span
|
|
85
|
-
v-
|
|
85
|
+
v-if="name"
|
|
86
86
|
class="leading-none"
|
|
87
87
|
>{{ initials }}</span>
|
|
88
88
|
|
|
@@ -22,11 +22,6 @@ const theme: AvatarTheme = {
|
|
|
22
22
|
styles({ props, colors, css, data }) {
|
|
23
23
|
const color = colors.getPalette(props.color)
|
|
24
24
|
|
|
25
|
-
if (data.source) return css.variables({
|
|
26
|
-
bg: 'transparent',
|
|
27
|
-
border: props.outlined ? color[500] : 'transparent',
|
|
28
|
-
})
|
|
29
|
-
|
|
30
25
|
return css.variables({
|
|
31
26
|
bg: color[100],
|
|
32
27
|
text: color[500],
|
|
@@ -190,7 +190,7 @@ const { styles, classes, className } = useTheme('Datepicker', {}, props)
|
|
|
190
190
|
|
|
191
191
|
<template>
|
|
192
192
|
<div
|
|
193
|
-
:style="[styles, { '--dp-clear-btn-top': !!label ? '2.
|
|
193
|
+
:style="[styles, { '--dp-clear-btn-top': !!label ? '2.75rem' : '1.2rem' }]"
|
|
194
194
|
:class="[
|
|
195
195
|
className,
|
|
196
196
|
classes.wrapper,
|
|
@@ -339,6 +339,11 @@ const { styles, classes, className } = useTheme('Datepicker', {}, props)
|
|
|
339
339
|
top: var(--dp-clear-btn-top, 2.75rem) !important;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
.dp--clear-btn svg {
|
|
343
|
+
height: 1rem;
|
|
344
|
+
width: 1rem;
|
|
345
|
+
}
|
|
346
|
+
|
|
342
347
|
.dp__theme_dark {
|
|
343
348
|
--dp-background-color: var(--x-datepicker-dark-bg, #212121) !important;
|
|
344
349
|
--dp-text-color: var(--x-datepicker-dark-text, #fff) !important;
|
|
@@ -137,7 +137,13 @@ function onEnter(el: Element, done: () => void) {
|
|
|
137
137
|
|
|
138
138
|
return
|
|
139
139
|
}
|
|
140
|
-
|
|
140
|
+
|
|
141
|
+
const handler = () => {
|
|
142
|
+
el.removeEventListener('transitionend', handler)
|
|
143
|
+
done()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
el.addEventListener('transitionend', handler)
|
|
141
147
|
setTimeout(() => {
|
|
142
148
|
if (props.backdrop) el.classList.add('bg-slate-500/30')
|
|
143
149
|
if (props.position === 'top') (el as HTMLElement).style.top = '0'
|
|
@@ -150,7 +156,12 @@ function onEnter(el: Element, done: () => void) {
|
|
|
150
156
|
function onBeforeLeave(el: Element) {}
|
|
151
157
|
|
|
152
158
|
function onLeave(el: Element, done: () => void) {
|
|
153
|
-
|
|
159
|
+
const handler = () => {
|
|
160
|
+
el.removeEventListener('transitionend', handler)
|
|
161
|
+
done()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
el.addEventListener('transitionend', handler)
|
|
154
165
|
setTimeout(() => {
|
|
155
166
|
if (props.backdrop) el.classList.remove('bg-slate-500/30')
|
|
156
167
|
if (props.position === 'top') (el as HTMLElement).style.top = `-${props.height}px`
|
|
@@ -26,6 +26,8 @@ const inputProps = {
|
|
|
26
26
|
},
|
|
27
27
|
step: [Number, String],
|
|
28
28
|
block: Boolean,
|
|
29
|
+
showCounter: Boolean,
|
|
30
|
+
clearable: Boolean,
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export type InputProps = ExtractPublicPropTypes<typeof inputProps>
|
|
@@ -49,7 +51,7 @@ import { useColors } from '../../composables/useColors'
|
|
|
49
51
|
import { useCommon } from '../../composables/useCommon'
|
|
50
52
|
import { useInputtable } from '../../composables/useInputtable'
|
|
51
53
|
import { useInteractive } from '../../composables/useInteractive'
|
|
52
|
-
import { eyeIcon, eyeVisibleIcon } from '../../common/icons'
|
|
54
|
+
import { closeIcon, eyeIcon, eyeVisibleIcon } from '../../common/icons'
|
|
53
55
|
|
|
54
56
|
import XLabel from '../label/Label.vue'
|
|
55
57
|
import XIcon from '../icon/Icon.vue'
|
|
@@ -95,6 +97,8 @@ function togglePasswordVisibility() {
|
|
|
95
97
|
currentType.value = currentType.value === 'password' ? 'text' : 'password'
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
const showClearIcon = computed(() => props.clearable && props.modelValue !== '')
|
|
101
|
+
|
|
98
102
|
const { focus, blur } = useInteractive(elRef)
|
|
99
103
|
|
|
100
104
|
const {
|
|
@@ -107,6 +111,12 @@ const {
|
|
|
107
111
|
setError,
|
|
108
112
|
} = useInputtable(props, { focus, emit })
|
|
109
113
|
|
|
114
|
+
const currentLength = computed(() => {
|
|
115
|
+
const value = props.modelValue
|
|
116
|
+
|
|
117
|
+
return value ? String(value).length : 0
|
|
118
|
+
})
|
|
119
|
+
|
|
110
120
|
const { styles, classes, className } = useTheme('Input', {}, props, { errorInternal })
|
|
111
121
|
|
|
112
122
|
defineExpose({ focus, blur, reset, validate, setError })
|
|
@@ -170,6 +180,14 @@ defineExpose({ focus, blur, reset, validate, setError })
|
|
|
170
180
|
/>
|
|
171
181
|
|
|
172
182
|
<slot name="suffix">
|
|
183
|
+
<x-icon
|
|
184
|
+
v-if="showClearIcon"
|
|
185
|
+
:size="size"
|
|
186
|
+
:icon="closeIcon"
|
|
187
|
+
class="mr-2 right-1 cursor-pointer"
|
|
188
|
+
:class="classes.icon"
|
|
189
|
+
@click="reset()"
|
|
190
|
+
/>
|
|
173
191
|
<x-icon
|
|
174
192
|
v-if="iconRight"
|
|
175
193
|
:size="size"
|
|
@@ -188,6 +206,13 @@ defineExpose({ focus, blur, reset, validate, setError })
|
|
|
188
206
|
</slot>
|
|
189
207
|
</div>
|
|
190
208
|
|
|
191
|
-
<x-input-footer
|
|
209
|
+
<x-input-footer
|
|
210
|
+
v-if="!hideFooterInternal"
|
|
211
|
+
:error="errorInternal"
|
|
212
|
+
:helper="helper"
|
|
213
|
+
:character-count="currentLength"
|
|
214
|
+
:max-characters="maxlength"
|
|
215
|
+
:show-counter="showCounter"
|
|
216
|
+
/>
|
|
192
217
|
</x-label>
|
|
193
218
|
</template>
|
|
@@ -2,28 +2,60 @@
|
|
|
2
2
|
const inputFooterProps = {
|
|
3
3
|
helper: String,
|
|
4
4
|
error: String,
|
|
5
|
+
characterCount: Number,
|
|
6
|
+
maxCharacters: [Number, String],
|
|
7
|
+
showCounter: Boolean,
|
|
5
8
|
}
|
|
6
9
|
|
|
7
10
|
export type InputFooterProps = ExtractPublicPropTypes<typeof inputFooterProps>
|
|
8
11
|
|
|
9
|
-
type InternalClasses = 'wrapper' | 'helperText' | 'errorText'
|
|
12
|
+
type InternalClasses = 'wrapper' | 'helperText' | 'errorText' | 'container' | 'counter'
|
|
10
13
|
export interface InputFooterTheme extends ThemeComponent<InputFooterProps, InternalClasses> {}
|
|
11
14
|
|
|
12
15
|
export default { name: 'XInputFooter' }
|
|
13
16
|
</script>
|
|
14
17
|
|
|
15
18
|
<script setup lang="ts">
|
|
19
|
+
import { computed } from 'vue'
|
|
16
20
|
import type { ExtractPublicPropTypes } from 'vue'
|
|
17
21
|
import { useTheme, type ThemeComponent } from '../../composables/useTheme'
|
|
18
22
|
|
|
19
23
|
const props = defineProps(inputFooterProps)
|
|
20
24
|
|
|
21
25
|
const { styles, classes, className } = useTheme('InputFooter', {}, props)
|
|
26
|
+
|
|
27
|
+
const maxChars = computed(() => {
|
|
28
|
+
return props.maxCharacters ? Number(props.maxCharacters) : undefined
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const counterText = computed(() => {
|
|
32
|
+
if (props.characterCount === undefined) return ''
|
|
33
|
+
|
|
34
|
+
if (maxChars.value) {
|
|
35
|
+
return `${props.characterCount}/${maxChars.value}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `${props.characterCount}`
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const hasMessage = computed(() => props.error || props.helper)
|
|
22
42
|
</script>
|
|
23
43
|
|
|
24
44
|
<template>
|
|
25
45
|
<div :class="[className, classes.wrapper]" :style="styles">
|
|
26
|
-
<
|
|
27
|
-
|
|
46
|
+
<div v-if="hasMessage || showCounter" :class="classes.container">
|
|
47
|
+
<div>
|
|
48
|
+
<p v-if="error" :class="classes.errorText">{{ error }}</p>
|
|
49
|
+
<p v-else-if="helper" :class="classes.helperText">{{ helper }}</p>
|
|
50
|
+
</div>
|
|
51
|
+
<p
|
|
52
|
+
v-if="showCounter"
|
|
53
|
+
:class="classes.counter"
|
|
54
|
+
role="status"
|
|
55
|
+
aria-live="polite"
|
|
56
|
+
>
|
|
57
|
+
{{ counterText }}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
28
60
|
</div>
|
|
29
61
|
</template>
|
|
@@ -3,8 +3,10 @@ import type { InputFooterTheme } from '../InputFooter.vue'
|
|
|
3
3
|
const theme: InputFooterTheme = {
|
|
4
4
|
classes: {
|
|
5
5
|
wrapper: 'text-xs mt-1',
|
|
6
|
+
container: 'flex justify-between items-start gap-2',
|
|
6
7
|
helperText: 'text-secondary-500 dark:text-secondary-400',
|
|
7
8
|
errorText: 'text-error-500 dark:text-error-400',
|
|
9
|
+
counter: 'text-secondary-500 dark:text-secondary-400 whitespace-nowrap',
|
|
8
10
|
},
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -3,8 +3,10 @@ import type { InputFooterTheme } from '../InputFooter.vue'
|
|
|
3
3
|
const theme: InputFooterTheme = {
|
|
4
4
|
classes: {
|
|
5
5
|
wrapper: 'text-xs mt-1',
|
|
6
|
+
container: 'flex justify-between items-start gap-2',
|
|
6
7
|
helperText: 'text-secondary-500 dark:text-secondary-400',
|
|
7
8
|
errorText: 'text-error-500 dark:text-error-400',
|
|
9
|
+
counter: 'text-secondary-500 dark:text-secondary-400 whitespace-nowrap',
|
|
8
10
|
},
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -110,19 +110,32 @@ const selected = computed<any | any[]>({
|
|
|
110
110
|
},
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
+
const labelCache = computed(() => {
|
|
114
|
+
if (!props.options) return new Map<SelectOption, string>()
|
|
115
|
+
|
|
116
|
+
return new Map(props.options.map((option) => [option, option.label.toLowerCase()]))
|
|
117
|
+
})
|
|
118
|
+
|
|
113
119
|
const internalOptions = computed(() => {
|
|
114
120
|
if (!props.options || props.options.length === 0) return []
|
|
115
121
|
|
|
122
|
+
const filterLower = filter.value.toLowerCase()
|
|
123
|
+
const hasFilter = filter.value !== ''
|
|
124
|
+
|
|
125
|
+
const selectedSet = new Set(
|
|
126
|
+
internalMultiple.value && Array.isArray(selected.value)
|
|
127
|
+
? selected.value
|
|
128
|
+
: [],
|
|
129
|
+
)
|
|
130
|
+
const singleSelectedValue = !internalMultiple.value ? selected.value : null
|
|
131
|
+
const cache = labelCache.value
|
|
132
|
+
|
|
116
133
|
return props.options
|
|
117
|
-
.filter((option) =>
|
|
134
|
+
.filter((option) => !hasFilter || cache.get(option)?.includes(filterLower))
|
|
118
135
|
.map((option) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
isActive = selected.value.includes(option.value)
|
|
123
|
-
} else {
|
|
124
|
-
isActive = option.value === selected.value
|
|
125
|
-
}
|
|
136
|
+
const isActive = internalMultiple.value
|
|
137
|
+
? selectedSet.has(option.value)
|
|
138
|
+
: option.value === singleSelectedValue
|
|
126
139
|
|
|
127
140
|
return {
|
|
128
141
|
value: option.value,
|
|
@@ -50,6 +50,9 @@ const tableProps = {
|
|
|
50
50
|
type: Boolean,
|
|
51
51
|
default: true,
|
|
52
52
|
},
|
|
53
|
+
toFn: Function as PropType<(item: unknown) => string | Record<string, unknown> | undefined>,
|
|
54
|
+
hrefFn: Function as PropType<(item: unknown) => string>,
|
|
55
|
+
hrefTarget: String as PropType<'_blank' | '_self' | '_parent' | '_top'>,
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export type TableHeader = {
|
|
@@ -97,21 +100,15 @@ const props = defineProps({
|
|
|
97
100
|
type: Array as PropType<T[]>,
|
|
98
101
|
default: () => [],
|
|
99
102
|
},
|
|
103
|
+
toFn: Function as PropType<(item: T) => string | Record<string, unknown> | undefined>,
|
|
104
|
+
hrefFn: Function as PropType<(item: T) => string>,
|
|
105
|
+
hrefTarget: String as PropType<'_blank' | '_self' | '_parent' | '_top'>,
|
|
100
106
|
})
|
|
101
107
|
|
|
102
108
|
const selected = defineModel<(number | string) | (number | string)[]>('selected')
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function clone<T>(source: T[]): T[] {
|
|
109
|
-
try {
|
|
110
|
-
return JSON.parse(JSON.stringify(source))
|
|
111
|
-
} catch (e) {
|
|
112
|
-
return []
|
|
113
|
-
}
|
|
114
|
-
}
|
|
110
|
+
// Use Map for expandable state to handle virtual list correctly
|
|
111
|
+
const expandedState = ref(new Map<number | string, boolean>())
|
|
115
112
|
|
|
116
113
|
const items = computed(() => props.items)
|
|
117
114
|
|
|
@@ -126,9 +123,40 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
|
|
126
123
|
},
|
|
127
124
|
)
|
|
128
125
|
|
|
129
|
-
|
|
126
|
+
// Helper function to get item key with validation
|
|
127
|
+
function getItemKey(item: T, index: number): number | string {
|
|
128
|
+
if (!props.keyProp || !item || typeof item !== 'object' || item === null) {
|
|
129
|
+
return index
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const keyValue = (item as Record<string, unknown>)[props.keyProp]
|
|
133
|
+
|
|
134
|
+
// Validate that the key exists and is a valid type
|
|
135
|
+
if (keyValue === undefined || keyValue === null) {
|
|
136
|
+
console.warn(`[XTable] keyProp "${props.keyProp}" is undefined/null for item at index ${index}. Falling back to index.`)
|
|
137
|
+
|
|
138
|
+
return index
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof keyValue !== 'string' && typeof keyValue !== 'number') {
|
|
142
|
+
console.warn(`[XTable] keyProp "${props.keyProp}" must be a string or number, got ${typeof keyValue}. Falling back to index.`)
|
|
143
|
+
|
|
144
|
+
return index
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return keyValue as number | string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Helper function to get original index from virtual list item
|
|
151
|
+
// Note: useVirtualList always preserves the original index in item.index
|
|
152
|
+
function getOriginalIndex(virtualItem: { data: T; index: number; }): number {
|
|
153
|
+
return virtualItem.index
|
|
154
|
+
}
|
|
130
155
|
|
|
131
|
-
const emit = defineEmits
|
|
156
|
+
const emit = defineEmits<{
|
|
157
|
+
(e: 'update:sort', sortValues: string[]): void;
|
|
158
|
+
(e: 'click-row', item: T, index: number): void;
|
|
159
|
+
}>()
|
|
132
160
|
|
|
133
161
|
function getSort(headerValue: string | undefined, sort: string[]): TableHeaderSort {
|
|
134
162
|
if (!headerValue) return undefined
|
|
@@ -173,10 +201,37 @@ function sortHeader(header: TableHeader) {
|
|
|
173
201
|
emit('update:sort', sort)
|
|
174
202
|
}
|
|
175
203
|
|
|
176
|
-
|
|
204
|
+
const pathCache = new Map<string, string[]>()
|
|
205
|
+
|
|
206
|
+
function getValue(item: T, path: string | string[] | undefined): unknown {
|
|
177
207
|
if (!path) return ''
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
if (!item) return ''
|
|
209
|
+
|
|
210
|
+
let pathArray: string[] | null
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(path)) {
|
|
213
|
+
pathArray = path
|
|
214
|
+
} else {
|
|
215
|
+
// Check cache first
|
|
216
|
+
if (pathCache.has(path)) {
|
|
217
|
+
pathArray = pathCache.get(path)!
|
|
218
|
+
} else {
|
|
219
|
+
// Parse and cache the result
|
|
220
|
+
pathArray = path.match(/([^[.\]])+/g)
|
|
221
|
+
if (pathArray) {
|
|
222
|
+
pathCache.set(path, pathArray)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!pathArray || pathArray.length === 0) return ''
|
|
228
|
+
|
|
229
|
+
const result = pathArray.reduce((prevObj: unknown, key: string) => {
|
|
230
|
+
if (prevObj === null || prevObj === undefined) return null
|
|
231
|
+
if (typeof prevObj !== 'object') return null
|
|
232
|
+
|
|
233
|
+
return (prevObj as Record<string, unknown>)[key]
|
|
234
|
+
}, item)
|
|
180
235
|
|
|
181
236
|
return result ?? ''
|
|
182
237
|
}
|
|
@@ -184,38 +239,59 @@ function getValue(item: any, path: string | string[] | undefined) {
|
|
|
184
239
|
const allKeys = computed<(number | string)[]>(() => {
|
|
185
240
|
if (!props.selectable) return []
|
|
186
241
|
|
|
187
|
-
return items.value.map((item, index) =>
|
|
242
|
+
return items.value.map((item, index) => getItemKey(item, index))
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const selectedSet = computed(() => {
|
|
246
|
+
if (!props.selectable || props.singleSelect) return new Set<number | string>()
|
|
247
|
+
if (!Array.isArray(selected.value)) return new Set<number | string>()
|
|
248
|
+
|
|
249
|
+
return new Set(selected.value)
|
|
188
250
|
})
|
|
189
251
|
|
|
190
252
|
const allRowsSelected = computed(() => {
|
|
191
253
|
if (!props.selectable || props.singleSelect) return false
|
|
254
|
+
if (!Array.isArray(selected.value) || selected.value.length === 0) return false
|
|
255
|
+
|
|
256
|
+
const keysLength = allKeys.value.length
|
|
257
|
+
|
|
258
|
+
if (keysLength === 0) return false
|
|
192
259
|
|
|
193
|
-
return
|
|
260
|
+
return selected.value.length === keysLength
|
|
194
261
|
})
|
|
195
262
|
|
|
196
263
|
const someRowsSelected = computed(() => {
|
|
197
264
|
if (!props.selectable || props.singleSelect) return false
|
|
265
|
+
if (!Array.isArray(selected.value) || selected.value.length === 0) return false
|
|
198
266
|
|
|
199
|
-
|
|
267
|
+
const keysLength = allKeys.value.length
|
|
268
|
+
|
|
269
|
+
if (keysLength === 0) return false
|
|
270
|
+
|
|
271
|
+
return selected.value.length > 0 && selected.value.length !== keysLength
|
|
200
272
|
})
|
|
201
273
|
|
|
202
|
-
function isRowSelected(rowKey:
|
|
274
|
+
function isRowSelected(rowKey: number | string): boolean {
|
|
203
275
|
if (!props.selectable) return false
|
|
276
|
+
|
|
204
277
|
if (props.singleSelect) {
|
|
205
278
|
return selected.value === rowKey
|
|
206
|
-
} else {
|
|
207
|
-
return Array.isArray(selected.value) && selected.value.includes(rowKey)
|
|
208
279
|
}
|
|
280
|
+
|
|
281
|
+
return selectedSet.value.has(rowKey)
|
|
209
282
|
}
|
|
210
283
|
|
|
211
|
-
function toggleRowSelection(rowKey:
|
|
284
|
+
function toggleRowSelection(rowKey: number | string) {
|
|
212
285
|
if (!props.selectable) return
|
|
286
|
+
|
|
213
287
|
if (props.singleSelect) {
|
|
214
288
|
selected.value = selected.value === rowKey ? undefined : rowKey
|
|
215
289
|
} else {
|
|
216
290
|
if (!Array.isArray(selected.value)) selected.value = []
|
|
217
|
-
|
|
218
|
-
|
|
291
|
+
|
|
292
|
+
// Use Set for O(1) lookup instead of includes O(n)
|
|
293
|
+
if (selectedSet.value.has(rowKey)) {
|
|
294
|
+
selected.value = selected.value.filter((k: number | string) => k !== rowKey)
|
|
219
295
|
} else {
|
|
220
296
|
selected.value = [...selected.value, rowKey]
|
|
221
297
|
}
|
|
@@ -232,25 +308,68 @@ function toggleSelectAll() {
|
|
|
232
308
|
}
|
|
233
309
|
}
|
|
234
310
|
|
|
235
|
-
function
|
|
311
|
+
function toggleExpanded(virtualItem: { data: T; index: number; }) {
|
|
312
|
+
if (!props.expandable) return
|
|
313
|
+
const itemKey = getItemKey(virtualItem.data, getOriginalIndex(virtualItem))
|
|
314
|
+
|
|
315
|
+
expandedState.value.set(itemKey, !expandedState.value.get(itemKey))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isExpanded(virtualItem: { data: T; index: number; }): boolean {
|
|
319
|
+
if (!props.expandable) return false
|
|
320
|
+
const itemKey = getItemKey(virtualItem.data, getOriginalIndex(virtualItem))
|
|
321
|
+
|
|
322
|
+
return expandedState.value.get(itemKey) ?? false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function onTableRowClick(item: T, virtualItem: { data: T; index: number; }) {
|
|
326
|
+
// Get the original index from the items array
|
|
327
|
+
const originalIndex = getOriginalIndex(virtualItem)
|
|
328
|
+
|
|
236
329
|
if (props.selectable && props.singleSelect) {
|
|
237
|
-
|
|
330
|
+
const itemKey = getItemKey(item, originalIndex)
|
|
331
|
+
|
|
332
|
+
toggleRowSelection(itemKey)
|
|
238
333
|
}
|
|
239
334
|
|
|
240
|
-
emit('click-row', item,
|
|
335
|
+
emit('click-row', item, originalIndex)
|
|
241
336
|
}
|
|
242
337
|
|
|
243
|
-
|
|
244
|
-
|
|
338
|
+
// Compute column count for colspan
|
|
339
|
+
const columnCount = computed(() => {
|
|
340
|
+
let count = props.headers.length
|
|
341
|
+
|
|
342
|
+
if (props.selectable && !props.singleSelect) count++
|
|
343
|
+
if (props.expandable) count++
|
|
344
|
+
|
|
345
|
+
return count
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
watch(items, (newValue: T[]) => {
|
|
349
|
+
const currentKeys = new Set<number | string>()
|
|
350
|
+
|
|
351
|
+
newValue.forEach((item, index) => {
|
|
352
|
+
currentKeys.add(getItemKey(item, index))
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// Clear expanded state for items that no longer exist
|
|
356
|
+
if (props.expandable) {
|
|
357
|
+
expandedState.value.forEach((_, key) => {
|
|
358
|
+
if (!currentKeys.has(key)) {
|
|
359
|
+
expandedState.value.delete(key)
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
}
|
|
245
363
|
|
|
364
|
+
// Clear selected items that no longer exist
|
|
246
365
|
if (props.selectable && props.autoClearSelected) {
|
|
247
366
|
if (props.singleSelect) {
|
|
248
|
-
if (!
|
|
367
|
+
if (!currentKeys.has(selected.value as number | string)) {
|
|
249
368
|
selected.value = undefined
|
|
250
369
|
}
|
|
251
370
|
} else {
|
|
252
|
-
if (Array.isArray(selected.value)) {
|
|
253
|
-
selected.value = selected.value.filter((k:
|
|
371
|
+
if (Array.isArray(selected.value) && selected.value.length > 0) {
|
|
372
|
+
selected.value = selected.value.filter((k: number | string) => currentKeys.has(k))
|
|
254
373
|
}
|
|
255
374
|
}
|
|
256
375
|
}
|
|
@@ -335,33 +454,33 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
335
454
|
</template>
|
|
336
455
|
<template v-else-if="error">
|
|
337
456
|
<tr>
|
|
338
|
-
<td colspan="
|
|
457
|
+
<td :colspan="columnCount">
|
|
339
458
|
<slot name="error"></slot>
|
|
340
459
|
</td>
|
|
341
460
|
</tr>
|
|
342
461
|
</template>
|
|
343
462
|
<template v-else-if="!items || items.length === 0">
|
|
344
463
|
<tr>
|
|
345
|
-
<td colspan="
|
|
464
|
+
<td :colspan="columnCount">
|
|
346
465
|
<slot name="empty"></slot>
|
|
347
466
|
</td>
|
|
348
467
|
</tr>
|
|
349
468
|
</template>
|
|
350
|
-
<template v-for="
|
|
469
|
+
<template v-for="item in list" v-else :key="getItemKey(item.data, item.index)">
|
|
351
470
|
<x-table-row
|
|
352
|
-
:pointer="pointer"
|
|
471
|
+
:pointer="pointer || (!!toFn || !!hrefFn)"
|
|
353
472
|
:striped="striped"
|
|
354
|
-
:selected="isRowSelected(
|
|
473
|
+
:selected="isRowSelected(getItemKey(item.data, item.index))"
|
|
355
474
|
:single-select="singleSelect"
|
|
356
|
-
@click="onTableRowClick(item.data, item
|
|
475
|
+
@click="onTableRowClick(item.data, item)"
|
|
357
476
|
>
|
|
358
|
-
<x-table-cell v-if="props.selectable && !singleSelect" width="40" class="!pl-3.5 !pr-0.5 cursor-pointer" @click.stop="toggleRowSelection(
|
|
477
|
+
<x-table-cell v-if="props.selectable && !singleSelect" width="40" class="!pl-3.5 !pr-0.5 cursor-pointer" @click.stop="toggleRowSelection(getItemKey(item.data, item.index))">
|
|
359
478
|
<x-checkbox
|
|
360
|
-
:model-value="isRowSelected(
|
|
479
|
+
:model-value="isRowSelected(getItemKey(item.data, item.index))"
|
|
361
480
|
hide-footer
|
|
362
|
-
:aria-label="`Select row ${
|
|
481
|
+
:aria-label="`Select row ${getOriginalIndex(item) + 1}`"
|
|
363
482
|
skip-form-registry
|
|
364
|
-
@click.prevent.stop="toggleRowSelection(
|
|
483
|
+
@click.prevent.stop="toggleRowSelection(getItemKey(item.data, item.index))"
|
|
365
484
|
/>
|
|
366
485
|
</x-table-cell>
|
|
367
486
|
<x-table-cell v-if="expandable" width="48" class="!p-1">
|
|
@@ -369,14 +488,14 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
369
488
|
type="button"
|
|
370
489
|
class="px-3 p-2"
|
|
371
490
|
:class="[dense ? 'p-0.5' : 'px-3 py-2']"
|
|
372
|
-
@click="
|
|
491
|
+
@click.stop="toggleExpanded(item)"
|
|
373
492
|
>
|
|
374
493
|
<x-icon
|
|
375
494
|
:icon="chevronDownIcon"
|
|
376
495
|
:size="dense ? 'xs' : 'md'"
|
|
377
496
|
class="transition-transform"
|
|
378
497
|
:class="{
|
|
379
|
-
'rotate-180':
|
|
498
|
+
'rotate-180': isExpanded(item),
|
|
380
499
|
}"
|
|
381
500
|
/>
|
|
382
501
|
</button>
|
|
@@ -394,15 +513,18 @@ const { styles, classes, className } = useTheme('Table', {}, props)
|
|
|
394
513
|
overflow: 'hidden',
|
|
395
514
|
whiteSpace: 'nowrap',
|
|
396
515
|
} : {}]"
|
|
516
|
+
:href="hrefFn ? hrefFn(item.data) : undefined"
|
|
517
|
+
:to="toFn ? toFn(item.data) : undefined"
|
|
518
|
+
:target="hrefFn ? hrefTarget : undefined"
|
|
397
519
|
>
|
|
398
520
|
<slot :name="`item-${header.value}`" :item="item.data">
|
|
399
521
|
{{ getValue(item.data, header.value) }}
|
|
400
522
|
</slot>
|
|
401
523
|
</x-table-cell>
|
|
402
524
|
</x-table-row>
|
|
403
|
-
<tr v-if="expandable" :class="{ 'hidden': !
|
|
404
|
-
<td colspan="
|
|
405
|
-
<div class="overflow-hidden transition-opacity" :class="[
|
|
525
|
+
<tr v-if="expandable" :class="{ 'hidden': !isExpanded(item) }">
|
|
526
|
+
<td :colspan="columnCount">
|
|
527
|
+
<div class="overflow-hidden transition-opacity" :class="[isExpanded(item) ? '' : 'opacity-0 max-h-0']">
|
|
406
528
|
<slot name="expanded-row" :item="item.data"></slot>
|
|
407
529
|
</div>
|
|
408
530
|
</td>
|