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-dom": "15.4.1",
|
||||||
"react-dropzone": "3.7.3",
|
"react-dropzone": "3.7.3",
|
||||||
"react-intl": "2.1.5",
|
"react-intl": "2.1.5",
|
||||||
|
"react-portal": "3.0.0",
|
||||||
"react-redux": "4.4.6",
|
"react-redux": "4.4.6",
|
||||||
"react-router": "3.0.0",
|
"react-router": "3.0.0",
|
||||||
"react-router-redux": "4.0.7",
|
"react-router-redux": "4.0.7",
|
||||||
|
@ -22,7 +22,7 @@ import { ContextProvider, muiTheme } from '~/ui';
|
|||||||
|
|
||||||
import DetailsStep from './';
|
import DetailsStep from './';
|
||||||
|
|
||||||
import { CONTRACT } from '../executeContract.test.js';
|
import { STORE, CONTRACT } from '../executeContract.test.js';
|
||||||
|
|
||||||
let component;
|
let component;
|
||||||
let onAmountChange;
|
let onAmountChange;
|
||||||
@ -41,7 +41,7 @@ function render (props) {
|
|||||||
onValueChange = sinon.stub();
|
onValueChange = sinon.stub();
|
||||||
|
|
||||||
component = mount(
|
component = mount(
|
||||||
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ {} }>
|
<ContextProvider api={ {} } muiTheme={ muiTheme } store={ STORE }>
|
||||||
<DetailsStep
|
<DetailsStep
|
||||||
{ ...props }
|
{ ...props }
|
||||||
contract={ CONTRACT }
|
contract={ CONTRACT }
|
||||||
|
@ -42,7 +42,7 @@ function render (props) {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('modals/ExecuteContract/DetailsStep', () => {
|
describe('modals/ExecuteContract', () => {
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
expect(render({ accounts: {} })).to.be.ok;
|
expect(render({ accounts: {} })).to.be.ok;
|
||||||
});
|
});
|
||||||
|
@ -53,6 +53,12 @@ const STORE = {
|
|||||||
},
|
},
|
||||||
nodeStatus: {
|
nodeStatus: {
|
||||||
gasLimit: new BigNumber(123)
|
gasLimit: new BigNumber(123)
|
||||||
|
},
|
||||||
|
personal: {
|
||||||
|
accountsInfo: {}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
backgroundSeed: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -44,12 +44,12 @@ export default class Extras extends Component {
|
|||||||
error={ minBlockError }
|
error={ minBlockError }
|
||||||
hint={
|
hint={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='executeContract.advanced.minBlock.hint'
|
id='transferModal.minBlock.hint'
|
||||||
defaultMessage='Only post the transaction after this block' />
|
defaultMessage='Only post the transaction after this block' />
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='executeContract.advanced.minBlock.label'
|
id='transferModal.minBlock.label'
|
||||||
defaultMessage='BlockNumber to send from' />
|
defaultMessage='BlockNumber to send from' />
|
||||||
}
|
}
|
||||||
value={ minBlock }
|
value={ minBlock }
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// 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';
|
import store from 'store';
|
||||||
|
|
||||||
const LS_UPDATE = '_parity::update';
|
const LS_UPDATE = '_parity::update';
|
||||||
@ -129,7 +129,10 @@ export default class Store {
|
|||||||
this._api.parity.versionInfo()
|
this._api.parity.versionInfo()
|
||||||
])
|
])
|
||||||
.then(([available, consensusCapability, version]) => {
|
.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);
|
this.setVersions(available, version, consensusCapability);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -48,7 +48,7 @@ function setBalances (_balances) {
|
|||||||
|
|
||||||
const balance = Object.assign({}, balances[address]);
|
const balance = Object.assign({}, balances[address]);
|
||||||
const { tokens, txCount = balance.txCount } = nextBalances[address];
|
const { tokens, txCount = balance.txCount } = nextBalances[address];
|
||||||
const nextTokens = [].concat(balance.tokens);
|
const nextTokens = balance.tokens.slice();
|
||||||
|
|
||||||
tokens.forEach((t) => {
|
tokens.forEach((t) => {
|
||||||
const { token, value } = t;
|
const { token, value } = t;
|
||||||
|
@ -127,6 +127,10 @@ export default class CertificationsMiddleware {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
if (/does not exist/.test(err.toString())) {
|
||||||
|
return console.warn(err.toString());
|
||||||
|
}
|
||||||
|
|
||||||
console.warn(`Could not fetch certifier ${id}:`, err);
|
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
|
/* You should have received a copy of the GNU General Public License
|
||||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
.account {
|
|
||||||
padding: 0.25em 0;
|
.input {
|
||||||
display: flex;
|
box-sizing: border-box;
|
||||||
align-items: center;
|
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 {
|
&::placeholder {
|
||||||
height: 32px;
|
color: #a2a2a2;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.paddedInput input {
|
.inputAddress {
|
||||||
padding-left: 46px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&:hover, *:hover {
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem {
|
.main {
|
||||||
min-height: 0 !important;
|
position: relative;
|
||||||
line-height: inherit !important;
|
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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// 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 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 { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { fromWei } from '~/api/util/wei';
|
import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
|
||||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
|
||||||
|
|
||||||
import AutoComplete from '../AutoComplete';
|
import AccountCard from '~/ui/AccountCard';
|
||||||
import IdentityIcon from '../../IdentityIcon';
|
import InputAddress from '~/ui/Form/InputAddress';
|
||||||
import IdentityName from '../../IdentityName';
|
import Portal from '~/ui/Portal';
|
||||||
|
import { validateAddress } from '~/util/validation';
|
||||||
|
|
||||||
import styles from './addressSelect.css';
|
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 = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
muiTheme: PropTypes.object.isRequired
|
||||||
}
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
// Required props
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// Redux props
|
||||||
|
accountsInfo: PropTypes.object,
|
||||||
accounts: PropTypes.object,
|
accounts: PropTypes.object,
|
||||||
allowInput: PropTypes.bool,
|
|
||||||
balances: PropTypes.object,
|
balances: PropTypes.object,
|
||||||
contacts: PropTypes.object,
|
contacts: PropTypes.object,
|
||||||
contracts: PropTypes.object,
|
contracts: PropTypes.object,
|
||||||
disabled: PropTypes.bool,
|
|
||||||
error: nodeOrStringProptype(),
|
|
||||||
hint: nodeOrStringProptype(),
|
|
||||||
label: nodeOrStringProptype(),
|
|
||||||
tokens: PropTypes.object,
|
tokens: PropTypes.object,
|
||||||
value: PropTypes.string,
|
wallets: PropTypes.object,
|
||||||
wallets: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
// Optional props
|
||||||
autocompleteEntries: [],
|
allowInput: PropTypes.bool,
|
||||||
entries: {},
|
disabled: PropTypes.bool,
|
||||||
addresses: [],
|
error: PropTypes.string,
|
||||||
value: ''
|
hint: PropTypes.string,
|
||||||
}
|
label: PropTypes.string,
|
||||||
|
value: PropTypes.string
|
||||||
// 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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { autocompleteEntries, entries };
|
static defaultProps = {
|
||||||
}
|
value: ''
|
||||||
|
};
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
state = {
|
||||||
const keys = [ 'error', 'value' ];
|
expanded: false,
|
||||||
|
focused: false,
|
||||||
const prevValues = pick(this.props, keys);
|
focusedCat: null,
|
||||||
const nextValues = pick(nextProps, keys);
|
focusedItem: null,
|
||||||
|
inputFocused: false,
|
||||||
return !isEqual(prevValues, nextValues);
|
inputValue: '',
|
||||||
}
|
values: []
|
||||||
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
const { value } = this.props;
|
this.setValues();
|
||||||
const { entries, autocompleteEntries } = this.entriesFromProps();
|
|
||||||
const addresses = Object.keys(entries).sort();
|
|
||||||
|
|
||||||
this.setState({ autocompleteEntries, entries, addresses, value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (newProps.value !== this.props.value) {
|
if (this.values && this.values.length > 0) {
|
||||||
this.setState({ value: newProps.value });
|
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 () {
|
render () {
|
||||||
const { allowInput, disabled, error, hint, label } = this.props;
|
const input = this.renderInput();
|
||||||
const { autocompleteEntries, value } = this.state;
|
const content = this.renderContent();
|
||||||
|
|
||||||
const searchText = this.getSearchText();
|
const classes = [ styles.main ];
|
||||||
const icon = this.renderIdentityIcon(value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.container }>
|
<div
|
||||||
<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
|
|
||||||
className={ classes.join(' ') }
|
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) => {
|
renderInput () {
|
||||||
const { address, name } = entry;
|
const { focused } = this.state;
|
||||||
|
const { accountsInfo, disabled, error, hint, label, value } = this.props;
|
||||||
|
|
||||||
const _balance = this.getBalance(address);
|
const input = (
|
||||||
const balance = _balance ? _balance.toNumber() : _balance;
|
<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) {
|
if (disabled) {
|
||||||
this.items[address] = {
|
return input;
|
||||||
address,
|
|
||||||
balance,
|
|
||||||
text: name && name.toUpperCase() || address,
|
|
||||||
value: this.renderMenuItem(address)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.items[address];
|
return (
|
||||||
|
<div className={ styles.inputAddress }>
|
||||||
|
{ input }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBalance (address) {
|
renderContent () {
|
||||||
const { balances = {} } = this.props;
|
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];
|
const balance = balances[address];
|
||||||
|
const account = {
|
||||||
if (!balance) {
|
...accountsInfo[address],
|
||||||
return null;
|
address, index
|
||||||
}
|
};
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.balance }>
|
<AccountCard
|
||||||
{ value.toFormat(3) }<small> { 'ETH' }</small>
|
account={ account }
|
||||||
</div>
|
balance={ balance }
|
||||||
|
key={ `account_${index}` }
|
||||||
|
onClick={ this.handleClick }
|
||||||
|
onFocus={ this.focusItem }
|
||||||
|
ref={ `account_${index}` }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMenuItem (address) {
|
setInputRef = (refId) => {
|
||||||
const balance = this.props.balances
|
this.inputRef = refId;
|
||||||
? 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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchText () {
|
handleCustomInput = () => {
|
||||||
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) => {
|
|
||||||
const { allowInput } = this.props;
|
const { allowInput } = this.props;
|
||||||
const { value } = this.state;
|
const { inputValue, values } = this.state;
|
||||||
|
|
||||||
const address = entry && entry.address
|
// If input is HEX and allowInput === true, send it
|
||||||
? entry.address
|
if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) {
|
||||||
: ((empty && !allowInput) ? '' : value);
|
return this.handleClick(inputValue);
|
||||||
|
|
||||||
this.props.onChange(null, address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateInput = (query, choices) => {
|
// If only one value, select it
|
||||||
const { api } = this.context;
|
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}`)) {
|
// Simulate click on input address if enter is pressed
|
||||||
const checksumed = api.util.toChecksumAddress(`0x${address}`);
|
if (code === 'enter') {
|
||||||
return this.props.onChange(null, checksumed);
|
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.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'
|
borderBottom: 'solid 2px'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UNDERLINE_FOCUSED = {
|
||||||
|
transform: 'scaleX(1.0)'
|
||||||
|
};
|
||||||
|
|
||||||
const NAME_ID = ' ';
|
const NAME_ID = ' ';
|
||||||
|
|
||||||
export default class Input extends Component {
|
export default class Input extends Component {
|
||||||
@ -51,6 +55,7 @@ export default class Input extends Component {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
error: nodeOrStringProptype(),
|
error: nodeOrStringProptype(),
|
||||||
|
focused: PropTypes.bool,
|
||||||
readOnly: PropTypes.bool,
|
readOnly: PropTypes.bool,
|
||||||
floatCopy: PropTypes.bool,
|
floatCopy: PropTypes.bool,
|
||||||
hint: nodeOrStringProptype(),
|
hint: nodeOrStringProptype(),
|
||||||
@ -61,9 +66,12 @@ export default class Input extends Component {
|
|||||||
multiLine: PropTypes.bool,
|
multiLine: PropTypes.bool,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
rows: PropTypes.number,
|
rows: PropTypes.number,
|
||||||
|
tabIndex: PropTypes.number,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
submitOnBlur: PropTypes.bool,
|
submitOnBlur: PropTypes.bool,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
@ -92,11 +100,20 @@ export default class Input extends Component {
|
|||||||
if ((newProps.value !== this.props.value) && (newProps.value !== this.state.value)) {
|
if ((newProps.value !== this.props.value) && (newProps.value !== this.state.value)) {
|
||||||
this.setValue(newProps.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 () {
|
render () {
|
||||||
const { value } = this.state;
|
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;
|
const readOnly = this.props.readOnly || disabled;
|
||||||
|
|
||||||
@ -111,6 +128,11 @@ export default class Input extends Component {
|
|||||||
textFieldStyle.height = 'initial';
|
textFieldStyle.height = 'initial';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const underlineStyle = readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL;
|
||||||
|
const underlineFocusStyle = focused
|
||||||
|
? UNDERLINE_FOCUSED
|
||||||
|
: readOnly && typeof focused !== 'boolean' ? { display: 'none' } : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.container } style={ style }>
|
<div className={ styles.container } style={ style }>
|
||||||
{ this.renderCopyButton() }
|
{ this.renderCopyButton() }
|
||||||
@ -130,15 +152,19 @@ export default class Input extends Component {
|
|||||||
name={ NAME_ID }
|
name={ NAME_ID }
|
||||||
onBlur={ this.onBlur }
|
onBlur={ this.onBlur }
|
||||||
onChange={ this.onChange }
|
onChange={ this.onChange }
|
||||||
|
onClick={ onClick }
|
||||||
onKeyDown={ this.onKeyDown }
|
onKeyDown={ this.onKeyDown }
|
||||||
|
onFocus={ onFocus }
|
||||||
onPaste={ this.onPaste }
|
onPaste={ this.onPaste }
|
||||||
readOnly={ readOnly }
|
readOnly={ readOnly }
|
||||||
|
ref='input'
|
||||||
rows={ rows }
|
rows={ rows }
|
||||||
style={ textFieldStyle }
|
style={ textFieldStyle }
|
||||||
|
tabIndex={ tabIndex }
|
||||||
type={ type || 'text' }
|
type={ type || 'text' }
|
||||||
underlineDisabledStyle={ UNDERLINE_DISABLED }
|
underlineDisabledStyle={ UNDERLINE_DISABLED }
|
||||||
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
|
underlineStyle={ underlineStyle }
|
||||||
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
|
underlineFocusStyle={ underlineFocusStyle }
|
||||||
underlineShow={ !hideUnderline }
|
underlineShow={ !hideUnderline }
|
||||||
value={ value }>
|
value={ value }>
|
||||||
{ children }
|
{ children }
|
||||||
|
@ -34,12 +34,17 @@ class InputAddress extends Component {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
|
focused: PropTypes.bool,
|
||||||
hideUnderline: PropTypes.bool,
|
hideUnderline: PropTypes.bool,
|
||||||
hint: nodeOrStringProptype(),
|
hint: nodeOrStringProptype(),
|
||||||
label: nodeOrStringProptype(),
|
label: nodeOrStringProptype(),
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
|
readOnly: PropTypes.bool,
|
||||||
small: PropTypes.bool,
|
small: PropTypes.bool,
|
||||||
|
tabIndex: PropTypes.number,
|
||||||
text: PropTypes.bool,
|
text: PropTypes.bool,
|
||||||
tokens: PropTypes.object,
|
tokens: PropTypes.object,
|
||||||
value: PropTypes.string
|
value: PropTypes.string
|
||||||
@ -52,10 +57,11 @@ class InputAddress extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { className, disabled, error, hint, label, text, value } = this.props;
|
const { accountsInfo, allowCopy, className, disabled, error, focused, hint } = this.props;
|
||||||
const { accountsInfo, allowCopy, hideUnderline, onSubmit, small, tokens } = 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();
|
const icon = this.renderIcon();
|
||||||
|
|
||||||
@ -63,7 +69,7 @@ class InputAddress extends Component {
|
|||||||
classes.push(!icon ? styles.inputEmpty : styles.input);
|
classes.push(!icon ? styles.inputEmpty : styles.input);
|
||||||
|
|
||||||
const containerClasses = [ styles.container ];
|
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) {
|
if (small) {
|
||||||
containerClasses.push(styles.small);
|
containerClasses.push(styles.small);
|
||||||
@ -76,11 +82,16 @@ class InputAddress extends Component {
|
|||||||
className={ classes.join(' ') }
|
className={ classes.join(' ') }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
error={ error }
|
error={ error }
|
||||||
|
focused={ focused }
|
||||||
hideUnderline={ hideUnderline }
|
hideUnderline={ hideUnderline }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
label={ label }
|
label={ label }
|
||||||
onChange={ this.handleInputChange }
|
onChange={ this.handleInputChange }
|
||||||
|
onClick={ onClick }
|
||||||
|
onFocus={ onFocus }
|
||||||
onSubmit={ onSubmit }
|
onSubmit={ onSubmit }
|
||||||
|
readOnly={ readOnly }
|
||||||
|
tabIndex={ tabIndex }
|
||||||
value={
|
value={
|
||||||
text && account
|
text && account
|
||||||
? account.name
|
? account.name
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import AddIcon from 'material-ui/svg-icons/content/add';
|
import AddIcon from 'material-ui/svg-icons/content/add';
|
||||||
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
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 ContractIcon from 'material-ui/svg-icons/action/code';
|
||||||
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
||||||
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||||
@ -25,6 +26,7 @@ import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
|||||||
export {
|
export {
|
||||||
AddIcon,
|
AddIcon,
|
||||||
CancelIcon,
|
CancelIcon,
|
||||||
|
CloseIcon,
|
||||||
ContractIcon,
|
ContractIcon,
|
||||||
DoneIcon,
|
DoneIcon,
|
||||||
PrevIcon,
|
PrevIcon,
|
||||||
|
@ -18,48 +18,70 @@ import React, { Component, PropTypes } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
class ParityBackground extends Component {
|
class ParityBackground extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
muiTheme: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
style: PropTypes.object.isRequired,
|
backgroundSeed: PropTypes.string,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
onClick: PropTypes.func
|
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 () {
|
render () {
|
||||||
const { children, className, style, onClick } = this.props;
|
const { children, className, onClick } = this.props;
|
||||||
|
const { style } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ className }
|
className={ className }
|
||||||
style={ style }
|
style={ style }
|
||||||
onTouchTap={ onClick }>
|
onTouchTap={ onClick }
|
||||||
|
>
|
||||||
{ children }
|
{ children }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (_, initProps) {
|
function mapStateToProps (state) {
|
||||||
const { gradient, seed, muiTheme } = initProps;
|
|
||||||
|
|
||||||
let _seed = seed;
|
|
||||||
let _props = { style: muiTheme.parity.getBackgroundStyle(gradient, seed) };
|
|
||||||
|
|
||||||
return (state, props) => {
|
|
||||||
const { backgroundSeed } = state.settings;
|
const { backgroundSeed } = state.settings;
|
||||||
const { seed } = props;
|
return { backgroundSeed };
|
||||||
|
|
||||||
const newSeed = seed || backgroundSeed;
|
|
||||||
|
|
||||||
if (newSeed === _seed) {
|
|
||||||
return _props;
|
|
||||||
}
|
|
||||||
|
|
||||||
_seed = newSeed;
|
|
||||||
_props = { style: muiTheme.parity.getBackgroundStyle(gradient, newSeed) };
|
|
||||||
|
|
||||||
return _props;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
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;
|
border-radius: 16px;
|
||||||
margin: 0.75em 0.5em 0 0;
|
margin: 0.75em 0.5em 0 0;
|
||||||
padding: 0.25em 1em;
|
padding: 0.25em 1em;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagClickable:hover {
|
.tagClickable:hover {
|
||||||
|
@ -20,8 +20,9 @@ import styles from './tags.css';
|
|||||||
|
|
||||||
export default class Tags extends Component {
|
export default class Tags extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tags: PropTypes.array,
|
handleAddSearchToken: PropTypes.func,
|
||||||
handleAddSearchToken: PropTypes.func
|
setRefs: PropTypes.func,
|
||||||
|
tags: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@ -31,13 +32,17 @@ export default class Tags extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTags () {
|
renderTags () {
|
||||||
const { handleAddSearchToken } = this.props;
|
const { handleAddSearchToken, setRefs } = this.props;
|
||||||
const tags = this.props.tags || [];
|
const tags = this.props.tags || [];
|
||||||
|
|
||||||
const tagClasses = handleAddSearchToken
|
const tagClasses = handleAddSearchToken
|
||||||
? [ styles.tag, styles.tagClickable ]
|
? [ styles.tag, styles.tagClickable ]
|
||||||
: [ styles.tag ];
|
: [ styles.tag ];
|
||||||
|
|
||||||
|
const setRef = setRefs
|
||||||
|
? (ref) => { setRefs(ref); }
|
||||||
|
: () => {};
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
.sort()
|
.sort()
|
||||||
.map((tag, idx) => {
|
.map((tag, idx) => {
|
||||||
@ -49,7 +54,9 @@ export default class Tags extends Component {
|
|||||||
<div
|
<div
|
||||||
key={ idx }
|
key={ idx }
|
||||||
className={ tagClasses.join(' ') }
|
className={ tagClasses.join(' ') }
|
||||||
onClick={ onClick }>
|
onClick={ onClick }
|
||||||
|
ref={ setRef }
|
||||||
|
>
|
||||||
{ tag }
|
{ tag }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -22,9 +22,6 @@ import { Errors, ParityBackground, Tooltips } from '~/ui';
|
|||||||
import styles from '../application.css';
|
import styles from '../application.css';
|
||||||
|
|
||||||
export default class Container extends Component {
|
export default class Container extends Component {
|
||||||
static contextTypes = {
|
|
||||||
muiTheme: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
@ -34,13 +31,10 @@ export default class Container extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { muiTheme } = this.context;
|
|
||||||
const { children, onCloseFirstRun, showFirstRun, upgradeStore } = this.props;
|
const { children, onCloseFirstRun, showFirstRun, upgradeStore } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParityBackground
|
<ParityBackground className={ styles.container }>
|
||||||
className={ styles.container }
|
|
||||||
muiTheme={ muiTheme }>
|
|
||||||
<FirstRun
|
<FirstRun
|
||||||
onClose={ onCloseFirstRun }
|
onClose={ onCloseFirstRun }
|
||||||
visible={ showFirstRun } />
|
visible={ showFirstRun } />
|
||||||
|
@ -28,9 +28,6 @@ import imagesEthcoreBlock from '../../../assets/images/parity-logo-white-no-text
|
|||||||
import styles from './parityBar.css';
|
import styles from './parityBar.css';
|
||||||
|
|
||||||
class ParityBar extends Component {
|
class ParityBar extends Component {
|
||||||
static contextTypes = {
|
|
||||||
muiTheme: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
pending: PropTypes.array,
|
pending: PropTypes.array,
|
||||||
@ -66,7 +63,6 @@ class ParityBar extends Component {
|
|||||||
|
|
||||||
renderBar () {
|
renderBar () {
|
||||||
const { dapp } = this.props;
|
const { dapp } = this.props;
|
||||||
const { muiTheme } = this.context;
|
|
||||||
|
|
||||||
if (!dapp) {
|
if (!dapp) {
|
||||||
return null;
|
return null;
|
||||||
@ -80,7 +76,7 @@ class ParityBar extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.bar }>
|
<div className={ styles.bar }>
|
||||||
<ParityBackground className={ styles.corner } muiTheme={ muiTheme }>
|
<ParityBackground className={ styles.corner }>
|
||||||
<div className={ styles.cornercolor }>
|
<div className={ styles.cornercolor }>
|
||||||
<Link to='/apps'>
|
<Link to='/apps'>
|
||||||
<Button
|
<Button
|
||||||
@ -100,11 +96,9 @@ class ParityBar extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded () {
|
renderExpanded () {
|
||||||
const { muiTheme } = this.context;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.overlay }>
|
<div className={ styles.overlay }>
|
||||||
<ParityBackground className={ styles.expanded } muiTheme={ muiTheme }>
|
<ParityBackground className={ styles.expanded }>
|
||||||
<div className={ styles.header }>
|
<div className={ styles.header }>
|
||||||
<div className={ styles.title }>
|
<div className={ styles.title }>
|
||||||
<ContainerTitle title='Parity Signer: Pending' />
|
<ContainerTitle title='Parity Signer: Pending' />
|
||||||
|
@ -95,7 +95,6 @@ class Background extends Component {
|
|||||||
renderBackgrounds () {
|
renderBackgrounds () {
|
||||||
const { settings } = this.props;
|
const { settings } = this.props;
|
||||||
const { seeds } = this.state;
|
const { seeds } = this.state;
|
||||||
const { muiTheme } = this.context;
|
|
||||||
|
|
||||||
return seeds.map((seed, index) => {
|
return seeds.map((seed, index) => {
|
||||||
return (
|
return (
|
||||||
@ -105,7 +104,6 @@ class Background extends Component {
|
|||||||
className={ settings.backgroundSeed === seed ? styles.seedactive : styles.seed }
|
className={ settings.backgroundSeed === seed ? styles.seedactive : styles.seed }
|
||||||
seed={ seed }
|
seed={ seed }
|
||||||
onClick={ this.onSelect(seed) }
|
onClick={ this.onSelect(seed) }
|
||||||
muiTheme={ muiTheme }
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user