mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-24 18:36:51 +08:00
refactor(Tabs): refactor with composition api
This commit is contained in:
parent
8ef46764c9
commit
882e3ef5e7
@ -1,61 +0,0 @@
|
|||||||
import { on } from '../utils/dom/event';
|
|
||||||
|
|
||||||
const MIN_DISTANCE = 10;
|
|
||||||
|
|
||||||
function getDirection(x, y) {
|
|
||||||
if (x > y && x > MIN_DISTANCE) {
|
|
||||||
return 'horizontal';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y > x && y > MIN_DISTANCE) {
|
|
||||||
return 'vertical';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TouchMixin = {
|
|
||||||
data() {
|
|
||||||
return { direction: '' };
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
touchStart(event) {
|
|
||||||
this.resetTouchStatus();
|
|
||||||
this.startX = event.touches[0].clientX;
|
|
||||||
this.startY = event.touches[0].clientY;
|
|
||||||
},
|
|
||||||
|
|
||||||
touchMove(event) {
|
|
||||||
const touch = event.touches[0];
|
|
||||||
this.deltaX = touch.clientX - this.startX;
|
|
||||||
this.deltaY = touch.clientY - this.startY;
|
|
||||||
this.offsetX = Math.abs(this.deltaX);
|
|
||||||
this.offsetY = Math.abs(this.deltaY);
|
|
||||||
this.direction =
|
|
||||||
this.direction || getDirection(this.offsetX, this.offsetY);
|
|
||||||
},
|
|
||||||
|
|
||||||
resetTouchStatus() {
|
|
||||||
this.direction = '';
|
|
||||||
this.deltaX = 0;
|
|
||||||
this.deltaY = 0;
|
|
||||||
this.offsetX = 0;
|
|
||||||
this.offsetY = 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
// avoid Vue 2.6 event bubble issues by manually binding events
|
|
||||||
// https://github.com/youzan/vant/issues/3015
|
|
||||||
bindTouchEvent(el) {
|
|
||||||
const { onTouchStart, onTouchMove, onTouchEnd } = this;
|
|
||||||
|
|
||||||
on(el, 'touchstart', onTouchStart);
|
|
||||||
on(el, 'touchmove', onTouchMove);
|
|
||||||
|
|
||||||
if (onTouchEnd) {
|
|
||||||
on(el, 'touchend', onTouchEnd);
|
|
||||||
on(el, 'touchcancel', onTouchEnd);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,12 +1,14 @@
|
|||||||
|
import { ref, watch, nextTick } from 'vue';
|
||||||
import { createNamespace } from '../utils';
|
import { createNamespace } from '../utils';
|
||||||
import { ChildrenMixin } from '../mixins/relation';
|
import { TABS_KEY } from '../tabs';
|
||||||
|
|
||||||
|
// Composition
|
||||||
import { routeProps } from '../composition/use-route';
|
import { routeProps } from '../composition/use-route';
|
||||||
|
import { useParent } from '../composition/use-relation';
|
||||||
|
|
||||||
const [createComponent, bem] = createNamespace('tab');
|
const [createComponent, bem] = createNamespace('tab');
|
||||||
|
|
||||||
export default createComponent({
|
export default createComponent({
|
||||||
mixins: [ChildrenMixin('vanTabs')],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
...routeProps,
|
...routeProps,
|
||||||
dot: Boolean,
|
dot: Boolean,
|
||||||
@ -17,53 +19,63 @@ export default createComponent({
|
|||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
setup(props, { slots }) {
|
||||||
return {
|
const root = ref();
|
||||||
inited: false,
|
const inited = ref(false);
|
||||||
};
|
const { parent, index } = useParent(TABS_KEY, {
|
||||||
},
|
getRoot: () => root.value,
|
||||||
|
props,
|
||||||
|
slots,
|
||||||
|
});
|
||||||
|
|
||||||
computed: {
|
if (!parent) {
|
||||||
computedName() {
|
throw new Error('[Vant] Tabs: <van-tab> must be used inside <van-tabs>');
|
||||||
return this.name ?? this.index;
|
|
||||||
},
|
|
||||||
|
|
||||||
isActive() {
|
|
||||||
const active = this.computedName === this.parent.currentName;
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
this.inited = true;
|
|
||||||
}
|
}
|
||||||
return active;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
const getName = () => props.name ?? index.value;
|
||||||
title() {
|
|
||||||
this.parent.setLine();
|
|
||||||
},
|
|
||||||
|
|
||||||
inited(val) {
|
const init = () => {
|
||||||
if (this.parent.lazyRender && val) {
|
inited.value = true;
|
||||||
this.$nextTick(() => {
|
|
||||||
this.parent.$emit('rendered', this.computedName, this.title);
|
if (parent.props.lazyRender) {
|
||||||
|
nextTick(() => {
|
||||||
|
parent.emit('rendered', getName(), props.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
const isActive = () => {
|
||||||
const { parent, isActive } = this;
|
const active = getName() === parent.currentName.value;
|
||||||
const shouldRender = this.inited || parent.scrollspy || !parent.lazyRender;
|
|
||||||
const show = parent.scrollspy || isActive;
|
|
||||||
const Content = shouldRender ? this.$slots.default?.() : null;
|
|
||||||
|
|
||||||
if (parent.animated) {
|
if (active && !inited.value) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return active;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.title,
|
||||||
|
() => {
|
||||||
|
parent.setLine();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const { animated, scrollspy, lazyRender } = parent.props;
|
||||||
|
const active = isActive();
|
||||||
|
const show = scrollspy || active;
|
||||||
|
|
||||||
|
const shouldRender = inited.value || scrollspy || !lazyRender;
|
||||||
|
const Content = shouldRender ? slots.default?.() : null;
|
||||||
|
|
||||||
|
if (animated) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={root}
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
aria-hidden={!isActive}
|
aria-hidden={!active}
|
||||||
class={bem('pane-wrapper', { inactive: !isActive })}
|
class={bem('pane-wrapper', { inactive: !active })}
|
||||||
>
|
>
|
||||||
<div class={bem('pane')}>{Content}</div>
|
<div class={bem('pane')}>{Content}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -71,9 +83,10 @@ export default createComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div vShow={show} role="tabpanel" class={bem('pane')}>
|
<div v-show={show} ref={root} role="tabpanel" class={bem('pane')}>
|
||||||
{Content}
|
{Content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,33 @@
|
|||||||
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
provide,
|
||||||
|
computed,
|
||||||
|
reactive,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onActivated,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { createNamespace, isDef, addUnit } from '../utils';
|
import { createNamespace, isDef, addUnit } from '../utils';
|
||||||
import { scrollLeftTo, scrollTopTo } from './utils';
|
import { scrollLeftTo, scrollTopTo } from './utils';
|
||||||
import { route } from '../composition/use-route';
|
import { route } from '../composition/use-route';
|
||||||
import { isHidden } from '../utils/dom/style';
|
import { isHidden } from '../utils/dom/style';
|
||||||
import { on, off } from '../utils/dom/event';
|
|
||||||
import { unitToPx } from '../utils/format/unit';
|
import { unitToPx } from '../utils/format/unit';
|
||||||
import { BORDER_TOP_BOTTOM } from '../utils/constant';
|
import { BORDER_TOP_BOTTOM } from '../utils/constant';
|
||||||
import { callInterceptor } from '../utils/interceptor';
|
import { callInterceptor } from '../utils/interceptor';
|
||||||
import {
|
import {
|
||||||
getScroller,
|
|
||||||
getVisibleTop,
|
getVisibleTop,
|
||||||
getElementTop,
|
getElementTop,
|
||||||
getVisibleHeight,
|
getVisibleHeight,
|
||||||
setRootScrollTop,
|
setRootScrollTop,
|
||||||
} from '../utils/dom/scroll';
|
} from '../utils/dom/scroll';
|
||||||
|
|
||||||
// Mixins
|
// Composition
|
||||||
import { ParentMixin } from '../mixins/relation';
|
import { useRefs } from '../composition/use-refs';
|
||||||
import { BindEventMixin } from '../mixins/bind-event';
|
import { useExpose } from '../composition/use-expose';
|
||||||
|
import { useWindowSize, useScrollParent, useEventListener } from '@vant/use';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Sticky from '../sticky';
|
import Sticky from '../sticky';
|
||||||
@ -26,22 +36,9 @@ import TabsContent from './TabsContent';
|
|||||||
|
|
||||||
const [createComponent, bem] = createNamespace('tabs');
|
const [createComponent, bem] = createNamespace('tabs');
|
||||||
|
|
||||||
|
export const TABS_KEY = 'vanTabs';
|
||||||
|
|
||||||
export default createComponent({
|
export default createComponent({
|
||||||
mixins: [
|
|
||||||
ParentMixin('vanTabs'),
|
|
||||||
BindEventMixin(function (bind) {
|
|
||||||
if (!this.scroller) {
|
|
||||||
this.scroller = getScroller(this.$el);
|
|
||||||
}
|
|
||||||
|
|
||||||
bind(window, 'resize', this.resize, true);
|
|
||||||
|
|
||||||
if (this.scrollspy) {
|
|
||||||
bind(this.scroller, 'scroll', this.onScroll, true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
color: String,
|
color: String,
|
||||||
border: Boolean,
|
border: Boolean,
|
||||||
@ -87,139 +84,109 @@ export default createComponent({
|
|||||||
|
|
||||||
emits: ['click', 'change', 'scroll', 'disabled', 'rendered', 'update:active'],
|
emits: ['click', 'change', 'scroll', 'disabled', 'rendered', 'update:active'],
|
||||||
|
|
||||||
data() {
|
setup(props, { emit, slots }) {
|
||||||
this.titleRefs = [];
|
let inited;
|
||||||
|
let tabHeight;
|
||||||
|
let lockScroll;
|
||||||
|
let stickyFixed;
|
||||||
|
|
||||||
return {
|
const root = ref();
|
||||||
|
const navRef = ref();
|
||||||
|
const wrapRef = ref();
|
||||||
|
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
const scroller = useScrollParent(root);
|
||||||
|
const [titleRefs, setTitleRefs] = useRefs();
|
||||||
|
|
||||||
|
const children = reactive([]);
|
||||||
|
const state = reactive({
|
||||||
position: '',
|
position: '',
|
||||||
currentIndex: -1,
|
currentIndex: -1,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
backgroundColor: this.color,
|
backgroundColor: props.color,
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
computed: {
|
|
||||||
// whether the nav is scrollable
|
// whether the nav is scrollable
|
||||||
scrollable() {
|
const scrollable = computed(
|
||||||
return this.children.length > this.swipeThreshold || !this.ellipsis;
|
() => children.length > props.swipeThreshold || !props.ellipsis
|
||||||
},
|
);
|
||||||
|
|
||||||
navStyle() {
|
const navStyle = computed(() => ({
|
||||||
return {
|
borderColor: props.color,
|
||||||
borderColor: this.color,
|
background: props.background,
|
||||||
background: this.background,
|
}));
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
currentName() {
|
const getTabName = (tab, index) => tab.props.name ?? index;
|
||||||
const activeTab = this.children[this.currentIndex];
|
|
||||||
|
const currentName = computed(() => {
|
||||||
|
const activeTab = children[state.currentIndex];
|
||||||
|
|
||||||
if (activeTab) {
|
if (activeTab) {
|
||||||
return activeTab.computedName;
|
return getTabName(activeTab, state.currentIndex);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
offsetTopPx() {
|
const offsetTopPx = computed(() => unitToPx(props.offsetTop));
|
||||||
return unitToPx(this.offsetTop);
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollOffset() {
|
const scrollOffset = computed(() => {
|
||||||
if (this.sticky) {
|
if (props.sticky) {
|
||||||
return this.offsetTopPx + this.tabHeight;
|
return offsetTopPx.value + tabHeight;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
color: 'setLine',
|
|
||||||
|
|
||||||
active(name) {
|
|
||||||
if (name !== this.currentName) {
|
|
||||||
this.setCurrentIndexByName(name);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
children() {
|
|
||||||
this.setCurrentIndexByName(this.currentName || this.active);
|
|
||||||
this.setLine();
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollIntoView(true);
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
currentIndex() {
|
// scroll active tab into view
|
||||||
this.scrollIntoView();
|
const scrollIntoView = (immediate) => {
|
||||||
this.setLine();
|
const nav = navRef.value;
|
||||||
|
const titles = titleRefs.value;
|
||||||
|
|
||||||
// scroll to correct position
|
if (!scrollable.value || !titles || !titles[state.currentIndex]) {
|
||||||
if (this.stickyFixed && !this.scrollspy) {
|
return;
|
||||||
setRootScrollTop(Math.ceil(getElementTop(this.$el) - this.offsetTopPx));
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
scrollspy(val) {
|
const title = titles[state.currentIndex].$el;
|
||||||
if (val) {
|
const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2;
|
||||||
on(this.scroller, 'scroll', this.onScroll, true);
|
|
||||||
} else {
|
|
||||||
off(this.scroller, 'scroll', this.onScroll);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
scrollLeftTo(nav, to, immediate ? 0 : +props.duration);
|
||||||
this.init();
|
};
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
const init = () => {
|
||||||
this.init();
|
nextTick(() => {
|
||||||
this.setLine();
|
inited = true;
|
||||||
},
|
tabHeight = getVisibleHeight(wrapRef.value);
|
||||||
|
scrollIntoView(true);
|
||||||
methods: {
|
|
||||||
// @exposed-api
|
|
||||||
resize() {
|
|
||||||
this.setLine();
|
|
||||||
},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.inited = true;
|
|
||||||
this.tabHeight = getVisibleHeight(this.$refs.wrap);
|
|
||||||
this.scrollIntoView(true);
|
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
// update nav bar style
|
// update nav bar style
|
||||||
setLine() {
|
const setLine = () => {
|
||||||
const shouldAnimate = this.inited;
|
const shouldAnimate = inited;
|
||||||
|
|
||||||
this.$nextTick(() => {
|
nextTick(() => {
|
||||||
const { titleRefs } = this;
|
const titles = titleRefs.value;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!titleRefs ||
|
!titles ||
|
||||||
!titleRefs[this.currentIndex] ||
|
!titles[state.currentIndex] ||
|
||||||
this.type !== 'line' ||
|
props.type !== 'line' ||
|
||||||
isHidden(this.$el)
|
isHidden(root.value)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = titleRefs[this.currentIndex].$el;
|
const title = titles[state.currentIndex].$el;
|
||||||
const { lineWidth, lineHeight } = this;
|
const { lineWidth, lineHeight } = props;
|
||||||
const left = title.offsetLeft + title.offsetWidth / 2;
|
const left = title.offsetLeft + title.offsetWidth / 2;
|
||||||
|
|
||||||
const lineStyle = {
|
const lineStyle = {
|
||||||
width: addUnit(lineWidth),
|
width: addUnit(lineWidth),
|
||||||
backgroundColor: this.color,
|
backgroundColor: props.color,
|
||||||
transform: `translateX(${left}px) translateX(-50%)`,
|
transform: `translateX(${left}px) translateX(-50%)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldAnimate) {
|
if (shouldAnimate) {
|
||||||
lineStyle.transitionDuration = `${this.duration}s`;
|
lineStyle.transitionDuration = `${props.duration}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDef(lineHeight)) {
|
if (isDef(lineHeight)) {
|
||||||
@ -228,207 +195,247 @@ export default createComponent({
|
|||||||
lineStyle.borderRadius = height;
|
lineStyle.borderRadius = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lineStyle = lineStyle;
|
state.lineStyle = lineStyle;
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
// correct the index of active tab
|
const findAvailableTab = (index) => {
|
||||||
setCurrentIndexByName(name) {
|
const diff = index < state.currentIndex ? -1 : 1;
|
||||||
const matched = this.children.filter((tab) => tab.computedName === name);
|
|
||||||
const defaultIndex = (this.children[0] || {}).index || 0;
|
|
||||||
this.setCurrentIndex(matched.length ? matched[0].index : defaultIndex);
|
|
||||||
},
|
|
||||||
|
|
||||||
setCurrentIndex(currentIndex) {
|
while (index >= 0 && index < children.length) {
|
||||||
currentIndex = this.findAvailableTab(currentIndex);
|
if (!children[index].props.disabled) {
|
||||||
|
|
||||||
if (isDef(currentIndex) && currentIndex !== this.currentIndex) {
|
|
||||||
const shouldEmitChange = this.currentIndex !== null;
|
|
||||||
this.currentIndex = currentIndex;
|
|
||||||
this.$emit('update:active', this.currentName);
|
|
||||||
|
|
||||||
if (shouldEmitChange) {
|
|
||||||
this.$emit(
|
|
||||||
'change',
|
|
||||||
this.currentName,
|
|
||||||
this.children[currentIndex].title
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
findAvailableTab(index) {
|
|
||||||
const diff = index < this.currentIndex ? -1 : 1;
|
|
||||||
|
|
||||||
while (index >= 0 && index < this.children.length) {
|
|
||||||
if (!this.children[index].disabled) {
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
index += diff;
|
index += diff;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
// emit event when clicked
|
const setCurrentIndex = (currentIndex) => {
|
||||||
onClick(item, index) {
|
currentIndex = findAvailableTab(currentIndex);
|
||||||
const { title, disabled, computedName } = this.children[index];
|
|
||||||
if (disabled) {
|
|
||||||
this.$emit('disabled', computedName, title);
|
|
||||||
} else {
|
|
||||||
callInterceptor({
|
|
||||||
interceptor: this.beforeChange,
|
|
||||||
args: [computedName],
|
|
||||||
done: () => {
|
|
||||||
this.setCurrentIndex(index);
|
|
||||||
this.scrollToCurrentContent();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$emit('click', computedName, title);
|
if (isDef(currentIndex) && currentIndex !== state.currentIndex) {
|
||||||
route(item.$router, item);
|
const shouldEmitChange = state.currentIndex !== null;
|
||||||
|
state.currentIndex = currentIndex;
|
||||||
|
emit('update:active', currentName.value);
|
||||||
|
|
||||||
|
if (shouldEmitChange) {
|
||||||
|
emit('change', currentName.value, children[currentIndex].props.title);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
// scroll active tab into view
|
|
||||||
scrollIntoView(immediate) {
|
|
||||||
const { titleRefs } = this;
|
|
||||||
|
|
||||||
if (!this.scrollable || !titleRefs || !titleRefs[this.currentIndex]) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { nav } = this.$refs;
|
// correct the index of active tab
|
||||||
const title = titleRefs[this.currentIndex].$el;
|
const setCurrentIndexByName = (name) => {
|
||||||
const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2;
|
const matched = children.filter(
|
||||||
|
(tab, index) => getTabName(tab, index) === name
|
||||||
|
);
|
||||||
|
|
||||||
scrollLeftTo(nav, to, immediate ? 0 : +this.duration);
|
const index = matched[0] ? children.indexOf(matched[0]) : 0;
|
||||||
},
|
setCurrentIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
onSticktScroll(params) {
|
const scrollToCurrentContent = (immediate = false) => {
|
||||||
this.stickyFixed = params.isFixed;
|
if (props.scrollspy) {
|
||||||
this.$emit('scroll', params);
|
const target = children[state.currentIndex];
|
||||||
},
|
const el = target?.getRoot();
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
scrollTo(name) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.setCurrentIndexByName(name);
|
|
||||||
this.scrollToCurrentContent(true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollToCurrentContent(immediate = false) {
|
|
||||||
if (this.scrollspy) {
|
|
||||||
const target = this.children[this.currentIndex];
|
|
||||||
const el = target?.$el;
|
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
const to = getElementTop(el, this.scroller) - this.scrollOffset;
|
const to = getElementTop(el, scroller.value) - scrollOffset.value;
|
||||||
|
|
||||||
this.lockScroll = true;
|
lockScroll = true;
|
||||||
scrollTopTo(this.scroller, to, immediate ? 0 : +this.duration, () => {
|
scrollTopTo(
|
||||||
this.lockScroll = false;
|
scroller.value,
|
||||||
|
to,
|
||||||
|
immediate ? 0 : +props.duration,
|
||||||
|
() => {
|
||||||
|
lockScroll = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// emit event when clicked
|
||||||
|
const onClick = (item, index) => {
|
||||||
|
const { title, disabled } = children[index].props;
|
||||||
|
const name = getTabName(children[index], index);
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
emit('disabled', name, title);
|
||||||
|
} else {
|
||||||
|
callInterceptor({
|
||||||
|
interceptor: props.beforeChange,
|
||||||
|
args: [name],
|
||||||
|
done: () => {
|
||||||
|
setCurrentIndex(index);
|
||||||
|
scrollToCurrentContent();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onScroll() {
|
emit('click', name, title);
|
||||||
if (this.scrollspy && !this.lockScroll) {
|
route(item.$router, item);
|
||||||
const index = this.getCurrentIndexOnScroll();
|
|
||||||
this.setCurrentIndex(index);
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
getCurrentIndexOnScroll() {
|
const onStickyScroll = (params) => {
|
||||||
const { children } = this;
|
stickyFixed = params.isFixed;
|
||||||
|
emit('scroll', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollTo = (name) => {
|
||||||
|
nextTick(() => {
|
||||||
|
setCurrentIndexByName(name);
|
||||||
|
scrollToCurrentContent(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentIndexOnScroll = () => {
|
||||||
for (let index = 0; index < children.length; index++) {
|
for (let index = 0; index < children.length; index++) {
|
||||||
const top = getVisibleTop(children[index].$el);
|
const top = getVisibleTop(children[index].getRoot());
|
||||||
|
|
||||||
if (top > this.scrollOffset) {
|
if (top > scrollOffset.value) {
|
||||||
return index === 0 ? 0 : index - 1;
|
return index === 0 ? 0 : index - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return children.length - 1;
|
return children.length - 1;
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
const onScroll = () => {
|
||||||
const { type, animated, scrollable } = this;
|
if (props.scrollspy && !lockScroll) {
|
||||||
|
const index = getCurrentIndexOnScroll();
|
||||||
|
setCurrentIndex(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Nav = this.children.map((item, index) => {
|
const renderNav = () =>
|
||||||
return (
|
children.map((item, index) => (
|
||||||
<TabsTitle
|
<TabsTitle
|
||||||
ref={(val) => {
|
ref={setTitleRefs(index)}
|
||||||
this.titleRefs[index] = val;
|
dot={item.props.dot}
|
||||||
}}
|
type={props.type}
|
||||||
dot={item.dot}
|
badge={item.props.badge}
|
||||||
type={type}
|
title={item.props.title}
|
||||||
badge={item.badge}
|
color={props.color}
|
||||||
title={item.title}
|
style={item.props.titleStyle}
|
||||||
color={this.color}
|
isActive={index === state.currentIndex}
|
||||||
style={item.titleStyle}
|
disabled={item.props.disabled}
|
||||||
isActive={index === this.currentIndex}
|
scrollable={scrollable.value}
|
||||||
disabled={item.disabled}
|
renderTitle={item.slots.title}
|
||||||
scrollable={scrollable}
|
activeColor={props.titleActiveColor}
|
||||||
renderTitle={item.$slots.title}
|
inactiveColor={props.titleInactiveColor}
|
||||||
activeColor={this.titleActiveColor}
|
|
||||||
inactiveColor={this.titleInactiveColor}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.onClick(item, index);
|
onClick(item, index);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
));
|
||||||
});
|
|
||||||
|
|
||||||
const Wrap = (
|
const renderHeader = () => {
|
||||||
|
const { type, border } = props;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
ref="wrap"
|
ref={wrapRef}
|
||||||
class={[
|
class={[
|
||||||
bem('wrap', { scrollable }),
|
bem('wrap', { scrollable: scrollable.value }),
|
||||||
{ [BORDER_TOP_BOTTOM]: type === 'line' && this.border },
|
{ [BORDER_TOP_BOTTOM]: type === 'line' && border },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="nav"
|
ref={navRef}
|
||||||
role="tablist"
|
role="tablist"
|
||||||
class={bem('nav', [type, { complete: this.scrollable }])}
|
class={bem('nav', [type, { complete: scrollable.value }])}
|
||||||
style={this.navStyle}
|
style={navStyle.value}
|
||||||
>
|
>
|
||||||
{this.$slots['nav-left']?.()}
|
{slots['nav-left']?.()}
|
||||||
{Nav}
|
{renderNav()}
|
||||||
{type === 'line' && (
|
{type === 'line' && (
|
||||||
<div class={bem('line')} style={this.lineStyle} />
|
<div class={bem('line')} style={state.lineStyle} />
|
||||||
)}
|
)}
|
||||||
{this.$slots['nav-right']?.()}
|
{slots['nav-right']?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
watch([() => props.color, windowSize.width], setLine);
|
||||||
<div class={bem([type])}>
|
|
||||||
{this.sticky ? (
|
watch(
|
||||||
|
() => props.active,
|
||||||
|
(value) => {
|
||||||
|
if (value !== currentName.value) {
|
||||||
|
setCurrentIndexByName(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(children, () => {
|
||||||
|
setCurrentIndexByName(currentName.value || props.active);
|
||||||
|
setLine();
|
||||||
|
nextTick(() => {
|
||||||
|
scrollIntoView(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.currentIndex,
|
||||||
|
() => {
|
||||||
|
scrollIntoView();
|
||||||
|
setLine();
|
||||||
|
|
||||||
|
// scroll to correct position
|
||||||
|
if (stickyFixed && !props.scrollspy) {
|
||||||
|
setRootScrollTop(
|
||||||
|
Math.ceil(getElementTop(root.value) - offsetTopPx.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(init);
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
init();
|
||||||
|
setLine();
|
||||||
|
});
|
||||||
|
|
||||||
|
useExpose({
|
||||||
|
resize: setLine,
|
||||||
|
scrollTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEventListener('scroll', onScroll, { target: scroller.value });
|
||||||
|
|
||||||
|
provide(TABS_KEY, {
|
||||||
|
emit,
|
||||||
|
props,
|
||||||
|
setLine,
|
||||||
|
children,
|
||||||
|
currentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div ref={root} class={bem([props.type])}>
|
||||||
|
{props.sticky ? (
|
||||||
<Sticky
|
<Sticky
|
||||||
container={this.$el}
|
container={root.value}
|
||||||
offsetTop={this.offsetTop}
|
offsetTop={offsetTopPx.value}
|
||||||
onScroll={this.onSticktScroll}
|
onScroll={onStickyScroll}
|
||||||
>
|
>
|
||||||
{Wrap}
|
{renderHeader()}
|
||||||
</Sticky>
|
</Sticky>
|
||||||
) : (
|
) : (
|
||||||
Wrap
|
renderHeader()
|
||||||
)}
|
)}
|
||||||
<TabsContent
|
<TabsContent
|
||||||
count={this.children.length}
|
count={children.length}
|
||||||
animated={animated}
|
animated={props.animated}
|
||||||
duration={this.duration}
|
duration={props.duration}
|
||||||
swipeable={this.swipeable}
|
swipeable={props.swipeable}
|
||||||
currentIndex={this.currentIndex}
|
currentIndex={state.currentIndex}
|
||||||
onChange={this.setCurrentIndex}
|
onChange={setCurrentIndex}
|
||||||
>
|
>
|
||||||
{this.$slots.default?.()}
|
{slots.default?.()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user