npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nami-ui/use-slider

v0.0.5

Published

a slider hook.

Downloads

15

Readme


id: use-slider title: useSlider subtitle: 滑动交互

提供一套滑动交互逻辑,可以用于实现大多数的滑动组件,类似 滑块 或 旋纽 等。

一个完整的 slider 应至少包含如下三个元素:

  • 容器(root):包含其它所有元素,响应指针、滚轮及键盘事件,可以获取焦点;
  • 滑轨(rail):用于限定滑块的滑动区域;
  • 滑块(thumb):指示当前滑动位置及滑动状态。

具体可查看下面示例的代码。

示例

横向滑动选择器

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles({
  root: {
    maxWidth: 300,
    height: 40,
    background: '#ddd',
    borderRadius: 20,
    position: 'relative',
  },
  rail: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 20,
    right: 20,
  },
  thumb: {
    width: 40,
    height: 40,
    borderRadius: 20,
    transform: 'translate(-20px, 0)',
    position: 'absolute',
    top: 0,

    // TODO: 不知道这样性能怎么样,有时间可以测试一下
    left: (props) => props.value * 100 + '%',
    background: (props) => (props.sliding ? '#000' : '#777'),
  },
})

export default () => {
  const [value, setValue] = useState([0])

  const slider = useSlider({
    value,
    setValue,
    moving: ({ px }) => px,
  })

  const classes = useStyles({
    value: value[0],
    sliding: slider.sliding,
  })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <div className={classes.rail} {...slider.railProps}>
        <div
          className={classes.thumb}
          {...slider.thumbProps}
        />
      </div>
    </div>
  )
}

垂直滑动选择器

将横向滑动选择器改为垂直方向非常简单,只需要在 moving 中将 px 改为 py,剩下的的就都是样式上的调整:

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles({
  root: {
    width: 40,
    height: 300,
    background: '#ddd',
    borderRadius: 20,
    position: 'relative',
  },
  rail: {
    position: 'absolute',
    top: 20,
    bottom: 20,
    left: 0,
    right: 0,
  },
  thumb: {
    width: 40,
    height: 40,
    borderRadius: 20,
    transform: 'translate(0, -20px)',
    position: 'absolute',
    left: 0,

    top: (props) => props.value * 100 + '%',
    background: (props) => (props.sliding ? '#000' : '#777'),
  },
})

export default () => {
  const [value, setValue] = useState([0])

  const slider = useSlider({
    value,
    setValue,
    moving: ({ py }) => py,
  })

  const classes = useStyles({
    value: value[0],
    sliding: slider.sliding,
  })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <div className={classes.rail} {...slider.railProps}>
        <div
          className={classes.thumb}
          {...slider.thumbProps}
        />
      </div>
    </div>
  )
}

平面滑动选择器

而同时使用 pxpy,就可以实现平面滑动选择器了:

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles({
  root: {
    maxWidth: 300,
    height: 200,
    background: '#ddd',
    borderRadius: 20,
    position: 'relative',
  },
  rail: {
    position: 'absolute',
    top: 20,
    bottom: 20,
    left: 20,
    right: 20,
  },
  thumb: {
    width: 40,
    height: 40,
    borderRadius: 20,
    transform: 'translate(-20px, -20px)',
    position: 'absolute',

    top: (props) => props.value.y * 100 + '%',
    left: (props) => props.value.x * 100 + '%',
    background: (props) => (props.sliding ? '#000' : '#777'),
  },
})

export default () => {
  const [value, setValue] = useState([{ x: 0, y: 0 }])

  const slider = useSlider({
    value,
    setValue,
    moving: ({ px, py }) => ({ x: px, y: py }),
  })

  const classes = useStyles({
    value: value[0],
    sliding: slider.sliding,
  })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <div className={classes.rail} {...slider.railProps}>
        <div
          className={classes.thumb}
          {...slider.thumbProps}
        />
      </div>
    </div>
  )
}

表格行列数选择器(常用于富文本编辑器)

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'
import clsx from 'clsx'

const COL_SIZE = 26
const COL_SPACE = 1
const MIN_LEN = 3
const MAX_LEN = 10

const useStyles = createUseStyles({
  root: {
    position: 'relative',
    width: (props) =>
      props.collen * COL_SIZE +
      (props.collen - 1) * COL_SPACE,
  },
  rail: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
  },
})

export default () => {
  const [value, setValue] = useState([{ row: 2, col: 2 }])

  const slider = useSlider({
    value,
    setValue,
    axis: { step: 1, min: 1, max: MAX_LEN },
    moving: ({ breakX: x, breakY: y }) => ({
      row: Math.ceil(y / (COL_SIZE + COL_SPACE)),
      col: Math.ceil(x / (COL_SIZE + COL_SPACE)),
    }),
  })

  const { row, col } = value[0]
  const rowlen = Math.max(MIN_LEN, row)
  const collen = Math.max(MIN_LEN, col)

  const classes = useStyles({ rowlen, collen })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <TableMock
        row={row}
        col={col}
        rowlen={rowlen}
        collen={collen}
      />
      <div className={classes.rail} {...slider.railProps}>
        <div {...slider.thumbProps} />
      </div>
    </div>
  )
}

const useTableMockStyles = createUseStyles({
  row: {
    display: 'flex',
    marginTop: COL_SPACE,
  },
  col: {
    width: COL_SIZE,
    height: COL_SIZE,
    border: '1px solid #ddd',
    marginLeft: COL_SPACE,
  },
  firstRow: {
    marginTop: 0,
  },
  firstCol: {
    marginLeft: 0,
  },
  colInFirstRow: {
    background: '#f0f0f0',
  },
  selectedCol: {
    position: 'relative',
    '&:after': {
      content: '""',
      position: 'absolute',
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
      background: 'rgb(11 142 208 / 15%)',
    },
  },
})

function TableMock({ row, col, rowlen, collen }) {
  const classes = useTableMockStyles()

  const rows = []
  for (let i = 0; i < rowlen; i++) {
    const isFirstRow = i === 0

    const cols = []
    for (let j = 0; j < collen; j++) {
      const isFirstCol = j === 0

      cols.push(
        <div
          key={j}
          className={clsx(classes.col, {
            [classes.firstCol]: isFirstCol,
            [classes.colInFirstRow]: isFirstRow,
            [classes.selectedCol]: i < row && j < col,
          })}
        />,
      )
    }

    rows.push(
      <div
        key={i}
        className={clsx(classes.row, {
          [classes.firstRow]: isFirstRow,
        })}
      >
        {cols}
      </div>,
    )
  }

  return rows
}

类型定义

useSlider 所接收参数及返回值类型定义如下:

interface useSlider {
    (props: UseSliderProps): Slider
}

interface UseSliderProps {
    // 一组值,分别对应每个滑块
    value: Value
    // 设置值
    setValue: React.Dispatch<React.SetStateAction<Value>>

    // 数轴
    axis: Axis | { [prop: string]: Axis }

    // 是否禁用
    disabled: boolean

    // 处理指针(鼠标、手指)拖拽事件
    moving: (event: MovingEvent) => ValuePatch

    // 处理滚轮事件
    wheel?: (event: WheelEvent) => ValuePatch

    // 快捷键
    hotkeys?: Hotkey[]
}

interface Slider {
    // 需要分别注入到对应元素上的属性
    rootProps: HTMLAttributes<HTMLElement>
    railProps: HTMLAttributes<HTMLElement>
    thumbProps: HTMLAttributes<HTMLElement>

    // 是否在滑动中
    sliding: boolean

    // 当前正在滑动的滑块索引
    thumb: number | undefined
}

// 每个滑块的值;可以是单个数值,如:`[0, 1]`,
// 也可以是包含一组数值的对象,如:`[ { x: 0, y: 0 }, { x: 1, y: 1 } ]`
type Value = number[] | { [prop: string]: number }[]

// 数轴,用于限定滑块在某个方向上的数值;若滑块值为单个数值,则只能指定一个数轴,
// 而若是包含一组数值的对象,则既可以指定一个通用数轴,也可以分别为每个数值属性指定对应的数轴,
// 如:{ x: { min: 0, max: 100 }, y: { min: 0, max: 60 }  }
interface Axis {
    // 最小值
    min: number

    // 最大值
    max: number

    // 步长
    step?: number

    // 额外数值点
    points?: number[]
}

// 用于更新滑块值,需要在事件处理器中返回,如:
// - 更新到指定数值:`0.2`,或 `{ x: 0.2 }`;
// - 更新到下一个或上一个值:`'next'` 或 `{ x: 'prev' }`;
// - 在当前值的基础上加减指定值:`'+0.2'`,或 `{ x: '-0.2' }`;
// - 自定义更新,如:`prevValue => prevValue / 2` 或 `prevValue => ( { x: prevValue.x / 2 } )`
type ValuePatch =
    | number
    | 'prev' | 'next' | string
    | { [prop: string]: number | 'prev' | 'next' | string }
    | (value: Value) => Value

// 指针拖拽事件
interface MovingEvent {
    // 指针相对于轨道元素的坐标位置
    x: number
    y: number
    breakX: number
    breakY: number

    // 指针相对于轨道元素的位置百分比
    px: number
    py: number
    breakPX: number
    breakPY: number

    // 指针移动方向
    dirX: 'left' | 'right'
    dirY: 'up' | 'down'

    // 指针移动速度
    velocity: number
}

// 滚轮事件
interface WheelEvent {
    // 滚轮滚动值
    deltaX: number
    deltaY: number
    deltaZ: number

    // 修饰键
    altKey: boolean
    ctrlKey: boolean
    metaKey: boolean
    shiftKey: boolean
}

// 快捷键事件
interface Hotkey {
    // 需要监听的快捷键,如: `a, ctrl-a`(特殊值 `ANY`,用于监听任意快捷键)
    keys: string

    // 可选修饰键,如 `{ key: 'ctrl-a', shift: true }` 表示监听快捷键 `ctrl-a` 及 `ctrl-shift-a`。
    shift?: boolean
    ctrl?: boolean
    meta?: boolean
    alt?: boolean

    // 事件处理函数
    handle: (eventData: HotkeyEvent) => ValuePatch | void
}

interface HotkeyEvent {
    // 用户所按下的快捷键,对应在 Hotkey.keys 中配置的某个快捷键
    readonly key: string

    /** 修饰键 */
    readonly ctrl: boolean
    readonly shift: boolean
    readonly meta: boolean
    readonly alt: boolean

    // 停止事件传播
    readonly stopPropagation: () => void

    // 阻止事件默认行为
    readonly preventDefault: () => void

    // 是否已阻止事件默认行为
    readonly defaultPrevented: boolean

    // 是否已匹配并触发某个快捷键
    readonly dispatched: boolean
}

基础样式

这三者的基础样式可如下所示:

.container {
  position: relative;
}

.rail {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.thumb {
  width: 40px;
  height: 40px;

  position: absolute;
  left: 0;
  top: 0;

  transform: translate(-20px, 0);
}