Commit ec1a5e268486e40269f82d36b47e8c0a05d87abc
Committed by
GitHub
Merge pull request #2156 from marxy/tabs-scroll
Tabs add scroll
Showing
3 changed files
with
161 additions
and
6 deletions
Show diff stats
examples/routers/tabs.vue
@@ -70,7 +70,7 @@ | @@ -70,7 +70,7 @@ | ||
70 | <!--</script>--> | 70 | <!--</script>--> |
71 | 71 | ||
72 | 72 | ||
73 | -<template> | 73 | +<!-- <template> |
74 | <Tabs type="card" closable @on-tab-remove="handleTabRemove"> | 74 | <Tabs type="card" closable @on-tab-remove="handleTabRemove"> |
75 | <TabPane label="标签一" v-if="tab0">标签一的内容</TabPane> | 75 | <TabPane label="标签一" v-if="tab0">标签一的内容</TabPane> |
76 | <TabPane label="标签二" v-if="tab1">标签二的内容</TabPane> | 76 | <TabPane label="标签二" v-if="tab1">标签二的内容</TabPane> |
@@ -92,4 +92,41 @@ | @@ -92,4 +92,41 @@ | ||
92 | } | 92 | } |
93 | } | 93 | } |
94 | } | 94 | } |
95 | +</script> --> | ||
96 | + | ||
97 | +<template> | ||
98 | + <div> | ||
99 | + <Button type="ghost" @click="toFirst" size="small">to first</Button> | ||
100 | + <Button type="ghost" @click="toLast" size="small">to last</Button> | ||
101 | + <Tabs type="card" :animated="animated" v-model="activeTab"> | ||
102 | + <TabPane v-for="tab in tabs" :key="tab" :label="'标签' + tab" :name="tab+''" closable>标签{{ tab }}</TabPane> | ||
103 | + <div slot="extra"> | ||
104 | + <Button type="ghost" @click="handleTabsAdd" size="small">增加</Button> | ||
105 | + </div> | ||
106 | + </Tabs> | ||
107 | + </div> | ||
108 | +</template> | ||
109 | +<script> | ||
110 | + export default { | ||
111 | + data () { | ||
112 | + return { | ||
113 | + tabs: 2, | ||
114 | + activeTab:"2", | ||
115 | + animated:true | ||
116 | + } | ||
117 | + }, | ||
118 | + methods: { | ||
119 | + handleTabsAdd () { | ||
120 | + this.tabs ++; | ||
121 | + this.activeTab = this.tabs + ''; | ||
122 | + }, | ||
123 | + toFirst () { | ||
124 | + this.activeTab = '1'; | ||
125 | + }, | ||
126 | + toLast () { | ||
127 | + this.activeTab = this.tabs+''; | ||
128 | + } | ||
129 | + } | ||
130 | + } | ||
95 | </script> | 131 | </script> |
132 | + |
src/components/tabs/tabs.vue
1 | <template> | 1 | <template> |
2 | <div :class="classes"> | 2 | <div :class="classes"> |
3 | <div :class="[prefixCls + '-bar']"> | 3 | <div :class="[prefixCls + '-bar']"> |
4 | + <div :class="[prefixCls + '-nav-right']" v-if="showSlot"><slot name="extra"></slot></div> | ||
4 | <div :class="[prefixCls + '-nav-container']"> | 5 | <div :class="[prefixCls + '-nav-container']"> |
5 | - <div :class="[prefixCls + '-nav-wrap']"> | ||
6 | - <div :class="[prefixCls + '-nav-scroll']"> | ||
7 | - <div :class="[prefixCls + '-nav']" ref="nav"> | 6 | + <div ref="navWrap" :class="[prefixCls + '-nav-wrap', scrollable ? prefixCls + '-nav-scrollable' : '']" > |
7 | + <span :class="[prefixCls + '-nav-prev', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollPrev"><Icon type="chevron-left"></Icon></span> | ||
8 | + <span :class="[prefixCls + '-nav-next', scrollable ? '' : prefixCls + '-nav-scroll-disabled']" @click="scrollNext"><Icon type="chevron-right"></Icon></span> | ||
9 | + <div ref="navScroll" :class="[prefixCls + '-nav-scroll']"> | ||
10 | + <div ref="nav" :class="[prefixCls + '-nav']" class="nav-text" :style="navStyle"> | ||
8 | <div :class="barClasses" :style="barStyle"></div> | 11 | <div :class="barClasses" :style="barStyle"></div> |
9 | <div :class="tabCls(item)" v-for="(item, index) in navList" @click="handleChange(index)"> | 12 | <div :class="tabCls(item)" v-for="(item, index) in navList" @click="handleChange(index)"> |
10 | <Icon v-if="item.icon !== ''" :type="item.icon"></Icon> | 13 | <Icon v-if="item.icon !== ''" :type="item.icon"></Icon> |
@@ -13,7 +16,6 @@ | @@ -13,7 +16,6 @@ | ||
13 | <Icon v-if="showClose(item)" type="ios-close-empty" @click.native.stop="handleRemove(index)"></Icon> | 16 | <Icon v-if="showClose(item)" type="ios-close-empty" @click.native.stop="handleRemove(index)"></Icon> |
14 | </div> | 17 | </div> |
15 | </div> | 18 | </div> |
16 | - <div :class="[prefixCls + '-nav-right']" v-if="showSlot"><slot name="extra"></slot></div> | ||
17 | </div> | 19 | </div> |
18 | </div> | 20 | </div> |
19 | </div> | 21 | </div> |
@@ -26,6 +28,7 @@ | @@ -26,6 +28,7 @@ | ||
26 | import Render from '../base/render'; | 28 | import Render from '../base/render'; |
27 | import { oneOf } from '../../utils/assist'; | 29 | import { oneOf } from '../../utils/assist'; |
28 | import Emitter from '../../mixins/emitter'; | 30 | import Emitter from '../../mixins/emitter'; |
31 | + import elementResizeDetectorMaker from 'element-resize-detector'; | ||
29 | 32 | ||
30 | const prefixCls = 'ivu-tabs'; | 33 | const prefixCls = 'ivu-tabs'; |
31 | 34 | ||
@@ -65,7 +68,11 @@ | @@ -65,7 +68,11 @@ | ||
65 | barWidth: 0, | 68 | barWidth: 0, |
66 | barOffset: 0, | 69 | barOffset: 0, |
67 | activeKey: this.value, | 70 | activeKey: this.value, |
68 | - showSlot: false | 71 | + showSlot: false, |
72 | + navStyle:{ | ||
73 | + transform: '' | ||
74 | + }, | ||
75 | + scrollable:false | ||
69 | }; | 76 | }; |
70 | }, | 77 | }, |
71 | computed: { | 78 | computed: { |
@@ -163,6 +170,7 @@ | @@ -163,6 +170,7 @@ | ||
163 | } else { | 170 | } else { |
164 | this.barOffset = 0; | 171 | this.barOffset = 0; |
165 | } | 172 | } |
173 | + this.updateNavScroll(); | ||
166 | }); | 174 | }); |
167 | }, | 175 | }, |
168 | updateStatus () { | 176 | updateStatus () { |
@@ -222,6 +230,85 @@ | @@ -222,6 +230,85 @@ | ||
222 | } else { | 230 | } else { |
223 | return false; | 231 | return false; |
224 | } | 232 | } |
233 | + }, | ||
234 | + scrollPrev() { | ||
235 | + const containerWidth = this.$refs.navScroll.offsetWidth; | ||
236 | + const currentOffset = this.getCurrentScrollOffset(); | ||
237 | + | ||
238 | + if (!currentOffset) return; | ||
239 | + | ||
240 | + let newOffset = currentOffset > containerWidth | ||
241 | + ? currentOffset - containerWidth | ||
242 | + : 0; | ||
243 | + | ||
244 | + this.setOffset(newOffset); | ||
245 | + }, | ||
246 | + scrollNext() { | ||
247 | + const navWidth = this.$refs.nav.offsetWidth; | ||
248 | + const containerWidth = this.$refs.navScroll.offsetWidth; | ||
249 | + const currentOffset = this.getCurrentScrollOffset(); | ||
250 | + if (navWidth - currentOffset <= containerWidth) return; | ||
251 | + | ||
252 | + let newOffset = navWidth - currentOffset > containerWidth * 2 | ||
253 | + ? currentOffset + containerWidth | ||
254 | + : (navWidth - containerWidth); | ||
255 | + | ||
256 | + this.setOffset(newOffset); | ||
257 | + }, | ||
258 | + getCurrentScrollOffset() { | ||
259 | + const { navStyle } = this; | ||
260 | + return navStyle.transform | ||
261 | + ? Number(navStyle.transform.match(/translateX\(-(\d+(\.\d+)*)px\)/)[1]) | ||
262 | + : 0; | ||
263 | + }, | ||
264 | + setOffset(value) { | ||
265 | + this.navStyle.transform = `translateX(-${value}px)`; | ||
266 | + }, | ||
267 | + scrollToActiveTab() { | ||
268 | + if (!this.scrollable) return; | ||
269 | + const nav = this.$refs.nav; | ||
270 | + const activeTab = this.$el.querySelector(`.${prefixCls}-tab-active`); | ||
271 | + if(!activeTab) return; | ||
272 | + | ||
273 | + const navScroll = this.$refs.navScroll; | ||
274 | + const activeTabBounding = activeTab.getBoundingClientRect(); | ||
275 | + const navScrollBounding = navScroll.getBoundingClientRect(); | ||
276 | + const navBounding = nav.getBoundingClientRect(); | ||
277 | + const currentOffset = this.getCurrentScrollOffset(); | ||
278 | + let newOffset = currentOffset; | ||
279 | + | ||
280 | + if (navBounding.right < navScrollBounding.right) { | ||
281 | + newOffset = nav.offsetWidth - navScrollBounding.width; | ||
282 | + } | ||
283 | + | ||
284 | + if (activeTabBounding.left < navScrollBounding.left) { | ||
285 | + newOffset = currentOffset - (navScrollBounding.left - activeTabBounding.left); | ||
286 | + }else if (activeTabBounding.right > navScrollBounding.right) { | ||
287 | + newOffset = currentOffset + activeTabBounding.right - navScrollBounding.right; | ||
288 | + } | ||
289 | + | ||
290 | + if(currentOffset !== newOffset){ | ||
291 | + this.setOffset(Math.max(newOffset, 0)); | ||
292 | + } | ||
293 | + }, | ||
294 | + updateNavScroll(){ | ||
295 | + const navWidth = this.$refs.nav.offsetWidth; | ||
296 | + const containerWidth = this.$refs.navScroll.offsetWidth; | ||
297 | + const currentOffset = this.getCurrentScrollOffset(); | ||
298 | + if (containerWidth < navWidth) { | ||
299 | + this.scrollable = true; | ||
300 | + if (navWidth - currentOffset < containerWidth) { | ||
301 | + this.setOffset(navWidth - containerWidth); | ||
302 | + } | ||
303 | + } else { | ||
304 | + this.scrollable = false; | ||
305 | + if (currentOffset > 0) { | ||
306 | + this.setOffset(0); | ||
307 | + } | ||
308 | + } | ||
309 | + }, | ||
310 | + handleResize(){ | ||
311 | + this.updateNavScroll(); | ||
225 | } | 312 | } |
226 | }, | 313 | }, |
227 | watch: { | 314 | watch: { |
@@ -232,10 +319,18 @@ | @@ -232,10 +319,18 @@ | ||
232 | this.updateBar(); | 319 | this.updateBar(); |
233 | this.updateStatus(); | 320 | this.updateStatus(); |
234 | this.broadcast('Table', 'on-visible-change', true); | 321 | this.broadcast('Table', 'on-visible-change', true); |
322 | + this.$nextTick(function(){ | ||
323 | + this.scrollToActiveTab(); | ||
324 | + }); | ||
235 | } | 325 | } |
236 | }, | 326 | }, |
237 | mounted () { | 327 | mounted () { |
238 | this.showSlot = this.$slots.extra !== undefined; | 328 | this.showSlot = this.$slots.extra !== undefined; |
329 | + this.observer = elementResizeDetectorMaker(); | ||
330 | + this.observer.listenTo(this.$refs.navWrap, this.handleResize); | ||
331 | + }, | ||
332 | + beforeDestroy() { | ||
333 | + this.observer.removeListener(this.$refs.navWrap, this.handleResize); | ||
239 | } | 334 | } |
240 | }; | 335 | }; |
241 | </script> | 336 | </script> |
src/styles/components/tabs.less
@@ -56,6 +56,29 @@ | @@ -56,6 +56,29 @@ | ||
56 | 56 | ||
57 | &-nav-right{ | 57 | &-nav-right{ |
58 | float: right; | 58 | float: right; |
59 | + margin-left: 5px; | ||
60 | + } | ||
61 | + | ||
62 | + &-nav-prev{ | ||
63 | + position:absolute; | ||
64 | + line-height: 32px; | ||
65 | + cursor: pointer; | ||
66 | + left:0; | ||
67 | + } | ||
68 | + | ||
69 | + &-nav-next{ | ||
70 | + position:absolute; | ||
71 | + line-height: 32px; | ||
72 | + cursor: pointer; | ||
73 | + right:0; | ||
74 | + } | ||
75 | + | ||
76 | + &-nav-scrollable{ | ||
77 | + padding: 0 12px; | ||
78 | + } | ||
79 | + | ||
80 | + &-nav-scroll-disabled{ | ||
81 | + display: none; | ||
59 | } | 82 | } |
60 | 83 | ||
61 | &-nav { | 84 | &-nav { |