// Copyright 2015, 2016 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import { connect } from 'react-redux'; import keycode, { codes } from 'keycode'; import { FormattedMessage } from 'react-intl'; import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline'; import AccountCard from '~/ui/AccountCard'; import InputAddress from '~/ui/Form/InputAddress'; import Portal from '~/ui/Portal'; import { validateAddress } from '~/util/validation'; import styles from './addressSelect.css'; const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' }; // Current Form ID let currentId = 1; class AddressSelect extends Component { static contextTypes = { muiTheme: PropTypes.object.isRequired }; static propTypes = { // Required props onChange: PropTypes.func.isRequired, // Redux props accountsInfo: PropTypes.object, accounts: PropTypes.object, balances: PropTypes.object, contacts: PropTypes.object, contracts: PropTypes.object, tokens: PropTypes.object, wallets: PropTypes.object, // Optional props allowInput: PropTypes.bool, disabled: PropTypes.bool, error: PropTypes.string, hint: PropTypes.string, label: PropTypes.string, value: PropTypes.string }; static defaultProps = { value: '' }; state = { expanded: false, focused: false, focusedCat: null, focusedItem: null, inputFocused: false, inputValue: '', values: [] }; componentWillMount () { this.setValues(); } componentWillReceiveProps (nextProps) { if (this.values && this.values.length > 0) { return; } this.setValues(nextProps); } setValues (props = this.props) { const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props; const accountsN = Object.keys(accounts).length; const contractsN = Object.keys(contracts).length; const contactsN = Object.keys(contacts).length; const walletsN = Object.keys(wallets).length; if (accountsN + contractsN + contactsN + walletsN === 0) { return; } this.values = [ { label: 'accounts', values: [].concat( Object.values(wallets), Object.values(accounts) ) }, { label: 'contacts', values: Object.values(contacts) }, { label: 'contracts', values: Object.values(contracts) } ].filter((cat) => cat.values.length > 0); this.handleChange(); } render () { const input = this.renderInput(); const content = this.renderContent(); const classes = [ styles.main ]; return (
{ input } { content }
); } renderInput () { const { focused } = this.state; const { accountsInfo, disabled, error, hint, label, value } = this.props; const input = ( ); if (disabled) { return input; } return (
{ input }
); } renderContent () { const { muiTheme } = this.context; const { hint, disabled, label } = this.props; const { expanded, inputFocused } = this.state; if (disabled) { return null; } const id = `addressSelect_${++currentId}`; return (
{ this.renderCurrentInput() } { this.renderAccounts() }
); } renderCurrentInput () { const { inputValue } = this.state; if (!this.props.allowInput || !inputValue) { return null; } const { address, addressError } = validateAddress(inputValue); if (addressError) { return null; } return (
{ this.renderAccountCard({ address }) }
); } renderAccounts () { const { values } = this.state; if (values.length === 0) { return (
); } const categories = values.map((category) => { return this.renderCategory(category.label, category.values); }); return (
{ categories }
); } renderCategory (name, values = []) { let content; if (values.length === 0) { content = (

); } else { const cards = values .map((account) => this.renderAccountCard(account)); content = (
{ cards }
); } return (
{ name }
{ content }
); } renderAccountCard (_account) { const { balances, accountsInfo } = this.props; const { address, index = null } = _account; const balance = balances[address]; const account = { ...accountsInfo[address], address, index }; return ( ); } setInputRef = (refId) => { this.inputRef = refId; } handleCustomInput = () => { const { allowInput } = this.props; const { inputValue, values } = this.state; // If input is HEX and allowInput === true, send it if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) { return this.handleClick(inputValue); } // If only one value, select it if (values.length === 1 && values[0].values.length === 1) { const value = values[0].values[0]; return this.handleClick(value.address); } } handleInputAddresKeydown = (event) => { const code = keycode(event); // Simulate click on input address if enter is pressed if (code === 'enter') { return this.handleDOMAction('inputAddress', 'click'); } } handleKeyDown = (event) => { const codeName = keycode(event); if (event.ctrlKey) { return event; } switch (codeName) { case 'enter': const index = this.state.focusedItem; if (!index) { return this.handleCustomInput(); } return this.handleDOMAction(`account_${index}`, 'click'); case 'left': case 'right': case 'up': case 'down': return this.handleNavigation(codeName, event); default: const code = codes[codeName]; // @see https://github.com/timoxley/keycode/blob/master/index.js // lower case chars if (code >= (97 - 32) && code <= (122 - 32)) { return this.handleDOMAction(this.inputRef, 'focus'); } // numbers if (code >= 48 && code <= 57) { return this.handleDOMAction(this.inputRef, 'focus'); } return event; } } handleDOMAction = (ref, method) => { const refItem = typeof ref === 'string' ? this.refs[ref] : ref; const element = ReactDOM.findDOMNode(refItem); if (!element || typeof element[method] !== 'function') { console.warn('could not find', ref, 'or method', method); return; } return element[method](); } focusItem = (index) => { this.setState({ focusedItem: index }); return this.handleDOMAction(`account_${index}`, 'focus'); } handleNavigation = (direction, event) => { const { focusedItem, focusedCat, values } = this.state; // Don't do anything if no values if (values.length === 0) { return event; } // Focus on the first element if none selected yet if going down if (!focusedItem) { if (direction !== 'down') { return event; } event.preventDefault(); const nextValues = values[focusedCat || 0]; const nextFocus = nextValues ? nextValues.values[0] : null; return this.focusItem(nextFocus && nextFocus.index || 1); } event.preventDefault(); // Find the previous focused category const prevCategoryIndex = values.findIndex((category) => { return category.values.find((value) => value.index === focusedItem); }); const prevFocusIndex = values[prevCategoryIndex].values.findIndex((a) => a.index === focusedItem); let nextCategory = prevCategoryIndex; let nextFocusIndex; // If down: increase index if possible if (direction === 'down') { const prevN = values[prevCategoryIndex].values.length; nextFocusIndex = Math.min(prevFocusIndex + 1, prevN - 1); } // If up: decrease index if possible if (direction === 'up') { // Focus on search if at the top if (prevFocusIndex === 0) { return this.handleDOMAction(this.inputRef, 'focus'); } nextFocusIndex = prevFocusIndex - 1; } // If right: next category if (direction === 'right') { nextCategory = Math.min(prevCategoryIndex + 1, values.length - 1); } // If right: previous category if (direction === 'left') { nextCategory = Math.max(prevCategoryIndex - 1, 0); } // If left or right: try to keep the horizontal index if (direction === 'left' || direction === 'right') { this.setState({ focusedCat: nextCategory }); nextFocusIndex = Math.min(prevFocusIndex, values[nextCategory].values.length - 1); } const nextFocus = values[nextCategory].values[nextFocusIndex].index; return this.focusItem(nextFocus); } handleClick = (address) => { // Don't do anything if it's only text-selection if (window.getSelection && window.getSelection().type === 'Range') { return; } this.props.onChange(null, address); this.handleClose(); } handleMainBlur = () => { if (window.document.hasFocus() && !this.state.expanded) { this.closing = false; this.setState({ focused: false }); } } handleMainFocus = () => { if (this.state.focused) { return; } this.setState({ focused: true }, () => { if (this.closing) { this.closing = false; return; } this.handleFocus(); }); } handleFocus = () => { this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => { window.setTimeout(() => { this.handleDOMAction(this.inputRef, 'focus'); }); }); } handleClose = () => { this.closing = true; if (this.refs.inputAddress) { this.handleDOMAction('inputAddress', 'focus'); } this.setState({ expanded: false }); } /** * Filter the given values based on the given * filter */ filterValues = (values = [], _filter = '') => { const filter = _filter.toLowerCase(); return values // Remove empty accounts .filter((a) => a) .filter((account) => { const address = account.address.toLowerCase(); const inAddress = address.includes(filter); if (!account.name || inAddress) { return inAddress; } const name = account.name.toLowerCase(); const inName = name.includes(filter); const { meta = {} } = account; if (!meta.tags || inName) { return inName; } const tags = (meta.tags || []).join(''); return tags.includes(filter); }) .sort((accA, accB) => { const nameA = accA.name || accA.address; const nameB = accB.name || accB.address; return nameA.localeCompare(nameB); }); } handleInputBlur = () => { this.setState({ inputFocused: false }); } handleInputFocus = () => { this.setState({ focusedItem: null, inputFocused: true }); } handleChange = (event = { target: {} }) => { const { value = '' } = event.target; let index = 0; const values = this.values .map((category) => { const filteredValues = this .filterValues(category.values, value) .map((value) => { index++; return { ...value, index: parseInt(index) }; }); return { label: category.label, values: filteredValues }; }); this.setState({ values, focusedItem: null, inputValue: value }); } } function mapStateToProps (state) { const { accountsInfo } = state.personal; const { balances } = state.balances; return { accountsInfo, balances }; } export default connect( mapStateToProps )(AddressSelect);