import React, {PureComponent, PropTypes} from 'react'; import autobind from 'autobind-decorator'; import classnames from 'classnames'; import {DEBOUNCE_MILLIS} from '../../../common/constants'; import Lazy from '../base/lazy'; import KeyValueEditorRow from './row'; import {generateId, nullFn} from '../../../common/misc'; const NAME = 'name'; const VALUE = 'value'; const ENTER = 13; const BACKSPACE = 8; const UP = 38; const DOWN = 40; const LEFT = 37; const RIGHT = 39; @autobind class Editor extends PureComponent { constructor (props) { super(props); this._focusedPairId = null; this._focusedField = NAME; this._rows = []; // Migrate and add IDs to all pairs (pairs didn't used to have IDs) const pairs = [...props.pairs]; for (const pair of pairs) { if (props.maxPairs !== 1 && !pair.id) { pair.id = generateId('pair'); } } this.state = { pairs: pairs }; } _setRowRef (n) { // NOTE: We're not handling unmounting (may lead to a bug) if (n) { this._rows[n.props.pair.id] = n; } } _handlePairChange (pair) { const i = this._getPairIndex(pair); const pairs = [ ...this.state.pairs.slice(0, i), Object.assign({}, pair), ...this.state.pairs.slice(i + 1) ]; this._onChange(pairs); } _handleMove (pairToMove, pairToTarget, targetOffset) { if (pairToMove.id === pairToTarget.id) { // Nothing to do return; } const withoutPair = this.state.pairs.filter(p => p.id !== pairToMove.id); let toIndex = withoutPair.findIndex(p => p.id === pairToTarget.id); // If we're moving below, add 1 to the index if (targetOffset < 0) { toIndex += 1; } const pairs = [ ...withoutPair.slice(0, toIndex), Object.assign({}, pairToMove), ...withoutPair.slice(toIndex) ]; this._onChange(pairs); } _handlePairDelete (pair) { const i = this.state.pairs.findIndex(p => p.id === pair.id); this._deletePair(i, true); } _handleBlurName () { this._focusedField = null; } _handleBlurValue () { this._focusedField = null; } _handleFocusName (pair) { this._setFocusedPair(pair); this._focusedField = NAME; this._rows[pair.id].focusNameEnd(); } _handleFocusValue (pair) { this._setFocusedPair(pair); this._focusedField = VALUE; this._rows[pair.id].focusValueEnd(); } _handleAddFromName () { this._focusedField = NAME; this._addPair(); } // Sometimes multiple focus events come in, so lets debounce it _handleAddFromValue () { this._focusedField = VALUE; this._addPair(); } _handleKeyDown (pair, e, value) { if (e.metaKey || e.ctrlKey) { return; } if (e.keyCode === ENTER) { this._focusNext(true); } else if (e.keyCode === BACKSPACE) { if (!value) { this._focusPrevious(true); } } else if (e.keyCode === DOWN) { e.preventDefault(); this._focusNextPair(); } else if (e.keyCode === UP) { e.preventDefault(); this._focusPreviousPair(); } else if (e.keyCode === LEFT) { // TODO: Implement this } else if (e.keyCode === RIGHT) { // TODO: Implement this } } _onChange (pairs) { clearTimeout(this._triggerTimeout); this._triggerTimeout = setTimeout(() => this.props.onChange(pairs), DEBOUNCE_MILLIS); this.setState({pairs}); } _addPair (position) { 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; const pair = { name: '', value: '' }; // Only add ids if we need 'em if (this.props.maxPairs !== 1) { pair.id = generateId('pair'); } const pairs = [ ...this.state.pairs.slice(0, position), pair, ...this.state.pairs.slice(position) ]; this._setFocusedPair(pair); this._onChange(pairs); this.props.onCreate && this.props.onCreate(); } _deletePair (position, breakFocus = false) { if (this.props.disableDelete) { return; } const focusedPosition = this._getFocusedPairIndex(); const pair = this.state.pairs[position]; this.props.onDelete && this.props.onDelete(pair); const pairs = [ ...this.state.pairs.slice(0, position), ...this.state.pairs.slice(position + 1) ]; if (focusedPosition >= position) { const newPosition = breakFocus ? -1 : focusedPosition - 1; this._setFocusedPair(pairs[newPosition]); } this._onChange(pairs); } _focusNext (addIfValue = false) { if (this.props.maxPairs === 1) { return; } if (this._focusedField === NAME) { this._focusedField = VALUE; this._updateFocus(); } else if (this._focusedField === VALUE) { this._focusedField = NAME; if (addIfValue) { this._addPair(this._getFocusedPairIndex() + 1); } else { this._focusNextPair(); } } } _focusPrevious (deleteIfEmpty = false) { if (this._focusedField === VALUE) { this._focusedField = NAME; this._updateFocus(); } else if (this._focusedField === NAME) { const p = this._getFocusedPair(); const notEmpty = !p.name && !p.value && !p.fileName; if (!this.props.disableDelete && notEmpty && deleteIfEmpty) { this._focusedField = VALUE; this._deletePair(this._getFocusedPairIndex()); } else if (!p.name) { this._focusedField = VALUE; this._focusPreviousPair(); } } } _focusNextPair () { if (this.props.maxPairs === 1) { return; } const i = this._getFocusedPairIndex(); if (i === -1) { // No focused pair currently return; } if (i >= this.state.pairs.length - 1) { // Focused on last one, so add another this._addPair(); } else { this._setFocusedPair(this.state.pairs[i + 1]); this._updateFocus(); } } _focusPreviousPair () { if (this.props.maxPairs === 1) { return; } const i = this._getFocusedPairIndex(); if (i > 0) { this._setFocusedPair(this.state.pairs[i - 1]); this._updateFocus(); } } _updateFocus () { const pair = this._getFocusedPair(); const id = pair ? pair.id : 'n/a'; const row = this._rows[id]; if (!row) { return; } if (this._focusedField === NAME) { row.focusNameEnd(); } else if (this._focusedField === VALUE) { row.focusValueEnd(); } } _getPairIndex (pair) { if (pair) { return this.state.pairs.findIndex(p => p.id === pair.id); } else { return -1; } } _getFocusedPairIndex () { return this._getPairIndex(this._getFocusedPair()); } _getFocusedPair () { return this.state.pairs.find(p => p.id === this._focusedPairId) || null; } _setFocusedPair (pair) { if (pair) { this._focusedPairId = pair.id; } else { this._focusedPairId = null; } } componentDidUpdate () { this._updateFocus(); } render () { const { maxPairs, className, valueInputType, valuePlaceholder, namePlaceholder, handleRender, handleGetRenderContext, handleGetAutocompleteNameConstants, handleGetAutocompleteValueConstants, allowFile, allowMultiline, sortable, disableDelete } = this.props; const {pairs} = this.state; const classes = classnames('key-value-editor', 'wide', className); return ( 20 ? 50 : -1}> ); } } Editor.propTypes = { onChange: PropTypes.func.isRequired, pairs: PropTypes.arrayOf(PropTypes.object).isRequired, // Optional handleRender: PropTypes.func, handleGetRenderContext: PropTypes.func, handleGetAutocompleteNameConstants: PropTypes.func, handleGetAutocompleteValueConstants: PropTypes.func, allowFile: PropTypes.bool, allowMultiline: PropTypes.bool, sortable: PropTypes.bool, maxPairs: PropTypes.number, namePlaceholder: PropTypes.string, valuePlaceholder: PropTypes.string, valueInputType: PropTypes.string, disableDelete: PropTypes.bool, onToggleDisable: PropTypes.func, onChangeType: PropTypes.func, onChooseFile: PropTypes.func, onDelete: PropTypes.func, onCreate: PropTypes.func, className: PropTypes.string }; export default Editor;