react-toolbox/components/input/Input.js

223 lines
7.1 KiB
JavaScript

import React, { PropTypes } from 'react';
import classnames from 'classnames';
import { themr } from 'react-css-themr';
import { INPUT } from '../identifiers';
import InjectedFontIcon from '../font_icon/FontIcon';
const factory = (FontIcon) => {
class Input extends React.Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
error: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
]),
floating: PropTypes.bool,
hint: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
]),
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
]),
maxLength: PropTypes.number,
multiline: PropTypes.bool,
name: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onFocus: PropTypes.func,
onKeyPress: PropTypes.func,
required: PropTypes.bool,
rows: PropTypes.number,
theme: PropTypes.shape({
bar: PropTypes.string,
counter: PropTypes.string,
disabled: PropTypes.string,
error: PropTypes.string,
errored: PropTypes.string,
hidden: PropTypes.string,
hint: PropTypes.string,
icon: PropTypes.string,
input: PropTypes.string,
inputElement: PropTypes.string,
required: PropTypes.string,
withIcon: PropTypes.string,
}),
type: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
]),
};
static defaultProps = {
className: '',
hint: '',
disabled: false,
floating: true,
multiline: false,
required: false,
type: 'text',
};
componentDidMount() {
if (this.props.multiline) {
window.addEventListener('resize', this.handleAutoresize);
this.handleAutoresize();
}
}
componentWillReceiveProps(nextProps) {
if (!this.props.multiline && nextProps.multiline) {
window.addEventListener('resize', this.handleAutoresize);
} else if (this.props.multiline && !nextProps.multiline) {
window.removeEventListener('resize', this.handleAutoresize);
}
}
componentDidUpdate() {
// resize the textarea, if nessesary
if (this.props.multiline) this.handleAutoresize();
}
componentWillUnmount() {
if (this.props.multiline) window.removeEventListener('resize', this.handleAutoresize);
}
handleChange = (event) => {
const { onChange, multiline, maxLength } = this.props;
const valueFromEvent = event.target.value;
// Trim value to maxLength if that exists (only on multiline inputs).
// Note that this is still required even tho we have the onKeyPress filter
// because the user could paste smt in the textarea.
const haveToTrim = (multiline && maxLength && event.target.value.length > maxLength);
const value = haveToTrim ? valueFromEvent.substr(0, maxLength) : valueFromEvent;
// propagate to to store and therefore to the input
if (onChange) onChange(value, event);
};
handleAutoresize = () => {
const element = this.inputNode;
const rows = this.props.rows;
if (typeof rows === 'number' && !isNaN(rows)) {
element.style.height = null;
} else {
// compute the height difference between inner height and outer height
const style = getComputedStyle(element, null);
const heightOffset = style.boxSizing === 'content-box'
? -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom))
: parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
// resize the input to its content size
element.style.height = 'auto';
element.style.height = `${element.scrollHeight + heightOffset}px`;
}
}
handleKeyPress = (event) => {
// prevent insertion of more characters if we're a multiline input
// and maxLength exists
const { multiline, maxLength, onKeyPress } = this.props;
if (multiline && maxLength) {
// check if smt is selected, in which case the newly added charcter would
// replace the selected characters, so the length of value doesn't actually
// increase.
const isReplacing = event.target.selectionEnd - event.target.selectionStart;
const value = event.target.value;
if (!isReplacing && value.length === maxLength) {
event.preventDefault();
event.stopPropagation();
return undefined;
}
}
if (onKeyPress) onKeyPress(event);
return undefined;
};
blur() {
this.inputNode.blur();
}
focus() {
this.inputNode.focus();
}
render() {
const { children, disabled, error, floating, hint, icon,
name, label: labelText, maxLength, multiline, required,
theme, type, value, onKeyPress, rows = 1, ...others } = this.props;
const length = maxLength && value ? value.length : 0;
const labelClassName = classnames(theme.label, { [theme.fixed]: !floating });
const className = classnames(theme.input, {
[theme.disabled]: disabled,
[theme.errored]: error,
[theme.hidden]: type === 'hidden',
[theme.withIcon]: icon,
}, this.props.className);
const valuePresent = value !== null
&& value !== undefined
&& value !== ''
&& !(typeof value === Number && isNaN(value)); // eslint-disable-line
const inputElementProps = {
...others,
className: classnames(theme.inputElement, { [theme.filled]: valuePresent }),
onChange: this.handleChange,
ref: (node) => { this.inputNode = node; },
role: 'input',
name,
disabled,
required,
type,
value,
};
if (!multiline) {
inputElementProps.maxLength = maxLength;
inputElementProps.onKeyPress = onKeyPress;
} else {
inputElementProps.rows = rows;
inputElementProps.onKeyPress = this.handleKeyPress;
}
return (
<div data-react-toolbox="input" className={className}>
{React.createElement(multiline ? 'textarea' : 'input', inputElementProps)}
{icon ? <FontIcon className={theme.icon} value={icon} /> : null}
<span className={theme.bar} />
{labelText
? <label className={labelClassName} htmlFor={name}>
{labelText}
{required ? <span className={theme.required}> * </span> : null}
</label>
: null}
{hint ? <span hidden={labelText} className={theme.hint}>{hint}</span> : null}
{error ? <span className={theme.error}>{error}</span> : null}
{maxLength ? <span className={theme.counter}>{length}/{maxLength}</span> : null}
{children}
</div>
);
}
}
return Input;
};
const Input = factory(InjectedFontIcon);
export default themr(INPUT, null, { withRef: true })(Input);
export { factory as inputFactory };
export { Input };