Skip to content

Instantly share code, notes, and snippets.

@ecentinela
Created February 24, 2012 09:13
Show Gist options
  • Save ecentinela/1899680 to your computer and use it in GitHub Desktop.
Save ecentinela/1899680 to your computer and use it in GitHub Desktop.
jmvc3.2.2
(function( $ ) {
// Several of the methods in this plugin use code adapated from Prototype
// Prototype JavaScript framework, version 1.6.0.1
// (c) 2005-2007 Sam Stephenson
var regs = {
undHash: /_|-/,
colons: /::/,
words: /([A-Z]+)([A-Z][a-z])/g,
lowUp: /([a-z\d])([A-Z])/g,
dash: /([a-z\d])([A-Z])/g,
replacer: /\{([^\}]+)\}/g,
dot: /\./
},
// gets the nextPart property from current
// add - if true and nextPart doesnt exist, create it as an empty object
getNext = function(current, nextPart, add){
return current[nextPart] !== undefined ? current[nextPart] : ( add && (current[nextPart] = {}) );
},
// returns true if the object can have properties (no nulls)
isContainer = function(current){
var type = typeof current;
return current && ( type == 'function' || type == 'object' );
},
// a reference
getObject,
/**
* @class jQuery.String
* @parent jquerymx.lang
*
* A collection of useful string helpers. Available helpers are:
* <ul>
* <li>[jQuery.String.capitalize|capitalize]: Capitalizes a string (some_string &raquo; Some_string)</li>
* <li>[jQuery.String.camelize|camelize]: Capitalizes a string from something undercored
* (some_string &raquo; someString, some-string &raquo; someString)</li>
* <li>[jQuery.String.classize|classize]: Like [jQuery.String.camelize|camelize],
* but the first part is also capitalized (some_string &raquo; SomeString)</li>
* <li>[jQuery.String.niceName|niceName]: Like [jQuery.String.classize|classize], but a space separates each 'word' (some_string &raquo; Some String)</li>
* <li>[jQuery.String.underscore|underscore]: Underscores a string (SomeString &raquo; some_string)</li>
* <li>[jQuery.String.sub|sub]: Returns a string with {param} replaced values from data.
* <code><pre>
* $.String.sub("foo {bar}",{bar: "far"})
* //-> "foo far"</pre></code>
* </li>
* </ul>
*
*/
str = $.String = $.extend( $.String || {} , {
/**
* @function getObject
* Gets an object from a string. It can also modify objects on the
* 'object path' by removing or adding properties.
*
* Foo = {Bar: {Zar: {"Ted"}}}
* $.String.getObject("Foo.Bar.Zar") //-> "Ted"
*
* @param {String} name the name of the object to look for
* @param {Array} [roots] an array of root objects to look for the
* name. If roots is not provided, the window is used.
* @param {Boolean} [add] true to add missing objects to
* the path. false to remove found properties. undefined to
* not modify the root object
* @return {Object} The object.
*/
getObject : getObject = function( name, roots, add ) {
// the parts of the name we are looking up
// ['App','Models','Recipe']
var parts = name ? name.split(regs.dot) : [],
length = parts.length,
current,
ret,
i,
r = 0,
type;
// make sure roots is an array
roots = $.isArray(roots) ? roots : [roots || window];
if(length == 0){
return roots[0];
}
// for each root, mark it as current
while( current = roots[r++] ) {
// walk current to the 2nd to last object
// or until there is not a container
for (i =0; i < length - 1 && isContainer(current); i++ ) {
current = getNext(current, parts[i], add);
}
// if we can get a property from the 2nd to last object
if( isContainer(current) ) {
// get (and possibly set) the property
ret = getNext(current, parts[i], add);
// if there is a value, we exit
if( ret !== undefined ) {
// if add is false, delete the property
if ( add === false ) {
delete current[parts[i]];
}
return ret;
}
}
}
},
/**
* Capitalizes a string
* @param {String} s the string.
* @return {String} a string with the first character capitalized.
*/
capitalize: function( s, cache ) {
return s.charAt(0).toUpperCase() + s.substr(1);
},
/**
* Capitalizes a string from something undercored. Examples:
* @codestart
* jQuery.String.camelize("one_two") //-> "oneTwo"
* "three-four".camelize() //-> threeFour
* @codeend
* @param {String} s
* @return {String} a the camelized string
*/
camelize: function( s ) {
s = str.classize(s);
return s.charAt(0).toLowerCase() + s.substr(1);
},
/**
* Like [jQuery.String.camelize|camelize], but the first part is also capitalized
* @param {String} s
* @return {String} the classized string
*/
classize: function( s , join) {
var parts = s.split(regs.undHash),
i = 0;
for (; i < parts.length; i++ ) {
parts[i] = str.capitalize(parts[i]);
}
return parts.join(join || '');
},
/**
* Like [jQuery.String.classize|classize], but a space separates each 'word'
* @codestart
* jQuery.String.niceName("one_two") //-> "One Two"
* @codeend
* @param {String} s
* @return {String} the niceName
*/
niceName: function( s ) {
return str.classize(s,' ');
},
/**
* Underscores a string.
* @codestart
* jQuery.String.underscore("OneTwo") //-> "one_two"
* @codeend
* @param {String} s
* @return {String} the underscored string
*/
underscore: function( s ) {
return s.replace(regs.colons, '/').replace(regs.words, '$1_$2').replace(regs.lowUp, '$1_$2').replace(regs.dash, '_').toLowerCase();
},
/**
* Returns a string with {param} replaced values from data.
*
* $.String.sub("foo {bar}",{bar: "far"})
* //-> "foo far"
*
* @param {String} s The string to replace
* @param {Object} data The data to be used to look for properties. If it's an array, multiple
* objects can be used.
* @param {Boolean} [remove] if a match is found, remove the property from the object
*/
sub: function( s, data, remove ) {
var obs = [],
remove = typeof remove == 'boolean' ? !remove : remove;
obs.push(s.replace(regs.replacer, function( whole, inside ) {
//convert inside to type
var ob = getObject(inside, data, remove);
// if a container, push into objs (which will return objects found)
if( isContainer(ob) ){
obs.push(ob);
return "";
}else{
return ""+ob;
}
}));
return obs.length <= 1 ? obs[0] : obs;
},
_regs : regs
});
})(jQuery);
(function( $ ) {
// =============== HELPERS =================
// if we are initializing a new class
var initializing = false,
makeArray = $.makeArray,
isFunction = $.isFunction,
isArray = $.isArray,
extend = $.extend,
getObject = $.String.getObject,
concatArgs = function(arr, args){
return arr.concat(makeArray(args));
},
// tests if we can get super in .toString()
fnTest = /xyz/.test(function() {
xyz;
}) ? /\b_super\b/ : /.*/,
// overwrites an object with methods, sets up _super
// newProps - new properties
// oldProps - where the old properties might be
// addTo - what we are adding to
inheritProps = function( newProps, oldProps, addTo ) {
addTo = addTo || newProps
for ( var name in newProps ) {
// Check if we're overwriting an existing function
addTo[name] = isFunction(newProps[name]) &&
isFunction(oldProps[name]) &&
fnTest.test(newProps[name]) ? (function( name, fn ) {
return function() {
var tmp = this._super,
ret;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = oldProps[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, newProps[name]) : newProps[name];
}
},
STR_PROTOTYPE = 'prototype'
/**
* @class jQuery.Class
* @plugin jquery/class
* @parent jquerymx
* @download dist/jquery/jquery.class.js
* @test jquery/class/qunit.html
* @description Easy inheritance in JavaScript.
*
* Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between
* jQuery's functional programming style and Object Oriented Programming. It
* is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class]
* Inheritance library. Besides prototypal inheritance, it includes a few important features:
*
* - Static inheritance
* - Introspection
* - Namespaces
* - Setup and initialization methods
* - Easy callback function creation
*
*
* The [mvc.class Get Started with jQueryMX] has a good walkthrough of $.Class.
*
* ## Static v. Prototype
*
* Before learning about Class, it's important to
* understand the difference between
* a class's __static__ and __prototype__ properties.
*
* //STATIC
* MyClass.staticProperty //shared property
*
* //PROTOTYPE
* myclass = new MyClass()
* myclass.prototypeMethod() //instance method
*
* A static (or class) property is on the Class constructor
* function itself
* and can be thought of being shared by all instances of the
* Class. Prototype propertes are available only on instances of the Class.
*
* ## A Basic Class
*
* The following creates a Monster class with a
* name (for introspection), static, and prototype members.
* Every time a monster instance is created, the static
* count is incremented.
*
* @codestart
* $.Class('Monster',
* /* @static *|
* {
* count: 0
* },
* /* @prototype *|
* {
* init: function( name ) {
*
* // saves name on the monster instance
* this.name = name;
*
* // sets the health
* this.health = 10;
*
* // increments count
* this.constructor.count++;
* },
* eat: function( smallChildren ){
* this.health += smallChildren;
* },
* fight: function() {
* this.health -= 2;
* }
* });
*
* hydra = new Monster('hydra');
*
* dragon = new Monster('dragon');
*
* hydra.name // -> hydra
* Monster.count // -> 2
* Monster.shortName // -> 'Monster'
*
* hydra.eat(2); // health = 12
*
* dragon.fight(); // health = 8
*
* @codeend
*
*
* Notice that the prototype <b>init</b> function is called when a new instance of Monster is created.
*
*
* ## Inheritance
*
* When a class is extended, all static and prototype properties are available on the new class.
* If you overwrite a function, you can call the base class's function by calling
* <code>this._super</code>. Lets create a SeaMonster class. SeaMonsters are less
* efficient at eating small children, but more powerful fighters.
*
*
* Monster("SeaMonster",{
* eat: function( smallChildren ) {
* this._super(smallChildren / 2);
* },
* fight: function() {
* this.health -= 1;
* }
* });
*
* lockNess = new SeaMonster('Lock Ness');
* lockNess.eat(4); //health = 12
* lockNess.fight(); //health = 11
*
* ### Static property inheritance
*
* You can also inherit static properties in the same way:
*
* $.Class("First",
* {
* staticMethod: function() { return 1;}
* },{})
*
* First("Second",{
* staticMethod: function() { return this._super()+1;}
* },{})
*
* Second.staticMethod() // -> 2
*
* ## Namespaces
*
* Namespaces are a good idea! We encourage you to namespace all of your code.
* It makes it possible to drop your code into another app without problems.
* Making a namespaced class is easy:
*
*
* $.Class("MyNamespace.MyClass",{},{});
*
* new MyNamespace.MyClass()
*
*
* <h2 id='introspection'>Introspection</h2>
*
* Often, it's nice to create classes whose name helps determine functionality. Ruby on
* Rails's [http://api.rubyonrails.org/classes/ActiveRecord/Base.html|ActiveRecord] ORM class
* is a great example of this. Unfortunately, JavaScript doesn't have a way of determining
* an object's name, so the developer must provide a name. Class fixes this by taking a String name for the class.
*
* $.Class("MyOrg.MyClass",{},{})
* MyOrg.MyClass.shortName //-> 'MyClass'
* MyOrg.MyClass.fullName //-> 'MyOrg.MyClass'
*
* The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's
* static properties.
*
*
* ## Setup and initialization methods
*
* <p>
* Class provides static and prototype initialization functions.
* These come in two flavors - setup and init.
* Setup is called before init and
* can be used to 'normalize' init's arguments.
* </p>
* <div class='whisper'>PRO TIP: Typically, you don't need setup methods in your classes. Use Init instead.
* Reserve setup methods for when you need to do complex pre-processing of your class before init is called.
*
* </div>
* @codestart
* $.Class("MyClass",
* {
* setup: function() {} //static setup
* init: function() {} //static constructor
* },
* {
* setup: function() {} //prototype setup
* init: function() {} //prototype constructor
* })
* @codeend
*
* ### Setup
*
* Setup functions are called before init functions. Static setup functions are passed
* the base class followed by arguments passed to the extend function.
* Prototype static functions are passed the Class constructor
* function arguments.
*
* If a setup function returns an array, that array will be used as the arguments
* for the following init method. This provides setup functions the ability to normalize
* arguments passed to the init constructors. They are also excellent places
* to put setup code you want to almost always run.
*
*
* The following is similar to how [jQuery.Controller.prototype.setup]
* makes sure init is always called with a jQuery element and merged options
* even if it is passed a raw
* HTMLElement and no second parameter.
*
* $.Class("jQuery.Controller",{
* ...
* },{
* setup: function( el, options ) {
* ...
* return [$(el),
* $.extend(true,
* this.Class.defaults,
* options || {} ) ]
* }
* })
*
* Typically, you won't need to make or overwrite setup functions.
*
* ### Init
*
* Init functions are called after setup functions.
* Typically, they receive the same arguments
* as their preceding setup function. The Foo class's <code>init</code> method
* gets called in the following example:
*
* $.Class("Foo", {
* init: function( arg1, arg2, arg3 ) {
* this.sum = arg1+arg2+arg3;
* }
* })
* var foo = new Foo(1,2,3);
* foo.sum //-> 6
*
* ## Proxies
*
* Similar to jQuery's proxy method, Class provides a
* [jQuery.Class.static.proxy proxy]
* function that returns a callback to a method that will always
* have
* <code>this</code> set to the class or instance of the class.
*
*
* The following example uses this.proxy to make sure
* <code>this.name</code> is available in <code>show</code>.
*
* $.Class("Todo",{
* init: function( name ) {
* this.name = name
* },
* get: function() {
* $.get("/stuff",this.proxy('show'))
* },
* show: function( txt ) {
* alert(this.name+txt)
* }
* })
* new Todo("Trash").get()
*
* Callback is available as a static and prototype method.
*
* ## Demo
*
* @demo jquery/class/class.html
*
*
* @constructor
*
* To create a Class call:
*
* $.Class( [NAME , STATIC,] PROTOTYPE ) -> Class
*
* <div class='params'>
* <div class='param'><label>NAME</label><code>{optional:String}</code>
* <p>If provided, this sets the shortName and fullName of the
* class and adds it and any necessary namespaces to the
* window object.</p>
* </div>
* <div class='param'><label>STATIC</label><code>{optional:Object}</code>
* <p>If provided, this creates static properties and methods
* on the class.</p>
* </div>
* <div class='param'><label>PROTOTYPE</label><code>{Object}</code>
* <p>Creates prototype methods on the class.</p>
* </div>
* </div>
*
* When a Class is created, the static [jQuery.Class.static.setup setup]
* and [jQuery.Class.static.init init] methods are called.
*
* To create an instance of a Class, call:
*
* new Class([args ... ]) -> instance
*
* The created instance will have all the
* prototype properties and methods defined by the PROTOTYPE object.
*
* When an instance is created, the prototype [jQuery.Class.prototype.setup setup]
* and [jQuery.Class.prototype.init init] methods
* are called.
*/
clss = $.Class = function() {
if (arguments.length) {
clss.extend.apply(clss, arguments);
}
};
/* @Static*/
extend(clss, {
/**
* @function proxy
* Returns a callback function for a function on this Class.
* Proxy ensures that 'this' is set appropriately.
* @codestart
* $.Class("MyClass",{
* getData: function() {
* this.showing = null;
* $.get("data.json",this.proxy('gotData'),'json')
* },
* gotData: function( data ) {
* this.showing = data;
* }
* },{});
* MyClass.showData();
* @codeend
* <h2>Currying Arguments</h2>
* Additional arguments to proxy will fill in arguments on the returning function.
* @codestart
* $.Class("MyClass",{
* getData: function( <b>callback</b> ) {
* $.get("data.json",this.proxy('process',<b>callback</b>),'json');
* },
* process: function( <b>callback</b>, jsonData ) { //callback is added as first argument
* jsonData.processed = true;
* callback(jsonData);
* }
* },{});
* MyClass.getData(showDataFunc)
* @codeend
* <h2>Nesting Functions</h2>
* Proxy can take an array of functions to call as
* the first argument. When the returned callback function
* is called each function in the array is passed the return value of the prior function. This is often used
* to eliminate currying initial arguments.
* @codestart
* $.Class("MyClass",{
* getData: function( callback ) {
* //calls process, then callback with value from process
* $.get("data.json",this.proxy(['process2',callback]),'json')
* },
* process2: function( type,jsonData ) {
* jsonData.processed = true;
* return [jsonData];
* }
* },{});
* MyClass.getData(showDataFunc);
* @codeend
* @param {String|Array} fname If a string, it represents the function to be called.
* If it is an array, it will call each function in order and pass the return value of the prior function to the
* next function.
* @return {Function} the callback function.
*/
proxy: function( funcs ) {
//args that should be curried
var args = makeArray(arguments),
self;
// get the functions to callback
funcs = args.shift();
// if there is only one function, make funcs into an array
if (!isArray(funcs) ) {
funcs = [funcs];
}
// keep a reference to us in self
self = this;
return function class_cb() {
// add the arguments after the curried args
var cur = concatArgs(args, arguments),
isString,
length = funcs.length,
f = 0,
func;
// go through each function to call back
for (; f < length; f++ ) {
func = funcs[f];
if (!func ) {
continue;
}
// set called with the name of the function on self (this is how this.view works)
isString = typeof func == "string";
if ( isString && self._set_called ) {
self.called = func;
}
// call the function
cur = (isString ? self[func] : func).apply(self, cur || []);
// pass the result to the next function (if there is a next function)
if ( f < length - 1 ) {
cur = !isArray(cur) || cur._use_call ? [cur] : cur
}
}
return cur;
}
},
/**
* @function newInstance
* Creates a new instance of the class. This method is useful for creating new instances
* with arbitrary parameters.
* <h3>Example</h3>
* @codestart
* $.Class("MyClass",{},{})
* var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10))
* @codeend
* @return {class} instance of the class
*/
newInstance: function() {
// get a raw instance objet (init is not called)
var inst = this.rawInstance(),
args;
// call setup if there is a setup
if ( inst.setup ) {
args = inst.setup.apply(inst, arguments);
}
// call init if there is an init, if setup returned args, use those as the arguments
if ( inst.init ) {
inst.init.apply(inst, isArray(args) ? args : arguments);
}
return inst;
},
/**
* Setup gets called on the inherting class with the base class followed by the
* inheriting class's raw properties.
*
* Setup will deeply extend a static defaults property on the base class with
* properties on the base class. For example:
*
* $.Class("MyBase",{
* defaults : {
* foo: 'bar'
* }
* },{})
*
* MyBase("Inheriting",{
* defaults : {
* newProp : 'newVal'
* }
* },{}
*
* Inheriting.defaults -> {foo: 'bar', 'newProp': 'newVal'}
*
* @param {Object} baseClass the base class that is being inherited from
* @param {String} fullName the name of the new class
* @param {Object} staticProps the static properties of the new class
* @param {Object} protoProps the prototype properties of the new class
*/
setup: function( baseClass, fullName ) {
// set defaults as the merger of the parent defaults and this object's defaults
this.defaults = extend(true, {}, baseClass.defaults, this.defaults);
return arguments;
},
rawInstance: function() {
// prevent running init
initializing = true;
var inst = new this();
initializing = false;
// allow running init
return inst;
},
/**
* Extends a class with new static and prototype functions. There are a variety of ways
* to use extend:
*
* // with className, static and prototype functions
* $.Class('Task',{ STATIC },{ PROTOTYPE })
* // with just classname and prototype functions
* $.Class('Task',{ PROTOTYPE })
* // with just a className
* $.Class('Task')
*
* You no longer have to use <code>.extend</code>. Instead, you can pass those options directly to
* $.Class (and any inheriting classes):
*
* // with className, static and prototype functions
* $.Class('Task',{ STATIC },{ PROTOTYPE })
* // with just classname and prototype functions
* $.Class('Task',{ PROTOTYPE })
* // with just a className
* $.Class('Task')
*
* @param {String} [fullName] the classes name (used for classes w/ introspection)
* @param {Object} [klass] the new classes static/class functions
* @param {Object} [proto] the new classes prototype functions
*
* @return {jQuery.Class} returns the new class
*/
extend: function( fullName, klass, proto ) {
// figure out what was passed and normalize it
if ( typeof fullName != 'string' ) {
proto = klass;
klass = fullName;
fullName = null;
}
if (!proto ) {
proto = klass;
klass = null;
}
proto = proto || {};
var _super_class = this,
_super = this[STR_PROTOTYPE],
name, shortName, namespace, prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
inheritProps(proto, _super, prototype);
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( initializing ) return;
// we are being called w/o new, we are extending
if ( this.constructor !== Class && arguments.length ) {
return arguments.callee.extend.apply(arguments.callee, arguments)
} else { //we are being called w/ new
return this.Class.newInstance.apply(this.Class, arguments)
}
}
// Copy old stuff onto class
for ( name in this ) {
if ( this.hasOwnProperty(name) ) {
Class[name] = this[name];
}
}
// copy new static props on class
inheritProps(klass, this, Class);
// do namespace stuff
if ( fullName ) {
var parts = fullName.split(/\./),
shortName = parts.pop(),
current = getObject(parts.join('.'), window, true),
namespace = current;
current[shortName] = Class;
}
// set things that can't be overwritten
extend(Class, {
prototype: prototype,
/**
* @attribute namespace
* The namespaces object
*
* $.Class("MyOrg.MyClass",{},{})
* MyOrg.MyClass.namespace //-> MyOrg
*
*/
namespace: namespace,
/**
* @attribute shortName
* The name of the class without its namespace, provided for introspection purposes.
*
* $.Class("MyOrg.MyClass",{},{})
* MyOrg.MyClass.shortName //-> 'MyClass'
* MyOrg.MyClass.fullName //-> 'MyOrg.MyClass'
*
*/
shortName: shortName,
constructor: Class,
/**
* @attribute fullName
* The full name of the class, including namespace, provided for introspection purposes.
*
* $.Class("MyOrg.MyClass",{},{})
* MyOrg.MyClass.shortName //-> 'MyClass'
* MyOrg.MyClass.fullName //-> 'MyOrg.MyClass'
*
*/
fullName: fullName
});
//make sure our prototype looks nice
Class[STR_PROTOTYPE].Class = Class[STR_PROTOTYPE].constructor = Class;
// call the class setup
var args = Class.setup.apply(Class, concatArgs([_super_class],arguments));
// call the class init
if ( Class.init ) {
Class.init.apply(Class, args || concatArgs([_super_class],arguments));
}
/* @Prototype*/
return Class;
/**
* @function setup
* If a setup method is provided, it is called when a new
* instances is created. It gets passed the same arguments that
* were given to the Class constructor function (<code> new Class( arguments ... )</code>).
*
* $.Class("MyClass",
* {
* setup: function( val ) {
* this.val = val;
* }
* })
* var mc = new MyClass("Check Check")
* mc.val //-> 'Check Check'
*
* Setup is called before [jQuery.Class.prototype.init init]. If setup
* return an array, those arguments will be used for init.
*
* $.Class("jQuery.Controller",{
* setup : function(htmlElement, rawOptions){
* return [$(htmlElement),
* $.extend({}, this.Class.defaults, rawOptions )]
* }
* })
*
* <div class='whisper'>PRO TIP:
* Setup functions are used to normalize constructor arguments and provide a place for
* setup code that extending classes don't have to remember to call _super to
* run.
* </div>
*
* Setup is not defined on $.Class itself, so calling super in inherting classes
* will break. Don't do the following:
*
* $.Class("Thing",{
* setup : function(){
* this._super(); // breaks!
* }
* })
*
* @return {Array|undefined} If an array is return, [jQuery.Class.prototype.init] is
* called with those arguments; otherwise, the original arguments are used.
*/
//break up
/**
* @function init
* If an <code>init</code> method is provided, it gets called when a new instance
* is created. Init gets called after [jQuery.Class.prototype.setup setup], typically with the
* same arguments passed to the Class
* constructor: (<code> new Class( arguments ... )</code>).
*
* $.Class("MyClass",
* {
* init: function( val ) {
* this.val = val;
* }
* })
* var mc = new MyClass(1)
* mc.val //-> 1
*
* [jQuery.Class.prototype.setup Setup] is able to modify the arguments passed to init. Read
* about it there.
*
*/
//Breaks up code
/**
* @attribute constructor
*
* A reference to the Class (or constructor function). This allows you to access
* a class's static properties from an instance.
*
* ### Quick Example
*
* // a class with a static property
* $.Class("MyClass", {staticProperty : true}, {});
*
* // a new instance of myClass
* var mc1 = new MyClass();
*
* // read the static property from the instance:
* mc1.constructor.staticProperty //-> true
*
* Getting static properties with the constructor property, like
* [jQuery.Class.static.fullName fullName], is very common.
*
*/
}
})
clss.callback = clss[STR_PROTOTYPE].callback = clss[STR_PROTOTYPE].
/**
* @function proxy
* Returns a method that sets 'this' to the current instance. This does the same thing as
* and is described better in [jQuery.Class.static.proxy].
* The only difference is this proxy works
* on a instance instead of a class.
* @param {String|Array} fname If a string, it represents the function to be called.
* If it is an array, it will call each function in order and pass the return value of the prior function to the
* next function.
* @return {Function} the callback function
*/
proxy = clss.proxy;
})(jQuery);
(function( $ ) {
/**
* @attribute destroyed
* @parent specialevents
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/destroyed/destroyed.js
* @test jquery/event/destroyed/qunit.html
* Provides a destroyed event on an element.
* <p>
* The destroyed event is called when the element
* is removed as a result of jQuery DOM manipulators like remove, html,
* replaceWith, etc. Destroyed events do not bubble, so make sure you don't use live or delegate with destroyed
* events.
* </p>
* <h2>Quick Example</h2>
* @codestart
* $(".foo").bind("destroyed", function(){
* //clean up code
* })
* @codeend
* <h2>Quick Demo</h2>
* @demo jquery/event/destroyed/destroyed.html
* <h2>More Involved Demo</h2>
* @demo jquery/event/destroyed/destroyed_menu.html
*/
var oldClean = jQuery.cleanData;
$.cleanData = function( elems ) {
for ( var i = 0, elem;
(elem = elems[i]) !== undefined; i++ ) {
$(elem).triggerHandler("destroyed");
//$.event.remove( elem, 'destroyed' );
}
oldClean(elems);
};
})(jQuery);
(function( $ ) {
// ------- HELPER FUNCTIONS ------
// Binds an element, returns a function that unbinds
var bind = function( el, ev, callback ) {
var wrappedCallback,
binder = el.bind && el.unbind ? el : $(isFunction(el) ? [el] : el);
//this is for events like >click.
if ( ev.indexOf(">") === 0 ) {
ev = ev.substr(1);
wrappedCallback = function( event ) {
if ( event.target === el ) {
callback.apply(this, arguments);
}
};
}
binder.bind(ev, wrappedCallback || callback);
// if ev name has >, change the name and bind
// in the wrapped callback, check that the element matches the actual element
return function() {
binder.unbind(ev, wrappedCallback || callback);
el = ev = callback = wrappedCallback = null;
};
},
makeArray = $.makeArray,
isArray = $.isArray,
isFunction = $.isFunction,
extend = $.extend,
Str = $.String,
each = $.each,
STR_PROTOTYPE = 'prototype',
STR_CONSTRUCTOR = 'constructor',
slice = Array[STR_PROTOTYPE].slice,
// Binds an element, returns a function that unbinds
delegate = function( el, selector, ev, callback ) {
var binder = el.delegate && el.undelegate ? el : $(isFunction(el) ? [el] : el)
binder.delegate(selector, ev, callback);
return function() {
binder.undelegate(selector, ev, callback);
binder = el = ev = callback = selector = null;
};
},
// calls bind or unbind depending if there is a selector
binder = function( el, ev, callback, selector ) {
return selector ? delegate(el, selector, ev, callback) : bind(el, ev, callback);
},
// moves 'this' to the first argument, wraps it with jQuery if it's an element
shifter = function shifter(context, name) {
var method = typeof name == "string" ? context[name] : name;
return function() {
context.called = name;
return method.apply(context, [this.nodeName ? $(this) : this].concat( slice.call(arguments, 0) ) );
};
},
// matches dots
dotsReg = /\./g,
// matches controller
controllersReg = /_?controllers?/ig,
//used to remove the controller from the name
underscoreAndRemoveController = function( className ) {
return Str.underscore(className.replace("jQuery.", "").replace(dotsReg, '_').replace(controllersReg, ""));
},
// checks if it looks like an action
actionMatcher = /[^\w]/,
// handles parameterized action names
parameterReplacer = /\{([^\}]+)\}/g,
breaker = /^(?:(.*?)\s)?([\w\.\:>]+)$/,
basicProcessor,
data = function(el, data){
return $.data(el, "controllers", data)
};
/**
* @class jQuery.Controller
* @parent jquerymx
* @plugin jquery/controller
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/controller/controller.js
* @test jquery/controller/qunit.html
* @inherits jQuery.Class
* @description jQuery widget factory.
*
* jQuery.Controller helps create organized, memory-leak free, rapidly performing
* jQuery widgets. Its extreme flexibility allows it to serve as both
* a traditional View and a traditional Controller.
*
* This means it is used to
* create things like tabs, grids, and contextmenus as well as
* organizing them into higher-order business rules.
*
* Controllers make your code deterministic, reusable, organized and can tear themselves
* down auto-magically. Read about [http://jupiterjs.com/news/writing-the-perfect-jquery-plugin
* the theory behind controller] and
* a [http://jupiterjs.com/news/organize-jquery-widgets-with-jquery-controller walkthrough of its features]
* on Jupiter's blog. [mvc.controller Get Started with jQueryMX] also has a great walkthrough.
*
* Controller inherits from [jQuery.Class $.Class] and makes heavy use of
* [http://api.jquery.com/delegate/ event delegation]. Make sure
* you understand these concepts before using it.
*
* ## Basic Example
*
* Instead of
*
*
* $(function(){
* $('#tabs').click(someCallbackFunction1)
* $('#tabs .tab').click(someCallbackFunction2)
* $('#tabs .delete click').click(someCallbackFunction3)
* });
*
* do this
*
* $.Controller('Tabs',{
* click: function() {...},
* '.tab click' : function() {...},
* '.delete click' : function() {...}
* })
* $('#tabs').tabs();
*
*
* ## Tabs Example
*
* @demo jquery/controller/controller.html
*
* ## Using Controller
*
* Controller helps you build and organize jQuery plugins. It can be used
* to build simple widgets, like a slider, or organize multiple
* widgets into something greater.
*
* To understand how to use Controller, you need to understand
* the typical lifecycle of a jQuery widget and how that maps to
* controller's functionality:
*
* ### A controller class is created.
*
* $.Controller("MyWidget",
* {
* defaults : {
* message : "Remove Me"
* }
* },
* {
* init : function(rawEl, rawOptions){
* this.element.append(
* "<div>"+this.options.message+"</div>"
* );
* },
* "div click" : function(div, ev){
* div.remove();
* }
* })
*
* This creates a <code>$.fn.my_widget</code> jQuery helper function
* that can be used to create a new controller instance on an element. Find
* more information [jquery.controller.plugin here] about the plugin gets created
* and the rules around its name.
*
* ### An instance of controller is created on an element
*
* $('.thing').my_widget(options) // calls new MyWidget(el, options)
*
* This calls <code>new MyWidget(el, options)</code> on
* each <code>'.thing'</code> element.
*
* When a new [jQuery.Class Class] instance is created, it calls the class's
* prototype setup and init methods. Controller's [jQuery.Controller.prototype.setup setup]
* method:
*
* - Sets [jQuery.Controller.prototype.element this.element] and adds the controller's name to element's className.
* - Merges passed in options with defaults object and sets it as [jQuery.Controller.prototype.options this.options]
* - Saves a reference to the controller in <code>$.data</code>.
* - [jquery.controller.listening Binds all event handler methods].
*
*
* ### The controller responds to events
*
* Typically, Controller event handlers are automatically bound. However, there are
* multiple ways to [jquery.controller.listening listen to events] with a controller.
*
* Once an event does happen, the callback function is always called with 'this'
* referencing the controller instance. This makes it easy to use helper functions and
* save state on the controller.
*
*
* ### The widget is destroyed
*
* If the element is removed from the page, the
* controller's [jQuery.Controller.prototype.destroy] method is called.
* This is a great place to put any additional teardown functionality.
*
* You can also teardown a controller programatically like:
*
* $('.thing').my_widget('destroy');
*
* ## Todos Example
*
* Lets look at a very basic example -
* a list of todos and a button you want to click to create a new todo.
* Your HTML might look like:
*
* @codestart html
* &lt;div id='todos'>
* &lt;ol>
* &lt;li class="todo">Laundry&lt;/li>
* &lt;li class="todo">Dishes&lt;/li>
* &lt;li class="todo">Walk Dog&lt;/li>
* &lt;/ol>
* &lt;a class="create">Create&lt;/a>
* &lt;/div>
* @codeend
*
* To add a mousover effect and create todos, your controller might look like:
*
* $.Controller('Todos',{
* ".todo mouseover" : function( el, ev ) {
* el.css("backgroundColor","red")
* },
* ".todo mouseout" : function( el, ev ) {
* el.css("backgroundColor","")
* },
* ".create click" : function() {
* this.find("ol").append("<li class='todo'>New Todo</li>");
* }
* })
*
* Now that you've created the controller class, you've must attach the event handlers on the '#todos' div by
* creating [jQuery.Controller.prototype.setup|a new controller instance]. There are 2 ways of doing this.
*
* @codestart
* //1. Create a new controller directly:
* new Todos($('#todos'));
* //2. Use jQuery function
* $('#todos').todos();
* @codeend
*
* ## Controller Initialization
*
* It can be extremely useful to add an init method with
* setup functionality for your widget.
*
* In the following example, I create a controller that when created, will put a message as the content of the element:
*
* $.Controller("SpecialController",
* {
* init: function( el, message ) {
* this.element.html(message)
* }
* })
* $(".special").special("Hello World")
*
* ## Removing Controllers
*
* Controller removal is built into jQuery. So to remove a controller, you just have to remove its element:
*
* @codestart
* $(".special_controller").remove()
* $("#containsControllers").html("")
* @codeend
*
* It's important to note that if you use raw DOM methods (<code>innerHTML, removeChild</code>), the controllers won't be destroyed.
*
* If you just want to remove controller functionality, call destroy on the controller instance:
*
* @codestart
* $(".special_controller").controller().destroy()
* @codeend
*
* ## Accessing Controllers
*
* Often you need to get a reference to a controller, there are a few ways of doing that. For the
* following example, we assume there are 2 elements with <code>className="special"</code>.
*
* @codestart
* //creates 2 foo controllers
* $(".special").foo()
*
* //creates 2 bar controllers
* $(".special").bar()
*
* //gets all controllers on all elements:
* $(".special").controllers() //-> [foo, bar, foo, bar]
*
* //gets only foo controllers
* $(".special").controllers(FooController) //-> [foo, foo]
*
* //gets all bar controllers
* $(".special").controllers(BarController) //-> [bar, bar]
*
* //gets first controller
* $(".special").controller() //-> foo
*
* //gets foo controller via data
* $(".special").data("controllers")["FooController"] //-> foo
* @codeend
*
* ## Calling methods on Controllers
*
* Once you have a reference to an element, you can call methods on it. However, Controller has
* a few shortcuts:
*
* @codestart
* //creates foo controller
* $(".special").foo({name: "value"})
*
* //calls FooController.prototype.update
* $(".special").foo({name: "value2"})
*
* //calls FooController.prototype.bar
* $(".special").foo("bar","something I want to pass")
* @codeend
*
* These methods let you call one controller from another controller.
*
*/
$.Class("jQuery.Controller",
/**
* @Static
*/
{
/**
* Does 2 things:
*
* - Creates a jQuery helper for this controller.</li>
* - Calculates and caches which functions listen for events.</li>
*
* ### jQuery Helper Naming Examples
*
*
* "TaskController" -> $().task_controller()
* "Controllers.Task" -> $().controllers_task()
*
*/
setup: function() {
// Allow contollers to inherit "defaults" from superclasses as it done in $.Class
this._super.apply(this, arguments);
// if you didn't provide a name, or are controller, don't do anything
if (!this.shortName || this.fullName == "jQuery.Controller" ) {
return;
}
// cache the underscored names
this._fullName = underscoreAndRemoveController(this.fullName);
this._shortName = underscoreAndRemoveController(this.shortName);
var controller = this,
/**
* @attribute pluginName
* Setting the <code>pluginName</code> property allows you
* to change the jQuery plugin helper name from its
* default value.
*
* $.Controller("Mxui.Layout.Fill",{
* pluginName: "fillWith"
* },{});
*
* $("#foo").fillWith();
*/
pluginname = this.pluginName || this._fullName,
funcName, forLint;
// create jQuery plugin
if (!$.fn[pluginname] ) {
$.fn[pluginname] = function( options ) {
var args = makeArray(arguments),
//if the arg is a method on this controller
isMethod = typeof options == "string" && isFunction(controller[STR_PROTOTYPE][options]),
meth = args[0];
return this.each(function() {
//check if created
var controllers = data(this),
//plugin is actually the controller instance
plugin = controllers && controllers[pluginname];
if ( plugin ) {
if ( isMethod ) {
// call a method on the controller with the remaining args
plugin[meth].apply(plugin, args.slice(1));
} else {
// call the plugin's update method
plugin.update.apply(plugin, args);
}
} else {
//create a new controller instance
controller.newInstance.apply(controller, [this].concat(args));
}
});
};
}
// make sure listensTo is an array
// calculate and cache actions
this.actions = {};
for ( funcName in this[STR_PROTOTYPE] ) {
if (funcName == 'constructor' || !isFunction(this[STR_PROTOTYPE][funcName]) ) {
continue;
}
if ( this._isAction(funcName) ) {
this.actions[funcName] = this._action(funcName);
}
}
},
hookup: function( el ) {
return new this(el);
},
/**
* @hide
* @param {String} methodName a prototype function
* @return {Boolean} truthy if an action or not
*/
_isAction: function( methodName ) {
if ( actionMatcher.test(methodName) ) {
return true;
} else {
return $.inArray(methodName, this.listensTo) > -1 || $.event.special[methodName] || processors[methodName];
}
},
/**
* @hide
* This takes a method name and the options passed to a controller
* and tries to return the data necessary to pass to a processor
* (something that binds things).
*
* For performance reasons, this called twice. First, it is called when
* the Controller class is created. If the methodName is templated
* like : "{window} foo", it returns null. If it is not templated
* it returns event binding data.
*
* The resulting data is added to this.actions.
*
* When a controller instance is created, _action is called again, but only
* on templated actions.
*
* @param {Object} methodName the method that will be bound
* @param {Object} [options] first param merged with class default options
* @return {Object} null or the processor and pre-split parts.
* The processor is what does the binding/subscribing.
*/
_action: function( methodName, options ) {
// reset the test index
parameterReplacer.lastIndex = 0;
//if we don't have options (a controller instance), we'll run this later
if (!options && parameterReplacer.test(methodName) ) {
return null;
}
// If we have options, run sub to replace templates "{}" with a value from the options
// or the window
var convertedName = options ? Str.sub(methodName, [options, window]) : methodName,
// If a "{}" resolves to an object, convertedName will be an array
arr = isArray(convertedName),
// get the parts of the function = [convertedName, delegatePart, eventPart]
parts = (arr ? convertedName[1] : convertedName).match(breaker),
event = parts[2],
processor = processors[event] || basicProcessor;
return {
processor: processor,
parts: parts,
delegate : arr ? convertedName[0] : undefined
};
},
/**
* @attribute processors
* An object of {eventName : function} pairs that Controller uses to hook up events
* auto-magically. A processor function looks like:
*
* jQuery.Controller.processors.
* myprocessor = function( el, event, selector, cb, controller ) {
* //el - the controller's element
* //event - the event (myprocessor)
* //selector - the left of the selector
* //cb - the function to call
* //controller - the binding controller
* };
*
* This would bind anything like: "foo~3242 myprocessor".
*
* The processor must return a function that when called,
* unbinds the event handler.
*
* Controller already has processors for the following events:
*
* - change
* - click
* - contextmenu
* - dblclick
* - focusin
* - focusout
* - keydown
* - keyup
* - keypress
* - mousedown
* - mouseenter
* - mouseleave
* - mousemove
* - mouseout
* - mouseover
* - mouseup
* - reset
* - resize
* - scroll
* - select
* - submit
*
* Listen to events on the document or window
* with templated event handlers:
*
*
* $.Controller('Sized',{
* "{window} resize" : function(){
* this.element.width(this.element.parent().width() / 2);
* }
* });
*
* $('.foo').sized();
*/
processors: {},
/**
* @attribute listensTo
* An array of special events this controller
* listens too. You only need to add event names that
* are whole words (ie have no special characters).
*
* $.Controller('TabPanel',{
* listensTo : ['show']
* },{
* 'show' : function(){
* this.element.show();
* }
* })
*
* $('.foo').tab_panel().trigger("show");
*
*/
listensTo: [],
/**
* @attribute defaults
* A object of name-value pairs that act as default values for a controller's
* [jQuery.Controller.prototype.options options].
*
* $.Controller("Message",
* {
* defaults : {
* message : "Hello World"
* }
* },{
* init : function(){
* this.element.text(this.options.message);
* }
* })
*
* $("#el1").message(); //writes "Hello World"
* $("#el12").message({message: "hi"}); //writes hi
*
* In [jQuery.Controller.prototype.setup setup] the options passed to the controller
* are merged with defaults. This is not a deep merge.
*/
defaults: {}
},
/**
* @Prototype
*/
{
/**
* Setup is where most of controller's magic happens. It does the following:
*
* ### 1. Sets this.element
*
* The first parameter passed to new Controller(el, options) is expected to be
* an element. This gets converted to a jQuery wrapped element and set as
* [jQuery.Controller.prototype.element this.element].
*
* ### 2. Adds the controller's name to the element's className.
*
* Controller adds it's plugin name to the element's className for easier
* debugging. For example, if your Controller is named "Foo.Bar", it adds
* "foo_bar" to the className.
*
* ### 3. Saves the controller in $.data
*
* A reference to the controller instance is saved in $.data. You can find
* instances of "Foo.Bar" like:
*
* $("#el").data("controllers")['foo_bar'].
*
* ### Binds event handlers
*
* Setup does the event binding described in [jquery.controller.listening Listening To Events].
*
* @param {HTMLElement} element the element this instance operates on.
* @param {Object} [options] option values for the controller. These get added to
* this.options and merged with [jQuery.Controller.static.defaults defaults].
* @return {Array} return an array if you wan to change what init is called with. By
* default it is called with the element and options passed to the controller.
*/
setup: function( element, options ) {
var funcName, ready, cls = this[STR_CONSTRUCTOR];
//want the raw element here
element = (typeof element == 'string' ? $(element) :
(element.jquery ? element : [element]) )[0];
//set element and className on element
var pluginname = cls.pluginName || cls._fullName;
//set element and className on element
this.element = $(element).addClass(pluginname);
//set in data
(data(element) || data(element, {}))[pluginname] = this;
/**
* @attribute options
*
* Options are used to configure an controller. They are
* the 2nd argument
* passed to a controller (or the first argument passed to the
* [jquery.controller.plugin controller's jQuery plugin]).
*
* For example:
*
* $.Controller('Hello')
*
* var h1 = new Hello($('#content1'), {message: 'World'} );
* equal( h1.options.message , "World" )
*
* var h2 = $('#content2').hello({message: 'There'})
* .controller();
* equal( h2.options.message , "There" )
*
* Options are merged with [jQuery.Controller.static.defaults defaults] in
* [jQuery.Controller.prototype.setup setup].
*
* For example:
*
* $.Controller("Tabs",
* {
* defaults : {
* activeClass: "ui-active-state"
* }
* },
* {
* init : function(){
* this.element.addClass(this.options.activeClass);
* }
* })
*
* $("#tabs1").tabs() // adds 'ui-active-state'
* $("#tabs2").tabs({activeClass : 'active'}) // adds 'active'
*
* Options are typically updated by calling
* [jQuery.Controller.prototype.update update];
*
*/
this.options = extend( extend(true, {}, cls.defaults), options);
/**
* @attribute called
* String name of current function being called on controller instance. This is
* used for picking the right view in render.
* @hide
*/
this.called = "init";
// bind all event handlers
this.bind();
/**
* @attribute element
* The controller instance's delegated element. This
* is set by [jQuery.Controller.prototype.setup setup]. It
* is a jQuery wrapped element.
*
* For example, if I add MyWidget to a '#myelement' element like:
*
* $.Controller("MyWidget",{
* init : function(){
* this.element.css("color","red")
* }
* })
*
* $("#myelement").my_widget()
*
* MyWidget will turn #myelement's font color red.
*
* ## Using a different element.
*
* Sometimes, you want a different element to be this.element. A
* very common example is making progressively enhanced form widgets.
*
* To change this.element, overwrite Controller's setup method like:
*
* $.Controller("Combobox",{
* setup : function(el, options){
* this.oldElement = $(el);
* var newEl = $('<div/>');
* this.oldElement.wrap(newEl);
* this._super(newEl, options);
* },
* init : function(){
* this.element //-> the div
* },
* ".option click" : function(){
* // event handler bound on the div
* },
* destroy : function(){
* var div = this.element; //save reference
* this._super();
* div.replaceWith(this.oldElement);
* }
* }
*/
return [this.element, this.options].concat(makeArray(arguments).slice(2));
/**
* @function init
*
* Implement this.
*/
},
/**
* Bind attaches event handlers that will be
* removed when the controller is removed.
*
* This used to be a good way to listen to events outside the controller's
* [jQuery.Controller.prototype.element element]. However,
* using templated event listeners is now the prefered way of doing this.
*
* ### Example:
*
* init: function() {
* // calls somethingClicked(el,ev)
* this.bind('click','somethingClicked')
*
* // calls function when the window is clicked
* this.bind(window, 'click', function(ev){
* //do something
* })
* },
* somethingClicked: function( el, ev ) {
*
* }
*
* @param {HTMLElement|jQuery.fn|Object} [el=this.element]
* The element to be bound. If an eventName is provided,
* the controller's element is used instead.
*
* @param {String} eventName The event to listen for.
* @param {Function|String} func A callback function or the String name of a controller function. If a controller
* function name is given, the controller function is called back with the bound element and event as the first
* and second parameter. Otherwise the function is called back like a normal bind.
* @return {Integer} The id of the binding in this._bindings
*/
bind: function( el, eventName, func ) {
if( el === undefined ) {
//adds bindings
this._bindings = [];
//go through the cached list of actions and use the processor to bind
var cls = this[STR_CONSTRUCTOR],
bindings = this._bindings,
actions = cls.actions,
element = this.element;
for ( funcName in actions ) {
if ( actions.hasOwnProperty(funcName) ) {
ready = actions[funcName] || cls._action(funcName, this.options);
bindings.push(
ready.processor(ready.delegate || element,
ready.parts[2],
ready.parts[1],
funcName,
this));
}
}
//setup to be destroyed ... don't bind b/c we don't want to remove it
var destroyCB = shifter(this,"destroy");
element.bind("destroyed", destroyCB);
bindings.push(function( el ) {
$(el).unbind("destroyed", destroyCB);
});
return bindings.length;
}
if ( typeof el == 'string' ) {
func = eventName;
eventName = el;
el = this.element;
}
return this._binder(el, eventName, func);
},
_binder: function( el, eventName, func, selector ) {
if ( typeof func == 'string' ) {
func = shifter(this,func);
}
this._bindings.push(binder(el, eventName, func, selector));
return this._bindings.length;
},
_unbind : function(){
var el = this.element[0];
each(this._bindings, function( key, value ) {
value(el);
});
//adds bindings
this._bindings = [];
},
/**
* Delegate will delegate on an elememt and will be undelegated when the controller is removed.
* This is a good way to delegate on elements not in a controller's element.<br/>
* <h3>Example:</h3>
* @codestart
* // calls function when the any 'a.foo' is clicked.
* this.delegate(document.documentElement,'a.foo', 'click', function(ev){
* //do something
* })
* @codeend
* @param {HTMLElement|jQuery.fn} [element=this.element] the element to delegate from
* @param {String} selector the css selector
* @param {String} eventName the event to bind to
* @param {Function|String} func A callback function or the String name of a controller function. If a controller
* function name is given, the controller function is called back with the bound element and event as the first
* and second parameter. Otherwise the function is called back like a normal bind.
* @return {Integer} The id of the binding in this._bindings
*/
delegate: function( element, selector, eventName, func ) {
if ( typeof element == 'string' ) {
func = eventName;
eventName = selector;
selector = element;
element = this.element;
}
return this._binder(element, eventName, func, selector);
},
/**
* Update extends [jQuery.Controller.prototype.options this.options]
* with the `options` argument and rebinds all events. It basically
* re-configures the controller.
*
* For example, the following controller wraps a recipe form. When the form
* is submitted, it creates the recipe on the server. When the recipe
* is `created`, it resets the form with a new instance.
*
* $.Controller('Creator',{
* "{recipe} created" : function(){
* this.update({recipe : new Recipe()});
* this.element[0].reset();
* this.find("[type=submit]").val("Create Recipe")
* },
* "submit" : function(el, ev){
* ev.preventDefault();
* var recipe = this.options.recipe;
* recipe.attrs( this.element.formParams() );
* this.find("[type=submit]").val("Saving...")
* recipe.save();
* }
* });
* $('#createRecipes').creator({recipe : new Recipe()})
*
*
* @demo jquery/controller/demo-update.html
*
* Update is called if a controller's [jquery.controller.plugin jQuery helper] is
* called on an element that already has a controller instance
* of the same type.
*
* For example, a widget that listens for model updates
* and updates it's html would look like.
*
* $.Controller('Updater',{
* // when the controller is created, update the html
* init : function(){
* this.updateView();
* },
*
* // update the html with a template
* updateView : function(){
* this.element.html( "content.ejs",
* this.options.model );
* },
*
* // if the model is updated
* "{model} updated" : function(){
* this.updateView();
* },
* update : function(options){
* // make sure you call super
* this._super(options);
*
* this.updateView();
* }
* })
*
* // create the controller
* // this calls init
* $('#item').updater({model: recipe1});
*
* // later, update that model
* // this calls "{model} updated"
* recipe1.update({name: "something new"});
*
* // later, update the controller with a new recipe
* // this calls update
* $('#item').updater({model: recipe2});
*
* // later, update the new model
* // this calls "{model} updated"
* recipe2.update({name: "something newer"});
*
* _NOTE:_ If you overwrite `update`, you probably need to call
* this._super.
*
* ### Example
*
* $.Controller("Thing",{
* init: function( el, options ) {
* alert( 'init:'+this.options.prop )
* },
* update: function( options ) {
* this._super(options);
* alert('update:'+this.options.prop)
* }
* });
* $('#myel').thing({prop : 'val1'}); // alerts init:val1
* $('#myel').thing({prop : 'val2'}); // alerts update:val2
*
* @param {Object} options A list of options to merge with
* [jQuery.Controller.prototype.options this.options]. Often, this method
* is called by the [jquery.controller.plugin jQuery helper function].
*/
update: function( options ) {
extend(this.options, options);
this._unbind();
this.bind();
},
/**
* Destroy unbinds and undelegates all event handlers on this controller,
* and prevents memory leaks. This is called automatically
* if the element is removed. You can overwrite it to add your own
* teardown functionality:
*
* $.Controller("ChangeText",{
* init : function(){
* this.oldText = this.element.text();
* this.element.text("Changed!!!")
* },
* destroy : function(){
* this.element.text(this.oldText);
* this._super(); //Always call this!
* })
*
* Make sure you always call <code>_super</code> when overwriting
* controller's destroy event. The base destroy functionality unbinds
* all event handlers the controller has created.
*
* You could call destroy manually on an element with ChangeText
* added like:
*
* $("#changed").change_text("destroy");
*
*/
destroy: function() {
if ( this._destroyed ) {
throw this[STR_CONSTRUCTOR].shortName + " controller already deleted";
}
var self = this,
fname = this[STR_CONSTRUCTOR].pluginName || this[STR_CONSTRUCTOR]._fullName,
controllers;
// mark as destroyed
this._destroyed = true;
// remove the className
this.element.removeClass(fname);
// unbind bindings
this._unbind();
// clean up
delete this._actions;
delete this.element.data("controllers")[fname];
$(this).triggerHandler("destroyed"); //in case we want to know if the controller is removed
this.element = null;
},
/**
* Queries from the controller's element.
* @codestart
* ".destroy_all click" : function() {
* this.find(".todos").remove();
* }
* @codeend
* @param {String} selector selection string
* @return {jQuery.fn} returns the matched elements
*/
find: function( selector ) {
return this.element.find(selector);
},
//tells callback to set called on this. I hate this.
_set_called: true
});
var processors = $.Controller.processors,
//------------- PROCESSSORS -----------------------------
//processors do the binding. They return a function that
//unbinds when called.
//the basic processor that binds events
basicProcessor = function( el, event, selector, methodName, controller ) {
return binder(el, event, shifter(controller, methodName), selector);
};
//set common events to be processed as a basicProcessor
each("change click contextmenu dblclick keydown keyup keypress mousedown mousemove mouseout mouseover mouseup reset resize scroll select submit focusin focusout mouseenter mouseleave".split(" "), function( i, v ) {
processors[v] = basicProcessor;
});
/**
* @add jQuery.fn
*/
//used to determine if a controller instance is one of controllers
//controllers can be strings or classes
var i, isAControllerOf = function( instance, controllers ) {
for ( i = 0; i < controllers.length; i++ ) {
if ( typeof controllers[i] == 'string' ? instance[STR_CONSTRUCTOR]._shortName == controllers[i] : instance instanceof controllers[i] ) {
return true;
}
}
return false;
};
$.fn.extend({
/**
* @function controllers
* Gets all controllers in the jQuery element.
* @return {Array} an array of controller instances.
*/
controllers: function() {
var controllerNames = makeArray(arguments),
instances = [],
controllers, c, cname;
//check if arguments
this.each(function() {
controllers = $.data(this, "controllers");
for ( cname in controllers ) {
if ( controllers.hasOwnProperty(cname) ) {
c = controllers[cname];
if (!controllerNames.length || isAControllerOf(c, controllerNames) ) {
instances.push(c);
}
}
}
});
return instances;
},
/**
* @function controller
* Gets a controller in the jQuery element. With no arguments, returns the first one found.
* @param {Object} controller (optional) if exists, the first controller instance with this class type will be returned.
* @return {jQuery.Controller} the first controller.
*/
controller: function( controller ) {
return this.controllers.apply(this, arguments)[0];
}
});
})(jQuery);
(function(){
// prevent re-definition of the OpenAjax object
if(!window["OpenAjax"]){
/**
* @class OpenAjax
* Use OpenAjax.hub to publish and subscribe to messages.
*/
OpenAjax = new function(){
var t = true;
var f = false;
var g = window;
var ooh = "org.openajax.hub.";
var h = {};
this.hub = h;
h.implementer = "http://openajax.org";
h.implVersion = "2.0";
h.specVersion = "2.0";
h.implExtraData = {};
var libs = {};
h.libraries = libs;
h.registerLibrary = function(prefix, nsURL, version, extra){
libs[prefix] = {
prefix: prefix,
namespaceURI: nsURL,
version: version,
extraData: extra
};
this.publish(ooh+"registerLibrary", libs[prefix]);
}
h.unregisterLibrary = function(prefix){
this.publish(ooh+"unregisterLibrary", libs[prefix]);
delete libs[prefix];
}
h._subscriptions = { c:{}, s:[] };
h._cleanup = [];
h._subIndex = 0;
h._pubDepth = 0;
h.subscribe = function(name, callback, scope, subscriberData, filter)
{
if(!scope){
scope = window;
}
var handle = name + "." + this._subIndex;
var sub = { scope: scope, cb: callback, fcb: filter, data: subscriberData, sid: this._subIndex++, hdl: handle };
var path = name.split(".");
this._subscribe(this._subscriptions, path, 0, sub);
return handle;
}
h.publish = function(name, message)
{
var path = name.split(".");
this._pubDepth++;
this._publish(this._subscriptions, path, 0, name, message);
this._pubDepth--;
if((this._cleanup.length > 0) && (this._pubDepth == 0)) {
for(var i = 0; i < this._cleanup.length; i++)
this.unsubscribe(this._cleanup[i].hdl);
delete(this._cleanup);
this._cleanup = [];
}
}
h.unsubscribe = function(sub)
{
var path = sub.split(".");
var sid = path.pop();
this._unsubscribe(this._subscriptions, path, 0, sid);
}
h._subscribe = function(tree, path, index, sub)
{
var token = path[index];
if(index == path.length)
tree.s.push(sub);
else {
if(typeof tree.c == "undefined")
tree.c = {};
if(typeof tree.c[token] == "undefined") {
tree.c[token] = { c: {}, s: [] };
this._subscribe(tree.c[token], path, index + 1, sub);
}
else
this._subscribe( tree.c[token], path, index + 1, sub);
}
}
h._publish = function(tree, path, index, name, msg, pid) {
if(typeof tree != "undefined") {
var node;
if(index == path.length) {
node = tree;
} else {
this._publish(tree.c[path[index]], path, index + 1, name, msg, pid);
this._publish(tree.c["*"], path, index + 1, name, msg, pid);
node = tree.c["**"];
}
if(typeof node != "undefined") {
var callbacks = node.s;
var max = callbacks.length;
for(var i = 0; i < max; i++) {
if(callbacks[i].cb) {
var sc = callbacks[i].scope;
var cb = callbacks[i].cb;
var fcb = callbacks[i].fcb;
var d = callbacks[i].data;
if(typeof cb == "string"){
// get a function object
cb = sc[cb];
}
if(typeof fcb == "string"){
// get a function object
fcb = sc[fcb];
}
if((!fcb) || (fcb.call(sc, name, msg, d))) {
cb.call(sc, name, msg, d, pid);
}
}
}
}
}
}
h._unsubscribe = function(tree, path, index, sid) {
if(typeof tree != "undefined") {
if(index < path.length) {
var childNode = tree.c[path[index]];
this._unsubscribe(childNode, path, index + 1, sid);
if(childNode.s.length == 0) {
for(var x in childNode.c)
return;
delete tree.c[path[index]];
}
return;
}
else {
var callbacks = tree.s;
var max = callbacks.length;
for(var i = 0; i < max; i++)
if(sid == callbacks[i].sid) {
if(this._pubDepth > 0) {
callbacks[i].cb = null;
this._cleanup.push(callbacks[i]);
}
else
callbacks.splice(i, 1);
return;
}
}
}
}
// The following function is provided for automatic testing purposes.
// It is not expected to be deployed in run-time OpenAjax Hub implementations.
h.reinit = function()
{
for (var lib in OpenAjax.hub.libraries) {
delete OpenAjax.hub.libraries[lib];
}
OpenAjax.hub.registerLibrary("OpenAjax", "http://openajax.org/hub", "1.0", {});
delete OpenAjax._subscriptions;
OpenAjax._subscriptions = {c:{},s:[]};
delete OpenAjax._cleanup;
OpenAjax._cleanup = [];
OpenAjax._subIndex = 0;
OpenAjax._pubDepth = 0;
}
};
// Register the OpenAjax Hub itself as a library.
OpenAjax.hub.registerLibrary("OpenAjax", "http://openajax.org/hub", "1.0", {});
}
OpenAjax.hub.registerLibrary("JavaScriptMVC", "http://JavaScriptMVC.com", "3.1", {});
})(jQuery);
(function() {
/**
* @function jQuery.Controller.static.processors.subscribe
* @parent jQuery.Controller.static.processors
* @plugin jquery/controller/subscribe
* Adds OpenAjax.Hub subscribing to controllers.
*
* $.Controller("Subscriber",{
* "recipe.updated subscribe" : function(called, recipe){
*
* },
* "todo.* subscribe" : function(called, todo){
*
* }
* })
*
* You should typically be listening to jQuery triggered events when communicating between
* controllers. Subscribe should be used for listening to model changes.
*
* ### API
*
* This is the call signiture for the processor, not the controller subscription callbacks.
*
* @param {HTMLElement} el the element being bound. This isn't used.
* @param {String} event the event type (subscribe).
* @param {String} selector the subscription name
* @param {String} cb the callback function's name
*/
jQuery.Controller.processors.subscribe = function( el, event, selector, cb, controller ) {
var subscription = OpenAjax.hub.subscribe(selector, function(){
return controller[cb].apply(controller, arguments)
});
return function() {
OpenAjax.hub.unsubscribe(subscription);
};
};
/**
* @add jQuery.Controller.prototype
*/
//breaker
/**
* @function publish
* @hide
* Publishes a message to OpenAjax.hub.
* @param {String} message Message name, ex: "Something.Happened".
* @param {Object} data The data sent.
*/
jQuery.Controller.prototype.publish = function() {
OpenAjax.hub.publish.apply(OpenAjax.hub, arguments);
};
})(jQuery);
(function($){
/**
* @function jQuery.fn.triggerAsync
* @plugin jquery/event/default
* @parent jquery.event.pause
*
* Triggers an event and calls success when the event has finished propagating through the DOM and preventDefault is not called.
*
* $('#panel').triggerAsync('show', function() {
* $('#panel').show();
* });
*
* You can also provide a callback that gets called if preventDefault was called on the event:
*
* $('panel').triggerAsync('show', function(){
* $('#panel').show();
* },function(){
* $('#other').addClass('error');
* });
*
* triggerAsync is design to work with the [jquery.event.pause]
* plugin although it is defined in _jquery/event/default_.
*
* @param {String} type The type of event
* @param {Object} data The data for the event
* @param {Function} success a callback function which occurs upon success
* @param {Function} prevented a callback function which occurs if preventDefault was called
*/
$.fn.triggerAsync = function(type, data, success, prevented){
if(typeof data == 'function'){
success = data;
data = undefined;
}
if ( this[0] ) {
var event = $.Event( type ),
old = event.preventDefault;
event.preventDefault = function(){
old.apply(this, arguments);
prevented && prevented(this)
}
//event._success= success;
jQuery.event.trigger( {type: type, _success: success}, data, this[0] );
} else{
success.call(this);
}
return this;
}
/**
* @add jQuery.event.special
*/
//cache default types for performance
var types = {}, rnamespaces= /\.(.*)$/, $event = $.event;
/**
* @attribute default
* @parent specialevents
* @plugin jquery/event/default
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/default/default.js
* @test jquery/event/default/qunit.html
* Allows you to perform default actions as a result of an event.
*
* Event based APIs are a powerful way of exposing functionality of your widgets. It also fits in
* quite nicely with how the DOM works.
*
*
* Like default events in normal functions (e.g. submitting a form), synthetic default events run after
* all event handlers have been triggered and no event handler has called
* preventDefault or returned false.
*
* To listen for a default event, just prefix the event with default.
*
* $("div").bind("default.show", function(ev){ ... });
* $("ul").delegate("li","default.activate", function(ev){ ... });
*
*
* ## Example
*
* Lets look at how you could build a simple tabs widget with default events.
* First with just jQuery:
*
* Default events are useful in cases where you want to provide an event based
* API for users of your widgets. Users can simply listen to your synthetic events and
* prevent your default functionality by calling preventDefault.
*
* In the example below, the tabs widget provides a show event. Users of the
* tabs widget simply listen for show, and if they wish for some reason, call preventDefault
* to avoid showing the tab.
*
* In this case, the application developer doesn't want to show the second
* tab until the checkbox is checked.
*
* @demo jquery/event/default/defaultjquery.html
*
* Lets see how we would build this with JavaScriptMVC:
*
* @demo jquery/event/default/default.html
*/
$event.special["default"] = {
add: function( handleObj ) {
//save the type
types[handleObj.namespace.replace(rnamespaces,"")] = true;
},
setup: function() {return true}
}
// overwrite trigger to allow default types
var oldTrigger = $event.trigger;
$event.trigger = function defaultTriggerer( event, data, elem, onlyHandlers){
// Event object or event type
var type = event.type || event,
namespaces = [],
// Caller can pass in an Event, Object, or just an event type string
event = typeof event === "object" ?
// jQuery.Event object
event[ jQuery.expando ] ? event :
// Object literal
new jQuery.Event( type, event ) :
// Just the event type (string)
new jQuery.Event( type );
//event._defaultActions = []; //set depth for possibly reused events
var res = oldTrigger.call($.event, event, data, elem, onlyHandlers);
if(!onlyHandlers && !event.isDefaultPrevented() && event.type.indexOf("default") !== 0){
oldTrigger("default."+event.type, data, elem)
if(event._success){
event._success(event)
}
}
// code for paused
if( event.isPaused && event.isPaused() ){
// set back original stuff
event.isDefaultPrevented =
event.pausedState.isDefaultPrevented;
event.isPropagationStopped =
event.pausedState.isPropagationStopped;
}
return res;
};
})(jQuery);
(function( $ ) {
/**
* @attribute destroyed
* @parent specialevents
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/destroyed/destroyed.js
* @test jquery/event/destroyed/qunit.html
* Provides a destroyed event on an element.
* <p>
* The destroyed event is called when the element
* is removed as a result of jQuery DOM manipulators like remove, html,
* replaceWith, etc. Destroyed events do not bubble, so make sure you don't use live or delegate with destroyed
* events.
* </p>
* <h2>Quick Example</h2>
* @codestart
* $(".foo").bind("destroyed", function(){
* //clean up code
* })
* @codeend
* <h2>Quick Demo</h2>
* @demo jquery/event/destroyed/destroyed.html
* <h2>More Involved Demo</h2>
* @demo jquery/event/destroyed/destroyed_menu.html
*/
var oldClean = jQuery.cleanData;
$.cleanData = function( elems ) {
for ( var i = 0, elem;
(elem = elems[i]) !== undefined; i++ ) {
$(elem).triggerHandler("destroyed");
//$.event.remove( elem, 'destroyed' );
}
oldClean(elems);
};
})(jQuery);
(function($){
var getSetZero = function(v){ return v !== undefined ? (this.array[0] = v) : this.array[0] },
getSetOne = function(v){ return v !== undefined ? (this.array[1] = v) : this.array[1] }
/**
* @class jQuery.Vector
* @parent jquerymx.lang
* A vector class
* @constructor creates a new vector instance from the arguments. Example:
* @codestart
* new jQuery.Vector(1,2)
* @codeend
*
*/
$.Vector = function() {
this.update($.makeArray(arguments));
};
$.Vector.prototype =
/* @Prototype*/
{
/**
* Applys the function to every item in the vector. Returns the new vector.
* @param {Function} f
* @return {jQuery.Vector} new vector class.
*/
app: function( f ) {
var i, vec, newArr = [];
for ( i = 0; i < this.array.length; i++ ) {
newArr.push(f(this.array[i]));
}
vec = new $.Vector();
return vec.update(newArr);
},
/**
* Adds two vectors together. Example:
* @codestart
* new Vector(1,2).plus(2,3) //-> &lt;3,5>
* new Vector(3,5).plus(new Vector(4,5)) //-> &lt;7,10>
* @codeend
* @return {$.Vector}
*/
plus: function() {
var i, args = arguments[0] instanceof $.Vector ? arguments[0].array : $.makeArray(arguments),
arr = this.array.slice(0),
vec = new $.Vector();
for ( i = 0; i < args.length; i++ ) {
arr[i] = (arr[i] ? arr[i] : 0) + args[i];
}
return vec.update(arr);
},
/**
* Like plus but subtracts 2 vectors
* @return {jQuery.Vector}
*/
minus: function() {
var i, args = arguments[0] instanceof $.Vector ? arguments[0].array : $.makeArray(arguments),
arr = this.array.slice(0),
vec = new $.Vector();
for ( i = 0; i < args.length; i++ ) {
arr[i] = (arr[i] ? arr[i] : 0) - args[i];
}
return vec.update(arr);
},
/**
* Returns the current vector if it is equal to the vector passed in.
* False if otherwise.
* @return {jQuery.Vector}
*/
equals: function() {
var i, args = arguments[0] instanceof $.Vector ? arguments[0].array : $.makeArray(arguments),
arr = this.array.slice(0),
vec = new $.Vector();
for ( i = 0; i < args.length; i++ ) {
if ( arr[i] != args[i] ) {
return null;
}
}
return vec.update(arr);
},
/**
* Returns the first value of the vector
* @return {Number}
*/
x: getSetZero,
/**
* same as x()
* @return {Number}
*/
left: getSetZero,
/**
* Returns the first value of the vector
* @return {Number}
*/
width: getSetZero,
/**
* Returns the 2nd value of the vector
* @return {Number}
*/
y: getSetOne,
/**
* Same as y()
* @return {Number}
*/
top: getSetOne,
/**
* Returns the 2nd value of the vector
* @return {Number}
*/
height: getSetOne,
/**
* returns (x,y)
* @return {String}
*/
toString: function() {
return "(" + this.array[0] + "," + this.array[1] + ")";
},
/**
* Replaces the vectors contents
* @param {Object} array
*/
update: function( array ) {
var i;
if ( this.array ) {
for ( i = 0; i < this.array.length; i++ ) {
delete this.array[i];
}
}
this.array = array;
for ( i = 0; i < array.length; i++ ) {
this[i] = this.array[i];
}
return this;
}
};
$.Event.prototype.vector = function() {
if ( this.originalEvent.synthetic ) {
var doc = document.documentElement,
body = document.body;
return new $.Vector(this.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0), this.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0));
} else {
return new $.Vector(this.pageX, this.pageY);
}
};
$.fn.offsetv = function() {
if ( this[0] == window ) {
return new $.Vector(window.pageXOffset ? window.pageXOffset : document.documentElement.scrollLeft, window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop);
} else {
var offset = this.offset();
return new $.Vector(offset.left, offset.top);
}
};
$.fn.dimensionsv = function( which ) {
if ( this[0] == window || !which ) {
return new $.Vector(this.width(), this.height());
}
else {
return new $.Vector(this[which + "Width"](), this[which + "Height"]());
}
};
})(jQuery);
(function() {
var event = jQuery.event,
//helper that finds handlers by type and calls back a function, this is basically handle
// events - the events object
// types - an array of event types to look for
// callback(type, handlerFunc, selector) - a callback
// selector - an optional selector to filter with, if there, matches by selector
// if null, matches anything, otherwise, matches with no selector
findHelper = function( events, types, callback, selector ) {
var t, type, typeHandlers, all, h, handle,
namespaces, namespace,
match;
for ( t = 0; t < types.length; t++ ) {
type = types[t];
all = type.indexOf(".") < 0;
if (!all ) {
namespaces = type.split(".");
type = namespaces.shift();
namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
}
typeHandlers = (events[type] || []).slice(0);
for ( h = 0; h < typeHandlers.length; h++ ) {
handle = typeHandlers[h];
match = (all || namespace.test(handle.namespace));
if(match){
if(selector){
if (handle.selector === selector ) {
callback(type, handle.origHandler || handle.handler);
}
} else if (selector === null){
callback(type, handle.origHandler || handle.handler, handle.selector);
}
else if (!handle.selector ) {
callback(type, handle.origHandler || handle.handler);
}
}
}
}
};
/**
* Finds event handlers of a given type on an element.
* @param {HTMLElement} el
* @param {Array} types an array of event names
* @param {String} [selector] optional selector
* @return {Array} an array of event handlers
*/
event.find = function( el, types, selector ) {
var events = ( $._data(el) || {} ).events,
handlers = [],
t, liver, live;
if (!events ) {
return handlers;
}
findHelper(events, types, function( type, handler ) {
handlers.push(handler);
}, selector);
return handlers;
};
/**
* Finds all events. Group by selector.
* @param {HTMLElement} el the element
* @param {Array} types event types
*/
event.findBySelector = function( el, types ) {
var events = $._data(el).events,
selectors = {},
//adds a handler for a given selector and event
add = function( selector, event, handler ) {
var select = selectors[selector] || (selectors[selector] = {}),
events = select[event] || (select[event] = []);
events.push(handler);
};
if (!events ) {
return selectors;
}
//first check live:
/*$.each(events.live || [], function( i, live ) {
if ( $.inArray(live.origType, types) !== -1 ) {
add(live.selector, live.origType, live.origHandler || live.handler);
}
});*/
//then check straight binds
findHelper(events, types, function( type, handler, selector ) {
add(selector || "", type, handler);
}, null);
return selectors;
};
event.supportTouch = "ontouchend" in document;
$.fn.respondsTo = function( events ) {
if (!this.length ) {
return false;
} else {
//add default ?
return event.find(this[0], $.isArray(events) ? events : [events]).length > 0;
}
};
$.fn.triggerHandled = function( event, data ) {
event = (typeof event == "string" ? $.Event(event) : event);
this.trigger(event, data);
return event.handled;
};
/**
* Only attaches one event handler for all types ...
* @param {Array} types llist of types that will delegate here
* @param {Object} startingEvent the first event to start listening to
* @param {Object} onFirst a function to call
*/
event.setupHelper = function( types, startingEvent, onFirst ) {
if (!onFirst ) {
onFirst = startingEvent;
startingEvent = null;
}
var add = function( handleObj ) {
var bySelector, selector = handleObj.selector || "";
if ( selector ) {
bySelector = event.find(this, types, selector);
if (!bySelector.length ) {
$(this).delegate(selector, startingEvent, onFirst);
}
}
else {
//var bySelector = event.find(this, types, selector);
if (!event.find(this, types, selector).length ) {
event.add(this, startingEvent, onFirst, {
selector: selector,
delegate: this
});
}
}
},
remove = function( handleObj ) {
var bySelector, selector = handleObj.selector || "";
if ( selector ) {
bySelector = event.find(this, types, selector);
if (!bySelector.length ) {
$(this).undelegate(selector, startingEvent, onFirst);
}
}
else {
if (!event.find(this, types, selector).length ) {
event.remove(this, startingEvent, onFirst, {
selector: selector,
delegate: this
});
}
}
};
$.each(types, function() {
event.special[this] = {
add: add,
remove: remove,
setup: function() {},
teardown: function() {}
};
});
};
})(jQuery);
(function( $ ) {
//modify live
//steal the live handler ....
var bind = function( object, method ) {
var args = Array.prototype.slice.call(arguments, 2);
return function() {
var args2 = [this].concat(args, $.makeArray(arguments));
return method.apply(object, args2);
};
},
event = $.event,
clearSelection = window.getSelection ? function(){
window.getSelection().removeAllRanges()
} : function(){};
// var handle = event.handle; //unused
/**
* @class jQuery.Drag
* @parent specialevents
* @plugin jquery/event/drag
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/drag/drag.js
* @test jquery/event/drag/qunit.html
* Provides drag events as a special events to jQuery.
* A jQuery.Drag instance is created on a drag and passed
* as a parameter to the drag event callbacks. By calling
* methods on the drag event, you can alter the drag's
* behavior.
* ## Drag Events
*
* The drag plugin allows you to listen to the following events:
*
* <ul>
* <li><code>dragdown</code> - the mouse cursor is pressed down</li>
* <li><code>draginit</code> - the drag motion is started</li>
* <li><code>dragmove</code> - the drag is moved</li>
* <li><code>dragend</code> - the drag has ended</li>
* <li><code>dragover</code> - the drag is over a drop point</li>
* <li><code>dragout</code> - the drag moved out of a drop point</li>
* </ul>
*
* Just by binding or delegating on one of these events, you make
* the element dragable. You can change the behavior of the drag
* by calling methods on the drag object passed to the callback.
*
* ### Example
*
* Here's a quick example:
*
* //makes the drag vertical
* $(".drags").delegate("draginit", function(event, drag){
* drag.vertical();
* })
* //gets the position of the drag and uses that to set the width
* //of an element
* $(".resize").delegate("dragmove",function(event, drag){
* $(this).width(drag.position.left() - $(this).offset().left )
* })
*
* ## Drag Object
*
* <p>The drag object is passed after the event to drag
* event callback functions. By calling methods
* and changing the properties of the drag object,
* you can alter how the drag behaves.
* </p>
* <p>The drag properties and methods:</p>
* <ul>
* <li><code>[jQuery.Drag.prototype.cancel cancel]</code> - stops the drag motion from happening</li>
* <li><code>[jQuery.Drag.prototype.ghost ghost]</code> - copys the draggable and drags the cloned element</li>
* <li><code>[jQuery.Drag.prototype.horizontal horizontal]</code> - limits the scroll to horizontal movement</li>
* <li><code>[jQuery.Drag.prototype.location location]</code> - where the drag should be on the screen</li>
* <li><code>[jQuery.Drag.prototype.mouseElementPosition mouseElementPosition]</code> - where the mouse should be on the drag</li>
* <li><code>[jQuery.Drag.prototype.only only]</code> - only have drags, no drops</li>
* <li><code>[jQuery.Drag.prototype.representative representative]</code> - move another element in place of this element</li>
* <li><code>[jQuery.Drag.prototype.revert revert]</code> - animate the drag back to its position</li>
* <li><code>[jQuery.Drag.prototype.vertical vertical]</code> - limit the drag to vertical movement</li>
* <li><code>[jQuery.Drag.prototype.limit limit]</code> - limit the drag within an element (*limit plugin)</li>
* <li><code>[jQuery.Drag.prototype.scrolls scrolls]</code> - scroll scrollable areas when dragging near their boundries (*scroll plugin)</li>
* </ul>
* <h2>Demo</h2>
* Now lets see some examples:
* @demo jquery/event/drag/drag.html 1000
* @constructor
* The constructor is never called directly.
*/
$.Drag = function() {};
/**
* @Static
*/
$.extend($.Drag, {
lowerName: "drag",
current: null,
distance: 0,
/**
* Called when someone mouses down on a draggable object.
* Gathers all callback functions and creates a new Draggable.
* @hide
*/
mousedown: function( ev, element ) {
var isLeftButton = ev.button === 0 || ev.button == 1;
if (!isLeftButton || this.current ) {
return;
} //only allows 1 drag at a time, but in future could allow more
//ev.preventDefault();
//create Drag
var drag = new $.Drag(),
delegate = ev.delegateTarget || element,
selector = ev.handleObj.selector,
self = this;
this.current = drag;
drag.setup({
element: element,
delegate: ev.delegateTarget || element,
selector: ev.handleObj.selector,
moved: false,
_distance: this.distance,
callbacks: {
dragdown: event.find(delegate, ["dragdown"], selector),
draginit: event.find(delegate, ["draginit"], selector),
dragover: event.find(delegate, ["dragover"], selector),
dragmove: event.find(delegate, ["dragmove"], selector),
dragout: event.find(delegate, ["dragout"], selector),
dragend: event.find(delegate, ["dragend"], selector)
},
destroyed: function() {
self.current = null;
}
}, ev);
}
});
/**
* @Prototype
*/
$.extend($.Drag.prototype, {
setup: function( options, ev ) {
$.extend(this, options);
this.element = $(this.element);
this.event = ev;
this.moved = false;
this.allowOtherDrags = false;
var mousemove = bind(this, this.mousemove),
mouseup = bind(this, this.mouseup);
this._mousemove = mousemove;
this._mouseup = mouseup;
this._distance = options.distance ? options.distance : 0;
this.mouseStartPosition = ev.vector(); //where the mouse is located
$(document).bind('mousemove', mousemove);
$(document).bind('mouseup', mouseup);
if (!this.callEvents('down', this.element, ev) ) {
this.noSelection(this.delegate);
//this is for firefox
clearSelection();
}
},
/**
* Unbinds listeners and allows other drags ...
* @hide
*/
destroy: function() {
$(document).unbind('mousemove', this._mousemove);
$(document).unbind('mouseup', this._mouseup);
if (!this.moved ) {
this.event = this.element = null;
}
this.selection(this.delegate);
this.destroyed();
},
mousemove: function( docEl, ev ) {
if (!this.moved ) {
var dist = Math.sqrt( Math.pow( ev.pageX - this.event.pageX, 2 ) + Math.pow( ev.pageY - this.event.pageY, 2 ));
if(dist < this._distance){
return false;
}
this.init(this.element, ev);
this.moved = true;
}
var pointer = ev.vector();
if ( this._start_position && this._start_position.equals(pointer) ) {
return;
}
//e.preventDefault();
this.draw(pointer, ev);
},
mouseup: function( docEl, event ) {
//if there is a current, we should call its dragstop
if ( this.moved ) {
this.end(event);
}
this.destroy();
},
/**
* noSelection method turns off text selection during a drag event.
* This method is called by default unless a event is listening to the 'dragdown' event.
*
* ## Example
*
* $('div.drag').bind('dragdown', function(elm,event,drag){
* drag.noSelection();
* });
*
* @param [elm] an element to prevent selection on. Defaults to the dragable element.
*/
noSelection: function(elm) {
elm = elm || this.delegate
document.documentElement.onselectstart = function() {
return false;
};
document.documentElement.unselectable = "on";
this.selectionDisabled = (this.selectionDisabled ? this.selectionDisabled.add(elm) : $(elm));
this.selectionDisabled.css('-moz-user-select', '-moz-none');
},
/**
* selection method turns on text selection that was previously turned off during the drag event.
* This method is called by default in 'destroy' unless a event is listening to the 'dragdown' event.
*
* ## Example
*
* $('div.drag').bind('dragdown', function(elm,event,drag){
* drag.noSelection();
* });
*/
selection: function(elm) {
if(this.selectionDisabled){
document.documentElement.onselectstart = function() {};
document.documentElement.unselectable = "off";
this.selectionDisabled.css('-moz-user-select', '');
}
},
init: function( element, event ) {
element = $(element);
var startElement = (this.movingElement = (this.element = $(element))); //the element that has been clicked on
//if a mousemove has come after the click
this._cancelled = false; //if the drag has been cancelled
this.event = event;
/**
* @attribute mouseElementPosition
* The position of start of the cursor on the element
*/
this.mouseElementPosition = this.mouseStartPosition.minus(this.element.offsetv()); //where the mouse is on the Element
//this.callStart(element, event);
this.callEvents('init', element, event);
//Check what they have set and respond accordingly
// if they canceled
if ( this._cancelled === true ) {
return;
}
//if they set something else as the element
this.startPosition = startElement != this.movingElement ? this.movingElement.offsetv() : this.currentDelta();
this.makePositioned(this.movingElement);
this.oldZIndex = this.movingElement.css('zIndex');
this.movingElement.css('zIndex', 1000);
if (!this._only && this.constructor.responder ) {
this.constructor.responder.compile(event, this);
}
},
makePositioned: function( that ) {
var style, pos = that.css('position');
if (!pos || pos == 'static' ) {
style = {
position: 'relative'
};
if ( window.opera ) {
style.top = '0px';
style.left = '0px';
}
that.css(style);
}
},
callEvents: function( type, element, event, drop ) {
var i, cbs = this.callbacks[this.constructor.lowerName + type];
for ( i = 0; i < cbs.length; i++ ) {
cbs[i].call(element, event, this, drop);
}
return cbs.length;
},
/**
* Returns the position of the movingElement by taking its top and left.
* @hide
* @return {Vector}
*/
currentDelta: function() {
return new $.Vector(parseInt(this.movingElement.css('left'), 10) || 0, parseInt(this.movingElement.css('top'), 10) || 0);
},
//draws the position of the dragmove object
draw: function( pointer, event ) {
// only drag if we haven't been cancelled;
if ( this._cancelled ) {
return;
}
clearSelection();
/**
* @attribute location
* The location of where the element should be in the page. This
* takes into account the start position of the cursor on the element.
*
* If the drag is going to be moved to an unacceptable location, you can call preventDefault in
* dragmove to prevent it from being moved there.
*
* $('.mover').bind("dragmove", function(ev, drag){
* if(drag.location.top() < 100){
* ev.preventDefault()
* }
* });
*
* You can also set the location to where it should be on the page.
*/
this.location = pointer.minus(this.mouseElementPosition); // the offset between the mouse pointer and the representative that the user asked for
// position = mouse - (dragOffset - dragTopLeft) - mousePosition
// call move events
this.move(event);
if ( this._cancelled ) {
return;
}
if (!event.isDefaultPrevented() ) {
this.position(this.location);
}
//fill in
if (!this._only && this.constructor.responder ) {
this.constructor.responder.show(pointer, this, event);
}
},
/**
* Sets the position of this drag.
*
* The limit and scroll plugins
* overwrite this to make sure the drag follows a particular path.
*
* @param {jQuery.Vector} newOffsetv the position of the element (not the mouse)
*/
position: function( newOffsetv ) { //should draw it on the page
var style, dragged_element_css_offset = this.currentDelta(),
// the drag element's current left + top css attributes
dragged_element_position_vector = // the vector between the movingElement's page and css positions
this.movingElement.offsetv().minus(dragged_element_css_offset); // this can be thought of as the original offset
this.required_css_position = newOffsetv.minus(dragged_element_position_vector);
this.offsetv = newOffsetv;
//dragged_element vector can probably be cached.
style = this.movingElement[0].style;
if (!this._cancelled && !this._horizontal ) {
style.top = this.required_css_position.top() + "px";
}
if (!this._cancelled && !this._vertical ) {
style.left = this.required_css_position.left() + "px";
}
},
move: function( event ) {
this.callEvents('move', this.element, event);
},
over: function( event, drop ) {
this.callEvents('over', this.element, event, drop);
},
out: function( event, drop ) {
this.callEvents('out', this.element, event, drop);
},
/**
* Called on drag up
* @hide
* @param {Event} event a mouseup event signalling drag/drop has completed
*/
end: function( event ) {
if ( this._cancelled ) {
return;
}
if (!this._only && this.constructor.responder ) {
this.constructor.responder.end(event, this);
}
this.callEvents('end', this.element, event);
if ( this._revert ) {
var self = this;
this.movingElement.animate({
top: this.startPosition.top() + "px",
left: this.startPosition.left() + "px"
}, function() {
self.cleanup.apply(self, arguments);
});
}
else {
this.cleanup();
}
this.event = null;
},
/**
* Cleans up drag element after drag drop.
* @hide
*/
cleanup: function() {
this.movingElement.css({
zIndex: this.oldZIndex
});
if ( this.movingElement[0] !== this.element[0] &&
!this.movingElement.has(this.element[0]).length &&
!this.element.has(this.movingElement[0]).length ) {
this.movingElement.css({
display: 'none'
});
}
if ( this._removeMovingElement ) {
this.movingElement.remove();
}
this.movingElement = this.element = this.event = null;
},
/**
* Stops drag drop from running.
*/
cancel: function() {
this._cancelled = true;
//this.end(this.event);
if (!this._only && this.constructor.responder ) {
this.constructor.responder.clear(this.event.vector(), this, this.event);
}
this.destroy();
},
/**
* Clones the element and uses it as the moving element.
* @return {jQuery.fn} the ghost
*/
ghost: function( loc ) {
// create a ghost by cloning the source element and attach the clone to the dom after the source element
var ghost = this.movingElement.clone().css('position', 'absolute');
(loc ? $(loc) : this.movingElement).after(ghost);
ghost.width(this.movingElement.width()).height(this.movingElement.height());
// put the ghost in the right location ...
ghost.offset(this.movingElement.offset())
// store the original element and make the ghost the dragged element
this.movingElement = ghost;
this.noSelection(ghost)
this._removeMovingElement = true;
return ghost;
},
/**
* Use a representative element, instead of the movingElement.
* @param {HTMLElement} element the element you want to actually drag
* @param {Number} offsetX the x position where you want your mouse on the object
* @param {Number} offsetY the y position where you want your mouse on the object
*/
representative: function( element, offsetX, offsetY ) {
this._offsetX = offsetX || 0;
this._offsetY = offsetY || 0;
var p = this.mouseStartPosition;
this.movingElement = $(element);
this.movingElement.css({
top: (p.y() - this._offsetY) + "px",
left: (p.x() - this._offsetX) + "px",
display: 'block',
position: 'absolute'
}).show();
this.noSelection(this.movingElement)
this.mouseElementPosition = new $.Vector(this._offsetX, this._offsetY);
},
/**
* Makes the movingElement go back to its original position after drop.
* @codestart
* ".handle dragend" : function( el, ev, drag ) {
* drag.revert()
* }
* @codeend
* @param {Boolean} [val] optional, set to false if you don't want to revert.
*/
revert: function( val ) {
this._revert = val === undefined ? true : val;
return this;
},
/**
* Isolates the drag to vertical movement.
*/
vertical: function() {
this._vertical = true;
return this;
},
/**
* Isolates the drag to horizontal movement.
*/
horizontal: function() {
this._horizontal = true;
return true;
},
/**
* Respondables will not be alerted to this drag.
*/
only: function( only ) {
return (this._only = (only === undefined ? true : only));
},
/**
* Sets the distance from the mouse before the item begins dragging.
* @param {Number} val
*/
distance:function(val){
if(val !== undefined){
this._distance = val;
return this;
}else{
return this._distance
}
}
});
/**
* @add jQuery.event.special
*/
event.setupHelper([
/**
* @attribute dragdown
* <p>Listens for when a drag movement has started on a mousedown.
* If you listen to this, the mousedown's default event (preventing
* text selection) is not prevented. You are responsible for calling it
* if you want it (you probably do). </p>
* <p><b>Why might you not want it?</b></p>
* <p>You might want it if you want to allow text selection on element
* within the drag element. Typically these are input elements.</p>
* <p>Drag events are covered in more detail in [jQuery.Drag].</p>
* @codestart
* $(".handles").delegate("dragdown", function(ev, drag){})
* @codeend
*/
'dragdown',
/**
* @attribute draginit
* Called when the drag starts.
* <p>Drag events are covered in more detail in [jQuery.Drag].</p>
*/
'draginit',
/**
* @attribute dragover
* Called when the drag is over a drop.
* <p>Drag events are covered in more detail in [jQuery.Drag].</p>
*/
'dragover',
/**
* @attribute dragmove
* Called when the drag is moved.
* <p>Drag events are covered in more detail in [jQuery.Drag].</p>
*/
'dragmove',
/**
* @attribute dragout
* When the drag leaves a drop point.
* <p>Drag events are covered in more detail in [jQuery.Drag].</p>
*/
'dragout',
/**
* @attribute dragend
* Called when the drag is done.
* <p>Drag events are covered in more detail in [jQuery.Drag].</p>
*/
'dragend'], "mousedown", function( e ) {
$.Drag.mousedown.call($.Drag, e, this);
});
})(jQuery);
(function($){
/**
* @function jQuery.fn.triggerAsync
* @plugin jquery/event/default
* @parent jquery.event.pause
*
* Triggers an event and calls success when the event has finished propagating through the DOM and preventDefault is not called.
*
* $('#panel').triggerAsync('show', function() {
* $('#panel').show();
* });
*
* You can also provide a callback that gets called if preventDefault was called on the event:
*
* $('panel').triggerAsync('show', function(){
* $('#panel').show();
* },function(){
* $('#other').addClass('error');
* });
*
* triggerAsync is design to work with the [jquery.event.pause]
* plugin although it is defined in _jquery/event/default_.
*
* @param {String} type The type of event
* @param {Object} data The data for the event
* @param {Function} success a callback function which occurs upon success
* @param {Function} prevented a callback function which occurs if preventDefault was called
*/
$.fn.triggerAsync = function(type, data, success, prevented){
if(typeof data == 'function'){
success = data;
data = undefined;
}
if ( this[0] ) {
var event = $.Event( type ),
old = event.preventDefault;
event.preventDefault = function(){
old.apply(this, arguments);
prevented && prevented(this)
}
//event._success= success;
jQuery.event.trigger( {type: type, _success: success}, data, this[0] );
} else{
success.call(this);
}
return this;
}
/**
* @add jQuery.event.special
*/
//cache default types for performance
var types = {}, rnamespaces= /\.(.*)$/, $event = $.event;
/**
* @attribute default
* @parent specialevents
* @plugin jquery/event/default
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/default/default.js
* @test jquery/event/default/qunit.html
* Allows you to perform default actions as a result of an event.
*
* Event based APIs are a powerful way of exposing functionality of your widgets. It also fits in
* quite nicely with how the DOM works.
*
*
* Like default events in normal functions (e.g. submitting a form), synthetic default events run after
* all event handlers have been triggered and no event handler has called
* preventDefault or returned false.
*
* To listen for a default event, just prefix the event with default.
*
* $("div").bind("default.show", function(ev){ ... });
* $("ul").delegate("li","default.activate", function(ev){ ... });
*
*
* ## Example
*
* Lets look at how you could build a simple tabs widget with default events.
* First with just jQuery:
*
* Default events are useful in cases where you want to provide an event based
* API for users of your widgets. Users can simply listen to your synthetic events and
* prevent your default functionality by calling preventDefault.
*
* In the example below, the tabs widget provides a show event. Users of the
* tabs widget simply listen for show, and if they wish for some reason, call preventDefault
* to avoid showing the tab.
*
* In this case, the application developer doesn't want to show the second
* tab until the checkbox is checked.
*
* @demo jquery/event/default/defaultjquery.html
*
* Lets see how we would build this with JavaScriptMVC:
*
* @demo jquery/event/default/default.html
*/
$event.special["default"] = {
add: function( handleObj ) {
//save the type
types[handleObj.namespace.replace(rnamespaces,"")] = true;
},
setup: function() {return true}
}
// overwrite trigger to allow default types
var oldTrigger = $event.trigger;
$event.trigger = function defaultTriggerer( event, data, elem, onlyHandlers){
// Event object or event type
var type = event.type || event,
namespaces = [],
// Caller can pass in an Event, Object, or just an event type string
event = typeof event === "object" ?
// jQuery.Event object
event[ jQuery.expando ] ? event :
// Object literal
new jQuery.Event( type, event ) :
// Just the event type (string)
new jQuery.Event( type );
//event._defaultActions = []; //set depth for possibly reused events
var res = oldTrigger.call($.event, event, data, elem, onlyHandlers);
if(!onlyHandlers && !event.isDefaultPrevented() && event.type.indexOf("default") !== 0){
oldTrigger("default."+event.type, data, elem)
if(event._success){
event._success(event)
}
}
// code for paused
if( event.isPaused && event.isPaused() ){
// set back original stuff
event.isDefaultPrevented =
event.pausedState.isDefaultPrevented;
event.isPropagationStopped =
event.pausedState.isPropagationStopped;
}
return res;
};
})(jQuery);
(function($){
var current,
rnamespaces = /\.(.*)$/,
returnFalse = function(){return false},
returnTrue = function(){return true};
/**
* @function
* @parent jquery.event.pause
* Pauses an event (to be resumed later);
*/
//
/**
* @function
* @parent jquery.event.pause
*
* Resumes an event
*/
//
/**
* @page jquery.event.pause Pause-Resume
* @plugin jquery/event/pause
* @parent specialevents
* The jquery/event/pause plugin adds the ability to pause and
* resume events.
*
* $('#todos').bind('show', function(ev){
* ev.pause();
*
* $(this).load('todos.html', function(){
* ev.resume();
* });
* })
*
* When an event is paused, stops calling other event handlers for the
* event (similar to event.stopImmediatePropagation() ). But when
* resume is called on the event, it will begin calling events on event handlers
* after the 'paused' event handler.
*
*
* Pause-able events complement the [jQuery.event.special.default default]
* events plugin, providing the ability to easy create widgets with
* an asynchronous API.
*
* ## Example
*
* Consider a basic tabs widget that:
*
* - trigger's a __show__ event on panels when they are to be displayed
* - shows the panel after the show event.
*
* The sudo code for this controller might look like:
*
* $.Controller('Tabs',{
* ".button click" : function( el ){
* var panel = this.getPanelFromButton( el );
* panel.triggerAsync('show', function(){
* panel.show();
* })
* }
* })
*
* Someone using this plugin would be able to delay the panel showing until ready:
*
* $('#todos').bind('show', function(ev){
* ev.pause();
*
* $(this).load('todos.html', function(){
* ev.resume();
* });
* })
*
* Or prevent the panel from showing at all:
*
* $('#todos').bind('show', function(ev){
* if(! isReady()){
* ev.preventDefault();
* }
* })
*
* ## Limitations
*
* The element and event handler that the <code>pause</code> is within can not be removed before
* resume is called.
*
* ## Big Example
*
* The following example shows a tabs widget where the user is prompted to save, ignore, or keep editing
* a tab when a new tab is clicked.
*
* @demo jquery/event/pause/pause.html
*
* It's a long, but great example of how to do some pretty complex state management with JavaScriptMVC.
*
*/
$.Event.prototype.isPaused = returnFalse
$.Event.prototype.pause = function(){
// stop the event from continuing temporarily
// keep the current state of the event ...
this.pausedState = {
isDefaultPrevented : this.isDefaultPrevented() ?
returnTrue : returnFalse,
isPropagationStopped : this.isPropagationStopped() ?
returnTrue : returnFalse
};
this.stopImmediatePropagation();
this.preventDefault();
this.isPaused = returnTrue;
};
$.Event.prototype.resume = function(){
// temporarily remove all event handlers of this type
var handleObj = this.handleObj,
currentTarget = this.currentTarget;
// temporarily overwrite special handle
var origType = jQuery.event.special[ handleObj.origType ],
origHandle = origType && origType.handle;
if(!origType){
jQuery.event.special[ handleObj.origType ] = {};
}
jQuery.event.special[ handleObj.origType ].handle = function(ev){
// remove this once we have passed the handleObj
if(ev.handleObj === handleObj && ev.currentTarget === currentTarget){
if(!origType){
delete jQuery.event.special[ handleObj.origType ];
} else {
jQuery.event.special[ handleObj.origType ].handle = origHandle;
}
}
}
delete this.pausedState;
// reset stuff
this.isPaused = this.isImmediatePropagationStopped = returnFalse;
// re-run dispatch
//$.event.dispatch.call(currentTarget, this)
// with the events removed, dispatch
if(!this.isPropagationStopped()){
// fire the event again, no events will get fired until
// same currentTarget / handler
$.event.trigger(this, [], this.target);
}
};
/*var oldDispatch = $.event.dispatch;
$.event.dispatch = function(){
}*/
// we need to finish handling
// and then trigger on next element ...
// can we fake the target ?
})(jQuery);
(function( $ ) {
/**
* @add jQuery.event.special
*/
var resizers = $(),
resizeCount = 0,
// bind on the window window resizes to happen
win = $(window),
windowWidth = 0,
windowHeight = 0,
timer;
$(function() {
windowWidth = win.width();
windowHeight = win.height();
})
/**
* @attribute resize
* @parent specialevents
*
* The resize event is useful for updating elements dimensions when a parent element
* has been resized. It allows you to only resize elements that need to be resized
* in the 'right order'.
*
* By listening to a resize event, you will be alerted whenever a parent
* element has a <code>resize</code> event triggered on it. For example:
*
* $('#foo').bind('resize', function(){
* // adjust #foo's dimensions
* })
*
* $(document.body).trigger("resize");
*
* ## The 'Right Order'
*
* When a control changes size, typically, you want only internal controls to have to adjust their
* dimensions. Furthermore, you want to adjust controls from the 'outside-in', meaning
* that the outermost control adjusts its dimensions before child controls adjust theirs.
*
* Resize calls resize events in exactly this manner.
*
* When you trigger a resize event, it will propagate up the DOM until it reaches
* an element with the first resize event
* handler. There it will move the event in the opposite direction, calling the element's
* children's resize event handlers.
*
* If your intent is to call resize without bubbling and only trigger child element's handlers,
* use the following:
*
* $("#foo").trigger("resize", false);
*
* ## Stopping Children Updates
*
* If your element doesn't need to change it's dimensions as a result of the parent element, it should
* call ev.stopPropagation(). This will only stop resize from being sent to child elements of the current element.
*
*
*/
$.event.special.resize = {
setup: function( handleObj ) {
// add and sort the resizers array
// don't add window because it can't be compared easily
if ( this !== window ) {
resizers.push(this);
$.unique(resizers);
}
// returns false if the window
return this !== window;
},
teardown: function() {
// we shouldn't have to sort
resizers = resizers.not(this);
// returns false if the window
return this !== window;
},
add: function( handleObj ) {
// increment the number of resizer elements
//$.data(this, "jquery.dom.resizers", ++$.data(this, "jquery.dom.resizers") );
var origHandler = handleObj.handler;
handleObj.origHandler = origHandler;
handleObj.handler = function( ev, data ) {
var isWindow = this === window;
// if we are the window and a real resize has happened
// then we check if the dimensions actually changed
// if they did, we will wait a brief timeout and
// trigger resize on the window
// this is for IE, to prevent window resize 'infinate' loop issues
if ( isWindow && ev.originalEvent ) {
var width = win.width(),
height = win.height();
if ((width != windowWidth || height != windowHeight)) {
//update the new dimensions
windowWidth = width;
windowHeight = height;
clearTimeout(timer)
timer = setTimeout(function() {
win.trigger("resize");
}, 1);
}
return;
}
// if this is the first handler for this event ...
if ( resizeCount === 0 ) {
// prevent others from doing what we are about to do
resizeCount++;
var where = data === false ? ev.target : this
//trigger all this element's handlers
$.event.handle.call(where, ev);
if ( ev.isPropagationStopped() ) {
resizeCount--;
return;
}
// get all other elements within this element that listen to resize
// and trigger their resize events
var index = resizers.index(this),
length = resizers.length,
child, sub;
// if index == -1 it's the window
while (++index < length && (child = resizers[index]) && (isWindow || $.contains(where, child)) ) {
// call the event
$.event.handle.call(child, ev);
if ( ev.isPropagationStopped() ) {
// move index until the item is not in the current child
while (++index < length && (sub = resizers[index]) ) {
if (!$.contains(child, sub) ) {
// set index back one
index--;
break
}
}
}
}
// prevent others from responding
ev.stopImmediatePropagation();
resizeCount--;
} else {
handleObj.origHandler.call(this, ev, data);
}
}
}
};
// automatically bind on these
$([document, window]).bind('resize', function() {})
})(jQuery);
(function( $ ) {
var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
rupper = /([A-Z])/g,
rdashAlpha = /-([a-z])/ig,
fcamelCase = function( all, letter ) {
return letter.toUpperCase();
},
getStyle = function( elem ) {
if ( getComputedStyle ) {
return getComputedStyle(elem, null);
}
else if ( elem.currentStyle ) {
return elem.currentStyle;
}
},
rfloat = /float/i,
rnumpx = /^-?\d+(?:px)?$/i,
rnum = /^-?\d/;
/**
* @add jQuery
*/
//
/**
* @function curStyles
* @param {HTMLElement} el
* @param {Array} styles An array of style names like <code>['marginTop','borderLeft']</code>
* @return {Object} an object of style:value pairs. Style names are camelCase.
*/
$.curStyles = function( el, styles ) {
if (!el ) {
return null;
}
var currentS = getStyle(el),
oldName, val, style = el.style,
results = {},
i = 0,
left, rsLeft, camelCase, name;
for (; i < styles.length; i++ ) {
name = styles[i];
oldName = name.replace(rdashAlpha, fcamelCase);
if ( rfloat.test(name) ) {
name = jQuery.support.cssFloat ? "float" : "styleFloat";
oldName = "cssFloat";
}
if ( getComputedStyle ) {
name = name.replace(rupper, "-$1").toLowerCase();
val = currentS.getPropertyValue(name);
if ( name === "opacity" && val === "" ) {
val = "1";
}
results[oldName] = val;
} else {
camelCase = name.replace(rdashAlpha, fcamelCase);
results[oldName] = currentS[name] || currentS[camelCase];
if (!rnumpx.test(results[oldName]) && rnum.test(results[oldName]) ) { //convert to px
// Remember the original values
left = style.left;
rsLeft = el.runtimeStyle.left;
// Put in the new values to get a computed value out
el.runtimeStyle.left = el.currentStyle.left;
style.left = camelCase === "fontSize" ? "1em" : (results[oldName] || 0);
results[oldName] = style.pixelLeft + "px";
// Revert the changed values
style.left = left;
el.runtimeStyle.left = rsLeft;
}
}
}
return results;
};
/**
* @add jQuery.fn
*/
$.fn
/**
* @parent dom
* @plugin jquery/dom/cur_styles
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/cur_styles/cur_styles.js
* @test jquery/dom/cur_styles/qunit.html
* Use curStyles to rapidly get a bunch of computed styles from an element.
* <h3>Quick Example</h3>
* @codestart
* $("#foo").curStyles('float','display') //->
* // {
* // cssFloat: "left", display: "block"
* // }
* @codeend
* <h2>Use</h2>
* <p>An element's <b>computed</b> style is the current calculated style of the property.
* This is different than the values on <code>element.style</code> as
* <code>element.style</code> doesn't reflect styles provided by css or the browser's default
* css properties.</p>
* <p>Getting computed values individually is expensive! This plugin lets you get all
* the style properties you need all at once.</p>
* <h2>Demo</h2>
* <p>The following demo illustrates the performance improvement curStyle provides by providing
* a faster 'height' jQuery function called 'fastHeight'.</p>
* @demo jquery/dom/cur_styles/cur_styles.html
* @param {String} style pass style names as arguments
* @return {Object} an object of style:value pairs
*/
.curStyles = function() {
return $.curStyles(this[0], $.makeArray(arguments));
};
})(jQuery);
(function( $ ) {
$.Drag.prototype
/**
* @function limit
* @plugin jquery/event/drag/limit
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/event/drag/limit/limit.js
* limits the drag to a containing element
* @param {jQuery} container
* @param {Object} [center] can set the limit to the center of the object. Can be
* 'x', 'y' or 'both'
* @return {$.Drag}
*/
.limit = function( container, center ) {
//on draws ... make sure this happens
var styles = container.curStyles('borderTopWidth', 'paddingTop', 'borderLeftWidth', 'paddingLeft'),
paddingBorder = new $.Vector(
parseInt(styles.borderLeftWidth, 10) + parseInt(styles.paddingLeft, 10) || 0, parseInt(styles.borderTopWidth, 10) + parseInt(styles.paddingTop, 10) || 0);
this._limit = {
offset: container.offsetv().plus(paddingBorder),
size: container.dimensionsv(),
center : center === true ? 'both' : center
};
return this;
};
var oldPosition = $.Drag.prototype.position;
$.Drag.prototype.position = function( offsetPositionv ) {
//adjust required_css_position accordingly
if ( this._limit ) {
var limit = this._limit,
center = limit.center && limit.center.toLowerCase(),
movingSize = this.movingElement.dimensionsv('outer'),
halfHeight = center && center != 'x' ? movingSize.height() / 2 : 0,
halfWidth = center && center != 'y' ? movingSize.width() / 2 : 0,
lot = limit.offset.top(),
lof = limit.offset.left(),
height = limit.size.height(),
width = limit.size.width();
//check if we are out of bounds ...
//above
if ( offsetPositionv.top()+halfHeight < lot ) {
offsetPositionv.top(lot - halfHeight);
}
//below
if ( offsetPositionv.top() + movingSize.height() - halfHeight > lot + height ) {
offsetPositionv.top(lot + height - movingSize.height() + halfHeight);
}
//left
if ( offsetPositionv.left()+halfWidth < lof ) {
offsetPositionv.left(lof - halfWidth);
}
//right
if ( offsetPositionv.left() + movingSize.width() -halfWidth > lof + width ) {
offsetPositionv.left(lof + width - movingSize.left()+halfWidth);
}
}
oldPosition.call(this, offsetPositionv);
};
})(jQuery);
(function($){ //needs drop to determine if respondable
/**
* @add jQuery.Drag.prototype
*/
$.Drag.prototype.
/**
* Will scroll elements with a scroll bar as the drag moves to borders.
* @plugin jquery/event/drag/scroll
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/drag/scroll/scroll.js
* @param {jQuery} elements to scroll. The window can be in this array.
* @param {Object} options changes the default settings.
*
* - distance {number} 30 - how many pixels away from a boundry where we start scrolling
* - delta(diff) {Function} - returns how far we should scroll. It is passed how many pixels the cursor is
* from the boundry.
* - direction {String} - direction scrolling should happen. "xy" is the default.
*/
scrolls = function(elements, options){
var elements = $(elements);
for(var i = 0 ; i < elements.length; i++){
this.constructor.responder._elements.push( elements.eq(i).data("_dropData", new $.Scrollable(elements[i], options) )[0] )
}
},
$.Scrollable = function(element, options){
this.element = jQuery(element);
this.options = $.extend({
// when we should start scrolling
distance : 30,
// how far we should move
delta : function(diff, distance){
return (distance - diff) / 2;
},
direction: "xy"
}, options);
this.x = this.options.direction.indexOf("x") != -1;
this.y = this.options.direction.indexOf("y") != -1;
}
$.extend($.Scrollable.prototype,{
init: function( element ) {
this.element = jQuery(element);
},
callHandlers: function( method, el, ev, drag ) {
this[method](el || this.element[0], ev, this, drag)
},
dropover: function() {
},
dropon: function() {
this.clear_timeout();
},
dropout: function() {
this.clear_timeout();
},
dropinit: function() {
},
dropend: function() {},
clear_timeout: function() {
if(this.interval){
clearTimeout(this.interval)
this.interval = null;
}
},
distance: function( diff ) {
return (30 - diff) / 2;
},
dropmove: function( el, ev, drop, drag ) {
//if we were about to call a move, clear it.
this.clear_timeout();
//position of the mouse
var mouse = ev.vector(),
//get the object we are going to get the boundries of
location_object = $(el == document.documentElement ? window : el),
//get the dimension and location of that object
dimensions = location_object.dimensionsv('outer'),
position = location_object.offsetv(),
//how close our mouse is to the boundries
bottom = position.y()+dimensions.y() - mouse.y(),
top = mouse.y() - position.y(),
right = position.x()+dimensions.x() - mouse.x(),
left = mouse.x() - position.x(),
//how far we should scroll
dx =0, dy =0,
distance = this.options.distance;
//check if we should scroll
if(bottom < distance && this.y)
dy = this.options.delta(bottom,distance);
else if(top < distance && this.y)
dy = -this.options.delta(top,distance)
if(right < distance && this.options && this.x)
dx = this.options.delta(right,distance);
else if(left < distance && this.x)
dx = -this.options.delta(left,distance);
//if we should scroll
if(dx || dy){
//set a timeout that will create a mousemove on that object
var self = this;
this.interval = setTimeout( function(){
self.move($(el), drag.movingElement, dx, dy, ev, ev.clientX, ev.clientY, ev.screenX, ev.screenY)
},15)
}
},
/**
* Scrolls an element then calls mouse a mousemove in the same location.
* @param {HTMLElement} scroll_element the element to be scrolled
* @param {HTMLElement} drag_element
* @param {Number} dx how far to scroll
* @param {Number} dy how far to scroll
* @param {Number} x the mouse position
* @param {Number} y the mouse position
*/
move: function( scroll_element, drag_element, dx, dy, ev/*, x,y,sx, sy*/ ) {
scroll_element.scrollTop( scroll_element.scrollTop() + dy);
scroll_element.scrollLeft(scroll_element.scrollLeft() + dx);
drag_element.trigger(
$.event.fix({type: "mousemove",
clientX: ev.clientX,
clientY: ev.clientY,
screenX: ev.screenX,
screenY: ev.screenY,
pageX: ev.pageX,
pageY: ev.pageY}))
//drag_element.synthetic('mousemove',{clientX: x, clientY: y, screenX: sx, screenY: sy})
}
})
})(jQuery);
(function($){
var withinBox = function(x, y, left, top, width, height ){
return (y >= top &&
y < top + height &&
x >= left &&
x < left + width);
}
/**
* @function within
* @parent dom
* @plugin jquery/dom/within
*
* Returns the elements are within the position.
*
* // get all elements that touch 200x200.
* $('*').within(200, 200);
*
* @param {Number} left the position from the left of the page
* @param {Number} top the position from the top of the page
* @param {Boolean} [useOffsetCache] cache the dimensions and offset of the elements.
* @return {jQuery} a jQuery collection of elements whos area
* overlaps the element position.
*/
$.fn.within= function(left, top, useOffsetCache) {
var ret = []
this.each(function(){
var q = jQuery(this);
if (this == document.documentElement) {
return ret.push(this);
}
var offset = useOffsetCache ?
jQuery.data(this,"offsetCache") || jQuery.data(this,"offsetCache", q.offset()) :
q.offset();
var res = withinBox(left, top, offset.left, offset.top,
this.offsetWidth, this.offsetHeight );
if (res) {
ret.push(this);
}
});
return this.pushStack( jQuery.unique( ret ), "within", left+","+top );
}
/**
* @function withinBox
* @parent jQuery.fn.within
* returns if elements are within the box
* @param {Object} left
* @param {Object} top
* @param {Object} width
* @param {Object} height
* @param {Object} cache
*/
$.fn.withinBox = function(left, top, width, height, cache){
var ret = []
this.each(function(){
var q = jQuery(this);
if(this == document.documentElement) return this.ret.push(this);
var offset = cache ?
jQuery.data(this,"offset") ||
jQuery.data(this,"offset", q.offset()) :
q.offset();
var ew = q.width(), eh = q.height();
res = !( (offset.top > top+height) || (offset.top +eh < top) || (offset.left > left+width ) || (offset.left+ew < left));
if(res)
ret.push(this);
});
return this.pushStack( jQuery.unique( ret ), "withinBox", jQuery.makeArray(arguments).join(",") );
}
})(jQuery);
(function($){
/**
* @function compare
* @parent dom
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/compare/compare.js
*
* Compares the position of two nodes and returns a bitmask detailing how they are positioned
* relative to each other.
*
* $('#foo').compare($('#bar')) //-> Number
*
* You can expect it to return the same results as
* [http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition | compareDocumentPosition].
* Parts of this documentation and source come from [http://ejohn.org/blog/comparing-document-position | John Resig].
*
* ## Demo
* @demo jquery/dom/compare/compare.html
* @test jquery/dom/compare/qunit.html
* @plugin dom/compare
*
*
* @param {HTMLElement|jQuery} element an element or jQuery collection to compare against.
* @return {Number} A bitmap number representing how the elements are positioned from each other.
*
* If the code looks like:
*
* $('#foo').compare($('#bar')) //-> Number
*
* Number is a bitmap with with the following values:
* <table class='options'>
* <tr><th>Bits</th><th>Number</th><th>Meaning</th></tr>
* <tr><td>000000</td><td>0</td><td>Elements are identical.</td></tr>
* <tr><td>000001</td><td>1</td><td>The nodes are in different
* documents (or one is outside of a document).</td></tr>
* <tr><td>000010</td><td>2</td><td>#bar precedes #foo.</td></tr>
* <tr><td>000100</td><td>4</td><td>#foo precedes #bar.</td></tr>
* <tr><td>001000</td><td>8</td><td>#bar contains #foo.</td></tr>
* <tr><td>010000</td><td>16</td><td>#foo contains #bar.</td></tr>
* </table>
*/
jQuery.fn.compare = function(element){ //usually
//element is usually a relatedTarget, but element/c it is we have to avoid a few FF errors
try{ //FF3 freaks out with XUL
element = element.jquery ? element[0] : element;
}catch(e){
return null;
}
if (window.HTMLElement) { //make sure we aren't coming from XUL element
var s = HTMLElement.prototype.toString.call(element)
if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]' || s === '[object Window]') {
return null;
}
}
if(this[0].compareDocumentPosition){
return this[0].compareDocumentPosition(element);
}
if(this[0] == document && element != document) return 8;
var number = (this[0] !== element && this[0].contains(element) && 16) + (this[0] != element && element.contains(this[0]) && 8),
docEl = document.documentElement;
if(this[0].sourceIndex){
number += (this[0].sourceIndex < element.sourceIndex && 4)
number += (this[0].sourceIndex > element.sourceIndex && 2)
number += (this[0].ownerDocument !== element.ownerDocument ||
(this[0] != docEl && this[0].sourceIndex <= 0 ) ||
(element != docEl && element.sourceIndex <= 0 )) && 1
}else{
var range = document.createRange(),
sourceRange = document.createRange(),
compare;
range.selectNode(this[0]);
sourceRange.selectNode(element);
compare = range.compareBoundaryPoints(Range.START_TO_START, sourceRange);
}
return number;
}
})(jQuery);
(function($){
var event = $.event;
//somehow need to keep track of elements with selectors on them. When element is removed, somehow we need to know that
//
/**
* @add jQuery.event.special
*/
var eventNames = [
/**
* @attribute dropover
* Called when a drag is first moved over this drop element.
* <p>Drop events are covered in more detail in [jQuery.Drop].</p>
*/
"dropover",
/**
* @attribute dropon
* Called when a drag is dropped on a drop element.
* <p>Drop events are covered in more detail in [jQuery.Drop].</p>
*/
"dropon",
/**
* @attribute dropout
* Called when a drag is moved out of this drop.
* <p>Drop events are covered in more detail in [jQuery.Drop].</p>
*/
"dropout",
/**
* @attribute dropinit
* Called when a drag motion starts and the drop elements are initialized.
* <p>Drop events are covered in more detail in [jQuery.Drop].</p>
*/
"dropinit",
/**
* @attribute dropmove
* Called repeatedly when a drag is moved over a drop.
* <p>Drop events are covered in more detail in [jQuery.Drop].</p>
*/
"dropmove",
/**
* @attribute dropend
* Called when the drag is done for this drop.
* <p>Drop events are covered in more detail in [jQuery.Drop].</p>
*/
"dropend"];
/**
* @class jQuery.Drop
* @parent specialevents
* @plugin jquery/event/drop
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/drop/drop.js
* @test jquery/event/drag/qunit.html
*
* Provides drop events as a special event to jQuery.
* By binding to a drop event, the your callback functions will be
* called during the corresponding phase of drag.
* <h2>Drop Events</h2>
* All drop events are called with the native event, an instance of drop, and the drag. Here are the available drop
* events:
* <ul>
* <li><code>dropinit</code> - the drag motion is started, drop positions are calculated.</li>
* <li><code>dropover</code> - a drag moves over a drop element, called once as the drop is dragged over the element.</li>
* <li><code>dropout</code> - a drag moves out of the drop element.</li>
* <li><code>dropmove</code> - a drag is moved over a drop element, called repeatedly as the element is moved.</li>
* <li><code>dropon</code> - a drag is released over a drop element.</li>
* <li><code>dropend</code> - the drag motion has completed.</li>
* </ul>
* <h2>Examples</h2>
* Here's how to listen for when a drag moves over a drop:
* @codestart
* $('.drop').delegate("dropover", function(ev, drop, drag){
* $(this).addClass("drop-over")
* })
* @codeend
* A bit more complex example:
* @demo jquery/event/drop/drop.html 1000
*
*
*
* ## How it works
*
* 1. When you bind on a drop event, it adds that element to the list of rootElements.
* RootElements might be drop points, or might have delegated drop points in them.
*
* 2. When a drag motion is started, each rootElement is queried for the events listening on it.
* These events might be delegated events so we need to query for the drop elements.
*
* 3. With each drop element, we add a Drop object with all the callbacks for that element.
* Each element might have multiple event provided by different rootElements. We merge
* callbacks into the Drop object if there is an existing Drop object.
*
* 4. Once Drop objects have been added to all elements, we go through them and call draginit
* if available.
*
*
* @constructor
* The constructor is never called directly.
*/
$.Drop = function(callbacks, element){
jQuery.extend(this,callbacks);
this.element = $(element);
}
// add the elements ...
$.each(eventNames, function(){
event.special[this] = {
add: function( handleObj ) {
//add this element to the compiles list
var el = $(this), current = (el.data("dropEventCount") || 0);
el.data("dropEventCount", current+1 )
if(current==0){
$.Drop.addElement(this);
}
},
remove: function() {
var el = $(this), current = (el.data("dropEventCount") || 0);
el.data("dropEventCount", current-1 )
if(current<=1){
$.Drop.removeElement(this);
}
}
}
});
$.extend($.Drop,{
lowerName: "drop",
_rootElements: [], //elements that are listening for drops
_elements: $(), //elements that can be dropped on
last_active: [],
endName: "dropon",
// adds an element as a 'root' element
// this element might have events that we need to respond to
addElement: function( el ) {
//check other elements
for(var i =0; i < this._rootElements.length ; i++ ){
if(el ==this._rootElements[i]) return;
}
this._rootElements.push(el);
},
removeElement: function( el ) {
for(var i =0; i < this._rootElements.length ; i++ ){
if(el == this._rootElements[i]){
this._rootElements.splice(i,1)
return;
}
}
},
/**
* @hide
* For a list of affected drops, sorts them by which is deepest in the DOM first.
*/
sortByDeepestChild: function( a, b ) {
var compare = a.element.compare(b.element);
if(compare & 16 || compare & 4) return 1;
if(compare & 8 || compare & 2) return -1;
return 0;
},
/**
* @hide
* Tests if a drop is within the point.
*/
isAffected: function( point, moveable, responder ) {
return ((responder.element != moveable.element) && (responder.element.within(point[0], point[1], responder._cache).length == 1));
},
/**
* @hide
* Calls dropout and sets last active to null
* @param {Object} drop
* @param {Object} drag
* @param {Object} event
*/
deactivate: function( responder, mover, event ) {
mover.out(event, responder)
responder.callHandlers(this.lowerName+'out',responder.element[0], event, mover)
},
/**
* @hide
* Calls dropover
* @param {Object} drop
* @param {Object} drag
* @param {Object} event
*/
activate: function( responder, mover, event ) { //this is where we should call over
mover.over(event, responder)
//this.last_active = responder;
responder.callHandlers(this.lowerName+'over',responder.element[0], event, mover);
},
move: function( responder, mover, event ) {
responder.callHandlers(this.lowerName+'move',responder.element[0], event, mover)
},
/**
* Gets all elements that are droppable and adds them to a list.
*
* This should be called if and when new drops are added to the page
* during the motion of a single drag.
*
* This is called by default when a drag motion starts.
*
* ## Use
*
* After adding an element or drop, call compile.
*
* $("#midpoint").bind("dropover",function(){
* // when a drop hovers over midpoint,
* // make drop a drop.
* $("#drop").bind("dropover", function(){
*
* });
* $.Drop.compile();
* });
*/
compile: function( event, drag ) {
// if we called compile w/o a current drag
if(!this.dragging && !drag){
return;
}else if(!this.dragging){
this.dragging = drag;
this.last_active = [];
//this._elements = $();
}
var el,
drops,
selector,
dropResponders,
newEls = [],
dragging = this.dragging;
// go to each root element and look for drop elements
for(var i=0; i < this._rootElements.length; i++){ //for each element
el = this._rootElements[i]
// gets something like {"": ["dropinit"], ".foo" : ["dropover","dropmove"] }
var drops = $.event.findBySelector(el, eventNames)
// get drop elements by selector
for(selector in drops){
dropResponders = selector ? jQuery(selector, el) : [el];
// for each drop element
for(var e= 0; e < dropResponders.length; e++){
// add the callbacks to the element's Data
// there already might be data, so we merge it
if( this.addCallbacks(dropResponders[e], drops[selector], dragging) ){
newEls.push(dropResponders[e])
};
}
}
}
// once all callbacks are added, call init on everything ...
// todo ... init could be called more than once?
this.add(newEls, event, dragging)
},
// adds the drag callbacks object to the element or merges other callbacks ...
// returns true or false if the element is new ...
// onlyNew lets only new elements add themselves
addCallbacks : function(el, callbacks, onlyNew){
var origData = $.data(el,"_dropData");
if(!origData){
$.data(el,"_dropData", new $.Drop(callbacks, el));
//this._elements.push(el);
return true;
}else if(!onlyNew){
var origCbs = origData;
// merge data
for(var eventName in callbacks){
origCbs[eventName] = origCbs[eventName] ?
origCbs[eventName].concat(callbacks[eventName]) :
callbacks[eventName];
}
return false;
}
},
// calls init on each element's drags.
// if its cancelled it's removed
// adds to the current elements ...
add: function( newEls, event, drag , dragging) {
var i = 0,
drop;
while(i < newEls.length){
drop = $.data(newEls[i],"_dropData");
drop.callHandlers(this.lowerName+'init', newEls[i], event, drag)
if(drop._canceled){
newEls.splice(i,1)
}else{
i++;
}
}
this._elements.push.apply(this._elements, newEls)
},
show: function( point, moveable, event ) {
var element = moveable.element;
if(!this._elements.length) return;
var respondable,
affected = [],
propagate = true,
i = 0,
j,
la,
toBeActivated,
aff,
oldLastActive = this.last_active,
responders = [],
self = this,
drag;
//what's still affected ... we can also move element out here
while( i < this._elements.length){
drag = $.data(this._elements[i],"_dropData");
if (!drag) {
this._elements.splice(i, 1)
}
else {
i++;
if (self.isAffected(point, moveable, drag)) {
affected.push(drag);
}
}
}
affected.sort(this.sortByDeepestChild); //we should only trigger on lowest children
event.stopRespondPropagate = function(){
propagate = false;
}
toBeActivated = affected.slice();
// all these will be active
this.last_active = affected;
//deactivate everything in last_active that isn't active
for (j = 0; j < oldLastActive.length; j++) {
la = oldLastActive[j];
i = 0;
while((aff = toBeActivated[i])){
if(la == aff){
toBeActivated.splice(i,1);break;
}else{
i++;
}
}
if(!aff){
this.deactivate(la, moveable, event);
}
if(!propagate) return;
}
for(var i =0; i < toBeActivated.length; i++){
this.activate(toBeActivated[i], moveable, event);
if(!propagate) return;
}
//activate everything in affected that isn't in last_active
for (i = 0; i < affected.length; i++) {
this.move(affected[i], moveable, event);
if(!propagate) return;
}
},
end: function( event, moveable ) {
var responder, la,
endName = this.lowerName+'end',
dropData;
// call dropon
//go through the actives ... if you are over one, call dropped on it
for(var i = 0; i < this.last_active.length; i++){
la = this.last_active[i]
if( this.isAffected(event.vector(), moveable, la) && la[this.endName]){
la.callHandlers(this.endName, null, event, moveable);
}
}
// call dropend
for(var r =0; r<this._elements.length; r++){
dropData = $.data(this._elements[r],"_dropData");
dropData && dropData.callHandlers(endName, null, event, moveable);
}
this.clear();
},
/**
* Called after dragging has stopped.
* @hide
*/
clear: function() {
this._elements.each(function(){
$.removeData(this,"_dropData")
})
this._elements = $();
delete this.dragging;
//this._responders = [];
}
})
$.Drag.responder = $.Drop;
$.extend($.Drop.prototype,{
callHandlers: function( method, el, ev, drag ) {
var length = this[method] ? this[method].length : 0
for(var i =0; i < length; i++){
this[method][i].call(el || this.element[0], ev, this, drag)
}
},
/**
* Caches positions of draggable elements. This should be called in dropinit. For example:
* @codestart
* dropinit: function( el, ev, drop ) { drop.cache_position() }
* @codeend
*/
cache: function( value ) {
this._cache = value != null ? value : true;
},
/**
* Prevents this drop from being dropped on.
*/
cancel: function() {
this._canceled = true;
}
} )
})(jQuery);
(function() {
var event = jQuery.event,
//helper that finds handlers by type and calls back a function, this is basically handle
// events - the events object
// types - an array of event types to look for
// callback(type, handlerFunc, selector) - a callback
// selector - an optional selector to filter with, if there, matches by selector
// if null, matches anything, otherwise, matches with no selector
findHelper = function( events, types, callback, selector ) {
var t, type, typeHandlers, all, h, handle,
namespaces, namespace,
match;
for ( t = 0; t < types.length; t++ ) {
type = types[t];
all = type.indexOf(".") < 0;
if (!all ) {
namespaces = type.split(".");
type = namespaces.shift();
namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
}
typeHandlers = (events[type] || []).slice(0);
for ( h = 0; h < typeHandlers.length; h++ ) {
handle = typeHandlers[h];
match = (all || namespace.test(handle.namespace));
if(match){
if(selector){
if (handle.selector === selector ) {
callback(type, handle.origHandler || handle.handler);
}
} else if (selector === null){
callback(type, handle.origHandler || handle.handler, handle.selector);
}
else if (!handle.selector ) {
callback(type, handle.origHandler || handle.handler);
}
}
}
}
};
/**
* Finds event handlers of a given type on an element.
* @param {HTMLElement} el
* @param {Array} types an array of event names
* @param {String} [selector] optional selector
* @return {Array} an array of event handlers
*/
event.find = function( el, types, selector ) {
var events = ( $._data(el) || {} ).events,
handlers = [],
t, liver, live;
if (!events ) {
return handlers;
}
findHelper(events, types, function( type, handler ) {
handlers.push(handler);
}, selector);
return handlers;
};
/**
* Finds all events. Group by selector.
* @param {HTMLElement} el the element
* @param {Array} types event types
*/
event.findBySelector = function( el, types ) {
var events = $._data(el).events,
selectors = {},
//adds a handler for a given selector and event
add = function( selector, event, handler ) {
var select = selectors[selector] || (selectors[selector] = {}),
events = select[event] || (select[event] = []);
events.push(handler);
};
if (!events ) {
return selectors;
}
//first check live:
/*$.each(events.live || [], function( i, live ) {
if ( $.inArray(live.origType, types) !== -1 ) {
add(live.selector, live.origType, live.origHandler || live.handler);
}
});*/
//then check straight binds
findHelper(events, types, function( type, handler, selector ) {
add(selector || "", type, handler);
}, null);
return selectors;
};
event.supportTouch = "ontouchend" in document;
$.fn.respondsTo = function( events ) {
if (!this.length ) {
return false;
} else {
//add default ?
return event.find(this[0], $.isArray(events) ? events : [events]).length > 0;
}
};
$.fn.triggerHandled = function( event, data ) {
event = (typeof event == "string" ? $.Event(event) : event);
this.trigger(event, data);
return event.handled;
};
/**
* Only attaches one event handler for all types ...
* @param {Array} types llist of types that will delegate here
* @param {Object} startingEvent the first event to start listening to
* @param {Object} onFirst a function to call
*/
event.setupHelper = function( types, startingEvent, onFirst ) {
if (!onFirst ) {
onFirst = startingEvent;
startingEvent = null;
}
var add = function( handleObj ) {
var bySelector, selector = handleObj.selector || "";
if ( selector ) {
bySelector = event.find(this, types, selector);
if (!bySelector.length ) {
$(this).delegate(selector, startingEvent, onFirst);
}
}
else {
//var bySelector = event.find(this, types, selector);
if (!event.find(this, types, selector).length ) {
event.add(this, startingEvent, onFirst, {
selector: selector,
delegate: this
});
}
}
},
remove = function( handleObj ) {
var bySelector, selector = handleObj.selector || "";
if ( selector ) {
bySelector = event.find(this, types, selector);
if (!bySelector.length ) {
$(this).undelegate(selector, startingEvent, onFirst);
}
}
else {
if (!event.find(this, types, selector).length ) {
event.remove(this, startingEvent, onFirst, {
selector: selector,
delegate: this
});
}
}
};
$.each(types, function() {
event.special[this] = {
add: add,
remove: remove,
setup: function() {},
teardown: function() {}
};
});
};
})(jQuery);
(function($){
/**
* @class jQuery.Hover
* @plugin jquery/event/hover
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/hover/hover.js
* Provides delegate-able hover events.
* <p>
* A hover happens when the mouse stops moving
* over an element for a period of time. You can listen
* and configure hover with the following events:
* </p>
* <ul>
* <li><code>[jQuery.event.special.hoverinit hoverinit]</code> - called on mouseenter, use this event to customize
* [jQuery.Hover.prototype.delay] and [jQuery.Hover.prototype.distance]</li>
* <li><code>[jQuery.event.special.hoverenter hoverenter]</code> - an element is being hovered</li>
* <li><code>[jQuery.event.special.hovermove hovermove]</code> - the mouse moves on an element that has been hovered</li>
* <li><code>[jQuery.event.special.hoverleave hoverleave]</code> - the mouse leaves the element that has been hovered</li>
* </ul>
* <h3>Quick Example</h3>
* The following listens for hoverenter and adds a class to style
* the element, and removes the class on hoverleave.
* @codestart
* $('#menu').delegate(".option","hoverenter",function(){
* $(this).addClass("hovering");
* }).delegate(".option","hoverleave",function(){
* $(this).removeClass("hovering");
* })
* @codeend
* <h2>Configuring Distance and Delay</h2>
* <p>An element is hovered when the mouse
* moves less than a certain distance in
* specific time over the element.
* </p>
* <p>
* You can configure that distance and time by
* adjusting the <code>distance</code> and
* <code>delay</code> values.
* </p>
* <p>You can set delay and distance globally
* by adjusting the static properties:</p>
* </p>
* @codestart
* $.Hover.delay = 10
* $.Hover.distance = 1
* @codeend
* <p>Or you can adjust delay and distance for
* an individual element in hoverenter:</p>
* @codestart
* $(".option").delegate("hoverinit", function(ev, hover){
* //set the distance to 10px
* hover.distance(10)
* //set the delay to 200ms
* hover.delay(10)
* })
* @codeend
* <h2>Demo</h2>
* @demo jquery/event/hover/hover.html
* @parent specialevents
* @constructor Creates a new hover. This is never
* called directly.
*/
$.Hover = function(){
this._delay = $.Hover.delay;
this._distance = $.Hover.distance;
this._leave = $.Hover.leave
};
/**
* @Static
*/
$.extend($.Hover,{
/**
* @attribute delay
* A hover is activated if it moves less than distance in this time.
* Set this value as a global default.
*/
delay: 100,
/**
* @attribute distance
* A hover is activated if it moves less than this distance in delay time.
* Set this value as a global default.
*/
distance: 10,
leave : 0
})
/**
* @Prototype
*/
$.extend($.Hover.prototype,{
/**
* Sets the delay for this hover. This method should
* only be used in hoverinit.
* @param {Number} delay the number of milliseconds used to determine a hover
*
*/
delay: function( delay ) {
this._delay = delay;
return this;
},
/**
* Sets the distance for this hover. This method should
* only be used in hoverinit.
* @param {Number} distance the max distance in pixels a mouse can move to be considered a hover
*/
distance: function( distance ) {
this._distance = distance;
return this;
},
leave : function(leave){
this._leave = leave;
return this;
}
})
var event = $.event,
handle = event.handle,
onmouseenter = function(ev){
//now start checking mousemoves to update location
var delegate = ev.delegateTarget || ev.currentTarget;
var selector = ev.handleObj.selector;
//prevents another mouseenter until current has run its course
if($.data(delegate,"_hover"+selector)){
return;
}
$.data(delegate,"_hover"+selector, true)
var loc = {
pageX : ev.pageX,
pageY : ev.pageY
},
dist = 0,
timer,
enteredEl = this,
hovered = false,
lastEv = ev,
hover = new $.Hover(),
leaveTimer,
callHoverLeave = function(){
$.each(event.find(delegate, ["hoverleave"], selector), function(){
this.call(enteredEl, ev, hover)
})
cleanUp();
},
mouseenter = function(ev){
clearTimeout(leaveTimer);
dist += Math.pow( ev.pageX-loc.pageX, 2 ) + Math.pow( ev.pageY-loc.pageY, 2 );
loc = {
pageX : ev.pageX,
pageY : ev.pageY
}
lastEv = ev
},
mouseleave = function(ev){
clearTimeout(timer);
// go right away
if(hovered){
if(hover._leave === 0){
callHoverLeave();
}else{
clearTimeout(leaveTimer);
leaveTimer = setTimeout(function(){
callHoverLeave();
}, hover._leave)
}
}else{
cleanUp();
}
},
cleanUp = function(){
$(enteredEl).unbind("mouseleave",mouseleave)
$(enteredEl).unbind("mousemove",mouseenter);
$.removeData(delegate,"_hover"+selector)
};
$(enteredEl).bind("mousemove",mouseenter).bind("mouseleave", mouseleave);
$.each(event.find(delegate, ["hoverinit"], selector), function(){
this.call(enteredEl, ev, hover)
})
timer = setTimeout(function(){
//check that we aren't moveing around
if(dist < hover._distance && $(enteredEl).queue().length == 0){
$.each(event.find(delegate, ["hoverenter"], selector), function(){
this.call(enteredEl, lastEv, hover)
})
hovered = true;
return;
}else{
dist = 0;
timer = setTimeout(arguments.callee, hover._delay)
}
}, hover._delay)
};
/**
* @add jQuery.event.special
*/
event.setupHelper( [
/**
* @attribute hoverinit
* Listen for hoverinit events to configure
* [jQuery.Hover.prototype.delay] and [jQuery.Hover.prototype.distance]
* for the current element. Hoverinit is called on mouseenter.
* @codestart
* $(".option").delegate("hoverinit", function(ev, hover){
* //set the distance to 10px
* hover.distance(10)
* //set the delay to 200ms
* hover.delay(10)
* })
* @codeend
*/
"hoverinit",
/**
* @attribute hoverenter
* Hoverenter events are called when the mouses less
* than [jQuery.Hover.prototype.distance] pixels in
* [jQuery.Hover.prototype.delay] milliseconds.
* @codestart
* $(".option").delegate("hoverenter", function(ev, hover){
* $(this).addClass("hovering");
* })
* @codeend
*/
"hoverenter",
/**
* @attribute hoverleave
* Called when the mouse leaves an element that has been
* hovered.
* @codestart
* $(".option").delegate("hoverleave", function(ev, hover){
* $(this).removeClass("hovering");
* })
* @codeend
*/
"hoverleave",
/**
* @attribute hovermove
* Called when the mouse moves on an element that
* has been hovered.
* @codestart
* $(".option").delegate("hovermove", function(ev, hover){
* //not sure why you would want to listen for this
* //but we provide it just in case
* })
* @codeend
*/
"hovermove"], "mouseenter", onmouseenter )
})(jQuery);
(function( $ ) {
// a path like string into something that's ok for an element ID
var toId = function( src ) {
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
},
makeArray = $.makeArray,
// used for hookup ids
id = 1;
// this might be useful for testing if html
// htmlTest = /^[\s\n\r\xA0]*<(.|[\r\n])*>[\s\n\r\xA0]*$/
/**
* @class jQuery.View
* @parent jquerymx
* @plugin jquery/view
* @test jquery/view/qunit.html
* @download dist/jquery.view.js
*
* @description A JavaScript template framework.
*
* View provides a uniform interface for using templates with
* jQuery. When template engines [jQuery.View.register register]
* themselves, you are able to:
*
* - Use views with jQuery extensions [jQuery.fn.after after], [jQuery.fn.append append],
* [jQuery.fn.before before], [jQuery.fn.html html], [jQuery.fn.prepend prepend],
* [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text].
* - Template loading from html elements and external files.
* - Synchronous and asynchronous template loading.
* - [view.deferreds Deferred Rendering].
* - Template caching.
* - Bundling of processed templates in production builds.
* - Hookup jquery plugins directly in the template.
*
* The [mvc.view Get Started with jQueryMX] has a good walkthrough of $.View.
*
* ## Use
*
*
* When using views, you're almost always wanting to insert the results
* of a rendered template into the page. jQuery.View overwrites the
* jQuery modifiers so using a view is as easy as:
*
* $("#foo").html('mytemplate.ejs',{message: 'hello world'})
*
* This code:
*
* - Loads the template a 'mytemplate.ejs'. It might look like:
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></pre></code>
*
* - Renders it with {message: 'hello world'}, resulting in:
* <pre><code>&lt;div id='foo'>"&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* - Inserts the result into the foo element. Foo might look like:
* <pre><code>&lt;div id='foo'>&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* ## jQuery Modifiers
*
* You can use a template with the following jQuery modifiers:
*
* <table>
* <tr><td>[jQuery.fn.after after]</td><td> <code>$('#bar').after('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.append append] </td><td> <code>$('#bar').append('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.before before] </td><td> <code>$('#bar').before('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.html html] </td><td> <code>$('#bar').html('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.prepend prepend] </td><td> <code>$('#bar').prepend('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.replaceWith replaceWith] </td><td> <code>$('#bar').replaceWith('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.text text] </td><td> <code>$('#bar').text('temp.jaml',{});</code></td></tr>
* </table>
*
* You always have to pass a string and an object (or function) for the jQuery modifier
* to user a template.
*
* ## Template Locations
*
* View can load from script tags or from files.
*
* ## From Script Tags
*
* To load from a script tag, create a script tag with your template and an id like:
*
* <pre><code>&lt;script type='text/ejs' id='recipes'>
* &lt;% for(var i=0; i &lt; recipes.length; i++){ %>
* &lt;li>&lt;%=recipes[i].name %>&lt;/li>
* &lt;%} %>
* &lt;/script></code></pre>
*
* Render with this template like:
*
* @codestart
* $("#foo").html('recipes',recipeData)
* @codeend
*
* Notice we passed the id of the element we want to render.
*
* ## From File
*
* You can pass the path of a template file location like:
*
* $("#foo").html('templates/recipes.ejs',recipeData)
*
* However, you typically want to make the template work from whatever page they
* are called from. To do this, use // to look up templates from JMVC root:
*
* $("#foo").html('//app/views/recipes.ejs',recipeData)
*
* Finally, the [jQuery.Controller.prototype.view controller/view] plugin can make looking
* up a thread (and adding helpers) even easier:
*
* $("#foo").html( this.view('recipes', recipeData) )
*
* ## Packaging Templates
*
* If you're making heavy use of templates, you want to organize
* them in files so they can be reused between pages and applications.
*
* But, this organization would come at a high price
* if the browser has to
* retrieve each template individually. The additional
* HTTP requests would slow down your app.
*
* Fortunately, [steal.static.views steal.views] can build templates
* into your production files. You just have to point to the view file like:
*
* steal.views('path/to/the/view.ejs');
*
* ## Asynchronous
*
* By default, retrieving requests is done synchronously. This is
* fine because StealJS packages view templates with your JS download.
*
* However, some people might not be using StealJS or want to delay loading
* templates until necessary. If you have the need, you can
* provide a callback paramter like:
*
* $("#foo").html('recipes',recipeData, function(result){
* this.fadeIn()
* });
*
* The callback function will be called with the result of the
* rendered template and 'this' will be set to the original jQuery object.
*
* ## Deferreds (3.0.6)
*
* If you pass deferreds to $.View or any of the jQuery
* modifiers, the view will wait until all deferreds resolve before
* rendering the view. This makes it a one-liner to make a request and
* use the result to render a template.
*
* The following makes a request for todos in parallel with the
* todos.ejs template. Once todos and template have been loaded, it with
* render the view with the todos.
*
* $('#todos').html("todos.ejs",Todo.findAll());
*
* ## Just Render Templates
*
* Sometimes, you just want to get the result of a rendered
* template without inserting it, you can do this with $.View:
*
* var out = $.View('path/to/template.jaml',{});
*
* ## Preloading Templates
*
* You can preload templates asynchronously like:
*
* $.get('path/to/template.jaml',{},function(){},'view');
*
* ## Supported Template Engines
*
* JavaScriptMVC comes with the following template languages:
*
* - EmbeddedJS
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></code></pre>
*
* - JAML
* <pre><code>h2(data.message);</code></pre>
*
* - Micro
* <pre><code>&lt;h2>{%= message %}&lt;/h2></code></pre>
*
* - jQuery.Tmpl
* <pre><code>&lt;h2>${message}&lt;/h2></code></pre>
*
* The popular <a href='http://awardwinningfjords.com/2010/08/09/mustache-for-javascriptmvc-3.html'>Mustache</a>
* template engine is supported in a 2nd party plugin.
*
* ## Using other Template Engines
*
* It's easy to integrate your favorite template into $.View and Steal. Read
* how in [jQuery.View.register].
*
* @constructor
*
* Looks up a template, processes it, caches it, then renders the template
* with data and optional helpers.
*
* With [stealjs StealJS], views are typically bundled in the production build.
* This makes it ok to use views synchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",{message: "Hello World"})
* @codeend
*
* If you aren't using StealJS, it's best to use views asynchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",
* {message: "Hello World"}, function(result){
* // do something with result
* })
* @codeend
*
* @param {String} view The url or id of an element to use as the template's source.
* @param {Object} data The data to be passed to the view.
* @param {Object} [helpers] Optional helper functions the view might use. Not all
* templates support helpers.
* @param {Object} [callback] Optional callback function. If present, the template is
* retrieved asynchronously. This is a good idea if you aren't compressing the templates
* into your view.
* @return {String} The rendered result of the view or if deferreds
* are passed, a deferred that will resolve to
* the rendered result of the view.
*/
var $view = $.View = function( view, data, helpers, callback ) {
// if helpers is a function, it is actually a callback
if ( typeof helpers === 'function' ) {
callback = helpers;
helpers = undefined;
}
// see if we got passed any deferreds
var deferreds = getDeferreds(data);
if ( deferreds.length ) { // does data contain any deferreds?
// the deferred that resolves into the rendered content ...
var deferred = $.Deferred();
// add the view request to the list of deferreds
deferreds.push(get(view, true))
// wait for the view and all deferreds to finish
$.when.apply($, deferreds).then(function( resolved ) {
// get all the resolved deferreds
var objs = makeArray(arguments),
// renderer is last [0] is the data
renderer = objs.pop()[0],
// the result of the template rendering with data
result;
// make data look like the resolved deferreds
if ( isDeferred(data) ) {
data = usefulPart(resolved);
}
else {
// go through each prop in data again,
// replace the defferreds with what they resolved to
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
data[prop] = usefulPart(objs.shift());
}
}
}
// get the rendered result
result = renderer(data, helpers);
//resolve with the rendered view
deferred.resolve(result);
// if there's a callback, call it back with the result
callback && callback(result);
});
// return the deferred ....
return deferred.promise();
}
else {
// no deferreds, render this bad boy
var response,
// if there's a callback function
async = typeof callback === "function",
// get the 'view' type
deferred = get(view, async);
// if we are async,
if ( async ) {
// return the deferred
response = deferred;
// and callback callback with the rendered result
deferred.done(function( renderer ) {
callback(renderer(data, helpers))
})
} else {
// otherwise, the deferred is complete, so
// set response to the result of the rendering
deferred.done(function( renderer ) {
response = renderer(data, helpers);
});
}
return response;
}
},
// makes sure there's a template, if not, has steal provide a warning
checkText = function( text, url ) {
if (!text.match(/[^\s]/) ) {
throw "$.View ERROR: There is no template or an empty template at " + url;
}
},
// returns a 'view' renderer deferred
// url - the url to the view template
// async - if the ajax request should be synchronous
get = function( url, async ) {
return $.ajax({
url: url,
dataType: "view",
async: async
});
},
// returns true if something looks like a deferred
isDeferred = function( obj ) {
return obj && $.isFunction(obj.always) // check if obj is a $.Deferred
},
// gets an array of deferreds from an object
// this only goes one level deep
getDeferreds = function( data ) {
var deferreds = [];
// pull out deferreds
if ( isDeferred(data) ) {
return [data]
} else {
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
deferreds.push(data[prop]);
}
}
}
return deferreds;
},
// gets the useful part of deferred
// this is for Models and $.ajax that resolve to array (with success and such)
// returns the useful, content part
usefulPart = function( resolved ) {
return $.isArray(resolved) && resolved.length === 3 && resolved[1] === 'success' ? resolved[0] : resolved
};
// you can request a view renderer (a function you pass data to and get html)
// Creates a 'view' transport. These resolve to a 'view' renderer
// a 'view' renderer takes data and returns a string result.
// For example:
//
// $.ajax({dataType : 'view', src: 'foo.ejs'}).then(function(renderer){
// renderer({message: 'hello world'})
// })
$.ajaxTransport("view", function( options, orig ) {
// the url (or possibly id) of the view content
var url = orig.url,
// check if a suffix exists (ex: "foo.ejs")
suffix = url.match(/\.[\w\d]+$/),
type,
// if we are reading a script element for the content of the template
// el will be set to that script element
el,
// a unique identifier for the view (used for caching)
// this is typically derived from the element id or
// the url for the template
id,
// the AJAX request used to retrieve the template content
jqXHR,
// used to generate the response
response = function( text ) {
// get the renderer function
var func = type.renderer(id, text);
// cache if if we are caching
if ( $view.cache ) {
$view.cached[id] = func;
}
// return the objects for the response's dataTypes
// (in this case view)
return {
view: func
};
};
// if we have an inline template, derive the suffix from the 'text/???' part
// this only supports '<script></script>' tags
if ( el = document.getElementById(url) ) {
suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2];
}
// if there is no suffix, add one
if (!suffix ) {
suffix = $view.ext;
url = url + $view.ext;
}
// convert to a unique and valid id
id = toId(url);
// if a absolute path, use steal to get it
// you should only be using // if you are using steal
if ( url.match(/^\/\//) ) {
var sub = url.substr(2);
url = typeof steal === "undefined" ?
url = "/" + sub :
steal.root.mapJoin(sub) +'';
}
//set the template engine type
type = $view.types[suffix];
// return the ajax transport contract: http://api.jquery.com/extending-ajax/
return {
send: function( headers, callback ) {
// if it is cached,
if ( $view.cached[id] ) {
// return the catched renderer
return callback(200, "success", {
view: $view.cached[id]
});
// otherwise if we are getting this from a script elment
} else if ( el ) {
// resolve immediately with the element's innerHTML
callback(200, "success", response(el.innerHTML));
} else {
// make an ajax request for text
jqXHR = $.ajax({
async: orig.async,
url: url,
dataType: "text",
error: function() {
checkText("", url);
callback(404);
},
success: function( text ) {
// make sure we got some text back
checkText(text, url);
// cache and send back text
callback(200, "success", response(text))
}
});
}
},
abort: function() {
jqXHR && jqXHR.abort();
}
}
})
$.extend($view, {
/**
* @attribute hookups
* @hide
* A list of pending 'hookups'
*/
hookups: {},
/**
* @function hookup
* Registers a hookup function that can be called back after the html is
* put on the page. Typically this is handled by the template engine. Currently
* only EJS supports this functionality.
*
* var id = $.View.hookup(function(el){
* //do something with el
* }),
* html = "<div data-view-id='"+id+"'>"
* $('.foo').html(html);
*
*
* @param {Function} cb a callback function to be called with the element
* @param {Number} the hookup number
*/
hookup: function( cb ) {
var myid = ++id;
$view.hookups[myid] = cb;
return myid;
},
/**
* @attribute cached
* @hide
* Cached are put in this object
*/
cached: {},
/**
* @attribute cache
* Should the views be cached or reloaded from the server. Defaults to true.
*/
cache: true,
/**
* @function register
* Registers a template engine to be used with
* view helpers and compression.
*
* ## Example
*
* @codestart
* $.View.register({
* suffix : "tmpl",
* plugin : "jquery/view/tmpl",
* renderer: function( id, text ) {
* return function(data){
* return jQuery.render( text, data );
* }
* },
* script: function( id, text ) {
* var tmpl = $.tmpl(text).toString();
* return "function(data){return ("+
* tmpl+
* ").call(jQuery, jQuery, data); }";
* }
* })
* @codeend
* Here's what each property does:
*
* * plugin - the location of the plugin
* * suffix - files that use this suffix will be processed by this template engine
* * renderer - returns a function that will render the template provided by text
* * script - returns a string form of the processed template function.
*
* @param {Object} info a object of method and properties
*
* that enable template integration:
* <ul>
* <li>plugin - the location of the plugin. EX: 'jquery/view/ejs'</li>
* <li>suffix - the view extension. EX: 'ejs'</li>
* <li>script(id, src) - a function that returns a string that when evaluated returns a function that can be
* used as the render (i.e. have func.call(data, data, helpers) called on it).</li>
* <li>renderer(id, text) - a function that takes the id of the template and the text of the template and
* returns a render function.</li>
* </ul>
*/
register: function( info ) {
this.types["." + info.suffix] = info;
if ( window.steal ) {
steal.type(info.suffix + " view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = type.script(id, options.text)
success();
})
}
},
types: {},
/**
* @attribute ext
* The default suffix to use if none is provided in the view's url.
* This is set to .ejs by default.
*/
ext: ".ejs",
/**
* Returns the text that
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function( type, id, src ) {
return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");";
},
/**
* @hide
* Called by a production script to pre-load a renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function( id, renderer ) {
$view.cached[id] = function( data, helpers ) {
return renderer.call(data, data, helpers);
};
}
});
if ( window.steal ) {
steal.type("view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})";
success();
})
}
//---- ADD jQUERY HELPERS -----
//converts jquery functions to use views
var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs,
// text and val cannot produce an element, so don't run hookups on them
noHookup = {'val':true,'text':true};
convert = function( func_name ) {
// save the old jQuery helper
var old = $.fn[func_name];
// replace it wiht our new helper
$.fn[func_name] = function() {
var args = makeArray(arguments),
callbackNum,
callback,
self = this,
result;
// if the first arg is a deferred
// wait until it finishes, and call
// modify with the result
if ( isDeferred(args[0]) ) {
args[0].done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
//check if a template
else if ( isTemplate(args) ) {
// if we should operate async
if ((callbackNum = getCallback(args))) {
callback = args[callbackNum];
args[callbackNum] = function( result ) {
modify.call(self, [result], old);
callback.call(self, result);
};
$view.apply($view, args);
return this;
}
// call view with args (there might be deferreds)
result = $view.apply($view, args);
// if we got a string back
if (!isDeferred(result) ) {
// we are going to call the old method with that string
args = [result];
} else {
// if there is a deferred, wait until it is done before calling modify
result.done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
}
return noHookup[func_name] ? old.apply(this,args) :
modify.call(this, args, old);
};
};
// modifies the content of the element
// but also will run any hookup
modify = function( args, old ) {
var res, stub, hooks;
//check if there are new hookups
for ( var hasHookups in $view.hookups ) {
break;
}
//if there are hookups, get jQuery object
if ( hasHookups && args[0] && isHTML(args[0]) ) {
hooks = $view.hookups;
$view.hookups = {};
args[0] = $(args[0]);
}
res = old.apply(this, args);
//now hookup the hookups
if ( hooks
/* && args.length*/
) {
hookupView(args[0], hooks);
}
return res;
};
// returns true or false if the args indicate a template is being used
// $('#foo').html('/path/to/template.ejs',{data})
// in general, we want to make sure the first arg is a string
// and the second arg is data
isTemplate = function( args ) {
// save the second arg type
var secArgType = typeof args[1];
// the first arg is a string
return typeof args[0] == "string" &&
// the second arg is an object or function
(secArgType == 'object' || secArgType == 'function') &&
// but it is not a dom element
!isDOM(args[1]);
};
// returns true if the arg is a jQuery object or HTMLElement
isDOM = function(arg){
return arg.nodeType || arg.jquery
};
// returns whether the argument is some sort of HTML data
isHTML = function( arg ) {
if ( isDOM(arg) ) {
// if jQuery object or DOM node we're good
return true;
} else if ( typeof arg === "string" ) {
// if string, do a quick sanity check that we're HTML
arg = $.trim(arg);
return arg.substr(0, 1) === "<" && arg.substr(arg.length - 1, 1) === ">" && arg.length >= 3;
} else {
// don't know what you are
return false;
}
};
//returns the callback arg number if there is one (for async view use)
getCallback = function( args ) {
return typeof args[3] === 'function' ? 3 : typeof args[2] === 'function' && 2;
};
hookupView = function( els, hooks ) {
//remove all hookups
var hookupEls, len, i = 0,
id, func;
els = els.filter(function() {
return this.nodeType != 3; //filter out text nodes
})
hookupEls = els.add("[data-view-id]", els);
len = hookupEls.length;
for (; i < len; i++ ) {
if ( hookupEls[i].getAttribute && (id = hookupEls[i].getAttribute('data-view-id')) && (func = hooks[id]) ) {
func(hookupEls[i], id);
delete hooks[id];
hookupEls[i].removeAttribute('data-view-id');
}
}
//copy remaining hooks back
$.extend($view.hookups, hooks);
};
/**
* @add jQuery.fn
* @parent jQuery.View
* Called on a jQuery collection that was rendered with $.View with pending hookups. $.View can render a
* template with hookups, but not actually perform the hookup, because it returns a string without actual DOM
* elements to hook up to. So hookup performs the hookup and clears the pending hookups, preventing errors in
* future templates.
*
* @codestart
* $($.View('//views/recipes.ejs',recipeData)).hookup()
* @codeend
*/
$.fn.hookup = function() {
var hooks = $view.hookups;
$view.hookups = {};
hookupView(this, hooks);
return this;
};
/**
* @add jQuery.fn
*/
$.each([
/**
* @function prepend
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/prepend/ jQuery().prepend()]
* to render [jQuery.View] templates inserted at the beginning of each element in the set of matched elements.
*
* $('#test').prepend('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"prepend",
/**
* @function append
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/append/ jQuery().append()]
* to render [jQuery.View] templates inserted at the end of each element in the set of matched elements.
*
* $('#test').append('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"append",
/**
* @function after
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/after/ jQuery().after()]
* to render [jQuery.View] templates inserted after each element in the set of matched elements.
*
* $('#test').after('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"after",
/**
* @function before
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/before/ jQuery().before()]
* to render [jQuery.View] templates inserted before each element in the set of matched elements.
*
* $('#test').before('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"before",
/**
* @function text
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/text/ jQuery().text()]
* to render [jQuery.View] templates as the content of each matched element.
* Unlike [jQuery.fn.html] jQuery.fn.text also works with XML, escaping the provided
* string as necessary.
*
* $('#test').text('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"text",
/**
* @function html
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/html/ jQuery().html()]
* to render [jQuery.View] templates as the content of each matched element.
*
* $('#test').html('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"html",
/**
* @function replaceWith
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/replaceWith/ jQuery().replaceWith()]
* to render [jQuery.View] templates replacing each element in the set of matched elements.
*
* $('#test').replaceWith('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"replaceWith", "val"],function(i, func){
convert(func);
});
//go through helper funcs and convert
})(jQuery);
(function( $ ) {
/**
* @add jQuery.String
*/
$.String.
/**
* Splits a string with a regex correctly cross browser
*
* $.String.rsplit("a.b.c.d", /\./) //-> ['a','b','c','d']
*
* @param {String} string The string to split
* @param {RegExp} regex A regular expression
* @return {Array} An array of strings
*/
rsplit = function( string, regex ) {
var result = regex.exec(string),
retArr = [],
first_idx, last_idx;
while ( result !== null ) {
first_idx = result.index;
last_idx = regex.lastIndex;
if ( first_idx !== 0 ) {
retArr.push(string.substring(0, first_idx));
string = string.slice(first_idx);
}
retArr.push(result[0]);
string = string.slice(result[0].length);
result = regex.exec(string);
}
if ( string !== '' ) {
retArr.push(string);
}
return retArr;
};
})(jQuery);
(function( $ ) {
// HELPER METHODS ==============
var myEval = function( script ) {
eval(script);
},
// removes the last character from a string
// this is no longer needed
// chop = function( string ) {
// return string.substr(0, string.length - 1);
//},
rSplit = $.String.rsplit,
extend = $.extend,
isArray = $.isArray,
// regular expressions for caching
returnReg = /\r\n/g,
retReg = /\r/g,
newReg = /\n/g,
nReg = /\n/,
slashReg = /\\/g,
quoteReg = /"/g,
singleQuoteReg = /'/g,
tabReg = /\t/g,
leftBracket = /\{/g,
rightBracket = /\}/g,
quickFunc = /\s*\(([\$\w]+)\)\s*->([^\n]*)/,
// escapes characters starting with \
clean = function( content ) {
return content.replace(slashReg, '\\\\').replace(newReg, '\\n').replace(quoteReg, '\\"').replace(tabReg, '\\t');
},
// escapes html
// - from prototype http://www.prototypejs.org/
escapeHTML = function( content ) {
return content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(quoteReg, '&#34;').replace(singleQuoteReg, "&#39;");
},
$View = $.View,
bracketNum = function(content){
var lefts = content.match(leftBracket),
rights = content.match(rightBracket);
return (lefts ? lefts.length : 0) -
(rights ? rights.length : 0);
},
/**
* @class jQuery.EJS
*
* @plugin jquery/view/ejs
* @parent jQuery.View
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/view/ejs/ejs.js
* @test jquery/view/ejs/qunit.html
*
*
* Ejs provides <a href="http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/">ERB</a>
* style client side templates. Use them with controllers to easily build html and inject
* it into the DOM.
*
* ### Example
*
* The following generates a list of tasks:
*
* @codestart html
* &lt;ul>
* &lt;% for(var i = 0; i < tasks.length; i++){ %>
* &lt;li class="task &lt;%= tasks[i].identity %>">&lt;%= tasks[i].name %>&lt;/li>
* &lt;% } %>
* &lt;/ul>
* @codeend
*
* For the following examples, we assume this view is in <i>'views\tasks\list.ejs'</i>.
*
*
* ## Use
*
* ### Loading and Rendering EJS:
*
* You should use EJS through the helper functions [jQuery.View] provides such as:
*
* - [jQuery.fn.after after]
* - [jQuery.fn.append append]
* - [jQuery.fn.before before]
* - [jQuery.fn.html html],
* - [jQuery.fn.prepend prepend],
* - [jQuery.fn.replaceWith replaceWith], and
* - [jQuery.fn.text text].
*
* or [jQuery.Controller.prototype.view].
*
* ### Syntax
*
* EJS uses 5 types of tags:
*
* - <code>&lt;% CODE %&gt;</code> - Runs JS Code.
* For example:
*
* <% alert('hello world') %>
*
* - <code>&lt;%= CODE %&gt;</code> - Runs JS Code and writes the _escaped_ result into the result of the template.
* For example:
*
* <h1><%= 'hello world' %></h1>
*
* - <code>&lt;%== CODE %&gt;</code> - Runs JS Code and writes the _unescaped_ result into the result of the template.
* For example:
*
* <h1><%== '<span>hello world</span>' %></h1>
*
* - <code>&lt;%%= CODE %&gt;</code> - Writes <%= CODE %> to the result of the template. This is very useful for generators.
*
* <%%= 'hello world' %>
*
* - <code>&lt;%# CODE %&gt;</code> - Used for comments. This does nothing.
*
* <%# 'hello world' %>
*
* ## Hooking up controllers
*
* After drawing some html, you often want to add other widgets and plugins inside that html.
* View makes this easy. You just have to return the Contoller class you want to be hooked up.
*
* @codestart
* &lt;ul &lt;%= Mxui.Tabs%>>...&lt;ul>
* @codeend
*
* You can even hook up multiple controllers:
*
* @codestart
* &lt;ul &lt;%= [Mxui.Tabs, Mxui.Filler]%>>...&lt;ul>
* @codeend
*
* To hook up a controller with options or any other jQuery plugin use the
* [jQuery.EJS.Helpers.prototype.plugin | plugin view helper]:
*
* @codestart
* &lt;ul &lt;%= plugin('mxui_tabs', { option: 'value' }) %>>...&lt;ul>
* @codeend
*
* Don't add a semicolon when using view helpers.
*
*
* <h2>View Helpers</h2>
* View Helpers return html code. View by default only comes with
* [jQuery.EJS.Helpers.prototype.view view] and [jQuery.EJS.Helpers.prototype.text text].
* You can include more with the view/helpers plugin. But, you can easily make your own!
* Learn how in the [jQuery.EJS.Helpers Helpers] page.
*
* @constructor Creates a new view
* @param {Object} options A hash with the following options
* <table class="options">
* <tbody><tr><th>Option</th><th>Default</th><th>Description</th></tr>
* <tr>
* <td>text</td>
* <td>&nbsp;</td>
* <td>uses the provided text as the template. Example:<br/><code>new View({text: '&lt;%=user%>'})</code>
* </td>
* </tr>
* <tr>
* <td>type</td>
* <td>'<'</td>
* <td>type of magic tags. Options are '&lt;' or '['
* </td>
* </tr>
* <tr>
* <td>name</td>
* <td>the element ID or url </td>
* <td>an optional name that is used for caching.
* </td>
* </tr>
* </tbody></table>
*/
EJS = function( options ) {
// If called without new, return a function that
// renders with data and helpers like
// EJS({text: '<%= message %>'})({message: 'foo'});
// this is useful for steal's build system
if ( this.constructor != EJS ) {
var ejs = new EJS(options);
return function( data, helpers ) {
return ejs.render(data, helpers);
};
}
// if we get a function directly, it probably is coming from
// a steal-packaged view
if ( typeof options == "function" ) {
this.template = {
fn: options
};
return;
}
//set options on self
extend(this, EJS.options, options);
this.template = compile(this.text, this.type, this.name);
};
// add EJS to jQuery if it exists
window.jQuery && (jQuery.EJS = EJS);
/**
* @Prototype
*/
EJS.prototype.
/**
* Renders an object with view helpers attached to the view.
*
* new EJS({text: "<%= message %>"}).render({
* message: "foo"
* },{helper: function(){ ... }})
*
* @param {Object} object data to be rendered
* @param {Object} [extraHelpers] an object with view helpers
* @return {String} returns the result of the string
*/
render = function( object, extraHelpers ) {
object = object || {};
this._extra_helpers = extraHelpers;
var v = new EJS.Helpers(object, extraHelpers || {});
return this.template.fn.call(object, object, v);
};
/**
* @Static
*/
extend(EJS, {
/**
* Used to convert what's in &lt;%= %> magic tags to a string
* to be inserted in the rendered output.
*
* Typically, it's a string, and the string is just inserted. However,
* if it's a function or an object with a hookup method, it can potentially be
* be ran on the element after it's inserted into the page.
*
* This is a very nice way of adding functionality through the view.
* Usually this is done with [jQuery.EJS.Helpers.prototype.plugin]
* but the following fades in the div element after it has been inserted:
*
* @codestart
* &lt;%= function(el){$(el).fadeIn()} %>
* @codeend
*
* @param {String|Object|Function} input the value in between the
* write magic tags: &lt;%= %>
* @return {String} returns the content to be added to the rendered
* output. The content is different depending on the type:
*
* * string - the original string
* * null or undefined - the empty string ""
* * an object with a hookup method - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View
* * a function - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View
* * an array - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View
*/
text: function( input ) {
// if it's a string, return
if ( typeof input == 'string' ) {
return input;
}
// if has no value
if ( input === null || input === undefined ) {
return '';
}
// if it's an object, and it has a hookup method
var hook = (input.hookup &&
// make a function call the hookup method
function( el, id ) {
input.hookup.call(input, el, id);
}) ||
// or if it's a function, just use the input
(typeof input == 'function' && input) ||
// of it its an array, make a function that calls hookup or the function
// on each item in the array
(isArray(input) &&
function( el, id ) {
for ( var i = 0; i < input.length; i++ ) {
input[i].hookup ? input[i].hookup(el, id) : input[i](el, id);
}
});
// finally, if there is a funciton to hookup on some dom
// pass it to hookup to get the data-view-id back
if ( hook ) {
return "data-view-id='" + $View.hookup(hook) + "'";
}
// finally, if all else false, toString it
return input.toString ? input.toString() : "";
},
/**
* Escapes the text provided as html if it's a string.
* Otherwise, the value is passed to EJS.text(text).
*
* @param {String|Object|Array|Function} text to escape. Otherwise,
* the result of [jQuery.EJS.text] is returned.
* @return {String} the escaped text or likely a $.View data-view-id attribute.
*/
clean: function( text ) {
//return sanatized text
if ( typeof text == 'string' ) {
return escapeHTML(text);
} else if ( typeof text == 'number' ) {
return text;
} else {
return EJS.text(text);
}
},
/**
* @attribute options
* Sets default options for all views.
*
* $.EJS.options.type = '['
*
* Only one option is currently supported: type.
*
* Type is the left hand magic tag.
*/
options: {
type: '<',
ext: '.ejs'
}
});
// ========= SCANNING CODE =========
// Given a scanner, and source content, calls block with each token
// scanner - an object of magicTagName : values
// source - the source you want to scan
// block - function(token, scanner), called with each token
var scan = function( scanner, source, block ) {
// split on /\n/ to have new lines on their own line.
var source_split = rSplit(source, nReg),
i = 0;
for (; i < source_split.length; i++ ) {
scanline(scanner, source_split[i], block);
}
},
scanline = function( scanner, line, block ) {
scanner.lines++;
var line_split = rSplit(line, scanner.splitter),
token;
for ( var i = 0; i < line_split.length; i++ ) {
token = line_split[i];
if ( token !== null ) {
block(token, scanner);
}
}
},
// creates a 'scanner' object. This creates
// values for the left and right magic tags
// it's splitter property is a regexp that splits content
// by all tags
makeScanner = function( left, right ) {
var scanner = {};
extend(scanner, {
left: left + '%',
right: '%' + right,
dLeft: left + '%%',
dRight: '%%' + right,
eeLeft: left + '%==',
eLeft: left + '%=',
cmnt: left + '%#',
scan: scan,
lines: 0
});
scanner.splitter = new RegExp("(" + [scanner.dLeft, scanner.dRight, scanner.eeLeft, scanner.eLeft, scanner.cmnt, scanner.left, scanner.right + '\n', scanner.right, '\n'].join(")|(").
replace(/\[/g, "\\[").replace(/\]/g, "\\]") + ")");
return scanner;
},
// compiles a template where
// source - template text
// left - the left magic tag
// name - the name of the template (for debugging)
// returns an object like: {out : "", fn : function(){ ... }} where
// out - the converted JS source of the view
// fn - a function made from the JS source
compile = function( source, left, name ) {
// make everything only use \n
source = source.replace(returnReg, "\n").replace(retReg, "\n");
// if no left is given, assume <
left = left || '<';
// put and insert cmds are used for adding content to the template
// currently they are identical, I am not sure why
var put_cmd = "___v1ew.push(",
insert_cmd = put_cmd,
// the text that starts the view code (or block function)
startTxt = 'var ___v1ew = [];',
// the text that ends the view code (or block function)
finishTxt = "return ___v1ew.join('')",
// initialize a buffer
buff = new EJS.Buffer([startTxt], []),
// content is used as the current 'processing' string
// this is the content between magic tags
content = '',
// adds something to be inserted into the view template
// this comes out looking like __v1ew.push("CONENT")
put = function( content ) {
buff.push(put_cmd, '"', clean(content), '");');
},
// the starting magic tag
startTag = null,
// cleans the running content
empty = function() {
content = ''
},
// what comes after clean or text
doubleParen = "));",
// a stack used to keep track of how we should end a bracket }
// once we have a <%= %> with a leftBracket
// we store how the file should end here (either '))' or ';' )
endStack =[];
// start going token to token
scan(makeScanner(left, left === '[' ? ']' : '>'), source || "", function( token, scanner ) {
// if we don't have a start pair
var bn;
if ( startTag === null ) {
switch ( token ) {
case '\n':
content = content + "\n";
put(content);
buff.cr();
empty();
break;
// set start tag, add previous content (if there is some)
// clean content
case scanner.left:
case scanner.eLeft:
case scanner.eeLeft:
case scanner.cmnt:
// a new line, just add whatever content w/i a clean
// reset everything
startTag = token;
if ( content.length > 0 ) {
put(content);
}
empty();
break;
case scanner.dLeft:
// replace <%% with <%
content += scanner.left;
break;
default:
content += token;
break;
}
}
else {
//we have a start tag
switch ( token ) {
case scanner.right:
// %>
switch ( startTag ) {
case scanner.left:
// <%
// get the number of { minus }
bn = bracketNum(content);
// how are we ending this statement
var last =
// if the stack has value and we are ending a block
endStack.length && bn == -1 ?
// use the last item in the block stack
endStack.pop() :
// or use the default ending
";";
// if we are ending a returning block
// add the finish text which returns the result of the
// block
if(last === doubleParen) {
buff.push(finishTxt)
}
// add the remaining content
buff.push(content, last);
// if we have a block, start counting
if(bn === 1 ){
endStack.push(";")
}
break;
case scanner.eLeft:
// <%= clean content
bn = bracketNum(content);
if( bn ) {
endStack.push(doubleParen)
}
if(quickFunc.test(content)){
var parts = content.match(quickFunc)
content = "function(__){var "+parts[1]+"=$(__);"+parts[2]+"}"
}
buff.push(insert_cmd, "jQuery.EJS.clean(", content,bn ? startTxt : doubleParen);
break;
case scanner.eeLeft:
// <%== content
// get the number of { minus }
bn = bracketNum(content);
// if we have more {, it means there is a block
if( bn ){
// when we return to the same # of { vs } end wiht a doubleParen
endStack.push(doubleParen)
}
buff.push(insert_cmd, "jQuery.EJS.text(", content,
// if we have a block
bn ?
// start w/ startTxt "var _v1ew = [])"
startTxt :
// if not, add doubleParent to close push and text
doubleParen
);
break;
}
startTag = null;
empty();
break;
case scanner.dRight:
content += scanner.right;
break;
default:
content += token;
break;
}
}
})
if ( content.length > 0 ) {
// Should be content.dump in Ruby
buff.push(put_cmd, '"', clean(content) + '");');
}
var template = buff.close(),
out = {
out: 'try { with(_VIEW) { with (_CONTEXT) {' + template + " "+finishTxt+"}}}catch(e){e.lineNumber=null;throw e;}"
};
//use eval instead of creating a function, b/c it is easier to debug
myEval.call(out, 'this.fn = (function(_CONTEXT,_VIEW){' + out.out + '});\r\n//@ sourceURL=' + name + ".js");
return out;
};
// A Buffer used to add content to.
// This is useful for performance and simplifying the
// code above.
// We also can use this so we know line numbers when there
// is an error.
// pre_cmd - code that sets up the buffer
// post - code that finalizes the buffer
EJS.Buffer = function( pre_cmd, post ) {
// the current line we are on
this.line = [];
// the combined content added to this buffer
this.script = [];
// content at the end of the buffer
this.post = post;
// add the pre commands to the first line
this.push.apply(this, pre_cmd);
};
EJS.Buffer.prototype = {
// add content to this line
// need to maintain your own semi-colons (for performance)
push: function() {
this.line.push.apply(this.line, arguments);
},
// starts a new line
cr: function() {
this.script.push(this.line.join(''), "\n");
this.line = [];
},
//returns the script too
close: function() {
// if we have ending line content, add it to the script
if ( this.line.length > 0 ) {
this.script.push(this.line.join(''));
this.line = [];
}
// if we have ending content, add it
this.post.length && this.push.apply(this, this.post);
// always end in a ;
this.script.push(";");
return this.script.join("");
}
};
/**
* @class jQuery.EJS.Helpers
* @parent jQuery.EJS
* By adding functions to jQuery.EJS.Helpers.prototype, those functions will be available in the
* views.
*
* The following helper converts a given string to upper case:
*
* $.EJS.Helpers.prototype.toUpper = function(params)
* {
* return params.toUpperCase();
* }
*
* Use it like this in any EJS template:
*
* <%= toUpper('javascriptmvc') %>
*
* To access the current DOM element return a function that takes the element as a parameter:
*
* $.EJS.Helpers.prototype.upperHtml = function(params)
* {
* return function(el) {
* $(el).html(params.toUpperCase());
* }
* }
*
* In your EJS view you can then call the helper on an element tag:
*
* <div <%= upperHtml('javascriptmvc') %>></div>
*
*
* @constructor Creates a view helper. This function
* is called internally. You should never call it.
* @param {Object} data The data passed to the
* view. Helpers have access to it through this._data
*/
EJS.Helpers = function( data, extras ) {
this._data = data;
this._extras = extras;
extend(this, extras);
};
/**
* @prototype
*/
EJS.Helpers.prototype = {
/**
* Hooks up a jQuery plugin on.
* @param {String} name the plugin name
*/
plugin: function( name ) {
var args = $.makeArray(arguments),
widget = args.shift();
return function( el ) {
var jq = $(el);
jq[widget].apply(jq, args);
};
},
/**
* Renders a partial view. This is deprecated in favor of <code>$.View()</code>.
*/
view: function( url, data, helpers ) {
helpers = helpers || this._extras;
data = data || this._data;
return $View(url, data, helpers); //new EJS(options).render(data, helpers);
}
};
// options for steal's build
$View.register({
suffix: "ejs",
//returns a function that renders the view
script: function( id, src ) {
return "jQuery.EJS(function(_CONTEXT,_VIEW) { " + new EJS({
text: src,
name: id
}).template.out + " })";
},
renderer: function( id, text ) {
return EJS({
text: text,
name: id
});
}
});
})(jQuery);
(function(){
/**
* @function closest
* @parent dom
* @plugin jquery/dom/closest
* Overwrites closest to allow open > selectors. This allows controller
* actions such as:
*
* ">li click" : function( el, ev ) { ... }
*/
var oldClosest = jQuery.fn.closest;
jQuery.fn.closest = function(selectors, context){
var rooted = {}, res, result, thing, i, j, selector, rootedIsEmpty = true, selector, selectorsArr = selectors;
if(typeof selectors == "string") selectorsArr = [selectors];
$.each(selectorsArr, function(i, selector){
if(selector.indexOf(">") == 0 ){
if(selector.indexOf(" ") != -1){
throw " closest does not work with > followed by spaces!"
}
rooted[( selectorsArr[i] = selector.substr(1) )] = selector;
if(typeof selectors == "string") selectors = selector.substr(1);
rootedIsEmpty = false;
}
})
res = oldClosest.call(this, selectors, context);
if(rootedIsEmpty) return res;
i =0;
while(i < res.length){
result = res[i], selector = result.selector;
if (rooted[selector] !== undefined) {
result.selector = rooted[selector];
rooted[selector] = false;
if(typeof result.selector !== "string" || result.elem.parentNode !== context ){
res.splice(i,1);
continue;
}
}
i++;
}
return res;
}
})(jQuery);
(function($){
/**
* @function compare
* @parent dom
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/compare/compare.js
*
* Compares the position of two nodes and returns a bitmask detailing how they are positioned
* relative to each other.
*
* $('#foo').compare($('#bar')) //-> Number
*
* You can expect it to return the same results as
* [http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition | compareDocumentPosition].
* Parts of this documentation and source come from [http://ejohn.org/blog/comparing-document-position | John Resig].
*
* ## Demo
* @demo jquery/dom/compare/compare.html
* @test jquery/dom/compare/qunit.html
* @plugin dom/compare
*
*
* @param {HTMLElement|jQuery} element an element or jQuery collection to compare against.
* @return {Number} A bitmap number representing how the elements are positioned from each other.
*
* If the code looks like:
*
* $('#foo').compare($('#bar')) //-> Number
*
* Number is a bitmap with with the following values:
* <table class='options'>
* <tr><th>Bits</th><th>Number</th><th>Meaning</th></tr>
* <tr><td>000000</td><td>0</td><td>Elements are identical.</td></tr>
* <tr><td>000001</td><td>1</td><td>The nodes are in different
* documents (or one is outside of a document).</td></tr>
* <tr><td>000010</td><td>2</td><td>#bar precedes #foo.</td></tr>
* <tr><td>000100</td><td>4</td><td>#foo precedes #bar.</td></tr>
* <tr><td>001000</td><td>8</td><td>#bar contains #foo.</td></tr>
* <tr><td>010000</td><td>16</td><td>#foo contains #bar.</td></tr>
* </table>
*/
jQuery.fn.compare = function(element){ //usually
//element is usually a relatedTarget, but element/c it is we have to avoid a few FF errors
try{ //FF3 freaks out with XUL
element = element.jquery ? element[0] : element;
}catch(e){
return null;
}
if (window.HTMLElement) { //make sure we aren't coming from XUL element
var s = HTMLElement.prototype.toString.call(element)
if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]' || s === '[object Window]') {
return null;
}
}
if(this[0].compareDocumentPosition){
return this[0].compareDocumentPosition(element);
}
if(this[0] == document && element != document) return 8;
var number = (this[0] !== element && this[0].contains(element) && 16) + (this[0] != element && element.contains(this[0]) && 8),
docEl = document.documentElement;
if(this[0].sourceIndex){
number += (this[0].sourceIndex < element.sourceIndex && 4)
number += (this[0].sourceIndex > element.sourceIndex && 2)
number += (this[0].ownerDocument !== element.ownerDocument ||
(this[0] != docEl && this[0].sourceIndex <= 0 ) ||
(element != docEl && element.sourceIndex <= 0 )) && 1
}else{
var range = document.createRange(),
sourceRange = document.createRange(),
compare;
range.selectNode(this[0]);
sourceRange.selectNode(element);
compare = range.compareBoundaryPoints(Range.START_TO_START, sourceRange);
}
return number;
}
})(jQuery);
(function( $ ) {
var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
rupper = /([A-Z])/g,
rdashAlpha = /-([a-z])/ig,
fcamelCase = function( all, letter ) {
return letter.toUpperCase();
},
getStyle = function( elem ) {
if ( getComputedStyle ) {
return getComputedStyle(elem, null);
}
else if ( elem.currentStyle ) {
return elem.currentStyle;
}
},
rfloat = /float/i,
rnumpx = /^-?\d+(?:px)?$/i,
rnum = /^-?\d/;
/**
* @add jQuery
*/
//
/**
* @function curStyles
* @param {HTMLElement} el
* @param {Array} styles An array of style names like <code>['marginTop','borderLeft']</code>
* @return {Object} an object of style:value pairs. Style names are camelCase.
*/
$.curStyles = function( el, styles ) {
if (!el ) {
return null;
}
var currentS = getStyle(el),
oldName, val, style = el.style,
results = {},
i = 0,
left, rsLeft, camelCase, name;
for (; i < styles.length; i++ ) {
name = styles[i];
oldName = name.replace(rdashAlpha, fcamelCase);
if ( rfloat.test(name) ) {
name = jQuery.support.cssFloat ? "float" : "styleFloat";
oldName = "cssFloat";
}
if ( getComputedStyle ) {
name = name.replace(rupper, "-$1").toLowerCase();
val = currentS.getPropertyValue(name);
if ( name === "opacity" && val === "" ) {
val = "1";
}
results[oldName] = val;
} else {
camelCase = name.replace(rdashAlpha, fcamelCase);
results[oldName] = currentS[name] || currentS[camelCase];
if (!rnumpx.test(results[oldName]) && rnum.test(results[oldName]) ) { //convert to px
// Remember the original values
left = style.left;
rsLeft = el.runtimeStyle.left;
// Put in the new values to get a computed value out
el.runtimeStyle.left = el.currentStyle.left;
style.left = camelCase === "fontSize" ? "1em" : (results[oldName] || 0);
results[oldName] = style.pixelLeft + "px";
// Revert the changed values
style.left = left;
el.runtimeStyle.left = rsLeft;
}
}
}
return results;
};
/**
* @add jQuery.fn
*/
$.fn
/**
* @parent dom
* @plugin jquery/dom/cur_styles
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/cur_styles/cur_styles.js
* @test jquery/dom/cur_styles/qunit.html
* Use curStyles to rapidly get a bunch of computed styles from an element.
* <h3>Quick Example</h3>
* @codestart
* $("#foo").curStyles('float','display') //->
* // {
* // cssFloat: "left", display: "block"
* // }
* @codeend
* <h2>Use</h2>
* <p>An element's <b>computed</b> style is the current calculated style of the property.
* This is different than the values on <code>element.style</code> as
* <code>element.style</code> doesn't reflect styles provided by css or the browser's default
* css properties.</p>
* <p>Getting computed values individually is expensive! This plugin lets you get all
* the style properties you need all at once.</p>
* <h2>Demo</h2>
* <p>The following demo illustrates the performance improvement curStyle provides by providing
* a faster 'height' jQuery function called 'fastHeight'.</p>
* @demo jquery/dom/cur_styles/cur_styles.html
* @param {String} style pass style names as arguments
* @return {Object} an object of style:value pairs
*/
.curStyles = function() {
return $.curStyles(this[0], $.makeArray(arguments));
};
})(jQuery);
(function($) {
/**
* @page dimensions dimensions
* @parent dom
* @plugin jquery/dom/dimensions
*
* The dimensions plugin adds support for setting+animating inner+outer height and widths.
*
* ### Quick Examples
*
* $('#foo').outerWidth(100).innerHeight(50);
* $('#bar').animate({outerWidth: 500});
*
* ## Use
*
* When writing reusable plugins, you often want to
* set or animate an element's width and height that include its padding,
* border, or margin. This is especially important in plugins that
* allow custom styling.
*
* The dimensions plugin overwrites [jQuery.fn.outerHeight outerHeight],
* [jQuery.fn.outerWidth outerWidth], [jQuery.fn.innerHeight innerHeight]
* and [jQuery.fn.innerWidth innerWidth]
* to let you set and animate these properties.
*
*
*
*
* ## Demo
*
* @demo jquery/dom/dimensions/dimensions.html
*/
var weird = /button|select/i, //margin is inside border
getBoxes = {},
checks = {
width: ["Left", "Right"],
height: ['Top', 'Bottom'],
oldOuterHeight: $.fn.outerHeight,
oldOuterWidth: $.fn.outerWidth,
oldInnerWidth: $.fn.innerWidth,
oldInnerHeight: $.fn.innerHeight
};
/**
* @add jQuery.fn
*/
$.each({
/**
* @function outerWidth
* @parent dimensions
* Lets you set the outer width on an object
* @param {Number} [height]
* @param {Boolean} [includeMargin=false] Makes setting the outerWidth adjust
* for margin. Defaults to false.
*
* $('#hasMargin').outerWidth(50, true);
*
* @return {jQuery|Number} If you are setting the value, returns the jQuery wrapped elements.
*/
width:
/**
* @function innerWidth
* @parent dimensions
* Lets you set the inner height of an object
* @param {Number} [height]
*/
"Width",
/**
* @function outerHeight
* @parent dimensions
* Lets you set the outer height of an object where: <br/>
* <code>outerHeight = height + padding + border + (margin)</code>.
* @codestart
* $("#foo").outerHeight(100); //sets outer height
* $("#foo").outerHeight(100, true); //uses margins
* $("#foo").outerHeight(); //returns outer height
* $("#foo").outerHeight(true); //returns outer height with margins
* @codeend
* When setting the outerHeight, it adjusts the height of the element.
* @param {Number|Boolean} [height] If a number is provided -> sets the outer height of the object.<br/>
* If true is given -> returns the outer height and includes margins.<br/>
* If no value is given -> returns the outer height without margin.
* @param {Boolean} [includeMargin] Makes setting the outerHeight adjust for margin.
* @return {jQuery|Number} If you are setting the value, returns the jQuery wrapped elements.
* Otherwise, returns outerHeight in pixels.
*/
height:
/**
* @function innerHeight
* @parent dimensions
* Lets you set the outer width on an object
* @param {Number} [height]
*/
"Height" }, function(lower, Upper) {
//used to get the padding and border for an element in a given direction
getBoxes[lower] = function(el, boxes) {
var val = 0;
if (!weird.test(el.nodeName)) {
//make what to check for ....
var myChecks = [];
$.each(checks[lower], function() {
var direction = this;
$.each(boxes, function(name, val) {
if (val)
myChecks.push(name + direction+ (name == 'border' ? "Width" : "") );
})
})
$.each($.curStyles(el, myChecks), function(name, value) {
val += (parseFloat(value) || 0);
})
}
return val;
}
//getter / setter
$.fn["outer" + Upper] = function(v, margin) {
var first = this[0];
if (typeof v == 'number') {
first && this[lower](v - getBoxes[lower](first, {padding: true, border: true, margin: margin}))
return this;
} else {
return first ? checks["oldOuter" + Upper].call(this, v) : null;
}
}
$.fn["inner" + Upper] = function(v) {
var first = this[0];
if (typeof v == 'number') {
first&& this[lower](v - getBoxes[lower](first, { padding: true }))
return this;
} else {
return first ? checks["oldInner" + Upper].call(this, v) : null;
}
}
//provides animations
var animate = function(boxes){
return function(fx){
if (fx.state == 0) {
fx.start = $(fx.elem)[lower]();
fx.end = fx.end - getBoxes[lower](fx.elem,boxes);
}
fx.elem.style[lower] = (fx.pos * (fx.end - fx.start) + fx.start) + "px"
}
}
$.fx.step["outer" + Upper] = animate({padding: true, border: true})
$.fx.step["outer" + Upper+"Margin"] = animate({padding: true, border: true, margin: true})
$.fx.step["inner" + Upper] = animate({padding: true})
})
})(jQuery);
(function($){
var isArray = $.isArray,
// essentially returns an object that has all the must have comparisons ...
// must haves, do not return true when provided undefined
cleanSet = function(obj, compares){
var copy = $.extend({}, obj);
for(var prop in copy) {
var compare = compares[prop] === undefined ? compares["*"] : compares[prop];
if( same(copy[prop], undefined, compare ) ) {
delete copy[prop]
}
}
return copy;
},
propCount = function(obj){
var count = 0;
for(var prop in obj) count++;
return count;
};
/**
* @class jQuery.Object
* @parent jquerymx.lang
*
* Object contains several helper methods that
* help compare objects.
*
* ## same
*
* Returns true if two objects are similar.
*
* $.Object.same({foo: "bar"} , {bar: "foo"}) //-> false
*
* ## subset
*
* Returns true if an object is a set of another set.
*
* $.Object.subset({}, {foo: "bar"} ) //-> true
*
* ## subsets
*
* Returns the subsets of an object
*
* $.Object.subsets({userId: 20},
* [
* {userId: 20, limit: 30},
* {userId: 5},
* {}
* ])
* //-> [{userId: 20, limit: 30}]
*/
$.Object = {};
/**
* @function same
* Returns if two objects are the same. It takes an optional compares object that
* can be used to make comparisons.
*
* This function does not work with objects that create circular references.
*
* ## Examples
*
* $.Object.same({name: "Justin"},
* {name: "JUSTIN"}) //-> false
*
* // ignore the name property
* $.Object.same({name: "Brian"},
* {name: "JUSTIN"},
* {name: null}) //-> true
*
* // ignore case
* $.Object.same({name: "Justin"},
* {name: "JUSTIN"},
* {name: "i"}) //-> true
*
* // deep rule
* $.Object.same({ person : { name: "Justin" } },
* { person : { name: "JUSTIN" } },
* { person : { name: "i" } }) //-> true
*
* // supplied compare function
* $.Object.same({age: "Thirty"},
* {age: 30},
* {age: function( a, b ){
* if( a == "Thirty" ) {
* a = 30
* }
* if( b == "Thirty" ) {
* b = 30
* }
* return a === b;
* }}) //-> true
*
* @param {Object} a an object to compare
* @param {Object} b an object to compare
* @param {Object} [compares] an object that indicates how to
* compare specific properties.
* Typically this is a name / value pair
*
* $.Object.same({name: "Justin"},{name: "JUSTIN"},{name: "i"})
*
* There are two compare functions that you can specify with a string:
*
* - 'i' - ignores case
* - null - ignores this property
*
* @param {Object} [deep] used internally
*/
var same = $.Object.same = function(a, b, compares, aParent, bParent, deep){
var aType = typeof a,
aArray = isArray(a),
comparesType = typeof compares,
compare;
if(comparesType == 'string' || compares === null ){
compares = compareMethods[compares];
comparesType = 'function'
}
if(comparesType == 'function'){
return compares(a, b, aParent, bParent)
}
compares = compares || {};
if(deep === -1){
return aType === 'object' || a === b;
}
if(aType !== typeof b || aArray !== isArray(b)){
return false;
}
if(a === b){
return true;
}
if(aArray){
if(a.length !== b.length){
return false;
}
for(var i =0; i < a.length; i ++){
compare = compares[i] === undefined ? compares["*"] : compares[i]
if(!same(a[i],b[i], a, b, compare )){
return false;
}
};
return true;
} else if(aType === "object" || aType === 'function'){
var bCopy = $.extend({}, b);
for(var prop in a){
compare = compares[prop] === undefined ? compares["*"] : compares[prop];
if(! same( a[prop], b[prop], compare , a, b, deep === false ? -1 : undefined )){
return false;
}
delete bCopy[prop];
}
// go through bCopy props ... if there is no compare .. return false
for(prop in bCopy){
if( compares[prop] === undefined ||
! same( undefined, b[prop], compares[prop] , a, b, deep === false ? -1 : undefined )){
return false;
}
}
return true;
}
return false;
};
/**
* @function subsets
* Returns the sets in 'sets' that are a subset of checkSet
* @param {Object} checkSet
* @param {Object} sets
*/
$.Object.subsets = function(checkSet, sets, compares){
var len = sets.length,
subsets = [],
checkPropCount = propCount(checkSet),
setLength;
for(var i =0; i < len; i++){
//check this subset
var set = sets[i];
if( $.Object.subset(checkSet, set, compares) ){
subsets.push(set)
}
}
return subsets;
};
/**
* @function subset
* Compares if checkSet is a subset of set
* @param {Object} checkSet
* @param {Object} set
* @param {Object} [compares]
* @param {Object} [checkPropCount]
*/
$.Object.subset = function(subset, set, compares){
// go through set {type: 'folder'} and make sure every property
// is in subset {type: 'folder', parentId :5}
// then make sure that set has fewer properties
// make sure we are only checking 'important' properties
// in subset (ones that have to have a value)
var setPropCount =0,
compares = compares || {};
for(var prop in set){
if(! same(subset[prop], set[prop], compares[prop], subset, set ) ){
return false;
}
}
return true;
}
var compareMethods = {
"null" : function(){
return true;
},
i : function(a, b){
return (""+a).toLowerCase() == (""+b).toLowerCase()
}
}
})(jQuery);
(function( $ ) {
//used to check urls
// the pre-filter needs to re-route the url
$.ajaxPrefilter( function( settings, originalOptions, jqXHR ) {
// if fixtures are on
if(! $.fixture.on) {
return;
}
// add the fixture option if programmed in
var data = overwrite(settings);
// if we don't have a fixture, do nothing
if(!settings.fixture){
if(window.location.protocol === "file:"){
;
}
return;
}
//if referencing something else, update the fixture option
if ( typeof settings.fixture === "string" && $.fixture[settings.fixture] ) {
settings.fixture = $.fixture[settings.fixture];
}
// if a string, we just point to the right url
if ( typeof settings.fixture == "string" ) {
var url = settings.fixture;
if (/^\/\//.test(url) ) {
url = steal.root.mapJoin(settings.fixture.substr(2))+'';
}
settings.url = url;
settings.data = null;
settings.type = "GET";
if (!settings.error ) {
settings.error = function( xhr, error, message ) {
throw "fixtures.js Error " + error + " " + message;
};
}
}else {
//it's a function ... add the fixture datatype so our fixture transport handles it
// TODO: make everything go here for timing and other fun stuff
settings.dataTypes.splice(0,0,"fixture");
if(data){
$.extend(originalOptions.data, data)
}
// add to settings data from fixture ...
}
});
$.ajaxTransport( "fixture", function( s, original ) {
// remove the fixture from the datatype
s.dataTypes.shift();
//we'll return the result of the next data type
var next = s.dataTypes[0],
timeout;
return {
send: function( headers , callback ) {
// callback after a timeout
timeout = setTimeout(function() {
// get the callback data from the fixture function
var response = s.fixture(original, s, headers);
// normalize the fixture data into a response
if(!$.isArray(response)){
var tmp = [{}];
tmp[0][next] = response
response = tmp;
}
if(typeof response[0] != 'number'){
response.unshift(200,"success")
}
// make sure we provide a response type that matches the first datatype (typically json)
if(!response[2] || !response[2][next]){
var tmp = {}
tmp[next] = response[2];
response[2] = tmp;
}
// pass the fixture data back to $.ajax
callback.apply(null, response );
}, $.fixture.delay);
},
abort: function() {
clearTimeout(timeout)
}
};
});
var typeTest = /^(script|json|test|jsonp)$/,
// a list of 'overwrite' settings object
overwrites = [],
// returns the index of an overwrite function
find = function(settings, exact){
for(var i =0; i < overwrites.length; i++){
if($fixture._similar(settings, overwrites[i], exact)){
return i;
}
}
return -1;
},
// overwrites the settings fixture if an overwrite matches
overwrite = function(settings){
var index = find(settings);
if(index > -1){
settings.fixture = overwrites[index].fixture;
return $fixture._getData(overwrites[index].url, settings.url)
}
},
/**
* Makes an attempt to guess where the id is at in the url and returns it.
* @param {Object} settings
*/
getId = function(settings){
var id = settings.data.id;
if(id === undefined && typeof settings.data === "number") {
id = settings.data;
}
/*
Check for id in params(if query string)
If this is just a string representation of an id, parse
if(id === undefined && typeof settings.data === "string") {
id = settings.data;
}
//*/
if(id === undefined){
settings.url.replace(/\/(\d+)(\/|$|\.)/g, function(all, num){
id = num;
});
}
if(id === undefined){
id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function(all, num){
if(num != 'update'){
id = num;
}
})
}
if(id === undefined){ // if still not set, guess a random number
id = Math.round(Math.random()*1000)
}
return id;
};
/**
* @function jQuery.fixture
* @plugin jquery/dom/fixture
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/fixture/fixture.js
* @test jquery/dom/fixture/qunit.html
* @parent dom
*
* <code>$.fixture</code> intercepts a AJAX request and simulates
* the response with a file or function. They are a great technique
* when you want to develop JavaScript
* independently of the backend.
*
* ## Types of Fixtures
*
* There are two common ways of using fixtures. The first is to
* map Ajax requests to another file. The following
* intercepts requests to <code>/tasks.json</code> and directs them
* to <code>fixtures/tasks.json</code>:
*
* $.fixture("/tasks.json","fixtures/tasks.json");
*
* The other common option is to generate the Ajax response with
* a function. The following intercepts updating tasks at
* <code>/tasks/ID.json</code> and responds with updated data:
*
* $.fixture("PUT /tasks/{id}.json", function(original, settings, headers){
* return { updatedAt : new Date().getTime() }
* })
*
* We categorize fixtures into the following types:
*
* - __Static__ - the response is in a file.
* - __Dynamic__ - the response is generated by a function.
*
* There are different ways to lookup static and dynamic fixtures.
*
* ## Static Fixtures
*
* Static fixtures use an alternate url as the response of the Ajax request.
*
* // looks in fixtures/tasks1.json relative to page
* $.fixture("tasks/1", "fixtures/task1.json");
*
* $.fixture("tasks/1", "//fixtures/task1.json");
*
* ## Dynamic Fixtures
*
* Dynamic Fixtures are functions that get the details of
* the Ajax request and return the result of the mocked service
* request from your server.
*
* For example, the following returns a successful response
* with JSON data from the server:
*
* $.fixture("/foobar.json", function(orig, settings, headers){
* return [200, "success", {json: {foo: "bar" } }, {} ]
* })
*
* The fixture function has the following signature:
*
* function( originalOptions, options, headers ) {
* return [ status, statusText, responses, responseHeaders ]
* }
*
* where the fixture function is called with:
*
* - originalOptions - are the options provided to the ajax method, unmodified,
* and thus, without defaults from ajaxSettings
* - options - are the request options
* - headers - a map of key/value request headers
*
* and the fixture function returns an array as arguments for ajaxTransport's <code>completeCallback</code> with:
*
* - status - is the HTTP status code of the response.
* - statusText - the status text of the response
* - responses - a map of dataType/value that contains the responses for each data format supported
* - headers - response headers
*
* However, $.fixture handles the
* common case where you want a successful response with JSON data. The
* previous can be written like:
*
* $.fixture("/foobar.json", function(orig, settings, headers){
* return {foo: "bar" };
* })
*
* If you want to return an array of data, wrap your array in another array:
*
* $.fixture("/tasks.json", function(orig, settings, headers){
* return [ [ "first","second","third"] ];
* })
*
* $.fixture works closesly with jQuery's
* ajaxTransport system. Understanding it is the key to creating advanced
* fixtures.
*
* ### Templated Urls
*
* Often, you want a dynamic fixture to handle urls
* for multiple resources (for example a REST url scheme). $.fixture's
* templated urls allow you to match urls with a wildcard.
*
* The following example simulates services that get and update 100 todos.
*
* // create todos
* var todos = {};
* for(var i = 0; i < 100; i++) {
* todos[i] = {
* id: i,
* name: "Todo "+i
* }
* }
* $.fixture("GET /todos/{id}", function(orig){
* // return the JSON data
* // notice that id is pulled from the url and added to data
* return todos[orig.data.id]
* })
* $.fixture("PUT /todos/{id}", function(orig){
* // update the todo's data
* $.extend( todos[orig.data.id], orig.data );
*
* // return data
* return {};
* })
*
* Notice that data found in templated urls (ex: <code>{id}</code>) is added to the original
* data object.
*
* ## Simulating Errors
*
* The following simulates an unauthorized request
* to <code>/foo</code>.
*
* $.fixture("/foo", function(){
* return [401,"{type: 'unauthorized'}"]
* });
*
* This could be received by the following Ajax request:
*
* $.ajax({
* url: '/foo',
* error : function(jqXhr, status, statusText){
* // status === 'error'
* // statusText === "{type: 'unauthorized'}"
* }
* })
*
* ## Turning off Fixtures
*
* You can remove a fixture by passing <code>null</code> for the fixture option:
*
* // add a fixture
* $.fixture("GET todos.json","//fixtures/todos.json");
*
* // remove the fixture
* $.fixture("GET todos.json", null)
*
* You can also set [jQuery.fixture.on $.fixture.on] to false:
*
* $.fixture.on = false;
*
* ## Make
*
* [jQuery.fixture.make $.fixture.make] makes a CRUD service layer that handles sorting, grouping,
* filtering and more.
*
* ## Testing Performance
*
* Dynamic fixtures are awesome for performance testing. Want to see what
* 10000 files does to your app's performance? Make a fixture that returns 10000 items.
*
* What to see what the app feels like when a request takes 5 seconds to return? Set
* [jQuery.fixture.delay] to 5000.
*
* @demo jquery/dom/fixture/fixture.html
*
* @param {Object|String} settings Configures the AJAX requests the fixture should
* intercept. If an __object__ is passed, the object's properties and values
* are matched against the settings passed to $.ajax.
*
* If a __string__ is passed, it can be used to match the url and type. Urls
* can be templated, using <code>{NAME}</code> as wildcards.
*
* @param {Function|String} fixture The response to use for the AJAX
* request. If a __string__ url is passed, the ajax request is redirected
* to the url. If a __function__ is provided, it looks like:
*
* fixture( originalSettings, settings, headers )
*
* where:
*
* - originalSettings - the orignal settings passed to $.ajax
* - settings - the settings after all filters have run
* - headers - request headers
*
* If __null__ is passed, and there is a fixture at settings, that fixture will be removed,
* allowing the AJAX request to behave normally.
*/
var $fixture = $.fixture = function( settings , fixture ){
// if we provide a fixture ...
if(fixture !== undefined){
if(typeof settings == 'string'){
// handle url strings
var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i);
if(!matches){
settings = {
url : settings
};
} else {
settings = {
url : matches[2],
type: matches[1]
};
}
}
//handle removing. An exact match if fixture was provided, otherwise, anything similar
var index = find(settings, !!fixture);
if(index > -1){
overwrites.splice(index,1)
}
if(fixture == null){
return
}
settings.fixture = fixture;
overwrites.push(settings)
}
};
var replacer = $.String._regs.replacer;
$.extend($.fixture, {
// given ajax settings, find an overwrite
_similar : function(settings, overwrite, exact){
if(exact){
return $.Object.same(settings , overwrite, {fixture : null})
} else {
return $.Object.subset(settings, overwrite, $.fixture._compare)
}
},
_compare : {
url : function(a, b){
return !! $fixture._getData(b, a)
},
fixture : null,
type : "i"
},
// gets data from a url like "/todo/{id}" given "todo/5"
_getData : function(fixtureUrl, url){
var order = [],
fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.').replace('?', '\\?'),
res = new RegExp(fixtureUrlAdjusted.replace(replacer, function(whole, part){
order.push(part)
return "([^\/]+)"
})+"$").exec(url),
data = {};
if(!res){
return null;
}
res.shift();
$.each(order, function(i, name){
data[name] = res.shift()
})
return data;
},
/**
* @hide
* Provides a rest update fixture function
*/
"-restUpdate": function( settings ) {
return [200,"succes",{
id: getId(settings)
},{
location: settings.url+"/"+getId(settings)
}];
},
/**
* @hide
* Provides a rest destroy fixture function
*/
"-restDestroy": function( settings, cbType ) {
return {};
},
/**
* @hide
* Provides a rest create fixture function
*/
"-restCreate": function( settings, cbType, nul, id ) {
var id = id || parseInt(Math.random() * 100000, 10);
return [200,"succes",{
id: id
},{
location: settings.url+"/"+id
}];
},
/**
* @function jQuery.fixture.make
* @parent jQuery.fixture
* Used to make fixtures for findAll / findOne style requests.
*
* //makes a nested list of messages
* $.fixture.make(["messages","message"],1000, function(i, messages){
* return {
* subject: "This is message "+i,
* body: "Here is some text for this message",
* date: Math.floor( new Date().getTime() ),
* parentId : i < 100 ? null : Math.floor(Math.random()*i)
* }
* })
* //uses the message fixture to return messages limited by offset, limit, order, etc.
* $.ajax({
* url: "messages",
* data:{
* offset: 100,
* limit: 50,
* order: ["date ASC"],
* parentId: 5},
* },
* fixture: "-messages",
* success: function( messages ) { ... }
* });
*
* @param {Array|String} types An array of the fixture names or the singular fixture name.
* If an array, the first item is the plural fixture name (prefixed with -) and the second
* item is the singular name. If a string, it's assumed to be the singular fixture name. Make
* will simply add s to the end of it for the plural name.
* @param {Number} count the number of items to create
* @param {Function} make a function that will return json data representing the object. The
* make function is called back with the id and the current array of items.
* @param {Function} filter (optional) a function used to further filter results. Used for to simulate
* server params like searchText or startDate. The function should return true if the item passes the filter,
* false otherwise. For example:
*
*
* function(item, settings){
* if(settings.data.searchText){
* var regex = new RegExp("^"+settings.data.searchText)
* return regex.test(item.name);
* }
* }
*
*/
make: function( types, count, make, filter ) {
if(typeof types === "string"){
types = [types+"s",types ]
}
// make all items
var items = ($.fixture["~" + types[0]] = []), // TODO: change this to a hash
findOne = function(id){
for ( var i = 0; i < items.length; i++ ) {
if ( id == items[i].id ) {
return items[i];
}
}
};
for ( var i = 0; i < (count); i++ ) {
//call back provided make
var item = make(i, items);
if (!item.id ) {
item.id = i;
}
items.push(item);
}
//set plural fixture for findAll
$.fixture["-" + types[0]] = function( settings ) {
//copy array of items
var retArr = items.slice(0);
settings.data = settings.data || {};
//sort using order
//order looks like ["age ASC","gender DESC"]
$.each((settings.data.order || []).slice(0).reverse(), function( i, name ) {
var split = name.split(" ");
retArr = retArr.sort(function( a, b ) {
if ( split[1].toUpperCase() !== "ASC" ) {
if( a[split[0]] < b[split[0]] ) {
return 1;
} else if(a[split[0]] == b[split[0]]){
return 0
} else {
return -1;
}
}
else {
if( a[split[0]] < b[split[0]] ) {
return -1;
} else if(a[split[0]] == b[split[0]]){
return 0
} else {
return 1;
}
}
});
});
//group is just like a sort
$.each((settings.data.group || []).slice(0).reverse(), function( i, name ) {
var split = name.split(" ");
retArr = retArr.sort(function( a, b ) {
return a[split[0]] > b[split[0]];
});
});
var offset = parseInt(settings.data.offset, 10) || 0,
limit = parseInt(settings.data.limit, 10) || (items.length - offset),
i = 0;
//filter results if someone added an attr like parentId
for ( var param in settings.data ) {
i=0;
if ( settings.data[param] !== undefined && // don't do this if the value of the param is null (ignore it)
(param.indexOf("Id") != -1 || param.indexOf("_id") != -1) ) {
while ( i < retArr.length ) {
if ( settings.data[param] != retArr[i][param] ) {
retArr.splice(i, 1);
} else {
i++;
}
}
}
}
if( filter ) {
i = 0;
while (i < retArr.length) {
if (!filter(retArr[i], settings)) {
retArr.splice(i, 1);
} else {
i++;
}
}
}
//return data spliced with limit and offset
return [{
"count": retArr.length,
"limit": settings.data.limit,
"offset": settings.data.offset,
"data": retArr.slice(offset, offset + limit)
}];
};
// findOne
$.fixture["-" + types[1]] = function( settings ) {
var item = findOne(getId(settings));
return item ? [item] : [];
};
// update
$.fixture["-" + types[1]+"Update"] = function( settings, cbType ) {
var id = getId(settings);
// TODO: make it work with non-linear ids ..
$.extend(findOne(id), settings.data);
return $.fixture["-restUpdate"](settings, cbType)
};
$.fixture["-" + types[1]+"Destroy"] = function( settings, cbType ) {
var id = getId(settings);
for(var i = 0; i < items.length; i ++ ){
if(items[i].id == id){
items.splice(i, 1);
break;
}
}
// TODO: make it work with non-linear ids ..
$.extend(findOne(id), settings.data);
return $.fixture["-restDestroy"](settings, cbType)
};
$.fixture["-" + types[1]+"Create"] = function( settings, cbType ) {
var item = make(items.length, items);
$.extend(item, settings.data);
if(!item.id){
item.id = items.length;
}
items.push(item);
return $.fixture["-restCreate"](settings, cbType, undefined, item.id );
};
return {
getId: getId,
findOne : findOne,
find : function(settings){
return findOne( getId(settings) );
}
}
},
/**
* @function jQuery.fixture.rand
* @parent jQuery.fixture
*
* Creates random integers or random arrays of
* other arrays.
*
* ## Examples
*
* var rand = $.fixture.rand;
*
* // get a random integer between 0 and 10 (inclusive)
* rand(11);
*
* // get a random number between -5 and 5 (inclusive)
* rand(-5, 6);
*
* // pick a random item from an array
* rand(["j","m","v","c"],1)[0]
*
* // pick a random number of items from an array
* rand(["j","m","v","c"])
*
* // pick 2 items from an array
* rand(["j","m","v","c"],2)
*
* // pick between 2 and 3 items at random
* rand(["j","m","v","c"],2,3)
*
*
* @param {Array|Number} arr An array of items to select from.
* If a number is provided, a random number is returned.
* If min and max are not provided, a random number of items are selected
* from this array.
* @param {Number} [min] If only min is provided, min items
* are selected.
* @param {Number} [max] If min and max are provided, a random number of
* items between min and max (inclusive) is selected.
*/
rand : function(arr, min, max){
if(typeof arr == 'number'){
if(typeof min == 'number'){
return arr+ Math.floor(Math.random() * (min - arr) );
} else {
return Math.floor(Math.random() * arr);
}
}
var rand = arguments.callee;
// get a random set
if(min === undefined){
return rand(arr, rand(arr.length+1))
}
// get a random selection of arr
var res = [];
arr = arr.slice(0);
// set max
if(!max){
max = min;
}
//random max
max = min + Math.round( rand(max - min) )
for(var i=0; i < max; i++){
res.push(arr.splice( rand(arr.length), 1 )[0])
}
return res;
},
/**
* @hide
* Use $.fixture.xhr to create an object that looks like an xhr object.
*
* ## Example
*
* The following example shows how the -restCreate fixture uses xhr to return
* a simulated xhr object:
* @codestart
* "-restCreate" : function( settings, cbType ) {
* switch(cbType){
* case "success":
* return [
* {id: parseInt(Math.random()*1000)},
* "success",
* $.fixture.xhr()];
* case "complete":
* return [
* $.fixture.xhr({
* getResponseHeader: function() {
* return settings.url+"/"+parseInt(Math.random()*1000);
* }
* }),
* "success"];
* }
* }
* @codeend
* @param {Object} [xhr] properties that you want to overwrite
* @return {Object} an object that looks like a successful XHR object.
*/
xhr: function( xhr ) {
return $.extend({}, {
abort: $.noop,
getAllResponseHeaders: function() {
return "";
},
getResponseHeader: function() {
return "";
},
open: $.noop,
overrideMimeType: $.noop,
readyState: 4,
responseText: "",
responseXML: null,
send: $.noop,
setRequestHeader: $.noop,
status: 200,
statusText: "OK"
}, xhr);
},
/**
* @attribute on
* On lets you programatically turn off fixtures. This is mostly used for testing.
*
* $.fixture.on = false
* Task.findAll({}, function(){
* $.fixture.on = true;
* })
*/
on : true
});
/**
* @attribute $.fixture.delay
* @parent $.fixture
* Sets the delay in milliseconds between an ajax request is made and
* the success and complete handlers are called. This only sets
* functional fixtures. By default, the delay is 200ms.
* @codestart
* steal('jquery/dom/fixtures').then(function(){
* $.fixture.delay = 1000;
* })
* @codeend
*/
$.fixture.delay = 200;
$.fixture["-handleFunction"] = function( settings ) {
if ( typeof settings.fixture === "string" && $.fixture[settings.fixture] ) {
settings.fixture = $.fixture[settings.fixture];
}
if ( typeof settings.fixture == "function" ) {
setTimeout(function() {
if ( settings.success ) {
settings.success.apply(null, settings.fixture(settings, "success"));
}
if ( settings.complete ) {
settings.complete.apply(null, settings.fixture(settings, "complete"));
}
}, $.fixture.delay);
return true;
}
return false;
};
/**
* @page jquery.fixture.0organizing Organizing Fixtures
* @parent jQuery.fixture
*
* The __best__ way of organizing fixtures is to have a 'fixtures.js' file that steals
* <code>jquery/dom/fixture</code> and defines all your fixtures. For example,
* if you have a 'todo' application, you might
* have <code>todo/fixtures/fixtures.js</code> look like:
*
* steal({
* path: '//jquery/dom/fixture.js',
* ignore: true
* })
* .then(function(){
*
* $.fixture({
* type: 'get',
* url: '/services/todos.json'
* },
* '//todo/fixtures/todos.json');
*
* $.fixture({
* type: 'post',
* url: '/services/todos.json'
* },
* function(settings){
* return {id: Math.random(),
* name: settings.data.name}
* });
*
* })
*
* __Notice__: We used steal's ignore option to prevent
* loading the fixture plugin in production.
*
* Finally, we steal <code>todo/fixtures/fixtures.js</code> in the
* app file (<code>todo/todo.js</code>) like:
*
*
* steal({path: '//todo/fixtures/fixtures.js',ignore: true});
*
* //start of your app's steals
* steal( ... )
*
* We typically keep it a one liner so it's easy to comment out.
*
* ## Switching Between Sets of Fixtures
*
* If you are using fixtures for testing, you often want to use different
* sets of fixtures. You can add something like the following to your fixtures.js file:
*
* if( /fixtureSet1/.test( window.location.search) ){
* $.fixture("/foo","//foo/fixtures/foo1.json');
* } else if(/fixtureSet2/.test( window.location.search)){
* $.fixture("/foo","//foo/fixtures/foo1.json');
* } else {
* // default fixtures (maybe no fixtures)
* }
*
*/
//Expose this for fixture debugging
$.fixture.overwrites = overwrites;
})(jQuery);
(function( $ ) {
var radioCheck = /radio|checkbox/i,
keyBreaker = /[^\[\]]+/g,
numberMatcher = /^[\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?$/;
var isNumber = function( value ) {
if ( typeof value == 'number' ) {
return true;
}
if ( typeof value != 'string' ) {
return false;
}
return value.match(numberMatcher);
};
$.fn.extend({
/**
* @parent dom
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/form_params/form_params.js
* @plugin jquery/dom/form_params
* @test jquery/dom/form_params/qunit.html
*
* Returns an object of name-value pairs that represents values in a form.
* It is able to nest values whose element's name has square brackets.
*
* When convert is set to true strings that represent numbers and booleans will
* be converted and empty string will not be added to the object.
*
* Example html:
* @codestart html
* &lt;form>
* &lt;input name="foo[bar]" value='2'/>
* &lt;input name="foo[ced]" value='4'/>
* &lt;form/>
* @codeend
* Example code:
*
* $('form').formParams() //-> { foo:{bar:'2', ced: '4'} }
*
*
* @demo jquery/dom/form_params/form_params.html
*
* @param {Object} [params] If an object is passed, the form will be repopulated
* with the values of the object based on the name of the inputs within
* the form
* @param {Boolean} [convert=false] True if strings that look like numbers
* and booleans should be converted and if empty string should not be added
* to the result. Defaults to false.
* @return {Object} An object of name-value pairs.
*/
formParams: function( params, convert ) {
// Quick way to determine if something is a boolean
if ( !! params === params ) {
convert = params;
params = null;
}
if ( params ) {
return this.setParams( params );
} else if ( this[0].nodeName.toLowerCase() == 'form' && this[0].elements ) {
return jQuery(jQuery.makeArray(this[0].elements)).getParams(convert);
}
return jQuery("input[name], textarea[name], select[name]", this[0]).getParams(convert);
},
setParams: function( params ) {
// Find all the inputs
this.find("[name]").each(function() {
var value = params[ $(this).attr("name") ],
$this;
// Don't do all this work if there's no value
if ( value !== undefined ) {
$this = $(this);
// Nested these if statements for performance
if ( $this.is(":radio") ) {
if ( $this.val() == value ) {
$this.attr("checked", true);
}
} else if ( $this.is(":checkbox") ) {
// Convert single value to an array to reduce
// complexity
value = $.isArray( value ) ? value : [value];
if ( $.inArray( $this.val(), value ) > -1) {
$this.attr("checked", true);
}
} else {
$this.val( value );
}
}
});
},
getParams: function( convert ) {
var data = {},
current;
convert = convert === undefined ? false : convert;
this.each(function() {
var el = this,
type = el.type && el.type.toLowerCase();
//if we are submit, ignore
if ((type == 'submit') || !el.name ) {
return;
}
var key = el.name,
value = $.data(el, "value") || $.fn.val.call([el]),
isRadioCheck = radioCheck.test(el.type),
parts = key.match(keyBreaker),
write = !isRadioCheck || !! el.checked,
//make an array of values
lastPart;
if ( convert ) {
if ( isNumber(value) ) {
value = parseFloat(value);
} else if ( value === 'true') {
value = true;
} else if ( value === 'false' ) {
value = false;
}
if(value === '') {
value = undefined;
}
}
// go through and create nested objects
current = data;
for ( var i = 0; i < parts.length - 1; i++ ) {
if (!current[parts[i]] ) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
lastPart = parts[parts.length - 1];
//now we are on the last part, set the value
if (current[lastPart]) {
if (!$.isArray(current[lastPart]) ) {
current[lastPart] = current[lastPart] === undefined ? [] : [current[lastPart]];
}
if ( write ) {
current[lastPart].push(value);
}
} else if ( write || !current[lastPart] ) {
current[lastPart] = write ? value : undefined;
}
});
return data;
}
});
})(jQuery);
(function($){
var withinBox = function(x, y, left, top, width, height ){
return (y >= top &&
y < top + height &&
x >= left &&
x < left + width);
}
/**
* @function within
* @parent dom
* @plugin jquery/dom/within
*
* Returns the elements are within the position.
*
* // get all elements that touch 200x200.
* $('*').within(200, 200);
*
* @param {Number} left the position from the left of the page
* @param {Number} top the position from the top of the page
* @param {Boolean} [useOffsetCache] cache the dimensions and offset of the elements.
* @return {jQuery} a jQuery collection of elements whos area
* overlaps the element position.
*/
$.fn.within= function(left, top, useOffsetCache) {
var ret = []
this.each(function(){
var q = jQuery(this);
if (this == document.documentElement) {
return ret.push(this);
}
var offset = useOffsetCache ?
jQuery.data(this,"offsetCache") || jQuery.data(this,"offsetCache", q.offset()) :
q.offset();
var res = withinBox(left, top, offset.left, offset.top,
this.offsetWidth, this.offsetHeight );
if (res) {
ret.push(this);
}
});
return this.pushStack( jQuery.unique( ret ), "within", left+","+top );
}
/**
* @function withinBox
* @parent jQuery.fn.within
* returns if elements are within the box
* @param {Object} left
* @param {Object} top
* @param {Object} width
* @param {Object} height
* @param {Object} cache
*/
$.fn.withinBox = function(left, top, width, height, cache){
var ret = []
this.each(function(){
var q = jQuery(this);
if(this == document.documentElement) return this.ret.push(this);
var offset = cache ?
jQuery.data(this,"offset") ||
jQuery.data(this,"offset", q.offset()) :
q.offset();
var ew = q.width(), eh = q.height();
res = !( (offset.top > top+height) || (offset.top +eh < top) || (offset.left > left+width ) || (offset.left+ew < left));
if(res)
ret.push(this);
});
return this.pushStack( jQuery.unique( ret ), "withinBox", jQuery.makeArray(arguments).join(",") );
}
})(jQuery);
(function( $ ) {
var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
rupper = /([A-Z])/g,
rdashAlpha = /-([a-z])/ig,
fcamelCase = function( all, letter ) {
return letter.toUpperCase();
},
getStyle = function( elem ) {
if ( getComputedStyle ) {
return getComputedStyle(elem, null);
}
else if ( elem.currentStyle ) {
return elem.currentStyle;
}
},
rfloat = /float/i,
rnumpx = /^-?\d+(?:px)?$/i,
rnum = /^-?\d/;
/**
* @add jQuery
*/
//
/**
* @function curStyles
* @param {HTMLElement} el
* @param {Array} styles An array of style names like <code>['marginTop','borderLeft']</code>
* @return {Object} an object of style:value pairs. Style names are camelCase.
*/
$.curStyles = function( el, styles ) {
if (!el ) {
return null;
}
var currentS = getStyle(el),
oldName, val, style = el.style,
results = {},
i = 0,
left, rsLeft, camelCase, name;
for (; i < styles.length; i++ ) {
name = styles[i];
oldName = name.replace(rdashAlpha, fcamelCase);
if ( rfloat.test(name) ) {
name = jQuery.support.cssFloat ? "float" : "styleFloat";
oldName = "cssFloat";
}
if ( getComputedStyle ) {
name = name.replace(rupper, "-$1").toLowerCase();
val = currentS.getPropertyValue(name);
if ( name === "opacity" && val === "" ) {
val = "1";
}
results[oldName] = val;
} else {
camelCase = name.replace(rdashAlpha, fcamelCase);
results[oldName] = currentS[name] || currentS[camelCase];
if (!rnumpx.test(results[oldName]) && rnum.test(results[oldName]) ) { //convert to px
// Remember the original values
left = style.left;
rsLeft = el.runtimeStyle.left;
// Put in the new values to get a computed value out
el.runtimeStyle.left = el.currentStyle.left;
style.left = camelCase === "fontSize" ? "1em" : (results[oldName] || 0);
results[oldName] = style.pixelLeft + "px";
// Revert the changed values
style.left = left;
el.runtimeStyle.left = rsLeft;
}
}
}
return results;
};
/**
* @add jQuery.fn
*/
$.fn
/**
* @parent dom
* @plugin jquery/dom/cur_styles
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/cur_styles/cur_styles.js
* @test jquery/dom/cur_styles/qunit.html
* Use curStyles to rapidly get a bunch of computed styles from an element.
* <h3>Quick Example</h3>
* @codestart
* $("#foo").curStyles('float','display') //->
* // {
* // cssFloat: "left", display: "block"
* // }
* @codeend
* <h2>Use</h2>
* <p>An element's <b>computed</b> style is the current calculated style of the property.
* This is different than the values on <code>element.style</code> as
* <code>element.style</code> doesn't reflect styles provided by css or the browser's default
* css properties.</p>
* <p>Getting computed values individually is expensive! This plugin lets you get all
* the style properties you need all at once.</p>
* <h2>Demo</h2>
* <p>The following demo illustrates the performance improvement curStyle provides by providing
* a faster 'height' jQuery function called 'fastHeight'.</p>
* @demo jquery/dom/cur_styles/cur_styles.html
* @param {String} style pass style names as arguments
* @return {Object} an object of style:value pairs
*/
.curStyles = function() {
return $.curStyles(this[0], $.makeArray(arguments));
};
})(jQuery);
(function() {
// Common helper methods taken from jQuery (or other places)
// Keep here so someday can be abstracted
var $String = $.String,
getObject = $String.getObject,
underscore = $String.underscore,
classize = $String.classize,
isArray = $.isArray,
makeArray = $.makeArray,
extend = $.extend,
each = $.each,
trigger = function(obj, event, args){
$.event.trigger(event, args, obj, true)
},
// used to make an ajax request where
// ajaxOb - a bunch of options
// data - the attributes or data that will be sent
// success - callback function
// error - error callback
// fixture - the name of the fixture (typically a path or something on $.fixture
// type - the HTTP request type (defaults to "post")
// dataType - how the data should return (defaults to "json")
ajax = function(ajaxOb, data, success, error, fixture, type, dataType ) {
// if we get a string, handle it
if ( typeof ajaxOb == "string" ) {
// if there's a space, it's probably the type
var sp = ajaxOb.indexOf(" ")
if ( sp > -1 ) {
ajaxOb = {
url: ajaxOb.substr(sp + 1),
type: ajaxOb.substr(0, sp)
}
} else {
ajaxOb = {url : ajaxOb}
}
}
// if we are a non-array object, copy to a new attrs
ajaxOb.data = typeof data == "object" && !isArray(data) ?
extend(ajaxOb.data || {}, data) : data;
// get the url with any templated values filled out
ajaxOb.url = $String.sub(ajaxOb.url, ajaxOb.data, true);
return $.ajax(extend({
type: type || "post",
dataType: dataType ||"json",
fixture: fixture,
success : success,
error: error
},ajaxOb));
},
// guesses at a fixture name where
// extra - where to look for 'MODELNAME'+extra fixtures (ex: "Create" -> '-recipeCreate')
// or - if the first fixture fails, default to this
fixture = function( model, extra, or ) {
// get the underscored shortName of this Model
var u = underscore(model.shortName),
// the first place to look for fixtures
f = "-" + u + (extra || "");
// if the fixture exists in $.fixture
return $.fixture && $.fixture[f] ?
// return the name
f :
// or return or
or ||
// or return a fixture derived from the path
"//" + underscore(model.fullName).replace(/\.models\..*/, "").replace(/\./g, "/") + "/fixtures/" + u + (extra || "") + ".json";
},
// takes attrs, and adds it to the attrs (so it can be added to the url)
// if attrs already has an id, it means it's trying to update the id
// in this case, it sets the new ID as newId.
addId = function( model, attrs, id ) {
attrs = attrs || {};
var identity = model.id;
if ( attrs[identity] && attrs[identity] !== id ) {
attrs["new" + $String.capitalize(id)] = attrs[identity];
delete attrs[identity];
}
attrs[identity] = id;
return attrs;
},
// returns the best list-like object (list is passed)
getList = function( type ) {
var listType = type || $.Model.List || Array;
return new listType();
},
// a helper function for getting an id from an instance
getId = function( inst ) {
return inst[inst.constructor.id]
},
// returns a collection of unique items
// this works on objects by adding a "__u Nique" property.
unique = function( items ) {
var collect = [];
// check unique property, if it isn't there, add to collect
each(items, function( i, item ) {
if (!item["__u Nique"] ) {
collect.push(item);
item["__u Nique"] = 1;
}
});
// remove unique
return each(collect, function( i, item ) {
delete item["__u Nique"];
});
},
// helper makes a request to a static ajax method
// it also calls updated, created, or destroyed
// and it returns a deferred that resolvesWith self and the data
// returned from the ajax request
makeRequest = function( self, type, success, error, method ) {
// create the deferred makeRequest will return
var deferred = $.Deferred(),
// on a successful ajax request, call the
// updated | created | destroyed method
// then resolve the deferred
resolve = function( data ) {
self[method || type + "d"](data);
deferred.resolveWith(self, [self, data, type]);
},
// on reject reject the deferred
reject = function( data ) {
deferred.rejectWith(self, [data])
},
// the args to pass to the ajax method
args = [self.serialize(), resolve, reject],
// the Model
model = self.constructor,
jqXHR,
promise = deferred.promise();
// destroy does not need data
if ( type == 'destroy' ) {
args.shift();
}
// update and destroy need the id
if ( type !== 'create' ) {
args.unshift(getId(self))
}
// hook up success and error
deferred.then(success);
deferred.fail(error);
// call the model's function and hook up
// abort
jqXHR = model[type].apply(model, args);
if(jqXHR && jqXHR.abort){
promise.abort = function(){
jqXHR.abort();
}
}
return promise;
},
// a quick way to tell if it's an object and not some string
isObject = function( obj ) {
return typeof obj === 'object' && obj !== null && obj;
},
$method = function( name ) {
return function( eventType, handler ) {
return $.fn[name].apply($([this]), arguments);
}
},
bind = $method('bind'),
unbind = $method('unbind'),
STR_CONSTRUCTOR = 'constructor';
/**
* @class jQuery.Model
* @parent jquerymx
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/model.js
* @test jquery/model/qunit.html
* @plugin jquery/model
* @description Models and apps data layer.
*
* Models super-charge an application's
* data layer, making it easy to:
*
* - Get and modify data from the server
* - Listen to changes in data
* - Setting and retrieving models on elements
* - Deal with lists of data
* - Do other good stuff
*
* Model inherits from [jQuery.Class $.Class] and make use
* of REST services and [http://api.jquery.com/category/deferred-object/ deferreds]
* so these concepts are worth exploring. Also,
* the [mvc.model Get Started with jQueryMX] has a good walkthrough of $.Model.
*
*
* ## Get and modify data from the server
*
* $.Model makes connecting to a JSON REST service
* really easy. The following models <code>todos</code> by
* describing the services that can create, retrieve,
* update, and delete todos.
*
* $.Model('Todo',{
* findAll: 'GET /todos.json',
* findOne: 'GET /todos/{id}.json',
* create: 'POST /todos.json',
* update: 'PUT /todos/{id}.json',
* destroy: 'DELETE /todos/{id}.json'
* },{});
*
* This lets you create, retrieve, update, and delete
* todos programatically:
*
* __Create__
*
* Create a todo instance and
* call <code>[$.Model::save save]\(success, error\)</code>
* to create the todo on the server.
*
* // create a todo instance
* var todo = new Todo({name: "do the dishes"})
*
* // save it on the server
* todo.save();
*
* __Retrieve__
*
* Retrieve a list of todos from the server with
* <code>[$.Model.findAll findAll]\(params, callback(items)\)</code>:
*
* Todo.findAll({}, function( todos ){
*
* // print out the todo names
* $.each(todos, function(i, todo){
* console.log( todo.name );
* });
* });
*
* Retrieve a single todo from the server with
* <code>[$.Model.findOne findOne]\(params, callback(item)\)</code>:
*
* Todo.findOne({id: 5}, function( todo ){
*
* // print out the todo name
* console.log( todo.name );
* });
*
* __Update__
*
* Once an item has been created on the server,
* you can change its properties and call
* <code>save</code> to update it on the server.
*
* // update the todos' name
* todo.attr('name','Take out the trash')
*
* // update it on the server
* todo.save()
*
*
* __Destroy__
*
* Call <code>[$.Model.prototype.destroy destroy]\(success, error\)</code>
* to delete an item on the server.
*
* todo.destroy()
*
* ## Listen to changes in data
*
* Listening to changes in data is a critical part of
* the [http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller Model-View-Controller]
* architecture. $.Model lets you listen to when an item is created, updated, destroyed
* or its properties are changed. Use
* <code>Model.[$.Model.bind bind]\(eventType, handler(event, model)\)</code>
* to listen to all events of type on a model and
* <code>model.[$.Model.prototype.bind bind]\(eventType, handler(event)\)</code>
* to listen to events on a specific instance.
*
* __Create__
*
* // listen for when any todo is created
* Todo.bind('created', function( ev, todo ) {...})
*
* // listen for when a specific todo is created
* var todo = new Todo({name: 'do dishes'})
* todo.bind('created', function( ev ) {...})
*
* __Update__
*
* // listen for when any todo is updated
* Todo.bind('updated', function( ev, todo ) {...})
*
* // listen for when a specific todo is created
* Todo.findOne({id: 6}, function( todo ) {
* todo.bind('updated', function( ev ) {...})
* })
*
* __Destroy__
*
* // listen for when any todo is destroyed
* Todo.bind('destroyed', function( ev, todo ) {...})
*
* // listen for when a specific todo is destroyed
* todo.bind('destroyed', function( ev ) {...})
*
* __Property Changes__
*
* // listen for when the name property changes
* todo.bind('name', function(ev){ })
*
* __Listening with Controller__
*
* You should be using controller to listen to model changes like:
*
* $.Controller('Todos',{
* "{Todo} updated" : function(Todo, ev, todo) {...}
* })
*
*
* ## Setting and retrieving data on elements
*
* Almost always, we use HTMLElements to represent
* data to the user. When that data changes, we update those
* elements to reflect the new data.
*
* $.Model has helper methods that make this easy. They
* let you "add" a model to an element and also find
* all elements that have had a model "added" to them.
*
* Consider a todo list widget
* that lists each todo in the page and when a todo is
* deleted, removes it.
*
* <code>[jQuery.fn.model $.fn.model]\(item\)</code> lets you set or read a model
* instance from an element:
*
* Todo.findAll({}, function( todos ) {
*
* $.each(todos, function(todo) {
* $('<li>').model(todo)
* .text(todo.name)
* .appendTo('#todos')
* });
* });
*
* When a todo is deleted, get its element with
* <code>item.[$.Model.prototype.elements elements]\(context\)</code>
* and remove it from the page.
*
* Todo.bind('destroyed', function( ev, todo ) {
* todo.elements( $('#todos') ).remove()
* })
*
* __Using EJS and $.Controller__
*
* [jQuery.View $.View] and [jQuery.EJS EJS] makes adding model data
* to elements easy. We can implement the todos widget like the following:
*
* $.Controller('Todos',{
* init: function(){
* this.element.html('//todos/views/todos.ejs', Todo.findAll({}) );
* },
* "{Todo} destroyed": function(Todo, ev, todo) {
* todo.elements( this.element ).remove()
* }
* })
*
* In todos.ejs
*
* @codestart html
* &lt;% for(var i =0; i &lt; todos.length; i++){ %>
* &lt;li &lt;%= todos[i] %>>&lt;%= todos[i].name %>&lt;/li>
* &lt;% } %>
* @codeend
*
* Notice how you can add a model to an element with <code>&lt;%= model %&gt;</code>
*
* ## Lists
*
* [$.Model.List $.Model.List] lets you handle multiple model instances
* with ease. A List acts just like an <code>Array</code>, but you can add special properties
* to it and listen to events on it.
*
* <code>$.Model.List</code> has become its own plugin, read about it
* [$.Model.List here].
*
* ## Other Good Stuff
*
* Model can make a lot of other common tasks much easier.
*
* ### Type Conversion
*
* Data from the server often needs massaging to make it more useful
* for JavaScript. A typical example is date data which is
* commonly passed as
* a number representing the Julian date like:
*
* { name: 'take out trash',
* id: 1,
* dueDate: 1303173531164 }
*
* But instead, you want a JavaScript date object:
*
* date.attr('dueDate') //-> new Date(1303173531164)
*
* By defining property-type pairs in [$.Model.attributes attributes],
* you can have model auto-convert values from the server into more useful types:
*
* $.Model('Todo',{
* attributes : {
* dueDate: 'date'
* }
* },{})
*
* ### Associations
*
* The [$.Model.attributes attributes] property also
* supports associations. For example, todo data might come back with
* User data as an owner property like:
*
* { name: 'take out trash',
* id: 1,
* owner: { name: 'Justin', id: 3} }
*
* To convert owner into a User model, set the owner type as the User's
* [$.Model.model model]<code>( data )</code> method:
*
* $.Model('Todo',{
* attributes : {
* owner: 'User.model'
* }
* },{})
*
* ### Helper Functions
*
* Often, you need to perform repeated calculations
* with a model's data. You can create methods in the model's
* prototype and they will be available on
* all model instances.
*
* The following creates a <code>timeRemaining</code> method that
* returns the number of seconds left to complete the todo:
*
* $.Model('Todo',{
* },{
* timeRemaining : function(){
* return new Date() - new Date(this.dueDate)
* }
* })
*
* // create a todo
* var todo = new Todo({dueDate: new Date()});
*
* // show off timeRemaining
* todo.timeRemaining() //-> Number
*
* ### Deferreds
*
* Model methods that make requests to the server such as:
* [$.Model.findAll findAll], [$.Model.findOne findOne],
* [$.Model.prototype.save save], and [$.Model.prototype.destroy destroy] return a
* [jquery.model.deferreds deferred] that resolves to the item(s)
* being retrieved or modified.
*
* Deferreds can make a lot of asynchronous code much easier. For example, the following
* waits for all users and tasks before continuing :
*
* $.when(Task.findAll(), User.findAll())
* .then(function( tasksRes, usersRes ){ ... })
*
* ### Validations
*
* [jquery.model.validations Validate] your model's attributes.
*
* $.Model("Contact",{
* init : function(){
* this.validate("birthday",function(){
* if(this.birthday > new Date){
* return "your birthday needs to be in the past"
* }
* })
* }
* ,{});
*
*
*/
// methods that we'll weave into model if provided
ajaxMethods =
/**
* @Static
*/
{
create: function( str ) {
/**
* @function create
* Create is used by [$.Model.prototype.save save] to create a model instance on the server.
*
* The easiest way to implement create is to give it the url to post data to:
*
* $.Model("Recipe",{
* create: "/recipes"
* },{})
*
* This lets you create a recipe like:
*
* new Recipe({name: "hot dog"}).save();
*
* You can also implement create by yourself. Create gets called with:
*
* - `attrs` - the [$.Model.serialize serialized] model attributes.
* - `success(newAttrs)` - a success handler.
* - `error` - an error handler.
*
* You just need to call success back with
* an object that contains the id of the new instance and any other properties that should be
* set on the instance.
*
* For example, the following code makes a request
* to `POST /recipes.json {'name': 'hot+dog'}` and gets back
* something that looks like:
*
* {
* "id": 5,
* "createdAt": 2234234329
* }
*
* The code looks like:
*
* $.Model("Recipe", {
* create : function(attrs, success, error){
* $.post("/recipes.json",attrs, success,"json");
* }
* },{})
*
*
* @param {Object} attrs Attributes on the model instance
* @param {Function} success(newAttrs) the callback function, it must be called with an object
* that has the id of the new instance and any other attributes the service needs to add.
* @param {Function} error a function to callback if something goes wrong.
*/
return function( attrs, success, error ) {
return ajax(str || this._shortName, attrs, success, error, fixture(this, "Create", "-restCreate"))
};
},
update: function( str ) {
/**
* @function update
* Update is used by [$.Model.prototype.save save] to
* update a model instance on the server.
*
* The easist way to implement update is to just give it the url to `PUT` data to:
*
* $.Model("Recipe",{
* update: "/recipes/{id}"
* },{})
*
* This lets you update a recipe like:
*
* // PUT /recipes/5 {name: "Hot Dog"}
* Recipe.update(5, {name: "Hot Dog"},
* function(){
* this.name //this is the updated recipe
* })
*
* If your server doesn't use PUT, you can change it to post like:
*
* $.Model("Recipe",{
* update: "POST /recipes/{id}"
* },{})
*
* Your server should send back an object with any new attributes the model
* should have. For example if your server udpates the "updatedAt" property, it
* should send back something like:
*
* // PUT /recipes/4 {name: "Food"} ->
* {
* updatedAt : "10-20-2011"
* }
*
* You can also implement create by yourself. You just need to call success back with
* an object that contains any properties that should be
* set on the instance.
*
* For example, the following code makes a request
* to '/recipes/5.json?name=hot+dog' and gets back
* something that looks like:
*
* {
* updatedAt: "10-20-2011"
* }
*
* The code looks like:
*
* $.Model("Recipe", {
* update : function(id, attrs, success, error){
* $.post("/recipes/"+id+".json",attrs, success,"json");
* }
* },{})
*
*
* @param {String} id the id of the model instance
* @param {Object} attrs Attributes on the model instance
* @param {Function} success(attrs) the callback function. It optionally accepts
* an object of attribute / value pairs of property changes the client doesn't already
* know about. For example, when you update a name property, the server might
* update other properties as well (such as updatedAt). The server should send
* these properties as the response to updates. Passing them to success will
* update the model instance with these properties.
*
* @param {Function} error a function to callback if something goes wrong.
*/
return function( id, attrs, success, error ) {
return ajax( str || this._shortName+"/{"+this.id+"}", addId(this, attrs, id), success, error, fixture(this, "Update", "-restUpdate"), "put")
}
},
destroy: function( str ) {
/**
* @function destroy
* Destroy is used to remove a model instance from the server.
*
* You can implement destroy with a string like:
*
* $.Model("Thing",{
* destroy : "POST /thing/destroy/{id}"
* })
*
* Or you can implement destroy manually like:
*
* $.Model("Thing",{
* destroy : function(id, success, error){
* $.post("/thing/destroy/"+id,{}, success);
* }
* })
*
* You just have to call success if the destroy was successful.
*
* @param {String|Number} id the id of the instance you want destroyed
* @param {Function} success the callback function, it must be called with an object
* that has the id of the new instance and any other attributes the service needs to add.
* @param {Function} error a function to callback if something goes wrong.
*/
return function( id, success, error ) {
var attrs = {};
attrs[this.id] = id;
return ajax( str || this._shortName+"/{"+this.id+"}", attrs, success, error, fixture(this, "Destroy", "-restDestroy"), "delete")
}
},
findAll: function( str ) {
/**
* @function findAll
* FindAll is used to retrive a model instances from the server.
* findAll returns a deferred ($.Deferred).
*
* You can implement findAll with a string:
*
* $.Model("Thing",{
* findAll : "/things.json"
* },{})
*
* Or you can implement it yourself. The `dataType` attribute
* is used to convert a JSON array of attributes
* to an array of instances. It calls <code>[$.Model.models]\(raw\)</code>. For example:
*
* $.Model("Thing",{
* findAll : function(params, success, error){
* return $.ajax({
* url: '/things.json',
* type: 'get',
* dataType: 'json thing.models',
* data: params,
* success: success,
* error: error})
* }
* },{})
*
*
* @param {Object} params data to refine the results. An example might be passing {limit : 20} to
* limit the number of items retrieved.
* @param {Function} success(items) called with an array (or Model.List) of model instances.
* @param {Function} error
*/
return function( params, success, error ) {
return ajax( str || this._shortName, params, success, error, fixture(this, "s"), "get", "json " + this._shortName + ".models");
};
},
findOne: function( str ) {
/**
* @function findOne
* FindOne is used to retrive a model instances from the server. By implementing
* findOne along with the rest of the [jquery.model.services service api], your models provide an abstract
* service API.
*
* You can implement findOne with a string:
*
* $.Model("Thing",{
* findOne : "/things/{id}.json"
* },{})
*
* Or you can implement it yourself.
*
* $.Model("Thing",{
* findOne : function(params, success, error){
* var self = this,
* id = params.id;
* delete params.id;
* return $.get("/things/"+id+".json",
* params,
* success,
* "json thing.model")
* }
* },{})
*
*
* @param {Object} params data to refine the results. This is often something like {id: 5}.
* @param {Function} success(item) called with a model instance
* @param {Function} error
*/
return function( params, success, error ) {
return ajax(str || this._shortName+"/{"+this.id+"}", params, success, error, fixture(this), "get", "json " + this._shortName + ".model");
};
}
};
jQuery.Class("jQuery.Model", {
setup: function( superClass, stat, proto ) {
var self = this,
fullName = this.fullName;
//we do not inherit attributes (or validations)
each(["attributes", "validations"], function( i, name ) {
if (!self[name] || superClass[name] === self[name] ) {
self[name] = {};
}
})
//add missing converters and serializes
each(["convert", "serialize"], function( i, name ) {
if ( superClass[name] != self[name] ) {
self[name] = extend({}, superClass[name], self[name]);
}
});
this._fullName = underscore(fullName.replace(/\./g, "_"));
this._shortName = underscore(this.shortName);
if ( fullName.indexOf("jQuery") == 0 ) {
return;
}
//add this to the collection of models
//$.Model.models[this._fullName] = this;
if ( this.listType ) {
this.list = new this.listType([]);
}
each(ajaxMethods, function(name, method){
var prop = self[name];
if ( typeof prop !== 'function' ) {
self[name] = method(prop);
}
});
//add ajax converters
var converters = {},
convertName = "* " + this._shortName + ".model";
converters[convertName + "s"] = this.proxy('models');
converters[convertName] = this.proxy('model');
$.ajaxSetup({
converters: converters
});
},
/**
* @attribute attributes
* Attributes contains a map of attribute names/types.
* You can use this in conjunction with
* [$.Model.convert] to provide automatic
* [jquery.model.typeconversion type conversion] (including
* associations).
*
* The following converts dueDates to JavaScript dates:
*
*
* $.Model("Contact",{
* attributes : {
* birthday : 'date'
* },
* convert : {
* date : function(raw){
* if(typeof raw == 'string'){
* var matches = raw.match(/(\d+)-(\d+)-(\d+)/)
* return new Date( matches[1],
* (+matches[2])-1,
* matches[3] )
* }else if(raw instanceof Date){
* return raw;
* }
* }
* }
* },{})
*
* ## Associations
*
* Attribute type values can also represent the name of a
* function. The most common case this is used is for
* associated data.
*
* For example, a Deliverable might have many tasks and
* an owner (which is a Person). The attributes property might
* look like:
*
* attributes : {
* tasks : "App.Models.Task.models"
* owner: "App.Models.Person.model"
* }
*
* This points tasks and owner properties to use
* <code>Task.models</code> and <code>Person.model</code>
* to convert the raw data into an array of Tasks and a Person.
*
* Note that the full names of the models themselves are <code>App.Models.Task</code>
* and <code>App.Models.Person</code>. The _.model_ and _.models_ parts are appended
* for the benefit of [$.Model.convert convert] to identify the types as
* models.
*
* @demo jquery/model/pages/associations.html
*
*/
attributes: {},
/**
* $.Model.model is used as a [http://api.jquery.com/extending-ajax/#Converters Ajax converter]
* to convert the response of a [$.Model.findOne] request
* into a model instance.
*
* You will never call this method directly. Instead, you tell $.ajax about it in findOne:
*
* $.Model('Recipe',{
* findOne : function(params, success, error ){
* return $.ajax({
* url: '/services/recipes/'+params.id+'.json',
* type: 'get',
*
* dataType : 'json recipe.model' //LOOK HERE!
* });
* }
* },{})
*
* This makes the result of findOne a [http://api.jquery.com/category/deferred-object/ $.Deferred]
* that resolves to a model instance:
*
* var deferredRecipe = Recipe.findOne({id: 6});
*
* deferredRecipe.then(function(recipe){
* console.log('I am '+recipes.description+'.');
* })
*
* ## Non-standard Services
*
* $.jQuery.model expects data to be name-value pairs like:
*
* {id: 1, name : "justin"}
*
* It can also take an object with attributes in a data, attributes, or
* 'shortName' property. For a App.Models.Person model the following will all work:
*
* { data : {id: 1, name : "justin"} }
*
* { attributes : {id: 1, name : "justin"} }
*
* { person : {id: 1, name : "justin"} }
*
*
* ### Overwriting Model
*
* If your service returns data like:
*
* {id : 1, name: "justin", data: {foo : "bar"} }
*
* This will confuse $.Model.model. You will want to overwrite it to create
* an instance manually:
*
* $.Model('Person',{
* model : function(data){
* return new this(data);
* }
* },{})
*
*
* @param {Object} attributes An object of name-value pairs or an object that has a
* data, attributes, or 'shortName' property that maps to an object of name-value pairs.
* @return {Model} an instance of the model
*/
model: function( attributes ) {
if (!attributes ) {
return null;
}
if ( attributes instanceof this ) {
attributes = attributes.serialize();
}
return new this(
// checks for properties in an object (like rails 2.0 gives);
isObject(attributes[this._shortName]) || isObject(attributes.data) || isObject(attributes.attributes) || attributes);
},
/**
* $.Model.models is used as a [http://api.jquery.com/extending-ajax/#Converters Ajax converter]
* to convert the response of a [$.Model.findAll] request
* into an array (or [$.Model.List $.Model.List]) of model instances.
*
* You will never call this method directly. Instead, you tell $.ajax about it in findAll:
*
* $.Model('Recipe',{
* findAll : function(params, success, error ){
* return $.ajax({
* url: '/services/recipes.json',
* type: 'get',
* data: params
*
* dataType : 'json recipe.models' //LOOK HERE!
* });
* }
* },{})
*
* This makes the result of findAll a [http://api.jquery.com/category/deferred-object/ $.Deferred]
* that resolves to a list of model instances:
*
* var deferredRecipes = Recipe.findAll({});
*
* deferredRecipes.then(function(recipes){
* console.log('I have '+recipes.length+'recipes.');
* })
*
* ## Non-standard Services
*
* $.jQuery.models expects data to be an array of name-value pairs like:
*
* [{id: 1, name : "justin"},{id:2, name: "brian"}, ...]
*
* It can also take an object with additional data about the array like:
*
* {
* count: 15000 //how many total items there might be
* data: [{id: 1, name : "justin"},{id:2, name: "brian"}, ...]
* }
*
* In this case, models will return an array of instances found in
* data, but with additional properties as expandos on the array:
*
* var people = Person.models({
* count : 1500,
* data : [{id: 1, name: 'justin'}, ...]
* })
* people[0].name // -> justin
* people.count // -> 1500
*
* ### Overwriting Models
*
* If your service returns data like:
*
* {ballers: [{name: "justin", id: 5}]}
*
* You will want to overwrite models to pass the base models what it expects like:
*
* $.Model('Person',{
* models : function(data){
* return this._super(data.ballers);
* }
* },{})
*
* @param {Array} instancesRawData an array of raw name - value pairs.
* @return {Array} a JavaScript array of instances or a [$.Model.List list] of instances
* if the model list plugin has been included.
*/
models: function( instancesRawData ) {
if (!instancesRawData ) {
return null;
}
// get the list type
var res = getList(this.List),
// did we get an array
arr = isArray(instancesRawData),
// cache model list
ML = $.Model.List,
// did we get a model list?
ml = (ML && instancesRawData instanceof ML),
// get the raw array of objects
raw = arr ?
// if an array, return the array
instancesRawData :
// otherwise if a model list
(ml ?
// get the raw objects from the list
instancesRawData.serialize() :
// get the object's data
instancesRawData.data),
// the number of items
length = raw ? raw.length : null,
i = 0;
for (; i < length; i++ ) {
res.push(this.model(raw[i]));
}
if (!arr ) { //push other stuff onto array
each(instancesRawData, function(prop, val){
if ( prop !== 'data' ) {
res[prop] = val;
}
})
}
return res;
},
/**
* The name of the id field. Defaults to 'id'. Change this if it is something different.
*
* For example, it's common in .NET to use Id. Your model might look like:
*
* @codestart
* $.Model("Friends",{
* id: "Id"
* },{});
* @codeend
*/
id: 'id',
//if null, maybe treat as an array?
/**
* Adds an attribute to the list of attributes for this class.
* @hide
* @param {String} property
* @param {String} type
*/
addAttr: function( property, type ) {
var stub, attrs = this.attributes;
stub = attrs[property] || (attrs[property] = type);
return type;
},
/**
* @attribute convert
* @type Object
* An object of name-function pairs that are used to convert attributes.
* Check out [$.Model.attributes] or
* [jquery.model.typeconversion type conversion]
* for examples.
*
* Convert comes with the following types:
*
* - date - Converts to a JS date. Accepts integers or strings that work with Date.parse
* - number - an integer or number that can be passed to parseFloat
* - boolean - converts "false" to false, and puts everything else through Boolean()
*/
convert: {
"date": function( str ) {
var type = typeof str;
if ( type === "string" ) {
return isNaN(Date.parse(str)) ? null : Date.parse(str)
} else if ( type === 'number' ) {
return new Date(str)
} else {
return str
}
},
"number": function( val ) {
return parseFloat(val);
},
"boolean": function( val ) {
return Boolean(val === "false" ? 0 : val);
},
"default": function( val, error, type ) {
var construct = getObject(type),
context = window,
realType;
// if type has a . we need to look it up
if ( type.indexOf(".") >= 0 ) {
// get everything before the last .
realType = type.substring(0, type.lastIndexOf("."));
// get the object before the last .
context = getObject(realType);
}
return typeof construct == "function" ? construct.call(context, val) : val;
}
},
/**
* @attribute serialize
* @type Object
* An object of name-function pairs that are used to serialize attributes.
* Similar to [$.Model.convert], in that the keys of this object
* correspond to the types specified in [$.Model.attributes].
*
* For example, to serialize all dates to ISO format:
*
*
* $.Model("Contact",{
* attributes : {
* birthday : 'date'
* },
* serialize : {
* date : function(val, type){
* return new Date(val).toISOString();
* }
* }
* },{})
*
* new Contact({ birthday: new Date("Oct 25, 1973") }).serialize()
* // { "birthday" : "1973-10-25T05:00:00.000Z" }
*
*/
serialize: {
"default": function( val, type ) {
return isObject(val) && val.serialize ? val.serialize() : val;
},
"date": function( val ) {
return val && val.getTime()
}
},
/**
* @function bind
*/
bind: bind,
/**
* @function unbind
*/
unbind: unbind,
_ajax: ajax
},
/**
* @Prototype
*/
{
/**
* Setup is called when a new model instance is created.
* It adds default attributes, then whatever attributes
* are passed to the class.
* Setup should never be called directly.
*
* @codestart
* $.Model("Recipe")
* var recipe = new Recipe({foo: "bar"});
* recipe.foo //-> "bar"
* recipe.attr("foo") //-> "bar"
* @codeend
*
* @param {Object} attributes a hash of attributes
*/
setup: function( attributes ) {
// so we know not to fire events
this._init = true;
this.attrs(extend({}, this.constructor.defaults, attributes));
delete this._init;
},
/**
* Sets the attributes on this instance and calls save.
* The instance needs to have an id. It will use
* the instance class's [$.Model.update update]
* method.
*
* @codestart
* recipe.update({name: "chicken"}, success, error);
* @codeend
*
* The model will also publish a _updated_ event with [jquery.model.events Model Events].
*
* @param {Object} attrs the model's attributes
* @param {Function} success called if a successful update
* @param {Function} error called if there's an error
*/
update: function( attrs, success, error ) {
this.attrs(attrs);
return this.save(success, error); //on success, we should
},
/**
* Runs the validations on this model. You can
* also pass it an array of attributes to run only those attributes.
* It returns nothing if there are no errors, or an object
* of errors by attribute.
*
* To use validations, it's suggested you use the
* model/validations plugin.
*
* $.Model("Task",{
* init : function(){
* this.validatePresenceOf("dueDate")
* }
* },{});
*
* var task = new Task(),
* errors = task.errors()
*
* errors.dueDate[0] //-> "can't be empty"
*
* @param {Array} [attrs] an optional list of attributes to get errors for:
*
* task.errors(['dueDate']);
*
* @return {Object} an object of attributeName : [errors] like:
*
* task.errors() // -> {dueDate: ["cant' be empty"]}
*/
errors: function( attrs ) {
// convert attrs to an array
if ( attrs ) {
attrs = isArray(attrs) ? attrs : makeArray(arguments);
}
var errors = {},
self = this,
attr,
// helper function that adds error messages to errors object
// attr - the name of the attribute
// funcs - the validation functions
addErrors = function( attr, funcs ) {
each(funcs, function( i, func ) {
var res = func.call(self);
if ( res ) {
if (!errors[attr] ) {
errors[attr] = [];
}
errors[attr].push(res);
}
});
},
validations = this.constructor.validations;
// go through each attribute or validation and
// add any errors
each(attrs || validations || {}, function( attr, funcs ) {
// if we are iterating through an array, use funcs
// as the attr name
if ( typeof attr == 'number' ) {
attr = funcs;
funcs = validations[attr];
}
// add errors to the
addErrors(attr, funcs || []);
});
// return errors as long as we have one
return $.isEmptyObject(errors) ? null : errors;
},
/**
* Gets or sets an attribute on the model using setters and
* getters if available.
*
* @codestart
* $.Model("Recipe")
* var recipe = new Recipe();
* recipe.attr("foo","bar")
* recipe.foo //-> "bar"
* recipe.attr("foo") //-> "bar"
* @codeend
*
* ## Setters
*
* If you add a set<i>AttributeName</i> method on your model,
* it will be used to set the value. The set method is called
* with the value and is expected to return the converted value.
*
* @codestart
* $.Model("Recipe",{
* setCreatedAt : function(raw){
* return Date.parse(raw)
* }
* })
* var recipe = new Recipe();
* recipe.attr("createdAt","Dec 25, 1995")
* recipe.createAt //-> Date
* @codeend
*
* ## Asynchronous Setters
*
* Sometimes, you want to perform an ajax request when
* you set a property. You can do this with setters too.
*
* To do this, your setter should return undefined and
* call success with the converted value. For example:
*
* @codestart
* $.Model("Recipe",{
* setTitle : function(title, success, error){
* $.post(
* "recipe/update/"+this.id+"/title",
* title,
* function(){
* success(title);
* },
* "json")
* }
* })
*
* recipe.attr("title","fish")
* @codeend
*
* ## Events
*
* When you use attr, it can also trigger events. This is
* covered in [$.Model.prototype.bind].
*
* @param {String} attribute the attribute you want to set or get
* @param {String|Number|Boolean} [value] value the value you want to set.
* @param {Function} [success] an optional success callback.
* This gets called if the attribute was successful.
* @param {Function} [error] an optional success callback.
* The error function is called with validation errors.
*/
attr: function( attribute, value, success, error ) {
// get the getter name getAttrName
var cap = classize(attribute),
get = "get" + cap;
// if we are setting the property
if ( value !== undefined ) {
// the potential setter name
var setName = "set" + cap,
//the old value
old = this[attribute],
self = this,
// if an error happens, this gets called
// it calls back the error handler
errorCallback = function( errors ) {
var stub;
stub = error && error.call(self, errors);
trigger(self, "error." + attribute, errors);
};
// if we have a setter
if ( this[setName] &&
// call the setter, if returned value is undefined,
// this means the setter is async so we
// do not call update property and return right away
(value = this[setName](value,
// a success handler we pass to the setter, it needs to call
// this if it returns undefined
this.proxy('_updateProperty', attribute, value, old, success, errorCallback), errorCallback)) === undefined ) {
return;
}
// call update property which will actually update the property
this._updateProperty(attribute, value, old, success, errorCallback);
return this;
}
// get the attribute, check if we have a getter, otherwise, just get the data
return this[get] ? this[get]() : this[attribute];
},
/**
* @function bind
* Binds to events on this model instance. Typically
* you'll bind to an attribute name. Handler will be called
* every time the attribute value changes. For example:
*
* @codestart
* $.Model("School")
* var school = new School();
* school.bind("address", function(ev, address){
* alert('address changed to '+address);
* })
* school.attr("address","1124 Park St");
* @codeend
*
* You can also bind to attribute errors.
*
* @codestart
* $.Model("School",{
* setName : function(name, success, error){
* if(!name){
* error("no name");
* }
* return error;
* }
* })
* var school = new School();
* school.bind("error.name", function(ev, mess){
* mess // -> "no name";
* })
* school.attr("name","");
* @codeend
*
* You can also bind to created, updated, and destroyed events.
*
* @param {String} eventType the name of the event.
* @param {Function} handler a function to call back when an event happens on this model.
* @return {model} the model instance for chaining
*/
bind: bind,
/**
* @function unbind
* Unbinds an event handler from this instance.
* Read [$.Model.prototype.bind] for
* more information.
* @param {String} eventType
* @param {Function} handler
*/
unbind: unbind,
// Actually updates a property on a model. This
// - Triggers events when a property has been updated
// - uses converters to change the data type
// propety - the attribute name
// value - the new value
// old - the old value
// success -
_updateProperty: function( property, value, old, success, errorCallback ) {
var Class = this.constructor,
// the value that we will set
val,
// the type of the attribute
type = Class.attributes[property] || Class.addAttr(property, "string"),
//the converter
converter = Class.convert[type] || Class.convert['default'],
// errors for this property
errors = null,
// the event name prefix (might be error.)
prefix = "",
global = "updated.",
args, globalArgs, callback = success,
list = Class.list;
// set the property value
// notice that even if there's an error
// property values get set
val = this[property] =
//if the value is null
( value === null ?
// it should be null
null :
// otherwise, the converters to make it something useful
converter.call(Class, value, function() {}, type) );
//validate (only if not initializing, this is for performance)
if (!this._init ) {
errors = this.errors(property);
}
// args triggered on the property event name
args = [val];
// args triggered on the 'global' event (updated.attr)
globalArgs = [property, val, old];
// if there are errors, change props so we trigger error events
if ( errors ) {
prefix = global = "error.";
callback = errorCallback;
globalArgs.splice(1, 0, errors);
args.unshift(errors)
}
// as long as we changed values, trigger events
if ( old !== val && !this._init ) {
!errors && trigger(this, prefix + property, args);
trigger(this,global + "attr", globalArgs);
}
callback && callback.apply(this, args);
//if this class has a global list, add / remove from the list.
if ( property === Class.id && val !== null && list ) {
// if we didn't have an old id, add ourselves
if (!old ) {
list.push(this);
} else if ( old != val ) {
// if our id has changed ... well this should be ok
list.remove(old);
list.push(this);
}
}
},
/**
* Removes an attribute from the list existing of attributes.
* Each attribute is set with [$.Model.prototype.attr attr].
*
* @codestart
* recipe.removeAttr('name')
* @codeend
*
* @param {Object} [attribute] the attribute to remove
*/
removeAttr: function( attr ) {
var old = this[attr],
deleted = false,
attrs = this.constructor.attributes;
//- pop it off the object
if ( this[attr] ) {
delete this[attr];
}
//- pop it off the Class attributes collection
if ( attrs[attr] ) {
delete attrs[attr];
deleted = true;
}
//- trigger the update
if (!this._init && deleted && old ) {
trigger(this,"updated.attr", [attr, null, old]);
}
},
/**
* Gets or sets a list of attributes.
* Each attribute is set with [$.Model.prototype.attr attr].
*
* @codestart
* recipe.attrs({
* name: "ice water",
* instructions : "put water in a glass"
* })
* @codeend
*
* This can be used nicely with [jquery.model.events].
*
* @param {Object} [attributes] if present, the list of attributes to send
* @return {Object} the current attributes of the model
*/
attrs: function( attributes ) {
var key, constructor = this.constructor,
attrs = constructor.attributes;
if (!attributes ) {
attributes = {};
for ( key in attrs ) {
if ( attrs.hasOwnProperty(key) ) {
attributes[key] = this.attr(key);
}
}
} else {
var idName = constructor.id;
//always set the id last
for ( key in attributes ) {
if ( key != idName ) {
this.attr(key, attributes[key]);
}
}
if ( idName in attributes ) {
this.attr(idName, attributes[idName]);
}
}
return attributes;
},
/**
* Get a serialized object for the model. Serialized data is typically
* used to send back to a server. See [$.Model.serialize].
*
* model.serialize() // -> { name: 'Fred' }
*
* @return {Object} a JavaScript object that can be serialized with
* `JSON.stringify` or other methods.
*/
serialize: function() {
var Class = this.constructor,
attrs = Class.attributes,
type, converter, data = {},
attr;
attributes = {};
for ( attr in attrs ) {
if ( attrs.hasOwnProperty(attr) ) {
type = attrs[attr];
// the attribute's converter or the default converter for the class
converter = Class.serialize[type] || Class.serialize['default'];
data[attr] = converter.call(Class, this[attr], type);
}
}
return data;
},
/**
* Returns if the instance is a new object. This is essentially if the
* id is null or undefined.
*
* new Recipe({id: 1}).isNew() //-> false
* @return {Boolean} false if an id is set, true if otherwise.
*/
isNew: function() {
var id = getId(this);
return (id === undefined || id === null || id === ''); //if null or undefined
},
/**
* Creates or updates the instance using [$.Model.create] or
* [$.Model.update] depending if the instance
* [$.Model.prototype.isNew has an id or not].
*
* When a save is successful, `success` is called and depending if the
* instance was created or updated, a created or updated event is fired.
*
* ### Example
*
* $.Model('Recipe',{
* created : "/recipes",
* updated : "/recipes/{id}.json"
* },{})
*
* // create a new instance
* var recipe = new Recipe({name: "ice water"});
*
* // listen for when it is created or updated
* recipe.bind('created', function(ev, recipe){
* console.log('created', recipe.id)
* }).bind('updated', function(ev, recipe){
* console.log('updated', recipe.id );
* })
*
* // create the recipe on the server
* recipe.save(function(){
* // update the recipe's name
* recipe.attr('name','Ice Water');
*
* // update the recipe on the server
* recipe.save();
* }, error);
*
*
* @param {Function} [success] called with (instance,data) if a successful save.
* @param {Function} [error] error handler function called with (jqXHR) if the
* save was not successful. It is passed the ajax request's jQXHR object.
* @return {$.Deferred} a jQuery deferred that resolves to the instance, but
* after it has been created or updated.
*/
save: function( success, error ) {
return makeRequest(this, this.isNew() ? 'create' : 'update', success, error);
},
/**
* Destroys the instance by calling
* [$.Model.destroy] with the id of the instance.
*
* @codestart
* recipe.destroy(success, error);
* @codeend
*
* If OpenAjax.hub is available, after a successful
* destroy "<i>modelName</i>.destroyed" is published
* with the model instance.
*
* @param {Function} [success] called if a successful destroy
* @param {Function} [error] called if an unsuccessful destroy
*/
destroy: function( success, error ) {
return makeRequest(this, 'destroy', success, error, 'destroyed');
},
/**
* Returns a unique identifier for the model instance. For example:
* @codestart
* new Todo({id: 5}).identity() //-> 'todo_5'
* @codeend
* Typically this is used in an element's shortName property so you can find all elements
* for a model with [$.Model.prototype.elements elements].
* @return {String}
*/
identity: function() {
var id = getId(this),
constructor = this.constructor;
return (constructor._fullName + '_' + (constructor.escapeIdentity ? encodeURIComponent(id) : id)).replace(/ /g, '_');
},
/**
* Returns elements that represent this model instance. For this to work, your element's should
* us the [$.Model.prototype.identity identity] function in their class name. Example:
*
* <div class='todo <%= todo.identity() %>'> ... </div>
*
* This also works if you hooked up the model:
*
* <div <%= todo %>> ... </div>
*
* Typically, you'll use this as a response to a Model Event:
*
* "{Todo} destroyed": function(Todo, event, todo){
* todo.elements(this.element).remove();
* }
*
*
* @param {String|jQuery|element} context If provided, only elements inside this element
* that represent this model will be returned.
*
* @return {jQuery} Returns a jQuery wrapped nodelist of elements that have this model instances
* identity in their class name.
*/
elements: function( context ) {
var id = this.identity();
if( this.constructor.escapeIdentity ) {
id = id.replace(/([ #;&,.+*~\'%:"!^$[\]()=>|\/])/g,'\\$1')
}
return $("." + id, context);
},
hookup: function( el ) {
var shortName = this.constructor._shortName,
models = $.data(el, "models") || $.data(el, "models", {});
$(el).addClass(shortName + " " + this.identity());
models[shortName] = this;
}
});
each([
/**
* @function created
* @hide
* Called by save after a new instance is created. Publishes 'created'.
* @param {Object} attrs
*/
"created",
/**
* @function updated
* @hide
* Called by save after an instance is updated. Publishes 'updated'.
* @param {Object} attrs
*/
"updated",
/**
* @function destroyed
* @hide
* Called after an instance is destroyed.
* - Publishes "shortName.destroyed".
* - Triggers a "destroyed" event on this model.
* - Removes the model from the global list if its used.
*
*/
"destroyed"], function( i, funcName ) {
$.Model.prototype[funcName] = function( attrs ) {
var stub, constructor = this.constructor;
// remove from the list if instance is destroyed
if ( funcName === 'destroyed' && constructor.list ) {
constructor.list.remove(getId(this));
}
// update attributes if attributes have been passed
stub = attrs && typeof attrs == 'object' && this.attrs(attrs.attrs ? attrs.attrs() : attrs);
// call event on the instance
trigger(this,funcName);
// call event on the instance's Class
trigger(constructor,funcName, this);
return [this].concat(makeArray(arguments)); // return like this for this.proxy chains
};
});
/**
* @add jQuery.fn
*/
// break
/**
* @function models
* Returns a list of models. If the models are of the same
* type, and have a [$.Model.List], it will return
* the models wrapped with the list.
*
* @codestart
* $(".recipes").models() //-> [recipe, ...]
* @codeend
*
* @param {jQuery.Class} [type] if present only returns models of the provided type.
* @return {Array|$.Model.List} returns an array of model instances that are represented by the contained elements.
*/
$.fn.models = function( type ) {
//get it from the data
var collection = [],
kind, ret, retType;
this.each(function() {
each($.data(this, "models") || {}, function( name, instance ) {
//either null or the list type shared by all classes
kind = kind === undefined ? instance.constructor.List || null : (instance.constructor.List === kind ? kind : null);
collection.push(instance);
});
});
ret = getList(kind);
ret.push.apply(ret, unique(collection));
return ret;
};
/**
* @function model
*
* Returns the first model instance found from [jQuery.fn.models] or
* sets the model instance on an element.
*
* //gets an instance
* ".edit click" : function(el) {
* el.closest('.todo').model().destroy()
* },
* // sets an instance
* list : function(items){
* var el = this.element;
* $.each(item, function(item){
* $('<div/>').model(item)
* .appendTo(el)
* })
* }
*
* @param {Object} [type] The type of model to return. If a model instance is provided
* it will add the model to the element.
*/
$.fn.model = function( type ) {
if ( type && type instanceof $.Model ) {
type.hookup(this[0]);
return this;
} else {
return this.models.apply(this, arguments)[0];
}
};
})(jQuery);
(function($){
var isArray = $.isArray,
propCount = function(obj){
var count = 0;
for(var prop in obj) count++;
return count;
},
same = function(a, b, deep){
var aType = typeof a,
aArray = isArray(a);
if(deep === -1){
return aType === 'object' || a === b;
}
if(aType !== typeof b || aArray !== isArray(b)){
return false;
}
if(a === b){
return true;
}
if(aArray){
if(a.length !== b.length){
return false;
}
for(var i =0; i < a.length; i ++){
if(!same(a[i],b[i])){
return false;
}
};
return true;
} else if(aType === "object" || aType === 'function'){
var count = 0;
for(var prop in a){
if(!same(a[prop],b[prop], deep === false ? -1 : undefined )){
return false;
}
count++;
}
return count === propCount(b)
}
return false;
},
flatProps = function(a){
var obj = {};
for(var prop in a){
if(typeof a[prop] !== 'object' || a[prop] === null){
obj[prop] = a[prop]
}
}
return obj;
};
/**
@page jquerymx.model.backup Backup / Restore
@parent jQuery.Model
@plugin jquery/model/backup
@test jquery/model/backup/qunit.html
@download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/backup/backup.js
You can backup and restore instance data with the jquery/model/backup
plugin.
To backup a model instance call [jQuery.Model.prototype.backup backup] like:
@codestart
var recipe = new Recipe({name: "cheese"});
recipe.backup()
@codeend
You can check if the instance is dirty with [jQuery.Model.prototype.isDirty isDirty]:
@codestart
recipe.name = 'blah'
recipe.isDirty() //-> true
@codeend
Finally, you can restore the original attributes with
[jQuery.Model.prototype.backup backup].
@codestart
recipe.restore();
recipe.name //-> "cheese"
@codeend
See this in action:
@demo jquery/model/backup/backup.html
*/
$.extend($.Model.prototype,{
/**
* @function jQuery.Model.prototype.backup
* @parent jquerymx.model.backup
* Backs up an instance of a model, so it can be restored later.
* The plugin also adds an [jQuery.Model.prototype.isDirty isDirty]
* method for checking if it is dirty.
*/
backup: function() {
this._backupStore = this.serialize();
return this;
},
/**
* @function jQuery.Model.prototype.isDirty
* @plugin jquery/model/backup
* @parent jquerymx.model.backup
* Returns if the instance needs to be saved. This will go
* through associations too.
* @return {Boolean} true if there are changes, false if otherwise
*/
isDirty: function(checkAssociations) {
// check if it serializes the same
if(!this._backupStore){
return false;
} else {
return !same(this.serialize(), this._backupStore, !!checkAssociations);
}
},
/**
* @function jQuery.Model.prototype.restore
* @parent jquery.model.backup
* restores this instance to its backup data.
* @return {model} the instance (for chaining)
*/
restore: function(restoreAssociations) {
var props = restoreAssociations ? this._backupStore : flatProps(this._backupStore)
this.attrs(props);
return this;
}
})
})(jQuery);
(function( $ ) {
var getArgs = function( args ) {
if ( args[0] && ($.isArray(args[0])) ) {
return args[0]
} else if ( args[0] instanceof $.Model.List ) {
return $.makeArray(args[0])
} else {
return $.makeArray(args)
}
},
//used for namespacing
id = 0,
getIds = function( item ) {
return item[item.constructor.id]
},
expando = jQuery.expando,
each = $.each,
ajax = $.Model._ajax,
/**
* @class jQuery.Model.List
* @parent jQuery.Model
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/list/list.js
* @test jquery/model/list/qunit.html
* @plugin jquery/model/list
*
* Model.Lists manage a lists (or arrays) of
* model instances. Similar to [jQuery.Model $.Model],
* they are used to:
*
* - create events when a list changes
* - make Ajax requests on multiple instances
* - add helper function for multiple instances (ACLs)
*
* The [todo] app demonstrates using a $.Controller to
* implement an interface for a $.Model.List.
*
* ## Creating A List Class
*
* Create a `$.Model.List [jQuery.Class class] for a $.Model
* like:
*
* $.Model('Todo')
* $.Model.List('Todo.List',{
* // static properties
* },{
* // prototype properties
* })
*
* This creates a `Todo.List` class for the `Todo`
* class. This creates some nifty magic that we will see soon.
*
* `static` properties are typically used to describe how
* a list makes requests. `prototype` properties are
* helper functions that operate on an instance of
* a list.
*
* ## Make a Helper Function
*
* Often, a user wants to select multiple items on a
* page and perform some action on them (for example,
* deleting them). The app
* needs to indicate if this is possible (for example,
* by enabling a "DELETE" button).
*
*
* If we get todo data back like:
*
* // GET /todos.json ->
* [{
* "id" : 1,
* "name" : "dishes",
* "acl" : "rwd"
* },{
* "id" : 2,
* "name" : "laundry",
* "acl" : "r"
* }, ... ]
*
* We can add a helper function to let us know if we can
* delete all the instances:
*
* $.Model.List('Todo.List',{
*
* },{
* canDelete : function(){
* return this.grep(function(todo){
* return todo.acl.indexOf("d") != 0
* }).length == this.length
* }
* })
*
* `canDelete` gets a list of all todos that have
* __d__ in their acl. If all todos have __d__,
* then `canDelete` returns true.
*
* ## Get a List Instance
*
* You can create a model list instance by using
* `new Todo.List( instances )` like:
*
* var todos = new Todo.List([
* new Todo({id: 1, name: ...}),
* new Todo({id: 2, name: ...}),
* ]);
*
* And call `canDelete` on it like:
*
* todos.canDelete() //-> boolean
*
* BUT! $.Model, [jQuery.fn.models $.fn.models], and $.Model.List are designed
* to work with each other.
*
* When you use `Todo.findAll`, it will callback with an instance
* of `Todo.List`:
*
* Todo.findAll({}, function(todos){
* todos.canDelete() //-> boolean
* })
*
* If you are adding the model instance to elements and
* retrieving them back with `$().models()`, it will
* return a instance of `Todo.List`. The following
* returns if the checked `.todo` elements are
* deletable:
*
* // get the checked inputs
* $('.todo input:checked')
* // get the todo elements
* .closest('.todo')
* // get the model list
* .models()
* // check canDelete
* .canDelete()
*
* ## Make Ajax Requests with Lists
*
* After checking if we can delete the todos,
* we should delete them from the server. Like
* `$.Model`, we can add a
* static [jQuery.Model.List.static.destroy destroy] url:
*
* $.Model.List('Todo.List',{
* destroy : 'POST /todos/delete'
* },{
* canDelete : function(){
* return this.grep(function(todo){
* return todo.acl.indexOf("d") != 0
* }).length == this.length
* }
* })
*
*
* and call [jQuery.Model.List.prototype.destroy destroy] on
* our list.
*
* // get the checked inputs
* var todos = $('.todo input:checked')
* // get the todo elements
* .closest('.todo')
* // get the model list
* .models()
*
* if( todos.canDelete() ) {
* todos.destroy()
* }
*
* By default, destroy will create an AJAX request to
* delete these instances on the server, when
* the AJAX request is successful, the instances are removed
* from the list and events are dispatched.
*
* ## Listening to events on Lists
*
* Use [jQuery.Model.List.prototype.bind bind]`(eventName, handler(event, data))`
* to listen to __add__, __remove__, and __updated__ events on a
* list.
*
* When a model instance is destroyed, it is removed from
* all lists. In the todo example, we can bind to remove to know
* when a todo has been destroyed. The following
* removes all the todo elements from the page when they are removed
* from the list:
*
* todos.bind('remove', function(ev, removedTodos){
* removedTodos.elements().remove();
* })
*
* ## Demo
*
* The following demo illustrates the previous features with
* a contacts list. Check
* multiple Contacts and click "DESTROY ALL"
*
* @demo jquery/model/list/list.html
*
* ## Other List Features
*
* - Store and retrieve multiple instances
* - Fast HTML inserts
*
* ### Store and retrieve multiple instances
*
* Once you have a collection of models, you often want to retrieve and update
* that list with new instances. Storing and retrieving is a powerful feature
* you can leverage to manage and maintain a list of models.
*
* To store a new model instance in a list...
*
* listInstance.push(new Animal({ type: dog, id: 123 }))
*
* To later retrieve that instance in your list...
*
* var animal = listInstance.get(123);
*
*
* ### Faster Inserts
*
* The 'easy' way to add a model to an element is simply inserting
* the model into the view like:
*
* @codestart xml
* &lt;div &lt;%= task %>> A task &lt;/div>
* @codeend
*
* And then you can use [jQuery.fn.models $('.task').models()].
*
* This pattern is fast enough for 90% of all widgets. But it
* does require an extra query. Lists help you avoid this.
*
* The [jQuery.Model.List.prototype.get get] method takes elements and
* uses their className to return matched instances in the list.
*
* To use get, your elements need to have the instance's
* identity in their className. So to setup a div to reprsent
* a task, you would have the following in a view:
*
* @codestart xml
* &lt;div class='task &lt;%= task.identity() %>'> A task &lt;/div>
* @codeend
*
* Then, with your model list, you could use get to get a list of
* tasks:
*
* @codestart
* taskList.get($('.task'))
* @codeend
*
* The following demonstrates how to use this technique:
*
* @demo jquery/model/list/list-insert.html
*
*/
ajaxMethods =
/**
* @static
*/
{
update: function( str ) {
/**
* @function update
* Update is used to update a set of model instances on the server. By implementing
* update along with the rest of the [jquery.model.services service api], your models provide an abstract
* API for services.
*
* The easist way to implement update is to just give it the url to put data to:
*
* $.Model.List("Recipe",{
* update: "PUT /thing/update/"
* },{})
*
* Or you can implement update manually like:
*
* $.Model.List("Thing",{
* update : function(ids, attrs, success, error){
* return $.ajax({
* url: "/thing/update/",
* success: success,
* type: "PUT",
* data: { ids: ids, attrs : attrs }
* error: error
* });
* }
* })
*
* Then you update models by calling the [jQuery.Model.List.prototype.update prototype update method].
*
* listInstance.update({ name: "Food" })
*
*
* By default, the request will PUT an array of ids to be updated and
* the changed attributes of the model instances in the body of the Ajax request.
*
* {
* ids: [5,10,20],
* attrs: {
* name: "Food"
* }
* }
*
* Your server should send back an object with any new attributes the model
* should have. For example if your server udpates the "updatedAt" property, it
* should send back something like:
*
* // PUT /recipes/4,25,20 { name: "Food" } ->
* {
* updatedAt : "10-20-2011"
* }
*
* @param {Array} ids the ids of the model instance
* @param {Object} attrs Attributes on the model instance
* @param {Function} success the callback function. It optionally accepts
* an object of attribute / value pairs of property changes the client doesn't already
* know about. For example, when you update a name property, the server might
* update other properties as well (such as updatedAt). The server should send
* these properties as the response to updates. Passing them to success will
* update the model instances with these properties.
* @param {Function} error a function to callback if something goes wrong.
*/
return function( ids, attrs, success, error ) {
return ajax(str, {
ids: ids,
attrs: attrs
}, success, error, "-updateAll", "put")
}
},
destroy: function( str ) {
/**
* @function destroy
* Destroy is used to remove a set of model instances from the server. By implementing
* destroy along with the rest of the [jquery.model.services service api], your models provide an abstract
* service API.
*
* You can implement destroy with a string like:
*
* $.Model.List("Thing",{
* destroy : "POST /thing/destroy/"
* })
*
* Or you can implement destroy manually like:
*
* $.Model.List("Thing",{
* destroy : function(ids, success, error){
* return $.ajax({
* url: "/thing/destroy/",
* data: ids,
* success: success,
* error: error,
* type: "POST"
* });
* }
* })
*
* Then you delete models by calling the [jQuery.Model.List.prototype.destroy prototype delete method].
*
* listInstance.destroy();
*
* By default, the request will POST an array of ids to be deleted in the body of the Ajax request.
*
* {
* ids: [5,10,20]
* }
*
* @param {Array} ids the ids of the instances you want destroyed
* @param {Function} success the callback function
* @param {Function} error a function to callback if something goes wrong.
*/
return function( ids, success, error ) {
return ajax(str, ids, success, error, "-destroyAll", "post")
}
}
};
$.Class("jQuery.Model.List", {
setup: function() {
for ( var name in ajaxMethods ) {
if ( typeof this[name] !== 'function' ) {
this[name] = ajaxMethods[name](this[name]);
}
}
}
},
/**
* @Prototype
*/
{
init: function( instances, noEvents ) {
this.length = 0;
// a cache for quick lookup by id
this._data = {};
//a namespace so we can remove all events bound by this list
this._namespace = ".list" + (++id), this.push.apply(this, $.makeArray(instances || []));
},
/**
* The slice method selects a part of an array, and returns another instance of this model list's class.
*
* list.slice(start, end)
*
* @param {Number} start the start index to select
* @param {Number} end the last index to select
*/
slice: function() {
return new this.Class(Array.prototype.slice.apply(this, arguments));
},
/**
* Returns a list of all instances who's property matches the given value.
*
* list.match('candy', 'snickers')
*
* @param {String} property the property to match
* @param {Object} value the value the property must equal
*/
match: function( property, value ) {
return this.grep(function( inst ) {
return inst[property] == value;
});
},
/**
* Finds the instances of the list which satisfy a callback filter function. The original array is not affected.
*
* var matchedList = list.grep(function(instanceInList, indexInArray){
* return instanceInList.date < new Date();
* });
*
* @param {Function} callback the function to call back. This function has the same call pattern as what jQuery.grep provides.
* @param {Object} args
*/
grep: function( callback, args ) {
return new this.Class($.grep(this, callback, args));
},
_makeData: function() {
var data = this._data = {};
this.each(function( i, inst ) {
data[inst[inst.constructor.id]] = inst;
})
},
/**
* Gets a list of elements by ID or element.
*
* To fetch by id:
*
* var match = list.get(23);
*
* or to fetch by element:
*
* var match = list.get($('#content')[0])
*
* @param {Object} args elements or ids to retrieve.
* @return {$.Model.List} A sub-Model.List with the elements that were queried.
*/
get: function() {
if (!this.length ) {
return new this.Class([]);
}
if ( this._changed ) {
this._makeData();
}
var list = [],
constructor = this[0].constructor,
underscored = constructor._fullName,
idName = constructor.id,
test = new RegExp(underscored + "_([^ ]+)"),
matches, val, args = getArgs(arguments);
for ( var i = 0; i < args.length; i++ ) {
if ( args[i].nodeName && (matches = args[i].className.match(test)) ) {
// If this is a dom element
val = this._data[matches[1]]
} else {
// Else an id was provided as a number or string.
val = this._data[typeof args[i] == 'string' || typeof args[i] == 'number' ? args[i] : args[i][idName]]
}
val && list.push(val)
}
return new this.Class(list)
},
/**
* Removes instances from this list by id or by an element.
*
* To remove by id:
*
* var match = list.remove(23);
*
* or to remove by element:
*
* var match = list.remove($('#content')[0])
*
* @param {Object} args elements or ids to remove.
* @return {$.Model.List} A Model.List of the elements that were removed.
*/
remove: function( args ) {
if (!this.length ) {
return [];
}
var list = [],
constructor = this[0].constructor,
underscored = constructor._fullName,
idName = constructor.id,
test = new RegExp(underscored + "_([^ ]+)"),
matches, val;
args = getArgs(arguments)
//for performance, we will go through each and splice it
var i = 0;
while ( i < this.length ) {
//check
var inst = this[i],
found = false
for ( var a = 0; a < args.length; a++ ) {
var id = (args[a].nodeName && (matches = args[a].className.match(test)) && matches[1]) || (typeof args[a] == 'string' || typeof args[a] == 'number' ? args[a] : args[a][idName]);
if ( inst[idName] == id ) {
list.push.apply(list, this.splice(i, 1));
args.splice(a, 1);
found = true;
break;
}
}
if (!found ) {
i++;
}
}
var ret = new this.Class(list);
if ( ret.length ) {
$([this]).trigger("remove", [ret])
}
return ret;
},
/**
* Returns elements that represent this list. For this to work, your element's should
* us the [jQuery.Model.prototype.identity identity] function in their class name. Example:
*
* <div class='todo <%= todo.identity() %>'> ... </div>
*
* This also works if you hooked up the model:
*
* <div <%= todo %>> ... </div>
*
* Typically, you'll use this as a response to a Model Event:
*
* "{Todo} destroyed": function(Todo, event, todo){
* todo.elements(this.element).remove();
* }
*
* @param {String|jQuery|element} context If provided, only elements inside this element that represent this model will be returned.
* @return {jQuery} Returns a jQuery wrapped nodelist of elements that have these model instances identities in their class names.
*/
elements: function( context ) {
// TODO : this can probably be done with 1 query.
return $(
this.map(function( item ) {
return "." + item.identity()
}).join(','), context);
},
model: function() {
return this.constructor.namespace
},
/**
* Finds items and adds them to this list. This uses [jQuery.Model.static.findAll]
* to find items with the params passed.
*
* @param {Object} params options to refind the returned items
* @param {Function} success called with the list
* @param {Object} error
*/
findAll: function( params, success, error ) {
var self = this;
this.model().findAll(params, function( items ) {
self.push(items);
success && success(self)
}, error)
},
/**
* Destroys all items in this list. This will use the List's
* [jQuery.Model.List.static.destroy static destroy] method.
*
* list.destroy(function(destroyedItems){
* //success
* }, function(){
* //error
* });
*
* @param {Function} success a handler called back with the destroyed items. The original list will be emptied.
* @param {Function} error a handler called back when the destroy was unsuccessful.
*/
destroy: function( success, error ) {
var ids = this.map(getIds),
items = this.slice(0, this.length);
if ( ids.length ) {
this.constructor.destroy(ids, function() {
each(items, function() {
this.destroyed();
})
success && success(items)
}, error);
} else {
success && success(this);
}
return this;
},
/**
* Updates items in the list with attributes. This makes a
* request using the list class's [jQuery.Model.List.static.update static update].
*
* list.update(function(updatedItems){
* //success
* }, function(){
* //error
* });
*
* @param {Object} attrs attributes to update the list with.
* @param {Function} success a handler called back with the updated items.
* @param {Function} error a handler called back when the update was unsuccessful.
*/
update: function( attrs, success, error ) {
var ids = this.map(getIds),
items = this.slice(0, this.length);
if ( ids.length ) {
this.constructor.update(ids, attrs, function( newAttrs ) {
// final attributes to update with
var attributes = $.extend(attrs, newAttrs || {})
each(items, function() {
this.updated(attributes);
})
success && success(items)
}, error);
} else {
success && success(this);
}
return this;
},
/**
* Listens for an events on this list. The only useful events are:
*
* . add - when new items are added
* . update - when an item is updated
* . remove - when items are removed from the list (typically because they are destroyed).
*
* ## Listen for items being added
*
* list.bind('add', function(ev, newItems){
*
* })
*
* ## Listen for items being removed
*
* list.bind('remove',function(ev, removedItems){
*
* })
*
* ## Listen for an item being updated
*
* list.bind('update',function(ev, updatedItem){
*
* })
*/
bind: function() {
if ( this[expando] === undefined ) {
this.bindings(this);
// we should probably remove destroyed models here
}
$.fn.bind.apply($([this]), arguments);
return this;
},
/**
* Unbinds an event on this list. Once all events are unbound,
* unbind stops listening to all elements in the collection.
*
* list.unbind("update") //unbinds all update events
*/
unbind: function() {
$.fn.unbind.apply($([this]), arguments);
if ( this[expando] === undefined ) {
$(this).unbind(this._namespace)
}
return this;
},
// listens to destroyed and updated on instances so when an item is
// updated - updated is called on model
// destroyed - it is removed from the list
bindings: function( items ) {
var self = this;
$(items).bind("destroyed" + this._namespace, function() {
//remove from me
self.remove(this); //triggers the remove event
}).bind("updated" + this._namespace, function() {
$([self]).trigger("updated", this)
});
},
/**
* @function push
* Adds an instance or instances to the list
*
* list.push(new Recipe({id: 5, name: "Water"}))
*
* @param args {Object} The instance(s) to push onto the list.
* @return {Number} The number of elements in the list after the new element was pushed in.
*/
push: function() {
var args = getArgs(arguments);
//listen to events on this only if someone is listening on us, this means remove won't
//be called if we aren't listening for removes
if ( this[expando] !== undefined ) {
this.bindings(args);
}
this._changed = true;
var res = push.apply(this, args)
//do this first so we could prevent?
if ( this[expando] && args.length ) {
$([this]).trigger("add", [args]);
}
return res;
},
serialize: function() {
return this.map(function( item ) {
return item.serialize()
});
}
});
var push = [].push,
modifiers = {
/**
* @function pop
* Removes the last instance of the list, and returns that instance.
*
* list.pop()
*
*/
pop: [].pop,
/**
* @function shift
* Removes the first instance of the list, and returns that instance.
*
* list.shift()
*
*/
shift: [].shift,
/**
* @function unshift
* Adds a new instance to the beginning of an array, and returns the new length.
*
* list.unshift(element1,element2,...)
*
*/
unshift: [].unshift,
/**
* @function splice
* The splice method adds and/or removes instances to/from the list, and returns the removed instance(s).
*
* list.splice(index,howmany)
*
*/
splice: [].splice,
/**
* @function sort
* Sorts the instances in the list.
*
* list.sort(sortfunc)
*
*/
sort: [].sort,
/**
* @function reverse
* Reverse the list in place
*
* list.reverse()
*
*/
reverse: [].reverse
}
each(modifiers, function( name, func ) {
$.Model.List.prototype[name] = function() {
this._changed = true;
return func.apply(this, arguments);
}
})
each([
/**
* @function each
* Iterates through the list of model instances, calling the callback function on each iteration.
*
* list.each(function(indexInList, modelOfList){
* ...
* });
*
* @param {Function} callback The function that will be executed on every object.
*/
'each',
/**
* @function map
* Iterates through the list of model instances, calling the callback function on each iteration.
*
* list.map(function(modelOfList, indexInList){
* ...
* });
*
* @param {Function} callback The function to process each item against.
*/
'map'], function( i, name ) {
$.Model.List.prototype[name] = function( callback, args ) {
return $[name](this, callback, args);
}
})
})(jQuery);
(function($){
/**
* @page jQuery.toJSON jQuery.toJSON
* @parent jquerymx.lang
*
* jQuery.toJSON( json-serializble )
*
* Converts the given argument into a JSON respresentation.
*
* If an object has a "toJSON" function, that will
* be used to get the representation.
* Non-integer/string keys are skipped in the
* object, as are keys that point to a function.
*
* json-serializble:
* The *thing* to be converted.
*/
$.toJSON = function(o, replacer, space, recurse)
{
if (typeof(JSON) == 'object' && JSON.stringify)
return JSON.stringify(o, replacer, space);
if (!recurse && $.isFunction(replacer))
o = replacer("", o);
if (typeof space == "number")
space = " ".substring(0, space);
space = (typeof space == "string") ? space.substring(0, 10) : "";
var type = typeof(o);
if (o === null)
return "null";
if (type == "undefined" || type == "function")
return undefined;
if (type == "number" || type == "boolean")
return o + "";
if (type == "string")
return $.quoteString(o);
if (type == 'object')
{
if (typeof o.toJSON == "function")
return $.toJSON( o.toJSON(), replacer, space, true );
if (o.constructor === Date)
{
var month = o.getUTCMonth() + 1;
if (month < 10) month = '0' + month;
var day = o.getUTCDate();
if (day < 10) day = '0' + day;
var year = o.getUTCFullYear();
var hours = o.getUTCHours();
if (hours < 10) hours = '0' + hours;
var minutes = o.getUTCMinutes();
if (minutes < 10) minutes = '0' + minutes;
var seconds = o.getUTCSeconds();
if (seconds < 10) seconds = '0' + seconds;
var milli = o.getUTCMilliseconds();
if (milli < 100) milli = '0' + milli;
if (milli < 10) milli = '0' + milli;
return '"' + year + '-' + month + '-' + day + 'T' +
hours + ':' + minutes + ':' + seconds +
'.' + milli + 'Z"';
}
var process = ($.isFunction(replacer)) ?
function (k, v) { return replacer(k, v); } :
function (k, v) { return v; },
nl = (space) ? "\n" : "",
sp = (space) ? " " : "";
if (o.constructor === Array)
{
var ret = [];
for (var i = 0; i < o.length; i++)
ret.push(( $.toJSON( process(i, o[i]), replacer, space, true ) || "null" ).replace(/^/gm, space));
return "[" + nl + ret.join("," + nl) + nl + "]";
}
var pairs = [], proplist;
if ($.isArray(replacer)) {
proplist = $.map(replacer, function (v) {
return (typeof v == "string" || typeof v == "number") ?
v + "" :
null;
});
}
for (var k in o) {
var name, val, type = typeof k;
if (proplist && $.inArray(k + "", proplist) == -1)
continue;
if (type == "number")
name = '"' + k + '"';
else if (type == "string")
name = $.quoteString(k);
else
continue; //skip non-string or number keys
val = $.toJSON( process(k, o[k]), replacer, space, true );
if (typeof val == "undefined")
continue; //skip pairs where the value is a function.
pairs.push((name + ":" + sp + val).replace(/^/gm, space));
}
return "{" + nl + pairs.join("," + nl) + nl + "}";
}
};
/**
* @function jQuery.evalJSON
* Evaluates a given piece of json source.
**/
$.evalJSON = function(src)
{
if (typeof(JSON) == 'object' && JSON.parse)
return JSON.parse(src);
return eval("(" + src + ")");
};
/**
* @function jQuery.secureEvalJSON
* Evals JSON in a way that is *more* secure.
**/
$.secureEvalJSON = function(src)
{
if (typeof(JSON) == 'object' && JSON.parse)
return JSON.parse(src);
var filtered = src;
filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@');
filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
if (/^[\],:{}\s]*$/.test(filtered))
return eval("(" + src + ")");
else
throw new SyntaxError("Error parsing JSON, source is not valid.");
};
/**
* @function jQuery.quoteString
*
* Returns a string-repr of a string, escaping quotes intelligently.
* Mostly a support function for toJSON.
*
* Examples:
*
* jQuery.quoteString("apple") //-> "apple"
*
* jQuery.quoteString('"Where are we going?", she asked.')
* // -> "\"Where are we going?\", she asked."
**/
$.quoteString = function(string)
{
if (string.match(_escapeable))
{
return '"' + string.replace(_escapeable, function (a)
{
var c = _meta[a];
if (typeof c === 'string') return c;
c = a.charCodeAt();
return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
}) + '"';
}
return '"' + string + '"';
};
var _escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
var _meta = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
};
})(jQuery);
(function() {
// break
/**
* @function jQuery.cookie
* @parent dom
* @plugin jquery/dom/cookie
* @author Klaus Hartl/[email protected]
*
* JavaScriptMVC's packaged cookie plugin is written by
* Klaus Hartl (stilbuero.de)<br />
* Dual licensed under the MIT and GPL licenses:<br />
* http://www.opensource.org/licenses/mit-license.php<br />
* http://www.gnu.org/licenses/gpl.html
* </p>
* <p>
* Create a cookie with the given name and value and other optional parameters.
* / Get the value of a cookie with the given name.
* </p>
* <h3>Quick Examples</h3>
*
* Set the value of a cookie.
*
* $.cookie('the_cookie', 'the_value');
*
* Create a cookie with all available options.
* @codestart
* $.cookie('the_cookie', 'the_value',
* { expires: 7, path: '/', domain: 'jquery.com', secure: true });
* @codeend
*
* Create a session cookie.
* @codestart
* $.cookie('the_cookie', 'the_value');
* @codeend
*
* Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
* used when the cookie was set.
* @codestart
* $.cookie('the_cookie', null);
* @codeend
*
* Get the value of a cookie.
* @codestart
* $.cookie('the_cookie');
* @codeend
*
*
* @param {String} [name] The name of the cookie.
* @param {String} [value] The value of the cookie.
* @param {Object} [options] An object literal containing key/value pairs to provide optional cookie attributes.<br />
* @param {Number|Date} [expires] Either an integer specifying the expiration date from now on in days or a Date object.
* If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
* If set to null or omitted, the cookie will be a session cookie and will not be retained
* when the the browser exits.<br />
* @param {String} [path] The value of the path atribute of the cookie (default: path of page that created the cookie).<br />
* @param {String} [domain] The value of the domain attribute of the cookie (default: domain of page that created the cookie).<br />
* @param {Boolean} secure If true, the secure attribute of the cookie will be set and the cookie transmission will
* require a secure protocol (like HTTPS).<br />
* @return {String} the value of the cookie or {undefined} when setting the cookie.
*/
jQuery.cookie = function(name, value, options) {
if (typeof value != 'undefined') { // name and value given, set cookie
options = options ||
{};
if (value === null) {
value = '';
options.expires = -1;
}
if (typeof value == 'object' && jQuery.toJSON) {
value = jQuery.toJSON(value);
}
var expires = '';
if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
var date;
if (typeof options.expires == 'number') {
date = new Date();
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
}
else {
date = options.expires;
}
expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
}
// CAUTION: Needed to parenthesize options.path and options.domain
// in the following expressions, otherwise they evaluate to undefined
// in the packed version for some reason...
var path = options.path ? '; path=' + (options.path) : '';
var domain = options.domain ? '; domain=' + (options.domain) : '';
var secure = options.secure ? '; secure' : '';
document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
}
else { // only name given, get cookie
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
if (jQuery.evalJSON && cookieValue && cookieValue.match(/^\s*\{/)) {
try {
cookieValue = jQuery.evalJSON(cookieValue);
}
catch (e) {
}
}
return cookieValue;
}
};
})(jQuery);
(function($){
/**
* @class jQuery.Model.List.Cookie
* @plugin jquery/model/list/cookie
* @test jquery/model/list/cookie/qunit.html
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/list/cookie/cookie.js
* @parent jQuery.Model.List
*
* Provides a store-able list of model instances. The following
* retrieves and saves a list of contacts:
*
* @codestart
* var contacts = new Contact.List([]).retrieve("contacts");
*
* // add each contact to the page
* contacts.each(function(){
addContact(this);
* });
*
* // when a new cookie is crated
* $("#contact").submit(function(ev){
* ev.preventDefault();
* var data = $(this).formParams();
*
* // gives it a random id
* data.id = +new Date();
* var contact = new Contact(data);
*
* //add it to the list of contacts
* contacts.push(contact);
*
* //store the current list
* contacts.store("contacts");
*
* //show the contact
* addContact(contact);
* })
* @codeend
*
* You can see this in action in the following demo. Create a contact, then
* refresh the page.
*
* @demo jquery/model/list/cookie/cookie.html
*/
$.Model.List("jQuery.Model.List.Cookie",
/**
* @Prototype
*/
{
days : null,
/**
* Deserializes a list of instances in the cookie with the provided name
* @param {String} name the name of the cookie to use.
* @return {jQuery.Model} returns this model instance.
*/
retrieve : function(name){
// each also needs what they are referencd by ?
var props = $.cookie( name ) || {type : null, ids : []},
instances = [],
Class = props.type ? $.String.getObject(props.type) : null;
for(var i =0; i < props.ids.length;i++){
var identity = props.ids[i],
instanceData = $.cookie( identity );
instances.push( new Class(instanceData) )
}
this.push.apply(this,instances);
return this;
},
/**
* Serializes and saves this list of model instances to the cookie in name.
* @param {String} name the name of the cookie
* @return {jQuery.Model} returns this model instance.
*/
store : function(name){
// go through and listen to instance updating
var ids = [], days = this.days;
this.each(function(i, inst){
$.cookie(inst.identity(), $.toJSON(inst.attrs()), { expires: days });
ids.push(inst.identity());
});
$.cookie(name, $.toJSON({
type: this[0] && this[0].constructor.fullName,
ids: ids
}), { expires: this.days });
return this;
}
})
})(jQuery);
(function($){
/**
* @page jQuery.toJSON jQuery.toJSON
* @parent jquerymx.lang
*
* jQuery.toJSON( json-serializble )
*
* Converts the given argument into a JSON respresentation.
*
* If an object has a "toJSON" function, that will
* be used to get the representation.
* Non-integer/string keys are skipped in the
* object, as are keys that point to a function.
*
* json-serializble:
* The *thing* to be converted.
*/
$.toJSON = function(o, replacer, space, recurse)
{
if (typeof(JSON) == 'object' && JSON.stringify)
return JSON.stringify(o, replacer, space);
if (!recurse && $.isFunction(replacer))
o = replacer("", o);
if (typeof space == "number")
space = " ".substring(0, space);
space = (typeof space == "string") ? space.substring(0, 10) : "";
var type = typeof(o);
if (o === null)
return "null";
if (type == "undefined" || type == "function")
return undefined;
if (type == "number" || type == "boolean")
return o + "";
if (type == "string")
return $.quoteString(o);
if (type == 'object')
{
if (typeof o.toJSON == "function")
return $.toJSON( o.toJSON(), replacer, space, true );
if (o.constructor === Date)
{
var month = o.getUTCMonth() + 1;
if (month < 10) month = '0' + month;
var day = o.getUTCDate();
if (day < 10) day = '0' + day;
var year = o.getUTCFullYear();
var hours = o.getUTCHours();
if (hours < 10) hours = '0' + hours;
var minutes = o.getUTCMinutes();
if (minutes < 10) minutes = '0' + minutes;
var seconds = o.getUTCSeconds();
if (seconds < 10) seconds = '0' + seconds;
var milli = o.getUTCMilliseconds();
if (milli < 100) milli = '0' + milli;
if (milli < 10) milli = '0' + milli;
return '"' + year + '-' + month + '-' + day + 'T' +
hours + ':' + minutes + ':' + seconds +
'.' + milli + 'Z"';
}
var process = ($.isFunction(replacer)) ?
function (k, v) { return replacer(k, v); } :
function (k, v) { return v; },
nl = (space) ? "\n" : "",
sp = (space) ? " " : "";
if (o.constructor === Array)
{
var ret = [];
for (var i = 0; i < o.length; i++)
ret.push(( $.toJSON( process(i, o[i]), replacer, space, true ) || "null" ).replace(/^/gm, space));
return "[" + nl + ret.join("," + nl) + nl + "]";
}
var pairs = [], proplist;
if ($.isArray(replacer)) {
proplist = $.map(replacer, function (v) {
return (typeof v == "string" || typeof v == "number") ?
v + "" :
null;
});
}
for (var k in o) {
var name, val, type = typeof k;
if (proplist && $.inArray(k + "", proplist) == -1)
continue;
if (type == "number")
name = '"' + k + '"';
else if (type == "string")
name = $.quoteString(k);
else
continue; //skip non-string or number keys
val = $.toJSON( process(k, o[k]), replacer, space, true );
if (typeof val == "undefined")
continue; //skip pairs where the value is a function.
pairs.push((name + ":" + sp + val).replace(/^/gm, space));
}
return "{" + nl + pairs.join("," + nl) + nl + "}";
}
};
/**
* @function jQuery.evalJSON
* Evaluates a given piece of json source.
**/
$.evalJSON = function(src)
{
if (typeof(JSON) == 'object' && JSON.parse)
return JSON.parse(src);
return eval("(" + src + ")");
};
/**
* @function jQuery.secureEvalJSON
* Evals JSON in a way that is *more* secure.
**/
$.secureEvalJSON = function(src)
{
if (typeof(JSON) == 'object' && JSON.parse)
return JSON.parse(src);
var filtered = src;
filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@');
filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
if (/^[\],:{}\s]*$/.test(filtered))
return eval("(" + src + ")");
else
throw new SyntaxError("Error parsing JSON, source is not valid.");
};
/**
* @function jQuery.quoteString
*
* Returns a string-repr of a string, escaping quotes intelligently.
* Mostly a support function for toJSON.
*
* Examples:
*
* jQuery.quoteString("apple") //-> "apple"
*
* jQuery.quoteString('"Where are we going?", she asked.')
* // -> "\"Where are we going?\", she asked."
**/
$.quoteString = function(string)
{
if (string.match(_escapeable))
{
return '"' + string.replace(_escapeable, function (a)
{
var c = _meta[a];
if (typeof c === 'string') return c;
c = a.charCodeAt();
return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
}) + '"';
}
return '"' + string + '"';
};
var _escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
var _meta = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
};
})(jQuery);
(function() {
// break
/**
* @function jQuery.cookie
* @parent dom
* @plugin jquery/dom/cookie
* @author Klaus Hartl/[email protected]
*
* JavaScriptMVC's packaged cookie plugin is written by
* Klaus Hartl (stilbuero.de)<br />
* Dual licensed under the MIT and GPL licenses:<br />
* http://www.opensource.org/licenses/mit-license.php<br />
* http://www.gnu.org/licenses/gpl.html
* </p>
* <p>
* Create a cookie with the given name and value and other optional parameters.
* / Get the value of a cookie with the given name.
* </p>
* <h3>Quick Examples</h3>
*
* Set the value of a cookie.
*
* $.cookie('the_cookie', 'the_value');
*
* Create a cookie with all available options.
* @codestart
* $.cookie('the_cookie', 'the_value',
* { expires: 7, path: '/', domain: 'jquery.com', secure: true });
* @codeend
*
* Create a session cookie.
* @codestart
* $.cookie('the_cookie', 'the_value');
* @codeend
*
* Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
* used when the cookie was set.
* @codestart
* $.cookie('the_cookie', null);
* @codeend
*
* Get the value of a cookie.
* @codestart
* $.cookie('the_cookie');
* @codeend
*
*
* @param {String} [name] The name of the cookie.
* @param {String} [value] The value of the cookie.
* @param {Object} [options] An object literal containing key/value pairs to provide optional cookie attributes.<br />
* @param {Number|Date} [expires] Either an integer specifying the expiration date from now on in days or a Date object.
* If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
* If set to null or omitted, the cookie will be a session cookie and will not be retained
* when the the browser exits.<br />
* @param {String} [path] The value of the path atribute of the cookie (default: path of page that created the cookie).<br />
* @param {String} [domain] The value of the domain attribute of the cookie (default: domain of page that created the cookie).<br />
* @param {Boolean} secure If true, the secure attribute of the cookie will be set and the cookie transmission will
* require a secure protocol (like HTTPS).<br />
* @return {String} the value of the cookie or {undefined} when setting the cookie.
*/
jQuery.cookie = function(name, value, options) {
if (typeof value != 'undefined') { // name and value given, set cookie
options = options ||
{};
if (value === null) {
value = '';
options.expires = -1;
}
if (typeof value == 'object' && jQuery.toJSON) {
value = jQuery.toJSON(value);
}
var expires = '';
if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
var date;
if (typeof options.expires == 'number') {
date = new Date();
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
}
else {
date = options.expires;
}
expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
}
// CAUTION: Needed to parenthesize options.path and options.domain
// in the following expressions, otherwise they evaluate to undefined
// in the packed version for some reason...
var path = options.path ? '; path=' + (options.path) : '';
var domain = options.domain ? '; domain=' + (options.domain) : '';
var secure = options.secure ? '; secure' : '';
document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
}
else { // only name given, get cookie
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
if (jQuery.evalJSON && cookieValue && cookieValue.match(/^\s*\{/)) {
try {
cookieValue = jQuery.evalJSON(cookieValue);
}
catch (e) {
}
}
return cookieValue;
}
};
})(jQuery);
(function($){
/**
* @class jQuery.Model.List.Local
* @plugin jquery/model/list/local
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/list/local/local.js
* @parent jQuery.Model.List
* Works exactly the same as [jQuery.Model.List.Cookie] except uses
* a local store instead of cookies.
*/
$.Model.List("jQuery.Model.List.Local",
{
retrieve : function(name){
// each also needs what they are referencd by ?
var props = window.localStorage[ name ] || "[]",
instances = [],
Class = props.type ? $.String.getObject(props.type) : null;
for(var i =0; i < props.ids.length;i++){
var identity = props.ids[i],
instanceData = window.localStorage[ identity ];
instances.push( new Class(instanceData) )
}
this.push.apply(this,instances);
return this;
},
store : function(name){
// go through and listen to instance updating
var ids = [], days = this.days;
this.each(function(i, inst){
window.localStorage[inst.identity()] = instance.attrs();
ids.push(inst.identity());
});
window.localStorage[name] = {
type: this[0] && this[0].constructor.fullName,
ids: ids
};
return this;
}
});
})(jQuery);
(function($){
/**
@page jquery.model.validations Validations
@plugin jquery/model/validations
@download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/validations/validations.js
@test jquery/model/validations/qunit.html
@parent jQuery.Model
In many apps, it's important to validate data before sending it to the server.
The jquery/model/validations plugin provides validations on models.
## Example
To use validations, you need to call a validate method on the Model class.
The best place to do this is in a Class's init function.
@codestart
$.Model("Contact",{
init : function(){
// validates that birthday is in the future
this.validate("birthday",function(){
if(this.birthday > new Date){
return "your birthday needs to be in the past"
}
})
}
},{});
@codeend
## Demo
Click a person's name to update their birthday. If you put the date
in the future, say the year 2525, it will report back an error.
@demo jquery/model/validations/validations.html
*/
//validations object is by property. You can have validations that
//span properties, but this way we know which ones to run.
// proc should return true if there's an error or the error message
var validate = function(attrNames, options, proc) {
if(!proc){
proc = options;
options = {};
}
options = options || {};
attrNames = $.makeArray(attrNames)
if(options.testIf && !options.testIf.call(this)){
return;
}
var self = this;
$.each(attrNames, function(i, attrName) {
// Call the validate proc function in the instance context
if(!self.validations[attrName]){
self.validations[attrName] = [];
}
self.validations[attrName].push(function(){
var res = proc.call(this, this[attrName]);
return res === undefined ? undefined : (options.message || res);
})
});
};
$.extend($.Model, {
/**
* @function jQuery.Model.static.validate
* @parent jquery.model.validations
* Validates each of the specified attributes with the given function. See [jquery.model.validations validation] for more on validations.
* @param {Array|String} attrNames Attribute name(s) to to validate
* @param {Function} validateProc Function used to validate each given attribute. Returns nothing if valid and an error message otherwise. Function is called in the instance context and takes the value to validate.
* @param {Object} options (optional) Options for the validations. Valid options include 'message' and 'testIf'.
*/
validate: validate,
/**
* @attribute jQuery.Model.static.validationMessages
* @parent jquery.model.validations
* The default validation error messages that will be returned by the builtin
* validation methods. These can be overwritten by assigning new messages
* to $.Model.validationMessages.&lt;message> in your application setup.
*
* The following messages (with defaults) are available:
*
* * format - "is invalid"
* * inclusion - "is not a valid option (perhaps out of range)"
* * lengthShort - "is too short"
* * lengthLong - "is too long"
* * presence - "can't be empty"
* * range - "is out of range"
*
* It is important to steal jquery/model/validations before
* overwriting the messages, otherwise the changes will
* be lost once steal loads it later.
*
* ## Example
*
* $.Model.validationMessages.format = "is invalid dummy!"
*/
validationMessages : {
format : "is invalid",
inclusion : "is not a valid option (perhaps out of range)",
lengthShort : "is too short",
lengthLong : "is too long",
presence : "can't be empty",
range : "is out of range"
},
/**
* @function jQuery.Model.static.validateFormatOf
* @parent jquery.model.validations
* Validates where the values of specified attributes are of the correct form by
* matching it against the regular expression provided. See [jquery.model.validations validation] for more on validations.
* @param {Array|String} attrNames Attribute name(s) to to validate
* @param {RegExp} regexp Regular expression used to match for validation
* @param {Object} options (optional) Options for the validations. Valid options include 'message' and 'testIf'.
*
*/
validateFormatOf: function(attrNames, regexp, options) {
validate.call(this, attrNames, options, function(value) {
if( (typeof value != 'undefined' && value != '')
&& String(value).match(regexp) == null )
{
return this.Class.validationMessages.format;
}
});
},
/**
* @function jQuery.Model.static.validateInclusionOf
* @parent jquery.model.validations
* Validates whether the values of the specified attributes are available in a particular
* array. See [jquery.model.validations validation] for more on validations.
* @param {Array|String} attrNames Attribute name(s) to to validate
* @param {Array} inArray Array of options to test for inclusion
* @param {Object} options (optional) Options for the validations. Valid options include 'message' and 'testIf'.
*
*/
validateInclusionOf: function(attrNames, inArray, options) {
validate.call(this, attrNames, options, function(value) {
if(typeof value == 'undefined')
return;
if($.grep(inArray, function(elm) { return (elm == value);}).length == 0)
return this.Class.validationMessages.inclusion;
});
},
/**
* @function jQuery.Model.static.validateLengthOf
* @parent jquery.model.validations
* Validates that the specified attributes' lengths are in the given range. See [jquery.model.validations validation] for more on validations.
* @param {Array|String} attrNames Attribute name(s) to to validate
* @param {Number} min Minimum length (inclusive)
* @param {Number} max Maximum length (inclusive)
* @param {Object} options (optional) Options for the validations. Valid options include 'message' and 'testIf'.
*
*/
validateLengthOf: function(attrNames, min, max, options) {
validate.call(this, attrNames, options, function(value) {
if((typeof value == 'undefined' && min > 0) || value.length < min)
return this.Class.validationMessages.lengthShort + " (min=" + min + ")";
else if(typeof value != 'undefined' && value.length > max)
return this.Class.validationMessages.lengthLong + " (max=" + max + ")";
});
},
/**
* @function jQuery.Model.static.validatePresenceOf
* @parent jquery.model.validations
* Validates that the specified attributes are not blank. See [jquery.model.validations validation] for more on validations.
* @param {Array|String} attrNames Attribute name(s) to to validate
* @param {Object} options (optional) Options for the validations. Valid options include 'message' and 'testIf'.
*
*/
validatePresenceOf: function(attrNames, options) {
validate.call(this, attrNames, options, function(value) {
if(typeof value == 'undefined' || value == "" || value === null)
return this.Class.validationMessages.presence;
});
},
/**
* @function jQuery.Model.static.validateRangeOf
* @parent jquery.model.validations
* Validates that the specified attributes are in the given numeric range. See [jquery.model.validations validation] for more on validations.
* @param {Array|String} attrNames Attribute name(s) to to validate
* @param {Number} low Minimum value (inclusive)
* @param {Number} hi Maximum value (inclusive)
* @param {Object} options (optional) Options for the validations. Valid options include 'message' and 'testIf'.
*
*/
validateRangeOf: function(attrNames, low, hi, options) {
validate.call(this, attrNames, options, function(value) {
if(typeof value != 'undefined' && value < low || value > hi)
return this.Class.validationMessages.range + " [" + low + "," + hi + "]";
});
}
});
})(jQuery);
(function( $ ) {
// a path like string into something that's ok for an element ID
var toId = function( src ) {
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
},
makeArray = $.makeArray,
// used for hookup ids
id = 1;
// this might be useful for testing if html
// htmlTest = /^[\s\n\r\xA0]*<(.|[\r\n])*>[\s\n\r\xA0]*$/
/**
* @class jQuery.View
* @parent jquerymx
* @plugin jquery/view
* @test jquery/view/qunit.html
* @download dist/jquery.view.js
*
* @description A JavaScript template framework.
*
* View provides a uniform interface for using templates with
* jQuery. When template engines [jQuery.View.register register]
* themselves, you are able to:
*
* - Use views with jQuery extensions [jQuery.fn.after after], [jQuery.fn.append append],
* [jQuery.fn.before before], [jQuery.fn.html html], [jQuery.fn.prepend prepend],
* [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text].
* - Template loading from html elements and external files.
* - Synchronous and asynchronous template loading.
* - [view.deferreds Deferred Rendering].
* - Template caching.
* - Bundling of processed templates in production builds.
* - Hookup jquery plugins directly in the template.
*
* The [mvc.view Get Started with jQueryMX] has a good walkthrough of $.View.
*
* ## Use
*
*
* When using views, you're almost always wanting to insert the results
* of a rendered template into the page. jQuery.View overwrites the
* jQuery modifiers so using a view is as easy as:
*
* $("#foo").html('mytemplate.ejs',{message: 'hello world'})
*
* This code:
*
* - Loads the template a 'mytemplate.ejs'. It might look like:
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></pre></code>
*
* - Renders it with {message: 'hello world'}, resulting in:
* <pre><code>&lt;div id='foo'>"&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* - Inserts the result into the foo element. Foo might look like:
* <pre><code>&lt;div id='foo'>&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* ## jQuery Modifiers
*
* You can use a template with the following jQuery modifiers:
*
* <table>
* <tr><td>[jQuery.fn.after after]</td><td> <code>$('#bar').after('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.append append] </td><td> <code>$('#bar').append('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.before before] </td><td> <code>$('#bar').before('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.html html] </td><td> <code>$('#bar').html('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.prepend prepend] </td><td> <code>$('#bar').prepend('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.replaceWith replaceWith] </td><td> <code>$('#bar').replaceWith('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.text text] </td><td> <code>$('#bar').text('temp.jaml',{});</code></td></tr>
* </table>
*
* You always have to pass a string and an object (or function) for the jQuery modifier
* to user a template.
*
* ## Template Locations
*
* View can load from script tags or from files.
*
* ## From Script Tags
*
* To load from a script tag, create a script tag with your template and an id like:
*
* <pre><code>&lt;script type='text/ejs' id='recipes'>
* &lt;% for(var i=0; i &lt; recipes.length; i++){ %>
* &lt;li>&lt;%=recipes[i].name %>&lt;/li>
* &lt;%} %>
* &lt;/script></code></pre>
*
* Render with this template like:
*
* @codestart
* $("#foo").html('recipes',recipeData)
* @codeend
*
* Notice we passed the id of the element we want to render.
*
* ## From File
*
* You can pass the path of a template file location like:
*
* $("#foo").html('templates/recipes.ejs',recipeData)
*
* However, you typically want to make the template work from whatever page they
* are called from. To do this, use // to look up templates from JMVC root:
*
* $("#foo").html('//app/views/recipes.ejs',recipeData)
*
* Finally, the [jQuery.Controller.prototype.view controller/view] plugin can make looking
* up a thread (and adding helpers) even easier:
*
* $("#foo").html( this.view('recipes', recipeData) )
*
* ## Packaging Templates
*
* If you're making heavy use of templates, you want to organize
* them in files so they can be reused between pages and applications.
*
* But, this organization would come at a high price
* if the browser has to
* retrieve each template individually. The additional
* HTTP requests would slow down your app.
*
* Fortunately, [steal.static.views steal.views] can build templates
* into your production files. You just have to point to the view file like:
*
* steal.views('path/to/the/view.ejs');
*
* ## Asynchronous
*
* By default, retrieving requests is done synchronously. This is
* fine because StealJS packages view templates with your JS download.
*
* However, some people might not be using StealJS or want to delay loading
* templates until necessary. If you have the need, you can
* provide a callback paramter like:
*
* $("#foo").html('recipes',recipeData, function(result){
* this.fadeIn()
* });
*
* The callback function will be called with the result of the
* rendered template and 'this' will be set to the original jQuery object.
*
* ## Deferreds (3.0.6)
*
* If you pass deferreds to $.View or any of the jQuery
* modifiers, the view will wait until all deferreds resolve before
* rendering the view. This makes it a one-liner to make a request and
* use the result to render a template.
*
* The following makes a request for todos in parallel with the
* todos.ejs template. Once todos and template have been loaded, it with
* render the view with the todos.
*
* $('#todos').html("todos.ejs",Todo.findAll());
*
* ## Just Render Templates
*
* Sometimes, you just want to get the result of a rendered
* template without inserting it, you can do this with $.View:
*
* var out = $.View('path/to/template.jaml',{});
*
* ## Preloading Templates
*
* You can preload templates asynchronously like:
*
* $.get('path/to/template.jaml',{},function(){},'view');
*
* ## Supported Template Engines
*
* JavaScriptMVC comes with the following template languages:
*
* - EmbeddedJS
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></code></pre>
*
* - JAML
* <pre><code>h2(data.message);</code></pre>
*
* - Micro
* <pre><code>&lt;h2>{%= message %}&lt;/h2></code></pre>
*
* - jQuery.Tmpl
* <pre><code>&lt;h2>${message}&lt;/h2></code></pre>
*
* The popular <a href='http://awardwinningfjords.com/2010/08/09/mustache-for-javascriptmvc-3.html'>Mustache</a>
* template engine is supported in a 2nd party plugin.
*
* ## Using other Template Engines
*
* It's easy to integrate your favorite template into $.View and Steal. Read
* how in [jQuery.View.register].
*
* @constructor
*
* Looks up a template, processes it, caches it, then renders the template
* with data and optional helpers.
*
* With [stealjs StealJS], views are typically bundled in the production build.
* This makes it ok to use views synchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",{message: "Hello World"})
* @codeend
*
* If you aren't using StealJS, it's best to use views asynchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",
* {message: "Hello World"}, function(result){
* // do something with result
* })
* @codeend
*
* @param {String} view The url or id of an element to use as the template's source.
* @param {Object} data The data to be passed to the view.
* @param {Object} [helpers] Optional helper functions the view might use. Not all
* templates support helpers.
* @param {Object} [callback] Optional callback function. If present, the template is
* retrieved asynchronously. This is a good idea if you aren't compressing the templates
* into your view.
* @return {String} The rendered result of the view or if deferreds
* are passed, a deferred that will resolve to
* the rendered result of the view.
*/
var $view = $.View = function( view, data, helpers, callback ) {
// if helpers is a function, it is actually a callback
if ( typeof helpers === 'function' ) {
callback = helpers;
helpers = undefined;
}
// see if we got passed any deferreds
var deferreds = getDeferreds(data);
if ( deferreds.length ) { // does data contain any deferreds?
// the deferred that resolves into the rendered content ...
var deferred = $.Deferred();
// add the view request to the list of deferreds
deferreds.push(get(view, true))
// wait for the view and all deferreds to finish
$.when.apply($, deferreds).then(function( resolved ) {
// get all the resolved deferreds
var objs = makeArray(arguments),
// renderer is last [0] is the data
renderer = objs.pop()[0],
// the result of the template rendering with data
result;
// make data look like the resolved deferreds
if ( isDeferred(data) ) {
data = usefulPart(resolved);
}
else {
// go through each prop in data again,
// replace the defferreds with what they resolved to
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
data[prop] = usefulPart(objs.shift());
}
}
}
// get the rendered result
result = renderer(data, helpers);
//resolve with the rendered view
deferred.resolve(result);
// if there's a callback, call it back with the result
callback && callback(result);
});
// return the deferred ....
return deferred.promise();
}
else {
// no deferreds, render this bad boy
var response,
// if there's a callback function
async = typeof callback === "function",
// get the 'view' type
deferred = get(view, async);
// if we are async,
if ( async ) {
// return the deferred
response = deferred;
// and callback callback with the rendered result
deferred.done(function( renderer ) {
callback(renderer(data, helpers))
})
} else {
// otherwise, the deferred is complete, so
// set response to the result of the rendering
deferred.done(function( renderer ) {
response = renderer(data, helpers);
});
}
return response;
}
},
// makes sure there's a template, if not, has steal provide a warning
checkText = function( text, url ) {
if (!text.match(/[^\s]/) ) {
throw "$.View ERROR: There is no template or an empty template at " + url;
}
},
// returns a 'view' renderer deferred
// url - the url to the view template
// async - if the ajax request should be synchronous
get = function( url, async ) {
return $.ajax({
url: url,
dataType: "view",
async: async
});
},
// returns true if something looks like a deferred
isDeferred = function( obj ) {
return obj && $.isFunction(obj.always) // check if obj is a $.Deferred
},
// gets an array of deferreds from an object
// this only goes one level deep
getDeferreds = function( data ) {
var deferreds = [];
// pull out deferreds
if ( isDeferred(data) ) {
return [data]
} else {
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
deferreds.push(data[prop]);
}
}
}
return deferreds;
},
// gets the useful part of deferred
// this is for Models and $.ajax that resolve to array (with success and such)
// returns the useful, content part
usefulPart = function( resolved ) {
return $.isArray(resolved) && resolved.length === 3 && resolved[1] === 'success' ? resolved[0] : resolved
};
// you can request a view renderer (a function you pass data to and get html)
// Creates a 'view' transport. These resolve to a 'view' renderer
// a 'view' renderer takes data and returns a string result.
// For example:
//
// $.ajax({dataType : 'view', src: 'foo.ejs'}).then(function(renderer){
// renderer({message: 'hello world'})
// })
$.ajaxTransport("view", function( options, orig ) {
// the url (or possibly id) of the view content
var url = orig.url,
// check if a suffix exists (ex: "foo.ejs")
suffix = url.match(/\.[\w\d]+$/),
type,
// if we are reading a script element for the content of the template
// el will be set to that script element
el,
// a unique identifier for the view (used for caching)
// this is typically derived from the element id or
// the url for the template
id,
// the AJAX request used to retrieve the template content
jqXHR,
// used to generate the response
response = function( text ) {
// get the renderer function
var func = type.renderer(id, text);
// cache if if we are caching
if ( $view.cache ) {
$view.cached[id] = func;
}
// return the objects for the response's dataTypes
// (in this case view)
return {
view: func
};
};
// if we have an inline template, derive the suffix from the 'text/???' part
// this only supports '<script></script>' tags
if ( el = document.getElementById(url) ) {
suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2];
}
// if there is no suffix, add one
if (!suffix ) {
suffix = $view.ext;
url = url + $view.ext;
}
// convert to a unique and valid id
id = toId(url);
// if a absolute path, use steal to get it
// you should only be using // if you are using steal
if ( url.match(/^\/\//) ) {
var sub = url.substr(2);
url = typeof steal === "undefined" ?
url = "/" + sub :
steal.root.mapJoin(sub) +'';
}
//set the template engine type
type = $view.types[suffix];
// return the ajax transport contract: http://api.jquery.com/extending-ajax/
return {
send: function( headers, callback ) {
// if it is cached,
if ( $view.cached[id] ) {
// return the catched renderer
return callback(200, "success", {
view: $view.cached[id]
});
// otherwise if we are getting this from a script elment
} else if ( el ) {
// resolve immediately with the element's innerHTML
callback(200, "success", response(el.innerHTML));
} else {
// make an ajax request for text
jqXHR = $.ajax({
async: orig.async,
url: url,
dataType: "text",
error: function() {
checkText("", url);
callback(404);
},
success: function( text ) {
// make sure we got some text back
checkText(text, url);
// cache and send back text
callback(200, "success", response(text))
}
});
}
},
abort: function() {
jqXHR && jqXHR.abort();
}
}
})
$.extend($view, {
/**
* @attribute hookups
* @hide
* A list of pending 'hookups'
*/
hookups: {},
/**
* @function hookup
* Registers a hookup function that can be called back after the html is
* put on the page. Typically this is handled by the template engine. Currently
* only EJS supports this functionality.
*
* var id = $.View.hookup(function(el){
* //do something with el
* }),
* html = "<div data-view-id='"+id+"'>"
* $('.foo').html(html);
*
*
* @param {Function} cb a callback function to be called with the element
* @param {Number} the hookup number
*/
hookup: function( cb ) {
var myid = ++id;
$view.hookups[myid] = cb;
return myid;
},
/**
* @attribute cached
* @hide
* Cached are put in this object
*/
cached: {},
/**
* @attribute cache
* Should the views be cached or reloaded from the server. Defaults to true.
*/
cache: true,
/**
* @function register
* Registers a template engine to be used with
* view helpers and compression.
*
* ## Example
*
* @codestart
* $.View.register({
* suffix : "tmpl",
* plugin : "jquery/view/tmpl",
* renderer: function( id, text ) {
* return function(data){
* return jQuery.render( text, data );
* }
* },
* script: function( id, text ) {
* var tmpl = $.tmpl(text).toString();
* return "function(data){return ("+
* tmpl+
* ").call(jQuery, jQuery, data); }";
* }
* })
* @codeend
* Here's what each property does:
*
* * plugin - the location of the plugin
* * suffix - files that use this suffix will be processed by this template engine
* * renderer - returns a function that will render the template provided by text
* * script - returns a string form of the processed template function.
*
* @param {Object} info a object of method and properties
*
* that enable template integration:
* <ul>
* <li>plugin - the location of the plugin. EX: 'jquery/view/ejs'</li>
* <li>suffix - the view extension. EX: 'ejs'</li>
* <li>script(id, src) - a function that returns a string that when evaluated returns a function that can be
* used as the render (i.e. have func.call(data, data, helpers) called on it).</li>
* <li>renderer(id, text) - a function that takes the id of the template and the text of the template and
* returns a render function.</li>
* </ul>
*/
register: function( info ) {
this.types["." + info.suffix] = info;
if ( window.steal ) {
steal.type(info.suffix + " view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = type.script(id, options.text)
success();
})
}
},
types: {},
/**
* @attribute ext
* The default suffix to use if none is provided in the view's url.
* This is set to .ejs by default.
*/
ext: ".ejs",
/**
* Returns the text that
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function( type, id, src ) {
return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");";
},
/**
* @hide
* Called by a production script to pre-load a renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function( id, renderer ) {
$view.cached[id] = function( data, helpers ) {
return renderer.call(data, data, helpers);
};
}
});
if ( window.steal ) {
steal.type("view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})";
success();
})
}
//---- ADD jQUERY HELPERS -----
//converts jquery functions to use views
var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs,
// text and val cannot produce an element, so don't run hookups on them
noHookup = {'val':true,'text':true};
convert = function( func_name ) {
// save the old jQuery helper
var old = $.fn[func_name];
// replace it wiht our new helper
$.fn[func_name] = function() {
var args = makeArray(arguments),
callbackNum,
callback,
self = this,
result;
// if the first arg is a deferred
// wait until it finishes, and call
// modify with the result
if ( isDeferred(args[0]) ) {
args[0].done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
//check if a template
else if ( isTemplate(args) ) {
// if we should operate async
if ((callbackNum = getCallback(args))) {
callback = args[callbackNum];
args[callbackNum] = function( result ) {
modify.call(self, [result], old);
callback.call(self, result);
};
$view.apply($view, args);
return this;
}
// call view with args (there might be deferreds)
result = $view.apply($view, args);
// if we got a string back
if (!isDeferred(result) ) {
// we are going to call the old method with that string
args = [result];
} else {
// if there is a deferred, wait until it is done before calling modify
result.done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
}
return noHookup[func_name] ? old.apply(this,args) :
modify.call(this, args, old);
};
};
// modifies the content of the element
// but also will run any hookup
modify = function( args, old ) {
var res, stub, hooks;
//check if there are new hookups
for ( var hasHookups in $view.hookups ) {
break;
}
//if there are hookups, get jQuery object
if ( hasHookups && args[0] && isHTML(args[0]) ) {
hooks = $view.hookups;
$view.hookups = {};
args[0] = $(args[0]);
}
res = old.apply(this, args);
//now hookup the hookups
if ( hooks
/* && args.length*/
) {
hookupView(args[0], hooks);
}
return res;
};
// returns true or false if the args indicate a template is being used
// $('#foo').html('/path/to/template.ejs',{data})
// in general, we want to make sure the first arg is a string
// and the second arg is data
isTemplate = function( args ) {
// save the second arg type
var secArgType = typeof args[1];
// the first arg is a string
return typeof args[0] == "string" &&
// the second arg is an object or function
(secArgType == 'object' || secArgType == 'function') &&
// but it is not a dom element
!isDOM(args[1]);
};
// returns true if the arg is a jQuery object or HTMLElement
isDOM = function(arg){
return arg.nodeType || arg.jquery
};
// returns whether the argument is some sort of HTML data
isHTML = function( arg ) {
if ( isDOM(arg) ) {
// if jQuery object or DOM node we're good
return true;
} else if ( typeof arg === "string" ) {
// if string, do a quick sanity check that we're HTML
arg = $.trim(arg);
return arg.substr(0, 1) === "<" && arg.substr(arg.length - 1, 1) === ">" && arg.length >= 3;
} else {
// don't know what you are
return false;
}
};
//returns the callback arg number if there is one (for async view use)
getCallback = function( args ) {
return typeof args[3] === 'function' ? 3 : typeof args[2] === 'function' && 2;
};
hookupView = function( els, hooks ) {
//remove all hookups
var hookupEls, len, i = 0,
id, func;
els = els.filter(function() {
return this.nodeType != 3; //filter out text nodes
})
hookupEls = els.add("[data-view-id]", els);
len = hookupEls.length;
for (; i < len; i++ ) {
if ( hookupEls[i].getAttribute && (id = hookupEls[i].getAttribute('data-view-id')) && (func = hooks[id]) ) {
func(hookupEls[i], id);
delete hooks[id];
hookupEls[i].removeAttribute('data-view-id');
}
}
//copy remaining hooks back
$.extend($view.hookups, hooks);
};
/**
* @add jQuery.fn
* @parent jQuery.View
* Called on a jQuery collection that was rendered with $.View with pending hookups. $.View can render a
* template with hookups, but not actually perform the hookup, because it returns a string without actual DOM
* elements to hook up to. So hookup performs the hookup and clears the pending hookups, preventing errors in
* future templates.
*
* @codestart
* $($.View('//views/recipes.ejs',recipeData)).hookup()
* @codeend
*/
$.fn.hookup = function() {
var hooks = $view.hookups;
$view.hookups = {};
hookupView(this, hooks);
return this;
};
/**
* @add jQuery.fn
*/
$.each([
/**
* @function prepend
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/prepend/ jQuery().prepend()]
* to render [jQuery.View] templates inserted at the beginning of each element in the set of matched elements.
*
* $('#test').prepend('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"prepend",
/**
* @function append
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/append/ jQuery().append()]
* to render [jQuery.View] templates inserted at the end of each element in the set of matched elements.
*
* $('#test').append('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"append",
/**
* @function after
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/after/ jQuery().after()]
* to render [jQuery.View] templates inserted after each element in the set of matched elements.
*
* $('#test').after('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"after",
/**
* @function before
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/before/ jQuery().before()]
* to render [jQuery.View] templates inserted before each element in the set of matched elements.
*
* $('#test').before('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"before",
/**
* @function text
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/text/ jQuery().text()]
* to render [jQuery.View] templates as the content of each matched element.
* Unlike [jQuery.fn.html] jQuery.fn.text also works with XML, escaping the provided
* string as necessary.
*
* $('#test').text('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"text",
/**
* @function html
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/html/ jQuery().html()]
* to render [jQuery.View] templates as the content of each matched element.
*
* $('#test').html('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"html",
/**
* @function replaceWith
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/replaceWith/ jQuery().replaceWith()]
* to render [jQuery.View] templates replacing each element in the set of matched elements.
*
* $('#test').replaceWith('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"replaceWith", "val"],function(i, func){
convert(func);
});
//go through helper funcs and convert
})(jQuery);
(function( $ ) {
// a path like string into something that's ok for an element ID
var toId = function( src ) {
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
},
makeArray = $.makeArray,
// used for hookup ids
id = 1;
// this might be useful for testing if html
// htmlTest = /^[\s\n\r\xA0]*<(.|[\r\n])*>[\s\n\r\xA0]*$/
/**
* @class jQuery.View
* @parent jquerymx
* @plugin jquery/view
* @test jquery/view/qunit.html
* @download dist/jquery.view.js
*
* @description A JavaScript template framework.
*
* View provides a uniform interface for using templates with
* jQuery. When template engines [jQuery.View.register register]
* themselves, you are able to:
*
* - Use views with jQuery extensions [jQuery.fn.after after], [jQuery.fn.append append],
* [jQuery.fn.before before], [jQuery.fn.html html], [jQuery.fn.prepend prepend],
* [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text].
* - Template loading from html elements and external files.
* - Synchronous and asynchronous template loading.
* - [view.deferreds Deferred Rendering].
* - Template caching.
* - Bundling of processed templates in production builds.
* - Hookup jquery plugins directly in the template.
*
* The [mvc.view Get Started with jQueryMX] has a good walkthrough of $.View.
*
* ## Use
*
*
* When using views, you're almost always wanting to insert the results
* of a rendered template into the page. jQuery.View overwrites the
* jQuery modifiers so using a view is as easy as:
*
* $("#foo").html('mytemplate.ejs',{message: 'hello world'})
*
* This code:
*
* - Loads the template a 'mytemplate.ejs'. It might look like:
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></pre></code>
*
* - Renders it with {message: 'hello world'}, resulting in:
* <pre><code>&lt;div id='foo'>"&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* - Inserts the result into the foo element. Foo might look like:
* <pre><code>&lt;div id='foo'>&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* ## jQuery Modifiers
*
* You can use a template with the following jQuery modifiers:
*
* <table>
* <tr><td>[jQuery.fn.after after]</td><td> <code>$('#bar').after('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.append append] </td><td> <code>$('#bar').append('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.before before] </td><td> <code>$('#bar').before('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.html html] </td><td> <code>$('#bar').html('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.prepend prepend] </td><td> <code>$('#bar').prepend('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.replaceWith replaceWith] </td><td> <code>$('#bar').replaceWith('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.text text] </td><td> <code>$('#bar').text('temp.jaml',{});</code></td></tr>
* </table>
*
* You always have to pass a string and an object (or function) for the jQuery modifier
* to user a template.
*
* ## Template Locations
*
* View can load from script tags or from files.
*
* ## From Script Tags
*
* To load from a script tag, create a script tag with your template and an id like:
*
* <pre><code>&lt;script type='text/ejs' id='recipes'>
* &lt;% for(var i=0; i &lt; recipes.length; i++){ %>
* &lt;li>&lt;%=recipes[i].name %>&lt;/li>
* &lt;%} %>
* &lt;/script></code></pre>
*
* Render with this template like:
*
* @codestart
* $("#foo").html('recipes',recipeData)
* @codeend
*
* Notice we passed the id of the element we want to render.
*
* ## From File
*
* You can pass the path of a template file location like:
*
* $("#foo").html('templates/recipes.ejs',recipeData)
*
* However, you typically want to make the template work from whatever page they
* are called from. To do this, use // to look up templates from JMVC root:
*
* $("#foo").html('//app/views/recipes.ejs',recipeData)
*
* Finally, the [jQuery.Controller.prototype.view controller/view] plugin can make looking
* up a thread (and adding helpers) even easier:
*
* $("#foo").html( this.view('recipes', recipeData) )
*
* ## Packaging Templates
*
* If you're making heavy use of templates, you want to organize
* them in files so they can be reused between pages and applications.
*
* But, this organization would come at a high price
* if the browser has to
* retrieve each template individually. The additional
* HTTP requests would slow down your app.
*
* Fortunately, [steal.static.views steal.views] can build templates
* into your production files. You just have to point to the view file like:
*
* steal.views('path/to/the/view.ejs');
*
* ## Asynchronous
*
* By default, retrieving requests is done synchronously. This is
* fine because StealJS packages view templates with your JS download.
*
* However, some people might not be using StealJS or want to delay loading
* templates until necessary. If you have the need, you can
* provide a callback paramter like:
*
* $("#foo").html('recipes',recipeData, function(result){
* this.fadeIn()
* });
*
* The callback function will be called with the result of the
* rendered template and 'this' will be set to the original jQuery object.
*
* ## Deferreds (3.0.6)
*
* If you pass deferreds to $.View or any of the jQuery
* modifiers, the view will wait until all deferreds resolve before
* rendering the view. This makes it a one-liner to make a request and
* use the result to render a template.
*
* The following makes a request for todos in parallel with the
* todos.ejs template. Once todos and template have been loaded, it with
* render the view with the todos.
*
* $('#todos').html("todos.ejs",Todo.findAll());
*
* ## Just Render Templates
*
* Sometimes, you just want to get the result of a rendered
* template without inserting it, you can do this with $.View:
*
* var out = $.View('path/to/template.jaml',{});
*
* ## Preloading Templates
*
* You can preload templates asynchronously like:
*
* $.get('path/to/template.jaml',{},function(){},'view');
*
* ## Supported Template Engines
*
* JavaScriptMVC comes with the following template languages:
*
* - EmbeddedJS
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></code></pre>
*
* - JAML
* <pre><code>h2(data.message);</code></pre>
*
* - Micro
* <pre><code>&lt;h2>{%= message %}&lt;/h2></code></pre>
*
* - jQuery.Tmpl
* <pre><code>&lt;h2>${message}&lt;/h2></code></pre>
*
* The popular <a href='http://awardwinningfjords.com/2010/08/09/mustache-for-javascriptmvc-3.html'>Mustache</a>
* template engine is supported in a 2nd party plugin.
*
* ## Using other Template Engines
*
* It's easy to integrate your favorite template into $.View and Steal. Read
* how in [jQuery.View.register].
*
* @constructor
*
* Looks up a template, processes it, caches it, then renders the template
* with data and optional helpers.
*
* With [stealjs StealJS], views are typically bundled in the production build.
* This makes it ok to use views synchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",{message: "Hello World"})
* @codeend
*
* If you aren't using StealJS, it's best to use views asynchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",
* {message: "Hello World"}, function(result){
* // do something with result
* })
* @codeend
*
* @param {String} view The url or id of an element to use as the template's source.
* @param {Object} data The data to be passed to the view.
* @param {Object} [helpers] Optional helper functions the view might use. Not all
* templates support helpers.
* @param {Object} [callback] Optional callback function. If present, the template is
* retrieved asynchronously. This is a good idea if you aren't compressing the templates
* into your view.
* @return {String} The rendered result of the view or if deferreds
* are passed, a deferred that will resolve to
* the rendered result of the view.
*/
var $view = $.View = function( view, data, helpers, callback ) {
// if helpers is a function, it is actually a callback
if ( typeof helpers === 'function' ) {
callback = helpers;
helpers = undefined;
}
// see if we got passed any deferreds
var deferreds = getDeferreds(data);
if ( deferreds.length ) { // does data contain any deferreds?
// the deferred that resolves into the rendered content ...
var deferred = $.Deferred();
// add the view request to the list of deferreds
deferreds.push(get(view, true))
// wait for the view and all deferreds to finish
$.when.apply($, deferreds).then(function( resolved ) {
// get all the resolved deferreds
var objs = makeArray(arguments),
// renderer is last [0] is the data
renderer = objs.pop()[0],
// the result of the template rendering with data
result;
// make data look like the resolved deferreds
if ( isDeferred(data) ) {
data = usefulPart(resolved);
}
else {
// go through each prop in data again,
// replace the defferreds with what they resolved to
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
data[prop] = usefulPart(objs.shift());
}
}
}
// get the rendered result
result = renderer(data, helpers);
//resolve with the rendered view
deferred.resolve(result);
// if there's a callback, call it back with the result
callback && callback(result);
});
// return the deferred ....
return deferred.promise();
}
else {
// no deferreds, render this bad boy
var response,
// if there's a callback function
async = typeof callback === "function",
// get the 'view' type
deferred = get(view, async);
// if we are async,
if ( async ) {
// return the deferred
response = deferred;
// and callback callback with the rendered result
deferred.done(function( renderer ) {
callback(renderer(data, helpers))
})
} else {
// otherwise, the deferred is complete, so
// set response to the result of the rendering
deferred.done(function( renderer ) {
response = renderer(data, helpers);
});
}
return response;
}
},
// makes sure there's a template, if not, has steal provide a warning
checkText = function( text, url ) {
if (!text.match(/[^\s]/) ) {
throw "$.View ERROR: There is no template or an empty template at " + url;
}
},
// returns a 'view' renderer deferred
// url - the url to the view template
// async - if the ajax request should be synchronous
get = function( url, async ) {
return $.ajax({
url: url,
dataType: "view",
async: async
});
},
// returns true if something looks like a deferred
isDeferred = function( obj ) {
return obj && $.isFunction(obj.always) // check if obj is a $.Deferred
},
// gets an array of deferreds from an object
// this only goes one level deep
getDeferreds = function( data ) {
var deferreds = [];
// pull out deferreds
if ( isDeferred(data) ) {
return [data]
} else {
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
deferreds.push(data[prop]);
}
}
}
return deferreds;
},
// gets the useful part of deferred
// this is for Models and $.ajax that resolve to array (with success and such)
// returns the useful, content part
usefulPart = function( resolved ) {
return $.isArray(resolved) && resolved.length === 3 && resolved[1] === 'success' ? resolved[0] : resolved
};
// you can request a view renderer (a function you pass data to and get html)
// Creates a 'view' transport. These resolve to a 'view' renderer
// a 'view' renderer takes data and returns a string result.
// For example:
//
// $.ajax({dataType : 'view', src: 'foo.ejs'}).then(function(renderer){
// renderer({message: 'hello world'})
// })
$.ajaxTransport("view", function( options, orig ) {
// the url (or possibly id) of the view content
var url = orig.url,
// check if a suffix exists (ex: "foo.ejs")
suffix = url.match(/\.[\w\d]+$/),
type,
// if we are reading a script element for the content of the template
// el will be set to that script element
el,
// a unique identifier for the view (used for caching)
// this is typically derived from the element id or
// the url for the template
id,
// the AJAX request used to retrieve the template content
jqXHR,
// used to generate the response
response = function( text ) {
// get the renderer function
var func = type.renderer(id, text);
// cache if if we are caching
if ( $view.cache ) {
$view.cached[id] = func;
}
// return the objects for the response's dataTypes
// (in this case view)
return {
view: func
};
};
// if we have an inline template, derive the suffix from the 'text/???' part
// this only supports '<script></script>' tags
if ( el = document.getElementById(url) ) {
suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2];
}
// if there is no suffix, add one
if (!suffix ) {
suffix = $view.ext;
url = url + $view.ext;
}
// convert to a unique and valid id
id = toId(url);
// if a absolute path, use steal to get it
// you should only be using // if you are using steal
if ( url.match(/^\/\//) ) {
var sub = url.substr(2);
url = typeof steal === "undefined" ?
url = "/" + sub :
steal.root.mapJoin(sub) +'';
}
//set the template engine type
type = $view.types[suffix];
// return the ajax transport contract: http://api.jquery.com/extending-ajax/
return {
send: function( headers, callback ) {
// if it is cached,
if ( $view.cached[id] ) {
// return the catched renderer
return callback(200, "success", {
view: $view.cached[id]
});
// otherwise if we are getting this from a script elment
} else if ( el ) {
// resolve immediately with the element's innerHTML
callback(200, "success", response(el.innerHTML));
} else {
// make an ajax request for text
jqXHR = $.ajax({
async: orig.async,
url: url,
dataType: "text",
error: function() {
checkText("", url);
callback(404);
},
success: function( text ) {
// make sure we got some text back
checkText(text, url);
// cache and send back text
callback(200, "success", response(text))
}
});
}
},
abort: function() {
jqXHR && jqXHR.abort();
}
}
})
$.extend($view, {
/**
* @attribute hookups
* @hide
* A list of pending 'hookups'
*/
hookups: {},
/**
* @function hookup
* Registers a hookup function that can be called back after the html is
* put on the page. Typically this is handled by the template engine. Currently
* only EJS supports this functionality.
*
* var id = $.View.hookup(function(el){
* //do something with el
* }),
* html = "<div data-view-id='"+id+"'>"
* $('.foo').html(html);
*
*
* @param {Function} cb a callback function to be called with the element
* @param {Number} the hookup number
*/
hookup: function( cb ) {
var myid = ++id;
$view.hookups[myid] = cb;
return myid;
},
/**
* @attribute cached
* @hide
* Cached are put in this object
*/
cached: {},
/**
* @attribute cache
* Should the views be cached or reloaded from the server. Defaults to true.
*/
cache: true,
/**
* @function register
* Registers a template engine to be used with
* view helpers and compression.
*
* ## Example
*
* @codestart
* $.View.register({
* suffix : "tmpl",
* plugin : "jquery/view/tmpl",
* renderer: function( id, text ) {
* return function(data){
* return jQuery.render( text, data );
* }
* },
* script: function( id, text ) {
* var tmpl = $.tmpl(text).toString();
* return "function(data){return ("+
* tmpl+
* ").call(jQuery, jQuery, data); }";
* }
* })
* @codeend
* Here's what each property does:
*
* * plugin - the location of the plugin
* * suffix - files that use this suffix will be processed by this template engine
* * renderer - returns a function that will render the template provided by text
* * script - returns a string form of the processed template function.
*
* @param {Object} info a object of method and properties
*
* that enable template integration:
* <ul>
* <li>plugin - the location of the plugin. EX: 'jquery/view/ejs'</li>
* <li>suffix - the view extension. EX: 'ejs'</li>
* <li>script(id, src) - a function that returns a string that when evaluated returns a function that can be
* used as the render (i.e. have func.call(data, data, helpers) called on it).</li>
* <li>renderer(id, text) - a function that takes the id of the template and the text of the template and
* returns a render function.</li>
* </ul>
*/
register: function( info ) {
this.types["." + info.suffix] = info;
if ( window.steal ) {
steal.type(info.suffix + " view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = type.script(id, options.text)
success();
})
}
},
types: {},
/**
* @attribute ext
* The default suffix to use if none is provided in the view's url.
* This is set to .ejs by default.
*/
ext: ".ejs",
/**
* Returns the text that
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function( type, id, src ) {
return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");";
},
/**
* @hide
* Called by a production script to pre-load a renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function( id, renderer ) {
$view.cached[id] = function( data, helpers ) {
return renderer.call(data, data, helpers);
};
}
});
if ( window.steal ) {
steal.type("view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})";
success();
})
}
//---- ADD jQUERY HELPERS -----
//converts jquery functions to use views
var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs,
// text and val cannot produce an element, so don't run hookups on them
noHookup = {'val':true,'text':true};
convert = function( func_name ) {
// save the old jQuery helper
var old = $.fn[func_name];
// replace it wiht our new helper
$.fn[func_name] = function() {
var args = makeArray(arguments),
callbackNum,
callback,
self = this,
result;
// if the first arg is a deferred
// wait until it finishes, and call
// modify with the result
if ( isDeferred(args[0]) ) {
args[0].done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
//check if a template
else if ( isTemplate(args) ) {
// if we should operate async
if ((callbackNum = getCallback(args))) {
callback = args[callbackNum];
args[callbackNum] = function( result ) {
modify.call(self, [result], old);
callback.call(self, result);
};
$view.apply($view, args);
return this;
}
// call view with args (there might be deferreds)
result = $view.apply($view, args);
// if we got a string back
if (!isDeferred(result) ) {
// we are going to call the old method with that string
args = [result];
} else {
// if there is a deferred, wait until it is done before calling modify
result.done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
}
return noHookup[func_name] ? old.apply(this,args) :
modify.call(this, args, old);
};
};
// modifies the content of the element
// but also will run any hookup
modify = function( args, old ) {
var res, stub, hooks;
//check if there are new hookups
for ( var hasHookups in $view.hookups ) {
break;
}
//if there are hookups, get jQuery object
if ( hasHookups && args[0] && isHTML(args[0]) ) {
hooks = $view.hookups;
$view.hookups = {};
args[0] = $(args[0]);
}
res = old.apply(this, args);
//now hookup the hookups
if ( hooks
/* && args.length*/
) {
hookupView(args[0], hooks);
}
return res;
};
// returns true or false if the args indicate a template is being used
// $('#foo').html('/path/to/template.ejs',{data})
// in general, we want to make sure the first arg is a string
// and the second arg is data
isTemplate = function( args ) {
// save the second arg type
var secArgType = typeof args[1];
// the first arg is a string
return typeof args[0] == "string" &&
// the second arg is an object or function
(secArgType == 'object' || secArgType == 'function') &&
// but it is not a dom element
!isDOM(args[1]);
};
// returns true if the arg is a jQuery object or HTMLElement
isDOM = function(arg){
return arg.nodeType || arg.jquery
};
// returns whether the argument is some sort of HTML data
isHTML = function( arg ) {
if ( isDOM(arg) ) {
// if jQuery object or DOM node we're good
return true;
} else if ( typeof arg === "string" ) {
// if string, do a quick sanity check that we're HTML
arg = $.trim(arg);
return arg.substr(0, 1) === "<" && arg.substr(arg.length - 1, 1) === ">" && arg.length >= 3;
} else {
// don't know what you are
return false;
}
};
//returns the callback arg number if there is one (for async view use)
getCallback = function( args ) {
return typeof args[3] === 'function' ? 3 : typeof args[2] === 'function' && 2;
};
hookupView = function( els, hooks ) {
//remove all hookups
var hookupEls, len, i = 0,
id, func;
els = els.filter(function() {
return this.nodeType != 3; //filter out text nodes
})
hookupEls = els.add("[data-view-id]", els);
len = hookupEls.length;
for (; i < len; i++ ) {
if ( hookupEls[i].getAttribute && (id = hookupEls[i].getAttribute('data-view-id')) && (func = hooks[id]) ) {
func(hookupEls[i], id);
delete hooks[id];
hookupEls[i].removeAttribute('data-view-id');
}
}
//copy remaining hooks back
$.extend($view.hookups, hooks);
};
/**
* @add jQuery.fn
* @parent jQuery.View
* Called on a jQuery collection that was rendered with $.View with pending hookups. $.View can render a
* template with hookups, but not actually perform the hookup, because it returns a string without actual DOM
* elements to hook up to. So hookup performs the hookup and clears the pending hookups, preventing errors in
* future templates.
*
* @codestart
* $($.View('//views/recipes.ejs',recipeData)).hookup()
* @codeend
*/
$.fn.hookup = function() {
var hooks = $view.hookups;
$view.hookups = {};
hookupView(this, hooks);
return this;
};
/**
* @add jQuery.fn
*/
$.each([
/**
* @function prepend
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/prepend/ jQuery().prepend()]
* to render [jQuery.View] templates inserted at the beginning of each element in the set of matched elements.
*
* $('#test').prepend('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"prepend",
/**
* @function append
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/append/ jQuery().append()]
* to render [jQuery.View] templates inserted at the end of each element in the set of matched elements.
*
* $('#test').append('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"append",
/**
* @function after
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/after/ jQuery().after()]
* to render [jQuery.View] templates inserted after each element in the set of matched elements.
*
* $('#test').after('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"after",
/**
* @function before
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/before/ jQuery().before()]
* to render [jQuery.View] templates inserted before each element in the set of matched elements.
*
* $('#test').before('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"before",
/**
* @function text
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/text/ jQuery().text()]
* to render [jQuery.View] templates as the content of each matched element.
* Unlike [jQuery.fn.html] jQuery.fn.text also works with XML, escaping the provided
* string as necessary.
*
* $('#test').text('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"text",
/**
* @function html
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/html/ jQuery().html()]
* to render [jQuery.View] templates as the content of each matched element.
*
* $('#test').html('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"html",
/**
* @function replaceWith
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/replaceWith/ jQuery().replaceWith()]
* to render [jQuery.View] templates replacing each element in the set of matched elements.
*
* $('#test').replaceWith('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"replaceWith", "val"],function(i, func){
convert(func);
});
//go through helper funcs and convert
})(jQuery);
(function(){
/**
* @class Jaml
* @plugin jquery/view/jaml
* @parent jQuery.View
* @author Ed Spencer (http://edspencer.net)
* Jaml is a simple JavaScript library which makes
* HTML generation easy and pleasurable.
*
* Instead of magic tags, Jaml is pure JS. It looks like:
*
* @codestart
* function(data) {
* h3(data.message);
* }
* @codeend
*
* Jaml is integrated into jQuery.View so you can use it like:
*
* @codestart
* $("#foo").html('//app/views/template.jaml',{});
* @codeend
*
* ## Use
*
* For more info check out:
*
* - [http://edspencer.net/2009/11/jaml-beautiful-html-generation-for-javascript.html introduction]
* - [http://edspencer.github.com/jaml examples]
*
*/
Jaml = function() {
return {
templates: {},
helpers : {},
/**
* Registers a template by name
* @param {String} name The name of the template
* @param {Function} template The template function
*/
register: function(name, template ) {
this.templates[name] = template;
},
/**
* Renders the given template name with an optional data object
* @param {String} name The name of the template to render
* @param {Object} data Optional data object
*/
render: function(name, data ) {
var template = this.templates[name],
renderer = new Jaml.Template(template);
return renderer.render(data);
},
/**
* Registers a helper function
* @param {String} name The name of the helper
* @param {Function} helperFn The helper function
*/
registerHelper: function(name, helperFn ) {
this.helpers[name] = helperFn;
}
};
}();
/**
* @class
* @constructor
* @param {String} tagName The tag name this node represents (e.g. 'p', 'div', etc)
*/
Jaml.Node = function(tagName) {
/**
* @attribute tagName
* @type String
* This node's current tag
*/
this.tagName = tagName;
/**
* @attribute attributes
* @type Object
* Sets of attributes on this node (e.g. 'cls', 'id', etc)
*/
this.attributes = {};
/**
* @attribute children
* @type Array
* Array of rendered child nodes that will be steald as this node's innerHTML
*/
this.children = [];
};
Jaml.Node.prototype = {
/**
* Adds attributes to this node
* @param {Object} attrs Object containing key: value pairs of node attributes
*/
setAttributes: function(attrs ) {
for (var key in attrs) {
//convert cls to class
var mappedKey = key == 'cls' ? 'class' : key;
this.attributes[mappedKey] = attrs[key];
}
},
/**
* Adds a child string to this node. This can be called as often as needed to add children to a node
* @param {String} childText The text of the child node
*/
addChild: function(childText ) {
this.children.push(childText);
},
/**
* Renders this node with its attributes and children
* @param {Number} lpad Amount of whitespace to add to the left of the string (defaults to 0)
* @return {String} The rendered node
*/
render: function(lpad ) {
lpad = lpad || 0;
var node = [],
attrs = [],
textnode = (this instanceof Jaml.TextNode),
multiline = this.multiLineTag();
for (var key in this.attributes) {
attrs.push(key + '=' + this.attributes[key]);
}
//add any left padding
if (!textnode) node.push(this.getPadding(lpad));
//open the tag
node.push("<" + this.tagName);
//add any tag attributes
for (var key in this.attributes) {
node.push(" " + key + "=\"" + this.attributes[key] + "\"");
}
if (this.isSelfClosing()) {
node.push(" />\n");
} else {
node.push(">");
if (multiline) node.push("\n");
for (var i=0; i < this.children.length; i++) {
node.push(this.children[i].render(lpad + 2));
}
if (multiline) node.push(this.getPadding(lpad));
node.push("</", this.tagName, ">\n");
}
return node.join("");
},
/**
* Returns true if this tag should be rendered with multiple newlines (e.g. if it contains child nodes)
* @return {Boolean} True to render this tag as multi-line
*/
multiLineTag: function() {
var childLength = this.children.length,
multiLine = childLength > 0;
if (childLength == 1 && this.children[0] instanceof Jaml.TextNode) multiLine = false;
return multiLine;
},
/**
* Returns a string with the given number of whitespace characters, suitable for padding
* @param {Number} amount The number of whitespace characters to add
* @return {String} A padding string
*/
getPadding: function(amount ) {
return new Array(amount + 1).join(" ");
},
/**
* Returns true if this tag should close itself (e.g. no </tag> element)
* @return {Boolean} True if this tag should close itself
*/
isSelfClosing: function() {
var selfClosing = false;
for (var i = this.selfClosingTags.length - 1; i >= 0; i--){
if (this.tagName == this.selfClosingTags[i]) selfClosing = true;
}
return selfClosing;
},
/**
* @attribute selfClosingTags
* @type Array
* An array of all tags that should be self closing
*/
selfClosingTags: ['img', 'meta', 'br', 'hr']
};
Jaml.TextNode = function(text) {
this.text = text;
};
Jaml.TextNode.prototype = {
render: function() {
return this.text;
}
};
/**
* Represents a single registered template. Templates consist of an arbitrary number
* of trees (e.g. there may be more than a single root node), and are not compiled.
* When a template is rendered its node structure is computed with any provided template
* data, culminating in one or more root nodes. The root node(s) are then joined together
* and returned as a single output string.
*
* The render process uses two dirty but necessary hacks. First, the template function is
* decompiled into a string (but is not modified), so that it can be eval'ed within the scope
* of Jaml.Template.prototype. This allows the second hack, which is the use of the 'with' keyword.
* This allows us to keep the pretty DSL-like syntax, though is not as efficient as it could be.
*/
Jaml.Template = function(tpl) {
/**
* @attribute tpl
* @type Function
* The function this template was created from
*/
this.tpl = tpl;
this.nodes = [];
};
Jaml.Template.prototype = {
/**
* Renders this template given the supplied data
* @param {Object} data Optional data object
* @return {String} The rendered HTML string
*/
render: function(data ) {
data = data || {};
//the 'data' argument can come in two flavours - array or non-array. Normalise it
//here so that it always looks like an array.
if (data.constructor.toString().indexOf("Array") == -1) {
data = [data];
}
with(this) {
for (var i=0; i < data.length; i++) {
eval("(" + this.tpl.toString() + ")(data[i])");
};
}
var roots = this.getRoots(),
output = "";
for (var i=0; i < roots.length; i++) {
output += roots[i].render();
};
return output;
},
/**
* Returns all top-level (root) nodes in this template tree.
* Templates are tree structures, but there is no guarantee that there is a
* single root node (e.g. a single DOM element that all other elements nest within)
* @return {Array} The array of root nodes
*/
getRoots: function() {
var roots = [];
for (var i=0; i < this.nodes.length; i++) {
var node = this.nodes[i];
if (node.parent == undefined) roots.push(node);
};
return roots;
},
tags: [
"html", "head", "body", "script", "meta", "title", "link", "script",
"div", "p", "span", "a", "img", "br", "hr",
"table", "tr", "th", "td", "thead", "tbody",
"ul", "ol", "li",
"dl", "dt", "dd",
"h1", "h2", "h3", "h4", "h5", "h6", "h7",
"form", "input", "label"
]
};
/**
* Adds a function for each tag onto Template's prototype
*/
(function() {
var tags = Jaml.Template.prototype.tags;
for (var i = tags.length - 1; i >= 0; i--){
var tagName = tags[i];
/**
* This function is created for each tag name and assigned to Template's
* prototype below
*/
var fn = function(tagName) {
return function(attrs) {
var node = new Jaml.Node(tagName);
var firstArgIsAttributes = (typeof attrs == 'object')
&& !(attrs instanceof Jaml.Node)
&& !(attrs instanceof Jaml.TextNode);
if (firstArgIsAttributes) node.setAttributes(attrs);
var startIndex = firstArgIsAttributes ? 1 : 0;
for (var i=startIndex; i < arguments.length; i++) {
var arg = arguments[i];
if (typeof arg == "string" || arg == undefined) {
arg = new Jaml.TextNode(arg || "");
}
if (arg instanceof Jaml.Node || arg instanceof Jaml.TextNode) {
arg.parent = node;
}
node.addChild(arg);
};
this.nodes.push(node);
return node;
};
};
Jaml.Template.prototype[tagName] = fn(tagName);
};
})();
$.View.register({
suffix : "jaml",
script: function(id, str ) {
return "((function(){ Jaml.register("+id+", "+str+"); return function(data){return Jaml.render("+id+", data)} })())"
},
renderer: function(id, text ) {
var func;
eval("func = ("+text+")");
Jaml.register(id, func);
return function(data){
return Jaml.render(id, data)
}
}
})
})(jQuery);
(function( $ ) {
// a path like string into something that's ok for an element ID
var toId = function( src ) {
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
},
makeArray = $.makeArray,
// used for hookup ids
id = 1;
// this might be useful for testing if html
// htmlTest = /^[\s\n\r\xA0]*<(.|[\r\n])*>[\s\n\r\xA0]*$/
/**
* @class jQuery.View
* @parent jquerymx
* @plugin jquery/view
* @test jquery/view/qunit.html
* @download dist/jquery.view.js
*
* @description A JavaScript template framework.
*
* View provides a uniform interface for using templates with
* jQuery. When template engines [jQuery.View.register register]
* themselves, you are able to:
*
* - Use views with jQuery extensions [jQuery.fn.after after], [jQuery.fn.append append],
* [jQuery.fn.before before], [jQuery.fn.html html], [jQuery.fn.prepend prepend],
* [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text].
* - Template loading from html elements and external files.
* - Synchronous and asynchronous template loading.
* - [view.deferreds Deferred Rendering].
* - Template caching.
* - Bundling of processed templates in production builds.
* - Hookup jquery plugins directly in the template.
*
* The [mvc.view Get Started with jQueryMX] has a good walkthrough of $.View.
*
* ## Use
*
*
* When using views, you're almost always wanting to insert the results
* of a rendered template into the page. jQuery.View overwrites the
* jQuery modifiers so using a view is as easy as:
*
* $("#foo").html('mytemplate.ejs',{message: 'hello world'})
*
* This code:
*
* - Loads the template a 'mytemplate.ejs'. It might look like:
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></pre></code>
*
* - Renders it with {message: 'hello world'}, resulting in:
* <pre><code>&lt;div id='foo'>"&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* - Inserts the result into the foo element. Foo might look like:
* <pre><code>&lt;div id='foo'>&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* ## jQuery Modifiers
*
* You can use a template with the following jQuery modifiers:
*
* <table>
* <tr><td>[jQuery.fn.after after]</td><td> <code>$('#bar').after('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.append append] </td><td> <code>$('#bar').append('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.before before] </td><td> <code>$('#bar').before('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.html html] </td><td> <code>$('#bar').html('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.prepend prepend] </td><td> <code>$('#bar').prepend('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.replaceWith replaceWith] </td><td> <code>$('#bar').replaceWith('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.text text] </td><td> <code>$('#bar').text('temp.jaml',{});</code></td></tr>
* </table>
*
* You always have to pass a string and an object (or function) for the jQuery modifier
* to user a template.
*
* ## Template Locations
*
* View can load from script tags or from files.
*
* ## From Script Tags
*
* To load from a script tag, create a script tag with your template and an id like:
*
* <pre><code>&lt;script type='text/ejs' id='recipes'>
* &lt;% for(var i=0; i &lt; recipes.length; i++){ %>
* &lt;li>&lt;%=recipes[i].name %>&lt;/li>
* &lt;%} %>
* &lt;/script></code></pre>
*
* Render with this template like:
*
* @codestart
* $("#foo").html('recipes',recipeData)
* @codeend
*
* Notice we passed the id of the element we want to render.
*
* ## From File
*
* You can pass the path of a template file location like:
*
* $("#foo").html('templates/recipes.ejs',recipeData)
*
* However, you typically want to make the template work from whatever page they
* are called from. To do this, use // to look up templates from JMVC root:
*
* $("#foo").html('//app/views/recipes.ejs',recipeData)
*
* Finally, the [jQuery.Controller.prototype.view controller/view] plugin can make looking
* up a thread (and adding helpers) even easier:
*
* $("#foo").html( this.view('recipes', recipeData) )
*
* ## Packaging Templates
*
* If you're making heavy use of templates, you want to organize
* them in files so they can be reused between pages and applications.
*
* But, this organization would come at a high price
* if the browser has to
* retrieve each template individually. The additional
* HTTP requests would slow down your app.
*
* Fortunately, [steal.static.views steal.views] can build templates
* into your production files. You just have to point to the view file like:
*
* steal.views('path/to/the/view.ejs');
*
* ## Asynchronous
*
* By default, retrieving requests is done synchronously. This is
* fine because StealJS packages view templates with your JS download.
*
* However, some people might not be using StealJS or want to delay loading
* templates until necessary. If you have the need, you can
* provide a callback paramter like:
*
* $("#foo").html('recipes',recipeData, function(result){
* this.fadeIn()
* });
*
* The callback function will be called with the result of the
* rendered template and 'this' will be set to the original jQuery object.
*
* ## Deferreds (3.0.6)
*
* If you pass deferreds to $.View or any of the jQuery
* modifiers, the view will wait until all deferreds resolve before
* rendering the view. This makes it a one-liner to make a request and
* use the result to render a template.
*
* The following makes a request for todos in parallel with the
* todos.ejs template. Once todos and template have been loaded, it with
* render the view with the todos.
*
* $('#todos').html("todos.ejs",Todo.findAll());
*
* ## Just Render Templates
*
* Sometimes, you just want to get the result of a rendered
* template without inserting it, you can do this with $.View:
*
* var out = $.View('path/to/template.jaml',{});
*
* ## Preloading Templates
*
* You can preload templates asynchronously like:
*
* $.get('path/to/template.jaml',{},function(){},'view');
*
* ## Supported Template Engines
*
* JavaScriptMVC comes with the following template languages:
*
* - EmbeddedJS
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></code></pre>
*
* - JAML
* <pre><code>h2(data.message);</code></pre>
*
* - Micro
* <pre><code>&lt;h2>{%= message %}&lt;/h2></code></pre>
*
* - jQuery.Tmpl
* <pre><code>&lt;h2>${message}&lt;/h2></code></pre>
*
* The popular <a href='http://awardwinningfjords.com/2010/08/09/mustache-for-javascriptmvc-3.html'>Mustache</a>
* template engine is supported in a 2nd party plugin.
*
* ## Using other Template Engines
*
* It's easy to integrate your favorite template into $.View and Steal. Read
* how in [jQuery.View.register].
*
* @constructor
*
* Looks up a template, processes it, caches it, then renders the template
* with data and optional helpers.
*
* With [stealjs StealJS], views are typically bundled in the production build.
* This makes it ok to use views synchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",{message: "Hello World"})
* @codeend
*
* If you aren't using StealJS, it's best to use views asynchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",
* {message: "Hello World"}, function(result){
* // do something with result
* })
* @codeend
*
* @param {String} view The url or id of an element to use as the template's source.
* @param {Object} data The data to be passed to the view.
* @param {Object} [helpers] Optional helper functions the view might use. Not all
* templates support helpers.
* @param {Object} [callback] Optional callback function. If present, the template is
* retrieved asynchronously. This is a good idea if you aren't compressing the templates
* into your view.
* @return {String} The rendered result of the view or if deferreds
* are passed, a deferred that will resolve to
* the rendered result of the view.
*/
var $view = $.View = function( view, data, helpers, callback ) {
// if helpers is a function, it is actually a callback
if ( typeof helpers === 'function' ) {
callback = helpers;
helpers = undefined;
}
// see if we got passed any deferreds
var deferreds = getDeferreds(data);
if ( deferreds.length ) { // does data contain any deferreds?
// the deferred that resolves into the rendered content ...
var deferred = $.Deferred();
// add the view request to the list of deferreds
deferreds.push(get(view, true))
// wait for the view and all deferreds to finish
$.when.apply($, deferreds).then(function( resolved ) {
// get all the resolved deferreds
var objs = makeArray(arguments),
// renderer is last [0] is the data
renderer = objs.pop()[0],
// the result of the template rendering with data
result;
// make data look like the resolved deferreds
if ( isDeferred(data) ) {
data = usefulPart(resolved);
}
else {
// go through each prop in data again,
// replace the defferreds with what they resolved to
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
data[prop] = usefulPart(objs.shift());
}
}
}
// get the rendered result
result = renderer(data, helpers);
//resolve with the rendered view
deferred.resolve(result);
// if there's a callback, call it back with the result
callback && callback(result);
});
// return the deferred ....
return deferred.promise();
}
else {
// no deferreds, render this bad boy
var response,
// if there's a callback function
async = typeof callback === "function",
// get the 'view' type
deferred = get(view, async);
// if we are async,
if ( async ) {
// return the deferred
response = deferred;
// and callback callback with the rendered result
deferred.done(function( renderer ) {
callback(renderer(data, helpers))
})
} else {
// otherwise, the deferred is complete, so
// set response to the result of the rendering
deferred.done(function( renderer ) {
response = renderer(data, helpers);
});
}
return response;
}
},
// makes sure there's a template, if not, has steal provide a warning
checkText = function( text, url ) {
if (!text.match(/[^\s]/) ) {
throw "$.View ERROR: There is no template or an empty template at " + url;
}
},
// returns a 'view' renderer deferred
// url - the url to the view template
// async - if the ajax request should be synchronous
get = function( url, async ) {
return $.ajax({
url: url,
dataType: "view",
async: async
});
},
// returns true if something looks like a deferred
isDeferred = function( obj ) {
return obj && $.isFunction(obj.always) // check if obj is a $.Deferred
},
// gets an array of deferreds from an object
// this only goes one level deep
getDeferreds = function( data ) {
var deferreds = [];
// pull out deferreds
if ( isDeferred(data) ) {
return [data]
} else {
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
deferreds.push(data[prop]);
}
}
}
return deferreds;
},
// gets the useful part of deferred
// this is for Models and $.ajax that resolve to array (with success and such)
// returns the useful, content part
usefulPart = function( resolved ) {
return $.isArray(resolved) && resolved.length === 3 && resolved[1] === 'success' ? resolved[0] : resolved
};
// you can request a view renderer (a function you pass data to and get html)
// Creates a 'view' transport. These resolve to a 'view' renderer
// a 'view' renderer takes data and returns a string result.
// For example:
//
// $.ajax({dataType : 'view', src: 'foo.ejs'}).then(function(renderer){
// renderer({message: 'hello world'})
// })
$.ajaxTransport("view", function( options, orig ) {
// the url (or possibly id) of the view content
var url = orig.url,
// check if a suffix exists (ex: "foo.ejs")
suffix = url.match(/\.[\w\d]+$/),
type,
// if we are reading a script element for the content of the template
// el will be set to that script element
el,
// a unique identifier for the view (used for caching)
// this is typically derived from the element id or
// the url for the template
id,
// the AJAX request used to retrieve the template content
jqXHR,
// used to generate the response
response = function( text ) {
// get the renderer function
var func = type.renderer(id, text);
// cache if if we are caching
if ( $view.cache ) {
$view.cached[id] = func;
}
// return the objects for the response's dataTypes
// (in this case view)
return {
view: func
};
};
// if we have an inline template, derive the suffix from the 'text/???' part
// this only supports '<script></script>' tags
if ( el = document.getElementById(url) ) {
suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2];
}
// if there is no suffix, add one
if (!suffix ) {
suffix = $view.ext;
url = url + $view.ext;
}
// convert to a unique and valid id
id = toId(url);
// if a absolute path, use steal to get it
// you should only be using // if you are using steal
if ( url.match(/^\/\//) ) {
var sub = url.substr(2);
url = typeof steal === "undefined" ?
url = "/" + sub :
steal.root.mapJoin(sub) +'';
}
//set the template engine type
type = $view.types[suffix];
// return the ajax transport contract: http://api.jquery.com/extending-ajax/
return {
send: function( headers, callback ) {
// if it is cached,
if ( $view.cached[id] ) {
// return the catched renderer
return callback(200, "success", {
view: $view.cached[id]
});
// otherwise if we are getting this from a script elment
} else if ( el ) {
// resolve immediately with the element's innerHTML
callback(200, "success", response(el.innerHTML));
} else {
// make an ajax request for text
jqXHR = $.ajax({
async: orig.async,
url: url,
dataType: "text",
error: function() {
checkText("", url);
callback(404);
},
success: function( text ) {
// make sure we got some text back
checkText(text, url);
// cache and send back text
callback(200, "success", response(text))
}
});
}
},
abort: function() {
jqXHR && jqXHR.abort();
}
}
})
$.extend($view, {
/**
* @attribute hookups
* @hide
* A list of pending 'hookups'
*/
hookups: {},
/**
* @function hookup
* Registers a hookup function that can be called back after the html is
* put on the page. Typically this is handled by the template engine. Currently
* only EJS supports this functionality.
*
* var id = $.View.hookup(function(el){
* //do something with el
* }),
* html = "<div data-view-id='"+id+"'>"
* $('.foo').html(html);
*
*
* @param {Function} cb a callback function to be called with the element
* @param {Number} the hookup number
*/
hookup: function( cb ) {
var myid = ++id;
$view.hookups[myid] = cb;
return myid;
},
/**
* @attribute cached
* @hide
* Cached are put in this object
*/
cached: {},
/**
* @attribute cache
* Should the views be cached or reloaded from the server. Defaults to true.
*/
cache: true,
/**
* @function register
* Registers a template engine to be used with
* view helpers and compression.
*
* ## Example
*
* @codestart
* $.View.register({
* suffix : "tmpl",
* plugin : "jquery/view/tmpl",
* renderer: function( id, text ) {
* return function(data){
* return jQuery.render( text, data );
* }
* },
* script: function( id, text ) {
* var tmpl = $.tmpl(text).toString();
* return "function(data){return ("+
* tmpl+
* ").call(jQuery, jQuery, data); }";
* }
* })
* @codeend
* Here's what each property does:
*
* * plugin - the location of the plugin
* * suffix - files that use this suffix will be processed by this template engine
* * renderer - returns a function that will render the template provided by text
* * script - returns a string form of the processed template function.
*
* @param {Object} info a object of method and properties
*
* that enable template integration:
* <ul>
* <li>plugin - the location of the plugin. EX: 'jquery/view/ejs'</li>
* <li>suffix - the view extension. EX: 'ejs'</li>
* <li>script(id, src) - a function that returns a string that when evaluated returns a function that can be
* used as the render (i.e. have func.call(data, data, helpers) called on it).</li>
* <li>renderer(id, text) - a function that takes the id of the template and the text of the template and
* returns a render function.</li>
* </ul>
*/
register: function( info ) {
this.types["." + info.suffix] = info;
if ( window.steal ) {
steal.type(info.suffix + " view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = type.script(id, options.text)
success();
})
}
},
types: {},
/**
* @attribute ext
* The default suffix to use if none is provided in the view's url.
* This is set to .ejs by default.
*/
ext: ".ejs",
/**
* Returns the text that
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function( type, id, src ) {
return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");";
},
/**
* @hide
* Called by a production script to pre-load a renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function( id, renderer ) {
$view.cached[id] = function( data, helpers ) {
return renderer.call(data, data, helpers);
};
}
});
if ( window.steal ) {
steal.type("view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})";
success();
})
}
//---- ADD jQUERY HELPERS -----
//converts jquery functions to use views
var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs,
// text and val cannot produce an element, so don't run hookups on them
noHookup = {'val':true,'text':true};
convert = function( func_name ) {
// save the old jQuery helper
var old = $.fn[func_name];
// replace it wiht our new helper
$.fn[func_name] = function() {
var args = makeArray(arguments),
callbackNum,
callback,
self = this,
result;
// if the first arg is a deferred
// wait until it finishes, and call
// modify with the result
if ( isDeferred(args[0]) ) {
args[0].done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
//check if a template
else if ( isTemplate(args) ) {
// if we should operate async
if ((callbackNum = getCallback(args))) {
callback = args[callbackNum];
args[callbackNum] = function( result ) {
modify.call(self, [result], old);
callback.call(self, result);
};
$view.apply($view, args);
return this;
}
// call view with args (there might be deferreds)
result = $view.apply($view, args);
// if we got a string back
if (!isDeferred(result) ) {
// we are going to call the old method with that string
args = [result];
} else {
// if there is a deferred, wait until it is done before calling modify
result.done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
}
return noHookup[func_name] ? old.apply(this,args) :
modify.call(this, args, old);
};
};
// modifies the content of the element
// but also will run any hookup
modify = function( args, old ) {
var res, stub, hooks;
//check if there are new hookups
for ( var hasHookups in $view.hookups ) {
break;
}
//if there are hookups, get jQuery object
if ( hasHookups && args[0] && isHTML(args[0]) ) {
hooks = $view.hookups;
$view.hookups = {};
args[0] = $(args[0]);
}
res = old.apply(this, args);
//now hookup the hookups
if ( hooks
/* && args.length*/
) {
hookupView(args[0], hooks);
}
return res;
};
// returns true or false if the args indicate a template is being used
// $('#foo').html('/path/to/template.ejs',{data})
// in general, we want to make sure the first arg is a string
// and the second arg is data
isTemplate = function( args ) {
// save the second arg type
var secArgType = typeof args[1];
// the first arg is a string
return typeof args[0] == "string" &&
// the second arg is an object or function
(secArgType == 'object' || secArgType == 'function') &&
// but it is not a dom element
!isDOM(args[1]);
};
// returns true if the arg is a jQuery object or HTMLElement
isDOM = function(arg){
return arg.nodeType || arg.jquery
};
// returns whether the argument is some sort of HTML data
isHTML = function( arg ) {
if ( isDOM(arg) ) {
// if jQuery object or DOM node we're good
return true;
} else if ( typeof arg === "string" ) {
// if string, do a quick sanity check that we're HTML
arg = $.trim(arg);
return arg.substr(0, 1) === "<" && arg.substr(arg.length - 1, 1) === ">" && arg.length >= 3;
} else {
// don't know what you are
return false;
}
};
//returns the callback arg number if there is one (for async view use)
getCallback = function( args ) {
return typeof args[3] === 'function' ? 3 : typeof args[2] === 'function' && 2;
};
hookupView = function( els, hooks ) {
//remove all hookups
var hookupEls, len, i = 0,
id, func;
els = els.filter(function() {
return this.nodeType != 3; //filter out text nodes
})
hookupEls = els.add("[data-view-id]", els);
len = hookupEls.length;
for (; i < len; i++ ) {
if ( hookupEls[i].getAttribute && (id = hookupEls[i].getAttribute('data-view-id')) && (func = hooks[id]) ) {
func(hookupEls[i], id);
delete hooks[id];
hookupEls[i].removeAttribute('data-view-id');
}
}
//copy remaining hooks back
$.extend($view.hookups, hooks);
};
/**
* @add jQuery.fn
* @parent jQuery.View
* Called on a jQuery collection that was rendered with $.View with pending hookups. $.View can render a
* template with hookups, but not actually perform the hookup, because it returns a string without actual DOM
* elements to hook up to. So hookup performs the hookup and clears the pending hookups, preventing errors in
* future templates.
*
* @codestart
* $($.View('//views/recipes.ejs',recipeData)).hookup()
* @codeend
*/
$.fn.hookup = function() {
var hooks = $view.hookups;
$view.hookups = {};
hookupView(this, hooks);
return this;
};
/**
* @add jQuery.fn
*/
$.each([
/**
* @function prepend
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/prepend/ jQuery().prepend()]
* to render [jQuery.View] templates inserted at the beginning of each element in the set of matched elements.
*
* $('#test').prepend('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"prepend",
/**
* @function append
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/append/ jQuery().append()]
* to render [jQuery.View] templates inserted at the end of each element in the set of matched elements.
*
* $('#test').append('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"append",
/**
* @function after
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/after/ jQuery().after()]
* to render [jQuery.View] templates inserted after each element in the set of matched elements.
*
* $('#test').after('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"after",
/**
* @function before
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/before/ jQuery().before()]
* to render [jQuery.View] templates inserted before each element in the set of matched elements.
*
* $('#test').before('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"before",
/**
* @function text
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/text/ jQuery().text()]
* to render [jQuery.View] templates as the content of each matched element.
* Unlike [jQuery.fn.html] jQuery.fn.text also works with XML, escaping the provided
* string as necessary.
*
* $('#test').text('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"text",
/**
* @function html
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/html/ jQuery().html()]
* to render [jQuery.View] templates as the content of each matched element.
*
* $('#test').html('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"html",
/**
* @function replaceWith
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/replaceWith/ jQuery().replaceWith()]
* to render [jQuery.View] templates replacing each element in the set of matched elements.
*
* $('#test').replaceWith('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"replaceWith", "val"],function(i, func){
convert(func);
});
//go through helper funcs and convert
})(jQuery);
(function(){
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
var cache = {};
/**
* @function Micro
* @parent jQuery.View
* @plugin jquery/view/micro
* A very lightweight template engine.
* Magic tags look like:
*
* @codestart
* <h3>{%= message %}</h3>
* @codeend
*
* Micro is integrated in JavaScriptMVC so
* you can use it like:
*
* @codestart
* $("#foo").html('//app/views/bar.micro',{});
* @codeend
*
* ## Pros
*
* - Very Lightweight
*
* ## Cons
*
* - Doesn't handle nested tags.
* - Doesn't handle {%= "%}" %}.
* - More difficult to debug.
* - Removes newlines and tabs.
*
* ## Use
*
* For more information on micro, see John Resig's
* [http://ejohn.org/blog/javascript-micro-templating/ write up].
*
* @param {String} str template content.
* @param {Object} data render's the template with this content.
*/
function Micro(str, data){
var body =
"var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str.replace(/[\r\t\n]/g, " ")
.replace(/'(?=[^%]*%})/g,"\t")
.split("'").join("\\'")
.split("\t").join("'")
.replace(/{%=(.+?)%}/g, "',$1,'")
.split("{%").join("');")
.split("%}").join("p.push('")+ "');}return p.join('');"
var fn = new Function("obj",body);
fn.body = body;
// Provide some basic currying to the user
return data ? fn( data ) : fn;
};
$.View.register({
suffix : "micro",
renderer: function( id, text ) {
var mt = Micro(text)
return function(data){
return mt(data)
}
},
script: function( id, str ) {
return "function(obj){"+Micro(str).body+"}";
}
})
jQuery.View.ext = ".micro"
})(jQuery);
(function( $ ) {
// a path like string into something that's ok for an element ID
var toId = function( src ) {
return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_");
},
makeArray = $.makeArray,
// used for hookup ids
id = 1;
// this might be useful for testing if html
// htmlTest = /^[\s\n\r\xA0]*<(.|[\r\n])*>[\s\n\r\xA0]*$/
/**
* @class jQuery.View
* @parent jquerymx
* @plugin jquery/view
* @test jquery/view/qunit.html
* @download dist/jquery.view.js
*
* @description A JavaScript template framework.
*
* View provides a uniform interface for using templates with
* jQuery. When template engines [jQuery.View.register register]
* themselves, you are able to:
*
* - Use views with jQuery extensions [jQuery.fn.after after], [jQuery.fn.append append],
* [jQuery.fn.before before], [jQuery.fn.html html], [jQuery.fn.prepend prepend],
* [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text].
* - Template loading from html elements and external files.
* - Synchronous and asynchronous template loading.
* - [view.deferreds Deferred Rendering].
* - Template caching.
* - Bundling of processed templates in production builds.
* - Hookup jquery plugins directly in the template.
*
* The [mvc.view Get Started with jQueryMX] has a good walkthrough of $.View.
*
* ## Use
*
*
* When using views, you're almost always wanting to insert the results
* of a rendered template into the page. jQuery.View overwrites the
* jQuery modifiers so using a view is as easy as:
*
* $("#foo").html('mytemplate.ejs',{message: 'hello world'})
*
* This code:
*
* - Loads the template a 'mytemplate.ejs'. It might look like:
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></pre></code>
*
* - Renders it with {message: 'hello world'}, resulting in:
* <pre><code>&lt;div id='foo'>"&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* - Inserts the result into the foo element. Foo might look like:
* <pre><code>&lt;div id='foo'>&lt;h2>hello world&lt;/h2>&lt;/div></pre></code>
*
* ## jQuery Modifiers
*
* You can use a template with the following jQuery modifiers:
*
* <table>
* <tr><td>[jQuery.fn.after after]</td><td> <code>$('#bar').after('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.append append] </td><td> <code>$('#bar').append('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.before before] </td><td> <code>$('#bar').before('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.html html] </td><td> <code>$('#bar').html('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.prepend prepend] </td><td> <code>$('#bar').prepend('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.replaceWith replaceWith] </td><td> <code>$('#bar').replaceWith('temp.jaml',{});</code></td></tr>
* <tr><td>[jQuery.fn.text text] </td><td> <code>$('#bar').text('temp.jaml',{});</code></td></tr>
* </table>
*
* You always have to pass a string and an object (or function) for the jQuery modifier
* to user a template.
*
* ## Template Locations
*
* View can load from script tags or from files.
*
* ## From Script Tags
*
* To load from a script tag, create a script tag with your template and an id like:
*
* <pre><code>&lt;script type='text/ejs' id='recipes'>
* &lt;% for(var i=0; i &lt; recipes.length; i++){ %>
* &lt;li>&lt;%=recipes[i].name %>&lt;/li>
* &lt;%} %>
* &lt;/script></code></pre>
*
* Render with this template like:
*
* @codestart
* $("#foo").html('recipes',recipeData)
* @codeend
*
* Notice we passed the id of the element we want to render.
*
* ## From File
*
* You can pass the path of a template file location like:
*
* $("#foo").html('templates/recipes.ejs',recipeData)
*
* However, you typically want to make the template work from whatever page they
* are called from. To do this, use // to look up templates from JMVC root:
*
* $("#foo").html('//app/views/recipes.ejs',recipeData)
*
* Finally, the [jQuery.Controller.prototype.view controller/view] plugin can make looking
* up a thread (and adding helpers) even easier:
*
* $("#foo").html( this.view('recipes', recipeData) )
*
* ## Packaging Templates
*
* If you're making heavy use of templates, you want to organize
* them in files so they can be reused between pages and applications.
*
* But, this organization would come at a high price
* if the browser has to
* retrieve each template individually. The additional
* HTTP requests would slow down your app.
*
* Fortunately, [steal.static.views steal.views] can build templates
* into your production files. You just have to point to the view file like:
*
* steal.views('path/to/the/view.ejs');
*
* ## Asynchronous
*
* By default, retrieving requests is done synchronously. This is
* fine because StealJS packages view templates with your JS download.
*
* However, some people might not be using StealJS or want to delay loading
* templates until necessary. If you have the need, you can
* provide a callback paramter like:
*
* $("#foo").html('recipes',recipeData, function(result){
* this.fadeIn()
* });
*
* The callback function will be called with the result of the
* rendered template and 'this' will be set to the original jQuery object.
*
* ## Deferreds (3.0.6)
*
* If you pass deferreds to $.View or any of the jQuery
* modifiers, the view will wait until all deferreds resolve before
* rendering the view. This makes it a one-liner to make a request and
* use the result to render a template.
*
* The following makes a request for todos in parallel with the
* todos.ejs template. Once todos and template have been loaded, it with
* render the view with the todos.
*
* $('#todos').html("todos.ejs",Todo.findAll());
*
* ## Just Render Templates
*
* Sometimes, you just want to get the result of a rendered
* template without inserting it, you can do this with $.View:
*
* var out = $.View('path/to/template.jaml',{});
*
* ## Preloading Templates
*
* You can preload templates asynchronously like:
*
* $.get('path/to/template.jaml',{},function(){},'view');
*
* ## Supported Template Engines
*
* JavaScriptMVC comes with the following template languages:
*
* - EmbeddedJS
* <pre><code>&lt;h2>&lt;%= message %>&lt;/h2></code></pre>
*
* - JAML
* <pre><code>h2(data.message);</code></pre>
*
* - Micro
* <pre><code>&lt;h2>{%= message %}&lt;/h2></code></pre>
*
* - jQuery.Tmpl
* <pre><code>&lt;h2>${message}&lt;/h2></code></pre>
*
* The popular <a href='http://awardwinningfjords.com/2010/08/09/mustache-for-javascriptmvc-3.html'>Mustache</a>
* template engine is supported in a 2nd party plugin.
*
* ## Using other Template Engines
*
* It's easy to integrate your favorite template into $.View and Steal. Read
* how in [jQuery.View.register].
*
* @constructor
*
* Looks up a template, processes it, caches it, then renders the template
* with data and optional helpers.
*
* With [stealjs StealJS], views are typically bundled in the production build.
* This makes it ok to use views synchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",{message: "Hello World"})
* @codeend
*
* If you aren't using StealJS, it's best to use views asynchronously like:
*
* @codestart
* $.View("//myplugin/views/init.ejs",
* {message: "Hello World"}, function(result){
* // do something with result
* })
* @codeend
*
* @param {String} view The url or id of an element to use as the template's source.
* @param {Object} data The data to be passed to the view.
* @param {Object} [helpers] Optional helper functions the view might use. Not all
* templates support helpers.
* @param {Object} [callback] Optional callback function. If present, the template is
* retrieved asynchronously. This is a good idea if you aren't compressing the templates
* into your view.
* @return {String} The rendered result of the view or if deferreds
* are passed, a deferred that will resolve to
* the rendered result of the view.
*/
var $view = $.View = function( view, data, helpers, callback ) {
// if helpers is a function, it is actually a callback
if ( typeof helpers === 'function' ) {
callback = helpers;
helpers = undefined;
}
// see if we got passed any deferreds
var deferreds = getDeferreds(data);
if ( deferreds.length ) { // does data contain any deferreds?
// the deferred that resolves into the rendered content ...
var deferred = $.Deferred();
// add the view request to the list of deferreds
deferreds.push(get(view, true))
// wait for the view and all deferreds to finish
$.when.apply($, deferreds).then(function( resolved ) {
// get all the resolved deferreds
var objs = makeArray(arguments),
// renderer is last [0] is the data
renderer = objs.pop()[0],
// the result of the template rendering with data
result;
// make data look like the resolved deferreds
if ( isDeferred(data) ) {
data = usefulPart(resolved);
}
else {
// go through each prop in data again,
// replace the defferreds with what they resolved to
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
data[prop] = usefulPart(objs.shift());
}
}
}
// get the rendered result
result = renderer(data, helpers);
//resolve with the rendered view
deferred.resolve(result);
// if there's a callback, call it back with the result
callback && callback(result);
});
// return the deferred ....
return deferred.promise();
}
else {
// no deferreds, render this bad boy
var response,
// if there's a callback function
async = typeof callback === "function",
// get the 'view' type
deferred = get(view, async);
// if we are async,
if ( async ) {
// return the deferred
response = deferred;
// and callback callback with the rendered result
deferred.done(function( renderer ) {
callback(renderer(data, helpers))
})
} else {
// otherwise, the deferred is complete, so
// set response to the result of the rendering
deferred.done(function( renderer ) {
response = renderer(data, helpers);
});
}
return response;
}
},
// makes sure there's a template, if not, has steal provide a warning
checkText = function( text, url ) {
if (!text.match(/[^\s]/) ) {
throw "$.View ERROR: There is no template or an empty template at " + url;
}
},
// returns a 'view' renderer deferred
// url - the url to the view template
// async - if the ajax request should be synchronous
get = function( url, async ) {
return $.ajax({
url: url,
dataType: "view",
async: async
});
},
// returns true if something looks like a deferred
isDeferred = function( obj ) {
return obj && $.isFunction(obj.always) // check if obj is a $.Deferred
},
// gets an array of deferreds from an object
// this only goes one level deep
getDeferreds = function( data ) {
var deferreds = [];
// pull out deferreds
if ( isDeferred(data) ) {
return [data]
} else {
for ( var prop in data ) {
if ( isDeferred(data[prop]) ) {
deferreds.push(data[prop]);
}
}
}
return deferreds;
},
// gets the useful part of deferred
// this is for Models and $.ajax that resolve to array (with success and such)
// returns the useful, content part
usefulPart = function( resolved ) {
return $.isArray(resolved) && resolved.length === 3 && resolved[1] === 'success' ? resolved[0] : resolved
};
// you can request a view renderer (a function you pass data to and get html)
// Creates a 'view' transport. These resolve to a 'view' renderer
// a 'view' renderer takes data and returns a string result.
// For example:
//
// $.ajax({dataType : 'view', src: 'foo.ejs'}).then(function(renderer){
// renderer({message: 'hello world'})
// })
$.ajaxTransport("view", function( options, orig ) {
// the url (or possibly id) of the view content
var url = orig.url,
// check if a suffix exists (ex: "foo.ejs")
suffix = url.match(/\.[\w\d]+$/),
type,
// if we are reading a script element for the content of the template
// el will be set to that script element
el,
// a unique identifier for the view (used for caching)
// this is typically derived from the element id or
// the url for the template
id,
// the AJAX request used to retrieve the template content
jqXHR,
// used to generate the response
response = function( text ) {
// get the renderer function
var func = type.renderer(id, text);
// cache if if we are caching
if ( $view.cache ) {
$view.cached[id] = func;
}
// return the objects for the response's dataTypes
// (in this case view)
return {
view: func
};
};
// if we have an inline template, derive the suffix from the 'text/???' part
// this only supports '<script></script>' tags
if ( el = document.getElementById(url) ) {
suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2];
}
// if there is no suffix, add one
if (!suffix ) {
suffix = $view.ext;
url = url + $view.ext;
}
// convert to a unique and valid id
id = toId(url);
// if a absolute path, use steal to get it
// you should only be using // if you are using steal
if ( url.match(/^\/\//) ) {
var sub = url.substr(2);
url = typeof steal === "undefined" ?
url = "/" + sub :
steal.root.mapJoin(sub) +'';
}
//set the template engine type
type = $view.types[suffix];
// return the ajax transport contract: http://api.jquery.com/extending-ajax/
return {
send: function( headers, callback ) {
// if it is cached,
if ( $view.cached[id] ) {
// return the catched renderer
return callback(200, "success", {
view: $view.cached[id]
});
// otherwise if we are getting this from a script elment
} else if ( el ) {
// resolve immediately with the element's innerHTML
callback(200, "success", response(el.innerHTML));
} else {
// make an ajax request for text
jqXHR = $.ajax({
async: orig.async,
url: url,
dataType: "text",
error: function() {
checkText("", url);
callback(404);
},
success: function( text ) {
// make sure we got some text back
checkText(text, url);
// cache and send back text
callback(200, "success", response(text))
}
});
}
},
abort: function() {
jqXHR && jqXHR.abort();
}
}
})
$.extend($view, {
/**
* @attribute hookups
* @hide
* A list of pending 'hookups'
*/
hookups: {},
/**
* @function hookup
* Registers a hookup function that can be called back after the html is
* put on the page. Typically this is handled by the template engine. Currently
* only EJS supports this functionality.
*
* var id = $.View.hookup(function(el){
* //do something with el
* }),
* html = "<div data-view-id='"+id+"'>"
* $('.foo').html(html);
*
*
* @param {Function} cb a callback function to be called with the element
* @param {Number} the hookup number
*/
hookup: function( cb ) {
var myid = ++id;
$view.hookups[myid] = cb;
return myid;
},
/**
* @attribute cached
* @hide
* Cached are put in this object
*/
cached: {},
/**
* @attribute cache
* Should the views be cached or reloaded from the server. Defaults to true.
*/
cache: true,
/**
* @function register
* Registers a template engine to be used with
* view helpers and compression.
*
* ## Example
*
* @codestart
* $.View.register({
* suffix : "tmpl",
* plugin : "jquery/view/tmpl",
* renderer: function( id, text ) {
* return function(data){
* return jQuery.render( text, data );
* }
* },
* script: function( id, text ) {
* var tmpl = $.tmpl(text).toString();
* return "function(data){return ("+
* tmpl+
* ").call(jQuery, jQuery, data); }";
* }
* })
* @codeend
* Here's what each property does:
*
* * plugin - the location of the plugin
* * suffix - files that use this suffix will be processed by this template engine
* * renderer - returns a function that will render the template provided by text
* * script - returns a string form of the processed template function.
*
* @param {Object} info a object of method and properties
*
* that enable template integration:
* <ul>
* <li>plugin - the location of the plugin. EX: 'jquery/view/ejs'</li>
* <li>suffix - the view extension. EX: 'ejs'</li>
* <li>script(id, src) - a function that returns a string that when evaluated returns a function that can be
* used as the render (i.e. have func.call(data, data, helpers) called on it).</li>
* <li>renderer(id, text) - a function that takes the id of the template and the text of the template and
* returns a render function.</li>
* </ul>
*/
register: function( info ) {
this.types["." + info.suffix] = info;
if ( window.steal ) {
steal.type(info.suffix + " view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = type.script(id, options.text)
success();
})
}
},
types: {},
/**
* @attribute ext
* The default suffix to use if none is provided in the view's url.
* This is set to .ejs by default.
*/
ext: ".ejs",
/**
* Returns the text that
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function( type, id, src ) {
return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");";
},
/**
* @hide
* Called by a production script to pre-load a renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function( id, renderer ) {
$view.cached[id] = function( data, helpers ) {
return renderer.call(data, data, helpers);
};
}
});
if ( window.steal ) {
steal.type("view js", function( options, success, error ) {
var type = $view.types["." + options.type],
id = toId(options.rootSrc+'');
options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})";
success();
})
}
//---- ADD jQUERY HELPERS -----
//converts jquery functions to use views
var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs,
// text and val cannot produce an element, so don't run hookups on them
noHookup = {'val':true,'text':true};
convert = function( func_name ) {
// save the old jQuery helper
var old = $.fn[func_name];
// replace it wiht our new helper
$.fn[func_name] = function() {
var args = makeArray(arguments),
callbackNum,
callback,
self = this,
result;
// if the first arg is a deferred
// wait until it finishes, and call
// modify with the result
if ( isDeferred(args[0]) ) {
args[0].done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
//check if a template
else if ( isTemplate(args) ) {
// if we should operate async
if ((callbackNum = getCallback(args))) {
callback = args[callbackNum];
args[callbackNum] = function( result ) {
modify.call(self, [result], old);
callback.call(self, result);
};
$view.apply($view, args);
return this;
}
// call view with args (there might be deferreds)
result = $view.apply($view, args);
// if we got a string back
if (!isDeferred(result) ) {
// we are going to call the old method with that string
args = [result];
} else {
// if there is a deferred, wait until it is done before calling modify
result.done(function( res ) {
modify.call(self, [res], old);
})
return this;
}
}
return noHookup[func_name] ? old.apply(this,args) :
modify.call(this, args, old);
};
};
// modifies the content of the element
// but also will run any hookup
modify = function( args, old ) {
var res, stub, hooks;
//check if there are new hookups
for ( var hasHookups in $view.hookups ) {
break;
}
//if there are hookups, get jQuery object
if ( hasHookups && args[0] && isHTML(args[0]) ) {
hooks = $view.hookups;
$view.hookups = {};
args[0] = $(args[0]);
}
res = old.apply(this, args);
//now hookup the hookups
if ( hooks
/* && args.length*/
) {
hookupView(args[0], hooks);
}
return res;
};
// returns true or false if the args indicate a template is being used
// $('#foo').html('/path/to/template.ejs',{data})
// in general, we want to make sure the first arg is a string
// and the second arg is data
isTemplate = function( args ) {
// save the second arg type
var secArgType = typeof args[1];
// the first arg is a string
return typeof args[0] == "string" &&
// the second arg is an object or function
(secArgType == 'object' || secArgType == 'function') &&
// but it is not a dom element
!isDOM(args[1]);
};
// returns true if the arg is a jQuery object or HTMLElement
isDOM = function(arg){
return arg.nodeType || arg.jquery
};
// returns whether the argument is some sort of HTML data
isHTML = function( arg ) {
if ( isDOM(arg) ) {
// if jQuery object or DOM node we're good
return true;
} else if ( typeof arg === "string" ) {
// if string, do a quick sanity check that we're HTML
arg = $.trim(arg);
return arg.substr(0, 1) === "<" && arg.substr(arg.length - 1, 1) === ">" && arg.length >= 3;
} else {
// don't know what you are
return false;
}
};
//returns the callback arg number if there is one (for async view use)
getCallback = function( args ) {
return typeof args[3] === 'function' ? 3 : typeof args[2] === 'function' && 2;
};
hookupView = function( els, hooks ) {
//remove all hookups
var hookupEls, len, i = 0,
id, func;
els = els.filter(function() {
return this.nodeType != 3; //filter out text nodes
})
hookupEls = els.add("[data-view-id]", els);
len = hookupEls.length;
for (; i < len; i++ ) {
if ( hookupEls[i].getAttribute && (id = hookupEls[i].getAttribute('data-view-id')) && (func = hooks[id]) ) {
func(hookupEls[i], id);
delete hooks[id];
hookupEls[i].removeAttribute('data-view-id');
}
}
//copy remaining hooks back
$.extend($view.hookups, hooks);
};
/**
* @add jQuery.fn
* @parent jQuery.View
* Called on a jQuery collection that was rendered with $.View with pending hookups. $.View can render a
* template with hookups, but not actually perform the hookup, because it returns a string without actual DOM
* elements to hook up to. So hookup performs the hookup and clears the pending hookups, preventing errors in
* future templates.
*
* @codestart
* $($.View('//views/recipes.ejs',recipeData)).hookup()
* @codeend
*/
$.fn.hookup = function() {
var hooks = $view.hookups;
$view.hookups = {};
hookupView(this, hooks);
return this;
};
/**
* @add jQuery.fn
*/
$.each([
/**
* @function prepend
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/prepend/ jQuery().prepend()]
* to render [jQuery.View] templates inserted at the beginning of each element in the set of matched elements.
*
* $('#test').prepend('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"prepend",
/**
* @function append
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/append/ jQuery().append()]
* to render [jQuery.View] templates inserted at the end of each element in the set of matched elements.
*
* $('#test').append('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"append",
/**
* @function after
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/after/ jQuery().after()]
* to render [jQuery.View] templates inserted after each element in the set of matched elements.
*
* $('#test').after('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"after",
/**
* @function before
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/before/ jQuery().before()]
* to render [jQuery.View] templates inserted before each element in the set of matched elements.
*
* $('#test').before('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"before",
/**
* @function text
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/text/ jQuery().text()]
* to render [jQuery.View] templates as the content of each matched element.
* Unlike [jQuery.fn.html] jQuery.fn.text also works with XML, escaping the provided
* string as necessary.
*
* $('#test').text('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"text",
/**
* @function html
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/html/ jQuery().html()]
* to render [jQuery.View] templates as the content of each matched element.
*
* $('#test').html('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"html",
/**
* @function replaceWith
* @parent jQuery.View
*
* Extending the original [http://api.jquery.com/replaceWith/ jQuery().replaceWith()]
* to render [jQuery.View] templates replacing each element in the set of matched elements.
*
* $('#test').replaceWith('path/to/template.ejs', { name : 'javascriptmvc' });
*
* @param {String|Object|Function} content A template filename or the id of a view script tag
* or a DOM element, array of elements, HTML string, or jQuery object.
* @param {Object} [data] The data to render the view with.
* If rendering a view template this parameter always has to be present
* (use the empty object initializer {} for no data).
*/
"replaceWith", "val"],function(i, func){
convert(func);
});
//go through helper funcs and convert
})(jQuery);
(function(){
// Override the DOM manipulation function
var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = [];
function newTmplItem( options, parentItem, fn, data ) {
// Returns a template item data structure for a new rendered instance of a template (a 'template item').
// The content field is a hierarchical array of strings and nested items (to be
// removed and replaced by nodes field of dom elements, once inserted in DOM).
var newItem = {
data: data || (parentItem ? parentItem.data : {}),
_wrap: parentItem ? parentItem._wrap : null,
tmpl: null,
parent: parentItem || null,
nodes: [],
calls: tiCalls,
nest: tiNest,
wrap: tiWrap,
html: tiHtml,
update: tiUpdate
};
if ( options ) {
jQuery.extend( newItem, options, { nodes: [], parent: parentItem } );
}
if ( fn ) {
// Build the hierarchical content to be used during insertion into DOM
newItem.tmpl = fn;
newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem );
newItem.key = ++itemKey;
// Keep track of new template item, until it is stored as jQuery Data on DOM element
(stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
}
return newItem;
}
// Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
jQuery.each({
appendTo: "append",
prependTo: "prepend",
insertBefore: "before",
insertAfter: "after",
replaceAll: "replaceWith"
}, function( name, original ) {
jQuery.fn[ name ] = function( selector ) {
var ret = [], insert = jQuery( selector ), elems, i, l, tmplItems,
parent = this.length === 1 && this[0].parentNode;
appendToTmplItems = newTmplItems || {};
if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
insert[ original ]( this[0] );
ret = this;
} else {
for ( i = 0, l = insert.length; i < l; i++ ) {
cloneIndex = i;
elems = (i > 0 ? this.clone(true) : this).get();
jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
ret = ret.concat( elems );
}
cloneIndex = 0;
ret = this.pushStack( ret, name, insert.selector );
}
tmplItems = appendToTmplItems;
appendToTmplItems = null;
jQuery.tmpl.complete( tmplItems );
return ret;
};
});
jQuery.fn.extend({
// Use first wrapped element as template markup.
// Return wrapped set of template items, obtained by rendering template against data.
tmpl: function( data, options, parentItem ) {
return jQuery.tmpl( this[0], data, options, parentItem );
},
// Find which rendered template item the first wrapped DOM element belongs to
tmplItem: function() {
return jQuery.tmplItem( this[0] );
},
// Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
template: function( name ) {
return jQuery.template( name, this[0] );
},
domManip: function( args, table, callback, options ) {
// This appears to be a bug in the appendTo, etc. implementation
// it should be doing .call() instead of .apply(). See #6227
if ( args[0] && args[0].nodeType ) {
var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem;
while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {}
if ( argsLength > 1 ) {
dmArgs[0] = [jQuery.makeArray( args )];
}
if ( tmplItem && cloneIndex ) {
dmArgs[2] = function( fragClone ) {
// Handler called by oldManip when rendered template has been inserted into DOM.
jQuery.tmpl.afterManip( this, fragClone, callback );
};
}
oldManip.apply( this, dmArgs );
} else {
oldManip.apply( this, arguments );
}
cloneIndex = 0;
if ( !appendToTmplItems ) {
jQuery.tmpl.complete( newTmplItems );
}
return this;
}
});
jQuery.extend({
// Return wrapped set of template items, obtained by rendering template against data.
tmpl: function( tmpl, data, options, parentItem ) {
var ret, topLevel = !parentItem;
if ( topLevel ) {
// This is a top-level tmpl call (not from a nested template using {{tmpl}})
parentItem = topTmplItem;
tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );
wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
} else if ( !tmpl ) {
// The template item is already associated with DOM - this is a refresh.
// Re-evaluate rendered template for the parentItem
tmpl = parentItem.tmpl;
newTmplItems[parentItem.key] = parentItem;
parentItem.nodes = [];
if ( parentItem.wrapped ) {
updateWrapped( parentItem, parentItem.wrapped );
}
// Rebuild, without creating a new template item
return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) ));
}
if ( !tmpl ) {
return []; // Could throw...
}
if ( typeof data === "function" ) {
data = data.call( parentItem || {} );
}
if ( options && options.wrapped ) {
updateWrapped( options, options.wrapped );
}
ret = jQuery.isArray( data ) ?
jQuery.map( data, function( dataItem ) {
return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null;
}) :
[ newTmplItem( options, parentItem, tmpl, data ) ];
return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret;
},
// Return rendered template item for an element.
tmplItem: function( elem ) {
var tmplItem;
if ( elem instanceof jQuery ) {
elem = elem[0];
}
while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}
return tmplItem || topTmplItem;
},
// Set:
// Use $.template( name, tmpl ) to cache a named template,
// where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
// Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
// Get:
// Use $.template( name ) to access a cached template.
// Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
// will return the compiled template, without adding a name reference.
// If templateString includes at least one HTML tag, $.template( templateString ) is equivalent
// to $.template( null, templateString )
template: function( name, tmpl ) {
if (tmpl) {
// Compile template and associate with name
if ( typeof tmpl === "string" ) {
// This is an HTML string being passed directly in.
tmpl = buildTmplFn( tmpl )
} else if ( tmpl instanceof jQuery ) {
tmpl = tmpl[0] || {};
}
if ( tmpl.nodeType ) {
// If this is a template block, use cached copy, or generate tmpl function and cache.
tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));
}
return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;
}
// Return named compiled template
return name ? (typeof name !== "string" ? jQuery.template( null, name ):
(jQuery.template[name] ||
// If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null;
},
encode: function( text ) {
// Do HTML encoding replacing < > & and ' and " by corresponding entities.
return ("" + text).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;");
}
});
jQuery.extend( jQuery.tmpl, {
tag: {
"tmpl": {
_default: { $2: "null" },
open: "if($notnull_1){_=_.concat($item.nest($1,$2));}"
// tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
// This means that {{tmpl foo}} treats foo as a template (which IS a function).
// Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
},
"wrap": {
_default: { $2: "null" },
open: "$item.calls(_,$1,$2);_=[];",
close: "call=$item.calls();_=call._.concat($item.wrap(call,_));"
},
"each": {
_default: { $2: "$index, $value" },
open: "if($notnull_1){$.each($1a,function($2){with(this){",
close: "}});}"
},
"if": {
open: "if(($notnull_1) && $1a){",
close: "}"
},
"else": {
_default: { $1: "true" },
open: "}else if(($notnull_1) && $1a){"
},
"html": {
// Unecoded expression evaluation.
open: "if($notnull_1){_.push($1a);}"
},
"=": {
// Encoded expression evaluation. Abbreviated form is ${}.
_default: { $1: "$data" },
open: "if($notnull_1){_.push($.encode($1a));}"
},
"!": {
// Comment tag. Skipped by parser
open: ""
}
},
// This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
complete: function( items ) {
newTmplItems = {};
},
// Call this from code which overrides domManip, or equivalent
// Manage cloning/storing template items etc.
afterManip: function afterManip( elem, fragClone, callback ) {
// Provides cloned fragment ready for fixup prior to and after insertion into DOM
var content = fragClone.nodeType === 11 ?
jQuery.makeArray(fragClone.childNodes) :
fragClone.nodeType === 1 ? [fragClone] : [];
// Return fragment to original caller (e.g. append) for DOM insertion
callback.call( elem, fragClone );
// Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
storeTmplItems( content );
cloneIndex++;
}
});
//========================== Private helper functions, used by code above ==========================
function build( tmplItem, nested, content ) {
// Convert hierarchical content into flat string array
// and finally return array of fragments ready for DOM insertion
var frag, ret = content ? jQuery.map( content, function( item ) {
return (typeof item === "string") ?
// Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
(tmplItem.key ? item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : item) :
// This is a child template item. Build nested template.
build( item, tmplItem, item._ctnt );
}) :
// If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
tmplItem;
if ( nested ) {
return ret;
}
// top-level template
ret = ret.join("");
// Support templates which have initial or final text nodes, or consist only of text
// Also support HTML entities within the HTML markup.
ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) {
frag = jQuery( middle ).get();
storeTmplItems( frag );
if ( before ) {
frag = unencode( before ).concat(frag);
}
if ( after ) {
frag = frag.concat(unencode( after ));
}
});
return frag ? frag : unencode( ret );
}
function unencode( text ) {
// Use createElement, since createTextNode will not render HTML entities correctly
var el = document.createElement( "div" );
el.innerHTML = text;
return jQuery.makeArray(el.childNodes);
}
// Generate a reusable function that will serve to render a template against data
function buildTmplFn( markup ) {
return new Function("jQuery","$item",
"var $=jQuery,call,_=[],$data=$item.data;" +
// Introduce the data as local variables using with(){}
"with($data){_.push('" +
// Convert the template into pure JavaScript
jQuery.trim(markup)
.replace( /([\\'])/g, "\\$1" )
.replace( /[\r\t\n]/g, " " )
.replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
.replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
function( all, slash, type, fnargs, target, parens, args ) {
var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect;
if ( !tag ) {
throw "Template command not found: " + type;
}
def = tag._default || [];
if ( parens && !/\w$/.test(target)) {
target += parens;
parens = "";
}
if ( target ) {
target = unescape( target );
args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : "");
// Support for target being things like a.toLowerCase();
// In that case don't call with template item as 'this' pointer. Just evaluate...
expr = parens ? (target.indexOf(".") > -1 ? target + parens : ("(" + target + ").call($item" + args)) : target;
exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
} else {
exprAutoFnDetect = expr = def.$1 || "null";
}
fnargs = unescape( fnargs );
return "');" +
tag[ slash ? "close" : "open" ]
.split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )
.split( "$1a" ).join( exprAutoFnDetect )
.split( "$1" ).join( expr )
.split( "$2" ).join( fnargs ?
fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) {
params = params ? ("," + params + ")") : (parens ? ")" : "");
return params ? ("(" + name + ").call($item" + params) : all;
})
: (def.$2||"")
) +
"_.push('";
}) +
"');}return _;"
);
}
function updateWrapped( options, wrapped ) {
// Build the wrapped content.
options._wrap = build( options, true,
// Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string.
jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()]
).join("");
}
function unescape( args ) {
return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null;
}
function outerHtml( elem ) {
var div = document.createElement("div");
div.appendChild( elem.cloneNode(true) );
return div.innerHTML;
}
// Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance.
function storeTmplItems( content ) {
var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m;
for ( i = 0, l = content.length; i < l; i++ ) {
if ( (elem = content[i]).nodeType !== 1 ) {
continue;
}
elems = elem.getElementsByTagName("*");
for ( m = elems.length - 1; m >= 0; m-- ) {
processItemKey( elems[m] );
}
processItemKey( elem );
}
function processItemKey( el ) {
var pntKey, pntNode = el, pntItem, tmplItem, key;
// Ensure that each rendered template inserted into the DOM has its own template item,
if ( (key = el.getAttribute( tmplItmAtt ))) {
while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }
if ( pntKey !== key ) {
// The next ancestor with a _tmplitem expando is on a different key than this one.
// So this is a top-level element within this template item
// Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0;
if ( !(tmplItem = newTmplItems[key]) ) {
// The item is for wrapped content, and was copied from the temporary parent wrappedItem.
tmplItem = wrappedItems[key];
tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode], null, true );
tmplItem.key = ++itemKey;
newTmplItems[itemKey] = tmplItem;
}
if ( cloneIndex ) {
cloneTmplItem( key );
}
}
el.removeAttribute( tmplItmAtt );
} else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) {
// This was a rendered element, cloned during append or appendTo etc.
// TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
cloneTmplItem( tmplItem.key );
newTmplItems[tmplItem.key] = tmplItem;
pntNode = jQuery.data( el.parentNode, "tmplItem" );
pntNode = pntNode ? pntNode.key : 0;
}
if ( tmplItem ) {
pntItem = tmplItem;
// Find the template item of the parent element.
// (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
while ( pntItem && pntItem.key != pntNode ) {
// Add this element as a top-level node for this rendered template item, as well as for any
// ancestor items between this item and the item of its parent element
pntItem.nodes.push( el );
pntItem = pntItem.parent;
}
// Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
delete tmplItem._ctnt;
delete tmplItem._wrap;
// Store template item as jQuery data on the element
jQuery.data( el, "tmplItem", tmplItem );
}
function cloneTmplItem( key ) {
key = key + keySuffix;
tmplItem = newClonedItems[key] =
(newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true ));
}
}
}
//---- Helper functions for template item ----
function tiCalls( content, tmpl, data, options ) {
if ( !content ) {
return stack.pop();
}
stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options });
}
function tiNest( tmpl, data, options ) {
// nested template, using {{tmpl}} tag
return jQuery.tmpl( jQuery.template( tmpl ), data, options, this );
}
function tiWrap( call, wrapped ) {
// nested template, using {{wrap}} tag
var options = call.options || {};
options.wrapped = wrapped;
// Apply the template, which may incorporate wrapped content,
return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item );
}
function tiHtml( filter, textOnly ) {
var wrapped = this._wrap;
return jQuery.map(
jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ),
function(e) {
return textOnly ?
e.innerText || e.textContent :
e.outerHTML || outerHtml(e);
});
}
function tiUpdate() {
var coll = this.nodes;
jQuery.tmpl( null, null, null, this).insertBefore( coll[0] );
jQuery( coll ).remove();
}
$.View.register({
suffix : "tmpl",
renderer: function( id, text ) {
var tmpl = $.template( null, text );
return function(data){
return tmpl.call($, $, {data: data}).join('');
//$(text).tmpl(data);//jQuery.render( text, data );
};
},
script: function( id, str ) {
var tmpl = $.template( null, str );
return "function(data){return ("+tmpl+").call(jQuery, jQuery, {data: data}).join(''); }";
}
})
jQuery.View.ext = ".tmpl"
})(jQuery)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment