machine-framework
Version:
self-assembling client-side web framework
294 lines (196 loc) • 6.82 kB
JavaScript
import AliasContext from './aliasContext.js';
import ScriptMonitor from './scriptMonitor.js';
import ScriptLoader from './scriptLoader.js';
import Placeholder from './placeholder.js';
import Catbus from './catbus.es.js';
import Cog from './cog.js';
import PartBuilder from './partBuilder.js';
let _id = 0;
function Chain(url, slot, parent, def, sourceName, keyField){
this.id = ++_id;
this.head = null;
this.placeholder = slot;
this.elements = [];
this.namedElements = {};
this.children = [];
this.parent = parent || null;
this.scope = parent ? parent.scope.createChild() : Catbus.createChild();
this.url = url;
this.root = '';
this.script = null;
this.config = null; //(def && def.config) || def || {};
this.scriptMonitor = null;
this.aliasValveMap = null;
this.aliasContext = null;
this.sourceName = sourceName;
this.keyField = keyField;
this.bus = null;
this.buildConfig(def);
this.load();
}
Chain.prototype.buildConfig = PartBuilder.buildConfig;
Chain.prototype.killPlaceholder = function() {
if(!this.placeholder)
return;
Placeholder.give(this.placeholder);
this.placeholder = null;
};
Chain.prototype.load = function() {
if(ScriptLoader.has(this.url)){
this.onScriptReady();
} else {
ScriptLoader.request(this.url, this.onScriptReady.bind(this));
}
};
Chain.prototype.onScriptReady = function() {
this.script = Object.create(ScriptLoader.read(this.url));
this.script.id = this.id;
this.script.config = this.config;
this.root = this.script.root;
this.prep();
};
Chain.prototype.prep = function(){
const parent = this.parent;
const aliasValveMap = parent ? parent.aliasValveMap : null;
//const aliasList = this.script.alias;
const aliasHash = this.script.aliases;
if(parent && parent.root === this.root && !aliasHash && !aliasValveMap){
// same relative path, no new aliases and no valves, reuse parent context
this.aliasContext = parent.aliasContext;
this.aliasContext.shared = true;
} else {
// new context, apply valves from parent then add aliases from cog
this.aliasContext = parent
? parent.aliasContext.clone()
: new AliasContext(this.root); // root of application
this.aliasContext.restrictAliasList(aliasValveMap);
//this.aliasContext.injectAliasList(aliasList);
this.aliasContext.injectAliasHash(aliasHash);
}
this.loadBooks();
};
Chain.prototype.loadBooks = function loadBooks(){
if(this.script.books.length === 0) {
this.loadTraits();
return;
}
const urls = this.aliasContext.freshUrls(this.script.books);
if (urls.length) {
this.scriptMonitor = new ScriptMonitor(urls, this.readBooks.bind(this));
} else {
this.readBooks()
}
};
Chain.prototype.readBooks = function readBooks() {
const urls = this.script.books;
if(this.aliasContext.shared) // need a new context
this.aliasContext = this.aliasContext.clone();
for (let i = 0; i < urls.length; ++i) {
const url = urls[i];
const book = ScriptLoader.read(url);
if(book.type !== 'book')
console.log('EXPECTED BOOK: got ', book.type, book.url);
this.aliasContext.injectAliasList(book.alias);
}
this.loadTraits();
};
Chain.prototype.loadTraits = function loadTraits(){
const urls = this.aliasContext.freshUrls(this.script.traits);
if(urls.length){
this.scriptMonitor = new ScriptMonitor(urls, this.build.bind(this));
} else {
this.build();
}
};
Chain.prototype.getNamedElement = function getNamedElement(name){
if(!name)
return null;
const el = this.namedElements[name];
if(!el)
throw new Error('Named element ' + name + ' not found in display!');
return el;
};
Chain.prototype.build = function build(){ // urls loaded
const meow = this.sourceName + ' * buildCogsByIndex';
this.bus = this.scope.bus().context(this).meow(meow).pull();
};
Chain.prototype.buildCogsByIndex = function buildCogsByIndex(msg){
const len = msg.length;
const children = this.children;
const childCount = children.length;
const updateCount = len > childCount ? childCount : len;
// update existing
for(let i = 0; i < updateCount; ++i){
const d = msg[i];
const c = children[i];
c.source.write(d);
}
if(len === 0 && childCount > 0){
// restore placeholder as all children will be gone
const el = this.getFirstElement(); // grab first child element
this.placeholder = Placeholder.take();
el.parentNode.insertBefore(this.placeholder, el);
}
if(childCount < len) { // create new children
const lastEl = this.getLastElement();
const nextEl = lastEl.nextElementSibling;
const parentEl = lastEl.parentNode;
const before = !!nextEl;
const el = nextEl || parentEl;
for (let i = childCount; i < len; ++i) {
// create cogs for new data
const slot = Placeholder.take();
if (before) {
el.parentNode.insertBefore(slot, el);
} else {
el.appendChild(slot);
}
const cog = new Cog(this.url, slot, this, this.config, i);
const d = msg[i];
cog.source.write(d);
children.push(cog);
}
} else {
for (let i = childCount - 1; i >= len; --i) {
// remove cogs without corresponding data
children[i].destroy();
children.splice(i, 1);
}
}
this.tail = children.length > 0 ? children[children.length - 1] : null;
this.head = children.length > 0 ? children[0] : null;
};
Chain.prototype.getFirstElement = function(){
let c = this;
while(c && !c.placeholder && c.elements.length === 0){
c = c.head;
}
return c.placeholder || c.elements[0];
};
Chain.prototype.getLastElement = function(){
let c = this;
while(c && !c.placeholder && c.elements.length === 0){
c = c.tail;
}
return c.placeholder || c.elements[c.elements.length - 1];
};
Chain.prototype.destroy = function(){
this.dead = true;
const len = this.children.length;
for(let i = 0; i < len; ++i){
const c = this.children[i];
c.destroy();
}
if(this.placeholder){
this.killPlaceholder();
} else {
const len = this.elements.length;
for(let i = 0; i < len; ++i){
const e = this.elements[i];
e.parentNode.removeChild(e);
}
}
this.scope.destroy();
this.children = [];
};
export default Chain;