]> git.wincent.com - hextrapolate.git/blob - src/Field.react.js
a2e39a839c610a1a33d8715db70558699131d511
[hextrapolate.git] / src / Field.react.js
1 /**
2  * Copyright 2015-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) { // eslint-disable-line no-empty
83       // Swallow.
84       this.setState({copySucceeded: false});
85     }
86   }
87
88   _copyLink() {
89     // Would check `document.queryCommandSupported('copy')` here, but that
90     // doesn't work; see:
91     // - https://code.google.com/p/chromium/issues/detail?id=476508
92     // - https://github.com/w3c/clipboard-apis/issues/4
93     return (
94       <span
95         className="hextrapolate-copy"
96         onClick={this._onCopy}
97         title="Copy to Clipboard">
98         copy
99       </span>
100     );
101   }
102
103   _copyStatus() {
104     const classNames = cx({
105       'hextrapolate-copy-status': true,
106       'hextrapolate-copy-success': this.state.copySucceeded,
107     });
108     return <span className={classNames}>&#x2713;</span>;
109   }
110
111   focus() {
112     React.findDOMNode(this._input).focus();
113   }
114
115   render() {
116     return (
117       <span className="hextrapolate-field">
118         <input
119           onChange={this._onChange}
120           ref={ref => this._input = ref}
121           type="text"
122           value={fromValue(this.props.value, this.props.base)}
123         />
124         {this._copyStatus()}
125         {this._copyLink()}
126       </span>
127     );
128   }
129 }