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