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