wave-ui 1.65.2 → 1.66.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": "1.65.2",
3
+ "version": "1.66.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",
@@ -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")
32
- transition(:name="transitionName" :mode="transitionMode")
33
- keep-alive
31
+ .w-tabs__content-wrap(v-if="tabs.length")
32
+ transition-group(v-if="keepInDom" :name="transitionName")
33
+ tab-content(
34
+ v-for="(tab, i) in tabs"
35
+ :key="tab._uid"
36
+ :item="tab"
37
+ v-show="tab._uid === activeTab._uid"
38
+ :class="contentClass")
39
+ slot(
40
+ v-if="$scopedSlots[`item-content.${tab._index + 1}`]"
41
+ :name="`item-content.${tab._index + 1}`"
42
+ :item="getOriginalItem(tab)"
43
+ :index="tab._index + 1"
44
+ :active="tab._index === activeTab._index")
45
+ slot(
46
+ v-else
47
+ name="item-content"
48
+ :item="getOriginalItem(tab)"
49
+ :index="tab._index + 1"
50
+ :active="tab._index === activeTab._index")
51
+ div(v-if="tab[itemContentKey]" v-html="tab[itemContentKey]")
52
+ transition(v-else :name="transitionName" :mode="transitionMode")
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,15 +259,16 @@ 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
270
+
271
+ this.openTab(this.tabs[index]._uid)
197
272
 
198
273
  // Scroll the new active tab item title into view if needed.
199
274
  this.$nextTick(() => {
@@ -207,11 +282,15 @@ export default {
207
282
 
208
283
  // Return the original item (so there is no `_index`, etc.).
209
284
  getOriginalItem (item) {
210
- return this.items[item._index]
285
+ return this.items[item._index] || {}
211
286
  }
212
287
  },
213
288
 
214
289
  beforeMount () {
290
+ this.tabs = [] // Reset for hot-reloading.
291
+ const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items
292
+ items.forEach(this.addTab)
293
+
215
294
  this.updateActiveTab(this.value)
216
295
 
217
296
  this.$nextTick(() => {
@@ -229,13 +308,17 @@ export default {
229
308
 
230
309
  watch: {
231
310
  value (index) {
232
- this.updateActiveTab(index)
311
+ if (index !== this.activeTabIndex) this.updateActiveTab(index)
233
312
  },
234
- items () {
235
- // When deleting a tab, activate the previous one.
236
- while (this.activeTabIndex > 0 && !this.tabsItems[this.activeTabIndex]) this.activeTabIndex--
313
+ items: {
314
+ handler () {
315
+ this.refreshTabs()
237
316
 
238
- if (!this.noSlider) this.$nextTick(this.updateSlider)
317
+ if (this.tabs.length) this.reopenTheActiveTab()
318
+
319
+ if (!this.noSlider) this.$nextTick(this.updateSlider)
320
+ },
321
+ deep: true
239
322
  },
240
323
  fillBar () {
241
324
  if (!this.noSlider) this.$nextTick(this.updateSlider)
@@ -374,8 +457,10 @@ export default {
374
457
  .w-tabs-slide-left-leave-active,
375
458
  .w-tabs-slide-right-leave-active {
376
459
  position: absolute;
460
+ top: 0;
377
461
  left: 0;
378
462
  right: 0;
463
+ overflow: hidden;
379
464
  }
380
465
 
381
466
  .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>