]> git.wincent.com - hextrapolate.git/blob - src/Field.react.js
912a2aee80e88ec52e9ee9acef1ca7ab7e8ff9f4
[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
13 export type Value = {
14   base: number;
15   value: string;
16 };
17 export const ValuePropType = React.PropTypes.shape({
18   base: React.PropTypes.number,
19   value: React.PropTypes.string,
20 });
21
22 const DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz';
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   }
54
55   _isValid(value: string): boolean {
56     const validator = new RegExp(
57       `^[${DIGITS.slice(0, this.props.base)}]*$`
58     );
59     return validator.test(value.trim().toLowerCase());
60   }
61
62   _onChange = event => {
63     const value = event.currentTarget.value;
64     if (this._isValid(value)) {
65       this.props.onValueChange({
66         base: this.props.base,
67         value,
68       });
69     }
70   }
71
72   _onCopy = () => {
73     React.findDOMNode(this._input).select();
74
75     // May throw a SecurityError.
76     try {
77       document.execCommand('copy');
78     } catch(error) { // eslint-disable-line no-empty
79       // Swallow.
80     }
81   }
82
83   _copyLink() {
84     // Would check `document.queryCommandSupported('copy')` here, but that
85     // doesn't work; see:
86     // - https://code.google.com/p/chromium/issues/detail?id=476508
87     // - https://github.com/w3c/clipboard-apis/issues/4
88     return (
89       <span
90         className="hextrapolate-copy"
91         onClick={this._onCopy}
92         title="Copy to Clipboard">
93         copy
94       </span>
95     );
96   }
97
98   focus() {
99     React.findDOMNode(this._input).focus();
100   }
101
102   render() {
103     return (
104       <span className="hextrapolate-field">
105         <input
106           onChange={this._onChange}
107           ref={ref => this._input = ref}
108           type="text"
109           value={fromValue(this.props.value, this.props.base)}
110         />
111         {this._copyLink()}
112       </span>
113     );
114   }
115 }