未验证 提交 9a7027cf 编写于 作者: G gyt95 提交者: GitHub

fix: Tab title bar supports automatic scrolling with content (#1950)

* feat(tabs): title bar supports automatic scrolling

* feat(tabs): add name prop in taro demo

* docs(tabs): update docs

* feat(tabs): change timerFunc
上级 a9e27a7b
import { pxCheck } from '@/packages/utils/pxCheck';
import { TypeOfFun, getScrollTopRoot } from '@/packages/utils/util';
import { useRect } from '@/packages/utils/useRect';
import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch, nextTick } from 'vue';
import raf from '@/packages/utils/raf';
export class Title {
title: string = '';
titleSlot?: VNode[];
......@@ -115,6 +116,36 @@ export const component = {
currentIndex.value = index;
}
};
const navRef = ref<HTMLElement>();
const titleRef = ref([]) as Ref<HTMLElement[]>;
const scrollIntoView = (immediate?: boolean) => {
const nav = navRef.value;
const _titles = titleRef.value;
if (!nav || !_titles || !_titles[currentIndex.value]) {
return;
}
const title = _titles[currentIndex.value];
const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2;
scrollLeftTo(nav, to, immediate ? 0 : 0.3);
};
const scrollLeftTo = (nav: any, to: number, duration: number) => {
let count = 0;
const from = nav.scrollLeft;
const frames = duration === 0 ? 1 : Math.round((duration * 1000) / 16);
function animate() {
nav.scrollLeft += (to - from) / frames;
if (++count < frames) {
raf(animate);
}
}
animate();
};
const init = (vnodes: VNode[] = slots.default?.()) => {
titles.value = [];
vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
......@@ -122,6 +153,9 @@ export const component = {
renderTitles(vnodes);
}
findTabsIndex(props.modelValue);
nextTick(() => {
scrollIntoView();
});
};
const onStickyScroll = (params: { top: number; fixed: boolean }) => {
stickyFixed = params.fixed;
......@@ -138,6 +172,7 @@ export const component = {
() => props.modelValue,
(value: string | number) => {
findTabsIndex(value);
scrollIntoView();
if (stickyFixed) {
let top = useRect(container.value!).top + getScrollTopRoot();
let value = Math.ceil(top - props.top);
......@@ -185,9 +220,13 @@ export const component = {
currentIndex.value = index;
emit('update:modelValue', item.paneKey);
emit('change', item);
},
setTabItemRef: (el: HTMLElement, index: number) => {
titleRef.value[index] = el;
}
};
return {
navRef,
titles,
contentStyle,
tabsNavStyle,
......
......@@ -173,10 +173,12 @@ export default {
### A large number of scrolling operations
In the `taro` environment, when multiple `tabs` are included in the same page, `name` needs to be set as a unique identifier to enable the automatic scrolling function of the title bar.
:::demo
```html
<template>
<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10">
<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10" name="tab4value">
<nut-tabpane v-for="item in state.list4" :title="'Tab '+ item">
Tab {{item}}
</nut-tabpane>
......@@ -350,6 +352,10 @@ export default {
| auto-height`v3.1.21` | Automatic height. When set to `true`, `nut-tabs` and `nut-tabs__content` will change with the height of the current `nut-tabpane`. | boolean | false |
| sticky`v3.2.3` `applet not supported` | Whether to use sticky mode| boolean | false |
| top`v3.2.3` `applet not supported` | Sticky offset top | number | 0 |
| name | In the `taro` environment, when multiple `tabs` are included in the same page, `name` needs to be set as a unique identifier to enable the automatic scrolling function of the title bar. | string | '' |
### Tabs Slots
......
......@@ -173,10 +173,12 @@ export default {
### 数量多,滚动操作
`taro`环境下,当同一页面中包含多个`tabs`时,需要设置`name`作为唯一标识符来开启标题栏自动滚动功能。
:::demo
```html
<template>
<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10">
<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10" name="tab4value">
<nut-tabpane v-for="item in state.list4" :title="'Tab '+ item">
Tab {{item}}
</nut-tabpane>
......@@ -350,6 +352,7 @@ export default {
| auto-height`v3.1.21` | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tabpane 的高度而发生变化。 | boolean | false |
| sticky`v3.2.3` `小程序不支持` | 是否使用粘性布局 | boolean | false |
| top`v3.2.3` `小程序不支持` | 粘性布局下的吸顶距离 | number | 0 |
| name | 在`taro`环境下,当同一页面中包含多个`tabs`时,需要设置`name`作为唯一标识符来开启标题栏自动滚动功能。 | string | '' |
### Tabs Slots
......
<template>
<view class="nut-tabs" :class="[direction]" ref="container" id="container">
<view
class="nut-tabs__titles"
<Nut-Scroll-View
:scroll-x="true"
:scroll-with-animation="scrollWithAnimation"
:scroll-left="scrollLeft"
:enable-flex="true"
:id="`nut-tabs__titles_${name}`"
class="nut-tabs__titles tabs-scrollview"
:class="{ [type]: type, scrollable: titleScroll, [size]: size }"
:style="tabsNavStyle"
@scroll="handleScroll"
>
<slot v-if="$slots.titles" name="titles"></slot>
<template v-else>
......@@ -22,7 +28,7 @@
<view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
</view>
</template>
</view>
</Nut-Scroll-View>
<view class="nut-tabs__content" :style="contentStyle">
<slot name="default"></slot>
</view>
......@@ -30,7 +36,306 @@
</template>
<script lang="ts">
import { createComponent } from '@/packages/utils/create';
import { component } from './common';
import { pxCheck } from '@/packages/utils/pxCheck';
import { TypeOfFun, getScrollTopRoot } from '@/packages/utils/util';
import { useRect } from '@/packages/utils/useRect';
import NutScrollView from '../scrollView/index.taro.vue';
import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch, nextTick } from 'vue';
import raf from '@/packages/utils/raf';
import Taro from '@tarojs/taro';
export class Title {
title: string = '';
titleSlot?: VNode[];
paneKey: string = '';
disabled: boolean = false;
constructor() {}
}
export type TabsSize = 'large' | 'normal' | 'small';
const { create } = createComponent('tabs');
export default create(component);
export default create({
props: {
modelValue: {
type: [String, Number],
default: 0
},
color: {
type: String,
default: ''
},
direction: {
type: String,
default: 'horizontal' //vertical
},
size: {
type: String as import('vue').PropType<TabsSize>,
default: 'normal'
},
type: {
type: String,
default: 'line' //card、line、smile
},
titleScroll: {
type: Boolean,
default: false
},
ellipsis: {
type: Boolean,
default: true
},
autoHeight: {
type: Boolean,
default: false
},
background: {
type: String,
default: ''
},
animatedTime: {
type: [Number, String],
default: 300
},
titleGutter: {
type: [Number, String],
default: 0
},
sticky: {
type: Boolean,
default: false
},
top: {
type: Number,
default: 0
},
name: {
type: String,
default: ''
}
},
components: {
NutScrollView
},
emits: ['update:modelValue', 'click', 'change'],
setup(props: any, { emit, slots }: any) {
const container = ref(null);
let stickyFixed: boolean;
provide('activeKey', { activeKey: computed(() => props.modelValue) });
provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
const titles: Ref<Title[]> = ref([]);
const renderTitles = (vnodes: VNode[]) => {
vnodes.forEach((vnode: VNode, index: number) => {
let type = vnode.type;
type = (type as any).name || type;
if (type == 'nut-tabpane') {
let title = new Title();
if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.['paneKey']) {
let paneKeyType = TypeOfFun(vnode.props?.['pane-key']);
let paneIndex =
paneKeyType == 'number' || paneKeyType == 'string' ? String(vnode.props?.['pane-key']) : null;
let camelPaneKeyType = TypeOfFun(vnode.props?.['paneKey']);
let camelPaneIndex =
camelPaneKeyType == 'number' || camelPaneKeyType == 'string' ? String(vnode.props?.['paneKey']) : null;
title.title = vnode.props?.title;
title.paneKey = paneIndex || camelPaneIndex || String(index);
title.disabled = vnode.props?.disabled;
} else {
// title.titleSlot = vnode.children?.title() as VNode[];
}
titles.value.push(title);
} else {
if (vnode.children == ' ') {
return;
}
renderTitles(vnode.children as VNode[]);
}
});
};
const currentIndex = ref((props.modelValue as number) || 0);
const findTabsIndex = (value: string | number) => {
let index = titles.value.findIndex((item) => item.paneKey == value);
if (titles.value.length == 0) {
console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
} else if (index == -1) {
console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
} else {
currentIndex.value = index;
}
};
const titleRef = ref([]) as Ref<HTMLElement[]>;
const scrollLeft = ref(0);
const scrollWithAnimation = ref(true);
const getRect = (selector: string) => {
return new Promise((resolve) => {
Taro.createSelectorQuery()
.select(selector)
.boundingClientRect()
.exec((rect = []) => {
console.log(rect);
resolve(rect[0]);
});
});
};
const getAllRect = (selector: string) => {
return new Promise((resolve) => {
Taro.createSelectorQuery()
.selectAll(selector)
.boundingClientRect()
.exec((rect = []) => resolve(rect[0]));
});
};
const inited = ref(false);
const navRectRef = ref();
const titleRectRef = ref([]);
const scrollIntoView = () => {
raf(() => {
Promise.all([
getRect(`#nut-tabs__titles_${props.name}`),
getAllRect(`#nut-tabs__titles_${props.name} .nut-tabs__titles-item`)
]).then(([navRect, titleRects]) => {
if (!inited.value) {
navRectRef.value = navRect;
titleRectRef.value = titleRects;
inited.value = true;
}
const titleRect = titleRectRef.value[currentIndex.value];
let to = titleRect.left - (navRectRef.value.width - titleRect.width) / 2;
nextTick(() => {
scrollWithAnimation.value = true;
});
scrollLeftTo(to);
});
});
};
const scrollLeftTo = (to: number) => {
let count = 0;
const from = scrollLeft.value;
const frames = 1;
function animate() {
scrollLeft.value += (to - from) / frames;
if (++count < frames) {
raf(animate);
}
}
animate();
};
const init = (vnodes: VNode[] = slots.default?.()) => {
titles.value = [];
vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
if (vnodes && vnodes.length) {
renderTitles(vnodes);
}
findTabsIndex(props.modelValue);
setTimeout(() => {
scrollIntoView();
}, 500);
};
const onStickyScroll = (params: { top: number; fixed: boolean }) => {
stickyFixed = params.fixed;
};
watch(
() => slots.default?.(),
(vnodes: VNode[]) => {
init(vnodes);
}
);
watch(
() => props.modelValue,
(value: string | number) => {
findTabsIndex(value);
scrollIntoView();
if (stickyFixed) {
let top = useRect(container.value!).top + getScrollTopRoot();
let value = Math.ceil(top - props.top);
window.scrollTo({
top: value,
behavior: 'smooth'
});
}
}
);
onMounted(init);
onActivated(init);
const contentStyle = computed(() => {
return {
transform:
props.direction == 'horizontal'
? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
: `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
transitionDuration: `${props.animatedTime}ms`
};
});
const tabsNavStyle = computed(() => {
return {
background: props.background
};
});
const tabsActiveStyle = computed(() => {
return {
color: props.type == 'smile' ? props.color : '',
background: props.type == 'line' ? props.color : ''
};
});
const titleStyle = computed(() => {
return {
marginLeft: pxCheck(props.titleGutter),
marginRight: pxCheck(props.titleGutter)
};
});
const methods = {
tabChange: (item: Title, index: number) => {
emit('click', item);
if (item.disabled) {
return;
}
currentIndex.value = index;
emit('update:modelValue', item.paneKey);
emit('change', item);
},
setTabItemRef: (el: HTMLElement, index: number) => {
titleRef.value[index] = el;
}
};
const handleScroll = (e: any) => {
console.log(e);
};
return {
titles,
contentStyle,
tabsNavStyle,
titleStyle,
tabsActiveStyle,
container,
scrollLeft,
scrollWithAnimation,
onStickyScroll,
handleScroll,
...methods
};
}
});
</script>
<style lang="less">
.tabs-scrollview {
white-space: nowrap;
}
.nut-tabs__titles-item {
display: inline-block !important;
height: 46px;
line-height: 46px;
}
</style>
......@@ -6,6 +6,7 @@
class="nut-tabs__titles"
:class="{ [type]: type, scrollable: titleScroll, [size]: size }"
:style="tabsNavStyle"
ref="navRef"
>
<slot v-if="$slots.titles" name="titles"></slot>
<template v-else>
......@@ -32,6 +33,7 @@
class="nut-tabs__titles"
:class="{ [type]: type, scrollable: titleScroll, [size]: size }"
:style="tabsNavStyle"
ref="navRef"
>
<slot v-if="$slots.titles" name="titles"></slot>
<template v-else>
......@@ -42,6 +44,7 @@
:class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
v-for="(item, index) in titles"
:key="item.paneKey"
:ref="(e) => setTabItemRef(e as HTMLElement, index)"
>
<view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
<view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
......
......@@ -35,7 +35,7 @@
</nut-tabs>
<h2>数量多,滚动操作</h2>
<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10">
<nut-tabs v-model="state.tab4value" title-scroll title-gutter="10" name="tab4value">
<nut-tabpane v-for="item in state.list4" :title="'Tab ' + item"> Tab {{ item }} </nut-tabpane>
</nut-tabs>
<h2>左右布局</h2>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册