@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.
- package/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/ts-components.d.mts +1662 -0
- package/dist/ts-components.d.mts.map +1 -0
- package/dist/ts-components.mjs +9200 -0
- package/dist/ts-components.mjs.map +1 -0
- package/package.json +130 -0
- package/src/defineComponents.ts +87 -0
- package/src/index.ts +270 -0
- package/src/registerStyles.ts +63 -0
- package/src/standard/alertBox/alertBox.ts +64 -0
- package/src/standard/autoId.ts +25 -0
- package/src/standard/badge/badge.ts +41 -0
- package/src/standard/badge/badgeStyle.ts +21 -0
- package/src/standard/barChart/barChart.ts +338 -0
- package/src/standard/barChart/barChartStyle.ts +81 -0
- package/src/standard/btn/btn.ts +191 -0
- package/src/standard/btn/btnStyle.ts +59 -0
- package/src/standard/btnGroup/btnGroup.ts +208 -0
- package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
- package/src/standard/chart/cartesianChart.ts +99 -0
- package/src/standard/chart/chartDefaults.ts +25 -0
- package/src/standard/chart/chartUtils.ts +84 -0
- package/src/standard/classicLogo/classicLogo.ts +429 -0
- package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
- package/src/standard/componentVariant.ts +153 -0
- package/src/standard/composer/composer.ts +1035 -0
- package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
- package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
- package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
- package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
- package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
- package/src/standard/composer/composerStyle.ts +156 -0
- package/src/standard/composer/emailHtmlPolicy.ts +474 -0
- package/src/standard/consent/consent.ts +173 -0
- package/src/standard/consent/consentStyle.ts +44 -0
- package/src/standard/contactForm/contactForm.ts +135 -0
- package/src/standard/contactForm/contactFormStyle.ts +116 -0
- package/src/standard/doughnutChart/doughnutChart.ts +527 -0
- package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
- package/src/standard/dropFiles/dropFiles.ts +480 -0
- package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
- package/src/standard/expandablePanel/expandablePanel.ts +106 -0
- package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
- package/src/standard/flex/flex.ts +269 -0
- package/src/standard/flex/flexStyle.ts +556 -0
- package/src/standard/footer/footer.ts +93 -0
- package/src/standard/footer/footerStyle.ts +39 -0
- package/src/standard/form/autoCompleteInput.ts +567 -0
- package/src/standard/form/form.ts +205 -0
- package/src/standard/form/formCheck.ts +96 -0
- package/src/standard/form/formInputField.ts +184 -0
- package/src/standard/form/formSelectField.ts +152 -0
- package/src/standard/form/formStyle.ts +585 -0
- package/src/standard/form/multiAutoCompleteInput.ts +880 -0
- package/src/standard/grid/grid.ts +177 -0
- package/src/standard/grid/gridStyle.ts +56 -0
- package/src/standard/icon/icon.ts +96 -0
- package/src/standard/icon/iconStyle.ts +50 -0
- package/src/standard/landing/codeShowcase.ts +54 -0
- package/src/standard/landing/comparisonColumn.ts +30 -0
- package/src/standard/landing/comparisonFeature.ts +22 -0
- package/src/standard/landing/comparisonTable.ts +38 -0
- package/src/standard/landing/ctaSection.ts +59 -0
- package/src/standard/landing/featureCard.ts +41 -0
- package/src/standard/landing/landing.ts +52 -0
- package/src/standard/landing/landingBand.ts +217 -0
- package/src/standard/landing/landingSection.ts +43 -0
- package/src/standard/landing/landingTypes.ts +141 -0
- package/src/standard/landing/metricItem.ts +25 -0
- package/src/standard/landing/metricStrip.ts +20 -0
- package/src/standard/lineChart/lineChart.ts +462 -0
- package/src/standard/lineChart/lineChartStyle.ts +91 -0
- package/src/standard/logo/logo.ts +192 -0
- package/src/standard/logo/logoStyle.ts +164 -0
- package/src/standard/modal/modal.ts +171 -0
- package/src/standard/modal/modalStore.ts +154 -0
- package/src/standard/modal/modalStyle.ts +190 -0
- package/src/standard/navMenu/navMenu.ts +278 -0
- package/src/standard/navMenu/navMenuStyle.ts +323 -0
- package/src/standard/pageLinks/pageLinks.ts +90 -0
- package/src/standard/pageScript/pageScript.ts +155 -0
- package/src/standard/pageToc/pageToc.ts +154 -0
- package/src/standard/pageToc/pageTocStyle.ts +372 -0
- package/src/standard/panel/panel.ts +55 -0
- package/src/standard/panel/panelStyle.ts +15 -0
- package/src/standard/pricing/pricing.ts +146 -0
- package/src/standard/searchBox/searchBox.ts +54 -0
- package/src/standard/searchBox/searchBoxStyle.ts +180 -0
- package/src/standard/sectionHeader/sectionHeader.ts +99 -0
- package/src/standard/signIn/signIn.ts +167 -0
- package/src/standard/signIn/signInStyle.ts +70 -0
- package/src/standard/tabs/tabs.ts +193 -0
- package/src/standard/tabs/tabsStyle.ts +231 -0
- package/src/standard/themeToggle/themeToggle.ts +33 -0
- package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
- package/src/standard/toast/toastHost.ts +42 -0
- package/src/standard/toast/toastStore.ts +50 -0
- package/src/standard/topBar/topBar.ts +101 -0
- package/src/standard/topBar/topBarStyle.ts +169 -0
- package/src/standard/virtualList/variableVirtualTable.ts +371 -0
- package/src/standard/virtualList/virtualList.ts +423 -0
- package/src/standard/virtualList/virtualListStyle.ts +34 -0
- package/src/standard/virtualList/virtualTable.ts +179 -0
- package/src/standard/virtualList/virtualTableStyle.ts +24 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,567 @@
|
|
|
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
|
+
unref,
|
|
14
|
+
} from 'regor'
|
|
15
|
+
import { createAutoId } from '../autoId'
|
|
16
|
+
import type { ComponentVariant } from '../componentVariant'
|
|
17
|
+
import type { FormInputFieldType } from './formInputField'
|
|
18
|
+
|
|
19
|
+
const resolveAutoCompleteInputId = createAutoId('auto-complete-input')
|
|
20
|
+
|
|
21
|
+
export type AutoCompleteValue = string | number
|
|
22
|
+
|
|
23
|
+
export interface AutoCompleteOption {
|
|
24
|
+
label: RefOrValue<string>
|
|
25
|
+
value?: RefOrValue<AutoCompleteValue>
|
|
26
|
+
disabled?: RefOrValue<boolean>
|
|
27
|
+
keywords?: RefOrValue<string | string[]>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResolvedAutoCompleteOption {
|
|
31
|
+
label: string
|
|
32
|
+
value: AutoCompleteValue
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
keywords: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AutoCompleteRow {
|
|
38
|
+
id: string
|
|
39
|
+
index: number
|
|
40
|
+
option: ResolvedAutoCompleteOption
|
|
41
|
+
active: boolean
|
|
42
|
+
selected: boolean
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
classes: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AutoCompleteInput {
|
|
48
|
+
id?: RefOrValue<string>
|
|
49
|
+
label?: RefOrValue<string>
|
|
50
|
+
tone?: RefOrValue<SemanticTone>
|
|
51
|
+
variant?: RefOrValue<ComponentVariant>
|
|
52
|
+
model?: Ref<string>
|
|
53
|
+
selectedValue?: Ref<AutoCompleteValue | null>
|
|
54
|
+
type?: RefOrValue<FormInputFieldType>
|
|
55
|
+
name?: RefOrValue<string>
|
|
56
|
+
required?: RefOrValue<boolean>
|
|
57
|
+
autocomplete?: RefOrValue<string>
|
|
58
|
+
placeholder?: RefOrValue<string>
|
|
59
|
+
disabled?: RefOrValue<boolean>
|
|
60
|
+
icon?: RefOrValue<string>
|
|
61
|
+
iconEnd?: RefOrValue<string>
|
|
62
|
+
options?: RefOrValue<AutoCompleteOption[]>
|
|
63
|
+
rowComponent?: RefOrValue<string>
|
|
64
|
+
minLength?: RefOrValue<number | string>
|
|
65
|
+
maxResults?: RefOrValue<number | string>
|
|
66
|
+
openOnFocus?: RefOrValue<boolean>
|
|
67
|
+
loading?: RefOrValue<boolean>
|
|
68
|
+
emptyText?: RefOrValue<string>
|
|
69
|
+
loadingText?: RefOrValue<string>
|
|
70
|
+
onSelect?: (option: ResolvedAutoCompleteOption) => void
|
|
71
|
+
onSearch?: (query: string) => void
|
|
72
|
+
emits?: Emits<'optionselect' | 'querychange'>
|
|
73
|
+
listboxId?: string
|
|
74
|
+
isOpen?: Ref<boolean>
|
|
75
|
+
activeIndex?: Ref<number>
|
|
76
|
+
query?: ComputedRef<string>
|
|
77
|
+
resolvedOptions?: ComputedRef<ResolvedAutoCompleteOption[]>
|
|
78
|
+
filteredOptions?: ComputedRef<ResolvedAutoCompleteOption[]>
|
|
79
|
+
rows?: ComputedRef<AutoCompleteRow[]>
|
|
80
|
+
activeOptionId?: ComputedRef<string | undefined>
|
|
81
|
+
shouldRenderPopup?: ComputedRef<boolean>
|
|
82
|
+
showLoading?: ComputedRef<boolean>
|
|
83
|
+
showEmpty?: ComputedRef<boolean>
|
|
84
|
+
resolvedRowComponent?: ComputedRef<string>
|
|
85
|
+
resolvedIconEnd?: ComputedRef<string | undefined>
|
|
86
|
+
handleFocusIn?: () => void
|
|
87
|
+
handleFocusOut?: () => void
|
|
88
|
+
handleInput?: () => void
|
|
89
|
+
handleKeydown?: (event: KeyboardEvent) => void
|
|
90
|
+
handleOptionMouseDown?: (event: MouseEvent) => void
|
|
91
|
+
selectRow?: (row: AutoCompleteRow) => void
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AutoCompleteOptionRow {
|
|
95
|
+
option: RefOrValue<ResolvedAutoCompleteOption | null>
|
|
96
|
+
active?: RefOrValue<boolean>
|
|
97
|
+
selected?: RefOrValue<boolean>
|
|
98
|
+
query?: RefOrValue<string>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const autoCompleteInputTemplate = html`<div
|
|
102
|
+
class="auto-complete-input"
|
|
103
|
+
@focusin="handleFocusIn"
|
|
104
|
+
@focusout="handleFocusOut"
|
|
105
|
+
>
|
|
106
|
+
<FormInputField
|
|
107
|
+
:id="id"
|
|
108
|
+
:label="label"
|
|
109
|
+
:tone="tone"
|
|
110
|
+
:variant="variant"
|
|
111
|
+
:model="model"
|
|
112
|
+
:type="type || 'search'"
|
|
113
|
+
:name="name"
|
|
114
|
+
:required="required"
|
|
115
|
+
:autocomplete="autocomplete || 'off'"
|
|
116
|
+
:placeholder="placeholder"
|
|
117
|
+
:disabled="disabled"
|
|
118
|
+
:icon="icon"
|
|
119
|
+
:iconEnd="resolvedIconEnd"
|
|
120
|
+
role="combobox"
|
|
121
|
+
aria-autocomplete="list"
|
|
122
|
+
:aria-controls="listboxId"
|
|
123
|
+
:aria-expanded="isOpen"
|
|
124
|
+
:aria-activedescendant="activeOptionId"
|
|
125
|
+
@input="handleInput"
|
|
126
|
+
@keydown="handleKeydown"
|
|
127
|
+
/>
|
|
128
|
+
<div
|
|
129
|
+
class="auto-complete-input__popup"
|
|
130
|
+
r-if="shouldRenderPopup"
|
|
131
|
+
>
|
|
132
|
+
<div
|
|
133
|
+
class="auto-complete-input__message"
|
|
134
|
+
r-if="showLoading"
|
|
135
|
+
role="status"
|
|
136
|
+
aria-live="polite"
|
|
137
|
+
>
|
|
138
|
+
{{ loadingText || 'Loading suggestions' }}
|
|
139
|
+
</div>
|
|
140
|
+
<div
|
|
141
|
+
class="auto-complete-input__message"
|
|
142
|
+
r-if="showEmpty"
|
|
143
|
+
role="status"
|
|
144
|
+
aria-live="polite"
|
|
145
|
+
>
|
|
146
|
+
{{ emptyText || 'No suggestions' }}
|
|
147
|
+
</div>
|
|
148
|
+
<div
|
|
149
|
+
class="auto-complete-input__listbox"
|
|
150
|
+
:id="listboxId"
|
|
151
|
+
role="listbox"
|
|
152
|
+
r-if="!showLoading && !showEmpty"
|
|
153
|
+
>
|
|
154
|
+
<div
|
|
155
|
+
class="auto-complete-input__option"
|
|
156
|
+
:class="row.classes"
|
|
157
|
+
role="option"
|
|
158
|
+
:id="row.id"
|
|
159
|
+
:aria-selected="row.active"
|
|
160
|
+
:aria-disabled="row.disabled"
|
|
161
|
+
r-for="row in rows"
|
|
162
|
+
@mousedown="handleOptionMouseDown"
|
|
163
|
+
@click="selectRow(row)"
|
|
164
|
+
>
|
|
165
|
+
<div
|
|
166
|
+
:is="resolvedRowComponent"
|
|
167
|
+
:option="row.option"
|
|
168
|
+
:item="row.option"
|
|
169
|
+
:index="row.index"
|
|
170
|
+
:query="query"
|
|
171
|
+
:active="row.active"
|
|
172
|
+
:selected="row.selected"
|
|
173
|
+
:disabled="row.disabled"
|
|
174
|
+
></div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>`
|
|
179
|
+
|
|
180
|
+
const autoCompleteOptionRowTemplate = html`<div class="auto-complete-input__default-row">
|
|
181
|
+
<span class="auto-complete-input__default-label">{{ option?.label }}</span>
|
|
182
|
+
<Icon
|
|
183
|
+
class="auto-complete-input__default-check"
|
|
184
|
+
name="lucide:check"
|
|
185
|
+
r-if="selected"
|
|
186
|
+
/>
|
|
187
|
+
</div>`
|
|
188
|
+
|
|
189
|
+
function defineAutoCompleteInputComponent() {
|
|
190
|
+
return defineComponent<AutoCompleteInput>(autoCompleteInputTemplate, {
|
|
191
|
+
props: [
|
|
192
|
+
'id',
|
|
193
|
+
'label',
|
|
194
|
+
'tone',
|
|
195
|
+
'variant',
|
|
196
|
+
'model',
|
|
197
|
+
'selectedValue',
|
|
198
|
+
'type',
|
|
199
|
+
'name',
|
|
200
|
+
'required',
|
|
201
|
+
'autocomplete',
|
|
202
|
+
'placeholder',
|
|
203
|
+
'disabled',
|
|
204
|
+
'icon',
|
|
205
|
+
'iconEnd',
|
|
206
|
+
'options',
|
|
207
|
+
'rowComponent',
|
|
208
|
+
'minLength',
|
|
209
|
+
'maxResults',
|
|
210
|
+
'openOnFocus',
|
|
211
|
+
'loading',
|
|
212
|
+
'emptyText',
|
|
213
|
+
'loadingText',
|
|
214
|
+
'onSelect',
|
|
215
|
+
'onSearch',
|
|
216
|
+
],
|
|
217
|
+
context: (head) => new AutoCompleteInputContext(head),
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function defineAutoCompleteOptionRowComponent() {
|
|
222
|
+
return defineComponent<AutoCompleteOptionRow>(autoCompleteOptionRowTemplate, {
|
|
223
|
+
props: ['option', 'active', 'selected', 'query'],
|
|
224
|
+
context: (head) => head.props,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function defineAutoCompleteInputComponents() {
|
|
229
|
+
return {
|
|
230
|
+
autoCompleteInput: defineAutoCompleteInputComponent(),
|
|
231
|
+
autoCompleteOptionRow: defineAutoCompleteOptionRowComponent(),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
class AutoCompleteInputContext implements AutoCompleteInput {
|
|
236
|
+
readonly id: string
|
|
237
|
+
readonly label?: RefOrValue<string>
|
|
238
|
+
readonly tone?: RefOrValue<SemanticTone>
|
|
239
|
+
readonly variant?: RefOrValue<ComponentVariant>
|
|
240
|
+
readonly model: Ref<string>
|
|
241
|
+
readonly selectedValue: Ref<AutoCompleteValue | null>
|
|
242
|
+
readonly type?: RefOrValue<FormInputFieldType>
|
|
243
|
+
readonly name?: RefOrValue<string>
|
|
244
|
+
readonly required?: RefOrValue<boolean>
|
|
245
|
+
readonly autocomplete?: RefOrValue<string>
|
|
246
|
+
readonly placeholder?: RefOrValue<string>
|
|
247
|
+
readonly disabled?: RefOrValue<boolean>
|
|
248
|
+
readonly icon?: RefOrValue<string>
|
|
249
|
+
readonly iconEnd?: RefOrValue<string>
|
|
250
|
+
readonly options?: RefOrValue<AutoCompleteOption[]>
|
|
251
|
+
readonly rowComponent?: RefOrValue<string>
|
|
252
|
+
readonly minLength?: RefOrValue<number | string>
|
|
253
|
+
readonly maxResults?: RefOrValue<number | string>
|
|
254
|
+
readonly openOnFocus?: RefOrValue<boolean>
|
|
255
|
+
readonly loading?: RefOrValue<boolean>
|
|
256
|
+
readonly emptyText?: RefOrValue<string>
|
|
257
|
+
readonly loadingText?: RefOrValue<string>
|
|
258
|
+
readonly onSelect?: (option: ResolvedAutoCompleteOption) => void
|
|
259
|
+
readonly onSearch?: (query: string) => void
|
|
260
|
+
readonly emits?: Emits<'optionselect' | 'querychange'>
|
|
261
|
+
readonly listboxId: string
|
|
262
|
+
readonly isOpen = ref(false)
|
|
263
|
+
readonly activeIndex = ref(-1)
|
|
264
|
+
readonly query: ComputedRef<string>
|
|
265
|
+
readonly resolvedOptions: ComputedRef<ResolvedAutoCompleteOption[]>
|
|
266
|
+
readonly filteredOptions: ComputedRef<ResolvedAutoCompleteOption[]>
|
|
267
|
+
readonly rows: ComputedRef<AutoCompleteRow[]>
|
|
268
|
+
readonly activeOptionId: ComputedRef<string | undefined>
|
|
269
|
+
readonly shouldRenderPopup: ComputedRef<boolean>
|
|
270
|
+
readonly showLoading: ComputedRef<boolean>
|
|
271
|
+
readonly showEmpty: ComputedRef<boolean>
|
|
272
|
+
readonly resolvedRowComponent: ComputedRef<string>
|
|
273
|
+
readonly resolvedIconEnd: ComputedRef<string | undefined>
|
|
274
|
+
private readonly emit: ComponentHead<AutoCompleteInput>['emit']
|
|
275
|
+
private closingTimer: ReturnType<typeof setTimeout> | undefined
|
|
276
|
+
|
|
277
|
+
constructor(head: ComponentHead<AutoCompleteInput>) {
|
|
278
|
+
const props = head.props
|
|
279
|
+
Object.assign(this, props)
|
|
280
|
+
this.emit = head.emit
|
|
281
|
+
this.id = resolveAutoCompleteInputId(props.id)
|
|
282
|
+
this.listboxId = `${this.id}-listbox`
|
|
283
|
+
this.model = isRef(props.model) ? props.model : ref('')
|
|
284
|
+
this.selectedValue = isRef(props.selectedValue)
|
|
285
|
+
? props.selectedValue
|
|
286
|
+
: ref<AutoCompleteValue | null>(null)
|
|
287
|
+
this.query = computed(() => String(unref(this.model) ?? ''))
|
|
288
|
+
this.resolvedOptions = computed(() => {
|
|
289
|
+
const options = unref(props.options)
|
|
290
|
+
return Array.isArray(options)
|
|
291
|
+
? options.map(resolveAutoCompleteOption)
|
|
292
|
+
: []
|
|
293
|
+
})
|
|
294
|
+
this.filteredOptions = computed(() => this.resolveFilteredOptions())
|
|
295
|
+
this.rows = computed(() => this.resolveRows())
|
|
296
|
+
this.activeOptionId = computed(() => this.rows()[this.activeIndex()]?.id)
|
|
297
|
+
this.showLoading = computed(() => this.isOpen() && !!unref(this.loading))
|
|
298
|
+
this.showEmpty = computed(
|
|
299
|
+
() =>
|
|
300
|
+
this.isOpen() &&
|
|
301
|
+
!this.showLoading() &&
|
|
302
|
+
this.meetsMinimumQueryLength() &&
|
|
303
|
+
this.filteredOptions().length === 0,
|
|
304
|
+
)
|
|
305
|
+
this.shouldRenderPopup = computed(
|
|
306
|
+
() =>
|
|
307
|
+
this.isOpen() &&
|
|
308
|
+
(this.showLoading() ||
|
|
309
|
+
this.showEmpty() ||
|
|
310
|
+
this.filteredOptions().length > 0),
|
|
311
|
+
)
|
|
312
|
+
this.resolvedRowComponent = computed(() => {
|
|
313
|
+
const component = unref(props.rowComponent)
|
|
314
|
+
return typeof component === 'string' && component.trim()
|
|
315
|
+
? component
|
|
316
|
+
: 'AutoCompleteOptionRow'
|
|
317
|
+
})
|
|
318
|
+
this.resolvedIconEnd = computed(
|
|
319
|
+
() =>
|
|
320
|
+
unref(props.iconEnd) ||
|
|
321
|
+
(unref(props.loading) ? 'lucide:loader-circle' : 'lucide:chevron-down'),
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
handleFocusIn = () => {
|
|
326
|
+
this.clearClosingTimer()
|
|
327
|
+
if (!this.shouldOpenOnFocus()) return
|
|
328
|
+
|
|
329
|
+
this.open()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
handleFocusOut = () => {
|
|
333
|
+
this.clearClosingTimer()
|
|
334
|
+
this.closingTimer = setTimeout(() => {
|
|
335
|
+
this.close()
|
|
336
|
+
}, 0)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
handleInput = () => {
|
|
340
|
+
const query = this.query()
|
|
341
|
+
this.onSearch?.(query)
|
|
342
|
+
this.emit('querychange', { query })
|
|
343
|
+
this.selectedValue(null)
|
|
344
|
+
if (this.meetsMinimumQueryLength()) this.open()
|
|
345
|
+
else this.close()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
handleKeydown = (event: KeyboardEvent) => {
|
|
349
|
+
if (unref(this.disabled)) return
|
|
350
|
+
|
|
351
|
+
if (event.key === 'ArrowDown') {
|
|
352
|
+
event.preventDefault()
|
|
353
|
+
if (!this.isOpen()) {
|
|
354
|
+
this.open()
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
this.open()
|
|
358
|
+
this.moveActive(1)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
if (event.key === 'ArrowUp') {
|
|
362
|
+
event.preventDefault()
|
|
363
|
+
if (!this.isOpen()) {
|
|
364
|
+
this.open()
|
|
365
|
+
this.activateLast()
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
this.open()
|
|
369
|
+
this.moveActive(-1)
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
if (event.key === 'Home' && this.isOpen()) {
|
|
373
|
+
event.preventDefault()
|
|
374
|
+
this.activateFirst()
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
if (event.key === 'End' && this.isOpen()) {
|
|
378
|
+
event.preventDefault()
|
|
379
|
+
this.activateLast()
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
if (event.key === 'Enter' && this.isOpen()) {
|
|
383
|
+
const row = this.rows()[this.activeIndex()]
|
|
384
|
+
if (!row) return
|
|
385
|
+
|
|
386
|
+
event.preventDefault()
|
|
387
|
+
this.selectRow(row)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
if (event.key === 'Escape') {
|
|
391
|
+
this.close()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
handleOptionMouseDown = (event: MouseEvent) => {
|
|
396
|
+
event.preventDefault()
|
|
397
|
+
this.clearClosingTimer()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
selectRow = (row: AutoCompleteRow) => {
|
|
401
|
+
if (row.disabled || unref(this.disabled)) return
|
|
402
|
+
|
|
403
|
+
const option = row.option
|
|
404
|
+
this.model(option.label)
|
|
405
|
+
this.selectedValue(option.value)
|
|
406
|
+
this.onSelect?.(option)
|
|
407
|
+
this.emit('optionselect', { option })
|
|
408
|
+
this.close()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private resolveFilteredOptions() {
|
|
412
|
+
const query = normalizeSearchText(this.query())
|
|
413
|
+
if (!this.meetsMinimumQueryLength()) return []
|
|
414
|
+
|
|
415
|
+
const options = this.resolvedOptions().filter(
|
|
416
|
+
(option) =>
|
|
417
|
+
!query ||
|
|
418
|
+
option.keywords.some((keyword) =>
|
|
419
|
+
normalizeSearchText(keyword).includes(query),
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
return options.slice(0, this.readPositiveInteger(this.maxResults, 10))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private resolveRows() {
|
|
426
|
+
const activeIndex = this.activeIndex()
|
|
427
|
+
const selectedValue = this.selectedValue()
|
|
428
|
+
return this.filteredOptions().map(
|
|
429
|
+
(option, index): AutoCompleteRow => ({
|
|
430
|
+
id: `${this.listboxId}-option-${index}`,
|
|
431
|
+
index,
|
|
432
|
+
option,
|
|
433
|
+
active: index === activeIndex,
|
|
434
|
+
selected: selectedValue !== null && option.value === selectedValue,
|
|
435
|
+
disabled: option.disabled,
|
|
436
|
+
classes: [
|
|
437
|
+
index === activeIndex ? 'auto-complete-input__option--active' : '',
|
|
438
|
+
selectedValue !== null && option.value === selectedValue
|
|
439
|
+
? 'auto-complete-input__option--selected'
|
|
440
|
+
: '',
|
|
441
|
+
option.disabled ? 'auto-complete-input__option--disabled' : '',
|
|
442
|
+
]
|
|
443
|
+
.filter(Boolean)
|
|
444
|
+
.join(' '),
|
|
445
|
+
}),
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private open() {
|
|
450
|
+
if (unref(this.disabled) || !this.meetsMinimumQueryLength()) return
|
|
451
|
+
|
|
452
|
+
this.isOpen(true)
|
|
453
|
+
this.ensureActiveIndex()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private close() {
|
|
457
|
+
this.isOpen(false)
|
|
458
|
+
this.activeIndex(-1)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private moveActive(direction: 1 | -1) {
|
|
462
|
+
const rows = this.rows()
|
|
463
|
+
if (rows.length === 0) {
|
|
464
|
+
this.activeIndex(-1)
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const current = this.activeIndex()
|
|
469
|
+
for (let offset = 1; offset <= rows.length; offset += 1) {
|
|
470
|
+
const next = wrapIndex(current + direction * offset, rows.length)
|
|
471
|
+
if (!rows[next]?.disabled) {
|
|
472
|
+
this.activeIndex(next)
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private activateFirst() {
|
|
479
|
+
const index = this.rows().findIndex((row) => !row.disabled)
|
|
480
|
+
this.activeIndex(index)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private activateLast() {
|
|
484
|
+
const rows = this.rows()
|
|
485
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
486
|
+
if (rows[index]?.disabled) continue
|
|
487
|
+
|
|
488
|
+
this.activeIndex(index)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
this.activeIndex(-1)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private ensureActiveIndex() {
|
|
495
|
+
const rows = this.rows()
|
|
496
|
+
const current = this.activeIndex()
|
|
497
|
+
if (current >= 0 && current < rows.length && !rows[current]?.disabled)
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
this.activateFirst()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private shouldOpenOnFocus() {
|
|
504
|
+
return unref(this.openOnFocus) !== false && this.meetsMinimumQueryLength()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private meetsMinimumQueryLength() {
|
|
508
|
+
return (
|
|
509
|
+
this.query().trim().length >= this.readPositiveInteger(this.minLength, 0)
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private readPositiveInteger(
|
|
514
|
+
value: RefOrValue<number | string> | undefined,
|
|
515
|
+
fallback: number,
|
|
516
|
+
) {
|
|
517
|
+
const raw = unref(value)
|
|
518
|
+
const parsed =
|
|
519
|
+
typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
|
|
520
|
+
return Number.isFinite(parsed) && parsed >= 0
|
|
521
|
+
? Math.trunc(parsed)
|
|
522
|
+
: fallback
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private clearClosingTimer() {
|
|
526
|
+
if (this.closingTimer === undefined) return
|
|
527
|
+
|
|
528
|
+
clearTimeout(this.closingTimer)
|
|
529
|
+
this.closingTimer = undefined
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function resolveAutoCompleteOption(
|
|
534
|
+
optionInput: RefOrValue<AutoCompleteOption>,
|
|
535
|
+
): ResolvedAutoCompleteOption {
|
|
536
|
+
const option = unref(optionInput)
|
|
537
|
+
const label = String(unref(option.label) ?? '')
|
|
538
|
+
const value = unref(option.value) ?? label
|
|
539
|
+
return {
|
|
540
|
+
label,
|
|
541
|
+
value,
|
|
542
|
+
disabled: unref(option.disabled),
|
|
543
|
+
keywords: resolveKeywords(option, label, value),
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function resolveKeywords(
|
|
548
|
+
option: AutoCompleteOption,
|
|
549
|
+
label: string,
|
|
550
|
+
value: AutoCompleteValue,
|
|
551
|
+
) {
|
|
552
|
+
const keywords = unref(option.keywords)
|
|
553
|
+
const source: string[] = Array.isArray(keywords)
|
|
554
|
+
? keywords.map((keyword) => String(unref(keyword) ?? ''))
|
|
555
|
+
: typeof keywords === 'string'
|
|
556
|
+
? keywords.split(/\s+/)
|
|
557
|
+
: []
|
|
558
|
+
return [label, String(value), ...source].filter(Boolean)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function normalizeSearchText(value: string) {
|
|
562
|
+
return value.trim().toLocaleLowerCase()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function wrapIndex(index: number, length: number) {
|
|
566
|
+
return ((index % length) + length) % length
|
|
567
|
+
}
|