2016-07-16 02:06:10 +00:00
|
|
|
import React, {Component, PropTypes} from 'react';
|
|
|
|
import classnames from 'classnames';
|
2016-11-10 02:40:53 +00:00
|
|
|
import {DEBOUNCE_MILLIS} from '../../../common/constants';
|
2016-11-23 19:33:24 +00:00
|
|
|
import FileInputButton from '../base/FileInputButton';
|
|
|
|
import {Dropdown, DropdownItem, DropdownButton} from './dropdown/index';
|
2016-12-05 22:42:40 +00:00
|
|
|
import PromptButton from '../base/PromptButton';
|
2016-04-09 01:14:25 +00:00
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
const NAME = 'name';
|
|
|
|
const VALUE = 'value';
|
|
|
|
const ENTER = 13;
|
|
|
|
const BACKSPACE = 8;
|
|
|
|
const UP = 38;
|
|
|
|
const DOWN = 40;
|
2016-07-20 23:16:28 +00:00
|
|
|
const LEFT = 37;
|
|
|
|
const RIGHT = 39;
|
2016-04-09 18:47:51 +00:00
|
|
|
|
2016-04-09 01:14:25 +00:00
|
|
|
class KeyValueEditor extends Component {
|
2016-04-09 18:47:51 +00:00
|
|
|
constructor (props) {
|
|
|
|
super(props);
|
2016-06-20 06:05:40 +00:00
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
this._focusedPair = -1;
|
|
|
|
this._focusedField = NAME;
|
2016-11-22 19:42:10 +00:00
|
|
|
this._nameInputs = {};
|
|
|
|
this._valueInputs = {};
|
|
|
|
this._focusedInput = null;
|
2016-06-20 06:05:40 +00:00
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
this.state = {
|
|
|
|
pairs: props.pairs
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-29 20:55:31 +00:00
|
|
|
_handleAddFromName = () => {
|
|
|
|
this._focusedField = NAME;
|
|
|
|
this._addPair();
|
|
|
|
};
|
|
|
|
|
|
|
|
_handleAddFromValue = () => {
|
|
|
|
this._focusedField = VALUE;
|
|
|
|
this._addPair();
|
|
|
|
};
|
|
|
|
|
|
|
|
_handleAddFromMultipart = type => {
|
|
|
|
this._focusedField = null;
|
|
|
|
this._addPair(this.state.pairs.length, {type});
|
|
|
|
};
|
|
|
|
|
2016-08-15 17:04:36 +00:00
|
|
|
_onChange (pairs, updateState = true) {
|
2016-11-22 19:42:10 +00:00
|
|
|
clearTimeout(this._triggerTimeout);
|
|
|
|
this._triggerTimeout = setTimeout(() => this.props.onChange(pairs), DEBOUNCE_MILLIS);
|
2016-08-15 17:04:36 +00:00
|
|
|
updateState && this.setState({pairs});
|
2016-04-09 18:47:51 +00:00
|
|
|
}
|
|
|
|
|
2016-11-29 20:55:31 +00:00
|
|
|
_addPair (position, patch) {
|
2016-09-21 03:00:40 +00:00
|
|
|
const numPairs = this.state.pairs.length;
|
|
|
|
const {maxPairs} = this.props;
|
|
|
|
|
|
|
|
// Don't add any more pairs
|
|
|
|
if (maxPairs !== undefined && numPairs >= maxPairs) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
position = position === undefined ? numPairs : position;
|
2016-04-09 18:47:51 +00:00
|
|
|
this._focusedPair = position;
|
|
|
|
const pairs = [
|
|
|
|
...this.state.pairs.slice(0, position),
|
2016-11-29 20:55:31 +00:00
|
|
|
Object.assign({name: '', value: ''}, patch),
|
2016-04-09 18:47:51 +00:00
|
|
|
...this.state.pairs.slice(position)
|
|
|
|
];
|
|
|
|
|
2016-11-23 19:33:24 +00:00
|
|
|
this.props.onCreate && this.props.onCreate();
|
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
this._onChange(pairs);
|
|
|
|
}
|
|
|
|
|
|
|
|
_deletePair (position) {
|
|
|
|
if (this._focusedPair >= position) {
|
|
|
|
this._focusedPair = this._focusedPair - 1;
|
|
|
|
}
|
2016-11-23 19:33:24 +00:00
|
|
|
|
|
|
|
const pair = this.state.pairs[position];
|
|
|
|
this.props.onDelete && this.props.onDelete(pair);
|
|
|
|
|
|
|
|
const pairs = this.state.pairs.filter((_, i) => i !== position);
|
|
|
|
|
|
|
|
this._onChange(pairs);
|
2016-04-09 01:14:25 +00:00
|
|
|
}
|
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
_updatePair (position, pairPatch) {
|
2016-11-23 19:33:24 +00:00
|
|
|
const pairs = this.state.pairs.map((p, i) => (
|
|
|
|
i == position ? Object.assign({}, p, pairPatch) : p
|
|
|
|
));
|
|
|
|
|
2016-08-04 02:41:51 +00:00
|
|
|
this._onChange(pairs);
|
2016-04-09 18:47:51 +00:00
|
|
|
}
|
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
_togglePair (position) {
|
|
|
|
const pairs = this.state.pairs.map(
|
|
|
|
(p, i) => i == position ? Object.assign({}, p, {disabled: !p.disabled}) : p
|
|
|
|
);
|
2016-11-23 19:33:24 +00:00
|
|
|
|
|
|
|
const pair = pairs[position];
|
|
|
|
this.props.onToggleDisable && this.props.onToggleDisable(pair);
|
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
this._onChange(pairs, true);
|
|
|
|
}
|
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
_focusNext (addIfValue = false) {
|
|
|
|
if (this._focusedField === NAME) {
|
|
|
|
this._focusedField = VALUE;
|
|
|
|
this._updateFocus();
|
|
|
|
} else if (this._focusedField === VALUE) {
|
|
|
|
this._focusedField = NAME;
|
|
|
|
if (addIfValue) {
|
|
|
|
this._addPair(this._focusedPair + 1);
|
|
|
|
} else {
|
|
|
|
this._focusNextPair();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_focusPrevious (deleteIfEmpty = false) {
|
|
|
|
if (this._focusedField === VALUE) {
|
|
|
|
this._focusedField = NAME;
|
|
|
|
this._updateFocus();
|
|
|
|
} else if (this._focusedField === NAME) {
|
|
|
|
const pair = this.state.pairs[this._focusedPair];
|
|
|
|
if (!pair.name && !pair.value && deleteIfEmpty) {
|
|
|
|
this._focusedField = VALUE;
|
|
|
|
this._deletePair(this._focusedPair);
|
|
|
|
} else if (!pair.name) {
|
|
|
|
this._focusedField = VALUE;
|
|
|
|
this._focusPreviousPair();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_focusNextPair () {
|
|
|
|
if (this._focusedPair >= this.state.pairs.length - 1) {
|
|
|
|
this._addPair();
|
|
|
|
} else {
|
|
|
|
this._focusedPair++;
|
|
|
|
this._updateFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_focusPreviousPair () {
|
2016-09-14 23:25:19 +00:00
|
|
|
if (this._focusedPair > 0) {
|
2016-04-09 18:47:51 +00:00
|
|
|
this._focusedPair--;
|
|
|
|
this._updateFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_keyDown (e) {
|
2016-08-15 17:04:36 +00:00
|
|
|
if (e.metaKey || e.ctrlKey) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-09 18:47:51 +00:00
|
|
|
if (e.keyCode === ENTER) {
|
|
|
|
e.preventDefault();
|
|
|
|
this._focusNext(true);
|
|
|
|
} else if (e.keyCode === BACKSPACE) {
|
|
|
|
if (!e.target.value) {
|
|
|
|
e.preventDefault();
|
|
|
|
this._focusPrevious(true);
|
|
|
|
}
|
|
|
|
} else if (e.keyCode === DOWN) {
|
|
|
|
e.preventDefault();
|
|
|
|
this._focusNextPair();
|
|
|
|
} else if (e.keyCode === UP) {
|
|
|
|
e.preventDefault();
|
|
|
|
this._focusPreviousPair();
|
2016-07-20 23:16:28 +00:00
|
|
|
} else if (e.keyCode === LEFT) {
|
|
|
|
// TODO: Implement this
|
|
|
|
} else if (e.keyCode === RIGHT) {
|
|
|
|
// TODO: Implement this
|
2016-04-09 18:47:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateFocus () {
|
2016-11-22 19:42:10 +00:00
|
|
|
let ref;
|
|
|
|
if (this._focusedField === NAME) {
|
|
|
|
ref = this._nameInputs[this._focusedPair];
|
|
|
|
} else {
|
|
|
|
ref = this._valueInputs[this._focusedPair];
|
|
|
|
}
|
2016-04-09 18:47:51 +00:00
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
// If you focus an already focused input
|
|
|
|
if (!ref || this._focusedInput === ref) {
|
|
|
|
return;
|
2016-04-09 18:47:51 +00:00
|
|
|
}
|
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
// Focus at the end of the text
|
|
|
|
ref.focus();
|
|
|
|
ref.selectionStart = ref.selectionEnd = ref.value.length;
|
2016-04-09 18:47:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate () {
|
|
|
|
this._updateFocus();
|
2016-04-09 01:14:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
render () {
|
2016-04-09 18:47:51 +00:00
|
|
|
const {pairs} = this.state;
|
2016-11-23 19:33:24 +00:00
|
|
|
const {maxPairs, className, valueInputType, multipart} = this.props;
|
2016-04-09 18:47:51 +00:00
|
|
|
|
2016-04-09 01:14:25 +00:00
|
|
|
return (
|
2016-12-05 22:42:40 +00:00
|
|
|
<ul key={pairs.length} className={classnames('key-value-editor', 'wide', className)}>
|
2016-11-22 19:42:10 +00:00
|
|
|
{pairs.map((pair, i) => (
|
|
|
|
<li key={`${i}.pair`}
|
|
|
|
className={classnames(
|
|
|
|
'key-value-editor__row',
|
|
|
|
{'key-value-editor__row--disabled': pair.disabled}
|
|
|
|
)}>
|
2016-11-29 20:55:31 +00:00
|
|
|
<div className="form-control form-control--underlined form-control--wide">
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
key="name"
|
|
|
|
ref={n => this._nameInputs[i] = n}
|
|
|
|
placeholder={this.props.namePlaceholder || 'Name'}
|
|
|
|
defaultValue={pair.name}
|
|
|
|
onChange={e => this._updatePair(i, {name: e.target.value})}
|
|
|
|
onFocus={e => {
|
|
|
|
this._focusedPair = i;
|
|
|
|
this._focusedField = NAME;
|
|
|
|
this._focusedInput = e.target;
|
|
|
|
}}
|
|
|
|
onBlur={() => {
|
|
|
|
this._focusedPair = -1
|
|
|
|
}}
|
|
|
|
onKeyDown={this._keyDown.bind(this)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="form-control form-control--wide wide form-control--underlined">
|
|
|
|
{pair.type === 'file' ? (
|
|
|
|
<FileInputButton
|
|
|
|
showFileName={true}
|
|
|
|
className="btn btn--clicky wide ellipsis txt-sm"
|
|
|
|
path={pair.fileName || ''}
|
|
|
|
onChange={fileName => {
|
|
|
|
this._updatePair(i, {fileName});
|
|
|
|
this.props.onChooseFile && this.props.onChooseFile();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
) : (
|
2016-11-22 19:42:10 +00:00
|
|
|
<input
|
2016-11-29 20:55:31 +00:00
|
|
|
type={valueInputType || 'text'}
|
|
|
|
placeholder={this.props.valuePlaceholder || 'Value'}
|
|
|
|
ref={n => this._valueInputs[i] = n}
|
|
|
|
defaultValue={pair.value}
|
|
|
|
onChange={e => this._updatePair(i, {value: e.target.value})}
|
|
|
|
onBlur={() => this._focusedPair = -1}
|
|
|
|
onKeyDown={this._keyDown.bind(this)}
|
2016-11-22 19:42:10 +00:00
|
|
|
onFocus={e => {
|
|
|
|
this._focusedPair = i;
|
2016-11-29 20:55:31 +00:00
|
|
|
this._focusedField = VALUE;
|
2016-11-22 19:42:10 +00:00
|
|
|
this._focusedInput = e.target;
|
|
|
|
}}
|
|
|
|
/>
|
2016-11-29 20:55:31 +00:00
|
|
|
)}
|
2016-04-09 21:08:55 +00:00
|
|
|
</div>
|
2016-11-22 19:42:10 +00:00
|
|
|
|
2016-11-23 19:33:24 +00:00
|
|
|
{multipart ? (
|
|
|
|
<Dropdown right={true}>
|
|
|
|
<DropdownButton className="tall">
|
|
|
|
<i className="fa fa-caret-down"></i>
|
|
|
|
</DropdownButton>
|
|
|
|
<DropdownItem onClick={e => {
|
|
|
|
this._updatePair(i, {type: 'text', value: '', fileName: ''});
|
|
|
|
this.props.onChangeType && this.props.onChangeType('text');
|
|
|
|
}}>
|
|
|
|
Text
|
|
|
|
</DropdownItem>
|
|
|
|
<DropdownItem onClick={e => {
|
|
|
|
this._updatePair(i, {type: 'file', value: '', fileName: ''});
|
|
|
|
this.props.onChangeType && this.props.onChangeType('file');
|
|
|
|
}}>
|
|
|
|
File
|
|
|
|
</DropdownItem>
|
|
|
|
</Dropdown>
|
|
|
|
) : null}
|
|
|
|
|
2016-11-29 20:55:31 +00:00
|
|
|
<button onClick={e => this._togglePair(i)}
|
|
|
|
title={pair.disabled ? 'Enable item' : 'Disable item'}>
|
2016-11-22 19:42:10 +00:00
|
|
|
{pair.disabled ?
|
2016-11-29 20:55:31 +00:00
|
|
|
<i className="fa fa-square-o"/> :
|
|
|
|
<i className="fa fa-check-square-o"/>
|
2016-11-22 19:42:10 +00:00
|
|
|
}
|
|
|
|
</button>
|
|
|
|
|
2016-12-05 22:42:40 +00:00
|
|
|
<PromptButton key={Math.random()}
|
|
|
|
tabIndex="-1"
|
|
|
|
confirmMessage=" "
|
|
|
|
addIcon={true}
|
|
|
|
onClick={e => this._deletePair(i)}
|
|
|
|
title="Delete item">
|
2016-11-22 19:42:10 +00:00
|
|
|
<i className="fa fa-trash-o"></i>
|
2016-12-05 22:42:40 +00:00
|
|
|
</PromptButton>
|
2016-11-22 19:42:10 +00:00
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
{!maxPairs || pairs.length < maxPairs ? (
|
2016-11-29 20:55:31 +00:00
|
|
|
<li className="key-value-editor__row">
|
|
|
|
<div className="form-control form-control--underlined form-control--wide faded">
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
placeholder={this.props.namePlaceholder || 'Name'}
|
2016-11-29 22:28:55 +00:00
|
|
|
onFocus={this._handleAddFromName}
|
2016-11-29 20:55:31 +00:00
|
|
|
/>
|
2016-11-22 19:42:10 +00:00
|
|
|
</div>
|
2016-11-29 20:55:31 +00:00
|
|
|
<div className="form-control form-control--underlined form-control--wide faded">
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
placeholder={this.props.valuePlaceholder || 'Value'}
|
2016-11-29 22:28:55 +00:00
|
|
|
onFocus={this._handleAddFromValue}
|
2016-11-29 20:55:31 +00:00
|
|
|
/>
|
2016-11-22 19:42:10 +00:00
|
|
|
</div>
|
2016-11-23 19:33:24 +00:00
|
|
|
|
|
|
|
{multipart ? (
|
2016-11-29 22:28:55 +00:00
|
|
|
<button disabled={true} tabIndex="-1">
|
|
|
|
<i className="fa fa-blank"></i>
|
|
|
|
</button>
|
2016-11-23 19:33:24 +00:00
|
|
|
) : null}
|
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
<button disabled={true} tabIndex="-1">
|
|
|
|
<i className="fa fa-blank"></i>
|
|
|
|
</button>
|
2016-11-23 19:33:24 +00:00
|
|
|
|
2016-11-22 19:42:10 +00:00
|
|
|
<button disabled={true} tabIndex="-1">
|
|
|
|
<i className="fa fa-blank"></i>
|
|
|
|
</button>
|
2016-05-01 19:56:30 +00:00
|
|
|
</li>
|
2016-04-09 21:08:55 +00:00
|
|
|
) : null}
|
2016-05-01 19:56:30 +00:00
|
|
|
</ul>
|
2016-04-09 01:14:25 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
KeyValueEditor.propTypes = {
|
|
|
|
onChange: PropTypes.func.isRequired,
|
2016-11-22 19:42:10 +00:00
|
|
|
pairs: PropTypes.arrayOf(PropTypes.object).isRequired,
|
2016-10-05 04:43:48 +00:00
|
|
|
|
|
|
|
// Optional
|
2016-11-23 19:33:24 +00:00
|
|
|
multipart: PropTypes.bool,
|
2016-04-10 18:23:41 +00:00
|
|
|
maxPairs: PropTypes.number,
|
|
|
|
namePlaceholder: PropTypes.string,
|
2016-07-19 19:13:51 +00:00
|
|
|
valuePlaceholder: PropTypes.string,
|
2016-11-23 19:33:24 +00:00
|
|
|
valueInputType: PropTypes.string,
|
|
|
|
onToggleDisable: PropTypes.func,
|
|
|
|
onChangeType: PropTypes.func,
|
|
|
|
onChooseFile: PropTypes.func,
|
|
|
|
onDelete: PropTypes.func,
|
|
|
|
onCreate: PropTypes.func,
|
2016-04-09 01:14:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export default KeyValueEditor;
|