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-tabs>
</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-tabs>
</example-block><example-block title="设置切换tab的动画时间">
@ -23,13 +21,24 @@
<van-tab title="选项三">内容三</van-tab>
</van-tabs>
</example-block><example-block title="禁用tab">
</example-block><example-block title="多于4个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-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>
@ -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-tabs>
</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-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-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-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-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
在对应的`van-tab`上设置`disabled`属性即可,如果需要监听禁用事件,可以监听`disabled`事件。
@ -139,11 +156,10 @@ export default {
:::demo 禁用tab
```html
<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-tabs>
<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-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-tabs>
<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-tabs>
<script>

View File

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

View File

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

View File

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

View File

@ -194,6 +194,7 @@ export default {
},
end: () => {
/* istanbul ignore else */
if (this.isDragging) {
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>
<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
v-else
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
@ -23,6 +41,9 @@
</template>
<script>
import swipe from './swipe';
import translateUtil from 'src/utils/transition';
export default {
name: 'van-tabs',
@ -48,13 +69,21 @@
return {
tabs: [],
isReady: false,
curActive: +this.active
curActive: +this.active,
isSwiping: false
};
},
watch: {
active(val) {
this.curActive = +val;
},
curActive() {
/* istanbul ignore else */
if (this.tabs.length > 4) {
this.doOnValueChange();
}
}
},
@ -72,12 +101,38 @@
return {
width: offsetWidth,
transform: `translate3d(${offsetLeft}, 0px, 0px)`,
transform: `translate3d(${offsetLeft}, 0, 0)`,
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: {
/**
* tab点击事件
@ -93,15 +148,67 @@
this.$emit('click', index);
this.curActive = index;
}
},
},
mounted() {
//
this.$nextTick(() => {
// computednav-barcss
this.isReady = true;
});
/**
* 将当前value值转换为需要translate的值
*/
value2Translate(value) {
/* 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>

View File

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

View File

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

View File

@ -16,6 +16,7 @@ describe('ImagePreview', () => {
it('create a image preview', (done) => {
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/15/FvexrWlG_WxtCE9Omo5l27n_mAG_.jpeg'
]);
@ -40,6 +41,7 @@ describe('ImagePreview', () => {
document.body.style.overflow = 'hidden';
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/15/FvexrWlG_WxtCE9Omo5l27n_mAG_.jpeg'
]);

View File

@ -1,6 +1,7 @@
import Tabs from 'packages/tabs';
import { mount } from 'avoriaz';
import TabsTestComponent from '../components/tabs';
import MoreTabsTestComponent from '../components/more-tabs';
describe('Tabs', () => {
let wrapper;
@ -74,4 +75,75 @@ describe('Tabs', () => {
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'
}
})
});
});