1 | <template>
|
2 | <nav class="md-tab-bar">
|
3 | <div class="md-tab-bar-inner" ref="wrapper">
|
4 | <template v-if="scrollable">
|
5 | <div class="md-tab-bar-start" v-show="maskStartShown"></div>
|
6 | <div class="md-tab-bar-end" v-show="maskEndShown"></div>
|
7 | </template>
|
8 | <md-scroll-view
|
9 | ref="scroller"
|
10 | :scrolling-x="scrollable"
|
11 | :scrolling-y="false"
|
12 | :key="scrollerTmpKey"
|
13 | @scroll="$_onScroll"
|
14 | >
|
15 | <div class="md-tab-bar-list" :style="{width: contentW + 'px'}">
|
16 | <a
|
17 | class="md-tab-bar-item"
|
18 | :class="{
|
19 | 'is-active': currentName === item.name,
|
20 | 'is-disabled': !!item.disabled
|
21 | }"
|
22 | v-for="(item, index) in items"
|
23 | :key="item.name"
|
24 | ref="items"
|
25 | @click="$_onClick(item, index)"
|
26 | >
|
27 | <slot
|
28 | name="item"
|
29 | :item="item"
|
30 | :items="items"
|
31 | :index="index"
|
32 | :currentName="currentName"
|
33 | >{{ item.label }}</slot>
|
34 | </a>
|
35 | </div>
|
36 | <span
|
37 | class="md-tab-bar-ink"
|
38 | :class="{
|
39 | 'is-disabled': currentTab && currentTab.disabled
|
40 | }"
|
41 | v-if="hasInk"
|
42 | :style="{
|
43 | width: inkWidth + 'px',
|
44 | transform: 'translateX(' + inkPos + 'px)',
|
45 | }"
|
46 | ></span>
|
47 | </md-scroll-view>
|
48 | </div>
|
49 | </nav>
|
50 | </template>
|
51 |
|
52 | <script>
import ScrollView from '../scroll-view'
|
53 |
|
54 | export default {
|
55 | name: 'md-tab-bar',
|
56 |
|
57 | components: {
|
58 | [ScrollView.name]: ScrollView,
|
59 | },
|
60 |
|
61 | props: {
|
62 | value: {
|
63 | type: [String, Number],
|
64 | default: '',
|
65 | },
|
66 | items: {
|
67 | type: Array,
|
68 | default: () => [],
|
69 | },
|
70 | hasInk: {
|
71 | type: Boolean,
|
72 | default: true,
|
73 | },
|
74 | inkLength: {
|
75 | type: Number,
|
76 | default: 100,
|
77 | },
|
78 | immediate: {
|
79 | type: Boolean,
|
80 | default: false,
|
81 | },
|
82 | },
|
83 |
|
84 | data() {
|
85 | return {
|
86 | currentName: '',
|
87 | wrapperW: 0,
|
88 | contentW: 0,
|
89 | inkWidth: 0,
|
90 | inkPos: 0,
|
91 | scrollerTmpKey: Date.now(),
|
92 | maskStartShown: false,
|
93 | maskEndShown: true,
|
94 | }
|
95 | },
|
96 |
|
97 | computed: {
|
98 | scrollable() {
|
99 | return this.contentW > this.wrapperW
|
100 | },
|
101 | currentIndex() {
|
102 | for (let i = 0, len = this.items.length; i < len; i++) {
|
103 | if (this.items[i].name === this.currentName) {
|
104 | return i
|
105 | }
|
106 | }
|
107 | },
|
108 | currentTab() {
|
109 | if (this.currentIndex) {
|
110 | return this.items[this.currentIndex]
|
111 | }
|
112 | },
|
113 | },
|
114 |
|
115 | watch: {
|
116 | value: {
|
117 | immediate: true,
|
118 | handler(val) {
|
119 | if (val !== this.currentName) {
|
120 | this.currentName = val
|
121 | }
|
122 | },
|
123 | },
|
124 | inkWidth() {
|
125 |
|
126 | this.$nextTick(function() {
|
127 | this.reflow()
|
128 | })
|
129 | },
|
130 | items() {
|
131 |
|
132 | this.$nextTick(function() {
|
133 | this.reflow()
|
134 | })
|
135 | },
|
136 | currentIndex() {
|
137 | this.$nextTick(function() {
|
138 | this.reflow()
|
139 | })
|
140 | },
|
141 | scrollable() {
|
142 |
|
143 | this.scrollerTmpKey = Date.now()
|
144 | },
|
145 | },
|
146 |
|
147 | created() {
|
148 | if (this.currentName === '' && this.items.length) {
|
149 | this.currentName = this.items[0].name
|
150 | this.$emit('change', this.items[0], 0, 0)
|
151 | }
|
152 | },
|
153 | mounted() {
|
154 | this.$_resizeEnterBehavior()
|
155 | },
|
156 | activated() {
|
157 |
|
158 | this.$_resizeEnterBehavior()
|
159 | },
|
160 | deactivated() {
|
161 |
|
162 | this.$_resizeLeaveBehavior()
|
163 | },
|
164 | beforeDestroy() {
|
165 | this.$_resizeLeaveBehavior()
|
166 | },
|
167 |
|
168 | methods: {
|
169 |
|
170 | $_onScroll({scrollLeft}) {
|
171 | if (scrollLeft > 0) {
|
172 | this.maskStartShown = true
|
173 | } else {
|
174 | this.maskStartShown = false
|
175 | }
|
176 |
|
177 | if (scrollLeft >= this.$refs.scroller.contentW - this.$refs.scroller.containerW) {
|
178 | this.maskEndShown = false
|
179 | } else {
|
180 | this.maskEndShown = true
|
181 | }
|
182 | },
|
183 | $_onClick(item, index) {
|
184 | if (item.disabled) {
|
185 | return
|
186 | }
|
187 | this.$emit('change', item, index, this.currentIndex)
|
188 | this.currentName = item.name
|
189 | this.$emit('input', item.name)
|
190 | },
|
191 | $_resizeEnterBehavior() {
|
192 | window.addEventListener('resize', this.reflow)
|
193 | this.reflow()
|
194 |
|
195 | if (this.immediate) {
|
196 | this.$nextTick(() => {
|
197 | this.$emit('change', this.items[this.currentIndex], this.currentIndex)
|
198 | })
|
199 | }
|
200 | },
|
201 | $_resizeLeaveBehavior() {
|
202 | window.removeEventListener('resize', this.reflow)
|
203 | },
|
204 |
|
205 | reflow() {
|
206 |
|
207 | if (!this.$refs.items || this.$refs.items.length === 0) {
|
208 | return
|
209 | }
|
210 |
|
211 | const wrapperReact = this.$refs.wrapper.getBoundingClientRect()
|
212 | this.wrapperW = wrapperReact.width
|
213 |
|
214 | let contentWidth = 0
|
215 | for (let i = 0, len = this.items.length; i < len; i++) {
|
216 | const {width} = this.$refs.items[i].getBoundingClientRect()
|
217 | contentWidth += width
|
218 | }
|
219 | this.contentW = contentWidth
|
220 | this.$refs.scroller.reflowScroller()
|
221 | this.$nextTick(() => {
|
222 |
|
223 | if (!this.$refs.items || !this.$refs.items[this.currentIndex]) {
|
224 | return
|
225 | }
|
226 |
|
227 | const target = this.$refs.items[this.currentIndex]
|
228 | this.inkWidth = target.offsetWidth * this.inkLength / 100
|
229 | this.inkPos = target.offsetLeft + (target.offsetWidth - this.inkWidth) / 2
|
230 |
|
231 | const prevTarget = this.$refs.items[this.currentIndex - 1]
|
232 | const nextTarget = this.$refs.items[this.currentIndex + 1]
|
233 |
|
234 | if (!prevTarget) {
|
235 | this.$refs.scroller.scrollTo(0, 0, true)
|
236 | return
|
237 | }
|
238 |
|
239 | if (!nextTarget) {
|
240 | this.$refs.scroller.scrollTo(this.contentW, 0, true)
|
241 | return
|
242 | }
|
243 |
|
244 | const wrapperRect = this.$refs.wrapper.getBoundingClientRect()
|
245 | const prevTargetRect = prevTarget.getBoundingClientRect()
|
246 | const nextTargetRect = nextTarget.getBoundingClientRect()
|
247 |
|
248 |
|
249 | if (prevTargetRect && prevTargetRect.left < wrapperRect.left) {
|
250 | this.$refs.scroller.scrollTo(prevTarget.offsetLeft, 0, true)
|
251 | } else if (nextTargetRect && nextTargetRect.right > wrapperRect.right) {
|
252 | this.$refs.scroller.scrollTo(nextTarget.offsetLeft + nextTarget.offsetWidth - this.wrapperW, 0, true)
|
253 | }
|
254 | })
|
255 | },
|
256 | },
|
257 | }
|
258 |
</script>
|
259 |
|
260 | <style lang="stylus">
|
261 | .md-tab-bar
|
262 | position relative
|
263 | padding-left tab-offset
|
264 | padding-right tab-offset
|
265 | background-color tab-bg
|
266 |
|
267 | .md-tab-bar-inner
|
268 | position relative
|
269 | width 100%
|
270 | // line-height 0
|
271 |
|
272 | .md-tab-bar-list
|
273 | display flex
|
274 | justify-content space-between
|
275 | min-width 100%
|
276 |
|
277 | .md-tab-bar-item
|
278 | flex auto
|
279 | flex-shrink 0
|
280 | position relative
|
281 | display inline-flex
|
282 | align-items center
|
283 | justify-content center
|
284 | color tab-text-color
|
285 | font-size tab-font-size
|
286 | font-weight tab-font-weight
|
287 | min-height tab-height
|
288 | padding 0 tab-item-gap
|
289 | margin 0 auto
|
290 | box-sizing border-box
|
291 | -webkit-user-select none
|
292 | -webkit-tap-highlight-color transparent
|
293 | &.is-active
|
294 | color tab-active-color
|
295 | &.is-disabled
|
296 | color tab-disabled-color
|
297 |
|
298 | .md-tab-bar-ink
|
299 | position absolute
|
300 | bottom 0
|
301 | left 0
|
302 | display block
|
303 | height tab-ink-height
|
304 | background-color tab-active-color
|
305 | transition all 300ms
|
306 | &.is-disabled
|
307 | background-color tab-disabled-color
|
308 |
|
309 | .md-tab-bar-start,
|
310 | .md-tab-bar-end
|
311 | position absolute
|
312 | top 0
|
313 | left 0
|
314 | width 14px
|
315 | height tab-height
|
316 | overflow hidden
|
317 | &::after
|
318 | content ''
|
319 | display block
|
320 | position absolute
|
321 | left -14px
|
322 | top 50%
|
323 | width 14px
|
324 | if tab-height is a 'unit'
|
325 | margin-top 0 - tab-height * 0.4
|
326 | height tab-height * 0.8
|
327 | else
|
328 | margin-top "calc(0 - %s * 0.4)" % tab-height
|
329 | height "calc(%s * 0.8)" % tab-height
|
330 | border-radius 50%
|
331 | box-shadow: -1px 0 12px 0 rgba(0,0,0,0.2)
|
332 | .md-tab-bar-end
|
333 | left auto
|
334 | right 0
|
335 | transform rotate(180deg)
|
336 |
|
337 | .md-tab-bar
|
338 | .md-scroll-view
|
339 | display block
|
340 | .scroll-view-container
|
341 | min-width 100%
|
342 | </style>
|