react-better-password
Version:
Password input for ReactJS that mimics mobile password masking behavior. Delays the masking of the last character to improve user accuracy.
108 lines (85 loc) • 3.04 kB
JavaScript
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
class ReactBetterPassword extends React.Component {
constructor(props) {
super(props);
this.state = {
unmasked: props.value,
value: Array(props.value.length + 1).join(props.mask),
};
this.onChange = this.onChange.bind(this);
}
componentWillReceiveProps(nextProps) {
const { mask } = this.props;
if(nextProps.value !== this.state.unmasked) {
this.setState({
unmasked: nextProps.value,
value: Array(nextProps.value.length + 1).join(nextProps.mask)
});
}
if(nextProps.mask && nextProps.mask !== mask) {
this.setState({ value: Array(this.state.value.length + 1).join(nextProps.mask)});
}
}
onChange(ev) {
const { timeout, onChange, mask } = this.props;
// Reset the previous showing character
clearTimeout(this.timer);
// Save the current cursor position to restore after masking
const cursorPos = this.input.selectionEnd;
const value = ev.target.value;
const { unmasked } = this.state;
// This is going to be the new original value (unmasked)
const newValue = value.replace(new RegExp(`${ cursorPos ? `(^\\${mask}{1,${cursorPos}})|` : '' }(\\${mask}+)`, 'g'), (match, part, offset, string) => {
if(!offset && cursorPos) return unmasked.substr(0, match.length);
else return unmasked.substr(-match.length);
});
// Mask the value leaving the last character entered intact
const maskedValue = value.split('').map((c, index) => index === cursorPos-1 ? c : mask).join('');
this.setState({ value: maskedValue, unmasked: newValue }, () => {
if(onChange) onChange(newValue);
// Restore cursor position
this.input.selectionStart = cursorPos;
this.input.selectionEnd = cursorPos;
// Set a timeout to replace the last showing character
this.timer = setTimeout(() => {
// Save user's current selection
const start = this.input.selectionStart;
const end = this.input.selectionEnd;
this.setState({ value: Array(value.length + 1).join(mask) }, () => {
// Restore cursor position once again
this.input.selectionStart = start;
this.input.selectionEnd = end;
});
}, timeout);
});
}
render() {
const { value, unmasked } = this.state;
const { show } = this.props;
const inputProps = _.omit(this.props, _.keys(ReactBetterPassword.propTypes));
return (
<input
{ ...inputProps }
ref={(input) => { this.input = input; }}
type='text'
value={ show ? unmasked : value }
onChange={this.onChange} />
);
}
}
ReactBetterPassword.propTypes = {
value: PropTypes.string,
mask: PropTypes.string,
timeout: PropTypes.number,
onChange: PropTypes.func,
show: PropTypes.bool,
};
ReactBetterPassword.defaultProps = {
value: '',
timeout: 1000,
mask: '•',
show: false,
};
export default ReactBetterPassword;