@v-c/slider 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Handles/Handle.cjs +1 -0
  3. package/dist/Handles/Handle.d.ts +107 -0
  4. package/dist/Handles/Handle.js +203 -0
  5. package/dist/Handles/index.cjs +1 -0
  6. package/dist/Handles/index.d.ts +98 -0
  7. package/dist/Handles/index.js +117 -0
  8. package/dist/Marks/Mark.cjs +1 -0
  9. package/dist/Marks/Mark.d.ts +9 -0
  10. package/dist/Marks/Mark.js +39 -0
  11. package/dist/Marks/index.cjs +1 -0
  12. package/dist/Marks/index.d.ts +15 -0
  13. package/dist/Marks/index.js +31 -0
  14. package/dist/Slider.cjs +1 -0
  15. package/dist/Slider.d.ts +253 -0
  16. package/dist/Slider.js +343 -0
  17. package/dist/Steps/Dot.cjs +1 -0
  18. package/dist/Steps/Dot.d.ts +9 -0
  19. package/dist/Steps/Dot.js +38 -0
  20. package/dist/Steps/index.cjs +1 -0
  21. package/dist/Steps/index.d.ts +11 -0
  22. package/dist/Steps/index.js +41 -0
  23. package/dist/Tracks/Track.cjs +1 -0
  24. package/dist/Tracks/Track.d.ts +61 -0
  25. package/dist/Tracks/Track.js +82 -0
  26. package/dist/Tracks/index.cjs +1 -0
  27. package/dist/Tracks/index.d.ts +47 -0
  28. package/dist/Tracks/index.js +83 -0
  29. package/dist/context.cjs +1 -0
  30. package/dist/context.d.ts +51 -0
  31. package/dist/context.js +27 -0
  32. package/dist/hooks/useDrag.cjs +1 -0
  33. package/dist/hooks/useDrag.d.ts +11 -0
  34. package/dist/hooks/useDrag.js +88 -0
  35. package/dist/hooks/useOffset.cjs +1 -0
  36. package/dist/hooks/useOffset.d.ts +10 -0
  37. package/dist/hooks/useOffset.js +98 -0
  38. package/dist/hooks/useRange.cjs +1 -0
  39. package/dist/hooks/useRange.d.ts +8 -0
  40. package/dist/hooks/useRange.js +10 -0
  41. package/dist/index.cjs +1 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -0
  44. package/dist/interface.cjs +1 -0
  45. package/dist/interface.d.ts +7 -0
  46. package/dist/interface.js +1 -0
  47. package/dist/util.cjs +1 -0
  48. package/dist/util.d.ts +6 -0
  49. package/dist/util.js +29 -0
  50. package/docs/TooltipSlider.tsx +94 -0
  51. package/docs/assets/anim.less +63 -0
  52. package/docs/assets/bootstrap.less +163 -0
  53. package/docs/assets/index.less +337 -0
  54. package/docs/debug.vue +60 -0
  55. package/docs/editable.vue +59 -0
  56. package/docs/handle.vue +45 -0
  57. package/docs/marks.vue +85 -0
  58. package/docs/multiple.vue +54 -0
  59. package/docs/range.vue +211 -0
  60. package/docs/slider.stories.vue +45 -0
  61. package/docs/sliderDemo.vue +267 -0
  62. package/docs/vertical.vue +122 -0
  63. package/package.json +35 -0
  64. package/src/Handles/Handle.tsx +226 -0
  65. package/src/Handles/index.tsx +124 -0
  66. package/src/Marks/Mark.tsx +40 -0
  67. package/src/Marks/index.tsx +40 -0
  68. package/src/Slider.tsx +582 -0
  69. package/src/Steps/Dot.tsx +40 -0
  70. package/src/Steps/index.tsx +54 -0
  71. package/src/Tracks/Track.tsx +89 -0
  72. package/src/Tracks/index.tsx +92 -0
  73. package/src/context.ts +65 -0
  74. package/src/hooks/useDrag.ts +244 -0
  75. package/src/hooks/useOffset.ts +264 -0
  76. package/src/hooks/useRange.ts +24 -0
  77. package/src/index.ts +8 -0
  78. package/src/interface.ts +17 -0
  79. package/src/util.ts +41 -0
  80. package/vite.config.ts +18 -0
  81. package/vitest.config.ts +11 -0
@@ -0,0 +1,89 @@
1
+ import type { CSSProperties, PropType } from 'vue'
2
+ import type { OnStartMove } from '../interface'
3
+ import cls from 'classnames'
4
+ import { defineComponent } from 'vue'
5
+ import { useInjectSlider } from '../context'
6
+ import { getOffset } from '../util'
7
+
8
+ export interface TrackProps {
9
+ prefixCls: string
10
+ /** Replace with origin prefix concat className */
11
+ replaceCls?: string
12
+ start: number
13
+ end: number
14
+ index: number
15
+ onStartMove?: OnStartMove
16
+ }
17
+
18
+ const Track = defineComponent({
19
+ name: 'Track',
20
+ props: {
21
+ prefixCls: { type: String, required: true },
22
+ replaceCls: { type: String },
23
+ start: { type: Number, required: true },
24
+ end: { type: Number, required: true },
25
+ index: { type: Number, default: () => null },
26
+ onStartMove: { type: Function as PropType<OnStartMove> },
27
+ },
28
+ setup(props, { attrs }) {
29
+ const { direction, min, max, disabled, range, classNames } = useInjectSlider()
30
+
31
+ // ============================ Events ============================
32
+ const onInternalStartMove = (e: MouseEvent | TouchEvent) => {
33
+ if (!disabled && props.onStartMove) {
34
+ props.onStartMove(e, -1)
35
+ }
36
+ }
37
+
38
+ // ============================ Render ============================
39
+ const positionStyle: CSSProperties = {}
40
+
41
+ return () => {
42
+ const { prefixCls, index, onStartMove, replaceCls, start, end } = props
43
+
44
+ const offsetStart = getOffset(start, min.value, max.value)
45
+ const offsetEnd = getOffset(end, min.value, max.value)
46
+
47
+ const trackPrefixCls = `${prefixCls}-track`
48
+ const className
49
+ = replaceCls
50
+ || cls(
51
+ trackPrefixCls,
52
+ {
53
+ [`${trackPrefixCls}-${index + 1}`]: index !== null && range,
54
+ [`${prefixCls}-track-draggable`]: onStartMove,
55
+ },
56
+ classNames.track,
57
+ )
58
+ switch (direction.value) {
59
+ case 'rtl':
60
+ positionStyle.right = `${offsetStart * 100}%`
61
+ positionStyle.width = `${offsetEnd * 100 - offsetStart * 100}%`
62
+ break
63
+
64
+ case 'btt':
65
+ positionStyle.bottom = `${offsetStart * 100}%`
66
+ positionStyle.height = `${offsetEnd * 100 - offsetStart * 100}%`
67
+ break
68
+
69
+ case 'ttb':
70
+ positionStyle.top = `${offsetStart * 100}%`
71
+ positionStyle.height = `${offsetEnd * 100 - offsetStart * 100}%`
72
+ break
73
+
74
+ default:
75
+ positionStyle.left = `${offsetStart * 100}%`
76
+ positionStyle.width = `${offsetEnd * 100 - offsetStart * 100}%`
77
+ }
78
+ return (
79
+ <div
80
+ class={className}
81
+ style={{ ...positionStyle, ...attrs.style as CSSProperties }}
82
+ onMousedown={onStartMove ? onInternalStartMove : undefined}
83
+ />
84
+ )
85
+ }
86
+ },
87
+ })
88
+
89
+ export default Track
@@ -0,0 +1,92 @@
1
+ import type { CSSProperties, PropType } from 'vue'
2
+ import type { OnStartMove } from '../interface'
3
+ import cls from 'classnames'
4
+ import { computed, defineComponent } from 'vue'
5
+ import { useInjectSlider } from '../context'
6
+ import { getIndex } from '../util'
7
+ import Track from './Track'
8
+
9
+ export interface TrackProps {
10
+ prefixCls: string
11
+ style?: CSSProperties | CSSProperties[]
12
+ values: number[]
13
+ onStartMove?: OnStartMove
14
+ startPoint?: number
15
+ }
16
+
17
+ const Tracks = defineComponent({
18
+ name: 'Tracks',
19
+ props: {
20
+ prefixCls: { type: String, required: true },
21
+ trackStyle: { type: [Object, Array] as PropType<CSSProperties | CSSProperties[]> },
22
+ values: { type: Array as PropType<number[]>, required: true },
23
+ onStartMove: { type: Function as PropType<OnStartMove> },
24
+ startPoint: { type: Number },
25
+ },
26
+ setup(props) {
27
+ const { included, range, min, styles, classNames } = useInjectSlider()
28
+
29
+ // =========================== List ===========================
30
+ const trackList = computed(() => {
31
+ if (!range) {
32
+ // null value do not have track
33
+ if (props.values.length === 0) {
34
+ return []
35
+ }
36
+
37
+ const startValue = props.startPoint ?? min.value
38
+ const endValue = props.values[0]
39
+
40
+ return [{ start: Math.min(startValue, endValue), end: Math.max(startValue, endValue) }]
41
+ }
42
+
43
+ // Multiple
44
+ const list: { start: number, end: number }[] = []
45
+
46
+ for (let i = 0; i < props.values.length - 1; i += 1) {
47
+ list.push({ start: props.values[i], end: props.values[i + 1] })
48
+ }
49
+
50
+ return list
51
+ })
52
+
53
+ return () => {
54
+ if (!included) {
55
+ return null
56
+ }
57
+
58
+ // ========================== Render ==========================
59
+ const tracksNode
60
+ = trackList.value?.length && (classNames.tracks || styles.tracks)
61
+ ? (
62
+ <Track
63
+ index={0}
64
+ prefixCls={props.prefixCls}
65
+ start={trackList.value[0].start}
66
+ end={trackList.value[trackList.value.length - 1].end}
67
+ replaceCls={cls(classNames.tracks, `${props.prefixCls}-tracks`)}
68
+ style={styles.tracks}
69
+ />
70
+ )
71
+ : null
72
+ return (
73
+ <>
74
+ {tracksNode}
75
+ {trackList.value.map(({ start, end }, index) => (
76
+ <Track
77
+ index={index}
78
+ prefixCls={props.prefixCls}
79
+ style={{ ...getIndex(props.trackStyle, index), ...styles?.track }}
80
+ start={start}
81
+ end={end}
82
+ key={index}
83
+ onStartMove={props.onStartMove}
84
+ />
85
+ ))}
86
+ </>
87
+ )
88
+ }
89
+ },
90
+ })
91
+
92
+ export default Tracks
package/src/context.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { InjectionKey, ShallowRef } from 'vue'
2
+ import type { AriaValueFormat, Direction, SliderClassNames, SliderStyles } from './interface'
3
+ import { inject, provide } from 'vue'
4
+
5
+ export interface SliderContextProps {
6
+ min: ShallowRef<number>
7
+ max: ShallowRef<number>
8
+ includedStart: number
9
+ includedEnd: number
10
+ direction: ShallowRef<Direction>
11
+ disabled?: boolean
12
+ keyboard?: boolean
13
+ included?: boolean
14
+ step: ShallowRef<number | null>
15
+ range?: boolean
16
+ tabIndex: number | number[]
17
+ ariaLabelForHandle?: string | string[]
18
+ ariaLabelledByForHandle?: string | string[]
19
+ ariaRequired?: boolean
20
+ ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[]
21
+ classNames: SliderClassNames
22
+ styles: SliderStyles
23
+ }
24
+
25
+ const SliderContextKey: InjectionKey<SliderContextProps> = Symbol('SliderContext')
26
+
27
+ export const defaultSliderContextValue = {
28
+ min: 0,
29
+ max: 0,
30
+ direction: 'ltr',
31
+ step: 1,
32
+ includedStart: 0,
33
+ includedEnd: 0,
34
+ tabIndex: 0,
35
+ keyboard: true,
36
+ styles: {},
37
+ classNames: {},
38
+ }
39
+
40
+ export function useProviderSliderContext(ctx: SliderContextProps) {
41
+ provide(SliderContextKey, ctx)
42
+ }
43
+ export function useInjectSlider(): SliderContextProps {
44
+ return inject(SliderContextKey)!
45
+ }
46
+
47
+ export interface UnstableContextProps {
48
+ onDragStart?: (info: {
49
+ rawValues: number[]
50
+ draggingIndex: number
51
+ draggingValue: number
52
+ }) => void
53
+ onDragChange?: (info: {
54
+ rawValues: number[]
55
+ deleteIndex: number
56
+ draggingIndex: number
57
+ draggingValue: number
58
+ }) => void
59
+ }
60
+
61
+ /** @private NOT PROMISE AVAILABLE. DO NOT USE IN PRODUCTION. */
62
+ export const UnstableContextKey: InjectionKey<UnstableContextProps> = Symbol('UnstableContext')
63
+
64
+ // 默认值
65
+ export const defaultUnstableContextValue: UnstableContextProps = {}
@@ -0,0 +1,244 @@
1
+ import type { Ref, ShallowRef } from 'vue'
2
+ import type { Direction, OnStartMove } from '../interface'
3
+ import type { OffsetValues } from './useOffset'
4
+ import useEvent from '@v-c/util/dist/hooks/useEvent'
5
+ import { computed, inject, onUnmounted, ref } from 'vue'
6
+ import { defaultUnstableContextValue, UnstableContextKey } from '../context'
7
+
8
+ /** Drag to delete offset. It's a user experience number for dragging out */
9
+ const REMOVE_DIST = 130
10
+
11
+ function getPosition(e: MouseEvent | TouchEvent) {
12
+ const obj = 'targetTouches' in e ? e.targetTouches[0] : e
13
+
14
+ return { pageX: obj.pageX, pageY: obj.pageY }
15
+ }
16
+
17
+ function useDrag(
18
+ containerRef: Ref<HTMLDivElement>,
19
+ direction: ShallowRef<Direction>,
20
+ rawValues: Ref<number[]>,
21
+ min: ShallowRef<number>,
22
+ max: ShallowRef<number>,
23
+ formatValue: (value: number) => number,
24
+ triggerChange: (values: number[]) => void,
25
+ finishChange: (draggingDelete: boolean) => void,
26
+ offsetValues: OffsetValues,
27
+ editable: boolean,
28
+ minCount: number,
29
+ ): [
30
+ draggingIndex: Ref<number>,
31
+ draggingValue: Ref<number>,
32
+ draggingDelete: Ref<boolean>,
33
+ returnValues: Ref<number[]>,
34
+ onStartMove: OnStartMove,
35
+ ] {
36
+ const draggingValue = ref<number | null>(null)
37
+ const draggingIndex = ref<number>(-1)
38
+ const draggingDelete = ref<boolean>(false)
39
+ const cacheValues = ref<number[]>(rawValues.value)
40
+ const originValues = ref<number[]>(rawValues.value)
41
+
42
+ const mouseMoveEventRef = ref<((event: MouseEvent | TouchEvent) => void) | null>(null)
43
+ const mouseUpEventRef = ref<((event: MouseEvent | TouchEvent) => void) | null>(null)
44
+ const touchEventTargetRef = ref<EventTarget | null>(null)
45
+
46
+ const unstableContext = inject(UnstableContextKey, defaultUnstableContextValue)
47
+ const { onDragStart, onDragChange } = unstableContext
48
+
49
+ if (draggingIndex.value === -1) {
50
+ cacheValues.value = rawValues.value
51
+ }
52
+
53
+ // Clean up event
54
+ onUnmounted(() => {
55
+ document.removeEventListener('mousemove', mouseMoveEventRef.value)
56
+ document.removeEventListener('mouseup', mouseUpEventRef.value)
57
+ if (touchEventTargetRef.value) {
58
+ touchEventTargetRef.value.removeEventListener('touchmove', mouseMoveEventRef.value)
59
+ touchEventTargetRef.value.removeEventListener('touchend', mouseUpEventRef.value)
60
+ }
61
+ })
62
+
63
+ const flushValues = (nextValues: number[], nextValue?: number, deleteMark?: boolean) => {
64
+ // Perf: Only update state when value changed
65
+ if (nextValue !== undefined) {
66
+ draggingValue.value = nextValue
67
+ }
68
+ cacheValues.value = nextValues
69
+
70
+ let changeValues = nextValues
71
+ if (deleteMark) {
72
+ changeValues = nextValues.filter((_, i) => i !== draggingIndex.value)
73
+ }
74
+ triggerChange(changeValues)
75
+
76
+ if (onDragChange) {
77
+ onDragChange({
78
+ rawValues: nextValues,
79
+ deleteIndex: deleteMark ? draggingIndex.value : -1,
80
+ draggingIndex: draggingIndex.value,
81
+ draggingValue: nextValue!,
82
+ })
83
+ }
84
+ }
85
+
86
+ const updateCacheValue = useEvent(
87
+ (valueIndex: number, offsetPercent: number, deleteMark: boolean) => {
88
+ if (valueIndex === -1) {
89
+ // >>>> Dragging on the track
90
+ const startValue = originValues.value[0]
91
+ const endValue = originValues.value[originValues.value.length - 1]
92
+ const maxStartOffset = min.value - startValue
93
+ const maxEndOffset = max.value - endValue
94
+
95
+ // Get valid offset
96
+ let offset = offsetPercent * (max.value - min.value)
97
+ offset = Math.max(offset, maxStartOffset)
98
+ offset = Math.min(offset, maxEndOffset)
99
+
100
+ // Use first value to revert back of valid offset (like steps marks)
101
+ const formatStartValue = formatValue(startValue + offset)
102
+ offset = formatStartValue - startValue
103
+ const cloneCacheValues = originValues.value.map<number>(val => val + offset)
104
+ flushValues(cloneCacheValues)
105
+ }
106
+ else {
107
+ // >>>> Dragging on the handle
108
+ const offsetDist = (max.value - min.value) * offsetPercent
109
+
110
+ // Always start with the valueIndex origin value
111
+ const cloneValues = [...cacheValues.value]
112
+ cloneValues[valueIndex] = originValues.value[valueIndex]
113
+
114
+ const next = offsetValues(cloneValues, offsetDist, valueIndex, 'dist')
115
+
116
+ flushValues(next.values, next.value, deleteMark)
117
+ }
118
+ },
119
+ )
120
+
121
+ const onStartMove: OnStartMove = (e, valueIndex, startValues?: number[]) => {
122
+ e.stopPropagation()
123
+ console.log('onStartMove', valueIndex)
124
+ // 如果是点击 track 触发的,需要传入变化后的初始值,而不能直接用 rawValues
125
+ const initialValues = startValues || rawValues.value
126
+ const originValue = initialValues[valueIndex]
127
+
128
+ draggingIndex.value = valueIndex
129
+ draggingValue.value = originValue
130
+ originValues.value = initialValues
131
+ cacheValues.value = initialValues
132
+ draggingDelete.value = false
133
+
134
+ const { pageX: startX, pageY: startY } = getPosition(e)
135
+
136
+ // We declare it here since closure can't get outer latest value
137
+ let deleteMark = false
138
+
139
+ // Internal trigger event
140
+ if (onDragStart) {
141
+ onDragStart({
142
+ rawValues: initialValues,
143
+ draggingIndex: valueIndex,
144
+ draggingValue: originValue,
145
+ })
146
+ }
147
+
148
+ // Moving
149
+ const onMouseMove = (event: MouseEvent | TouchEvent) => {
150
+ event.preventDefault()
151
+ const { pageX: moveX, pageY: moveY } = getPosition(event)
152
+ const offsetX = moveX - startX
153
+ const offsetY = moveY - startY
154
+
155
+ const { width, height } = containerRef.value.getBoundingClientRect()
156
+
157
+ let offSetPercent: number
158
+ let removeDist: number
159
+
160
+ switch (direction.value) {
161
+ case 'btt':
162
+ offSetPercent = -offsetY / height
163
+ removeDist = offsetX
164
+ break
165
+
166
+ case 'ttb':
167
+ offSetPercent = offsetY / height
168
+ removeDist = offsetX
169
+ break
170
+
171
+ case 'rtl':
172
+ offSetPercent = -offsetX / width
173
+ removeDist = offsetY
174
+ break
175
+
176
+ default:
177
+ offSetPercent = offsetX / width
178
+ removeDist = offsetY
179
+ }
180
+
181
+ // Check if need mark remove
182
+ deleteMark = editable
183
+ ? Math.abs(removeDist) > REMOVE_DIST && minCount < cacheValues.value.length
184
+ : false
185
+ draggingDelete.value = deleteMark
186
+
187
+ updateCacheValue(valueIndex, offSetPercent, deleteMark)
188
+ }
189
+
190
+ // End
191
+ const onMouseUp = (event: MouseEvent | TouchEvent) => {
192
+ event.preventDefault()
193
+
194
+ document.removeEventListener('mouseup', onMouseUp)
195
+ document.removeEventListener('mousemove', onMouseMove)
196
+ if (touchEventTargetRef.value) {
197
+ touchEventTargetRef.value.removeEventListener('touchmove', mouseMoveEventRef.value)
198
+ touchEventTargetRef.value.removeEventListener('touchend', mouseUpEventRef.value)
199
+ }
200
+ mouseMoveEventRef.value = null
201
+ mouseUpEventRef.value = null
202
+ touchEventTargetRef.value = null
203
+
204
+ finishChange(deleteMark)
205
+
206
+ draggingIndex.value = -1
207
+ draggingDelete.value = false
208
+ }
209
+
210
+ document.addEventListener('mouseup', onMouseUp)
211
+ document.addEventListener('mousemove', onMouseMove)
212
+ e.currentTarget.addEventListener('touchend', onMouseUp)
213
+ e.currentTarget.addEventListener('touchmove', onMouseMove)
214
+ mouseMoveEventRef.value = onMouseMove
215
+ mouseUpEventRef.value = onMouseUp
216
+ touchEventTargetRef.value = e.currentTarget
217
+ }
218
+
219
+ // Only return cache value when it mapping with rawValues
220
+ const returnValues = computed(() => {
221
+ const sourceValues = [...rawValues.value].sort((a, b) => a - b)
222
+ const targetValues = [...cacheValues.value].sort((a, b) => a - b)
223
+
224
+ const counts: Record<number, number> = {}
225
+ targetValues.forEach((val) => {
226
+ counts[val] = (counts[val] || 0) + 1
227
+ })
228
+ sourceValues.forEach((val) => {
229
+ counts[val] = (counts[val] || 0) - 1
230
+ })
231
+
232
+ const maxDiffCount = editable ? 1 : 0
233
+ const diffCount: number = Object.values(counts).reduce(
234
+ (prev, next) => prev + Math.abs(next),
235
+ 0,
236
+ )
237
+
238
+ return diffCount <= maxDiffCount ? cacheValues.value : rawValues.value
239
+ })
240
+
241
+ return [draggingIndex, draggingValue, draggingDelete, returnValues, onStartMove]
242
+ }
243
+
244
+ export default useDrag