Browse Source

feat(tabs): title bar supports automatic scrolling (#2035)

gyt95 2 years ago
parent
commit
c23096b2bd

+ 3 - 0
src/packages/__VUE/tabs/doc.en-US.md

@@ -169,6 +169,8 @@ export default {
 
 ### A large number of scrolling operations
 
+In the `taro` environment, `name` must be set to enable the automatic scrolling function of the title bar.
+
 :::demo
 ```html
 <template>
@@ -346,6 +348,7 @@ export default {
 | auto-height   | Automatic height. When set to `true`, `nut-tabs` and `nut-tabs__content` will change with the height of the current `nut-tab-pane`. | boolean          | `false`      |
 | sticky        | Whether to use sticky mode                                                                                                          | boolean          | `false`      |
 | top           | Sticky offset top                                                                                                                   | number           | `0`          |
+| name        | In the `taro` environment, `name` must be set to enable the automatic scrolling function of the title bar.                              | string | ''        |
 
 ### Tabs Slots
 

+ 4 - 1
src/packages/__VUE/tabs/doc.md

@@ -169,10 +169,12 @@ export default {
 
 ### 数量多,滚动操作
 
+在`taro`环境下,必须设置`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-tab-pane v-for="item in state.list4" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
@@ -346,6 +348,7 @@ export default {
 | auto-height   | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化。 | boolean          | `false`      |
 | sticky        | 是否使用粘性布局                                                                                   | boolean          | `false`      |
 | top           | 粘性布局下的吸顶距离                                                                               | number           | `0`          |
+| name        | 在`taro`环境下,必须设置`name`以开启标题栏自动滚动功能。                              | string | ''        |
 
 ### Tabs Slots
 

+ 4 - 1
src/packages/__VUE/tabs/doc.taro.md

@@ -169,10 +169,12 @@ export default {
 
 ### 数量多,滚动操作
 
+在`taro`环境下,必须设置`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-tab-pane v-for="item in state.list4" :title="'Tab '+ item">
     Tab {{item}}
   </nut-tab-pane>
@@ -344,6 +346,7 @@ export default {
 | title-gutter  | 标签间隙                                                                                           | number \| string | `0`          |
 | size          | 标签栏字体尺寸大小 可选值  large normal small                                                      | string           | `normal`     |
 | auto-height   | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化。 | boolean          | `false`      |
+| name        | 在`taro`环境下,必须设置`name`以开启标题栏自动滚动功能。                              | string | ''        |
 
 ### Tabs Slots
 

+ 13 - 0
src/packages/__VUE/tabs/index.scss

@@ -191,3 +191,16 @@
     box-sizing: border-box;
   }
 }
+.tabs-scrollview {
+  white-space: nowrap;
+}
+.nut-tabs__titles-item {
+  &.nut-tabs__titles-placeholder {
+    width: auto;
+    min-width: 10px;
+  }
+  .taro {
+    height: 46px;
+    line-height: 46px;
+  }
+}

+ 109 - 11
src/packages/__VUE/tabs/index.taro.vue

@@ -1,14 +1,19 @@
 <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"
     >
       <slot v-if="$slots.titles" name="titles"></slot>
       <template v-else>
         <view
-          class="nut-tabs__titles-item"
+          class="nut-tabs__titles-item taro"
           :style="titleStyle"
           @click="tabChange(item, index)"
           :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
@@ -21,8 +26,9 @@
           </view>
           <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
         </view>
+        <view v-if="canShowLabel" class="nut-tabs__titles-item nut-tabs__titles-placeholder"></view>
       </template>
-    </view>
+    </Nut-Scroll-View>
     <view class="nut-tabs__content" :style="contentStyle">
       <slot name="default"></slot>
     </view>
@@ -33,8 +39,11 @@ import { createComponent } from '@/packages/utils/create';
 import { JoySmile } from '@nutui/icons-vue-taro';
 import { pxCheck } from '@/packages/utils/pxCheck';
 import { TypeOfFun } from '@/packages/utils/util';
-import { useRect } from '@/packages/utils/useRect';
-import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
+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';
+import type { RectItem } from './types';
 export class Title {
   title: string = '';
   titleSlot?: VNode[];
@@ -46,7 +55,8 @@ export type TabsSize = 'large' | 'normal' | 'small';
 const { create } = createComponent('tabs');
 export default create({
   components: {
-    JoySmile
+    JoySmile,
+    NutScrollView
   },
   props: {
     modelValue: {
@@ -100,13 +110,16 @@ export default create({
     top: {
       type: Number,
       default: 0
+    },
+    name: {
+      type: String,
+      default: ''
     }
   },
   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([]);
@@ -150,6 +163,84 @@ export default create({
         currentIndex.value = index;
       }
     };
+
+    const titleRef = ref([]) as Ref<HTMLElement[]>;
+    const scrollLeft = ref(0);
+    const scrollWithAnimation = ref(false);
+    const getRect = (selector: string) => {
+      return new Promise((resolve) => {
+        Taro.createSelectorQuery()
+          .select(selector)
+          .boundingClientRect()
+          .exec((rect = []) => {
+            resolve(rect[0]);
+          });
+      });
+    };
+    const getAllRect = (selector: string) => {
+      return new Promise((resolve) => {
+        Taro.createSelectorQuery()
+          .selectAll(selector)
+          .boundingClientRect()
+          .exec((rect = []) => resolve(rect[0]));
+      });
+    };
+    const navRectRef = ref();
+    const titleRectRef = ref<RectItem[]>([]);
+    const canShowLabel = ref(false);
+    const scrollIntoView = () => {
+      if (!props.name) return;
+
+      raf(() => {
+        Promise.all([
+          getRect(`#nut-tabs__titles_${props.name}`),
+          getAllRect(`#nut-tabs__titles_${props.name} .nut-tabs__titles-item`)
+        ]).then(([navRect, titleRects]: any) => {
+          navRectRef.value = navRect;
+          titleRectRef.value = titleRects;
+
+          if (navRectRef.value) {
+            const titlesTotalWidth = titleRects.reduce((prev: number, curr: RectItem) => prev + curr.width, 0);
+            if (titlesTotalWidth > navRectRef.value.width) {
+              canShowLabel.value = true;
+            } else {
+              canShowLabel.value = false;
+            }
+          }
+
+          const titleRect: RectItem = titleRectRef.value[currentIndex.value];
+
+          const left = titleRects
+            .slice(0, currentIndex.value)
+            .reduce((prev: number, curr: RectItem) => prev + curr.width + 20, 31);
+
+          const to = 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');
@@ -157,6 +248,9 @@ export default create({
         renderTitles(vnodes);
       }
       findTabsIndex(props.modelValue);
+      setTimeout(() => {
+        scrollIntoView();
+      }, 500);
     };
 
     watch(
@@ -165,13 +259,11 @@ export default create({
         init(vnodes);
       }
     );
-    const getScrollTopRoot = () => {
-      return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
-    };
     watch(
       () => props.modelValue,
       (value: string | number) => {
         findTabsIndex(value);
+        scrollIntoView();
       }
     );
     onMounted(init);
@@ -211,6 +303,9 @@ export default create({
         currentIndex.value = index;
         emit('update:modelValue', item.paneKey);
         emit('change', item);
+      },
+      setTabItemRef: (el: HTMLElement, index: number) => {
+        titleRef.value[index] = el;
       }
     };
     return {
@@ -220,6 +315,9 @@ export default create({
       titleStyle,
       tabsActiveStyle,
       container,
+      scrollLeft,
+      scrollWithAnimation,
+      canShowLabel,
       ...methods
     };
   }

+ 43 - 1
src/packages/__VUE/tabs/index.vue

@@ -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'">
@@ -62,7 +65,8 @@ import { createComponent } from '@/packages/utils/create';
 import { pxCheck } from '@/packages/utils/pxCheck';
 import { TypeOfFun } 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[];
@@ -178,6 +182,36 @@ export default create({
         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');
@@ -185,6 +219,9 @@ export default create({
         renderTitles(vnodes);
       }
       findTabsIndex(props.modelValue);
+      nextTick(() => {
+        scrollIntoView();
+      });
     };
     const onStickyScroll = (params: { top: number; fixed: boolean }) => {
       stickyFixed = params.fixed;
@@ -203,6 +240,7 @@ export default create({
       () => props.modelValue,
       (value: string | number) => {
         findTabsIndex(value);
+        scrollIntoView();
         if (stickyFixed) {
           let top = useRect(container.value!).top + getScrollTopRoot();
           let value = Math.ceil(top - props.top);
@@ -250,9 +288,13 @@ export default create({
         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,

+ 10 - 0
src/packages/__VUE/tabs/types.ts

@@ -0,0 +1,10 @@
+export type RectItem = {
+  bottom: number;
+  dataset: { sid: string };
+  height: number;
+  id: string;
+  left: number;
+  right: number;
+  top: number;
+  width: number;
+};

+ 1 - 1
src/sites/mobile-taro/vue/src/nav/pages/tabs/index.vue

@@ -36,7 +36,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-tab-pane v-for="item in state.list4" :title="'Tab ' + item"> Tab {{ item }} </nut-tab-pane>
     </nut-tabs>
     <h2>左右布局</h2>