Skip to content

Instantly share code, notes, and snippets.

@souporserious
Last active August 27, 2017 05:28
Show Gist options
  • Save souporserious/3a52bdacf1c12b20518020244cc5c6e1 to your computer and use it in GitHub Desktop.
Save souporserious/3a52bdacf1c12b20518020244cc5c6e1 to your computer and use it in GitHub Desktop.
class HighlightItems extends Component {
render() {
const { items } = this.props
return (
<ItemGroup items={items}>
{({
selectItem,
selectNextItem,
selectPreviousItem,
deselectItem,
isItemSelected,
}) =>
<Box>
<Input
defaultValue=""
onKeyDown={e => {
if (e.key === 'ArrowUp') {
e.preventDefault()
selectPreviousItem()
}
if (e.key === 'ArrowDown') {
e.preventDefault()
selectNextItem()
}
}}
/>
{items.map(item =>
<Box
key={item}
onMouseEnter={() => selectItem(item)}
onMouseLeave={() => deselectItem(item)}
backgroundColor={
isItemSelected(item) ? 'orange' : 'transparent'
}
>
{item}
</Box>
)}
</Box>}
</ItemGroup>
)
}
}
class ItemGroup extends Component {
static defaultProps = {
defaultSelectedItems: [],
selectedItems: [],
items: [],
onSelect: () => null,
}
static childContextTypes = {
itemGroup: PropTypes.object,
}
state = {
items: this.props.items,
selectedItems: this.props.defaultSelectedItems,
}
getChildContext() {
return {
itemGroup: this.getControllerStateAndMethods(),
}
}
getControllerStateAndMethods() {
return {
addItem: this.addItem,
removeItem: this.removeItem,
deselectAllItems: this.deselectAllItems,
deselectItem: this.deselectItem,
fillToItem: this.fillToItem,
fillPreviousItem: this.fillPreviousItem,
fillNextItem: this.fillNextItem,
getItemIndex: this.getItemIndex,
getNextItem: this.getNextItem,
getPreviousItem: this.getPreviousItem,
isItemSelected: this.isItemSelected,
selectAllItems: this.selectAllItems,
selectItem: this.selectItem,
selectFirstItem: this.selectFirstItem,
selectLastItem: this.selectLastItem,
selectNextItem: this.selectNextItem,
selectPreviousItem: this.selectPreviousItem,
toggleItem: this.toggleItem,
}
}
addItem = item => {
this.setState(({ items }) => {
const newItems = [...items]
const itemPosition = items.indexOf(item)
if (itemPosition > -1) {
newItems.splice(itemPosition, 1)
}
newItems.push(item)
return { items: newItems }
})
}
removeItem = item => {
this.setState(({ items }) => {
const newItems = [...items]
const itemPosition = items.indexOf(item)
if (itemPosition > -1) {
newItems.splice(itemPosition, 1)
}
return { items: newItems }
})
}
getPreviousItem = ({ contain, wrap } = {}) => {
const activeIndex = this.getItemIndex(this.lastSelectedItem)
let targetIndex = activeIndex
if (activeIndex === -1) {
targetIndex = this.state.items.length - 1
} else if (activeIndex > 0) {
targetIndex = activeIndex - 1
} else if (!contain) {
targetIndex = null
} else if (wrap) {
targetIndex = this.state.items.length - 1
}
return this.state.items[targetIndex]
}
getNextItem = ({ contain, wrap } = {}) => {
const activeIndex = this.getItemIndex(this.lastSelectedItem)
let targetIndex = activeIndex
if (activeIndex < this.state.items.length - 1) {
targetIndex = activeIndex + 1
} else if (!contain) {
targetIndex = null
} else if (wrap) {
targetIndex = 0
}
return this.state.items[targetIndex]
}
getItemIndex = item => {
return this.state.items.indexOf(item)
}
isItemSelected = item => {
return this.state.selectedItems.indexOf(item) > -1
}
selectItem = (item, { merge } = {}) => {
this.setState(({ selectedItems }) => {
if (merge) {
const itemPosition = selectedItems.indexOf(item)
const newSelectedItems = [...selectedItems]
if (itemPosition > -1) {
newSelectedItems.splice(itemPosition, 1)
}
return { selectedItems: [...newSelectedItems, item] }
} else {
return { selectedItems: [item] }
}
})
this.lastSelectedItem = item
}
deselectItem = item => {
this.setState(({ selectedItems }) => {
const itemPosition = selectedItems.indexOf(item)
const newSelectedItems = [...selectedItems]
if (itemPosition > -1) {
newSelectedItems.splice(itemPosition, 1)
}
return { selectedItems: newSelectedItems }
})
if (this.lastSelectedItem === item) {
this.lastSelectedItem = null
}
}
toggleItem = (item, { merge } = {}) => {
if (this.isItemSelected(item)) {
this.deselectItem(item, { merge })
} else {
this.selectItem(item, { merge })
}
}
fillToItem = item => {
this.setState(({ items, selectedItems }) => {
const startIndex = this.getItemIndex(selectedItems[0])
const selectedIndex = this.getItemIndex(item)
const lowEnd = startIndex > selectedIndex ? selectedIndex : startIndex
const highEnd = startIndex > selectedIndex ? startIndex : selectedIndex
const newSelectedItems = []
for (var i = lowEnd; i <= highEnd; i++) {
newSelectedItems.push(items[i])
}
if (startIndex > selectedIndex) {
newSelectedItems.reverse()
}
return {
selectedItems: newSelectedItems,
}
})
this.lastSelectedItem = item
}
selectPreviousItem = ({ contain, wrap } = {}) => {
this.selectItem(this.getPreviousItem({ contain, wrap }))
}
fillPreviousItem = ({ contain, wrap } = {}) => {
this.fillToItem(this.getPreviousItem({ contain, wrap }))
}
selectNextItem = ({ contain, wrap } = {}) => {
this.selectItem(this.getNextItem({ contain, wrap }))
}
fillNextItem = ({ contain, wrap } = {}) => {
this.fillToItem(this.getNextItem({ contain, wrap }))
}
selectFirstItem = ({ merge } = {}) => {
this.selectItem(this.state.items[0], { merge })
}
selectLastItem = ({ merge } = {}) => {
this.selectItem(this.state.items[this.state.items.length - 1], { merge })
}
selectAllItems = () => {
this.setState({ selectedItems: this.props.items })
}
deselectAllItems = () => {
this.setState({ selectedItems: [] })
this.lastSelectedItem = null
}
render() {
return this.props.children(this.getControllerStateAndMethods())
}
}
class ItemsList extends Component {
handleKeyDown = e => {
if (e.key === 'ArrowUp') {
e.preventDefault()
if (e.metaKey) {
this.itemGroup.selectFirstItem()
} else {
if (e.shiftKey) {
this.itemGroup.fillPreviousItem()
} else {
this.itemGroup.selectPreviousItem()
}
}
}
if (e.key === 'ArrowDown') {
e.preventDefault()
if (e.metaKey) {
this.itemGroup.selectLastItem()
} else {
if (e.shiftKey) {
this.itemGroup.fillNextItem()
} else {
this.itemGroup.selectNextItem()
}
}
}
if (e.key === 'Enter') {
e.preventDefault()
console.log('selected: ', this.itemGroup.state.selectedItems)
}
this.metaKey = e.metaKey
this.shiftKey = e.shiftKey
}
handleKeyUp = () => {
this.metaKey = false
this.shiftKey = false
}
handleItemSelect = item => {
if (this.shiftKey) {
this.itemGroup.fillToItem(item)
} else if (this.metaKey) {
if (this.itemGroup.isItemSelected(item)) {
this.itemGroup.deselectItem(item, { merge: true })
} else {
this.itemGroup.selectItem(item, { merge: true })
}
} else {
this.itemGroup.deselectAllItems()
this.itemGroup.selectItem(item)
}
}
render() {
const { items } = this.props
return (
<ItemGroup ref={c => (this.itemGroup = c)} items={items}>
{({
deselectAllItems,
isItemSelected,
selectAllItems,
selectNextItem,
selectPreviousItem,
}) =>
<div
tabIndex={0}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
>
{items.map(item =>
<div
key={item.value}
onClick={e => this.handleItemSelect(item)}
style={{ color: isItemSelected(item) ? 'orange' : '#000' }}
>
{item.label}
</div>
)}
<button onClick={() => selectAllItems()}>Select All</button>
<button onClick={() => deselectAllItems()}>Deselect All</button>
<button onClick={() => selectPreviousItem()}>Prev</button>
<button onClick={() => selectNextItem()}>Next</button>
</div>}
</ItemGroup>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment