quick
Version:
QuickJS is a declarative language engine to run inside a browser. The node module contains the offline compiler.
436 lines (356 loc) • 12.1 kB
JavaScript
// Copyright (c) 2012 Johannes Zellner webmaster@nebulon.de - All Rights Reserved
"use strict";
/*
**************************************************
* Singleton engine
**************************************************
*
* Handles mainly toplevel elements and detects bindings.
* This should contain as less as possible!
*
*/
if (!Quick) {
var Quick = {};
}
// create main singleton object
if (!Quick.Engine) {
Quick.Engine = (function () {
var ret = {};
var getterCalled = {};
var _dirtyElements = {};
ret.magicBindingState = false;
ret.verbose = false;
ret._elementIndex = 0;
function log(msg, error) {
if (ret.verbose || error) {
console.log("[Quick.Engine] " + msg);
}
}
// try to create a renderer backend, currently on DOM supported
try {
var renderer = new Quick.RendererDOM();
ret.createElement = renderer.createElement;
ret.addElement = renderer.addElement;
ret.addElements = renderer.addElements;
ret.renderElement = renderer.renderElement;
ret.removeElement = renderer.removeElement;
} catch (e) {
log("Cannot create DOM renderer", true);
ret.createElement = function () {};
ret.addElements = function () {};
ret.addElement = function () {};
ret.renderElement = function () {};
ret.removeElement = function () {};
}
// begin binding detection
ret.enterMagicBindingState = function () {
log("enterMagicBindingState");
getterCalled = {};
ret.magicBindingState = true;
};
// end binding detection
ret.exitMagicBindingState = function () {
log("exitMagicBindingState\n\n");
ret.magicBindingState = false;
return getterCalled;
};
ret.addCalledGetter = function (element, property) {
getterCalled[element.id + "." + property] = { element: element, property: property };
};
// TODO should be part of the dom renderer?
var renderInterval;
var fps = {};
fps.d = Date.now();
fps.l = 0;
function advance() {
for (var i in _dirtyElements) {
_dirtyElements[i].render();
}
_dirtyElements = {};
if (Quick.verbose) {
if ((Date.now() - fps.d) >= 2000) {
console.log("FPS: " + fps.l / 2.0);
fps.d = Date.now();
fps.l = 0;
} else {
++(fps.l);
}
}
}
ret.start = function () {
renderInterval = window.setInterval(advance, 1000/60.0);
advance();
};
ret.stop = function () {
window.clearInterval(renderInterval);
};
ret.dirty = function (element, property) {
// ignore properties prefixed with _
if (property[0] === '_') {
return;
}
element._dirtyProperties[property] = true;
if (!_dirtyElements[element._internalIndex]) {
_dirtyElements[element._internalIndex] = element;
}
};
return ret;
}());
}
/*
**************************************************
* Basic Element
**************************************************
*
* The main element, which handles its connections
* and properties. It also calls into the renderer
* by using render hooks.
*
*/
Quick.Element = function (id, parent, typeHint) {
this.id = id;
this.typeHint = typeHint;
this.parent = parent;
if (typeHint !== "object") {
this.element = Quick.Engine.createElement(typeHint, this);
} else {
this.element = null;
}
// internal use only
this._internalIndex = Quick.Engine._elementIndex++;
this._dirtyProperties = {};
this._properties = {};
this._connections = {};
this._children = {};
this._bound = {};
this._initializeBindingsStep = false;
if (this.parent) {
this.parent.addChild(this);
}
};
Quick.Element.prototype.children = function () {
if (Quick.Engine.magicBindingState) {
Quick.Engine.addCalledGetter(this, 'children');
}
return this._children;
};
// TODO both removes need to break the bindings for the children as well
Quick.Element.prototype.removeChild = function(child) {
Quick.Engine.removeElement(child, this);
delete this._children[child._internalIndex];
this.emit("children");
};
Quick.Element.prototype.removeChildren = function () {
var i;
for (i in this._children) {
if (this._children.hasOwnProperty(i)) {
// TODO do we leak things here? elements are still referenced so maybe a delete?
Quick.Engine.removeElement(this._children[i], this);
}
}
this._children = {};
this.emit("children");
};
Quick.Element.prototype.addChild = function (child) {
// adds child id to the namespace
if (child.id)
this[child.id] = child;
// adds the parents id to the child
if (this.id)
child[this.id] = this;
// add child to siblings scope and vice versa
for (var i in this._children) {
if (this._children.hasOwnProperty(i)) {
if (child.id)
this._children[i][child.id] = child;
if (this._children[i].id)
child[this._children[i].id] = this._children[i];
}
}
// add newly added child to internal children array
this._children[child._internalIndex] = child;
child.parent = this;
Quick.Engine.addElement(child, this);
this.emit("children");
return child;
};
Quick.Element.prototype.render = function () {
Quick.Engine.renderElement(this);
};
Quick.Element.prototype.addChanged = function (signal, callback) {
if (!this._connections[signal]) {
this._connections[signal] = [];
}
this._connections[signal][this._connections[signal].length] = callback;
// console.log("connections for " + signal + " " + this._connections[signal].length);
};
Quick.Element.prototype.removeChanged = function (obj, signal) {
var signalConnections = this._connections[signal];
// check if there are any connections for this signal
if (!signalConnections) {
return;
}
for (var i = 0; i < signalConnections.length; ++i) {
// TODO do implementation
}
};
Quick.Element.prototype.addBinding = function (name, value) {
var that = this;
var hasBinding = false;
// FIXME does not catch changing conditions in expression
// x: mouseArea.clicked ? a.y() : b:z();
Quick.Engine.enterMagicBindingState();
var val = value.apply(this);
var getters = Quick.Engine.exitMagicBindingState();
this.breakBindings(name);
var bindingFunction = function() {
that[name] = value.apply(that);
};
// store found bindings
for (var getter in getters) {
if (getters.hasOwnProperty(getter)) {
var tmp = getters[getter];
// store bindings to this for breaking
this._bound[name][this._bound[name].length] = {
element: tmp.element,
property: tmp.property
};
tmp.element.addChanged(tmp.property, bindingFunction);
hasBinding = true;
}
}
return { hasBindings: hasBinding, value: val };
};
Quick.Element.prototype.addEventHandler = function (event, handler) {
var that = this;
var signal = event;
if (signal === "" || typeof handler !== 'function') {
return;
}
if (signal.indexOf('on') === 0) {
signal = signal.slice(2);
}
this.addChanged(signal, function () {
if (!that._initializeBindingsStep)
handler.apply(that);
});
};
// Breaks all bindings assigned to this property
Quick.Element.prototype.breakBindings = function (name) {
// break all previous bindings
if (this._bound[name]) {
for (var i = 0; i < this._bound[name].length; ++i) {
this._bound[name][i].element.removeChanged(this, name);
}
}
this._bound[name] = [];
};
// This allows to set the property without emit the change
// Does not break the binding!
Quick.Element.prototype.setSilent = function (name, value) {
var setter = this.__lookupSetter__(name);
if (typeof setter === 'function') {
setter.call(this, value, true);
}
};
// This allows to get the property without notify the get
Quick.Element.prototype.getSilent = function (name) {
var getter = this.__lookupGetter__(name);
if (typeof getter === 'function') {
return getter.call(this, true);
}
};
// This breaks all previous bindings and adds a new binding
Quick.Element.prototype.set = function (name, value) {
this.breakBindings(name);
if (typeof value === 'function') {
var ret = this.addBinding(name, value);
if (ret.hasBindings) {
this[name] = value;
} else {
this[name] = ret.value;
}
} else {
this[name] = value;
}
};
Quick.Element.prototype.addFunction = function (name, value) {
this[name] = value;
};
var defPropCount = 0;
var notdefPropCount = 0;
Quick.Element.prototype.addProperty = function (name, value) {
var that = this;
var valueStore;
// register property
this._properties[name] = value;
if (!this.hasOwnProperty(name)) {
Object.defineProperty(this, name, {
get: function (silent) {
// console.log("getter: ", that.id, name);
if (!silent && Quick.Engine.magicBindingState)
Quick.Engine.addCalledGetter(that, name);
if (typeof valueStore === 'function')
return valueStore.apply(that);
return valueStore;
},
set: function (val, silent) {
// console.log("setter: ", that.id, name, val);
if (valueStore === val)
return;
valueStore = val;
// connections are called like the properties
if (!silent) {
that.emit(name);
that.emit('changed');
}
Quick.Engine.dirty(that, name);
}
});
}
};
// initial set of all properties and binding evaluation
// should only be called once
Quick.Element.prototype.initializeBindings = function (options) {
var name, i;
this._initializeBindingsStep = true;
for (name in this._properties) {
if (this._properties.hasOwnProperty(name)) {
var value = this._properties[name];
// console.log("Element.initializeBindings()", this.id, name, value);
// initial set and binding discovery
if (typeof value === 'function') {
var ret = this.addBinding(name, value);
if (ret.hasBindings) {
this[name] = value;
} else {
this[name] = ret.value;
}
} else {
this[name] = value;
}
}
}
// force property being set on the elements
if (!options || !options.deferRender) {
this.render();
}
for (i in this._children) {
if (this._children.hasOwnProperty(i)) {
this._children[i].initializeBindings(options);
}
}
this._initializeBindingsStep = false;
// this calls the onload slot, if defined
this.emit("load");
};
Quick.Element.prototype.emit = function (signal) {
if (signal in this._connections) {
var slots = this._connections[signal];
for (var slot in slots) {
if (slots.hasOwnProperty(slot)) {
slots[slot].apply();
}
}
}
};