feat(slider): add new slider component

This commit is contained in:
moyus 2018-11-11 16:18:01 +08:00
parent 3bfe6684d6
commit 3ad8a6a329
17 changed files with 612 additions and 2 deletions

View File

@ -218,6 +218,11 @@ input-item-placeholder = color-text-placeholder
input-item-placeholder-highlight = color-primary
input-item-icon = color-text-placeholder // delete icon
// slider
slider-bg-base = color-bg-base
slider-bg-tap = color-primary
slider-handle-bg = color-bg-inverse
// landscape
landscape-width = 540px
landscape-radius = radius-normal

View File

@ -57,6 +57,7 @@ import WaterMark from './water-mark'
import Transition from './transition'
import DetailItem from './detail-item'
import Overlay from './overlay'
import Slider from './slider'
/* @init<%import ${componentNameUpper} from './${componentName}'%> */
// 全量引入提醒
@ -124,6 +125,7 @@ export const components = {
Transition,
DetailItem,
Overlay,
Slider,
/* @init<%${componentNameUpper},%> */
}
@ -208,6 +210,7 @@ export {
Transition,
DetailItem,
Overlay,
Slider,
/* @init<%${componentNameUpper},%> */
}

View File

@ -0,0 +1,28 @@
---
title: Slider
preview: https://didi.github.io/mand-mobile/examples/#/slider
---
### Import
```javascript
import { Slider } from 'mand-mobile'
Vue.component(Slider.name, Slider)
```
### Code Examples
<!-- DEMO -->
### API
#### Slider Props
|Props | Description | Type | Default | Note|
|----|-----|------|------|------|
|v-model|the value of slider, when <code>range</code> is false, use <code>number</code>, otherwise, use <code>[number, number]</code>|number | number[]|`0`|-|
|disabled|whether to disable slider|Boolean|`false`|-|
|min|the minimum value the slider can slide from|number|`0`|-|
|max|the maximum value the slider can slide to|number|`100`|-|
|step|the granularity the slider can step through|number|`1`|-|
|range|dual thumb mode|Boolean|`false`|-|
|format|slider will pass its value to <code>format</code>, and display its value in tooltip|Function|`(val) => {return val}`|-|

View File

@ -0,0 +1,28 @@
---
title: Slider 滑块
preview: https://didi.github.io/mand-mobile/examples/#/slider
---
### 引入
```javascript
import { Slider } from '@didi/mand-mobile'
Vue.component(Slider.name, Slider)
```
### 代码演示
<!-- DEMO -->
### API
#### Slider Props
|属性 | 说明 | 类型 | 默认值 | 备注|
|----|-----|------|------|------|
|v-model|双向绑定的值, 当开启<code>range</code>时, 其值为数组形式</code>|number | number[]|`0`|-|
|disabled|是否禁用滑块|Boolean|`false`|-|
|min|可拖动的最小值|number|`0`|-|
|max|可拖动的最大值|number|`100`|-|
|step|步长|number|`1`|-|
|range|是否启动双向拖动|Boolean|`false`|-|
|format|显示文本的格式化函数|Function|`(val) => {return val}`|-|

View File

@ -0,0 +1,7 @@
export default {
'name': 'slider',
'text': '滑块',
'category': 'form',
'description': '',
'author': 'moyu <moyuboy@gmail.com>'
}

View File

@ -0,0 +1,27 @@
<template>
<div class="md-example-child md-example-child-slider">
<md-slider v-model="quantity"></md-slider>
</div>
</template>
<script> import {Slider} from 'mand-mobile'
export default {
name: 'slider-demo',
/* DELETE */
title: '基本',
titleEnUS: 'Basic',
/* DELETE */
components: {
[Slider.name]: Slider,
},
data() {
return {
quantity: 25,
}
},
}
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="md-example-child md-example-child-slider">
<md-slider v-model="quantity" disabled></md-slider>
</div>
</template>
<script> import {Slider} from 'mand-mobile'
export default {
name: 'slider-demo',
/* DELETE */
title: '禁用',
titleEnUS: 'Disabled',
/* DELETE */
components: {
[Slider.name]: Slider,
},
data() {
return {
quantity: 50,
}
},
}
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="md-example-child md-example-child-slider">
<md-slider v-model="quantity" :format="format"></md-slider>
</div>
</template>
<script> import {Slider} from 'mand-mobile'
export default {
name: 'slider-demo',
/* DELETE */
title: '格式化',
titleEnUS: 'Format',
/* DELETE */
components: {
[Slider.name]: Slider,
},
data() {
return {
quantity: 50,
}
},
methods: {
format(val) {
return '¥' + val
},
},
}
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="md-example-child md-example-child-slider">
<md-slider v-model="range" range></md-slider>
</div>
</template>
<script> import {Slider} from 'mand-mobile'
export default {
name: 'slider-demo',
/* DELETE */
title: '范围',
titleEnUS: 'Range',
/* DELETE */
components: {
[Slider.name]: Slider,
},
data() {
return {
range: [25, 50],
}
},
}
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="md-example-child md-example-child-slider">
<md-slider v-model="bucket" :step="10"></md-slider>
</div>
</template>
<script> import {Slider} from 'mand-mobile'
export default {
name: 'slider-demo',
/* DELETE */
title: '步长',
titleEnUS: 'Steps',
/* DELETE */
components: {
[Slider.name]: Slider,
},
data() {
return {
bucket: 10,
}
},
}
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="md-example-child md-example-child-slider">
<md-slider v-model="range" :min="15" :max="80" range></md-slider>
</div>
</template>
<script> import {Slider} from 'mand-mobile'
export default {
name: 'slider-demo',
/* DELETE */
title: '边界值',
titleEnUS: 'Min & Max',
/* DELETE */
components: {
[Slider.name]: Slider,
},
data() {
return {
range: [25, 50],
}
},
}
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="md-example slider">
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
<div class="md-example-title" v-html="demo.title"></div>
<div class="md-example-describe" v-html="demo.describe"></div>
<div class="md-example-content">
<component :is="demo"></component>
</div>
</section>
</div>
</template>
<script> import createDemoModule from '../../../examples/create-demo-module'
import Demo0 from './cases/demo0'
import Demo1 from './cases/demo1'
import Demo2 from './cases/demo2'
import Demo3 from './cases/demo3'
import Demo4 from './cases/demo4'
import Demo5 from './cases/demo5'
export default createDemoModule('slider', [Demo0, Demo1, Demo2, Demo3, Demo4, Demo5])
</script>
<style lang="stylus">
.md-example.slider
position relative
</style>

324
components/slider/index.vue Normal file
View File

@ -0,0 +1,324 @@
<template>
<div class="md-slider" :class="{'is-disabled': disabled}">
<template v-if="range">
<div class="md-slider-bar" :style="barStyle"></div>
<div class="md-slider-handle is-lower"
:data-hint="format(values[0])"
:class="{
'is-active': isDragging && !isDragingUpper
}"
:style="{'left': lowerHandlePosition + '%'}">
<span
@mousedown="$_startLowerDrag"
@touchstart="$_startLowerDrag"
></span>
</div>
<div class="md-slider-handle is-higher"
:data-hint="format(values[1])"
:class="{
'is-active': isDragging && isDragingUpper
}"
:style="{'left': upperHandlePosition + '%'}">
<span
@mousedown="$_startUpperDrag"
@touchstart="$_startUpperDrag"
></span>
</div>
</template>
<template v-else>
<div class="md-slider-bar" :style="barStyle"></div>
<div class="md-slider-handle"
:data-hint="format(values[0])"
:class="{
'is-active': isDragging
}"
:style="{'left': lowerHandlePosition + '%'}">
<span
@mousedown="$_startLowerDrag"
@touchstart="$_startLowerDrag"
></span>
</div>
</template>
</div>
</template>
<script> export default {
name: 'md-slider',
props: {
value: {
type: [Array, Number],
default: 0,
},
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 100,
},
step: {
type: Number,
default: 1,
},
range: {
type: Boolean,
default: false,
},
format: {
type: Function,
default(val) {
return val
},
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
isDragging: false,
isDragingUpper: false,
values: [this.min, this.max],
startDragMousePos: 0,
startVal: 0,
}
},
watch: {
value: {
immediate: true,
handler(val) {
if (
(Array.isArray(val) && (val[0] !== this.values[0] || val[1] !== this.values[1])) ||
val !== this.values[0]
) {
this.$_updateValue(val)
}
},
},
disabled(newVal) {
if (!newVal) {
this.$_stopDrag()
}
},
},
computed: {
lowerHandlePosition() {
return (this.values[0] - this.min) / (this.max - this.min) * 100
},
upperHandlePosition() {
return (this.values[1] - this.min) / (this.max - this.min) * 100
},
barStyle() {
if (this.range) {
return {
width: (this.values[1] - this.values[0]) / (this.max - this.min) * 100 + '%',
left: this.lowerHandlePosition + '%',
}
} else {
return {
width: this.values[0] / (this.max - this.min) * 100 + '%',
}
}
},
},
methods: {
$_updateValue(newVal) {
let newValues = []
if (Array.isArray(newVal)) {
newValues = [newVal[0], newVal[1]]
} else {
newValues[0] = newVal
}
if (typeof newValues[0] !== 'number') {
newValues[0] = this.values[0]
} else {
newValues[0] = Math.round((newValues[0] - this.min) / this.step) * this.step + this.min
}
if (typeof newValues[1] !== 'number') {
newValues[1] = this.values[1]
} else {
newValues[1] = Math.round((newValues[1] - this.min) / this.step) * this.step + this.min
}
// value boundary adjust
if (newValues[0] < this.min) {
newValues[0] = this.min
}
if (newValues[1] > this.max) {
newValues[1] = this.max
}
if (newValues[0] > newValues[1]) {
if (newValues[0] === this.values[0]) {
newValues[1] = newValues[0]
} else {
newValues[0] = newValues[1]
}
}
if (this.values[0] === newValues[0] && this.values[1] === newValues[1]) {
return
}
this.values = newValues
if (this.range) {
this.$emit('input', this.values)
} else {
this.$emit('input', this.values[0])
}
},
$_startLowerDrag(e) {
if (this.disabled) {
return
}
e.preventDefault()
e.stopPropagation()
e = e.changedTouches ? e.changedTouches[0] : e
this.startDragMousePos = e.pageX
this.startVal = this.values[0]
this.isDragingUpper = false
this.isDragging = true
window.addEventListener('mousemove', this.$_onDrag)
window.addEventListener('touchmove', this.$_onDrag)
window.addEventListener('mouseup', this.$_onUp)
window.addEventListener('touchend', this.$_onUp)
},
$_startUpperDrag(e) {
if (this.disabled) {
return
}
e.preventDefault()
e.stopPropagation()
e = e.changedTouches ? e.changedTouches[0] : e
this.startDragMousePos = e.pageX
this.startVal = this.values[1]
this.isDragingUpper = true
this.isDragging = true
window.addEventListener('mousemove', this.$_onDrag)
window.addEventListener('touchmove', this.$_onDrag)
window.addEventListener('mouseup', this.$_onUp)
window.addEventListener('touchend', this.$_onUp)
},
$_onDrag(e) {
if (this.disabled) {
return
}
e.preventDefault()
e.stopPropagation()
if (!this.isDragging) {
return
}
e = e.changedTouches ? e.changedTouches[0] : e
window.requestAnimationFrame(() => {
let diff = (e.pageX - this.startDragMousePos) / this.$el.offsetWidth * (this.max - this.min)
let nextVal = this.startVal + diff
if (this.isDragging) {
if (this.isDragingUpper) {
this.$_updateValue([null, nextVal])
} else {
this.$_updateValue([nextVal, null])
}
}
})
},
$_onUp(e) {
e.preventDefault()
e.stopPropagation()
this.$_stopDrag()
},
$_stopDrag() {
this.isDragging = false
this.isDragingUpper = false
window.removeEventListener('mousemove', this.$_onDrag)
window.removeEventListener('touchmove', this.$_onDrag)
window.removeEventListener('mouseup', this.$_onUp)
window.removeEventListener('touchend', this.$_onUp)
},
},
}
</script>
<style lang="stylus">
.md-slider
position relative
width 100%
height 60px
&::before
content ''
position absolute
top 28px
left 0
right 0
height 4px
border-radius 2px
background-color slider-bg-base
&.is-disabled
.md-slider-bar
opacity 0.35
.md-slider-handle span
cursor: not-allowed
.md-slider-bar
position absolute
left 0
top 28px
height 4px
background-color slider-bg-tap
border-radius 2px
z-index 5
.md-slider-handle
position absolute
top 10px
left 0
margin-left -20px
z-index 15
overflow visible
&::after
content attr(data-hint)
color tip-color
position absolute
pointer-events none
opacity 0
visibility hidden
z-index 15
font-size font-minor-normal
line-height 1.25
padding 8px 16px
border-radius radius-normal
background-color tip-fill
white-space nowrap
left 50%
bottom 100%
margin-bottom 20px
transform translateX(-50%)
&:hover::after,
&:active::after
opacity 1
visibility visible
&.is-higher
z-index 20
&.is-active span
transform scale(1.3)
span
display block
cursor pointer
width 40px
height 40px
background-color slider-handle-bg
border-radius 50%
box-shadow 0 1px 2px rgba(0, 0, 0, 0.2)
transition transform 200ms
</style>

View File

@ -0,0 +1,16 @@
import Slider from '../index'
import {mount} from 'avoriaz'
describe('Slider', () => {
let wrapper
afterEach(() => {
wrapper && wrapper.destroy()
})
it('create a simple slider', () => {
wrapper = mount(Slider)
expect(wrapper.hasClass('md-slider')).to.be.true
})
})

View File

@ -227,6 +227,11 @@
"path": "/radio",
"icon": "radio",
"text": "单选框"
}, {
"name": "Slider",
"path": "/slider",
"icon": "slider",
"text": "滑块"
}, {
"name": "Switch",
"path": "/switch",

View File

@ -45,4 +45,4 @@ export {default as WaterMark} from '../components/water-mark/demo'
export {default as Transition} from '../components/transition/demo'
export {default as DetailItem} from '../components/detail-item/demo'
export {default as Overlay} from '../components/overlay/demo'
/* @init<%export {default as ${componentNameUpper}} from '../components/${componentName}/demo'%> */
export {default as Slider} from '../components/slider/demo' /* @init<%export {default as ${componentNameUpper}} from '../components/${componentName}/demo'%> */

View File

@ -45,4 +45,4 @@ export const Bill = r => require.ensure([], () => r(require('../components/bill
export const WaterMark = r => require.ensure([], () => r(require('../components/water-mark/demo')), 'water-mark')
export const Transition = r => require.ensure([], () => r(require('../components/transition/demo')), 'transition')
export const DetailItem = r => require.ensure([], () => r(require('../components/detail-item/demo')), 'detail-item')
export const Overlay = r => require.ensure([], () => r(require('../components/overlay/demo')), 'overlay') /* @init<%export const ${componentNameUpper} = r => require.ensure([], () => r(require('../components/${componentName}/demo')), '${componentName}')%> */
export const Overlay = r => require.ensure([], () => r(require('../components/overlay/demo')), 'overlay') export const Slider = r => require.ensure([], () => r(require('../components/slider/demo')), 'slider') /* @init<%export const ${componentNameUpper} = r => require.ensure([], () => r(require('../components/${componentName}/demo')), '${componentName}')%> */