src/selectedList.js
import { findDOMNode } from 'react-dom'
import mouseMath from './mouseMath.js'
export default class selectList {
nodes = []
bounds = []
indices = {}
selectedIndices = []
removed = []
added = []
transaction = {
}
constructor() {
const self = this
this.accessor = {
nodes() {
return [...self.nodes]
},
node(idx) {
return self.nodes[idx]
},
nodeIndicesOfType(types) {
const mytypes = [].concat(types)
return self.nodes.filter(node => mytypes
.every(type => node.types.indexOf(type) !== -1))
.map(node => self.nodes.indexOf(node))
},
selectedIndices() {
return [...self.selectedIndices]
},
selectedNodeList() {
return self.selectedIndices.map(idx => self.nodes[idx].component)
},
selectedValueList() {
return self.selectedIndices.map(idx => self.nodes[idx].value)
},
selectedNodes() {
return self.selectedIndices.reduce((val, idx) => {
const ret = val
ret[self.nodes[idx].key] = {
node: self.nodes[idx].component,
bounds: self.bounds[idx]
}
return ret
}, {})
},
selectedValues() {
return self.selectedIndices.reduce((val, idx) => {
const ret = val
ret[self.nodes[idx].key] = self.nodes[idx].value
return ret
}, {})
}
}
}
setNodes(nodes) {
this.nodes = nodes
this.nodes.forEach((node, idx) => { this.indices[node.key] = idx })
}
begin(selectedIndices, props) {
this.transaction = {
previousSelection: [...selectedIndices],
mostRecentSelection: [...selectedIndices],
additionalSelectionMap: {},
firstNode: false
}
this.selectedIndices = []
this.removed = []
this.added = []
this.props = props
}
commit() {
this.transaction = {}
}
addItem(idx, selectedIndices = this.selectedIndices) {
if (!this.transaction.firstNode) {
this.transaction.firstNode = this.nodes[idx]
}
const si = selectedIndices
// determine how to insert the value prior to insertion sort
if (!si.length || idx > si[si.len - 1]) {
si.push(idx)
return
}
if (idx < si[0]) {
si.unshift(idx)
return
}
const len = si.length
// if the index is closer to one end than the other, start there
if (si[len - 1] - idx <= idx - si[0]) {
si.push(idx)
let curIdx = len
// insertion sort from end
while (curIdx >= 1 && si[curIdx - 1] > idx) {
si[curIdx] = si[curIdx - 1]
si[--curIdx] = idx
}
} else {
si.unshift(idx)
let curIdx = 0
// insertion sort from start
while (curIdx <= len && si[curIdx + 1] < idx) {
si[curIdx] = si[curIdx + 1]
si[++curIdx] = idx
}
}
}
removeItem(idx) {
const index = this.selectedIndices.indexOf(idx)
if (index === -1) return
this.selectedIndices.splice(index, 1)
}
selectItem(idx) {
// first check to see if this index is the same type as the first node selected
const node = this.nodes[idx]
if (!node.selectable) return
if (this.props.hasOwnProperty('acceptedTypes')) {
// by default we accept all types, this prop restricts types accepted
if (!this.props.acceptedTypes.reduce((last, type) =>
last || node.types.indexOf(type) !== -1, false)) {
return
}
}
if (this.transaction.firstNode) {
// does this node share any types in common with the first selected node?
if (!this.transaction.firstNode.types.reduce((last, type) =>
last || node.types.indexOf(type) !== -1, false)) {
// no
return
}
}
if (this.selectedIndices.indexOf(idx) !== -1) return
this.addItem(idx)
}
deselectItem(idx) {
this.removeItem(idx)
}
testNodes({ selectionRectangle, props, findit, mouse }, node, idx) {
let bounds
if (node.bounds) {
bounds = node.bounds
} else {
const domnode = findit(node.component)
bounds = domnode ? mouse.getBoundsForNode(domnode) : false
}
this.bounds[idx] = bounds
if (bounds && mouse.objectsCollide(selectionRectangle, bounds, this.clickTolerance, node.key)) {
// node is in the selection rectangle
this.selectItem(idx)
} else {
// node is not in the selection rectangle
this.deselectItem(idx)
}
}
changed(newSelected, prevSelected) {
return prevSelected.filter(idx => newSelected.indexOf(idx) === -1)
}
xor(newSelected, prevSelected) {
const ret = [...prevSelected]
newSelected.forEach(idx => (prevSelected.indexOf(idx) === -1 ?
this.addItem(idx, ret) :
ret.splice(ret.indexOf(idx), 1)))
return ret
}
or(newSelected, prevSelected) {
const ret = [...prevSelected]
newSelected.forEach(idx => prevSelected.indexOf(idx) === -1 ?
this.addItem(idx, ret) :
null)
return ret
}
selectItemsInRectangle(selectionRectangle, props, findit = findDOMNode, mouse = mouseMath) {
if (!this.transaction.previousSelection) {
// fail-safe
this.begin([])
}
this.selectedIndices = []
this.props = props
this.removed = []
this.added = []
// get a list of all nodes that are potential selects from the selection rectangle
this.nodes.forEach(this.testNodes.bind(this, { selectionRectangle, props, findit, mouse }))
// add the nodes that are logically selected in-between
const options = props.selectionOptions
if (options.inBetween && this.selectedIndices.length) {
const min = Math.min(...this.selectedIndices)
const max = Math.max(...this.selectedIndices)
const filled = Array.apply(min, Array(max - min)).map((x, y) => min + y + 1)
filled.unshift(min)
filled.forEach((idx) => this.selectItem(idx))
}
// for additive, we will use xor
if (options.additive) {
this.selectedIndices = this.xor(this.selectedIndices, this.transaction.previousSelection)
} else {
this.selectedIndices = this.or(this.selectedIndices, this.transaction.previousSelection)
}
const test = this.transaction.additionalSelectionMap[this.keyize(this.selectedIndices)]
if (test) {
this.selectedIndices = test
}
if (this.selectedIndices.length === this.transaction.mostRecentSelection.length) {
if (this.selectedIndices
.every((idx, i) => this.transaction.mostRecentSelection[i] === idx)) {
return false
}
}
this.removed = this.changed(this.selectedIndices, this.transaction.mostRecentSelection)
this.added = this.changed(this.transaction.mostRecentSelection, this.selectedIndices)
this.transaction.previousMostRecentSelection = [...this.transaction.mostRecentSelection]
this.transaction.mostRecentSelection = [...this.selectedIndices]
return true
}
notifyChangedNodes() {
this.removed.map(idx => (this.nodes[idx].callback ? this.nodes[idx].callback(false) : null))
this.added.map(idx => (this.nodes[idx].callback ? this.nodes[idx].callback(true) : null))
}
clear() {
this.added = []
this.removed = []
if (this.selectedIndices.length === 0) return false
this.selectedIndices.forEach(idx => this.nodes[idx].callback && this.nodes[idx].callback(false))
this.selectedIndices = []
return true
}
revert() {
const add = this.removed
const remove = this.added
add.forEach(idx => this.addItem(idx, this.selectedIndices))
remove.forEach(idx => this.removeItem(idx, this.selectedIndices))
}
keyize(indices) {
return indices.toString()
}
setSelection(indices) {
this.transaction.additionalSelectionMap[this.keyize(this.selectedIndices)] = indices
this.selectedIndices = [...indices]
this.removed = this.changed(this.selectedIndices, this.transaction.previousMostRecentSelection)
this.added = this.changed(this.transaction.previousMostRecentSelection, this.selectedIndices)
this.mostRecentSelection = [...indices]
}
}