【ElementPlus源码】Scrollbar 滚动条

news2024/11/17 5:26:41

文章目录

    • thumb
      • clickThumbHandler
      • startDrag
      • mouseMoveDocumentHandler
      • mouseUpDocumentHandler
      • clickTrackHandler
      • 其他
    • bar
    • Scrollbar
      • 导出的方法
      • noresize
      • 更新滚动条相关属性
    • utils
    • runtime.ts
      • buildProps

看源码时候做的笔记。若有问题,请指出!
路径相关格式请看button的源码阅读!

Scrollbar 滚动条组件从里到外分为三个组件:thumb(滚动条的可拖动部分)、bar(thumb的相关信息)和Scrollbar(滚动条组件)。

本博客将从里到外学习这三个组件。

thumb

路径:src/thumb

thumb定义了滚动条的可拖动部分。下面的模板代码定义了一个带有过渡效果的滚动条,包括滚动条本身(track)和可拖动部分(thumb)。

<template>
  <transition :name="ns.b('fade')">
    <div
      v-show="always || visible"
      ref="instance"
      :class="[ns.e('bar'), ns.is(bar.key)]"
      @mousedown="clickTrackHandler"
    >
      <div
        ref="thumb"
        :class="ns.e('thumb')"
        :style="thumbStyle"
        @mousedown="clickThumbHandler"
      />
    </div>
  </transition>
</template>

transition:是vue的一个内置组件,用于在元素或组件的插入、更新、移除时应用过渡效果。它可以在你的组件中添加 进入/离开 的动画和过渡

v-show、class、ref、style等属性不赘述。

此组件绑定了两个事件,分别是clickTrackHandler,处理鼠标点击滚动条轨道的事件;clickThumbHandler,处理鼠标点击滚动条可拖动部分事件。

clickThumbHandler

此方法处理了鼠标点击滚动条可拖动部分事件,做了如下事情:

  1. 阻止事件冒泡
  2. 按下ctrl键或鼠标中/右键则return
  3. 清空选区,方便拖动
  4. startDrag()
  5. 拖动完毕后,计算鼠标点击位置相对于滚动条起始位置的距离

其中,计算鼠标点击位置相对于滚动条起始位置的距离thumbState.value[bar.value.axis],在startDrag() 中会改变。

const clickThumbHandler = (e: MouseEvent) => {
  // prevent click event of middle and right button
  e.stopPropagation()
  // 按下了ctrl键或鼠标中键/右键
  if (e.ctrlKey || [1, 2].includes(e.button)) return
  // 清除所有选区,防止在拖动滚动条时选中文本
  window.getSelection()?.removeAllRanges()
  //  开始拖动
  startDrag(e)

  const el = e.currentTarget as HTMLDivElement
  if (!el) return

  // 鼠标点击位置相对于滚动条的起始位置的距离
  thumbState.value[bar.value.axis] =
    el[bar.value.offset] -
    (e[bar.value.client] - el.getBoundingClientRect()[bar.value.direction])
}

startDrag

此方法是拖动操作。

const startDrag = (e: MouseEvent) => {
  e.stopImmediatePropagation()
  cursorDown = true
  document.addEventListener('mousemove', mouseMoveDocumentHandler)
  document.addEventListener('mouseup', mouseUpDocumentHandler)
  //   阻止用户在拖动过程中选择文本
  originalOnSelectStart = document.onselectstart
  document.onselectstart = () => false
}

mouseMoveDocumentHandler

在拖动方法中调用,添加为mousemove的监听事件。

鼠标移动时,函数会根据鼠标位置更新滚动条thumb的位置,并调整滚动区域的滚动位置。

通俗的语言就是:点击滚动条然后拖动,滚动条和滚动区域都会更新位置,就做了这样的事情。

const mouseMoveDocumentHandler = (e: MouseEvent) => {
  if (!instance.value || !thumb.value) return
  if (cursorDown === false) return

  //   点击时滚动条的位置
  const prevPage = thumbState.value[bar.value.axis]
  if (!prevPage) return

  //   当前鼠标位置与滚动条 轨道 起始位置的距离
  const offset =
    (instance.value.getBoundingClientRect()[bar.value.direction] -
      e[bar.value.client]) *
    -1
  // 当前鼠标位置相对于滚动条"thumb"的起始位置的距离
  const thumbClickPosition = thumb.value[bar.value.offset] - prevPage
  const thumbPositionPercentage =
    ((offset - thumbClickPosition) * 100 * offsetRatio.value) /
    instance.value[bar.value.offset]
  scrollbar.wrapElement[bar.value.scroll] =
    (thumbPositionPercentage * scrollbar.wrapElement[bar.value.scrollSize]) /
    100
}

mouseUpDocumentHandler

鼠标释放时,结束拖动操作。

  • thumbState.value[bar.value.axis] 表示存储滚动条thumb 可拖动部分 的位置
  • bar.value.axis表示正在操作的轴
  • 将可拖动部分设置为0,清除设置的监听
  • document.onselectstart赋值回去

对于if (cursorLeave) visible.value = false,可以在elementPlus滚动条的文档中试验一下,拖动滚动条结束后,滚动条的显示会消失。

const mouseUpDocumentHandler = () => {
  cursorDown = false
  thumbState.value[bar.value.axis] = 0
  document.removeEventListener('mousemove', mouseMoveDocumentHandler)
  document.removeEventListener('mouseup', mouseUpDocumentHandler)
  // document.onselectstart的原始值,允许用户选择文本
  restoreOnselectstart()
  if (cursorLeave) visible.value = false
}

对于restoreOnselectstart():在前面拖动时startDrag(),为了阻止用户在拖动过程中选择到文本,将document.onselectstart的值存到originalOnSelectStart中,将document.onselectstart赋值为一个只返回false的回调函数,它会阻止用户的选择操作。

现在把它恢复。

const restoreOnselectstart = () => {
  if (document.onselectstart !== originalOnSelectStart)
    document.onselectstart = originalOnSelectStart
}

clickTrackHandler

处理鼠标点击滚动条轨道的事件。此方法做的事情是:将滚动条thumb移动到点击位置,并相应地调整滚动区域的滚动位置。

const clickTrackHandler = (e: MouseEvent) => {
  if (!thumb.value || !instance.value || !scrollbar.wrapElement) return

  //   计算点击位置与起始位置的距离
  const offset = Math.abs(
    (e.target as HTMLElement).getBoundingClientRect()[bar.value.direction] -
      e[bar.value.client]
  )
  const thumbHalf = thumb.value[bar.value.offset] / 2
  const thumbPositionPercentage =
    ((offset - thumbHalf) * 100 * offsetRatio.value) /
    instance.value[bar.value.offset]

  scrollbar.wrapElement[bar.value.scroll] =
    (thumbPositionPercentage * scrollbar.wrapElement[bar.value.scrollSize]) /
    100
}

如点击这里:滚动条就会弹到这里,整个容器也会显示到对应位置。

在这里插入图片描述

其他

BAR_MAP,一个只读的枚举对象。

export const BAR_MAP = {
  vertical: {
    offset: 'offsetHeight',
    scroll: 'scrollTop',
    scrollSize: 'scrollHeight',
    size: 'height',
    key: 'vertical',
    axis: 'Y',
    client: 'clientY',
    direction: 'top',
  },
  horizontal: {
    offset: 'offsetWidth',
    scroll: 'scrollLeft',
    scrollSize: 'scrollWidth',
    size: 'width',
    key: 'horizontal',
    axis: 'X',
    client: 'clientX',
    direction: 'left',
  },
} as const

通过props.vertical选择滚动条是垂直或水平,写进style中:

const bar = computed(() => BAR_MAP[props.vertical ? 'vertical' : 'horizontal'])

const thumbStyle = computed(() =>
  renderThumbStyle({
    size: props.size,
    move: props.move,
    bar: bar.value,
  })
)
<div
  ref="thumb"
  :class="ns.e('thumb')"
  :style="thumbStyle"
  @mousedown="clickThumbHandler"
/>

bar

路径:src/bar

包含两个滚动条,水平滚动和垂直滚动。

导出两个方法,handleScrollupdate。分别是:滚动时更新moveY和moveX,更新滚动条的大小和比率。

<template>
  <thumb :move="moveX" :ratio="ratioX" :size="sizeWidth" :always="always" />
  <thumb
    :move="moveY"
    :ratio="ratioY"
    :size="sizeHeight"
    vertical
    :always="always"
  />
</template>

Scrollbar

导出的方法

查看导出的方法:

defineExpose({
  /** @description scrollbar wrap ref */
  wrapRef,
  /** @description update scrollbar state manually */
  update,
  /** @description scrolls to a particular set of coordinates */
  scrollTo,
  /** @description set distance to scroll top */
  setScrollTop,
  /** @description set distance to scroll left */
  setScrollLeft,
  /** @description handle scroll event */
  handleScroll,
})

在这里插入图片描述
wrapRef是滚动条包裹的ref对象。

<template>
  <div ref="scrollbarRef" :class="ns.b()">
    <div
      ref="wrapRef"
      :class="wrapKls"
      :style="wrapStyle"
      @scroll="handleScroll"
    >
      <component
        :is="tag"
        :id="id"
        ref="resizeRef"
        :class="resizeKls"
        :style="viewStyle"
        :role="role"
        :aria-label="ariaLabel"
        :aria-orientation="ariaOrientation"
      >
        <slot />
      </component>
    </div>
    <!-- 如果不用原生的滚动条,就使用自己封装的 两个滚动条 -->
    <template v-if="!native">
      <bar ref="barRef" :always="always" :min-size="minSize" />
    </template>
  </div>
</template>

update是调用bar中导出的update,更新滚动条的大小和比率。

scrollTo用于滚动到指定位置,重载了,有两种调用方式:传入xy坐标和传入包含滚动选项的对象:

(根据注释,这一段代码之后要被重构)

// TODO: refactor method overrides, due to script setup dts
// @ts-nocheck
function scrollTo(xCord: number, yCord?: number): void
function scrollTo(options: ScrollToOptions): void
function scrollTo(arg1: unknown, arg2?: number) {
  // ScrollToOptions,即第二种调用方式
  if (isObject(arg1)) {
    wrapRef.value!.scrollTo(arg1)
    // 第一种调用方式
  } else if (isNumber(arg1) && isNumber(arg2)) {
    wrapRef.value!.scrollTo(arg1, arg2)
  }
}

setScrollTopsetScrollLeft:设置滚动条到顶部/左边的距离。传入参数赋值即可。

const setScrollTop = (value: number) => {
  if (!isNumber(value)) {
    debugWarn(COMPONENT_NAME, 'value must be a number')
    return
  }
  wrapRef.value!.scrollTop = value
}

const setScrollLeft = (value: number) => {
  if (!isNumber(value)) {
    debugWarn(COMPONENT_NAME, 'value must be a number')
    return
  }
  wrapRef.value!.scrollLeft = value
}

noresize

对于noresize这个属性,不响应容器尺寸变化,如果容器尺寸不会发生变化,最好设置它。可以优化性能。

优化原理:

代码中初始化2个没有参数没有返回值(或返回值为undefined)的变量/方法,用来存储停止某些监听器的方法,初始化为undefined:

let stopResizeObserver: (() => void) | undefined = undefined
let stopResizeListener: (() => void) | undefined = undefined

这两个方法是由useResizeObserver和useEventListener这两个hook解构出的。

watch监听noresize这个属性:如果它是true,表示容器尺寸不会变化,就会调用这两个“停止监听”的方法。

  1. 若noresize初始为true且一直为true,则stopResizeObserver和stopResizeListener一直是undefined,无事发生,不会启动监听;
  2. 若noresize为false,则启动监听useResizeObserver和useEventListener,并解构出停止监听的方法。当noresize为true时,调用它们。停止监听。
watch(
  () => props.noresize,
  (noresize) => {
    if (noresize) {
      stopResizeObserver?.()
      stopResizeListener?.()
    } else {
      ;({ stop: stopResizeObserver } = useResizeObserver(resizeRef, update))
      stopResizeListener = useEventListener('resize', update)
    }
  },
  { immediate: true }
)

因此,设置属性noresize可以优化性能,因为它可以停止监听。

import { useEventListener, useResizeObserver } from '@vueuse/core'
这两个hook来自vueuse:useEventListener | VueUse 中文网 (nodejs.cn)

更新滚动条相关属性

props.maxHeightprops.height中任意一个变量变化时,都会触发下面的回调函数,即在下一个DOM更新周期之后更新滚动条的大小和比率(update()),并且处理一些滚动事件.

watch(
  () => [props.maxHeight, props.height],
  () => {
    if (!props.native)
      nextTick(() => {
        update()
        if (wrapRef.value) {
          barRef.value?.handleScroll(wrapRef.value)
        }
      })
  }
)

utils

dom/style

用于将单位添加到给定的值,默认单位为px。

export function addUnit(value?: string | number, defaultUnit = 'px') {
  if (!value) return ''
  if (isNumber(value) || isStringNumber(value)) {
    return `${value}${defaultUnit}`
  } else if (isString(value)) {
    return value
  }

  //   如果value既不是数字也不是字符串,则发出警告:"绑定值必须是字符串或数字"
  debugWarn(SCOPE, 'binding value must be a string or number')
}

runtime.ts

buildProps

用于创建类型安全的props。 具体没看懂

export const buildProps = <
  Props extends Record<
    string,
    | { [epPropKey]: true }
    | NativePropType
    | EpPropInput<any, any, any, any, any>
  >
>(
  props: Props
): {
  [K in keyof Props]: IfEpProp<
    Props[K],
    Props[K],
    IfNativePropType<Props[K], Props[K], EpPropConvert<Props[K]>>
  >
} =>
  fromPairs(
    Object.entries(props).map(([key, option]) => [
      key,
      buildProp(option as any, key),
    ])
  ) as any

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1883009.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是协程?协程和线程的区别

文章目录 前置知识应用程序和内核阻塞和非阻塞同步和异步并发和并行IO 发展历史同步编程异步多线程/进程异步消息 回调函数&#xff08;响应式编程&#xff09; 协程协程基本概念go 示例代码协程和线程的区别 个人简介 前置知识 在了解协程前&#xff0c;我们先理解一些相关的…

VBA数据库解决方案第十二讲:如何判断数据库中数据表是否存在

《VBA数据库解决方案》教程&#xff08;版权10090845&#xff09;是我推出的第二套教程&#xff0c;目前已经是第二版修订了。这套教程定位于中级&#xff0c;是学完字典后的另一个专题讲解。数据库是数据处理的利器&#xff0c;教程中详细介绍了利用ADO连接ACCDB和EXCEL的方法…

平安养老险陕西分公司参加2024上半年省级单位驻富平帮扶团联席会

6月28日&#xff0c;平安养老险陕西分公司工会副主席武媛携驻村工作队赴富平县庄里镇永安村参加2024上半年度省级单位驻富平帮扶团联席会议。 会议由省委金融办副主任、省委金融工委委员李嘉辉及省委金融办选派挂职干部、富平县副县长席玮共同主持。 会上&#xff0c;席玮县长带…

全球AI新闻速递6.28

全球AI新闻速递 1.首款 Transformer 专用 AI 芯片 Sohu 登场。 2.钉钉&#xff1a;宣布对所有AI大模型厂商开放&#xff0c;首批7家接入。 3.华为联合清华大学发布《AI 终端白皮书》。 4.国家卫生健康委&#xff1a;推动AI技术在制定个性化营养、运动干预方案中的应用。 …

地下水电站3D虚拟仿真展示平台

借助先进的VR技术&#xff0c;我们将水电站的每一个角落、每一处细节都以三维全景的形式真实呈现。您可以自由穿梭于水电站的各个区域&#xff0c;无论是发电机组、巍峨的水坝&#xff0c;还是错综复杂的输水管道&#xff0c;都近在咫尺。感受水流的澎湃力量&#xff0c;聆听机…

中文TeX,各种数学符号和表格

\documentclass{article} \usepackage{amsmath,amssymb,amsfonts} \usepackage{CJKutf8} \begin{document}\begin{CJK}{UTF8}{gkai}%正文放在此行下与\end{CJK}之间就行你好, LaTeX!平方根 $\sqrt{x}$立方根 $\sqrt[3]{x}$分数的代码是 $\frac{a}{b}$求和的代码是 $\sum_{i1}^{…

自闭症儿童能不能用药

在星贝育园自闭症儿童康复学校&#xff0c;我们一直秉持着谨慎且保守的态度对待自闭症儿童的用药问题。我们坚定地认为&#xff0c;在大多数情况下&#xff0c;药物并非自闭症儿童康复的首选。 自闭症是一种神经发育障碍&#xff0c;其核心症状包括社交沟通障碍、重复刻…

API-元素尺寸与位置

学习目标&#xff1a; 掌握元素尺寸与位置 学习内容&#xff1a; 元素尺寸与位置仿京东固定导航栏案例实现bilibili点击小滑块移动效果 元素尺寸与位置&#xff1a; 使用场景&#xff1a; 前面案例滚动多少距离&#xff0c;都是我们自己算的&#xff0c;最好是页面滚动到某个…

快钱支付股东全部股权已被质押!

根据近期工商信息&#xff0c;第三方支付机构快钱支付清算信息有限公司&#xff08;简称“快钱支付”&#xff09;实际控股方快钱金融服务&#xff08;上海&#xff09;有限公司&#xff08;简称“快钱金融”&#xff09;&#xff0c;作为出质股权标的企业&#xff0c;被出质给…

MQTT协议详述

MQTT 概述 消息队列遥测传输&#xff08;英语&#xff1a;Message Queuing Telemetry Transport&#xff0c;缩写&#xff1a;MQTT&#xff09;&#xff0c;是基于发布&#xff08;Publish&#xff09;/订阅&#xff08;Subscribe&#xff09;范式的消息协议&#xff0c;位于…

qt QTreeView的简单使用(多级子节点)

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);setWindowTitle("QTreeView的简单使用");model new QStandardItemModel;model->setHorizontalHeaderLabels(QStringList() << "left&q…

解决OneDrive “拒绝访问文件” 问题

问题描述&#xff1a; 在尝试将其他文件拖入oneDrive或是打开OneDrive中的文件时。出现如下报错&#xff1a; 拒绝访问文件 无法访问XXXXXXX中的文件。可能已移动或删除了此文件&#xff0c;或者受制于文件权限而不能访问。 ERR_ACCESS_DENIED 解决办法&#xff1a; 1. 找到O…

统计学三学习笔记

一&#xff0c;t分布 二&#xff0c;置信区间 最终要用② n越大&#xff0c;s越小&#xff0c;置信区间越小 三&#xff0c;配对样本t检验 假如有两个族群&#xff1a;

2024-07-01_外语学习

文章目录 前言1、Los Angeles至于单个los 是什么意思&#xff1f;我们可以逐词翻译这个西班牙语句子 2. Extraneous non-props attributes (style) were passed to component but could not be automatically inherited because component renders fragment or text root nodes…

【UE5.1】Chaos物理系统基础——02 场系统的应用

目录 步骤 一、运用临时场&#xff08;外部张力&#xff09;破裂几何体集 二、使用构造场固定几何体集 步骤 在上一篇中&#xff08;【UE5.1】Chaos物理系统基础——01 创建可被破坏的物体&#xff09;我们已经创建了可被破碎的几何体集&#xff0c;在最后我们防止几何体集…

python(6)numpy的使用详细讲解

在numpy中&#xff0c;最基本的数据结构是数组&#xff0c;因此我们首先需要了解如何创建一个数组。numpy提供了多种数组创建方法&#xff0c;包括从列表或元组创建、从文件中读取数据、使用特定函数创建等。下面是一些常用的创建方法&#xff1a; 一、创建数组 1. 从列表或元…

【YOLOv5/v7改进系列】更换损失函数为CIOU、GIOU、SIOU、DIOU、EIOU、WIOUv1/v2/v3、Focal C/G/S/D/EIOU等

一、导言 在目标检测任务中&#xff0c;损失函数的主要作用是衡量模型预测的边界框&#xff08;bounding boxes&#xff09;与真实边界框之间的匹配程度&#xff0c;并指导模型学习如何更精确地定位和分类目标。损失函数通常由两部分构成&#xff1a;分类损失&#xff08;用于…

叮!云原生虚拟数仓 PieCloudDB Database 动态包裹已送达

第一部分 PieCloudDB Database 最新动态 支持动态配置查询簇 PieCloudDB 最新内核版本 v2.14.0 新增动态配置查询簇功能。PieCloudDB 动态配置查询簇功能实现可伸缩的并行化查询&#xff0c;可提升单个查询并行使用底层资源的能力&#xff0c;同时加快查询响应速度。 动态配…

【论文阅读】-- TimeNotes:时间序列数据的有效图表可视化和交互技术研究

TimeNotes: A Study on Effective Chart Visualization and Interaction Techniques for Time-Series Data 摘要1 介绍和动机2 文献2.1 时间序列数据探索2.1.1 数据聚合2.1.2 基于透镜2.1.3 基于布局 3 任务和设计3.1 数据3.2 领域表征3.3 探索、分析和呈现 4 TimeNotes4.1 布局…