Created
September 26, 2015 11:00
Reactor.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(function (){ | |
"use strict"; | |
var el = document.createElement('DIV'); | |
if (Array.prototype.forEach) { | |
var coreForEach = Array.prototype.forEach; | |
var forEach = function(collection, fn) { | |
coreForEach.call(collection, fn); | |
}; | |
} | |
else { | |
var forEach = function(collection, fn) { | |
for (var i = 0, len = collection.length; i < len; i++) { | |
fn(collection[i], i); | |
} | |
}; | |
} | |
if (el.getElementsByClassName === undefined) { | |
var getElementsByClassName = function(parent, cls) { | |
var elements = parent.getElementsByTagName('*'); | |
var match = []; | |
for (var i = 0, leni = elements.length; i < leni; i++) { | |
if (hasClass(elements[i], cls)) { | |
match.push(elements[i]); | |
} | |
} | |
return match; | |
}; | |
} | |
else { | |
var getElementsByClassName = function(parent, cls) { | |
return parent.getElementsByClassName(cls); | |
}; | |
} | |
var getElementsByTagName = function(parent, tag) { | |
return parent.getElementsByTagName(tag); | |
}; | |
var _ = { | |
forEach: forEach, | |
cls: getElementsByClassName, | |
tag: getElementsByTagName, | |
id: function(id) { return document.getElementById(id); } | |
}; | |
var Signal = function() { | |
var self = this; | |
this.listeners = []; | |
this.connect = function(callback) { | |
if (callback === undefined) { | |
wtf(); | |
} | |
self.listeners.push(callback); | |
}; | |
this.disconnect = function(callback) { | |
if (callback === undefined) { | |
self.listeners = []; | |
return; | |
} | |
var pos = self.listeners.indexOf(callback); | |
if (pos === -1) { | |
console.log('Not connected', callback); | |
return; | |
} | |
self.listeners.splice(pos, 1); | |
}; | |
this.send = function() { | |
var sendArguments = arguments; | |
_.forEach(self.listeners, function(listener) { | |
listener.apply(this, sendArguments); | |
}); | |
}; | |
}; | |
var E = (function () { | |
var P = function(name, value) { | |
var name = name.split('='); | |
this.name = name[0]; | |
this.propname = name[1]; | |
this.value = value; | |
}; | |
return function(name, propname, value) { | |
return new P(name, propname, value); | |
} | |
}()); | |
var Etree = (function () { | |
var getText = function(node) { | |
return node.textContent; | |
}; | |
var setText = function(node, value) { | |
node.textContent = value; | |
}; | |
var getHTML = function(node) { | |
return node.innerHTML; | |
}; | |
var setHTML = function(node, value) { | |
node.innerHTML = value; | |
}; | |
var getAttribute = function(node, attribute) { | |
return node.getAttribute(attribute); | |
}; | |
var setAttribute = function(node, attribute, value) { | |
node.setAttribute(attribute, value); | |
}; | |
var setData = function(self, data) { | |
for (var key in data) { | |
if (_.has(data, key) && _.has(self.set, key)) { | |
self.set[key](data[key]); | |
} | |
} | |
}; | |
return function(root, initialData) { | |
var getters = {}; | |
var setters = {}; | |
var elements = {}; | |
var elementNumber = 0; | |
var valueAccessors = {}; | |
var buildElement = function(nodeData, parent) { | |
if (nodeData.name === '#text') { | |
var node = document.createTextNode(nodeData.value); | |
if (nodeData.propname !== undefined) { | |
valueAccessors[nodeData.propname] = [elementNumber, getText, setText]; | |
} | |
elementNumber++; | |
parent.appendChild(node); | |
} | |
else if (nodeData.name[0] === '@') { | |
var attribute = nodeData.name.substr(1) | |
parent.setAttribute(attribute, nodeData.value); | |
if (nodeData.propname !== undefined) { | |
valueAccessors[nodeData.propname] = [elementNumber - 1, function(node) { return getAttribute(node, attribute); }, function(node, value) { return setAttribute(node, attribute, value); }]; | |
} | |
} | |
else { | |
var node = document.createElement(nodeData.name); | |
if (nodeData.propname !== undefined) { | |
valueAccessors[nodeData.propname] = [elementNumber, getHTML, setHTML]; | |
} | |
elementNumber++; | |
if (nodeData.value) { | |
_.forEach(nodeData.value, function(subElement) { | |
buildElement(subElement, node) | |
}); | |
} | |
if (parent) { | |
parent.appendChild(node); | |
} | |
return node; | |
} | |
}; | |
if (root.cache) { | |
var tree = root.cache.tree.cloneNode(true); | |
var valueAccessors = root.cache.accessors; | |
} | |
else { | |
var tree = buildElement(root); | |
root.cache = root.cache || {}; | |
root.cache.tree = tree.cloneNode(true); | |
root.cache.accessors = valueAccessors; | |
} | |
var flatNodesList = []; | |
var flatNodes = function(root) { | |
flatNodesList.push(root); | |
if (root.childNodes) { | |
_.forEach(root.childNodes, flatNodes); | |
} | |
}; | |
flatNodes(tree); | |
for (var propname in valueAccessors) { | |
var accessors = valueAccessors[propname]; | |
(function (accessors) { | |
var node = flatNodesList[accessors[0]]; | |
getters[propname] = function() { return accessors[1](node); } | |
setters[propname] = function(value) { return accessors[2](node, value); } | |
elements[propname] = node; | |
}(accessors)); | |
} | |
var component = { | |
tree: tree, | |
get: getters, | |
set: setters, | |
elements: elements | |
}; | |
component.setData = function(data) { setData(component, data); }; | |
if (initialData !== undefined) { | |
component.setData(initialData); | |
} | |
return component; | |
} | |
}()); | |
var ModelInstance = function(context) { | |
var self = this; | |
this.context = context; | |
this.signals = { | |
changed: new Signal() | |
}; | |
this.updateContext = function(context) { | |
self.context = context; | |
this.signals.changed.send(self); | |
}; | |
}; | |
var ListModel = function() { | |
var self = this; | |
this.list = []; | |
this.signals = { | |
inserted: new Signal(), | |
removed: new Signal() | |
}; | |
this.insert = function(instance, position) { | |
self.list.splice(position, 0, instance); | |
self.signals.inserted.send({ | |
position: position, | |
instance: instance, | |
moved: false | |
}); | |
}; | |
this.move = function(positionFrom, positionTo) { | |
var moved = {from: positionFrom, to: positionTo}; | |
var instance = self.list[positionFrom]; | |
self.list.splice(positionFrom, 1); | |
self.signals.removed.send({ | |
position: positionFrom, | |
instance: instance, | |
moved: moved | |
}); | |
self.list.splice(positionTo, 0, instance); | |
self.signals.inserted.send({ | |
position: positionTo, | |
instance: instance, | |
moved: moved | |
}); | |
}; | |
this.remove = function(position) { | |
var instance = self.list[position]; | |
self.list.splice(position, 1); | |
self.signals.removed.send({position: position, instance: instance, moved: false}); | |
}; | |
this.indexOf = function(instance) { | |
return self.list.indexOf(instance); | |
}; | |
this.has = function(instance) { | |
return self.list.indexOf(instance) !== -1; | |
}; | |
}; | |
var SortedListModel = function(compareFunction) { | |
var self = this; | |
var model = new ListModel(); | |
this.list = model.list; | |
this.signals = { | |
inserted: model.signals.inserted, | |
removed: model.signals.removed | |
}; | |
this.insert = function(instance) { | |
var position = self.findPosition(instance); | |
model.insert(instance, position); | |
instance.signals.changed.connect(self.moveSortedInstance); | |
}; | |
this.remove= function(instance) { | |
if (!model.has(instance)) { | |
return; | |
} | |
model.remove(model.indexOf(instance)); | |
instance.signals.changed.disconnect(self.moveSortedInstance); | |
}; | |
this.findPosition = function(instance) { | |
for (var i = 0, leni = self.list.length; i < leni; i++) { | |
if (compareFunction(self.list[i], instance) > 0) { | |
return i; | |
} | |
} | |
return self.list.length; | |
}; | |
this.moveSortedPosition = function(position) { | |
var instance = self.list[position]; | |
self.list.splice(position, 1); | |
var targetPosition = self.findPosition(instance); | |
self.list.splice(position, 0, instance); | |
if (position !== targetPosition) { | |
model.move(position, targetPosition); | |
} | |
}; | |
this.moveSortedInstance = function(instance) { | |
self.moveSortedPosition(model.indexOf(instance)); | |
}; | |
this.indexOf = function(instance) { | |
return model.indexOf(instance); | |
}; | |
this.has = function(instance) { | |
return self.list.indexOf(instance) !== -1; | |
}; | |
}; | |
var AutoListModel = function(options) { | |
var self = this; | |
var diffAdded = function(item, index) { | |
self.model.insert(item, index); | |
}; | |
var diffDeleted = function(item, index) { | |
self.model.remove(index); | |
}; | |
var diffMoved = function(pos_from, pos_to) { | |
self.model.move(pos_from, pos_to); | |
}; | |
var o = { | |
added: diffAdded, | |
deleted: diffDeleted, | |
moved: diffMoved | |
}; | |
for (var k in options) { | |
if (!k in o) { | |
o[k] = options[k]; | |
} | |
else { | |
var old = o[k]; | |
var custom = options[k]; | |
(function (old, custom) { | |
o[k] = function() { | |
custom.apply(null, arguments); | |
old.apply(self, arguments); | |
} | |
}(old, custom)); | |
} | |
} | |
if (!_.has(o, 'getId')) { | |
if (o.wrapModel) { | |
o.getId = function(item) { return item.context.id; }; | |
} | |
else { | |
o.getId = function(item) { return item.id; }; | |
} | |
} | |
if (o.wrapModel) { | |
o.applyChanges = function(oldList, newList) { | |
for (var i = 0, leni = oldList.length; i < leni; i++) { | |
var oldInstance = oldList[i]; | |
var newInstance = newList[i]; | |
if (!_.isEqual(oldInstance.context, newInstance.context)) { | |
oldInstance.updateContext(newInstance.context); | |
} | |
} | |
}; | |
} | |
this.model = new ListModel(); | |
this.differ = new ListDiffer(o); | |
this.list = this.model.list; | |
this.setList = function(list) { | |
var wrappedList = list; | |
if (o.wrapModel) { | |
wrappedList = []; | |
_.forEach(list, function(item) { | |
wrappedList.push(new ModelInstance(item)); | |
}); | |
} | |
self.differ.setList(wrappedList); | |
}; | |
this.signals = { | |
inserted: this.model.signals.inserted, | |
removed: this.model.signals.removed | |
}; | |
}; | |
var ListView = function(element, model, widget) { | |
var self = this; | |
var instances = []; | |
var widgets = []; | |
var movingWidget = undefined; | |
this.destroy = function() { | |
model.signals.inserted.disconnect(self.insert); | |
model.signals.removed.disconnect(self.remove); | |
while (instances.length) { | |
self.remove({position: 0}); | |
} | |
}; | |
this.insert = function(data) { | |
var append = data.position === instances.length; | |
instances.splice(data.position, 0, data.instance); | |
if (movingWidget === undefined) { | |
var widgetInstance = new widget(); | |
widgetInstance.modelInstance = model; | |
} | |
else { | |
var widgetInstance = movingWidget; | |
movingWidget = undefined; | |
} | |
if (!append) { | |
var old = widgets[data.position]; | |
} | |
if (!data.move) { | |
widgetInstance.construct(data.instance); | |
} | |
widgets.splice(data.position, 0, widgetInstance); | |
if (append) { | |
element.appendChild(widgetInstance.component.tree); | |
} | |
else { | |
element.insertBefore(widgetInstance.component.tree, old.component.tree); | |
} | |
}; | |
this.remove = function(data) { | |
instances.splice(data.position, 1); | |
var widgetInstance = widgets[data.position]; | |
if (widgetInstance.component.tree.parentNode) { | |
widgetInstance.component.tree.parentNode.removeChild(widgetInstance.component.tree); | |
} | |
if (data.move) { | |
movingWidget = widgetInstance; | |
} | |
else { | |
widgetInstance.destroy(); | |
} | |
widgets.splice(data.position, 1); | |
} | |
_.forEach(model.list, function(data) { | |
self.insert({ | |
position: instances.length, | |
instance: data, | |
move: false | |
}); | |
}); | |
model.signals.inserted.connect(this.insert); | |
model.signals.removed.connect(this.remove); | |
}; | |
var dummyFunction = function() {}; | |
var Widget = function(options) { | |
return function () { | |
var self = this; | |
var constructWidget = dummyFunction; | |
var destroyWidget = dummyFunction; | |
var updateWidget = dummyFunction; | |
this.options = options; | |
this.template = options.template; | |
this.component = undefined; | |
this.data = undefined; | |
this.modelInstance = undefined; | |
if (options.construct !== undefined) { | |
constructWidget = options.construct.bind(this); | |
} | |
if (options.destroy !== undefined) { | |
destroyWidget = options.destroy.bind(this); | |
} | |
if (options.update !== undefined) { | |
updateWidget = options.update.bind(this); | |
} | |
this.construct = function(data) { | |
self.component = Etree(self.template); | |
if (data === undefined) { | |
self.data = {}; | |
} | |
else { | |
self.update(data); | |
} | |
if (self.data.signals !== undefined) { | |
data.signals.changed.connect(self.update); | |
} | |
constructWidget(data); | |
return self.component; | |
}; | |
this.destroy = function() { | |
destroyWidget(); | |
if (self.data.signals !== undefined) { | |
self.data.signals.changed.disconnect(self.update); | |
} | |
self.data = undefined; | |
self.component = undefined | |
self.modelInstance = undefined; | |
}; | |
this.update = function(data) { | |
updateWidget(data); | |
self.data = data; | |
}; | |
}; | |
}; | |
var ListDiffer = function(options) { | |
var o = { | |
initial: [], | |
added: function() {}, | |
deleted: function() {}, | |
moved: function() {}, | |
applyChanges: function(oldList, newList) { oldList = newList; }, | |
getId: function(item) { return item.id; } | |
}; | |
for (var k in options) { if (options.hasOwnProperty(k)) o[k] = options[k]; } | |
var list = o.initial; | |
var added = o.added; | |
var deleted = o.deleted; | |
var moved = o.moved; | |
var applyChanges = o.applyChanges; | |
var getId = o.getId; | |
this.setList = function(newList) { | |
var currentDict = {}; | |
var newDict = {}; | |
_.forEach(list, function(item) { | |
currentDict[getId(item)] = item; | |
}); | |
_.forEach(newList, function(item) { | |
newDict[getId(item)] = item; | |
}); | |
var toCreate = []; | |
var toDelete = []; | |
_.forEach(newList, function(item) { | |
var id = getId(item); | |
if (!_.has(currentDict, id)) { | |
toCreate.push(id); | |
} | |
}); | |
_.forEach(list, function(item) { | |
var id = getId(item); | |
if (!_.has(newDict, id)) { | |
toDelete.push(id); | |
} | |
}); | |
toDelete.reverse(); | |
var indexList = []; | |
_.forEach(list, function(item) { | |
indexList.push(getId(item)); | |
}); | |
_.forEach(toDelete, function(id) { | |
var index = indexList.indexOf(id); | |
var item = list[index]; | |
indexList.splice(index, 1); | |
list.splice(index, 1); | |
deleted(item, index); | |
}); | |
_.forEach(toCreate, function(id) { | |
var item = newDict[id]; | |
var index = list.length; | |
indexList.push(id); | |
list.push(item); | |
added(item, index); | |
}); | |
for (var i = 0, leni = newList.length; i < leni; i++) { | |
var newId = getId(newList[i]); | |
if (newId !== indexList[i]) { | |
var oldIndex = indexList.indexOf(newId); | |
var listItem = list.splice(oldIndex, 1)[0]; | |
list.splice(i, 0, listItem); | |
listItem = indexList.splice(oldIndex, 1)[0]; | |
indexList.splice(i, 0, listItem); | |
moved(oldIndex, i); | |
} | |
} | |
applyChanges(list, newList); | |
}; | |
}; | |
window.Reactor = { | |
Signal: Signal, | |
E: E, | |
Etree: Etree, | |
//EtreeHighlight: EtreeHighlight, | |
ModelInstance: ModelInstance, | |
ListModel: ListModel, | |
SortedListModel: SortedListModel, | |
AutoListModel: AutoListModel, | |
ListView: ListView, | |
Widget: Widget, | |
ListDiffer: ListDiffer, | |
utils: _ | |
}; | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment