Created
April 13, 2017 21:22
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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