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:
kano 2022-02-18 12:44:57 +08:00 committed by GitHub
parent e5b44cef2d
commit d7738f3f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2700 additions and 806 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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();
});

View File

@ -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 &#124; string]: ReactNode }</code> or <code>{ [number &#124; 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` 触发时机一致,把当前值作为参数传入 |

View File

@ -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)};

View File

@ -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);
}
}

View File

@ -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事件
* mousemovemouseup
*/
@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事件
* mousemovemouseup
*/
@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>
);
}
}

View File

@ -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,

View 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

View File

@ -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()}