You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
9.8 KiB
302 lines
9.8 KiB
import { VantComponent } from '../common/component'; |
|
import { touch } from '../mixins/touch'; |
|
import { getAllRect, getRect, groupSetData, nextTick, requestAnimationFrame, } from '../common/utils'; |
|
import { isDef } from '../common/validator'; |
|
import { useChildren } from '../common/relation'; |
|
VantComponent({ |
|
mixins: [touch], |
|
classes: [ |
|
'nav-class', |
|
'tab-class', |
|
'tab-active-class', |
|
'line-class', |
|
'wrap-class', |
|
], |
|
relation: useChildren('tab', function () { |
|
this.updateTabs(); |
|
}), |
|
props: { |
|
sticky: Boolean, |
|
border: Boolean, |
|
swipeable: Boolean, |
|
titleActiveColor: String, |
|
titleInactiveColor: String, |
|
color: String, |
|
animated: { |
|
type: Boolean, |
|
observer() { |
|
this.children.forEach((child, index) => child.updateRender(index === this.data.currentIndex, this)); |
|
}, |
|
}, |
|
lineWidth: { |
|
type: null, |
|
value: 40, |
|
observer: 'resize', |
|
}, |
|
lineHeight: { |
|
type: null, |
|
value: -1, |
|
}, |
|
active: { |
|
type: null, |
|
value: 0, |
|
observer(name) { |
|
if (name !== this.getCurrentName()) { |
|
this.setCurrentIndexByName(name); |
|
} |
|
}, |
|
}, |
|
type: { |
|
type: String, |
|
value: 'line', |
|
}, |
|
ellipsis: { |
|
type: Boolean, |
|
value: true, |
|
}, |
|
duration: { |
|
type: Number, |
|
value: 0.3, |
|
}, |
|
zIndex: { |
|
type: Number, |
|
value: 1, |
|
}, |
|
swipeThreshold: { |
|
type: Number, |
|
value: 5, |
|
observer(value) { |
|
this.setData({ |
|
scrollable: this.children.length > value || !this.data.ellipsis, |
|
}); |
|
}, |
|
}, |
|
offsetTop: { |
|
type: Number, |
|
value: 0, |
|
}, |
|
lazyRender: { |
|
type: Boolean, |
|
value: true, |
|
}, |
|
useBeforeChange: { |
|
type: Boolean, |
|
value: false, |
|
}, |
|
}, |
|
data: { |
|
tabs: [], |
|
scrollLeft: 0, |
|
scrollable: false, |
|
currentIndex: 0, |
|
container: null, |
|
skipTransition: true, |
|
scrollWithAnimation: false, |
|
lineOffsetLeft: 0, |
|
inited: false, |
|
}, |
|
mounted() { |
|
requestAnimationFrame(() => { |
|
this.swiping = true; |
|
this.setData({ |
|
container: () => this.createSelectorQuery().select('.van-tabs'), |
|
}); |
|
this.resize(); |
|
this.scrollIntoView(); |
|
}); |
|
}, |
|
methods: { |
|
updateTabs() { |
|
const { children = [], data } = this; |
|
this.setData({ |
|
tabs: children.map((child) => child.data), |
|
scrollable: this.children.length > data.swipeThreshold || !data.ellipsis, |
|
}); |
|
this.setCurrentIndexByName(data.active || this.getCurrentName()); |
|
}, |
|
trigger(eventName, child) { |
|
const { currentIndex } = this.data; |
|
const data = this.getChildData(currentIndex, child); |
|
if (!isDef(data)) { |
|
return; |
|
} |
|
this.$emit(eventName, data); |
|
}, |
|
onTap(event) { |
|
const { index } = event.currentTarget.dataset; |
|
const child = this.children[index]; |
|
if (child.data.disabled) { |
|
this.trigger('disabled', child); |
|
return; |
|
} |
|
this.onBeforeChange(index).then(() => { |
|
this.setCurrentIndex(index); |
|
nextTick(() => { |
|
this.trigger('click'); |
|
}); |
|
}); |
|
}, |
|
// correct the index of active tab |
|
setCurrentIndexByName(name) { |
|
const { children = [] } = this; |
|
const matched = children.filter((child) => child.getComputedName() === name); |
|
if (matched.length) { |
|
this.setCurrentIndex(matched[0].index); |
|
} |
|
}, |
|
setCurrentIndex(currentIndex) { |
|
const { data, children = [] } = this; |
|
if (!isDef(currentIndex) || |
|
currentIndex >= children.length || |
|
currentIndex < 0) { |
|
return; |
|
} |
|
groupSetData(this, () => { |
|
children.forEach((item, index) => { |
|
const active = index === currentIndex; |
|
if (active !== item.data.active || !item.inited) { |
|
item.updateRender(active, this); |
|
} |
|
}); |
|
}); |
|
if (currentIndex === data.currentIndex) { |
|
if (!data.inited) { |
|
this.resize(); |
|
} |
|
return; |
|
} |
|
const shouldEmitChange = data.currentIndex !== null; |
|
this.setData({ currentIndex }); |
|
requestAnimationFrame(() => { |
|
this.resize(); |
|
this.scrollIntoView(); |
|
}); |
|
nextTick(() => { |
|
this.trigger('input'); |
|
if (shouldEmitChange) { |
|
this.trigger('change'); |
|
} |
|
}); |
|
}, |
|
getCurrentName() { |
|
const activeTab = this.children[this.data.currentIndex]; |
|
if (activeTab) { |
|
return activeTab.getComputedName(); |
|
} |
|
}, |
|
resize() { |
|
if (this.data.type !== 'line') { |
|
return; |
|
} |
|
const { currentIndex, ellipsis, skipTransition } = this.data; |
|
Promise.all([ |
|
getAllRect(this, '.van-tab'), |
|
getRect(this, '.van-tabs__line'), |
|
]).then(([rects = [], lineRect]) => { |
|
const rect = rects[currentIndex]; |
|
if (rect == null) { |
|
return; |
|
} |
|
let lineOffsetLeft = rects |
|
.slice(0, currentIndex) |
|
.reduce((prev, curr) => prev + curr.width, 0); |
|
lineOffsetLeft += |
|
(rect.width - lineRect.width) / 2 + (ellipsis ? 0 : 8); |
|
this.setData({ lineOffsetLeft, inited: true }); |
|
this.swiping = true; |
|
if (skipTransition) { |
|
// waiting transition end |
|
setTimeout(() => { |
|
this.setData({ skipTransition: false }); |
|
}, this.data.duration); |
|
} |
|
}); |
|
}, |
|
// scroll active tab into view |
|
scrollIntoView() { |
|
const { currentIndex, scrollable, scrollWithAnimation } = this.data; |
|
if (!scrollable) { |
|
return; |
|
} |
|
Promise.all([ |
|
getAllRect(this, '.van-tab'), |
|
getRect(this, '.van-tabs__nav'), |
|
]).then(([tabRects, navRect]) => { |
|
const tabRect = tabRects[currentIndex]; |
|
const offsetLeft = tabRects |
|
.slice(0, currentIndex) |
|
.reduce((prev, curr) => prev + curr.width, 0); |
|
this.setData({ |
|
scrollLeft: offsetLeft - (navRect.width - tabRect.width) / 2, |
|
}); |
|
if (!scrollWithAnimation) { |
|
nextTick(() => { |
|
this.setData({ scrollWithAnimation: true }); |
|
}); |
|
} |
|
}); |
|
}, |
|
onTouchScroll(event) { |
|
this.$emit('scroll', event.detail); |
|
}, |
|
onTouchStart(event) { |
|
if (!this.data.swipeable) |
|
return; |
|
this.swiping = true; |
|
this.touchStart(event); |
|
}, |
|
onTouchMove(event) { |
|
if (!this.data.swipeable || !this.swiping) |
|
return; |
|
this.touchMove(event); |
|
}, |
|
// watch swipe touch end |
|
onTouchEnd() { |
|
if (!this.data.swipeable || !this.swiping) |
|
return; |
|
const { direction, deltaX, offsetX } = this; |
|
const minSwipeDistance = 50; |
|
if (direction === 'horizontal' && offsetX >= minSwipeDistance) { |
|
const index = this.getAvaiableTab(deltaX); |
|
if (index !== -1) { |
|
this.onBeforeChange(index).then(() => this.setCurrentIndex(index)); |
|
} |
|
} |
|
this.swiping = false; |
|
}, |
|
getAvaiableTab(direction) { |
|
const { tabs, currentIndex } = this.data; |
|
const step = direction > 0 ? -1 : 1; |
|
for (let i = step; currentIndex + i < tabs.length && currentIndex + i >= 0; i += step) { |
|
const index = currentIndex + i; |
|
if (index >= 0 && |
|
index < tabs.length && |
|
tabs[index] && |
|
!tabs[index].disabled) { |
|
return index; |
|
} |
|
} |
|
return -1; |
|
}, |
|
onBeforeChange(index) { |
|
const { useBeforeChange } = this.data; |
|
if (!useBeforeChange) { |
|
return Promise.resolve(); |
|
} |
|
return new Promise((resolve, reject) => { |
|
this.$emit('before-change', Object.assign(Object.assign({}, this.getChildData(index)), { callback: (status) => (status ? resolve() : reject()) })); |
|
}); |
|
}, |
|
getChildData(index, child) { |
|
const currentChild = child || this.children[index]; |
|
if (!isDef(currentChild)) { |
|
return; |
|
} |
|
return { |
|
index: currentChild.index, |
|
name: currentChild.getComputedName(), |
|
title: currentChild.data.title, |
|
}; |
|
}, |
|
}, |
|
});
|
|
|