wave-ui 3.27.2 → 4.0.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/dist/types/types/$waveui.d.ts +21 -1
- package/dist/types/types/colors.d.ts +2 -0
- package/dist/types/types/components/WAccordion.d.ts +99 -6
- package/dist/types/types/components/WAutocomplete.d.ts +437 -0
- package/dist/types/types/components/WBreadcrumbs.d.ts +7 -0
- package/dist/types/types/components/WButton.d.ts +7 -0
- package/dist/types/types/components/WCheckbox.d.ts +34 -0
- package/dist/types/types/components/WCheckboxes.d.ts +30 -0
- package/dist/types/types/components/WInput.d.ts +34 -0
- package/dist/types/types/components/WList.d.ts +7 -0
- package/dist/types/types/components/WMenu.d.ts +12 -6
- package/dist/types/types/components/WRadio.d.ts +34 -0
- package/dist/types/types/components/WRadios.d.ts +30 -0
- package/dist/types/types/components/WScrollable.d.ts +143 -0
- package/dist/types/types/components/WScrollable.js +2 -0
- package/dist/types/types/components/WSelect.d.ts +34 -0
- package/dist/types/types/components/WSwitch.d.ts +34 -0
- package/dist/types/types/components/WTable.d.ts +7 -0
- package/dist/types/types/components/WTabs.d.ts +7 -0
- package/dist/types/types/components/WTag.d.ts +7 -0
- package/dist/types/types/components/WTooltip.d.ts +20 -7
- package/dist/types/types/components/WTransitions.d.ts +104 -0
- package/dist/types/types/components/WTransitions.js +2 -0
- package/dist/types/types/components/WTree.d.ts +7 -0
- package/dist/types/types/components/index.d.ts +3 -1
- package/dist/wave-ui.cjs.js +3 -3
- package/dist/wave-ui.css +1 -1
- package/dist/wave-ui.esm.js +2190 -1350
- package/dist/wave-ui.umd.js +3 -3
- package/package.json +6 -6
- package/src/wave-ui/components/index.js +0 -1
- package/src/wave-ui/components/transitions/w-transition-bounce.vue +2 -1
- package/src/wave-ui/components/transitions/w-transition-expand.vue +3 -2
- package/src/wave-ui/components/transitions/w-transition-fade.vue +2 -1
- package/src/wave-ui/components/transitions/w-transition-scale-fade.vue +2 -1
- package/src/wave-ui/components/transitions/w-transition-scale.vue +2 -1
- package/src/wave-ui/components/transitions/w-transition-slide-fade.vue +2 -1
- package/src/wave-ui/components/transitions/w-transition-slide.vue +2 -1
- package/src/wave-ui/components/transitions/w-transition-twist.vue +2 -1
- package/src/wave-ui/components/w-accordion/index.vue +15 -6
- package/src/wave-ui/components/w-accordion/item.vue +71 -26
- package/src/wave-ui/components/w-alert.vue +27 -29
- package/src/wave-ui/components/w-autocomplete.vue +626 -192
- package/src/wave-ui/components/w-badge.vue +54 -53
- package/src/wave-ui/components/w-breadcrumbs.vue +20 -11
- package/src/wave-ui/components/w-button/button.vue +36 -24
- package/src/wave-ui/components/w-button/index.vue +6 -5
- package/src/wave-ui/components/w-card.vue +8 -7
- package/src/wave-ui/components/w-checkbox.vue +31 -11
- package/src/wave-ui/components/w-checkboxes.vue +21 -3
- package/src/wave-ui/components/w-confirm.vue +22 -22
- package/src/wave-ui/components/w-dialog.vue +1 -1
- package/src/wave-ui/components/w-divider.vue +5 -5
- package/src/wave-ui/components/w-drawer.vue +3 -3
- package/src/wave-ui/components/w-form-element.vue +2 -2
- package/src/wave-ui/components/w-icon.vue +12 -14
- package/src/wave-ui/components/w-image.vue +1 -1
- package/src/wave-ui/components/w-input.vue +43 -25
- package/src/wave-ui/components/w-list.vue +23 -12
- package/src/wave-ui/components/w-menu.vue +57 -55
- package/src/wave-ui/components/w-notification.vue +4 -4
- package/src/wave-ui/components/w-progress.vue +6 -7
- package/src/wave-ui/components/w-radio.vue +32 -7
- package/src/wave-ui/components/w-radios.vue +28 -3
- package/src/wave-ui/components/w-rating.vue +7 -9
- package/src/wave-ui/components/w-scrollable.vue +670 -97
- package/src/wave-ui/components/w-select.vue +119 -101
- package/src/wave-ui/components/w-slider.vue +26 -26
- package/src/wave-ui/components/w-spinner.vue +5 -7
- package/src/wave-ui/components/w-switch.vue +71 -47
- package/src/wave-ui/components/w-table.vue +69 -36
- package/src/wave-ui/components/w-tabs/index.vue +31 -24
- package/src/wave-ui/components/w-tag.vue +49 -38
- package/src/wave-ui/components/w-textarea.vue +22 -22
- package/src/wave-ui/components/w-timeline.vue +6 -6
- package/src/wave-ui/components/w-toolbar.vue +8 -8
- package/src/wave-ui/components/w-tooltip.vue +30 -25
- package/src/wave-ui/components/w-tree.vue +35 -16
- package/src/wave-ui/core.js +11 -1
- package/src/wave-ui/mixins/detachable.js +98 -43
- package/src/wave-ui/mixins/ripple.js +39 -0
- package/src/wave-ui/scss/_base.scss +82 -17
- package/src/wave-ui/scss/_colors.scss +6 -75
- package/src/wave-ui/scss/_layout.scss +39 -47
- package/src/wave-ui/scss/_ripple.scss +37 -0
- package/src/wave-ui/scss/_transitions.scss +19 -19
- package/src/wave-ui/scss/_typography.scss +8 -9
- package/src/wave-ui/scss/index.scss +1 -0
- package/src/wave-ui/scss/variables/_mixins.scss +24 -25
- package/src/wave-ui/scss/variables/_variables.scss +4 -151
- package/src/wave-ui/utils/colors.js +7 -4
- package/src/wave-ui/utils/config.js +5 -4
- package/src/wave-ui/utils/dynamic-css.js +42 -20
- package/src/wave-ui/utils/ripple.js +72 -0
- package/src/wave-ui/utils/wave-ripple-directive.js +40 -0
- package/dist/types/types/components/WApp.d.ts +0 -83
- package/src/wave-ui/components/w-app.vue +0 -24
- /package/dist/types/types/components/{WApp.js → WAutocomplete.js} +0 -0
|
@@ -1,100 +1,196 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
2
|
+
component(
|
|
3
|
+
ref="formEl"
|
|
4
|
+
:is="formRegister ? 'w-form-element' : 'div'"
|
|
5
|
+
v-bind="formRegister && { validators, inputValue: selectionValue, disabled: isDisabled, readonly: isReadonly, isFocused, noBlurValidation }"
|
|
6
|
+
v-model:valid="valid"
|
|
7
|
+
@reset="onReset"
|
|
8
|
+
:wrap="hasLabel && labelPosition !== 'inside'"
|
|
9
|
+
:class="classes"
|
|
10
|
+
:style="$attrs.style")
|
|
11
|
+
template(v-if="labelPosition === 'left'")
|
|
12
|
+
label.w-autocomplete__label.w-autocomplete__label--left.w-form-el-shakable(
|
|
13
|
+
v-if="$slots.default || label"
|
|
14
|
+
:for="inputId"
|
|
15
|
+
:class="labelClasses")
|
|
16
|
+
slot {{ label }}
|
|
17
|
+
|
|
18
|
+
w-menu(
|
|
19
|
+
ref="menu"
|
|
20
|
+
v-model="menuOpen"
|
|
21
|
+
@close="onMenuClose"
|
|
22
|
+
:menu-class="`w-autocomplete__menu${menuClass ? ' ' + menuClass : ''}`"
|
|
23
|
+
transition="slide-fade-down"
|
|
24
|
+
:append-to="menuPropsComputed.appendTo"
|
|
25
|
+
align-left
|
|
26
|
+
custom
|
|
27
|
+
min-width="activator"
|
|
28
|
+
v-bind="menuPropsComputed")
|
|
29
|
+
.w-autocomplete__input-wrap(
|
|
30
|
+
@click="!isDisabled && !isReadonly && onWrapClick()"
|
|
31
|
+
:class="inputWrapClasses")
|
|
32
|
+
slot(name="icon-left")
|
|
33
|
+
w-icon.w-autocomplete__icon.w-autocomplete__icon--inner-left(
|
|
34
|
+
v-if="innerIconLeft"
|
|
35
|
+
tag="label"
|
|
36
|
+
:for="inputId"
|
|
37
|
+
@click.stop="$emit('click:inner-icon-left', $event)") {{ innerIconLeft }}
|
|
38
|
+
|
|
39
|
+
template(v-if="selection.length")
|
|
40
|
+
.w-autocomplete__selection(v-for="(item, i) in selection" :key="item.uid")
|
|
41
|
+
slot(name="selection" :item="item" :unselect="() => unselectItem(i)")
|
|
42
|
+
span(v-html="item[itemLabelKey]")
|
|
43
|
+
w-button(@click.stop="unselectItem(i)" icon="wi-cross" xs text color="currentColor")
|
|
44
|
+
|
|
45
|
+
.w-autocomplete__placeholder(
|
|
46
|
+
v-if="!selection.length && !keywords && placeholder && !showFloatingLabel"
|
|
47
|
+
v-html="placeholder")
|
|
48
|
+
|
|
49
|
+
input.w-autocomplete__input(
|
|
50
|
+
ref="input"
|
|
51
|
+
:id="inputId"
|
|
52
|
+
:value="keywords"
|
|
53
|
+
:name="inputName"
|
|
54
|
+
:disabled="isDisabled || null"
|
|
55
|
+
:readonly="isReadonly || null"
|
|
56
|
+
:tabindex="tabindex || null"
|
|
57
|
+
v-on="inputEventListeners"
|
|
58
|
+
v-bind="inputAttrs")
|
|
59
|
+
|
|
60
|
+
template(v-if="labelPosition === 'inside' && showLabelInside")
|
|
61
|
+
label.w-autocomplete__label.w-autocomplete__label--inside.w-form-el-shakable(
|
|
62
|
+
v-if="$slots.default || label"
|
|
63
|
+
:for="inputId"
|
|
64
|
+
:class="labelClasses")
|
|
65
|
+
slot {{ label }}
|
|
66
|
+
|
|
67
|
+
slot(name="icon-right")
|
|
68
|
+
w-icon.w-autocomplete__icon.w-autocomplete__icon--inner-right(
|
|
69
|
+
v-if="innerIconRight"
|
|
70
|
+
tag="label"
|
|
71
|
+
:for="inputId"
|
|
72
|
+
@click.stop="$emit('click:inner-icon-right', $event)") {{ innerIconRight }}
|
|
73
|
+
|
|
74
|
+
template(#content)
|
|
75
|
+
.w-autocomplete__list-wrap(
|
|
76
|
+
ref="listWrap"
|
|
77
|
+
@mousedown.capture="menuIsBeingClicked = true"
|
|
78
|
+
@mouseup.capture="setEndOfMenuClick"
|
|
79
|
+
@touchstart.capture="menuIsBeingClicked = true"
|
|
80
|
+
@touchend.capture="setEndOfMenuClick")
|
|
81
|
+
w-list.w-autocomplete__list(
|
|
82
|
+
v-if="filteredItems.length"
|
|
83
|
+
ref="list"
|
|
84
|
+
:items="listItems"
|
|
85
|
+
:model-value="null"
|
|
86
|
+
:item-label-key="itemLabelKey"
|
|
87
|
+
:item-value-key="itemValueKey"
|
|
88
|
+
:color="color"
|
|
89
|
+
:selection-color="color"
|
|
90
|
+
@item-select="onListItemSelect")
|
|
91
|
+
template(#item="{ item }")
|
|
92
|
+
slot(name="item" :item="item" :highlighted="highlightedItem === item.uid")
|
|
93
|
+
span(v-html="item[itemLabelKey]")
|
|
94
|
+
.w-autocomplete__no-match(
|
|
95
|
+
v-if="!filteredItems.length"
|
|
96
|
+
:class="{ 'w-autocomplete__no-match--default': !$slots['no-match'] }")
|
|
97
|
+
slot(name="no-match")
|
|
98
|
+
.caption(v-html="noMatch ?? 'No match.'")
|
|
99
|
+
.w-autocomplete__extra-item(
|
|
100
|
+
v-if="$slots['extra-item']"
|
|
101
|
+
@click.stop="selectExtraItem"
|
|
102
|
+
:class="{ highlighted: highlightedItem === 'extra-item' }")
|
|
103
|
+
slot(name="extra-item")
|
|
104
|
+
|
|
105
|
+
template(v-if="labelPosition === 'right'")
|
|
106
|
+
label.w-autocomplete__label.w-autocomplete__label--right.w-form-el-shakable(
|
|
107
|
+
v-if="$slots.default || label"
|
|
108
|
+
:for="inputId"
|
|
109
|
+
:class="labelClasses")
|
|
110
|
+
slot {{ label }}
|
|
41
111
|
</template>
|
|
42
112
|
|
|
43
113
|
<script>
|
|
114
|
+
import { computed } from 'vue'
|
|
115
|
+
import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
|
|
116
|
+
|
|
44
117
|
export default {
|
|
45
118
|
name: 'w-autocomplete',
|
|
46
|
-
|
|
119
|
+
mixins: [FormElementMixin],
|
|
120
|
+
inheritAttrs: false,
|
|
121
|
+
|
|
122
|
+
setup () {
|
|
123
|
+
return useWaveUiFormIds()
|
|
124
|
+
},
|
|
47
125
|
|
|
48
126
|
props: {
|
|
49
127
|
items: { type: Array, required: true },
|
|
50
|
-
modelValue: { type: [String, Number, Array] },
|
|
128
|
+
modelValue: { type: [String, Number, Array] },
|
|
51
129
|
placeholder: { type: String },
|
|
52
|
-
|
|
130
|
+
label: { type: String },
|
|
131
|
+
labelPosition: { type: String, default: 'inside' },
|
|
132
|
+
staticLabel: { type: Boolean },
|
|
133
|
+
innerIconLeft: { type: String },
|
|
134
|
+
innerIconRight: { type: String },
|
|
135
|
+
openOnKeydown: { type: Boolean },
|
|
53
136
|
multiple: { type: Boolean },
|
|
54
|
-
// When multiple is on, prevents duplicate items selections by default, unless this is set to true.
|
|
55
137
|
allowDuplicates: { type: Boolean },
|
|
56
138
|
noMatch: { type: String },
|
|
57
|
-
// Contains the unique selection value for each item.
|
|
58
|
-
// Can be a numeric ID, a slug, etc. (outside of Wave UI)
|
|
59
139
|
itemValueKey: { type: String, default: 'value' },
|
|
60
|
-
// Contains the string to display for each item.
|
|
61
140
|
itemLabelKey: { type: String, default: 'label' },
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
141
|
+
itemSearchableKey: { type: String, default: 'searchable' },
|
|
142
|
+
color: { type: String, default: 'primary' },
|
|
143
|
+
bgColor: { type: String },
|
|
144
|
+
labelColor: { type: String, default: 'primary' },
|
|
145
|
+
outline: { type: Boolean },
|
|
146
|
+
round: { type: Boolean },
|
|
147
|
+
shadow: { type: Boolean },
|
|
148
|
+
tile: { type: Boolean },
|
|
149
|
+
xs: { type: Boolean },
|
|
150
|
+
sm: { type: Boolean },
|
|
151
|
+
md: { type: Boolean },
|
|
152
|
+
lg: { type: Boolean },
|
|
153
|
+
xl: { type: Boolean },
|
|
154
|
+
dark: { type: Boolean },
|
|
155
|
+
light: { type: Boolean },
|
|
156
|
+
fitToContent: { type: Boolean },
|
|
157
|
+
menuClass: { type: String },
|
|
158
|
+
menuProps: { type: Object }
|
|
159
|
+
// From mixin: id, name, disabled, readonly, required, tabindex, validators, noBlurValidation.
|
|
160
|
+
// Computed from mixin: inputId, inputName, isDisabled, isReadonly, labelClasses.
|
|
65
161
|
},
|
|
66
162
|
|
|
67
|
-
|
|
68
|
-
|
|
163
|
+
emits: [
|
|
164
|
+
'update:modelValue', 'input', 'focus', 'blur', 'keydown',
|
|
165
|
+
'item-click', 'item-select', 'extra-item-select',
|
|
166
|
+
'click:inner-icon-left', 'click:inner-icon-right'
|
|
167
|
+
],
|
|
69
168
|
|
|
70
169
|
data: () => ({
|
|
71
170
|
keywords: '',
|
|
72
171
|
selection: [],
|
|
73
172
|
menuOpen: false,
|
|
74
173
|
highlightedItem: null,
|
|
75
|
-
|
|
76
|
-
// item clicking. So keep the focus on as long as the user is interacting with the autocomplete.
|
|
174
|
+
isFocused: false,
|
|
77
175
|
menuIsBeingClicked: false
|
|
78
176
|
}),
|
|
79
177
|
|
|
80
178
|
computed: {
|
|
81
|
-
// Keep the autocomplete matching as fast as possible by caching optimized search strings.
|
|
82
179
|
normalizedKeywords () {
|
|
83
180
|
return this.normalize(this.keywords)
|
|
84
181
|
},
|
|
85
182
|
|
|
86
|
-
// Keep the autocomplete matching as fast as possible by caching optimized search strings.
|
|
87
183
|
optimizedItemsForSearch () {
|
|
88
184
|
return this.items.map((item, i) => ({
|
|
89
185
|
...item,
|
|
90
186
|
uid: i,
|
|
91
|
-
searchable: this.normalize(item[this.itemSearchableKey] || '')
|
|
187
|
+
searchable: this.normalize(item[this.itemSearchableKey] || item[this.itemLabelKey] || '')
|
|
92
188
|
}))
|
|
93
189
|
},
|
|
94
190
|
|
|
95
191
|
filteredItems () {
|
|
96
|
-
let items = this.optimizedItemsForSearch
|
|
97
|
-
const isItemNotSelected = item => !this.selection.find(
|
|
192
|
+
let items = this.optimizedItemsForSearch
|
|
193
|
+
const isItemNotSelected = item => !this.selection.find(s => s.uid === item.uid)
|
|
98
194
|
|
|
99
195
|
if (this.keywords) {
|
|
100
196
|
items = items.filter(item => {
|
|
@@ -103,7 +199,6 @@ export default {
|
|
|
103
199
|
return true
|
|
104
200
|
})
|
|
105
201
|
}
|
|
106
|
-
|
|
107
202
|
else if (this.multiple && !this.allowDuplicates) items = items.filter(isItemNotSelected)
|
|
108
203
|
|
|
109
204
|
return items
|
|
@@ -112,20 +207,89 @@ export default {
|
|
|
112
207
|
highlightedItemIndex () {
|
|
113
208
|
if (this.highlightedItem === null) return -1
|
|
114
209
|
if (this.highlightedItem === 'extra-item') return this.filteredItems.length
|
|
115
|
-
|
|
116
210
|
return this.filteredItems.findIndex(item => item.uid === this.highlightedItem)
|
|
117
211
|
},
|
|
118
212
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
213
|
+
// filteredItems enriched with a per-item `class` property for the highlighted state.
|
|
214
|
+
// w-list reads item[itemClassKey] (default: 'class') and applies it to the item label div.
|
|
215
|
+
listItems () {
|
|
216
|
+
return this.filteredItems.map(item => ({
|
|
217
|
+
...item,
|
|
218
|
+
class: this.highlightedItem === item.uid ? 'highlighted' : undefined
|
|
219
|
+
}))
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
hasValue () {
|
|
223
|
+
return this.selection.length > 0 || !!this.keywords
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
hasLabel () {
|
|
227
|
+
return !!(this.label || this.$slots.default)
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
showFloatingLabel () {
|
|
231
|
+
return this.hasLabel && this.labelPosition === 'inside' && !this.staticLabel
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
showLabelInside () {
|
|
235
|
+
return !this.staticLabel || (!this.hasValue && !this.placeholder)
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
selectionValue () {
|
|
239
|
+
if (!this.selection.length) return null
|
|
240
|
+
if (this.multiple) return this.selection.map(item => item[this.itemValueKey])
|
|
241
|
+
return this.selection[0][this.itemValueKey]
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
presetSize () {
|
|
245
|
+
return (this.xs && 'xs') || (this.sm && 'sm') || (this.md && 'md') || (this.lg && 'lg') || (this.xl && 'xl') || null
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
menuPropsComputed () {
|
|
249
|
+
const { appendTo, ...rest } = this.menuProps || {}
|
|
250
|
+
return { appendTo, ...rest }
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
classes () {
|
|
254
|
+
return {
|
|
255
|
+
'w-autocomplete': true,
|
|
256
|
+
'w-autocomplete--dark': this.dark,
|
|
257
|
+
'w-autocomplete--light': this.light,
|
|
258
|
+
'w-autocomplete--disabled': this.isDisabled,
|
|
259
|
+
'w-autocomplete--readonly': this.isReadonly,
|
|
260
|
+
'w-autocomplete--fit-to-content': this.fitToContent,
|
|
261
|
+
[`w-autocomplete--${this.hasValue ? 'filled' : 'empty'}`]: true,
|
|
262
|
+
'w-autocomplete--focused': (this.isFocused || this.menuOpen) && !this.isReadonly,
|
|
263
|
+
'w-autocomplete--floating-label': this.showFloatingLabel,
|
|
264
|
+
'w-autocomplete--no-padding': !this.outline && !this.bgColor && !this.shadow && !this.round,
|
|
265
|
+
'w-autocomplete--has-placeholder': !!this.placeholder,
|
|
266
|
+
'w-autocomplete--inner-icon-left': !!this.innerIconLeft,
|
|
267
|
+
'w-autocomplete--inner-icon-right': !!this.innerIconRight,
|
|
268
|
+
'w-autocomplete--open': this.menuOpen,
|
|
269
|
+
'w-autocomplete--multiple': this.multiple,
|
|
270
|
+
[`size--${this.presetSize}`]: !!this.presetSize,
|
|
271
|
+
[this.$attrs.class]: !!this.$attrs.class
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
inputWrapClasses () {
|
|
276
|
+
return {
|
|
277
|
+
[this.valid === false ? this.validationColor : this.color]: this.color || this.valid === false,
|
|
278
|
+
[`${this.bgColor}--bg`]: this.bgColor,
|
|
279
|
+
'w-autocomplete__input-wrap--round': this.round,
|
|
280
|
+
'w-autocomplete__input-wrap--tile': this.tile,
|
|
281
|
+
'w-autocomplete__input-wrap--box': this.outline || this.bgColor || this.shadow,
|
|
282
|
+
'w-autocomplete__input-wrap--underline': !this.outline,
|
|
283
|
+
'w-autocomplete__input-wrap--shadow': this.shadow,
|
|
284
|
+
'w-autocomplete__input-wrap--no-padding': !this.outline && !this.bgColor && !this.shadow && !this.round,
|
|
285
|
+
'w-autocomplete__input-wrap--inner-icon-left': !!this.innerIconLeft,
|
|
286
|
+
'w-autocomplete__input-wrap--inner-icon-right': !!this.innerIconRight
|
|
287
|
+
}
|
|
122
288
|
},
|
|
123
289
|
|
|
124
290
|
inputAttrs () {
|
|
125
|
-
// Remove class and style which are meant to stay on the wrapper.
|
|
126
291
|
// eslint-disable-next-line no-unused-vars
|
|
127
292
|
const { style, class: classes, ...attrs } = this.$attrs
|
|
128
|
-
|
|
129
293
|
return attrs
|
|
130
294
|
},
|
|
131
295
|
|
|
@@ -136,11 +300,14 @@ export default {
|
|
|
136
300
|
},
|
|
137
301
|
focus: e => {
|
|
138
302
|
if (this.menuIsBeingClicked) return
|
|
139
|
-
this.
|
|
303
|
+
this.isFocused = true
|
|
304
|
+
if (!this.openOnKeydown) this.openMenu()
|
|
140
305
|
this.$emit('focus', e)
|
|
141
306
|
},
|
|
142
307
|
blur: e => {
|
|
143
|
-
if (
|
|
308
|
+
if (this.menuIsBeingClicked) return
|
|
309
|
+
this.isFocused = false
|
|
310
|
+
this.$emit('blur', e)
|
|
144
311
|
},
|
|
145
312
|
keydown: e => {
|
|
146
313
|
this.onKeydown(e)
|
|
@@ -150,47 +317,44 @@ export default {
|
|
|
150
317
|
compositionstart: this.onCompositionStart,
|
|
151
318
|
compositionupdate: this.onCompositionUpdate
|
|
152
319
|
}
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
classes () {
|
|
156
|
-
return {
|
|
157
|
-
'w-autocomplete--open': this.menuOpen,
|
|
158
|
-
'w-autocomplete--filled': this.selection.length,
|
|
159
|
-
'w-autocomplete--has-keywords': this.keywords,
|
|
160
|
-
'w-autocomplete--empty': !this.selection.length && !this.keywords,
|
|
161
|
-
// With the inheritAttrs set to false any class on the component would be lost, so add it back.
|
|
162
|
-
[this.$attrs.class]: !!this.$attrs.class
|
|
163
|
-
}
|
|
164
320
|
}
|
|
165
321
|
},
|
|
166
322
|
|
|
167
323
|
methods: {
|
|
168
|
-
// Replace all the accents and non-latin characters with equivalent letters. E.g. é -> e.
|
|
169
324
|
normalize (string) {
|
|
170
|
-
return string.toLowerCase().normalize('NFKD').replace(/\p{Diacritic}/gu, '').replace(/œ/g, 'oe')
|
|
325
|
+
return String(string).toLowerCase().normalize('NFKD').replace(/\p{Diacritic}/gu, '').replace(/œ/g, 'oe')
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
findItemByValue (value) {
|
|
329
|
+
return this.optimizedItemsForSearch.find(
|
|
330
|
+
item => item[this.itemValueKey] === value || String(item[this.itemValueKey]) === String(value)
|
|
331
|
+
)
|
|
171
332
|
},
|
|
172
333
|
|
|
173
|
-
// Selection can be made from click or enter key.
|
|
174
334
|
selectItem (item) {
|
|
175
335
|
if (!this.multiple) this.selection = []
|
|
176
336
|
this.selection.push(item)
|
|
177
337
|
this.highlightedItem = item.uid
|
|
178
338
|
this.keywords = ''
|
|
179
|
-
const emitPayload = this.multiple
|
|
180
|
-
|
|
339
|
+
const emitPayload = this.multiple
|
|
340
|
+
? this.selection.map(i => i[this.itemValueKey])
|
|
341
|
+
: item[this.itemValueKey]
|
|
181
342
|
this.$emit('item-select', item)
|
|
182
343
|
this.$emit('update:modelValue', emitPayload)
|
|
183
344
|
this.$emit('input', emitPayload)
|
|
184
|
-
this.$refs.input
|
|
345
|
+
this.$nextTick(() => this.$refs.input?.focus())
|
|
185
346
|
if (!this.multiple) this.closeMenu()
|
|
186
347
|
},
|
|
187
348
|
|
|
188
349
|
unselectItem (i) {
|
|
189
350
|
this.selection.splice(i ?? this.selection.length - 1, 1)
|
|
190
351
|
this.highlightedItem = null
|
|
191
|
-
this
|
|
192
|
-
|
|
193
|
-
|
|
352
|
+
const emitPayload = this.multiple
|
|
353
|
+
? (this.selection.length ? this.selection.map(item => item[this.itemValueKey]) : null)
|
|
354
|
+
: null
|
|
355
|
+
this.$emit('update:modelValue', emitPayload)
|
|
356
|
+
this.$emit('input', emitPayload)
|
|
357
|
+
this.$nextTick(() => this.$refs.input?.focus())
|
|
194
358
|
},
|
|
195
359
|
|
|
196
360
|
selectExtraItem () {
|
|
@@ -199,126 +363,155 @@ export default {
|
|
|
199
363
|
this.closeMenu()
|
|
200
364
|
},
|
|
201
365
|
|
|
202
|
-
|
|
203
|
-
|
|
366
|
+
onItemClick (item) {
|
|
367
|
+
this.selectItem(item)
|
|
368
|
+
this.$emit('item-click', item)
|
|
204
369
|
},
|
|
205
370
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this
|
|
371
|
+
// Called by w-list @item-select. The item is the cleaned item object (uid still present).
|
|
372
|
+
onListItemSelect (item) {
|
|
373
|
+
this.onItemClick(item)
|
|
209
374
|
},
|
|
210
375
|
|
|
211
|
-
|
|
212
|
-
|
|
376
|
+
setEndOfMenuClick () {
|
|
377
|
+
setTimeout(() => (this.menuIsBeingClicked = false), 100)
|
|
213
378
|
},
|
|
214
379
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// this.closeMenu()
|
|
220
|
-
// },
|
|
380
|
+
onWrapClick () {
|
|
381
|
+
if (!this.openOnKeydown) this.openMenu()
|
|
382
|
+
this.$refs.input?.focus()
|
|
383
|
+
},
|
|
221
384
|
|
|
222
385
|
onKeydown (e) {
|
|
223
386
|
const itemsCount = this.filteredItems.length + (this.$slots['extra-item'] ? 1 : 0)
|
|
224
|
-
|
|
387
|
+
|
|
225
388
|
if (!this.openOnKeydown || ((this.keywords || e.key.length === 1) && !this.menuOpen)) this.openMenu()
|
|
226
389
|
|
|
227
|
-
// Tab key.
|
|
390
|
+
// Tab key - close menu.
|
|
228
391
|
if (e.keyCode === 9) this.closeMenu()
|
|
229
392
|
|
|
230
|
-
//
|
|
393
|
+
// Backspace - unselect last chip when input is empty.
|
|
231
394
|
else if (e.keyCode === 8 && (!this.keywords || (!e.target.selectionStart && !e.target.selectionEnd))) {
|
|
232
395
|
this.unselectItem()
|
|
233
396
|
}
|
|
234
397
|
|
|
235
398
|
// Enter key.
|
|
236
399
|
else if (e.keyCode === 13) {
|
|
237
|
-
e.preventDefault()
|
|
400
|
+
e.preventDefault()
|
|
238
401
|
if (this.highlightedItem === 'extra-item') this.selectExtraItem()
|
|
239
402
|
else if (this.highlightedItemIndex >= 0) this.selectItem(this.filteredItems[this.highlightedItemIndex])
|
|
240
403
|
}
|
|
241
404
|
|
|
242
405
|
// Up & down arrow keys.
|
|
243
406
|
else if ([38, 40].includes(e.keyCode)) {
|
|
244
|
-
e.preventDefault()
|
|
407
|
+
e.preventDefault()
|
|
245
408
|
let index = this.highlightedItemIndex
|
|
246
409
|
if (index === -1) index = e.keyCode === 38 ? itemsCount - 1 : 0
|
|
247
|
-
else index = (index + (e.keyCode === 38 ? -1 : 1) + itemsCount) % itemsCount
|
|
410
|
+
else index = (index + (e.keyCode === 38 ? -1 : 1) + itemsCount) % itemsCount
|
|
248
411
|
|
|
249
412
|
if (this.$slots['extra-item'] && index === itemsCount - 1) this.highlightedItem = 'extra-item'
|
|
250
|
-
else this.highlightedItem = this.filteredItems[index]?.uid
|
|
413
|
+
else this.highlightedItem = this.filteredItems[index]?.uid ?? 0
|
|
251
414
|
|
|
252
|
-
// Scroll the
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
415
|
+
// Scroll the menu list if highlighted item is not in view.
|
|
416
|
+
// listWrap is the scroll container; w-list.$el.childNodes are the <li> elements.
|
|
417
|
+
const scrollEl = this.$refs.listWrap
|
|
418
|
+
if (scrollEl) {
|
|
419
|
+
if (this.$slots['extra-item'] && index === itemsCount - 1) scrollEl.scrollTop = scrollEl.scrollHeight
|
|
256
420
|
else {
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
259
|
-
|
|
421
|
+
const liEl = this.$refs.list?.$el?.childNodes[index]
|
|
422
|
+
if (liEl) {
|
|
423
|
+
const { offsetHeight: itemElHeight, offsetTop: itemElTop } = liEl
|
|
424
|
+
if (scrollEl.scrollTop + scrollEl.offsetHeight - itemElHeight < itemElTop) {
|
|
425
|
+
scrollEl.scrollTop = itemElTop - scrollEl.offsetHeight + itemElHeight
|
|
426
|
+
}
|
|
427
|
+
else if (scrollEl.scrollTop > itemElTop) scrollEl.scrollTop = itemElTop
|
|
260
428
|
}
|
|
261
|
-
else if (menuEl.scrollTop > itemElTop) menuEl.scrollTop = itemElTop
|
|
262
429
|
}
|
|
263
430
|
}
|
|
264
431
|
}
|
|
265
432
|
|
|
266
|
-
//
|
|
267
|
-
else if (!this.multiple && this.selection.length &&
|
|
433
|
+
// Prevent typing when single selection is already filled.
|
|
434
|
+
else if (!this.multiple && this.selection.length && e.key.length === 1) e.preventDefault()
|
|
268
435
|
},
|
|
269
436
|
|
|
270
|
-
// On drag & drop of a text in the input field, don't paste if single selection and already selected.
|
|
271
437
|
onDrop (e) {
|
|
272
438
|
if (!this.multiple && this.selection.length) e.preventDefault()
|
|
273
439
|
},
|
|
274
440
|
|
|
275
|
-
// When starting a sequence of keys that produces a character.
|
|
276
441
|
onCompositionStart (e) {
|
|
277
|
-
// e.preventDefault() does not work. https://stackoverflow.com/a/77556830/2012407
|
|
278
442
|
if (!this.multiple && this.selection.length) e.target.setAttribute('readonly', true)
|
|
279
443
|
},
|
|
444
|
+
|
|
280
445
|
onCompositionUpdate (e) {
|
|
281
446
|
if (!this.multiple && this.selection.length) setTimeout(() => e.target.removeAttribute('readonly'), 200)
|
|
282
447
|
},
|
|
283
448
|
|
|
449
|
+
onReset () {
|
|
450
|
+
this.selection = []
|
|
451
|
+
this.keywords = ''
|
|
452
|
+
this.highlightedItem = null
|
|
453
|
+
const emitPayload = this.multiple ? [] : null
|
|
454
|
+
this.$emit('update:modelValue', emitPayload)
|
|
455
|
+
this.$emit('input', emitPayload)
|
|
456
|
+
},
|
|
457
|
+
|
|
284
458
|
openMenu () {
|
|
285
|
-
if (this.menuOpen) return
|
|
459
|
+
if (this.menuOpen || this.isDisabled || this.isReadonly) return
|
|
286
460
|
this.menuOpen = true
|
|
287
|
-
document.addEventListener('click', this.onDocumentClick)
|
|
288
461
|
},
|
|
289
462
|
|
|
290
463
|
closeMenu () {
|
|
464
|
+
if (!this.menuOpen) return
|
|
291
465
|
this.menuOpen = false
|
|
292
|
-
|
|
466
|
+
this.highlightedItem = null
|
|
293
467
|
},
|
|
294
468
|
|
|
295
|
-
|
|
296
|
-
|
|
469
|
+
// Called when the w-menu emits @close (outside click or programmatic close).
|
|
470
|
+
onMenuClose () {
|
|
471
|
+
this.menuOpen = false
|
|
472
|
+
this.highlightedItem = null
|
|
297
473
|
}
|
|
298
474
|
},
|
|
299
475
|
|
|
300
476
|
created () {
|
|
301
|
-
if (this.modelValue) {
|
|
477
|
+
if (this.modelValue !== null && this.modelValue !== undefined) {
|
|
302
478
|
const arrayOfValues = Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]
|
|
303
479
|
arrayOfValues.forEach(value => {
|
|
304
|
-
|
|
480
|
+
const item = this.findItemByValue(value)
|
|
481
|
+
if (item) this.selection.push(item)
|
|
305
482
|
})
|
|
306
483
|
}
|
|
307
484
|
},
|
|
308
485
|
|
|
309
|
-
beforeUnmount () {
|
|
310
|
-
document.removeEventListener('click', this.onDocumentClick)
|
|
311
|
-
},
|
|
312
|
-
|
|
313
486
|
watch: {
|
|
314
487
|
modelValue (value) {
|
|
315
488
|
this.selection = []
|
|
316
|
-
if (value) {
|
|
489
|
+
if (value !== null && value !== undefined) {
|
|
317
490
|
const arrayOfValues = Array.isArray(value) ? value : [value]
|
|
318
|
-
arrayOfValues.forEach(
|
|
319
|
-
|
|
491
|
+
arrayOfValues.forEach(v => {
|
|
492
|
+
const item = this.findItemByValue(v)
|
|
493
|
+
if (item) this.selection.push(item)
|
|
320
494
|
})
|
|
321
495
|
}
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
items () {
|
|
499
|
+
// Re-sync selection when items change (e.g. async loading).
|
|
500
|
+
const currentValues = this.selection.map(item => item[this.itemValueKey])
|
|
501
|
+
this.selection = []
|
|
502
|
+
currentValues.forEach(value => {
|
|
503
|
+
const item = this.findItemByValue(value)
|
|
504
|
+
if (item) this.selection.push(item)
|
|
505
|
+
})
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
// When the number of visible items changes (user is typing), recompute the menu position so
|
|
509
|
+
// that a menu opening above the input stays anchored to the input's top edge instead of
|
|
510
|
+
// drifting upward as the list shrinks.
|
|
511
|
+
filteredItems (newVal, oldVal) {
|
|
512
|
+
if (this.menuOpen && newVal.length !== oldVal.length) {
|
|
513
|
+
this.$nextTick(() => this.$refs.menu?.computeDetachableCoords())
|
|
514
|
+
}
|
|
322
515
|
}
|
|
323
516
|
}
|
|
324
517
|
}
|
|
@@ -326,89 +519,330 @@ export default {
|
|
|
326
519
|
|
|
327
520
|
<style lang="scss">
|
|
328
521
|
.w-autocomplete {
|
|
522
|
+
position: relative;
|
|
329
523
|
display: flex;
|
|
524
|
+
flex-grow: 1;
|
|
330
525
|
flex-wrap: wrap;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
526
|
+
align-items: center;
|
|
527
|
+
font-size: var(--w-base-font-size);
|
|
528
|
+
|
|
529
|
+
@include themeable;
|
|
530
|
+
|
|
531
|
+
&--disabled {
|
|
532
|
+
color: var(--w-disabled-color);
|
|
533
|
+
cursor: not-allowed;
|
|
534
|
+
-webkit-tap-highlight-color: transparent;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
&--fit-to-content {
|
|
538
|
+
display: inline-flex;
|
|
539
|
+
flex-grow: 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Input field wrapper.
|
|
543
|
+
// ------------------------------------------------------
|
|
544
|
+
&__input-wrap {
|
|
545
|
+
position: relative;
|
|
546
|
+
display: inline-flex;
|
|
547
|
+
flex: 1 1 auto;
|
|
548
|
+
flex-wrap: wrap;
|
|
549
|
+
align-items: center;
|
|
550
|
+
gap: 4px;
|
|
551
|
+
min-height: var(--w-form-field-height);
|
|
552
|
+
padding: 3px calc(var(--w-base-increment) * 2);
|
|
553
|
+
border-radius: var(--w-border-radius);
|
|
554
|
+
border: var(--w-border);
|
|
555
|
+
transition: border var(--w-transition-duration);
|
|
556
|
+
cursor: text;
|
|
557
|
+
|
|
558
|
+
&--tile { border-radius: initial; }
|
|
559
|
+
&--shadow { box-shadow: var(--w-box-shadow); }
|
|
560
|
+
.w-autocomplete[class^="bdrs"] &, .w-autocomplete[class*=" bdrs"] & { border-radius: inherit; }
|
|
561
|
+
|
|
562
|
+
.w-autocomplete--floating-label & { margin-top: calc(var(--w-base-increment) * 3); }
|
|
563
|
+
|
|
564
|
+
&--underline {
|
|
565
|
+
border-bottom-left-radius: initial;
|
|
566
|
+
border-bottom-right-radius: initial;
|
|
567
|
+
border-width: 0 0 1px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
&--round { border-radius: 99em; }
|
|
571
|
+
|
|
572
|
+
.w-autocomplete--focused &,
|
|
573
|
+
.w-autocomplete--open & { border-color: currentColor; }
|
|
574
|
+
|
|
575
|
+
// Focus indicator line (underline style).
|
|
576
|
+
&--underline:after {
|
|
577
|
+
content: '';
|
|
578
|
+
position: absolute;
|
|
579
|
+
bottom: -1px;
|
|
580
|
+
left: 0;
|
|
581
|
+
width: 100%;
|
|
582
|
+
height: 0;
|
|
583
|
+
border-bottom: 2px solid currentColor;
|
|
584
|
+
transition: var(--w-transition-duration);
|
|
585
|
+
transform: scaleX(0);
|
|
586
|
+
pointer-events: none;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.w-autocomplete--focused &--underline:after,
|
|
590
|
+
.w-autocomplete--open &--underline:after { transform: scaleX(1); }
|
|
591
|
+
|
|
592
|
+
&--round.w-autocomplete__input-wrap--underline:after {
|
|
593
|
+
border-radius: 99em;
|
|
594
|
+
transition: var(--w-transition-duration), height 0.035s;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.w-autocomplete--focused &--round.w-autocomplete__input-wrap--underline:after,
|
|
598
|
+
.w-autocomplete--open &--round.w-autocomplete__input-wrap--underline:after {
|
|
599
|
+
height: 100%;
|
|
600
|
+
transition: var(--w-transition-duration), height 0s calc(var(--w-transition-duration) - 0.035s);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
&--no-padding {
|
|
604
|
+
padding-left: 0;
|
|
605
|
+
padding-right: 0;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Icon clearance — modifier classes on the wrap itself (no ancestor selector), so these
|
|
609
|
+
// are always 0-1-0 when alone but come after --no-padding in source order (wins the tie).
|
|
610
|
+
// The combined --no-padding + --inner-icon cases are 0-2-0 and beat both single rules.
|
|
611
|
+
&--inner-icon-left { padding-left: 28px; }
|
|
612
|
+
&--inner-icon-right { padding-right: 28px; }
|
|
613
|
+
&--no-padding.w-autocomplete__input-wrap--inner-icon-left { padding-left: 22px; }
|
|
614
|
+
&--no-padding.w-autocomplete__input-wrap--inner-icon-right { padding-right: 22px; }
|
|
341
615
|
}
|
|
342
616
|
|
|
617
|
+
// Selected item chips (multiple mode).
|
|
618
|
+
// ------------------------------------------------------
|
|
343
619
|
&__selection {
|
|
344
|
-
display: flex;
|
|
620
|
+
display: inline-flex;
|
|
345
621
|
align-items: center;
|
|
346
622
|
background: color-mix(in srgb, var(--w-contrast-bg-color) 3.5%, transparent);
|
|
347
623
|
border: 1px solid color-mix(in srgb, var(--w-contrast-bg-color) 5%, transparent);
|
|
348
|
-
border-radius:
|
|
349
|
-
padding: 0 2px 0
|
|
624
|
+
border-radius: var(--w-border-radius);
|
|
625
|
+
padding: 0 2px 0 6px;
|
|
350
626
|
flex-shrink: 0;
|
|
627
|
+
line-height: 1;
|
|
628
|
+
|
|
629
|
+
span {
|
|
630
|
+
font-size: 0.875em;
|
|
631
|
+
margin-top: -1px;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.w-button .w-icon:before {
|
|
635
|
+
font-size: 0.8em;
|
|
636
|
+
line-height: 0;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
351
639
|
|
|
352
|
-
|
|
353
|
-
|
|
640
|
+
// Placeholder.
|
|
641
|
+
// ------------------------------------------------------
|
|
642
|
+
&__placeholder {
|
|
643
|
+
color: color-mix(in srgb, var(--w-base-color) 50%, transparent);
|
|
644
|
+
pointer-events: none;
|
|
645
|
+
white-space: nowrap;
|
|
646
|
+
align-self: center;
|
|
354
647
|
}
|
|
355
648
|
|
|
649
|
+
// The text input.
|
|
650
|
+
// ------------------------------------------------------
|
|
356
651
|
&__input {
|
|
652
|
+
flex: 1 1 60px;
|
|
357
653
|
min-width: 0;
|
|
358
|
-
|
|
654
|
+
font: inherit;
|
|
359
655
|
color: inherit;
|
|
656
|
+
background: none;
|
|
360
657
|
border: none;
|
|
361
|
-
|
|
362
|
-
|
|
658
|
+
outline: none;
|
|
659
|
+
padding: 0;
|
|
660
|
+
// Ensure the input takes up a comfortable height in the flex row.
|
|
661
|
+
min-height: calc(var(--w-form-field-height) - 8px);
|
|
662
|
+
align-self: center;
|
|
663
|
+
|
|
664
|
+
.w-autocomplete--disabled & {
|
|
665
|
+
color: var(--w-disabled-color);
|
|
666
|
+
cursor: not-allowed;
|
|
667
|
+
-webkit-tap-highlight-color: transparent;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.w-autocomplete--readonly & { pointer-events: none; }
|
|
363
671
|
}
|
|
364
672
|
|
|
365
|
-
|
|
366
|
-
|
|
673
|
+
// Icons inside.
|
|
674
|
+
// ------------------------------------------------------
|
|
675
|
+
&__icon {
|
|
676
|
+
position: absolute;
|
|
677
|
+
font-size: 1.4em;
|
|
678
|
+
cursor: pointer;
|
|
679
|
+
border-radius: 5em;
|
|
680
|
+
@include default-transition;
|
|
681
|
+
|
|
682
|
+
.w-autocomplete--focused &,
|
|
683
|
+
.w-autocomplete--open & { color: currentColor; }
|
|
684
|
+
|
|
685
|
+
.w-autocomplete--disabled &,
|
|
686
|
+
.w-autocomplete--readonly & {
|
|
687
|
+
color: var(--w-disabled-color);
|
|
688
|
+
cursor: not-allowed;
|
|
689
|
+
-webkit-tap-highlight-color: transparent;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
&--inner-left { left: var(--w-base-increment); }
|
|
693
|
+
&--inner-right { right: var(--w-base-increment); }
|
|
694
|
+
|
|
695
|
+
.w-autocomplete--no-padding &--inner-left { left: 1px; }
|
|
696
|
+
.w-autocomplete--no-padding &--inner-right { right: 1px; }
|
|
697
|
+
|
|
698
|
+
&:hover { background: rgba(0, 0, 0, 0.05); }
|
|
699
|
+
|
|
700
|
+
.w-autocomplete--disabled &:hover,
|
|
701
|
+
.w-autocomplete--readonly &:hover { background-color: transparent; }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Label.
|
|
705
|
+
// ------------------------------------------------------
|
|
706
|
+
&__label {
|
|
707
|
+
display: flex;
|
|
708
|
+
align-items: center;
|
|
709
|
+
transition: color var(--w-transition-duration);
|
|
710
|
+
cursor: pointer;
|
|
711
|
+
user-select: none;
|
|
712
|
+
|
|
713
|
+
&--left { margin-right: calc(var(--w-base-increment) * 2); }
|
|
714
|
+
&--right { margin-left: calc(var(--w-base-increment) * 2); }
|
|
715
|
+
|
|
716
|
+
.w-autocomplete--disabled & {
|
|
717
|
+
color: var(--w-disabled-color);
|
|
718
|
+
cursor: not-allowed;
|
|
719
|
+
-webkit-tap-highlight-color: transparent;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.w-autocomplete--readonly.w-autocomplete--empty & {
|
|
723
|
+
opacity: 0.5;
|
|
724
|
+
cursor: auto;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Nesting under __input-wrap gives this block 2-class specificity (0-2-0), which beats
|
|
729
|
+
// .w-form-el-shakable { position: relative } (0-1-0) regardless of stylesheet load order.
|
|
730
|
+
&__input-wrap &__label--inside {
|
|
731
|
+
position: absolute;
|
|
732
|
+
// top: 50% would slide down as chips wrap to more rows; a fixed value always points to the
|
|
733
|
+
// center of the first row, keeping the label anchored and the floating animation correct.
|
|
734
|
+
top: calc(var(--w-form-field-height) / 2);
|
|
735
|
+
left: 0;
|
|
736
|
+
padding-left: calc(var(--w-base-increment) * 2);
|
|
737
|
+
white-space: nowrap;
|
|
738
|
+
transform: translateY(-50%);
|
|
367
739
|
pointer-events: none;
|
|
368
|
-
|
|
740
|
+
|
|
741
|
+
.w-autocomplete--no-padding & { padding-left: 0; }
|
|
742
|
+
.w-autocomplete__input-wrap--round & { padding-left: calc(var(--w-base-increment) * 3); }
|
|
743
|
+
// When an inner-left icon is present, remove padding and align directly with input text start
|
|
744
|
+
// (matches input-wrap's icon clearance padding: 28px regular, 22px no-padding).
|
|
745
|
+
.w-autocomplete--inner-icon-left & { left: 28px; padding-left: 0; }
|
|
746
|
+
.w-autocomplete--no-padding.w-autocomplete--inner-icon-left & { left: 22px; }
|
|
747
|
+
.w-autocomplete--inner-icon-right & { padding-right: 26px; }
|
|
748
|
+
|
|
749
|
+
.w-autocomplete--floating-label & {
|
|
750
|
+
transform-origin: 0 0;
|
|
751
|
+
transition: var(--w-transition-duration) ease;
|
|
752
|
+
will-change: transform;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Float the label up when focused, filled or has placeholder (underline style).
|
|
756
|
+
.w-autocomplete--focused.w-autocomplete--floating-label &,
|
|
757
|
+
.w-autocomplete--filled.w-autocomplete--floating-label &,
|
|
758
|
+
.w-autocomplete--has-placeholder.w-autocomplete--floating-label & {
|
|
759
|
+
transform: translateY(-160%) scale(0.85);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Float higher for box styles (outline / shadow / bg-color).
|
|
763
|
+
.w-autocomplete--focused.w-autocomplete--floating-label .w-autocomplete__input-wrap--box &,
|
|
764
|
+
.w-autocomplete--filled.w-autocomplete--floating-label .w-autocomplete__input-wrap--box &,
|
|
765
|
+
.w-autocomplete--has-placeholder.w-autocomplete--floating-label .w-autocomplete__input-wrap--box & {
|
|
766
|
+
transform: translateY(-180%) scale(0.85);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.w-autocomplete--focused.w-autocomplete--floating-label.w-autocomplete--inner-icon-left &,
|
|
770
|
+
.w-autocomplete--filled.w-autocomplete--floating-label.w-autocomplete--inner-icon-left & { left: 0; }
|
|
369
771
|
}
|
|
370
772
|
|
|
773
|
+
// Sizes.
|
|
774
|
+
// ------------------------------------------------------
|
|
775
|
+
&.size--xs { --w-form-field-height: round(nearest, calc(1.43 * var(--w-base-font-size)), 1px); }
|
|
776
|
+
&.size--sm { --w-form-field-height: round(nearest, calc(1.71 * var(--w-base-font-size)), 1px); }
|
|
777
|
+
&.size--lg { --w-form-field-height: round(nearest, calc(2.29 * var(--w-base-font-size)), 1px); }
|
|
778
|
+
&.size--xl { --w-form-field-height: round(nearest, calc(2.71 * var(--w-base-font-size)), 1px); }
|
|
779
|
+
|
|
780
|
+
// Dropdown menu (rendered via w-menu, outside overflow:hidden parents).
|
|
781
|
+
// ------------------------------------------------------
|
|
371
782
|
&__menu {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
background-color: $base-bg-color;
|
|
378
|
-
border: 1px solid color-mix(in srgb, var(--w-contrast-bg-color) 20%, transparent);
|
|
379
|
-
border-top: none;
|
|
380
|
-
border-bottom-left-radius: $border-radius;
|
|
381
|
-
border-bottom-right-radius: $border-radius;
|
|
382
|
-
overflow: auto;
|
|
383
|
-
z-index: 10;
|
|
384
|
-
|
|
385
|
-
li {
|
|
386
|
-
position: relative;
|
|
387
|
-
list-style-type: none;
|
|
388
|
-
margin: 0;
|
|
389
|
-
padding: 4px 8px;
|
|
390
|
-
|
|
391
|
-
&:hover {background-color: rgba($primary, 0.1);}
|
|
392
|
-
|
|
393
|
-
&:before, &:after {
|
|
394
|
-
content: '';
|
|
395
|
-
position: absolute;
|
|
396
|
-
inset: 0;
|
|
397
|
-
}
|
|
783
|
+
overflow: hidden;
|
|
784
|
+
background-color: var(--w-base-bg-color);
|
|
785
|
+
border: var(--w-border);
|
|
786
|
+
border-radius: var(--w-border-radius);
|
|
787
|
+
}
|
|
398
788
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
789
|
+
// Scroll container for the w-list + no-match + extra-item.
|
|
790
|
+
&__list-wrap {
|
|
791
|
+
position: relative;
|
|
792
|
+
max-height: 300px;
|
|
793
|
+
overflow-y: auto;
|
|
794
|
+
flex-grow: 1;
|
|
795
|
+
}
|
|
404
796
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
797
|
+
// w-list inside the dropdown — let the wrap handle overflow; add padding here.
|
|
798
|
+
&__list.w-list {
|
|
799
|
+
padding: 4px 0;
|
|
800
|
+
width: 100%;
|
|
801
|
+
|
|
802
|
+
// Keyboard-highlighted item: left indicator + tinted background (layered via :after
|
|
803
|
+
// so it doesn't interfere with w-list's own :before hover/active states).
|
|
804
|
+
.w-list__item-label.highlighted:after {
|
|
805
|
+
content: '';
|
|
806
|
+
position: absolute;
|
|
807
|
+
inset: 0;
|
|
808
|
+
border-left: 2px solid currentColor;
|
|
809
|
+
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
|
810
|
+
pointer-events: none;
|
|
409
811
|
}
|
|
410
812
|
}
|
|
411
|
-
}
|
|
412
813
|
|
|
413
|
-
|
|
814
|
+
&__no-match {
|
|
815
|
+
padding: 6px 12px;
|
|
816
|
+
cursor: default;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
&__extra-item {
|
|
820
|
+
display: flex;
|
|
821
|
+
align-items: center;
|
|
822
|
+
position: relative;
|
|
823
|
+
padding: 6px 12px;
|
|
824
|
+
cursor: pointer;
|
|
825
|
+
border-top: var(--w-border);
|
|
826
|
+
|
|
827
|
+
&:before {
|
|
828
|
+
content: '';
|
|
829
|
+
position: absolute;
|
|
830
|
+
inset: 0;
|
|
831
|
+
background-color: transparent;
|
|
832
|
+
transition: background-color 0.2s;
|
|
833
|
+
pointer-events: none;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
&:hover:before { background-color: color-mix(in srgb, currentColor 8%, transparent); }
|
|
837
|
+
|
|
838
|
+
&.highlighted:after {
|
|
839
|
+
content: '';
|
|
840
|
+
position: absolute;
|
|
841
|
+
inset: 0;
|
|
842
|
+
border-left: 2px solid currentColor;
|
|
843
|
+
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
|
844
|
+
pointer-events: none;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
414
848
|
</style>
|