From 3578f9774ec175062a308de335c93f68aeb7cd56 Mon Sep 17 00:00:00 2001 From: wangxueliang Date: Wed, 4 Apr 2018 09:44:19 +0800 Subject: [PATCH] add vc-m-feedback --- components/vc-input-number/assets/index.less | 115 ++++ .../vc-input-number/src/InputHandler.js | 37 ++ components/vc-input-number/src/index.js | 610 ++++++++++++++++++ components/vc-m-feedback/demo/simple.jsx | 17 + components/vc-m-feedback/demo/simple.less | 7 + components/vc-m-feedback/index.js | 3 + components/vc-m-feedback/src/PropTypes.jsx | 13 + .../vc-m-feedback/src/TouchFeedback.jsx | 98 +++ examples/routes.js | 2 +- 9 files changed, 901 insertions(+), 1 deletion(-) create mode 100755 components/vc-input-number/assets/index.less create mode 100755 components/vc-input-number/src/InputHandler.js create mode 100755 components/vc-input-number/src/index.js create mode 100644 components/vc-m-feedback/demo/simple.jsx create mode 100644 components/vc-m-feedback/demo/simple.less create mode 100755 components/vc-m-feedback/index.js create mode 100755 components/vc-m-feedback/src/PropTypes.jsx create mode 100755 components/vc-m-feedback/src/TouchFeedback.jsx diff --git a/components/vc-input-number/assets/index.less b/components/vc-input-number/assets/index.less new file mode 100755 index 000000000..e1cc8df2b --- /dev/null +++ b/components/vc-input-number/assets/index.less @@ -0,0 +1,115 @@ +@inputNumberPrefixCls: rc-input-number; + +.@{inputNumberPrefixCls} { + margin: 0; + padding: 0; + line-height: 26px; + font-size: 12px; + height: 26px; + display: inline-block; + vertical-align: middle; + border: 1px solid #D9D9D9; + border-radius: 5px; + + &-handler { + text-align: center; + line-height: 12px; + height: 12px; + overflow: hidden; + display:block; + touch-action: none; + + &-active { + background: #ddd; + } + } + + &-handler-up-inner, &-handler-down-inner { + color: #666666; + user-select: none; + -webkit-user-select: none; + } + + &:hover { + border-color: #23c0fa; + + .@{inputNumberPrefixCls}-handler-up, .@{inputNumberPrefixCls}-handler-wrap { + border-color: #23c0fa; + } + } + + &-disabled:hover { + border-color: #d9d9d9; + + .@{inputNumberPrefixCls}-handler-up, .@{inputNumberPrefixCls}-handler-wrap { + border-color: #d9d9d9; + } + } + + &-input-wrap { + overflow: hidden; + height: 26px; + } + + &-input { + width: 100%; + text-align: center; + outline: 0; + -moz-appearance: textfield; + line-height: 26px; + height: 26px; + transition: all 0.3s ease; + color: #666666; + border: 0; + border-radius: 5px; + padding: 0; + } + + &-handler-wrap { + float: right; + border-left: 1px solid #D9D9D9; + width: 20px; + height: 26px; + } + + &-handler-up { + border-bottom: 1px solid #D9D9D9; + padding-top: 1px; + &-inner { + &:after { + content: '+'; + } + } + } + + &-handler-down { + &-inner { + &:after { + content: '-'; + } + } + } + + .handler-disabled() { + opacity: 0.72; + &:hover { + color: #999; + border-color: #d9d9d9; + } + } + + &-handler-down-disabled, &-handler-up-disabled { + .handler-disabled(); + } + + &-disabled { + .@{inputNumberPrefixCls}-input { + opacity: 0.72; + cursor: not-allowed; + background-color: #f3f3f3; + } + .@{inputNumberPrefixCls}-handler { + .handler-disabled(); + } + } +} diff --git a/components/vc-input-number/src/InputHandler.js b/components/vc-input-number/src/InputHandler.js new file mode 100755 index 000000000..2e079c5ca --- /dev/null +++ b/components/vc-input-number/src/InputHandler.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Touchable from 'rmc-feedback'; + +class InputHandler extends Component { + render() { + const { + prefixCls, disabled, onTouchStart, onTouchEnd, + onMouseDown, onMouseUp, onMouseLeave, ...otherProps, + } = this.props; + return ( + + + + ); + } +} + +InputHandler.propTypes = { + prefixCls: PropTypes.string, + disabled: PropTypes.bool, + onTouchStart: PropTypes.func, + onTouchEnd: PropTypes.func, + onMouseDown: PropTypes.func, + onMouseUp: PropTypes.func, + onMouseLeave: PropTypes.func, +}; + +export default InputHandler; diff --git a/components/vc-input-number/src/index.js b/components/vc-input-number/src/index.js new file mode 100755 index 000000000..293dc9270 --- /dev/null +++ b/components/vc-input-number/src/index.js @@ -0,0 +1,610 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import isNegativeZero from 'is-negative-zero'; +import InputHandler from './InputHandler'; + +function noop() { +} + +function preventDefault(e) { + e.preventDefault(); +} + +function defaultParser(input) { + return input.replace(/[^\w\.-]+/g, ''); +} + +/** + * When click and hold on a button - the speed of auto changin the value. + */ +const SPEED = 200; + +/** + * When click and hold on a button - the delay before auto changin the value. + */ +const DELAY = 600; + +/** + * Max Safe Integer -- on IE this is not available, so manually set the number in that case. + * The reason this is used, instead of Infinity is because numbers above the MSI are unstable + */ +const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1; + +export default class InputNumber extends React.Component { + static propTypes = { + value: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + defaultValue: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + focusOnUpDown: PropTypes.bool, + autoFocus: PropTypes.bool, + onChange: PropTypes.func, + onKeyDown: PropTypes.func, + onKeyUp: PropTypes.func, + prefixCls: PropTypes.string, + tabIndex: PropTypes.string, + disabled: PropTypes.bool, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + readOnly: PropTypes.bool, + max: PropTypes.number, + min: PropTypes.number, + step: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + upHandler: PropTypes.node, + downHandler: PropTypes.node, + useTouch: PropTypes.bool, + formatter: PropTypes.func, + parser: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onMouseOver: PropTypes.func, + onMouseOut: PropTypes.func, + precision: PropTypes.number, + required: PropTypes.bool, + pattern: PropTypes.string, + } + + static defaultProps = { + focusOnUpDown: true, + useTouch: false, + prefixCls: 'rc-input-number', + min: -MAX_SAFE_INTEGER, + step: 1, + style: {}, + onChange: noop, + onKeyDown: noop, + onFocus: noop, + onBlur: noop, + parser: defaultParser, + required: false, + } + + constructor(props) { + super(props); + + let value; + if ('value' in props) { + value = props.value; + } else { + value = props.defaultValue; + } + value = this.toNumber(value); + + this.state = { + inputValue: this.toPrecisionAsStep(value), + value, + focused: props.autoFocus, + }; + } + + componentDidMount() { + this.componentDidUpdate(); + } + + componentWillReceiveProps(nextProps) { + if ('value' in nextProps) { + const value = this.state.focused + ? nextProps.value : this.getValidValue(nextProps.value, nextProps.min, nextProps.max); + this.setState({ + value, + inputValue: this.inputting ? value : this.toPrecisionAsStep(value), + }); + } + } + + componentWillUpdate() { + try { + this.start = this.input.selectionStart; + this.end = this.input.selectionEnd; + } catch (e) { + // Fix error in Chrome: + // Failed to read the 'selectionStart' property from 'HTMLInputElement' + // http://stackoverflow.com/q/21177489/3040605 + } + } + + componentDidUpdate() { + // pressingUpOrDown is true means that someone just click up or down button + // https://github.com/ant-design/ant-design/issues/9204 + if (!this.pressingUpOrDown) { + return; + } + if (this.props.focusOnUpDown && this.state.focused) { + const selectionRange = this.input.setSelectionRange; + if (selectionRange && + typeof selectionRange === 'function' && + this.start !== undefined && + this.end !== undefined) { + this.input.setSelectionRange(this.start, this.end); + } else { + this.focus(); + } + this.pressingUpOrDown = false; + } + } + + componentWillUnmount() { + this.stop(); + } + + onKeyDown = (e, ...args) => { + if (e.keyCode === 38) { + const ratio = this.getRatio(e); + this.up(e, ratio); + this.stop(); + } else if (e.keyCode === 40) { + const ratio = this.getRatio(e); + this.down(e, ratio); + this.stop(); + } + const { onKeyDown } = this.props; + if (onKeyDown) { + onKeyDown(e, ...args); + } + } + + onKeyUp = (e, ...args) => { + this.stop(); + const { onKeyUp } = this.props; + if (onKeyUp) { + onKeyUp(e, ...args); + } + } + + onChange = (e) => { + if (this.state.focused) { + this.inputting = true; + } + const input = this.props.parser(this.getValueFromEvent(e)); + this.setState({ inputValue: input }); + this.props.onChange(this.toNumberWhenUserInput(input)); // valid number or invalid string + } + + onFocus = (...args) => { + this.setState({ + focused: true, + }); + this.props.onFocus(...args); + } + + onBlur = (e, ...args) => { + this.inputting = false; + this.setState({ + focused: false, + }); + const value = this.getCurrentValidValue(this.state.inputValue); + e.persist(); // fix https://github.com/react-component/input-number/issues/51 + this.setValue(value, () => { + this.props.onBlur(e, ...args); + }); + } + + getCurrentValidValue(value) { + let val = value; + if (val === '') { + val = ''; + } else if (!this.isNotCompleteNumber(val)) { + val = this.getValidValue(val); + } else { + val = this.state.value; + } + return this.toNumber(val); + } + + getRatio(e) { + let ratio = 1; + if (e.metaKey || e.ctrlKey) { + ratio = 0.1; + } else if (e.shiftKey) { + ratio = 10; + } + return ratio; + } + + getValueFromEvent(e) { + // optimize for chinese input expierence + // https://github.com/ant-design/ant-design/issues/8196 + return e.target.value.trim().replace(/。/g, '.'); + } + + getValidValue(value, min = this.props.min, max = this.props.max) { + let val = parseFloat(value, 10); + // https://github.com/ant-design/ant-design/issues/7358 + if (isNaN(val)) { + return value; + } + if (val < min) { + val = min; + } + if (val > max) { + val = max; + } + return val; + } + + setValue(v, callback) { + // trigger onChange + const newValue = this.isNotCompleteNumber(parseFloat(v, 10)) ? undefined : parseFloat(v, 10); + const changed = newValue !== this.state.value || + `${newValue}` !== `${this.state.inputValue}`; // https://github.com/ant-design/ant-design/issues/7363 + if (!('value' in this.props)) { + this.setState({ + value: newValue, + inputValue: this.toPrecisionAsStep(v), + }, callback); + } else { + // always set input value same as value + this.setState({ + inputValue: this.toPrecisionAsStep(this.state.value), + }, callback); + } + if (changed) { + this.props.onChange(newValue); + } + } + + getPrecision(value) { + if ('precision' in this.props) { + return this.props.precision; + } + const valueString = value.toString(); + if (valueString.indexOf('e-') >= 0) { + return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10); + } + let precision = 0; + if (valueString.indexOf('.') >= 0) { + precision = valueString.length - valueString.indexOf('.') - 1; + } + return precision; + } + + // step={1.0} value={1.51} + // press + + // then value should be 2.51, rather than 2.5 + // if this.props.precision is undefined + // https://github.com/react-component/input-number/issues/39 + getMaxPrecision(currentValue, ratio = 1) { + if ('precision' in this.props) { + return this.props.precision; + } + const { step } = this.props; + const ratioPrecision = this.getPrecision(ratio); + const stepPrecision = this.getPrecision(step); + const currentValuePrecision = this.getPrecision(currentValue); + if (!currentValue) { + return ratioPrecision + stepPrecision; + } + return Math.max(currentValuePrecision, ratioPrecision + stepPrecision); + } + + getPrecisionFactor(currentValue, ratio = 1) { + const precision = this.getMaxPrecision(currentValue, ratio); + return Math.pow(10, precision); + } + + focus() { + this.input.focus(); + } + + blur() { + this.input.blur(); + } + + formatWrapper(num) { + // http://2ality.com/2012/03/signedzero.html + // https://github.com/ant-design/ant-design/issues/9439 + if (isNegativeZero(num)) { + return '-0'; + } + if (this.props.formatter) { + return this.props.formatter(num); + } + return num; + } + + toPrecisionAsStep(num) { + if (this.isNotCompleteNumber(num) || num === '') { + return num; + } + const precision = Math.abs(this.getMaxPrecision(num)); + if (precision === 0) { + return num.toString(); + } + if (!isNaN(precision)) { + return Number(num).toFixed(precision); + } + return num.toString(); + } + + // '1.' '1x' 'xx' '' => are not complete numbers + isNotCompleteNumber(num) { + return ( + isNaN(num) || + num === '' || + num === null || + (num && num.toString().indexOf('.') === num.toString().length - 1) + ); + } + + toNumber(num) { + if (this.isNotCompleteNumber(num)) { + return num; + } + if ('precision' in this.props) { + return Number(Number(num).toFixed(this.props.precision)); + } + return Number(num); + } + + // '1.0' '1.00' => may be a inputing number + toNumberWhenUserInput(num) { + // num.length > 16 => prevent input large number will became Infinity + if ((/\.\d*0$/.test(num) || num.length > 16) && this.state.focused) { + return num; + } + return this.toNumber(num); + } + + upStep(val, rat) { + const { step, min } = this.props; + const precisionFactor = this.getPrecisionFactor(val, rat); + const precision = Math.abs(this.getMaxPrecision(val, rat)); + let result; + if (typeof val === 'number') { + result = + ((precisionFactor * val + precisionFactor * step * rat) / + precisionFactor).toFixed(precision); + } else { + result = min === -Infinity ? step : min; + } + return this.toNumber(result); + } + + downStep(val, rat) { + const { step, min } = this.props; + const precisionFactor = this.getPrecisionFactor(val, rat); + const precision = Math.abs(this.getMaxPrecision(val, rat)); + let result; + if (typeof val === 'number') { + result = + ((precisionFactor * val - precisionFactor * step * rat) / + precisionFactor).toFixed(precision); + } else { + result = min === -Infinity ? -step : min; + } + return this.toNumber(result); + } + + step(type, e, ratio = 1, recursive) { + this.stop(); + if (e) { + e.persist(); + e.preventDefault(); + } + const props = this.props; + if (props.disabled) { + return; + } + const value = this.getCurrentValidValue(this.state.inputValue) || 0; + if (this.isNotCompleteNumber(value)) { + return; + } + let val = this[`${type}Step`](value, ratio); + const outOfRange = val > props.max || val < props.min; + if (val > props.max) { + val = props.max; + } else if (val < props.min) { + val = props.min; + } + this.setValue(val); + this.setState({ + focused: true, + }); + if (outOfRange) { + return; + } + this.autoStepTimer = setTimeout(() => { + this[type](e, ratio, true); + }, recursive ? SPEED : DELAY); + } + + stop = () => { + if (this.autoStepTimer) { + clearTimeout(this.autoStepTimer); + } + } + + down = (e, ratio, recursive) => { + this.pressingUpOrDown = true; + this.step('down', e, ratio, recursive); + } + + up = (e, ratio, recursive) => { + this.pressingUpOrDown = true; + this.step('up', e, ratio, recursive); + } + + saveInput = (node) => { + this.input = node; + } + + render() { + const props = { ...this.props }; + const { prefixCls, disabled, readOnly, useTouch } = props; + const classes = classNames({ + [prefixCls]: true, + [props.className]: !!props.className, + [`${prefixCls}-disabled`]: disabled, + [`${prefixCls}-focused`]: this.state.focused, + }); + let upDisabledClass = ''; + let downDisabledClass = ''; + const { value } = this.state; + if (value || value === 0) { + if (!isNaN(value)) { + const val = Number(value); + if (val >= props.max) { + upDisabledClass = `${prefixCls}-handler-up-disabled`; + } + if (val <= props.min) { + downDisabledClass = `${prefixCls}-handler-down-disabled`; + } + } else { + upDisabledClass = `${prefixCls}-handler-up-disabled`; + downDisabledClass = `${prefixCls}-handler-down-disabled`; + } + } + + const editable = !props.readOnly && !props.disabled; + + // focus state, show input value + // unfocus state, show valid value + let inputDisplayValue; + if (this.state.focused) { + inputDisplayValue = this.state.inputValue; + } else { + inputDisplayValue = this.toPrecisionAsStep(this.state.value); + } + + if (inputDisplayValue === undefined || inputDisplayValue === null) { + inputDisplayValue = ''; + } + + let upEvents; + let downEvents; + if (useTouch) { + upEvents = { + onTouchStart: (editable && !upDisabledClass) ? this.up : noop, + onTouchEnd: this.stop, + }; + downEvents = { + onTouchStart: (editable && !downDisabledClass) ? this.down : noop, + onTouchEnd: this.stop, + }; + } else { + upEvents = { + onMouseDown: (editable && !upDisabledClass) ? this.up : noop, + onMouseUp: this.stop, + onMouseLeave: this.stop, + }; + downEvents = { + onMouseDown: (editable && !downDisabledClass) ? this.down : noop, + onMouseUp: this.stop, + onMouseLeave: this.stop, + }; + } + const inputDisplayValueFormat = this.formatWrapper(inputDisplayValue); + const isUpDisabled = !!upDisabledClass || disabled || readOnly; + const isDownDisabled = !!downDisabledClass || disabled || readOnly; + // ref for test + return ( +
+
+ + {this.props.upHandler || } + + + {this.props.downHandler || } + +
+
+ +
+
+ ); + } +} diff --git a/components/vc-m-feedback/demo/simple.jsx b/components/vc-m-feedback/demo/simple.jsx new file mode 100644 index 000000000..6e28fe781 --- /dev/null +++ b/components/vc-m-feedback/demo/simple.jsx @@ -0,0 +1,17 @@ +import TouchFeedback from '../index' +import './simple.less' + +export default { + render () { + return ( +
+ +
console.log('click div')}>click to active
+
+
+ ) + }, +} diff --git a/components/vc-m-feedback/demo/simple.less b/components/vc-m-feedback/demo/simple.less new file mode 100644 index 000000000..64b495b33 --- /dev/null +++ b/components/vc-m-feedback/demo/simple.less @@ -0,0 +1,7 @@ +.normal { + color: '#000' +} + +.active { + font-size: 40px; +} diff --git a/components/vc-m-feedback/index.js b/components/vc-m-feedback/index.js new file mode 100755 index 000000000..09cf6add8 --- /dev/null +++ b/components/vc-m-feedback/index.js @@ -0,0 +1,3 @@ + +import TouchFeedback from './src/TouchFeedback' +export default TouchFeedback diff --git a/components/vc-m-feedback/src/PropTypes.jsx b/components/vc-m-feedback/src/PropTypes.jsx new file mode 100755 index 000000000..3d65d05a0 --- /dev/null +++ b/components/vc-m-feedback/src/PropTypes.jsx @@ -0,0 +1,13 @@ +import PropTypes from '../../_util/vue-types' + +export const ITouchProps = { + disabled: PropTypes.bool, + activeClassName: PropTypes.string, + activeStyle: PropTypes.any, + // onTouchStart: PropTypes.func, + // onTouchEnd: PropTypes.func, + // onTouchCancel: PropTypes.func, + // onMouseDown: PropTypes.func, + // onMouseUp: PropTypes.func, + // onMouseLeave: PropTypes.func, +} diff --git a/components/vc-m-feedback/src/TouchFeedback.jsx b/components/vc-m-feedback/src/TouchFeedback.jsx new file mode 100755 index 000000000..336f1b4b3 --- /dev/null +++ b/components/vc-m-feedback/src/TouchFeedback.jsx @@ -0,0 +1,98 @@ +import { initDefaultProps } from '../../_util/props-util' +import { cloneElement } from '../../_util/vnode' +import warning from '../../_util/warning' +import BaseMixin from '../../_util/BaseMixin' +import { ITouchProps } from './PropTypes' + +export default { + name: 'TouchFeedback', + mixins: [BaseMixin], + props: initDefaultProps(ITouchProps, { + disabled: false, + }), + data () { + return { + active: false, + } + }, + mounted () { + this.$nextTick(() => { + if (this.disabled && this.active) { + this.setState({ + active: false, + }) + } + }) + }, + methods: { + triggerEvent (type, isActive, ev) { + // const eventType = `on${type}` + // if (this.props[eventType]) { + // this.props[eventType](ev) + // } + this.$emit(type, ev) + if (isActive !== this.active) { + this.setState({ + active: isActive, + }) + } + }, + onTouchStart (e) { + this.triggerEvent('touchstart', true, e) + }, + onTouchMove (e) { + this.triggerEvent('touchmove', false, e) + }, + onTouchEnd (e) { + this.triggerEvent('touchend', false, e) + }, + onTouchCancel (e) { + this.triggerEvent('touchcancel', false, e) + }, + onMouseDown (e) { + // todo + // pc simulate mobile + // if (this.props.onTouchStart) { + this.triggerEvent('touchstart', true, e) + // } + this.triggerEvent('mousedown', true, e) + }, + onMouseUp (e) { + // if (this.props.onTouchEnd) { + this.triggerEvent('touchend', false, e) + // } + this.triggerEvent('mouseup', false, e) + }, + onMouseLeave (e) { + this.triggerEvent('mouseleave', false, e) + }, + }, + render () { + const { disabled, activeClassName = '', activeStyle = {}} = this.$props + + const child = this.$slots.default + if (child.length !== 1) { + warning(false, '只能包含一个子元素') + } + let childProps = { + on: disabled ? {} : { + touchstart: this.onTouchStart, + touchmove: this.onTouchMove, + touchend: this.onTouchEnd, + touchcancel: this.onTouchCancel, + mousedown: this.onMouseDown, + mouseup: this.onMouseUp, + mouseleave: this.onMouseLeave, + }, + } + + if (!disabled && this.active) { + childProps = { ...childProps, ...{ + style: activeStyle, + class: activeClassName, + }} + } + + return cloneElement(child, childProps) + }, +} diff --git a/examples/routes.js b/examples/routes.js index d9670c48e..e6820c0c8 100644 --- a/examples/routes.js +++ b/examples/routes.js @@ -3,7 +3,7 @@ const AsyncComp = () => { const hashs = window.location.hash.split('/') const d = hashs[hashs.length - 1] return { - component: import(`../components/grid/demo/${d}`), + component: import(`../components/vc-m-feedback/demo/${d}`), } } export default [