@v-c/slider 1.0.1 → 1.0.3

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.
package/src/Slider.tsx CHANGED
@@ -1,592 +1,602 @@
1
- import type { ComputedRef, CSSProperties, ExtractPropTypes, PropType, Ref } from 'vue'
2
- import type { HandlesRef } from './Handles'
3
- import type {
4
- AriaValueFormat,
5
- Direction,
6
- OnStartMove,
7
- SliderClassNames,
8
- SliderStyles,
9
- } from './interface'
10
- import type { InternalMarkObj, MarkObj } from './Marks'
11
- import isEqual from '@v-c/util/dist/isEqual'
12
- import warning from '@v-c/util/dist/warning'
13
- import cls from 'classnames'
14
- import { computed, defineComponent, isVNode, ref, shallowRef, watch, watchEffect } from 'vue'
15
- import { useProviderSliderContext } from './context'
16
- import Handles from './Handles'
17
- import useDrag from './hooks/useDrag'
18
- import useOffset from './hooks/useOffset'
19
- import useRange from './hooks/useRange'
20
- import Marks from './Marks'
21
- import Steps from './Steps'
22
- import Tracks from './Tracks'
23
-
24
- export interface RangeConfig {
25
- editable?: boolean
26
- draggableTrack?: boolean
27
- /** Set min count when `editable` */
28
- minCount?: number
29
- /** Set max count when `editable` */
30
- maxCount?: number
31
- }
32
-
33
- type ValueType = number | number[]
34
-
35
- function sliderProps() {
36
- return {
37
- prefixCls: { type: String, default: 'vc-slider' },
38
- className: String,
39
- classNames: Object as PropType<SliderClassNames>,
40
- styles: Object as PropType<SliderStyles>,
41
- id: String,
42
- disabled: { type: Boolean, default: false },
43
- keyboard: { type: Boolean, default: true },
44
- autoFocus: Boolean,
45
- min: { type: Number, default: 0 },
46
- max: { type: Number, default: 100 },
47
- step: { type: Number, default: 1 },
48
- value: [Number, Array] as PropType<ValueType>,
49
- defaultValue: [Number, Array] as PropType<ValueType>,
50
- range: [Boolean, Object] as PropType<boolean | RangeConfig>,
51
- count: Number,
52
- allowCross: { type: Boolean, default: true },
53
- pushable: { type: [Boolean, Number], default: false },
54
- reverse: Boolean,
55
- vertical: Boolean,
56
- included: { type: Boolean, default: true },
57
- startPoint: Number,
58
- trackStyle: [Object, Array] as PropType<Record<string, any> | Record<string, any>[]>,
59
- handleStyle: [Object, Array] as PropType<Record<string, any> | Record<string, any>[]>,
60
- railStyle: Object as PropType<Record<string, any>>,
61
- dotStyle: [Object, Function] as PropType<Record<string, any> | ((dotValue: number) => Record<string, any>)>,
62
- activeDotStyle: [Object, Function] as PropType<Record<string, any> | ((dotValue: number) => Record<string, any>)>,
63
- marks: Object as PropType<Record<string | number, any | MarkObj>>,
64
- dots: Boolean,
65
- handleRender: Function,
66
- activeHandleRender: Function,
67
- track: { type: Boolean, default: true },
68
- tabIndex: { type: [Number, Array] as PropType<ValueType>, default: 0 },
69
- ariaLabelForHandle: [String, Array] as PropType<string | string[]>,
70
- ariaLabelledByForHandle: [String, Array] as PropType<string | string[]>,
71
- ariaRequired: Boolean,
72
- ariaValueTextFormatterForHandle: [Function, Array] as PropType<AriaValueFormat | AriaValueFormat[]>,
73
- onFocus: Function as PropType<(e: FocusEvent) => void>,
74
- onBlur: Function as PropType<(e: FocusEvent) => void>,
75
- onChange: Function as PropType<(value: ValueType) => void>,
76
- /** @deprecated It's always better to use `onChange` instead */
77
- onBeforeChange: Function as PropType<(value: ValueType) => void>,
78
- /** @deprecated Use `onChangeComplete` instead */
79
- onAfterChange: Function as PropType<(value: ValueType) => void>,
80
- onChangeComplete: Function as PropType<(value: ValueType) => void>,
81
- }
82
- }
83
- export type SliderProps = Partial<ExtractPropTypes<ReturnType<typeof sliderProps>>>
84
-
85
- export interface SliderRef {
86
- focus: () => void
87
- blur: () => void
88
- }
89
-
90
- export default defineComponent({
91
- name: 'Slider',
92
- props: {
93
- ...sliderProps(),
94
- },
95
- emits: ['focus', 'blur', 'change', 'beforeChange', 'afterChange', 'changeComplete'],
96
- setup(props, { attrs, emit, expose }) {
97
- const handlesRef = ref<HandlesRef>()
98
- const containerRef = ref<HTMLDivElement>()
99
-
100
- const direction = shallowRef<Direction>('ltr')
101
- watch([() => props.reverse, () => props.vertical], ([newReverse, newVertical]) => {
102
- if (newVertical) {
103
- direction.value = newReverse ? 'ttb' : 'btt'
104
- }
105
- else {
106
- direction.value = newReverse ? 'rtl' : 'ltr'
107
- }
108
- }, { immediate: true })
109
-
110
- const mergedMin = shallowRef(0)
111
- const mergedMax = shallowRef(100)
112
- const mergedStep = shallowRef(1)
113
- const markList = ref<InternalMarkObj[]>([])
114
-
115
- const mergedValue = ref<ValueType>(props.defaultValue! || props.value!)
116
- const rawValues = ref<number[] | ComputedRef<number[]>>([])
117
- const getRange = ref()
118
- const getOffset = ref()
119
-
120
- watchEffect(() => {
121
- const {
122
- range,
123
- min,
124
- max,
125
- step,
126
- pushable,
127
- marks,
128
- allowCross,
129
- value,
130
- count,
131
- } = props
132
- // ============================ Range =============================
133
- const [rangeEnabled, rangeEditable, rangeDraggableTrack, minCount, maxCount] = useRange(range)
134
- getRange.value = {
135
- rangeEnabled,
136
- rangeEditable,
137
- rangeDraggableTrack,
138
- minCount,
139
- maxCount,
140
- }
141
-
142
- mergedMin.value = isFinite(min) ? min : 0
143
- mergedMax.value = isFinite(max) ? max : 100
144
-
145
- // ============================= Step =============================
146
- mergedStep.value = step !== null && step <= 0 ? 1 : step
147
-
148
- // ============================= Push =============================
149
- const mergedPush = computed(() => {
150
- if (typeof pushable === 'boolean') {
151
- return pushable ? mergedStep.value : false
152
- }
153
- return pushable >= 0 ? pushable : false
154
- })
155
-
156
- // ============================ Marks =============================
157
- markList.value = Object.keys(marks || {})
158
- .map<InternalMarkObj>((key) => {
159
- const mark = marks?.[key]
160
- const markObj: InternalMarkObj = {
161
- value: Number(key),
162
- }
163
-
164
- if (
165
- mark
166
- && typeof mark === 'object'
167
- && !isVNode(mark)
168
- && ('label' in mark || 'style' in mark)
169
- ) {
170
- markObj.style = mark.style
171
- markObj.label = mark.label
172
- }
173
- else {
174
- markObj.label = mark
175
- }
176
-
177
- return markObj
178
- })
179
- .filter(({ label }) => label || typeof label === 'number')
180
- .sort((a, b) => a.value - b.value)
181
-
182
- // ============================ Format ============================
183
- const [formatValue, offsetValues] = useOffset(
184
- mergedMin.value,
185
- mergedMax.value,
186
- mergedStep.value,
187
- markList.value,
188
- allowCross,
189
- mergedPush.value,
190
- )
191
- getOffset.value = {
192
- formatValue,
193
- offsetValues,
194
- }
195
-
196
- // ============================ Values ============================
197
- if (value !== undefined) {
198
- mergedValue.value = value
199
- }
200
-
201
- const getRawValues = computed(() => {
202
- const valueList
203
- = mergedValue.value === null || mergedValue.value === undefined
204
- ? []
205
- : Array.isArray(mergedValue.value)
206
- ? mergedValue.value
207
- : [mergedValue.value]
208
-
209
- const [val0 = mergedMin.value] = valueList
210
- let returnValues = mergedValue.value === null ? [] : [val0]
211
-
212
- // Format as range
213
- if (rangeEnabled) {
214
- returnValues = [...valueList]
215
-
216
- // When count provided or value is `undefined`, we fill values
217
- if (count || mergedValue.value === undefined) {
218
- const pointCount = count! >= 0 ? count! + 1 : 2
219
- returnValues = returnValues.slice(0, pointCount)
220
-
221
- // Fill with count
222
- while (returnValues.length < pointCount) {
223
- returnValues.push(returnValues[returnValues.length - 1] ?? mergedMin.value)
224
- }
225
- }
226
- returnValues.sort((a, b) => a - b)
227
- }
228
-
229
- // Align in range
230
- returnValues.forEach((val, index) => {
231
- returnValues[index] = formatValue(val)
232
- })
233
-
234
- return returnValues
235
- })
236
-
237
- rawValues.value = getRawValues.value
238
- })
239
-
240
- // =========================== onChange ===========================
241
- const getTriggerValue = (triggerValues: number[]) => {
242
- return getRange.value.rangeEnabled ? triggerValues : triggerValues[0]
243
- }
244
-
245
- const triggerChange = (nextValues: number[]) => {
246
- // Order first
247
- const cloneNextValues = [...nextValues].sort((a, b) => a - b)
248
-
249
- // Trigger event if needed
250
- if (!isEqual(cloneNextValues, rawValues.value, true)) {
251
- emit('change', getTriggerValue(cloneNextValues))
252
- }
253
-
254
- // We set this later since it will re-render component immediately
255
- mergedValue.value = cloneNextValues
256
- }
257
-
258
- const finishChange = (draggingDelete?: boolean) => {
259
- // Trigger from `useDrag` will tell if it's a delete action
260
- if (draggingDelete) {
261
- handlesRef.value?.hideHelp()
262
- }
263
-
264
- const finishValue = getTriggerValue(rawValues.value)
265
- if (props.onAfterChange) {
266
- emit('afterChange', finishValue)
267
- warning(
268
- false,
269
- '[vc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
270
- )
271
- }
272
- emit('changeComplete', finishValue)
273
- }
274
-
275
- const onDelete = (index: number) => {
276
- if (props.disabled || !getRange.value.rangeEditable || rawValues.value.length <= getRange.value.minCount) {
277
- return
278
- }
279
-
280
- const cloneNextValues = [...rawValues.value]
281
- cloneNextValues.splice(index, 1)
282
-
283
- emit('beforeChange', getTriggerValue(cloneNextValues))
284
- triggerChange(cloneNextValues)
285
-
286
- const nextFocusIndex = Math.max(0, index - 1)
287
- handlesRef.value?.hideHelp()
288
- handlesRef.value?.focus(nextFocusIndex)
289
- }
290
- const [draggingIndex, draggingValue, draggingDelete, cacheValues, onStartDrag] = useDrag(
291
- containerRef as Ref<HTMLDivElement>,
292
- direction,
293
- rawValues,
294
- mergedMin,
295
- mergedMax,
296
- getOffset.value.formatValue,
297
- triggerChange,
298
- finishChange,
299
- getOffset.value.offsetValues,
300
- getRange.value.rangeEditable,
301
- getRange.value.minCount,
302
- )
303
-
304
- /**
305
- * When `rangeEditable` will insert a new value in the values array.
306
- * Else it will replace the value in the values array.
307
- */
308
- const changeToCloseValue = (newValue: number, e?: MouseEvent) => {
309
- if (!props.disabled) {
310
- // Create new values
311
- const cloneNextValues = [...rawValues.value]
312
-
313
- let valueIndex = 0
314
- let valueBeforeIndex = 0 // Record the index which value < newValue
315
- let valueDist = mergedMax.value - mergedMin.value
316
-
317
- rawValues.value.forEach((val, index) => {
318
- const dist = Math.abs(newValue - val)
319
- if (dist <= valueDist) {
320
- valueDist = dist
321
- valueIndex = index
322
- }
323
-
324
- if (val < newValue) {
325
- valueBeforeIndex = index
326
- }
327
- })
328
-
329
- let focusIndex = valueIndex
330
-
331
- if (getRange.value.rangeEditable && valueDist !== 0 && (!getRange.value.maxCount || rawValues.value.length < getRange.value.maxCount)) {
332
- cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue)
333
- focusIndex = valueBeforeIndex + 1
334
- }
335
- else {
336
- cloneNextValues[valueIndex] = newValue
337
- }
338
- // Fill value to match default 2 (only when `rawValues` is empty)
339
- if (getRange.value.rangeEnabled && !rawValues.value.length && props.count === undefined) {
340
- cloneNextValues.push(newValue)
341
- }
342
-
343
- const nextValue = getTriggerValue(cloneNextValues)
344
- emit('beforeChange', nextValue)
345
- triggerChange(cloneNextValues)
346
-
347
- if (e) {
348
- (document.activeElement as HTMLElement)?.blur?.()
349
- handlesRef.value?.focus(focusIndex)
350
- onStartDrag(e, focusIndex, cloneNextValues)
351
- }
352
- else {
353
- if (props.onAfterChange) {
354
- // https://github.com/ant-design/ant-design/issues/49997
355
- emit('afterChange', nextValue)
356
- warning(
357
- false,
358
- '[vc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
359
- )
360
- }
361
- emit('changeComplete', nextValue)
362
- }
363
- }
364
- }
365
-
366
- // ============================ Click =============================
367
- const onSliderMouseDown = (e: MouseEvent) => {
368
- e.preventDefault()
369
- const { width, height, left, top, bottom, right }
370
- = containerRef.value!.getBoundingClientRect()
371
- const { clientX, clientY } = e
372
-
373
- let percent: number
374
- switch (direction.value) {
375
- case 'btt':
376
- percent = (bottom - clientY) / height
377
- break
378
-
379
- case 'ttb':
380
- percent = (clientY - top) / height
381
- break
382
-
383
- case 'rtl':
384
- percent = (right - clientX) / width
385
- break
386
-
387
- default:
388
- percent = (clientX - left) / width
389
- }
390
-
391
- const nextValue = mergedMin.value + percent * (mergedMax.value - mergedMin.value)
392
- console.log('click', nextValue, getOffset.value.formatValue(nextValue))
393
- changeToCloseValue(getOffset.value.formatValue(nextValue), e)
394
- }
395
-
396
- // =========================== Keyboard ===========================
397
- const keyboardValue = ref<number | null>(null)
398
-
399
- const onHandleOffsetChange = (offset: number | 'min' | 'max', valueIndex: number) => {
400
- if (!props.disabled) {
401
- const next = getOffset.value.offsetValues(rawValues.value, offset, valueIndex)
402
-
403
- emit('beforeChange', getTriggerValue(rawValues.value))
404
- triggerChange(next.values)
405
-
406
- keyboardValue.value = next.value
407
- }
408
- }
409
-
410
- watchEffect(() => {
411
- if (keyboardValue.value !== null) {
412
- const valueIndex = rawValues.value.indexOf(keyboardValue.value)
413
- if (valueIndex >= 0) {
414
- handlesRef.value?.focus(valueIndex)
415
- }
416
- }
417
-
418
- keyboardValue.value = null
419
- })
420
-
421
- // ============================= Drag =============================
422
- const mergedDraggableTrack = computed(() => {
423
- if (getRange.value.rangeDraggableTrack && mergedStep.value === null) {
424
- if (process.env.NODE_ENV !== 'production') {
425
- warning(false, '`draggableTrack` is not supported when `step` is `null`.')
426
- }
427
- return false
428
- }
429
- return getRange.value.rangeDraggableTrack
430
- })
431
-
432
- const onStartMove: OnStartMove = (e, valueIndex) => {
433
- console.log('onStartMove-valueIndex', valueIndex)
434
- onStartDrag(e, valueIndex)
435
-
436
- emit('beforeChange', getTriggerValue(rawValues.value))
437
- }
438
-
439
- // Auto focus for updated handle
440
- const dragging = computed(() => draggingIndex.value !== -1)
441
- watchEffect(() => {
442
- if (!dragging.value) {
443
- const valueIndex = rawValues.value.lastIndexOf(draggingValue.value)
444
- handlesRef.value?.focus(valueIndex)
445
- }
446
- })
447
-
448
- // =========================== Included ===========================
449
- const sortedCacheValues = computed(
450
- () => [...cacheValues.value].sort((a, b) => a - b),
451
- )
452
-
453
- // Provide a range values with included [min, max]
454
- // Used for Track, Mark & Dot
455
- const [includedStart, includedEnd] = computed(() => {
456
- if (!getRange.value.rangeEnabled) {
457
- return [mergedMin.value, sortedCacheValues.value[0]]
458
- }
459
-
460
- return [sortedCacheValues.value[0], sortedCacheValues.value[sortedCacheValues.value.length - 1]]
461
- }).value
462
-
463
- // ============================= Refs =============================
464
- expose({
465
- focus: () => {
466
- handlesRef.value?.focus(0)
467
- },
468
- blur: () => {
469
- const { activeElement } = document
470
- if (containerRef.value?.contains(activeElement)) {
471
- (activeElement as HTMLElement)?.blur()
472
- }
473
- },
474
- })
475
-
476
- // ========================== Auto Focus ==========================
477
- watchEffect(() => {
478
- if (props.autoFocus) {
479
- handlesRef.value?.focus(0)
480
- }
481
- })
482
- // =========================== Context ============================
483
- const context = computed(() => ({
484
- min: mergedMin,
485
- max: mergedMax,
486
- direction,
487
- disabled: props.disabled,
488
- keyboard: props.keyboard,
489
- step: mergedStep,
490
- included: props.included,
491
- includedStart,
492
- includedEnd,
493
- range: getRange.value.rangeEnabled,
494
- tabIndex: props.tabIndex,
495
- ariaLabelForHandle: props.ariaLabelForHandle,
496
- ariaLabelledByForHandle: props.ariaLabelledByForHandle,
497
- ariaRequired: props.ariaRequired,
498
- ariaValueTextFormatterForHandle: props.ariaValueTextFormatterForHandle,
499
- styles: props.styles || {},
500
- classNames: props.classNames || {},
501
- }))
502
- useProviderSliderContext(context.value)
503
-
504
- // ============================ Render ============================
505
- return () => {
506
- const {
507
- prefixCls = 'vc-slider',
508
- id,
509
-
510
- // Status
511
- disabled = false,
512
- vertical,
513
-
514
- // Style
515
- startPoint,
516
- trackStyle,
517
- handleStyle,
518
- railStyle,
519
- dotStyle,
520
- activeDotStyle,
521
-
522
- // Decorations
523
- dots,
524
- handleRender,
525
- activeHandleRender,
526
-
527
- // Components
528
- track,
529
- classNames,
530
- styles,
531
- } = props
532
- return (
533
- <div
534
- ref={containerRef}
535
- class={cls(prefixCls, [attrs.class], {
536
- [`${prefixCls}-disabled`]: disabled,
537
- [`${prefixCls}-vertical`]: vertical,
538
- [`${prefixCls}-horizontal`]: !vertical,
539
- [`${prefixCls}-with-marks`]: markList.value.length,
540
- })}
541
- style={attrs.style as CSSProperties}
542
- onMousedown={onSliderMouseDown}
543
- id={id}
544
- >
545
- <div
546
- class={cls(`${prefixCls}-rail`, classNames?.rail)}
547
- style={{ ...railStyle, ...styles?.rail }}
548
- />
549
-
550
- {track && (
551
- <Tracks
552
- prefixCls={prefixCls}
553
- // 将style换成trackStyle,因为vue通过attrs取style,数组会合并,相同的样式名如backgroundColor后一个会覆盖前面的
554
- trackStyle={trackStyle}
555
- values={rawValues.value}
556
- startPoint={startPoint}
557
- onStartMove={mergedDraggableTrack.value ? onStartMove : undefined}
558
- />
559
- )}
560
-
561
- <Steps
562
- prefixCls={prefixCls}
563
- marks={markList.value}
564
- dots={dots}
565
- style={dotStyle}
566
- activeStyle={activeDotStyle}
567
- />
568
-
569
- <Handles
570
- ref={handlesRef}
571
- prefixCls={prefixCls}
572
- // 原因如⬆️trackStyle
573
- handleStyle={handleStyle}
574
- values={cacheValues.value}
575
- draggingIndex={draggingIndex.value}
576
- draggingDelete={draggingDelete.value}
577
- onStartMove={onStartMove}
578
- onOffsetChange={onHandleOffsetChange}
579
- onFocus={(e: FocusEvent) => emit('focus', e)}
580
- onBlur={(e: FocusEvent) => emit('blur', e)}
581
- handleRender={handleRender}
582
- activeHandleRender={activeHandleRender}
583
- onChangeComplete={finishChange}
584
- onDelete={getRange.value.rangeEditable ? onDelete : () => {}}
585
- />
586
-
587
- <Marks prefixCls={prefixCls} marks={markList.value} onClick={changeToCloseValue} />
588
- </div>
589
- )
590
- }
591
- },
592
- })
1
+ import type { ComputedRef, CSSProperties, ExtractPropTypes, PropType, Ref, SlotsType } from 'vue'
2
+ import type { HandlesRef } from './Handles'
3
+ import type {
4
+ AriaValueFormat,
5
+ Direction,
6
+ OnStartMove,
7
+ SliderClassNames,
8
+ SliderStyles,
9
+ } from './interface'
10
+ import type { InternalMarkObj, MarkObj } from './Marks'
11
+ import isEqual from '@v-c/util/dist/isEqual'
12
+ import warning from '@v-c/util/dist/warning'
13
+ import cls from 'classnames'
14
+ import { computed, defineComponent, isVNode, ref, shallowRef, watch, watchEffect } from 'vue'
15
+ import { useProviderSliderContext } from './context'
16
+ import Handles from './Handles'
17
+ import useDrag from './hooks/useDrag'
18
+ import useOffset from './hooks/useOffset'
19
+ import useRange from './hooks/useRange'
20
+ import Marks from './Marks'
21
+ import Steps from './Steps'
22
+ import Tracks from './Tracks'
23
+
24
+ export interface RangeConfig {
25
+ editable?: boolean
26
+ draggableTrack?: boolean
27
+ /** Set min count when `editable` */
28
+ minCount?: number
29
+ /** Set max count when `editable` */
30
+ maxCount?: number
31
+ }
32
+
33
+ export interface RenderProps {
34
+ index: number
35
+ prefixCls: string
36
+ value: number
37
+ dragging: boolean
38
+ draggingDelete: boolean
39
+ node: any
40
+ }
41
+
42
+ type ValueType = number | number[]
43
+
44
+ function sliderProps() {
45
+ return {
46
+ prefixCls: { type: String, default: 'vc-slider' },
47
+ className: String,
48
+ classNames: Object as PropType<SliderClassNames>,
49
+ styles: Object as PropType<SliderStyles>,
50
+ id: String,
51
+ disabled: { type: Boolean, default: false },
52
+ keyboard: { type: Boolean, default: true },
53
+ autoFocus: Boolean,
54
+ min: { type: Number, default: 0 },
55
+ max: { type: Number, default: 100 },
56
+ step: { type: [Number, null], default: 1 },
57
+ value: [Number, Array, null] as PropType<ValueType | null>,
58
+ defaultValue: [Number, Array] as PropType<ValueType>,
59
+ range: [Boolean, Object] as PropType<boolean | RangeConfig>,
60
+ count: Number,
61
+ allowCross: { type: Boolean, default: true },
62
+ pushable: { type: [Boolean, Number], default: false },
63
+ reverse: Boolean,
64
+ vertical: Boolean,
65
+ included: { type: Boolean, default: true },
66
+ startPoint: Number,
67
+ trackStyle: [Object, Array] as PropType<Record<string, any> | Record<string, any>[]>,
68
+ handleStyle: [Object, Array] as PropType<Record<string, any> | Record<string, any>[]>,
69
+ railStyle: Object as PropType<Record<string, any>>,
70
+ dotStyle: [Object, Function] as PropType<Record<string, any> | ((dotValue: number) => Record<string, any>)>,
71
+ activeDotStyle: [Object, Function] as PropType<Record<string, any> | ((dotValue: number) => Record<string, any>)>,
72
+ marks: Object as PropType<Record<string | number, any | MarkObj>>,
73
+ dots: Boolean,
74
+ handleRender: Function as PropType<(props: RenderProps) => any>,
75
+ activeHandleRender: Function as PropType<(props: RenderProps) => any>,
76
+ track: { type: Boolean, default: true },
77
+ tabIndex: { type: [Number, Array] as PropType<ValueType>, default: 0 },
78
+ ariaLabelForHandle: [String, Array] as PropType<string | string[]>,
79
+ ariaLabelledByForHandle: [String, Array] as PropType<string | string[]>,
80
+ ariaRequired: Boolean,
81
+ ariaValueTextFormatterForHandle: [Function, Array] as PropType<AriaValueFormat | AriaValueFormat[]>,
82
+ onFocus: Function as PropType<(e: FocusEvent) => void>,
83
+ onBlur: Function as PropType<(e: FocusEvent) => void>,
84
+ onChange: Function as PropType<(value: ValueType) => void>,
85
+ /** @deprecated It's always better to use `onChange` instead */
86
+ onBeforeChange: Function as PropType<(value: ValueType) => void>,
87
+ /** @deprecated Use `onChangeComplete` instead */
88
+ onAfterChange: Function as PropType<(value: ValueType) => void>,
89
+ onChangeComplete: Function as PropType<(value: ValueType) => void>,
90
+ }
91
+ }
92
+ export type SliderProps = Partial<ExtractPropTypes<ReturnType<typeof sliderProps>>>
93
+
94
+ export interface SliderRef {
95
+ focus: () => void
96
+ blur: () => void
97
+ }
98
+
99
+ export default defineComponent({
100
+ name: 'Slider',
101
+ props: {
102
+ ...sliderProps(),
103
+ },
104
+ emits: ['focus', 'blur', 'change', 'beforeChange', 'afterChange', 'changeComplete'],
105
+ slots: Object as SlotsType<{
106
+ mark: ({ point, label }: { point: number, label: unknown }) => any
107
+ }>,
108
+ setup(props, { attrs, emit, expose, slots }) {
109
+ const handlesRef = ref<HandlesRef>()
110
+ const containerRef = ref<HTMLDivElement>()
111
+
112
+ const direction = shallowRef<Direction>('ltr')
113
+ watch([() => props.reverse, () => props.vertical], ([newReverse, newVertical]) => {
114
+ if (newVertical) {
115
+ direction.value = newReverse ? 'ttb' : 'btt'
116
+ }
117
+ else {
118
+ direction.value = newReverse ? 'rtl' : 'ltr'
119
+ }
120
+ }, { immediate: true })
121
+
122
+ const mergedMin = shallowRef(0)
123
+ const mergedMax = shallowRef(100)
124
+ const mergedStep = shallowRef<number | null>(1)
125
+ const markList = ref<InternalMarkObj[]>([])
126
+
127
+ const mergedValue = ref<ValueType | null>(props.defaultValue! || props.value!)
128
+ const rawValues = ref<number[] | ComputedRef<number[]>>([])
129
+ const getRange = ref()
130
+ const getOffset = ref()
131
+
132
+ watchEffect(() => {
133
+ const {
134
+ range,
135
+ min,
136
+ max,
137
+ step,
138
+ pushable,
139
+ marks,
140
+ allowCross,
141
+ value,
142
+ count,
143
+ } = props
144
+ // ============================ Range =============================
145
+ const [rangeEnabled, rangeEditable, rangeDraggableTrack, minCount, maxCount] = useRange(range)
146
+ getRange.value = {
147
+ rangeEnabled,
148
+ rangeEditable,
149
+ rangeDraggableTrack,
150
+ minCount,
151
+ maxCount,
152
+ }
153
+
154
+ mergedMin.value = isFinite(min) ? min : 0
155
+ mergedMax.value = isFinite(max) ? max : 100
156
+
157
+ // ============================= Step =============================
158
+ mergedStep.value = step !== null && step <= 0 ? 1 : step
159
+
160
+ // ============================= Push =============================
161
+ const mergedPush = computed(() => {
162
+ if (typeof pushable === 'boolean') {
163
+ return pushable ? mergedStep.value : false
164
+ }
165
+ return pushable >= 0 ? pushable : false
166
+ })
167
+
168
+ // ============================ Marks =============================
169
+ markList.value = Object.keys(marks || {})
170
+ .map<InternalMarkObj>((key) => {
171
+ const mark = marks?.[key]
172
+ const markObj: InternalMarkObj = {
173
+ value: Number(key),
174
+ }
175
+
176
+ if (
177
+ mark
178
+ && typeof mark === 'object'
179
+ && !isVNode(mark)
180
+ && ('label' in mark || 'style' in mark)
181
+ ) {
182
+ markObj.style = mark.style
183
+ markObj.label = mark.label
184
+ }
185
+ else {
186
+ markObj.label = mark
187
+ }
188
+
189
+ return markObj
190
+ })
191
+ .filter(({ label }) => label || typeof label === 'number')
192
+ .sort((a, b) => a.value - b.value)
193
+
194
+ // ============================ Format ============================
195
+ const [formatValue, offsetValues] = useOffset(
196
+ mergedMin.value,
197
+ mergedMax.value,
198
+ mergedStep.value,
199
+ markList.value,
200
+ allowCross,
201
+ mergedPush.value,
202
+ )
203
+ getOffset.value = {
204
+ formatValue,
205
+ offsetValues,
206
+ }
207
+
208
+ // ============================ Values ============================
209
+ if (value !== undefined) {
210
+ mergedValue.value = value
211
+ }
212
+
213
+ const getRawValues = computed(() => {
214
+ const valueList
215
+ = mergedValue.value === null || mergedValue.value === undefined
216
+ ? []
217
+ : Array.isArray(mergedValue.value)
218
+ ? mergedValue.value
219
+ : [mergedValue.value]
220
+
221
+ const [val0 = mergedMin.value] = valueList
222
+ let returnValues: number[] = mergedValue.value === null ? [] : [val0]
223
+
224
+ // Format as range
225
+ if (rangeEnabled) {
226
+ returnValues = [...valueList]
227
+
228
+ // When count provided or value is `undefined`, we fill values
229
+ if (count || mergedValue.value === undefined) {
230
+ const pointCount = count! >= 0 ? count! + 1 : 2
231
+ returnValues = returnValues.slice(0, pointCount)
232
+
233
+ // Fill with count
234
+ while (returnValues.length < pointCount) {
235
+ returnValues.push(returnValues[returnValues.length - 1] ?? mergedMin.value)
236
+ }
237
+ }
238
+ returnValues.sort((a, b) => a - b)
239
+ }
240
+
241
+ // Align in range
242
+ returnValues.forEach((val, index) => {
243
+ returnValues[index] = formatValue(val)
244
+ })
245
+
246
+ return returnValues
247
+ })
248
+
249
+ rawValues.value = getRawValues.value
250
+ })
251
+
252
+ // =========================== onChange ===========================
253
+ const getTriggerValue = (triggerValues: number[]) => {
254
+ return getRange.value.rangeEnabled ? triggerValues : triggerValues[0]
255
+ }
256
+
257
+ const triggerChange = (nextValues: number[]) => {
258
+ // Order first
259
+ const cloneNextValues = [...nextValues].sort((a, b) => a - b)
260
+
261
+ // Trigger event if needed
262
+ if (!isEqual(cloneNextValues, rawValues.value, true)) {
263
+ emit('change', getTriggerValue(cloneNextValues))
264
+ }
265
+
266
+ // We set this later since it will re-render component immediately
267
+ mergedValue.value = cloneNextValues
268
+ }
269
+
270
+ const finishChange = (draggingDelete?: boolean) => {
271
+ // Trigger from `useDrag` will tell if it's a delete action
272
+ if (draggingDelete) {
273
+ handlesRef.value?.hideHelp()
274
+ }
275
+
276
+ const finishValue = getTriggerValue(rawValues.value)
277
+ if (props.onAfterChange) {
278
+ emit('afterChange', finishValue)
279
+ warning(
280
+ false,
281
+ '[vc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
282
+ )
283
+ }
284
+ emit('changeComplete', finishValue)
285
+ }
286
+
287
+ const onDelete = (index: number) => {
288
+ if (props.disabled || !getRange.value.rangeEditable || rawValues.value.length <= getRange.value.minCount) {
289
+ return
290
+ }
291
+
292
+ const cloneNextValues = [...rawValues.value]
293
+ cloneNextValues.splice(index, 1)
294
+
295
+ emit('beforeChange', getTriggerValue(cloneNextValues))
296
+ triggerChange(cloneNextValues)
297
+
298
+ const nextFocusIndex = Math.max(0, index - 1)
299
+ handlesRef.value?.hideHelp()
300
+ handlesRef.value?.focus(nextFocusIndex)
301
+ }
302
+ const [draggingIndex, draggingValue, draggingDelete, cacheValues, onStartDrag] = useDrag(
303
+ containerRef as Ref<HTMLDivElement>,
304
+ direction,
305
+ rawValues,
306
+ mergedMin,
307
+ mergedMax,
308
+ getOffset.value.formatValue,
309
+ triggerChange,
310
+ finishChange,
311
+ getOffset.value.offsetValues,
312
+ getRange.value.rangeEditable,
313
+ getRange.value.minCount,
314
+ )
315
+
316
+ /**
317
+ * When `rangeEditable` will insert a new value in the values array.
318
+ * Else it will replace the value in the values array.
319
+ */
320
+ const changeToCloseValue = (newValue: number, e?: MouseEvent) => {
321
+ if (!props.disabled) {
322
+ // Create new values
323
+ const cloneNextValues = [...rawValues.value]
324
+
325
+ let valueIndex = 0
326
+ let valueBeforeIndex = 0 // Record the index which value < newValue
327
+ let valueDist = mergedMax.value - mergedMin.value
328
+
329
+ rawValues.value.forEach((val, index) => {
330
+ const dist = Math.abs(newValue - val)
331
+ if (dist <= valueDist) {
332
+ valueDist = dist
333
+ valueIndex = index
334
+ }
335
+
336
+ if (val < newValue) {
337
+ valueBeforeIndex = index
338
+ }
339
+ })
340
+
341
+ let focusIndex = valueIndex
342
+
343
+ if (getRange.value.rangeEditable && valueDist !== 0 && (!getRange.value.maxCount || rawValues.value.length < getRange.value.maxCount)) {
344
+ cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue)
345
+ focusIndex = valueBeforeIndex + 1
346
+ }
347
+ else {
348
+ cloneNextValues[valueIndex] = newValue
349
+ }
350
+ // Fill value to match default 2 (only when `rawValues` is empty)
351
+ if (getRange.value.rangeEnabled && !rawValues.value.length && props.count === undefined) {
352
+ cloneNextValues.push(newValue)
353
+ }
354
+
355
+ const nextValue = getTriggerValue(cloneNextValues)
356
+ emit('beforeChange', nextValue)
357
+ triggerChange(cloneNextValues)
358
+
359
+ if (e) {
360
+ (document.activeElement as HTMLElement)?.blur?.()
361
+ handlesRef.value?.focus(focusIndex)
362
+ onStartDrag(e, focusIndex, cloneNextValues)
363
+ }
364
+ else {
365
+ if (props.onAfterChange) {
366
+ // https://github.com/ant-design/ant-design/issues/49997
367
+ emit('afterChange', nextValue)
368
+ warning(
369
+ false,
370
+ '[vc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
371
+ )
372
+ }
373
+ emit('changeComplete', nextValue)
374
+ }
375
+ }
376
+ }
377
+
378
+ // ============================ Click =============================
379
+ const onSliderMouseDown = (e: MouseEvent) => {
380
+ e.preventDefault()
381
+ const { width, height, left, top, bottom, right }
382
+ = containerRef.value!.getBoundingClientRect()
383
+ const { clientX, clientY } = e
384
+
385
+ let percent: number
386
+ switch (direction.value) {
387
+ case 'btt':
388
+ percent = (bottom - clientY) / height
389
+ break
390
+
391
+ case 'ttb':
392
+ percent = (clientY - top) / height
393
+ break
394
+
395
+ case 'rtl':
396
+ percent = (right - clientX) / width
397
+ break
398
+
399
+ default:
400
+ percent = (clientX - left) / width
401
+ }
402
+
403
+ const nextValue = mergedMin.value + percent * (mergedMax.value - mergedMin.value)
404
+ changeToCloseValue(getOffset.value.formatValue(nextValue), e)
405
+ }
406
+
407
+ // =========================== Keyboard ===========================
408
+ const keyboardValue = ref<number | null>(null)
409
+
410
+ const onHandleOffsetChange = (offset: number | 'min' | 'max', valueIndex: number) => {
411
+ if (!props.disabled) {
412
+ const next = getOffset.value.offsetValues(rawValues.value, offset, valueIndex)
413
+
414
+ emit('beforeChange', getTriggerValue(rawValues.value))
415
+ triggerChange(next.values)
416
+
417
+ keyboardValue.value = next.value
418
+ }
419
+ }
420
+
421
+ watchEffect(() => {
422
+ if (keyboardValue.value !== null) {
423
+ const valueIndex = rawValues.value.indexOf(keyboardValue.value)
424
+ if (valueIndex >= 0) {
425
+ handlesRef.value?.focus(valueIndex)
426
+ }
427
+ }
428
+
429
+ keyboardValue.value = null
430
+ })
431
+
432
+ // ============================= Drag =============================
433
+ const mergedDraggableTrack = computed(() => {
434
+ if (getRange.value.rangeDraggableTrack && mergedStep.value === null) {
435
+ if (process.env.NODE_ENV !== 'production') {
436
+ warning(false, '`draggableTrack` is not supported when `step` is `null`.')
437
+ }
438
+ return false
439
+ }
440
+ return getRange.value.rangeDraggableTrack
441
+ })
442
+
443
+ const onStartMove: OnStartMove = (e, valueIndex) => {
444
+ onStartDrag(e, valueIndex)
445
+
446
+ emit('beforeChange', getTriggerValue(rawValues.value))
447
+ }
448
+
449
+ // Auto focus for updated handle
450
+ const dragging = computed(() => draggingIndex.value !== -1)
451
+ watchEffect(() => {
452
+ if (!dragging.value) {
453
+ const valueIndex = rawValues.value.lastIndexOf(draggingValue.value)
454
+ handlesRef.value?.focus(valueIndex)
455
+ }
456
+ })
457
+
458
+ // =========================== Included ===========================
459
+ const sortedCacheValues = computed(
460
+ () => [...cacheValues.value].sort((a, b) => a - b),
461
+ )
462
+
463
+ // Provide a range values with included [min, max]
464
+ // Used for Track, Mark & Dot
465
+ const [includedStart, includedEnd] = computed(() => {
466
+ if (!getRange.value.rangeEnabled) {
467
+ return [mergedMin.value, sortedCacheValues.value[0]]
468
+ }
469
+
470
+ return [sortedCacheValues.value[0], sortedCacheValues.value[sortedCacheValues.value.length - 1]]
471
+ }).value
472
+
473
+ // ============================= Refs =============================
474
+ expose({
475
+ focus: () => {
476
+ handlesRef.value?.focus(0)
477
+ },
478
+ blur: () => {
479
+ const { activeElement } = document
480
+ if (containerRef.value?.contains(activeElement)) {
481
+ (activeElement as HTMLElement)?.blur()
482
+ }
483
+ },
484
+ })
485
+
486
+ // ========================== Auto Focus ==========================
487
+ watchEffect(() => {
488
+ if (props.autoFocus) {
489
+ handlesRef.value?.focus(0)
490
+ }
491
+ })
492
+ // =========================== Context ============================
493
+ const context = computed(() => ({
494
+ min: mergedMin,
495
+ max: mergedMax,
496
+ direction,
497
+ disabled: props.disabled,
498
+ keyboard: props.keyboard,
499
+ step: mergedStep,
500
+ included: props.included,
501
+ includedStart,
502
+ includedEnd,
503
+ range: getRange.value.rangeEnabled,
504
+ tabIndex: props.tabIndex,
505
+ ariaLabelForHandle: props.ariaLabelForHandle,
506
+ ariaLabelledByForHandle: props.ariaLabelledByForHandle,
507
+ ariaRequired: props.ariaRequired,
508
+ ariaValueTextFormatterForHandle: props.ariaValueTextFormatterForHandle,
509
+ styles: props.styles || {},
510
+ classNames: props.classNames || {},
511
+ }))
512
+ useProviderSliderContext(context.value)
513
+
514
+ // ============================ Render ============================
515
+ return () => {
516
+ const {
517
+ prefixCls = 'vc-slider',
518
+ id,
519
+
520
+ // Status
521
+ disabled = false,
522
+ vertical,
523
+
524
+ // Style
525
+ startPoint,
526
+ trackStyle,
527
+ handleStyle,
528
+ railStyle,
529
+ dotStyle,
530
+ activeDotStyle,
531
+
532
+ // Decorations
533
+ dots,
534
+ handleRender,
535
+ activeHandleRender,
536
+
537
+ // Components
538
+ track,
539
+ classNames,
540
+ styles,
541
+ } = props
542
+ return (
543
+ <div
544
+ ref={containerRef}
545
+ class={cls(prefixCls, [attrs.class], {
546
+ [`${prefixCls}-disabled`]: disabled,
547
+ [`${prefixCls}-vertical`]: vertical,
548
+ [`${prefixCls}-horizontal`]: !vertical,
549
+ [`${prefixCls}-with-marks`]: markList.value.length,
550
+ })}
551
+ style={attrs.style as CSSProperties}
552
+ onMousedown={onSliderMouseDown}
553
+ id={id}
554
+ >
555
+ <div
556
+ class={cls(`${prefixCls}-rail`, classNames?.rail)}
557
+ style={{ ...railStyle, ...styles?.rail }}
558
+ />
559
+
560
+ {track && (
561
+ <Tracks
562
+ prefixCls={prefixCls}
563
+ // 将style换成trackStyle,因为vue通过attrs取style,数组会合并,相同的样式名如backgroundColor后一个会覆盖前面的
564
+ trackStyle={trackStyle}
565
+ values={rawValues.value}
566
+ startPoint={startPoint}
567
+ onStartMove={mergedDraggableTrack.value ? onStartMove : undefined}
568
+ />
569
+ )}
570
+
571
+ <Steps
572
+ prefixCls={prefixCls}
573
+ marks={markList.value}
574
+ dots={dots}
575
+ style={dotStyle}
576
+ activeStyle={activeDotStyle}
577
+ />
578
+
579
+ <Handles
580
+ ref={handlesRef}
581
+ prefixCls={prefixCls}
582
+ // 原因如⬆️trackStyle
583
+ handleStyle={handleStyle}
584
+ values={cacheValues.value}
585
+ draggingIndex={draggingIndex.value}
586
+ draggingDelete={draggingDelete.value}
587
+ onStartMove={onStartMove}
588
+ onOffsetChange={onHandleOffsetChange}
589
+ onFocus={(e: FocusEvent) => emit('focus', e)}
590
+ onBlur={(e: FocusEvent) => emit('blur', e)}
591
+ handleRender={handleRender}
592
+ activeHandleRender={activeHandleRender}
593
+ onChangeComplete={finishChange}
594
+ onDelete={getRange.value.rangeEditable ? onDelete : () => {}}
595
+ />
596
+
597
+ <Marks prefixCls={prefixCls} marks={markList.value} onClick={changeToCloseValue} v-slots={slots} />
598
+ </div>
599
+ )
600
+ }
601
+ },
602
+ })