@globalbrain/sefirot 4.57.0 → 4.59.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/lib/blocks/lens/components/LensCatalog.vue +55 -32
- package/lib/blocks/lens/components/LensFormFilterCondition.vue +2 -1
- package/lib/blocks/lens/components/LensFormFilterGroup.vue +1 -1
- package/lib/blocks/lens/components/{LensAvatarInput.vue → LensInputAvatar.vue} +5 -3
- package/lib/blocks/lens/components/LensInputDropdown.vue +102 -0
- package/lib/blocks/lens/components/LensSheetAvatarField.vue +2 -2
- package/lib/blocks/lens/components/LensTable.vue +9 -6
- package/lib/blocks/lens/components/LensTableAvatarCell.vue +2 -2
- package/lib/blocks/lens/components/LensTableEditableCell.vue +8 -7
- package/lib/blocks/lens/composables/LensEdit.ts +9 -0
- package/lib/blocks/lens/fields/AvatarField.ts +2 -2
- package/lib/blocks/lens/fields/Field.ts +10 -6
- package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +2 -1
- package/lib/components/SButton.vue +2 -10
- package/lib/components/SControlInputSearch.vue +6 -3
- package/lib/components/SDescPill.vue +3 -2
- package/lib/components/SDescState.vue +3 -2
- package/lib/components/SInputAddon.vue +4 -2
- package/lib/components/{SInputSelectSearch.vue → SInputAsyncDropdown.vue} +91 -83
- package/lib/components/SInputBase.vue +1 -3
- package/lib/components/SInputCheckbox.vue +10 -18
- package/lib/components/SInputCheckboxes.vue +16 -33
- package/lib/components/SInputDate.vue +10 -20
- package/lib/components/SInputDropdown.vue +22 -43
- package/lib/components/SInputDropdownItem.vue +7 -24
- package/lib/components/SInputFile.vue +10 -18
- package/lib/components/SInputFileUpload.vue +12 -34
- package/lib/components/SInputFileUploadItem.vue +7 -21
- package/lib/components/SInputHMS.vue +6 -7
- package/lib/components/SInputImage.vue +11 -16
- package/lib/components/SInputNumber.vue +1 -3
- package/lib/components/SInputRadio.vue +9 -19
- package/lib/components/SInputRadios.vue +24 -40
- package/lib/components/SInputSegments.vue +15 -26
- package/lib/components/SInputSegmentsOption.vue +7 -7
- package/lib/components/SInputSelect.vue +14 -19
- package/lib/components/SInputSwitch.vue +10 -19
- package/lib/components/SInputSwitches.vue +16 -30
- package/lib/components/SInputText.vue +1 -4
- package/lib/components/SInputTextarea.vue +0 -2
- package/lib/components/SInputYMD.vue +6 -7
- package/lib/components/SPagination.vue +1 -1
- package/lib/components/SPill.vue +2 -2
- package/lib/components/SState.vue +2 -2
- package/lib/components/STableCellPill.vue +3 -2
- package/lib/components/STableCellState.vue +3 -2
- package/lib/composables/Table.ts +8 -15
- package/lib/support/Color.ts +4 -0
- package/lib/support/InputBase.ts +5 -0
- package/lib/support/InputDropdown.ts +15 -0
- package/lib/support/InputFileUpload.ts +22 -0
- package/lib/support/InputOption.ts +8 -0
- package/lib/support/InputText.ts +6 -0
- package/package.json +1 -1
- package/lib/blocks/lens/components/LensInputSelect.vue +0 -92
|
@@ -1,51 +1,40 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T, Multiple extends boolean = false">
|
|
2
2
|
import IconCaretDown from '~icons/ph/caret-down'
|
|
3
3
|
import IconCaretUp from '~icons/ph/caret-up'
|
|
4
4
|
import IconCheck from '~icons/ph/check'
|
|
5
|
-
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
|
5
|
+
import { type Ref, computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
|
6
6
|
import { useManualDropdownPosition } from '../composables/Dropdown'
|
|
7
7
|
import { useFlyout } from '../composables/Flyout'
|
|
8
8
|
import { useTrans } from '../composables/Lang'
|
|
9
|
+
import { type Option } from '../support/InputDropdown'
|
|
9
10
|
import SDropdownSectionFilterItem from './SDropdownSectionFilterItem.vue'
|
|
10
11
|
import SInputBase, { type Props as BaseProps } from './SInputBase.vue'
|
|
11
12
|
import SInputDropdownItem from './SInputDropdownItem.vue'
|
|
12
13
|
import SSpinner from './SSpinner.vue'
|
|
13
14
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
export type Option = OptionText | OptionAvatar
|
|
17
|
-
|
|
18
|
-
export interface OptionBase {
|
|
19
|
-
type?: 'text' | 'avatar'
|
|
20
|
-
value: any
|
|
21
|
-
disabled?: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface OptionText extends OptionBase {
|
|
25
|
-
type?: 'text'
|
|
26
|
-
label: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface OptionAvatar extends OptionBase {
|
|
30
|
-
type: 'avatar'
|
|
31
|
-
label: string
|
|
32
|
-
image?: string | null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface Props extends BaseProps {
|
|
15
|
+
export interface Props<T = any, Multiple extends boolean = false> extends BaseProps {
|
|
36
16
|
placeholder?: string
|
|
37
|
-
// Fetch the
|
|
17
|
+
// Fetch the items matching `query` from the server. Called when the dropdown
|
|
38
18
|
// opens (with the current query, usually empty for the initial list) and again,
|
|
39
|
-
// debounced, on each keystroke. Out-of-order responses are discarded, so it
|
|
40
|
-
// not need to guard against races itself.
|
|
41
|
-
fetch: (query: string) => Promise<
|
|
19
|
+
// debounced, on each keystroke. Out-of-order responses are discarded, so it
|
|
20
|
+
// does not need to guard against races itself.
|
|
21
|
+
fetch: (query: string) => Promise<T[]>
|
|
22
|
+
// Render an item as its dropdown option and, once selected, as its chip. The
|
|
23
|
+
// option's `value` is the item's identity — selection, de-duplication and
|
|
24
|
+
// removal all key off it. Applied to fetched items and to the bound model
|
|
25
|
+
// alike, so a seeded selection renders without appearing in any fetch.
|
|
26
|
+
toOption: (item: T) => Option
|
|
42
27
|
// Decide whether a `fetch` rejection should propagate (be re-thrown) instead of
|
|
43
28
|
// being swallowed into an empty list — e.g. to let auth/session failures reach
|
|
44
29
|
// the app's re-auth flow. By default every rejection is swallowed.
|
|
45
30
|
rethrow?: (error: unknown) => boolean
|
|
46
|
-
// Whether multiple
|
|
47
|
-
//
|
|
48
|
-
|
|
31
|
+
// Whether multiple items can be selected. The model is then an array of items;
|
|
32
|
+
// otherwise it is a single item or `null`. Typed `boolean & Multiple` (equal to
|
|
33
|
+
// `Multiple`, since it extends `boolean`) rather than a bare `Multiple`: the
|
|
34
|
+
// intersection keeps Vue's runtime Boolean casting (a bare `multiple` attribute
|
|
35
|
+
// would otherwise arrive as `''` and read as false), while `Multiple` still
|
|
36
|
+
// drives the arity-conditional model type.
|
|
37
|
+
multiple?: boolean & Multiple
|
|
49
38
|
// Whether the (single) value can be cleared, or the (multiple) selection emptied.
|
|
50
39
|
nullable?: boolean
|
|
51
40
|
disabled?: boolean
|
|
@@ -60,12 +49,18 @@ export interface Props extends BaseProps {
|
|
|
60
49
|
// `closeOnSelect` defaults to `undefined` (not Vue's usual absent-Boolean → false)
|
|
61
50
|
// so the `?? !multiple` fallback in onSelect can tell "omitted" from an explicit
|
|
62
51
|
// `false`, keeping the single-select-closes / multiple-stays-open default.
|
|
63
|
-
const props = withDefaults(defineProps<Props
|
|
52
|
+
const props = withDefaults(defineProps<Props<T, Multiple>>(), { closeOnSelect: undefined })
|
|
64
53
|
|
|
65
|
-
// The
|
|
66
|
-
// `
|
|
67
|
-
//
|
|
68
|
-
const model = defineModel<
|
|
54
|
+
// The selection: an array of items when `multiple`, otherwise a single item or
|
|
55
|
+
// `null`. Items are the consumer's own model type; the option rendered for each
|
|
56
|
+
// is derived on demand via `toOption`, so nothing extra is stored on the model.
|
|
57
|
+
const model = defineModel<Multiple extends true ? T[] : T | null>({ required: true })
|
|
58
|
+
|
|
59
|
+
// Write the model regardless of arity. The public type is arity-conditional, so
|
|
60
|
+
// the concrete array / single value is cast at this single boundary.
|
|
61
|
+
function commit(value: T[] | T | null): void {
|
|
62
|
+
model.value = value as typeof model.value
|
|
63
|
+
}
|
|
69
64
|
|
|
70
65
|
const { t } = useTrans({
|
|
71
66
|
en: {
|
|
@@ -91,10 +86,10 @@ const { inset, update: updatePosition } = useManualDropdownPosition(
|
|
|
91
86
|
)
|
|
92
87
|
|
|
93
88
|
const query = ref('')
|
|
94
|
-
const
|
|
89
|
+
const items = ref([]) as Ref<T[]>
|
|
95
90
|
const loading = ref(false)
|
|
96
91
|
|
|
97
|
-
// Monotonic token so only the latest fetch may assign `
|
|
92
|
+
// Monotonic token so only the latest fetch may assign `items` / clear `loading`.
|
|
98
93
|
// With a debounced, user-driven search several requests can overlap; an older one
|
|
99
94
|
// resolving late must not clobber the newer result.
|
|
100
95
|
let fetchSeq = 0
|
|
@@ -104,20 +99,38 @@ const classes = computed(() => [
|
|
|
104
99
|
{ disabled: props.disabled }
|
|
105
100
|
])
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
// The selected items as a flat array, regardless of arity, for internal handling.
|
|
103
|
+
const selectedItems = computed<T[]>(() => {
|
|
108
104
|
if (props.multiple) {
|
|
109
|
-
return Array.isArray(model.value) ? (model.value as
|
|
105
|
+
return Array.isArray(model.value) ? (model.value as T[]) : []
|
|
110
106
|
}
|
|
111
|
-
return
|
|
107
|
+
return model.value != null ? [model.value as T] : []
|
|
112
108
|
})
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
// The identities currently selected, for O(1) active / de-dup checks.
|
|
111
|
+
const selectedValues = computed(
|
|
112
|
+
() => new Set(selectedItems.value.map((item) => props.toOption(item).value))
|
|
116
113
|
)
|
|
117
114
|
|
|
115
|
+
// The option(s) rendered for the current selection: an array of chips when
|
|
116
|
+
// multiple, a single chip otherwise (matching SInputDropdownItem's `item`).
|
|
117
|
+
const selectedOptions = computed<Option | Option[] | null>(() => {
|
|
118
|
+
if (props.multiple) {
|
|
119
|
+
return selectedItems.value.map((item) => props.toOption(item))
|
|
120
|
+
}
|
|
121
|
+
return model.value != null ? props.toOption(model.value as T) : null
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// The fetched items paired with their rendered option, for the dropdown list.
|
|
125
|
+
const optionItems = computed(() =>
|
|
126
|
+
items.value.map((item) => ({ item, option: props.toOption(item) }))
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const hasSelected = computed(() => selectedItems.value.length > 0)
|
|
130
|
+
|
|
118
131
|
const removable = computed(() => {
|
|
119
132
|
if (props.multiple) {
|
|
120
|
-
return !!props.nullable ||
|
|
133
|
+
return !!props.nullable || selectedItems.value.length > 1
|
|
121
134
|
}
|
|
122
135
|
return !!props.nullable
|
|
123
136
|
})
|
|
@@ -128,12 +141,12 @@ async function runFetch(q: string): Promise<void> {
|
|
|
128
141
|
try {
|
|
129
142
|
const res = await props.fetch(q)
|
|
130
143
|
if (seq === fetchSeq) {
|
|
131
|
-
|
|
144
|
+
items.value = res
|
|
132
145
|
}
|
|
133
146
|
} catch (e) {
|
|
134
147
|
// A failed search yields no options (the empty state shows)…
|
|
135
148
|
if (seq === fetchSeq) {
|
|
136
|
-
|
|
149
|
+
items.value = []
|
|
137
150
|
}
|
|
138
151
|
// …unless the consumer wants this failure propagated (e.g. an auth/session
|
|
139
152
|
// error that must reach the app's re-auth flow rather than look like "no
|
|
@@ -199,14 +212,14 @@ function onOpen() {
|
|
|
199
212
|
watch(isOpen, (value) => {
|
|
200
213
|
// On close: drop any pending refetch, invalidate any in-flight fetch (so a late
|
|
201
214
|
// response can't repopulate the list while closed), and clear the query /
|
|
202
|
-
//
|
|
215
|
+
// items / loading. Reopening then starts fresh instead of briefly rendering
|
|
203
216
|
// the previous query's results (which would be selectable). The selected chips
|
|
204
|
-
// are unaffected — they render from the model, not from `
|
|
217
|
+
// are unaffected — they render from the model, not from the fetched `items`.
|
|
205
218
|
if (!value) {
|
|
206
219
|
cancelPendingFetch()
|
|
207
220
|
fetchSeq++
|
|
208
221
|
query.value = ''
|
|
209
|
-
|
|
222
|
+
items.value = []
|
|
210
223
|
loading.value = false
|
|
211
224
|
}
|
|
212
225
|
})
|
|
@@ -223,36 +236,32 @@ watch(() => props.disabled, (disabled) => {
|
|
|
223
236
|
})
|
|
224
237
|
|
|
225
238
|
function isActive(value: any): boolean {
|
|
226
|
-
|
|
227
|
-
return (selected.value as Option[]).some((o) => o.value === value)
|
|
228
|
-
}
|
|
229
|
-
return (selected.value as Option | null)?.value === value
|
|
239
|
+
return selectedValues.value.has(value)
|
|
230
240
|
}
|
|
231
241
|
|
|
232
|
-
function onSelect(
|
|
242
|
+
function onSelect(item: T): void {
|
|
243
|
+
const option = props.toOption(item)
|
|
233
244
|
if (props.disabled || option.disabled) {
|
|
234
245
|
return
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
props.validation?.$touch()
|
|
238
249
|
|
|
250
|
+
const value = option.value
|
|
251
|
+
|
|
239
252
|
if (props.multiple) {
|
|
240
|
-
const current =
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: [...current, option]
|
|
253
|
+
const current = selectedItems.value
|
|
254
|
+
const next = selectedValues.value.has(value)
|
|
255
|
+
? current.filter((i) => props.toOption(i).value !== value)
|
|
256
|
+
: [...current, item]
|
|
245
257
|
// Keep at least one unless clearing is allowed (mirrors SInputDropdown).
|
|
246
258
|
if (next.length !== 0 || props.nullable) {
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
const currentValue = (model.value as Option | null)?.value
|
|
251
|
-
if (currentValue !== option.value) {
|
|
252
|
-
model.value = option
|
|
253
|
-
} else if (props.nullable) {
|
|
254
|
-
model.value = null
|
|
259
|
+
commit(next)
|
|
255
260
|
}
|
|
261
|
+
} else if (!selectedValues.value.has(value)) {
|
|
262
|
+
commit(item)
|
|
263
|
+
} else if (props.nullable) {
|
|
264
|
+
commit(null)
|
|
256
265
|
}
|
|
257
266
|
|
|
258
267
|
if (props.closeOnSelect ?? !props.multiple) {
|
|
@@ -265,10 +274,9 @@ function onRemove(value: any): void {
|
|
|
265
274
|
props.validation?.$touch()
|
|
266
275
|
|
|
267
276
|
if (props.multiple) {
|
|
268
|
-
|
|
269
|
-
model.value = current.filter((o) => o.value !== value)
|
|
277
|
+
commit(selectedItems.value.filter((item) => props.toOption(item).value !== value))
|
|
270
278
|
} else {
|
|
271
|
-
|
|
279
|
+
commit(null)
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
|
|
@@ -306,7 +314,7 @@ function focusNext(event: any): void {
|
|
|
306
314
|
|
|
307
315
|
<template>
|
|
308
316
|
<SInputBase
|
|
309
|
-
class="
|
|
317
|
+
class="SInputAsyncDropdown"
|
|
310
318
|
:class="classes"
|
|
311
319
|
:size
|
|
312
320
|
:name
|
|
@@ -335,7 +343,7 @@ function focusNext(event: any): void {
|
|
|
335
343
|
<div class="box-content">
|
|
336
344
|
<SInputDropdownItem
|
|
337
345
|
v-if="hasSelected"
|
|
338
|
-
:item="
|
|
346
|
+
:item="selectedOptions!"
|
|
339
347
|
:size="size ?? 'small'"
|
|
340
348
|
:removable
|
|
341
349
|
:disabled="disabled ?? false"
|
|
@@ -369,11 +377,11 @@ function focusNext(event: any): void {
|
|
|
369
377
|
>
|
|
370
378
|
<!-- A refetch while options are already shown keeps the (stale) list
|
|
371
379
|
visible; this spinner signals that newer results are loading. -->
|
|
372
|
-
<SSpinner v-if="loading &&
|
|
380
|
+
<SSpinner v-if="loading && items.length" class="search-spinner" />
|
|
373
381
|
</div>
|
|
374
382
|
|
|
375
|
-
<ul v-if="
|
|
376
|
-
<li v-for="option in
|
|
383
|
+
<ul v-if="items.length" ref="list" class="list">
|
|
384
|
+
<li v-for="{ item, option } in optionItems" :key="option.value" class="item">
|
|
377
385
|
<button
|
|
378
386
|
class="button"
|
|
379
387
|
:class="{ active: isActive(option.value) }"
|
|
@@ -382,7 +390,7 @@ function focusNext(event: any): void {
|
|
|
382
390
|
tabindex="0"
|
|
383
391
|
@keyup.up.prevent="focusPrev"
|
|
384
392
|
@keyup.down.prevent="focusNext"
|
|
385
|
-
@click="onSelect(
|
|
393
|
+
@click="onSelect(item)"
|
|
386
394
|
>
|
|
387
395
|
<span v-if="multiple" class="checkbox">
|
|
388
396
|
<span class="checkbox-box">
|
|
@@ -646,8 +654,8 @@ function focusNext(event: any): void {
|
|
|
646
654
|
color: var(--c-text-2);
|
|
647
655
|
}
|
|
648
656
|
|
|
649
|
-
.
|
|
650
|
-
.
|
|
657
|
+
.SInputAsyncDropdown.sm,
|
|
658
|
+
.SInputAsyncDropdown.mini {
|
|
651
659
|
.box {
|
|
652
660
|
min-height: 32px;
|
|
653
661
|
}
|
|
@@ -668,7 +676,7 @@ function focusNext(event: any): void {
|
|
|
668
676
|
}
|
|
669
677
|
}
|
|
670
678
|
|
|
671
|
-
.
|
|
679
|
+
.SInputAsyncDropdown.md {
|
|
672
680
|
.box {
|
|
673
681
|
min-height: 36px;
|
|
674
682
|
}
|
|
@@ -689,7 +697,7 @@ function focusNext(event: any): void {
|
|
|
689
697
|
}
|
|
690
698
|
}
|
|
691
699
|
|
|
692
|
-
.
|
|
700
|
+
.SInputAsyncDropdown.small {
|
|
693
701
|
.box {
|
|
694
702
|
min-height: 40px;
|
|
695
703
|
}
|
|
@@ -710,7 +718,7 @@ function focusNext(event: any): void {
|
|
|
710
718
|
}
|
|
711
719
|
}
|
|
712
720
|
|
|
713
|
-
.
|
|
721
|
+
.SInputAsyncDropdown.medium {
|
|
714
722
|
.box {
|
|
715
723
|
min-height: 48px;
|
|
716
724
|
}
|
|
@@ -731,7 +739,7 @@ function focusNext(event: any): void {
|
|
|
731
739
|
}
|
|
732
740
|
}
|
|
733
741
|
|
|
734
|
-
.
|
|
742
|
+
.SInputAsyncDropdown.disabled {
|
|
735
743
|
.box {
|
|
736
744
|
border-color: var(--input-disabled-border-color);
|
|
737
745
|
background-color: var(--input-disabled-bg-color);
|
|
@@ -745,7 +753,7 @@ function focusNext(event: any): void {
|
|
|
745
753
|
}
|
|
746
754
|
}
|
|
747
755
|
|
|
748
|
-
.
|
|
756
|
+
.SInputAsyncDropdown.has-error {
|
|
749
757
|
.box {
|
|
750
758
|
border-color: var(--input-error-border-color);
|
|
751
759
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import IconQuestion from '~icons/ph/question'
|
|
3
3
|
import { type Component, computed, unref, useSlots } from 'vue'
|
|
4
4
|
import { type Validatable } from '../composables/Validation'
|
|
5
|
+
import { type Color, type Size } from '../support/InputBase'
|
|
5
6
|
import STooltip from './STooltip.vue'
|
|
6
7
|
|
|
7
8
|
export interface Props {
|
|
@@ -20,9 +21,6 @@ export interface Props {
|
|
|
20
21
|
hideWarning?: boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export type Size = 'sm' | 'md' | 'mini' | 'small' | 'medium'
|
|
24
|
-
export type Color = 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
|
|
25
|
-
|
|
26
24
|
const props = defineProps<Props>()
|
|
27
25
|
|
|
28
26
|
const slots = useSlots()
|
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import IconCheck from '~icons/ph/check-bold'
|
|
3
3
|
import IconMinus from '~icons/ph/minus-bold'
|
|
4
|
-
import {
|
|
5
|
-
import { type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export type { Color, Size }
|
|
9
|
-
|
|
10
|
-
const props = withDefaults(defineProps<{
|
|
11
|
-
size?: Size
|
|
12
|
-
label?: string
|
|
13
|
-
info?: string
|
|
14
|
-
note?: string
|
|
15
|
-
help?: string
|
|
16
|
-
checkIcon?: Component
|
|
17
|
-
checkText?: string
|
|
18
|
-
checkColor?: Color
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import SInputBase, { type Props as BaseProps } from './SInputBase.vue'
|
|
6
|
+
|
|
7
|
+
export interface Props extends BaseProps {
|
|
19
8
|
text?: string
|
|
20
9
|
disabled?: boolean
|
|
21
10
|
value?: boolean | 'indeterminate'
|
|
22
11
|
modelValue?: boolean | 'indeterminate'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
26
15
|
value: undefined,
|
|
27
16
|
modelValue: undefined
|
|
28
17
|
})
|
|
@@ -62,6 +51,7 @@ function onClick() {
|
|
|
62
51
|
class="SInputCheckbox"
|
|
63
52
|
:class="classes"
|
|
64
53
|
:size
|
|
54
|
+
:name
|
|
65
55
|
:label
|
|
66
56
|
:note
|
|
67
57
|
:info
|
|
@@ -70,7 +60,9 @@ function onClick() {
|
|
|
70
60
|
:check-text
|
|
71
61
|
:check-color
|
|
72
62
|
:validation
|
|
63
|
+
:warning
|
|
73
64
|
:hide-error
|
|
65
|
+
:hide-warning
|
|
74
66
|
>
|
|
75
67
|
<div class="container">
|
|
76
68
|
<div
|
|
@@ -1,43 +1,24 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { type
|
|
4
|
-
import SInputBase, { type
|
|
1
|
+
<script setup lang="ts" generic="T = any">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { type Option } from '../support/InputOption'
|
|
4
|
+
import SInputBase, { type Props as BaseProps } from './SInputBase.vue'
|
|
5
5
|
import SInputCheckbox from './SInputCheckbox.vue'
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface Option {
|
|
12
|
-
label: string
|
|
13
|
-
value: Value
|
|
7
|
+
export interface Props<T = any> extends BaseProps {
|
|
8
|
+
options: Option<T>[]
|
|
9
|
+
nullable?: boolean
|
|
14
10
|
disabled?: boolean
|
|
11
|
+
value?: T[]
|
|
12
|
+
modelValue?: T[]
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
const props = withDefaults(defineProps<{
|
|
18
|
-
size?: Size
|
|
19
|
-
name?: string
|
|
20
|
-
label?: string
|
|
21
|
-
info?: string
|
|
22
|
-
note?: string
|
|
23
|
-
help?: string
|
|
24
|
-
checkIcon?: Component
|
|
25
|
-
checkText?: string
|
|
26
|
-
checkColor?: Color
|
|
27
|
-
options: Option[]
|
|
28
|
-
nullable?: boolean
|
|
29
|
-
disabled?: boolean
|
|
30
|
-
value?: Value[]
|
|
31
|
-
modelValue?: Value[]
|
|
32
|
-
validation?: Validatable
|
|
33
|
-
hideError?: boolean
|
|
34
|
-
}>(), {
|
|
15
|
+
const props = withDefaults(defineProps<Props<T>>(), {
|
|
35
16
|
nullable: true
|
|
36
17
|
})
|
|
37
18
|
|
|
38
19
|
const emit = defineEmits<{
|
|
39
|
-
'update:model-value': [value:
|
|
40
|
-
'change': [value:
|
|
20
|
+
'update:model-value': [value: T[]]
|
|
21
|
+
'change': [value: T[]]
|
|
41
22
|
}>()
|
|
42
23
|
|
|
43
24
|
const _value = computed(() => {
|
|
@@ -48,11 +29,11 @@ const _value = computed(() => {
|
|
|
48
29
|
: []
|
|
49
30
|
})
|
|
50
31
|
|
|
51
|
-
function isChecked(value:
|
|
32
|
+
function isChecked(value: T): boolean {
|
|
52
33
|
return _value.value.includes(value)
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
function onChange(value:
|
|
36
|
+
function onChange(value: T): void {
|
|
56
37
|
const distinct = _value.value
|
|
57
38
|
.filter((v) => v !== value)
|
|
58
39
|
.concat(_value.value.includes(value) ? [] : [value])
|
|
@@ -80,7 +61,9 @@ function onChange(value: Value): void {
|
|
|
80
61
|
:check-text
|
|
81
62
|
:check-color
|
|
82
63
|
:validation
|
|
64
|
+
:warning
|
|
83
65
|
:hide-error
|
|
66
|
+
:hide-warning
|
|
84
67
|
>
|
|
85
68
|
<div class="container">
|
|
86
69
|
<div class="row">
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { DatePicker } from 'v-calendar'
|
|
3
|
-
import {
|
|
4
|
-
import { type Validatable } from '../composables/Validation'
|
|
3
|
+
import { computed } from 'vue'
|
|
5
4
|
import { type Day, day } from '../support/Day'
|
|
6
|
-
import SInputBase, { type
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
const props = defineProps<{
|
|
11
|
-
size?: Size
|
|
12
|
-
name?: string
|
|
13
|
-
label?: string
|
|
14
|
-
info?: string
|
|
15
|
-
note?: string
|
|
16
|
-
help?: string
|
|
17
|
-
checkIcon?: Component
|
|
18
|
-
checkText?: string
|
|
19
|
-
checkColor?: Color
|
|
5
|
+
import SInputBase, { type Props as BaseProps } from './SInputBase.vue'
|
|
6
|
+
|
|
7
|
+
export interface Props extends BaseProps {
|
|
20
8
|
block?: boolean
|
|
21
9
|
disabled?: boolean
|
|
22
10
|
tabindex?: -1 | 0 | number
|
|
23
11
|
modelValue: Day | null
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = defineProps<Props>()
|
|
27
15
|
|
|
28
16
|
const emit = defineEmits<{
|
|
29
17
|
'update:model-value': [value: Day | null]
|
|
@@ -61,8 +49,10 @@ function onBlur() {
|
|
|
61
49
|
:check-icon
|
|
62
50
|
:check-text
|
|
63
51
|
:check-color
|
|
64
|
-
:hide-error
|
|
65
52
|
:validation
|
|
53
|
+
:warning
|
|
54
|
+
:hide-error
|
|
55
|
+
:hide-warning
|
|
66
56
|
>
|
|
67
57
|
<div class="container">
|
|
68
58
|
<DatePicker
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T = any">
|
|
2
2
|
import IconCaretDown from '~icons/ph/caret-down'
|
|
3
3
|
import IconCaretUp from '~icons/ph/caret-up'
|
|
4
4
|
import xor from 'lodash-es/xor'
|
|
@@ -6,14 +6,14 @@ import { computed, ref } from 'vue'
|
|
|
6
6
|
import { type DropdownSectionFilter, useManualDropdownPosition } from '../composables/Dropdown'
|
|
7
7
|
import { useFlyout } from '../composables/Flyout'
|
|
8
8
|
import { useTrans } from '../composables/Lang'
|
|
9
|
+
import { type Option } from '../support/InputDropdown'
|
|
9
10
|
import SDropdown from './SDropdown.vue'
|
|
10
11
|
import SInputBase, { type Props as BaseProps } from './SInputBase.vue'
|
|
11
12
|
import SInputDropdownItem from './SInputDropdownItem.vue'
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
export interface Props extends BaseProps {
|
|
14
|
+
export interface Props<T = any> extends BaseProps {
|
|
15
15
|
placeholder?: string
|
|
16
|
-
options: Option[]
|
|
16
|
+
options: Option<T>[]
|
|
17
17
|
position?: 'top' | 'bottom'
|
|
18
18
|
noSearch?: boolean
|
|
19
19
|
nullable?: boolean
|
|
@@ -21,32 +21,12 @@ export interface Props extends BaseProps {
|
|
|
21
21
|
disabled?: boolean
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
export type ArrayValue = any[]
|
|
26
|
-
export type OptionValue = any
|
|
24
|
+
const props = defineProps<Props<T>>()
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
value: OptionValue
|
|
33
|
-
disabled?: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface OptionText extends OptionBase {
|
|
37
|
-
type?: 'text'
|
|
38
|
-
label: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface OptionAvatar extends OptionBase {
|
|
42
|
-
type: 'avatar'
|
|
43
|
-
label: string
|
|
44
|
-
image?: string | null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const props = defineProps<Props>()
|
|
48
|
-
|
|
49
|
-
const model = defineModel<PrimitiveValue | ArrayValue>({ required: true })
|
|
26
|
+
// Single- vs multi-select is inferred from the bound model at runtime: an array
|
|
27
|
+
// model selects multiple values, anything else a single value (or `null`) — so no
|
|
28
|
+
// `multiple` prop is needed (unlike SInputAsyncDropdown).
|
|
29
|
+
const model = defineModel<T | T[] | null>({ required: true })
|
|
50
30
|
|
|
51
31
|
const { t } = useTrans({
|
|
52
32
|
en: {
|
|
@@ -78,14 +58,12 @@ const dropdownOptions = computed<DropdownSectionFilter[]>(() => [{
|
|
|
78
58
|
onClick: onSelect
|
|
79
59
|
}])
|
|
80
60
|
|
|
81
|
-
const selected = computed(() => {
|
|
61
|
+
const selected = computed<Option<T> | Option<T>[] | null>(() => {
|
|
82
62
|
if (Array.isArray(model.value)) {
|
|
83
|
-
return props.options.filter((o) => (model.value as
|
|
63
|
+
return props.options.filter((o) => (model.value as T[]).includes(o.value))
|
|
84
64
|
}
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return item ?? null
|
|
66
|
+
return props.options.find((o) => o.value === model.value) ?? null
|
|
89
67
|
})
|
|
90
68
|
|
|
91
69
|
const hasSelected = computed(() => {
|
|
@@ -94,7 +72,7 @@ const hasSelected = computed(() => {
|
|
|
94
72
|
|
|
95
73
|
const removable = computed(() => {
|
|
96
74
|
if (Array.isArray(model.value)) {
|
|
97
|
-
return props.nullable || (selected.value as Option[]).length > 1
|
|
75
|
+
return !!props.nullable || (selected.value as Option<T>[]).length > 1
|
|
98
76
|
}
|
|
99
77
|
|
|
100
78
|
return !!props.nullable
|
|
@@ -107,20 +85,18 @@ async function onOpen() {
|
|
|
107
85
|
}
|
|
108
86
|
}
|
|
109
87
|
|
|
110
|
-
function onSelect(value:
|
|
88
|
+
function onSelect(value: T) {
|
|
111
89
|
props.validation?.$touch()
|
|
112
90
|
|
|
113
91
|
if (Array.isArray(model.value)) {
|
|
114
|
-
const toggled = xor(model.value, [value])
|
|
92
|
+
const toggled = xor(model.value as T[], [value])
|
|
115
93
|
if (toggled.length !== 0 || props.nullable) {
|
|
116
94
|
model.value = toggled
|
|
117
95
|
}
|
|
118
|
-
} else {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
model.value = null
|
|
123
|
-
}
|
|
96
|
+
} else if (value !== model.value) {
|
|
97
|
+
model.value = value
|
|
98
|
+
} else if (props.nullable) {
|
|
99
|
+
model.value = null
|
|
124
100
|
}
|
|
125
101
|
|
|
126
102
|
props.closeOnClick && close()
|
|
@@ -132,6 +108,7 @@ function onSelect(value: OptionValue) {
|
|
|
132
108
|
class="SInputDropdown"
|
|
133
109
|
:class="classes"
|
|
134
110
|
:size
|
|
111
|
+
:name
|
|
135
112
|
:label
|
|
136
113
|
:note
|
|
137
114
|
:info
|
|
@@ -140,7 +117,9 @@ function onSelect(value: OptionValue) {
|
|
|
140
117
|
:check-text
|
|
141
118
|
:check-color
|
|
142
119
|
:validation
|
|
120
|
+
:warning
|
|
143
121
|
:hide-error
|
|
122
|
+
:hide-warning
|
|
144
123
|
>
|
|
145
124
|
<div ref="container" class="container">
|
|
146
125
|
<div
|