tabs组件支持滑动 (#52)

This commit is contained in:
张敏 2017-07-11 14:35:39 +08:00 committed by GitHub
parent aaaba3888f
commit 2cc8f2ae5e
15 changed files with 331 additions and 62 deletions

View File

@ -4,7 +4,6 @@
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
</example-block><example-block title="基础用法"> </example-block><example-block title="基础用法">
@ -13,7 +12,6 @@
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
</example-block><example-block title="设置切换tab的动画时间"> </example-block><example-block title="设置切换tab的动画时间">
@ -23,13 +21,24 @@
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
</van-tabs> </van-tabs>
</example-block><example-block title="禁用tab"> </example-block><example-block title="多于4个tab时">
<van-tabs> <van-tabs>
<van-tab title="选项">内容一</van-tab> <van-tab title="选项">内容一</van-tab>
<van-tab title="选项二" disabled @disabled="popalert">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab> <van-tab title="选项五">内容五</van-tab>
<van-tab title="选项六">内容六</van-tab>
<van-tab title="选项七">内容七</van-tab>
<van-tab title="选项八">内容八</van-tab>
</van-tabs>
</example-block><example-block title="禁用tab">
<van-tabs>
<van-tab title="选项一">内容一</van-tab>
<van-tab title="选项二" disabled @disabled="popalert">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab>
</van-tabs> </van-tabs>
@ -40,7 +49,6 @@
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
</example-block><example-block title="自定义样式"> </example-block><example-block title="自定义样式">
@ -49,7 +57,6 @@
<van-tab title="选项二" class="custom-pane">内容二</van-tab> <van-tab title="选项二" class="custom-pane">内容二</van-tab>
<van-tab title="选项三" class="custom-pane">内容三</van-tab> <van-tab title="选项三" class="custom-pane">内容三</van-tab>
<van-tab title="选项四" class="custom-pane">内容四</van-tab> <van-tab title="选项四" class="custom-pane">内容四</van-tab>
<van-tab title="选项五" class="custom-pane">内容五</van-tab>
</van-tabs> </van-tabs>
@ -60,7 +67,6 @@
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>

View File

@ -97,7 +97,6 @@ export default {
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
``` ```
::: :::
@ -113,7 +112,6 @@ export default {
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
``` ```
::: :::
@ -132,6 +130,25 @@ export default {
``` ```
::: :::
#### 多于4个tab时
多于4个tab时可以横向滚动tab。
:::demo 多于4个tab时
```html
<van-tabs>
<van-tab title="选项一">内容一</van-tab>
<van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
<van-tab title="选项六">内容六</van-tab>
<van-tab title="选项七">内容七</van-tab>
<van-tab title="选项八">内容八</van-tab>
</van-tabs>
```
:::
#### 禁用tab #### 禁用tab
在对应的`van-tab`上设置`disabled`属性即可,如果需要监听禁用事件,可以监听`disabled`事件。 在对应的`van-tab`上设置`disabled`属性即可,如果需要监听禁用事件,可以监听`disabled`事件。
@ -139,11 +156,10 @@ export default {
:::demo 禁用tab :::demo 禁用tab
```html ```html
<van-tabs> <van-tabs>
<van-tab title="选项">内容一</van-tab> <van-tab title="选项">内容一</van-tab>
<van-tab title="选项二" disabled @disabled="popalert">内容二</van-tab> <van-tab title="选项二" disabled @disabled="popalert">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
<script> <script>
@ -169,7 +185,6 @@ export default {
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
``` ```
::: :::
@ -198,7 +213,6 @@ export default {
<van-tab title="选项二" class="custom-pane">内容二</van-tab> <van-tab title="选项二" class="custom-pane">内容二</van-tab>
<van-tab title="选项三" class="custom-pane">内容三</van-tab> <van-tab title="选项三" class="custom-pane">内容三</van-tab>
<van-tab title="选项四" class="custom-pane">内容四</van-tab> <van-tab title="选项四" class="custom-pane">内容四</van-tab>
<van-tab title="选项五" class="custom-pane">内容五</van-tab>
</van-tabs> </van-tabs>
<style> <style>
@ -228,7 +242,6 @@ export default {
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab> <van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
<script> <script>

View File

@ -32,6 +32,7 @@ const showNextDialog = () => {
initInstance(); initInstance();
} }
/* istanbul ignore else */
if (!instance.value && dialogQueue.length > 0) { if (!instance.value && dialogQueue.length > 0) {
currentDialog = dialogQueue.shift(); currentDialog = dialogQueue.shift();

View File

@ -6,6 +6,7 @@ let instance;
const ImagePreviewConstructor = Vue.extend(ImagePreview); const ImagePreviewConstructor = Vue.extend(ImagePreview);
const initInstance = () => { const initInstance = () => {
/* istanbul ignore if */
if (Vue.prototype.$isServer) return; if (Vue.prototype.$isServer) return;
instance = new ImagePreviewConstructor({ instance = new ImagePreviewConstructor({
el: document.createElement('div') el: document.createElement('div')
@ -13,6 +14,7 @@ const initInstance = () => {
}; };
var ImagePreviewBox = images => { var ImagePreviewBox = images => {
/* istanbul ignore if */
if (Vue.prototype.$isServer) return; if (Vue.prototype.$isServer) return;
if (!instance) { if (!instance) {
initInstance(); initInstance();

View File

@ -92,6 +92,7 @@ export default {
const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window; const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window;
const container = this.$refs.previewContainer; const container = this.$refs.previewContainer;
/* istanbul ignore else */
if (supportTouch) { if (supportTouch) {
let touchStartTime; let touchStartTime;
@ -99,6 +100,7 @@ export default {
touchStartTime = new Date(); touchStartTime = new Date();
}); });
container.addEventListener('touchend', () => { container.addEventListener('touchend', () => {
/* istanbul ignore else */
if (new Date() - touchStartTime < 1500) { if (new Date() - touchStartTime < 1500) {
this.value = false; this.value = false;
} }

View File

@ -194,6 +194,7 @@ export default {
}, },
end: () => { end: () => {
/* istanbul ignore else */
if (this.isDragging) { if (this.isDragging) {
this.isDragging = false; this.isDragging = false;

46
packages/tab/src/swipe.js Normal file
View File

@ -0,0 +1,46 @@
import Vue from 'vue';
let isSwiping = false;
const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window;
export default function(element, options) {
const moveFn = function(event) {
if (options.drag) {
options.drag(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
};
const endFn = function(event) {
if (!supportTouch) {
document.removeEventListener('mousemove', moveFn);
document.removeEventListener('mouseup', endFn);
}
isSwiping = false;
if (options.end) {
options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
};
element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) {
if (isSwiping) return;
if (!supportTouch) {
document.addEventListener('mousemove', moveFn);
document.addEventListener('mouseup', endFn);
}
isSwiping = true;
if (options.start) {
options.start(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
});
if (supportTouch) {
element.addEventListener('touchmove', moveFn);
element.addEventListener('touchend', endFn);
element.addEventListener('touchcancel', endFn);
}
};

View File

@ -1,8 +1,26 @@
<template> <template>
<div class="van-tabs" :class="[`van-tabs--${type}`]"> <div class="van-tabs" :class="[`van-tabs--${type}`]">
<div class="van-tabs__nav-wrap" v-if="type === 'line' && tabs.length > 4">
<div class="van-tabs__swipe" ref="swipe">
<div class="van-tabs__nav van-tabs__nav--line">
<div class="van-tabs__nav-bar" :style="navBarStyle"></div>
<div
v-for="(tab, index) in tabs"
:key="index"
class="van-tab"
:class="{'van-tab--active': index === curActive}"
ref="tabkey"
@click="handleTabClick(index, tab)"
>
{{ tab.title }}
</div>
</div>
</div>
</div>
<div <div
v-else
class="van-tabs__nav" class="van-tabs__nav"
:class="[`van-tabs__nav--${this.type}`, `van-tabs--col-${this.tabs.length}`]" :class="[`van-tabs__nav--${this.type}`]"
> >
<div class="van-tabs__nav-bar" :style="navBarStyle" v-if="type === 'line'"></div> <div class="van-tabs__nav-bar" :style="navBarStyle" v-if="type === 'line'"></div>
<div <div
@ -23,6 +41,9 @@
</template> </template>
<script> <script>
import swipe from './swipe';
import translateUtil from 'src/utils/transition';
export default { export default {
name: 'van-tabs', name: 'van-tabs',
@ -48,13 +69,21 @@
return { return {
tabs: [], tabs: [],
isReady: false, isReady: false,
curActive: +this.active curActive: +this.active,
isSwiping: false
}; };
}, },
watch: { watch: {
active(val) { active(val) {
this.curActive = +val; this.curActive = +val;
},
curActive() {
/* istanbul ignore else */
if (this.tabs.length > 4) {
this.doOnValueChange();
}
} }
}, },
@ -72,12 +101,38 @@
return { return {
width: offsetWidth, width: offsetWidth,
transform: `translate3d(${offsetLeft}, 0px, 0px)`, transform: `translate3d(${offsetLeft}, 0, 0)`,
transitionDuration: `${this.duration}s` transitionDuration: `${this.duration}s`
}; };
},
swipeWidth() {
return this.$refs.swipe && this.$refs.swipe.getBoundingClientRect().width;
},
maxTranslate() {
/* istanbul ignore if */
if (!this.$refs.tabkey) return;
const lastTab = this.$refs.tabkey[this.tabs.length - 1];
const lastTabWidth = lastTab.offsetWidth;
const lastTabOffsetLeft = lastTab.offsetLeft;
return (lastTabOffsetLeft + lastTabWidth) - this.swipeWidth;
} }
}, },
mounted() {
//
this.$nextTick(() => {
// computednav-barcss
this.isReady = true;
this.initEvents();
if (this.tabs.length > 4) {
this.doOnValueChange();
}
});
},
methods: { methods: {
/** /**
* tab点击事件 * tab点击事件
@ -93,15 +148,67 @@
this.$emit('click', index); this.$emit('click', index);
this.curActive = index; this.curActive = index;
} },
},
mounted() { /**
// * 将当前value值转换为需要translate的值
this.$nextTick(() => { */
// computednav-barcss value2Translate(value) {
this.isReady = true; /* istanbul ignore if */
}); if (!this.$refs.tabkey) return 0;
const tab = this.$refs.tabkey[value];
const maxTranslate = this.maxTranslate;
const tabWidth = tab.offsetWidth;
const tabOffsetLeft = tab.offsetLeft;
let translate = tabOffsetLeft + (tabWidth * 2.7) - this.swipeWidth;
if (translate < 0) {
translate = 0;
}
return -1 * (translate > maxTranslate ? maxTranslate : translate);
},
initEvents() {
const el = this.$refs.swipe;
if (!el) return;
let swipeState = {};
swipe(el, {
start: event => {
swipeState = {
start: new Date(),
startLeft: event.pageX,
startTranslateLeft: translateUtil.getElementTranslate(el).left
};
},
drag: event => {
this.isSwiping = true;
swipeState.left = event.pageX;
const deltaX = swipeState.left - swipeState.startLeft;
const translate = swipeState.startTranslateLeft + deltaX;
/* istanbul ignore else */
if (translate > 0 || (translate * -1) > this.maxTranslate ) return;
translateUtil.translateElement(el, translate, null);
},
end: () => {
this.isSwiping = false;
}
})
},
doOnValueChange() {
const value = +this.curActive;
const swipe = this.$refs.swipe;
translateUtil.translateElement(swipe, this.value2Translate(value), null);
}
} }
}; };
</script> </script>

View File

@ -5,27 +5,20 @@
@b tabs { @b tabs {
position: relative; position: relative;
@m col-2 { @e nav-wrap {
.van-tab { overflow: hidden;
width: 50%;
}
} }
@m col-3 { @e swipe {
.van-tab { user-select: none;
width: 33.33333333333333%; transition: transform ease .3s;
}
}
@m col-4 {
.van-tab { .van-tab {
width: 25%; flex: 0 0 22%;
} }
}
@m col-5 { .van-tabs__nav {
.van-tab { overflow: visible;
width: 20%;
} }
} }
@ -33,16 +26,15 @@
overflow: hidden; overflow: hidden;
transition: transform .5s cubic-bezier(.645, .045, .355, 1); transition: transform .5s cubic-bezier(.645, .045, .355, 1);
position: relative; position: relative;
display: flex;
@m line { @m line {
height: 44px; height: 44px;
background-color: $c-white;
&::after { .van-tab {
@mixin border-retina (top); &::after {
@mixin border-retina (bottom); @mixin border-retina (top, bottom);
} }
@b tabs-nav-bar {
display: block;
} }
} }
@ -55,14 +47,16 @@
overflow: hidden; overflow: hidden;
.van-tab { .van-tab {
color: #666666; color: #666;
line-height: 28px; line-height: 28px;
border-right: 1px solid #666666; border-right: 1px solid #666;
&:last-child { &:last-child {
border-right: none; border-right: none;
} }
&.van-tab--active { &.van-tab--active {
background-color: #666666; background-color: #666;
color: $c-white; color: $c-white;
} }
} }
@ -82,13 +76,16 @@
} }
@b tab { @b tab {
position: relative;
color: $c-black; color: $c-black;
background-color: $c-white;
font-size: 14px; font-size: 14px;
line-height: 44px; line-height: 44px;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
float: left; flex: 1;
-webkit-tap-highlight-color: rgba(0,0,0,0);
@m active { @m active {
color: #FF4444; color: #FF4444;

View File

@ -0,0 +1,28 @@
<template>
<van-tabs :active="active">
<van-tab title="选项一">内容一</van-tab>
<van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三">内容三</van-tab>
<van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
<van-tab title="选项六">内容六</van-tab>
<van-tab title="选项七">内容七</van-tab>
<van-tab title="选项八">内容八</van-tab>
</van-tabs>
</template>
<script>
import Tab from 'packages/tab';
import Tabs from 'packages/tabs';
export default {
components: {
'van-tab': Tab,
'van-tabs': Tabs
},
props: {
active: [String, Number]
}
};
</script>

View File

@ -4,7 +4,6 @@
<van-tab title="选项二">内容二</van-tab> <van-tab title="选项二">内容二</van-tab>
<van-tab title="选项三" disabled>内容三</van-tab> <van-tab title="选项三" disabled>内容三</van-tab>
<van-tab title="选项四">内容四</van-tab> <van-tab title="选项四">内容四</van-tab>
<van-tab title="选项五">内容五</van-tab>
</van-tabs> </van-tabs>
</template> </template>

View File

@ -1,4 +1,5 @@
import Dialog from 'packages/dialog'; import Dialog from 'packages/dialog';
import Vue from 'vue';
describe('Dialog', () => { describe('Dialog', () => {
afterEach(() => { afterEach(() => {
@ -51,6 +52,6 @@ describe('Dialog', () => {
document.querySelector('.van-dialog__cancel').click(); document.querySelector('.van-dialog__cancel').click();
expect(dialogAction).to.equal('cancel'); expect(dialogAction).to.equal('cancel');
done(); done();
}, 50); }, 500);
}); });
}); });

View File

@ -16,6 +16,7 @@ describe('ImagePreview', () => {
it('create a image preview', (done) => { it('create a image preview', (done) => {
ImagePreview([ ImagePreview([
'https://img.yzcdn.cn/upload_files/2017/03/15/FkubrzN7AgGwLlTeb1E89-T_ZjBg.png',
'https://img.yzcdn.cn/upload_files/2017/03/14/FmTPs0SeyQaAOSK1rRe1sL8RcwSY.jpeg', 'https://img.yzcdn.cn/upload_files/2017/03/14/FmTPs0SeyQaAOSK1rRe1sL8RcwSY.jpeg',
'https://img.yzcdn.cn/upload_files/2017/03/15/FvexrWlG_WxtCE9Omo5l27n_mAG_.jpeg' 'https://img.yzcdn.cn/upload_files/2017/03/15/FvexrWlG_WxtCE9Omo5l27n_mAG_.jpeg'
]); ]);
@ -40,6 +41,7 @@ describe('ImagePreview', () => {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
ImagePreview([ ImagePreview([
'https://img.yzcdn.cn/upload_files/2017/03/15/FkubrzN7AgGwLlTeb1E89-T_ZjBg.png',
'https://img.yzcdn.cn/upload_files/2017/03/14/FmTPs0SeyQaAOSK1rRe1sL8RcwSY.jpeg', 'https://img.yzcdn.cn/upload_files/2017/03/14/FmTPs0SeyQaAOSK1rRe1sL8RcwSY.jpeg',
'https://img.yzcdn.cn/upload_files/2017/03/15/FvexrWlG_WxtCE9Omo5l27n_mAG_.jpeg' 'https://img.yzcdn.cn/upload_files/2017/03/15/FvexrWlG_WxtCE9Omo5l27n_mAG_.jpeg'
]); ]);

View File

@ -1,6 +1,7 @@
import Tabs from 'packages/tabs'; import Tabs from 'packages/tabs';
import { mount } from 'avoriaz'; import { mount } from 'avoriaz';
import TabsTestComponent from '../components/tabs'; import TabsTestComponent from '../components/tabs';
import MoreTabsTestComponent from '../components/more-tabs';
describe('Tabs', () => { describe('Tabs', () => {
let wrapper; let wrapper;
@ -74,4 +75,75 @@ describe('Tabs', () => {
expect(wrapper.style.transitionDuration != '').to.be.true; expect(wrapper.style.transitionDuration != '').to.be.true;
}); });
it('create a tabs greater then 4', (done) => {
wrapper = mount(MoreTabsTestComponent, {
attachToDocument: true
});
wrapper.vm.$nextTick(() => {
const nTab = wrapper.find('.van-tab')[4];
nTab.trigger('click');
done();
});
});
it('create a tabs greater then 4 then click last tab', (done) => {
wrapper = mount(MoreTabsTestComponent, {
attachToDocument: true,
propsData: {
active: 7
}
});
wrapper.vm.$nextTick(() => {
const nTab = wrapper.find('.van-tab')[6];
nTab.trigger('click');
done();
});
});
it('test swipe', (done) => {
wrapper = mount(MoreTabsTestComponent, {
attachToDocument: true
});
setTimeout(() => {
const nSwipe = wrapper.find('.van-tabs__swipe')[0];
const eventMouseObject = new window.Event('mousedown');
eventMouseObject.pageX = 200;
nSwipe.element.dispatchEvent(eventMouseObject);
const eventTouchObject = new window.Event('touchstart');
eventTouchObject.changedTouches = [{ pageX: 200 }];
nSwipe.element.dispatchEvent(eventTouchObject);
}, 500);
setTimeout(() => {
const nSwipe = wrapper.find('.van-tabs__swipe')[0];
const eventMouseMoveObject = new window.Event('mousemove');
eventMouseMoveObject.pageX = 0;
document.dispatchEvent(eventMouseMoveObject);
const eventObject = new window.Event('touchmove');
eventObject.changedTouches = [{ pageX: 0 }];
nSwipe.element.dispatchEvent(eventObject);
// 结束滑动
const eventMouseUpObject = new window.Event('mouseup');
document.dispatchEvent(eventMouseUpObject);
const eventEndObject = new window.Event('touchend');
eventEndObject.changedTouches = [{}];
nSwipe.element.dispatchEvent(eventEndObject);
}, 1000);
setTimeout(() => {
const nItem = wrapper.find('.van-tab')[0];
expect(nItem.hasClass('van-tab--active')).to.be.true;
done();
}, 1200);
});
}); });

View File

@ -18,12 +18,4 @@ describe('Tag', () => {
} }
}) })
}); });
it('create with wrong typeProps', () => {
wrapper = mount(Tag, {
propsData: {
type: 'wrong'
}
})
});
}); });