liquor-tree
Version:
A Vue.js tree component.
707 lines (539 loc) • 13.8 kB
JavaScript
import { recurseDown } from '../utils/recurse'
import find from '../utils/find'
import uuidV4 from '../utils/uuidV4'
import Selection from '../lib/Selection'
export default class Node {
constructor (tree, item) {
if (!item) {
throw new Error('Node can not be empty')
}
this.id = item.id || uuidV4()
this.states = item.state || {}
this.showChildren = true
this.children = item.children || []
this.parent = item.parent || null
this.isBatch = item.isBatch || false
this.isEditing = false
this.data = Object.assign({}, item.data || {}, {
text: item.text
})
if (!tree) {
throw new Error('Node must have a Tree context!')
}
this.tree = tree
}
$emit (evnt, ...args) {
this.tree.$emit(`node:${evnt}`, this, ...args)
}
getPath () {
if (!this.parent) {
return [this]
}
const path = [this]
let el = this
while ((el = el.parent) !== null) {
path.push(el)
}
return path
}
get key () {
return this.id + this.text
}
get depth () {
let depth = 0
let parent = this.parent
if (!parent || this.showChildren === false) {
return depth
}
do {
depth++
} while (parent = parent.parent)
return depth
}
get text () {
return this.data.text
}
set text (text) {
const oldText = this.text
if (oldText !== text) {
this.data.text = text
this.$emit('text:changed', text, oldText)
}
}
setData (data) {
this.data = Object.assign({}, this.data, data)
this.$emit('data:changed', this.data)
return this.data
}
state (name, value) {
if (undefined === value) {
return this.states[name]
}
// TODO: check if it for example `selectable` state it should unselect node
this.states[name] = value
return this
}
recurseUp (fn, node = this) {
if (!node.parent) {
return
}
if (fn(node.parent) !== false) {
return this.recurseUp(fn, node.parent)
}
}
recurseDown (fn, ignoreThis) {
if (ignoreThis !== true) {
fn(this)
}
if (this.hasChildren()) {
recurseDown(this.children, fn)
}
}
refreshIndeterminateState () {
if (!this.tree.options.autoCheckChildren) {
return this
}
this.state('indeterminate', false)
if (this.hasChildren()) {
const childrenCount = this.children.length
let checked = 0
let indeterminate = 0
let disabled = 0
this.children.forEach(child => {
if (child.checked()) {
checked++
}
if (child.disabled()) {
disabled++
}
if (child.indeterminate()) {
indeterminate++
}
})
if (checked > 0 && checked === childrenCount - disabled) {
if (!this.checked()) {
this.state('checked', true)
this.tree.check(this)
this.$emit('checked')
}
} else {
if (this.checked()) {
this.state('checked', false)
this.tree.uncheck(this)
this.$emit('unchecked')
}
this.state(
'indeterminate',
indeterminate > 0 || (checked > 0 && checked < childrenCount)
)
}
}
if (this.parent) {
this.parent.refreshIndeterminateState()
}
}
indeterminate () {
return this.state('indeterminate')
}
editable () {
return !this.state('disabled') && this.state('editable')
}
selectable () {
return !this.state('disabled') && this.state('selectable')
}
selected () {
return this.state('selected')
}
select (extendList) {
if (!this.selectable() || this.selected()) {
return this
}
this.tree.select(this, extendList)
this.state('selected', true)
this.$emit('selected')
return this
}
unselect () {
if (!this.selectable() || !this.selected()) {
return this
}
this.tree.unselect(this)
this.state('selected', false)
this.$emit('unselected')
return this
}
checked () {
return this.state('checked')
}
check () {
if (this.checked() || this.disabled()) {
return this
}
if (this.indeterminate()) {
return this.uncheck()
}
const checkDisabledChildren = this.tree.options.checkDisabledChildren
const targetNode = this
if (this.tree.options.autoCheckChildren) {
this.recurseDown(node => {
node.state('indeterminate', false)
if (node.disabled() && !checkDisabledChildren) {
return
}
if (!node.checked()) {
this.tree.check(node)
node.state('checked', true)
node.$emit('checked', node.id === targetNode.id ? undefined : targetNode)
}
})
if (this.parent) {
this.parent.refreshIndeterminateState()
}
} else {
this.tree.check(this)
this.state('checked', true)
this.$emit('checked')
}
return this
}
uncheck () {
if (!this.indeterminate() && !this.checked() || this.disabled()) {
return this
}
const targetNode = this
if (this.tree.options.autoCheckChildren) {
this.recurseDown(node => {
node.state('indeterminate', false)
if (node.checked()) {
this.tree.uncheck(node)
node.state('checked', false)
node.$emit('unchecked', node.id === targetNode.id ? undefined : targetNode)
}
})
if (this.parent) {
this.parent.refreshIndeterminateState()
}
} else {
this.tree.uncheck(this)
this.state('checked', false)
this.$emit('unchecked')
}
return this
}
show () {
if (this.visible()) {
return this
}
this.state('visible', true)
this.$emit('shown')
return this
}
hide () {
if (this.hidden()) {
return this
}
this.state('visible', false)
this.$emit('hidden')
return this
}
visible () {
return this.state('visible')
}
hidden () {
return !this.state('visible')
}
enable () {
if (this.enabled()) {
return this
}
if (this.tree.options.autoDisableChildren) {
this.recurseDown(node => {
if (node.disabled()) {
node.state('disabled', false)
node.$emit('enabled')
}
})
} else {
this.state('disabled', false)
this.$emit('enabled')
}
return this
}
enabled () {
return !this.state('disabled')
}
disable () {
if (this.disabled()) {
return this
}
if (this.tree.options.autoDisableChildren) {
this.recurseDown(node => {
if (node.enabled()) {
node.state('disabled', true)
node.$emit('disabled')
}
})
} else {
this.state('disabled', true)
this.$emit('disabled')
}
return this
}
disabled () {
return this.state('disabled')
}
expandTop (ignoreEvent) {
this.recurseUp(parent => {
parent.state('expanded', true)
if (ignoreEvent !== true) {
this.$emit('expanded', parent)
}
})
}
expand () {
if (!this.canExpand()) {
return this
}
if (this.isBatch) {
this.tree.loadChildren(this).then(_ => {
this.state('expanded', true)
this.$emit('expanded')
})
} else {
this.state('expanded', true)
this.$emit('expanded')
}
return this
}
canExpand () {
if (this.disabled() || !this.hasChildren()) {
return false
}
return this.collapsed() &&
(!this.tree.autoDisableChildren || this.disabled())
}
canCollapse () {
if (this.disabled() || !this.hasChildren()) {
return false
}
return this.expanded() &&
(!this.tree.autoDisableChildren || this.disabled())
}
expanded () {
return this.state('expanded')
}
collapse () {
if (!this.canCollapse()) {
return this
}
this.state('expanded', false)
this.$emit('collapsed')
return this
}
collapsed () {
return !this.state('expanded')
}
toggleExpand () {
return this._toggleOpenedState()
}
toggleCollapse () {
return this._toggleOpenedState()
}
_toggleOpenedState () {
if (this.canCollapse()) {
return this.collapse()
} else if (this.canExpand()) {
return this.expand()
}
}
isDropable () {
return this.enabled() && this.state('dropable')
}
isDraggable () {
return this.enabled() && this.state('draggable') && !this.isEditing
}
startDragging () {
if (!this.isDraggable() || this.state('dragging')) {
return false
}
// root element
if (this.isRoot() && this.tree.model.length === 1) {
return false
}
if (this.tree.options.store) {
this.tree.__silence = true
}
this.select()
this.state('dragging', true)
this.$emit('dragging:start')
this.tree.__silence = false
return true
}
finishDragging (destination, destinationPosition) {
if (!destination.isDropable() && destinationPosition === 'drag-on') {
return
}
const tree = this.tree
const clone = this.clone()
const parent = this.parent
clone.id = this.id
tree.__silence = true
this.remove()
if (destinationPosition === 'drag-on') {
tree.append(destination, clone)
} else if (destinationPosition === 'drag-below') {
tree.after(destination, clone)
} else if (destinationPosition === 'drag-above') {
tree.before(destination, clone)
}
destination.refreshIndeterminateState()
parent && parent.refreshIndeterminateState()
tree.__silence = false
clone.state('dragging', false)
this.state('dragging', false)
// need to call emit on the clone, because we need to have node.parent filled in the event listener
clone.$emit('dragging:finish', destination, destinationPosition)
if (clone.state('selected')) {
tree.selectedNodes.remove(this)
tree.selectedNodes.add(clone)
tree.vm.$set(this.state, 'selected', false)
tree.vm.$set(clone.state, 'selected', true)
}
if (this.tree.options.store) {
this.tree.vm.$emit('LIQUOR_NOISE')
}
}
startEditing () {
if (this.disabled()) {
return false
}
if (!this.isEditing) {
this.tree._editingNode = this
this.tree.activeElement = this
this.isEditing = true
this.$emit('editing:start')
}
}
stopEditing (newText) {
if (!this.isEditing) {
return
}
this.isEditing = false
this.tree._editingNode = null
this.tree.activeElement = null
const prevText = this.text
if (newText && newText !== false && this.text !== newText) {
this.text = newText
}
this.$emit('editing:stop', prevText)
}
index (verbose) {
return this.tree.index(this, verbose)
}
first () {
if (!this.hasChildren()) {
return null
}
return this.children[0]
}
last () {
if (!this.hasChildren()) {
return null
}
return this.children[this.children.length - 1]
}
next () {
return this.tree.nextNode(this)
}
prev () {
return this.tree.prevNode(this)
}
insertAt (node, index = this.children.length) {
if (!node) {
return
}
node = this.tree.objectToNode(node)
if (Array.isArray(node)) {
node
.reverse()
.map(n => this.insertAt(n, index))
return new Selection(this.tree, [...node])
}
node.parent = this
this.children.splice(
index, 0, node
)
if (node.disabled() && node.hasChildren()) {
node.recurseDown(child => {
child.state('disabled', true)
})
}
if (!this.isBatch) {
this.$emit('added', node)
}
return node
}
addChild (node) {
return this.insertAt(node)
}
append (node) {
return this.addChild(node)
}
prepend (node) {
return this.insertAt(node, 0)
}
before (node) {
return this.tree.before(this, node)
}
after (node) {
return this.tree.after(this, node)
}
empty () {
let node
while (node = this.children.pop()) {
node.remove()
}
return this
}
remove () {
return this.tree.removeNode(this)
}
removeChild (criteria) {
const node = this.find(criteria)
if (node) {
return this.tree.removeNode(node)
}
return null
}
find (criteria, deep) {
if (this.tree.isNode(criteria)) {
return criteria
}
return find(this.children, criteria, deep)
}
focus () {
if (this.vm) {
this.vm.focus()
}
}
hasChildren () {
return this.showChildren && this.isBatch || this.children.length > 0
}
/**
* Sometimes it's no need to have a parent. It possible to have more than 1 parent
*/
isRoot () {
return this.parent === null
}
clone () {
return this.tree.objectToNode(this.toJSON())
}
toJSON () {
return {
text: this.text,
data: this.data,
state: this.states,
children: this.children.map(node => this.tree.objectToNode(node).toJSON())
}
}
}