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