wave-ui 3.3.0 → 3.4.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/dist/wave-ui.cjs.js +1 -1
- package/dist/wave-ui.css +1 -1
- package/dist/wave-ui.es.js +1318 -1109
- 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 +144 -57
- package/src/wave-ui/components/w-tabs/tab-content.vue +8 -1
- 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.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",
|
|
@@ -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,34 +263,42 @@ 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
|
-
this.activeTabIndex = index
|
|
202
274
|
|
|
203
|
-
//
|
|
204
|
-
this
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
275
|
+
// Only open the tab if it is found.
|
|
276
|
+
if (this.tabs[index]?._uid) {
|
|
277
|
+
this.openTab(this.tabs[index]?._uid)
|
|
278
|
+
|
|
279
|
+
// Scroll the new active tab item title into view if needed.
|
|
280
|
+
this.$nextTick(() => {
|
|
281
|
+
const ref = this.$refs['tabs-bar']
|
|
282
|
+
this.activeTabEl = ref && ref.querySelector(`.w-tabs__bar-item:nth-child(${index + 1})`)
|
|
283
|
+
if (this.activeTabEl) {
|
|
284
|
+
this.activeTabEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
211
288
|
},
|
|
212
289
|
|
|
213
290
|
// Return the original item (so there is no `_index`, etc.).
|
|
214
291
|
getOriginalItem (item) {
|
|
215
|
-
return this.items[item._index]
|
|
292
|
+
return this.items[item._index] || {}
|
|
216
293
|
}
|
|
217
294
|
},
|
|
218
295
|
|
|
219
296
|
beforeMount () {
|
|
220
|
-
this.
|
|
297
|
+
this.tabs = [] // Reset for hot-reloading.
|
|
298
|
+
const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items
|
|
299
|
+
items.forEach(this.addTab)
|
|
300
|
+
|
|
301
|
+
if (this.modelValue ?? false) this.updateActiveTab(this.modelValue)
|
|
221
302
|
|
|
222
303
|
this.$nextTick(() => {
|
|
223
304
|
this.updateSlider()
|
|
@@ -234,13 +315,17 @@ export default {
|
|
|
234
315
|
|
|
235
316
|
watch: {
|
|
236
317
|
modelValue (index) {
|
|
237
|
-
this.updateActiveTab(index)
|
|
318
|
+
if (index !== this.activeTabIndex) this.updateActiveTab(index)
|
|
238
319
|
},
|
|
239
|
-
items
|
|
240
|
-
|
|
241
|
-
|
|
320
|
+
items: {
|
|
321
|
+
handler () {
|
|
322
|
+
this.refreshTabs()
|
|
242
323
|
|
|
243
|
-
|
|
324
|
+
if (this.tabs.length) this.reopenTheActiveTab()
|
|
325
|
+
|
|
326
|
+
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
327
|
+
},
|
|
328
|
+
deep: true
|
|
244
329
|
},
|
|
245
330
|
fillBar () {
|
|
246
331
|
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
@@ -381,8 +466,10 @@ export default {
|
|
|
381
466
|
.w-tabs-slide-left-leave-active,
|
|
382
467
|
.w-tabs-slide-right-leave-active {
|
|
383
468
|
position: absolute;
|
|
469
|
+
top: 0;
|
|
384
470
|
left: 0;
|
|
385
471
|
right: 0;
|
|
472
|
+
overflow: hidden;
|
|
386
473
|
}
|
|
387
474
|
|
|
388
475
|
.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>
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<template lang="pug">
|
|
2
|
-
.w-scrollbar
|
|
3
|
-
</template>
|
|
4
|
-
|
|
5
|
-
<script>
|
|
6
|
-
export default {
|
|
7
|
-
name: 'w-scrollbar',
|
|
8
|
-
props: {
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
emits: [],
|
|
13
|
-
|
|
14
|
-
data: () => ({
|
|
15
|
-
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<style lang="scss">
|
|
21
|
-
.w-scrollbar {
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
</style>
|