tapspace
Version:
A zoomable user interface lib for web apps
440 lines (378 loc) • 11.1 kB
JavaScript
//
// AbstractNode
//
// Emits
// childAdded
// after a descendant has been added
// childRemoved
// after a descendant has been removed
// added
// after added to new parent
// removed
// after detached from parent
//
// 2018-01-18
// Changes to 2017-12-00 dev decision.
//
// Events about child node addition or removal are no longer emitted
// by all ancestors. The parent of the new node is the only one to
// emit childAdded and childRemoved. The new refactored SpaceView
// listens the elements directly instead of the root, Space. Therefore
// the bubbling of childAdded and childRemoved events is no longer
// needed.
//
// 2017-12-00
// Dev decision about listening events:
//
// Do not listen externally accessible events internally.
// Use recursive function calls instead.
// There is a couple of reasons for this.
// First, chains of event handlers are somewhat hard to debug.
// It gets harder when an event has multiple handlers.
// Second, the execution order of external and internal handlers
// is unspecified. If same events are listened both internally and
// externally, execution-order related bugs will happen and
// those are very hard to debug.
//
// Example:
// The node emits 'removed' after being detached from parent.
// In v3, the parent listened for this event and handled it
// by emitting 'childRemoved'. The parent also listened
// the child's 'childRemoved' and handled it by echoing.
// The goal was to let SpaceViews react to removals by
// just listening to Space. This caused a bug in setParent
// with non-trivial node hierarchy. Debugging it required
// following complex event chains and there were suprising
// handlers along the way. The bug was impossible to find.
//
var Emitter = require('component-emitter')
var extend = require('extend')
// Unique ID generator. Unique over session.
// Usage: seqid.next()
// Return: int
var seqid = require('seqid')(0)
var AbstractNode = function () {
Emitter.call(this)
// Each node has an id. That is used by the parent nodes and in views.
this.id = seqid.next().toString()
// Nodes with null parent are root nodes i.e. spaces.
// AbstractNode#remove sets _parent to null.
this._parent = null
// Keep track on node siblings in two-way linked list manner.
this._prevSibling = null
this._nextSibling = null
// Dict because good key search time complexity
this._children = {}
// List for children order
this._order = []
}
var p = extend({}, Emitter.prototype)
AbstractNode.prototype = p
p.addChild = function (child, i) {
// Add the given AbstractNode as a child and remove it from its old parent.
child.setParent(this, i)
return this
}
p.bringAbove = function (sibling) {
// Remove this node from the old parent and add to a new parent so that
// the given node becomes the next sibling of the node.
//
if (sibling.isRoot()) {
throw new Error('Cannot send after a root node.')
}
var newParent = sibling._parent
var index = newParent._order.indexOf(sibling)
// Index is off by 1 if same parent and the item is moved toward back.
// Smaller or EQUAL to is needed to ensure correct position if indices equal.
var oldParent = this._parent
if (oldParent === newParent) {
if (oldParent._order.indexOf(this) <= index) {
index = index - 1
}
}
this.setParent(sibling._parent, index + 1)
}
p.bringToFront = function () {
// Remove this node and reinsert it as the last child.
//
if (this.isRoot()) {
// Already back
return
}
var index = this._parent._order.length
this.setParent(this._parent, index)
}
p.getAncestors = function () {
// Return an array of AbstractNodes, _parent at [0], _parent._parent at [1],
// and so on.
//
var pa = this._parent
var ancs = []
while (pa !== null) {
ancs.push(pa)
pa = pa._parent
}
return ancs
}
p.getChildren = function () {
// Return child AbstractNodes in a list.
// Does not include the children of the children.
//
// Return
// Array
//
return this._order.slice() // copy
}
p.getDescendants = function () {
// All descendants in a list, including the children.
//
var i, children, child, arr
arr = []
children = this.getChildren()
for (i = 0; i < children.length; i += 1) {
child = children[i]
arr = arr.concat(child, child.getDescendants())
}
return arr
}
p.getFirstChild = function () {
if (this._order.length < 1) {
return null
}
return this._order[0]
}
p.getLastChild = function () {
if (this._order.length < 1) {
return null
}
return this._order[this._order.length - 1]
}
p.getNextSibling = function () {
return this._nextSibling
}
p.getParent = function () {
return this._parent
}
p.getPreviousSibling = function () {
return this._prevSibling
}
p.getRootParent = function () {
// Get the predecessor without parents in recursive manner.
if (this._parent === null) {
return this
} // else
return this._parent.getRootParent()
}
p.hasChild = function (abstractNode) {
// Return
// true if abstractNode is a child of this.
return abstractNode._parent === this
}
p.hasDescendant = function (abstractNode) {
// Return
// true if abstractNode is a descendant of this.
//
var p = abstractNode._parent
while (p !== null && p !== this) {
p = p._parent
}
if (p === null) {
return false
} // else
return true
}
p.isRoot = function () {
return this._parent === null
}
p.remove = function () {
// Remove this space node from its parent.
// Return: see setParent
return this.setParent(null)
}
p.sendBelow = function (sibling) {
// Remove this node from the old parent and add to a new parent so that
// the given node becomes the next sibling of the node.
//
if (sibling.isRoot()) {
throw new Error('Cannot bring before a root node.')
}
var newParent = sibling._parent
var index = newParent._order.indexOf(sibling)
// Index is off by 1 if same parent and the item is moved toward back.
var oldParent = this._parent
if (oldParent === newParent) {
if (oldParent._order.indexOf(this) < index) {
index = index - 1
}
}
this.setParent(sibling._parent, index)
}
p.sendToBack = function () {
// Remove this node and reinsert it as the first child.
//
if (this.isRoot()) {
// Already first
return
}
this.setParent(this._parent, 0)
}
p.setParent = function (newParent, index) {
// Add node to new parent. Will be removed from old parent.
// Optional index defines the new position among siblings.
//
// Parameters:
// newParent
// an AbstractNode
// index
// integer, optional, default to last index. Index of 0 will
// insert the node as the first child.
//
// Emits:
// removed, after node is removed from old parent
// added, after node is added to new parent
//
// Causes emits:
// oldParent emits childRemoved after node is removed
// newParent emits childAdded after node is added
//
//
// Dev note about cyclic relationship detection:
// A
// |
// / \
// B C
//
// Different cases. The emitter relationship status changes...
// from root to root:
// no worries about cyclic structures
// from root to child:
// If the new parent is a descendant of the emitter, problems.
// If the new parent the emitter itself, problems.
// from child to root:
// Loses parenthood. No cyclic worries.
// from child to child:
// If the new parent is a descendant of the emitter, problems.
// If the new parent the emitter itself, problems.
// If new parent has the emitter as descendant already...
// then no worries because emitter would only create a new branch.
//
if (typeof newParent === 'undefined') {
throw new Error('Parameter \'newParent\' is required.')
}
if (typeof index === 'undefined' && newParent !== null) {
index = newParent._order.length
}
var oldParent = this._parent
if (oldParent === null) {
if (newParent === null) {
// AbstractNode's position changed from root to root.
// Do nothing
} else {
// From root to child.
// Add only.
// Prevent cycles.
if (this === newParent || this.hasDescendant(newParent)) {
throw new Error('Cyclic parent-child relationships are forbidden.')
}
newParent._addChild(this, index)
this.emit('added', {
source: this,
newParent: newParent,
oldParent: null
})
newParent.emit('childAdded', {
source: newParent,
newChild: this,
oldParent: null
})
}
} else {
if (newParent === null) {
// From child to root.
// Remove only.
oldParent._removeChild(this)
this.emit('removed', {
source: this,
newParent: null,
oldParent: oldParent
})
oldParent.emit('childRemoved', {
source: oldParent,
oldChild: this,
newParent: null
})
} else {
// From child to child.
// Remove and add.
// Prevent cycles.
if (this === newParent || this.hasDescendant(newParent)) {
throw new Error('Cyclic parent-child relationships are forbidden.')
}
oldParent._removeChild(this)
newParent._addChild(this, index)
// With both oldParent and newParent, SpaceView is able to
// decide whether to keep same HTMLElement or recreate it.
this.emit('removed', {
source: this,
newParent: newParent,
oldParent: oldParent
})
oldParent.emit('childRemoved', {
source: oldParent,
oldChild: this,
newParent: newParent
})
this.emit('added', {
source: this,
newParent: newParent,
oldParent: oldParent
})
newParent.emit('childAdded', {
source: newParent,
newChild: this,
oldParent: oldParent
})
}
}
}
p._addChild = function (abstractNode, index) {
// To be called from abstractNode.setParent().
// If called from anywhere else, ensure cyclic relationships are detected.
var n = abstractNode
var prev = this._order[index - 1]
var next = this._order[index]
n._parent = this
if (prev) {
n._prevSibling = prev
prev._nextSibling = n
} else {
n._prevSibling = null
}
if (next) {
n._nextSibling = next
next._prevSibling = n
} else {
n._nextSibling = null
}
this._children[n.id] = n
this._order.splice(index, 0, n)
}
p._removeChild = function (abstractNode) {
// To be called from abstractNode.setParent().
// Precondition: abstractNode is a child of this
var n = abstractNode
var prev = n._prevSibling
var next = n._nextSibling
n._parent = null
n._prevSibling = null
n._nextSibling = null
if (prev) {
prev._nextSibling = next // null next is ok
}
if (next) {
next._prevSibling = prev // null prev is ok
}
delete this._children[n.id]
this._order.splice(this._order.indexOf(n), 1)
}
module.exports = AbstractNode