wave-ui 3.7.2 → 3.8.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/wave-ui.cjs.js +1 -1
- package/dist/wave-ui.css +1 -1
- package/dist/wave-ui.es.js +870 -674
- package/dist/wave-ui.umd.js +1 -1
- package/package.json +8 -8
- package/src/wave-ui/components/index.js +1 -0
- package/src/wave-ui/components/w-autocomplete.vue +309 -0
- package/src/wave-ui/components/w-card.vue +2 -0
- package/src/wave-ui/components/w-input.vue +13 -11
- package/src/wave-ui/components/w-select.vue +35 -34
- package/src/wave-ui/components/w-textarea.vue +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "An emerging UI framework for Vue.js (2 & 3) with only the bright side. :sunny:",
|
|
5
5
|
"author": "Antoni Andre <antoniandre.web@gmail.com>",
|
|
6
6
|
"homepage": "https://antoniandre.github.io/wave-ui",
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"publish-doc": "npm run build && npm run build-bundle && git add . && git commit -m 'Publish documentation on Github.' && git push && git push --tag"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@babel/core": "^7.23.
|
|
53
|
+
"@babel/core": "^7.23.2",
|
|
54
54
|
"@babel/eslint-parser": "^7.22.15",
|
|
55
|
-
"@faker-js/faker": "^8.
|
|
55
|
+
"@faker-js/faker": "^8.2.0",
|
|
56
56
|
"@mdi/font": "^6.9.96",
|
|
57
57
|
"@vitejs/plugin-vue": "^3.2.0",
|
|
58
58
|
"@vue/compiler-sfc": "3.3.4",
|
|
59
59
|
"autoprefixer": "^10.4.16",
|
|
60
|
-
"axios": "^1.
|
|
61
|
-
"eslint": "^8.
|
|
62
|
-
"eslint-plugin-vue": "^9.
|
|
60
|
+
"axios": "^1.6.0",
|
|
61
|
+
"eslint": "^8.52.0",
|
|
62
|
+
"eslint-plugin-vue": "^9.18.1",
|
|
63
63
|
"font-awesome": "^4.7.0",
|
|
64
64
|
"gsap": "^3.12.2",
|
|
65
65
|
"ionicons": "^4.6.3",
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
"postcss": "^8.4.31",
|
|
68
68
|
"pug": "^3.0.2",
|
|
69
69
|
"rollup-plugin-delete": "^2.0.0",
|
|
70
|
-
"sass": "^1.69.
|
|
70
|
+
"sass": "^1.69.5",
|
|
71
71
|
"simple-syntax-highlighter": "^3.0.2",
|
|
72
72
|
"splitpanes": "^3.1.5",
|
|
73
73
|
"standard": "^17.1.0",
|
|
74
74
|
"vite": "^3.2.7",
|
|
75
75
|
"vite-svg-loader": "^4.0.0",
|
|
76
|
-
"vue": "^3.3.
|
|
76
|
+
"vue": "^3.3.7",
|
|
77
77
|
"vue-router": "^4.2.5",
|
|
78
78
|
"vueperslides": "^3.5.1",
|
|
79
79
|
"vuex": "^4.1.0"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as WAccordion } from './w-accordion.vue'
|
|
2
2
|
export { default as WAlert } from './w-alert.vue'
|
|
3
|
+
export { default as WAutocomplete } from './w-autocomplete.vue'
|
|
3
4
|
export { default as WApp } from './w-app.vue'
|
|
4
5
|
export { default as WBadge } from './w-badge.vue'
|
|
5
6
|
export { default as WBreadcrumbs } from './w-breadcrumbs.vue'
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.w-autocomplete(:class="classes" @click="onClick")
|
|
3
|
+
template(v-if="selection.length")
|
|
4
|
+
.w-autocomplete__selection(v-for="(item, i) in selection")
|
|
5
|
+
span(v-html="item[itemLabelKey]")
|
|
6
|
+
w-button(@click.stop="unselectItem(i)" icon="i-cross" xs text color="currentColor")
|
|
7
|
+
.w-autocomplete__placeholder(v-if="!selection.length && !keywords && placeholder" v-html="placeholder")
|
|
8
|
+
input.w-autocomplete__input(
|
|
9
|
+
ref="input"
|
|
10
|
+
v-model="keywords"
|
|
11
|
+
@focus="onFocus"
|
|
12
|
+
@keydown="onKeydown"
|
|
13
|
+
@drop="onDrop"
|
|
14
|
+
@compositionstart="onCompositionStart"
|
|
15
|
+
@compositionupdate="onCompositionUpdate"
|
|
16
|
+
v-on="$listeners")
|
|
17
|
+
w-transition-slide-fade(y)
|
|
18
|
+
ul.w-autocomplete__menu(v-if="menuOpen" ref="menu")
|
|
19
|
+
li(
|
|
20
|
+
v-for="(item, i) in filteredItems"
|
|
21
|
+
:key="i"
|
|
22
|
+
@click.stop="selectItem(item)"
|
|
23
|
+
:class="{ highlighted: highlightedItem === item.uid }")
|
|
24
|
+
span(v-html="item[itemLabelKey]")
|
|
25
|
+
li.w-autocomplete__no-match(
|
|
26
|
+
v-if="!filteredItems.length"
|
|
27
|
+
:class="{ 'w-autocomplete__no-match--default': !$slots.noMatch }")
|
|
28
|
+
slot(name="no-match")
|
|
29
|
+
.caption(v-html="noMatch ?? 'No match.'")
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
export default {
|
|
34
|
+
name: 'w-autocomplete',
|
|
35
|
+
|
|
36
|
+
props: {
|
|
37
|
+
items: { type: Array, required: true },
|
|
38
|
+
modelValue: { type: [String, Number, Array] }, // String or Number if single selections, Array if multiple.
|
|
39
|
+
placeholder: { type: String },
|
|
40
|
+
openOnKeydown: { type: Boolean }, // By default the menu is always open for selection.
|
|
41
|
+
multiple: { type: Boolean },
|
|
42
|
+
// When multiple is on, prevents duplicate items selections by default, unless this is set to true.
|
|
43
|
+
allowDuplicates: { type: Boolean },
|
|
44
|
+
noMatch: { type: String },
|
|
45
|
+
// Contains the unique selection value for each item.
|
|
46
|
+
// Can be a numeric ID, a slug, etc. (outside of Wave UI)
|
|
47
|
+
itemValueKey: { type: String, default: 'value' },
|
|
48
|
+
// Contains the string to display for each item.
|
|
49
|
+
itemLabelKey: { type: String, default: 'label' },
|
|
50
|
+
// Contains the string to search keywords into for each item.
|
|
51
|
+
// This can for instance be an aggregation of multiple fields (outside of Wave UI).
|
|
52
|
+
itemSearchableKey: { type: String, default: 'searchable' }
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
emits: ['input'],
|
|
56
|
+
|
|
57
|
+
data: () => ({
|
|
58
|
+
keywords: '',
|
|
59
|
+
selection: [],
|
|
60
|
+
menuOpen: false,
|
|
61
|
+
highlightedItem: null
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
computed: {
|
|
65
|
+
// Keep the autocomplete matching as fast as possible by caching optimized search strings.
|
|
66
|
+
normalizedKeywords () {
|
|
67
|
+
return this.normalize(this.keywords)
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Keep the autocomplete matching as fast as possible by caching optimized search strings.
|
|
71
|
+
// An array of optimized strings.
|
|
72
|
+
normalizedSelection () {
|
|
73
|
+
return this.selection.map(item => this.normalize(item?.searchable))
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Keep the autocomplete matching as fast as possible by caching optimized search strings.
|
|
77
|
+
optimizedItemsForSearch () {
|
|
78
|
+
return this.items.map((item, i) => ({
|
|
79
|
+
...item,
|
|
80
|
+
uid: i,
|
|
81
|
+
searchable: this.normalize(item[this.itemSearchableKey] || '')
|
|
82
|
+
}))
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
filteredItems () {
|
|
86
|
+
let items = this.optimizedItemsForSearch // Array of objects.
|
|
87
|
+
const selection = this.normalizedSelection.join(',') // Optimized string of coma separated words.
|
|
88
|
+
const isItemNotSelected = item => !selection.includes(item.searchable)
|
|
89
|
+
|
|
90
|
+
if (this.keywords) {
|
|
91
|
+
items = items.filter(item => {
|
|
92
|
+
if (!item.searchable.includes(this.normalizedKeywords)) return false
|
|
93
|
+
else if (this.multiple && !this.allowDuplicates) return isItemNotSelected(item)
|
|
94
|
+
else return true
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
else if (this.multiple && !this.allowDuplicates) items = items.filter(isItemNotSelected)
|
|
99
|
+
|
|
100
|
+
return items
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
highlightedItemIndex () {
|
|
104
|
+
if (this.highlightedItem === null) return -1
|
|
105
|
+
return this.filteredItems.findIndex(item => item.uid === this.highlightedItem)
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
classes () {
|
|
109
|
+
return {
|
|
110
|
+
'w-autocomplete--open': this.menuOpen
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
methods: {
|
|
116
|
+
// Replace all the accents and non-latin characters with equivalent letters. E.g. é -> e.
|
|
117
|
+
normalize (string) {
|
|
118
|
+
return string.toLowerCase().normalize('NFKD').replace(/\p{Diacritic}/gu, '').replace(/œ/g, 'oe')
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Selection can be made from click or enter key.
|
|
122
|
+
selectItem (item) {
|
|
123
|
+
if (!this.multiple) this.selection = []
|
|
124
|
+
this.selection.push(item)
|
|
125
|
+
this.highlightedItem = item.uid
|
|
126
|
+
this.keywords = ''
|
|
127
|
+
this.$emit('input', this.multiple ? this.selection.map(item => item[this.itemValueKey]) : item[this.itemValueKey])
|
|
128
|
+
this.$refs.input.focus()
|
|
129
|
+
if (!this.multiple) this.closeMenu()
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
unselectItem (i) {
|
|
133
|
+
this.selection.splice(i ?? this.selection.length - 1, 1)
|
|
134
|
+
this.highlightedItem = null
|
|
135
|
+
this.$emit('input', null)
|
|
136
|
+
this.$refs.input.focus()
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
onClick () {
|
|
140
|
+
if (!this.openOnKeydown) this.openMenu()
|
|
141
|
+
this.$refs.input.focus()
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
onFocus () {
|
|
145
|
+
if (!this.openOnKeydown) this.openMenu()
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
onKeydown (e) {
|
|
149
|
+
const itemsCount = this.filteredItems.length
|
|
150
|
+
// `e.key.length === 1`: is all the keyboard keys that generate a character.
|
|
151
|
+
if (!this.openOnKeydown || ((this.keywords || e.key.length === 1) && !this.menuOpen)) this.openMenu()
|
|
152
|
+
|
|
153
|
+
// Delete key.
|
|
154
|
+
if (e.keyCode === 8 && (!this.keywords || (!e.target.selectionStart && !e.target.selectionEnd))) {
|
|
155
|
+
this.unselectItem()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Enter key.
|
|
159
|
+
else if (e.keyCode === 13) {
|
|
160
|
+
e.preventDefault() // Prevent form submissions.
|
|
161
|
+
if (this.highlightedItemIndex >= 0) this.selectItem(this.filteredItems[this.highlightedItemIndex])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Up & down arrow keys.
|
|
165
|
+
else if ([38, 40].includes(e.keyCode)) {
|
|
166
|
+
e.preventDefault() // Prevent moving the cursor to the left of the text while selecting item.
|
|
167
|
+
let index = this.highlightedItemIndex
|
|
168
|
+
if (index === -1) index = e.keyCode === 38 ? itemsCount - 1 : 0
|
|
169
|
+
else index = (index + (e.keyCode === 38 ? -1 : 1) + itemsCount) % itemsCount // Never out of range.
|
|
170
|
+
|
|
171
|
+
this.highlightedItem = this.filteredItems[index]?.uid || 0
|
|
172
|
+
|
|
173
|
+
// Scroll the container if highlighted item is not in view.
|
|
174
|
+
const menuEl = this.$refs.menu
|
|
175
|
+
if (menuEl) {
|
|
176
|
+
const { offsetHeight: itemElHeight, offsetTop: itemElTop } = menuEl.childNodes[index] || {}
|
|
177
|
+
if (menuEl.scrollTop + menuEl.offsetHeight - itemElHeight < itemElTop) {
|
|
178
|
+
menuEl.scrollTop = itemElTop - menuEl.offsetHeight + itemElHeight
|
|
179
|
+
}
|
|
180
|
+
else if (menuEl.scrollTop > itemElTop) menuEl.scrollTop = itemElTop
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// `e.key.length === 1`: allow all control keys but no character creation.
|
|
185
|
+
else if (!this.multiple && this.selection.length && (e.key.length === 1)) e.preventDefault()
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// On drag & drop of a text in the input field, don't paste if single selection and already selected.
|
|
189
|
+
onDrop (e) {
|
|
190
|
+
if (!this.multiple && this.selection.length) e.preventDefault()
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// When starting a sequence of keys that produces a character.
|
|
194
|
+
onCompositionStart (e) {
|
|
195
|
+
// e.preventDefault() does not work. https://stackoverflow.com/a/77556830/2012407
|
|
196
|
+
if (!this.multiple && this.selection.length) e.target.setAttribute('readonly', true)
|
|
197
|
+
},
|
|
198
|
+
onCompositionUpdate (e) {
|
|
199
|
+
if (!this.multiple && this.selection.length) setTimeout(() => e.target.removeAttribute('readonly'), 200)
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
openMenu () {
|
|
203
|
+
if (this.menuOpen) return
|
|
204
|
+
this.menuOpen = true
|
|
205
|
+
document.addEventListener('click', this.onDocumentClick)
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
closeMenu () {
|
|
209
|
+
this.menuOpen = false
|
|
210
|
+
document.removeEventListener('click', this.onDocumentClick)
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
onDocumentClick (e) {
|
|
214
|
+
if (!this.$el.contains(e.target) && !this.$el.isSameNode(e.target)) this.closeMenu()
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
created () {
|
|
219
|
+
if (this.modelValue) {
|
|
220
|
+
const arrayOfValues = Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]
|
|
221
|
+
arrayOfValues.forEach(value => {
|
|
222
|
+
this.selection.push(this.optimizedItemsForSearch.find(item => item[this.itemValueKey] === +value))
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
beforeUnmount () {
|
|
228
|
+
document.removeEventListener('click', this.onDocumentClick)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
</script>
|
|
232
|
+
|
|
233
|
+
<style lang="scss">
|
|
234
|
+
.w-autocomplete {
|
|
235
|
+
display: flex;
|
|
236
|
+
flex-wrap: wrap;
|
|
237
|
+
gap: 4px;
|
|
238
|
+
position: relative;
|
|
239
|
+
border-radius: 4px;
|
|
240
|
+
border: 1px solid rgba(#000, 0.2);
|
|
241
|
+
padding: 2px 4px;
|
|
242
|
+
user-select: none;
|
|
243
|
+
|
|
244
|
+
&--open {
|
|
245
|
+
border-bottom-left-radius: 0;
|
|
246
|
+
border-bottom-right-radius: 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
&__selection {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
background: rgba(#000, 0.035);
|
|
253
|
+
border: 1px solid rgba(#000, 0.05);
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
padding: 0 2px 0 4px;
|
|
256
|
+
flex-shrink: 0;
|
|
257
|
+
|
|
258
|
+
span {margin-top: -1px;line-height: 1;}
|
|
259
|
+
.w-button .w-icon:before {font-size: 0.8em;line-height: 0;}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
&__input {
|
|
263
|
+
min-width: 0;
|
|
264
|
+
flex: 1 1 0;
|
|
265
|
+
color: inherit;
|
|
266
|
+
border: none;
|
|
267
|
+
background-color: transparent;
|
|
268
|
+
line-height: 18px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
&__placeholder {
|
|
272
|
+
position: absolute;
|
|
273
|
+
color: #ccc;
|
|
274
|
+
pointer-events: none;
|
|
275
|
+
line-height: 18px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
&__menu {
|
|
279
|
+
position: absolute;
|
|
280
|
+
inset: 100% -1px auto;
|
|
281
|
+
max-height: clamp(20px, 400px, 80vh);
|
|
282
|
+
margin-top: -1px;
|
|
283
|
+
margin-left: 0;
|
|
284
|
+
background-color: #fff;
|
|
285
|
+
border: 1px solid rgba(#000, 0.2);
|
|
286
|
+
border-top: none;
|
|
287
|
+
border-bottom-left-radius: inherit;
|
|
288
|
+
border-bottom-right-radius: inherit;
|
|
289
|
+
overflow: auto;
|
|
290
|
+
z-index: 10;
|
|
291
|
+
|
|
292
|
+
li {
|
|
293
|
+
list-style-type: none;
|
|
294
|
+
margin: 0;
|
|
295
|
+
padding: 4px 8px;
|
|
296
|
+
border-left: 2px solid transparent;
|
|
297
|
+
|
|
298
|
+
&:hover {background-color: rgba($primary, 0.1);}
|
|
299
|
+
|
|
300
|
+
&.highlighted {
|
|
301
|
+
background-color: rgba($primary, 0.15);
|
|
302
|
+
border-left-color: rgba($primary, 0.75);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
li.w-autocomplete__no-match--default:hover {background-color: transparent;}
|
|
309
|
+
</style>
|
|
@@ -103,6 +103,8 @@ export default {
|
|
|
103
103
|
padding: (2 * $base-increment) (3 * $base-increment);
|
|
104
104
|
font-size: 1.3em;
|
|
105
105
|
border-bottom: $border;
|
|
106
|
+
border-top-left-radius: inherit;
|
|
107
|
+
border-top-right-radius: inherit;
|
|
106
108
|
|
|
107
109
|
&--has-toolbar {padding: 0;border-bottom: none;}
|
|
108
110
|
}
|
|
@@ -20,11 +20,12 @@ component(
|
|
|
20
20
|
|
|
21
21
|
//- Input wrapper.
|
|
22
22
|
.w-input__input-wrap(:class="inputWrapClasses")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
slot(name="icon-left" :input-id="`w-input--${_.uid}`")
|
|
24
|
+
w-icon.w-input__icon.w-input__icon--inner-left(
|
|
25
|
+
v-if="innerIconLeft"
|
|
26
|
+
tag="label"
|
|
27
|
+
:for="`w-input--${_.uid}`"
|
|
28
|
+
@click="$emit('click:inner-icon-left', $event)") {{ innerIconLeft }}
|
|
28
29
|
//- All types of input except file.
|
|
29
30
|
input.w-input__input(
|
|
30
31
|
v-if="type !== 'file'"
|
|
@@ -78,14 +79,15 @@ component(
|
|
|
78
79
|
template(v-if="labelPosition === 'inside' && showLabelInside")
|
|
79
80
|
label.w-input__label.w-input__label--inside.w-form-el-shakable(
|
|
80
81
|
v-if="$slots.default || label"
|
|
81
|
-
:for="`w-input--${_.uid}`"
|
|
82
82
|
:class="labelClasses")
|
|
83
83
|
slot {{ label }}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
|
|
85
|
+
slot(name="icon-right" :input-id="`w-input--${_.uid}`")
|
|
86
|
+
w-icon.w-input__icon.w-input__icon--inner-right(
|
|
87
|
+
v-if="innerIconRight"
|
|
88
|
+
tag="label"
|
|
89
|
+
:for="`w-input--${_.uid}`"
|
|
90
|
+
@click="$emit('click:inner-icon-right', $event)") {{ innerIconRight }}
|
|
89
91
|
|
|
90
92
|
w-progress.fill-width(
|
|
91
93
|
v-if="hasLoading || (showProgress && (uploadInProgress || uploadComplete))"
|
|
@@ -24,7 +24,7 @@ component(
|
|
|
24
24
|
custom
|
|
25
25
|
min-width="activator"
|
|
26
26
|
v-bind="menuProps || {}")
|
|
27
|
-
template(#activator
|
|
27
|
+
template(#activator)
|
|
28
28
|
//- Input wrapper.
|
|
29
29
|
.w-select__selection-wrap(
|
|
30
30
|
@click="!isDisabled && !isReadonly && onInputFieldClick()"
|
|
@@ -43,16 +43,11 @@ component(
|
|
|
43
43
|
slot(name="selection" :item="multiple ? inputValue : inputValue[0]")
|
|
44
44
|
.w-select__selection(
|
|
45
45
|
ref="selection-input"
|
|
46
|
-
:contenteditable="isDisabled || isReadonly ? 'false' : 'true'"
|
|
47
46
|
@focus="!isDisabled && !isReadonly && onFocus($event)"
|
|
48
47
|
@blur="onBlur"
|
|
49
48
|
@keydown="!isDisabled && !isReadonly && onKeydown($event)"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
readonly
|
|
53
|
-
aria-readonly="true"
|
|
54
|
-
:tabindex="tabindex || null"
|
|
55
|
-
v-html="$slots.selection ? '' : selectionString || placeholder")
|
|
49
|
+
v-bind="selectionAttributes"
|
|
50
|
+
v-html="selectionHtml")
|
|
56
51
|
//- For standard HTML form submission.
|
|
57
52
|
input(
|
|
58
53
|
v-for="(val, i) in (inputValue.length ? inputValue : [{}])"
|
|
@@ -76,7 +71,7 @@ component(
|
|
|
76
71
|
@item-click="$emit('item-click', $event)"
|
|
77
72
|
@item-select="onListItemSelect"
|
|
78
73
|
@keydown:enter="noUnselect && !multiple && closeMenu()"
|
|
79
|
-
@keydown:escape="showMenu && (
|
|
74
|
+
@keydown:escape="showMenu && (showMenu = false) /* Will call closeMenu() from w-menu(@close). */"
|
|
80
75
|
:items="selectItems"
|
|
81
76
|
:multiple="multiple"
|
|
82
77
|
arrows-navigation
|
|
@@ -87,7 +82,7 @@ component(
|
|
|
87
82
|
:item-color-key="itemColorKey"
|
|
88
83
|
role="listbox"
|
|
89
84
|
tabindex="-1")
|
|
90
|
-
template(v-for="i in items.length"
|
|
85
|
+
template(v-for="i in items.length" #[`item.${i}`]="{ item, selected, index }")
|
|
91
86
|
slot(
|
|
92
87
|
v-if="$slots[`item.${i}`] && $slots[`item.${i}`](item, selected, index)"
|
|
93
88
|
:name="`item.${i}`"
|
|
@@ -178,20 +173,32 @@ export default {
|
|
|
178
173
|
return obj
|
|
179
174
|
})
|
|
180
175
|
},
|
|
181
|
-
hasValue () {
|
|
182
|
-
return Array.isArray(this.inputValue) ? this.inputValue.length : (this.inputValue !== null)
|
|
183
|
-
},
|
|
184
176
|
hasLabel () {
|
|
185
177
|
return this.label || this.$slots.default
|
|
186
178
|
},
|
|
187
179
|
showLabelInside () {
|
|
188
|
-
return !this.staticLabel || (!this.
|
|
180
|
+
return !this.staticLabel || (!this.inputValue.length && !this.placeholder)
|
|
181
|
+
},
|
|
182
|
+
selectionAttributes () {
|
|
183
|
+
return {
|
|
184
|
+
class: { 'w-select__selection--placeholder': !this.$slots.selection && !this.selectionString && this.placeholder },
|
|
185
|
+
disabled: this.isDisabled || null,
|
|
186
|
+
readonly: true,
|
|
187
|
+
ariareadonly: 'true',
|
|
188
|
+
tabindex: this.tabindex ?? null,
|
|
189
|
+
contenteditable: this.isDisabled || this.isReadonly ? 'false' : 'true'
|
|
190
|
+
}
|
|
189
191
|
},
|
|
190
192
|
selectionString () {
|
|
191
|
-
return this.inputValue
|
|
193
|
+
return this.inputValue.map(
|
|
192
194
|
item => item[this.itemValueKey] !== undefined ? item[this.itemLabelKey] : (item[this.itemLabelKey] ?? item)
|
|
193
195
|
).join(', ')
|
|
194
196
|
},
|
|
197
|
+
selectionHtml () {
|
|
198
|
+
if (!this.inputValue.length) return this.placeholder || ''
|
|
199
|
+
if (this.$slots.selection) return ''
|
|
200
|
+
return this.selectionString
|
|
201
|
+
},
|
|
195
202
|
classes () {
|
|
196
203
|
return {
|
|
197
204
|
'w-select': true,
|
|
@@ -200,7 +207,7 @@ export default {
|
|
|
200
207
|
'w-select--disabled': this.isDisabled,
|
|
201
208
|
'w-select--fit-to-content': this.fitToContent,
|
|
202
209
|
'w-select--readonly': this.isReadonly,
|
|
203
|
-
[`w-select--${this.
|
|
210
|
+
[`w-select--${this.inputValue.length ? 'filled' : 'empty'}`]: true,
|
|
204
211
|
'w-select--focused': (this.isFocused || this.showMenu) && !this.isReadonly,
|
|
205
212
|
'w-select--floating-label': this.hasLabel && this.labelPosition === 'inside' && !this.staticLabel,
|
|
206
213
|
'w-select--no-padding': !this.outline && !this.bgColor && !this.shadow && !this.round,
|
|
@@ -298,7 +305,7 @@ export default {
|
|
|
298
305
|
onInput (items) {
|
|
299
306
|
this.inputValue = items === null ? [] : (this.multiple ? items : [items])
|
|
300
307
|
// Return the original items when returnObject is true (no `value` if there wasn't),
|
|
301
|
-
// or the
|
|
308
|
+
// or the item value otherwise.
|
|
302
309
|
items = this.inputValue.map(item => this.returnObject ? this.items[item.index] : item.value)
|
|
303
310
|
|
|
304
311
|
// Emit the selection to the v-model.
|
|
@@ -307,6 +314,7 @@ export default {
|
|
|
307
314
|
this.$emit('update:modelValue', selection)
|
|
308
315
|
this.$emit('input', selection)
|
|
309
316
|
},
|
|
317
|
+
|
|
310
318
|
onInputFieldClick () {
|
|
311
319
|
if (this.showMenu) this.showMenu = false // Will call `closeMenu()` from w-menu(@close).
|
|
312
320
|
else this.openMenu()
|
|
@@ -339,7 +347,7 @@ export default {
|
|
|
339
347
|
return items.map(item => {
|
|
340
348
|
let value = item
|
|
341
349
|
if (item && typeof item === 'object') { // `null` is also an object!
|
|
342
|
-
value = item[this.itemValueKey]
|
|
350
|
+
value = item[this.itemValueKey] ?? item[this.itemLabelKey] ?? item
|
|
343
351
|
}
|
|
344
352
|
|
|
345
353
|
return this.selectItems[allValues.indexOf(value)]
|
|
@@ -474,6 +482,9 @@ export default {
|
|
|
474
482
|
align-items: center;
|
|
475
483
|
cursor: pointer;
|
|
476
484
|
caret-color: transparent;
|
|
485
|
+
border-radius: inherit;
|
|
486
|
+
|
|
487
|
+
&--placeholder {color: #888;}
|
|
477
488
|
|
|
478
489
|
.w-select__selection-slot + & {
|
|
479
490
|
position: absolute;
|
|
@@ -500,8 +511,6 @@ export default {
|
|
|
500
511
|
}
|
|
501
512
|
|
|
502
513
|
.w-select--readonly & {cursor: auto;}
|
|
503
|
-
|
|
504
|
-
&--placeholder {color: #888;}
|
|
505
514
|
}
|
|
506
515
|
|
|
507
516
|
&__selection-slot {
|
|
@@ -563,14 +572,12 @@ export default {
|
|
|
563
572
|
|
|
564
573
|
&__label--inside {
|
|
565
574
|
position: absolute;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
right: 0;
|
|
575
|
+
inset: 0 0 auto;
|
|
576
|
+
min-height: inherit;
|
|
569
577
|
white-space: nowrap;
|
|
570
578
|
// Use margin instead of padding as the scale transformation below decreases the real padding
|
|
571
579
|
// size and misaligns the label.
|
|
572
580
|
margin-left: 2 * $base-increment;
|
|
573
|
-
transform: translateY(-50%);
|
|
574
581
|
pointer-events: none;
|
|
575
582
|
|
|
576
583
|
.w-select--inner-icon-right & {padding-right: 26px;}
|
|
@@ -594,21 +601,15 @@ export default {
|
|
|
594
601
|
.w-select--open.w-select--floating-label &,
|
|
595
602
|
.w-select--filled.w-select--floating-label &,
|
|
596
603
|
.w-select--has-placeholder.w-select--floating-label & {
|
|
597
|
-
transform: translateY(-
|
|
604
|
+
transform: translateY(-80%) scale(0.85);
|
|
598
605
|
}
|
|
599
|
-
// Chrome & Safari - Must
|
|
606
|
+
// Chrome & Safari - Must stay a separated rule or Firefox discards the whole rule seeing -webkit-.
|
|
600
607
|
.w-select--floating-label .w-select__select:-webkit-autofill & {
|
|
601
|
-
transform: translateY(-
|
|
602
|
-
}
|
|
603
|
-
// Move label with outline style or with shadow.
|
|
604
|
-
.w-select--open.w-select--floating-label .w-select__selection-wrap--box &,
|
|
605
|
-
.w-select--filled.w-select--floating-label .w-select__selection-wrap--box &,
|
|
606
|
-
.w-select--has-placeholder.w-select--floating-label .w-select__selection-wrap--box & {
|
|
607
|
-
transform: translateY(-180%) scale(0.85);
|
|
608
|
+
transform: translateY(-80%) scale(0.85);
|
|
608
609
|
}
|
|
609
610
|
.w-select--open.w-select--floating-label.w-select--inner-icon-left &,
|
|
610
611
|
.w-select--filled.w-select--floating-label.w-select--inner-icon-left & {left: 0;}
|
|
611
|
-
// Chrome & Safari - Must
|
|
612
|
+
// Chrome & Safari - Must stay a separated rule or Firefox discards the whole rule seeing -webkit-.
|
|
612
613
|
.w-select--floating-label.w-select--inner-icon-left .w-select__select:-webkit-autofill & {left: 0;}
|
|
613
614
|
}
|
|
614
615
|
|
|
@@ -42,7 +42,6 @@ component(
|
|
|
42
42
|
template(v-if="labelPosition === 'inside' && showLabelInside")
|
|
43
43
|
label.w-textarea__label.w-textarea__label--inside.w-form-el-shakable(
|
|
44
44
|
v-if="$slots.default || label"
|
|
45
|
-
:for="`w-textarea--${_.uid}`"
|
|
46
45
|
:class="labelClasses")
|
|
47
46
|
slot {{ label }}
|
|
48
47
|
w-icon.w-textarea__icon.w-textarea__icon--inner-right(
|