wave-ui 1.65.2 → 1.66.1

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": "1.65.2",
3
+ "version": "1.66.1",
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",
@@ -2,56 +2,79 @@
2
2
  .w-tabs(:class="tabsClasses")
3
3
  .w-tabs__bar(ref="tabs-bar" :class="tabsBarClasses")
4
4
  .w-tabs__bar-item(
5
- v-for="(item, i) in tabsItems"
5
+ v-for="(tab, i) in tabs"
6
6
  :key="i"
7
- :class="barItemClasses(item)"
8
- @click="!item._disabled && openTab(item)"
9
- @focus="$emit('focus', getOriginalItem(item))"
10
- :tabindex="!item._disabled && 0"
11
- @keypress.enter="!item._disabled && openTab(item)"
12
- :aria-selected="item._index === activeTabIndex ? 'true' : 'false'"
7
+ :class="barItemClasses(tab)"
8
+ @click="!tab._disabled && tab._uid !== activeTabUid && openTab(tab._uid)"
9
+ @focus="$emit('focus', getOriginalItem(tab))"
10
+ :tabindex="!tab._disabled && 0"
11
+ @keypress.enter="!tab._disabled && openTab(tab._uid)"
12
+ :aria-selected="tab._uid === activeTabUid ? 'true' : 'false'"
13
13
  role="tab")
14
- slot(
15
- v-if="$scopedSlots[`item-title.${item.id || i + 1}`]"
16
- :name="`item-title.${item.id || i + 1}`"
17
- :item="getOriginalItem(item)"
18
- :index="i + 1"
19
- :active="item._index === activeTabIndex")
20
- slot(
21
- v-else
22
- name="item-title"
23
- :item="getOriginalItem(item)"
24
- :index="i + 1"
25
- :active="item._index === activeTabIndex")
26
- div(v-html="item[itemTitleKey]")
14
+ slot(
15
+ v-if="$scopedSlots[`item-title.${tab.id || i + 1}`]"
16
+ :name="`item-title.${tab.id || i + 1}`"
17
+ :item="getOriginalItem(tab)"
18
+ :index="i + 1"
19
+ :active="tab._uid === activeTabUid")
20
+ slot(
21
+ v-else
22
+ name="item-title"
23
+ :item="getOriginalItem(tab)"
24
+ :index="i + 1"
25
+ :active="tab._uid === activeTabUid")
26
+ div(v-html="tab[itemTitleKey]")
27
27
  .w-tabs__bar-extra(v-if="$scopedSlots['tabs-bar-extra']")
28
28
  slot(name="tabs-bar-extra")
29
29
  .w-tabs__slider(v-if="!noSlider && !card" :class="sliderColor" :style="sliderStyles")
30
30
 
31
- .w-tabs__content-wrap(v-if="tabsItems.length")
31
+ transition-group.w-tabs__content-wrap(v-if="keepInDom" :name="transitionName" tag="div")
32
+ tab-content(
33
+ v-for="(tab, i) in tabs"
34
+ :key="tab._uid"
35
+ :item="tab"
36
+ v-show="tab._uid === activeTab._uid"
37
+ :class="contentClass")
38
+ slot(
39
+ v-if="$scopedSlots[`item-content.${tab._index + 1}`]"
40
+ :name="`item-content.${tab._index + 1}`"
41
+ :item="getOriginalItem(tab)"
42
+ :index="tab._index + 1"
43
+ :active="tab._index === activeTab._index")
44
+ slot(
45
+ v-else
46
+ name="item-content"
47
+ :item="getOriginalItem(tab)"
48
+ :index="tab._index + 1"
49
+ :active="tab._index === activeTab._index")
50
+ div(v-if="tab[itemContentKey]" v-html="tab[itemContentKey]")
51
+ .w-tabs__content-wrap(v-else)
32
52
  transition(:name="transitionName" :mode="transitionMode")
33
- keep-alive
53
+ keep-alive(:exclude="keepAlive ? '' : 'tab-content'")
34
54
  //- Keep-alive only works with components, not with DOM nodes.
35
- tab-content(:class="contentClass" :key="activeTab._index")
36
- slot(
37
- v-if="$scopedSlots[`item-content.${activeTab.id || activeTab._index + 1}`]"
38
- :name="`item-content.${activeTab.id || activeTab._index + 1}`"
39
- :item="getOriginalItem(activeTab)"
40
- :index="activeTab._index + 1"
41
- :active="activeTab._index === activeTabIndex")
42
- slot(
43
- v-else
44
- name="item-content"
45
- :item="getOriginalItem(activeTab)"
46
- :index="activeTab._index + 1"
47
- :active="activeTab._index === activeTabIndex")
48
- div(v-html="activeTab[itemContentKey]")
55
+ tab-content(:key="activeTabUid" :item="activeTab" :class="contentClass")
56
+ template(#default="{ item }")
57
+ template(v-if="item")
58
+ slot(
59
+ v-if="$scopedSlots[`item-content.${item._index + 1}`]"
60
+ :name="`item-content.${item._index + 1}`"
61
+ :item="getOriginalItem(item)"
62
+ :index="item._index + 1"
63
+ :active="item._uid === activeTabUid")
64
+ slot(
65
+ v-else
66
+ name="item-content"
67
+ :item="getOriginalItem(item)"
68
+ :index="item._index + 1"
69
+ :active="item._uid === activeTabUid")
70
+ div(v-if="item[itemContentKey]" v-html="item[itemContentKey]")
49
71
  </template>
50
72
 
51
73
  <script>
52
- import Vue from 'vue'
53
74
  import TabContent from './tab-content.vue'
54
75
 
76
+ let uid = 0
77
+
55
78
  export default {
56
79
  name: 'w-tabs',
57
80
 
@@ -60,6 +83,7 @@ export default {
60
83
  color: { type: String },
61
84
  bgColor: { type: String },
62
85
  items: { type: [Array, Number] },
86
+ itemIdKey: { type: String, default: 'id' },
63
87
  itemTitleKey: { type: String, default: 'title' },
64
88
  itemContentKey: { type: String, default: 'content' },
65
89
  titleClass: { type: String },
@@ -72,7 +96,9 @@ export default {
72
96
  fillBar: { type: Boolean },
73
97
  center: { type: Boolean },
74
98
  right: { type: Boolean },
75
- card: { type: Boolean }
99
+ card: { type: Boolean },
100
+ keepAlive: { type: Boolean, default: true },
101
+ keepInDom: { type: Boolean, default: false }
76
102
  },
77
103
 
78
104
  components: { TabContent },
@@ -80,7 +106,9 @@ export default {
80
106
  emits: ['input', 'update:modelValue', 'focus'],
81
107
 
82
108
  data: () => ({
109
+ tabs: [],
83
110
  activeTabEl: null,
111
+ activeTabUid: null,
84
112
  activeTabIndex: 0,
85
113
  prevTabIndex: -1, // To detect transition direction.
86
114
  slider: {
@@ -104,19 +132,13 @@ export default {
104
132
  return this.activeTab._index < this.prevTabIndex ? 'right' : 'left'
105
133
  },
106
134
 
107
- tabsItems () {
108
- const items = typeof this.items === 'number' ? Array(this.items).fill({}) : this.items
109
-
110
- // eslint-disable-next-line new-cap
111
- return items.map((item, _index) => new Vue.observable({
112
- ...item,
113
- _index,
114
- _disabled: !!item.disabled
115
- }))
135
+ activeTab () {
136
+ return this.tabsByUid[this.activeTabUid] || this.tabs[0] || {}
116
137
  },
117
138
 
118
- activeTab () {
119
- return this.tabsItems[this.activeTabIndex] || this.tabsItems[0] || {}
139
+ // An object indexing the tabs by their uid.
140
+ tabsByUid () {
141
+ return this.tabs.reduce((obj, tab) => ((obj[tab._uid] = tab) && obj), {})
120
142
  },
121
143
 
122
144
  tabsClasses () {
@@ -145,6 +167,54 @@ export default {
145
167
  },
146
168
 
147
169
  methods: {
170
+ // Adding a tab in the list.
171
+ addTab (item) {
172
+ // If there is no unique ID provided, inject one in each tab.
173
+ // This will cause a single other update from watching the tabs items and stop there.
174
+ if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = +`${this._uid}${++uid}`
175
+
176
+ this.tabs.push({
177
+ _uid: item[this.itemIdKey] ?? item._uid,
178
+ _index: this.tabs.length,
179
+ ...item,
180
+ _disabled: !!item.disabled
181
+ })
182
+ },
183
+
184
+ refreshTabs () {
185
+ let items = this.items
186
+ if (typeof items === 'number') items = Array(items).fill().map((_, i) => this.tabs[i] || {})
187
+
188
+ this.tabs = items.map((item, _index) => {
189
+ // If there is no unique ID provided, inject one in each tab.
190
+ // This will cause a single other update from watching the tabs items and stop there.
191
+ if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = +`${this._uid}${++uid}`
192
+
193
+ return {
194
+ ...item,
195
+ _uid: item[this.itemIdKey] ?? item._uid,
196
+ _index,
197
+ _disabled: !!item.disabled
198
+ }
199
+ })
200
+ },
201
+
202
+ reopenTheActiveTab () {
203
+ // If there is only 1 tab left open it.
204
+ if (this.tabs.length === 1) return this.openTab(this.tabs[0]._uid)
205
+
206
+ // First try to find the same uid in remaining tabs.
207
+ let uid = this.tabsByUid[this.activeTabUid]?._uid
208
+
209
+ // If not found, try to open the tab with the same index.
210
+ if (!uid) uid = this.tabs[this.activeTabIndex]?._uid
211
+
212
+ // If not found (no tab to the right), try to open the next tab to the left.
213
+ if (!uid) uid = this.tabs[Math.max(this.activeTabIndex - 1, this.tabs.length - 1)]?._uid
214
+
215
+ if (uid) this.openTab(uid)
216
+ },
217
+
148
218
  onResize () {
149
219
  this.updateSlider(false)
150
220
  },
@@ -161,11 +231,14 @@ export default {
161
231
  }
162
232
  },
163
233
 
164
- openTab (item) {
234
+ // Switching tabs.
235
+ openTab (uid) {
165
236
  this.prevTabIndex = this.activeTabIndex // To resolve the transition direction.
166
- this.activeTabIndex = item._index
167
- this.$emit('update:modelValue', item._index)
168
- this.$emit('input', item._index)
237
+ const tab = this.tabsByUid[uid]
238
+ this.activeTabIndex = tab._index
239
+ this.activeTabUid = tab._uid
240
+ this.$emit('update:modelValue', tab._index)
241
+ this.$emit('input', tab._index)
169
242
 
170
243
  if (!this.noSlider) this.$nextTick(this.updateSlider)
171
244
  },
@@ -173,7 +246,8 @@ export default {
173
246
  // Updates the slider position.
174
247
  updateSlider (domLookup = true) {
175
248
  if (domLookup) {
176
- this.activeTabEl = this.$refs['tabs-bar'].querySelector('.w-tabs__bar-item--active')
249
+ const ref = this.$refs['tabs-bar']
250
+ this.activeTabEl = ref && ref.querySelector('.w-tabs__bar-item--active')
177
251
  }
178
252
 
179
253
  if (!this.fillBar && this.activeTabEl) {
@@ -185,34 +259,42 @@ export default {
185
259
  this.slider.width = `${width}px`
186
260
  }
187
261
  else {
188
- this.slider.left = `${this.activeTab._index * 100 / this.tabsItems.length}%`
189
- this.slider.width = `${100 / this.tabsItems.length}%`
262
+ this.slider.left = `${this.activeTab._index * 100 / this.tabs.length}%`
263
+ this.slider.width = `${100 / this.tabs.length}%`
190
264
  }
191
265
  },
192
266
 
193
267
  updateActiveTab (index) {
194
268
  if (typeof index === 'string') index = ~~index
195
269
  else if (isNaN(index) || index < 0) index = 0
196
- this.activeTabIndex = index
197
270
 
198
- // Scroll the new active tab item title into view if needed.
199
- this.$nextTick(() => {
200
- const ref = this.$refs['tabs-bar']
201
- this.activeTabEl = ref && ref.querySelector(`.w-tabs__bar-item:nth-child(${index + 1})`)
202
- if (this.activeTabEl) {
203
- this.activeTabEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
204
- }
205
- })
271
+ // Only open the tab if it is found.
272
+ if (this.tabs[index]?._uid) {
273
+ this.openTab(this.tabs[index]?._uid)
274
+
275
+ // Scroll the new active tab item title into view if needed.
276
+ this.$nextTick(() => {
277
+ const ref = this.$refs['tabs-bar']
278
+ this.activeTabEl = ref && ref.querySelector(`.w-tabs__bar-item:nth-child(${index + 1})`)
279
+ if (this.activeTabEl) {
280
+ this.activeTabEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
281
+ }
282
+ })
283
+ }
206
284
  },
207
285
 
208
286
  // Return the original item (so there is no `_index`, etc.).
209
287
  getOriginalItem (item) {
210
- return this.items[item._index]
288
+ return this.items[item._index] || {}
211
289
  }
212
290
  },
213
291
 
214
292
  beforeMount () {
215
- this.updateActiveTab(this.value)
293
+ this.tabs = [] // Reset for hot-reloading.
294
+ const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items
295
+ items.forEach(this.addTab)
296
+
297
+ if (this.value ?? false) this.updateActiveTab(this.value)
216
298
 
217
299
  this.$nextTick(() => {
218
300
  this.updateSlider()
@@ -229,13 +311,17 @@ export default {
229
311
 
230
312
  watch: {
231
313
  value (index) {
232
- this.updateActiveTab(index)
314
+ if (index !== this.activeTabIndex) this.updateActiveTab(index)
233
315
  },
234
- items () {
235
- // When deleting a tab, activate the previous one.
236
- while (this.activeTabIndex > 0 && !this.tabsItems[this.activeTabIndex]) this.activeTabIndex--
316
+ items: {
317
+ handler () {
318
+ this.refreshTabs()
237
319
 
238
- if (!this.noSlider) this.$nextTick(this.updateSlider)
320
+ if (this.tabs.length) this.reopenTheActiveTab()
321
+
322
+ if (!this.noSlider) this.$nextTick(this.updateSlider)
323
+ },
324
+ deep: true
239
325
  },
240
326
  fillBar () {
241
327
  if (!this.noSlider) this.$nextTick(this.updateSlider)
@@ -374,8 +460,10 @@ export default {
374
460
  .w-tabs-slide-left-leave-active,
375
461
  .w-tabs-slide-right-leave-active {
376
462
  position: absolute;
463
+ top: 0;
377
464
  left: 0;
378
465
  right: 0;
466
+ overflow: hidden;
379
467
  }
380
468
 
381
469
  .w-tabs-slide-left-enter-active {animation: w-tabs-slide-left-enter $transition-duration + 0.15s;}
@@ -1,8 +1,14 @@
1
1
  <template lang="pug">
2
2
  .w-tabs__content
3
- slot
3
+ slot(:item="item")
4
4
  </template>
5
5
 
6
6
  <script>
7
7
  // Keep-alive only works with components, not with DOM nodes.
8
+
9
+ export default {
10
+ name: 'tab-content', // Keep-alive include/exclude mechanism is component-name-based.
11
+
12
+ props: { item: Object }
13
+ }
8
14
  </script>