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