wave-ui 3.2.0 → 3.4.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 +1356 -1111
- package/dist/wave-ui.umd.js +1 -1
- package/package.json +1 -1
- package/src/wave-ui/components/index.js +1 -1
- package/src/wave-ui/components/w-scrollable.vue +243 -0
- package/src/wave-ui/components/w-tabs/index.vue +132 -48
- package/src/wave-ui/components/w-tabs/tab-content.vue +8 -1
- package/src/wave-ui/components/w-tree.vue +75 -16
- package/src/wave-ui/components/w-scrollbar.vue +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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",
|
|
@@ -28,7 +28,7 @@ export { default as WProgress } from './w-progress.vue'
|
|
|
28
28
|
export { default as WRadio } from './w-radio.vue'
|
|
29
29
|
export { default as WRadios } from './w-radios.vue'
|
|
30
30
|
export { default as WRating } from './w-rating.vue'
|
|
31
|
-
export { default as
|
|
31
|
+
export { default as WScrollable } from './w-scrollable.vue'
|
|
32
32
|
export { default as WSelect } from './w-select.vue'
|
|
33
33
|
export { default as WSlider } from './w-slider.vue'
|
|
34
34
|
export { default as WSpinner } from './w-spinner.vue'
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.w-scrollable(
|
|
3
|
+
ref="scrollable"
|
|
4
|
+
@mouseenter="onMouseEnter"
|
|
5
|
+
@mouseleave="onMouseLeave"
|
|
6
|
+
@mousewheel="onMouseWheel"
|
|
7
|
+
:class="scrollableClasses"
|
|
8
|
+
v-bind="$attrs"
|
|
9
|
+
:style="scrollableStyles")
|
|
10
|
+
slot
|
|
11
|
+
.w-scrollbar(ref="track" @mousedown="onTrackMouseDown" :class="scrollbarClasses")
|
|
12
|
+
.w-scrollbar__thumb(ref="thumb" :style="thumbStyles")
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
const domProps = {
|
|
17
|
+
h: {
|
|
18
|
+
horizOrVert: 'horizontal',
|
|
19
|
+
topOrLeft: 'left',
|
|
20
|
+
widthOrHeight: 'width',
|
|
21
|
+
offsetWidthOrHeight: 'offsetWidth',
|
|
22
|
+
maxWidthOrHeight: 'max-width',
|
|
23
|
+
scrollWidthOrHeight: 'scrollWidth',
|
|
24
|
+
clientXorY: 'clientX',
|
|
25
|
+
deltaXorY: 'deltaX',
|
|
26
|
+
scrollTopOrLeft: 'scrollLeft'
|
|
27
|
+
},
|
|
28
|
+
v: {
|
|
29
|
+
horizOrVert: 'vertical',
|
|
30
|
+
topOrLeft: 'top',
|
|
31
|
+
widthOrHeight: 'height',
|
|
32
|
+
offsetWidthOrHeight: 'offsetHeight',
|
|
33
|
+
maxWidthOrHeight: 'max-height',
|
|
34
|
+
scrollWidthOrHeight: 'scrollHeight',
|
|
35
|
+
clientXorY: 'clientY',
|
|
36
|
+
deltaXorY: 'deltaY',
|
|
37
|
+
scrollTopOrLeft: 'scrollTop'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
name: 'w-scrollable',
|
|
43
|
+
props: {
|
|
44
|
+
color: { type: String, default: 'primary' },
|
|
45
|
+
bgColor: { type: String },
|
|
46
|
+
width: { type: [Number, String] },
|
|
47
|
+
height: { type: [Number, String] }
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
emits: [],
|
|
51
|
+
|
|
52
|
+
data: () => ({
|
|
53
|
+
mounted: false,
|
|
54
|
+
scrollable: {
|
|
55
|
+
top: null,
|
|
56
|
+
left: null,
|
|
57
|
+
hovered: false
|
|
58
|
+
},
|
|
59
|
+
scrollValuePercent: 0
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
computed: {
|
|
63
|
+
isHorizontal () {
|
|
64
|
+
if (!this.mounted) return false
|
|
65
|
+
console.log('💂♂️', this.$refs.scrollable?.scrollWidth, this.$refs.scrollable?.offsetWidth)
|
|
66
|
+
return (this.width && !this.height) || (this.$refs.scrollable?.scrollWidth > this.$refs.scrollable?.offsetWidth)
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
m () { // m = shorthand for map of DOM properties.
|
|
70
|
+
return domProps[this.isHorizontal ? 'h' : 'v']
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
scrollableClasses () {
|
|
74
|
+
return {
|
|
75
|
+
[`w-scrollable--${this.m.horizOrVert}`]: true
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
scrollbarClasses () {
|
|
80
|
+
return {
|
|
81
|
+
[`w-scrollbar--${this.m.horizOrVert}`]: true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
thumbSizePercent () {
|
|
86
|
+
if (!this.mounted) return 0
|
|
87
|
+
console.log('😒', this[this.m.widthOrHeight], this.$refs.scrollable[[this.m.offsetWidthOrHeight]])
|
|
88
|
+
const widthOrHeight = this[this.m.widthOrHeight] ?? this.$refs.scrollable[[this.m.offsetWidthOrHeight]]
|
|
89
|
+
// if (widthOrHeight === undefined) widthOrHeight = this.$refs.scrollable.offsetWidthOrHeight
|
|
90
|
+
return (widthOrHeight * 100 / this.$refs.scrollable?.[this.m.scrollWidthOrHeight]) || 0
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
scrollableStyles () {
|
|
94
|
+
return {
|
|
95
|
+
[this.m.maxWidthOrHeight]: `${this[this.m.widthOrHeight]}px`
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
thumbStyles () {
|
|
100
|
+
let topOrLeftValue = this.scrollValuePercent
|
|
101
|
+
topOrLeftValue = Math.max(0, Math.min(topOrLeftValue, 100 - this.thumbSizePercent))
|
|
102
|
+
return {
|
|
103
|
+
[this.m.widthOrHeight]: `${this.thumbSizePercent}%`,
|
|
104
|
+
[this.m.topOrLeft]: `${topOrLeftValue}%`
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
methods: {
|
|
110
|
+
onTrackMouseDown (e) {
|
|
111
|
+
if (this.isDisabled || this.isReadonly) return
|
|
112
|
+
// On touch screen don't listen for both touchstart & mousedown.
|
|
113
|
+
if ('ontouchstart' in window && e.type === 'mousedown') return
|
|
114
|
+
|
|
115
|
+
const { top, left, width, height } = this.$refs.track.getBoundingClientRect()
|
|
116
|
+
if (this.isHorizontal) {
|
|
117
|
+
this.$refs.track.width = width
|
|
118
|
+
this.$refs.track.left = left
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.$refs.track.height = height
|
|
122
|
+
this.$refs.track.top = top
|
|
123
|
+
}
|
|
124
|
+
this.dragging = true
|
|
125
|
+
|
|
126
|
+
this.computeScroll(e.type === 'touchstart' ? e.touches[0][this.m.clientXorY] : e[this.m.clientXorY])
|
|
127
|
+
this.scroll()
|
|
128
|
+
|
|
129
|
+
document.addEventListener(e.type === 'touchstart' ? 'touchmove' : 'mousemove', this.onDrag)
|
|
130
|
+
document.addEventListener(e.type === 'touchstart' ? 'touchend' : 'mouseup', this.onMouseUp, { once: true })
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
onDrag (e) {
|
|
134
|
+
this.computeScroll((e.type === 'touchmove' ? e.touches[0][this.m.clientXorY] : e[this.m.clientXorY]))
|
|
135
|
+
this.scroll()
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
onMouseUp (e) {
|
|
139
|
+
this.dragging = false
|
|
140
|
+
document.removeEventListener(e.type === 'touchend' ? 'touchmove' : 'mousemove', this.onDrag)
|
|
141
|
+
if (this.$refs.thumb) this.$refs.thumb.focus()
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
onMouseEnter () {
|
|
145
|
+
this.scrollable.hovered = true
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
onMouseLeave () {
|
|
149
|
+
this.scrollable.hovered = false
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
onMouseWheel (e) {
|
|
153
|
+
if (!this.scrollable.hovered) return // Only scroll a w-scrollable element that is being hovered.
|
|
154
|
+
|
|
155
|
+
// When scrolling beyond limits, release the mousewheel and scroll the parent.
|
|
156
|
+
if (this.scrollValuePercent <= 0 && e[this.m.deltaXorY] < 0) return
|
|
157
|
+
else if (this.scrollValuePercent >= 100 - this.thumbSizePercent && e[this.m.deltaXorY] > 0) return
|
|
158
|
+
|
|
159
|
+
e.preventDefault() // Hold the scroll in the hovered w-scrollable element.
|
|
160
|
+
|
|
161
|
+
this.scrollValuePercent += e[this.m.deltaXorY] * 0.05
|
|
162
|
+
this.scrollValuePercent = Math.max(0, Math.min(this.scrollValuePercent, 100))
|
|
163
|
+
this.scroll()
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
computeScroll (cursorPositionXorY) {
|
|
167
|
+
const { top, left, width, height } = this.$refs.scrollable.getBoundingClientRect()
|
|
168
|
+
const topOrLeft = this.isHorizontal ? left : top
|
|
169
|
+
const widthOrHeight = this.isHorizontal ? width : height
|
|
170
|
+
this.scrollValuePercent = Math.max(0, Math.min(((cursorPositionXorY - topOrLeft) / widthOrHeight) * 100, 100))
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
scroll () {
|
|
174
|
+
this.$refs.scrollable[this.m.scrollTopOrLeft] = this.scrollValuePercent * this.$refs.scrollable?.[this.m.scrollWidthOrHeight] / 100
|
|
175
|
+
this.updateThumbPosition()
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
updateThumbPosition () {
|
|
179
|
+
this.$refs.thumb.style[this.m.topOrLeft] = this.scrollValuePercent
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
mounted () {
|
|
184
|
+
this.mounted = true
|
|
185
|
+
const { top, left } = this.$refs.scrollable.getBoundingClientRect()
|
|
186
|
+
this.scrollable.top = top
|
|
187
|
+
this.scrollable.left = left
|
|
188
|
+
|
|
189
|
+
this.$el.parentNode.style.position = 'relative'
|
|
190
|
+
this.$el.parentNode.style[this.m.maxWidthOrHeight] = `${this[this.m.widthOrHeight]}px`
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<style lang="scss">
|
|
196
|
+
.w-scrollable {
|
|
197
|
+
position: relative;
|
|
198
|
+
overflow: hidden;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.w-scrollbar {
|
|
202
|
+
position: absolute;
|
|
203
|
+
background: #000;
|
|
204
|
+
user-select: none;
|
|
205
|
+
|
|
206
|
+
&--horizontal {
|
|
207
|
+
left: 0;
|
|
208
|
+
right: 0;
|
|
209
|
+
bottom: 0;
|
|
210
|
+
height: 8px;
|
|
211
|
+
}
|
|
212
|
+
&--vertical {
|
|
213
|
+
top: 0;
|
|
214
|
+
bottom: 0;
|
|
215
|
+
right: 0;
|
|
216
|
+
width: 8px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
&__thumb {
|
|
220
|
+
position: absolute;
|
|
221
|
+
background: #333;
|
|
222
|
+
border-radius: $border-radius;
|
|
223
|
+
z-index: 1;
|
|
224
|
+
will-change: top left;
|
|
225
|
+
|
|
226
|
+
&:hover {background: #444;}
|
|
227
|
+
}
|
|
228
|
+
&--horizontal &__thumb {
|
|
229
|
+
height: 6px;
|
|
230
|
+
left: 0;
|
|
231
|
+
right: 0;
|
|
232
|
+
margin-top: 1px;
|
|
233
|
+
margin-bottom: 1px;
|
|
234
|
+
}
|
|
235
|
+
&--vertical &__thumb {
|
|
236
|
+
width: 6px;
|
|
237
|
+
top: 0;
|
|
238
|
+
bottom: 0;
|
|
239
|
+
margin-left: 1px;
|
|
240
|
+
margin-right: 1px;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
</style>
|
|
@@ -2,57 +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
|
|
5
|
+
v-for="(item, i) in tabs"
|
|
6
6
|
:key="i"
|
|
7
7
|
:class="barItemClasses(item)"
|
|
8
|
-
@click="!item._disabled && openTab(item)"
|
|
8
|
+
@click="!item._disabled && item._uid !== activeTabUid && openTab(item._uid)"
|
|
9
9
|
@focus="$emit('focus', getOriginalItem(item))"
|
|
10
10
|
:tabindex="!item._disabled && 0"
|
|
11
|
-
@keypress.enter="!item._disabled && openTab(item)"
|
|
12
|
-
:aria-selected="item.
|
|
11
|
+
@keypress.enter="!item._disabled && openTab(item._uid)"
|
|
12
|
+
:aria-selected="item._uid === activeTabUid ? 'true' : 'false'"
|
|
13
13
|
role="tab")
|
|
14
14
|
slot(
|
|
15
15
|
v-if="$slots[`item-title.${item.id || i + 1}`]"
|
|
16
16
|
:name="`item-title.${item.id || i + 1}`"
|
|
17
17
|
:item="getOriginalItem(item)"
|
|
18
18
|
:index="i + 1"
|
|
19
|
-
:active="item.
|
|
19
|
+
:active="item._uid === activeTabUid")
|
|
20
20
|
slot(
|
|
21
21
|
v-else
|
|
22
22
|
name="item-title"
|
|
23
23
|
:item="getOriginalItem(item)"
|
|
24
24
|
:index="i + 1"
|
|
25
|
-
:active="item.
|
|
25
|
+
:active="item._uid === activeTabUid")
|
|
26
26
|
div(v-html="item[itemTitleKey]")
|
|
27
27
|
.w-tabs__bar-extra(v-if="$slots['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="
|
|
32
|
-
transition(
|
|
33
|
-
|
|
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="$slots[`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(:key="
|
|
55
|
+
tab-content(:key="activeTabUid" :item="activeTab" :class="contentClass")
|
|
36
56
|
template(#default="{ item }")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
template(v-if="item")
|
|
58
|
+
slot(
|
|
59
|
+
v-if="$slots[`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]")
|
|
50
71
|
</template>
|
|
51
72
|
|
|
52
73
|
<script>
|
|
53
|
-
import { reactive } from 'vue'
|
|
54
74
|
import TabContent from './tab-content.vue'
|
|
55
75
|
|
|
76
|
+
let uid = 0
|
|
77
|
+
|
|
56
78
|
export default {
|
|
57
79
|
name: 'w-tabs',
|
|
58
80
|
|
|
@@ -61,6 +83,7 @@ export default {
|
|
|
61
83
|
color: { type: String },
|
|
62
84
|
bgColor: { type: String },
|
|
63
85
|
items: { type: [Array, Number] },
|
|
86
|
+
itemIdKey: { type: String, default: 'id' },
|
|
64
87
|
itemTitleKey: { type: String, default: 'title' },
|
|
65
88
|
itemContentKey: { type: String, default: 'content' },
|
|
66
89
|
titleClass: { type: String },
|
|
@@ -75,7 +98,9 @@ export default {
|
|
|
75
98
|
right: { type: Boolean },
|
|
76
99
|
card: { type: Boolean },
|
|
77
100
|
dark: { type: Boolean },
|
|
78
|
-
light: { type: Boolean }
|
|
101
|
+
light: { type: Boolean },
|
|
102
|
+
keepAlive: { type: Boolean, default: true },
|
|
103
|
+
keepInDom: { type: Boolean, default: false }
|
|
79
104
|
},
|
|
80
105
|
|
|
81
106
|
components: { TabContent },
|
|
@@ -83,7 +108,9 @@ export default {
|
|
|
83
108
|
emits: ['input', 'update:modelValue', 'focus'],
|
|
84
109
|
|
|
85
110
|
data: () => ({
|
|
111
|
+
tabs: [],
|
|
86
112
|
activeTabEl: null,
|
|
113
|
+
activeTabUid: null,
|
|
87
114
|
activeTabIndex: 0,
|
|
88
115
|
prevTabIndex: -1, // To detect transition direction.
|
|
89
116
|
slider: {
|
|
@@ -107,18 +134,13 @@ export default {
|
|
|
107
134
|
return this.activeTab._index < this.prevTabIndex ? 'right' : 'left'
|
|
108
135
|
},
|
|
109
136
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return items.map((item, _index) => reactive({
|
|
114
|
-
...item,
|
|
115
|
-
_index,
|
|
116
|
-
_disabled: !!item.disabled
|
|
117
|
-
}))
|
|
137
|
+
activeTab () {
|
|
138
|
+
return this.tabsByUid[this.activeTabUid] || this.tabs[0] || {}
|
|
118
139
|
},
|
|
119
140
|
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
// An object indexing the tabs by their uid.
|
|
142
|
+
tabsByUid () {
|
|
143
|
+
return this.tabs.reduce((obj, tab) => ((obj[tab._uid] = tab) && obj), {})
|
|
122
144
|
},
|
|
123
145
|
|
|
124
146
|
tabsClasses () {
|
|
@@ -149,6 +171,54 @@ export default {
|
|
|
149
171
|
},
|
|
150
172
|
|
|
151
173
|
methods: {
|
|
174
|
+
// Adding a tab in the list.
|
|
175
|
+
addTab (item) {
|
|
176
|
+
// If there is no unique ID provided, inject one in each tab.
|
|
177
|
+
// This will cause a single other update from watching the tabs items and stop there.
|
|
178
|
+
if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = +`${this._.uid}${++uid}`
|
|
179
|
+
|
|
180
|
+
this.tabs.push({
|
|
181
|
+
_uid: item[this.itemIdKey] ?? item._uid,
|
|
182
|
+
_index: this.tabs.length,
|
|
183
|
+
...item,
|
|
184
|
+
_disabled: !!item.disabled
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
refreshTabs () {
|
|
189
|
+
let items = this.items
|
|
190
|
+
if (typeof items === 'number') items = Array(items).fill().map((_, i) => this.tabs[i] || {})
|
|
191
|
+
|
|
192
|
+
this.tabs = items.map((item, _index) => {
|
|
193
|
+
// If there is no unique ID provided, inject one in each tab.
|
|
194
|
+
// This will cause a single other update from watching the tabs items and stop there.
|
|
195
|
+
if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = +`${this._.uid}${++uid}`
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...item,
|
|
199
|
+
_uid: item[this.itemIdKey] ?? item._uid,
|
|
200
|
+
_index,
|
|
201
|
+
_disabled: !!item.disabled
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
reopenTheActiveTab () {
|
|
207
|
+
// If there is only 1 tab left open it.
|
|
208
|
+
if (this.tabs.length === 1) return this.openTab(this.tabs[0]._uid)
|
|
209
|
+
|
|
210
|
+
// First try to find the same uid in remaining tabs.
|
|
211
|
+
let uid = this.tabsByUid[this.activeTabUid]?._uid
|
|
212
|
+
|
|
213
|
+
// If not found, try to open the tab with the same index.
|
|
214
|
+
if (!uid) uid = this.tabs[this.activeTabIndex]?._uid
|
|
215
|
+
|
|
216
|
+
// If not found (no tab to the right), try to open the next tab to the left.
|
|
217
|
+
if (!uid) uid = this.tabs[Math.max(this.activeTabIndex - 1, this.tabs.length - 1)]?._uid
|
|
218
|
+
|
|
219
|
+
if (uid) this.openTab(uid)
|
|
220
|
+
},
|
|
221
|
+
|
|
152
222
|
onResize () {
|
|
153
223
|
this.updateSlider(false)
|
|
154
224
|
},
|
|
@@ -165,11 +235,14 @@ export default {
|
|
|
165
235
|
}
|
|
166
236
|
},
|
|
167
237
|
|
|
168
|
-
|
|
238
|
+
// Switching tabs.
|
|
239
|
+
openTab (uid) {
|
|
169
240
|
this.prevTabIndex = this.activeTabIndex // To resolve the transition direction.
|
|
170
|
-
|
|
171
|
-
this
|
|
172
|
-
this
|
|
241
|
+
const tab = this.tabsByUid[uid]
|
|
242
|
+
this.activeTabIndex = tab._index
|
|
243
|
+
this.activeTabUid = tab._uid
|
|
244
|
+
this.$emit('update:modelValue', tab._index)
|
|
245
|
+
this.$emit('input', tab._index)
|
|
173
246
|
|
|
174
247
|
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
175
248
|
},
|
|
@@ -190,15 +263,16 @@ export default {
|
|
|
190
263
|
this.slider.width = `${width}px`
|
|
191
264
|
}
|
|
192
265
|
else {
|
|
193
|
-
this.slider.left = `${this.activeTab._index * 100 / this.
|
|
194
|
-
this.slider.width = `${100 / this.
|
|
266
|
+
this.slider.left = `${this.activeTab._index * 100 / this.tabs.length}%`
|
|
267
|
+
this.slider.width = `${100 / this.tabs.length}%`
|
|
195
268
|
}
|
|
196
269
|
},
|
|
197
270
|
|
|
198
271
|
updateActiveTab (index) {
|
|
199
272
|
if (typeof index === 'string') index = ~~index
|
|
200
273
|
else if (isNaN(index) || index < 0) index = 0
|
|
201
|
-
|
|
274
|
+
|
|
275
|
+
this.openTab(this.tabs[index]._uid)
|
|
202
276
|
|
|
203
277
|
// Scroll the new active tab item title into view if needed.
|
|
204
278
|
this.$nextTick(() => {
|
|
@@ -212,11 +286,15 @@ export default {
|
|
|
212
286
|
|
|
213
287
|
// Return the original item (so there is no `_index`, etc.).
|
|
214
288
|
getOriginalItem (item) {
|
|
215
|
-
return this.items[item._index]
|
|
289
|
+
return this.items[item._index] || {}
|
|
216
290
|
}
|
|
217
291
|
},
|
|
218
292
|
|
|
219
293
|
beforeMount () {
|
|
294
|
+
this.tabs = [] // Reset for hot-reloading.
|
|
295
|
+
const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items
|
|
296
|
+
items.forEach(this.addTab)
|
|
297
|
+
|
|
220
298
|
this.updateActiveTab(this.modelValue)
|
|
221
299
|
|
|
222
300
|
this.$nextTick(() => {
|
|
@@ -234,13 +312,17 @@ export default {
|
|
|
234
312
|
|
|
235
313
|
watch: {
|
|
236
314
|
modelValue (index) {
|
|
237
|
-
this.updateActiveTab(index)
|
|
315
|
+
if (index !== this.activeTabIndex) this.updateActiveTab(index)
|
|
238
316
|
},
|
|
239
|
-
items
|
|
240
|
-
|
|
241
|
-
|
|
317
|
+
items: {
|
|
318
|
+
handler () {
|
|
319
|
+
this.refreshTabs()
|
|
242
320
|
|
|
243
|
-
|
|
321
|
+
if (this.tabs.length) this.reopenTheActiveTab()
|
|
322
|
+
|
|
323
|
+
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
324
|
+
},
|
|
325
|
+
deep: true
|
|
244
326
|
},
|
|
245
327
|
fillBar () {
|
|
246
328
|
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
@@ -381,8 +463,10 @@ export default {
|
|
|
381
463
|
.w-tabs-slide-left-leave-active,
|
|
382
464
|
.w-tabs-slide-right-leave-active {
|
|
383
465
|
position: absolute;
|
|
466
|
+
top: 0;
|
|
384
467
|
left: 0;
|
|
385
468
|
right: 0;
|
|
469
|
+
overflow: hidden;
|
|
386
470
|
}
|
|
387
471
|
|
|
388
472
|
.w-tabs-slide-left-enter-active {animation: w-tabs-slide-left-enter $transition-duration + 0.15s;}
|
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
<script>
|
|
7
7
|
// Keep-alive only works with components, not with DOM nodes.
|
|
8
|
+
|
|
8
9
|
export default {
|
|
9
|
-
|
|
10
|
+
name: 'tab-content', // Keep-alive include/exclude mechanism is component-name-based.
|
|
11
|
+
|
|
12
|
+
props: { item: Object },
|
|
13
|
+
|
|
14
|
+
mounted () {
|
|
15
|
+
console.log('mounted!', this.item._uid)
|
|
16
|
+
}
|
|
10
17
|
}
|
|
11
18
|
</script>
|