mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 14:49:53 +00:00
386 lines
9.5 KiB
JavaScript
386 lines
9.5 KiB
JavaScript
import React, {PureComponent} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import ReactDOM from 'react-dom';
|
|
import classnames from 'classnames';
|
|
import autobind from 'autobind-decorator';
|
|
import CodeEditor from './code-editor';
|
|
import Input from '../base/debounced-input';
|
|
|
|
const MODE_INPUT = 'input';
|
|
const MODE_EDITOR = 'editor';
|
|
const TYPE_TEXT = 'text';
|
|
const NUNJUCKS_REGEX = /({%|%}|{{|}})/;
|
|
|
|
@autobind
|
|
class OneLineEditor extends PureComponent {
|
|
constructor (props) {
|
|
super(props);
|
|
|
|
let mode;
|
|
if (props.forceInput) {
|
|
mode = MODE_INPUT;
|
|
} else if (props.forceEditor) {
|
|
mode = MODE_EDITOR;
|
|
} else if (this._mayContainNunjucks(props.defaultValue)) {
|
|
mode = MODE_EDITOR;
|
|
} else {
|
|
mode = MODE_INPUT;
|
|
}
|
|
|
|
this.state = {
|
|
mode
|
|
};
|
|
}
|
|
|
|
focus (setToEnd = false) {
|
|
if (this.state.mode === MODE_EDITOR) {
|
|
if (this._editor && !this._editor.hasFocus()) {
|
|
setToEnd ? this._editor.focusEnd() : this._editor.focus();
|
|
}
|
|
} else {
|
|
if (this._input && !this._input.hasFocus()) {
|
|
setToEnd ? this._input.focusEnd() : this._input.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
focusEnd () {
|
|
this.focus(true);
|
|
}
|
|
|
|
selectAll () {
|
|
if (this.state.mode === MODE_EDITOR) {
|
|
this._editor.selectAll();
|
|
} else {
|
|
this._input.select();
|
|
}
|
|
}
|
|
|
|
getValue () {
|
|
if (this.state.mode === MODE_EDITOR) {
|
|
return this._editor.getValue();
|
|
} else {
|
|
return this._input.getValue();
|
|
}
|
|
}
|
|
|
|
getSelectionStart () {
|
|
if (this._editor) {
|
|
return this._editor.getSelectionStart();
|
|
} else {
|
|
console.warn('Tried to get selection start of one-line-editor when <input>');
|
|
return this._input.value.length;
|
|
}
|
|
}
|
|
|
|
getSelectionEnd () {
|
|
if (this._editor) {
|
|
return this._editor.getSelectionEnd();
|
|
} else {
|
|
console.warn('Tried to get selection end of one-line-editor when <input>');
|
|
return this._input.value.length;
|
|
}
|
|
}
|
|
|
|
componentDidMount () {
|
|
document.body.addEventListener('mousedown', this._handleDocumentMousedown);
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
document.body.removeEventListener('mousedown', this._handleDocumentMousedown);
|
|
}
|
|
|
|
_handleDocumentMousedown (e) {
|
|
if (!this._editor) {
|
|
return;
|
|
}
|
|
|
|
// Clear the selection if mousedown happens outside the input so we act like
|
|
// a regular <input>
|
|
// NOTE: Must be "mousedown", not "click" because "click" triggers on selection drags
|
|
const node = ReactDOM.findDOMNode(this._editor);
|
|
const clickWasOutsideOfComponent = !node.contains(e.target);
|
|
if (clickWasOutsideOfComponent) {
|
|
this._editor.clearSelection();
|
|
}
|
|
}
|
|
|
|
_handleInputDragEnter () {
|
|
this._convertToEditorPreserveFocus();
|
|
}
|
|
|
|
_handleInputMouseEnter () {
|
|
// Convert to editor when user hovers mouse over input
|
|
/*
|
|
* NOTE: we're doing it in a timeout because we don't want to convert if the
|
|
* mouse goes in an out right away.
|
|
*/
|
|
this._mouseEnterTimeout = setTimeout(this._convertToEditorPreserveFocus, 100);
|
|
}
|
|
|
|
_handleInputMouseLeave () {
|
|
clearTimeout(this._mouseEnterTimeout);
|
|
}
|
|
|
|
_handleEditorMouseLeave () {
|
|
this._convertToInputIfNotFocused();
|
|
}
|
|
|
|
_handleEditorFocus (e) {
|
|
const focusedFromTabEvent = !!e.sourceCapabilities;
|
|
|
|
if (focusedFromTabEvent) {
|
|
this._editor.focusEnd();
|
|
}
|
|
|
|
if (!this._editor) {
|
|
console.warn('Tried to focus editor when it was not mounted', this);
|
|
return;
|
|
}
|
|
|
|
// Set focused state
|
|
this._editor.setAttribute('data-focused', 'on');
|
|
|
|
this.props.onFocus && this.props.onFocus(e);
|
|
}
|
|
|
|
_handleInputFocus (e) {
|
|
// If we're focusing the whole thing, blur the input. This happens when
|
|
// the user tabs to the field.
|
|
const start = this._input.getSelectionStart();
|
|
const end = this._input.getSelectionEnd();
|
|
const focusedFromTabEvent = start === 0 && end === e.target.value.length;
|
|
|
|
if (focusedFromTabEvent) {
|
|
this._input.focusEnd();
|
|
|
|
// Also convert to editor if we tabbed to it. Just in case the user
|
|
// needs an editor
|
|
this._convertToEditorPreserveFocus();
|
|
}
|
|
|
|
// Set focused state
|
|
this._input.setAttribute('data-focused', 'on');
|
|
|
|
// Also call the regular callback
|
|
this.props.onFocus && this.props.onFocus(e);
|
|
}
|
|
|
|
_handleInputChange (value) {
|
|
this._convertToEditorPreserveFocus();
|
|
this.props.onChange && this.props.onChange(value);
|
|
}
|
|
|
|
_handleInputKeyDown (e) {
|
|
if (this.props.onKeyDown) {
|
|
this.props.onKeyDown(e, e.target.value);
|
|
}
|
|
}
|
|
|
|
_handleInputBlur () {
|
|
// Set focused state
|
|
this._input.removeAttribute('data-focused');
|
|
|
|
this.props.onBlur && this.props.onBlur();
|
|
}
|
|
|
|
_handleEditorBlur () {
|
|
// Editor was already removed from the DOM, so do nothing
|
|
if (!this._editor) {
|
|
return;
|
|
}
|
|
|
|
// Set focused state
|
|
this._editor.removeAttribute('data-focused');
|
|
|
|
if (!this.props.forceEditor) {
|
|
// Convert back to input sometime in the future.
|
|
// NOTE: this was originally added because the input would disappear if
|
|
// the user tabbed away very shortly after typing, but it's actually a pretty
|
|
// good feature.
|
|
setTimeout(() => {
|
|
this._convertToInputIfNotFocused();
|
|
}, 2000);
|
|
}
|
|
|
|
this.props.onBlur && this.props.onBlur();
|
|
}
|
|
|
|
_handleKeyDown (e) {
|
|
// submit form if needed
|
|
if (e.keyCode === 13) {
|
|
let node = e.target;
|
|
for (let i = 0; i < 20 && node; i++) {
|
|
if (node.tagName === 'FORM') {
|
|
node.dispatchEvent(new window.Event('submit'));
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
break;
|
|
}
|
|
node = node.parentNode;
|
|
}
|
|
}
|
|
|
|
this.props.onKeyDown && this.props.onKeyDown(e, this.getValue());
|
|
}
|
|
|
|
_convertToEditorPreserveFocus () {
|
|
if (this.state.mode !== MODE_INPUT || this.props.forceInput) {
|
|
return;
|
|
}
|
|
|
|
if (!this._input) {
|
|
return;
|
|
}
|
|
|
|
if (this._input.hasFocus()) {
|
|
const start = this._input.getSelectionStart();
|
|
const end = this._input.getSelectionEnd();
|
|
|
|
// Wait for the editor to swap and restore cursor position
|
|
const check = () => {
|
|
if (this._editor) {
|
|
this._editor.focus();
|
|
this._editor.setSelection(start, end);
|
|
} else {
|
|
setTimeout(check, 40);
|
|
}
|
|
};
|
|
|
|
// Tell the component to show the editor
|
|
setTimeout(check);
|
|
}
|
|
|
|
this.setState({mode: MODE_EDITOR});
|
|
}
|
|
|
|
_convertToInputIfNotFocused () {
|
|
if (this.state.mode === MODE_INPUT || this.props.forceEditor) {
|
|
return;
|
|
}
|
|
|
|
if (!this._editor || this._editor.hasFocus()) {
|
|
return;
|
|
}
|
|
|
|
if (this._mayContainNunjucks(this.getValue())) {
|
|
return;
|
|
}
|
|
|
|
this.setState({mode: MODE_INPUT});
|
|
}
|
|
|
|
_setEditorRef (n) {
|
|
this._editor = n;
|
|
}
|
|
|
|
_setInputRef (n) {
|
|
this._input = n;
|
|
}
|
|
|
|
_mayContainNunjucks (text) {
|
|
return !!(text && text.match(NUNJUCKS_REGEX));
|
|
}
|
|
|
|
render () {
|
|
const {
|
|
id,
|
|
defaultValue,
|
|
className,
|
|
onChange,
|
|
placeholder,
|
|
render,
|
|
onPaste,
|
|
getRenderContext,
|
|
nunjucksPowerUserMode,
|
|
getAutocompleteConstants,
|
|
mode: syntaxMode,
|
|
type: originalType
|
|
} = this.props;
|
|
|
|
const {mode} = this.state;
|
|
|
|
const type = originalType || TYPE_TEXT;
|
|
const showEditor = mode === MODE_EDITOR;
|
|
|
|
if (showEditor) {
|
|
return (
|
|
<CodeEditor
|
|
ref={this._setEditorRef}
|
|
defaultTabBehavior
|
|
hideLineNumbers
|
|
hideScrollbars
|
|
noMatchBrackets
|
|
noStyleActiveLine
|
|
noLint
|
|
singleLine
|
|
tabIndex={0}
|
|
id={id}
|
|
type={type}
|
|
mode={syntaxMode}
|
|
placeholder={placeholder}
|
|
onPaste={onPaste}
|
|
onBlur={this._handleEditorBlur}
|
|
onKeyDown={this._handleKeyDown}
|
|
onFocus={this._handleEditorFocus}
|
|
onMouseLeave={this._handleEditorMouseLeave}
|
|
onChange={onChange}
|
|
render={render}
|
|
getRenderContext={getRenderContext}
|
|
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
|
getAutocompleteConstants={getAutocompleteConstants}
|
|
className={classnames('editor--single-line', className)}
|
|
defaultValue={defaultValue}
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<Input
|
|
ref={this._setInputRef}
|
|
id={id}
|
|
type={type}
|
|
className={className}
|
|
style={{
|
|
// background: 'rgba(255, 0, 0, 0.05)', // For debugging
|
|
width: '100%'
|
|
}}
|
|
placeholder={placeholder}
|
|
defaultValue={defaultValue}
|
|
onBlur={this._handleInputBlur}
|
|
onChange={this._handleInputChange}
|
|
onMouseEnter={this._handleInputMouseEnter}
|
|
onMouseLeave={this._handleInputMouseLeave}
|
|
onDragEnter={this._handleInputDragEnter}
|
|
onPaste={onPaste}
|
|
onFocus={this._handleInputFocus}
|
|
onKeyDown={this._handleInputKeyDown}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
OneLineEditor.propTypes = {
|
|
defaultValue: PropTypes.string.isRequired,
|
|
|
|
// Optional
|
|
id: PropTypes.string,
|
|
type: PropTypes.string,
|
|
mode: PropTypes.string,
|
|
onBlur: PropTypes.func,
|
|
onKeyDown: PropTypes.func,
|
|
onFocus: PropTypes.func,
|
|
onChange: PropTypes.func,
|
|
onPaste: PropTypes.func,
|
|
render: PropTypes.func,
|
|
getRenderContext: PropTypes.func,
|
|
nunjucksPowerUserMode: PropTypes.bool,
|
|
getAutocompleteConstants: PropTypes.func,
|
|
placeholder: PropTypes.string,
|
|
className: PropTypes.string,
|
|
forceEditor: PropTypes.bool,
|
|
forceInput: PropTypes.bool
|
|
};
|
|
|
|
export default OneLineEditor;
|