提交 6c6c32bc 编写于 作者: D DCloud_LXH

feat: slider

上级 cc9c1d47
......@@ -2,7 +2,11 @@ import { defineComponent, inject, provide, ref } from 'vue'
import type { Ref, ExtractPropTypes, ComputedRef } from 'vue'
import { PolySymbol } from '@dcloudio/uni-core'
import { UniFormCtx, uniFormKey } from '../form'
import { CustomEventTrigger, useCustomEvent } from '../../helpers/useEvent'
import {
CustomEventTrigger,
useCustomEvent,
EmitEvent,
} from '../../helpers/useEvent'
export const uniCheckGroupKey = PolySymbol(__DEV__ ? 'uniCheckGroup' : 'ucg')
......@@ -29,10 +33,10 @@ type CheckBoxGroupProps = ExtractPropTypes<typeof props>
export default /*#__PURE__*/ defineComponent({
name: 'CheckboxGroup',
props,
// emits: ['change'],
emits: ['change'],
setup(props, { emit, slots }) {
const rootRef: Ref<HTMLElement | null> = ref(null)
const trigger = useCustomEvent(rootRef, emit)
const trigger = useCustomEvent<EmitEvent<typeof emit>>(rootRef, emit)
useProvideCheckGroup(props, trigger)
......
......@@ -20,7 +20,7 @@ import RadioGroup from './radio-group/index'
import ResizeSensor from './resize-sensor/index'
import RichText from './rich-text/index.vue'
import ScrollView from './scroll-view/index.vue'
import Slider from './slider/index.vue'
import Slider from './slider/index'
// import Swiper from './swiper/index.vue'
import SwiperItem from './swiper-item/index.vue'
import Switch from './switch/index.vue'
......
import {
computed,
defineComponent,
inject,
onMounted,
onBeforeUnmount,
ref,
} from 'vue'
import type { ExtractPropTypes, Ref } from 'vue'
import { useTouchtrack, TouchtrackEvent } from '../../helpers/useTouchtrack'
import {
CustomEventTrigger,
useCustomEvent,
EmitEvent,
} from '../../helpers/useEvent'
import { UniFormCtx, uniFormKey } from '../form'
const props = {
name: {
type: String,
default: '',
},
min: {
type: [Number, String],
default: 0,
},
max: {
type: [Number, String],
default: 100,
},
value: {
type: [Number, String],
default: 0,
},
step: {
type: [Number, String],
default: 1,
},
disabled: {
type: [Boolean, String],
default: false,
},
color: {
type: String,
default: '#e9e9e9',
},
backgroundColor: {
type: String,
default: '#e9e9e9',
},
activeColor: {
type: String,
default: '#007aff',
},
selectedColor: {
type: String,
default: '#007aff',
},
blockColor: {
type: String,
default: '#ffffff',
},
blockSize: {
type: [Number, String],
default: 28,
},
showValue: {
type: [Boolean, String],
default: false,
},
}
type SliderProps = ExtractPropTypes<typeof props>
type HTMLRef = Ref<HTMLElement | null>
export default /*#__PURE__*/ defineComponent({
name: 'Slider',
props,
emits: ['changing', 'change'],
setup(props, { emit }) {
const sliderRef: HTMLRef = ref(null)
const sliderValueRef: HTMLRef = ref(null)
const sliderHandleRef: HTMLRef = ref(null)
const sliderValue = ref(Number(props.value))
const trigger = useCustomEvent<EmitEvent<typeof emit>>(sliderRef, emit)
const state = useSliderState(props, sliderValue)
const { _onClick, _onTrack } = useSliderLoader(
props,
sliderValue,
sliderRef,
sliderValueRef,
trigger
)
onMounted(() => {
useTouchtrack(sliderHandleRef.value!, _onTrack)
})
return () => {
const { setBgColor, setBlockBg, setActiveColor, setBlockStyle } = state
return (
<uni-slider ref={sliderRef} onClick={_onClick}>
<div class="uni-slider-wrapper">
<div class="uni-slider-tap-area">
<div style={setBgColor.value} class="uni-slider-handle-wrapper">
<div
ref={sliderHandleRef}
style={setBlockBg.value}
class="uni-slider-handle"
/>
<div style={setBlockStyle.value} class="uni-slider-thumb" />
<div style={setActiveColor.value} class="uni-slider-track" />
</div>
</div>
<span
v-show={props.showValue}
ref={sliderValueRef}
class="uni-slider-value"
>
{sliderValue.value}
</span>
</div>
<slot />
</uni-slider>
)
}
},
})
function useSliderState(props: SliderProps, sliderValue: Ref<number>) {
const _getValueWidth = () => {
const max = Number(props.max)
const min = Number(props.min)
return (100 * (sliderValue.value - min)) / (max - min) + '%'
}
const _getBgColor = () => {
return props.backgroundColor !== '#e9e9e9'
? props.backgroundColor
: props.color !== '#007aff'
? props.color
: '#007aff'
}
const _getActiveColor = () => {
return props.activeColor !== '#007aff'
? props.activeColor
: props.selectedColor !== '#e9e9e9'
? props.selectedColor
: '#e9e9e9'
}
const state = {
setBgColor: computed(() => ({ backgroundColor: _getBgColor() })),
setBlockBg: computed(() => ({ left: _getValueWidth() })),
setActiveColor: computed(() => ({
backgroundColor: _getActiveColor(),
width: _getValueWidth(),
})),
setBlockStyle: computed(() => ({
width: props.blockSize + 'px',
height: props.blockSize + 'px',
marginLeft: -props.blockSize / 2 + 'px',
marginTop: -props.blockSize / 2 + 'px',
left: _getValueWidth(),
backgroundColor: props.blockColor,
})),
}
return state
}
function useSliderLoader(
props: SliderProps,
sliderValue: Ref<number>,
sliderRef: HTMLRef,
sliderValueRef: HTMLRef,
trigger: CustomEventTrigger
) {
const _onClick = ($event: MouseEvent) => {
if (props.disabled) {
return
}
_onUserChangedValue($event)
trigger('change', $event, {
value: sliderValue.value,
})
}
const _filterValue = (e: number) => {
const max = Number(props.max)
const min = Number(props.min)
const step = Number(props.step)
return e < min
? min
: e > max
? max
: computeController.mul.call(Math.round((e - min) / step), step) + min
}
const _onUserChangedValue = (e: MouseEvent) => {
const max = Number(props.max)
const min = Number(props.min)
const sliderRightBox = sliderValueRef.value!
const sliderRightBoxLeft = getComputedStyle(sliderRightBox, null).marginLeft
let sliderRightBoxWidth = sliderRightBox.offsetWidth
sliderRightBoxWidth = sliderRightBoxWidth + parseInt(sliderRightBoxLeft)
const slider = sliderRef.value!
const offsetWidth =
slider.offsetWidth - (props.showValue ? sliderRightBoxWidth : 0)
const boxLeft = slider.getBoundingClientRect().left
const value = ((e.x - boxLeft) * (max - min)) / offsetWidth + min
sliderValue.value = _filterValue(value)
}
const _onTrack = (e: TouchtrackEvent) => {
if (!props.disabled) {
return e.detail.state === 'move'
? (_onUserChangedValue({
x: e.detail.x0,
} as MouseEvent),
trigger('changing', e as any, {
value: sliderValue.value,
}),
!1)
: e.detail.state === 'end' &&
trigger('change', e as any, {
value: sliderValue.value,
})
}
}
const uniForm = inject<UniFormCtx>(
uniFormKey,
(false as unknown) as UniFormCtx
)
if (!!uniForm) {
const field = {
reset: () => (sliderValue.value = Number(props.min)),
submit: () => {
const data: [string, any] = ['', null]
if (props.name !== '') {
data[0] = props.name
data[1] = sliderValue.value
}
return data
},
}
uniForm.addField(field)
onBeforeUnmount(() => {
uniForm.removeField(field)
})
}
return { _onClick, _onTrack }
}
var computeController = {
mul: function (arg: number) {
let m = 0
let s1 = this.toString()
let s2 = arg.toString()
try {
// 获得小数位数
m += s1.split('.')[1].length
} catch (e) {}
try {
// 获得小数位数
m += s2.split('.')[1].length
} catch (e) {}
// 转为十进制计算后,要除以两个数的共同小数位数
return (
(Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) /
Math.pow(10, m)
)
},
}
<template>
<uni-slider
ref="uni-slider"
v-bind="$attrs"
@click="_onClick"
>
<div class="uni-slider-wrapper">
<div class="uni-slider-tap-area">
<div
:style="setBgColor"
class="uni-slider-handle-wrapper"
>
<div
ref="uni-slider-handle"
:style="setBlockBg"
class="uni-slider-handle"
/>
<div
:style="setBlockStyle"
class="uni-slider-thumb"
/>
<div
:style="setActiveColor"
class="uni-slider-track"
/>
</div>
</div>
<span
v-show="showValue"
class="uni-slider-value"
>{{ sliderValue }}</span>
</div>
<slot />
</uni-slider>
</template>
<script>
import {
emitter,
listeners
} from '../../mixins'
import touchtrack from '../../mixins/touchtrack'
export default {
name: 'Slider',
mixins: [emitter, listeners, touchtrack],
props: {
name: {
type: String,
default: ''
},
min: {
type: [Number, String],
default: 0
},
max: {
type: [Number, String],
default: 100
},
value: {
type: [Number, String],
default: 0
},
step: {
type: [Number, String],
default: 1
},
disabled: {
type: [Boolean, String],
default: false
},
color: {
type: String,
default: '#e9e9e9'
},
backgroundColor: {
type: String,
default: '#e9e9e9'
},
activeColor: {
type: String,
default: '#007aff'
},
selectedColor: {
type: String,
default: '#007aff'
},
blockColor: {
type: String,
default: '#ffffff'
},
blockSize: {
type: [Number, String],
default: 28
},
showValue: {
type: [Boolean, String],
default: false
}
},
data () {
return {
sliderValue: Number(this.value)
}
},
computed: {
setBlockStyle () {
return {
width: this.blockSize + 'px',
height: this.blockSize + 'px',
marginLeft: -this.blockSize / 2 + 'px',
marginTop: -this.blockSize / 2 + 'px',
left: this._getValueWidth(),
backgroundColor: this.blockColor
}
},
setBgColor () {
return {
backgroundColor: this._getBgColor()
}
},
setBlockBg () {
return {
left: this._getValueWidth()
}
},
setActiveColor () { // 有问题,设置最大值最小值是有问题
return {
backgroundColor: this._getActiveColor(),
width: this._getValueWidth()
}
}
},
watch: {
value (val) {
this.sliderValue = Number(val)
}
},
mounted () {
this.touchtrack(this.$refs['uni-slider-handle'], '_onTrack')
},
created () {
this.$dispatch('Form', 'uni-form-group-update', {
type: 'add',
vm: this
})
},
beforeDestroy () {
this.$dispatch('Form', 'uni-form-group-update', {
type: 'remove',
vm: this
})
},
methods: {
_onUserChangedValue (e) {
const slider = this.$refs['uni-slider']
const offsetWidth = slider.offsetWidth
const boxLeft = slider.getBoundingClientRect().left
const value = (e.x - boxLeft) * (this.max - this.min) / offsetWidth + Number(this.min)
this.sliderValue = this._filterValue(value)
},
_filterValue (e) {
return e < this.min ? this.min : e > this.max ? this.max : Math.round((e - this.min) / this
.step) * this.step + Number(this.min)
},
_getValueWidth () {
return 100 * (this.sliderValue - this.min) / (this.max - this.min) + '%'
},
_getBgColor () {
return this.backgroundColor !== '#e9e9e9' ? this.backgroundColor : (this.color !== '#007aff' ? this.color
: '#007aff')
},
_getActiveColor () {
return this.activeColor !== '#007aff' ? this.activeColor : (this.selectedColor !== '#e9e9e9' ? this.selectedColor
: '#e9e9e9')
},
_onTrack: function (e) {
if (!this.disabled) {
return e.detail.state === 'move' ? (this._onUserChangedValue({
x: e.detail.x0
}), this.$trigger('changing', e, {
value: this.sliderValue
}), !1) : (e.detail.state === 'end' && this.$trigger('change', e, {
value: this.sliderValue
}))
}
},
_onClick ($event) {
if (this.disabled) {
return
}
this._onUserChangedValue($event)
this.$trigger('change', $event, {
value: this.sliderValue
})
},
_resetFormData () {
this.sliderValue = this.min
},
_getFormData () {
const data = {}
if (this.name !== '') {
data.value = this.sliderValue
data.key = this.name
}
return data
}
}
}
</script>
import { Ref, SetupContext } from 'vue'
import { Ref, SetupContext, EmitsOptions } from 'vue'
import { normalizeTarget } from '@dcloudio/uni-shared'
type EventDetail = Record<string, any>
export type CustomEventTrigger = ReturnType<typeof useCustomEvent>
export type EmitEvent<E extends (...args: any) => any> = [Parameters<E>[0]]
export function withWebEvent(fn: Function) {
return ((fn as any).__wwe = true), fn
}
export function useCustomEvent(
export function useCustomEvent<E extends EmitsOptions>(
ref: Ref<HTMLElement | null>,
emit: SetupContext['emit']
emit: SetupContext<E>['emit']
) {
return (name: string, evt: Event, detail?: EventDetail) => {
emit(
......
import { onBeforeUnmount } from 'vue'
const addListenerToElement = function (
element: HTMLElement,
type: string,
callback: Function,
capture?: boolean
) {
// 暂时忽略 capture
element.addEventListener(
type,
($event: Event) => {
if (typeof callback === 'function') {
if (callback($event) === false) {
if (
typeof $event.cancelable !== 'undefined' ? $event.cancelable : true
) {
$event.preventDefault()
}
$event.stopPropagation()
}
}
},
{
passive: false,
}
)
}
type TouchOrMouseEvent = TouchEvent | MouseEvent
type Detail = {
state: any
x0: number
y0: number
dx: number
dy: number
ddx: number
ddy: number
timeStamp: Event['timeStamp']
}
export interface TouchtrackEvent {
target: Event['target']
currentTarget: Event['currentTarget']
preventDefault: Event['preventDefault']
stopPropagation: Event['stopPropagation']
touches: TouchEvent['touches']
changedTouches: TouchEvent['changedTouches']
detail: Detail
}
let __mouseMoveEventListener: (this: Document, ev: MouseEvent) => any
let __mouseUpEventListener: (this: Document, ev: MouseEvent) => any
export function useTouchtrack(
element: HTMLElement,
method: Function,
useCancel?: boolean
) {
onBeforeUnmount(() => {
document.removeEventListener('mousemove', __mouseMoveEventListener)
document.removeEventListener('mouseup', __mouseUpEventListener)
})
let x0 = 0
let y0 = 0
let x1 = 0
let y1 = 0
const fn = function (
$event: TouchOrMouseEvent,
state: any,
x: number,
y: number
) {
if (
method({
target: $event.target,
currentTarget: $event.currentTarget,
preventDefault: $event.preventDefault.bind($event),
stopPropagation: $event.stopPropagation.bind($event),
touches: ($event as TouchEvent).touches,
changedTouches: ($event as TouchEvent).changedTouches,
detail: {
state,
x0: x,
y0: y,
dx: x - x0,
dy: y - y0,
ddx: x - x1,
ddy: y - y1,
timeStamp: $event.timeStamp,
},
}) === false
) {
return false
}
}
let $eventOld: TouchOrMouseEvent | null = null
let hasTouchStart: boolean
let hasMouseDown: boolean
addListenerToElement(element, 'touchstart', function ($event: TouchEvent) {
hasTouchStart = true
if ($event.touches.length === 1 && !$eventOld) {
$eventOld = $event
x0 = x1 = $event.touches[0].pageX
y0 = y1 = $event.touches[0].pageY
return fn($event, 'start', x0, y0)
}
})
addListenerToElement(element, 'mousedown', function ($event: MouseEvent) {
hasMouseDown = true
if (!hasTouchStart && !$eventOld) {
// TODO touches changedTouches
$eventOld = $event
x0 = x1 = $event.pageX
y0 = y1 = $event.pageY
return fn($event, 'start', x0, y0)
}
})
addListenerToElement(element, 'touchmove', function ($event: TouchEvent) {
if ($event.touches.length === 1 && $eventOld) {
const res = fn(
$event,
'move',
$event.touches[0].pageX,
$event.touches[0].pageY
)
x1 = $event.touches[0].pageX
y1 = $event.touches[0].pageY
return res
}
})
const mouseMoveEventListener = (__mouseMoveEventListener = function ($event) {
if (!hasTouchStart && hasMouseDown && $eventOld) {
// TODO target currentTarget touches changedTouches
const res = fn($event, 'move', $event.pageX, $event.pageY)
x1 = $event.pageX
y1 = $event.pageY
return res
}
})
document.addEventListener('mousemove', mouseMoveEventListener)
addListenerToElement(element, 'touchend', function ($event: TouchEvent) {
if ($event.touches.length === 0 && $eventOld) {
hasTouchStart = false
$eventOld = null
return fn(
$event,
'end',
$event.changedTouches[0].pageX,
$event.changedTouches[0].pageY
)
}
})
const mouseUpEventListener = (__mouseUpEventListener = function ($event) {
hasMouseDown = false
if (!hasTouchStart && $eventOld) {
// TODO target currentTarget touches changedTouches
$eventOld = null
return fn($event, 'end', $event.pageX, $event.pageY)
}
})
document.addEventListener('mouseup', mouseUpEventListener)
addListenerToElement(element, 'touchcancel', function ($event: TouchEvent) {
if ($eventOld) {
hasTouchStart = false
const $eventTemp = $eventOld
$eventOld = null
return fn(
$event,
useCancel ? 'cancel' : 'end',
($eventTemp as TouchEvent).touches[0].pageX,
($eventTemp as TouchEvent).touches[0].pageY
)
}
})
}
......@@ -8,18 +8,21 @@ uni-slider[hidden] {
display: none;
}
.uni-slider-wrapper {
uni-slider .uni-slider-wrapper {
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
min-height: 16px;
}
.uni-slider-tap-area {
uni-slider .uni-slider-tap-area {
-webkit-flex: 1;
flex: 1;
padding: 8px 0;
}
.uni-slider-handle-wrapper {
uni-slider .uni-slider-handle-wrapper {
position: relative;
height: 2px;
border-radius: 5px;
......@@ -29,15 +32,15 @@ uni-slider[hidden] {
-webkit-tap-highlight-color: transparent;
}
.uni-slider-track {
uni-slider .uni-slider-track {
height: 100%;
border-radius: 6px;
background-color: #007aff;
transition: background-color 0.3s ease;
}
.uni-slider-handle,
.uni-slider-thumb {
uni-slider .uni-slider-handle,
uni-slider .uni-slider-thumb {
position: absolute;
left: 50%;
top: 50%;
......@@ -46,7 +49,7 @@ uni-slider[hidden] {
transition: border-color 0.3s ease;
}
.uni-slider-handle {
uni-slider .uni-slider-handle {
width: 28px;
height: 28px;
margin-top: -14px;
......@@ -56,12 +59,12 @@ uni-slider[hidden] {
cursor: grab;
}
.uni-slider-thumb {
uni-slider .uni-slider-thumb {
z-index: 2;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}
.uni-slider-step {
uni-slider .uni-slider-step {
position: absolute;
width: 100%;
height: 2px;
......@@ -69,18 +72,18 @@ uni-slider[hidden] {
z-index: 1;
}
.uni-slider-value {
uni-slider .uni-slider-value {
width: 3ch;
color: #888;
font-size: 14px;
margin-left: 1em;
}
.uni-slider-disabled .uni-slider-track {
uni-slider .uni-slider-disabled .uni-slider-track {
background-color: #ccc;
}
.uni-slider-disabled .uni-slider-thumb {
uni-slider .uni-slider-disabled .uni-slider-thumb {
background-color: #fff;
border-color: #ccc;
}
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册