cmps
Version:
cmps is not only a server tool but also a powerful tool to design & make your component/UI quickly and best.
1,568 lines (1,329 loc) • 122 kB
JavaScript
(function(factory){
// AMD
if( typeof define !== "undefined" && define["amd"] ){
define(["exports", "knockout", "$", "_"], factory);
// No module loader
}else{
factory( window["qpf"] = {}, ko, $, _);
}
})(function(_exports, ko, $, _){
/**
* almond 0.2.5 Copyright (c) 2011-2012, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license.
* see: http://github.com/jrburke/almond for details
*/
//Going sloppy to avoid 'use strict' string cost, but strict practices should
//be followed.
/*jslint sloppy: true */
/*global setTimeout: false */
var requirejs, require, define;
(function (undef) {
var main, req, makeMap, handlers,
defined = {},
waiting = {},
config = {},
defining = {},
hasOwn = Object.prototype.hasOwnProperty,
aps = [].slice;
function hasProp(obj, prop) {
return hasOwn.call(obj, prop);
}
/**
* Given a relative module name, like ./something, normalize it to
* a real name that can be mapped to a path.
* @param {String} name the relative name
* @param {String} baseName a real name that the name arg is relative
* to.
* @returns {String} normalized name
*/
function normalize(name, baseName) {
var nameParts, nameSegment, mapValue, foundMap,
foundI, foundStarMap, starI, i, j, part,
baseParts = baseName && baseName.split("/"),
map = config.map,
starMap = (map && map['*']) || {};
//Adjust any relative paths.
if (name && name.charAt(0) === ".") {
//If have a base name, try to normalize against it,
//otherwise, assume it is a top-level require that will
//be relative to baseUrl in the end.
if (baseName) {
//Convert baseName to array, and lop off the last part,
//so that . matches that "directory" and not name of the baseName's
//module. For instance, baseName of "one/two/three", maps to
//"one/two/three.js", but we want the directory, "one/two" for
//this normalization.
baseParts = baseParts.slice(0, baseParts.length - 1);
name = baseParts.concat(name.split("/"));
//start trimDots
for (i = 0; i < name.length; i += 1) {
part = name[i];
if (part === ".") {
name.splice(i, 1);
i -= 1;
} else if (part === "..") {
if (i === 1 && (name[2] === '..' || name[0] === '..')) {
//End of the line. Keep at least one non-dot
//path segment at the front so it can be mapped
//correctly to disk. Otherwise, there is likely
//no path mapping for a path starting with '..'.
//This can still fail, but catches the most reasonable
//uses of ..
break;
} else if (i > 0) {
name.splice(i - 1, 2);
i -= 2;
}
}
}
//end trimDots
name = name.join("/");
} else if (name.indexOf('./') === 0) {
// No baseName, so this is ID is resolved relative
// to baseUrl, pull off the leading dot.
name = name.substring(2);
}
}
//Apply map config if available.
if ((baseParts || starMap) && map) {
nameParts = name.split('/');
for (i = nameParts.length; i > 0; i -= 1) {
nameSegment = nameParts.slice(0, i).join("/");
if (baseParts) {
//Find the longest baseName segment match in the config.
//So, do joins on the biggest to smallest lengths of baseParts.
for (j = baseParts.length; j > 0; j -= 1) {
mapValue = map[baseParts.slice(0, j).join('/')];
//baseName segment has config, find if it has one for
//this name.
if (mapValue) {
mapValue = mapValue[nameSegment];
if (mapValue) {
//Match, update name to the new value.
foundMap = mapValue;
foundI = i;
break;
}
}
}
}
if (foundMap) {
break;
}
//Check for a star map match, but just hold on to it,
//if there is a shorter segment match later in a matching
//config, then favor over this star map.
if (!foundStarMap && starMap && starMap[nameSegment]) {
foundStarMap = starMap[nameSegment];
starI = i;
}
}
if (!foundMap && foundStarMap) {
foundMap = foundStarMap;
foundI = starI;
}
if (foundMap) {
nameParts.splice(0, foundI, foundMap);
name = nameParts.join('/');
}
}
return name;
}
function makeRequire(relName, forceSync) {
return function () {
//A version of a require function that passes a moduleName
//value for items that may need to
//look up paths relative to the moduleName
return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync]));
};
}
function makeNormalize(relName) {
return function (name) {
return normalize(name, relName);
};
}
function makeLoad(depName) {
return function (value) {
defined[depName] = value;
};
}
function callDep(name) {
if (hasProp(waiting, name)) {
var args = waiting[name];
delete waiting[name];
defining[name] = true;
main.apply(undef, args);
}
if (!hasProp(defined, name) && !hasProp(defining, name)) {
throw new Error('No ' + name);
}
return defined[name];
}
//Turns a plugin!resource to [plugin, resource]
//with the plugin being undefined if the name
//did not have a plugin prefix.
function splitPrefix(name) {
var prefix,
index = name ? name.indexOf('!') : -1;
if (index > -1) {
prefix = name.substring(0, index);
name = name.substring(index + 1, name.length);
}
return [prefix, name];
}
/**
* Makes a name map, normalizing the name, and using a plugin
* for normalization if necessary. Grabs a ref to plugin
* too, as an optimization.
*/
makeMap = function (name, relName) {
var plugin,
parts = splitPrefix(name),
prefix = parts[0];
name = parts[1];
if (prefix) {
prefix = normalize(prefix, relName);
plugin = callDep(prefix);
}
//Normalize according
if (prefix) {
if (plugin && plugin.normalize) {
name = plugin.normalize(name, makeNormalize(relName));
} else {
name = normalize(name, relName);
}
} else {
name = normalize(name, relName);
parts = splitPrefix(name);
prefix = parts[0];
name = parts[1];
if (prefix) {
plugin = callDep(prefix);
}
}
//Using ridiculous property names for space reasons
return {
f: prefix ? prefix + '!' + name : name, //fullName
n: name,
pr: prefix,
p: plugin
};
};
function makeConfig(name) {
return function () {
return (config && config.config && config.config[name]) || {};
};
}
handlers = {
require: function (name) {
return makeRequire(name);
},
exports: function (name) {
var e = defined[name];
if (typeof e !== 'undefined') {
return e;
} else {
return (defined[name] = {});
}
},
module: function (name) {
return {
id: name,
uri: '',
exports: defined[name],
config: makeConfig(name)
};
}
};
main = function (name, deps, callback, relName) {
var cjsModule, depName, ret, map, i,
args = [],
usingExports;
//Use name if no relName
relName = relName || name;
//Call the callback to define the module, if necessary.
if (typeof callback === 'function') {
//Pull out the defined dependencies and pass the ordered
//values to the callback.
//Default to [require, exports, module] if no deps
deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
for (i = 0; i < deps.length; i += 1) {
map = makeMap(deps[i], relName);
depName = map.f;
//Fast path CommonJS standard dependencies.
if (depName === "require") {
args[i] = handlers.require(name);
} else if (depName === "exports") {
//CommonJS module spec 1.1
args[i] = handlers.exports(name);
usingExports = true;
} else if (depName === "module") {
//CommonJS module spec 1.1
cjsModule = args[i] = handlers.module(name);
} else if (hasProp(defined, depName) ||
hasProp(waiting, depName) ||
hasProp(defining, depName)) {
args[i] = callDep(depName);
} else if (map.p) {
map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
args[i] = defined[depName];
} else {
throw new Error(name + ' missing ' + depName);
}
}
ret = callback.apply(defined[name], args);
if (name) {
//If setting exports via "module" is in play,
//favor that over return value and exports. After that,
//favor a non-undefined return value over exports use.
if (cjsModule && cjsModule.exports !== undef &&
cjsModule.exports !== defined[name]) {
defined[name] = cjsModule.exports;
} else if (ret !== undef || !usingExports) {
//Use the return value from the function.
defined[name] = ret;
}
}
} else if (name) {
//May just be an object definition for the module. Only
//worry about defining if have a module name.
defined[name] = callback;
}
};
requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
if (typeof deps === "string") {
if (handlers[deps]) {
//callback in this case is really relName
return handlers[deps](callback);
}
//Just return the module wanted. In this scenario, the
//deps arg is the module name, and second arg (if passed)
//is just the relName.
//Normalize module name, if it contains . or ..
return callDep(makeMap(deps, callback).f);
} else if (!deps.splice) {
//deps is a config object, not an array.
config = deps;
if (callback.splice) {
//callback is an array, which means it is a dependency list.
//Adjust args if there are dependencies
deps = callback;
callback = relName;
relName = null;
} else {
deps = undef;
}
}
//Support require(['a'])
callback = callback || function () {};
//If relName is a function, it is an errback handler,
//so remove it.
if (typeof relName === 'function') {
relName = forceSync;
forceSync = alt;
}
//Simulate async callback;
if (forceSync) {
main(undef, deps, callback, relName);
} else {
//Using a non-zero value because of concern for what old browsers
//do, and latest browsers "upgrade" to 4 if lower value is used:
//http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
//If want a value immediately, use require('id') instead -- something
//that works in almond on the global level, but not guaranteed and
//unlikely to work in other AMD implementations.
setTimeout(function () {
main(undef, deps, callback, relName);
}, 4);
}
return req;
};
/**
* Just drops the config on the floor, but returns req in case
* the config return value is used.
*/
req.config = function (cfg) {
config = cfg;
if (config.deps) {
req(config.deps, config.callback);
}
return req;
};
define = function (name, deps, callback) {
//This module may not have dependencies
if (!deps.splice) {
//deps is not an array, so probably means
//an object literal or factory function for
//the value. Adjust args.
callback = deps;
deps = [];
}
if (!hasProp(defined, name) && !hasProp(waiting, name)) {
waiting[name] = [name, deps, callback];
}
};
define.amd = {
jQuery: true
};
}());
var _define = define;
// Here if we directly use define, the app build on it will have some problem when running the optimized version
// I guess the optimizer will use regexp to detemine if the depedencies is defined in the file already
// And it find jQuery, knokcout, underscore is defined in the qpf, even if it is in closure
// and won't affect the outer environment. And optimizer won't add the dependencies file in the final optimized js file.
_define("$", [], function(){
return $;
});
_define("knockout", [], function(){
return ko;
});
_define("_", [], function(){
return _;
});
define('core/mixin/derive',['require','_'],function(require){
var _ = require("_");
/**
* derive a sub class from base class
* @makeDefaultOpt [Object|Function] default option of this sub class,
method of the sub can use this.xxx to access this option
* @initialize [Function](optional) initialize after the sub class is instantiated
* @proto [Object](optional) prototype methods/property of the sub class
*
*/
function derive(makeDefaultOpt, initialize/*optional*/, proto/*optional*/){
if( typeof initialize == "object"){
proto = initialize;
initialize = null;
}
// extend default prototype method
var extendedProto = {
// instanceof operator cannot work well,
// so we write a method to simulate it
'instanceof' : function(constructor){
var selfConstructor = sub;
while(selfConstructor){
if( selfConstructor === constructor ){
return true;
}
selfConstructor = selfConstructor.__super__;
}
}
}
var _super = this;
var sub = function(options){
// call super constructor
_super.call( this );
// call defaultOpt generate function each time
// if it is a function, So we can make sure each
// property in the object is fresh
_.extend( this, typeof makeDefaultOpt == "function" ?
makeDefaultOpt.call(this) : makeDefaultOpt );
_.extend( this, options );
if( this.constructor == sub){
// find the base class, and the initialize function will be called
// in the order of inherit
var base = sub,
initializeChain = [initialize];
while(base.__super__){
base = base.__super__;
initializeChain.unshift( base.__initialize__ );
}
for(var i = 0; i < initializeChain.length; i++){
if( initializeChain[i] ){
initializeChain[i].call( this );
}
}
}
};
// save super constructor
sub.__super__ = _super;
// initialize function will be called after all the super constructor is called
sub.__initialize__ = initialize;
// extend prototype function
_.extend( sub.prototype, _super.prototype, extendedProto, proto);
sub.prototype.constructor = sub;
// extend the derive method as a static method;
sub.derive = _super.derive;
return sub;
}
return {
derive : derive
}
});
define('core/mixin/event',[],function(){
/**
* Event interface
* + on(eventName, handler[, context])
* + trigger(eventName[, arg1[, arg2]])
* + off(eventName[, handler])
*/
return{
trigger : function(){
if( ! this.__handlers__){
return;
}
var name = arguments[0];
var params = Array.prototype.slice.call( arguments, 1 );
var handlers = this.__handlers__[ name ];
if( handlers ){
for( var i = 0; i < handlers.length; i+=2){
var handler = handlers[i],
context = handlers[i+1];
handler.apply(context || this, params);
}
}
},
on : function( target, handler, context/*optional*/ ){
if( ! target){
return;
}
var handlers = this.__handlers__ || ( this.__handlers__={} );
if( ! handlers[target] ){
handlers[target] = [];
}
if( handlers[target].indexOf(handler) == -1){
// structure in list
// [handler,context,handler,context,handler,context..]
handlers[target].push( handler );
handlers[target].push( context );
}
return handler;
},
off : function( target, handler ){
var handlers = this.__handlers__ || ( this.__handlers__={} );
if( handlers[target] ){
if( handler ){
var arr = handlers[target];
// remove handler and context
var idx = arr.indexOf(handler);
if( idx >= 0)
arr.splice( idx, 2 );
}else{
handlers[target] = [];
}
}
}
}
});
define('core/clazz',['require','./mixin/derive','./mixin/event','_'],function(require){
var deriveMixin = require("./mixin/derive");
var eventMixin = require("./mixin/event");
var _ = require("_");
var Clazz = new Function();
_.extend(Clazz, deriveMixin);
_.extend(Clazz.prototype, eventMixin);
return Clazz;
});
//=====================================
// Base class of all components
// it also provides some util methods like
//=====================================
define('base',['require','core/clazz','core/mixin/event','knockout','$','_'],function(require){
var Clazz = require("core/clazz");
var Event = require("core/mixin/event");
var ko = require("knockout");
var $ = require("$");
var _ = require("_");
var repository = {}; //repository to store all the component instance
var Base = Clazz.derive(function(){
return { // Public properties
// Name of component, will be used in the query of the component
name : "",
// Tag of wrapper element
tag : "div",
// Attribute of the wrapper element
attr : {},
// Jquery element as a wrapper
// It will be created in the constructor
$el : null,
// Attribute will be applied to self
// WARNING: It will be only used in the constructor
// So there is no need to re-assign a new viewModel when created an instance
// if property in the attribute is a observable
// it will be binded to the property in viewModel
attributes : {},
parent : null,
// ui skin
skin : "",
// Class prefix
classPrefix : "qpf-ui-",
// Skin prefix
skinPrefix : "qpf-skin-",
id : ko.observable(""),
width : ko.observable(),
class : ko.observable(),
height : ko.observable(),
visible : ko.observable(true),
disable : ko.observable(false),
style : ko.observable(""),
// If the temporary is set true,
// It will not be stored in the repository and
// will be destroyed when there are no reference any more
// Maybe a ugly solution to prevent memory leak
temporary : false,
// events list inited at first time
events : {}
}}, function(){ //constructor
this.__GUID__ = genGUID();
// add to repository
if( ! this.temporary ){
repository[this.__GUID__] = this;
}
if( ! this.$el){
this.$el = $(document.createElement(this.tag));
}
this.$el[0].setAttribute("data-qpf-guid", this.__GUID__);
this.$el.attr(this.attr);
if( this.skin ){
this.$el.addClass( this.withPrefix(this.skin, this.skinPrefix) );
}
if( this.css ){
_.each( _.union(this.css), function(className){
this.$el.addClass( this.withPrefix(className, this.classPrefix) );
}, this)
}
// Class name of wrapper element is depend on the lowercase of component type
// this.$el.addClass( this.withPrefix(this.type.toLowerCase(), this.classPrefix) );
this.width.subscribe(function(newValue){
this.$el.width(newValue);
this.onResize();
}, this);
this.height.subscribe(function(newValue){
this.$el.height(newValue);
this.onResize();
}, this);
this.disable.subscribe(function(newValue){
this.$el[newValue?"addClass":"removeClass"]("qpf-disable");
}, this);
this.id.subscribe(function(newValue){
this.$el.attr("id", newValue);
}, this);
this.class.subscribe(function(newValue){
this.$el.addClass( newValue );
}, this);
this.visible.subscribe(function(newValue){
newValue ? this.$el.show() : this.$el.hide();
}, this);
this.style.subscribe(function(newValue){
var valueSv = newValue;
var styleRegex = /(\S*?)\s*:\s*(.*)/g;
// preprocess the style string
newValue = "{" + _.chain(newValue.split(";"))
.map(function(item){
return item.replace(/(^\s*)|(\s*$)/g, "") //trim
.replace(styleRegex, '"$1":"$2"');
})
.filter(function(item){return item;})
.value().join(",") + "}";
try{
var obj = ko.utils.parseJson(newValue);
this.$el.css(obj);
}catch(e){
console.error("Syntax Error of style: "+ valueSv);
}
}, this);
// register the events before initialize
for( var name in this.events ){
var handler = this.events[name];
if( typeof(handler) == "function"){
this.on(name, handler);
}
}
// apply attribute
this._mappingAttributes( this.attributes );
this.initialize();
this.trigger("initialize");
// Here we removed auto rendering at constructor
// to support deferred rendering after the $el is attached
// to the document
// this.render();
}, {// Prototype
// Type of component. The className of the wrapper element is
// depend on the type
type : "BASE",
// Template of the component, will be applyed binging with viewModel
template : "",
// Declare the events that will be provided
// Developers can use on method to subscribe these events
// It is used in the binding handlers to judge which parameter
// passed in is events
eventsProvided : ["click", "mousedown", "mouseup", "mousemove", "resize",
"initialize", "beforerender", "render", "dispose"],
// Will be called after the component first created
initialize : function(){},
// set the attribute in the modelView
set : function(key, value){
if( typeof(key) == "string" ){
var source = {};
source[key] = value;
}else{
source = key;
};
this._mappingAttributes( source, true );
},
// Call to refresh the component
// Will trigger beforeRender and afterRender hooks
// beforeRender and afterRender hooks is mainly provide for
// the subclasses
render : function(){
this.beforeRender && this.beforeRender();
this.trigger("beforerender");
this.doRender();
this.afterRender && this.afterRender();
this.trigger("render");
// trigger the resize events
this.onResize();
},
// Default render method
doRender : function(){
this.$el.children().each(function(){
Base.disposeDom( this );
})
this.$el.html(this.template);
ko.applyBindings( this, this.$el[0] );
},
// Dispose the component instance
dispose : function(){
if( this.$el ){
// remove the dom element
this.$el.remove()
}
// remove from repository
repository[this.__GUID__] = null;
this.trigger("dispose");
},
resize : function(width, height){
if( typeof(width) === "number"){
this.width( width );
}
if( typeof(height) == "number"){
this.height( height );
}
},
onResize : function(){
this.trigger('resize');
},
withPrefix : function(className, prefix){
if( className.indexOf(prefix) != 0 ){
return prefix + className;
}
},
withoutPrefix : function(className, prefix){
if( className.indexOf(prefix) == 0){
return className.substr(prefix.length);
}
},
_mappingAttributes : function(attributes, onlyUpdate){
for(var name in attributes){
var attr = attributes[name];
var propInVM = this[name];
// create new attribute when property is not existed, even if it will not be used
if( typeof(propInVM) === "undefined" ){
var value = ko.utils.unwrapObservable(attr);
// is observableArray or plain array
if( (ko.isObservable(attr) && attr.push) ||
attr.constructor == Array){
this[name] = ko.observableArray(value);
}else{
this[name] = ko.observable(value);
}
propInVM = this[name];
}
else if( ko.isObservable(propInVM) ){
propInVM(ko.utils.unwrapObservable(attr) );
}else{
this[name] = ko.utils.unwrapObservable(attr);
}
if( ! onlyUpdate){
// Two-way data binding if the attribute is an observable
if( ko.isObservable(attr) ){
bridge(propInVM, attr);
}
}
}
}
})
// register proxy events of dom
var proxyEvents = ["click", "mousedown", "mouseup", "mousemove"];
Base.prototype.on = function(eventName){
// lazy register events
if( proxyEvents.indexOf(eventName) >= 0 ){
this.$el.unbind(eventName, proxyHandler)
.bind(eventName, {context : this}, proxyHandler);
}
Event.on.apply(this, arguments);
}
function proxyHandler(e){
var context = e.data.context;
var eventType = e.type;
context.trigger(eventType);
}
// get a unique component by guid
Base.get = function(guid){
return repository[guid];
}
Base.getByDom = function(domNode){
var guid = domNode.getAttribute("data-qpf-guid");
return Base.get(guid);
}
// dispose all the components attached in the domNode and
// its children(if recursive is set true)
Base.disposeDom = function(domNode, resursive){
if(typeof(recursive) == "undefined"){
recursive = true;
}
function dispose(node){
var guid = node.getAttribute("data-qpf-guid");
var component = Base.get(guid);
if( component ){
// do not recursive traverse the children of component
// element
// hand over dispose of sub element task to the components
// it self
component.dispose();
}else{
if( recursive ){
for(var i = 0; i < node.childNodes.length; i++){
var child = node.childNodes[i];
if( child.nodeType == 1 ){
dispose( child );
}
}
}
}
}
dispose(domNode);
}
// util function of generate a unique id
var genGUID = (function(){
var id = 0;
return function(){
return id++;
}
})();
//----------------------------
// knockout extenders
ko.extenders.numeric = function(target, precision) {
var fixer = ko.computed({
read : target,
write : function(newValue){
if( newValue === "" ){
target("");
return;
}else{
var val = parseFloat(newValue);
}
val = isNaN( val ) ? 0 : val;
var precisionValue = parseFloat( ko.utils.unwrapObservable(precision) );
if( ! isNaN( precisionValue ) ) {
var multiplier = Math.pow(10, precisionValue);
val = Math.round(val * multiplier) / multiplier;
}
target(val);
}
});
fixer( target() );
return fixer;
};
ko.extenders.clamp = function(target, options){
var min = options.min;
var max = options.max;
var clamper = ko.computed({
read : target,
write : function(value){
var minValue = parseFloat( ko.utils.unwrapObservable(min) ),
maxValue = parseFloat( ko.utils.unwrapObservable(max) );
if( ! isNaN(minValue) ){
value = Math.max(minValue, value);
}
if( ! isNaN(maxValue) ){
value = Math.min(maxValue, value);
}
target(value);
}
})
clamper( target() );
return clamper;
}
//-------------------------------------------
// Handle bingings in the knockout template
var bindings = {};
Base.provideBinding = function(name, Component ){
bindings[name] = Component;
}
Base.create = function(name, config){
var Constructor = bindings[name];
if(Constructor){
return new Constructor(config);
}
}
// provide bindings to knockout
ko.bindingHandlers["qpf"] = {
createComponent : function(element, valueAccessor){
// dispose the previous component host on the element
var prevComponent = Base.getByDom( element );
if( prevComponent ){
prevComponent.dispose();
}
var component = createComponentFromDataBinding( element, valueAccessor, bindings );
return component;
},
init : function( element, valueAccessor ){
var component = ko.bindingHandlers["qpf"].createComponent(element, valueAccessor);
component.render();
// not apply bindings to the descendant doms in the UI component
return { 'controlsDescendantBindings': true };
},
update : function( element, valueAccessor ){}
}
// append the element of view in the binding
ko.bindingHandlers["qpf_view"] = {
init : function(element, valueAccessor){
var value = valueAccessor();
var subView = ko.utils.unwrapObservable(value);
if( subView && subView.$el ){
Base.disposeDom(element);
element.parentNode.replaceChild(subView.$el[0], element);
}
// PENDING
// handle disposal (if KO removes by the template binding)
// ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
// subView.dispose();
// });
return { 'controlsDescendantBindings': true };
},
update : function(element, valueAccessor){
}
}
//-----------------------------------
// Provide plugins to jquery
$.fn.qpf = function( op, viewModel ){
op = op || "get";
if( op === "get"){
var result = [];
this.each(function(){
var item = Base.getByDom(this);
if( item ){
result.push(item);
}
})
return result;
}else if( op === "init"){
this.each(function(){
ko.applyBindings(viewModel, this);
});
return this.qpf("get");
}else if(op === "dispose"){
this.each(function(){
Base.disposeDom(this);
})
}
}
//------------------------------------
// Util functions
var unwrap = ko.utils.unwrapObservable;
function createComponentFromDataBinding(element, valueAccessor){
var value = valueAccessor();
var options = unwrap(value) || {};
var type = unwrap(options.type);
if( type ){
var Constructor = bindings[type];
if( Constructor ){
var component = createComponentFromJSON( options, Constructor)
if( component ){
element.innerHTML = "";
element.appendChild( component.$el[0] );
$(element).addClass("qpf-wrapper");
}
// save the guid in the element data attribute
element.setAttribute("data-qpf-guid", component.__GUID__);
}else{
console.error("Unkown UI type, " + type);
}
}else{
console.error("UI type is needed");
}
return component;
}
function createComponentFromJSON(options, Constructor){
var type = unwrap(options.type),
name = unwrap(options.name),
attr = _.omit(options, "type", "name");
var events = {};
// Find which property is event
_.each(attr, function(value, key){
if( key.indexOf("on") == 0 &&
Constructor.prototype.eventsProvided.indexOf(key.substr("on".length)) >= 0 &&
typeof(value) == "function"){
delete attr[key];
events[key.substr("on".length)] = value;
}
})
var component = new Constructor({
name : name || "",
attributes : attr,
events : events
});
return component;
}
// build a bridge of twe observables
// and update the value from source to target
// at first time
function bridge(target, source){
target( source() );
// Save the previous value with clone method in underscore
// In case the notification is triggered by push methods of
// Observable Array and the commonValue instance is same with new value
// instance
// Reference : `set` method in backbone
var commonValue = _.clone( target() );
target.subscribe(function(newValue){
// Knockout will always suppose the value is mutated each time it is writted
// the value which is not primitive type(like array)
// So here will cause a recurse trigger if the value is not a primitive type
// We use underscore deep compare function to evaluate if the value is changed
// PENDING : use shallow compare function?
try{
if( ! _.isEqual(commonValue, newValue) ){
commonValue = _.clone( newValue );
source(newValue);
}
}catch(e){
// Normally when source is computed value
// and it don't have a write function
console.error(e.toString());
}
})
source.subscribe(function(newValue){
try{
if( ! _.isEqual(commonValue, newValue) ){
commonValue = _.clone( newValue );
target(newValue);
}
}catch(e){
console.error(e.toString());
}
})
}
// export the interface
return Base;
});
//============================================
// Base class of all container component
//============================================
define('container/container',['require','../base','knockout','$','_'],function(require){
var Base = require("../base");
var ko = require("knockout");
var $ = require("$");
var _ = require("_");
var Container = Base.derive(function(){
return {
// all child components
children : ko.observableArray()
}
}, {
type : "CONTAINER",
css : 'container',
template : '<div data-bind="foreach:children" class="qpf-children">\
<div data-bind="qpf_view:$data"></div>\
</div>',
initialize : function(){
var self = this,
oldArray = this.children().slice();
this.children.subscribe(function(newArray){
var differences = ko.utils.compareArrays( oldArray, newArray );
_.each(differences, function(item){
// In case the dispose operation is launched by the child component
if( item.status == "added"){
item.value.on("dispose", _onItemDispose, item.value);
}else if(item.status == "deleted"){
item.value.off("dispose", _onItemDispose);
}
}, this);
});
function _onItemDispose(){
self.remove( this );
}
},
// add child component
add : function( sub ){
sub.parent = this;
this.children.push( sub );
// Resize the child to fit the parent
sub.onResize();
},
// remove child component
remove : function( sub ){
sub.parent = null;
this.children.remove( sub );
},
removeAll : function(){
_.each(this.children(), function(child){
child.parent = null;
}, this);
this.children([]);
},
children : function(){
return this.children()
},
doRender : function(){
// do render in the hierarchy from parent to child
// traverse tree in pre-order
Base.prototype.doRender.call(this);
_.each(this.children(), function(child){
child.render();
})
},
// resize when width or height is changed
onResize : function(){
// stretch the children
if( this.height() ){
this.$el.children(".qpf-children").height( this.height() );
}
// trigger the after resize event in post-order
_.each(this.children(), function(child){
child.onResize();
}, this);
Base.prototype.onResize.call(this);
},
dispose : function(){
_.each(this.children(), function(child){
child.dispose();
});
Base.prototype.dispose.call( this );
},
// get child component by name
get : function( name ){
if( ! name ){
return;
}
return _.filter( this.children(), function(item){ return item.name === name } )[0];
}
})
Container.provideBinding = Base.provideBinding;
// modify the qpf bindler
var baseBindler = ko.bindingHandlers["qpf"];
ko.bindingHandlers["qpf"] = {
init : function(element, valueAccessor, allBindingsAccessor, viewModel){
//save the child nodes before the element's innerHTML is changed in the createComponentFromDataBinding method
var childNodes = Array.prototype.slice.call(element.childNodes);
var component = baseBindler.createComponent(element, valueAccessor);
if( component && component.instanceof(Container) ){
// hold the renderring of children until parent is renderred
// If the child renders first, the element is still not attached
// to the document. So any changes of observable will not work.
// Even worse, the dependantObservable is disposed so the observable
// is detached in to the dom
// https://groups.google.com/forum/?fromgroups=#!topic/knockoutjs/aREJNrD-Miw
var subViewModel = {
'__deferredrender__' : true
}
_.extend(subViewModel, viewModel);
// initialize from the dom element
for(var i = 0; i < childNodes.length; i++){
var child = childNodes[i];
if( ko.bindingProvider.prototype.nodeHasBindings(child) ){
// Binding with the container's viewModel
ko.applyBindings(subViewModel, child);
var sub = Base.getByDom( child );
if( sub ){
component.add( sub );
}
}
}
}
if( ! viewModel['__deferredrender__']){
component.render();
}
return { 'controlsDescendantBindings': true };
},
update : function(element, valueAccessor){
baseBindler.update(element, valueAccessor);
}
}
Container.provideBinding("container", Container);
return Container;
});
//=============================================================
// application.js
//
// Container of the whole web app, mainly for monitor the resize
// event of Window and resize all the component in the app
//=============================================================
define('container/application',['require','./container','knockout','$'],function(require){
var Container = require("./container");
var ko = require("knockout");
var $ = require("$");
var Application = Container.derive(function(){
}, {
type : "APPLICATION",
css : "application",
initialize : function(){
$(window).resize( this._resize.bind(this) );
this._resize();
},
_resize : function(){
this.width( $(window).width() );
this.height( $(window).height() );
}
})
Container.provideBinding("application", Application);
return Application;
});
//===============================================
// base class of vbox and hbox
//===============================================
define('container/box',['require','./container','knockout','$','_'],function(require){
var Container = require("./container");
var ko = require("knockout");
var $ = require("$");
var _ = require("_");
var Box = Container.derive(function(){
return {
}}, {
type : 'BOX',
css : 'box',
initialize : function(){
this.children.subscribe(function(children){
this.onResize();
// resize after the child resize happens will cause recursive
// reszie problem
// _.each(children, function(child){
// child.on('resize', this.onResize, this);
// }, this)
}, this);
this.$el.css("position", "relative");
Container.prototype.initialize.call(this);
},
_getMargin : function($el){
return {
left : parseInt($el.css("marginLeft")) || 0,
top : parseInt($el.css("marginTop")) || 0,
bottom : parseInt($el.css("marginBottom")) || 0,
right : parseInt($el.css("marginRight")) || 0,
}
},
_resizeTimeout : 0,
onResize : function(){
var self = this;
// put resize in next tick,
// if multiple child have triggered the resize event
// it will do only once;
if( this._resizeTimeout ){
clearTimeout( this._resizeTimeout );
}
this._resizeTimeout = setTimeout(function(){
self.resizeChildren();
Container.prototype.onResize.call(self);
});
}
})
// Container.provideBinding("box", Box);
return Box;
});
//===============================================
// hbox layout
//
// Items of hbox can have flex and prefer two extra properties
// About this tow properties, can reference to flexbox in css3
// http://www.w3.org/TR/css3-flexbox/
// https://github.com/doctyper/flexie/blob/master/src/flexie.js
//===============================================
define('container/hbox',['require','./container','./box','knockout','$','_'],function(require){
var Container = require("./container");
var Box = require("./box");
var ko = require("knockout");
var $ = require("$");
var _ = require("_");
var hBox = Box.derive(function(){
return {
}}, {
type : 'HBOX',
css : 'hbox',
resizeChildren : function(){
var flexSum = 0;
var remainderWidth = this.$el.width();
var childrenWithFlex = [];
var marginCache = [];
var marginCacheWithFlex = [];
_.each(this.children(), function(child, idx){
var margin = this._getMargin(child.$el);
marginCache.push(margin);
// stretch the height
// (when align is stretch)
child.height( this.$el.height()-margin.top-margin.bottom );
var prefer = ko.utils.unwrapObservable( child.prefer );
// item has a prefer size;
if( prefer ){
// TODO : if the prefer size is lager than vbox size??
prefer = Math.min(prefer, remainderWidth);
child.width( prefer );
remainderWidth -= prefer+margin.left+margin.right;
}else{
var flex = parseInt(ko.utils.unwrapObservable( child.flex ) || 1);
// put it in the next step to compute
// the height based on the flex property
childrenWithFlex.push(child);
marginCacheWithFlex.push(margin);
flexSum += flex;
}
}, this);
_.each( childrenWithFlex, function(child, idx){
var margin = marginCacheWithFlex[idx];
var flex = parseInt(ko.utils.unwrapObservable( child.flex ) || 1);
var ratio = flex / flexSum;
child.width( Math.floor(remainderWidth*ratio)-margin.left-margin.right );
})
var prevWidth = 0;
_.each(this.children(), function(child, idx){
var margin = marginCache[idx];
child.$el.css({
"position" : "absolute",
"top" : '0px',
"left" : prevWidth + "px"
});
prevWidth += child.width()+margin.left+margin.right;
})
}
})
Container.provideBinding("hbox", hBox);
return hBox;
});
//=============================================
// Inline Layout
//=============================================
define('container/inline',['require','./container','knockout','$'],function(require){
var Container = require("./container");
var ko = require("knockout");
var $ = require("$");
var Inline = Container.derive({
}, {
type : "INLINE",
css : "inline",
template : '<div data-bind="foreach:children" class="qpf-children">\
<div data-bind="qpf_view:$data"></div>\
</div>\
<div style="clear:both"></div>'
})
Container.provideBinding("inline", Inline);
return Inline;
});
//===================================
// Panel
// Container has title and content
//===================================
define('container/panel',['require','./container','knockout','$'],function(require){
var Container = require("./container");
var ko = require("knockout");
var $ = require("$");
var Panel = Container.derive(function(){
return {
title : ko.observable("")
}
}, {
type : 'PANEL',
css : 'panel',
template : '<div class="qpf-panel-header">\
<div class="qpf-panel-title" data-bind="html:title"></div>\
<div class="qpf-panel-tools"></div>\
</di