insomnia/app/ui/components/key-value-editor/editor.js
Gregory Schier 3056921fda Support multiline text in form data editors (#299)
* Support multiline text in form data editors

* Some tweaks around rendering and syntax

* Embed markdown editor
2017-06-09 14:42:19 -07:00

420 lines
10 KiB
JavaScript

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 (
<Lazy delay={pairs.length > 20 ? 50 : -1}>
<ul className={classes}>
{pairs.map((pair, i) => (
<KeyValueEditorRow
noDelete={disableDelete}
key={pair.id || 'no-id'}
index={i} // For dragging
ref={this._setRowRef}
sortable={sortable}
namePlaceholder={namePlaceholder}
valuePlaceholder={valuePlaceholder}
valueInputType={valueInputType}
onChange={this._handlePairChange}
onDelete={this._handlePairDelete}
onFocusName={this._handleFocusName}
onFocusValue={this._handleFocusValue}
onKeyDown={this._handleKeyDown}
onBlurName={this._handleBlurName}
onBlurValue={this._handleBlurValue}
onMove={this._handleMove}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
handleGetAutocompleteNameConstants={handleGetAutocompleteNameConstants}
handleGetAutocompleteValueConstants={handleGetAutocompleteValueConstants}
allowMultiline={allowMultiline}
allowFile={allowFile}
pair={pair}
/>
))}
{!maxPairs || pairs.length < maxPairs
? <KeyValueEditorRow
key="empty-row"
hideButtons
sortable
noDropZone
readOnly
forceInput
index={-1}
onChange={nullFn}
onDelete={nullFn}
className="key-value-editor__row-wrapper--clicker"
namePlaceholder={`New ${namePlaceholder}`}
valuePlaceholder={`New ${valuePlaceholder}`}
onFocusName={this._handleAddFromName}
onFocusValue={this._handleAddFromValue}
allowMultiline={allowMultiline}
allowFile={allowFile}
pair={{name: '', value: ''}}
/> : null
}
</ul>
</Lazy>
);
}
}
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;