mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(slider): refactor slider component completed
This commit is contained in:
@@ -28,6 +28,7 @@ import ElCollapse from '@element-plus/collapse'
|
||||
import ElPopper from '@element-plus/popper'
|
||||
import ElTabs from '@element-plus/tabs'
|
||||
import ElTooltip from '@element-plus/tooltip'
|
||||
import ElSlider from '@element-plus/slider'
|
||||
|
||||
export {
|
||||
ElAlert,
|
||||
@@ -58,6 +59,7 @@ export {
|
||||
ElCollapse,
|
||||
ElTabs,
|
||||
ElTooltip,
|
||||
ElSlider,
|
||||
}
|
||||
|
||||
export default function install(app: App): void {
|
||||
@@ -90,4 +92,5 @@ export default function install(app: App): void {
|
||||
ElPopper(app)
|
||||
ElTabs(app)
|
||||
ElTooltip(app)
|
||||
ElSlider(app)
|
||||
}
|
||||
|
||||
6
packages/slider/index.ts
Normal file
6
packages/slider/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { App } from 'vue'
|
||||
import Slider from './src/index.vue'
|
||||
|
||||
export default (app: App): void => {
|
||||
app.component(Slider.name, Slider)
|
||||
}
|
||||
12
packages/slider/package.json
Normal file
12
packages/slider/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/slider",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.0"
|
||||
}
|
||||
}
|
||||
110
packages/slider/src/Slider.d.ts
vendored
Normal file
110
packages/slider/src/Slider.d.ts
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
import { ComputedRef, Ref } from 'vue'
|
||||
|
||||
export interface ISliderProps {
|
||||
modelValue: number | number[]
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
value: number | number[]
|
||||
showInput: boolean
|
||||
showInputControls: boolean
|
||||
inputSize: string
|
||||
showStops: boolean
|
||||
showTooltip: boolean
|
||||
formatTooltip: (val: number) => number | string
|
||||
disabled: boolean
|
||||
range: boolean
|
||||
vertical: boolean
|
||||
height: string
|
||||
debounce: number
|
||||
label: string
|
||||
tooltipClass: string
|
||||
marks: Record<number, any>
|
||||
}
|
||||
|
||||
export interface ISliderInitData {
|
||||
firstValue: Nullable<number>
|
||||
secondValue: Nullable<number>
|
||||
oldValue: Nullable<number>
|
||||
dragging: boolean
|
||||
sliderSize: number
|
||||
}
|
||||
|
||||
export interface ISliderProvider {
|
||||
disabled: ComputedRef<boolean>
|
||||
min: ComputedRef<number>
|
||||
max: ComputedRef<number>
|
||||
step: ComputedRef<number>
|
||||
showTooltip: ComputedRef<boolean>
|
||||
precision: ComputedRef<number>
|
||||
sliderSize: ComputedRef<number>
|
||||
formatTooltip: ComputedRef<(value: number) => number | string>
|
||||
emitChange: () => void
|
||||
resetSize: () => void
|
||||
updateDragging: (val: boolean) => void
|
||||
}
|
||||
|
||||
export type Mark = {
|
||||
point: number
|
||||
position: number
|
||||
mark: {[s: string]: any;}
|
||||
}
|
||||
|
||||
export type Marks = ComputedRef<Mark[]>
|
||||
|
||||
export type Stops = {
|
||||
stops: ComputedRef<number[]>
|
||||
getStopStyle: (position: number) => CSSStyleDeclaration
|
||||
}
|
||||
|
||||
export type Slide = {
|
||||
slider: Ref<HTMLElement>
|
||||
firstButton: Ref<ISliderButton>
|
||||
secondButton: Ref<ISliderButton>
|
||||
sliderDisabled: ComputedRef<boolean>
|
||||
minValue: ComputedRef<number>
|
||||
maxValue: ComputedRef<number>
|
||||
runwayStyle: ComputedRef<CSSStyleDeclaration>
|
||||
barStyle: ComputedRef<CSSStyleDeclaration>
|
||||
resetSize: () => void
|
||||
setPosition: (percent: number) => void
|
||||
emitChange: () => void
|
||||
onSliderClick: (event: MouseEvent) => void
|
||||
}
|
||||
|
||||
export type ButtonRefs = {
|
||||
[s in 'firstButton' | 'secondButton']: Ref<Nullable<ISliderButton>>
|
||||
}
|
||||
|
||||
export interface ISliderButtonProps {
|
||||
modelValue: number
|
||||
vertical: boolean
|
||||
tooltipClass: string
|
||||
}
|
||||
|
||||
export interface ISliderButtonInitData {
|
||||
hovering: boolean
|
||||
dragging: boolean
|
||||
isClick: boolean
|
||||
startX: number
|
||||
currentX: number
|
||||
startY: number
|
||||
currentY: number
|
||||
startPosition: number
|
||||
newPosition: number
|
||||
oldValue: number
|
||||
}
|
||||
|
||||
export interface ISliderButton {
|
||||
tooltip: Ref<Nullable<HTMLElement>>
|
||||
tooltipVisible: Ref<boolean>
|
||||
showTooltip: Ref<boolean>
|
||||
wrapperStyle: ComputedRef<CSSStyleDeclaration>
|
||||
formatValue: ComputedRef<number | string>
|
||||
handleMouseEnter: () => void
|
||||
handleMouseLeave: () => void
|
||||
onButtonDown: (event: MouseEvent | TouchEvent) => void
|
||||
onLeftKeyDown: () => void
|
||||
onRightKeyDown: () => void
|
||||
setPosition: (newPosition: number) => void
|
||||
}
|
||||
119
packages/slider/src/button.vue
Normal file
119
packages/slider/src/button.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div
|
||||
ref="button"
|
||||
class="el-slider__button-wrapper"
|
||||
:class="{ 'hover': hovering, 'dragging': dragging }"
|
||||
:style="wrapperStyle"
|
||||
tabindex="0"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mousedown="onButtonDown"
|
||||
@touchstart="onButtonDown"
|
||||
@focus="handleMouseEnter"
|
||||
@blur="handleMouseLeave"
|
||||
@keydown.left="onLeftKeyDown"
|
||||
@keydown.right="onRightKeyDown"
|
||||
@keydown.down.prevent="onLeftKeyDown"
|
||||
@keydown.up.prevent="onRightKeyDown"
|
||||
>
|
||||
<el-popper
|
||||
v-if="showTooltip"
|
||||
ref="tooltip"
|
||||
placement="top"
|
||||
:popper-class="tooltipClass"
|
||||
manual-mode
|
||||
:value="tooltipVisible"
|
||||
>
|
||||
<template #default>{{ formatValue }}</template>
|
||||
<template #trigger>
|
||||
<div class="el-slider__button el-tooltip" :class="{ 'hover': hovering, 'dragging': dragging }"></div>
|
||||
</template>
|
||||
</el-popper>
|
||||
<div v-else class="el-slider__button el-tooltip" :class="{ 'hover': hovering, 'dragging': dragging }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Popper as ElPopper } from '@element-plus/popper'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
|
||||
import { defineComponent, reactive, toRefs } from 'vue'
|
||||
import { useSliderButton } from './useSliderButton'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElSliderButton',
|
||||
|
||||
components: {
|
||||
ElPopper,
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tooltipClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
emits: [UPDATE_MODEL_EVENT],
|
||||
|
||||
setup(props, { emit }) {
|
||||
|
||||
const initData = reactive({
|
||||
hovering: false,
|
||||
dragging: false,
|
||||
isClick: false,
|
||||
startX: 0,
|
||||
currentX: 0,
|
||||
startY: 0,
|
||||
currentY: 0,
|
||||
startPosition: 0,
|
||||
newPosition: 0,
|
||||
oldValue: props.modelValue,
|
||||
})
|
||||
|
||||
const {
|
||||
tooltip,
|
||||
tooltipVisible,
|
||||
showTooltip,
|
||||
wrapperStyle,
|
||||
formatValue,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
onButtonDown,
|
||||
onLeftKeyDown,
|
||||
onRightKeyDown,
|
||||
setPosition,
|
||||
} = useSliderButton(props, initData, emit)
|
||||
|
||||
|
||||
const {
|
||||
hovering,
|
||||
dragging,
|
||||
} = toRefs(initData)
|
||||
|
||||
return {
|
||||
tooltip,
|
||||
tooltipVisible,
|
||||
showTooltip,
|
||||
wrapperStyle,
|
||||
formatValue,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
onButtonDown,
|
||||
onLeftKeyDown,
|
||||
onRightKeyDown,
|
||||
setPosition,
|
||||
|
||||
hovering,
|
||||
dragging,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
420
packages/slider/src/index.vue
Normal file
420
packages/slider/src/index.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div
|
||||
ref="sliderWrapper"
|
||||
class="el-slider"
|
||||
:class="{ 'is-vertical': vertical, 'el-slider--with-input': showInput }"
|
||||
role="slider"
|
||||
:aria-valuemin="min"
|
||||
:aria-valuemax="max"
|
||||
:aria-orientation="vertical ? 'vertical': 'horizontal'"
|
||||
:aria-disabled="sliderDisabled"
|
||||
>
|
||||
<!-- <el-input-number-->
|
||||
<!-- v-if="showInput && !range"-->
|
||||
<!-- ref="input"-->
|
||||
<!-- v-model="firstValue"-->
|
||||
<!-- class="el-slider__input"-->
|
||||
<!-- :step="step"-->
|
||||
<!-- :disabled="sliderDisabled"-->
|
||||
<!-- :controls="showInputControls"-->
|
||||
<!-- :min="min"-->
|
||||
<!-- :max="max"-->
|
||||
<!-- :debounce="debounce"-->
|
||||
<!-- :size="inputSize"-->
|
||||
<!-- @change="emitChange"-->
|
||||
<!-- />-->
|
||||
<div
|
||||
ref="slider"
|
||||
class="el-slider__runway"
|
||||
:class="{ 'show-input': showInput, 'disabled': sliderDisabled }"
|
||||
:style="runwayStyle"
|
||||
@click="onSliderClick"
|
||||
>
|
||||
<div
|
||||
class="el-slider__bar"
|
||||
:style="barStyle"
|
||||
>
|
||||
</div>
|
||||
<slider-button
|
||||
ref="firstButton"
|
||||
v-model="firstValue"
|
||||
:vertical="vertical"
|
||||
:tooltip-class="tooltipClass"
|
||||
/>
|
||||
<slider-button
|
||||
v-if="range"
|
||||
ref="secondButton"
|
||||
v-model="secondValue"
|
||||
:vertical="vertical"
|
||||
:tooltip-class="tooltipClass"
|
||||
/>
|
||||
<div v-if="showStops">
|
||||
<div
|
||||
v-for="(item, key) in stops"
|
||||
:key="key"
|
||||
class="el-slider__stop"
|
||||
:style="getStopStyle(item)"
|
||||
></div>
|
||||
</div>
|
||||
<template v-if="markList.length > 0">
|
||||
<div>
|
||||
<div
|
||||
v-for="(item, key) in markList"
|
||||
:key="key"
|
||||
:style="getStopStyle(item.position)"
|
||||
class="el-slider__stop el-slider__marks-stop"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="el-slider__marks">
|
||||
<slider-marker
|
||||
v-for="(item, key) in markList"
|
||||
:key="key"
|
||||
:mark="item.mark"
|
||||
:style="getStopStyle(item.position)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
|
||||
import { off, on } from '@element-plus/utils/dom'
|
||||
import throwError from '@element-plus/utils/error'
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
PropType,
|
||||
provide,
|
||||
reactive,
|
||||
ref,
|
||||
Ref,
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue'
|
||||
// TODO: waiting for ElInputNumber component refactored
|
||||
// import ElInputNumber from '@element-plus/input-number'
|
||||
import SliderButton from './button.vue'
|
||||
import SliderMarker from './marker.vue'
|
||||
import { useMarks } from './useMarks'
|
||||
import { useSlide } from './useSlide'
|
||||
import { useStops } from './useStops'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElSlider',
|
||||
|
||||
components: {
|
||||
// ElInputNumber,
|
||||
SliderButton,
|
||||
SliderMarker,
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Number, Array] as PropType<number | number[]>,
|
||||
default: 0,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showInputControls: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
inputSize: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
showStops: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
formatTooltip: {
|
||||
type: Function as PropType<(val: number) => number | string>,
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
range: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
tooltipClass: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
marks: {
|
||||
type: Object,
|
||||
default: value => value,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [UPDATE_MODEL_EVENT, 'change'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const initData = reactive({
|
||||
firstValue: null,
|
||||
secondValue: null,
|
||||
oldValue: null,
|
||||
dragging: false,
|
||||
sliderSize: 1,
|
||||
})
|
||||
|
||||
const {
|
||||
slider,
|
||||
firstButton,
|
||||
secondButton,
|
||||
sliderDisabled,
|
||||
minValue,
|
||||
maxValue,
|
||||
runwayStyle,
|
||||
barStyle,
|
||||
resetSize,
|
||||
emitChange,
|
||||
onSliderClick,
|
||||
} = useSlide(props, initData, emit)
|
||||
|
||||
const {
|
||||
stops,
|
||||
getStopStyle,
|
||||
} = useStops(props, initData, minValue, maxValue)
|
||||
|
||||
const markList = useMarks(props)
|
||||
|
||||
useWatch(props, initData, minValue, maxValue, emit)
|
||||
|
||||
const precision = computed(() => {
|
||||
let precisions = [props.min, props.max, props.step].map(item => {
|
||||
let decimal = ('' + item).split('.')[1]
|
||||
return decimal ? decimal.length : 0
|
||||
})
|
||||
return Math.max.apply(null, precisions)
|
||||
})
|
||||
|
||||
const { sliderWrapper } = useLifecycle(props, initData, resetSize)
|
||||
|
||||
|
||||
const {
|
||||
firstValue,
|
||||
secondValue,
|
||||
oldValue,
|
||||
dragging,
|
||||
sliderSize,
|
||||
} = toRefs(initData)
|
||||
|
||||
const updateDragging = (val: boolean) => {
|
||||
initData.dragging = val
|
||||
}
|
||||
|
||||
provide('SliderProvider', {
|
||||
disabled: sliderDisabled,
|
||||
min: computed(() => props.min),
|
||||
max: computed(() => props.max),
|
||||
step: computed(() => props.step),
|
||||
showTooltip: computed(() => props.showTooltip),
|
||||
precision: precision,
|
||||
sliderSize: computed(() => sliderSize.value),
|
||||
formatTooltip: computed(() => props.formatTooltip),
|
||||
emitChange: emitChange,
|
||||
resetSize: resetSize,
|
||||
updateDragging: updateDragging,
|
||||
})
|
||||
|
||||
return {
|
||||
firstValue,
|
||||
secondValue,
|
||||
oldValue,
|
||||
dragging,
|
||||
sliderSize,
|
||||
|
||||
slider,
|
||||
firstButton,
|
||||
secondButton,
|
||||
sliderDisabled,
|
||||
runwayStyle,
|
||||
barStyle,
|
||||
emitChange,
|
||||
onSliderClick,
|
||||
getStopStyle,
|
||||
|
||||
stops,
|
||||
markList,
|
||||
|
||||
sliderWrapper,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const useWatch = (props, initData, minValue, maxValue, emit) => {
|
||||
|
||||
const _emit = (val: number | number[]) => {
|
||||
emit(UPDATE_MODEL_EVENT, val)
|
||||
}
|
||||
|
||||
const valueChanged = () => {
|
||||
if (props.range) {
|
||||
return ![minValue.value, maxValue.value]
|
||||
.every((item, index) => item === initData.oldValue[index])
|
||||
} else {
|
||||
return props.modelValue !== initData.oldValue
|
||||
}
|
||||
}
|
||||
|
||||
const setValues = () => {
|
||||
if (props.min > props.max) {
|
||||
throwError('Slider', 'min should not be greater than max.')
|
||||
return
|
||||
}
|
||||
const val = props.modelValue
|
||||
if (props.range && Array.isArray(val)) {
|
||||
if (val[1] < props.min) {
|
||||
_emit([props.min, props.min])
|
||||
} else if (val[0] > props.max) {
|
||||
_emit([props.max, props.max])
|
||||
} else if (val[0] < props.min) {
|
||||
_emit([props.min, val[1]])
|
||||
} else if (val[1] > props.max) {
|
||||
_emit([val[0], props.max])
|
||||
} else {
|
||||
initData.firstValue = val[0]
|
||||
initData.secondValue = val[1]
|
||||
if (valueChanged()) {
|
||||
_emit([minValue.value, maxValue.value])
|
||||
initData.oldValue = val.slice()
|
||||
}
|
||||
}
|
||||
} else if (!props.range && typeof val === 'number' && !isNaN(val)) {
|
||||
if (val < props.min) {
|
||||
_emit(props.min)
|
||||
} else if (val > props.max) {
|
||||
_emit(props.max)
|
||||
} else {
|
||||
initData.firstValue = val
|
||||
if (valueChanged()) {
|
||||
_emit(val)
|
||||
initData.oldValue = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => initData.dragging, val => {
|
||||
if (!val) {
|
||||
setValues()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => initData.firstValue, val => {
|
||||
if (props.range) {
|
||||
_emit([minValue.value, maxValue.value])
|
||||
} else {
|
||||
_emit(val)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => initData.secondValue, () => {
|
||||
if (props.range) {
|
||||
_emit([minValue.value, maxValue.value])
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val, oldVal) => {
|
||||
if (initData.dragging
|
||||
|| Array.isArray(val)
|
||||
&& Array.isArray(oldVal)
|
||||
&& val.every((item, index) => item === oldVal[index])) {
|
||||
return
|
||||
}
|
||||
setValues()
|
||||
})
|
||||
|
||||
watch(() => [props.min, props.max], () => {
|
||||
setValues()
|
||||
})
|
||||
}
|
||||
|
||||
const useLifecycle = (props, initData, resetSize) => {
|
||||
const sliderWrapper: Ref<Nullable<HTMLElement>> = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
let valuetext
|
||||
if (props.range) {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
initData.firstValue = Math.max(props.min, props.modelValue[0])
|
||||
initData.secondValue = Math.min(props.max, props.modelValue[1])
|
||||
} else {
|
||||
initData.firstValue = props.min
|
||||
initData.secondValue = props.max
|
||||
}
|
||||
initData.oldValue = [initData.firstValue, initData.secondValue]
|
||||
valuetext = `${ initData.firstValue }-${ initData.secondValue }`
|
||||
} else {
|
||||
if (typeof props.modelValue !== 'number' || isNaN(props.modelValue)) {
|
||||
initData.firstValue = props.min
|
||||
} else {
|
||||
initData.firstValue = Math.min(props.max, Math.max(props.min, props.modelValue))
|
||||
}
|
||||
initData.oldValue = initData.firstValue
|
||||
valuetext = initData.firstValue
|
||||
}
|
||||
|
||||
sliderWrapper.value.setAttribute('aria-valuetext', valuetext)
|
||||
|
||||
// label screen reader
|
||||
sliderWrapper.value.setAttribute('aria-label', props.label ? props.label : `slider between ${ props.min } and ${ props.max }`)
|
||||
|
||||
on(window, 'resize', resetSize)
|
||||
|
||||
await nextTick()
|
||||
resetSize()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
off(window, 'resize', resetSize)
|
||||
})
|
||||
|
||||
return {
|
||||
sliderWrapper,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
28
packages/slider/src/marker.vue
Normal file
28
packages/slider/src/marker.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, h, PropType } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMarker',
|
||||
props: {
|
||||
mark: {
|
||||
type: [String, Object] as PropType<string | Record<string, unknown>>,
|
||||
default: () => undefined,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const label = computed(() => {
|
||||
return typeof props.mark === 'string' ? props.mark : props.mark.label
|
||||
})
|
||||
|
||||
return {
|
||||
label,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return h('div', {
|
||||
class: 'el-slider__marks-text',
|
||||
style: this.mark?.style,
|
||||
}, this.label)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
21
packages/slider/src/useMarks.ts
Normal file
21
packages/slider/src/useMarks.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { computed } from 'vue'
|
||||
import { ISliderProps, Mark } from './Slider'
|
||||
|
||||
|
||||
export const useMarks = (props: ISliderProps) => {
|
||||
return computed(() => {
|
||||
if (!props.marks) {
|
||||
return []
|
||||
}
|
||||
|
||||
const marksKeys = Object.keys(props.marks)
|
||||
return marksKeys.map(parseFloat)
|
||||
.sort((a, b) => a - b)
|
||||
.filter(point => point <= props.max && point >= props.min)
|
||||
.map((point): Mark => ({
|
||||
point,
|
||||
position: (point - props.min) * 100 / (props.max - props.min),
|
||||
mark: props.marks[point],
|
||||
}))
|
||||
})
|
||||
}
|
||||
110
packages/slider/src/useSlide.ts
Normal file
110
packages/slider/src/useSlide.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { computed, inject, nextTick, ref } from 'vue'
|
||||
import { ButtonRefs, ISliderInitData, ISliderProps } from './Slider'
|
||||
|
||||
export const useSlide = (props: ISliderProps, initData: ISliderInitData, emit) => {
|
||||
const elForm: {disabled?: boolean;} = inject('elForm', {})
|
||||
|
||||
const slider = ref<Nullable<HTMLElement>>(null)
|
||||
|
||||
const firstButton = ref(null)
|
||||
|
||||
const secondButton = ref(null)
|
||||
|
||||
const buttonRefs: ButtonRefs = {
|
||||
firstButton,
|
||||
secondButton,
|
||||
}
|
||||
|
||||
const sliderDisabled = computed(() => {
|
||||
return props.disabled || (elForm?.disabled || false)
|
||||
})
|
||||
|
||||
const minValue = computed(() => {
|
||||
return Math.min(initData.firstValue, initData.secondValue)
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
return Math.max(initData.firstValue, initData.secondValue)
|
||||
})
|
||||
|
||||
const barSize = computed(() => {
|
||||
return props.range
|
||||
? `${ 100 * (maxValue.value - minValue.value) / (props.max - props.min) }%`
|
||||
: `${ 100 * (initData.firstValue - props.min) / (props.max - props.min) }%`
|
||||
})
|
||||
|
||||
const barStart = computed(() => {
|
||||
return props.range
|
||||
? `${ 100 * (minValue.value - props.min) / (props.max - props.min) }%`
|
||||
: '0%'
|
||||
})
|
||||
|
||||
const runwayStyle = computed(() => {
|
||||
return (props.vertical ? { height: props.height } : {}) as CSSStyleDeclaration
|
||||
})
|
||||
|
||||
const barStyle = computed(() => {
|
||||
return (props.vertical
|
||||
? {
|
||||
height: barSize.value,
|
||||
bottom: barStart.value,
|
||||
} : {
|
||||
width: barSize.value,
|
||||
left: barStart.value,
|
||||
}) as CSSStyleDeclaration
|
||||
})
|
||||
|
||||
const resetSize = () => {
|
||||
if (slider.value) {
|
||||
initData.sliderSize = slider.value[`client${ props.vertical ? 'Height' : 'Width' }`]
|
||||
}
|
||||
}
|
||||
|
||||
const setPosition = (percent: number) => {
|
||||
const targetValue = props.min + percent * (props.max - props.min) / 100
|
||||
if (!props.range) {
|
||||
firstButton.value.setPosition(percent)
|
||||
return
|
||||
}
|
||||
let buttonRefName: string
|
||||
if (Math.abs(minValue.value - targetValue) < Math.abs(maxValue.value - targetValue)) {
|
||||
buttonRefName = initData.firstValue < initData.secondValue ? 'firstButton' : 'secondButton'
|
||||
} else {
|
||||
buttonRefName = initData.firstValue > initData.secondValue ? 'firstButton' : 'secondButton'
|
||||
}
|
||||
buttonRefs[buttonRefName].value.setPosition(percent)
|
||||
}
|
||||
|
||||
const emitChange = async () => {
|
||||
await nextTick()
|
||||
emit('change', props.range ? [minValue.value, maxValue.value] : props.modelValue)
|
||||
}
|
||||
|
||||
const onSliderClick = (event: MouseEvent) => {
|
||||
if (sliderDisabled.value || initData.dragging) return
|
||||
resetSize()
|
||||
if (props.vertical) {
|
||||
const sliderOffsetBottom = slider.value.getBoundingClientRect().bottom
|
||||
setPosition((sliderOffsetBottom - event.clientY) / initData.sliderSize * 100)
|
||||
} else {
|
||||
const sliderOffsetLeft = slider.value.getBoundingClientRect().left
|
||||
setPosition((event.clientX - sliderOffsetLeft) / initData.sliderSize * 100)
|
||||
}
|
||||
emitChange()
|
||||
}
|
||||
|
||||
return {
|
||||
slider,
|
||||
firstButton,
|
||||
secondButton,
|
||||
sliderDisabled,
|
||||
minValue,
|
||||
maxValue,
|
||||
runwayStyle,
|
||||
barStyle,
|
||||
resetSize,
|
||||
setPosition,
|
||||
emitChange,
|
||||
onSliderClick,
|
||||
}
|
||||
}
|
||||
221
packages/slider/src/useSliderButton.ts
Normal file
221
packages/slider/src/useSliderButton.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
|
||||
import { off, on } from '@element-plus/utils/dom'
|
||||
import { computed, ComputedRef, inject, nextTick, ref, watch } from 'vue'
|
||||
import { ISliderButtonInitData, ISliderButtonProps, ISliderProvider } from './Slider'
|
||||
|
||||
|
||||
const useTooltip = (props: ISliderButtonProps, formatTooltip: ComputedRef<(value: number) => number | string>, showTooltip: ComputedRef<boolean>) => {
|
||||
|
||||
const tooltip = ref(null)
|
||||
|
||||
const tooltipVisible = ref(false)
|
||||
|
||||
const enableFormat = computed(() => {
|
||||
return formatTooltip.value instanceof Function
|
||||
})
|
||||
|
||||
const formatValue = computed(() => {
|
||||
return enableFormat.value && formatTooltip.value(props.modelValue) || props.modelValue
|
||||
})
|
||||
|
||||
const displayTooltip = () => {
|
||||
showTooltip.value && (tooltipVisible.value = true)
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
showTooltip.value && (tooltipVisible.value = false)
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip,
|
||||
tooltipVisible,
|
||||
formatValue,
|
||||
displayTooltip,
|
||||
hideTooltip,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const useSliderButton = (props: ISliderButtonProps, initData: ISliderButtonInitData, emit) => {
|
||||
const {
|
||||
disabled,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
showTooltip,
|
||||
precision,
|
||||
sliderSize,
|
||||
formatTooltip,
|
||||
emitChange,
|
||||
resetSize,
|
||||
updateDragging,
|
||||
} = inject<ISliderProvider>('SliderProvider')
|
||||
|
||||
const {
|
||||
tooltip,
|
||||
tooltipVisible,
|
||||
formatValue,
|
||||
displayTooltip,
|
||||
hideTooltip,
|
||||
} = useTooltip(props, formatTooltip, showTooltip)
|
||||
|
||||
const currentPosition = computed(() => {
|
||||
return `${ (props.modelValue - min.value) / (max.value - min.value) * 100 }%`
|
||||
})
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
return (props.vertical ? { bottom: currentPosition.value } : { left: currentPosition.value }) as CSSStyleDeclaration
|
||||
})
|
||||
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
initData.hovering = true
|
||||
displayTooltip()
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
initData.hovering = false
|
||||
hideTooltip()
|
||||
}
|
||||
|
||||
const onButtonDown = (event: MouseEvent | TouchEvent) => {
|
||||
if (disabled.value) return
|
||||
event.preventDefault()
|
||||
onDragStart(event)
|
||||
on(window, 'mousemove', onDragging)
|
||||
on(window, 'touchmove', onDragging)
|
||||
on(window, 'mouseup', onDragEnd)
|
||||
on(window, 'touchend', onDragEnd)
|
||||
on(window, 'contextmenu', onDragEnd)
|
||||
}
|
||||
|
||||
const onLeftKeyDown = () => {
|
||||
if (disabled.value) return
|
||||
initData.newPosition = parseFloat(currentPosition.value) - step.value / (max.value - min.value) * 100
|
||||
setPosition(initData.newPosition)
|
||||
emitChange()
|
||||
}
|
||||
|
||||
const onRightKeyDown = () => {
|
||||
if (disabled.value) return
|
||||
initData.newPosition = parseFloat(currentPosition.value) + step.value / (max.value - min.value) * 100
|
||||
setPosition(initData.newPosition)
|
||||
emitChange()
|
||||
}
|
||||
|
||||
const getClientXY = (event: MouseEvent | TouchEvent) => {
|
||||
let clientX: number
|
||||
let clientY: number
|
||||
if (event.type === 'touchstart') {
|
||||
clientY = (event as TouchEvent).touches[0].clientY
|
||||
clientX = (event as TouchEvent).touches[0].clientX
|
||||
} else {
|
||||
clientY = (event as MouseEvent).clientY
|
||||
clientX = (event as MouseEvent).clientX
|
||||
}
|
||||
return {
|
||||
clientX,
|
||||
clientY,
|
||||
}
|
||||
}
|
||||
|
||||
const onDragStart = (event: MouseEvent | TouchEvent) => {
|
||||
initData.dragging = true
|
||||
initData.isClick = true
|
||||
const {
|
||||
clientX,
|
||||
clientY,
|
||||
} = getClientXY(event)
|
||||
if (props.vertical) {
|
||||
initData.startY = clientY
|
||||
} else {
|
||||
initData.startX = clientX
|
||||
}
|
||||
initData.startPosition = parseFloat(currentPosition.value)
|
||||
initData.newPosition = initData.startPosition
|
||||
}
|
||||
|
||||
const onDragging = (event: MouseEvent | TouchEvent) => {
|
||||
if (initData.dragging) {
|
||||
initData.isClick = false
|
||||
displayTooltip()
|
||||
resetSize()
|
||||
let diff: number
|
||||
const {
|
||||
clientX,
|
||||
clientY,
|
||||
} = getClientXY(event)
|
||||
if (props.vertical) {
|
||||
initData.currentY = clientY
|
||||
diff = (initData.startY - initData.currentY) / sliderSize.value * 100
|
||||
} else {
|
||||
initData.currentX = clientX
|
||||
diff = (initData.currentX - initData.startX) / sliderSize.value * 100
|
||||
}
|
||||
initData.newPosition = initData.startPosition + diff
|
||||
setPosition(initData.newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
if (initData.dragging) {
|
||||
/*
|
||||
* 防止在 mouseup 后立即触发 click,导致滑块有几率产生一小段位移
|
||||
* 不使用 preventDefault 是因为 mouseup 和 click 没有注册在同一个 DOM 上
|
||||
*/
|
||||
setTimeout(() => {
|
||||
initData.dragging = false
|
||||
hideTooltip()
|
||||
if (!initData.isClick) {
|
||||
setPosition(initData.newPosition)
|
||||
emitChange()
|
||||
}
|
||||
}, 0)
|
||||
off(window, 'mousemove', onDragging)
|
||||
off(window, 'touchmove', onDragging)
|
||||
off(window, 'mouseup', onDragEnd)
|
||||
off(window, 'touchend', onDragEnd)
|
||||
off(window, 'contextmenu', onDragEnd)
|
||||
}
|
||||
}
|
||||
|
||||
const setPosition = async (newPosition: number) => {
|
||||
if (newPosition === null || isNaN(newPosition)) return
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0
|
||||
} else if (newPosition > 100) {
|
||||
newPosition = 100
|
||||
}
|
||||
const lengthPerStep = 100 / ((max.value - min.value) / step.value)
|
||||
const steps = Math.round(newPosition / lengthPerStep)
|
||||
let value = steps * lengthPerStep * (max.value - min.value) * 0.01 + min.value
|
||||
value = parseFloat(value.toFixed(precision.value))
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
|
||||
if (!initData.dragging && props.modelValue !== initData.oldValue) {
|
||||
initData.oldValue = props.modelValue
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
initData.dragging && displayTooltip()
|
||||
tooltip.value && tooltip.value.popperInstance.update()
|
||||
}
|
||||
|
||||
watch(() => initData.dragging, val => {
|
||||
updateDragging(val)
|
||||
})
|
||||
|
||||
return {
|
||||
tooltip,
|
||||
tooltipVisible,
|
||||
showTooltip,
|
||||
wrapperStyle,
|
||||
formatValue,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
onButtonDown,
|
||||
onLeftKeyDown,
|
||||
onRightKeyDown,
|
||||
setPosition,
|
||||
}
|
||||
}
|
||||
34
packages/slider/src/useStops.ts
Normal file
34
packages/slider/src/useStops.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { computed, ComputedRef } from 'vue'
|
||||
import { ISliderInitData, ISliderProps, Stops } from './Slider'
|
||||
|
||||
export const useStops = (props: ISliderProps, initData: ISliderInitData, minValue: ComputedRef<number>, maxValue: ComputedRef<number>): Stops => {
|
||||
const stops = computed(() => {
|
||||
if (!props.showStops || props.min > props.max) return []
|
||||
if (props.step === 0) {
|
||||
process.env.NODE_ENV !== 'production' && console.warn('[Element Warn][Slider]step should not be 0.')
|
||||
return []
|
||||
}
|
||||
|
||||
const stopCount = (props.max - props.min) / props.step
|
||||
const stepWidth = 100 * props.step / (props.max - props.min)
|
||||
const result = Array.from<number>({ length: stopCount - 1 }).map((_, index) => ((index + 1) * stepWidth))
|
||||
|
||||
if (props.range) {
|
||||
return result.filter(step => {
|
||||
return step < 100 * (minValue.value - props.min) / (props.max - props.min)
|
||||
|| step > 100 * (maxValue.value - props.min) / (props.max - props.min)
|
||||
})
|
||||
} else {
|
||||
return result.filter(step => step > 100 * (initData.firstValue - props.min) / (props.max - props.min))
|
||||
}
|
||||
})
|
||||
|
||||
const getStopStyle = (position: number) => {
|
||||
return (props.vertical ? { 'bottom': position + '%' } : { 'left': position + '%' }) as CSSStyleDeclaration
|
||||
}
|
||||
|
||||
return {
|
||||
stops,
|
||||
getStopStyle,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user