wave-ui 3.7.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-ui",
3
- "version": "3.7.1",
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,32 +50,31 @@
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.22.10",
54
- "@babel/eslint-parser": "^7.22.10",
55
- "@babel/plugin-proposal-class-properties": "^7.18.6",
56
- "@faker-js/faker": "^7.6.0",
53
+ "@babel/core": "^7.23.2",
54
+ "@babel/eslint-parser": "^7.22.15",
55
+ "@faker-js/faker": "^8.2.0",
57
56
  "@mdi/font": "^6.9.96",
58
57
  "@vitejs/plugin-vue": "^3.2.0",
59
- "@vue/compiler-sfc": "3.2.45",
60
- "autoprefixer": "^10.4.15",
61
- "axios": "^1.4.0",
62
- "eslint": "^8.47.0",
63
- "eslint-plugin-vue": "^9.17.0",
58
+ "@vue/compiler-sfc": "3.3.4",
59
+ "autoprefixer": "^10.4.16",
60
+ "axios": "^1.6.0",
61
+ "eslint": "^8.52.0",
62
+ "eslint-plugin-vue": "^9.18.1",
64
63
  "font-awesome": "^4.7.0",
65
64
  "gsap": "^3.12.2",
66
65
  "ionicons": "^4.6.3",
67
66
  "material-design-icons": "^3.0.1",
68
- "postcss": "^8.4.27",
67
+ "postcss": "^8.4.31",
69
68
  "pug": "^3.0.2",
70
69
  "rollup-plugin-delete": "^2.0.0",
71
- "sass": "^1.65.1",
72
- "simple-syntax-highlighter": "^2.2.5",
70
+ "sass": "^1.69.5",
71
+ "simple-syntax-highlighter": "^3.0.2",
73
72
  "splitpanes": "^3.1.5",
74
73
  "standard": "^17.1.0",
75
74
  "vite": "^3.2.7",
76
75
  "vite-svg-loader": "^4.0.0",
77
- "vue": "^3.3.4",
78
- "vue-router": "^4.2.4",
76
+ "vue": "^3.3.7",
77
+ "vue-router": "^4.2.5",
79
78
  "vueperslides": "^3.5.1",
80
79
  "vuex": "^4.1.0"
81
80
  },
@@ -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
- w-icon.w-input__icon.w-input__icon--inner-left(
24
- v-if="innerIconLeft"
25
- tag="label"
26
- :for="`w-input--${_.uid}`"
27
- @click="$emit('click:inner-icon-left', $event)") {{ innerIconLeft }}
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
- w-icon.w-input__icon.w-input__icon--inner-right(
85
- v-if="innerIconRight"
86
- tag="label"
87
- :for="`w-input--${_.uid}`"
88
- @click="$emit('click:inner-icon-right', $event)") {{ innerIconRight }}
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))"
@@ -160,8 +160,8 @@ export default {
160
160
  'w-list__item-label--focused': item._focused,
161
161
  'w-list__item-label--hoverable': this.hover,
162
162
  'w-list__item-label--selectable': this.isSelectable,
163
- [item.color]: !!item.color,
164
- [this.SelectionColor]: item._selected && !item.color && this.SelectionColor,
163
+ [item[this.itemColorKey]]: !!item[this.itemColorKey],
164
+ [this.SelectionColor]: item._selected && !item[this.itemColorKey] && this.SelectionColor,
165
165
  [item[this.itemClassKey] || this.itemClass]: item[this.itemClassKey] || this.itemClass
166
166
  }
167
167
  },