insomnia/app/ui/components/codemirror/one-line-editor.js
2017-10-13 10:22:13 +02:00

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;