wix-style-react
Version:
318 lines (295 loc) • 12 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
export const BulkSelectionContext = React.createContext();
export const BulkSelectionState = Object.freeze({
ALL: 'ALL',
NONE: 'NONE',
SOME: 'SOME',
});
export const ChangeType = Object.freeze({
ALL: 'ALL',
NONE: 'NONE',
SINGLE_TOGGLE: 'SINGLE_TOGGLE',
});
/** Helper for PropTypes for component which consume the BulkSelection context */
export const BulkSelectionContextPropTypes = {
isSelected: PropTypes.func,
selectedCount: PropTypes.number,
getSelectedIds: PropTypes.func,
getNotSelectedIds: PropTypes.func,
infiniteBulkSelected: PropTypes.bool,
bulkSelectionState: PropTypes.string,
toggleSelectionById: PropTypes.func,
deselectRowsByDefault: PropTypes.bool,
toggleAll: PropTypes.func,
selectAll: PropTypes.func,
deselectAll: PropTypes.func,
setSelectedIds: PropTypes.func,
selectionDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
};
/**
* BulkSelection manages the state and logic of bulk selection.
* Given an array of selectable items, it manages a bulk selection state (ALL, SOME, NONE),
* and provides helper methods for modifying the state.
*
* toggleBulkSelection(): changes the bulk state according to these state changes: ALL->NONE, SOME->ALL, NONE->ALL
*/
export class BulkSelection extends React.Component {
constructor(props) {
super(props);
const selectedIds = (props.selectedIds || []).slice();
const notSelectedIds = null;
this.state = {
selectedIds, // not exposed to context consumers
notSelectedIds, // not exposed to context consumers
helpers: this.createHelpers({ ...props, selectedIds, notSelectedIds }),
};
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (
nextProps.selectedIds &&
!this.areSelectedIdsEqual(nextProps.selectedIds, this.state.selectedIds)
) {
this.setSelectedIds(nextProps.selectedIds.slice(), undefined, nextProps);
} else if (
this.state.selectedIds &&
this.state.helpers.bulkSelectionState === BulkSelectionState.ALL &&
!this.areSelectedIdsEqual(nextProps.allIds, this.props.allIds)
) {
// change bulkSelectionState after load more
this.setSelectedIds(this.state.selectedIds, undefined, nextProps);
} else if (this.state.notSelectedIds && !nextProps.hasMoreInBulkSelection) {
// cancel infinite bulk selection mode if it is no longer relevant (e.g. when done loading)
const selectedIds = nextProps.allIds.filter(
id => !this.state.notSelectedIds.includes(id),
);
this.setSelectedIds(selectedIds, undefined, nextProps);
} else if (
this.props.selectionDisabled !== nextProps.selectionDisabled ||
!this.areSelectedIdsEqual(this.props.allIds, nextProps.allIds)
) {
const { selectedIds, notSelectedIds } = this.state;
const nextSelectedIds =
selectedIds && selectedIds.filter(id => nextProps.allIds.includes(id));
const nextNotSelectedIds =
notSelectedIds &&
notSelectedIds.filter(id => nextProps.allIds.includes(id));
this.setState({
selectedIds: nextSelectedIds,
notSelectedIds: nextNotSelectedIds,
helpers: this.createHelpers({
...nextProps,
selectedIds: nextSelectedIds,
notSelectedIds: nextNotSelectedIds,
}),
});
}
}
toggleAll = (enable, origin) => {
if (enable) {
if (this.props.hasMoreInBulkSelection) {
this.setNotSelectedIds([], { type: ChangeType.ALL, origin });
} else {
this.setSelectedIds(this.props.allIds, {
type: ChangeType.ALL,
origin,
});
}
} else {
this.setSelectedIds([], { type: ChangeType.NONE, origin });
}
};
toggleBulkSelection = (deselectRowsByDefault, origin) => {
const bulkSelectionState = this.state.helpers.bulkSelectionState;
if (bulkSelectionState === BulkSelectionState.SOME) {
this.toggleAll(!deselectRowsByDefault, origin);
} else if (bulkSelectionState === BulkSelectionState.ALL) {
this.toggleAll(false, origin);
} else {
this.toggleAll(true, origin);
}
};
toggleSelectionById = (id, origin) => {
const newSelectionValue = !this.state.helpers.isSelected(id);
const change = {
type: ChangeType.SINGLE_TOGGLE,
id,
value: newSelectionValue,
origin,
};
if (this.state.selectedIds) {
this.setSelectedIds(
newSelectionValue
? this.state.selectedIds.concat(id)
: this.state.selectedIds.filter(_id => _id !== id),
change,
);
} else {
this.setNotSelectedIds(
newSelectionValue
? this.state.notSelectedIds.filter(_id => _id !== id)
: this.state.notSelectedIds.concat(id),
change,
);
}
};
setSelectedIds = (selectedIds, change, props) => {
if (!Array.isArray(selectedIds)) {
throw new Error('selectedIds must be an array');
}
if (!props) {
props = this.props;
}
const notSelectedIds = null;
this.setState(
{
selectedIds,
notSelectedIds,
helpers: this.createHelpers({ ...props, selectedIds, notSelectedIds }),
},
() => {
this.props.onSelectionChanged &&
this.props.onSelectionChanged(selectedIds.slice(), change);
},
);
};
setNotSelectedIds = (notSelectedIds, change, props) => {
if (!Array.isArray(notSelectedIds)) {
throw new Error('notSelectedIds must be an array');
}
if (!props) {
props = this.props;
}
const selectedIds = null;
this.setState(
{
selectedIds,
notSelectedIds,
helpers: this.createHelpers({ ...props, selectedIds, notSelectedIds }),
},
() => {
this.props.onSelectionChanged &&
this.props.onSelectionChanged(null, change);
},
);
};
areSelectedIdsEqual = (selectedIds1, selectedIds2) => {
if (selectedIds1 === selectedIds2) {
return true;
}
return (
Array.isArray(selectedIds1) &&
Array.isArray(selectedIds2) &&
selectedIds1.length === selectedIds2.length &&
selectedIds1.every((item, index) => item === selectedIds2[index])
);
};
createHelpers({
selectedIds,
notSelectedIds,
allIds,
selectionDisabled,
deselectRowsByDefault,
totalCount = 0,
}) {
const selectedCount = selectedIds
? selectedIds.length
: totalCount - notSelectedIds.length;
const selectedIdsBulkState =
selectedCount === 0
? BulkSelectionState.NONE
: selectedCount === allIds.length
? BulkSelectionState.ALL
: BulkSelectionState.SOME;
const notSelectedIdsBulkState =
notSelectedIds && notSelectedIds.length === 0
? BulkSelectionState.ALL
: BulkSelectionState.SOME;
const bulkSelectionState = selectedIds
? selectedIdsBulkState
: notSelectedIdsBulkState;
return {
// Getters
/** Is the item with the given id selected. (id comes from the rowData.id if exists, if not then it is the rowIndex)
* Note: `selectedIds` and `notSelectedIds` are mutually exclusive and only one of them is defined.
* `notSelectedIds` is defined when `hasMoreInBulkSelection` is selected and user did bulk selection. Otherwise, selectedIds is defined. */
isSelected: id =>
selectedIds ? selectedIds.includes(id) : !notSelectedIds.includes(id),
/** Number of selected items */
selectedCount,
/** Get a copy (array) of selected ids when `infiniteBulkSelected` is `false`.
* If `infiniteBulkSelected` is true, returns `null` */
getSelectedIds: () => selectedIds && selectedIds.slice(),
/** Get a copy (array) of ids that were deselected after bulk selection was done, when `infiniteBulkSelected` is `true`.
* If `infiniteBulkSelected` is `false`, returns `null`. */
getNotSelectedIds: () => notSelectedIds && notSelectedIds.slice(),
/** Indicates whether bulk selection was done by the user and `hasMoreInBulkSelection` is `true` */
infiniteBulkSelected: selectedIds === null,
/** A string representing the BulkSelection state (not a React state).
* Possible values: ALL, SOME, NONE
*/
bulkSelectionState,
/** Indicates the `toggleAll` behaviour when some rows are selected. `true` means SOME -> NONE, `false` means SOME -> ALL */
deselectRowsByDefault,
/** Can be either a boolean or a function.
* A boolean affects selection of all table rows.
* A function will be called for every row in `data` to specify if its checkbox should be disabled. */
selectionDisabled:
selectionDisabled === true ||
allIds.length === 0 ||
(typeof selectionDisabled === 'function' && selectionDisabled),
// Modifiers
/** Toggle the selection state (selected/not-selected) of an item by id */
toggleSelectionById: this.toggleSelectionById,
/** Toggles the bulk selection state: NONE -> ALL, SOME -> ALL, ALL -> NONE */
toggleAll: this.toggleBulkSelection,
/** Select all items */
selectAll: origin => this.toggleAll(true, origin),
/** Deselect all items (clear selection) */
deselectAll: origin => this.toggleAll(false, origin),
/** Set the selection.
* An optional `change` argument will be passed "as is" to the Table's onSelectionChanged callback.
*/
setSelectedIds: this.setSelectedIds,
};
}
render() {
return (
<BulkSelectionContext.Provider value={this.state.helpers}>
{this.props.children}
</BulkSelectionContext.Provider>
);
}
}
BulkSelection.propTypes = {
/** Array of item selection boolean states. Should correspond in length to the data prop */
selectedIds: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(PropTypes.number),
]),
/** An array of all selectable item ids (string ids) */
allIds: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(PropTypes.number),
]).isRequired,
/** Called when item selection changes.
* Receives 2 arguments, the updated selectedIds array, and a `change` object.
* The `change` object has a `type` property with the following possible values: 'ALL', 'NONE', 'SINGLE_TOGGLE'.
* In case of 'SINGLE_TOGGLE' the `change` object will also include an `id` prop with the item's id,
* and a `value` prop with the new boolean selection state of the item.
* The `change` object also contains an `origin` property which indicates what initiated the selection change.
* The `origin` property can be set when selection is updated using a `SelectionContext` method.
* In case `totalSelectableCount` is set and the list is not fully loaded, and the user did bulk selection ("Select All"), the first parameter (selectedIds) will be null.
* You can use the selection context's getNotSelectedIds() method to get the items that the user unselected after selecting all items. */
onSelectionChanged: PropTypes.func,
/** Can be either a boolean or a function.
* If passed a boolean, affects selection for all table rows.
* If passed a function, it will be called for every row in `data` to specify if its checkbox should be disabled. Example: `isRowSelectionDisabled={(rowData) => !rowData.isSelectable}` */
selectionDisabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
/** Indicates whether the table is in infinite bulk selection mode (`infiniteScroll` and `totalSelectableCount` props are set) and there are more items to load (`hasMore` prop is `true`) */
hasMoreInBulkSelection: PropTypes.bool,
/** The table's `totalSelectableCount` prop */
totalCount: PropTypes.number,
/** Any - can consume the BulkSelectionProvider context */
children: PropTypes.any,
};