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

225 lines
6.3 KiB
TypeScript
Raw Normal View History

2019-12-06 13:16:08 +00:00
import React, { ChangeEvent, Component, KeyboardEvent } from "react";
import { connect } from "react-redux";
import { AppState } from "reducers";
import styled from "styled-components";
import _ from "lodash";
import {
getDynamicAutocompleteSearchTerm,
getDynamicBindings,
} from "utils/DynamicBindingUtils";
import {
BaseTextInput,
TextInputProps,
} from "components/designSystems/appsmith/TextInputComponent";
import {
getNameBindingsWithData,
2019-12-06 13:16:08 +00:00
NameBindingsWithData,
} from "selectors/nameBindingsWithDataSelector";
import TreeMenu, {
MatchSearchFunction,
TreeMenuItem,
TreeNodeInArray,
} from "react-simple-tree-menu";
import { DATA_BIND_AUTOCOMPLETE } from "constants/BindingsConstants";
import DataTreeNode from "components/editorComponents/DataTreeNode";
import { transformToTreeStructure } from "utils/DynamicTreeAutoCompleteUtils";
const Wrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
position: relative;
`;
const DataTreeWrapper = styled.div`
position: absolute;
top: 33px;
2019-12-10 13:30:16 +00:00
z-index: 21;
2019-12-06 13:16:08 +00:00
padding: 10px;
max-height: 400px;
width: 450px;
overflow-y: auto;
background-color: white;
border: 1px solid #ebeff2;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
border-radius: 4px;
font-size: 14px;
text-transform: none;
`;
const NoResultsMessage = styled.p`
color: ${props => props.theme.colors.textDefault};
`;
interface ReduxStateProps {
dynamicData: NameBindingsWithData;
}
type Props = ReduxStateProps & TextInputProps;
type State = {
tree: TreeNodeInArray[];
showTree: boolean;
focusedNode: string;
};
class DynamicAutocompleteInput extends Component<Props, State> {
private input: HTMLInputElement | null = null;
private search: Function | undefined;
constructor(props: Props) {
super(props);
this.state = {
tree: [],
showTree: true,
focusedNode: "",
};
}
componentDidMount(): void {
this.updateTree();
}
componentDidUpdate(prevProps: Readonly<Props>): void {
if (prevProps.dynamicData !== this.props.dynamicData) {
this.updateTree();
}
this.updateTreeVisibility();
}
updateTreeVisibility = () => {
const { showTree } = this.state;
const { input } = this.props;
let value;
let hasIncomplete = 0;
if (input && input.value) {
value = input.value;
}
if (value && typeof value === "string") {
2019-12-06 13:16:08 +00:00
const { bindings, paths } = getDynamicBindings(value);
bindings.forEach((binding, i) => {
if (binding.indexOf("{{") > -1 && paths[i] === "") {
hasIncomplete++;
}
});
}
if (showTree) {
if (hasIncomplete === 0) {
this.setState({ showTree: false });
}
} else {
if (hasIncomplete > 0) {
this.setState({ showTree: true });
}
}
};
handleNodeSearch: MatchSearchFunction = ({ path, searchTerm }) => {
const lowerCasePath = path.toLowerCase();
const lowerCaseSearchTerm = searchTerm.toLowerCase();
const matchPath = lowerCasePath.substr(0, searchTerm.length);
return matchPath === lowerCaseSearchTerm;
};
updateTree = () => {
const { dynamicData } = this.props;
const filters = Object.keys(dynamicData).map(name => ({ name }));
const tree = transformToTreeStructure(
dynamicData,
filters.map(f => f.name),
);
this.setState({ tree });
};
handleNodeSelected = (node: any) => {
if (this.props.input && this.props.input.value) {
const currentValue = String(this.props.input.value);
const path = node.path;
const { bindings, paths } = getDynamicBindings(currentValue);
const autoComplete = bindings.map((binding, i) => {
if (binding.indexOf("{{") > -1 && paths[i] === "") {
return binding.replace(DATA_BIND_AUTOCOMPLETE, `{{${path}}}`);
}
return binding;
});
this.props.input.onChange &&
this.props.input.onChange(autoComplete.join(""));
this.input && this.input.focus();
}
};
setInputRef = (ref: HTMLInputElement | null) => {
if (ref) {
this.input = ref;
}
};
handleInputChange = (e: ChangeEvent<{ value: string }>) => {
if (this.props.input && this.props.input.onChange) {
this.props.input.onChange(e);
}
const value = e.target.value;
if (this.search) {
const { bindings, paths } = getDynamicBindings(value);
bindings.forEach((binding, i) => {
if (binding.indexOf("{{") > -1 && paths[i] === "") {
const query = getDynamicAutocompleteSearchTerm(binding);
this.search && this.search(query);
}
});
}
};
handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowDown") {
if (
document.activeElement &&
document.activeElement.tagName === "INPUT"
) {
const tree = document.getElementById("tree");
const container =
tree && tree.closest<HTMLDivElement>("[tabindex='0']");
if (container) {
container.focus();
}
}
}
};
render() {
const { input, ...rest } = this.props;
return (
<Wrapper onKeyDown={this.handleKeyDown}>
<BaseTextInput
refHandler={this.setInputRef}
input={{
...input,
onChange: this.handleInputChange,
}}
{..._.omit(rest, ["dynamicData", "dispatch"])}
/>
{this.state.showTree && this.state.tree.length && (
<TreeMenu
data={this.state.tree}
matchSearch={this.handleNodeSearch}
onClickItem={this.handleNodeSelected}
initialFocusKey={this.state.tree[0].key}
disableKeyboard={false}
>
{({ search, items }) => (
<DataTreeWrapper id="tree">
{items.length === 0 ? (
<NoResultsMessage>No results found</NoResultsMessage>
) : (
items.map((item: TreeMenuItem) => {
this.search = search;
return <DataTreeNode key={item.key} item={item} />;
})
)}
</DataTreeWrapper>
)}
</TreeMenu>
)}
</Wrapper>
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
dynamicData: getNameBindingsWithData(state),
2019-12-06 13:16:08 +00:00
});
export default connect(mapStateToProps)(DynamicAutocompleteInput);