Created
September 19, 2017 16:48
-
-
Save souporserious/8ab46fd0b68127002ee2c2c859758ad4 to your computer and use it in GitHub Desktop.
Loops through children to provide selected states
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function firstDefined(...args) { | |
return args.filter(a => typeof a !== 'undefined')[0] | |
} | |
function shallowEqual(a, b) { | |
return JSON.stringify(a) === JSON.stringify(b) | |
} | |
class Item extends PureComponent { | |
static displayName = 'ItemList.Item' | |
render() { | |
const { | |
children, | |
clearHighlightedItem, | |
deselect, | |
highlight, | |
index, | |
isHighlighted, | |
isSelected, | |
multiple, | |
select, | |
value, | |
...restProps | |
} = this.props | |
if (typeof children === 'function') { | |
return children({ | |
clearHighlightedItem, | |
deselect, | |
highlight, | |
index, | |
isHighlighted, | |
isSelected, | |
multiple, | |
select, | |
itemProps: restProps, | |
}) | |
} | |
return ( | |
<Menu.Item | |
{...restProps} | |
onClick={multiple ? (isSelected ? deselect : select) : select} | |
onMouseOver={highlight} | |
onMouseOut={clearHighlightedItem} | |
backgroundColor={isHighlighted && 'grey-2'} | |
> | |
{isSelected && <Icon name="checkmark" />} | |
{children} | |
</Menu.Item> | |
) | |
} | |
} | |
let itemListId = 0 | |
class ItemGroup extends Component { | |
static displayName = 'ItemGroup' | |
static defaultProps = { | |
channel: 'item-list', | |
component: Menu, | |
defaultHighlightedIndex: null, | |
defaultValue: [], | |
multiple: false, | |
value: null, | |
onChange: () => null, | |
} | |
state = { | |
value: this.props.value || this.props.defaultValue, | |
highlightedIndex: this.props.defaultHighlightedIndex, | |
} | |
id = `${this.props.channel}-${itemListId++}` | |
lastSelectedValue = null | |
componentWillMount() { | |
const { defaultHighlightedIndex, defaultValue } = this.props | |
if (defaultValue !== undefined && defaultHighlightedIndex === null) { | |
this.highlightItem(this.getItemIndex(defaultValue)) | |
} | |
} | |
componentWillReceiveProps(nextProps) { | |
if (!shallowEqual(this.props.value, nextProps.value)) { | |
this.setState({ value: nextProps.value }) | |
} | |
} | |
isUncontrolled() { | |
return this.props.value === null | |
} | |
isItemHighlighted = index => { | |
return this.state.highlightedIndex === index | |
} | |
isItemSelected = value => { | |
return Array.isArray(this.state.value) | |
? this.state.value.some(selectedValue => | |
shallowEqual(selectedValue, value) | |
) | |
: shallowEqual(this.state.value, value) | |
} | |
getItemIndex = value => { | |
let itemIndex = -1 | |
this.getItems().some((item, index) => { | |
if (shallowEqual(item.props.value, value)) { | |
itemIndex = index | |
return true | |
} | |
}) | |
return itemIndex | |
} | |
getSelectedItemIndex = value => { | |
const values = Array.isArray(this.state.value) | |
? this.state.value | |
: [this.state.value] | |
let itemIndex = -1 | |
values.some((_value, index) => { | |
if (shallowEqual(_value, value)) { | |
itemIndex = index | |
return true | |
} | |
}) | |
return itemIndex | |
} | |
getChildren(children = this.props.children) { | |
return typeof children === 'function' | |
? children(this.getControllerStateAndMethods()).props.children | |
: children | |
} | |
getItems = (children = this.getChildren()) => { | |
return Children.toArray(children).reduce((items, child) => { | |
if (child === undefined || child === null) { | |
return items | |
} | |
if (child.type && child.type.displayName.indexOf('Group') > -1) { | |
return [ | |
...items, | |
...this.getItems(this.getChildren(child.props.children)), | |
] | |
} else if (child.type && child.type.displayName.indexOf('Item') > -1) { | |
return [...items, child] | |
} else { | |
return items | |
} | |
}, []) | |
} | |
highlightItem = index => { | |
this.setState({ highlightedIndex: index }) | |
} | |
moveHighlightedItem = (amount = 0, contain = true) => { | |
const items = this.getItems() | |
let nextIndex = null | |
if (this.state.highlightedIndex === null) { | |
nextIndex = amount >= 0 ? amount - 1 : items.length + amount | |
} else { | |
items.forEach((item, index) => { | |
if (this.state.highlightedIndex === index) { | |
nextIndex = index + amount | |
} | |
}) | |
} | |
if (nextIndex >= items.length) { | |
nextIndex = contain ? 0 : null | |
} else if (nextIndex < 0) { | |
nextIndex = contain ? items.length - 1 : null | |
} | |
this.setState({ highlightedIndex: nextIndex }) | |
} | |
clearHighlightedItem = () => { | |
this.setState({ highlightedIndex: null }) | |
} | |
selectHighlightedItem = (toggle = this.props.multiple) => { | |
const items = this.getItems() | |
const item = items[this.state.highlightedIndex] | |
if (item) { | |
const value = firstDefined(item.props.value, this.state.highlightedIndex) | |
if (toggle && this.isItemSelected(value)) { | |
this.deselectItem(value) | |
} else { | |
this.selectItem(value) | |
} | |
} | |
} | |
selectItem = itemValue => { | |
let value = itemValue | |
if (this.props.multiple) { | |
value = Array.isArray(this.state.value) ? [...this.state.value] : [] | |
const position = this.getSelectedItemIndex(itemValue) | |
if (position > -1) { | |
value.splice(position, 1) | |
} | |
value.push(itemValue) | |
} | |
if (this.isUncontrolled()) { | |
this.setState({ value }) | |
} | |
this.lastSelectedValue = itemValue | |
this.props.onChange({ value, selected: true }) | |
} | |
deselectItem = itemValue => { | |
let value = '' | |
if (this.props.multiple) { | |
value = Array.isArray(this.state.value) ? [...this.state.value] : [] | |
const itemIndex = this.getSelectedItemIndex(itemValue) | |
if (itemIndex > -1) { | |
value.splice(itemIndex, 1) | |
} | |
} | |
if (this.isUncontrolled()) { | |
this.setState({ value }) | |
} | |
if (this.lastSelectedValue === itemValue) { | |
this.lastSelectedValue = null | |
} | |
this.props.onChange({ value, selected: false }) | |
} | |
moveSelectedItem = (amount = 0, deselectLastSelectedItem = true) => { | |
const items = this.getItems() | |
let nextIndex = -1 | |
let nextItem = null | |
if (this.lastSelectedValue === null) { | |
nextIndex = amount >= 0 ? amount - 1 : items.length + amount | |
nextItem = items[nextIndex] | |
} else { | |
items.some((item, index) => { | |
if (this.lastSelectedValue === firstDefined(item.props.value, index)) { | |
nextIndex = index + amount | |
nextItem = items[nextIndex] | |
return true | |
} | |
}) | |
} | |
if (this.lastSelectedValue && deselectLastSelectedItem) { | |
this.deselectItem(this.lastSelectedValue) | |
} | |
if (nextItem) { | |
this.selectItem(firstDefined(nextItem.props.value, nextIndex)) | |
} | |
} | |
deselectAllItems = () => { | |
const value = this.props.multiple ? [] : '' | |
if (this.isUncontrolled()) { | |
this.setState({ value }) | |
} | |
this.props.onChange({ | |
value, | |
selected: false, | |
}) | |
} | |
selectAllItems = () => { | |
const items = this.getItems() | |
const values = items.map((item, index) => | |
firstDefined(item.props.value, index) | |
) | |
const value = this.props.multiple ? values : values[0] | |
if (this.isUncontrolled()) { | |
this.setState({ value }) | |
} | |
this.props.onChange({ | |
value, | |
selected: true, | |
}) | |
} | |
handleItemSelect = value => () => { | |
this.selectItem(value) | |
} | |
handleItemDeselect = value => () => { | |
this.deselectItem(value) | |
} | |
handleItemHighlight = index => () => { | |
this.highlightItem(index) | |
} | |
handleItemGroupChange = child => e => { | |
if (this.isUncontrolled()) { | |
this.setState(({ value: previousValue }) => { | |
const value = Array.isArray(previousValue) | |
? [...previousValue] | |
: [previousValue] | |
if (Array.isArray(e.value)) { | |
e.value.forEach(v => { | |
const position = value.indexOf(v) | |
if (position > -1) { | |
value.splice(position, 1) | |
} | |
value.push(v) | |
}) | |
} else { | |
const position = value.indexOf(e.value) | |
if (position > -1) { | |
value.splice(position, 1) | |
} | |
value.push(e.value) | |
} | |
const changeEvent = { | |
value, | |
selected: true, | |
} | |
this.props.onChange(changeEvent) | |
child.props.onChange(changeEvent) | |
return { value } | |
}) | |
} | |
} | |
getControllerStateAndMethods = () => { | |
return this.state | |
} | |
render() { | |
const { | |
channel, | |
children, | |
component, | |
defaultHighlightedIndex, | |
defaultValue, | |
multiple, | |
value, | |
...restProps | |
} = this.props | |
let itemIndex = 0 | |
const cloneItems = children => { | |
const items = [] | |
Children.toArray(children).forEach(child => { | |
let newChild = child | |
if (child === undefined || child === null) { | |
return | |
} | |
if (child.type) { | |
if (child.type && child.type.displayName.indexOf('Group') > -1) { | |
newChild = cloneElement(child, { | |
multiple, | |
value: this.state.value, | |
onChange: this.handleItemGroupChange(child), | |
children: cloneItems(child.props.children, itemIndex), | |
}) | |
} else if ( | |
child.type.displayName.indexOf('Item') > -1 && | |
child.props.index === undefined | |
) { | |
const index = itemIndex++ | |
const value = firstDefined(child.props.value, index) | |
newChild = cloneElement(child, { | |
index, | |
multiple, | |
id: `${this.id}-item-${index}`, | |
clearHighlightedItem: this.clearHighlightedItem, | |
deselect: this.handleItemDeselect(value), | |
highlight: this.handleItemHighlight(index), | |
isHighlighted: this.isItemHighlighted(index), | |
isSelected: this.isItemSelected(value), | |
select: this.handleItemSelect(value), | |
}) | |
} | |
} | |
if (newChild !== undefined && newChild !== null) { | |
items.push(newChild) | |
} | |
}) | |
return items | |
} | |
if (typeof children === 'function') { | |
const wrapper = children(this.getControllerStateAndMethods()) | |
return cloneElement( | |
wrapper, | |
restProps, | |
cloneItems(wrapper.props.children) | |
) | |
} else { | |
return createElement(component, restProps, cloneItems(children)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
usage: