web: Components: Attempt to re-write selector and fix check options

This commit is contained in:
Alexey Safronov 2019-11-01 17:55:23 +03:00
parent eae2a45b71
commit b445e25701
5 changed files with 1209 additions and 452 deletions

View File

@ -0,0 +1,220 @@
/* eslint-disable react/prop-types */
import React, { useRef, useEffect, useState } from "react";
import { storiesOf } from "@storybook/react";
import {
withKnobs
} from "@storybook/addon-knobs/react";
import withReadme from "storybook-readme/with-readme";
import Readme from "./README.md";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import faker, { name } from "faker";
import uniqueId from "lodash/uniqueId";
import Section from "../../../.storybook/decorators/section";
//import ADSelectorRow from "./sub-components/row";
import Checkbox from "../checkbox";
import CustomScrollbarsVirtualList from "../scrollbar/custom-scrollbars-virtual-list";
import DropDown from "../drop-down";
const ExampleWrapper = ({
// Are there more items to load?
// (This information comes from the most recent API request.)
hasNextPage,
// Are we currently loading a page of items?
// (This may be an in-flight flag in your Redux store for example.)
isNextPageLoading,
// Array of items loaded so far.
items,
// Callback function responsible for loading the next page of items.
loadNextPage,
sortOrder
}) => {
// We create a reference for the InfiniteLoader
const listRef = useRef(null);
const hasMountedRef = useRef(false);
const [selected, setSelected] = useState([]);
// Each time the sort prop changed we called the method resetloadMoreItemsCache to clear the cache
useEffect(() => {
if (listRef.current && hasMountedRef.current) {
listRef.current.resetloadMoreItemsCache();
}
hasMountedRef.current = true;
}, [sortOrder]);
// If there are more items to be loaded then add an extra row to hold a loading indicator.
const itemCount = hasNextPage ? items.length + 1 : items.length;
// Only load 1 page of items at a time.
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;
// Every row is loaded except for our loading indicator row.
const isItemLoaded = index => !hasNextPage || index < items.length;
const onChange = (e, item) => {
const newSelected = e.target.checked
? [item, ...selected]
: selected.filter(el => el.name !== item.name);
//console.log("OnChange", newSelected);
setSelected(newSelected);
};
const onSelect = (e, item) => {
console.log("onSelect", item);
};
// Render an item or a loading indicator.
const Item = ({ index, style }) => {
let content;
if (!isItemLoaded(index)) {
content = "Loading...";
} else {
const item = items[index];
const checked = selected.findIndex(el => el.id === item.id) > -1;
const newStyle = Object.assign({}, style, {
padding: "0 0.5rem",
lineHeight: "30px"
});
//console.log("Item render", item, checked, selected);
content = (
<Checkbox
key={item.id}
label={item.name}
isChecked={checked}
className="option_checkbox"
onChange={e => onChange(e, item)}
/>
/*<>
<input
id={item.id}
type="checkbox"
onChange={e => onChange(e, item)}
checked={checked}
/>
<label htmlFor={item.id}>{item.name}</label>
</>*/
/*<ADSelectorRow
key={item.id}
label={item.name}
isChecked={checked}
isMultiSelect={true}
isSelected={false}
className="ListItem"
style={{ padding: "0 0.5rem", lineHeight: "30px" }}
onChange={e => onChange(e, item)}
onSelect={e => onSelect(e, item)}
/>*/
);
}
return <div style={style}>{content}</div>;
};
// We passed down the ref to the InfiniteLoader component
return (
<InfiniteLoader
ref={listRef}
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
className="List"
style={{border: "1px solid #ddd", borderRadius: "0.25rem"}}
height={300}
itemCount={itemCount}
itemSize={30}
onItemsRendered={onItemsRendered}
ref={ref}
width={500}
outerElementType={CustomScrollbarsVirtualList}
>
{Item}
</List>
)}
</InfiniteLoader>
);
}
class ADSelectorExample extends React.PureComponent {
state = {
hasNextPage: true,
isNextPageLoading: false,
sortOrder: "asc",
items: []
};
constructor(props) {
super(props);
faker.seed(123);
this.persons = new Array(1000)
.fill(true)
.map(() => ({ name: name.findName(), id: uniqueId() }));
this.persons.sort((a, b) => a.name.localeCompare(b.name));
}
_loadNextPage = (...args) => {
this.setState({ isNextPageLoading: true }, () => {
setTimeout(() => {
this.setState(state => ({
hasNextPage: state.items.length < 100,
isNextPageLoading: false,
items: [...state.items].concat(
this.persons.slice(args[0], args[0] + 10)
)
}));
}, 2500);
});
};
_handleSortOrderChange = e => {
this.persons.sort((a, b) => {
if (e.target.value === "asc") {
return a.name.localeCompare(b.name);
}
return b.name.localeCompare(a.name);
});
this.setState({
sortOrder: e.target.value,
items: []
});
};
render() {
const { hasNextPage, isNextPageLoading, items, sortOrder } = this.state;
return (
<>
<div style={{position: "relative"}}>
<select onChange={this._handleSortOrderChange}>
<option value="asc">ASC</option>
<option value="desc">DESC</option>
</select>
</div>
<ExampleWrapper
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
items={items}
sortOrder={sortOrder}
loadNextPage={this._loadNextPage}
/>
</>
);
}
}
storiesOf("Components|AdvancedSelector", module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add("only list", () => {
return (
<Section>
<ADSelectorExample />
</Section>
);
});

View File

@ -1,19 +1,44 @@
import React from "react";
import PropTypes from "prop-types";
import CustomScrollbarsVirtualList from "../../../scrollbar/custom-scrollbars-virtual-list";
import { FixedSizeList } from "react-window";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import ADSelectorRow from "../row";
//import ADSelectorRow from "../row";
import Checkbox from "../../../checkbox";
import Loader from "../../../loader";
import { Text } from "../../../text";
import findIndex from "lodash/findIndex";
//import isEqual from "lodash/isEqual";
class ADSelectorOptionsBody extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
//this.hasMountedRef = React.createRef(false);
}
/*componentDidMount() {
this.hasMountedRef.current = true;
}
componentDidUpdate(prevProps) {
if(!isEqual(this.props.selectedOptions, prevProps.selectedOptions)) {
if(this.listRef.current && this.hasMountedRef.current) {
//this.listRef.current.resetloadMoreItemsCache(true);
//console.log("this.listRef.current", this.listRef.current)
}
}
}*/
renderRow = ({ index, style }) => {
//console.log("renderRow", option, isChecked, this.state.selectedOptions);
let content;
if (!this.isItemLoaded(index)) {
const isLoaded = this.isItemLoaded(index);
let key = "loader";
if (!isLoaded) {
content = (
<div className="option" style={style} key="loader">
<Loader
@ -34,9 +59,17 @@ class ADSelectorOptionsBody extends React.Component {
? selectedAll ||
findIndex(selectedOptions, { key: option.key }) > -1
: undefined;
key = option.key;
content = (
<ADSelectorRow
<Checkbox
key={option.key}
label={option.label}
isChecked={isChecked}
className="option_checkbox"
onChange={this.props.onRowChecked.bind(this, option)}
/>
/*<ADSelectorRow
key={option.key}
label={option.label}
isChecked={isChecked}
@ -44,11 +77,23 @@ class ADSelectorOptionsBody extends React.Component {
style={style}
onChange={this.props.onRowChecked.bind(this, option)}
onSelect={this.props.onRowSelect.bind(this, option)}
/>
/>*/
/*<div className="option" style={style}>
<input
id={option.key}
type="checkbox"
onChange={(e) => {
console.log("checkbox click", e);
this.props.onRowChecked(option, e);
}}
checked={isChecked}
/>
<label htmlFor={option.key}>{option.label}</label>
</div>*/
);
}
return <>{content}</>;
return <div className="option" style={style} key={key}>{content}</div>;
};
isItemLoaded = index =>
@ -71,12 +116,13 @@ class ADSelectorOptionsBody extends React.Component {
return (
<div>
<InfiniteLoader
ref={this.listRef}
isItemLoaded={this.isItemLoaded}
itemCount={itemCount}
loadMoreItems={this.loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
<List
className="options_list"
height={listHeight}
width={listWidth}
@ -87,7 +133,7 @@ class ADSelectorOptionsBody extends React.Component {
outerElementType={CustomScrollbarsVirtualList}
>
{this.renderRow}
</FixedSizeList>
</List>
)}
</InfiniteLoader>
</div>

View File

@ -15,10 +15,10 @@ class ADSelectorRow extends React.Component {
isSelected
} = this.props;
console.log("ADSelectorRow render", label, isChecked);
//console.log("ADSelectorRow render", label, isChecked);
return (
<div className={`option ${isSelected && "selected"}`} style={style}>
<div className={`option${isSelected ? "selected" : ""}`} style={style}>
{isMultiSelect ? (
<Checkbox
label={label}

View File

@ -1,21 +1,18 @@
import React from "react";
import React, { useRef, useEffect, useState } from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
import { FixedSizeList as List } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import Checkbox from "../../checkbox";
import ComboBox from "../../combobox";
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import isEmpty from "lodash/isEmpty";
import Loader from "../../loader";
import { Text } from "../../text";
import CustomScrollbarsVirtualList from "../../scrollbar/custom-scrollbars-virtual-list";
import ADSelectorOptionsHeader from "./options/header";
import ADSelectorOptionsBody from "./options/body";
import ADSelectorGroupsHeader from "./groups/header";
import ADSelectorGroupsBody from "./groups/body";
import ADSelectorFooter from "./footer";
import { handleAnyClick } from "../../../utils/event";
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const Container = ({
@ -53,253 +50,182 @@ const Container = ({
/* eslint-enable no-unused-vars */
const StyledContainer = styled(Container)`
display: flex;
flex-direction: column;
${props => (props.containerWidth ? `width: ${props.containerWidth};` : "")}
${props => (props.containerHeight ? `height: ${props.containerHeight};` : "")}
.data_container {
margin: 16px 16px -5px 16px;
.head_container {
display: flex;
margin-bottom: ${props => (props.displayType === "dropdown" ? 8 : 16)}px;
.options_searcher {
display: inline-block;
width: 100%;
display: flex;
flex-direction: column;
${props => (props.containerWidth ? `width: ${props.containerWidth};` : "")}
${props =>
props.containerHeight ? `height: ${props.containerHeight};` : ""}
.data_container {
margin: 16px 16px -5px 16px;
.head_container {
display: flex;
margin-bottom: ${props =>
props.displayType === "dropdown" ? 8 : 16}px;
.options_searcher {
display: inline-block;
width: 100%;
${props =>
props.displayType === "dropdown" &&
props.size === "full" &&
css`
margin-right: ${props => (props.allowCreation ? 8 : 16)}px;
`}
/*${props =>
props.allowCreation
? css`
width: 272px;
margin-right: 8px;
`
: css`
width: ${props => (props.isDropDown ? "313px" : "100%")};
`}*/
}
.add_new_btn {
${props =>
props.allowCreation &&
css`
display: inline-block;
vertical-align: top;
height: 32px;
width: 36px;
margin-right: 16px;
line-height: 18px;
`}
}
}
.options_group_selector {
margin-bottom: 12px;
}
.data_column_one {
${props =>
props.size === "full" &&
props.displayType === "dropdown" &&
props.groups &&
props.groups.length > 0
? css`
width: 50%;
display: inline-block;
`
: ""}
.options_list {
margin-top: 4px;
margin-left: -8px;
.option {
line-height: 32px;
padding-left: ${props => (props.isMultiSelect ? 8 : 0)}px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #f8f9f9;
}
}
}
}
.data_column_two {
${props =>
props.displayType === "dropdown" &&
props.size === "full" &&
css`
margin-right: ${props => (props.allowCreation ? 8 : 16)}px;
`}
/*${props =>
props.allowCreation
props.groups &&
props.groups.length > 0
? css`
width: 272px;
margin-right: 8px;
width: 50%;
display: inline-block;
border-left: 1px solid #eceef1;
`
: css`
width: ${props => (props.isDropDown ? "313px" : "100%")};
`}*/
}
.add_new_btn {
${props =>
props.allowCreation &&
css`
display: inline-block;
vertical-align: top;
height: 32px;
width: 36px;
margin-right: 16px;
line-height: 18px;
`}
}
}
.options_group_selector {
margin-bottom: 12px;
}
.data_column_one {
${props =>
props.size === "full" &&
props.displayType === "dropdown" &&
props.groups &&
props.groups.length > 0
? css`
width: 50%;
display: inline-block;
`
: ""}
.options_list {
margin-top: 4px;
margin-left: -8px;
.option {
line-height: 32px;
padding-left: ${props => (props.isMultiSelect ? 8 : 0)}px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
: ""}
.group_header {
font-weight: 600;
padding-left: 16px;
padding-bottom: 14px;
}
.group_list {
margin-left: 8px;
.option {
line-height: 32px;
padding-left: 8px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #f8f9f9;
}
}
&:hover {
background-color: #f8f9f9;
.option.selected {
background-color: #ECEEF1;
}
}
}
}
`;
.data_column_two {
${props =>
props.displayType === "dropdown" &&
props.groups &&
props.groups.length > 0
? css`
width: 50%;
display: inline-block;
border-left: 1px solid #eceef1;
`
: ""}
const ADSelector = props => {
const {
options,
groups,
hasNextPage,
isNextPageLoading,
loadNextPage,
value,
placeholder,
isDisabled,
onSearchChanged,
isMultiSelect,
buttonLabel,
selectAllLabel,
size,
displayType,
onAddNewClick,
allowCreation,
selectedOptions,
selectedGroups,
onSelect
} = props;
.group_header {
font-weight: 600;
padding-left: 16px;
padding-bottom: 14px;
}
// We create a reference for the InfiniteLoader
const listRef = useRef(null);
//const hasMountedRef = useRef(false);
const [selected, setSelected] = useState(selectedOptions || []);
const [selectedGrps, setSelectedGroups] = useState(selectedGroups || []);
const [selectedAll, setSelectedAll] = useState(false);
.group_list {
margin-left: 8px;
const convertGroups = items => {
if (!items) return [];
.option {
line-height: 32px;
padding-left: 8px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #f8f9f9;
}
}
.option.selected {
background-color: #ECEEF1;
}
}
}
}
`;
class ADSelector extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
const { groups, selectedOptions, selectedGroups, selectedAll, isOpen } = props;
const convertedGroups = this.convertGroups(groups);
const currentGroup = this.getCurrentGroup(convertedGroups);
this.state = {
selectedOptions: selectedOptions || [],
selectedAll: selectedAll || false,
groups: convertedGroups,
selectedGroups: selectedGroups || [],
currentGroup: currentGroup
};
if (isOpen) handleAnyClick(true, this.handleClick);
}
handleClick = e => {
const { onCancel, allowAnyClickClose, isOpen } = this.props;
if (
isOpen &&
allowAnyClickClose &&
this.ref &&
this.ref.current &&
!this.ref.current.contains(e.target) &&
e &&
e.target &&
e.target.className &&
typeof e.target.className.indexOf === "function" &&
e.target.className.indexOf("option_checkbox") === -1
) {
onCancel && onCancel();
}
};
componentWillUnmount() {
handleAnyClick(false, this.handleClick);
}
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
componentDidUpdate(prevProps) {
const {
groups,
selectedAll,
isMultiSelect,
selectedOptions,
selectedGroups,
allowAnyClickClose,
isOpen
} = this.props;
if (isOpen !== prevProps.isOpen) {
handleAnyClick(isOpen, this.handleClick);
}
if (allowAnyClickClose !== prevProps.allowAnyClickClose) {
handleAnyClick(allowAnyClickClose, this.handleClick);
}
let newState = {};
if (!isEqual(selectedOptions, prevProps.selectedOptions)) {
newState = { selectedOptions };
}
if (!isEqual(selectedGroups, prevProps.selectedGroups)) {
newState = Object.assign({}, newState, { selectedGroups });
}
if (isMultiSelect !== prevProps.isMultiSelect) {
newState = Object.assign({}, newState, {
selectedOptions: []
});
}
if (selectedAll !== prevProps.selectedAll) {
newState = Object.assign({}, newState, {
selectedAll
});
}
if (!isEqual(groups, prevProps.groups)) {
const newGroups = this.convertGroups(groups);
const currentGroup = this.getCurrentGroup(newGroups);
newState = Object.assign({}, newState, {
groups: newGroups,
currentGroup
});
}
if (!isEmpty(newState)) {
this.setState({ ...this.state, ...newState });
}
}
convertGroups = groups => {
if (!groups) return [];
const wrappedGroups = groups.map(this.convertGroup);
const wrappedGroups = items.map(convertGroup);
return wrappedGroups;
};
convertGroup = group => {
const convertGroup = group => {
return {
key: group.key,
label: `${group.label} (${group.total})`,
@ -307,231 +233,259 @@ class ADSelector extends React.Component {
};
};
getCurrentGroup = groups => {
const currentGroup = groups.length > 0 ? groups[0] : "No groups";
const getCurrentGroup = items => {
const currentGroup = items.length > 0 ? items[0] : "No groups";
return currentGroup;
};
onButtonClick = () => {
this.props.onSelect &&
this.props.onSelect(
this.state.selectedAll ? this.props.options : this.state.selectedOptions
);
const convertedGroups = convertGroups(groups);
const curGroup = getCurrentGroup(convertedGroups);
const [currentGroup, setCurrentGroup] = useState(curGroup);
// Each time the sort prop changed we called the method resetloadMoreItemsCache to clear the cache
/*useEffect(() => {
if (listRef.current && hasMountedRef.current) {
listRef.current.resetloadMoreItemsCache();
}
hasMountedRef.current = true;
}, [sortOrder]);*/
// If there are more items to be loaded then add an extra row to hold a loading indicator.
const itemCount = hasNextPage ? options.length + 1 : options.length;
// Only load 1 page of items at a time.
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;
// Every row is loaded except for our loading indicator row.
const isItemLoaded = index => !hasNextPage || index < options.length;
const onChange = (e, item) => {
const newSelected = e.target.checked
? [item, ...selected]
: selected.filter(el => el.name !== item.name);
//console.log("OnChange", newSelected);
setSelected(newSelected);
};
onSelectedAllChange = e => {
this.setState({
selectedAll: e.target.checked,
selectedOptions: e.target.checked ? this.props.options : []
});
};
onOptionSelect = option => {
this.props.onSelect && this.props.onSelect(option);
};
onOptionChange = (option, e) => {
const { selectedOptions } = this.state;
const newSelectedOptions = e.target.checked
? [...selectedOptions, option]
: filter(selectedOptions, obj => obj.key !== option.key);
//console.log("onChange", option, e.target.checked, newSelectedOptions);
this.setState({
selectedOptions: newSelectedOptions,
selectedAll: newSelectedOptions.length === this.props.options.length
});
};
onGroupChange = (group, e) => {
/*this.setState({
currentGroup: group
});
this.props.onGroupChange && this.props.onGroupChange(group);*/
const { selectedGroups } = this.state;
const onGroupChange = (e, item) => {
const newSelectedGroups = e.target.checked
? [...selectedGroups, group]
: filter(selectedGroups, obj => obj.key !== group.key);
//console.log("onChange", option, e.target.checked, newSelectedOptions);
this.setState({
selectedGroups: newSelectedGroups,
currentGroup: group
});
this.props.onGroupSelect && this.props.onGroupSelect(group);
? [item, ...selectedGrps]
: selectedGrps.filter(el => el.name !== item.name);
//console.log("onGroupChange", item);
setSelectedGroups(newSelectedGroups);
};
const onCurrentGroupChange = (e, item) => {
//console.log("onCurrentGroupChange", item);
setCurrentGroup(item);
};
const onSelectedAllChange = (e, item) => {
//console.log("onSelectedAllChange", item);
setSelectedAll(item);
};
render() {
//console.log("ADSelector render");
const {
options,
hasNextPage,
isNextPageLoading,
loadNextPage,
value,
placeholder,
isDisabled,
onSearchChanged,
isMultiSelect,
buttonLabel,
selectAllLabel,
size,
displayType,
onAddNewClick,
allowCreation
} = this.props;
const onButtonClick = () => {
onSelect && onSelect(selectedAll ? options : selectedOptions);
};
const { selectedOptions, selectedAll, currentGroup, groups, selectedGroups } = this.state;
let containerHeight;
let containerWidth;
let listHeight;
let listWidth;
const itemHeight = 32;
const hasGroups = groups && groups.length > 0;
switch (size) {
case "compact":
containerHeight = hasGroups ? "326px" : "100%";
containerWidth = "379px";
listWidth = displayType === "dropdown" ? 356 : 356;
listHeight = hasGroups ? 488 : isMultiSelect ? 176 : 226;
break;
case "full":
default:
containerHeight = "100%";
containerWidth = displayType === "dropdown" ? "690px" : "326px";
listWidth = displayType === "dropdown" ? 320 : 300;
listHeight = 488;
break;
// Render an item or a loading indicator.
const Item = ({ index, style }) => {
let content;
if (!isItemLoaded(index)) {
content = <div className="option" style={style} key="loader">
<Loader
type="oval"
size={16}
style={{
display: "inline",
marginRight: "10px"
}}
/>
<Text.Body as="span">Loading... Please wait...</Text.Body>
</div>;
} else {
const option = options[index];
const checked = selected.findIndex(el => el.key === option.key) > -1;
//console.log("Item render", item, checked, selected);
content = (
<Checkbox
key={option.key}
label={option.label}
isChecked={checked}
className="option_checkbox"
onChange={e => onChange(e, option)}
/>
/*<>
<input
id={item.id}
type="checkbox"
onChange={e => onChange(e, item)}
checked={checked}
/>
<label htmlFor={item.id}>{item.name}</label>
</>*/
/*<ADSelectorRow
key={item.id}
label={item.name}
isChecked={checked}
isMultiSelect={true}
isSelected={false}
className="ListItem"
style={{ padding: "0 0.5rem", lineHeight: "30px" }}
onChange={e => onChange(e, item)}
onSelect={e => onSelect(e, item)}
/>*/
);
}
// If there are more items to be loaded then add an extra row to hold a loading indicator.
//const itemCount = hasNextPage ? options.length + 1 : options.length;
return <div style={style}>{content}</div>;
};
return (
<StyledContainer
let containerHeight;
let containerWidth;
let listHeight;
let listWidth;
const itemHeight = 32;
const hasGroups = convertedGroups && convertedGroups.length > 0;
switch (size) {
case "compact":
containerHeight = hasGroups ? "326px" : "100%";
containerWidth = "379px";
listWidth = displayType === "dropdown" ? 356 : 356;
listHeight = hasGroups ? 488 : isMultiSelect ? 176 : 226;
break;
case "full":
default:
containerHeight = "100%";
containerWidth = displayType === "dropdown" ? "690px" : "326px";
listWidth = displayType === "dropdown" ? 320 : 300;
listHeight = 488;
break;
}
// We passed down the ref to the InfiniteLoader component
return (
<StyledContainer
containerHeight={containerHeight}
containerWidth={containerWidth}
{...this.props}
>
<div ref={this.ref}>
<div className="data_container">
<div className="data_column_one">
<ADSelectorOptionsHeader
value={value}
searchPlaceHolder={placeholder}
isDisabled={isDisabled}
allowCreation={allowCreation}
onAddNewClick={onAddNewClick}
onChange={onSearchChanged}
onClearSearch={onSearchChanged.bind(this, "")}
/>
{displayType === "aside" && groups && groups.length > 0 && (
<ComboBox
className="options_group_selector"
isDisabled={isDisabled}
options={groups}
selectedOption={currentGroup}
dropDownMaxHeight={200}
scaled={true}
scaledOptions={true}
size="content"
onSelect={this.onCurrentGroupChange}
/>
)}
{isMultiSelect && !groups && !groups.length && (
<Checkbox
label={selectAllLabel}
isChecked={
selectedAll || selectedOptions.length === options.length
}
isIndeterminate={!selectedAll && selectedOptions.length > 0}
className="option_select_all_checkbox"
onChange={this.onSelectedAllChange}
/>
)}
<ADSelectorOptionsBody
options={options}
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
loadNextPage={loadNextPage}
{...props}
>
<div className="data_container">
<div className="data_column_one">
<ADSelectorOptionsHeader
value={value}
searchPlaceHolder={placeholder}
isDisabled={isDisabled}
allowCreation={allowCreation}
onAddNewClick={onAddNewClick}
onChange={onSearchChanged}
onClearSearch={onSearchChanged.bind(this, "")}
/>
{displayType === "aside" && convertedGroups && convertedGroups.length > 0 && (
<ComboBox
className="options_group_selector"
isDisabled={isDisabled}
options={convertedGroups}
selectedOption={currentGroup}
dropDownMaxHeight={200}
scaled={true}
scaledOptions={true}
size="content"
onSelect={onCurrentGroupChange}
/>
)}
{isMultiSelect && !convertedGroups && !convertedGroups.length && (
<Checkbox
label={selectAllLabel}
isChecked={
selectedAll || selectedOptions.length === options.length
}
isIndeterminate={!selectedAll && selectedOptions.length > 0}
className="option_select_all_checkbox"
onChange={onSelectedAllChange}
/>
)}
<InfiniteLoader
ref={listRef}
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
className="options_list"
//style={{ border: "1px solid #ddd", borderRadius: "0.25rem" }}
height={listHeight}
itemCount={itemCount}
itemSize={itemHeight}
onItemsRendered={onItemsRendered}
ref={ref}
width={listWidth}
outerElementType={CustomScrollbarsVirtualList}
>
{Item}
</List>
)}
</InfiniteLoader>
</div>
{displayType === "dropdown" &&
size === "full" &&
convertedGroups &&
convertedGroups.length > 0 && (
<div className="data_column_two">
<ADSelectorGroupsHeader headerLabel="Groups" />
<ADSelectorGroupsBody
options={convertedGroups}
selectedOptions={selectedGrps}
isMultiSelect={isMultiSelect}
currentGroup={currentGroup}
listHeight={listHeight}
listWidth={listWidth}
itemHeight={itemHeight}
onRowChecked={this.onOptionChange}
onRowSelect={this.onOptionSelect}
selectedOptions={selectedOptions}
selectedAll={selectedAll}
onRowChecked={onGroupChange}
/>
</div>
{displayType === "dropdown" &&
size === "full" &&
groups &&
groups.length > 0 && (
<div className="data_column_two">
<ADSelectorGroupsHeader headerLabel="Groups" />
<ADSelectorGroupsBody
options={groups}
selectedOptions={selectedGroups}
isMultiSelect={isMultiSelect}
currentGroup={currentGroup}
listHeight={listHeight}
itemHeight={itemHeight}
onRowChecked={this.onGroupChange}
/>
</div>
)}
</div>
<ADSelectorFooter
buttonLabel={buttonLabel}
isDisabled={
!this.state.selectedOptions ||
!this.state.selectedOptions.length
}
isMultiSelect={isMultiSelect}
onClick={this.onButtonClick}
selectedOptions={selectedOptions}
/>
</div>
</StyledContainer>
);
}
}
)}
</div>
<ADSelectorFooter
buttonLabel={buttonLabel}
isDisabled={!selectedOptions || !selectedOptions.length}
isMultiSelect={isMultiSelect}
onClick={onButtonClick}
selectedOptions={selectedOptions}
/>
</StyledContainer>
);
};
ADSelector.propTypes = {
isOpen: PropTypes.bool,
options: PropTypes.array,
groups: PropTypes.array,
hasNextPage: PropTypes.bool,
isNextPageLoading: PropTypes.bool,
loadNextPage: PropTypes.func,
value: PropTypes.string,
placeholder: PropTypes.string,
isDisabled: PropTypes.bool,
onSearchChanged: PropTypes.func,
isMultiSelect: PropTypes.bool,
buttonLabel: PropTypes.string,
selectAllLabel: PropTypes.string,
size: PropTypes.string,
displayType: PropTypes.oneOf(["dropdown", "aside"]),
onAddNewClick: PropTypes.func,
allowCreation: PropTypes.bool,
onSelect: PropTypes.func,
onChange: PropTypes.func,
onGroupSelect: PropTypes.func,
onGroupChange: PropTypes.func,
selectedOptions: PropTypes.array,
selectedGroups: PropTypes.array,
selectedAll: PropTypes.bool,
onCancel: PropTypes.func,
allowAnyClickClose: PropTypes.bool,
isOpen: PropTypes.bool,
options: PropTypes.array,
groups: PropTypes.array,
hasNextPage: PropTypes.bool,
isNextPageLoading: PropTypes.bool,
loadNextPage: PropTypes.func,
value: PropTypes.string,
placeholder: PropTypes.string,
isDisabled: PropTypes.bool,
onSearchChanged: PropTypes.func,
isMultiSelect: PropTypes.bool,
buttonLabel: PropTypes.string,
selectAllLabel: PropTypes.string,
size: PropTypes.string,
displayType: PropTypes.oneOf(["dropdown", "aside"]),
onAddNewClick: PropTypes.func,
allowCreation: PropTypes.bool,
onSelect: PropTypes.func,
onChange: PropTypes.func,
onGroupSelect: PropTypes.func,
onGroupChange: PropTypes.func,
selectedOptions: PropTypes.array,
selectedGroups: PropTypes.array,
selectedAll: PropTypes.bool,
onCancel: PropTypes.func,
allowAnyClickClose: PropTypes.bool
};
export default ADSelector;

View File

@ -0,0 +1,537 @@
import React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
import Checkbox from "../../checkbox";
import ComboBox from "../../combobox";
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import isEmpty from "lodash/isEmpty";
import ADSelectorOptionsHeader from "./options/header";
import ADSelectorOptionsBody from "./options/body";
import ADSelectorGroupsHeader from "./groups/header";
import ADSelectorGroupsBody from "./groups/body";
import ADSelectorFooter from "./footer";
import { handleAnyClick } from "../../../utils/event";
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const Container = ({
value,
placeholder,
isMultiSelect,
size,
width,
maxHeight,
isDisabled,
onSelect,
onSearchChanged,
options,
selectedOptions,
buttonLabel,
selectAllLabel,
groups,
selectedGroups,
onGroupSelect,
onGroupChange,
isOpen,
displayType,
containerWidth,
containerHeight,
allowCreation,
onAddNewClick,
allowAnyClickClose,
hasNextPage,
isNextPageLoading,
loadNextPage,
isSelected,
...props
}) => <div {...props} />;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
const StyledContainer = styled(Container)`
display: flex;
flex-direction: column;
${props => (props.containerWidth ? `width: ${props.containerWidth};` : "")}
${props => (props.containerHeight ? `height: ${props.containerHeight};` : "")}
.data_container {
margin: 16px 16px -5px 16px;
.head_container {
display: flex;
margin-bottom: ${props => (props.displayType === "dropdown" ? 8 : 16)}px;
.options_searcher {
display: inline-block;
width: 100%;
${props =>
props.displayType === "dropdown" &&
props.size === "full" &&
css`
margin-right: ${props => (props.allowCreation ? 8 : 16)}px;
`}
/*${props =>
props.allowCreation
? css`
width: 272px;
margin-right: 8px;
`
: css`
width: ${props => (props.isDropDown ? "313px" : "100%")};
`}*/
}
.add_new_btn {
${props =>
props.allowCreation &&
css`
display: inline-block;
vertical-align: top;
height: 32px;
width: 36px;
margin-right: 16px;
line-height: 18px;
`}
}
}
.options_group_selector {
margin-bottom: 12px;
}
.data_column_one {
${props =>
props.size === "full" &&
props.displayType === "dropdown" &&
props.groups &&
props.groups.length > 0
? css`
width: 50%;
display: inline-block;
`
: ""}
.options_list {
margin-top: 4px;
margin-left: -8px;
.option {
line-height: 32px;
padding-left: ${props => (props.isMultiSelect ? 8 : 0)}px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #f8f9f9;
}
}
}
}
.data_column_two {
${props =>
props.displayType === "dropdown" &&
props.groups &&
props.groups.length > 0
? css`
width: 50%;
display: inline-block;
border-left: 1px solid #eceef1;
`
: ""}
.group_header {
font-weight: 600;
padding-left: 16px;
padding-bottom: 14px;
}
.group_list {
margin-left: 8px;
.option {
line-height: 32px;
padding-left: 8px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #f8f9f9;
}
}
.option.selected {
background-color: #ECEEF1;
}
}
}
}
`;
class ADSelector extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
const { groups, selectedOptions, selectedGroups, selectedAll, isOpen } = props;
const convertedGroups = this.convertGroups(groups);
const currentGroup = this.getCurrentGroup(convertedGroups);
this.state = {
selectedOptions: selectedOptions || [],
selectedAll: selectedAll || false,
groups: convertedGroups,
selectedGroups: selectedGroups || [],
currentGroup: currentGroup
};
if (isOpen) handleAnyClick(true, this.handleClick);
}
handleClick = e => {
const { onCancel, allowAnyClickClose, isOpen } = this.props;
if (
isOpen &&
allowAnyClickClose &&
this.ref &&
this.ref.current &&
!this.ref.current.contains(e.target) &&
e &&
e.target &&
e.target.className &&
typeof e.target.className.indexOf === "function" &&
e.target.className.indexOf("option_checkbox") === -1
) {
onCancel && onCancel();
}
};
componentWillUnmount() {
handleAnyClick(false, this.handleClick);
}
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
componentDidUpdate(prevProps) {
const {
groups,
selectedAll,
isMultiSelect,
selectedOptions,
selectedGroups,
allowAnyClickClose,
isOpen
} = this.props;
if (isOpen !== prevProps.isOpen) {
handleAnyClick(isOpen, this.handleClick);
}
if (allowAnyClickClose !== prevProps.allowAnyClickClose) {
handleAnyClick(allowAnyClickClose, this.handleClick);
}
let newState = {};
if (!isEqual(selectedOptions, prevProps.selectedOptions)) {
newState = { selectedOptions };
}
if (!isEqual(selectedGroups, prevProps.selectedGroups)) {
newState = Object.assign({}, newState, { selectedGroups });
}
if (isMultiSelect !== prevProps.isMultiSelect) {
newState = Object.assign({}, newState, {
selectedOptions: []
});
}
if (selectedAll !== prevProps.selectedAll) {
newState = Object.assign({}, newState, {
selectedAll
});
}
if (!isEqual(groups, prevProps.groups)) {
const newGroups = this.convertGroups(groups);
const currentGroup = this.getCurrentGroup(newGroups);
newState = Object.assign({}, newState, {
groups: newGroups,
currentGroup
});
}
if (!isEmpty(newState)) {
this.setState({ ...this.state, ...newState });
}
}
convertGroups = groups => {
if (!groups) return [];
const wrappedGroups = groups.map(this.convertGroup);
return wrappedGroups;
};
convertGroup = group => {
return {
key: group.key,
label: `${group.label} (${group.total})`,
total: group.total
};
};
getCurrentGroup = groups => {
const currentGroup = groups.length > 0 ? groups[0] : "No groups";
return currentGroup;
};
onButtonClick = () => {
this.props.onSelect &&
this.props.onSelect(
this.state.selectedAll ? this.props.options : this.state.selectedOptions
);
};
onSelectedAllChange = e => {
this.setState({
selectedAll: e.target.checked,
selectedOptions: e.target.checked ? this.props.options : []
});
};
onOptionSelect = option => {
this.props.onSelect && this.props.onSelect(option);
};
onOptionChange = (option, e) => {
const { selectedOptions } = this.state;
const newSelectedOptions = e.target.checked
? [...selectedOptions, option]
: filter(selectedOptions, obj => obj.key !== option.key);
//console.log("onChange", option, e.target.checked, newSelectedOptions);
this.setState({
selectedOptions: newSelectedOptions,
selectedAll: newSelectedOptions.length === this.props.options.length
});
};
onGroupChange = (group, e) => {
/*this.setState({
currentGroup: group
});
this.props.onGroupChange && this.props.onGroupChange(group);*/
const { selectedGroups } = this.state;
const newSelectedGroups = e.target.checked
? [...selectedGroups, group]
: filter(selectedGroups, obj => obj.key !== group.key);
//console.log("onChange", option, e.target.checked, newSelectedOptions);
this.setState({
selectedGroups: newSelectedGroups,
currentGroup: group
});
this.props.onGroupSelect && this.props.onGroupSelect(group);
};
render() {
//console.log("ADSelector render");
const {
options,
hasNextPage,
isNextPageLoading,
loadNextPage,
value,
placeholder,
isDisabled,
onSearchChanged,
isMultiSelect,
buttonLabel,
selectAllLabel,
size,
displayType,
onAddNewClick,
allowCreation
} = this.props;
const { selectedOptions, selectedAll, currentGroup, groups, selectedGroups } = this.state;
let containerHeight;
let containerWidth;
let listHeight;
let listWidth;
const itemHeight = 32;
const hasGroups = groups && groups.length > 0;
switch (size) {
case "compact":
containerHeight = hasGroups ? "326px" : "100%";
containerWidth = "379px";
listWidth = displayType === "dropdown" ? 356 : 356;
listHeight = hasGroups ? 488 : isMultiSelect ? 176 : 226;
break;
case "full":
default:
containerHeight = "100%";
containerWidth = displayType === "dropdown" ? "690px" : "326px";
listWidth = displayType === "dropdown" ? 320 : 300;
listHeight = 488;
break;
}
// If there are more items to be loaded then add an extra row to hold a loading indicator.
//const itemCount = hasNextPage ? options.length + 1 : options.length;
return (
<StyledContainer
containerHeight={containerHeight}
containerWidth={containerWidth}
{...this.props}
>
<div ref={this.ref}>
<div className="data_container">
<div className="data_column_one">
<ADSelectorOptionsHeader
value={value}
searchPlaceHolder={placeholder}
isDisabled={isDisabled}
allowCreation={allowCreation}
onAddNewClick={onAddNewClick}
onChange={onSearchChanged}
onClearSearch={onSearchChanged.bind(this, "")}
/>
{displayType === "aside" && groups && groups.length > 0 && (
<ComboBox
className="options_group_selector"
isDisabled={isDisabled}
options={groups}
selectedOption={currentGroup}
dropDownMaxHeight={200}
scaled={true}
scaledOptions={true}
size="content"
onSelect={this.onCurrentGroupChange}
/>
)}
{isMultiSelect && !groups && !groups.length && (
<Checkbox
label={selectAllLabel}
isChecked={
selectedAll || selectedOptions.length === options.length
}
isIndeterminate={!selectedAll && selectedOptions.length > 0}
className="option_select_all_checkbox"
onChange={this.onSelectedAllChange}
/>
)}
<ADSelectorOptionsBody
options={options}
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
loadNextPage={loadNextPage}
isMultiSelect={isMultiSelect}
listHeight={listHeight}
listWidth={listWidth}
itemHeight={itemHeight}
onRowChecked={this.onOptionChange}
onRowSelect={this.onOptionSelect}
selectedOptions={selectedOptions}
selectedAll={selectedAll}
/>
</div>
{displayType === "dropdown" &&
size === "full" &&
groups &&
groups.length > 0 && (
<div className="data_column_two">
<ADSelectorGroupsHeader headerLabel="Groups" />
<ADSelectorGroupsBody
options={groups}
selectedOptions={selectedGroups}
isMultiSelect={isMultiSelect}
currentGroup={currentGroup}
listHeight={listHeight}
itemHeight={itemHeight}
onRowChecked={this.onGroupChange}
/>
</div>
)}
</div>
<ADSelectorFooter
buttonLabel={buttonLabel}
isDisabled={
!this.state.selectedOptions ||
!this.state.selectedOptions.length
}
isMultiSelect={isMultiSelect}
onClick={this.onButtonClick}
selectedOptions={selectedOptions}
/>
</div>
</StyledContainer>
);
}
}
ADSelector.propTypes = {
isOpen: PropTypes.bool,
options: PropTypes.array,
groups: PropTypes.array,
hasNextPage: PropTypes.bool,
isNextPageLoading: PropTypes.bool,
loadNextPage: PropTypes.func,
value: PropTypes.string,
placeholder: PropTypes.string,
isDisabled: PropTypes.bool,
onSearchChanged: PropTypes.func,
isMultiSelect: PropTypes.bool,
buttonLabel: PropTypes.string,
selectAllLabel: PropTypes.string,
size: PropTypes.string,
displayType: PropTypes.oneOf(["dropdown", "aside"]),
onAddNewClick: PropTypes.func,
allowCreation: PropTypes.bool,
onSelect: PropTypes.func,
onChange: PropTypes.func,
onGroupSelect: PropTypes.func,
onGroupChange: PropTypes.func,
selectedOptions: PropTypes.array,
selectedGroups: PropTypes.array,
selectedAll: PropTypes.bool,
onCancel: PropTypes.func,
allowAnyClickClose: PropTypes.bool,
};
export default ADSelector;