Skip to content

Instantly share code, notes, and snippets.

@zanzamar
Created April 13, 2017 21:22
Show Gist options
  • Save zanzamar/ff87834ce3be898bb73fef6c57a61713 to your computer and use it in GitHub Desktop.
Save zanzamar/ff87834ce3be898bb73fef6c57a61713 to your computer and use it in GitHub Desktop.
A wrapper for react toolbox's input to utilize the InputMask utility for providing a masked input.
/* global document, navigator */
/**
* MaskedInput
*
* Provides a wrapper to the react-toolbox/lib/input using inputmask-core
*
* Original Source: https://gist.github.com/romgrk/9513d238e00bb5df60af7b2f661bbe17
* Which had the following comment:
* Shamelessly copied from: https://github.com/insin/react-maskedinput (MIT License)
*/
import React, { Component } from 'react';
import Input from 'react-toolbox/lib/input';
import InputMask from 'inputmask-core';
const KEYCODE_Z = 90;
const KEYCODE_Y = 89;
function isUndo(e) {
return (e.ctrlKey || e.metaKey) && e.keyCode === (e.shiftKey ? KEYCODE_Y : KEYCODE_Z);
}
function isRedo(e) {
return (e.ctrlKey || e.metaKey) && e.keyCode === (e.shiftKey ? KEYCODE_Z : KEYCODE_Y);
}
function getSelection(el) {
let start;
let end;
let rangeEl;
let clone;
if (el.selectionStart !== undefined) {
start = el.selectionStart;
end = el.selectionEnd;
} else {
try {
el.focus();
rangeEl = el.createTextRange();
clone = rangeEl.duplicate();
rangeEl.moveToBookmark(document.selection.createRange().getBookmark());
clone.setEndPoint('EndToStart', rangeEl);
start = clone.text.length;
end = start + rangeEl.text.length;
} catch (e) { /* not focused or not visible */ }
}
return { start, end };
}
function setSelection(el, selection) {
let rangeEl;
try {
if (el.selectionStart !== undefined) {
el.focus();
el.setSelectionRange(selection.start, selection.end);
} else {
el.focus();
rangeEl = el.createTextRange();
rangeEl.collapse(true);
rangeEl.moveStart('character', selection.start);
rangeEl.moveEnd('character', selection.end - selection.start);
rangeEl.select();
}
} catch (e) { /* not focused or not visible */ }
}
function keyPressPropName() {
if (typeof navigator !== 'undefined') {
return navigator.userAgent.match(/Android/i) ? 'onBeforeInput' : 'onKeyPress';
}
return 'onKeyPress';
}
export default class MaskedInput extends Component {
static propTypes = {
mask: React.PropTypes.string.isRequired,
formatCharacters: React.PropTypes.object,
placeholderChar: React.PropTypes.string,
value: React.PropTypes.string,
placeholder: React.PropTypes.string,
onChange: React.PropTypes.func,
size: React.PropTypes.number,
type: React.PropTypes.string,
};
constructor(props) {
super(props);
this.updatePattern = this.updatePattern.bind(this);
this.updateMaskSelection = this.updateMaskSelection.bind(this);
this.updateInputSelection = this.updateInputSelection.bind(this);
this.onChange = this.onChange.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onPaste = this.onPaste.bind(this);
this.focus = this.focus.bind(this);
this.blur = this.blur.bind(this);
this.getEventHandlers = this.getEventHandlers.bind(this);
this.getDisplayValue = this.getDisplayValue.bind(this);
this.state = {
value: '',
};
}
componentWillMount() {
const { formatCharacters, mask, placeholderChar, value } = this.props;
const options = {
pattern: mask,
value,
formatCharacters,
};
if (placeholderChar) {
options.placeholderChar = placeholderChar;
}
this.inputMask = new InputMask(options);
}
componentWillReceiveProps(nextProps) {
const { mask, value } = this.props;
if (mask !== nextProps.mask && value !== nextProps.mask) {
// if we get a new value and a new mask at the same time
// check if the mask.value is still the initial value
// - if so use the nextProps value
// - otherwise the `this.inputMask` has a value for us (most likely from paste action)
if (this.inputMask.getValue() === this.inputMask.emptyValue) {
this.inputMask.setPattern(nextProps.mask, { value: nextProps.value });
} else {
this.inputMask.setPattern(nextProps.mask, { value: this.inputMask.getRawValue() });
}
} else if (mask !== nextProps.mask) {
this.inputMask.setPattern(nextProps.mask, { value: this.inputMask.getRawValue() });
} else if (value !== nextProps.value) {
this.inputMask.setValue(nextProps.value);
}
}
componentWillUpdate(nextProps) {
if (nextProps.mask !== this.props.mask) {
this.updatePattern(nextProps);
}
}
componentDidUpdate(prevProps) {
if (prevProps.mask !== this.props.mask && this.inputMask.selection.start) {
this.updateInputSelection();
}
}
onChange(newValue) {
let value;
const maskValue = this.inputMask.getValue();
if (newValue !== maskValue) {
// Cut or delete operations will have shortened the value
if (newValue.length < maskValue.length) {
const sizeDiff = maskValue.length - newValue.length;
this.updateMaskSelection();
this.inputMask.selection.end = this.inputMask.selection.start + sizeDiff;
this.inputMask.backspace();
}
value = this.getDisplayValue();
this.setState({ value });
if (value) {
this.updateInputSelection();
}
}
if (this.props.onChange) {
this.props.onChange(value);
}
}
onKeyDown(e) {
if (isUndo(e)) {
e.preventDefault();
if (this.inputMask.undo()) {
e.target.value = this.getDisplayValue();
this.updateInputSelection();
if (this.props.onChange) {
this.props.onChange(this.getDisplayValue());
}
}
return;
} else if (isRedo(e)) {
e.preventDefault();
if (this.inputMask.redo()) {
e.target.value = this.getDisplayValue();
this.updateInputSelection();
if (this.props.onChange) {
this.props.onChange(this.getDisplayValue());
}
}
return;
}
if (e.key === 'Backspace') {
e.preventDefault();
this.updateMaskSelection();
if (this.inputMask.backspace()) {
const value = this.getDisplayValue();
e.target.value = value;
if (value) {
this.updateInputSelection();
}
if (this.props.onChange) {
this.props.onChange(this.getDisplayValue());
}
}
}
}
onKeyPress(e) {
// Ignore modified key presses
// Ignore enter key to allow form submission
if (e.metaKey || e.altKey || e.ctrlKey || e.key === 'Enter') { return; }
e.preventDefault();
this.updateMaskSelection();
if (this.inputMask.input((e.key || e.data))) {
e.target.value = this.inputMask.getValue();
this.updateInputSelection();
if (this.props.onChange) {
this.props.onChange(this.inputMask.getValue());
}
}
}
onPaste(e) {
e.preventDefault();
this.updateMaskSelection();
// getData value needed for IE also works in FF & Chrome
if (this.inputMask.paste(e.clipboardData.getData('Text'))) {
e.target.value = this.inputMask.getValue();
// Timeout needed for IE
setTimeout(this.updateInputSelection, 0);
if (this.props.onChange) {
this.props.onChange(this.inputMask.getValue());
}
}
}
/**
* getDisplayValue
*
* We only want to return a display value if we have one, ensuring we are able to see the field label when
* not in focus.
*
* @return {String}
*/
getDisplayValue() {
return this.props.value ? this.inputMask.getValue() : '';
}
getEventHandlers() {
return {
onChange: this.onChange,
onKeyDown: this.onKeyDown,
onPaste: this.onPaste,
[keyPressPropName()]: this.onKeyPress,
};
}
updatePattern(props) {
this.inputMask.setPattern(
props.mask,
{
value: this.inputMask.getRawValue(),
selection: getSelection(this.input),
}
);
}
updateMaskSelection() {
this.inputMask.selection = getSelection(this.input);
}
updateInputSelection() {
setSelection(this.input, this.inputMask.selection);
}
focus() {
this.input.focus();
}
blur() {
this.input.blur();
}
inputMask = undefined;
render() {
const ref = (r) => { if (r) { this.input = r.refs.wrappedInstance.inputNode; } };
const maxLength = this.inputMask.pattern.length;
const value = this.getDisplayValue();
const eventHandlers = this.getEventHandlers();
const { size = maxLength, placeholder = this.inputMask.emptyValue } = this.props;
const { type, placeholderChar, formatCharacters, ...cleanedProps } = this.props;
const inputProps = { ...cleanedProps, ...eventHandlers, ref, value, size, hint: placeholder, type };
return <Input {...inputProps} />;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment