observ-node-array
Version:
Populate an observable array from node descriptors.
261 lines (204 loc) • 6.05 kB
JavaScript
var NO_TRANSACTION = {}
var Value = require('@mmckegg/mutant/value')
var Event = require('geval')
var resolveNode = require('./resolve')
var deepEqual = require('deep-equal')
module.exports = ObservNodeArray
function ObservNodeArray(parentContext){
// context: nodes, nodeKey
var context = Object.create(parentContext)
var obs = Value([])
obs._type = 'NodeArray'
obs._list = []
context.collection = obs
obs.context = context
var removeListeners = []
var instanceDescriptors = []
var currentTransaction = NO_TRANSACTION
var broadcastUpdate = null
obs.onUpdate = Event(function(broadcast){
broadcastUpdate = broadcast
})
obs.getLength = function(){
return obs._list.length
}
obs.get = function(i){
return obs._list[i]
}
obs.indexOf = function(item){
return obs._list.indexOf(item)
}
obs.forEach = function(iterator, context){
obs._list.forEach(iterator, context)
}
obs.map = function(iterator, context){
return obs._list.map(iterator, context)
}
obs.move = function(item, targetIndex){
var currentIndex = obs._list.indexOf(item)
if (~currentIndex){
var descriptor = instanceDescriptors[currentIndex]
var listener = removeListeners[currentIndex]
var updates = []
if (currentIndex < targetIndex){
insert(targetIndex+1, item, descriptor, listener)
remove(currentIndex)
updates.push(
[targetIndex+1, 0, item],
[currentIndex, 1]
)
} else {
remove(currentIndex)
insert(targetIndex, item, descriptor, listener)
updates.push(
[currentIndex, 1],
[targetIndex, 0, item]
)
}
update()
updates.forEach(broadcastUpdate)
}
}
obs.remove = function(item){
var currentIndex = obs._list.indexOf(item)
if (~currentIndex){
unlisten(item, currentIndex)
remove(currentIndex)
update()
broadcastUpdate([currentIndex, 1])
}
}
obs.insert = function(descriptor, targetIndex){
var nodeName = getNode(descriptor)
var ctor = nodeName && resolveNode(context.nodes, nodeName)
if (typeof ctor === 'function'){
var item = ctor(context)
item.set(descriptor)
insert(targetIndex, item, descriptor)
listen(item, targetIndex)
update()
broadcastUpdate([targetIndex, 0, item])
return item
}
}
obs.push = function(descriptor){
return obs.insert(descriptor, obs._list.length)
}
obs.destroy = function(){
obs._list.forEach(unlisten)
}
obs(function(descriptors){
if (currentTransaction === descriptors){
return false
}
currentTransaction = descriptors
if (!Array.isArray(descriptors)){
descriptors = []
}
var maxLength = Math.max(descriptors.length, instanceDescriptors.length)
var minLength = Math.min(descriptors.length, instanceDescriptors.length)
var difference = descriptors.length - instanceDescriptors.length
var updates = []
for (var i=0;i<maxLength;i++){
if (updateNode(i, descriptors[i]) && i < minLength){
updates.push([i, 1, obs._list[i]])
}
}
obs._list.length = descriptors.length
removeListeners.length = descriptors.length
instanceDescriptors = descriptors.slice()
if (difference > 0){
var u = [minLength, 0]
for (var i=minLength;i<maxLength;i++){
u.push(obs._list[i])
}
updates.push(u)
} else if (difference < 0){
updates.push([minLength-1, -difference])
}
currentTransaction = NO_TRANSACTION
updates.forEach(broadcastUpdate)
})
return obs
// scoped
function onUpdate(item){
var index = obs._list.indexOf(item)
if (~index && instanceDescriptors[index]){
if (currentTransaction == NO_TRANSACTION){
var updates = []
var oldDescriptor = instanceDescriptors[index]
var descriptor = item()
if (getNode(descriptor) !== getNode(oldDescriptor)){
if (updateNode(index, descriptor)){
updates.push([index, 1, obs._list[index]])
}
}
instanceDescriptors[index] = descriptor
update()
updates.forEach(broadcastUpdate)
}
}
}
function update(){
var newValue = instanceDescriptors.slice()
currentTransaction = newValue
obs.set(newValue)
currentTransaction = NO_TRANSACTION
}
function listen(item, index){
removeListeners[index] = item(function(){
onUpdate(item)
})
}
function unlisten(item, index){
if (removeListeners[index]){
removeListeners[index]()
removeListeners[index] = null
}
if (item && item.destroy){
item.destroy()
}
}
function remove(index){
instanceDescriptors.splice(index, 1)
removeListeners.splice(index, 1)
obs._list.splice(index, 1)
}
function insert(index, obj, descriptor, listener){
instanceDescriptors.splice(index, 0, descriptor)
removeListeners.splice(index, 0, listener)
obs._list.splice(index, 0, obj)
}
function updateNode(index, descriptor){
var instance = obs._list[index]
var lastDescriptor = instanceDescriptors[index]
var nodeName = getNode(descriptor)
var ctor = descriptor && resolveNode(context.nodes, nodeName)
if (instance && nodeName === getNode(lastDescriptor)){
if (!deepEqual(instance, descriptor)) {
instance.set(descriptor)
}
} else {
if (instance){
unlisten(instance, index)
instance = null
}
obs._list[index] = null
if (descriptor){
// create
if (typeof ctor === 'function') {
var innerContext = Object.create(context)
instance = ctor(innerContext)
innerContext.node = instance
instance.set(descriptor)
listen(instance, index)
obs._list[index] = instance
}
}
return true
}
}
function getNode(value){
return value && value[context.nodeKey||'node'] || null
}
}