import { CategoryPreview } from "$components/CategoryPreview/CategoryPreview";
import { CategorySearchItem } from "$components/CategorySearchItem/CategorySearchItem";
import { DefaultSearchItem } from "$components/DefaultSearchItem/DefaultSearchItem";
import * as React from "react";
import memoizeOne from "memoize-one";
import { debounce, isEmpty } from "lodash/fp"; // TODO: decide whether to use lodash/fp or normal
import { Component } from "react";
import styled from "react-emotion";
import { FilteringHandler } from "./FilteringHandler";
import {
NestedInputTagsProps,
NestedInputTagsState
} from "./NestedInputTags.types";
import { TagInput } from "$components/TagInput/TagInput";
import { SelectBox } from "$components/SelectBox/SelectBox";
import { Option, getOptionKey } from "$components/Option/Option";
import { OptionMode } from "$components/Option/Option.types";
import { Option as OptionType } from "$lib-core-types/Option.types";
import { OptionDetails } from "$lib-core-types/OptionDetails.types";
import { OptionsDetailsCache } from "./OptionsDetailsCache";
import { toDetails } from "$utils/toDetails";
import { toOptionsWithParents } from "$utils/toOptionsWithPartens";
import { NITContext } from "./NITContext";
import { getDetailFromInitialValues } from "$utils/toDetails";
import { FlatOption } from "$components/FlatOption/FlatOption";
const Body = styled("div")`
width: 100%;
position: relative;
display: inline-block;
& * {
box-sizing: border-box;
}
`;
const getPristineState = (options: OptionDetails[] = []) => ({
currentSearchStr: null,
currentFilterStr: null,
currentOptions: options,
displayMode: OptionMode.Full
});
const getIdleState = () => ({
// TODO: add box opening
selectBoxVisible: false,
currentSearchStr: null,
currentFilterStr: null,
currentOptions: null,
displayMode: OptionMode.Full
});
export class NestedInputTags extends Component<
NestedInputTagsProps,
NestedInputTagsState
> {
state = {
// TODO: add box opening
selectBoxVisible: false,
currentSearchStr: null,
currentFilterStr: null,
currentOptions: null,
displayMode: OptionMode.Full
};
filteringHandler = new FilteringHandler();
//TODO: rename this item
detailsCache: OptionsDetailsCache;
bodyRef: any;
selectBoxRef: any;
constructor(props: NestedInputTagsProps) {
super(props);
const { options, initialValues } = props;
this.detailsCache = new OptionsDetailsCache();
if (initialValues) {
const getOptionDetail = getDetailFromInitialValues(options);
const details = initialValues.map(getOptionDetail);
details.forEach(detail => this.detailsCache.add(detail));
}
}
componentDidMount() {
document.addEventListener("mousedown", this.onDocumentMouseDown);
}
// TODO: move to mapPropsToState
UNSAFE_componentWillReceiveProps({
options: nextOptions
}: NestedInputTagsProps) {
const { options } = this.props;
if (nextOptions !== options) {
this.detailsCache = new OptionsDetailsCache();
this.setState(getIdleState());
}
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.onDocumentMouseDown);
}
getInitialOptions() {
const { options } = this.props;
return this.toOptionsDetails(options);
}
toOptionsDetails = memoizeOne((options: OptionType[]) =>
options.map(option => toDetails(option))
);
prepareOptions(optionsDetails: OptionDetails[], categories: string[]) {
const { displayMode } = this.state;
const { isFlat } = this.props;
const flatOptions = this.detailsCache.selectedOptions.get("") || {
optionsDetails: [] as OptionDetails[]
};
const flatOptionsIds = flatOptions.optionsDetails.map(
({ option: { id } }) => id
);
return displayMode === OptionMode.Short
? this.prepareSearchItems(optionsDetails, categories)
: optionsDetails.map(details => {
const {
option: { allowAny, categoryPreview, label }
} = details;
if (categoryPreview) {
return (
);
} else if (!isFlat) {
return (
);
}
return (
);
});
}
prepareSearchItems(optionsDetails: OptionDetails[], categories: string[]) {
const categoriesItems = categories.map(category => (
));
const detailsItems = optionsDetails.map(details => (
));
return [...categoriesItems, ...detailsItems];
}
onDocumentMouseDown = ({ target }: any) => {
if (this.bodyRef.contains(target)) {
this.setState({ selectBoxVisible: true });
} else {
this.setState(getIdleState());
}
};
setBodyRef = (ref: any) => (this.bodyRef = ref);
setSelectBoxRef = (ref: any) => (this.selectBoxRef = ref);
onCategorySelected = (category: string) => {
const { options } = this.props;
const allOptions = this.toOptionsDetails(options);
const categoryOptions = this.filteringHandler.findAllOptionsForCategory(
allOptions,
category
);
const categoryMetaOption = {
id: category,
label: category,
subOptions: categoryOptions
};
const details = toDetails(categoryMetaOption);
details.isCategory = true;
this.setState(getPristineState([details]));
};
onOptionSelected = (details: OptionDetails) => {
const { onSelected } = this.props;
const {
option: { subOptions, renderChildren }
} = details;
const areSubOptionsEmpty = isEmpty(subOptions);
const haveNotChildrenRenderer = !renderChildren;
const shouldBeSelected = areSubOptionsEmpty && haveNotChildrenRenderer;
if (shouldBeSelected) {
this.detailsCache.add(details);
const { option, parents } = details;
onSelected([{ option, parents }]);
}
this.setState(
getPristineState(shouldBeSelected ? this.getInitialOptions() : [details])
);
};
onAnySelected = (details: OptionDetails[], parent: OptionDetails) => {
const { onSelected } = this.props;
this.detailsCache.addFullGroup(details, parent);
this.setState({ currentOptions: this.getInitialOptions() }, () => {
const { option, parents } = parent;
onSelected([{ option, parents }]);
});
};
onTextChanged = (val: string) => {
this.setState(
{
currentSearchStr: val
},
() => {
const { currentSearchStr } = this.state;
if (currentSearchStr && (currentSearchStr as string).length !== 1) {
this.onFilterCriteriaChanged();
}
}
);
};
onFilterCriteriaChanged = debounce(250, () => {
this.setState(({ currentSearchStr }) => ({
currentFilterStr: currentSearchStr,
displayMode: currentSearchStr ? OptionMode.Short : OptionMode.Full
}));
});
onTagRemoved = (parentKey: string, details?: OptionDetails) => {
const { onRemoved } = this.props;
if (details) {
this.detailsCache.remove(details);
onRemoved(toOptionsWithParents(details), parentKey);
} else {
const removed = this.detailsCache.removeGroup(parentKey);
if (removed) {
onRemoved(toOptionsWithParents(removed.optionsDetails), parentKey);
}
}
this.forceUpdate();
};
render() {
const {
options,
className,
categoryPrefix = "in",
inputFactory
} = this.props;
const {
currentSearchStr,
currentFilterStr,
currentOptions,
selectBoxVisible
} = this.state;
const filterOptions =
currentOptions === null ? this.toOptionsDetails(options) : currentOptions;
const { details, categories } = this.filteringHandler.filter(
filterOptions,
currentFilterStr
);
const inputProps = {
inputValue: currentSearchStr || "",
onChange: this.onTextChanged,
onRemoved: this.onTagRemoved,
selectedOptions: this.detailsCache.getGroupedOptions()
};
return (
{inputFactory ? (
inputFactory(inputProps)
) : (
)}
{selectBoxVisible && (
{this.prepareOptions(details, categories)}
)}
);
}
}