mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
Feat/input range (#3405)
* feat: inputRange滑块组件 * feat: inputRange滑块组件 * feat: inputRange 单侧 * feat: inputRange 文案 * feat: inputRange 格式 * feat: inputRange CR问题修复 * feat: inputRange CR问题修复 Co-authored-by: huangying11 <huangying11@baidu.com>
This commit is contained in:
parent
e5b44cef2d
commit
d7738f3f88
File diff suppressed because it is too large
Load Diff
@ -4,20 +4,16 @@ import '../../../src/themes/default';
|
||||
import {render as amisRender} from '../../../src/index';
|
||||
import {makeEnv} from '../../helper';
|
||||
|
||||
test('Renderer:number', async () => {
|
||||
const {container, getByRole} = render(
|
||||
test('Renderer:range', async () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
api: '/api/xxx',
|
||||
controls: [
|
||||
{
|
||||
type: 'range',
|
||||
name: 'a',
|
||||
label: 'range',
|
||||
min: 0,
|
||||
max: 20,
|
||||
step: 2,
|
||||
type: 'input-range',
|
||||
name: 'range',
|
||||
value: 10,
|
||||
showInput: true
|
||||
}
|
||||
@ -30,19 +26,21 @@ test('Renderer:number', async () => {
|
||||
)
|
||||
);
|
||||
|
||||
fireEvent.mouseDown(getByRole('slider'));
|
||||
fireEvent.mouseMove(getByRole('slider'), {
|
||||
const slider = container.querySelector('.cxd-InputRange-handle');
|
||||
fireEvent.mouseDown(slider);
|
||||
fireEvent.mouseMove(slider, {
|
||||
clientX: 400,
|
||||
clientY: 400
|
||||
});
|
||||
fireEvent.mouseUp(getByRole('slider'));
|
||||
fireEvent.mouseUp(slider);
|
||||
|
||||
const input = container.querySelector('input[name=a]');
|
||||
const input = container.querySelector('.cxd-InputRange-input input');
|
||||
fireEvent.change(input!, {
|
||||
target: {
|
||||
value: '7'
|
||||
}
|
||||
});
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@ -54,17 +52,10 @@ test('Renderer:range:multiple', async () => {
|
||||
api: '/api/xxx',
|
||||
controls: [
|
||||
{
|
||||
type: 'range',
|
||||
name: 'a',
|
||||
label: 'range',
|
||||
min: 0,
|
||||
max: 20,
|
||||
step: 2,
|
||||
value: '10,15',
|
||||
type: 'input-range',
|
||||
name: 'range',
|
||||
multiple: true,
|
||||
joinValues: true,
|
||||
delimiter: ',',
|
||||
clearable: true,
|
||||
value: [10, 20],
|
||||
showInput: true
|
||||
}
|
||||
],
|
||||
@ -76,7 +67,7 @@ test('Renderer:range:multiple', async () => {
|
||||
)
|
||||
);
|
||||
|
||||
const inputs = container.querySelectorAll('input[name=a]');
|
||||
const inputs = container.querySelectorAll('.cxd-InputRange-input input');
|
||||
fireEvent.change(inputs[0], {
|
||||
target: {
|
||||
value: '7'
|
||||
@ -94,3 +85,86 @@ test('Renderer:range:multiple', async () => {
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renderer:range:showSteps', async () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
api: '/api/xxx',
|
||||
controls: [
|
||||
{
|
||||
type: 'input-range',
|
||||
name: 'range',
|
||||
max: 10,
|
||||
showSteps: true,
|
||||
showInput: true
|
||||
}
|
||||
],
|
||||
title: 'The form',
|
||||
actions: []
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renderer:range:marks', async () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
api: '/api/xxx',
|
||||
controls: [
|
||||
{
|
||||
type: 'input-range',
|
||||
name: 'range',
|
||||
parts: 5,
|
||||
marks: {
|
||||
'0': '0',
|
||||
'20%': '20Mbps',
|
||||
'40%': '40Mbps',
|
||||
'60%': '60Mbps',
|
||||
'80%': '80Mbps',
|
||||
'100': '100'
|
||||
}
|
||||
}
|
||||
],
|
||||
title: 'The form',
|
||||
actions: []
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renderer:range:tooltipVisible', async () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
api: '/api/xxx',
|
||||
controls: [
|
||||
{
|
||||
type: 'input-range',
|
||||
name: 'range',
|
||||
tooltipVisible: true,
|
||||
tooltipPlacement: 'right'
|
||||
}
|
||||
],
|
||||
title: 'The form',
|
||||
actions: []
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
@ -17,12 +17,14 @@ order: 38
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"name": "range",
|
||||
"label": "range"
|
||||
"label": '滑块',
|
||||
"name": 'range',
|
||||
"value": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -42,7 +44,11 @@ order: 38
|
||||
"type": "input-range",
|
||||
"name": "range",
|
||||
"label": "range",
|
||||
"multiple": true
|
||||
"multiple": true,
|
||||
"value": {
|
||||
"min": 10,
|
||||
"max": 50
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -55,6 +61,7 @@ order: 38
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
@ -69,19 +76,205 @@ order: 38
|
||||
}
|
||||
```
|
||||
|
||||
## 禁用
|
||||
|
||||
使用`disabled`禁用滑块。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"label": '滑块',
|
||||
"name": 'range',
|
||||
"value": 10,
|
||||
"disabled": true,
|
||||
"showInput": true,
|
||||
"clearable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 显示步长
|
||||
|
||||
开启`showSteps`可显示每个`step`长度
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"label": '滑块',
|
||||
"name": 'range',
|
||||
"max": 10,
|
||||
"showSteps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 分割块数
|
||||
|
||||
通过`parts`可对整个滑动条平均分为`parts`块。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"label": '滑块',
|
||||
"name": 'range',
|
||||
"showSteps": true,
|
||||
"parts": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 刻度标记
|
||||
|
||||
通过`marks`可对刻度进行自定义。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"label": '滑块',
|
||||
"name": 'range',
|
||||
"parts": 5,
|
||||
"marks": {
|
||||
'0': '0',
|
||||
'20%': '20Mbps',
|
||||
'40%': '40Mbps',
|
||||
'60%': '60Mbps',
|
||||
'80%': '80Mbps',
|
||||
'100': '100'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 输入框
|
||||
|
||||
通过开启`showInput`会展示输入框,输入框数据于滑块数据同步。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"name": "range",
|
||||
"label": "range",
|
||||
"value": 20,
|
||||
"showInput": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"name": "range",
|
||||
"label": "range",
|
||||
"multiple": true,
|
||||
"value": [10, 20],
|
||||
"showInput": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 清除输入
|
||||
|
||||
在打开`showInput`输入框的前提下,开启`clearable`可对数据进行清除。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"name": "range",
|
||||
"label": "range",
|
||||
"value": 20,
|
||||
"showInput": true,
|
||||
"clearable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 显示标签
|
||||
|
||||
标签默认在 hover 和拖拽过程中展示,通过`tooltipVisible`或者`tipFormatter`可指定标签是否展示。标签默认展示在滑块上方,通过`tooltipPlacement`可指定标签展示的位置。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-range",
|
||||
"name": "range",
|
||||
"label": "range",
|
||||
"value": 20,
|
||||
"tooltipVisible": true,
|
||||
"tooltipPlacement": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ---------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| className | `string` | | css 类名 |
|
||||
| min | `number` | | 最小值 |
|
||||
| max | `number` | | 最大值 |
|
||||
| step | `number` | | 步长 |
|
||||
| multiple | `boolean` | `false` | 支持选择范围 |
|
||||
| joinValuse | `boolean` | `true` | 默认为 `true`,选择的 `value` 会通过 `delimiter` 连接起来,否则直接将以`{min: 1, max: 100}`的形式提交,开启`multiple`时有效 |
|
||||
| delimiter | `string` | `,` | 分隔符 |
|
||||
| unit | `string` | | 单位 |
|
||||
| clearable | `boolean` | | 是否可清除 |
|
||||
| showInput | `boolean` | | 是否显示输入框 |
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ---------------- | ------------------------------------------------------------ | ------- | ------------------------------------------------------------ |
|
||||
| className | `string` | | css 类名 |
|
||||
| value | `number` or `string` or `{min: number, max: number}` or `[number, number]` | | |
|
||||
| min | `number` | `0` | 最小值 |
|
||||
| max | `number` | `100` | 最大值 |
|
||||
| disabled | `boolean` | `false` | 是否禁用 |
|
||||
| step | `number` | `1` | 步长 |
|
||||
| showSteps | `boolean` | `false` | 是否显示步长 |
|
||||
| parts | `number` | `1` | 分割的块数 |
|
||||
| marks | <code>{ [number | string]: ReactNode }</code> or <code>{ [number | string]: { style: CSSProperties, label: ReactNode } }</code> | | 刻度标记<br/>- 支持自定义样式<br/>- 设置百分比 |
|
||||
| tooltipVisible | `boolean` | `false` | 是否显示滑块标签 |
|
||||
| tooltipPlacement | `auto` or `bottom` or `left` or `right` | `top` | 滑块标签的位置,默认`auto`,方向自适应<br/>前置条件:tooltipVisible 不为 false 时有效 |
|
||||
| tipFormatter | `function` | | 控制滑块标签显隐函数<br/>前置条件:tooltipVisible 不为 false 时有效 |
|
||||
| multiple | `boolean` | `false` | 支持选择范围 |
|
||||
| joinValues | `boolean` | `true` | 默认为 `true`,选择的 `value` 会通过 `delimiter` 连接起来,否则直接将以`{min: 1, max: 100}`的形式提交<br/>前置条件:开启`multiple`时有效 |
|
||||
| delimiter | `string` | `,` | 分隔符 |
|
||||
| unit | `string` | | 单位 |
|
||||
| clearable | `boolean` | `false` | 是否可清除<br/>前置条件:开启`showInput`时有效 |
|
||||
| showInput | `boolean` | `false` | 是否显示输入框 |
|
||||
| onChange | `function` | | 当 组件 的值发生改变时,会触发 onChange 事件,并把改变后的值作为参数传入 |
|
||||
| onAfterChange | `function` | | 与 `onmouseup` 触发时机一致,把当前值作为参数传入 |
|
||||
|
||||
|
@ -830,36 +830,44 @@
|
||||
--InputGroup-select-onFocused-bg: var(--white);
|
||||
--InputGroup-select-onFocused-color: var(--Form-select-onFocused-color);
|
||||
|
||||
--InputRange-label--value-display: block;
|
||||
--InputRange-label--value-positionTop: #{px2rem(-36px)};
|
||||
--InputRange-label--value-positionLeft: #{px2rem(-6px)};
|
||||
--InputRange-label-color: var(--InputRange-neutralColor);
|
||||
--InputRange-label-fontSize: 0.8rem;
|
||||
--InputRange-label-positionBottom: -1.4rem;
|
||||
--InputRange-neutralColor: #aaaaaa;
|
||||
--InputRange-neutralLightColor: #eeeeee;
|
||||
--InputRange-onDisabled-color: #cccccc;
|
||||
--InputRange-primaryColor: var(--info);
|
||||
--InputRange-slider-bg: var(--InputRange-primaryColor);
|
||||
--InputRange-slider-border: #{px2rem(1px)} solid var(--InputRange-primaryColor);
|
||||
--InputRange-slider-height: #{px2rem(24px)};
|
||||
--InputRange-slider-onActive-transform: scale(1.3);
|
||||
--InputRange-slider-onDisabled-bg: var(--InputRange-onDisabled-color);
|
||||
--InputRange-slider-onDisabled-border: #{px2rem(1px)} solid var(--InputRange-onDisabled-color);
|
||||
--InputRange-slider-onFocus-borderRadius: var(--borderRadiusMd);
|
||||
--InputRange-slider-onFocus-boxShadow: 0 0 0
|
||||
var(--InputRange-slider-onFocus-borderRadius) #{transparentize($info, 0.8)};
|
||||
--InputRange-slider-transition: transform var(--animation-duration) ease-out,
|
||||
box-shadow var(--animation-duration) ease-out;
|
||||
--InputRange-slider-width: #{px2rem(18px)};
|
||||
--InputRange-sliderContainer-transition: left var(--animation-duration)
|
||||
ease-out;
|
||||
--InputRange-track-bg: var(--InputRange-neutralLightColor);
|
||||
--InputRange-track-height: #{px2rem(12px)};
|
||||
--InputRange-padding: #{px2rem(20px)};
|
||||
--InputRange-onDisabled-color: var(--light);
|
||||
--InputRange-primaryColor: var(--primary);
|
||||
--InputRange-track-height: #{px2rem(4px)};
|
||||
--InputRange-track-bg: #{$gray100};
|
||||
--InputRange-track-onDisabled-bg: var(--InputRange-onDisabled-color);
|
||||
--InputRange-track-onActive-bg: var(--InputRange-primaryColor);
|
||||
--InputRange-track-onDisabled-bg: var(--InputRange-neutralLightColor);
|
||||
--InputRange-track-onActive-onDisabled-bg: #{$gray200};
|
||||
--InputRange-track-onActive-transition: transform var(--animation-duration)
|
||||
ease-out left;
|
||||
--InputRange-track-border-radius: #{px2rem(4px)};
|
||||
--InputRange-track-dot-width: #{px2rem(4px)};
|
||||
--InputRange-track-dot-height: #{px2rem(4px)};
|
||||
--InputRange-track-dot-bg: var(--white);
|
||||
--InputRange-track-transition: left var(--animation-duration) ease-out,
|
||||
width var(--animation-duration) ease-out;
|
||||
--InputRange-handle-height: #{px2rem(16px)};
|
||||
--InputRange-handle-width: #{px2rem(16px)};
|
||||
--InputRange-handle-bg: var(--white);
|
||||
--InputRange-handle-border: #{px2rem(1px)} solid var(--InputRange-primaryColor);
|
||||
--InputRange-handle-onDisabled-border-color: #ceced1;
|
||||
--InputRange-handle-onActive-transform: scale(1.3);
|
||||
--InputRange-handle-onDrage-border-width: #{px2rem(2px)};
|
||||
--InputRange-handle-onDisabled-bg: var(--InputRange-onDisabled-color);
|
||||
--InputRange-handle-onDisabled-border: #{px2rem(1px)} solid var(--InputRange-onDisabled-color);
|
||||
--InputRange-handle-onFocus-borderRadius: var(--borderRadiusMd);
|
||||
--InputRange-handele-onFocus-boxShadow: 0 0 0
|
||||
var(--InputRange-slider-onFocus-borderRadius) #{transparentize($info, 0.8)};
|
||||
--InputRange-handle-transition: transform var(--animation-duration) ease-out,
|
||||
box-shadow var(--animation-duration) ease-out;
|
||||
--InputRange-handle-icon-width: #{px2rem(8px)};
|
||||
--InputRange-handle-icon-height: #{px2rem(8px)};
|
||||
--InputRange-label-padding: #{px2rem(8px)};
|
||||
--InputRange-label-bg: #000;
|
||||
--InputRange-label-color: var(--white);
|
||||
--InputRange-label-font-size: #{px2rem(14px)};
|
||||
--InputRange-label-border-radius: #{px2rem(4px)};
|
||||
--InputRange-label-position-bottom: calc(100% + 8px);
|
||||
|
||||
--Layout--offscreen-width: 75%;
|
||||
--Layout-aside--folded-width: #{px2rem(60px)};
|
||||
|
@ -1,103 +1,124 @@
|
||||
.#{$ns}RangeControl {
|
||||
position: relative;
|
||||
@include clearfix();
|
||||
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1.1rem;
|
||||
|
||||
&--withInput {
|
||||
.#{$ns}InputRange {
|
||||
width: calc(100% - 120px);
|
||||
}
|
||||
|
||||
.#{$ns}InputRange-label--mid {
|
||||
left: calc(50% - 60px);
|
||||
}
|
||||
|
||||
&.is-multiple {
|
||||
.#{$ns}InputRange {
|
||||
width: calc(100% - 210px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}InputRange {
|
||||
&-input {
|
||||
font-size: var(--fontSizeSm);
|
||||
position: absolute;
|
||||
right: px2rem(26px);
|
||||
top: px2rem(12px);
|
||||
height: px2rem(30px);
|
||||
|
||||
input {
|
||||
padding: px2rem(10px);
|
||||
width: px2rem(74px);
|
||||
height: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: var(--borderWidth) solid var(--info);
|
||||
}
|
||||
}
|
||||
|
||||
&-separator {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-unit {
|
||||
position: absolute;
|
||||
right: px2rem(10px);
|
||||
top: px2rem(7px);
|
||||
}
|
||||
|
||||
&-clear {
|
||||
position: absolute;
|
||||
top: px2rem(18px);
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
height: px2rem(16px);
|
||||
width: px2rem(16px);
|
||||
fill: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}InputRange {
|
||||
height: var(--InputRange-slider-height);
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--InputRange-padding) 0;
|
||||
width: 100%;
|
||||
|
||||
// slider
|
||||
&-slider {
|
||||
appearance: none;
|
||||
background: var(--InputRange-slider-bg);
|
||||
border: var(--InputRange-slider-border);
|
||||
// border-radius: 100%;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: var(--InputRange-slider-width);
|
||||
height: var(--InputRange-slider-height);
|
||||
margin-left: calc(var(--InputRange-slider-width) / -2);
|
||||
margin-top: calc(
|
||||
var(--InputRange-slider-height) / -2 + var(--InputRange-track-height) / -2
|
||||
);
|
||||
outline: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
transition: var(--InputRange-slider-transition);
|
||||
&-wrap {
|
||||
position: relative;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: var(--InputRange-slider-onActive-transform);
|
||||
&-input {
|
||||
width: 20 * 4px;
|
||||
height: 8 * 4px;
|
||||
margin: 0 2 * 4px;
|
||||
|
||||
.#{$ns}Number {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: var(--InputRange-slider-onFocus-boxShadow);
|
||||
.#{$ns}Number-handler {
|
||||
transition: 0.3s opacity;
|
||||
color: var(--Number-handler-color);
|
||||
|
||||
&-up-inner,
|
||||
&-down-inner {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--Number-handler-onHover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
height: px2rem(12px);
|
||||
width: px2rem(12px);
|
||||
fill: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// disabled
|
||||
&.is-disabled {
|
||||
.#{$ns}InputRange-track {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.#{$ns}InputRange-track-active {
|
||||
background-color: var(--InputRange-track-onActive-onDisabled-bg);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.#{$ns}InputRange-handle-icon {
|
||||
border-color: var(--InputRange-handle-onDisabled-border-color);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}Number-handler {
|
||||
cursor: not-allowed;
|
||||
|
||||
&-up-inner,
|
||||
&-down-inner {
|
||||
cursor: not-allowed;
|
||||
&:hover {
|
||||
color: var(--text--muted-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hander
|
||||
&-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0 auto calc(var(--InputRange-handle-width) / -2);
|
||||
width: var(--InputRange-handle-width);
|
||||
height: var(--InputRange-handle-height);
|
||||
|
||||
&-icon,
|
||||
&-drage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
appearance: none;
|
||||
background-color: var(--InputRange-handle-bg);
|
||||
border: var(--InputRange-handle-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
transition: var(--InputRange-handle-transition);
|
||||
|
||||
&:hover {
|
||||
transform: var(--InputRange-handle-onActive-transform);
|
||||
box-shadow: var(--InputRange-handle-onFocus-boxShadow);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: var(--InputRange-handle-onActive-transform);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: var(--InputRange-handle-onFocus-boxShadow);
|
||||
}
|
||||
}
|
||||
|
||||
&-drage {
|
||||
transform: var(--InputRange-handle-onActive-transform);
|
||||
box-shadow: var(--InputRange-handle-onFocus-boxShadow);
|
||||
border-width: var(--InputRange-handle-onDrage-border-width);
|
||||
}
|
||||
|
||||
.input-range--disabled & {
|
||||
@ -107,77 +128,34 @@
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '||';
|
||||
color: #fff;
|
||||
display: block;
|
||||
line-height: px2rem(22px);
|
||||
text-align: center;
|
||||
.icon-slider-handle {
|
||||
width: var(--InputRange-handle-icon-width);
|
||||
height: var(--InputRange-handle-icon-height);
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-sliderContainer {
|
||||
transition: var(--InputRange-sliderContainer-transition);
|
||||
}
|
||||
|
||||
// label
|
||||
&-label {
|
||||
color: var(--InputRange-label-color);
|
||||
font-size: var(--InputRange-label-fontSize);
|
||||
transform: translateZ(0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-label--min,
|
||||
&-label--max,
|
||||
&-label--mid {
|
||||
bottom: var(--InputRange-label-positionBottom);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&-label--mid {
|
||||
left: 50%;
|
||||
bottom: calc(var(--gap-xs) * -1);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&-label--max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&-label--value {
|
||||
position: absolute;
|
||||
display: var(--InputRange-label--value-display);
|
||||
top: var(--InputRange-label--value-positionTop);
|
||||
left: var(--InputRange-label--value-positionLeft);
|
||||
}
|
||||
|
||||
// label Container
|
||||
|
||||
// &-labelContainer {
|
||||
// left: -50%;
|
||||
// position: relative;
|
||||
// .#{$ns}InputRange-label--max & {
|
||||
// left: 50%;
|
||||
// }
|
||||
// }
|
||||
|
||||
// track
|
||||
&-track {
|
||||
background: var(--InputRange-track-bg);
|
||||
// border-radius: 0;
|
||||
border-radius: var(--InputRange-track-border-radius);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: var(--InputRange-track-height);
|
||||
position: relative;
|
||||
transition: var(--InputRange-track-transition);
|
||||
|
||||
.#{$ns}InputRange.is-disabled & {
|
||||
background: var(--InputRange-track-onDisabled-bg);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--InputRange-track-onActive-bg);
|
||||
transition: 0.3s all;
|
||||
}
|
||||
|
||||
&-dot {
|
||||
width: var(--InputRange-track-dot-height);
|
||||
height: var(--InputRange-track-dot-height);
|
||||
border-radius: 50%;
|
||||
background-color: var(--InputRange-track-dot-bg);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,28 +165,110 @@
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
width: 0.5rem;
|
||||
height: 100%;
|
||||
&-track-active {
|
||||
background: var(--InputRange-track-onActive-bg);
|
||||
border-radius: var(--InputRange-track-border-radius);
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
// label
|
||||
&-label {
|
||||
position: absolute;
|
||||
padding: var(--InputRange-label-padding);
|
||||
background-color: var(--InputRange-label-bg);
|
||||
color: var(--InputRange-label-color);
|
||||
font-size: var(--InputRange-label-font-size);
|
||||
border-radius: var(--InputRange-label-border-radius);
|
||||
visibility: hidden;
|
||||
|
||||
&-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.pos-top {
|
||||
left: 50%;
|
||||
bottom: var(--InputRange-label-position-bottom);
|
||||
transform: translateX(-50%);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -4px;
|
||||
border-width: 4px 4px 0 4px;
|
||||
border-style: solid;
|
||||
border-color: #000 transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.pos-bottom {
|
||||
top: var(--InputRange-label-position-bottom);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -4px;
|
||||
border-width: 0 4px 4px 4px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #000 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.pos-left {
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-100%) translateX(-12px);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: -4px;
|
||||
border-width: 4px 0 4px 4px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent transparent #000;
|
||||
}
|
||||
}
|
||||
|
||||
&.pos-right {
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(26px);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: -4px;
|
||||
border-width: 4px 4px 4px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent #000 transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// marks
|
||||
&-marks {
|
||||
position: relative;
|
||||
top: 8px;
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background: inherit;
|
||||
z-index: 1;
|
||||
span {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -0.5rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-track--active {
|
||||
background: var(--InputRange-track-onActive-bg);
|
||||
}
|
||||
}
|
||||
|
@ -4,60 +4,523 @@
|
||||
* @author fex
|
||||
*/
|
||||
|
||||
import range from 'lodash/range';
|
||||
import keys from 'lodash/keys';
|
||||
import isString from 'lodash/isString';
|
||||
import difference from 'lodash/difference';
|
||||
import React from 'react';
|
||||
import InputRange from 'react-input-range';
|
||||
import {uncontrollable} from 'uncontrollable';
|
||||
import {RendererProps} from '../factory';
|
||||
import {ClassNamesFn, themeable} from '../theme';
|
||||
|
||||
interface RangeProps extends RendererProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
min: number;
|
||||
max: number;
|
||||
value:
|
||||
| {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
| number;
|
||||
classPrefix: string;
|
||||
classnames: ClassNamesFn;
|
||||
onChange: (value: any) => void;
|
||||
import {RendererProps} from '../factory';
|
||||
import Overlay from './Overlay';
|
||||
import {ThemeProps, themeable} from '../theme';
|
||||
import {autobind, camel} from '../utils/helper';
|
||||
import {Icon} from '../../src';
|
||||
import {
|
||||
MultipleValue,
|
||||
Value,
|
||||
FormatValue,
|
||||
MarksType,
|
||||
RangeItemProps
|
||||
} from '../renderers/Form/InputRange';
|
||||
import {stripNumber} from '../utils/tpl-builtin';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
|
||||
interface HandleItemState {
|
||||
isDrag: boolean;
|
||||
labelActive: boolean;
|
||||
}
|
||||
|
||||
export class Range extends React.Component<RangeProps, any> {
|
||||
static defaultProps: Partial<RangeProps> = {
|
||||
min: 1,
|
||||
max: 100
|
||||
};
|
||||
interface HandleItemProps extends ThemeProps {
|
||||
disabled: boolean;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
type?: 'min' | 'max';
|
||||
onChange: (value: number, type: 'min' | 'max') => void;
|
||||
onAfterChange: () => void;
|
||||
tooltipVisible?: boolean;
|
||||
tipFormatter?: (value: Value) => boolean;
|
||||
unit?: string;
|
||||
tooltipPlacement?: string;
|
||||
}
|
||||
|
||||
interface LabelProps extends ThemeProps {
|
||||
show: boolean;
|
||||
value: number;
|
||||
tooltipVisible?: boolean;
|
||||
tipFormatter?: (value: Value) => boolean;
|
||||
unit?: string;
|
||||
placement?: string;
|
||||
activePlacement?: string;
|
||||
positionLeft?: number;
|
||||
positionTop?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块值 -> position.left
|
||||
* @param value 滑块值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @returns position.left
|
||||
*/
|
||||
const valueToOffsetLeft = (value: any, min: number, max: number) =>
|
||||
(value * 100) / (max - min) + '%';
|
||||
|
||||
/**
|
||||
* 滑块handle
|
||||
* 双滑块涉及两个handle,单独抽一个组件
|
||||
*/
|
||||
class HandleItem extends React.Component<HandleItemProps, HandleItemState> {
|
||||
handleRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
constructor(props: HandleItemProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isDrag: false,
|
||||
labelActive: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseDown事件
|
||||
* 防止拖动过快,全局监听 mousemove、mouseup
|
||||
*/
|
||||
@autobind
|
||||
onMouseDown() {
|
||||
this.setState({
|
||||
isDrag: true,
|
||||
labelActive: true
|
||||
});
|
||||
window.addEventListener('mousemove', this.onMouseMove);
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseMove事件
|
||||
* 触发公共onchange事件
|
||||
*/
|
||||
@autobind
|
||||
onMouseMove(e: MouseEvent) {
|
||||
const {isDrag} = this.state;
|
||||
const {type = 'min'} = this.props;
|
||||
if (!isDrag) {
|
||||
return;
|
||||
}
|
||||
this.props.onChange(e.pageX, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseUp事件
|
||||
* 移除全局 mousemove、mouseup
|
||||
*/
|
||||
@autobind
|
||||
onMouseUp() {
|
||||
this.setState({
|
||||
isDrag: false
|
||||
});
|
||||
this.props.onAfterChange();
|
||||
window.removeEventListener('mousemove', this.onMouseMove);
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseEnter事件
|
||||
* 鼠标移入 -> 展示label
|
||||
*/
|
||||
@autobind
|
||||
onMouseEnter() {
|
||||
this.setState({
|
||||
labelActive: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseLeave事件
|
||||
* 鼠标移出 & !isDrag -> 隐藏label
|
||||
*/
|
||||
@autobind
|
||||
onMouseLeave() {
|
||||
const {isDrag} = this.state;
|
||||
if (isDrag) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
labelActive: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {min, max, value, className, classPrefix: ns, multiple} = this.props;
|
||||
|
||||
const classNames = {
|
||||
activeTrack: multiple
|
||||
? `${ns}InputRange-track is-active`
|
||||
: `${ns}InputRange-track`,
|
||||
disabledInputRange: `${ns}InputRange is-disabled`,
|
||||
inputRange: `${ns}InputRange`,
|
||||
labelContainer: `${ns}InputRange-labelContainer`,
|
||||
maxLabel: `${ns}InputRange-label ${ns}InputRange-label--max`,
|
||||
minLabel: `${ns}InputRange-label ${ns}InputRange-label--min`,
|
||||
slider: `${ns}InputRange-slider`,
|
||||
sliderContainer: `${ns}InputRange-sliderContainer`,
|
||||
track: `${ns}InputRange-track ${ns}InputRange-track--background`,
|
||||
valueLabel: `${ns}InputRange-label ${ns}InputRange-label--value`
|
||||
const {
|
||||
classnames: cx,
|
||||
disabled,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
tooltipVisible,
|
||||
tipFormatter,
|
||||
unit,
|
||||
tooltipPlacement = 'auto'
|
||||
} = this.props;
|
||||
const {isDrag, labelActive} = this.state;
|
||||
const style = {
|
||||
left: valueToOffsetLeft(value, min, max),
|
||||
zIndex: isDrag ? 2 : 1
|
||||
};
|
||||
|
||||
return disabled ? (
|
||||
<div className={cx('InputRange-handle')} style={style}>
|
||||
<div className={cx('InputRange-handle-icon')}>
|
||||
<Icon icon="slider-handle" className="icon" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cx('InputRange-handle')}
|
||||
style={style}
|
||||
ref={this.handleRef}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
isDrag ? 'InputRange-handle-drage' : 'InputRange-handle-icon'
|
||||
)}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<Icon icon="slider-handle" className="icon" />
|
||||
</div>
|
||||
|
||||
<Overlay
|
||||
placement={tooltipPlacement}
|
||||
target={() => findDOMNode(this)}
|
||||
container={() => findDOMNode(this)}
|
||||
rootClose={false}
|
||||
show={true}
|
||||
>
|
||||
<Label
|
||||
show={labelActive}
|
||||
classPrefix={this.props.classPrefix}
|
||||
classnames={cx}
|
||||
value={value}
|
||||
tooltipVisible={tooltipVisible}
|
||||
tipFormatter={tipFormatter}
|
||||
unit={unit}
|
||||
placement={tooltipPlacement}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块标签
|
||||
*/
|
||||
class Label extends React.Component<LabelProps, any> {
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
value,
|
||||
show,
|
||||
tooltipVisible,
|
||||
tipFormatter,
|
||||
unit = '',
|
||||
positionLeft = 0,
|
||||
positionTop = 0
|
||||
} = this.props;
|
||||
|
||||
let {placement} = this.props;
|
||||
if (placement === 'auto') {
|
||||
positionLeft >= 0 && positionTop >= 0 && (placement = 'top');
|
||||
positionLeft >= 0 && positionTop < 0 && (placement = 'bottom');
|
||||
positionLeft < 0 && positionTop >= 0 && (placement = 'left');
|
||||
positionLeft < 0 && positionTop < 0 && (placement = 'right');
|
||||
}
|
||||
|
||||
// tooltipVisible 优先级 比show高
|
||||
// tooltipVisible 为 true时,tipFormatter才生效
|
||||
const isShow =
|
||||
tooltipVisible !== undefined
|
||||
? tooltipVisible && tipFormatter
|
||||
? tipFormatter(value)
|
||||
: tooltipVisible
|
||||
: show;
|
||||
return (
|
||||
<InputRange
|
||||
{...this.props}
|
||||
classNames={classNames}
|
||||
minValue={min}
|
||||
maxValue={max}
|
||||
value={value}
|
||||
/>
|
||||
<div
|
||||
className={cx('InputRange-label', `pos-${camel(placement)}`, {
|
||||
'InputRange-label-visible': isShow
|
||||
})}
|
||||
>
|
||||
<span>{value + unit}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Range extends React.Component<RangeItemProps, any> {
|
||||
multipleValue: MultipleValue = {
|
||||
min: (this.props.value as MultipleValue).min,
|
||||
max: (this.props.value as MultipleValue).max
|
||||
};
|
||||
trackRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
/**
|
||||
* 接收组件value变换
|
||||
* value变换 -> Range.updateValue
|
||||
* @param value
|
||||
*/
|
||||
@autobind
|
||||
updateValue(value: FormatValue) {
|
||||
this.props.updateValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 坐标、宽高
|
||||
*/
|
||||
@autobind
|
||||
getBoundingClient(dom: Element) {
|
||||
const {x, y, width, height} = dom?.getBoundingClientRect();
|
||||
return {x, y, width, height};
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标 -> 滑块值
|
||||
* @param pageX target.target 坐标
|
||||
* @returns 滑块值
|
||||
*/
|
||||
pageXToValue(pageX: number) {
|
||||
const {x, width} = this.getBoundingClient(this.trackRef.current as Element);
|
||||
const {max, min} = this.props;
|
||||
return ((pageX - x) * (max - min)) / width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块改变事件
|
||||
* @param pageX target.pageX 坐标
|
||||
* @param type min max
|
||||
* @returns void
|
||||
*/
|
||||
@autobind
|
||||
onChange(pageX: number, type: string = 'min') {
|
||||
const {max, min, step, multiple, value: originValue} = this.props;
|
||||
const value = this.pageXToValue(pageX);
|
||||
if (value > max || value < min) {
|
||||
return;
|
||||
}
|
||||
const result = stripNumber(this.getStepValue(value, step));
|
||||
if (multiple) {
|
||||
this.updateValue({...(originValue as MultipleValue), [type]: result});
|
||||
} else {
|
||||
this.updateValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取step为单位的value
|
||||
* @param value 拖拽后计算的value
|
||||
* @param step 步长
|
||||
* @returns step为单位的value
|
||||
*/
|
||||
getStepValue(value: number, step: number) {
|
||||
const surplus = value % step;
|
||||
let result = 0;
|
||||
// 余数 >= 步长一半 -> 向上取
|
||||
// 余数 < 步长一半 -> 向下取
|
||||
const _value = surplus >= step / 2 ? value : value - step;
|
||||
while (result <= _value) {
|
||||
result += step;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击滑轨 -> 触发onchange 改变value
|
||||
* @param e event
|
||||
* @returns void
|
||||
*/
|
||||
@autobind
|
||||
onClickTrack(e: any) {
|
||||
if (!!this.props.disabled) {
|
||||
return;
|
||||
}
|
||||
const {value} = this.props;
|
||||
const _value = this.pageXToValue(e.pageX);
|
||||
const type =
|
||||
Math.abs(_value - (value as MultipleValue).min) >
|
||||
Math.abs(_value - (value as MultipleValue).max)
|
||||
? 'max'
|
||||
: 'min';
|
||||
this.onChange(e.pageX, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置步长
|
||||
* @returns ReactNode
|
||||
*/
|
||||
@autobind
|
||||
renderSteps() {
|
||||
const {max, min, step, showSteps, classnames: cx} = this.props;
|
||||
const steps = Math.floor((max - min) / step);
|
||||
return (
|
||||
showSteps && (
|
||||
<div>
|
||||
{range(steps - 1).map(item => (
|
||||
<span
|
||||
key={item}
|
||||
className={cx('InputRange-track-dot')}
|
||||
style={{left: ((item + 1) * 100) / steps + '%'}}
|
||||
></span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 双滑块改变最大值、最小值
|
||||
* @param pageX 拖拽后的pageX
|
||||
* @param type 'min' | 'max'
|
||||
*/
|
||||
@autobind
|
||||
onGetChangeValue(pageX: number, type: keyof MultipleValue) {
|
||||
const {max, min} = this.props;
|
||||
const value = this.pageXToValue(pageX);
|
||||
if (value > max || value < min) {
|
||||
return;
|
||||
}
|
||||
this.multipleValue[type] = stripNumber(
|
||||
this.getStepValue(value, this.props.step)
|
||||
);
|
||||
const _min = Math.min(this.multipleValue.min, this.multipleValue.max);
|
||||
const _max = Math.max(this.multipleValue.min, this.multipleValue.max);
|
||||
this.updateValue({max: _max, min: _min});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算每个标记 position.left
|
||||
* @param value 滑块值
|
||||
* @returns
|
||||
*/
|
||||
@autobind
|
||||
getOffsetLeft(value: number | string) {
|
||||
const {max, min} = this.props;
|
||||
if (isString(value) && /^\d+%$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return (+value * 100) / (max - min) + '%';
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
marks,
|
||||
multiple,
|
||||
value,
|
||||
max,
|
||||
min,
|
||||
disabled,
|
||||
tooltipVisible,
|
||||
unit,
|
||||
tooltipPlacement,
|
||||
tipFormatter,
|
||||
onAfterChange
|
||||
} = this.props;
|
||||
|
||||
// trace
|
||||
const traceActiveStyle = {
|
||||
width: valueToOffsetLeft(
|
||||
multiple
|
||||
? (value as MultipleValue).max - (value as MultipleValue).min
|
||||
: value,
|
||||
min,
|
||||
max
|
||||
),
|
||||
left: valueToOffsetLeft(
|
||||
multiple ? (value as MultipleValue).min : 0,
|
||||
min,
|
||||
max
|
||||
)
|
||||
};
|
||||
|
||||
// handle 双滑块
|
||||
const diff = difference(
|
||||
Object.values(value as MultipleValue),
|
||||
Object.values(this.multipleValue)
|
||||
);
|
||||
if (diff && !!diff.length) {
|
||||
this.multipleValue = {
|
||||
min: (value as MultipleValue).min,
|
||||
max: (value as MultipleValue).max
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('InputRange-wrap')}>
|
||||
<div
|
||||
ref={this.trackRef}
|
||||
className={cx('InputRange-track', 'InputRange-track--background')}
|
||||
onClick={this.onClickTrack}
|
||||
>
|
||||
<div
|
||||
className={cx('InputRange-track-active')}
|
||||
style={traceActiveStyle}
|
||||
/>
|
||||
|
||||
{/* 显示步长 */}
|
||||
{this.renderSteps()}
|
||||
|
||||
{/* 滑块handle */}
|
||||
{multiple ? (
|
||||
['min', 'max'].map((type: 'min' | 'max') => (
|
||||
<HandleItem
|
||||
key={type}
|
||||
value={this.multipleValue[type]}
|
||||
type={type}
|
||||
min={min}
|
||||
max={max}
|
||||
classPrefix={this.props.classPrefix}
|
||||
classnames={cx}
|
||||
disabled={disabled}
|
||||
tooltipVisible={tooltipVisible}
|
||||
tipFormatter={tipFormatter}
|
||||
unit={unit}
|
||||
tooltipPlacement={tooltipPlacement}
|
||||
onAfterChange={onAfterChange}
|
||||
onChange={this.onGetChangeValue.bind(this)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<HandleItem
|
||||
value={+value}
|
||||
min={min}
|
||||
max={max}
|
||||
classPrefix={this.props.classPrefix}
|
||||
classnames={cx}
|
||||
disabled={disabled}
|
||||
tooltipVisible={tooltipVisible}
|
||||
tipFormatter={tipFormatter}
|
||||
unit={unit}
|
||||
tooltipPlacement={tooltipPlacement}
|
||||
onAfterChange={onAfterChange}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 刻度标记 */}
|
||||
{marks && (
|
||||
<div className={cx('InputRange-marks')}>
|
||||
{keys(marks).map((key: keyof MarksType) => (
|
||||
<div key={key} style={{left: this.getOffsetLeft(key)}}>
|
||||
<span style={(marks[key] as any)?.style}>
|
||||
{(marks[key] as any)?.label || marks[key]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ import AlertWarning from '../icons/alert-warning.svg';
|
||||
import AlertDanger from '../icons/alert-danger.svg';
|
||||
import FunctionIcon from '../icons/function.svg';
|
||||
import InputClearIcon from '../icons/input-clear.svg';
|
||||
import SliderHandleIcon from '../icons/slider-handle-icon.svg';
|
||||
|
||||
// 兼容原来的用法,后续不直接试用。
|
||||
|
||||
@ -184,6 +185,7 @@ registerIcon('alert-danger', AlertDanger);
|
||||
registerIcon('tree-down', TreeDownIcon);
|
||||
registerIcon('function', FunctionIcon);
|
||||
registerIcon('input-clear', InputClearIcon);
|
||||
registerIcon('slider-handle', SliderHandleIcon);
|
||||
|
||||
export function Icon({
|
||||
icon,
|
||||
|
6
src/icons/slider-handle-icon.svg
Normal file
6
src/icons/slider-handle-icon.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="6px" height="4px" viewBox="0 0 6 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="控件" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M1.5,2.28847549e-17 L1.5,4 L0.5,4 L0.5,-2.22044605e-16 L1.5,2.28847549e-17 Z M3.5,2.28847549e-17 L3.5,4 L2.5,4 L2.5,-2.22044605e-16 L3.5,2.28847549e-17 Z M5.5,2.28847549e-17 L5.5,4 L4.5,4 L4.5,-2.22044605e-16 L5.5,2.28847549e-17 Z" id="形状结合" fill="#D4E5FF"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
@ -1,21 +1,32 @@
|
||||
import React from 'react';
|
||||
import React, {CSSProperties, ReactNode} from 'react';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import isObject from 'lodash/isObject';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {FormItem, FormControlProps, FormBaseControl} from './Item';
|
||||
|
||||
import {FormItem, FormControlProps, FormBaseControl} from './Item';
|
||||
import InputRange from '../../components/Range';
|
||||
import NumberInput from '../../components/NumberInput';
|
||||
import {Icon} from '../../components/icons';
|
||||
import {FormOptionsControl} from './Options';
|
||||
import {stripNumber} from '../../utils/tpl-builtin';
|
||||
import {autobind} from '../../utils/helper';
|
||||
import {filter} from '../../utils/tpl';
|
||||
|
||||
/**
|
||||
* Range
|
||||
* 文档:https://baidu.gitee.io/amis/docs/components/form/range
|
||||
*/
|
||||
|
||||
export type Value = string | MultipleValue | number | [number, number];
|
||||
export type FormatValue = MultipleValue | number;
|
||||
export type TooltipPosType = 'auto' | 'top' | 'right' | 'bottom' | 'left';
|
||||
export interface RangeControlSchema extends FormBaseControl {
|
||||
type: 'input-range';
|
||||
|
||||
/**
|
||||
* 滑块值
|
||||
*/
|
||||
value?: Value;
|
||||
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
@ -35,25 +46,164 @@ export interface RangeControlSchema extends FormBaseControl {
|
||||
* 单位
|
||||
*/
|
||||
unit?: string;
|
||||
|
||||
/**
|
||||
* 是否展示步长
|
||||
*/
|
||||
showSteps?: boolean;
|
||||
|
||||
/**
|
||||
* 分割块数
|
||||
*/
|
||||
parts?: number;
|
||||
|
||||
/**
|
||||
* 刻度
|
||||
*/
|
||||
marks?: MarksType;
|
||||
|
||||
/**
|
||||
* 是否展示标签
|
||||
*/
|
||||
tooltipVisible?: boolean;
|
||||
|
||||
/**
|
||||
* 标签方向
|
||||
*/
|
||||
tooltipPlacement?: TooltipPosType;
|
||||
|
||||
/**
|
||||
* 是否为双滑块
|
||||
*/
|
||||
multiple?: boolean;
|
||||
|
||||
/**
|
||||
* 是否通过分隔符连接
|
||||
*/
|
||||
joinValues?: boolean;
|
||||
|
||||
/**
|
||||
* 分隔符
|
||||
*/
|
||||
delimiter?: string;
|
||||
|
||||
/**
|
||||
* 是否展示输入框
|
||||
*/
|
||||
showInput?: boolean;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type MarksType = {
|
||||
[index: number | string]: Record<
|
||||
number,
|
||||
React.ReactNode | {style?: React.CSSProperties; label?: string}
|
||||
>;
|
||||
};
|
||||
|
||||
export interface RangeProps extends FormControlProps {
|
||||
max?: number;
|
||||
min?: number;
|
||||
step?: number;
|
||||
/**
|
||||
* 滑块值
|
||||
*/
|
||||
value: Value;
|
||||
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min: number;
|
||||
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max: number;
|
||||
|
||||
/**
|
||||
* 步长
|
||||
*/
|
||||
step: number;
|
||||
|
||||
/**
|
||||
* 是否展示步长
|
||||
*/
|
||||
showSteps: boolean;
|
||||
|
||||
/**
|
||||
* 分割块数
|
||||
*/
|
||||
parts: number;
|
||||
|
||||
/**
|
||||
* 刻度
|
||||
*/
|
||||
marks?: MarksType;
|
||||
|
||||
/**
|
||||
* 是否展示标签
|
||||
*/
|
||||
tooltipVisible: boolean;
|
||||
|
||||
/**
|
||||
* 标签方向
|
||||
*/
|
||||
tooltipPlacement: TooltipPosType;
|
||||
|
||||
/**
|
||||
* 控制滑块标签显隐函数
|
||||
*/
|
||||
tipFormatter?: (value: Value) => boolean;
|
||||
|
||||
/**
|
||||
* 是否为双滑块
|
||||
*/
|
||||
multiple: boolean;
|
||||
|
||||
/**
|
||||
* 是否通过分隔符连接
|
||||
*/
|
||||
joinValues: boolean;
|
||||
|
||||
/**
|
||||
* 分隔符
|
||||
*/
|
||||
delimiter: string;
|
||||
|
||||
/**
|
||||
* 单位
|
||||
*/
|
||||
unit?: string;
|
||||
clearable?: boolean;
|
||||
name?: string;
|
||||
showInput?: boolean;
|
||||
className?: string;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
multiple?: boolean;
|
||||
joinValues?: boolean;
|
||||
delimiter?: string;
|
||||
|
||||
/**
|
||||
* 是否展示输入框
|
||||
*/
|
||||
showInput: boolean;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
* value改变事件
|
||||
*/
|
||||
onChange: (value: Value) => void;
|
||||
|
||||
/**
|
||||
* 鼠标松开事件
|
||||
*/
|
||||
onAfterChange?: (value: Value) => any;
|
||||
}
|
||||
|
||||
export interface MultipleValue {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface DefaultProps {
|
||||
value: Value;
|
||||
max: number;
|
||||
min: number;
|
||||
step: number;
|
||||
@ -64,56 +214,181 @@ export interface DefaultProps {
|
||||
multiple: boolean;
|
||||
joinValues: boolean;
|
||||
delimiter: string;
|
||||
showSteps: boolean;
|
||||
parts: number;
|
||||
tooltipPlacement: TooltipPosType;
|
||||
}
|
||||
|
||||
export function formatValue(
|
||||
value: string | number | {min: number; max: number},
|
||||
props: Partial<RangeProps>
|
||||
) {
|
||||
if (props.multiple) {
|
||||
if (typeof value === 'string') {
|
||||
const [minValue, maxValue] = value
|
||||
.split(props.delimiter || ',')
|
||||
.map(v => Number(v));
|
||||
return {
|
||||
min:
|
||||
(props.min && minValue < props.min && props.min) ||
|
||||
minValue ||
|
||||
props.min,
|
||||
max:
|
||||
(props.max && maxValue > props.max && props.max) ||
|
||||
maxValue ||
|
||||
props.max
|
||||
};
|
||||
} else if (typeof value === 'object') {
|
||||
return {
|
||||
min:
|
||||
(props.min && value.min < props.min && props.min) ||
|
||||
value.min ||
|
||||
props.min,
|
||||
max:
|
||||
(props.max && value.max > props.max && props.max) ||
|
||||
value.max ||
|
||||
props.max
|
||||
};
|
||||
}
|
||||
}
|
||||
return value ?? props.min;
|
||||
export interface RangeItemProps extends RangeProps {
|
||||
value: FormatValue;
|
||||
updateValue: (value: Value) => void;
|
||||
onAfterChange: () => void;
|
||||
}
|
||||
|
||||
type PropsWithDefaults = RangeProps & DefaultProps;
|
||||
|
||||
export interface RangeState {
|
||||
value:
|
||||
| {
|
||||
min?: number;
|
||||
max?: number;
|
||||
value: FormatValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化初始value值
|
||||
* @param value 初始value值 Value
|
||||
* @param props RangeProps
|
||||
* @returns number | {min: number, max: number}
|
||||
*/
|
||||
export function formatValue(
|
||||
value: Value,
|
||||
props: {
|
||||
multiple: boolean;
|
||||
delimiter: string;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
): FormatValue {
|
||||
if (props.multiple) {
|
||||
let {min, max} = props;
|
||||
// value是字符串
|
||||
if (typeof value === 'string') {
|
||||
[min, max] = value.split(props.delimiter || ',').map(v => Number(v));
|
||||
}
|
||||
// value是数组
|
||||
else if (Array.isArray(value)) {
|
||||
[min, max] = value;
|
||||
}
|
||||
// value是对象
|
||||
else if (typeof value === 'object') {
|
||||
min = value.min;
|
||||
max = value.max;
|
||||
}
|
||||
return {
|
||||
min: min === undefined || min < props.min ? props.min : min,
|
||||
max: max === undefined || max > props.max ? props.max : max
|
||||
};
|
||||
}
|
||||
return +value ?? props.min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框
|
||||
*/
|
||||
export class Input extends React.Component<RangeItemProps, any> {
|
||||
/**
|
||||
* onChange事件,只能输入数字
|
||||
* @param e React.ChangeEvent
|
||||
*/
|
||||
@autobind
|
||||
onChange(value: number) {
|
||||
const {multiple, value: originValue, type} = this.props;
|
||||
const _value = this.getValue(value, type);
|
||||
|
||||
this.props.updateValue(
|
||||
multiple ? {...(originValue as MultipleValue), [type]: _value} : value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 双滑块 更新value
|
||||
* @param value 输入的value值
|
||||
*/
|
||||
@autobind
|
||||
onUpdateValue(value: number) {
|
||||
const {multiple, value: originValue, type} = this.props;
|
||||
const _value = this.getValue(value, type);
|
||||
|
||||
this.props.updateValue(
|
||||
multiple ? {...(originValue as MultipleValue), [type]: _value} : value
|
||||
);
|
||||
}
|
||||
|
||||
checkNum(value: number | string | undefined) {
|
||||
if (typeof value !== 'number') {
|
||||
value = filter(value, this.props.data);
|
||||
value = /^[-]?\d+/.test(value) ? +value : undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取步长小数精度
|
||||
* @returns
|
||||
*/
|
||||
getStepPrecision() {
|
||||
const {step} = this.props;
|
||||
const stepIsDecimal = /^\d+\.\d+$/.test(step.toString());
|
||||
return !stepIsDecimal || step < 0
|
||||
? 0
|
||||
: step.toString().split('.')[1]?.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数据
|
||||
* @param value input数据
|
||||
* @param type min | max 双滑块
|
||||
* @returns 处理之后数据
|
||||
*/
|
||||
getValue(value: string | number, type?: string) {
|
||||
const {max, min, step, value: stateValue} = this.props as RangeItemProps;
|
||||
|
||||
// 校正value为step的倍数
|
||||
let _value = Math.round(parseFloat(value + '') / step) * step;
|
||||
// 同步value与步长小数位数
|
||||
_value = parseFloat(_value.toFixed(this.getStepPrecision()));
|
||||
// 单滑块只用考虑 轨道边界 ,双滑块需要考虑 两端滑块边界
|
||||
switch (type) {
|
||||
case 'min': {
|
||||
if (isObject(stateValue) && isNumber(stateValue.max)) {
|
||||
// 如果 大于当前双滑块最大值 取 当前双滑块max值 - 步长
|
||||
if (_value >= stateValue.max) {
|
||||
return stateValue.max - step;
|
||||
}
|
||||
return _value;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
| number
|
||||
| string
|
||||
| undefined;
|
||||
minValue?: any;
|
||||
maxValue?: any;
|
||||
case 'max':
|
||||
if (isObject(stateValue) && isNumber(stateValue.min)) {
|
||||
// 如果 小于当前双滑块最大值 取 当前双滑块min值 + 步长
|
||||
if (_value <= stateValue.min) {
|
||||
return stateValue.min + step;
|
||||
}
|
||||
return _value;
|
||||
}
|
||||
return max;
|
||||
default:
|
||||
// 轨道边界
|
||||
return (_value < min && min) || (_value > max && max) || _value;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
value,
|
||||
multiple,
|
||||
type,
|
||||
step,
|
||||
classPrefix: ns,
|
||||
disabled,
|
||||
max,
|
||||
min
|
||||
} = this.props;
|
||||
const _value = multiple
|
||||
? type === 'min'
|
||||
? Math.min((value as MultipleValue).min, (value as MultipleValue).max)
|
||||
: Math.max((value as MultipleValue).min, (value as MultipleValue).max)
|
||||
: value;
|
||||
return (
|
||||
<div className={cx(`${ns}InputRange-input`)}>
|
||||
<NumberInput
|
||||
value={+_value}
|
||||
step={step}
|
||||
max={this.checkNum(max)}
|
||||
min={this.checkNum(min)}
|
||||
onChange={this.onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class RangeControl extends React.PureComponent<
|
||||
@ -123,6 +398,7 @@ export default class RangeControl extends React.PureComponent<
|
||||
midLabel?: HTMLSpanElement;
|
||||
|
||||
static defaultProps: DefaultProps = {
|
||||
value: 0,
|
||||
max: 100,
|
||||
min: 0,
|
||||
step: 1,
|
||||
@ -132,7 +408,10 @@ export default class RangeControl extends React.PureComponent<
|
||||
showInput: false,
|
||||
multiple: false,
|
||||
joinValues: true,
|
||||
delimiter: ','
|
||||
delimiter: ',',
|
||||
showSteps: false,
|
||||
parts: 1,
|
||||
tooltipPlacement: 'auto'
|
||||
};
|
||||
|
||||
constructor(props: RangeProps) {
|
||||
@ -146,28 +425,20 @@ export default class RangeControl extends React.PureComponent<
|
||||
});
|
||||
|
||||
this.state = {
|
||||
value: value,
|
||||
minValue: isObject(value) ? value.min : min,
|
||||
maxValue: isObject(value) ? value.max : max
|
||||
value: this.getValue(value)
|
||||
};
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleEnd = this.handleEnd.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.midLabelRef = this.midLabelRef.bind(this);
|
||||
this.clearValue = this.clearValue.bind(this);
|
||||
this.handleMinInputBlur = this.handleMinInputBlur.bind(this);
|
||||
this.handleMaxInputBlur = this.handleMaxInputBlur.bind(this);
|
||||
this.handleMinInputChange = this.handleMinInputChange.bind(this);
|
||||
this.handleMaxInputChange = this.handleMaxInputChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RangeProps) {
|
||||
const {value} = prevProps;
|
||||
const {value: nextPropsValue, multiple, delimiter, min, max} = this.props;
|
||||
const {
|
||||
value: nextPropsValue,
|
||||
multiple,
|
||||
delimiter,
|
||||
min,
|
||||
max,
|
||||
onChange
|
||||
} = this.props;
|
||||
if (value !== nextPropsValue) {
|
||||
const value = formatValue(nextPropsValue, {
|
||||
multiple,
|
||||
@ -175,312 +446,117 @@ export default class RangeControl extends React.PureComponent<
|
||||
min,
|
||||
max
|
||||
});
|
||||
|
||||
this.setState({
|
||||
value: value,
|
||||
minValue: isObject(value) ? value.min : min,
|
||||
maxValue: isObject(value) ? value.max : max
|
||||
value: this.getValue(value)
|
||||
});
|
||||
}
|
||||
|
||||
if (prevProps.showInput !== this.props.showInput) {
|
||||
this.updateStyle();
|
||||
}
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
const {showInput, classPrefix: ns, max, min} = this.props;
|
||||
|
||||
let offsetWidth = (this.midLabel as HTMLSpanElement).offsetWidth;
|
||||
const midValue = parseFloat(
|
||||
((max! + min! - 0.000001) / 2).toFixed(this.getStepPrecision())
|
||||
);
|
||||
|
||||
let left = `${100 * ((midValue - min!) / (max! - min!))}%`;
|
||||
(this.midLabel as HTMLSpanElement).style.left = left;
|
||||
}
|
||||
|
||||
midLabelRef(ref: any) {
|
||||
this.midLabel = ref;
|
||||
}
|
||||
|
||||
handleChange(value: any) {
|
||||
this.setState({
|
||||
value: stripNumber(value),
|
||||
minValue: value.min,
|
||||
maxValue: value.max
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
clearValue() {
|
||||
const {multiple, joinValues, delimiter, min, max, onChange} = this.props;
|
||||
const {multiple, min, max} = this.props;
|
||||
if (multiple) {
|
||||
this.setState(
|
||||
{
|
||||
value: {
|
||||
min: min,
|
||||
max: max
|
||||
},
|
||||
minValue: min,
|
||||
maxValue: max
|
||||
},
|
||||
() =>
|
||||
onChange(
|
||||
joinValues
|
||||
? [min, max].join(delimiter || ',')
|
||||
: {
|
||||
min: min,
|
||||
max: max
|
||||
}
|
||||
)
|
||||
);
|
||||
this.updateValue({min, max});
|
||||
} else {
|
||||
this.setState(
|
||||
{
|
||||
value: min
|
||||
},
|
||||
() => onChange(min)
|
||||
);
|
||||
this.updateValue(min);
|
||||
}
|
||||
}
|
||||
|
||||
handleEnd(value: any) {
|
||||
const {multiple, joinValues, delimiter} = this.props;
|
||||
let endValue = value;
|
||||
if (multiple) {
|
||||
endValue = joinValues
|
||||
? [value.min, value.max].join(delimiter || ',')
|
||||
: {
|
||||
min: value.min,
|
||||
max: value.max
|
||||
};
|
||||
} else {
|
||||
endValue = stripNumber(value);
|
||||
}
|
||||
const {onChange} = this.props;
|
||||
this.setState(
|
||||
{
|
||||
value
|
||||
},
|
||||
() => onChange(endValue)
|
||||
);
|
||||
}
|
||||
|
||||
getStepPrecision() {
|
||||
const {step} = this.props;
|
||||
|
||||
return typeof step !== 'number' || step >= 1 || step < 0
|
||||
? 0
|
||||
: step.toString().split('.')[1]?.length;
|
||||
}
|
||||
|
||||
getValue(value: any, type?: string) {
|
||||
const {max, min, step} = this.props as PropsWithDefaults;
|
||||
const {value: stateValue} = this.state;
|
||||
|
||||
if (
|
||||
value === '' ||
|
||||
value === '-' ||
|
||||
new RegExp('^[-]?\\d+[.]{1}[0]{0,' + this.getStepPrecision() + '}$').test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
value = Math.round(parseFloat(value) / step) * step;
|
||||
value =
|
||||
step < 1 ? parseFloat(value.toFixed(this.getStepPrecision())) : ~~value;
|
||||
|
||||
switch (type) {
|
||||
case 'min': {
|
||||
if (isObject(stateValue) && isNumber(stateValue.max)) {
|
||||
if (value >= stateValue.max && min <= stateValue.max - step) {
|
||||
return stateValue.max - step;
|
||||
}
|
||||
if (value < stateValue.max - step) {
|
||||
return value;
|
||||
}
|
||||
@autobind
|
||||
getValue(value: FormatValue) {
|
||||
const {multiple} = this.props;
|
||||
return multiple
|
||||
? {
|
||||
max: stripNumber((value as MultipleValue).max),
|
||||
min: stripNumber((value as MultipleValue).min)
|
||||
}
|
||||
return min;
|
||||
}
|
||||
case 'max':
|
||||
return isObject(stateValue) && isNumber(stateValue.min)
|
||||
? (value > max && max) ||
|
||||
(value <= stateValue.min && stateValue.min + step) ||
|
||||
value
|
||||
: max;
|
||||
default:
|
||||
return (value < min && min) || (value > max && max) || value;
|
||||
}
|
||||
: stripNumber(value as number);
|
||||
}
|
||||
|
||||
handleInputChange(evt: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = this.getValue(evt.target.value);
|
||||
this.setState(
|
||||
{
|
||||
value
|
||||
},
|
||||
() => this.props.onChange(value)
|
||||
/**
|
||||
* 所有触发value变换 -> updateValue
|
||||
* @param value
|
||||
*/
|
||||
@autobind
|
||||
updateValue(value: FormatValue) {
|
||||
this.setState({value: this.getValue(value)});
|
||||
const {multiple, joinValues, delimiter, onChange} = this.props;
|
||||
onChange(
|
||||
multiple
|
||||
? joinValues
|
||||
? [(value as MultipleValue).min, (value as MultipleValue).max].join(
|
||||
delimiter || ','
|
||||
)
|
||||
: {
|
||||
min: (value as MultipleValue).min,
|
||||
max: (value as MultipleValue).max
|
||||
}
|
||||
: value
|
||||
);
|
||||
}
|
||||
|
||||
handleMinInputBlur(evt: React.ChangeEvent<HTMLInputElement>) {
|
||||
const {joinValues, delimiter} = this.props;
|
||||
const minValue = this.getValue(evt.target.value, 'min');
|
||||
/**
|
||||
* 鼠标松开事件
|
||||
*/
|
||||
@autobind
|
||||
onAfterChange() {
|
||||
const {value} = this.state;
|
||||
isObject(value)
|
||||
? this.setState(
|
||||
{
|
||||
value: {
|
||||
min: minValue,
|
||||
max: value.max
|
||||
},
|
||||
minValue: minValue
|
||||
},
|
||||
() =>
|
||||
this.props.onChange(
|
||||
joinValues
|
||||
? [minValue, value.max].join(delimiter || ',')
|
||||
: {
|
||||
min: minValue,
|
||||
max: value.max
|
||||
}
|
||||
)
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
handleMaxInputBlur(evt: React.ChangeEvent<HTMLInputElement>) {
|
||||
const {joinValues, delimiter} = this.props;
|
||||
const maxValue = this.getValue(evt.target.value, 'max');
|
||||
const {value} = this.state;
|
||||
|
||||
if (isObject(value)) {
|
||||
this.setState(
|
||||
{
|
||||
value: {
|
||||
min: value.min,
|
||||
max: maxValue
|
||||
},
|
||||
maxValue: maxValue
|
||||
},
|
||||
() =>
|
||||
this.props.onChange(
|
||||
joinValues
|
||||
? [value.min, maxValue].join(delimiter || ',')
|
||||
: {
|
||||
min: value.min,
|
||||
max: maxValue
|
||||
}
|
||||
)
|
||||
const {multiple, joinValues, delimiter, onAfterChange} = this.props;
|
||||
onAfterChange &&
|
||||
onAfterChange(
|
||||
multiple
|
||||
? joinValues
|
||||
? [(value as MultipleValue).min, (value as MultipleValue).max].join(
|
||||
delimiter || ','
|
||||
)
|
||||
: {
|
||||
min: (value as MultipleValue).min,
|
||||
max: (value as MultipleValue).max
|
||||
}
|
||||
: value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleMinInputChange(evt: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({
|
||||
minValue: evt.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleMaxInputChange(evt: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({
|
||||
maxValue: evt.target.value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {value} = this.state;
|
||||
const props: RangeItemProps = {
|
||||
...this.props,
|
||||
value,
|
||||
updateValue: this.updateValue,
|
||||
onAfterChange: this.onAfterChange
|
||||
};
|
||||
|
||||
const {
|
||||
max,
|
||||
min,
|
||||
step,
|
||||
unit,
|
||||
clearable,
|
||||
name,
|
||||
disabled,
|
||||
className,
|
||||
showInput,
|
||||
classPrefix: ns,
|
||||
multiple,
|
||||
parts,
|
||||
showInput,
|
||||
classnames: cx,
|
||||
classPrefix: ns
|
||||
} = this.props as PropsWithDefaults;
|
||||
const midValue = ((max + min - 0.000001) / 2).toFixed(
|
||||
this.getStepPrecision()
|
||||
);
|
||||
className,
|
||||
disabled,
|
||||
clearable,
|
||||
min,
|
||||
max
|
||||
} = props;
|
||||
|
||||
// 指定parts -> 重新计算步长
|
||||
if (parts > 1) {
|
||||
props.step = (props.max - props.min) / props.parts;
|
||||
props.showSteps = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'RangeControl',
|
||||
{
|
||||
'RangeControl--withInput': showInput,
|
||||
'RangeControl--clearable': clearable,
|
||||
'is-multiple': multiple
|
||||
},
|
||||
`${ns}InputRange`,
|
||||
{'is-disabled': disabled},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<InputRange
|
||||
classPrefix={ns}
|
||||
value={this.state.value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
onChangeComplete={this.handleEnd}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
formatLabel={(value: any) => value + unit}
|
||||
multiple={multiple}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={cx('InputRange-label InputRange-label--mid')}
|
||||
ref={this.midLabelRef}
|
||||
>
|
||||
<span className={cx('InputRange-labelContainer')}>
|
||||
{midValue}
|
||||
{unit}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{showInput ? (
|
||||
multiple && isObject(this.state.value) ? (
|
||||
<div className={cx('InputRange-input is-multiple')}>
|
||||
<input
|
||||
className={this.state.value.min !== min ? 'is-active' : ''}
|
||||
type="text"
|
||||
name={name}
|
||||
value={this.state.minValue}
|
||||
disabled={disabled}
|
||||
onChange={this.handleMinInputChange}
|
||||
onBlur={this.handleMinInputBlur}
|
||||
/>
|
||||
<span className={cx('InputRange-input-separator')}> - </span>
|
||||
<input
|
||||
className={this.state.value.max !== max ? 'is-active' : ''}
|
||||
type="text"
|
||||
name={name}
|
||||
value={this.state.maxValue}
|
||||
disabled={disabled}
|
||||
onChange={this.handleMaxInputChange}
|
||||
onBlur={this.handleMaxInputBlur}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cx('InputRange-input')}>
|
||||
<input
|
||||
className={this.state.value !== min ? 'is-active' : ''}
|
||||
type="text"
|
||||
name={name}
|
||||
value={!isObject(this.state.value) ? this.state.value : 0}
|
||||
disabled={disabled}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{showInput && multiple && <Input {...props} type="min" />}
|
||||
<InputRange {...props} />
|
||||
{showInput && <Input {...props} type="max" />}
|
||||
{clearable && !disabled && showInput ? (
|
||||
<a
|
||||
onClick={() => this.clearValue()}
|
||||
|
Loading…
Reference in New Issue
Block a user