]> git.wincent.com - hextrapolate.git/blob - src/Field.react.js
0b6dd200eb91a3bbdcabbf8ec1154c2702f2f5ce
[hextrapolate.git] / src / Field.react.js
1 /**
2  * Copyright 2003-present Greg Hurrell. All rights reserved.
3  * Licensed under the terms of the MIT license.
4  *
5  * @flow
6  */
7
8 import DIGITS from './DIGITS';
9 import React from 'react';
10 import convert from './convert';
11 import cx from 'classnames';
12
13 export type Value = {
14   base: number;
15   value: string;
16 };
17 export const ValuePropType = React.PropTypes.shape({
18   base: React.PropTypes.number,
19   value: React.PropTypes.string,
20 });
21
22 /**
23  * Convert from `value` to `base`.
24  */
25 function fromValue(value: ?Value, base: number): string {
26   if (value === null) {
27     return '';
28   } else if (value.base === base) {
29     return value.value;
30   }
31   return convert(value.value, value.base, base);
32 }
33
34 export default class Field extends React.Component {
35   static propTypes = {
36     base: React.PropTypes.number,
37     onValueChange: React.PropTypes.func.required,
38     value: ValuePropType,
39   };
40   static defaultProps = {
41     base: 10,
42   };
43
44   constructor(props) {
45     super(props);
46     if (props.base < 2 || props.base > DIGITS.length) {
47       throw new Error(
48         `base prop must be between 2..${DIGITS.length}`
49       );
50     }
51     this.state = {copySucceeded: false};
52   }
53
54   _isValid(value: string): boolean {
55     const validator = new RegExp(
56       `^[${DIGITS.slice(0, this.props.base)}]*$`
57     );
58     return validator.test(value.trim().toLowerCase());
59   }
60
61   _onChange = event => {
62     const value = event.currentTarget.value;
63     if (this._isValid(value)) {
64       this.props.onValueChange({
65         base: this.props.base,
66         value,
67       });
68     }
69   }
70
71   _onCopy = () => {
72     React.findDOMNode(this._input).select();
73
74     // May throw a SecurityError.
75     try {
76       this.setState({
77         copySucceeded: document.execCommand('copy'),
78       });
79       setTimeout(() => this.setState({copySucceeded: false}), 750);
80     } catch(error) {
81       this.setState({copySucceeded: false});
82     }
83   }
84
85   _copyLink() {
86     // Would check `document.queryCommandSupported('copy')` here, but that
87     // doesn't work; see:
88     // - https://code.google.com/p/chromium/issues/detail?id=476508
89     // - https://github.com/w3c/clipboard-apis/issues/4
90     return (
91       <span
92         className="hextrapolate-copy"
93         onClick={this._onCopy}
94         title="Copy to Clipboard">
95         copy
96       </span>
97     );
98   }
99
100   _copyStatus() {
101     const classNames = cx({
102       'hextrapolate-copy-status': true,
103       'hextrapolate-copy-success': this.state.copySucceeded,
104     });
105     return <span className={classNames}>&#x2713;</span>;
106   }
107
108   focus() {
109     React.findDOMNode(this._input).focus();
110   }
111
112   render() {
113     return (
114       <span className="hextrapolate-field">
115         <input
116           onChange={this._onChange}
117           ref={ref => this._input = ref}
118           type="text"
119           value={fromValue(this.props.value, this.props.base)}
120         />
121         {this._copyStatus()}
122         {this._copyLink()}
123       </span>
124     );
125   }
126 }