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