@v-c/slider 1.0.1 → 1.0.2

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