v3自定义滚动条组件

本文介绍了如何使用scrollContainer组件实现一个具有自定义滚动条并与内容联动的滚动区域,通过mousedown、mousemove和mouseup事件处理滑块的移动以及内容区域的同步滚动。
摘要由CSDN通过智能技术生成

1. scrollContainer 组件代码

  • template分为滚动区域与滚动条区域, 先隐藏浏览器滚动条,再做自定义滚动条与内容联动
  • 主要思路:
  1. 滑块绑定mousedown事件, 用于给body绑定mouseMove事件与mouseup事件, move事件用于处理滑块移动, 以及滑块移动后, 内容区域的移动
  2. mouseup事件用于解除mousemove事件
  3. 内容区域绑定scroll事件, 用于内容滚动影响滑块位置
  4. 内容区域绑定滚轮事件, 用于滚轮事件触发滚动事件, 滚动事件影响滑块位置, 形成联动
<template>
  <!-- 主体内容-->
  <div
    ref="scrollbarContent"
    class="scrollbar-content"
    :class="[scrollX ? 'scroll-x-center' : 'scroll-y-center']"
    :style="{
      height,
      width,
    }"
  >
    <!-- 滚动内容区域 -->
    <div
      class="scroll-wrap"
      ref="scrollWrap"
      :class="[isSelect ? 'cannotselect' : '']"
      :style="{
        [scrollX ? 'overflow-x' : 'overflow-y']: 'scroll',
      }"
      @scroll="handlerScroll"
      @wheel="handlerWheel"
      @DOMMouseScroll="handlerWheel"
    >
      <slot></slot>
    </div>
    <!-- 滚动条Y -->
    <div
      class="scrollbar-y"
      ref="scrollbarY"
      v-if="scrollY"
      :style="{
        height: '100%',
        width: barWidth,
        position: 'relative',
      }"
      @mousedown="handlerDown"
    >
      <!-- 滑块 -->
      <div
        class="sliding-block"
        ref="slidingBlockY"
        :style="{
          width: barWidth,
          height: sliderLength + 'px',
          position: 'absolute',
          top: slidingTop + 'px',
        }"
      ></div>
    </div>
    <!-- 滚动条X -->
    <div
      class="scrollbar-x"
      ref="scrollbarX"
      v-if="scrollX"
      :style="{
        width: '100%',
        height: barHeight,
        position: 'relative',
      }"
    >
      <!-- 滑块 -->
      <div
        class="sliding-block"
        :style="{
          height: barHeight,
          position: 'absolute',
          width: sliderLength + 'px',
          left: slidingLeft + 'px',
        }"
        @mousedown="handlerDown"
      ></div>
    </div>
  </div>
</template>
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    height?: string;
    scrollX?: boolean; //滚动方向x轴
    scrollY?: boolean; //滚动方向y轴
    width?: string;
    barHeight?: string; //滚动条高度, X轴传
    barWidth?: string; //滚轮动条宽度,Y轴传
    sliderLength?: number; //滑块边长
  }>(),
  {
    height: "100%",
    scrollX: false,
    scrollY: false,
    width: "100%",
    barHeight: "6px",
    barWidth: "6px",
    sliderLength: 100,
  }
);

const scrollbarContent = ref<HTMLElement>();
const scrollWrap = ref<HTMLElement>();
const scrollbarY = ref<HTMLElement>();
const slidingBlockY = ref<HTMLElement>(); //滑块
const scrollbarX = ref<HTMLElement>();
const slidingLeft = ref<number>(0); //x
const slidingTop = ref<number>(0); //y
const isSelect = ref<boolean>(false); //文字是否可被选中

// 鼠标按下事件
const handlerDown = (e:MouseEvent) => {
  if (!props.scrollX) {
    document.body?.addEventListener("mousemove", handleMoveY);
    handleMoveY(e) //用来处理点动
  } else {
    document.body?.addEventListener("mousemove", handleMoveX);
    handleMoveX(e) //用来处理点动
  }
  // 绑定移动事件
  isSelect.value = true;
  document.body.addEventListener("mouseup", handlerReomveMove);
};

// 鼠标移动事件Y
const handleMoveY = (e: MouseEvent) => {
  // 父元素距离可视区域顶部距离
  const scrollbarYTop = scrollbarY.value?.getBoundingClientRect().top ?? 0;
  //  滑块距父容器距离 =  鼠标点距可视区上边缘距离 -  父容器距离可视区上边缘距离
  let top = e.clientY - scrollbarYTop - props.sliderLength / 2;
  let maxMoveLength =
    (scrollbarY.value?.offsetHeight as number) - props.sliderLength; //最大移动长度
  if (top < 0) {
    top = 0;
  } else if (top > maxMoveLength) {
    top = maxMoveLength;
  }
  slidingTop.value = top;

  // 滚动内容,比例关系: (内容高度-盒子宽度)/可移动最大高度 =  内容移动距离(即页)/滑块移动距离
  if (scrollWrap.value) {
    scrollWrap.value.scrollTop =
      ((scrollWrap.value.scrollHeight - scrollWrap.value.offsetHeight) /
        maxMoveLength) *
      top;
  }
};
// 鼠标移动事件X
const handleMoveX = (e: MouseEvent) => {
  if (!scrollbarX.value || !scrollWrap.value) {
    return false;
  }
  // 滑块可移动的最大距离
  let maxLength = scrollbarX.value?.offsetWidth - props.sliderLength;
  // 父元素距离可视区左侧距离
  const scrollbarXLeft = scrollbarX.value?.getBoundingClientRect().left ?? 0;

  // 滑块移动距离为
  let left = e.clientX - scrollbarXLeft - props.sliderLength / 2;

  // 界限区域
  if (left < 0) {
    left = 0;
  } else if (left > maxLength) {
    left = maxLength;
  }

  // 设置滑块位置
  slidingLeft.value = left;

  // 设置内容块滚动区域, (内容总宽度 - 容器宽度)/滑块最大移动宽度 =  内容移动距离/ 滑块移动距离
  scrollWrap.value.scrollLeft =
    ((scrollWrap.value.scrollWidth - scrollWrap.value.offsetWidth) /
      maxLength) *
    left;
};

// 鼠标弹起与移出时,  解绑移动事件
const handlerReomveMove = () => {
  if (!props.scrollX) {
    document.body?.removeEventListener("mousemove", handleMoveY);
  } else {
    document.body?.removeEventListener("mousemove", handleMoveX);
  }
  isSelect.value = false;
};

// scroll事件,用来反过来控制滚动条
const handlerScroll = () => {
  if (!scrollWrap.value) {
    return false;
  }

  if (!props.scrollX) {
    let maxMoveLength =
      (scrollbarY.value?.offsetHeight as number) - props.sliderLength; //最大移动长度
    let contentMoveLength = scrollWrap.value?.scrollTop; //内容移动长度
    // 滑块移动长度
    slidingTop.value =
      (contentMoveLength * maxMoveLength) /
      (scrollWrap.value.scrollHeight - scrollWrap.value.offsetHeight);
  } else {
    // 滑块最大可移动距离
    let maxMoveLength =
      (scrollbarX.value?.offsetWidth as number) - props.sliderLength;
    // 内容移动距离
    let contentMoveLength = scrollWrap.value.scrollLeft;

    //滑块移动长度  (总宽度- 盒子宽度)/滑块最大可移动距离 = 内容移动距离/滑块移动距离
    slidingLeft.value =
      (contentMoveLength * maxMoveLength) /
      (scrollWrap.value.scrollWidth - scrollWrap.value.offsetWidth);
  }
};

let state = false;
// 滚轮事件, 用于滚轮控制内容区域滚动
const handlerWheel = (e) => {
  if (!scrollWrap.value) {
    return false;
  }
  // 内容区域最大滚动距离
  if (!state) {
    state = true;
    setTimeout(() => {
      let step = 0; //滚轮步长
      if (e.type == "wheel") {
        //谷歌
        step = e.wheelDelta > 0 ? -10 : +10;
      } else if (e.type == "DOMMouseScroll") {
        //火狐
        step = e.detail > 0 ? +10 : -10;
      }

      if (!props.scrollX) {
        let top = (scrollWrap.value?.scrollTop as number) + step;
        let maxScrollTop =
          (scrollWrap.value?.scrollHeight as number) -
          (scrollWrap.value?.offsetHeight as number);
        if (top < 0) {
          top = 0;
        } else if (top > maxScrollTop) {
          top = maxScrollTop;
        }
        (scrollWrap.value as HTMLElement).scrollTop = top;
      } else {
        // 此次滚动设置距离, Y方向步长为10, x方向为其20倍即200
        let left = (scrollWrap.value?.scrollLeft as number) + step * 20;
        // 最大滚动距离
        let maxScrollLeft =
          (scrollWrap.value?.scrollWidth as number) -
          (scrollWrap.value?.offsetWidth as number);

        if (left < 0) {
          left = 0;
        } else if (left > maxScrollLeft) {
          left = maxScrollLeft;
        }
        (scrollWrap.value as HTMLElement).scrollLeft = left;
        console.log(
          (scrollWrap.value as HTMLElement).scrollLeft,
          "(scrollWrap.value as HTMLElement).scrollLeft"
        );
      }

      state = false;
    }, 100);
  }
};

onUnmounted(() => {
  document.body.removeEventListener("mouseup", handlerReomveMove);
});
</script>
<style scoped lang="scss">
.scrollbar-content {
  display: flex;
  height: 100%;
  width: 100%;
  .scrollbar-y,
  .scrollbar-x {
    flex-shrink: 0;
  }
  // 滑块
  .sliding-block {
    background-color: #ccc;
    border-radius: 10px;
  }
  // 滚动内容
  .scroll-wrap {
    position: relative;
    height: 100%;
    width: 100%;
    flex: 1;
    /* /兼容火狐/ */
    scrollbar-width: none;
    /* / 兼容IE10+ */
    -ms-overflow-style: none;
  }
  /* 谷歌,元素隐藏必须设置宽度  不然无效 */
  .scroll-wrap::-webkit-scrollbar {
    width: 0px;
    height: 0px;
  }
  .cannotselect {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
}

.scroll-x-center {
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
}
.scroll-y-center {
  justify-content: space-between;
  align-items: center;
}
</style>

2.基础使用

//X方向滚动,  需要设置scroll-x和width   以及ul的样式, ul需要设置flex
 <scrollContainer scroll-x width="400px">
      <ul style="display:flex;flex-wrap: nowrap;list-style: none;">
        <li v-for="item in 100" :key="item" style="flex-shrink: 0;">{{ item }} 信息列表</li>
      </ul>
</scrollContainer>
//y方向滚动, 需要设置scroll-y和height
<scrollContainer scroll-y height="400px">
      <ul >
        <li v-for="item in 100" :key="item">{{ item }} 信息列表</li>
      </ul>
</scrollContainer>

3.属性

//传给组件的props
    height?: string;    //y方向滚动必传,指定滚动区域高度100%, 100px 
    scrollY?: boolean; //滚动方向y轴,与X轴方向指定二选一必传
    scrollX?: boolean; //滚动方向x轴
    width?: string;   //x方向滚动必传,指定滚动区域宽度, 100%, 100px 
    barHeight?: string; //滚动条高度, 滚动条粗细, X轴传, 为0时,滚动条消失
    barWidth?: string; //滚轮动条宽度, 滚动条粗细, Y轴传, 为0时,滚动条消失
    sliderLength?: number; // 即滑块长度, 默认为100px, 自带单位
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值