New Address Selector Component (#3829)
* WIP new address selector * WIP Working New Account Selector * WIP Fully working Addres Selector (missing keyboards navigation) * WIP Address Selector * Fully functionnal new Address Selector! * Implement disabled prop * Don't auto-open on focus + Text Selection fix * Add copy address capabilities * Better Address Selector Focus * Search from tags * [Address Selector] Better Focus // Parity Background * Linting * [Adress Selector] Better focused input style * Better focus support for inputs * Fix style issue * Add tags to accounts * linting * Add label to address selector * Removed old address selector + improved styling * Fixing address selection issues * fix test * Better logs... * PR Grumbles Part 1 * PR Grumbles Part 2 * PR Grumbles Part 3.1 * PR Grumbles Part 3.2 * PR Grumbles Part 3.3 * New Portal Component * Simpler Transition for Portal * PR Grumbles Part 4 * Align font-family with rest of UI * Fix null value input * Fix Webpack build...
This commit is contained in:
parent
4e51f93176
commit
1ffc6ac58c
@ -168,6 +168,7 @@
|
||||
"react-dom": "15.4.1",
|
||||
"react-dropzone": "3.7.3",
|
||||
"react-intl": "2.1.5",
|
||||
"react-portal": "3.0.0",
|
||||
"react-redux": "4.4.6",
|
||||
"react-router": "3.0.0",
|
||||
"react-router-redux": "4.0.7",
|
||||
|
@ -22,7 +22,7 @@ import { ContextProvider, muiTheme } from '~/ui';
|
||||
|
||||
import DetailsStep from './';
|
||||
|
||||
import { CONTRACT } from '../executeContract.test.js';
|
||||
import { STORE, CONTRACT } from '../executeContract.test.js';
|
||||
|
||||
let component;
|
||||
let onAmountChange;
|
||||
@ -41,7 +41,7 @@ function render (props) {
|
||||
onValueChange = sinon.stub();
|
||||
|
||||
component = mount(
|
||||
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ {} }>
|
||||
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ STORE }>
|
||||
<DetailsStep
|
||||
{ ...props }
|
||||
contract={ CONTRACT }
|
||||
|
@ -42,7 +42,7 @@ function render (props) {
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('modals/ExecuteContract/DetailsStep', () => {
|
||||
describe('modals/ExecuteContract', () => {
|
||||
it('renders', () => {
|
||||
expect(render({ accounts: {} })).to.be.ok;
|
||||
});
|
||||
|
@ -53,6 +53,12 @@ const STORE = {
|
||||
},
|
||||
nodeStatus: {
|
||||
gasLimit: new BigNumber(123)
|
||||
},
|
||||
personal: {
|
||||
accountsInfo: {}
|
||||
},
|
||||
settings: {
|
||||
backgroundSeed: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -44,12 +44,12 @@ export default class Extras extends Component {
|
||||
error={ minBlockError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='executeContract.advanced.minBlock.hint'
|
||||
id='transferModal.minBlock.hint'
|
||||
defaultMessage='Only post the transaction after this block' />
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='executeContract.advanced.minBlock.label'
|
||||
id='transferModal.minBlock.label'
|
||||
defaultMessage='BlockNumber to send from' />
|
||||
}
|
||||
value={ minBlock }
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { action, computed, observable, transaction } from 'mobx';
|
||||
import { action, computed, observable, transaction, toJS } from 'mobx';
|
||||
import store from 'store';
|
||||
|
||||
const LS_UPDATE = '_parity::update';
|
||||
@ -129,7 +129,10 @@ export default class Store {
|
||||
this._api.parity.versionInfo()
|
||||
])
|
||||
.then(([available, consensusCapability, version]) => {
|
||||
console.log('[checkUpgrade]', 'available:', available, 'version:', version, 'consensusCapability:', consensusCapability);
|
||||
if (!this.version || version.hash !== this.version.hash) {
|
||||
console.log('[checkUpgrade]', 'available:', available, 'version:', toJS(version.version), 'consensusCapability:', consensusCapability);
|
||||
}
|
||||
|
||||
this.setVersions(available, version, consensusCapability);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -48,7 +48,7 @@ function setBalances (_balances) {
|
||||
|
||||
const balance = Object.assign({}, balances[address]);
|
||||
const { tokens, txCount = balance.txCount } = nextBalances[address];
|
||||
const nextTokens = [].concat(balance.tokens);
|
||||
const nextTokens = balance.tokens.slice();
|
||||
|
||||
tokens.forEach((t) => {
|
||||
const { token, value } = t;
|
||||
|
@ -127,6 +127,10 @@ export default class CertificationsMiddleware {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (/does not exist/.test(err.toString())) {
|
||||
return console.warn(err.toString());
|
||||
}
|
||||
|
||||
console.warn(`Could not fetch certifier ${id}:`, err);
|
||||
});
|
||||
});
|
||||
|
105
js/src/ui/AccountCard/accountCard.css
Normal file
105
js/src/ui/AccountCard/accountCard.css
Normal file
@ -0,0 +1,105 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.account {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
transition: transform ease-out 0.1s;
|
||||
transform: scale(1);
|
||||
|
||||
&.copied {
|
||||
animation-duration: 0.25s;
|
||||
animation-name: copied;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
transform: scale(0.99);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
|
||||
> * {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
||||
.addressContainer {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.9em;
|
||||
|
||||
.address {
|
||||
&:hover {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accountName {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.balance {
|
||||
.tag {
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes copied {
|
||||
from {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
}
|
190
js/src/ui/AccountCard/accountCard.js
Normal file
190
js/src/ui/AccountCard/accountCard.js
Normal file
@ -0,0 +1,190 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import keycode from 'keycode';
|
||||
|
||||
import IdentityIcon from '~/ui/IdentityIcon';
|
||||
import Tags from '~/ui/Tags';
|
||||
|
||||
import { fromWei } from '~/api/util/wei';
|
||||
|
||||
import styles from './accountCard.css';
|
||||
|
||||
export default class AccountCard extends Component {
|
||||
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
|
||||
balance: PropTypes.object
|
||||
};
|
||||
|
||||
state = {
|
||||
copied: false
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
const { copied } = this.state;
|
||||
|
||||
const { address, name, meta = {} } = account;
|
||||
|
||||
const displayName = (name && name.toUpperCase()) || address;
|
||||
const { tags = [] } = meta;
|
||||
|
||||
const classes = [ styles.account ];
|
||||
|
||||
if (copied) {
|
||||
classes.push(styles.copied);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ address }
|
||||
tabIndex={ 0 }
|
||||
className={ classes.join(' ') }
|
||||
onClick={ this.onClick }
|
||||
onFocus={ this.onFocus }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
>
|
||||
<IdentityIcon address={ address } />
|
||||
<div className={ styles.accountInfo }>
|
||||
<div className={ styles.accountName }>
|
||||
<span>{ displayName }</span>
|
||||
</div>
|
||||
|
||||
{ this.renderTags(tags, address) }
|
||||
{ this.renderAddress(displayName, address) }
|
||||
{ this.renderBalance(address) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddress (name, address) {
|
||||
if (name === address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.addressContainer }>
|
||||
<span
|
||||
className={ styles.address }
|
||||
onClick={ this.preventEvent }
|
||||
ref={ `address` }
|
||||
title={ address }
|
||||
>
|
||||
{ address }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTags (tags = [], address) {
|
||||
if (tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tags tags={ tags } />
|
||||
);
|
||||
}
|
||||
|
||||
renderBalance (address) {
|
||||
const { balance = {} } = this.props;
|
||||
|
||||
if (!balance.tokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ethToken = balance.tokens
|
||||
.find((tok) => tok.token && (tok.token.tag || '').toLowerCase() === 'eth');
|
||||
|
||||
if (!ethToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = fromWei(ethToken.value).toFormat(3);
|
||||
|
||||
return (
|
||||
<div className={ styles.balance }>
|
||||
<span>{ value }</span>
|
||||
<span className={ styles.tag }>ETH</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleKeyDown = (event) => {
|
||||
const codeName = keycode(event);
|
||||
|
||||
if (event.ctrlKey) {
|
||||
// Copy the selected address if nothing selected and there is
|
||||
// a focused item
|
||||
const isSelection = !window.getSelection || window.getSelection().type === 'Range';
|
||||
|
||||
if (codeName === 'c' && !isSelection) {
|
||||
const element = ReactDOM.findDOMNode(this.refs.address);
|
||||
|
||||
// Copy the address from the right element
|
||||
// @see https://developers.google.com/web/updates/2015/04/cut-and-copy-commands
|
||||
try {
|
||||
const range = document.createRange();
|
||||
range.selectNode(element);
|
||||
window.getSelection().addRange(range);
|
||||
document.execCommand('copy');
|
||||
|
||||
try {
|
||||
window.getSelection().removeRange(range);
|
||||
} catch (e) {
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
|
||||
this.setState({ copied: true }, () => {
|
||||
window.setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 250);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('could not copy');
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
const { account, onClick } = this.props;
|
||||
onClick(account.address);
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
const { account, onFocus } = this.props;
|
||||
onFocus(account.index);
|
||||
}
|
||||
|
||||
preventEvent = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
setTagRef = (tagRef) => {
|
||||
this.tagRefs.push(tagRef);
|
||||
}
|
||||
}
|
17
js/src/ui/AccountCard/index.js
Normal file
17
js/src/ui/AccountCard/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default from './accountCard';
|
@ -14,53 +14,110 @@
|
||||
/* You should have received a copy of the GNU General Public License
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
.account {
|
||||
padding: 0.25em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
appearance: textfield;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
transition-property: font-size, padding;
|
||||
transition-duration: 0.5s;
|
||||
transition-timing-function: cubic-bezier(0.7,0,0.3,1);
|
||||
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
font-size: 2em;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
text-transform: uppercase;
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
.balance {
|
||||
color: #aaa;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
|
||||
&.noLabel {
|
||||
top: 11px;
|
||||
&::placeholder {
|
||||
color: #a2a2a2;
|
||||
}
|
||||
}
|
||||
|
||||
.paddedInput input {
|
||||
padding-left: 46px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
.inputAddress {
|
||||
position: relative;
|
||||
|
||||
&:hover, *:hover {
|
||||
cursor: text !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
min-height: 0 !important;
|
||||
line-height: inherit !important;
|
||||
.main {
|
||||
position: relative;
|
||||
left: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 1rem 2.5rem 0.25em;
|
||||
color: rgba(255, 255, 255, 0.498039);
|
||||
}
|
||||
|
||||
.underline {
|
||||
position: relative;
|
||||
margin: 0 9rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.input {
|
||||
font-size: 1.5em;
|
||||
padding: 0 9rem 0.5em 2.5rem;
|
||||
display: block;
|
||||
|
||||
padding-right: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
margin: 2rem 2rem 0;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 0.5em;
|
||||
max-width: 35em;
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
font-size: 1.5em;
|
||||
font-color: white;
|
||||
}
|
||||
|
||||
.cards {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
|
@ -14,277 +14,599 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { isEqual, pick } from 'lodash';
|
||||
import { MenuItem } from 'material-ui';
|
||||
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 { fromWei } from '~/api/util/wei';
|
||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
||||
import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
|
||||
|
||||
import AutoComplete from '../AutoComplete';
|
||||
import IdentityIcon from '../../IdentityIcon';
|
||||
import IdentityName from '../../IdentityName';
|
||||
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';
|
||||
|
||||
export default class AddressSelect extends Component {
|
||||
const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
|
||||
|
||||
// Current Form ID
|
||||
let currentId = 1;
|
||||
|
||||
class AddressSelect extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
}
|
||||
muiTheme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
// Required props
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// Redux props
|
||||
accountsInfo: PropTypes.object,
|
||||
accounts: PropTypes.object,
|
||||
allowInput: PropTypes.bool,
|
||||
balances: PropTypes.object,
|
||||
contacts: PropTypes.object,
|
||||
contracts: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
error: nodeOrStringProptype(),
|
||||
hint: nodeOrStringProptype(),
|
||||
label: nodeOrStringProptype(),
|
||||
tokens: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
wallets: PropTypes.object
|
||||
}
|
||||
wallets: PropTypes.object,
|
||||
|
||||
state = {
|
||||
autocompleteEntries: [],
|
||||
entries: {},
|
||||
addresses: [],
|
||||
value: ''
|
||||
}
|
||||
|
||||
// Cache autocomplete items
|
||||
items = {}
|
||||
|
||||
entriesFromProps (props = this.props) {
|
||||
const { accounts = {}, contacts = {}, contracts = {}, wallets = {} } = props;
|
||||
|
||||
const autocompleteEntries = [].concat(
|
||||
Object.values(wallets),
|
||||
'divider',
|
||||
Object.values(accounts),
|
||||
'divider',
|
||||
Object.values(contacts),
|
||||
'divider',
|
||||
Object.values(contracts)
|
||||
);
|
||||
|
||||
const entries = {
|
||||
...wallets,
|
||||
...accounts,
|
||||
...contacts,
|
||||
...contracts
|
||||
// Optional props
|
||||
allowInput: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
return { autocompleteEntries, entries };
|
||||
}
|
||||
static defaultProps = {
|
||||
value: ''
|
||||
};
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
const keys = [ 'error', 'value' ];
|
||||
|
||||
const prevValues = pick(this.props, keys);
|
||||
const nextValues = pick(nextProps, keys);
|
||||
|
||||
return !isEqual(prevValues, nextValues);
|
||||
}
|
||||
state = {
|
||||
expanded: false,
|
||||
focused: false,
|
||||
focusedCat: null,
|
||||
focusedItem: null,
|
||||
inputFocused: false,
|
||||
inputValue: '',
|
||||
values: []
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { value } = this.props;
|
||||
const { entries, autocompleteEntries } = this.entriesFromProps();
|
||||
const addresses = Object.keys(entries).sort();
|
||||
|
||||
this.setState({ autocompleteEntries, entries, addresses, value });
|
||||
this.setValues();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.value !== this.props.value) {
|
||||
this.setState({ value: newProps.value });
|
||||
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 { allowInput, disabled, error, hint, label } = this.props;
|
||||
const { autocompleteEntries, value } = this.state;
|
||||
const input = this.renderInput();
|
||||
const content = this.renderContent();
|
||||
|
||||
const searchText = this.getSearchText();
|
||||
const icon = this.renderIdentityIcon(value);
|
||||
const classes = [ styles.main ];
|
||||
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<AutoComplete
|
||||
className={ !icon ? '' : styles.paddedInput }
|
||||
disabled={ disabled }
|
||||
entries={ autocompleteEntries }
|
||||
entry={ this.getEntry() || {} }
|
||||
error={ error }
|
||||
filter={ this.handleFilter }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='ui.addressSelect.search.hint'
|
||||
defaultMessage='search for {hint}'
|
||||
values={ {
|
||||
hint: hint ||
|
||||
<FormattedMessage
|
||||
id='ui.addressSelect.search.address'
|
||||
defaultMessage='address' />
|
||||
} } />
|
||||
}
|
||||
label={ label }
|
||||
onBlur={ this.onBlur }
|
||||
onChange={ this.onChange }
|
||||
onUpdateInput={ allowInput && this.onUpdateInput }
|
||||
renderItem={ this.renderItem }
|
||||
value={ searchText } />
|
||||
{ icon }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderIdentityIcon (inputValue) {
|
||||
const { error, value, label } = this.props;
|
||||
|
||||
if (error || !inputValue || value.length !== 42) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = [ styles.icon ];
|
||||
|
||||
if (!label) {
|
||||
classes.push(styles.noLabel);
|
||||
}
|
||||
|
||||
return (
|
||||
<IdentityIcon
|
||||
address={ value }
|
||||
center
|
||||
<div
|
||||
className={ classes.join(' ') }
|
||||
inline />
|
||||
onBlur={ this.handleMainBlur }
|
||||
onClick={ this.handleFocus }
|
||||
onFocus={ this.handleMainFocus }
|
||||
onKeyDown={ this.handleInputAddresKeydown }
|
||||
ref='inputAddress'
|
||||
tabIndex={ 0 }
|
||||
>
|
||||
{ input }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderItem = (entry) => {
|
||||
const { address, name } = entry;
|
||||
renderInput () {
|
||||
const { focused } = this.state;
|
||||
const { accountsInfo, disabled, error, hint, label, value } = this.props;
|
||||
|
||||
const _balance = this.getBalance(address);
|
||||
const balance = _balance ? _balance.toNumber() : _balance;
|
||||
const input = (
|
||||
<InputAddress
|
||||
accountsInfo={ accountsInfo }
|
||||
allowCopy={ false }
|
||||
disabled={ disabled }
|
||||
error={ error }
|
||||
hint={ hint }
|
||||
focused={ focused }
|
||||
label={ label }
|
||||
readOnly
|
||||
tabIndex={ -1 }
|
||||
text
|
||||
value={ value }
|
||||
/>
|
||||
);
|
||||
|
||||
if (!this.items[address] || this.items[address].balance !== balance) {
|
||||
this.items[address] = {
|
||||
address,
|
||||
balance,
|
||||
text: name && name.toUpperCase() || address,
|
||||
value: this.renderMenuItem(address)
|
||||
};
|
||||
if (disabled) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return this.items[address];
|
||||
return (
|
||||
<div className={ styles.inputAddress }>
|
||||
{ input }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getBalance (address) {
|
||||
const { balances = {} } = this.props;
|
||||
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 (
|
||||
<Portal
|
||||
className={ styles.inputContainer }
|
||||
onClose={ this.handleClose }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
open={ expanded }
|
||||
>
|
||||
<label className={ styles.label } htmlFor={ id }>
|
||||
{ label }
|
||||
</label>
|
||||
<input
|
||||
id={ id }
|
||||
className={ styles.input }
|
||||
placeholder={ hint }
|
||||
|
||||
onBlur={ this.handleInputBlur }
|
||||
onFocus={ this.handleInputFocus }
|
||||
onChange={ this.handleChange }
|
||||
|
||||
ref={ this.setInputRef }
|
||||
/>
|
||||
|
||||
<div className={ styles.underline }>
|
||||
<TextFieldUnderline
|
||||
focus={ inputFocused }
|
||||
focusStyle={ BOTTOM_BORDER_STYLE }
|
||||
muiTheme={ muiTheme }
|
||||
style={ BOTTOM_BORDER_STYLE }
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.renderCurrentInput() }
|
||||
{ this.renderAccounts() }
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderCurrentInput () {
|
||||
const { inputValue } = this.state;
|
||||
|
||||
if (!this.props.allowInput || !inputValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { address, addressError } = validateAddress(inputValue);
|
||||
|
||||
if (addressError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.renderAccountCard({ address }) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { values } = this.state;
|
||||
|
||||
if (values.length === 0) {
|
||||
return (
|
||||
<div className={ styles.categories }>
|
||||
<div className={ styles.empty }>
|
||||
<FormattedMessage
|
||||
id='addressSelect.noAccount'
|
||||
defaultMessage='No account matches this query...'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = values.map((category) => {
|
||||
return this.renderCategory(category.label, category.values);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ styles.categories }>
|
||||
{ categories }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCategory (name, values = []) {
|
||||
let content;
|
||||
|
||||
if (values.length === 0) {
|
||||
content = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='addressSelect.noAccount'
|
||||
defaultMessage='No account matches this query...'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
const cards = values
|
||||
.map((account) => this.renderAccountCard(account));
|
||||
|
||||
content = (
|
||||
<div className={ styles.cards }>
|
||||
<div>{ cards }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.category } key={ name }>
|
||||
<div className={ styles.title }>{ name }</div>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccountCard (_account) {
|
||||
const { balances, accountsInfo } = this.props;
|
||||
const { address, index = null } = _account;
|
||||
|
||||
const balance = balances[address];
|
||||
|
||||
if (!balance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ethToken = balance.tokens.find((tok) => tok.token && tok.token.tag && tok.token.tag.toLowerCase() === 'eth');
|
||||
|
||||
if (!ethToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ethToken.value;
|
||||
}
|
||||
|
||||
renderBalance (address) {
|
||||
const balance = this.getBalance(address) || 0;
|
||||
const value = fromWei(balance);
|
||||
const account = {
|
||||
...accountsInfo[address],
|
||||
address, index
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ styles.balance }>
|
||||
{ value.toFormat(3) }<small> { 'ETH' }</small>
|
||||
</div>
|
||||
<AccountCard
|
||||
account={ account }
|
||||
balance={ balance }
|
||||
key={ `account_${index}` }
|
||||
onClick={ this.handleClick }
|
||||
onFocus={ this.focusItem }
|
||||
ref={ `account_${index}` }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMenuItem (address) {
|
||||
const balance = this.props.balances
|
||||
? this.renderBalance(address)
|
||||
: null;
|
||||
|
||||
const item = (
|
||||
<div className={ styles.account }>
|
||||
<IdentityIcon
|
||||
address={ address }
|
||||
center
|
||||
className={ styles.image }
|
||||
inline />
|
||||
<IdentityName
|
||||
address={ address }
|
||||
className={ styles.name } />
|
||||
{ balance }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={ styles.menuItem }
|
||||
key={ address }
|
||||
label={ item }
|
||||
value={ address }>
|
||||
{ item }
|
||||
</MenuItem>
|
||||
);
|
||||
setInputRef = (refId) => {
|
||||
this.inputRef = refId;
|
||||
}
|
||||
|
||||
getSearchText () {
|
||||
const entry = this.getEntry();
|
||||
|
||||
return entry && entry.name
|
||||
? entry.name.toUpperCase()
|
||||
: this.state.value;
|
||||
}
|
||||
|
||||
getEntry () {
|
||||
const { entries, value } = this.state;
|
||||
return value ? entries[value] : null;
|
||||
}
|
||||
|
||||
handleFilter = (searchText, name, item) => {
|
||||
const { address } = item;
|
||||
const entry = this.state.entries[address];
|
||||
const lowCaseSearch = (searchText || '').toLowerCase();
|
||||
|
||||
return [entry.name, entry.address]
|
||||
.some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1);
|
||||
}
|
||||
|
||||
onChange = (entry, empty) => {
|
||||
handleCustomInput = () => {
|
||||
const { allowInput } = this.props;
|
||||
const { value } = this.state;
|
||||
const { inputValue, values } = this.state;
|
||||
|
||||
const address = entry && entry.address
|
||||
? entry.address
|
||||
: ((empty && !allowInput) ? '' : value);
|
||||
|
||||
this.props.onChange(null, address);
|
||||
// If input is HEX and allowInput === true, send it
|
||||
if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) {
|
||||
return this.handleClick(inputValue);
|
||||
}
|
||||
|
||||
onUpdateInput = (query, choices) => {
|
||||
const { api } = this.context;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const address = query.trim();
|
||||
handleInputAddresKeydown = (event) => {
|
||||
const code = keycode(event);
|
||||
|
||||
if (!/^0x/.test(address) && api.util.isAddressValid(`0x${address}`)) {
|
||||
const checksumed = api.util.toChecksumAddress(`0x${address}`);
|
||||
return this.props.onChange(null, checksumed);
|
||||
// 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);
|
||||
|
@ -39,6 +39,10 @@ const UNDERLINE_NORMAL = {
|
||||
borderBottom: 'solid 2px'
|
||||
};
|
||||
|
||||
const UNDERLINE_FOCUSED = {
|
||||
transform: 'scaleX(1.0)'
|
||||
};
|
||||
|
||||
const NAME_ID = ' ';
|
||||
|
||||
export default class Input extends Component {
|
||||
@ -51,6 +55,7 @@ export default class Input extends Component {
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
error: nodeOrStringProptype(),
|
||||
focused: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
floatCopy: PropTypes.bool,
|
||||
hint: nodeOrStringProptype(),
|
||||
@ -61,9 +66,12 @@ export default class Input extends Component {
|
||||
multiLine: PropTypes.bool,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
rows: PropTypes.number,
|
||||
tabIndex: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
submitOnBlur: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
@ -92,11 +100,20 @@ export default class Input extends Component {
|
||||
if ((newProps.value !== this.props.value) && (newProps.value !== this.state.value)) {
|
||||
this.setValue(newProps.value);
|
||||
}
|
||||
|
||||
if (newProps.focused && !this.props.focused) {
|
||||
this.refs.input.setState({ isFocused: true });
|
||||
}
|
||||
|
||||
if (!newProps.focused && this.props.focused) {
|
||||
this.refs.input.setState({ isFocused: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.state;
|
||||
const { children, className, disabled, error, hideUnderline, hint, label, max, min, multiLine, rows, style, type } = this.props;
|
||||
const { children, className, hideUnderline, disabled, error, focused, label } = this.props;
|
||||
const { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props;
|
||||
|
||||
const readOnly = this.props.readOnly || disabled;
|
||||
|
||||
@ -111,6 +128,11 @@ export default class Input extends Component {
|
||||
textFieldStyle.height = 'initial';
|
||||
}
|
||||
|
||||
const underlineStyle = readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL;
|
||||
const underlineFocusStyle = focused
|
||||
? UNDERLINE_FOCUSED
|
||||
: readOnly && typeof focused !== 'boolean' ? { display: 'none' } : null;
|
||||
|
||||
return (
|
||||
<div className={ styles.container } style={ style }>
|
||||
{ this.renderCopyButton() }
|
||||
@ -130,15 +152,19 @@ export default class Input extends Component {
|
||||
name={ NAME_ID }
|
||||
onBlur={ this.onBlur }
|
||||
onChange={ this.onChange }
|
||||
onClick={ onClick }
|
||||
onKeyDown={ this.onKeyDown }
|
||||
onFocus={ onFocus }
|
||||
onPaste={ this.onPaste }
|
||||
readOnly={ readOnly }
|
||||
ref='input'
|
||||
rows={ rows }
|
||||
style={ textFieldStyle }
|
||||
tabIndex={ tabIndex }
|
||||
type={ type || 'text' }
|
||||
underlineDisabledStyle={ UNDERLINE_DISABLED }
|
||||
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
|
||||
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
|
||||
underlineStyle={ underlineStyle }
|
||||
underlineFocusStyle={ underlineFocusStyle }
|
||||
underlineShow={ !hideUnderline }
|
||||
value={ value }>
|
||||
{ children }
|
||||
|
@ -34,12 +34,17 @@ class InputAddress extends Component {
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
focused: PropTypes.bool,
|
||||
hideUnderline: PropTypes.bool,
|
||||
hint: nodeOrStringProptype(),
|
||||
label: nodeOrStringProptype(),
|
||||
onChange: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
small: PropTypes.bool,
|
||||
tabIndex: PropTypes.number,
|
||||
text: PropTypes.bool,
|
||||
tokens: PropTypes.object,
|
||||
value: PropTypes.string
|
||||
@ -52,10 +57,11 @@ class InputAddress extends Component {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { className, disabled, error, hint, label, text, value } = this.props;
|
||||
const { accountsInfo, allowCopy, hideUnderline, onSubmit, small, tokens } = this.props;
|
||||
const { accountsInfo, allowCopy, className, disabled, error, focused, hint } = this.props;
|
||||
const { hideUnderline, label, onClick, onFocus, onSubmit, readOnly, small } = this.props;
|
||||
const { tabIndex, text, tokens, value } = this.props;
|
||||
|
||||
const account = accountsInfo[value] || tokens[value];
|
||||
const account = value && (accountsInfo[value] || tokens[value]);
|
||||
|
||||
const icon = this.renderIcon();
|
||||
|
||||
@ -63,7 +69,7 @@ class InputAddress extends Component {
|
||||
classes.push(!icon ? styles.inputEmpty : styles.input);
|
||||
|
||||
const containerClasses = [ styles.container ];
|
||||
const nullName = new BigNumber(value).eq(0) ? 'null' : null;
|
||||
const nullName = value && new BigNumber(value).eq(0) ? 'null' : null;
|
||||
|
||||
if (small) {
|
||||
containerClasses.push(styles.small);
|
||||
@ -76,11 +82,16 @@ class InputAddress extends Component {
|
||||
className={ classes.join(' ') }
|
||||
disabled={ disabled }
|
||||
error={ error }
|
||||
focused={ focused }
|
||||
hideUnderline={ hideUnderline }
|
||||
hint={ hint }
|
||||
label={ label }
|
||||
onChange={ this.handleInputChange }
|
||||
onClick={ onClick }
|
||||
onFocus={ onFocus }
|
||||
onSubmit={ onSubmit }
|
||||
readOnly={ readOnly }
|
||||
tabIndex={ tabIndex }
|
||||
value={
|
||||
text && account
|
||||
? account.name
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import AddIcon from 'material-ui/svg-icons/content/add';
|
||||
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
||||
import CloseIcon from 'material-ui/svg-icons/navigation/close';
|
||||
import ContractIcon from 'material-ui/svg-icons/action/code';
|
||||
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
||||
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||
@ -25,6 +26,7 @@ import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
||||
export {
|
||||
AddIcon,
|
||||
CancelIcon,
|
||||
CloseIcon,
|
||||
ContractIcon,
|
||||
DoneIcon,
|
||||
PrevIcon,
|
||||
|
@ -18,48 +18,70 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
class ParityBackground extends Component {
|
||||
static contextTypes = {
|
||||
muiTheme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object.isRequired,
|
||||
backgroundSeed: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
style: {}
|
||||
};
|
||||
|
||||
_seed = null;
|
||||
|
||||
componentWillMount () {
|
||||
this.setStyle();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.setStyle(nextProps);
|
||||
}
|
||||
|
||||
shouldComponentUpdate (_, nextState) {
|
||||
return nextState.style !== this.state.style;
|
||||
}
|
||||
|
||||
setStyle (props = this.props) {
|
||||
const { seed, gradient, backgroundSeed } = props;
|
||||
|
||||
const _seed = seed || backgroundSeed;
|
||||
|
||||
// Don't update if it's the same seed...
|
||||
if (this._seed === _seed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { muiTheme } = this.context;
|
||||
|
||||
const style = muiTheme.parity.getBackgroundStyle(gradient, _seed);
|
||||
this.setState({ style });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, className, style, onClick } = this.props;
|
||||
const { children, className, onClick } = this.props;
|
||||
const { style } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ className }
|
||||
style={ style }
|
||||
onTouchTap={ onClick }>
|
||||
onTouchTap={ onClick }
|
||||
>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (_, initProps) {
|
||||
const { gradient, seed, muiTheme } = initProps;
|
||||
|
||||
let _seed = seed;
|
||||
let _props = { style: muiTheme.parity.getBackgroundStyle(gradient, seed) };
|
||||
|
||||
return (state, props) => {
|
||||
function mapStateToProps (state) {
|
||||
const { backgroundSeed } = state.settings;
|
||||
const { seed } = props;
|
||||
|
||||
const newSeed = seed || backgroundSeed;
|
||||
|
||||
if (newSeed === _seed) {
|
||||
return _props;
|
||||
}
|
||||
|
||||
_seed = newSeed;
|
||||
_props = { style: muiTheme.parity.getBackgroundStyle(gradient, newSeed) };
|
||||
|
||||
return _props;
|
||||
};
|
||||
return { backgroundSeed };
|
||||
}
|
||||
|
||||
export default connect(
|
||||
|
17
js/src/ui/Portal/index.js
Normal file
17
js/src/ui/Portal/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default from './portal';
|
74
js/src/ui/Portal/portal.css
Normal file
74
js/src/ui/Portal/portal.css
Normal file
@ -0,0 +1,74 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.parityBackground {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0.25;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
transform-origin: 100% 0;
|
||||
transition-property: opacity, z-index;
|
||||
transition-duration: 0.25s;
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
opacity: 0;
|
||||
z-index: -10;
|
||||
|
||||
* {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
font-size: 4em;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
&, * {
|
||||
height: 48px !important;
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
125
js/src/ui/Portal/portal.js
Normal file
125
js/src/ui/Portal/portal.js
Normal file
@ -0,0 +1,125 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Portal from 'react-portal';
|
||||
import keycode from 'keycode';
|
||||
|
||||
import { CloseIcon } from '~/ui/Icons';
|
||||
import ParityBackground from '~/ui/ParityBackground';
|
||||
|
||||
import styles from './portal.css';
|
||||
|
||||
export default class Protal extends Component {
|
||||
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
onKeyDown: PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
expanded: false
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.open !== nextProps.open) {
|
||||
const opening = nextProps.open;
|
||||
const closing = !opening;
|
||||
|
||||
if (opening) {
|
||||
return this.setState({ expanded: true });
|
||||
}
|
||||
|
||||
if (closing) {
|
||||
return this.setState({ expanded: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { expanded } = this.state;
|
||||
const { children, className } = this.props;
|
||||
|
||||
const classes = [ styles.overlay, className ];
|
||||
|
||||
if (expanded) {
|
||||
classes.push(styles.expanded);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal isOpened onClose={ this.handleClose }>
|
||||
<div
|
||||
className={ classes.join(' ') }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
>
|
||||
<ParityBackground className={ styles.parityBackground } />
|
||||
|
||||
{ this.renderCloseIcon() }
|
||||
{ children }
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderCloseIcon () {
|
||||
const { expanded } = this.state;
|
||||
|
||||
if (!expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.closeIcon } onClick={ this.handleClose }>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
handleKeyDown = (event) => {
|
||||
const codeName = keycode(event);
|
||||
|
||||
switch (codeName) {
|
||||
case 'esc':
|
||||
event.preventDefault();
|
||||
return this.handleClose();
|
||||
|
||||
default:
|
||||
event.persist();
|
||||
return this.props.onKeyDown(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]();
|
||||
}
|
||||
}
|
@ -29,6 +29,8 @@
|
||||
border-radius: 16px;
|
||||
margin: 0.75em 0.5em 0 0;
|
||||
padding: 0.25em 1em;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.tagClickable:hover {
|
||||
|
@ -20,8 +20,9 @@ import styles from './tags.css';
|
||||
|
||||
export default class Tags extends Component {
|
||||
static propTypes = {
|
||||
tags: PropTypes.array,
|
||||
handleAddSearchToken: PropTypes.func
|
||||
handleAddSearchToken: PropTypes.func,
|
||||
setRefs: PropTypes.func,
|
||||
tags: PropTypes.array
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -31,13 +32,17 @@ export default class Tags extends Component {
|
||||
}
|
||||
|
||||
renderTags () {
|
||||
const { handleAddSearchToken } = this.props;
|
||||
const { handleAddSearchToken, setRefs } = this.props;
|
||||
const tags = this.props.tags || [];
|
||||
|
||||
const tagClasses = handleAddSearchToken
|
||||
? [ styles.tag, styles.tagClickable ]
|
||||
: [ styles.tag ];
|
||||
|
||||
const setRef = setRefs
|
||||
? (ref) => { setRefs(ref); }
|
||||
: () => {};
|
||||
|
||||
return tags
|
||||
.sort()
|
||||
.map((tag, idx) => {
|
||||
@ -49,7 +54,9 @@ export default class Tags extends Component {
|
||||
<div
|
||||
key={ idx }
|
||||
className={ tagClasses.join(' ') }
|
||||
onClick={ onClick }>
|
||||
onClick={ onClick }
|
||||
ref={ setRef }
|
||||
>
|
||||
{ tag }
|
||||
</div>
|
||||
);
|
||||
|
@ -22,9 +22,6 @@ import { Errors, ParityBackground, Tooltips } from '~/ui';
|
||||
import styles from '../application.css';
|
||||
|
||||
export default class Container extends Component {
|
||||
static contextTypes = {
|
||||
muiTheme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
@ -34,13 +31,10 @@ export default class Container extends Component {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { muiTheme } = this.context;
|
||||
const { children, onCloseFirstRun, showFirstRun, upgradeStore } = this.props;
|
||||
|
||||
return (
|
||||
<ParityBackground
|
||||
className={ styles.container }
|
||||
muiTheme={ muiTheme }>
|
||||
<ParityBackground className={ styles.container }>
|
||||
<FirstRun
|
||||
onClose={ onCloseFirstRun }
|
||||
visible={ showFirstRun } />
|
||||
|
@ -28,9 +28,6 @@ import imagesEthcoreBlock from '../../../assets/images/parity-logo-white-no-text
|
||||
import styles from './parityBar.css';
|
||||
|
||||
class ParityBar extends Component {
|
||||
static contextTypes = {
|
||||
muiTheme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
pending: PropTypes.array,
|
||||
@ -66,7 +63,6 @@ class ParityBar extends Component {
|
||||
|
||||
renderBar () {
|
||||
const { dapp } = this.props;
|
||||
const { muiTheme } = this.context;
|
||||
|
||||
if (!dapp) {
|
||||
return null;
|
||||
@ -80,7 +76,7 @@ class ParityBar extends Component {
|
||||
|
||||
return (
|
||||
<div className={ styles.bar }>
|
||||
<ParityBackground className={ styles.corner } muiTheme={ muiTheme }>
|
||||
<ParityBackground className={ styles.corner }>
|
||||
<div className={ styles.cornercolor }>
|
||||
<Link to='/apps'>
|
||||
<Button
|
||||
@ -100,11 +96,9 @@ class ParityBar extends Component {
|
||||
}
|
||||
|
||||
renderExpanded () {
|
||||
const { muiTheme } = this.context;
|
||||
|
||||
return (
|
||||
<div className={ styles.overlay }>
|
||||
<ParityBackground className={ styles.expanded } muiTheme={ muiTheme }>
|
||||
<ParityBackground className={ styles.expanded }>
|
||||
<div className={ styles.header }>
|
||||
<div className={ styles.title }>
|
||||
<ContainerTitle title='Parity Signer: Pending' />
|
||||
|
@ -95,7 +95,6 @@ class Background extends Component {
|
||||
renderBackgrounds () {
|
||||
const { settings } = this.props;
|
||||
const { seeds } = this.state;
|
||||
const { muiTheme } = this.context;
|
||||
|
||||
return seeds.map((seed, index) => {
|
||||
return (
|
||||
@ -105,7 +104,6 @@ class Background extends Component {
|
||||
className={ settings.backgroundSeed === seed ? styles.seedactive : styles.seed }
|
||||
seed={ seed }
|
||||
onClick={ this.onSelect(seed) }
|
||||
muiTheme={ muiTheme }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user