react-box-selection
Version:
React component for making box selections on elements
241 lines (202 loc) • 7.31 kB
JSX
import React from 'react'
import PropTypes from 'prop-types'
import './Selection.css'
import { getTop, getLeft, clearEventBubble, scroll, isInPath, getParents } from '../utils/utils'
class Selection extends React.Component {
static propTypes = {
cols: PropTypes.number,
rows: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
gap: PropTypes.number,
wrapperScroll: PropTypes.object,
itemClass: PropTypes.string,
extraClass: PropTypes.array,
activeClass: PropTypes.string,
onMounted: PropTypes.func,
onHovered: PropTypes.func,
onLeaved: PropTypes.func,
onSelected: PropTypes.func,
onSingleSelected: PropTypes.func,
}
constructor(props) {
super(props)
this.state = {
cols: props.cols || 24,
rows: props.rows || 7,
width: props.width || 30,
height: props.height || 50,
margin: props.gap || 0,
isMouseDown: false,
templates: [...Array(props.rows || 7)].map(item => [...Array(props.cols || 24).keys()]),
allItems: [],
selectedItems: [],
selectedPostions: [],
selectEle: {
width: 0,
height: 0,
top: 0,
left: 0,
display: 'none'
}
}
this.selectionTable = null
}
addActiveClass = target => {
const { activeClass } = this.props
target.classList.add(activeClass || 'selection_item_active')
}
removeActiveClass = target => {
const { activeClass } = this.props
target.classList.remove(activeClass || 'selection_item_active')
}
componentDidMount () {
const { onMounted } = this.props
const table = this.selectionTable
const allItems = [...table.getElementsByTagName('td')]
onMounted && onMounted(allItems)
this.setState({ allItems })
}
componentWillUnmount () {
this.setState({ allItems: null, selectedItems: null })
this.setState = () => { }
}
down = e => {
clearEventBubble(e)
const { selectEle } = this.state
const { wrapperScroll } = this.props
const { clientX, clientY } = e
const top = getTop(e.currentTarget)
const left = getLeft(e.currentTarget)
const { scrollTop, scrollLeft } = wrapperScroll || scroll()
const startX = clientX - left + scrollLeft
const startY = clientY - top + scrollTop
selectEle.left = startX
selectEle.top = startY
this.setState({ isMouseDown: true, startX, startY, selectEle })
}
move = e => {
clearEventBubble(e)
const { isMouseDown, selectEle, startX, startY, selectedItems, selectedPostions } = this.state
const { wrapperScroll } = this.props
if (!isMouseDown) return
const top = getTop(e.currentTarget)
const left = getLeft(e.currentTarget)
const { scrollTop, scrollLeft } = wrapperScroll || scroll()
const { clientX, clientY } = e
const _x = clientX - left + scrollLeft
const _y = clientY - top + scrollTop
selectEle.left = _x > startX ? startX - 1 : _x + 1
selectEle.top = _y > startY ? startY - 1 : _y + 1
selectEle.width = Math.abs(_x - startX)
selectEle.height = Math.abs(_y - startY)
selectEle.display = 'block'
const items = [...e.currentTarget.getElementsByTagName('td')]
selectedPostions.length = 0
selectedItems.length = 0
items.forEach(item => {
this.removeActiveClass(item)
if (isInPath(item, selectEle)) {
this.addActiveClass(item)
selectedPostions.push(item.dataset['position'])
selectedItems.push(item)
}
})
this.setState({ selectEle, selectedItems, selectedPostions })
}
up = e => {
clearEventBubble(e)
const { allItems, selectEle, selectedPostions, selectedItems } = this.state
const { onSelected, onSingleSelected } = this.props
const target = e.target.tagName === 'TD' ? e.target : getParents(e.target, 'TD')
selectEle.left = 0
selectEle.top = 0
selectEle.width = 0
selectEle.height = 0
selectEle.display = 'none'
allItems.forEach(item => {
this.removeActiveClass(item)
})
if (selectedPostions.length === 0) {
onSingleSelected && onSingleSelected(target.dataset['position'], target, allItems)
} else if (selectedPostions.length === 1) {
onSingleSelected && onSingleSelected(selectedPostions[0], selectedItems, allItems)
} else {
onSelected && onSelected(selectedPostions, selectedItems, allItems)
}
this.setState({ isMouseDown: false, selectEle, selectedPostions: [] })
}
over = e => {
clearEventBubble(e)
const { onHovered } = this.props
const target = e.target.tagName === 'TD' ? e.target : getParents(e.target, 'TD')
this.addActiveClass(target)
onHovered && onHovered(target.dataset['position'], target)
}
leave = e => {
clearEventBubble(e)
const { isMouseDown } = this.state
const { onLeaved } = this.props
const target = e.target.tagName === 'TD' ? e.target : getParents(e.target, 'TD')
!isMouseDown && this.removeActiveClass(target)
onLeaved && onLeaved(target.dataset['position'], target)
}
render () {
const { templates, selectEle, width, height, cols, rows, margin } = this.state
const { children, itemClass, extraClass } = this.props
const self = this
return (
<div
className='selection_wrapper'
style={{
width: cols * width + margin * (1 + cols),
height: rows * height + margin * (1 + rows),
}}
onMouseDown={this.down}
onMouseMove={this.move}
onMouseUp={this.up}
>
<div className='selection_element' style={{ ...selectEle }} />
<table
className='selection_content'
border="1"
cellSpacing="0"
cellPadding="0"
ref={ele => this.selectionTable = ele}
>
<tbody>
{
templates.map(function (rows, row) {
return (
<tr key={`row-${row}`}>
{
rows.map(function (col) {
const className = `${itemClass || 'selection_item'} ${extraClass ? extraClass.join(' ') : ''}`
return (
<td
onMouseOver={self.over}
onMouseLeave={self.leave}
onClick={self.single}
className={className}
style={{ width, height, margin: `${margin}px ${margin}px 0 ${margin}px` }}
key={`col-${col}`}
data-position={`${row}-${col}`}
>
{
(children && children[row]) ? children[row][col] || '' : ''
}
</td>
)
})
}
</tr>
)
})
}
</tbody>
</table>
</div>
)
}
}
export default Selection