@purestack/ts-components 1.1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/ts-components.d.mts +1662 -0
  4. package/dist/ts-components.d.mts.map +1 -0
  5. package/dist/ts-components.mjs +9200 -0
  6. package/dist/ts-components.mjs.map +1 -0
  7. package/package.json +130 -0
  8. package/src/defineComponents.ts +87 -0
  9. package/src/index.ts +270 -0
  10. package/src/registerStyles.ts +63 -0
  11. package/src/standard/alertBox/alertBox.ts +64 -0
  12. package/src/standard/autoId.ts +25 -0
  13. package/src/standard/badge/badge.ts +41 -0
  14. package/src/standard/badge/badgeStyle.ts +21 -0
  15. package/src/standard/barChart/barChart.ts +338 -0
  16. package/src/standard/barChart/barChartStyle.ts +81 -0
  17. package/src/standard/btn/btn.ts +191 -0
  18. package/src/standard/btn/btnStyle.ts +59 -0
  19. package/src/standard/btnGroup/btnGroup.ts +208 -0
  20. package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
  21. package/src/standard/chart/cartesianChart.ts +99 -0
  22. package/src/standard/chart/chartDefaults.ts +25 -0
  23. package/src/standard/chart/chartUtils.ts +84 -0
  24. package/src/standard/classicLogo/classicLogo.ts +429 -0
  25. package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
  26. package/src/standard/componentVariant.ts +153 -0
  27. package/src/standard/composer/composer.ts +1035 -0
  28. package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
  29. package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
  30. package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
  31. package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
  32. package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
  33. package/src/standard/composer/composerStyle.ts +156 -0
  34. package/src/standard/composer/emailHtmlPolicy.ts +474 -0
  35. package/src/standard/consent/consent.ts +173 -0
  36. package/src/standard/consent/consentStyle.ts +44 -0
  37. package/src/standard/contactForm/contactForm.ts +135 -0
  38. package/src/standard/contactForm/contactFormStyle.ts +116 -0
  39. package/src/standard/doughnutChart/doughnutChart.ts +527 -0
  40. package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
  41. package/src/standard/dropFiles/dropFiles.ts +480 -0
  42. package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
  43. package/src/standard/expandablePanel/expandablePanel.ts +106 -0
  44. package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
  45. package/src/standard/flex/flex.ts +269 -0
  46. package/src/standard/flex/flexStyle.ts +556 -0
  47. package/src/standard/footer/footer.ts +93 -0
  48. package/src/standard/footer/footerStyle.ts +39 -0
  49. package/src/standard/form/autoCompleteInput.ts +567 -0
  50. package/src/standard/form/form.ts +205 -0
  51. package/src/standard/form/formCheck.ts +96 -0
  52. package/src/standard/form/formInputField.ts +184 -0
  53. package/src/standard/form/formSelectField.ts +152 -0
  54. package/src/standard/form/formStyle.ts +585 -0
  55. package/src/standard/form/multiAutoCompleteInput.ts +880 -0
  56. package/src/standard/grid/grid.ts +177 -0
  57. package/src/standard/grid/gridStyle.ts +56 -0
  58. package/src/standard/icon/icon.ts +96 -0
  59. package/src/standard/icon/iconStyle.ts +50 -0
  60. package/src/standard/landing/codeShowcase.ts +54 -0
  61. package/src/standard/landing/comparisonColumn.ts +30 -0
  62. package/src/standard/landing/comparisonFeature.ts +22 -0
  63. package/src/standard/landing/comparisonTable.ts +38 -0
  64. package/src/standard/landing/ctaSection.ts +59 -0
  65. package/src/standard/landing/featureCard.ts +41 -0
  66. package/src/standard/landing/landing.ts +52 -0
  67. package/src/standard/landing/landingBand.ts +217 -0
  68. package/src/standard/landing/landingSection.ts +43 -0
  69. package/src/standard/landing/landingTypes.ts +141 -0
  70. package/src/standard/landing/metricItem.ts +25 -0
  71. package/src/standard/landing/metricStrip.ts +20 -0
  72. package/src/standard/lineChart/lineChart.ts +462 -0
  73. package/src/standard/lineChart/lineChartStyle.ts +91 -0
  74. package/src/standard/logo/logo.ts +192 -0
  75. package/src/standard/logo/logoStyle.ts +164 -0
  76. package/src/standard/modal/modal.ts +171 -0
  77. package/src/standard/modal/modalStore.ts +154 -0
  78. package/src/standard/modal/modalStyle.ts +190 -0
  79. package/src/standard/navMenu/navMenu.ts +278 -0
  80. package/src/standard/navMenu/navMenuStyle.ts +323 -0
  81. package/src/standard/pageLinks/pageLinks.ts +90 -0
  82. package/src/standard/pageScript/pageScript.ts +155 -0
  83. package/src/standard/pageToc/pageToc.ts +154 -0
  84. package/src/standard/pageToc/pageTocStyle.ts +372 -0
  85. package/src/standard/panel/panel.ts +55 -0
  86. package/src/standard/panel/panelStyle.ts +15 -0
  87. package/src/standard/pricing/pricing.ts +146 -0
  88. package/src/standard/searchBox/searchBox.ts +54 -0
  89. package/src/standard/searchBox/searchBoxStyle.ts +180 -0
  90. package/src/standard/sectionHeader/sectionHeader.ts +99 -0
  91. package/src/standard/signIn/signIn.ts +167 -0
  92. package/src/standard/signIn/signInStyle.ts +70 -0
  93. package/src/standard/tabs/tabs.ts +193 -0
  94. package/src/standard/tabs/tabsStyle.ts +231 -0
  95. package/src/standard/themeToggle/themeToggle.ts +33 -0
  96. package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
  97. package/src/standard/toast/toastHost.ts +42 -0
  98. package/src/standard/toast/toastStore.ts +50 -0
  99. package/src/standard/topBar/topBar.ts +101 -0
  100. package/src/standard/topBar/topBarStyle.ts +169 -0
  101. package/src/standard/virtualList/variableVirtualTable.ts +371 -0
  102. package/src/standard/virtualList/virtualList.ts +423 -0
  103. package/src/standard/virtualList/virtualListStyle.ts +34 -0
  104. package/src/standard/virtualList/virtualTable.ts +179 -0
  105. package/src/standard/virtualList/virtualTableStyle.ts +24 -0
  106. package/tsconfig.json +23 -0
@@ -0,0 +1,880 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComponentHead,
4
+ type ComputedRef,
5
+ computed,
6
+ defineComponent,
7
+ type Emits,
8
+ html,
9
+ isRef,
10
+ type Ref,
11
+ type RefOrValue,
12
+ ref,
13
+ type SRef,
14
+ sref,
15
+ unref,
16
+ } from 'regor'
17
+ import { createAutoId } from '../autoId'
18
+ import {
19
+ type ComponentVariant,
20
+ resolveComponentClasses,
21
+ } from '../componentVariant'
22
+ import type {
23
+ AutoCompleteOption,
24
+ AutoCompleteValue,
25
+ ResolvedAutoCompleteOption,
26
+ } from './autoCompleteInput'
27
+
28
+ const resolveMultiAutoCompleteInputId = createAutoId(
29
+ 'multi-auto-complete-input',
30
+ )
31
+
32
+ export interface MultiAutoCompleteItem {
33
+ label: RefOrValue<string>
34
+ value?: RefOrValue<AutoCompleteValue>
35
+ disabled?: RefOrValue<boolean>
36
+ invalid?: RefOrValue<boolean>
37
+ tone?: RefOrValue<SemanticTone>
38
+ keywords?: RefOrValue<string | string[]>
39
+ }
40
+
41
+ export interface ResolvedMultiAutoCompleteItem {
42
+ label: string
43
+ value: AutoCompleteValue
44
+ disabled?: boolean
45
+ invalid?: boolean
46
+ tone?: SemanticTone
47
+ keywords: string[]
48
+ }
49
+
50
+ export type MultiAutoCompleteCreateItem = (
51
+ query: string,
52
+ ) => MultiAutoCompleteItem | null | undefined
53
+
54
+ export type MultiAutoCompleteOptionToItem = (
55
+ option: ResolvedAutoCompleteOption,
56
+ ) => MultiAutoCompleteItem | null | undefined
57
+
58
+ export type MultiAutoCompleteSplitInput = (value: string) => string[]
59
+
60
+ export type MultiAutoCompleteItemsChange = (
61
+ items: MultiAutoCompleteItem[],
62
+ ) => void
63
+
64
+ export type MultiAutoCompleteSelect = (
65
+ option: ResolvedAutoCompleteOption,
66
+ item: MultiAutoCompleteItem,
67
+ ) => void
68
+
69
+ export type MultiAutoCompleteSearch = (query: string) => void
70
+
71
+ export interface MultiAutoCompleteSelectedRow {
72
+ id: string
73
+ index: number
74
+ item: ResolvedMultiAutoCompleteItem
75
+ classes: string
76
+ }
77
+
78
+ export interface MultiAutoCompleteOptionRow {
79
+ id: string
80
+ index: number
81
+ option: ResolvedAutoCompleteOption
82
+ active: boolean
83
+ disabled?: boolean
84
+ classes: string
85
+ }
86
+
87
+ export interface MultiAutoCompleteInput {
88
+ id?: RefOrValue<string>
89
+ label?: RefOrValue<string>
90
+ tone?: RefOrValue<SemanticTone>
91
+ variant?: RefOrValue<ComponentVariant>
92
+ items?: SRef<MultiAutoCompleteItem[]>
93
+ model?: Ref<string>
94
+ name?: RefOrValue<string>
95
+ required?: RefOrValue<boolean>
96
+ autocomplete?: RefOrValue<string>
97
+ placeholder?: RefOrValue<string>
98
+ disabled?: RefOrValue<boolean>
99
+ icon?: RefOrValue<string>
100
+ iconEnd?: RefOrValue<string>
101
+ removeIcon?: RefOrValue<string>
102
+ options?: RefOrValue<AutoCompleteOption[]>
103
+ rowComponent?: RefOrValue<string>
104
+ minLength?: RefOrValue<number | string>
105
+ maxResults?: RefOrValue<number | string>
106
+ openOnFocus?: RefOrValue<boolean>
107
+ allowCustomValues?: RefOrValue<boolean>
108
+ allowDuplicates?: RefOrValue<boolean>
109
+ separators?: RefOrValue<string>
110
+ loading?: RefOrValue<boolean>
111
+ emptyText?: RefOrValue<string>
112
+ loadingText?: RefOrValue<string>
113
+ onCreateItem?: RefOrValue<MultiAutoCompleteCreateItem>
114
+ onOptionToItem?: RefOrValue<MultiAutoCompleteOptionToItem>
115
+ onSplitInput?: RefOrValue<MultiAutoCompleteSplitInput>
116
+ onItemsChange?: RefOrValue<MultiAutoCompleteItemsChange>
117
+ onSelect?: RefOrValue<MultiAutoCompleteSelect>
118
+ onSearch?: RefOrValue<MultiAutoCompleteSearch>
119
+ emits?: Emits<
120
+ 'itemschange' | 'itemremove' | 'itemcreate' | 'optionselect' | 'querychange'
121
+ >
122
+ inputId?: string
123
+ listboxId?: string
124
+ inputElement?: SRef<HTMLInputElement | null>
125
+ isOpen?: Ref<boolean>
126
+ activeIndex?: Ref<number>
127
+ query?: ComputedRef<string>
128
+ classes?: ComputedRef<string>
129
+ resolvedItems?: ComputedRef<ResolvedMultiAutoCompleteItem[]>
130
+ selectedRows?: ComputedRef<MultiAutoCompleteSelectedRow[]>
131
+ resolvedOptions?: ComputedRef<ResolvedAutoCompleteOption[]>
132
+ filteredOptions?: ComputedRef<ResolvedAutoCompleteOption[]>
133
+ optionRows?: ComputedRef<MultiAutoCompleteOptionRow[]>
134
+ activeOptionId?: ComputedRef<string | undefined>
135
+ shouldRenderPopup?: ComputedRef<boolean>
136
+ showLoading?: ComputedRef<boolean>
137
+ showEmpty?: ComputedRef<boolean>
138
+ resolvedRowComponent?: ComputedRef<string>
139
+ resolvedIconEnd?: ComputedRef<string | undefined>
140
+ resolvedRemoveIcon?: ComputedRef<string>
141
+ focusInput?: () => void
142
+ handleFocusIn?: () => void
143
+ handleFocusOut?: () => void
144
+ handleInput?: () => void
145
+ handlePaste?: (event: ClipboardEvent) => void
146
+ handleKeydown?: (event: KeyboardEvent) => void
147
+ handleOptionMouseDown?: (event: MouseEvent) => void
148
+ selectOptionRow?: (row: MultiAutoCompleteOptionRow) => void
149
+ removeSelectedRow?: (row: MultiAutoCompleteSelectedRow) => void
150
+ removeItemLabel?: (row: MultiAutoCompleteSelectedRow) => string
151
+ }
152
+
153
+ export interface MultiAutoCompleteDefaultOptionRow {
154
+ option: RefOrValue<ResolvedAutoCompleteOption | null>
155
+ active?: RefOrValue<boolean>
156
+ }
157
+
158
+ const multiAutoCompleteInputTemplate = html`<label
159
+ class="multi-auto-complete-input form-block__field"
160
+ :for="inputId"
161
+ @focusin="handleFocusIn"
162
+ @focusout="handleFocusOut"
163
+ >
164
+ <span class="form-block__label" r-if="label">{{ label }}</span>
165
+ <div
166
+ class="multi-auto-complete-input__shell form-block__input-shell"
167
+ :class="classes"
168
+ @click="focusInput"
169
+ >
170
+ <Icon class="form-block__input-icon" :name="icon" r-if="icon"/>
171
+ <div class="multi-auto-complete-input__control">
172
+ <span
173
+ class="multi-auto-complete-input__item"
174
+ :class="row.classes"
175
+ r-for="row in selectedRows"
176
+ >
177
+ <span class="multi-auto-complete-input__item-label">
178
+ {{ row.item.label }}
179
+ </span>
180
+ <button
181
+ class="multi-auto-complete-input__remove"
182
+ type="button"
183
+ :title="removeItemLabel(row)"
184
+ :aria-label="removeItemLabel(row)"
185
+ :disabled="disabled || row.item.disabled"
186
+ @click="removeSelectedRow(row)"
187
+ >
188
+ <Icon :name="resolvedRemoveIcon"/>
189
+ </button>
190
+ <input
191
+ r-if="name"
192
+ type="hidden"
193
+ :name="name"
194
+ :value="row.item.value"/>
195
+ </span>
196
+ <input
197
+ :id="inputId"
198
+ :ref="inputElement"
199
+ class="multi-auto-complete-input__query form-block__input"
200
+ type="text"
201
+ role="combobox"
202
+ aria-autocomplete="list"
203
+ :aria-controls="listboxId"
204
+ :aria-expanded="isOpen"
205
+ :aria-activedescendant="activeOptionId"
206
+ :required="required && selectedRows.length === 0"
207
+ :autocomplete="autocomplete || 'off'"
208
+ :placeholder="selectedRows.length === 0 ? placeholder : ''"
209
+ :disabled="disabled"
210
+ r-model="model"
211
+ @input="handleInput"
212
+ @paste="handlePaste"
213
+ @keydown="handleKeydown"/>
214
+ </div>
215
+ <Icon
216
+ class="form-block__input-icon"
217
+ :name="resolvedIconEnd"
218
+ r-if="resolvedIconEnd"/>
219
+ </div>
220
+ <div
221
+ class="multi-auto-complete-input__popup"
222
+ r-if="shouldRenderPopup"
223
+ >
224
+ <div
225
+ class="multi-auto-complete-input__message"
226
+ r-if="showLoading"
227
+ role="status"
228
+ aria-live="polite"
229
+ >
230
+ {{ loadingText || 'Loading suggestions' }}
231
+ </div>
232
+ <div
233
+ class="multi-auto-complete-input__message"
234
+ r-if="showEmpty"
235
+ role="status"
236
+ aria-live="polite"
237
+ >
238
+ {{ emptyText || 'No suggestions' }}
239
+ </div>
240
+ <div
241
+ class="multi-auto-complete-input__listbox"
242
+ :id="listboxId"
243
+ role="listbox"
244
+ r-if="!showLoading && !showEmpty"
245
+ >
246
+ <div
247
+ class="multi-auto-complete-input__option"
248
+ :class="row.classes"
249
+ role="option"
250
+ :id="row.id"
251
+ :aria-selected="row.active"
252
+ :aria-disabled="row.disabled"
253
+ r-for="row in optionRows"
254
+ @mousedown="handleOptionMouseDown"
255
+ @click="selectOptionRow(row)"
256
+ >
257
+ <div
258
+ :is="resolvedRowComponent"
259
+ :option="row.option"
260
+ :item="row.option"
261
+ :index="row.index"
262
+ :query="query"
263
+ :active="row.active"
264
+ :selected="false"
265
+ :disabled="row.disabled"
266
+ ></div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </label>`
271
+
272
+ const multiAutoCompleteOptionRowTemplate = html`<div class="multi-auto-complete-input__default-row">
273
+ <span class="multi-auto-complete-input__default-label">
274
+ {{ option?.label }}
275
+ </span>
276
+ </div>`
277
+
278
+ function defineMultiAutoCompleteInputComponent() {
279
+ return defineComponent<MultiAutoCompleteInput>(
280
+ multiAutoCompleteInputTemplate,
281
+ {
282
+ props: [
283
+ 'id',
284
+ 'label',
285
+ 'tone',
286
+ 'variant',
287
+ 'items',
288
+ 'model',
289
+ 'name',
290
+ 'required',
291
+ 'autocomplete',
292
+ 'placeholder',
293
+ 'disabled',
294
+ 'icon',
295
+ 'iconEnd',
296
+ 'removeIcon',
297
+ 'options',
298
+ 'rowComponent',
299
+ 'minLength',
300
+ 'maxResults',
301
+ 'openOnFocus',
302
+ 'allowCustomValues',
303
+ 'allowDuplicates',
304
+ 'separators',
305
+ 'loading',
306
+ 'emptyText',
307
+ 'loadingText',
308
+ 'onCreateItem',
309
+ 'onOptionToItem',
310
+ 'onSplitInput',
311
+ 'onItemsChange',
312
+ 'onSelect',
313
+ 'onSearch',
314
+ ],
315
+ context: (head) => new MultiAutoCompleteInputContext(head),
316
+ },
317
+ )
318
+ }
319
+
320
+ function defineMultiAutoCompleteOptionRowComponent() {
321
+ return defineComponent<MultiAutoCompleteDefaultOptionRow>(
322
+ multiAutoCompleteOptionRowTemplate,
323
+ {
324
+ props: ['option', 'active', 'selected', 'query'],
325
+ context: (head) => head.props,
326
+ },
327
+ )
328
+ }
329
+
330
+ export function defineMultiAutoCompleteInputComponents() {
331
+ return {
332
+ multiAutoCompleteInput: defineMultiAutoCompleteInputComponent(),
333
+ multiAutoCompleteOptionRow: defineMultiAutoCompleteOptionRowComponent(),
334
+ }
335
+ }
336
+
337
+ class MultiAutoCompleteInputContext implements MultiAutoCompleteInput {
338
+ readonly id: string
339
+ readonly label?: RefOrValue<string>
340
+ readonly tone?: RefOrValue<SemanticTone>
341
+ readonly variant?: RefOrValue<ComponentVariant>
342
+ readonly items: SRef<MultiAutoCompleteItem[]>
343
+ readonly model: Ref<string>
344
+ readonly name?: RefOrValue<string>
345
+ readonly required?: RefOrValue<boolean>
346
+ readonly autocomplete?: RefOrValue<string>
347
+ readonly placeholder?: RefOrValue<string>
348
+ readonly disabled?: RefOrValue<boolean>
349
+ readonly icon?: RefOrValue<string>
350
+ readonly iconEnd?: RefOrValue<string>
351
+ readonly removeIcon?: RefOrValue<string>
352
+ readonly options?: RefOrValue<AutoCompleteOption[]>
353
+ readonly rowComponent?: RefOrValue<string>
354
+ readonly minLength?: RefOrValue<number | string>
355
+ readonly maxResults?: RefOrValue<number | string>
356
+ readonly openOnFocus?: RefOrValue<boolean>
357
+ readonly allowCustomValues?: RefOrValue<boolean>
358
+ readonly allowDuplicates?: RefOrValue<boolean>
359
+ readonly separators?: RefOrValue<string>
360
+ readonly loading?: RefOrValue<boolean>
361
+ readonly emptyText?: RefOrValue<string>
362
+ readonly loadingText?: RefOrValue<string>
363
+ readonly onCreateItem?: RefOrValue<MultiAutoCompleteCreateItem>
364
+ readonly onOptionToItem?: RefOrValue<MultiAutoCompleteOptionToItem>
365
+ readonly onSplitInput?: RefOrValue<MultiAutoCompleteSplitInput>
366
+ readonly onItemsChange?: RefOrValue<MultiAutoCompleteItemsChange>
367
+ readonly onSelect?: RefOrValue<MultiAutoCompleteSelect>
368
+ readonly onSearch?: RefOrValue<MultiAutoCompleteSearch>
369
+ readonly inputId: string
370
+ readonly listboxId: string
371
+ readonly inputElement = sref<HTMLInputElement | null>(null)
372
+ readonly isOpen = ref(false)
373
+ readonly activeIndex = ref(-1)
374
+ readonly query: ComputedRef<string>
375
+ readonly classes: ComputedRef<string>
376
+ readonly resolvedItems: ComputedRef<ResolvedMultiAutoCompleteItem[]>
377
+ readonly selectedRows: ComputedRef<MultiAutoCompleteSelectedRow[]>
378
+ readonly resolvedOptions: ComputedRef<ResolvedAutoCompleteOption[]>
379
+ readonly filteredOptions: ComputedRef<ResolvedAutoCompleteOption[]>
380
+ readonly optionRows: ComputedRef<MultiAutoCompleteOptionRow[]>
381
+ readonly activeOptionId: ComputedRef<string | undefined>
382
+ readonly shouldRenderPopup: ComputedRef<boolean>
383
+ readonly showLoading: ComputedRef<boolean>
384
+ readonly showEmpty: ComputedRef<boolean>
385
+ readonly resolvedRowComponent: ComputedRef<string>
386
+ readonly resolvedIconEnd: ComputedRef<string | undefined>
387
+ readonly resolvedRemoveIcon: ComputedRef<string>
388
+ private readonly emit: ComponentHead<MultiAutoCompleteInput>['emit']
389
+ private closingTimer: ReturnType<typeof setTimeout> | undefined
390
+
391
+ constructor(head: ComponentHead<MultiAutoCompleteInput>) {
392
+ const props = head.props
393
+ Object.assign(this, props)
394
+ this.emit = head.emit
395
+ this.id = resolveMultiAutoCompleteInputId(props.id)
396
+ this.inputId = `${this.id}-query`
397
+ this.listboxId = `${this.id}-listbox`
398
+ this.items = isRef(props.items)
399
+ ? props.items
400
+ : sref<MultiAutoCompleteItem[]>([])
401
+ this.model = isRef(props.model) ? props.model : ref('')
402
+ this.query = computed(() => String(unref(this.model) ?? ''))
403
+ this.classes = computed(() =>
404
+ resolveComponentClasses(props, {
405
+ defaultVariant: 'surfaceAlt',
406
+ classes: [
407
+ unref(this.disabled)
408
+ ? 'multi-auto-complete-input__shell--disabled'
409
+ : '',
410
+ ],
411
+ }),
412
+ )
413
+ this.resolvedItems = computed(() =>
414
+ this.items().map(resolveMultiAutoCompleteItem),
415
+ )
416
+ this.selectedRows = computed(() => this.resolveSelectedRows())
417
+ this.resolvedOptions = computed(() => {
418
+ const options = unref(props.options)
419
+ return Array.isArray(options)
420
+ ? options.map(resolveAutoCompleteOption)
421
+ : []
422
+ })
423
+ this.filteredOptions = computed(() => this.resolveFilteredOptions())
424
+ this.optionRows = computed(() => this.resolveOptionRows())
425
+ this.activeOptionId = computed(
426
+ () => this.optionRows()[this.activeIndex()]?.id,
427
+ )
428
+ this.showLoading = computed(() => this.isOpen() && !!unref(this.loading))
429
+ this.showEmpty = computed(
430
+ () =>
431
+ this.isOpen() &&
432
+ !this.showLoading() &&
433
+ this.meetsMinimumQueryLength() &&
434
+ this.filteredOptions().length === 0,
435
+ )
436
+ this.shouldRenderPopup = computed(
437
+ () =>
438
+ this.isOpen() &&
439
+ (this.showLoading() ||
440
+ this.showEmpty() ||
441
+ this.filteredOptions().length > 0),
442
+ )
443
+ this.resolvedRowComponent = computed(() => {
444
+ const component = unref(props.rowComponent)
445
+ return typeof component === 'string' && component.trim()
446
+ ? component
447
+ : 'MultiAutoCompleteOptionRow'
448
+ })
449
+ this.resolvedIconEnd = computed(
450
+ () =>
451
+ unref(props.iconEnd) ||
452
+ (unref(props.loading) ? 'lucide:loader-circle' : 'lucide:chevron-down'),
453
+ )
454
+ this.resolvedRemoveIcon = computed(
455
+ () => unref(props.removeIcon) || 'lucide:x',
456
+ )
457
+ }
458
+
459
+ focusInput = () => {
460
+ if (unref(this.disabled)) return
461
+ this.inputElement()?.focus()
462
+ }
463
+
464
+ handleFocusIn = () => {
465
+ this.clearClosingTimer()
466
+ if (!this.shouldOpenOnFocus()) return
467
+
468
+ this.open()
469
+ }
470
+
471
+ handleFocusOut = () => {
472
+ this.clearClosingTimer()
473
+ this.closingTimer = setTimeout(() => {
474
+ this.close()
475
+ }, 0)
476
+ }
477
+
478
+ handleInput = () => {
479
+ const query = this.query()
480
+ unref(this.onSearch)?.(query)
481
+ this.emit('querychange', { query })
482
+ if (this.meetsMinimumQueryLength()) this.open()
483
+ else this.close()
484
+ }
485
+
486
+ handlePaste = (event: ClipboardEvent) => {
487
+ if (unref(this.disabled) || !this.canCreateCustomValues()) return
488
+
489
+ const text = event.clipboardData?.getData('text') ?? ''
490
+ const values = this.splitValues(text)
491
+ if (values.length <= 1) return
492
+
493
+ event.preventDefault()
494
+ this.addCreatedValues(values)
495
+ }
496
+
497
+ handleKeydown = (event: KeyboardEvent) => {
498
+ if (unref(this.disabled)) return
499
+
500
+ if (event.key === 'ArrowDown') {
501
+ event.preventDefault()
502
+ if (!this.isOpen()) {
503
+ this.open()
504
+ return
505
+ }
506
+ this.open()
507
+ this.moveActive(1)
508
+ return
509
+ }
510
+ if (event.key === 'ArrowUp') {
511
+ event.preventDefault()
512
+ if (!this.isOpen()) {
513
+ this.open()
514
+ this.activateLast()
515
+ return
516
+ }
517
+ this.open()
518
+ this.moveActive(-1)
519
+ return
520
+ }
521
+ if (event.key === 'Home' && this.isOpen()) {
522
+ event.preventDefault()
523
+ this.activateFirst()
524
+ return
525
+ }
526
+ if (event.key === 'End' && this.isOpen()) {
527
+ event.preventDefault()
528
+ this.activateLast()
529
+ return
530
+ }
531
+ if (event.key === 'Enter') {
532
+ const row = this.isOpen() ? this.optionRows()[this.activeIndex()] : null
533
+ if (row) {
534
+ event.preventDefault()
535
+ this.selectOptionRow(row)
536
+ return
537
+ }
538
+
539
+ if (this.commitQuery()) event.preventDefault()
540
+ return
541
+ }
542
+ if (event.key === 'Backspace' && !this.query()) {
543
+ const rows = this.selectedRows()
544
+ const row = rows[rows.length - 1]
545
+ if (!row) return
546
+
547
+ event.preventDefault()
548
+ this.removeSelectedRow(row)
549
+ return
550
+ }
551
+ if (event.key === 'Escape') {
552
+ this.close()
553
+ return
554
+ }
555
+ if (
556
+ event.key.length === 1 &&
557
+ this.resolvedSeparators().includes(event.key)
558
+ ) {
559
+ if (this.commitQuery()) event.preventDefault()
560
+ }
561
+ }
562
+
563
+ handleOptionMouseDown = (event: MouseEvent) => {
564
+ event.preventDefault()
565
+ this.clearClosingTimer()
566
+ }
567
+
568
+ selectOptionRow = (row: MultiAutoCompleteOptionRow) => {
569
+ if (row.disabled || unref(this.disabled)) return
570
+
571
+ const item = unref(this.onOptionToItem)?.(row.option) ?? {
572
+ label: row.option.label,
573
+ value: row.option.value,
574
+ }
575
+ if (!this.addItem(item)) return
576
+
577
+ this.model('')
578
+ unref(this.onSelect)?.(row.option, item)
579
+ this.emit('optionselect', { option: row.option, item })
580
+ this.close()
581
+ }
582
+
583
+ removeSelectedRow = (row: MultiAutoCompleteSelectedRow) => {
584
+ if (unref(this.disabled) || row.item.disabled) return
585
+
586
+ const next = this.items().filter((_, index) => index !== row.index)
587
+ this.writeItems(next)
588
+ this.emit('itemremove', { item: row.item, index: row.index })
589
+ }
590
+
591
+ removeItemLabel = (row: MultiAutoCompleteSelectedRow) =>
592
+ `Remove ${row.item.label}`
593
+
594
+ private commitQuery() {
595
+ if (!this.canCreateCustomValues()) return false
596
+
597
+ const values = this.splitValues(this.query())
598
+ if (values.length === 0) return false
599
+
600
+ const added = this.addCreatedValues(values)
601
+ if (added > 0) this.model('')
602
+ return added > 0
603
+ }
604
+
605
+ private addCreatedValues(values: string[]) {
606
+ const created: MultiAutoCompleteItem[] = []
607
+ for (const value of values) {
608
+ const item = unref(this.onCreateItem)?.(value) ?? {
609
+ label: value,
610
+ value,
611
+ }
612
+ if (!item || this.hasItemValue(item)) continue
613
+ created.push(item)
614
+ }
615
+ if (created.length === 0) return 0
616
+
617
+ const next = [...this.items(), ...created]
618
+ this.writeItems(next)
619
+ for (const item of created) this.emit('itemcreate', { item })
620
+ this.close()
621
+ return created.length
622
+ }
623
+
624
+ private addItem(item: MultiAutoCompleteItem) {
625
+ if (!item || this.hasItemValue(item)) return false
626
+
627
+ this.writeItems([...this.items(), item])
628
+ return true
629
+ }
630
+
631
+ private writeItems(items: MultiAutoCompleteItem[]) {
632
+ this.items(items)
633
+ unref(this.onItemsChange)?.(items)
634
+ this.emit('itemschange', { items })
635
+ }
636
+
637
+ private resolveSelectedRows() {
638
+ return this.resolvedItems().map(
639
+ (item, index): MultiAutoCompleteSelectedRow => ({
640
+ id: `${this.id}-item-${index}`,
641
+ index,
642
+ item,
643
+ classes: [
644
+ item.invalid ? 'multi-auto-complete-input__item--invalid' : '',
645
+ item.disabled ? 'multi-auto-complete-input__item--disabled' : '',
646
+ item.tone ? `tone-${item.tone}` : '',
647
+ ]
648
+ .filter(Boolean)
649
+ .join(' '),
650
+ }),
651
+ )
652
+ }
653
+
654
+ private resolveFilteredOptions() {
655
+ const query = normalizeSearchText(this.query())
656
+ if (!this.meetsMinimumQueryLength()) return []
657
+
658
+ const options = this.resolvedOptions().filter(
659
+ (option) =>
660
+ !this.hasOptionValue(option) &&
661
+ (!query ||
662
+ option.keywords.some((keyword) =>
663
+ normalizeSearchText(keyword).includes(query),
664
+ )),
665
+ )
666
+ return options.slice(0, this.readPositiveInteger(this.maxResults, 10))
667
+ }
668
+
669
+ private resolveOptionRows() {
670
+ const activeIndex = this.activeIndex()
671
+ return this.filteredOptions().map(
672
+ (option, index): MultiAutoCompleteOptionRow => ({
673
+ id: `${this.listboxId}-option-${index}`,
674
+ index,
675
+ option,
676
+ active: index === activeIndex,
677
+ disabled: option.disabled,
678
+ classes: [
679
+ index === activeIndex
680
+ ? 'multi-auto-complete-input__option--active'
681
+ : '',
682
+ option.disabled ? 'multi-auto-complete-input__option--disabled' : '',
683
+ ]
684
+ .filter(Boolean)
685
+ .join(' '),
686
+ }),
687
+ )
688
+ }
689
+
690
+ private open() {
691
+ if (unref(this.disabled) || !this.meetsMinimumQueryLength()) return
692
+
693
+ this.isOpen(true)
694
+ this.ensureActiveIndex()
695
+ }
696
+
697
+ private close() {
698
+ this.isOpen(false)
699
+ this.activeIndex(-1)
700
+ }
701
+
702
+ private moveActive(direction: 1 | -1) {
703
+ const rows = this.optionRows()
704
+ if (rows.length === 0) {
705
+ this.activeIndex(-1)
706
+ return
707
+ }
708
+
709
+ const current = this.activeIndex()
710
+ for (let offset = 1; offset <= rows.length; offset += 1) {
711
+ const next = wrapIndex(current + direction * offset, rows.length)
712
+ if (!rows[next]?.disabled) {
713
+ this.activeIndex(next)
714
+ return
715
+ }
716
+ }
717
+ }
718
+
719
+ private activateFirst() {
720
+ const index = this.optionRows().findIndex((row) => !row.disabled)
721
+ this.activeIndex(index)
722
+ }
723
+
724
+ private activateLast() {
725
+ const rows = this.optionRows()
726
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
727
+ if (rows[index]?.disabled) continue
728
+
729
+ this.activeIndex(index)
730
+ return
731
+ }
732
+ this.activeIndex(-1)
733
+ }
734
+
735
+ private ensureActiveIndex() {
736
+ const rows = this.optionRows()
737
+ const current = this.activeIndex()
738
+ if (current >= 0 && current < rows.length && !rows[current]?.disabled)
739
+ return
740
+
741
+ this.activateFirst()
742
+ }
743
+
744
+ private shouldOpenOnFocus() {
745
+ return unref(this.openOnFocus) !== false && this.meetsMinimumQueryLength()
746
+ }
747
+
748
+ private meetsMinimumQueryLength() {
749
+ return (
750
+ this.query().trim().length >= this.readPositiveInteger(this.minLength, 0)
751
+ )
752
+ }
753
+
754
+ private canCreateCustomValues() {
755
+ return unref(this.allowCustomValues) !== false
756
+ }
757
+
758
+ private hasOptionValue(option: ResolvedAutoCompleteOption) {
759
+ if (unref(this.allowDuplicates)) return false
760
+
761
+ const key = normalizeItemValue(option.value)
762
+ return this.resolvedItems().some(
763
+ (item) => normalizeItemValue(item.value) === key,
764
+ )
765
+ }
766
+
767
+ private hasItemValue(itemInput: MultiAutoCompleteItem) {
768
+ if (unref(this.allowDuplicates)) return false
769
+
770
+ const item = resolveMultiAutoCompleteItem(itemInput)
771
+ const key = normalizeItemValue(item.value)
772
+ return this.resolvedItems().some(
773
+ (existing) => normalizeItemValue(existing.value) === key,
774
+ )
775
+ }
776
+
777
+ private splitValues(value: string) {
778
+ const values =
779
+ unref(this.onSplitInput)?.(value) ??
780
+ splitBySeparators(value, this.resolvedSeparators())
781
+ return values.map((item) => item.trim()).filter(Boolean)
782
+ }
783
+
784
+ private resolvedSeparators() {
785
+ const value = unref(this.separators)
786
+ return typeof value === 'string' ? value : ',;'
787
+ }
788
+
789
+ private readPositiveInteger(
790
+ value: RefOrValue<number | string> | undefined,
791
+ fallback: number,
792
+ ) {
793
+ const raw = unref(value)
794
+ const parsed =
795
+ typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
796
+ return Number.isFinite(parsed) && parsed >= 0
797
+ ? Math.trunc(parsed)
798
+ : fallback
799
+ }
800
+
801
+ private clearClosingTimer() {
802
+ if (this.closingTimer === undefined) return
803
+
804
+ clearTimeout(this.closingTimer)
805
+ this.closingTimer = undefined
806
+ }
807
+ }
808
+
809
+ function resolveMultiAutoCompleteItem(
810
+ itemInput: RefOrValue<MultiAutoCompleteItem>,
811
+ ): ResolvedMultiAutoCompleteItem {
812
+ const item = unref(itemInput)
813
+ const label = String(unref(item.label) ?? '')
814
+ const value = unref(item.value) ?? label
815
+ return {
816
+ label,
817
+ value,
818
+ disabled: unref(item.disabled),
819
+ invalid: unref(item.invalid),
820
+ tone: unref(item.tone),
821
+ keywords: resolveKeywords(item, label, value),
822
+ }
823
+ }
824
+
825
+ function resolveAutoCompleteOption(
826
+ optionInput: RefOrValue<AutoCompleteOption>,
827
+ ): ResolvedAutoCompleteOption {
828
+ const option = unref(optionInput)
829
+ const label = String(unref(option.label) ?? '')
830
+ const value = unref(option.value) ?? label
831
+ return {
832
+ label,
833
+ value,
834
+ disabled: unref(option.disabled),
835
+ keywords: resolveKeywords(option, label, value),
836
+ }
837
+ }
838
+
839
+ function resolveKeywords(
840
+ item: Pick<AutoCompleteOption, 'keywords'>,
841
+ label: string,
842
+ value: AutoCompleteValue,
843
+ ) {
844
+ const keywords = unref(item.keywords)
845
+ const source: string[] = Array.isArray(keywords)
846
+ ? keywords.map((keyword) => String(unref(keyword) ?? ''))
847
+ : typeof keywords === 'string'
848
+ ? keywords.split(/\s+/)
849
+ : []
850
+ return [label, String(value), ...source].filter(Boolean)
851
+ }
852
+
853
+ function splitBySeparators(value: string, separators: string) {
854
+ const values: string[] = []
855
+ let current = ''
856
+ for (const char of value) {
857
+ if (char === '\n' || separators.includes(char)) {
858
+ values.push(current)
859
+ current = ''
860
+ continue
861
+ }
862
+
863
+ current += char
864
+ }
865
+
866
+ values.push(current)
867
+ return values
868
+ }
869
+
870
+ function normalizeSearchText(value: string) {
871
+ return value.trim().toLocaleLowerCase()
872
+ }
873
+
874
+ function normalizeItemValue(value: AutoCompleteValue) {
875
+ return String(value).trim().toLocaleLowerCase()
876
+ }
877
+
878
+ function wrapIndex(index: number, length: number) {
879
+ return ((index % length) + length) % length
880
+ }