lettuce
Version:
Lettuce JS, Mini Mobile Framework for Romantic with DSL.
698 lines (646 loc) • 25.5 kB
JavaScript
/*
* Copyright (C) 2012 by CoNarrative
*/
/**
* @class glu
* Core library class for GluJS
* @singleton
*/
if (window.glu != null) {
window.existingGlu = glu;
}
glu = {
/*
* * @cfg {Function} handler
*/
testMode:false,
mtypeRegistry:{},
bindingDirectiveRegistry:{},
/**
* Creates a view model and the associated view
* @param {Object} view model config
* @return {Object} the created view
*/
createViewmodelAndView:function (config, asWindow, viewMode) {
var vm;
if (config._private && config._private.isInstantiated) {
vm = config;
} else {
vm = glu.model(config);
}
var viewName = viewMode? vm.viewmodelName+'_'+viewMode : vm.viewmodelName;
var viewSpec = this.getViewSpec(vm, null, viewName);
if (glu.isString(viewSpec)) throw viewSpec;
if (vm._private && !vm._private.isInitialized) {
vm.init();
}
var view = glu.viewFromSpec(vm, viewSpec);
return view;
},
splitNs:function (vmName) {
var parts = vmName.split('.');
return {
className:parts[parts.length - 1],
namespace:parts.slice(0, parts.length - 1).join('.')
};
},
/*
* Fetches a fully configured view from a name specification readied for binding and view creation
* Returns an error string if could not process it.
*/
getViewSpec:function (vm, ns, viewmodelName, configOverlay, defaults) {
ns = ns || vm.ns;
viewmodelName = viewmodelName || vm.viewmodelName;
configOverlay = configOverlay || {};
var viewName = viewmodelName;
glu.log.debug('Creating view ' + ns + '.' + viewName);
var nsSubObj = glu.namespace(ns + '.' + glu.conventions.viewNs);
var viewSpec = nsSubObj[viewName];
if (!viewSpec || glu.isFunction(viewSpec)) { //functions are now automatically treated as factories instead of constructors...
var factory = viewSpec || nsSubObj[viewName + 'Factory'];
if (factory === undefined) {
return 'unable to find view config spec for ' + viewName;
}
configOverlay.vm = vm;
viewSpec = factory(configOverlay);
delete configOverlay.vm;
} else {
//TODO: Switch to making a prototype versus mixing properties in as
//that level of dynamism has not proven to be necessary
viewSpec = glu.deepApply({}, viewSpec);
}
if (viewSpec.parentLayout) {
var layoutName = viewSpec.parentLayout;
var layoutFactory = nsSubObj[layoutName + 'Factory']; //has to be a factory
if (layoutFactory === undefined) return "Could not find parent layout " + layoutName;
viewSpec.vm = vm;
viewSpec = layoutFactory(viewSpec);
delete viewSpec.vm;
if (viewSpec === undefined) return "Expected layout factory " + layoutName + " to return a view specification";
}
if (configOverlay) {
glu.apply(viewSpec, configOverlay);
}
if (defaults) {
glu.applyIf(viewSpec, defaults);
}
return viewSpec;
},
/*
* Initializes a view off of a view model.
* @param {glu.ViewModel} viewmodel
* @return {glu.view} The created view
*/
view:function (vm, ns, className, configOverlay, defaults, parent) {
var viewSpec = this.getViewSpec(vm, ns, className, configOverlay, defaults, parent);
if (glu.isString(viewSpec)) throw viewSpec;
var view = glu.viewFromSpec(vm, viewSpec, parent);
return view;
},
/**
* A factory function for creating a view once the view config has already been supplied.
* @private
* @param {Object} vm The view model bound to this view
* @param {Object} viewSpec The configuration for the view
* @return {Object}
*/
viewFromSpec:function (vm, viewSpecBase, parent) {
var viewSpec = viewSpecBase;
//WARNING: Does not make copy, assume viewspec is writable (should have already been cloned if from template)
//glu.deepApply(viewSpec, viewSpecBase); //always pass in a copy...
var view = glu.provider.view(vm, viewSpec, parent);
view._bindings = view._bindings || [];
view._bindings.viewmodel = vm;
return view;
},
/**
* Locate a glu provider component.
* @param {String} id The id of the component to locate
* @return {Object}
*/
getCmp:function (id) {
return glu.provider.getCmp(id);
},
S4:function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
},
/**
* Generate a guid.
* @param {String} prefix An optional prefix to start the guid with.
* @return {String}
*/
guid:function (prefix) {
return ((prefix ? prefix : 'a') + this.S4() + this.S4() + this.S4() + this.S4() + this.S4() + this.S4() + this.S4() + this.S4());
},
/**
* Returns true if the passed value is a JavaScript array, otherwise false.
* @param {Mixed} value The value to test
* @return {Boolean}
*/
isArray:function (value) {
return Object.prototype.toString.apply(value) === '[object Array]';
},
/**
* Returns true if the passed value is a JavaScript Object, otherwise false.
* @param {Mixed} value The value to test
* @return {Boolean}
*/
isObject:function (target) {
//return typeof(target)=='object';
return !!target && Object.prototype.toString.call(target) === '[object Object]';
},
/**
* Returns true if the passed object is a JavaScript date object, otherwise false.
* @param {Object} object The object to test
* @return {Boolean}
*/
isDate:function (value) {
return Object.prototype.toString.apply(value) === '[object Date]';
},
/**
* Returns true if the passed value is a JavaScript Function, otherwise false.
* @param {Mixed} value The value to test
* @return {Boolean}
*/
isFunction:function (target) {
return typeof(target) == 'function';
},
/**
* Returns true if the passed value is a string.
* @param {Mixed} value The value to test
* @return {Boolean}
*/
isString:function (target) {
return typeof(target) == 'string';
},
isNumber:function (target) {
return typeof(target) == 'number';
},
/**
* Returns true if this is an actual instantiated view model
*/
isInstantiated:function(target){
return target._private;
},
namespaces:{},
/**
* Creates namespace to be used for scoping variables and classes so that they are not global.
* Specifying the last node of a namespace implicitly creates all other nodes. Usage:
* <pre><code>
Ext.namespace('company.app1')
company.app1.Widget = function() { ... }
</code></pre>
* @param {String} namespace
* @return {Object} The namespace object.
*/
namespace:function (str) {
//if (this.namespaces[str]) return this.namespaces[str];
var tokens = str.split('\.');
var root = window;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
var existingChild = root[token];
if (existingChild === undefined) {
existingChild = {};
root[token] = existingChild;
}
root = existingChild;
}
this.namespaces[str] = root;
return root;
},
/**
* Walks a long object path (foo.bar.prop) without using eval to find the value at the end.
* Unlike namespace will not create the path if it does not exist
* @return {Object} the path, or null if it doesn't exist
*/
walk:function (str, root) {
if (str==null) return null;
var tokens = str.split('\.');
root = root || window;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
var existingChild = root[token];
if (existingChild === undefined) {
return null;
}
root = existingChild;
}
return root;
},
/**
* Copies all the properties of config to obj.
* @param {Object} target The receiver of the properties
* @param {Object} src The source of the properties
* @param {Object} defaults A different object that will also be applied for default values
* @return {Object} returns target
*/
apply:function (obj, config, defaults) {
return glu.provider.apply(obj, config, defaults);
},
/**
* Copies all the properties of config to obj, skipping if the property already exists on the target.
* @param {Object} target The receiver of the properties
* @param {Object} src The source of the properties
* @param {Object} defaults A different object that will also be applied for default values
* @return {Object} returns target
*/
applyIf:function (obj, config, defaults) {
return Ext.applyIf(obj, config, defaults);
},
/**
* Deep clones an object property by property from a source to a target
* @param target
* @param src
* @param noOverwrite
* @return {Object} the target
*/
deepApply:function (target, config, noOverwrite) {
for (var propName in config) {
var propValue = config[propName];
if (glu.isObject(propValue) &&
propName !== "parentVM" && //parent view model
propName !== 'rootVM' && //root view model
propName !== 'parentList' && //parent list
propName !== 'meta' && //don't remember
propName !== 'ownerCt' &&
!glu.isInstantiated(propValue) //make sure this isn't a glu object
)
{
// if (propValue.constructor!==Object.prototype.constructor) {
// debugger;
// throw 'Please only use raw objects to configure a view or view model, not other glu objects';
// }
target[propName] = target[propName] || {};
glu.deepApply(target[propName], propValue, noOverwrite);
continue;
}
if (noOverwrite && target.hasOwnProperty(propName)) continue;
if (glu.isArray(propValue)) {
var newArray = [];
for (var i = 0; i < propValue.length; i++) {
var item = propValue[i];
if (glu.isObject(item)) {
var newtarget = {};
glu.deepApply(newtarget, item, noOverwrite);
newArray.push(newtarget);
} else {
newArray.push(item);
}
}
target[propName] = newArray;
continue;
}
//primitive or function
target[propName] = propValue;
}
return target;
},
/**
* Deep clones an object property by property from a source to a target, skipping if the target property exists
* @param target
* @param src
* @param noOverwrite
* @return {Object} the target
*/
deepApplyIf:function (obj, config) {
return glu.deepApply(obj, config, true)
},
/*
* Mixins or Traits are object snippets with properties and behavior that are added into an object
* Unlike extjs plugins, they are parameterless and have a correctly scoped 'this' on initialization
* In other words, they really are "mixed in" to the javascript object
* This eliminates much of the syntactic cruft of trying to make javascript 'object-oriented'
* While still giving the same basic feel
* Mixins are the preferred way to go when you can do it as it makes things very simple...
*/
traitReg:{},
regTrait:function (name, trait) {
this.traitReg[name] = trait;
},
mixin:function () {
var target = arguments[0];
for (var i = 1; i < arguments.length; i++) {
this.mixinSingleTrait(target, arguments[i]);
}
},
mixinSingleTrait:function (target, traitName) {
glu.log.info('asked to mixin trait ' + traitName);
if (!glu.isString(traitName)) {
throw new Error("You must pass in the short string name of the trait, not the trait itself");
}
if (target.traits === undefined) {
target.traits = {};
}
if (target.traits[traitName] != null) {
return;
}
var trait = new this.traitReg[traitName];
if (trait === undefined) {
throw new Error("no such trait '" + traitName + "' exists");
}
if (trait.requiresTrait != null) {
glu.log.info('processing ' + trait.requiresTrait.length + ' requirements');
for (var i = 0; i < trait.requiresTrait.length; i++) {
var traitRef = trait.requiresTrait[i];
this.mixin(target, traitRef);
}
}
//apply everything...
var stripped = glu.apply({}, trait); //clone
var initTrait = stripped.initTrait; //some traits have an initializer
delete stripped.initTrait; //don't mixin the init funciton!
delete stripped.requiresTrait;
this.apply(target, trait);
glu.log.info(initTrait);
if (initTrait != null) {
initTrait.call(target);
}
target.traits[trait] = true;
},
mreg:function (mtype, constructor) {
this.mtypeRegistry[mtype] = constructor;
},
localizer:function (config) { //logic is not exposed for now
var nsGlobal = glu.namespace(config.ns + '.' + glu.conventions.localeNs);
if (nsGlobal === undefined) throw new Error('Could not find locale for namespace ' + config.ns);
var viewSpecific = config.viewmodel ? (nsGlobal[config.viewmodel.viewmodelName] || nsGlobal[config.viewmodel.recType] || {}) : {};
var key = config.key;
var value = viewSpecific[key] ||
nsGlobal[key] ||
glu.conventions.asLocaleKey(key);
if (value.indexOf('{') > -1) {
value = glu.string(value).format(config.params, config.viewmodel);
}
return value;
},
/**
* Localizes based on either simple key lookup or with an array of parameters
* @param config
* @param ns
* @param viewmodel
* @param params
* @return {*}
*/
localize:function (key, cfg) {
if (glu.isObject(key)) {
cfg = key;
} else {
cfg.key = key;
}
if( cfg.key.indexOf(glu.conventions.localizeStart) == 0 && glu.symbol(cfg.key).endsWith(glu.conventions.localizeEnd) ){
cfg.key = cfg.key.substring(glu.conventions.localizeStart.length, cfg.key.length - glu.conventions.localizeEnd.length);
}
cfg.ns = cfg.ns || cfg.viewmodel.ns;
cfg.params = cfg.params || {};
return this.localizer(cfg);
},
/**
* Sets the default localizer
* @param fn The localize function (see localize for its signature)
*/
setLocalizer:function (fn) {
this.localizer = fn;
},
confirm:function (title, message, fn, scope) {
return glu.provider.confirm(title, message, fn, scope);
},
message:function (title, message, fn, scope) {
return glu.provider.message(title, message, fn, scope);
},
prompt:function (title, message, fn, scope) {
return glu.provider.prompt(title, message, fn, scope);
},
/**
* Registers a GluJS view adapter
* @param name {String} the name of the adapter (the xtype if a component adapter)
* @param adatper {Object} the adapter definition
*/
regAdapter:function (name, adapter) {
return glu.provider.regAdapter(name, adapter);
},
regBindingDirective:function (name, bindingDirective) {
this.bindingDirectiveRegistry[name] = bindingDirective;
},
/*
* @{foo} //bind to foo if available, throw exception if cannot find
* @?{foo} //bind to foo if available, ignore if not
* @{!foo} //bind to inverse of foo if a boolean
* I am @{foo}; hear me roar //make a string substitution
* @>{foo} //oneway binding from model to view, do not track view back to model
* @1{foo} //bind one-time but do not track changes to foo
*/
parseBindingSyntax:function (bindingString) {
if (!glu.isString(bindingString)) {
return null; //not a binding
}
if ((bindingString.indexOf(glu.conventions.startDelimiter) == -1 && bindingString.indexOf(glu.conventions.localizeStart) == -1)) {
return null; //not using bind syntax and not using localization
}
if ((bindingString.indexOf(glu.conventions.endDelimiter) == -1 && bindingString.indexOf(glu.conventions.localizeEnd) == -1)) {
return {
valid:false,
bindExpression:bindingString,
reason:'Syntax Error: Missing closing delimiter'
};
}
var results = null;
if (bindingString.indexOf(glu.conventions.localizeStart) == 0) {
return {
valid:true,
bindExpression:bindingString,
localizationKey:bindingString.substring(glu.conventions.localizeStart.length, bindingString.length - glu.conventions.localizeEnd.length)
};
}
var boundObjectRegx = new RegExp(glu.conventions.bindingSymbol + "(.*?)\\" + glu.conventions.startDelimiter);
var boundObjectResults = bindingString.match(boundObjectRegx);
if (glu.isArray(boundObjectResults) && boundObjectResults.length > 0) {
var directive = boundObjectResults[boundObjectResults.length - 1];
if (!directive) {
directive = 'tocontrol';
}
for (var k in this.bindingDirectiveRegistry) {
var symbols = this.bindingDirectiveRegistry[k].symbols;
for (var i = 0; i < symbols.length; i++) {
if (symbols[i] == directive) {
results = glu.apply({valid:true}, this.bindingDirectiveRegistry[k]);
var directiveRegx = new RegExp("\\" + glu.conventions.startDelimiter + "(.*?)\\" + glu.conventions.endDelimiter);
var directiveResults = bindingString.match(directiveRegx);
if (glu.isArray(directiveResults) && directiveResults.length > 0) {
results.bindExpression = directiveResults[directiveResults.length - 1];
}
results.isFormula = false;
var startDelimiterLocation = bindingString.indexOf(glu.conventions.bindingSymbol);
var endDelimiterLocation = bindingString.indexOf(glu.conventions.endDelimiter);
if (startDelimiterLocation > 0 || endDelimiterLocation < bindingString.length - glu.conventions.endDelimiter.length) {
//it's a 'inline string.format'
//ONLY SUPPORT A SINGLE PROPERTY for now...
results.isFormula = true;
results.prefix = bindingString.substring(0, startDelimiterLocation);
results.suffix = bindingString.substring(endDelimiterLocation + 1);
}
// console.log(glu.provider.json.stringify(results));
return results;
}
}
}
}
return results;
},
openWindow:function (config, viewModel) {
return glu.provider.openWindow(config, viewModel);
},
/**
* Creates a glu ViewPort
*/
viewport : function(config){
return glu.provider.viewport(config);
},
panel:function () {
return glu.provider.panel.apply(glu.provider, arguments);
},
equivalent:function (oldVal, newVal) {
if ((oldVal == null && newVal != null) || (oldVal != null && newVal == null)) return false
if (glu.isArray(newVal)){
//array equivalency is if all the members are equivalent...
if (oldVal==newVal){
return true;
}
if (oldVal.length!=newVal.length) return false;
for (var i=0;i<oldVal.length;i++){
if (oldVal[i]!=newVal[i]) return false;
}
return true;
}
if (glu.isObject(newVal)) {
if (newVal == oldVal) {//by reference
return true;
}
//do comparison?
// try {
// return JSON.stringify(oldVal) == JSON.stringify(newVal);
// } catch (excp) {
// return false; //if cannot stringify, then does not count...
// }
}
return oldVal === newVal;
},
setTestMode:function () {
this.testMode = true;
},
validations:{
notEmpty:function (prop) {
return function () {
var val = this.get(prop);
if (val == null || val == '') {
return 'This field is required.';
}
return true;
}
}
},
widget:function (config) {
return glu.provider.widget(config);
},
_splitReference:function (fqname) {
var splitAt = fqname.lastIndexOf('\.');
if (splitAt === -1) {
throw new Error("Reference '" + fqname + "' requires a namespace");
}
return {
ns:fqname.substring(0, splitAt),
name:fqname.substring(splitAt + 1)
};
},
def:function (fqname, config, location) {
var capture = this._splitReference(fqname, config, location);
var nsObj = glu.ns(capture.ns + '.' + location);
nsObj[capture.name] = config;
},
/**
* Defines a view model (see Glu.ViewModel)
*
* @param {String} fqname The name of the view model
* @param {Object} config The view model configuration
*
*/
defModel:function (fqname, config) {
this.def(fqname, config, glu.conventions.viewmodelNs);
},
/**
* Defines a view according to the provider (e.g. ExtJS) using the provider's declarative JSON syntax
* @param {String} fqname The name of the view
* @param {String} viewmode (optional) Indicates the mode of this view
* @param {String} config The declarative configuration of the view
*/
defView:function (fqname, viewmode, config) {
if (glu.isString(viewmode)){
fqname = fqname + '_' + viewmode;
} else {
config = viewmode;
}
this.def(fqname, config, glu.conventions.viewNs);
},
extend:function (baseConstructor, classDef) {
var constructor = classDef.constructor === Object ? function () {
baseConstructor.apply(this, arguments);
} : classDef.constructor;
//dummy function to serve as temporary constructor so that we don't invoke actual base constructor until parent constructor chooses
var tempBaseConstructor = function () {
};
tempBaseConstructor.prototype = baseConstructor.prototype;
constructor.prototype = new tempBaseConstructor();
//for chaining within the child constructor function
constructor.superclass = baseConstructor.prototype;
constructor.prototype.constructor = constructor;
delete classDef.constructor;
glu.apply(constructor.prototype, classDef);
return constructor;
},
define:function (name, classDef) {
var baseCls = glu.walk(classDef.extend) || function(){};
var cls = glu.extend (baseCls, classDef);
var ref = glu._splitReference(name);
glu.ns(ref.ns)[ref.name] = cls;
return cls;
},
/**
* Informs glu that the UI is about to be changed. Used for accumulating UI changes (like
* suspending layouts in ExtJS until the thread is done)
*/
updatingUI : function(){
glu.provider.updatingUI();
},
getDataTypeOf:function (value) {
if (glu.isString(value)) {
type = 'string';
}
else if (glu.isNumber(value)) {
type = 'int';
}
else if (Ext.isBoolean(value)) {
type = 'boolean';
}
else if (glu.isObject(value)) {
type = 'object';
}
else if (Ext.isDate(value)) {
type = 'date';
}
return type;
},
plugin : function(name) {
this.plugins.push(name);
},
plugins:[]
};
if (window.existingGlu) {
glu.provider = existingGlu.provider;
glu.apply(glu,existingGlu);
delete window.existingGlu;
}
glu.ns = glu.namespace; //alias
//register the "empty" panel that can be configured at run time.
if (Ext.getProvider().provider != 'touch') {
glu.panel('glupanel', {});
}