diff options
Diffstat (limited to 'app/components')
| -rw-r--r-- | app/components/AccountInput.js | 184 | ||||
| -rw-r--r-- | app/components/Login.js | 21 |
2 files changed, 190 insertions, 15 deletions
diff --git a/app/components/AccountInput.js b/app/components/AccountInput.js new file mode 100644 index 0000000000..56097f5e74 --- /dev/null +++ b/app/components/AccountInput.js @@ -0,0 +1,184 @@ +import React, { Component, PropTypes } from 'react'; +import { formatAccount } from '../lib/formatters'; + +export default class AccountInput extends Component { + + static propTypes = { + value: PropTypes.string, + onEnter: PropTypes.func, + onChange: PropTypes.func + } + + constructor(props) { + super(props); + + this.state = { value: '', selectionRange: [0,0] }; + } + + componentWillReceiveProps(nextProps) { + const nextVal = (nextProps.value || '').replace(/[^0-9]/g, ''); + if(nextVal !== this.state.value) { + const len = nextVal.length; + this.setState({ value: nextVal, selectionRange: [len, len] }); + } + } + + shouldComponentUpdate(nextProps, nextState) { + return (this.props.value !== nextProps.value || + this.props.onEnter !== nextProps.onEnter || + this.props.onChange !== nextProps.onChange || + this.state.value !== nextState.value || + this.state.selectionRange[0] !== nextState.selectionRange[0] || + this.state.selectionRange[1] !== nextState.selectionRange[1]); + } + + insert(val, insert, selRange) { + const head = val.slice(0, selRange[0]); + const tail = val.slice(selRange[1], val.length); + const newVal = head + insert + tail; + const selectionOffset = head.length + insert.length; + + return { value: newVal, selectionRange: [selectionOffset, selectionOffset] }; + } + + remove(val, selRange) { + let newVal, selectionOffset; + + if(selRange[0] === selRange[1]) { + const head = val.slice(0, selRange[0] - 1); + const tail = val.slice(selRange[0], val.length); + newVal = head + tail; + selectionOffset = head.length; + } else { + const head = val.slice(0, selRange[0]); + const tail = val.slice(selRange[1], val.length); + newVal = head + tail; + selectionOffset = head.length; + } + + return { value: newVal, selectionRange: [selectionOffset, selectionOffset] }; + } + + toInternalSelectionRange(val, domRange) { + const countSpaces = (val) => { + return (val.match(/\s/g) || []).length; + }; + + const fmt = formatAccount(val || ''); + let start = domRange[0]; + let end = domRange[1]; + const spacesBefore = countSpaces(fmt.slice(0, start)); + const spacesWithin = countSpaces(fmt.slice(start, end)); + const finalStart = start - spacesBefore; + const finalEnd = end - (spacesBefore + spacesWithin); + + console.log('toInternalSelectionRange: spacesBefore: ' + spacesBefore + ', spacesWithin: ' + spacesWithin + + "\n<" + fmt.slice(0, start) + ">\n<" + fmt.slice(start, end) + ">" + "\nfinalStart: " + finalStart + "\nfinalEnd: " + finalEnd); + + + return [ finalStart, finalEnd ]; + } + + toDomSelection(val, selRange) { + const countSpaces = (val, untilIndex) => { + if(val.length > 12) { return 0; } + return Math.floor(untilIndex / 4); // groups of 4 digits + }; + + let start = selRange[0]; + let end = selRange[1]; + const startSpaces = countSpaces(val, start); + const endSpaces = countSpaces(val, end); + + console.log('toDomSelection: [' + start + ', ' + end + '] startSpaces: ' + startSpaces + ', endSpaces: ' + endSpaces); + + start += startSpaces; + end += startSpaces + (endSpaces - startSpaces); + + return [ start, end ]; + } + + onKeyDown(e) { + console.log('Entered ' + e.key); + + const { value, selectionRange } = this.state; + let result; + + // arrows. + if(e.which >= 37 && e.which <= 40) { + return; + } + + // prevent native keyboard input management + e.preventDefault(); + + if(e.which === 8) { // backspace + result = this.remove(value, selectionRange); + } else if(/[0-9]/.test(e.key)) { // digits + result = this.insert(value, e.key, selectionRange); + } else { // any other keys + return; + } + + this.setState(result, () => { + if(this.props.onChange) { + this.props.onChange(result.value); + } + }); + } + + onSelect(e) { + const ref = this._input; + let start = ref.selectionStart; + let end = ref.selectionEnd; + + console.log('onSelect: ' + start + ', ' + end); + + const { value, selectionRange: curRange } = this.state; + const selRange = this.toInternalSelectionRange(value, [start, end]); + // if(selRange[0] !== curRange[0] || selRange[1] !== curRange[1]) { + this.setState({ selectionRange: selRange }); + // } + } + + onKeyUp(e) { + if(e.which === 13 && this.props.onEnter) { + this.props.onEnter(); + } + console.log('onKeyUp: ', e); + } + + onRef(ref) { + this._input = ref; + if(ref) { + const { value, selectionRange } = this.state; + const domRange = this.toDomSelection(value, selectionRange); + // if(ref.selectionStart !== domRange[0] || ref.selectionEnd !== domRange[1]) { + ref.setSelectionRange(domRange[0], domRange[1]); + // } + } + } + + render() { + const displayString = formatAccount(this.state.value || ''); + const props = Object.assign({}, this.props); + + // exclude built-in props + for(let key of Object.keys(AccountInput.propTypes)) { + if(props.hasOwnProperty(key)) { + delete props[key]; + } + } + + return ( + <input type="text" + value={ displayString } + onSelect={ ::this.onSelect } + onKeyUp={ ::this.onKeyUp } + onKeyDown={ ::this.onKeyDown } + ref={ ::this.onRef } + { ...props } /> + ); + } + +}
\ No newline at end of file diff --git a/app/components/Login.js b/app/components/Login.js index 43cccf3f69..5dbce441c8 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -1,8 +1,8 @@ import React, { Component, PropTypes } from 'react'; import { If, Then } from 'react-if'; import { Layout, Container, Header } from './Layout'; -import { formatAccount } from '../lib/formatters'; import { LoginState } from '../enums'; +import AccountInput from './AccountInput'; export default class Login extends Component { static propTypes = { @@ -39,24 +39,16 @@ export default class Login extends Component { this.props.onExternalLink('createAccount'); } - onInputChange(e) { - const val = e.target.value.replace(/[^0-9]/g, ''); - + onInputChange(val) { + console.log('VAL: ' + val); // notify delegate on first change after login failure if(this.state.notifyOnFirstChangeAfterFailure) { this.setState({ notifyOnFirstChangeAfterFailure: false }); this.props.onFirstChangeAfterFailure(); } - this.props.onChange(val); } - onInputKeyUp(e) { - if(e.which === 13) { - this.onLogin(); - } - } - formTitle(s) { switch(s) { case LoginState.connecting: return 'Logging in...'; @@ -117,7 +109,6 @@ export default class Login extends Component { const { account, status, error } = this.props.user; const title = this.formTitle(status); const subtitle = this.formSubtitle(status, error); - const displayAccount = formatAccount(account || ''); const isConnecting = status === LoginState.connecting; const isFailed = status === LoginState.failed; const isLoggedIn = status === LoginState.ok; @@ -169,12 +160,12 @@ export default class Login extends Component { <div className={ 'login-form__fields' + (isLoggedIn ? ' login-form__fields--invisible' : '') }> <div className="login-form__subtitle">{ subtitle }</div> <div className={ inputWrapClass }> - <input className="login-form__input-field" + <AccountInput className="login-form__input-field" type="text" placeholder="e.g 0000 0000 0000" onChange={ ::this.onInputChange } - onKeyUp={ ::this.onInputKeyUp } - value={ displayAccount } + onEnter={ ::this.onLogin } + value={ account } disabled={ isConnecting } autoFocus={ true } ref={ autoFocusRef } /> |
