Skip to content

Instantly share code, notes, and snippets.

@mireq
Created September 26, 2015 11:00
Show Gist options
  • Save mireq/fe5b7cfc360d1ab40e7a to your computer and use it in GitHub Desktop.
Save mireq/fe5b7cfc360d1ab40e7a to your computer and use it in GitHub Desktop.
Reactor.js
;(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