PromucFlow_constructor/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx

282 lines
7.7 KiB
TypeScript
Raw Normal View History

2019-12-30 07:35:16 +00:00
import React, { Component } from "react";
2019-12-06 13:16:08 +00:00
import { connect } from "react-redux";
import { AppState } from "reducers";
2020-01-02 13:36:35 +00:00
import styled, { createGlobalStyle } from "styled-components";
import CodeMirror, { EditorConfiguration, LineHandle } from "codemirror";
2019-12-30 07:35:16 +00:00
import "codemirror/lib/codemirror.css";
import "codemirror/theme/monokai.css";
import "codemirror/addon/hint/show-hint";
import "codemirror/addon/hint/javascript-hint";
2020-01-02 13:36:35 +00:00
import "codemirror/addon/display/placeholder";
2019-12-06 13:16:08 +00:00
import {
getNameBindingsForAutocomplete,
2019-12-06 13:16:08 +00:00
NameBindingsWithData,
} from "selectors/nameBindingsWithDataSelector";
2020-01-02 13:36:35 +00:00
import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants";
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import _ from "lodash";
import { parseDynamicString } from "utils/DynamicBindingUtils";
2019-12-30 07:35:16 +00:00
require("codemirror/mode/javascript/javascript");
2019-12-06 13:16:08 +00:00
2020-01-02 13:36:35 +00:00
const HintStyles = createGlobalStyle`
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 5px;
font-size: 90%;
font-family: monospace;
max-height: 20em;
width: 200px;
overflow-y: auto;
background: #FFFFFF;
border: 1px solid #EBEFF2;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
border-radius: 4px;
}
.CodeMirror-hint {
height: 32px;
padding: 3px;
margin: 0;
white-space: pre;
color: #2E3D49;
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
}
li.CodeMirror-hint-active {
background: #E9FAF3;
border-radius: 4px;
}
`;
const Wrapper = styled.div<{
borderStyle?: THEME;
hasError: boolean;
}>`
border: 1px solid;
border-color: ${props =>
props.hasError
? props.theme.colors.error
: props.borderStyle !== THEMES.DARK
? "#d0d7dd"
: "transparent"};
2019-12-30 07:35:16 +00:00
border-radius: 4px;
2019-12-06 13:16:08 +00:00
display: flex;
flex: 1;
2020-01-02 13:36:35 +00:00
flex-direction: row;
2019-12-06 13:16:08 +00:00
position: relative;
text-transform: none;
2019-12-30 07:35:16 +00:00
min-height: 32px;
2020-01-02 13:36:35 +00:00
overflow: hidden;
height: auto;
&& {
.binding-highlight {
color: ${props =>
props.borderStyle === THEMES.DARK ? "#f7c75b" : "#ffb100"};
font-weight: 700;
}
.CodeMirror {
flex: 1;
line-height: 21px;
z-index: 0;
border-radius: 4px;
height: auto;
}
.CodeMirror pre.CodeMirror-placeholder {
color: #a3b3bf;
}
}
`;
const IconContainer = styled.div`
.bp3-icon {
border-radius: 4px 0 0 4px;
margin: 0;
2020-01-08 09:19:00 +00:00
height: 30px;
2020-01-02 13:36:35 +00:00
width: 30px;
display: flex;
align-items: center;
justify-content: center;
background-color: #eef2f5;
svg {
height: 20px;
width: 20px;
path {
fill: #979797;
}
}
}
2019-12-06 13:16:08 +00:00
`;
2020-01-02 13:36:35 +00:00
const THEMES = {
LIGHT: "LIGHT",
DARK: "DARK",
};
type THEME = "LIGHT" | "DARK";
2019-12-06 13:16:08 +00:00
interface ReduxStateProps {
dynamicData: NameBindingsWithData;
}
2020-01-02 13:36:35 +00:00
export type DynamicAutocompleteInputProps = {
placeholder?: string;
leftIcon?: Function;
height?: number;
2020-01-02 13:36:35 +00:00
theme?: THEME;
meta?: Partial<WrappedFieldMetaProps>;
showLineNumbers?: boolean;
allowTabIndent?: boolean;
2019-12-06 13:16:08 +00:00
};
2020-01-02 13:36:35 +00:00
type Props = ReduxStateProps &
DynamicAutocompleteInputProps & {
input: Partial<WrappedFieldInputProps>;
};
2019-12-30 07:35:16 +00:00
class DynamicAutocompleteInput extends Component<Props> {
textArea = React.createRef<HTMLTextAreaElement>();
editor: any;
2019-12-06 13:16:08 +00:00
componentDidMount(): void {
2019-12-30 07:35:16 +00:00
if (this.textArea.current) {
const options: EditorConfiguration = {};
if (this.props.theme === "DARK") options.theme = "monokai";
if (!this.props.input.onChange) options.readOnly = true;
2020-01-02 13:36:35 +00:00
if (this.props.showLineNumbers) options.lineNumbers = true;
const extraKeys: Record<string, any> = {
"Ctrl-Space": "autocomplete",
};
if (!this.props.allowTabIndent) extraKeys["Tab"] = false;
2019-12-30 07:35:16 +00:00
this.editor = CodeMirror.fromTextArea(this.textArea.current, {
mode: { name: "javascript", globalVars: true },
2020-01-02 13:36:35 +00:00
viewportMargin: 10,
2019-12-30 07:35:16 +00:00
value: this.props.input.value,
tabSize: 2,
indentWithTabs: true,
lineWrapping: true,
showHint: true,
2020-01-02 13:36:35 +00:00
extraKeys,
2019-12-30 07:35:16 +00:00
...options,
});
2020-01-08 09:19:00 +00:00
this.editor.on("change", _.debounce(this.handleChange, 100));
2020-01-02 13:36:35 +00:00
this.editor.on("cursorActivity", this.handleAutocompleteVisibility);
2019-12-30 07:35:16 +00:00
this.editor.setOption("hintOptions", {
completeSingle: false,
globalScope: this.props.dynamicData,
2019-12-06 13:16:08 +00:00
});
if (this.props.height) {
this.editor.setSize(0, this.props.height);
}
2020-01-02 13:36:35 +00:00
this.editor.eachLine(this.highlightBindings);
2019-12-06 13:16:08 +00:00
}
2019-12-30 07:35:16 +00:00
}
2019-12-06 13:16:08 +00:00
2019-12-30 07:35:16 +00:00
componentDidUpdate(): void {
if (this.editor) {
const editorValue = this.editor.getValue();
2020-01-02 13:36:35 +00:00
let inputValue = this.props.input.value;
if (typeof inputValue === "object") {
inputValue = JSON.stringify(inputValue, null, 2);
}
if ((!!inputValue || inputValue === "") && inputValue !== editorValue) {
2020-01-08 09:19:00 +00:00
const cursor = this.editor.getCursor();
2019-12-30 07:35:16 +00:00
this.editor.setValue(inputValue);
2020-01-08 09:19:00 +00:00
this.editor.setCursor(cursor);
2019-12-06 13:16:08 +00:00
}
}
2019-12-30 07:35:16 +00:00
}
handleChange = () => {
const value = this.editor.getValue();
2020-01-08 09:19:00 +00:00
const inputValue = this.props.input.value;
if (this.props.input.onChange && value !== inputValue) {
2019-12-30 07:35:16 +00:00
this.props.input.onChange(value);
2019-12-06 13:16:08 +00:00
}
2020-01-02 13:36:35 +00:00
this.editor.eachLine(this.highlightBindings);
2019-12-06 13:16:08 +00:00
};
2020-01-02 13:36:35 +00:00
handleAutocompleteVisibility = (cm: any) => {
let cursorBetweenBinding = false;
const cursor = this.editor.getCursor();
const value = this.editor.getValue();
let cumulativeCharCount = 0;
parseDynamicString(value).forEach(segment => {
const start = cumulativeCharCount;
const dynamicStart = segment.indexOf("{{");
const dynamicDoesStart = dynamicStart > -1;
const dynamicEnd = segment.indexOf("}}");
const dynamicDoesEnd = dynamicEnd > -1;
const dynamicStartIndex = dynamicStart + start + 1;
const dynamicEndIndex = dynamicEnd + start + 1;
if (
dynamicDoesStart &&
cursor.ch > dynamicStartIndex &&
((dynamicDoesEnd && cursor.ch < dynamicEndIndex) ||
(!dynamicDoesEnd && cursor.ch > dynamicStartIndex))
) {
cursorBetweenBinding = true;
}
cumulativeCharCount = start + segment.length;
});
const shouldShow = cursorBetweenBinding && !cm.state.completionActive;
if (shouldShow) {
2019-12-30 07:35:16 +00:00
cm.showHint(cm);
2019-12-06 13:16:08 +00:00
}
};
2020-01-02 13:36:35 +00:00
highlightBindings = (line: LineHandle) => {
const lineNo = this.editor.getLineNumber(line);
let match;
while ((match = AUTOCOMPLETE_MATCH_REGEX.exec(line.text)) != null) {
const start = match.index;
const end = AUTOCOMPLETE_MATCH_REGEX.lastIndex;
this.editor.markText(
{ ch: start, line: lineNo },
{ ch: end, line: lineNo },
{
className: "binding-highlight",
},
);
}
};
2019-12-06 13:16:08 +00:00
render() {
2020-01-02 13:36:35 +00:00
const { input, meta, theme } = this.props;
const hasError = !!(meta && meta.error);
let showError = false;
if (this.editor) {
showError = hasError && this.editor.hasFocus();
}
2019-12-06 13:16:08 +00:00
return (
<ErrorTooltip message={meta ? meta.error : ""} isOpen={showError}>
2020-01-02 13:36:35 +00:00
<Wrapper borderStyle={theme} hasError={hasError}>
<HintStyles />
<IconContainer>
{this.props.leftIcon && <this.props.leftIcon />}
</IconContainer>
<textarea
ref={this.textArea}
{..._.omit(this.props.input, ["onChange", "value"])}
defaultValue={input.value}
placeholder={this.props.placeholder}
/>
</Wrapper>
</ErrorTooltip>
2019-12-06 13:16:08 +00:00
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
dynamicData: getNameBindingsForAutocomplete(state),
2019-12-06 13:16:08 +00:00
});
export default connect(mapStateToProps)(DynamicAutocompleteInput);