wix-style-react
Version:
wix-style-react
338 lines • 15.4 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import SelectorListContent from './Content';
import Box from '../Box';
import Search from '../Search';
import Text from '../Text';
import ToggleAllCheckbox from './ToggleAllCheckbox';
import { dataHooks } from './SelectorList.helpers';
import { DEFAULT_IMAGE_SIZE_BY_SIZE, SEARCH_SIZE_BY_SIZE } from './constants';
import { st, classes } from './SelectorList.st.css';
/**
* Use this component when needed to select one / multiple items having complex descriptions.
* E.g.: choosing products to promote via ShoutOuts
*/
class SelectorList extends React.PureComponent {
constructor() {
super(...arguments);
this.state = {
isLoaded: false,
isSearching: false,
items: [],
searchValue: '',
selectedItems: [],
indeterminateItems: [],
noResultsFound: false,
isEmpty: false,
};
this._renderList = () => {
const { dataHook, emptyState, renderNoResults, height, maxHeight, size, imageSize, imageShape, multiple, showDivider, } = this.props;
const { items, isLoaded, isEmpty, isSearching, searchValue, noResultsFound, selectedItems, } = this.state;
const hasMore = this._hasMore();
const contentProps = {
items,
selectedItems,
onToggle: this._onToggle,
emptyState,
renderNoResults,
isEmpty,
isLoading: !isLoaded || isSearching,
noResultsFound,
size,
imageSize: imageSize || DEFAULT_IMAGE_SIZE_BY_SIZE[size],
imageShape,
multiple,
showDivider,
loadMore: this._loadMore,
hasMore,
checkIsSelected: this._checkIsSelected,
checkIndeterminate: this._checkIndeterminate,
searchValue,
};
const shouldRenderSubheader = isLoaded && !isEmpty;
return (React.createElement(Box, { direction: "vertical", overflow: "hidden", dataHook,
height,
maxHeight },
shouldRenderSubheader && this._renderSubheader(),
React.createElement(SelectorListContent, { ...contentProps })));
};
this._renderSubheader = () => {
const { subtitle, withSearch, size, searchDebounceMs, searchPlaceholder } = this.props;
const { searchValue } = this.state;
return (React.createElement("div", { className: st(classes.subheaderWrapper, {
withSearch,
size,
}) },
subtitle && (React.createElement("div", { className: classes.subtitleWrapper }, typeof subtitle === 'string' ? (React.createElement(Text, { dataHook: dataHooks.subtitle }, subtitle)) : (subtitle))),
withSearch && (React.createElement(Search, { dataHook: dataHooks.search, placeholder: searchPlaceholder, onChange: this._onSearchChange, onClear: this._onClear, debounceMs: searchDebounceMs, value: searchValue, size: SEARCH_SIZE_BY_SIZE[size] }))));
};
this._renderToggleAllCheckbox = () => {
const { selectAllText, deselectAllText, size } = this.props;
const { items, selectedItems } = this.state;
const enabledItemsAmount = this._getEnabledItems(items).length;
const selectedEnabledItemsAmount = this._getEnabledItems(selectedItems).length;
const checkboxProps = {
selectAllText,
deselectAllText,
size,
enabledItemsAmount,
selectedEnabledItemsAmount,
selectAll: this._selectAll,
deselectAll: this._deselectAll,
};
return React.createElement(ToggleAllCheckbox, { ...checkboxProps });
};
this._updateSearchValue = searchValue => this.setState({
searchValue,
isSearching: true,
items: [],
}, () => this._loadInitialItems(searchValue));
this._onSearchChange = event => this._updateSearchValue(event.target.value);
this._onClear = () => {
const searchValue = '';
this.setState({
searchValue,
isSearching: true,
items: [],
}, () => {
this._getInitialData(searchValue).then(dataSourceProps => {
const { items, selectedItems, indeterminateItems } = this.state;
const newItems = [...items, ...dataSourceProps.items];
const newSelectedItems = selectedItems;
const newIndeterminateItems = indeterminateItems;
const noResultsFound = newItems.length === 0 && Boolean(searchValue);
const isEmpty = newItems.length === 0 && !searchValue;
this.setState({
items: newItems,
selectedItems: newSelectedItems,
indeterminateItems: newIndeterminateItems,
isLoaded: true,
isEmpty,
isSearching: false,
noResultsFound,
totalCount: dataSourceProps.totalCount,
});
});
});
};
this._checkIsSelected = item => {
const { selectedItems } = this.state;
return !!selectedItems.find(({ id }) => item.id === id);
};
this._checkIndeterminate = item => {
const { indeterminateItems } = this.state;
return !!indeterminateItems.find(({ id }) => item.id === id);
};
this._toggleItem = item => {
const { multiple } = this.props;
this.setState(({ selectedItems, indeterminateItems }) => ({
selectedItems: multiple
? this._checkIsSelected(item)
? selectedItems.filter(({ id }) => item.id !== id)
: selectedItems.concat(item)
: [item],
indeterminateItems: indeterminateItems.filter(({ id }) => item.id !== id),
}));
};
this._onToggle = item => {
const { onSelect } = this.props;
this._toggleItem(item);
if (onSelect) {
onSelect(item);
}
};
this._selectAll = () => {
const { selectedItems, items } = this.state;
const enabledItems = this._getEnabledItems(items);
this.setState({
selectedItems: selectedItems.concat(enabledItems),
indeterminateItems: [],
});
};
this._deselectAll = () => this.setState(({ selectedItems }) => ({
selectedItems: selectedItems.filter(({ disabled }) => disabled),
indeterminateItems: [],
}));
this._updateItems = ({ resetItems, items: nextPageItems, totalCount, searchValue, }) => {
const { items, selectedItems, indeterminateItems } = this.state;
// react only to the resolve of the relevant search
if (searchValue !== this.state.searchValue) {
return;
}
const newItems = [...(resetItems ? [] : items), ...nextPageItems];
const newSelectedItems = selectedItems
.concat(nextPageItems.filter(({ selected }) => selected))
.filter((value, index, self) => self.findIndex(({ id }) => id === value.id) === index);
const newIndeterminateItems = indeterminateItems
.concat(nextPageItems.filter(({ indeterminate }) => indeterminate))
.filter((value, index, self) => self.findIndex(({ id }) => id === value.id) === index);
const noResultsFound = newItems.length === 0 && Boolean(searchValue);
const isEmpty = newItems.length === 0 && !searchValue;
this.setState({
items: newItems,
selectedItems: newSelectedItems,
indeterminateItems: newIndeterminateItems,
isLoaded: true,
isEmpty,
isSearching: false,
totalCount,
noResultsFound,
});
};
this._loadInitialItems = (searchValue = '', { resetItems } = {}) => {
this._getInitialData(searchValue).then(dataSourceProps => {
return this._updateItems({
resetItems,
...dataSourceProps,
searchValue,
});
});
};
this._getInitialData = (searchValue = '') => {
const { dataSource, itemsPerPage } = this.props;
const initialAmountToLoad = this.props.initialAmountToLoad || itemsPerPage;
return dataSource(searchValue, 0, initialAmountToLoad);
};
this._loadMore = () => {
const { dataSource, itemsPerPage } = this.props;
const { items, searchValue } = this.state;
dataSource(searchValue, items.length, itemsPerPage).then(dataSourceProps => this._updateItems({
...dataSourceProps,
searchValue,
}));
};
this._getEnabledItems = items => items.filter(({ disabled }) => !disabled);
}
componentDidMount() {
this._loadInitialItems();
}
/** Resets list items and loads first page from dataSource while persisting searchValue */
reloadInitialItems() {
const searchValue = this.state.searchValue;
this.setState({
searchValue,
isSearching: Boolean(searchValue),
isLoaded: false,
});
this._loadInitialItems(searchValue, { resetItems: true });
}
render() {
const { children } = this.props;
const { selectedItems } = this.state;
if (typeof children === 'function') {
return children({
renderList: this._renderList,
renderToggleAllCheckbox: this._renderToggleAllCheckbox,
selectedItems,
});
}
return this._renderList();
}
_hasMore() {
const { items, isLoaded, totalCount, isSearching } = this.state;
return ((items.length === 0 && !isLoaded) ||
items.length < totalCount ||
isSearching);
}
}
SelectorList.displayName = 'SelectorList';
SelectorList.propTypes = {
/** Applies a data-hook HTML attribute to be used in the tests */
dataHook: PropTypes.string,
/**
* Returns data source for the list described in a following structure:
*
* ```typescript
* (searchQuery: string, offset: number, limit: number) =>
* Promise<{
* items: Array<{
* id: number | string, // sets the unique item ID (required)
* title: node, defines // an item’s title (required)
* subtitle?: string, // defines an item’s subtitle
* extraText?: string, // contains any text at the end of an item
* extraNode?: node, // contains any component at the end of an item
* disabled?: boolean, // controls if an item is disabled for selection or not
* selected?: boolean, // sets an item as selected
* indeterminate?: boolean, // sets an item as indeterminate
* image?: node, // contains <Image/> or other component to illustrate an item
* subtitleNode?: node, // contains any component below an item’s subtitle
* belowNode?: node, // contains any component below the item, to be shown after an item is selected
* showBelowNodeOnSelect?: boolean, // allows to show belowNode content when an item is selected
* }>,
* offset: number, // specifies the item index in the data source to start fetching from<br>
* limit: number, // sets a max amount of items to load from the data source<br>
* totalCount: number, // sets a max amount of items to load from the data source on user’s search query
* }>
* ```
* */
dataSource: PropTypes.func.isRequired,
/** Controls the size of component paddings and list items */
size: PropTypes.oneOf(['small', 'medium']),
/** Controls the size of item images. Note: `portrait` and `cinema` sizes are only compatible with `rectangular` image shape. */
imageSize: PropTypes.oneOf([
'tiny',
'small',
'portrait',
'large',
'cinema',
]),
/**
* Controls the shape of item images
* */
imageShape: PropTypes.oneOf(['rectangular', 'circle']),
/**
* Use to display a divider between items
*/
showDivider: PropTypes.bool,
/** Defines placeholder value shown in the search input */
searchPlaceholder: PropTypes.string,
/**
* Contains a component which is shown when there are no items to display in the selector list.
*
* i.e. empty `{items:[], totalCount: 0}` was returned on the first call to `dataSource`. Render `<EmptyState/>` component in `section` theme for this purpose.
* */
emptyState: PropTypes.node,
/**
* Defines a function that gets the current `searchQuery` and returns the component that is shown when no items are found. Render `<EmptyState />` component in `section` theme for this purpose.
* */
renderNoResults: PropTypes.func,
/** Sets the number of items to be loaded each time users scroll down to the end of the list */
itemsPerPage: PropTypes.number,
/** Controls whether to display the search input */
withSearch: PropTypes.bool,
/** Sets search debounce in milliseconds */
searchDebounceMs: PropTypes.number,
/** Sets the height of the component in % or px */
height: PropTypes.string,
/** Sets the maximum height of the component in % or px */
maxHeight: PropTypes.string,
/** Renders checkboxes instead of radio buttons and allows users to select multiple items */
multiple: PropTypes.bool,
/** Defines callback that triggers on select and return selected item object */
onSelect: PropTypes.func,
/** Sets the label for the checkbox which allows to select all items */
selectAllText: PropTypes.string,
/** Sets the label for the checkbox which allows to deselect all selected items */
deselectAllText: PropTypes.string,
/** Sets the number of items to load on initial render or after search. If not defined, it will be equal to `itemsPerPage` value. */
initialAmountToLoad: PropTypes.number,
/** Contains text or other component in a fixed position at the top of the list */
subtitle: PropTypes.node,
/** Displays a checkbox which allows to select and deselect all items at once */
renderToggleAllCheckbox: PropTypes.func,
};
SelectorList.defaultProps = {
searchPlaceholder: 'Search...',
size: 'medium',
imageShape: 'rectangular',
showDivider: false,
itemsPerPage: 50,
withSearch: true,
height: '100%',
maxHeight: '100%',
deselectAllText: 'Deselect all',
multiple: false,
selectAllText: 'Select all',
};
export default SelectorList;
//# sourceMappingURL=SelectorList.js.map