From 9b32a13bef50e774269a6862e8846573bd7ae7c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?=
=?UTF-8?q?=E7=A9=BA=E6=B0=94?= <958414905@qq.com>
Date: Mon, 14 Sep 2020 06:49:30 +0800
Subject: [PATCH] feat(2.x/slider): add dual thumb mode for slider (#7176)
* feat(slider): add dual thumb mode for slider
* fix: test error
---
src/slider/README.md | 61 +++++++-
src/slider/README.zh-CN.md | 63 +++++++-
src/slider/demo/index.vue | 48 ++++--
src/slider/index.js | 148 ++++++++++++++----
src/slider/index.less | 27 +++-
.../test/__snapshots__/demo.spec.js.snap | 26 +++
6 files changed, 315 insertions(+), 58 deletions(-)
diff --git a/src/slider/README.md b/src/slider/README.md
index 0a342c50d..594fbac21 100644
--- a/src/slider/README.md
+++ b/src/slider/README.md
@@ -34,6 +34,32 @@ export default {
};
```
+### Dual thumb
+
+Add `range` attribute to open dual thumb mode
+
+```html
+
+```
+
+```js
+import { Toast } from 'vant';
+
+export default {
+ data() {
+ return {
+ // value must be an Array
+ value: [10, 50],
+ };
+ },
+ methods: {
+ onChange(value) {
+ Toast('current value:' + value);
+ },
+ },
+};
+```
+
### Range
```html
@@ -63,9 +89,7 @@ export default {
```html
-
- {{ value }}
-
+ {{ value }}
@@ -90,15 +114,44 @@ export default {
```
+### Vertical, Dual thumb mode
+
+Add `range` and `vertical` attributes at the same time, and make sure that the value of `value` is an array
+
+```html
+
+
+
+```
+
+```js
+import { Toast } from 'vant';
+
+export default {
+ data() {
+ return {
+ // value must be an array
+ value: [10, 50],
+ };
+ },
+ methods: {
+ onChange(value) {
+ Toast('Current value:' + value);
+ },
+ },
+};
+```
+
## API
### Props
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
-| value | Current value | _number_ | `0` |
+| value | Current value | _number \| array_ | `0` |
| max | Max value | _number \| string_ | `100` |
| min | Min value | _number \| string_ | `0` |
+| range | Dual thumb mode | _boolean_ | `false` |
| step | Step size | _number \| string_ | `1` |
| bar-height | Height of bar | _number \| string_ | `2px` |
| button-size `v2.4.5` | Button size | _number \| string_ | `24px` |
diff --git a/src/slider/README.zh-CN.md b/src/slider/README.zh-CN.md
index 99c43e4eb..bfbec503d 100644
--- a/src/slider/README.zh-CN.md
+++ b/src/slider/README.zh-CN.md
@@ -34,6 +34,32 @@ export default {
};
```
+### 双滑块
+
+添加`range`属性就可以开启双滑块模式,确保`value`的值是一个数组
+
+```html
+
+```
+
+```js
+import { Toast } from 'vant';
+
+export default {
+ data() {
+ return {
+ // 双滑块模式时,值必须是数组
+ value: [10, 50],
+ };
+ },
+ methods: {
+ onChange(value) {
+ Toast('当前值:' + value);
+ },
+ },
+};
+```
+
### 指定选择范围
```html
@@ -63,9 +89,7 @@ export default {
```html
-
- {{ value }}
-
+ {{ value }}
@@ -87,20 +111,49 @@ export default {
Slider 垂直展示时,高度为 100% 父元素高度
```html
-
+
```
+### 垂直方向,双滑块
+
+同时添加`range`和`vertical`属性,并确保`value`的值是一个数组
+
+```html
+
+
+
+```
+
+```js
+import { Toast } from 'vant';
+
+export default {
+ data() {
+ return {
+ // 双滑块模式时,值必须是数组
+ value: [10, 50],
+ };
+ },
+ methods: {
+ onChange(value) {
+ Toast('当前值:' + value);
+ },
+ },
+};
+```
+
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
-| value | 当前进度百分比 | _number_ | `0` |
+| value | 当前进度百分比 | _number \| array_ | `0` |
| max | 最大值 | _number \| string_ | `100` |
| min | 最小值 | _number \| string_ | `0` |
+| range | 双滑块模式 | _boolean_ | `false` |
| step | 步长 | _number \| string_ | `1` |
| bar-height | 进度条高度,默认单位为`px` | _number \| string_ | `2px` |
| button-size `v2.4.5` | 滑块按钮大小,默认单位为`px` | _number \| string_ | `24px` |
diff --git a/src/slider/demo/index.vue b/src/slider/demo/index.vue
index a510eefc6..98c460bf6 100644
--- a/src/slider/demo/index.vue
+++ b/src/slider/demo/index.vue
@@ -5,20 +5,24 @@
-
+
-
+
-
+
+
+
+
+
-
+
- {{ value6 }}
+ {{ value7 }}
-
+
+
+
+
+
+
+
@@ -46,38 +56,42 @@ export default {
i18n: {
'zh-CN': {
title1: '基础用法',
- title2: '指定选择范围',
- title3: '禁用',
- title4: '指定步长',
+ title2: '双滑块',
+ title3: '指定选择范围',
+ title4: '禁用',
+ title5: '指定步长',
customStyle: '自定义样式',
customButton: '自定义按钮',
text: '当前值:',
vertical: '垂直方向',
+ vertical2: '垂直方向,双滑块',
},
'en-US': {
title1: 'Basic Usage',
- title2: 'Range',
- title3: 'Disabled',
- title4: 'Step size',
+ title2: 'Dual thumb mode',
+ title3: 'Range',
+ title4: 'Disabled',
+ title5: 'Step size',
customStyle: 'Custom Style',
customButton: 'Custom Button',
text: 'Current value: ',
vertical: 'Vertical',
+ vertical2: 'Vertical, Dual thumb mode',
},
},
-
data() {
return {
value1: 50,
- value2: 0,
- value3: 50,
+ value2: [20, 60],
+ value3: 0,
value4: 50,
value5: 50,
value6: 50,
value7: 50,
+ value8: 50,
+ value9: [20, 60],
};
},
-
methods: {
onChange(value) {
this.$toast(this.t('text') + value);
diff --git a/src/slider/index.js b/src/slider/index.js
index d04f19605..fb33c087f 100644
--- a/src/slider/index.js
+++ b/src/slider/index.js
@@ -1,16 +1,22 @@
import { createNamespace, addUnit } from '../utils';
+import { deepClone } from '../utils/deep-clone';
import { preventDefault } from '../utils/dom/event';
import { TouchMixin } from '../mixins/touch';
import { FieldMixin } from '../mixins/field';
const [createComponent, bem] = createNamespace('slider');
+const isSameValue = (newValue, oldValue) => {
+ return JSON.stringify(newValue) === JSON.stringify(oldValue);
+};
+
export default createComponent({
mixins: [TouchMixin, FieldMixin],
props: {
disabled: Boolean,
vertical: Boolean,
+ range: Boolean,
barHeight: [Number, String],
buttonSize: [Number, String],
activeColor: String,
@@ -28,7 +34,7 @@ export default createComponent({
default: 1,
},
value: {
- type: Number,
+ type: [Number, Array],
default: 0,
},
},
@@ -40,7 +46,7 @@ export default createComponent({
},
computed: {
- range() {
+ scope() {
return this.max - this.min;
},
@@ -61,7 +67,12 @@ export default createComponent({
},
mounted() {
- this.bindTouchEvent(this.$refs.wrapper);
+ if (this.range) {
+ this.bindTouchEvent(this.$refs.wrapper0);
+ this.bindTouchEvent(this.$refs.wrapper1);
+ } else {
+ this.bindTouchEvent(this.$refs.wrapper);
+ }
},
methods: {
@@ -71,7 +82,12 @@ export default createComponent({
}
this.touchStart(event);
- this.startValue = this.format(this.value);
+ this.currentValue = this.value;
+ if (this.range) {
+ this.startValue = this.value.map(this.format);
+ } else {
+ this.startValue = this.format(this.value);
+ }
this.dragStatus = 'start';
},
@@ -91,10 +107,14 @@ export default createComponent({
const rect = this.$el.getBoundingClientRect();
const delta = this.vertical ? this.deltaY : this.deltaX;
const total = this.vertical ? rect.height : rect.width;
- const diff = (delta / total) * this.range;
+ const diff = (delta / total) * this.scope;
- this.newValue = this.startValue + diff;
- this.updateValue(this.newValue);
+ if (this.range) {
+ this.currentValue[this.index] = this.startValue[this.index] + diff;
+ } else {
+ this.currentValue = this.startValue + diff;
+ }
+ this.updateValue(this.currentValue);
},
onTouchEnd() {
@@ -103,7 +123,7 @@ export default createComponent({
}
if (this.dragStatus === 'draging') {
- this.updateValue(this.newValue, true);
+ this.updateValue(this.currentValue, true);
this.$emit('drag-end');
}
@@ -120,20 +140,44 @@ export default createComponent({
? event.clientY - rect.top
: event.clientX - rect.left;
const total = this.vertical ? rect.height : rect.width;
- const value = +this.min + (delta / total) * this.range;
+ let value = +this.min + (delta / total) * this.scope;
+
+ if (this.range) {
+ let [left, right] = this.value;
+ const middle = (left + right) / 2;
+ if (value <= middle) {
+ left = value;
+ } else {
+ right = value;
+ }
+ value = [left, right];
+ }
this.startValue = this.value;
this.updateValue(value, true);
},
- updateValue(value, end) {
- value = this.format(value);
+ // 处理两个滑块重叠之后的情况
+ handleOverlap(value) {
+ if (value[0] > value[1]) {
+ value = deepClone(value);
+ return value.reverse();
+ }
+ return value;
+ },
- if (value !== this.value) {
+ updateValue(value, end) {
+ if (this.range) {
+ value = this.handleOverlap(value).map(this.format);
+ } else {
+ value = this.format(value);
+ }
+
+ if (!isSameValue(value, this.value)) {
this.$emit('input', value);
}
- if (end && value !== this.startValue) {
+ if (end && !isSameValue(value, this.startValue)) {
this.$emit('change', value);
}
},
@@ -156,8 +200,28 @@ export default createComponent({
[crossAxis]: addUnit(this.barHeight),
};
+ // 计算选中条的长度百分比
+ const calcMainAxis = () => {
+ const { value, min, range, scope } = this;
+ if (range) {
+ return `${((value[1] - value[0]) * 100) / scope}%`;
+ }
+ return `${((value - min) * 100) / scope}%`;
+ };
+
+ // 计算选中条的开始位置的偏移量
+ const calcOffset = () => {
+ const { value, min, range, scope } = this;
+ if (range) {
+ return `${((value[0] - min) * 100) / scope}%`;
+ }
+ return null;
+ };
+
const barStyle = {
- [mainAxis]: `${((this.value - this.min) * 100) / this.range}%`,
+ [mainAxis]: calcMainAxis(),
+ left: this.vertical ? null : calcOffset(),
+ top: this.vertical ? calcOffset() : null,
background: this.activeColor,
};
@@ -165,6 +229,47 @@ export default createComponent({
barStyle.transition = 'none';
}
+ const renderButton = (i) => {
+ const map = ['left', 'right'];
+ const isNumber = typeof i === 'number';
+ const getClassName = () => {
+ if (isNumber) {
+ return `button-wrapper-${map[i]}`;
+ }
+ return `button-wrapper`;
+ };
+ const getRefName = () => {
+ if (isNumber) {
+ return `wrapper${i}`;
+ }
+ return `wrapper`;
+ };
+
+ return (
+ {
+ if (isNumber) {
+ // 保存当前按钮的索引
+ this.index = i;
+ }
+ }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ {this.slots('button') || (
+
+ )}
+
+ );
+ };
+
return (
-
+ {this.range ? [renderButton(0), renderButton(1)] : renderButton()}
);
diff --git a/src/slider/index.less b/src/slider/index.less
index 28d51adc8..881b5eb05 100644
--- a/src/slider/index.less
+++ b/src/slider/index.less
@@ -24,7 +24,7 @@
height: 100%;
background-color: @slider-active-background-color;
border-radius: inherit;
- transition: width @animation-duration-fast, height @animation-duration-fast;
+ transition: all @animation-duration-fast;
}
&__button {
@@ -34,20 +34,31 @@
border-radius: @slider-button-border-radius;
box-shadow: @slider-button-box-shadow;
- &-wrapper {
+ &-wrapper,
+ &-wrapper-right {
position: absolute;
top: 50%;
right: 0;
transform: translate3d(50%, -50%, 0);
cursor: grab;
}
+
+ &-wrapper-left {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translate3d(-50%, -50%, 0);
+ cursor: grab;
+ }
}
&--disabled {
cursor: not-allowed;
opacity: @slider-disabled-opacity;
- .van-slider__button-wrapper {
+ .van-slider__button-wrapper,
+ .van-slider__button-wrapper-left,
+ .van-slider__button-wrapper-right {
cursor: not-allowed;
}
}
@@ -57,13 +68,21 @@
width: @slider-bar-height;
height: 100%;
- .van-slider__button-wrapper {
+ .van-slider__button-wrapper,
+ .van-slider__button-wrapper-right {
top: auto;
right: 50%;
bottom: 0;
transform: translate3d(50%, 50%, 0);
}
+ .van-slider__button-wrapper-left {
+ top: 0;
+ right: 50%;
+ left: auto;
+ transform: translate3d(50%, -50%, 0);
+ }
+
// use pseudo element to expand click area
&::before {
top: 0;
diff --git a/src/slider/test/__snapshots__/demo.spec.js.snap b/src/slider/test/__snapshots__/demo.spec.js.snap
index e1a65f6e5..1e4852582 100644
--- a/src/slider/test/__snapshots__/demo.spec.js.snap
+++ b/src/slider/test/__snapshots__/demo.spec.js.snap
@@ -11,6 +11,18 @@ exports[`renders demo correctly 1`] = `
+
@@ -67,5 +79,19 @@ exports[`renders demo correctly 1`] = `
+
`;