UNPKG

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
(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