diff --git a/.gitignore b/.gitignore index 20ffecc9..96adf415 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .tmp* -dist/* \ No newline at end of file +dist +*.swp diff --git a/README b/README index 53e1f111..f5535b09 100644 --- a/README +++ b/README @@ -10,15 +10,16 @@ A. How to get (and contribute) JMVC http://github.com/jupiterjs/steal and http://github.com/jupiterjs/jquerymx - 3. Add steal and javascriptmvc as submodules of your project... + 3. Add steal and jquerymx as submodules of your project... git submodule add git@github.com:_YOU_/steal.git steal - git submodule add git@github.com:_YOU_/javascriptmvc.git jquery + git submodule add git@github.com:_YOU_/jquerymx.git jquery - * Notice javascriptmvc is under the jquery folder + * Notice jquerymx is under the jquery folder 4. Learn a little more about submodules ... http://johnleach.co.uk/words/archives/2008/10/12/323/git-submodules-in-n-easy-steps - 5. Make changes in steal or jmvc, and push them back to your fork. + 5. Make changes in steal or jquerymx, and push them back to your fork. 6. Make a pull request to your fork. + diff --git a/build.js b/build.js index 89aeb6dd..1d840746 100644 --- a/build.js +++ b/build.js @@ -1,8 +1,10 @@ -load('steal/rhino/steal.js') +// load('jquery/build.js') + +load('steal/rhino/rhino.js') var i, fileName, cmd, plugins = [ - "class", + "class" , "controller", { plugin: "controller/subscribe", @@ -14,6 +16,8 @@ var i, fileName, cmd, "event/default", "event/destroyed", "event/drag", + "event/pause", + "event/resize", { plugin: "event/drag/limit", exclude: ["jquery/lang/vector/vector.js", "jquery/event/livehack/livehack.js", "jquery/event/drag/drag.js"]}, @@ -36,14 +40,6 @@ var i, fileName, cmd, "dom/within", "dom/cur_styles", "model", - { - plugin: "model/associations", - exclude: ["jquery/class/class.js", - "jquery/lang/lang.js", - "jquery/event/destroyed/destroyed.js", - "jquery/lang/openajax/openajax.js", - "jquery/model/model.js"] - }, { plugin: "model/backup", exclude: ["jquery/class/class.js", @@ -94,7 +90,8 @@ var i, fileName, cmd, ] -steal('//steal/build/pluginify', function(s){ +steal.File('jquery/dist').mkdir(); +steal('steal/build/pluginify').then( function(s){ var plugin, exclude, fileDest, fileName; for(i=0; i< plugins.length; i++) { plugin = plugins[i]; exclude = []; @@ -137,4 +143,4 @@ for (i = 0; i < plugins.length; i++) { var minFileDest = fileDest.replace(".js", ".min.js") new steal.File(minFileDest).save(outBaos.toString()); print("***" + fileName + " pluginified and compressed") -} +}*/ diff --git a/buildAll.js b/buildAll.js new file mode 100644 index 00000000..95b0a721 --- /dev/null +++ b/buildAll.js @@ -0,0 +1,130 @@ +// load('jquery/buildAll.js') + +load('steal/rhino/rhino.js') + + +// load every plugin in a single app +// get dependency graph +// generate single script + +steal('steal/build/pluginify','steal/build/apps','steal/build/scripts').then( function(s){ + var ignore = /\.\w+|test|generate|dist|qunit|fixtures|pages/ + + var plugins = [], + /** + * {"path/to/file.js" : ["file2/thing.js", ...]} + */ + files = {}; + + s.File('jquery').contents(function( name, type, current ) { + if (type !== 'file' && !ignore.test(name)) { + var folder = current+"/"+name; + if(readFile(folder+"/"+name+".js")){ + print(folder); + plugins.push(folder); + steal.File(folder).contents(arguments.callee, folder) + } + + //steal.File(path + "/" + (current ? current + "/" : "") + name).contents(arguments.callee, (current ? current + "/" : "") + name); + } + },"jquery"); + + // tell it to load all plugins into this page + + + //steal.win().build_in_progress = true; + print(" LOADING APP ") + steal.build.open('steal/rhino/blank.html', { + startFiles: plugins + }, function(opener){ + + opener.each('js', function(options, text, stl){ + print(options.rootSrc) + var dependencies = files[options.rootSrc] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.options.rootSrc !== "jquery/jquery.js") { + dependencies.push(depend.options.rootSrc); + } + } + } + }) + + s.File("jquery/dist/standalone").mkdirs(); + s.File("jquery/dist/standalone/dependencies.json").save($.toJSON(files)); + //get each file ... + print("Creating jquery/dist/standalone/") + var compressor = s.build.builders.scripts.compressors[ "localClosure"]() + for(var path in files){ + if(path == "jquery/jquery.js"){ + continue; + } + var content = readFile(path); + var funcContent = s.build.pluginify.getFunction(content); + if(typeof funcContent == "undefined"){ + content = ""; + } else { + content = "("+s.build.pluginify.getFunction(content)+")(jQuery);"; + } + var out = path.replace(/\/\w+\.js/,"").replace(/\//g,"."); + content = s.build.builders.scripts.clean(content); + print(" "+out+""); + content = s.build.builders.scripts.clean(content); + s.File("jquery/dist/standalone/"+out+".js").save(content); + s.File("jquery/dist/standalone/"+out+".min.js").save(compressor(content)); + } + + }) + + /* + var pageSteal = steal.build.open("steal/rhino/empty.html").steal, + steals = pageSteal.total, + + files = {}, + depends = function(stl, steals){ + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if(!steals[depend.path]){ + steals[depend.path] = true; + print("123 " + depend.path); + //depends(depend, steals); + } + + + } + } + }, + all = function(c){ + for(var i =0; i < steals.length; i++){ + var pSteal =steals[i]; + + if(!pSteal.func){ + c(pSteal) + } + + } + + }; + print(" LOADED, GETTING DEPENDS"); + all(function(stl){ + files[stl.path] = stl; + }) + all(function(stl){ + print(stl.path) + var dependencies = files[stl.path] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.path !== "jquery/jquery.js") { + dependencies.push(depend.path); + } + } + } + })*/ + + + + +}) \ No newline at end of file diff --git a/class/class.html b/class/class.html index a4386612..ef16143d 100644 --- a/class/class.html +++ b/class/class.html @@ -61,11 +61,9 @@

History Tabs

- \ No newline at end of file diff --git a/class/class.js b/class/class.js index b3ed2fcc..b48d1d00 100644 --- a/class/class.js +++ b/class/class.js @@ -2,23 +2,38 @@ // This is a modified version of John Resig's class // http://ejohn.org/blog/simple-javascript-inheritance/ // It provides class level inheritance and callbacks. -//@steal-clean -steal.plugin("jquery").then(function( $ ) { +//!steal-clean +steal("jquery","jquery/lang/string",function( $ ) { - // if we are initializing a new class - var initializing = false, + // =============== 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] = typeof newProps[name] == "function" && typeof oldProps[name] == "function" && fnTest.test(newProps[name]) ? (function( name, fn ) { + addTo[name] = isFunction(newProps[name]) && + isFunction(oldProps[name]) && + fnTest.test(newProps[name]) ? (function( name, fn ) { return function() { var tmp = this._super, ret; @@ -35,53 +50,58 @@ steal.plugin("jquery").then(function( $ ) { }; })(name, newProps[name]) : newProps[name]; } - }; - + }, + STR_PROTOTYPE = 'prototype' /** * @class jQuery.Class * @plugin jquery/class - * @tag core + * @parent jquerymx * @download dist/jquery/jquery.class.js * @test jquery/class/qunit.html - * Class provides simulated inheritance in JavaScript. Use $.Class 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] + * @description Easy inheritance in JavaScript. + * + * Class provides simulated inheritance in JavaScript. Use Class 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 v. Prototype

- *

Before learning about Class, it's important to + * + * - 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. - *

- * @codestart - * //STATIC - * MyClass.staticProperty //shared property - * - * //PROTOTYPE - * myclass = new MyClass() - * myclass.prototypeMethod() //instance method - * @codeend - *

A static (or class) property is on the Class constructor + * 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 + * 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.extend('Monster', + * $.Class('Monster', * /* @static *| * { * count: 0 @@ -97,7 +117,7 @@ steal.plugin("jquery").then(function( $ ) { * this.health = 10; * * // increments count - * this.Class.count++; + * this.constructor.count++; * }, * eat: function( smallChildren ){ * this.health += smallChildren; @@ -121,68 +141,75 @@ steal.plugin("jquery").then(function( $ ) { * * @codeend * - *

+ * * Notice that the prototype init 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. + * + * + * ## 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 * this._super. Lets create a SeaMonster class. SeaMonsters are less * efficient at eating small children, but more powerful fighters. - *

- * @codestart - * Monster.extend("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 - * @codeend - *

Static property inheritance

+ * + * + * 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: - * @codestart - * $.Class.extend("First", - * { - * staticMethod: function() { return 1;} - * },{}) + * + * $.Class("First", + * { + * staticMethod: function() { return 1;} + * },{}) * - * First.extend("Second",{ - * staticMethod: function() { return this._super()+1;} - * },{}) + * First("Second",{ + * staticMethod: function() { return this._super()+1;} + * },{}) * - * Second.staticMethod() // -> 2 - * @codeend - *

Namespaces

- *

Namespaces are a good idea! We encourage you to namespace all of your code. + * 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: - *

- * @codestart - * $.Class.extend("MyNamespace.MyClass",{},{}); + * + * + * $.Class("MyNamespace.MyClass",{},{}); * - * new MyNamespace.MyClass() - * @codeend + * new MyNamespace.MyClass() + * + * *

Introspection

+ * * 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. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend + * + * $.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

+ * ## Setup and initialization methods + * *

* Class provides static and prototype initialization functions. * These come in two flavors - setup and init. @@ -194,7 +221,7 @@ steal.plugin("jquery").then(function( $ ) { * * * @codestart - * $.Class.extend("MyClass", + * $.Class("MyClass", * { * setup: function() {} //static setup * init: function() {} //static constructor @@ -205,95 +232,138 @@ steal.plugin("jquery").then(function( $ ) { * }) * @codeend * - *

Setup

- *

Setup functions are called before init functions. Static setup functions are passed + * ### 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 + * 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.

- *

+ * 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. - *

- * @codestart - * $.Class.extend("jQuery.Controller",{ - * ... - * },{ - * setup: function( el, options ) { - * ... - * return [$(el), - * $.extend(true, - * this.Class.defaults, - * options || {} ) ] - * } - * }) - * @codeend + * + * $.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 * - *

Init functions are called after setup functions. + * Init functions are called after setup functions. * Typically, they receive the same arguments * as their preceding setup function. The Foo class's init method * gets called in the following example: - *

- * @codestart - * $.Class.Extend("Foo", { - * init: function( arg1, arg2, arg3 ) { - * this.sum = arg1+arg2+arg3; - * } - * }) - * var foo = new Foo(1,2,3); - * foo.sum //-> 6 - * @codeend - *

Callbacks

- *

Similar to jQuery's proxy method, Class provides a - * [jQuery.Class.static.callback callback] + * + * $.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 * this set to the class or instance of the class. - *

- * The following example uses this.callback to make sure + * + * + * The following example uses this.proxy to make sure * this.name is available in show. - * @codestart - * $.Class.extend("Todo",{ - * init: function( name ) { this.name = name } - * get: function() { - * $.get("/stuff",this.callback('show')) - * }, - * show: function( txt ) { - * alert(this.name+txt) - * } - * }) - * new Todo("Trash").get() - * @codeend - *

Callback is available as a static and prototype method.

- *

Demo

+ * + * $.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 Creating a new instance of an object that has extended jQuery.Class - * calls the init prototype function and returns a new instance of the class. - * + * + * + * @constructor + * + * To create a Class call: + * + * $.Class( [NAME , STATIC,] PROTOTYPE ) -> Class + * + *
+ *
{optional:String} + *

If provided, this sets the shortName and fullName of the + * class and adds it and any necessary namespaces to the + * window object.

+ *
+ *
{optional:Object} + *

If provided, this creates static properties and methods + * on the class.

+ *
+ *
{Object} + *

Creates prototype methods on the class.

+ *
+ *
+ * + * 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. */ - jQuery.Class = function() { - if ( arguments.length ) this.extend.apply(this, arguments) + clss = $.Class = function() { + if (arguments.length) { + return clss.extend.apply(clss, arguments); + } }; /* @Static*/ - $.extend($.Class, { + extend(clss, { /** - * @function callback + * @function proxy * Returns a callback function for a function on this Class. - * The callback function ensures that 'this' is set appropriately. + * Proxy ensures that 'this' is set appropriately. * @codestart - * $.Class.extend("MyClass",{ + * $.Class("MyClass",{ * getData: function() { * this.showing = null; - * $.get("data.json",this.callback('gotData'),'json') + * $.get("data.json",this.proxy('gotData'),'json') * }, * gotData: function( data ) { * this.showing = data; @@ -302,11 +372,11 @@ steal.plugin("jquery").then(function( $ ) { * MyClass.showData(); * @codeend *

Currying Arguments

- * Additional arguments to callback will fill in arguments on the returning function. + * Additional arguments to proxy will fill in arguments on the returning function. * @codestart - * $.Class.extend("MyClass",{ + * $.Class("MyClass",{ * getData: function( callback ) { - * $.get("data.json",this.callback('process',callback),'json'); + * $.get("data.json",this.proxy('process',callback),'json'); * }, * process: function( callback, jsonData ) { //callback is added as first argument * jsonData.processed = true; @@ -316,14 +386,15 @@ steal.plugin("jquery").then(function( $ ) { * MyClass.getData(showDataFunc) * @codeend *

Nesting Functions

- * Callback can take an array of functions to call as the first argument. When the returned callback function + * 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.extend("MyClass",{ + * $.Class("MyClass",{ * getData: function( callback ) { * //calls process, then callback with value from process - * $.get("data.json",this.callback(['process2',callback]),'json') + * $.get("data.json",this.proxy(['process2',callback]),'json') * }, * process2: function( type,jsonData ) { * jsonData.processed = true; @@ -337,123 +408,156 @@ steal.plugin("jquery").then(function( $ ) { * next function. * @return {Function} the callback function. */ - callback: function( funcs ) { + proxy: function( funcs ) { //args that should be curried - var args = jQuery.makeArray(arguments), + var args = makeArray(arguments), self; + // get the functions to callback funcs = args.shift(); - if (!jQuery.isArray(funcs) ) { + // 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; - + + //!steal-remove-start + for( var i =0; i< funcs.length;i++ ) { + if(typeof funcs[i] == "string" && !isFunction(this[funcs[i]])){ + throw ("class.js "+( this.fullName || this.Class.fullName)+" does not have a "+funcs[i]+"method!"); + } + } + //!steal-remove-end return function class_cb() { - var cur = args.concat(jQuery.makeArray(arguments)), - isString, length = funcs.length, + // 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 = !jQuery.isArray(cur) || cur._use_call ? [cur] : cur + cur = !isArray(cur) || cur._use_call ? [cur] : cur } } return cur; } }, - /** - * @function getObject - * Gets an object from a String. - * If the object or namespaces the string represent do not - * exist it will create them. - * @codestart - * Foo = {Bar: {Zar: {"Ted"}}} - * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" - * @codeend - * @param {String} objectName the object you want to get - * @param {Object} [current=window] the object you want to look in. - * @return {Object} the object you are looking for. - */ - getObject: function( objectName, current ) { - var current = current || window, - parts = objectName ? objectName.split(/\./) : [], - i = 0; - for (; i < parts.length; i++ ) { - current = current[parts[i]] || (current[parts[i]] = {}) - } - return current; - }, /** * @function newInstance * Creates a new instance of the class. This method is useful for creating new instances * with arbitrary parameters. *

Example

* @codestart - * $.Class.extend("MyClass",{},{}) + * $.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); + inst.init.apply(inst, isArray(args) ? args : arguments); } return inst; }, /** - * Copy and overwrite options from old class - * @param {Object} oldClass - * @param {String} fullName - * @param {Object} staticProps - * @param {Object} protoProps + * 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( oldClass, fullName ) { - this.defaults = $.extend(true, {}, oldClass.defaults, this.defaults); + 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: - * @codestart - * //with className, static and prototype functions - * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) - * //with just classname and prototype functions - * $.Class.extend('Task',{ PROTOTYPE }) - * //With just a className - * $.Class.extend('Task') - * @codeend + * + * // 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 .extend. 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 + // figure out what was passed and normalize it if ( typeof fullName != 'string' ) { proto = klass; klass = fullName; @@ -466,7 +570,7 @@ steal.plugin("jquery").then(function( $ ) { proto = proto || {}; var _super_class = this, - _super = this.prototype, + _super = this[STR_PROTOTYPE], name, shortName, namespace, prototype; // Instantiate a base class (but only create the instance, @@ -474,29 +578,30 @@ steal.plugin("jquery").then(function( $ ) { 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; - if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new - return this.extend.apply(this, arguments) + // 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) && $.inArray(name, ['prototype', 'defaults', 'getObject']) == -1 ) { + if ( this.hasOwnProperty(name) ) { Class[name] = this[name]; } } - // do static inheritance + // copy new static props on class inheritProps(klass, this, Class); // do namespace stuff @@ -504,61 +609,94 @@ steal.plugin("jquery").then(function( $ ) { var parts = fullName.split(/\./), shortName = parts.pop(), - current = $.Class.getObject(parts.join('.')), + current = getObject(parts.join('.'), window, true), namespace = current; - //@steal-remove-start + //!steal-remove-start if (!Class.nameOk ) { - steal.dev.isHappyName(fullName) + //steal.dev.isHappyName(fullName) + } + if(current[shortName]){ + steal.dev.warn("class.js There's already something called "+fullName) } - //@steal-remove-end + //!steal-remove-end current[shortName] = Class; } // set things that can't be overwritten - $.extend(Class, { + 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.prototype.Class = Class.prototype.constructor = Class; + Class[STR_PROTOTYPE].Class = Class[STR_PROTOTYPE].constructor = Class; + - /** - * @attribute fullName - * The full name of the class, including namespace, provided for introspection purposes. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - */ - - var args = Class.setup.apply(Class, [_super_class].concat($.makeArray(arguments))); - + // 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 || []); + Class.init.apply(Class, args || concatArgs([_super_class],arguments)); } /* @Prototype*/ return Class; /** * @function setup - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * setup: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend + * 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 ( new Class( arguments ... )). + * + * $.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 )] + * } + * }) * *
PRO TIP: * Setup functions are used to normalize constructor arguments and provide a place for @@ -566,44 +704,60 @@ steal.plugin("jquery").then(function( $ ) { * run. *
* + * 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 - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * init: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend + * If an init 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: ( new Class( arguments ... )). + * + * $.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 Class - * References the static properties of the instance's class. - *

Quick Example

- * @codestart - * // a class with a static classProperty property - * $.Class.extend("MyClass", {classProperty : true}, {}); + * @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 new instance of myClass - * var mc1 = new MyClass(); + * // a class with a static property + * $.Class("MyClass", {staticProperty : true}, {}); + * + * // a new instance of myClass + * var mc1 = new MyClass(); * - * // - * mc1.Class.classProperty = false; + * // 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. * - * // creates a new MyClass - * var mc2 = new mc.Class(); - * @codeend - * Getting static properties via the Class property, such as it's - * [jQuery.Class.static.fullName fullName] is very common. */ } @@ -613,18 +767,19 @@ steal.plugin("jquery").then(function( $ ) { - jQuery.Class.prototype. + clss.callback = clss[STR_PROTOTYPE].callback = clss[STR_PROTOTYPE]. /** - * @function callback - * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. - * The only difference is this callback works + * @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 */ - callback = jQuery.Class.callback; + proxy = clss.proxy; -})(); \ No newline at end of file +})(); diff --git a/class/test/qunit/class_test.js b/class/class_test.js similarity index 75% rename from class/test/qunit/class_test.js rename to class/class_test.js index 018a5269..ec95179c 100644 --- a/class/test/qunit/class_test.js +++ b/class/class_test.js @@ -1,3 +1,6 @@ +steal("jquery/class") //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/class"); test("Creating", function(){ @@ -46,7 +49,7 @@ test("Creating", function(){ new Dog(); new Animal(); new Animal(); - ajax = new Ajax(1000); + var ajax = new Ajax(1000); equals(2, Animal.count, "right number of animals"); equals(1, Dog.count, "right number of animals") @@ -143,4 +146,60 @@ test("callback", function(){ curVal = 2; var cb2 = Car.callback('show',2) cb2(); +}); + +test("callback error", 1,function(){ + $.Class.extend("Car",{ + show: function( value ) { + equals(curVal, value) + } + },{ + show: function( value ) { + + } + }) + try{ + Car.callback('huh'); + ok(false, "I should have errored") + }catch(e){ + ok(true, "Error was thrown") + } }) + +test("Creating without extend", function(){ + $.Class("Bar",{ + ok : function(){ + ok(true, "ok called") + } + }); + new Bar().ok(); + + Bar("Foo",{ + dude : function(){ + ok(true, "dude called") + } + }); + new Foo().dude(true); +}) + + +/* Not sure I want to fix this yet. +test("Super in derived when parent doesn't have init", function(){ + $.Class("Parent",{ + }); + + Parent("Derived",{ + init : function(){ + this._super(); + } + }); + + try { + new Derived(); + ok(true, "Can call super in init safely") + } catch (e) { + ok(false, "Failed to call super in init with error: " + e) + } +})*/ + +}); \ No newline at end of file diff --git a/class/qunit.html b/class/qunit.html index 255812e4..503f699f 100644 --- a/class/qunit.html +++ b/class/qunit.html @@ -10,6 +10,6 @@

    - + \ No newline at end of file diff --git a/class/test/qunit/qunit.js b/class/test/qunit/qunit.js deleted file mode 100644 index 3a2d2068..00000000 --- a/class/test/qunit/qunit.js +++ /dev/null @@ -1,5 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/class") //load your app - .plugins('funcunit/qunit') //load qunit - .then("class_test") \ No newline at end of file diff --git a/controller/controller.html b/controller/controller.html index e2253166..a76e2ace 100644 --- a/controller/controller.html +++ b/controller/controller.html @@ -50,11 +50,9 @@ - \ No newline at end of file diff --git a/controller/controller.js b/controller/controller.js index 21862217..f3bd8476 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -1,45 +1,59 @@ -steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(function( $ ) { - - // ------- helpers ------ +steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( $ ) { + // ------- HELPER FUNCTIONS ------ + // Binds an element, returns a function that unbinds var bind = function( el, ev, callback ) { - var wrappedCallback; + 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); - } else { - event.handled = null; - } + } }; } - $(el).bind(ev, wrappedCallback || callback); + 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() { - $(el).unbind(ev, wrappedCallback || callback); + 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 ) { - $(el).delegate(selector, ev, callback); + var binder = el.delegate && el.undelegate ? el : $(isFunction(el) ? [el] : el) + binder.delegate(selector, ev, callback); return function() { - $(el).undelegate(selector, ev, callback); - el = ev = callback = selector = null; + 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 - */ - shifter = function shifter(cb) { + + // 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() { - return cb.apply(null, [$(this)].concat(Array.prototype.slice.call(arguments, 0))); + context.called = name; + return method.apply(context, [this.nodeName ? $(this) : this].concat( slice.call(arguments, 0) ) ); }; }, // matches dots @@ -48,78 +62,141 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func controllersReg = /_?controllers?/ig, //used to remove the controller from the name underscoreAndRemoveController = function( className ) { - return $.String.underscore(className.replace(dotsReg, '_').replace(controllersReg, "")); + return Str.underscore(className.replace("jQuery.", "").replace(dotsReg, '_').replace(controllersReg, "")); }, // checks if it looks like an action actionMatcher = /[^\w]/, - // gets jus the event - eventCleaner = /^(>?default\.)|(>)/, // handles parameterized action names parameterReplacer = /\{([^\}]+)\}/g, breaker = /^(?:(.*?)\s)?([\w\.\:>]+)$/, - basicProcessor; + basicProcessor, + data = function(el, data){ + return $.data(el, "controllers", data) + }; /** - * @tag core + * @class jQuery.Controller + * @parent jquerymx * @plugin jquery/controller - * @download jquery/dist/jquery.controller.js + * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/controller/controller.js * @test jquery/controller/qunit.html + * @inherits jQuery.Class + * @description jQuery widget factory. * - * Controllers organize event handlers using event delegation. - * If something happens in your application (a user click or a [jQuery.Model|Model] instance being updated), - * a controller should respond to it. + * 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. * - * ## Benefits + * This means it is used to + * create things like tabs, grids, and contextmenus as well as + * organizing them into higher-order business rules. * - * - Know your code. - * - * Group events and label your html in repeatable ways so it's easy to find your code. - * - * - Controllers are inheritable. - * - * Package, inherit, and reuse your widgets. - * - * - Write less. - * - * Controllers take care of setup / teardown auto-magically. + * 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 * - * Controllers organize jQuery code into resuable, inheritable, and extendable widgets. So instead of + * Instead of * - * @codestart - * $(function(){ - * $('#tabs').click(someCallbackFunction1) - * $('#tabs .tab').click(someCallbackFunction2) - * $('#tabs .delete click').click(someCallbackFunction3) - * }); - * @codeend + * + * $(function(){ + * $('#tabs').click(someCallbackFunction1) + * $('#tabs .tab').click(someCallbackFunction2) + * $('#tabs .delete click').click(someCallbackFunction3) + * }); * * do this * - * @codestart - * $.Controller.extend('Tabs',{ - * click: function() {...}, - * '.tab click' : function() {...}, - * '.delete click' : function() {...} - * }) - * $('#tabs').tabs(); - * @codeend + * $.Controller('Tabs',{ + * click: function() {...}, + * '.tab click' : function() {...}, + * '.delete click' : function() {...} + * }) + * $('#tabs').tabs(); + * * * ## Tabs Example * * @demo jquery/controller/controller.html * - * - * ## Using Controllers - * - * - * A Controller is mostly a list of functions that get called back when specific events happen. - * A function's name provides a description of when the function should be called. - * By naming your functions like "selector event", - * Controller recognizes them as an Action and binds them appropriately. - * - * The event binding happens when you create a [jQuery.Controller.prototype.setup|new controller instance]. + * ## 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( + * "
    "+this.options.message+"
    " + * ); + * }, + * "div click" : function(div, ev){ + * div.remove(); + * } + * }) + * + * This creates a $.fn.my_widget 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 new MyWidget(el, options) on + * each '.thing' 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 $.data. + * - [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. @@ -138,22 +215,20 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * * To add a mousover effect and create todos, your controller might look like: * - * @codestart - * $.Controller.extend('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>"); - * } - * }) - * @codeend + * $.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("
  1. New Todo
  2. "); + * } + * }) * * Now that you've created the controller class, you've must attach the event handlers on the '#todos' div by - * creating [jQuery.Controller.prototype.init|a new controller instance]. There are 2 ways of doing this. + * creating [jQuery.Controller.prototype.setup|a new controller instance]. There are 2 ways of doing this. * * @codestart * //1. Create a new controller directly: @@ -162,129 +237,20 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * $('#todos').todos(); * @codeend * - * As you've likely noticed, when the [jQuery.Controller.static.init|controller class is created], it creates helper - * functions on [jQuery.fn]. The "#todos" element is known as the controller element. - * - * ### Event Handler Matching - * - * With the exception of subscribe actions, controller uses jQuery.fn.bind or jQuery.fn.delegate to - * attach event handlers. Controller uses the following rules to determine if a function name is - * an event handler: - * - * - Does the function name contain a selector? Ex: "a.foo click" - * - Does the function name match an event in jQuery.event.special? Ex: "mouseenter" - * - Does the function name match a standard event name? Ex: "click" - * - Does the function name match a value in the controller's static listensTo array? Ex: "activate" - * - * In general, Controller will know automatically when to bind event handler functions except for - * one case - event names without selectors that are not in $.event.special. But to correct for this, you - * just need to add the function to the listensTo property. Here's how: - * - * @codestart - * $.Controller.extend("MyShow",{ - * listensTo: ["show"] - * },{ - * show: function( el, ev ) { - * el.show(); - * } - * }) - * $('.show').my_show().trigger("show"); - * @codeend - * - * - * ### Callback Parameters - * - * For most actions, the first two parameters are always: - * - * - el : the jQuery wrapped element. - * - ev : the jQuery wrapped DOM event. - * - * @codestart - * ".something click" : function( el, ev ) { - * el.slideUp() - * ev.stopDelegation(); //stops this event from delegating to any other - * // delegated events for this delegated element. - * ev.preventDefault(); //prevents the default action from happening. - * ev.stopPropagation(); //stops the event from going to other elements. - * } - * @codeend - * - * If the action provides different parameters, they are in each action's documentation. - * - * - * ## Document Controllers - * - * Document Controllers delegate on the documentElement. You don't have to attach an instance as this will be done - * for you when the controller class is created. Document Controllers, with the exception of MainControllers, - * add an implicit '#CONTROLLERNAME' before every selector. - * - * To create a document controller, you just have to set the controller's [jQuery.Controller.static.onDocument static onDocument] - * property to true. - * - * @codestart - * $.Controller.extend('TodosController', - * {onDocument: true}, - * { - * ".todo mouseover" : function( el, ev ) { //matches #todos .todo - * el.css("backgroundColor","red") - * }, - * ".todo mouseout" : function( el, ev ) { //matches #todos .todo - * el.css("backgroundColor","") - * }, - * ".create click" : function() { //matches #todos .create - * this.find("ol").append("<li class='todo'>New Todo</li>"); - * } - * }) - * @codeend - * - * DocumentControllers are typically used for page layout and functionality that is - * extremely unlikely to be repeated such as a SidebarController. - * Often, a Document Controller's "ready" event will be used to create - * necessary Element Controllers. - * - * @codestart - * $.Controller.extend('SidebarController', - * {onDocument: true}, - * { - * ready : function() { - * $(".slider").slider_controller() - * }, - * "a.tag click" : function() {..} - * }) - * @codeend - * - * ### MainControllers - * - * MainControllers are documentControllers that do not add '#CONTROLLERNAME' before every selector. This controller - * should only be used for page wide functionality and setup. - * - * @codestart - * $.Controller.extend("MainController",{ - * hasActiveElement : document.activeElement || false - * },{ - * focus : funtion(el){ - * if(!this.Class.hasActiveElement) - * document.activeElement = el[0] //tracks active element - * } - * }) - * @codeend - * * ## Controller Initialization * - * It can be extremely useful to overwrite [jQuery.Controller.prototype.init Controller.prototype.init] with + * 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: * - * @codestart - * $.Controller.extend("SpecialController", - * { - * init: function( el, message ) { - * this.element.html(message) - * } - * }) - * $(".special").special("Hello World") - * @codeend + * $.Controller("SpecialController", + * { + * init: function( el, message ) { + * this.element.html(message) + * } + * }) + * $(".special").special("Hello World") * * ## Removing Controllers * @@ -346,26 +312,32 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * //calls FooController.prototype.bar * $(".special").foo("bar","something I want to pass") * @codeend + * + * These methods let you call one controller from another controller. + * */ - $.Class.extend("jQuery.Controller", + $.Class("jQuery.Controller", /** * @Static */ { /** - * Does 3 things: - *
      - *
    1. Creates a jQuery helper for this controller.
    2. - *
    3. Calculates and caches which functions listen for events.
    4. - *
    5. and attaches this element to the documentElement if onDocument is true.
    6. - *
    - *

    jQuery Helper Naming Examples

    - * @codestart - * "TaskController" -> $().task_controller() - * "Controllers.Task" -> $().controllers_task() - * @codeend + * Does 2 things: + * + * - Creates a jQuery helper for this controller. + * - Calculates and caches which functions listen for events. + * + * ### jQuery Helper Naming Examples + * + * + * "TaskController" -> $().task_controller() + * "Controllers.Task" -> $().controllers_task() + * */ - init: function() { + 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; @@ -375,20 +347,32 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func this._shortName = underscoreAndRemoveController(this.shortName); var controller = this, - pluginname = this._fullName, + /** + * @attribute pluginName + * Setting the pluginName 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), + var args = makeArray(arguments), //if the arg is a method on this controller - isMethod = typeof options == "string" && $.isFunction(controller.prototype[options]), + isMethod = typeof options == "string" && isFunction(controller[STR_PROTOTYPE][options]), meth = args[0]; - this.each(function() { + return this.each(function() { //check if created - var controllers = $.data(this, "controllers"), + var controllers = data(this), //plugin is actually the controller instance plugin = controllers && controllers[pluginname]; @@ -406,36 +390,26 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func controller.newInstance.apply(controller, [this].concat(args)); } }); - //always return the element - return this; }; } // make sure listensTo is an array - //@steal-remove-start - if (!$.isArray(this.listensTo) ) { + //!steal-remove-start + if (!isArray(this.listensTo) ) { throw "listensTo is not an array in " + this.fullName; } - //@steal-remove-end + //!steal-remove-end // calculate and cache actions this.actions = {}; - for ( funcName in this.prototype ) { - if (!$.isFunction(this.prototype[funcName]) ) { + for ( funcName in this[STR_PROTOTYPE] ) { + if (funcName == 'constructor' || !isFunction(this[STR_PROTOTYPE][funcName]) ) { continue; } if ( this._isAction(funcName) ) { - this.actions[funcName] = this._getAction(funcName); + this.actions[funcName] = this._action(funcName); } } - - /** - * @attribute onDocument - * Set to true if you want to automatically attach this element to the documentElement. - */ - if ( this.onDocument ) { - forLint = new controller(document.documentElement); - } }, hookup: function( el ) { return new this(el); @@ -450,90 +424,253 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func if ( actionMatcher.test(methodName) ) { return true; } else { - var cleanedEvent = methodName.replace(eventCleaner, ""); - return $.inArray(cleanedEvent, this.listensTo) > -1 || $.event.special[cleanedEvent] || $.Controller.processors[cleanedEvent]; + 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. */ - _getAction: function( methodName, options ) { - //if we don't have a controller instance, we'll break this guy up later + _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; } - var convertedName = options ? methodName.replace(parameterReplacer, function( whole, inside ) { - //convert inside to type - return $.Class.getObject(inside, options).toString(); //gets the value in options - }) : methodName, - parts = convertedName.match(breaker), + // 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 = this.processors[event] || basicProcessor; + processor = processors[event] || basicProcessor; return { processor: processor, - parts: parts + parts: parts, + delegate : arr ? convertedName[0] : undefined }; }, /** * @attribute processors - * A has of eventName: function pairs that Controller uses to hook + * 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 - * A list of special events this controller listens too. You only need to add event names that + * 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. */ - listensTo: [] + defaults: {} }, /** * @Prototype */ { /** - * Does three things: - *
      - *
    1. Matches and creates actions.
    2. - *
    3. Set the controller's element.
    4. - *
    5. Saves a reference to this controller in the element's data.
    6. - *
    + * 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.Class; + var funcName, ready, cls = this[STR_CONSTRUCTOR]; //want the raw element here - element = element.jquery ? element[0] : element; + element = (typeof element == 'string' ? $(element) : + (element.jquery ? element : [element]) )[0]; //set element and className on element - this.element = $(element).addClass(cls._fullName); + var pluginname = cls.pluginName || cls._fullName; + + //set element and className on element + this.element = $(element).addClass(pluginname); //set in data - ($.data(element, "controllers") || $.data(element, "controllers", {}))[cls._fullName] = this; + (data(element) || data(element, {}))[pluginname] = this; - //adds bindings - this._bindings = []; + /** * @attribute options - * Options is automatically merged from this.Class.OPTIONS and the 2nd argument - * passed to a controller. + * + * 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); - - //go through the cached list of actions and use the processor to bind - for ( funcName in cls.actions ) { - ready = cls.actions[funcName] || cls._getAction(funcName, this.options); - - this._bindings.push( - ready.processor(element, ready.parts[2], ready.parts[1], this.callback(funcName), this)); - } + this.options = extend( extend(true, {}, cls.defaults), options); + /** * @attribute called @@ -543,47 +680,88 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func */ this.called = "init"; - //setup to be destroyed ... don't bind b/c we don't want to remove it - //this.element.bind('destroyed', this.callback('destroy')) - var destroyCB = shifter(this.callback("destroy")); - this.element.bind("destroyed", destroyCB); - this._bindings.push(function( el ) { - destroyCB.removed = true; - $(element).unbind("destroyed", destroyCB); - }); + // bind all event handlers + this.bind(); /** * @attribute element - * The controller instance's delegated element. This is set by [jQuery.Controller.prototype.init init]. - * It is a jQuery wrapped element. - * @codestart - * ".something click" : function() { - * this.element.css("color","red") - * } - * @codeend + * 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 = $('
    '); + * 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. */ - return this.element; }, /** - * Bind attaches event handlers that will be removed when the controller is removed. - * This is a good way to attach to an element not in the controller's element. - *
    - *

    Examples:

    - * @codestart - * 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 ) { - * - * } - * @codeend - * @param {HTMLElement|jQuery.fn} [element=this.element] element the element to be bound + * 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 @@ -591,6 +769,37 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * @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; @@ -600,11 +809,19 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func }, _binder: function( el, eventName, func, selector ) { if ( typeof func == 'string' ) { - func = shifter(this.callback(func)); + 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.
    @@ -615,9 +832,9 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * //do something * }) * @codeend - * @param {HTMLElement|jQuery.fn} [element=this.element] element + * @param {HTMLElement|jQuery.fn} [element=this.element] the element to delegate from * @param {String} selector the css selector - * @param {String} eventName + * @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. @@ -633,56 +850,155 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func return this._binder(element, eventName, func, selector); }, /** - * Called if an controller's jQuery helper is called on an element that already has a controller instance - * of the same type. Extends this.options with the options passed in. If you overwrite this, you might want to call + * 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. - *

    Examples

    - * @codestart - * $.Controller.extend("Thing",{ - * init: function( el, options ) { - * alert('init') - * }, - * update: function( options ) { - * this._super(options); - * alert('update') - * } - * }); - * $('#myel').thing(); // alerts init - * $('#myel').thing(); // alerts update - * @codeend - * @param {Object} options + * + * ### 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); + extend(this.options, options); + this._unbind(); + this.bind(); }, /** - * Destroy unbinds and undelegates all actions on this controller, and prevents any memory leaks. This is called automatically - * if the element is removed. + * 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 _super 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( ev ) { + destroy: function() { if ( this._destroyed ) { - throw this.Class.shortName + " controller instance has been deleted"; + throw this[STR_CONSTRUCTOR].shortName + " controller already deleted"; } var self = this, - fname = this.Class._fullName; + fname = this[STR_CONSTRUCTOR].pluginName || this[STR_CONSTRUCTOR]._fullName, + controllers; + + // mark as destroyed this._destroyed = true; + + // remove the className this.element.removeClass(fname); - $.each(this._bindings, function( key, value ) { - if ( $.isFunction(value) ) { - value(self.element[0]); - } - }); - + // unbind bindings + this._unbind(); + // clean up delete this._actions; - - var controllers = this.element.data("controllers"); - if ( controllers && controllers[fname] ) { - delete controllers[fname]; - } + delete this.element.data("controllers")[fname]; + $(this).triggerHandler("destroyed"); //in case we want to know if the controller is removed + this.element = null; }, /** @@ -702,97 +1018,72 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func _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, cb, controller ) { - var c = controller.Class; - - // document controllers use their name as an ID prefix. - if ( c.onDocument && !/^Main(Controller)?$/.test(c.shortName) ) { //prepend underscore name if necessary - selector = selector ? "#" + c._shortName + " " + selector : "#" + c._shortName; - } - return binder(el, event, shifter(cb), selector); + basicProcessor = function( el, event, selector, methodName, controller ) { + return binder(el, event, shifter(controller, methodName), selector); }; - var processors = $.Controller.processors, - //a window event only happens on the window - windowEvent = function( el, event, selector, cb ) { - return binder(window, event.replace(/window/, ""), shifter(cb)); - }; - //set commong events to be processed as a basicProcessor - $.each("change click contextmenu dblclick keydown keyup keypress mousedown mousemove mouseout mouseover mouseup reset windowresize resize windowscroll scroll select submit dblclick focusin focusout load unload ready hashchange mouseenter mouseleave".split(" "), function( i, v ) { + + //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; }); - $.each(["windowresize", "windowscroll", "load", "ready", "unload", "hashchange"], function( i, v ) { - processors[v] = windowEvent; - }); - //the ready processor happens on the document - processors.ready = function( el, event, selector, cb ) { - $(shifter(cb)); //cant really unbind - }; /** * @add jQuery.fn */ - $.fn.mixin = function() { - //create a bunch of controllers - var controllers = $.makeArray(arguments), - forLint; - return this.each(function() { - for ( var i = 0; i < controllers.length; i++ ) { - forLint = new controllers[i](this); - } - - }); - }; //used to determine if a controller instance is one of controllers //controllers can be strings or classes - var isAControllerOf = function( instance, controllers ) { - for ( var i = 0; i < controllers.length; i++ ) { - if ( typeof controllers[i] == 'string' ? instance.Class._shortName == controllers[i] : instance instanceof controllers[i] ) { + 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; }; - - /** - * @function controllers - * Gets all controllers in the jQuery element. - * @return {Array} an array of controller instances. - */ - $.fn.controllers = function() { - var controllerNames = $.makeArray(arguments), - instances = [], - controllers; - //check if arguments - this.each(function() { - controllers = $.data(this, "controllers"); - if (!controllers ) { - return; - } - for ( var cname in controllers ) { - var c = controllers[cname]; - if (!controllerNames.length || isAControllerOf(c, controllerNames) ) { - instances.push(c); + $.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. - */ - $.fn.controller = function( controller ) { - return this.controllers.apply(this, arguments)[0]; - }; + }); + 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]; + } + }); + -}); \ No newline at end of file +}); diff --git a/controller/controller_test.js b/controller/controller_test.js new file mode 100644 index 00000000..d94799e9 --- /dev/null +++ b/controller/controller_test.js @@ -0,0 +1,269 @@ +steal("jquery/controller",'jquery/controller/subscribe') //load your app + .then('funcunit/qunit') //load qunit + .then(function(){ + +module("jquery/controller") +test("subscribe testing works", function(){ + + var ta = $("
    ").appendTo( $("#qunit-test-area") ) + + ta.html("click here") + + var clicks = 0, destroys = 0; + var subscribes = 0; + $.Controller.extend("MyTest",{ + click: function() { + clicks++ + }, + "a.b subscribe" : function() { + subscribes++ + }, + destroy: function() { + + this._super() + destroys++; + } + }) + ta.my_test(); + ta.trigger("click") + equals(clicks,1, "can listen to clicks") + + OpenAjax.hub.publish("a.b",{}) + equals(subscribes,1, "can subscribe") + var controllerInstance = ta.controller('my_test') + ok( controllerInstance.Class == MyTest, "can get controller" ) + controllerInstance.destroy() + + equals(destroys,1, "destroy called once") + ok(!ta.controller(), "controller is removed") + + OpenAjax.hub.publish("a.b",{}) + equals(subscribes,1, "subscription is torn down") + ta.trigger("click") + equals(clicks,1, "No longer listening") + + + + ta.my_test(); + ta.trigger("click") + OpenAjax.hub.publish("a.b",{}) + equals(clicks,2, "can listen again to clicks") + equals(subscribes,2, "can listen again to subscription") + + ta.remove(); + + ta.trigger("click") + OpenAjax.hub.publish("a.b",{}) + equals(clicks,2, "Clicks stopped") + equals(subscribes,2, "Subscribes stopped") +}) + + + +test("bind to any special", function(){ + jQuery.event.special.crazyEvent = { + + } + var called = false; + jQuery.Controller.extend("WeirdBind",{ + crazyEvent: function() { + called = true; + } + }) + var a = $("
    ").appendTo($("#qunit-test-area")) + a.weird_bind(); + a.trigger("crazyEvent") + ok(called, "heard the trigger"); + + $("#qunit-test-area").html("") + +}) + +test("parameterized actions", function(){ + var called = false; + jQuery.Controller.extend("WeirderBind",{ + "{parameterized}" : function() { + called = true; + } + }) + var a = $("
    ").appendTo($("#qunit-test-area")) + a.weirder_bind({parameterized: "sillyEvent"}); + a.trigger("sillyEvent") + ok(called, "heard the trigger") + + $("#qunit-test-area").html("") +}) + +test("windowresize", function(){ + var called = false; + jQuery.Controller.extend("WindowBind",{ + "{window} resize" : function() { + called = true; + } + }) + $("#qunit-test-area").html("
    ") + $("#weird").window_bind(); + $(window).trigger('resize') + ok(called,"got window resize event"); + + $("#qunit-test-area").html("") +}) + +// this.delegate(this.cached.header.find('tr'), "th", "mousemove", "th_mousemove"); +test("delegate", function(){ + var called = false; + jQuery.Controller.extend("DelegateTest",{ + click: function() {} + }) + var els = $("
    click me
    ").appendTo($("#qunit-test-area")) + var c = els.delegate_test(); + c.controller().delegate(els.find("span"), "a", "click", function(){ + called = true; + }) + els.find("a").trigger('click') + ok(called, "delegate works") + $("#qunit-test-area").html("") +}) + +test("inherit", function(){ + var called = false; + $.Controller.extend( "Parent", { + click: function(){ + called = true; + } + }) + Parent.extend( "Child", { + + }) + var els = $("
    click me
    ").appendTo($("#qunit-test-area")) + els.child(); + els.find("a").trigger('click') + ok(called, "inherited the click method") + $("#qunit-test-area").html("") +}); + +test("objects in action", function(){ + $.Controller('Thing',{ + "{item} someEvent" : function(thing, ev){ + ok(true, "called"); + equals(ev.type, "someEvent","correct event") + equals(this.constructor.fullName, "Thing", "This is a controller isntance") + equals(thing.name,"Justin","Raw, not jQuery wrapped thing") + } + }); + + var thing1 = {name: "Justin"}; + + var ta = $("
    ").appendTo( $("#qunit-test-area") ) + ta.thing({item : thing1}); + + $(thing1).trigger("someEvent"); + + $("#qunit-test-area").html(""); + +}); + +test("dot",function(){ + $.Controller("Dot",{ + "foo.bar" : function(){ + ok(true,'called') + } + }); + + var ta = $("
    ").appendTo( $("#qunit-test-area") ); + ta.dot().trigger("foo.bar"); + $("#qunit-test-area").html(""); +}) + +// HTMLFormElement[0] breaks +test("the right element", 1, function(){ + $.Controller('FormTester',{ + init : function(){ + equals(this.element[0].nodeName.toLowerCase(), "form" ) + } + }) + $("
    ").appendTo( $("#qunit-test-area") ) + .form_tester(); + $("#qunit-test-area").html("") +}) + +test("pluginName", function() { + // Testing for controller pluginName fixes as reported in + // http://forum.javascriptmvc.com/#topic/32525000000253001 + // http://forum.javascriptmvc.com/#topic/32525000000488001 + expect(6); + + $.Controller("PluginName", { + pluginName : "my_plugin" + }, { + method : function(arg) { + ok(true, "Method called"); + }, + + update : function(options) { + this._super(options); + ok(true, "Update called"); + }, + + destroy : function() { + ok(true, "Destroyed"); + this._super(); + } + }); + + var ta = $("
    ").addClass('existing_class').appendTo( $("#qunit-test-area") ); + ta.my_plugin(); // Init + ok(ta.hasClass("my_plugin"), "Should have class my_plugin"); + ta.my_plugin(); // Update + ta.my_plugin("method"); // method() + ta.controller().destroy(); // destroy + ok(!ta.hasClass("my_plugin"), "Shouldn't have class my_plugin after being destroyed"); + ok(ta.hasClass("existing_class"), "Existing class should still be there"); +}) + +test("inherit defaults", function() { + $.Controller.extend("BaseController", { + defaults : { + foo: 'bar' + } + }, {}); + + BaseController.extend("InheritingController", { + defaults : { + newProp : 'newVal' + } + }, {}); + + ok(InheritingController.defaults.foo === 'bar', 'Class must inherit defaults from the parent class'); + ok(InheritingController.defaults.newProp == 'newVal', 'Class must have own defaults'); + var inst = new InheritingController($('
    '), {}); + ok(inst.options.foo === 'bar', 'Instance must inherit defaults from the parent class'); + ok(inst.options.newProp == 'newVal', 'Instance must have defaults of it`s class'); +}); + +test("update rebinding", 2, function(){ + var first = true; + $.Controller("Rebinder", { + "{item} foo" : function(item, ev){ + if(first){ + equals(item.id, 1, "first item"); + first = false; + } else { + equals(item.id, 2, "first item"); + } + } + }); + + var item1 = {id: 1}, + item2 = {id: 2}, + el = $('
    ').rebinder({item: item1}) + + $(item1).trigger("foo") + + el.rebinder({item: item2}); + + $(item2).trigger("foo") +}) + + +}); diff --git a/controller/demo-update.html b/controller/demo-update.html new file mode 100644 index 00000000..cb5e936b --- /dev/null +++ b/controller/demo-update.html @@ -0,0 +1,54 @@ + + + + Controller Example + + + +
    +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/controller/history/history.html b/controller/history/history.html deleted file mode 100644 index 3bc6b0a9..00000000 --- a/controller/history/history.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - hover - - - - - - - - diff --git a/controller/history/history.js b/controller/history/history.js deleted file mode 100644 index c3f9ee6e..00000000 --- a/controller/history/history.js +++ /dev/null @@ -1,172 +0,0 @@ -steal.plugins('jquery/controller/subscribe', - 'jquery/event/hashchange').then(function($){ - -/** - * @class - * The controller history plugin adds browser hash (#) based history support. - * This class itself helps break up parts of the hash of the url - * @constructor - * Takes a url and extracts information out of it. - * @param {Object} path - */ - -var keyBreaker = /([^\[\]]+)|(\[\])/g; - -$.Controller.History = { - /** - * - * returns the pathname part - * - * @codestart - * "#foo/bar&foo=bar" -> 'foo/bar' - * @codeend - */ - pathname : function(path) { - var parts = path.match(/#([^&]*)/); - return parts ? parts[1] : null - }, - /** - * returns the search part, but without the first & - * @codestart - * "#foo/bar&foo=bar" -> 'foo=barr' - * @codeend - */ - search : function(path) { - var parts = path.match(/#[^&]*&(.*)/); - return parts ? parts[1] : null - }, - getData: function(path) { - var search = $.Controller.History.search(path), - digitTest = /^\d+$/; - if(! search || ! search.match(/([^?#]*)(#.*)?$/) ) { - return {}; - } - - // Support the legacy format that used MVC.Object.to_query_string that used %20 for - // spaces and not the '+' sign; - search = search.replace(/\+/g,"%20") - - var data = {}, - pairs = search.split('&'), - current; - - for(var i=0; i < pairs.length; i++){ - current = data; - var pair = pairs[i].split('='); - - // if we find foo=1+1=2 - if(pair.length != 2) { - pair = [pair[0], pair.slice(1).join("=")] - } - - var key = decodeURIComponent(pair[0]), - value = decodeURIComponent(pair[1]), - parts = key.match(keyBreaker); - - for ( var j = 0; j < parts.length - 1; j++ ) { - var part = parts[j]; - if (!current[part] ) { - current[part] = digitTest.test(part) || parts[j+1] == "[]" ? [] : {} - } - current = current[part]; - } - lastPart = parts[parts.length - 1]; - if(lastPart == "[]"){ - current.push(value) - }else{ - current[lastPart] = value; - } - } - return data; - } -}; - - - - - -jQuery(function($) { - $(window).bind('hashchange',function() { - var data = $.Controller.History.getData(location.href), - folders = $.Controller.History.pathname(location.href) || 'index', - hasSlash = (folders.indexOf('/') != -1); - - if( !hasSlash && folders != 'index' ) { - folders += '/index'; - } - - OpenAjax.hub.publish("history."+folders.replace("/","."), data); - }); - - setTimeout(function(){ - $(window).trigger('hashchange') - },1) //immediately after ready -}) - - -$.extend($.Controller.prototype, { - /** - * Redirects to another page. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - */ - redirectTo: function(options){ - var point = this._get_history_point(options); - location.hash = point; - }, - /** - * Redirects to another page by replacing current URL with the given one. This - * call will not create a new entry in the history. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - */ - replaceWith: function(options){ - var point = this._get_history_point(options); - location.replace(location.href.split('#')[0] + point); - }, - /** - * Adds history point to browser history. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - * @param {Object} data extra data saved in history -- NO LONGER SUPPORTED - */ - historyAdd : function(options, data) { - var point = this._get_history_point(options); - location.hash = point; - }, - /** - * Creates a history point from given options. Resultant history point is like #controller/action¶m1=value1 - * @plugin 'dom/history' - * @param {Object} options an object that will turned into history point - */ - _get_history_point: function(options) { - var controller_name = options.controller || this.Class.underscoreName; - var action_name = options.action || 'index'; - - /* Convert the options to parameters (removing controller and action if needed) */ - if(options.controller) - delete options.controller; - if(options.action) - delete options.action; - - var paramString = (options) ? $.param(options) : ''; - if(paramString.length) - paramString = '&' + paramString; - - return '#' + controller_name + '/' + action_name + paramString; - }, - - /** - * Provides current window.location parameters as object properties. - * @plugin 'dom/history' - */ - pathData :function() { - return $.Controller.History.getData(location.href); - } -}); - - - - - -}); \ No newline at end of file diff --git a/controller/history/qunit/qunit.js b/controller/history/qunit/qunit.js deleted file mode 100644 index 9a6d5e71..00000000 --- a/controller/history/qunit/qunit.js +++ /dev/null @@ -1,38 +0,0 @@ -steal.plugins('funcunit/qunit','jquery/controller/history').then(function($){ - -module("jquery/controller/history",{ - setup: function(){ - - } -}) - -test("Basic getData",function(){ - - var data = $.Controller.History.getData("#foo/bar&a=b"); - equals(data.a,"b") - - var data = $.Controller.History.getData("#foo/bar&a=b&c=d"); - equals(data.a,"b") - equals(data.c,"d") -}) -test("Nested getData",function(){ - - var data = $.Controller.History.getData("#foo/bar&a[b]=1&a[c]=2"); - equals(data.a.b,1) - equals(data.a.c,2) - - var data = $.Controller.History.getData("#foo/bar&a[]=1&a[]=2"); - equals(data.a[0],1) - equals(data.a[1],2) - - var data = $.Controller.History.getData("#foo/bar&a[b][]=1&a[b][]=2"); - equals(data.a.b[0],1) - equals(data.a.b[1],2) - - var data = $.Controller.History.getData("#foo/bar&a[0]=1&a[1]=2"); - equals(data.a[0],1) - equals(data.a[1],2) -}) - - -}) diff --git a/controller/pages/listening.md b/controller/pages/listening.md new file mode 100644 index 00000000..c27ab175 --- /dev/null +++ b/controller/pages/listening.md @@ -0,0 +1,189 @@ +@page jquery.controller.listening Listening To Events +@parent jQuery.Controller + +Controllers make creating and tearing down event handlers extremely +easy. The tearingdown of event handlers is especially important +in preventing memory leaks in long lived applications. + +## Automatic Binding + +When a [jQuery.Controller.prototype.setup new controller is created], +contoller checks its prototype methods for functions that are named like +event handlers. It binds these functions to the +controller's [jQuery.Controller.prototype.element element] with +event delegation. When +the controller is destroyed (or it's element is removed from the page), controller +will unbind its event handlers automatically. + +For example, each of the following controller's functions will automatically +bound: + + $.Controller("Crazy",{ + + // listens to all clicks on this element + "click" : function(el, ev){}, + + // listens to all mouseovers on + // li elements withing this controller + "li mouseover" : function(el, ev){} + + // listens to the window being resized + "{window} resize" : function(window, ev){} + }) + +Controller will bind function names with spaces, standard DOM events, and +event names in $.event.special. + +In general, Controller will know automatically when to bind event handler functions except for +one case - event names without selectors that are not in $.event.special. + +But to correct for this, you just need to add the +function to the [jQuery.Controller.static.listensTo listensTo] +property. Here's how: + + $.Controller("MyShow",{ + listensTo: ["show"] + },{ + show: function( el, ev ) { + el.show(); + } + }) + $('.show').my_show().trigger("show"); + +## Callback parameters + +Event handlers bound with controller are called back with the element and the event +as parameters. this refers to the controller instance. For example: + + $.Controller("Tabs",{ + + // li - the list element that was clicked + // ev - the click event + "li click" : function(li, ev){ + this.tab(li).hide() + }, + tab : function(li){ + return $(li.find("a").attr("href")) + } + }) + +## Templated Event Bindings + +One of Controller's most powerful features is templated event +handlers. You can parameterize the event name, +the selector, or event the root element. + +### Templating event names and selectors: + +Often, you want to make a widget's behavior +configurable. A common example is configuring which event +a menu should show a sub-menu (ex: on click or mouseenter). The +following controller lets you configure when a menu should show +sub-menus: + +The following makes two buttons. One says hello on click, +the other on a 'tap' event. + + $.Controller("Menu",{ + "li {showEvent}" : function(el){ + el.children('ul').show() + } + }) + + $("#clickMe").menu({showEvent : "click"}); + $("#touchMe").menu({showEvent : "mouseenter"}); + +$.Controller replaces value in {} with +values in a +controller's [jQuery.Controller.prototype.options options]. This means +we can easily provide a default showEvent value and create +a menu without providing a value like: + + $.Controller("Menu", + { + defaults : { + showEvent : "click" + } + }, + { + "li {showEvent}" : function(el){ + el.children('ul').show() + } + }); + + $("#clickMe").menu(); //defaults to using click + +Sometimes, we might might want to configure our widget to +use different elements. The following makes the menu widget's +button elements configurable: + + $.Controller("Menu",{ + "{button} {showEvent}" : function(el){ + el.children('ul').show() + } + }) + + $('#buttonMenu').menu({button: "button"}); + +### Templating the root element. + +Finally, controller lets you bind to objects outside +of the [jQuery.Controller.prototype.element controller's element]. + +The following listens to clicks on the window: + + $.Controller("HideOnClick",{ + "{window} click" : function(){ + this.element.hide() + } + }) + +The following listens to Todos being created: + + $.Controller("NewTodos",{ + "{App.Models.Todo} created" : function(Todo, ev, newTodo){ + this.element.append("newTodos.ejs", newTodo) + } + }); + +But instead of making NewTodos only work with the Todo model, +we can make it configurable: + + $.Controller("Newbie",{ + "{model} created" : function(Model, ev, newItem){ + this.element.append(this.options.view, newItem) + } + }); + + $('#newItems').newbie({ + model: App.Models.Todo, + view: "newTodos.ejs" + }) + +### How Templated events work + +When looking up a value to replace {}, +controller first looks up the item in the options, then it looks +up the value in the window object. It does not use eval to look up the +object. Instead it uses [jQuery.String.getObject]. + + +## Subscribing to OpenAjax messages and custom bindings + +The jquery/controller/subscribe plugin allows controllers to listen +to OpenAjax.hub messages like: + + $.Controller("Listener",{ + "something.updated subscribe" : function(called, data){ + + } + }) + +You can create your own binders by adding to [jQuery.Controller.static.processors]. + +## Manually binding to events. + +The [jQuery.Controller.prototype.bind] and [jQuery.Controller.prototype.delegate] +methods let you listen to events on other elements. These event handlers will +be unbound when the controller instance is destroyed. + diff --git a/controller/pages/plugin.md b/controller/pages/plugin.md new file mode 100644 index 00000000..d3ee520f --- /dev/null +++ b/controller/pages/plugin.md @@ -0,0 +1,101 @@ +@page jquery.controller.plugin The generated jQuery plugin +@parent jQuery.Controller + +When you create a controller, it creates a jQuery plugin that can be +used to: + + - Create controllers on an element or elements + - Call controller methods + - Update a controller + +For example, the following controller: + + $.Controller("My.Widget",{ + say : function(){ + alert(this.options.message); + } + }) + +creates a jQuery.fn.my_widget method that you can use like: + + // create my_widget on each .thing + $(".thing").my_widget({message : "Hello"}) + + // alerts "Hello" + $(".thing").my_widget("say"); + + // updates the message option + $(".thing").my_widget({message : "World"}); + + // alerts "World" + $(".thing").my_widget("say"); + +Note that in every case, the my_widget plugin +returns the original jQuery collection for chaining ($('.thing')). If you want to +get a value from a controller, use the [jQuery.fn.controllers] or [jQuery.fn.controller]. + +## Creating controllers + +When a controller's jQuery plugin helper is used on a jQuery collection, it goes to each +element and tests if it has a controller instance on the element. If it does not, it creates one. + +It calls new YourController with the element and any additional arguments you passed +to the jQuery plugin helper. So for example, say there are 2 elements in $('.thing'). + +This: + + $(".thing").my_widget({message : "Hello"}) + +Does the exact same thing as: + + var things = $('.thing'), + options = {message : "Hello"}; + new My.Widget(things[0],options); + new My.Widget(things[1],options); + +Note, when a new Class is created, it calls your +class's prototype setup and init methods. Read [jQuery.Controller.prototype.setup controller's setup] +for the details on what happens when a new controller is created. + + +## Calling methods on controllers + +Once a Controller is already on an element, you can call methods on it with the same jQuery +helper. The first param to the helper is the name of the method, the following params are +passed to the jQuery function. For example: + + $.Controller("Adder",{ + sum : function(first, second, third){ + this.element.text(first+second+third); + } + }) + + // add an adder to the page + $("#myadder").adder() + + // show the sum of 1+2+3 + $("#myadder").adder("sum",1,2,3); + +## Naming + +By default, a controller's jQuery helper is the controller name: + + - [jQuery.String.underscore underscored] + - "." replaced with "_" + - with Controllers removed. + +Here are some examples: + + $.Controller("Foo") // -> .foo() + $.Controller("Foo.Bar") // -> .foo_bar() + $.Controller("Foo.Controllers.Bar") // -> .foo_bar() + +You can overwrite the Controller's default name by setting a static pluginName property: + + $.Controller("My.Tabs", + { + pluginName: "tabs" + }, + { ... }) + + $("#tabs").tabs() diff --git a/controller/qunit.html b/controller/qunit.html index 909f3130..ae8fad54 100644 --- a/controller/qunit.html +++ b/controller/qunit.html @@ -6,7 +6,7 @@ margin: 0px; padding: 0px; } - + diff --git a/controller/history/qunit.html b/controller/route/qunit.html similarity index 55% rename from controller/history/qunit.html rename to controller/route/qunit.html index d4edfd1e..25edc491 100644 --- a/controller/history/qunit.html +++ b/controller/route/qunit.html @@ -1,16 +1,13 @@ + - - + route QUnit Test + -

    Model Store Cookie Test Suite

    +

    route Test Suite

    diff --git a/controller/route/route.html b/controller/route/route.html new file mode 100644 index 00000000..178af929 --- /dev/null +++ b/controller/route/route.html @@ -0,0 +1,35 @@ + + + + route + + + +

    route Demo

    + foo/bar + foo/car + empty + + + + \ No newline at end of file diff --git a/controller/route/route.js b/controller/route/route.js new file mode 100644 index 00000000..bc0f1c67 --- /dev/null +++ b/controller/route/route.js @@ -0,0 +1,31 @@ +steal('jquery/dom/route','jquery/controller', function(){ + /** + * + * ":type route" // + * + * @param {Object} el + * @param {Object} event + * @param {Object} selector + * @param {Object} cb + */ + jQuery.Controller.processors.route = function(el, event, selector, funcName, controller){ + $.route(selector||"") + var batchNum; + var check = function(ev, attr, how){ + if($.route.attr('route') === (selector||"") && + (ev.batchNum === undefined || ev.batchNum !== batchNum ) ){ + + batchNum = ev.batchNum; + + var d = $.route.attrs(); + delete d.route; + + controller[funcName](d) + } + } + $.route.bind('change',check); + return function(){ + $.route.unbind('change',check) + } + } +}) diff --git a/controller/route/route_test.js b/controller/route/route_test.js new file mode 100644 index 00000000..6a140d6b --- /dev/null +++ b/controller/route/route_test.js @@ -0,0 +1,10 @@ +steal('funcunit/qunit','./route',function(){ + +module("route"); + +test("route testing works", function(){ + ok(true,"an assert is run"); +}); + + +}); \ No newline at end of file diff --git a/controller/subscribe/subscribe.html b/controller/subscribe/subscribe.html index fd4dc090..a26d72d5 100644 --- a/controller/subscribe/subscribe.html +++ b/controller/subscribe/subscribe.html @@ -28,29 +28,29 @@

    Turn OFF Above

    diff --git a/controller/subscribe/subscribe.js b/controller/subscribe/subscribe.js index bf5cb419..26280343 100644 --- a/controller/subscribe/subscribe.js +++ b/controller/subscribe/subscribe.js @@ -1,14 +1,39 @@ /*global OpenAjax: true */ -steal.plugins('jquery', 'jquery/controller', 'jquery/lang/openajax').then(function() { +steal('jquery/controller', 'jquery/lang/openajax').then(function() { /** - * Adds open ajax subscribing to controllers. + * @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, cb); + var subscription = OpenAjax.hub.subscribe(selector, function(){ + return controller[cb].apply(controller, arguments) + }); return function() { - var sub = subscription; - OpenAjax.hub.unsubscribe(sub); + OpenAjax.hub.unsubscribe(subscription); }; }; @@ -17,6 +42,8 @@ steal.plugins('jquery', 'jquery/controller', 'jquery/lang/openajax').then(functi */ //breaker /** + * @function publish + * @hide * Publishes a message to OpenAjax.hub. * @param {String} message Message name, ex: "Something.Happened". * @param {Object} data The data sent. diff --git a/controller/test/qunit/controller_test.js b/controller/test/qunit/controller_test.js deleted file mode 100644 index 210685a0..00000000 --- a/controller/test/qunit/controller_test.js +++ /dev/null @@ -1,170 +0,0 @@ -module("jquery/controller") -test("subscribe testing works", function(){ - - var ta = $("
    ").appendTo( $("#qunit-test-area") ) - - ta.html("click here") - - var clicks = 0, destroys = 0; - var subscribes = 0; - $.Controller.extend("MyTest",{ - click: function() { - clicks++ - }, - "a.b subscribe" : function() { - subscribes++ - }, - destroy: function() { - - this._super() - destroys++; - } - }) - ta.my_test(); - ta.trigger("click") - equals(clicks,1, "can listen to clicks") - - OpenAjax.hub.publish("a.b",{}) - equals(subscribes,1, "can subscribe") - var controllerInstance = ta.controller('my_test') - ok( controllerInstance.Class == MyTest, "can get controller" ) - controllerInstance.destroy() - - equals(destroys,1, "destroy called once") - ok(!ta.controller(), "controller is removed") - - OpenAjax.hub.publish("a.b",{}) - equals(subscribes,1, "subscription is torn down") - ta.trigger("click") - equals(clicks,1, "No longer listening") - - - - ta.my_test(); - ta.trigger("click") - OpenAjax.hub.publish("a.b",{}) - equals(clicks,2, "can listen again to clicks") - equals(subscribes,2, "can listen again to subscription") - - ta.remove(); - - ta.trigger("click") - OpenAjax.hub.publish("a.b",{}) - equals(clicks,2, "Clicks stopped") - equals(subscribes,2, "Subscribes stopped") -}) - - -test("document and main controllers", function(){ - var a = $("
    ").appendTo($("#qunit-test-area")), - a_inner = a.find('span'), - b = $("
    ").appendTo($("#qunit-test-area")), - b_inner = b.find('span'), - doc_outer_clicks = 0, - doc_inner_clicks = 0, - main_outer_clicks = 0, - main_inner_clicks = 0; - - $.Controller.extend("TestController", { onDocument: true }, { - click: function() { - doc_outer_clicks++; - }, - "span click" : function() { - doc_inner_clicks++; - } - }) - - a_inner.trigger("click"); - equals(doc_outer_clicks,1,"document controller handled (no-selector) click inside listening element"); - equals(doc_inner_clicks,1,"document controller handled (selector) click inside listening element"); - - b_inner.trigger("click"); - equals(doc_outer_clicks,1,"document controller ignored (no-selector) click outside listening element"); - equals(doc_inner_clicks,1,"document controller ignored (selector) click outside listening element"); - - $(document.documentElement).controller('test').destroy(); - - $.Controller.extend("MainController", { onDocument: true }, { - click: function() { - main_outer_clicks++; - }, - "span click" : function() { - main_inner_clicks++; - } - }) - - b_inner.trigger("click"); - equals(main_outer_clicks,1,"main controller handled (no-selector) click"); - equals(main_inner_clicks,1,"main controller handled (selector) click"); - - $(document.documentElement).controller('main').destroy(); - - a.remove(); - b.remove(); -}) - - -test("bind to any special", function(){ - jQuery.event.special.crazyEvent = { - - } - var called = false; - jQuery.Controller.extend("WeirdBind",{ - crazyEvent: function() { - called = true; - } - }) - var a = $("
    ").appendTo($("#qunit-test-area")) - a.weird_bind(); - a.trigger("crazyEvent") - ok(called, "heard the trigger"); - - $("#qunit-test-area").html("") - -}) - -test("parameterized actions", function(){ - var called = false; - jQuery.Controller.extend("WeirderBind",{ - "{parameterized}" : function() { - called = true; - } - }) - var a = $("
    ").appendTo($("#qunit-test-area")) - a.weirder_bind({parameterized: "sillyEvent"}); - a.trigger("sillyEvent") - ok(called, "heard the trigger") - - $("#qunit-test-area").html("") -}) - -test("windowresize", function(){ - var called = false; - jQuery.Controller.extend("WindowBind",{ - "windowresize" : function() { - called = true; - } - }) - $("#qunit-test-area").html("
    ") - $("#weird").window_bind(); - $(window).trigger('resize') - ok(called,"got window resize event"); - - $("#qunit-test-area").html("") -}) - -// this.delegate(this.cached.header.find('tr'), "th", "mousemove", "th_mousemove"); -test("delegate", function(){ - var called = false; - jQuery.Controller.extend("DelegateTest",{ - click: function() {} - }) - var els = $("").appendTo($("#qunit-test-area")) - var c = els.delegate_test(); - c.controller().delegate(els.find("span"), "a", "click", function(){ - called = true; - }) - els.find("a").trigger('click') - ok(called, "delegate works") - $("#qunit-test-area").html("") -}) diff --git a/controller/test/qunit/qunit.js b/controller/test/qunit/qunit.js deleted file mode 100644 index 611a8519..00000000 --- a/controller/test/qunit/qunit.js +++ /dev/null @@ -1,9 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/controller",'jquery/controller/subscribe') //load your app - .plugins('funcunit/qunit') //load qunit - .then("controller_test") - -if(steal.browser.rhino){ - steal.plugins('funcunit/qunit/env') -} \ No newline at end of file diff --git a/controller/view/qunit.html b/controller/view/qunit.html index cfb5649a..cc57a785 100644 --- a/controller/view/qunit.html +++ b/controller/view/qunit.html @@ -6,7 +6,7 @@ margin: 0px; padding: 0px; } - + diff --git a/controller/view/test/qunit/controller_view_test.js b/controller/view/test/qunit/controller_view_test.js index d35ce47c..506da897 100644 --- a/controller/view/test/qunit/controller_view_test.js +++ b/controller/view/test/qunit/controller_view_test.js @@ -1,15 +1,50 @@ -module("jquery/controller/view") -test("this.view", function(){ +steal('jquery/controller/view','jquery/view/micro','funcunit/qunit') //load qunit + .then(function(){ - $.Controller.extend("jquery.Controller.View.Test.Qunit",{ - init: function() { - this.element.html(this.view()) - } - }) - jQuery.View.ext = ".micro"; - $("#qunit-test-area").append("
    "); + module("jquery/controller/view"); + + test("this.view", function(){ + + $.Controller.extend("jquery.Controller.View.Test.Qunit",{ + init: function() { + this.element.html(this.view()) + } + }) + jQuery.View.ext = ".micro"; + $("#qunit-test-area").append("
    "); + + new jquery.Controller.View.Test.Qunit( $('#cont_view') ); + + ok(/Hello World/i.test($('#cont_view').text()),"view rendered") + }); - new jquery.Controller.View.Test.Qunit( $('#cont_view') ); + test("test.suffix.doubling", function(){ + + $.Controller.extend("jquery.Controller.View.Test.Qunit",{ + init: function() { + this.element.html(this.view('init.micro')) + } + }) + + jQuery.View.ext = ".ejs"; // Reset view extension to default + equal(".ejs", jQuery.View.ext); + + $("#qunit-test-area").append("
    "); + + new jquery.Controller.View.Test.Qunit( $('#suffix_test_cont_view') ); + + ok(/Hello World/i.test($('#suffix_test_cont_view').text()),"view rendered") + }); - ok(/Hello World/i.test($('#cont_view').text()),"view rendered") -}); \ No newline at end of file + test("complex paths nested inside a controller directory", function(){ + $.Controller.extend("Myproject.Controllers.Foo.Bar"); + + var path = jQuery.Controller._calculatePosition(Myproject.Controllers.Foo.Bar, "init.ejs", "init") + equals(path, "//myproject/views/foo/bar/init.ejs", "view path is correct") + + $.Controller.extend("Myproject.Controllers.FooBar"); + path = jQuery.Controller._calculatePosition(Myproject.Controllers.FooBar, "init.ejs", "init") + equals(path, "//myproject/views/foo_bar/init.ejs", "view path is correct") + }) +}); + diff --git a/controller/view/test/qunit/qunit.js b/controller/view/test/qunit/qunit.js index 38255d8c..7873c312 100644 --- a/controller/view/test/qunit/qunit.js +++ b/controller/view/test/qunit/qunit.js @@ -1,6 +1,5 @@ //we probably have to have this only describing where the tests are -steal - .plugins('jquery/controller/view','jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("controller_view_test") +steal('jquery/controller/view','jquery/view/micro') //load your app + .then('funcunit/qunit') //load qunit + .then("./controller_view_test.js") diff --git a/controller/view/view.js b/controller/view/view.js index 8872d138..c248fa5c 100644 --- a/controller/view/view.js +++ b/controller/view/view.js @@ -1,23 +1,30 @@ -steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { +steal('jquery/controller', 'jquery/view').then(function( $ ) { + var URI = steal.URI || steal.File; + jQuery.Controller.getFolder = function() { return jQuery.String.underscore(this.fullName.replace(/\./g, "/")).replace("/Controllers", ""); }; - var calculatePosition = function( Class, view, action_name ) { - var slashes = Class.fullName.replace(/\./g, "/"), - hasControllers = slashes.indexOf("/Controllers/" + Class.shortName) != -1, - path = jQuery.String.underscore(slashes.replace("/Controllers/" + Class.shortName, "")), - controller_name = Class._shortName, - suffix = (typeof view == "string" && view.match(/\.[\w\d]+$/)) || jQuery.View.ext; + jQuery.Controller._calculatePosition = function( Class, view, action_name ) { + + var classParts = Class.fullName.split('.'), + classPartsWithoutPrefix = classParts.slice(0); + classPartsWithoutPrefix.splice(0, 2); // Remove prefix (usually 2 elements) + var classPartsWithoutPrefixSlashes = classPartsWithoutPrefix.join('/'), + hasControllers = (classParts.length > 2) && classParts[1] == 'Controllers', + path = hasControllers? jQuery.String.underscore(classParts[0]): jQuery.String.underscore(classParts.join("/")), + controller_name = jQuery.String.underscore(classPartsWithoutPrefix.join('/')).toLowerCase(), + suffix = (typeof view == "string" && /\.[\w\d]+$/.test(view)) ? "" : jQuery.View.ext; + //calculate view if ( typeof view == "string" ) { if ( view.substr(0, 2) == "//" ) { //leave where it is } else { - view = "//" + new steal.File('views/' + (view.indexOf('/') !== -1 ? view : (hasControllers ? controller_name + '/' : "") + view)).joinFrom(path) + suffix; + view = "//" + URI(path).join( 'views/' + (view.indexOf('/') !== -1 ? view : (hasControllers ? controller_name + '/' : "") + view)) + suffix; } } else if (!view ) { - view = "//" + new steal.File('views/' + (hasControllers ? controller_name + '/' : "") + action_name.replace(/\.|#/g, '').replace(/ /g, '_')).joinFrom(path) + suffix; + view = "//" + URI(path).join('views/' + (hasControllers ? controller_name + '/' : "") + action_name.replace(/\.|#/g, '').replace(/ /g, '_'))+ suffix; } return view; }; @@ -38,14 +45,16 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { } //load from name var current = window; - var parts = this.Class.fullName.split(/\./); + var parts = this.constructor.fullName.split(/\./); for ( var i = 0; i < parts.length; i++ ) { - if ( typeof current.Helpers == 'object' ) { - jQuery.extend(helpers, current.Helpers); + if(current){ + if ( typeof current.Helpers == 'object' ) { + jQuery.extend(helpers, current.Helpers); + } + current = current[parts[i]]; } - current = current[parts[i]]; } - if ( typeof current.Helpers == 'object' ) { + if (current && typeof current.Helpers == 'object' ) { jQuery.extend(helpers, current.Helpers); } this._default_helpers = helpers; @@ -71,18 +80,20 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { * el.html( this.view() ) * // renders with views/tasks/under.ejs * el.after( this.view("under", [1,2]) ); + * // renders with views/tasks/under.micro + * el.after( this.view("under.micro", [1,2]) ); * // renders with views/shared/top.ejs * el.before( this.view("shared/top", {phrase: "hi"}) ); * } * }) * @codeend - * @plugin controller/view + * @plugin jquery/controller/view * @return {String} the rendered result of the view. - * @param {String} [optional1] view The view you are going to render. If a view isn't explicity given + * @param {String} [view] The view you are going to render. If a view isn't explicity given * this function will try to guess at the correct view as show in the example code above. - * @param {Object} [optional2] data data to be provided to the view. If not present, the controller instance + * @param {Object} [data] data to be provided to the view. If not present, the controller instance * is used. - * @param {Object} [optional3] myhelpers an object of helpers that will be available in the view. If not present + * @param {Object} [myhelpers] an object of helpers that will be available in the view. If not present * this controller class's "Helpers" property will be used. * */ @@ -94,7 +105,7 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { view = null; } //guess from controller name - view = calculatePosition(this.Class, view, this.called); + view = jQuery.Controller._calculatePosition(this.Class, view, this.called); //calculate data data = data || this; diff --git a/dist/jquery.class.js b/dist/jquery.class.js deleted file mode 100644 index 7fe40421..00000000 --- a/dist/jquery.class.js +++ /dev/null @@ -1,626 +0,0 @@ -// jquery/class/class.js - -(function($){ - - - // if we are initializing a new class - var initializing = false, - - // tests if we can get super in .toString() - fnTest = /xyz/.test(function() { - xyz; - }) ? /\b_super\b/ : /.*/, - - // overwrites an object with methods, sets up _super - inheritProps = function( newProps, oldProps, addTo ) { - addTo = addTo || newProps - for ( var name in newProps ) { - // Check if we're overwriting an existing function - addTo[name] = typeof newProps[name] == "function" && typeof oldProps[name] == "function" && 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]; - } - }; - - - /** - * @class jQuery.Class - * @plugin jquery/class - * @tag core - * @download dist/jquery/jquery.class.js - * @test jquery/class/qunit.html - * Class provides simulated inheritance in JavaScript. Use $.Class 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
    • - *
    - *

    Static v. Prototype

    - *

    Before learning about Class, it's important to - * understand the difference between - * a class's static and prototype properties. - *

    - * @codestart - * //STATIC - * MyClass.staticProperty //shared property - * - * //PROTOTYPE - * myclass = new MyClass() - * myclass.prototypeMethod() //instance method - * @codeend - *

    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.extend('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.Class.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 init 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 - * this._super. Lets create a SeaMonster class. SeaMonsters are less - * efficient at eating small children, but more powerful fighters. - *

    - * @codestart - * Monster.extend("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 - * @codeend - *

    Static property inheritance

    - * You can also inherit static properties in the same way: - * @codestart - * $.Class.extend("First", - * { - * staticMethod: function() { return 1;} - * },{}) - * - * First.extend("Second",{ - * staticMethod: function() { return this._super()+1;} - * },{}) - * - * Second.staticMethod() // -> 2 - * @codeend - *

    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: - *

    - * @codestart - * $.Class.extend("MyNamespace.MyClass",{},{}); - * - * new MyNamespace.MyClass() - * @codeend - *

    Introspection

    - * 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. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's - * static properties. - * - * - *

    Setup and initialization methods

    - *

    - * 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. - *

    - *
    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. - * - *
    - * @codestart - * $.Class.extend("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. - *

    - * @codestart - * $.Class.extend("jQuery.Controller",{ - * ... - * },{ - * setup: function( el, options ) { - * ... - * return [$(el), - * $.extend(true, - * this.Class.defaults, - * options || {} ) ] - * } - * }) - * @codeend - * 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 init method - * gets called in the following example: - *

    - * @codestart - * $.Class.Extend("Foo", { - * init: function( arg1, arg2, arg3 ) { - * this.sum = arg1+arg2+arg3; - * } - * }) - * var foo = new Foo(1,2,3); - * foo.sum //-> 6 - * @codeend - *

    Callbacks

    - *

    Similar to jQuery's proxy method, Class provides a - * [jQuery.Class.static.callback callback] - * function that returns a callback to a method that will always - * have - * this set to the class or instance of the class. - *

    - * The following example uses this.callback to make sure - * this.name is available in show. - * @codestart - * $.Class.extend("Todo",{ - * init: function( name ) { this.name = name } - * get: function() { - * $.get("/stuff",this.callback('show')) - * }, - * show: function( txt ) { - * alert(this.name+txt) - * } - * }) - * new Todo("Trash").get() - * @codeend - *

    Callback is available as a static and prototype method.

    - *

    Demo

    - * @demo jquery/class/class.html - * - * @init Creating a new instance of an object that has extended jQuery.Class - * calls the init prototype function and returns a new instance of the class. - * - */ - - jQuery.Class = function() { - if ( arguments.length ) this.extend.apply(this, arguments) - }; - - /* @Static*/ - $.extend($.Class, { - /** - * @function callback - * Returns a callback function for a function on this Class. - * The callback function ensures that 'this' is set appropriately. - * @codestart - * $.Class.extend("MyClass",{ - * getData: function() { - * this.showing = null; - * $.get("data.json",this.callback('gotData'),'json') - * }, - * gotData: function( data ) { - * this.showing = data; - * } - * },{}); - * MyClass.showData(); - * @codeend - *

    Currying Arguments

    - * Additional arguments to callback will fill in arguments on the returning function. - * @codestart - * $.Class.extend("MyClass",{ - * getData: function( callback ) { - * $.get("data.json",this.callback('process',callback),'json'); - * }, - * process: function( callback, jsonData ) { //callback is added as first argument - * jsonData.processed = true; - * callback(jsonData); - * } - * },{}); - * MyClass.getData(showDataFunc) - * @codeend - *

    Nesting Functions

    - * Callback 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.extend("MyClass",{ - * getData: function( callback ) { - * //calls process, then callback with value from process - * $.get("data.json",this.callback(['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. - */ - callback: function( funcs ) { - - //args that should be curried - var args = jQuery.makeArray(arguments), - self; - - funcs = args.shift(); - - if (!jQuery.isArray(funcs) ) { - funcs = [funcs]; - } - - self = this; - - return function class_cb() { - var cur = args.concat(jQuery.makeArray(arguments)), - isString, length = funcs.length, - f = 0, - func; - - for (; f < length; f++ ) { - func = funcs[f]; - if (!func ) { - continue; - } - - isString = typeof func == "string"; - if ( isString && self._set_called ) { - self.called = func; - } - cur = (isString ? self[func] : func).apply(self, cur || []); - if ( f < length - 1 ) { - cur = !jQuery.isArray(cur) || cur._use_call ? [cur] : cur - } - } - return cur; - } - }, - /** - * @function getObject - * Gets an object from a String. - * If the object or namespaces the string represent do not - * exist it will create them. - * @codestart - * Foo = {Bar: {Zar: {"Ted"}}} - * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" - * @codeend - * @param {String} objectName the object you want to get - * @param {Object} [current=window] the object you want to look in. - * @return {Object} the object you are looking for. - */ - getObject: function( objectName, current ) { - var current = current || window, - parts = objectName ? objectName.split(/\./) : [], - i = 0; - for (; i < parts.length; i++ ) { - current = current[parts[i]] || (current[parts[i]] = {}) - } - return current; - }, - /** - * @function newInstance - * Creates a new instance of the class. This method is useful for creating new instances - * with arbitrary parameters. - *

    Example

    - * @codestart - * $.Class.extend("MyClass",{},{}) - * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) - * @codeend - * @return {class} instance of the class - */ - newInstance: function() { - var inst = this.rawInstance(), - args; - if ( inst.setup ) { - args = inst.setup.apply(inst, arguments); - } - if ( inst.init ) { - inst.init.apply(inst, $.isArray(args) ? args : arguments); - } - return inst; - }, - /** - * Copy and overwrite options from old class - * @param {Object} oldClass - * @param {String} fullName - * @param {Object} staticProps - * @param {Object} protoProps - */ - setup: function( oldClass, fullName ) { - this.defaults = $.extend(true, {}, oldClass.defaults, this.defaults); - return arguments; - }, - rawInstance: function() { - initializing = true; - var inst = new this(); - initializing = false; - return inst; - }, - /** - * Extends a class with new static and prototype functions. There are a variety of ways - * to use extend: - * @codestart - * //with className, static and prototype functions - * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) - * //with just classname and prototype functions - * $.Class.extend('Task',{ PROTOTYPE }) - * //With just a className - * $.Class.extend('Task') - * @codeend - * @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 - if ( typeof fullName != 'string' ) { - proto = klass; - klass = fullName; - fullName = null; - } - if (!proto ) { - proto = klass; - klass = null; - } - - proto = proto || {}; - var _super_class = this, - _super = this.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; - - if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new - return this.extend.apply(this, 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) && $.inArray(name, ['prototype', 'defaults', 'getObject']) == -1 ) { - Class[name] = this[name]; - } - } - - // do static inheritance - inheritProps(klass, this, Class); - - // do namespace stuff - if ( fullName ) { - - var parts = fullName.split(/\./), - shortName = parts.pop(), - current = $.Class.getObject(parts.join('.')), - namespace = current; - - - current[shortName] = Class; - } - - // set things that can't be overwritten - $.extend(Class, { - prototype: prototype, - namespace: namespace, - shortName: shortName, - constructor: Class, - fullName: fullName - }); - - //make sure our prototype looks nice - Class.prototype.Class = Class.prototype.constructor = Class; - - - /** - * @attribute fullName - * The full name of the class, including namespace, provided for introspection purposes. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - */ - - var args = Class.setup.apply(Class, [_super_class].concat($.makeArray(arguments))); - - if ( Class.init ) { - Class.init.apply(Class, args || []); - } - - /* @Prototype*/ - return Class; - /** - * @function setup - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * setup: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend - * - *
    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. - *
    - * - * @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 - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * init: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend - */ - //Breaks up code - /** - * @attribute Class - * References the static properties of the instance's class. - *

    Quick Example

    - * @codestart - * // a class with a static classProperty property - * $.Class.extend("MyClass", {classProperty : true}, {}); - * - * // a new instance of myClass - * var mc1 = new MyClass(); - * - * // - * mc1.Class.classProperty = false; - * - * // creates a new MyClass - * var mc2 = new mc.Class(); - * @codeend - * Getting static properties via the Class property, such as it's - * [jQuery.Class.static.fullName fullName] is very common. - */ - } - - }) - - - - - - jQuery.Class.prototype. - /** - * @function callback - * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. - * The only difference is this callback 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 - */ - callback = jQuery.Class.callback; - - - -})(true); - diff --git a/dist/jquery.controller.js b/dist/jquery.controller.js deleted file mode 100644 index 694fba4c..00000000 --- a/dist/jquery.controller.js +++ /dev/null @@ -1,1559 +0,0 @@ -// jquery/class/class.js - -(function($){ - - - // if we are initializing a new class - var initializing = false, - - // tests if we can get super in .toString() - fnTest = /xyz/.test(function() { - xyz; - }) ? /\b_super\b/ : /.*/, - - // overwrites an object with methods, sets up _super - inheritProps = function( newProps, oldProps, addTo ) { - addTo = addTo || newProps - for ( var name in newProps ) { - // Check if we're overwriting an existing function - addTo[name] = typeof newProps[name] == "function" && typeof oldProps[name] == "function" && 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]; - } - }; - - - /** - * @class jQuery.Class - * @plugin jquery/class - * @tag core - * @download dist/jquery/jquery.class.js - * @test jquery/class/qunit.html - * Class provides simulated inheritance in JavaScript. Use $.Class 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
    • - *
    - *

    Static v. Prototype

    - *

    Before learning about Class, it's important to - * understand the difference between - * a class's static and prototype properties. - *

    - * @codestart - * //STATIC - * MyClass.staticProperty //shared property - * - * //PROTOTYPE - * myclass = new MyClass() - * myclass.prototypeMethod() //instance method - * @codeend - *

    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.extend('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.Class.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 init 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 - * this._super. Lets create a SeaMonster class. SeaMonsters are less - * efficient at eating small children, but more powerful fighters. - *

    - * @codestart - * Monster.extend("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 - * @codeend - *

    Static property inheritance

    - * You can also inherit static properties in the same way: - * @codestart - * $.Class.extend("First", - * { - * staticMethod: function() { return 1;} - * },{}) - * - * First.extend("Second",{ - * staticMethod: function() { return this._super()+1;} - * },{}) - * - * Second.staticMethod() // -> 2 - * @codeend - *

    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: - *

    - * @codestart - * $.Class.extend("MyNamespace.MyClass",{},{}); - * - * new MyNamespace.MyClass() - * @codeend - *

    Introspection

    - * 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. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's - * static properties. - * - * - *

    Setup and initialization methods

    - *

    - * 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. - *

    - *
    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. - * - *
    - * @codestart - * $.Class.extend("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. - *

    - * @codestart - * $.Class.extend("jQuery.Controller",{ - * ... - * },{ - * setup: function( el, options ) { - * ... - * return [$(el), - * $.extend(true, - * this.Class.defaults, - * options || {} ) ] - * } - * }) - * @codeend - * 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 init method - * gets called in the following example: - *

    - * @codestart - * $.Class.Extend("Foo", { - * init: function( arg1, arg2, arg3 ) { - * this.sum = arg1+arg2+arg3; - * } - * }) - * var foo = new Foo(1,2,3); - * foo.sum //-> 6 - * @codeend - *

    Callbacks

    - *

    Similar to jQuery's proxy method, Class provides a - * [jQuery.Class.static.callback callback] - * function that returns a callback to a method that will always - * have - * this set to the class or instance of the class. - *

    - * The following example uses this.callback to make sure - * this.name is available in show. - * @codestart - * $.Class.extend("Todo",{ - * init: function( name ) { this.name = name } - * get: function() { - * $.get("/stuff",this.callback('show')) - * }, - * show: function( txt ) { - * alert(this.name+txt) - * } - * }) - * new Todo("Trash").get() - * @codeend - *

    Callback is available as a static and prototype method.

    - *

    Demo

    - * @demo jquery/class/class.html - * - * @init Creating a new instance of an object that has extended jQuery.Class - * calls the init prototype function and returns a new instance of the class. - * - */ - - jQuery.Class = function() { - if ( arguments.length ) this.extend.apply(this, arguments) - }; - - /* @Static*/ - $.extend($.Class, { - /** - * @function callback - * Returns a callback function for a function on this Class. - * The callback function ensures that 'this' is set appropriately. - * @codestart - * $.Class.extend("MyClass",{ - * getData: function() { - * this.showing = null; - * $.get("data.json",this.callback('gotData'),'json') - * }, - * gotData: function( data ) { - * this.showing = data; - * } - * },{}); - * MyClass.showData(); - * @codeend - *

    Currying Arguments

    - * Additional arguments to callback will fill in arguments on the returning function. - * @codestart - * $.Class.extend("MyClass",{ - * getData: function( callback ) { - * $.get("data.json",this.callback('process',callback),'json'); - * }, - * process: function( callback, jsonData ) { //callback is added as first argument - * jsonData.processed = true; - * callback(jsonData); - * } - * },{}); - * MyClass.getData(showDataFunc) - * @codeend - *

    Nesting Functions

    - * Callback 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.extend("MyClass",{ - * getData: function( callback ) { - * //calls process, then callback with value from process - * $.get("data.json",this.callback(['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. - */ - callback: function( funcs ) { - - //args that should be curried - var args = jQuery.makeArray(arguments), - self; - - funcs = args.shift(); - - if (!jQuery.isArray(funcs) ) { - funcs = [funcs]; - } - - self = this; - - return function class_cb() { - var cur = args.concat(jQuery.makeArray(arguments)), - isString, length = funcs.length, - f = 0, - func; - - for (; f < length; f++ ) { - func = funcs[f]; - if (!func ) { - continue; - } - - isString = typeof func == "string"; - if ( isString && self._set_called ) { - self.called = func; - } - cur = (isString ? self[func] : func).apply(self, cur || []); - if ( f < length - 1 ) { - cur = !jQuery.isArray(cur) || cur._use_call ? [cur] : cur - } - } - return cur; - } - }, - /** - * @function getObject - * Gets an object from a String. - * If the object or namespaces the string represent do not - * exist it will create them. - * @codestart - * Foo = {Bar: {Zar: {"Ted"}}} - * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" - * @codeend - * @param {String} objectName the object you want to get - * @param {Object} [current=window] the object you want to look in. - * @return {Object} the object you are looking for. - */ - getObject: function( objectName, current ) { - var current = current || window, - parts = objectName ? objectName.split(/\./) : [], - i = 0; - for (; i < parts.length; i++ ) { - current = current[parts[i]] || (current[parts[i]] = {}) - } - return current; - }, - /** - * @function newInstance - * Creates a new instance of the class. This method is useful for creating new instances - * with arbitrary parameters. - *

    Example

    - * @codestart - * $.Class.extend("MyClass",{},{}) - * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) - * @codeend - * @return {class} instance of the class - */ - newInstance: function() { - var inst = this.rawInstance(), - args; - if ( inst.setup ) { - args = inst.setup.apply(inst, arguments); - } - if ( inst.init ) { - inst.init.apply(inst, $.isArray(args) ? args : arguments); - } - return inst; - }, - /** - * Copy and overwrite options from old class - * @param {Object} oldClass - * @param {String} fullName - * @param {Object} staticProps - * @param {Object} protoProps - */ - setup: function( oldClass, fullName ) { - this.defaults = $.extend(true, {}, oldClass.defaults, this.defaults); - return arguments; - }, - rawInstance: function() { - initializing = true; - var inst = new this(); - initializing = false; - return inst; - }, - /** - * Extends a class with new static and prototype functions. There are a variety of ways - * to use extend: - * @codestart - * //with className, static and prototype functions - * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) - * //with just classname and prototype functions - * $.Class.extend('Task',{ PROTOTYPE }) - * //With just a className - * $.Class.extend('Task') - * @codeend - * @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 - if ( typeof fullName != 'string' ) { - proto = klass; - klass = fullName; - fullName = null; - } - if (!proto ) { - proto = klass; - klass = null; - } - - proto = proto || {}; - var _super_class = this, - _super = this.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; - - if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new - return this.extend.apply(this, 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) && $.inArray(name, ['prototype', 'defaults', 'getObject']) == -1 ) { - Class[name] = this[name]; - } - } - - // do static inheritance - inheritProps(klass, this, Class); - - // do namespace stuff - if ( fullName ) { - - var parts = fullName.split(/\./), - shortName = parts.pop(), - current = $.Class.getObject(parts.join('.')), - namespace = current; - - - current[shortName] = Class; - } - - // set things that can't be overwritten - $.extend(Class, { - prototype: prototype, - namespace: namespace, - shortName: shortName, - constructor: Class, - fullName: fullName - }); - - //make sure our prototype looks nice - Class.prototype.Class = Class.prototype.constructor = Class; - - - /** - * @attribute fullName - * The full name of the class, including namespace, provided for introspection purposes. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - */ - - var args = Class.setup.apply(Class, [_super_class].concat($.makeArray(arguments))); - - if ( Class.init ) { - Class.init.apply(Class, args || []); - } - - /* @Prototype*/ - return Class; - /** - * @function setup - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * setup: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend - * - *
    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. - *
    - * - * @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 - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * init: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend - */ - //Breaks up code - /** - * @attribute Class - * References the static properties of the instance's class. - *

    Quick Example

    - * @codestart - * // a class with a static classProperty property - * $.Class.extend("MyClass", {classProperty : true}, {}); - * - * // a new instance of myClass - * var mc1 = new MyClass(); - * - * // - * mc1.Class.classProperty = false; - * - * // creates a new MyClass - * var mc2 = new mc.Class(); - * @codeend - * Getting static properties via the Class property, such as it's - * [jQuery.Class.static.fullName fullName] is very common. - */ - } - - }) - - - - - - jQuery.Class.prototype. - /** - * @function callback - * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. - * The only difference is this callback 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 - */ - callback = jQuery.Class.callback; - - - -})(true); - -// jquery/lang/lang.js - -(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, - lowerUpper: /([a-z\d])([A-Z])/g, - dash: /([a-z\d])([A-Z])/g - }; - - /** - * @class jQuery.String - */ - var str = ($.String = /* @Static*/ { - /** - * @function strip - * @param {String} s returns a string with leading and trailing whitespace removed. - */ - strip: function( string ) { - return string.replace(/^\s+/, '').replace(/\s+$/, ''); - }, - /** - * Capitalizes a string - * @param {String} s the string to be lowercased. - * @return {String} a string with the first character capitalized, and everything else lowercased - */ - capitalize: function( s, cache ) { - return s.charAt(0).toUpperCase() + s.substr(1); - }, - - /** - * Returns if string ends with another string - * @param {String} s String that is being scanned - * @param {String} pattern What the string might end with - * @return {Boolean} true if the string ends wtih pattern, false if otherwise - */ - endsWith: function( s, pattern ) { - var d = s.length - pattern.length; - return d >= 0 && s.lastIndexOf(pattern) === d; - }, - /** - * 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 ) { - var parts = s.split(regs.undHash), - i = 1; - parts[0] = parts[0].charAt(0).toLowerCase() + parts[0].substr(1); - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(''); - }, - /** - * Like camelize, but the first part is also capitalized - * @param {String} s - * @return {String} the classized string - */ - classize: function( s ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(''); - }, - /** - * Like [jQuery.String.static.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 ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(' '); - }, - - /** - * 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.lowerUpper, '$1_$2'). - replace(regs.dash, '_').toLowerCase() - } - }); - - -})(true); - -// jquery/event/destroyed/destroyed.js - -(function($){ - - /** - * @attribute destroyed - * @parent specialevents - * @download jquery/dist/jquery.event.destroyed.js - * @test jquery/event/destroyed/qunit.html - * Provides a destroyed event on an element. - *

    - * 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. - *

    - *

    Quick Example

    - * @codestart - * $(".foo").bind("destroyed", function(){ - * //clean up code - * }) - * @codeend - *

    Quick Demo

    - * @demo jquery/event/destroyed/destroyed.html - *

    More Involved Demo

    - * @demo jquery/event/destroyed/destroyed_menu.html - */ - - var oldClean = jQuery.cleanData - - $.cleanData = function( elems ) { - for ( var i = 0, elem; - (elem = elems[i]) != null; i++ ) { - $(elem).triggerHandler("destroyed") - //$.event.remove( elem, 'destroyed' ); - } - oldClean(elems) - } - - -})(true); - -// jquery/controller/controller.js - -(function($){ - - - // ------- helpers ------ - // Binds an element, returns a function that unbinds - var bind = function( el, ev, callback ) { - var wrappedCallback; - //this is for events like >click. - if ( ev.indexOf(">") == 0 ) { - ev = ev.substr(1); - wrappedCallback = function( event ) { - event.target === el ? callback.apply(this, arguments) : event.handled = null; - } - } - $(el).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() { - $(el).unbind(ev, wrappedCallback || callback); - el = ev = callback = wrappedCallback = null; - } - }, - // Binds an element, returns a function that unbinds - delegate = function( el, selector, ev, callback ) { - $(el).delegate(selector, ev, callback); - return function() { - $(el).undelegate(selector, ev, callback); - el = ev = callback = selector = null; - } - }, - binder = function( el, ev, callback, selector ) { - return selector ? delegate(el, selector, ev, callback) : bind(el, ev, callback) - }, - /** - * moves 'this' to the first argument - */ - shifter = function shifter(cb) { - return function() { - return cb.apply(null, [$(this)].concat(Array.prototype.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 $.String.underscore(className.replace(dotsReg, '_').replace(controllersReg, "")); - }, - // checks if it looks like an action - actionMatcher = /[^\w]/, - // gets jus the event - eventCleaner = /^(>?default\.)|(>)/, - // handles parameterized action names - parameterReplacer = /\{([^\}]+)\}/g, - breaker = /^(?:(.*?)\s)?([\w\.\:>]+)$/; - /** - * @tag core - * @plugin jquery/controller - * @download jquery/dist/jquery.controller.js - * @test jquery/controller/qunit.html - * - * Controllers organize event handlers using event delegation. - * If something happens in your application (a user click or a [jQuery.Model|Model] instance being updated), - * a controller should respond to it. - * - * ## Benefits - * - * - Know your code. - * - * Group events and label your html in repeatable ways so it's easy to find your code. - * - * - Controllers are inheritable. - * - * Package, inherit, and reuse your widgets. - * - * - Write less. - * - * Controllers take care of setup / teardown auto-magically. - * - * - * ## Basic Example - * - * Controllers organize jQuery code into resuable, inheritable, and extendable widgets. So instead of - * - * @codestart - * $(function(){ - * $('#tabs').click(someCallbackFunction1) - * $('#tabs .tab').click(someCallbackFunction2) - * $('#tabs .delete click').click(someCallbackFunction3) - * }); - * @codeend - * - * do this - * - * @codestart - * $.Controller.extend('Tabs',{ - * click: function() {...}, - * '.tab click' : function() {...}, - * '.delete click' : function() {...} - * }) - * $('#tabs').tabs(); - * @codeend - * - * ## Tabs Example - * - * @demo jquery/controller/controller.html - * - * - * ## Using Controllers - * - * - * A Controller is mostly a list of functions that get called back when specific events happen. - * A function's name provides a description of when the function should be called. - * By naming your functions like "selector event", - * Controller recognizes them as an Action and binds them appropriately. - * - * The event binding happens when you create a [jQuery.Controller.prototype.setup|new controller instance]. - * - * 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 - * <div id='todos'> - * <ol> - * <li class="todo">Laundry</li> - * <li class="todo">Dishes</li> - * <li class="todo">Walk Dog</li> - * </ol> - * <a class="create">Create</a> - * </div> - * @codeend - * - * To add a mousover effect and create todos, your controller might look like: - * - * @codestart - * $.Controller.extend('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>"); - * } - * }) - * @codeend - * - * Now that you've created the controller class, you've must attach the event handlers on the '#todos' div by - * creating [jQuery.Controller.prototype.init|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 - * - * As you've likely noticed, when the [jQuery.Controller.static.init|controller class is created], it creates helper - * functions on [jQuery.fn]. The "#todos" element is known as the controller element. - * - * ### Event Handler Matching - * - * With the exception of subscribe actions, controller uses jQuery.fn.bind or jQuery.fn.delegate to - * attach event handlers. Controller uses the following rules to determine if a function name is - * an event handler: - * - * - Does the function name contain a selector? Ex: "a.foo click" - * - Does the function name match an event in jQuery.event.special? Ex: "mouseenter" - * - Does the function name match a standard event name? Ex: "click" - * - Does the function name match a value in the controller's static listensTo array? Ex: "activate" - * - * In general, Controller will know automatically when to bind event handler functions except for - * one case - event names without selectors that are not in $.event.special. But to correct for this, you - * just need to add the function to the listensTo property. Here's how: - * - * @codestart - * $.Controller.extend("MyShow",{ - * listensTo: ["show"] - * },{ - * show: function( el, ev ) { - * el.show(); - * } - * }) - * $('.show').my_show().trigger("show"); - * @codeend - * - * - * ### Callback Parameters - * - * For most actions, the first two parameters are always: - * - * - el : the jQuery wrapped element. - * - ev : the jQuery wrapped DOM event. - * - * @codestart - * ".something click" : function( el, ev ) { - * el.slideUp() - * ev.stopDelegation(); //stops this event from delegating to any other - * // delegated events for this delegated element. - * ev.preventDefault(); //prevents the default action from happening. - * ev.stopPropagation(); //stops the event from going to other elements. - * } - * @codeend - * - * If the action provides different parameters, they are in each action's documentation. - * - * - * ## Document Controllers - * - * Document Controllers delegate on the documentElement. You don't have to attach an instance as this will be done - * for you when the controller class is created. Document Controllers, with the exception of MainControllers, - * add an implicit '#CONTROLLERNAME' before every selector. - * - * To create a document controller, you just have to set the controller's [jQuery.Controller.static.onDocument static onDocument] - * property to true. - * - * @codestart - * $.Controller.extend('TodosController', - * {onDocument: true}, - * { - * ".todo mouseover" : function( el, ev ) { //matches #todos .todo - * el.css("backgroundColor","red") - * }, - * ".todo mouseout" : function( el, ev ) { //matches #todos .todo - * el.css("backgroundColor","") - * }, - * ".create click" : function() { //matches #todos .create - * this.find("ol").append("<li class='todo'>New Todo</li>"); - * } - * }) - * @codeend - * - * DocumentControllers are typically used for page layout and functionality that is - * extremely unlikely to be repeated such as a SidebarController. - * Often, a Document Controller's "ready" event will be used to create - * necessary Element Controllers. - * - * @codestart - * $.Controller.extend('SidebarController', - * {onDocument: true}, - * { - * ready : function() { - * $(".slider").slider_controller() - * }, - * "a.tag click" : function() {..} - * }) - * @codeend - * - * ### MainControllers - * - * MainControllers are documentControllers that do not add '#CONTROLLERNAME' before every selector. This controller - * should only be used for page wide functionality and setup. - * - * @codestart - * $.Controller.extend("MainController",{ - * hasActiveElement : document.activeElement || false - * },{ - * focus : funtion(el){ - * if(!this.Class.hasActiveElement) - * document.activeElement = el[0] //tracks active element - * } - * }) - * @codeend - * - * ## Controller Initialization - * - * It can be extremely useful to overwrite [jQuery.Controller.prototype.init Controller.prototype.init] 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: - * - * @codestart - * $.Controller.extend("SpecialController", - * { - * init: function( el, message ) { - * this.element.html(message) - * } - * }) - * $(".special").special("Hello World") - * @codeend - * - * ## 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 (innerHTML, removeChild), 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 className="special". - * - * @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 - */ - $.Class.extend("jQuery.Controller", - /** - * @Static - */ - { - /** - * Does 3 things: - *
      - *
    1. Creates a jQuery helper for this controller.
    2. - *
    3. Calculates and caches which functions listen for events.
    4. - *
    5. and attaches this element to the documentElement if onDocument is true.
    6. - *
    - *

    jQuery Helper Naming Examples

    - * @codestart - * "TaskController" -> $().task_controller() - * "Controllers.Task" -> $().controllers_task() - * @codeend - */ - init: function() { - // 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 val, processor, controller = this, - pluginname = this._fullName, - funcName; - - // 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.prototype[options]), - meth = args[0]; - this.each(function() { - //check if created - var controllers = $.data(this, "controllers"), - //plugin is actually the controller instance - plugin = controllers && controllers[pluginname]; - - if ( plugin ) { - isMethod ? - // call a method on the controller with the remaining args - plugin[meth].apply(plugin, args.slice(1)) : - // call the plugin's update method - plugin.update.apply(plugin, args) - } else { - //create a new controller instance - controller.newInstance.apply(controller, [this].concat(args)) - } - }) - //always return the element - return this; - } - } - - // make sure listensTo is an array - - // calculate and cache actions - this.actions = {}; - - for ( funcName in this.prototype ) { - if (!$.isFunction(this.prototype[funcName]) ) { - continue; - } - this._isAction(funcName) && (this.actions[funcName] = this._getAction(funcName)); - } - - /** - * @attribute onDocument - * Set to true if you want to automatically attach this element to the documentElement. - */ - if ( this.onDocument ) new this(document.documentElement); - }, - 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 { - var cleanedEvent = methodName.replace(eventCleaner, ""); - return $.inArray(cleanedEvent, this.listensTo) > -1 || $.event.special[cleanedEvent] || $.Controller.processors[cleanedEvent] - } - - }, - /** - * @hide - * @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. - */ - _getAction: function( methodName, options ) { - //if we don't have a controller instance, we'll break this guy up later - parameterReplacer.lastIndex = 0; - if (!options && parameterReplacer.test(methodName) ) { - return null; - } - var convertedName = options ? methodName.replace(parameterReplacer, function( whole, inside ) { - //convert inside to type - return $.Class.getObject(inside, options).toString(); //gets the value in options - }) : methodName, - parts = convertedName.match(breaker), - event = parts[2], - processor = this.processors[event] || basicProcessor; - return { - processor: processor, - parts: parts - }; - }, - /** - * @attribute processors - * A has of eventName: function pairs that Controller uses to hook - */ - processors: {}, - /** - * @attribute listensTo - * A list of special events this controller listens too. You only need to add event names that - * are whole words (ie have no special characters). - */ - listensTo: [] - }, - /** - * @Prototype - */ - { - /** - * Does three things: - *
      - *
    1. Matches and creates actions.
    2. - *
    3. Set the controller's element.
    4. - *
    5. Saves a reference to this controller in the element's data.
    6. - *
    - * @param {HTMLElement} element the element this instance operates on. - */ - setup: function( element, options ) { - var funcName, cb, ready, cls = this.Class; - - //want the raw element here - element = element.jquery ? element[0] : element; - - //set element and className on element - this.element = $(element).addClass(cls._fullName); - - //set in data - ($.data(element, "controllers") || $.data(element, "controllers", {}))[cls._fullName] = this; - - //adds bindings - this._bindings = []; - /** - * @attribute options - * Options is automatically merged from this.Class.OPTIONS and the 2nd argument - * passed to a controller. - */ - this.options = $.extend($.extend(true, {}, cls.defaults), options); - - //go through the cached list of actions and use the processor to bind - for ( funcName in cls.actions ) { - ready = cls.actions[funcName] || cls._getAction(funcName, this.options); - - this._bindings.push( - ready.processor(element, ready.parts[2], ready.parts[1], this.callback(funcName), this)); - } - - - /** - * @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"; - - //setup to be destroyed ... don't bind b/c we don't want to remove it - //this.element.bind('destroyed', this.callback('destroy')) - var destroyCB = shifter(this.callback("destroy")) - this.element.bind("destroyed", destroyCB); - this._bindings.push(function( el ) { - destroyCB.removed = true; - $(element).unbind("destroyed", destroyCB); - }) - - /** - * @attribute element - * The controller instance's delegated element. This is set by [jQuery.Controller.prototype.init init]. - * It is a jQuery wrapped element. - * @codestart - * ".something click" : function() { - * this.element.css("color","red") - * } - * @codeend - */ - return this.element; - }, - /** - * Bind attaches event handlers that will be removed when the controller is removed. - * This is a good way to attach to an element not in the controller's element. - *
    - *

    Examples:

    - * @codestart - * 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 ) { - * - * } - * @codeend - * @param {HTMLElement|jQuery.fn} [element=this.element] element the element to be bound - * @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 ( 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.callback(func)) - } - this._bindings.push(binder(el, eventName, func, selector)) - return this._bindings.length; - }, - /** - * 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.
    - *

    Example:

    - * @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] element - * @param {String} selector the css selector - * @param {String} eventName - * @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) - }, - /** - * Called if an controller's jQuery helper is called on an element that already has a controller instance - * of the same type. Extends this.options with the options passed in. If you overwrite this, you might want to call - * this._super. - *

    Examples

    - * @codestart - * $.Controller.extend("Thing",{ - * init: function( el, options ) { - * alert('init') - * }, - * update: function( options ) { - * this._super(options); - * alert('update') - * } - * }); - * $('#myel').thing(); // alerts init - * $('#myel').thing(); // alerts update - * @codeend - * @param {Object} options - */ - update: function( options ) { - $.extend(this.options, options) - }, - /** - * Destroy unbinds and undelegates all actions on this controller, and prevents any memory leaks. This is called automatically - * if the element is removed. - * - */ - destroy: function( ev ) { - if ( this._destroyed ) { - throw this.Class.shortName + " controller instance has been deleted"; - } - var self = this, - fname = this.Class._fullName; - this._destroyed = true; - this.element.removeClass(fname); - - $.each(this._bindings, function( key, value ) { - if ( $.isFunction(value) ) value(self.element[0]); - }); - - delete this._actions; - - - var controllers = this.element.data("controllers"); - if (controllers && controllers[fname]) { - delete 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 - }); - - - //------------- PROCESSSORS ----------------------------- - //processors do the binding. They return a function that - //unbinds when called. - //the basic processor that binds events - var basicProcessor = function( el, event, selector, cb, controller ) { - var c = controller.Class; - - // document controllers use their name as an ID prefix. - if ( c.onDocument && !/^Main(Controller)?$/.test(c.shortName) ) { //prepend underscore name if necessary - selector = selector ? "#" + c._shortName + " " + selector : "#" + c._shortName - } - return binder(el, event, shifter(cb), selector); - }, - processors = $.Controller.processors, - - //a window event only happens on the window - windowEvent = function( el, event, selector, cb ) { - return binder(window, event.replace(/window/, ""), shifter(cb)) - } - - //set commong events to be processed as a basicProcessor - $.each("change click contextmenu dblclick keydown keyup keypress mousedown mousemove mouseout mouseover mouseup reset windowresize resize windowscroll scroll select submit dblclick focusin focusout load unload ready hashchange mouseenter mouseleave".split(" "), function( i, v ) { - processors[v] = basicProcessor; - }) - $.each(["windowresize", "windowscroll", "load", "ready", "unload", "hashchange"], function( i, v ) { - processors[v] = windowEvent; - }) - //the ready processor happens on the document - processors.ready = function( el, event, selector, cb ) { - $(shifter(cb)); //cant really unbind - } - /** - * @add jQuery.fn - */ - - $.fn.mixin = function() { - //create a bunch of controllers - var controllers = $.makeArray(arguments); - return this.each(function() { - for ( var i = 0; i < controllers.length; i++ ) { - new controllers[i](this) - } - - }) - } - //used to determine if a controller instance is one of controllers - //controllers can be strings or classes - var isAControllerOf = function( instance, controllers ) { - for ( var i = 0; i < controllers.length; i++ ) { - if ( typeof controllers[i] == 'string' ? instance.Class._shortName == controllers[i] : instance instanceof controllers[i] ) { - return true; - } - } - return false; - } - $.fn. - /** - * @function controllers - * Gets all controllers in the jQuery element. - * @return {Array} an array of controller instances. - */ - controllers = function() { - var controllerNames = $.makeArray(arguments), - instances = [], - controllers, cname; - //check if arguments - this.each(function() { - controllers = $.data(this, "controllers") - if (!controllers ) return; - for ( var cname in controllers ) { - var c = controllers[cname]; - if (!controllerNames.length || isAControllerOf(c, controllerNames)) { - instances.push(c); - } - } - }) - return instances; - }; - $.fn. - /** - * @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]; - }; - - -})(true); - diff --git a/dist/jquery.event.default.js b/dist/jquery.event.default.js deleted file mode 100644 index 899dc25f..00000000 --- a/dist/jquery.event.default.js +++ /dev/null @@ -1,195 +0,0 @@ -// jquery/event/default/default.js - -(function($){ - - -//cache default types for performance -var types = {}, rnamespaces= /\.(.*)$/; -/** - * @attribute default - * @parent specialevents - * @plugin jquery/event/default - * @download jquery/dist/jquery.event.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.

    - * @codestart - * $("div").bind("default.show", function(ev){ ... }); - * $("ul").delegate("li","default.activate", function(ev){ ... }); - * @codeend - *

    - * The default plugin also adds the [jQuery.fn.triggerDefault triggerDefault] and [jQuery.fn.triggerDefaults triggerDefaults] methods. These are used to trigger - * an event and report back whether preventDefault was called on the event. The only difference is [jQuery.fn.triggerDefault triggerDefault] - * doesn't bubble. - *

    - *

    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; - - //move the handler ... - var origHandler = handleObj.handler; - - handleObj.origHandler = origHandler; - handleObj.handler = function(ev, data){ - if(!ev._defaultActions) ev._defaultActions = []; - ev._defaultActions.push({element: this, handler: origHandler, event: ev, data: data, currentTarget: ev.currentTarget}) - } - }, - setup: function() {return true} -} - -// overwrite trigger to allow default types -var oldTrigger = $.event.trigger; -$.event.trigger = function defaultTriggerer( event, data, elem, bubbling){ - //always need to convert here so we know if we have default actions - var type = event.type || event - - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[$.expando] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); - - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } - event._defaultActions = []; //set depth for possibly reused events - } - - var defaultGetter = jQuery.Event("default."+event.type), - res; - - $.extend(defaultGetter,{ - target: elem, - _defaultActions: event._defaultActions, - exclusive : true - }); - - defaultGetter.stopPropagation(); - - //default events only work on elements - if(elem){ - oldTrigger.call($.event, defaultGetter, [defaultGetter, data], elem, true); - } - - //fire old trigger, this will call back here - res = oldTrigger.call($.event, event, data, elem, bubbling); - - //fire if there are default actions to run && - // we have not prevented default && - // propagation has been stopped or we are at the document element - // we have reached the document - if (!event.isDefaultPrevented() && - event._defaultActions && - ( ( event.isPropagationStopped() ) || - ( !elem.parentNode && !elem.ownerDocument ) ) - - ) { - - // put event back - event.namespace= event.type; - event.type = "default"; - event.liveFired = null; - - // call each event handler - for(var i = 0 ; i < event._defaultActions.length; i++){ - var a = event._defaultActions[i], - oldHandle = event.handled; - event.currentTarget = a.currentTarget; - a.handler.call(a.element, event, a.data); - event.handled = event.handled === null ? oldHandle : true; - } - event._defaultActions = null; //set to null so everyone else on this element ignores it - } -} -/** - * @add jQuery.fn - */ -$.fn. -/** - * Triggers the event, stops the event from propagating through the DOM, and - * returns whether or not the event's default action was prevented. - * If true, the default action was not prevented. If false, the - * default action was prevented. This is the same as triggerDefaults, but - * the event doesn't bubble. Use these methods to easily determine if default was - * prevented, and proceed accordingly. - * - *

    Widget developers might use this method to perform additional logic if an event - * handler doesn't prevent the default action. For example, a tabs widget might - * hide the currently shown tab if the application developer doesn't prevent default.

    - * @param {Object} type The type of event to trigger. - * @param {Object} data Some data to pass to callbacks listening to this - * event. - */ -triggerDefault = function(type, data){ - if ( this[0] ) { - var event = $.Event( type ); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return !event.isDefaultPrevented(); - } - return true; -} -$.fn. -/** - * Triggers the event and returns whether or not the event's - * default action was prevented. If true, the default action was not - * prevented. If false, the default action was prevented. This is the same - * as triggerDefault, but the event bubbles. Use these methods to easily determine if default was - * prevented, and proceed accordingly. - * @param {Object} type The type of event to trigger. - * @param {Object} data Some data to pass to callbacks listening to this - * event. - */ -triggerDefaults = function(type, data){ - if ( this[0] ) { - var event = $.Event( type ); - jQuery.event.trigger( event, data, this[0] ); - return !event.isDefaultPrevented(); - } - return true; -} - - - - - - - -})(true); - diff --git a/dist/jquery.event.destroyed.js b/dist/jquery.event.destroyed.js deleted file mode 100644 index b05382bf..00000000 --- a/dist/jquery.event.destroyed.js +++ /dev/null @@ -1,42 +0,0 @@ -// jquery/event/destroyed/destroyed.js - -(function($){ - - /** - * @attribute destroyed - * @parent specialevents - * @download jquery/dist/jquery.event.destroyed.js - * @test jquery/event/destroyed/qunit.html - * Provides a destroyed event on an element. - *

    - * 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. - *

    - *

    Quick Example

    - * @codestart - * $(".foo").bind("destroyed", function(){ - * //clean up code - * }) - * @codeend - *

    Quick Demo

    - * @demo jquery/event/destroyed/destroyed.html - *

    More Involved Demo

    - * @demo jquery/event/destroyed/destroyed_menu.html - */ - - var oldClean = jQuery.cleanData - - $.cleanData = function( elems ) { - for ( var i = 0, elem; - (elem = elems[i]) != null; i++ ) { - $(elem).triggerHandler("destroyed") - //$.event.remove( elem, 'destroyed' ); - } - oldClean(elems) - } - - -})(true); - diff --git a/dist/jquery.event.drag.js b/dist/jquery.event.drag.js deleted file mode 100644 index aa02cf12..00000000 --- a/dist/jquery.event.drag.js +++ /dev/null @@ -1,824 +0,0 @@ -// jquery/lang/vector/vector.js - -(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 - * A vector class - * @init creates a new vector instance from the arguments. Example: - * @codestart - * new jQuery.Vector(1,2) - * @codeend - * - */ -jQuery.Vector = function(){ - this.update( jQuery.makeArray(arguments) ); -}; -jQuery.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 newArr = []; - - for(var i=0; i < this.array.length; i++) - newArr.push( f( this.array[i] ) ); - var vec = new jQuery.Vector(); - return vec.update(newArr); - }, - /** - * Adds two vectors together. Example: - * @codestart - * new Vector(1,2).plus(2,3) //-> <3,5> - * new Vector(3,5).plus(new Vector(4,5)) //-> <7,10> - * @codeend - * @return {jQuery.Vector} - */ - plus: function() { - var args = arguments[0] instanceof jQuery.Vector ? - arguments[0].array : - jQuery.makeArray(arguments), - arr=this.array.slice(0), - vec = new jQuery.Vector(); - for(var 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 args = arguments[0] instanceof jQuery.Vector ? - arguments[0].array : - jQuery.makeArray(arguments), - arr=this.array.slice(0), vec = new jQuery.Vector(); - for(var 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 args = arguments[0] instanceof jQuery.Vector ? - arguments[0].array : - jQuery.makeArray(arguments), - arr=this.array.slice(0), vec = new jQuery.Vector(); - for(var i=0; i < args.length; i++) - if(arr[i] != args[i]) return null; - return vec.update(arr); - }, - /* - * Returns the 2nd value of the vector - * @return {Number} - */ - x : getSetZero, - width : getSetZero, - /** - * Returns the first value of the vector - * @return {Number} - */ - y : getSetOne, - height : getSetOne, - /** - * Same as x() - * @return {Number} - */ - top : getSetOne, - /** - * same as y() - * @return {Number} - */ - left : getSetZero, - /** - * returns (x,y) - * @return {String} - */ - toString: function() { - return "("+this.array[0]+","+this.array[1]+")"; - }, - /** - * Replaces the vectors contents - * @param {Object} array - */ - update: function( array ) { - if(this.array){ - for(var i =0; i < this.array.length; i++) delete this.array[i]; - } - this.array = array; - for(var i =0; i < array.length; i++) this[i]= this.array[i]; - return this; - } -}; - -jQuery.Event.prototype.vector = function(){ - if(this.originalEvent.synthetic){ - var doc = document.documentElement, body = document.body; - return new jQuery.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 jQuery.Vector(this.pageX, this.pageY); - } -} - -jQuery.fn.offsetv = function() { - if(this[0] == window){ - return new jQuery.Vector(window.pageXOffset ? window.pageXOffset : document.documentElement.scrollLeft, - window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop) - }else{ - var offset = this.offset(); - return new jQuery.Vector(offset.left, offset.top); - } -}; - -jQuery.fn.dimensionsv = function(){ - if(this[0] == window) - return new jQuery.Vector(this.width(), this.height()); - else - return new jQuery.Vector(this.outerWidth(), this.outerHeight()); -} -jQuery.fn.centerv = function(){ - return this.offsetv().plus( this.dimensionsv().app(function(u){return u /2;}) ) -} - -jQuery.fn.makePositioned = function() { - return this.each(function(){ - var that = jQuery(this); - var pos = that.css('position'); - - if (!pos || pos == 'static') { - var style = { position: 'relative' }; - - if (window.opera) { - style.top = '0px'; - style.left = '0px'; - } - that.css(style); - } - }); -}; - - -})(true); - -// jquery/event/livehack/livehack.js - -(function($){ - - - var event = jQuery.event, - - //helper that finds handlers by type and calls back a function, this is basically handle - findHelper = function(events, types, callback){ - for( var t =0; t< types.length; t++ ) { - var type = types[t], - typeHandlers, - all = type.indexOf(".") < 0, - namespaces, - namespace; - if ( !all ) { - namespaces = type.split("."); - type = namespaces.shift(); - namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - typeHandlers = ( events[type] || [] ).slice(0); - - for( var h = 0; h < live.length; t++ ) { - var liver = live[t]; - if( liver.selector === selector && $.inArray(liver.origType, types ) !== -1 ) { - handlers.push(liver.origHandler || liver.handler); - } - } - }else{ - // basically re-create handler's logic - findHelper(events, types, function(type, handler){ - handlers.push(handler); - }) - } - return handlers; - } - /** - * Finds - * @param {HTMLElement} el - * @param {Array} 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){ - add("", type, handler); - }) - - return selectors; - } - $.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 selector = handleObj.selector || ""; - if (selector) { - var 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 - }); - } - - } - - } - var remove = function(handleObj){ - var selector = handleObj.selector || ""; - if (selector) { - var 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() {} - }; - }); - } - -})(true); - -// jquery/event/drag/drag.js - -(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, handle = event.handle; - - /** - * @class jQuery.Drag - * @parent specialevents - * @plugin jquery/event/drag - * @download jquery/dist/jquery.event.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: - *
      - *
    • dragdown - the mouse cursor is pressed down
    • - *
    • draginit - the drag motion is started
    • - *
    • dragmove - the drag is moved
    • - *
    • dragend - the drag has ended
    • - *
    • dragover - the drag is over a drop point
    • - *
    • dragout - the drag moved out of a drop point
    • - *
    - *

    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: - * @codestart - * //makes the drag vertical - * $(".drags").live("draginit", function(event, drag){ - * drag.vertical(); - * }) - * //gets the position of the drag and uses that to set the width - * //of an element - * $(".resize").live("dragmove",function(event, drag){ - * $(this).width(drag.position.left() - $(this).offset().left ) - * }) - * @codeend - *

    Drag Object

    - *

    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. - *

    - *

    The drag properties and methods:

    - *
      - *
    • [jQuery.Drag.prototype.cancel cancel] - stops the drag motion from happening
    • - *
    • [jQuery.Drag.prototype.ghost ghost] - copys the draggable and drags the cloned element
    • - *
    • [jQuery.Drag.prototype.horizontal horizontal] - limits the scroll to horizontal movement
    • - *
    • [jQuery.Drag.prototype.location location] - where the drag should be on the screen
    • - *
    • [jQuery.Drag.prototype.mouseElementPosition mouseElementPosition] - where the mouse should be on the drag
    • - *
    • [jQuery.Drag.prototype.only only] - only have drags, no drops
    • - *
    • [jQuery.Drag.prototype.representative representative] - move another element in place of this element
    • - *
    • [jQuery.Drag.prototype.revert revert] - animate the drag back to its position
    • - *
    • [jQuery.Drag.prototype.vertical vertical] - limit the drag to vertical movement
    • - *
    • [jQuery.Drag.prototype.limit limit] - limit the drag within an element (*limit plugin)
    • - *
    • [jQuery.Drag.prototype.scrolls scrolls] - scroll scrollable areas when dragging near their boundries (*scroll plugin)
    • - *
    - *

    Demo

    - * 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, - /** - * 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.liveFired || element, - selector = ev.handleObj.selector, - self = this; - this.current = drag; - - drag.setup({ - element: element, - delegate: ev.liveFired || element, - selector: ev.handleObj.selector, - moved: false, - 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 ) { - //this.noSelection(); - $.extend(this,options); - this.element = $(this.element); - this.event = ev; - this.moved = false; - this.allowOtherDrags = false; - var mousemove = bind(this, this.mousemove); - var mouseup = bind(this, this.mouseup); - this._mousemove = mousemove; - this._mouseup = mouseup; - $(document).bind('mousemove' ,mousemove); - $(document).bind('mouseup',mouseup); - - if(! this.callEvents('down',this.element, ev) ){ - ev.preventDefault(); - } - }, - /** - * 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.destroyed(); - }, - mousemove: function( docEl, ev ) { - if(!this.moved){ - 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: function() { - document.documentElement.onselectstart = function() { return false; }; - document.documentElement.unselectable = "on"; - $(document.documentElement).css('-moz-user-select', 'none'); - }, - selection: function() { - document.documentElement.onselectstart = function() { }; - document.documentElement.unselectable = "off"; - $(document.documentElement).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; - this.mouseStartPosition = event.vector(); //where the mouse is located - /** - * @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.movingElement.makePositioned(); - this.oldZIndex = this.movingElement.css('zIndex'); - this.movingElement.css('zIndex',1000); - if(!this._only && this.constructor.responder) - this.constructor.responder.compile(event, this); - }, - callEvents: function( type, element, event, drop ) { - var cbs = this.callbacks[this.constructor.lowerName+type]; - for(var 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') ) || 0 , - parseInt( this.movingElement.css('top') ) || 0 ) ; - }, - //draws the position of the dragmove object - draw: function( pointer, event ) { - // only drag if we haven't been cancelled; - if(this._cancelled) return; - /** - * @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. - */ - 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 - 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); - }, - /** - * @hide - * Set the drag to only allow horizontal dragging. - * - * @param {Object} offsetPositionv the position of the element (not the mouse) - */ - position: function( offsetPositionv ) { //should draw it on the page - var dragged_element_page_offset = this.movingElement.offsetv(); // the drag element's current page location - - var dragged_element_css_offset = this.currentDelta(); // the drag element's current left + top css attributes - - var dragged_element_position_vector = // the vector between the movingElement's page and css positions - dragged_element_page_offset.minus(dragged_element_css_offset); // this can be thought of as the original offset - - this.required_css_position = offsetPositionv.minus(dragged_element_position_vector) - - - - var 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.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()) - - // store the original element and make the ghost the dragged element - this.movingElement = 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.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 == null ? true : val; - }, - /** - * Isolates the drag to vertical movement. - */ - vertical: function() { - this._vertical = true; - }, - /** - * Isolates the drag to horizontal movement. - */ - horizontal: function() { - this._horizontal = true; - }, - - - /** - * Respondables will not be alerted to this drag. - */ - only: function( only ) { - return (this._only = (only === undefined ? true : only)); - } - }); - - /** - * @add jQuery.event.special - */ - event.setupHelper( [ - /** - * @attribute dragdown - *

    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).

    - *

    Why might you not want it?

    - *

    You might want it if you want to allow text selection on element - * within the drag element. Typically these are input elements.

    - *

    Drag events are covered in more detail in [jQuery.Drag].

    - * @codestart - * $(".handles").live("dragdown", function(ev, drag){}) - * @codeend - */ - 'dragdown', - /** - * @attribute draginit - * Called when the drag starts. - *

    Drag events are covered in more detail in [jQuery.Drag].

    - */ - 'draginit', - /** - * @attribute dragover - * Called when the drag is over a drop. - *

    Drag events are covered in more detail in [jQuery.Drag].

    - */ - 'dragover', - /** - * @attribute dragmove - * Called when the drag is moved. - *

    Drag events are covered in more detail in [jQuery.Drag].

    - */ - 'dragmove', - /** - * @attribute dragout - * When the drag leaves a drop point. - *

    Drag events are covered in more detail in [jQuery.Drag].

    - */ - 'dragout', - /** - * @attribute dragend - * Called when the drag is done. - *

    Drag events are covered in more detail in [jQuery.Drag].

    - */ - 'dragend' - ], "mousedown", function(e){ - $.Drag.mousedown.call($.Drag, e, this) - - } ) - - - - - -})(true); - diff --git a/dist/jquery.event.hover.js b/dist/jquery.event.hover.js deleted file mode 100644 index 11f8fda8..00000000 --- a/dist/jquery.event.hover.js +++ /dev/null @@ -1,396 +0,0 @@ -// jquery/event/livehack/livehack.js - -(function($){ - - - var event = jQuery.event, - - //helper that finds handlers by type and calls back a function, this is basically handle - findHelper = function(events, types, callback){ - for( var t =0; t< types.length; t++ ) { - var type = types[t], - typeHandlers, - all = type.indexOf(".") < 0, - namespaces, - namespace; - if ( !all ) { - namespaces = type.split("."); - type = namespaces.shift(); - namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - typeHandlers = ( events[type] || [] ).slice(0); - - for( var h = 0; h < live.length; t++ ) { - var liver = live[t]; - if( liver.selector === selector && $.inArray(liver.origType, types ) !== -1 ) { - handlers.push(liver.origHandler || liver.handler); - } - } - }else{ - // basically re-create handler's logic - findHelper(events, types, function(type, handler){ - handlers.push(handler); - }) - } - return handlers; - } - /** - * Finds - * @param {HTMLElement} el - * @param {Array} 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){ - add("", type, handler); - }) - - return selectors; - } - $.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 selector = handleObj.selector || ""; - if (selector) { - var 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 - }); - } - - } - - } - var remove = function(handleObj){ - var selector = handleObj.selector || ""; - if (selector) { - var 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() {} - }; - }); - } - -})(true); - -// jquery/event/hover/hover.js - -(function($){ - -/** - * @class jQuery.Hover - * @plugin jquery/event/hover - * @download jquery/dist/jquery.event.hover.js - * Provides delegate-able hover events. - *

    - * 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: - *

    - *
      - *
    • [jQuery.event.special.hoverinit hoverinit] - called on mouseenter, use this event to customize - * [jQuery.Hover.prototype.delay] and [jQuery.Hover.prototype.distance]
    • - *
    • [jQuery.event.special.hoverenter hoverenter] - an element is being hovered
    • - *
    • [jQuery.event.special.hovermove hovermove] - the mouse moves on an element that has been hovered
    • - *
    • [jQuery.event.special.hoverleave hoverleave] - the mouse leaves the element that has been hovered
    • - *
    - *

    Quick Example

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

    Configuring Distance and Delay

    - *

    An element is hovered when the mouse - * moves less than a certain distance in - * specific time over the element. - *

    - *

    - * You can configure that distance and time by - * adjusting the distance and - * delay values. - *

    - *

    You can set delay and distance globally - * by adjusting the static properties:

    - *

    - * @codestart - * $.Hover.delay = 10 - * $.Hover.distance = 1 - * @codeend - *

    Or you can adjust delay and distance for - * an individual element in hoverenter:

    - * @codestart - * $(".option").live("hoverinit", function(ev, hover){ - * //set the distance to 10px - * hover.distance(10) - * //set the delay to 200ms - * hover.delay(10) - * }) - * @codeend - *

    Demo

    - * @demo jquery/event/hover/hover.html - * @parent specialevents - * @constructor Creates a new hover. This is never - * called directly. - */ -jQuery.Hover = function(){ - this._delay = jQuery.Hover.delay; - this._distance = jQuery.Hover.distance; -}; -/** - * @Static - */ -$.extend(jQuery.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 -}) - -/** - * @Prototype - */ -$.extend(jQuery.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; - }, - /** - * 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; - } -}) -var $ = jQuery, - event = jQuery.event, - handle = event.handle, - onmouseenter = function(ev){ - //now start checking mousemoves to update location - var delegate = ev.liveFired || ev.currentTarget; - var selector = ev.handleObj.selector; - var loc = { - pageX : ev.pageX, - pageY : ev.pageY - }, - dist = 0, - timer, - entered = this, - called = false, - lastEv = ev, - hover = new jQuery.Hover(); - - $(entered).bind("mousemove.specialMouseEnter", {}, function(ev){ - dist += Math.pow( ev.pageX-loc.pageX, 2 ) + Math.pow( ev.pageY-loc.pageY, 2 ); - loc = { - pageX : ev.pageX, - pageY : ev.pageY - } - lastEv = ev - }).bind("mouseleave.specialMouseLeave",{}, function(ev){ - clearTimeout(timer); - if(called){ - $.each(event.find(delegate, ["hoverleave"], selector), function(){ - this.call(entered, ev) - }) - } - $(entered).unbind("mouseleave.specialMouseLeave") - }) - $.each(event.find(delegate, ["hoverinit"], selector), function(){ - this.call(entered, ev, hover) - }) - timer = setTimeout(function(){ - //check that we aren't moveing around - if(dist < hover._distance && $(entered).queue().length == 0){ - $.each(event.find(delegate, ["hoverenter"], selector), function(){ - this.call(entered, lastEv, hover) - }) - called = true; - $(entered).unbind("mousemove.specialMouseEnter") - - }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").live("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").live("hoverenter", function(ev, hover){ - * $(this).addClass("hovering"); - * }) - * @codeend - */ -"hoverenter", -/** - * @attribute hoverleave - * Called when the mouse leaves an element that has been - * hovered. - * @codestart - * $(".option").live("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").live("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 ) - - - - -})(true); - diff --git a/dist/jquery.model.js b/dist/jquery.model.js deleted file mode 100644 index e2c7efee..00000000 --- a/dist/jquery.model.js +++ /dev/null @@ -1,1725 +0,0 @@ -// jquery/class/class.js - -(function($){ - - - // if we are initializing a new class - var initializing = false, - - // tests if we can get super in .toString() - fnTest = /xyz/.test(function() { - xyz; - }) ? /\b_super\b/ : /.*/, - - // overwrites an object with methods, sets up _super - inheritProps = function( newProps, oldProps, addTo ) { - addTo = addTo || newProps - for ( var name in newProps ) { - // Check if we're overwriting an existing function - addTo[name] = typeof newProps[name] == "function" && typeof oldProps[name] == "function" && 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]; - } - }; - - - /** - * @class jQuery.Class - * @plugin jquery/class - * @tag core - * @download dist/jquery/jquery.class.js - * @test jquery/class/qunit.html - * Class provides simulated inheritance in JavaScript. Use $.Class 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
    • - *
    - *

    Static v. Prototype

    - *

    Before learning about Class, it's important to - * understand the difference between - * a class's static and prototype properties. - *

    - * @codestart - * //STATIC - * MyClass.staticProperty //shared property - * - * //PROTOTYPE - * myclass = new MyClass() - * myclass.prototypeMethod() //instance method - * @codeend - *

    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.extend('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.Class.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 init 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 - * this._super. Lets create a SeaMonster class. SeaMonsters are less - * efficient at eating small children, but more powerful fighters. - *

    - * @codestart - * Monster.extend("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 - * @codeend - *

    Static property inheritance

    - * You can also inherit static properties in the same way: - * @codestart - * $.Class.extend("First", - * { - * staticMethod: function() { return 1;} - * },{}) - * - * First.extend("Second",{ - * staticMethod: function() { return this._super()+1;} - * },{}) - * - * Second.staticMethod() // -> 2 - * @codeend - *

    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: - *

    - * @codestart - * $.Class.extend("MyNamespace.MyClass",{},{}); - * - * new MyNamespace.MyClass() - * @codeend - *

    Introspection

    - * 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. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's - * static properties. - * - * - *

    Setup and initialization methods

    - *

    - * 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. - *

    - *
    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. - * - *
    - * @codestart - * $.Class.extend("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. - *

    - * @codestart - * $.Class.extend("jQuery.Controller",{ - * ... - * },{ - * setup: function( el, options ) { - * ... - * return [$(el), - * $.extend(true, - * this.Class.defaults, - * options || {} ) ] - * } - * }) - * @codeend - * 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 init method - * gets called in the following example: - *

    - * @codestart - * $.Class.Extend("Foo", { - * init: function( arg1, arg2, arg3 ) { - * this.sum = arg1+arg2+arg3; - * } - * }) - * var foo = new Foo(1,2,3); - * foo.sum //-> 6 - * @codeend - *

    Callbacks

    - *

    Similar to jQuery's proxy method, Class provides a - * [jQuery.Class.static.callback callback] - * function that returns a callback to a method that will always - * have - * this set to the class or instance of the class. - *

    - * The following example uses this.callback to make sure - * this.name is available in show. - * @codestart - * $.Class.extend("Todo",{ - * init: function( name ) { this.name = name } - * get: function() { - * $.get("/stuff",this.callback('show')) - * }, - * show: function( txt ) { - * alert(this.name+txt) - * } - * }) - * new Todo("Trash").get() - * @codeend - *

    Callback is available as a static and prototype method.

    - *

    Demo

    - * @demo jquery/class/class.html - * - * @init Creating a new instance of an object that has extended jQuery.Class - * calls the init prototype function and returns a new instance of the class. - * - */ - - jQuery.Class = function() { - if ( arguments.length ) this.extend.apply(this, arguments) - }; - - /* @Static*/ - $.extend($.Class, { - /** - * @function callback - * Returns a callback function for a function on this Class. - * The callback function ensures that 'this' is set appropriately. - * @codestart - * $.Class.extend("MyClass",{ - * getData: function() { - * this.showing = null; - * $.get("data.json",this.callback('gotData'),'json') - * }, - * gotData: function( data ) { - * this.showing = data; - * } - * },{}); - * MyClass.showData(); - * @codeend - *

    Currying Arguments

    - * Additional arguments to callback will fill in arguments on the returning function. - * @codestart - * $.Class.extend("MyClass",{ - * getData: function( callback ) { - * $.get("data.json",this.callback('process',callback),'json'); - * }, - * process: function( callback, jsonData ) { //callback is added as first argument - * jsonData.processed = true; - * callback(jsonData); - * } - * },{}); - * MyClass.getData(showDataFunc) - * @codeend - *

    Nesting Functions

    - * Callback 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.extend("MyClass",{ - * getData: function( callback ) { - * //calls process, then callback with value from process - * $.get("data.json",this.callback(['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. - */ - callback: function( funcs ) { - - //args that should be curried - var args = jQuery.makeArray(arguments), - self; - - funcs = args.shift(); - - if (!jQuery.isArray(funcs) ) { - funcs = [funcs]; - } - - self = this; - - return function class_cb() { - var cur = args.concat(jQuery.makeArray(arguments)), - isString, length = funcs.length, - f = 0, - func; - - for (; f < length; f++ ) { - func = funcs[f]; - if (!func ) { - continue; - } - - isString = typeof func == "string"; - if ( isString && self._set_called ) { - self.called = func; - } - cur = (isString ? self[func] : func).apply(self, cur || []); - if ( f < length - 1 ) { - cur = !jQuery.isArray(cur) || cur._use_call ? [cur] : cur - } - } - return cur; - } - }, - /** - * @function getObject - * Gets an object from a String. - * If the object or namespaces the string represent do not - * exist it will create them. - * @codestart - * Foo = {Bar: {Zar: {"Ted"}}} - * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" - * @codeend - * @param {String} objectName the object you want to get - * @param {Object} [current=window] the object you want to look in. - * @return {Object} the object you are looking for. - */ - getObject: function( objectName, current ) { - var current = current || window, - parts = objectName ? objectName.split(/\./) : [], - i = 0; - for (; i < parts.length; i++ ) { - current = current[parts[i]] || (current[parts[i]] = {}) - } - return current; - }, - /** - * @function newInstance - * Creates a new instance of the class. This method is useful for creating new instances - * with arbitrary parameters. - *

    Example

    - * @codestart - * $.Class.extend("MyClass",{},{}) - * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) - * @codeend - * @return {class} instance of the class - */ - newInstance: function() { - var inst = this.rawInstance(), - args; - if ( inst.setup ) { - args = inst.setup.apply(inst, arguments); - } - if ( inst.init ) { - inst.init.apply(inst, $.isArray(args) ? args : arguments); - } - return inst; - }, - /** - * Copy and overwrite options from old class - * @param {Object} oldClass - * @param {String} fullName - * @param {Object} staticProps - * @param {Object} protoProps - */ - setup: function( oldClass, fullName ) { - this.defaults = $.extend(true, {}, oldClass.defaults, this.defaults); - return arguments; - }, - rawInstance: function() { - initializing = true; - var inst = new this(); - initializing = false; - return inst; - }, - /** - * Extends a class with new static and prototype functions. There are a variety of ways - * to use extend: - * @codestart - * //with className, static and prototype functions - * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) - * //with just classname and prototype functions - * $.Class.extend('Task',{ PROTOTYPE }) - * //With just a className - * $.Class.extend('Task') - * @codeend - * @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 - if ( typeof fullName != 'string' ) { - proto = klass; - klass = fullName; - fullName = null; - } - if (!proto ) { - proto = klass; - klass = null; - } - - proto = proto || {}; - var _super_class = this, - _super = this.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; - - if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new - return this.extend.apply(this, 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) && $.inArray(name, ['prototype', 'defaults', 'getObject']) == -1 ) { - Class[name] = this[name]; - } - } - - // do static inheritance - inheritProps(klass, this, Class); - - // do namespace stuff - if ( fullName ) { - - var parts = fullName.split(/\./), - shortName = parts.pop(), - current = $.Class.getObject(parts.join('.')), - namespace = current; - - - current[shortName] = Class; - } - - // set things that can't be overwritten - $.extend(Class, { - prototype: prototype, - namespace: namespace, - shortName: shortName, - constructor: Class, - fullName: fullName - }); - - //make sure our prototype looks nice - Class.prototype.Class = Class.prototype.constructor = Class; - - - /** - * @attribute fullName - * The full name of the class, including namespace, provided for introspection purposes. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - */ - - var args = Class.setup.apply(Class, [_super_class].concat($.makeArray(arguments))); - - if ( Class.init ) { - Class.init.apply(Class, args || []); - } - - /* @Prototype*/ - return Class; - /** - * @function setup - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * setup: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend - * - *
    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. - *
    - * - * @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 - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * init: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend - */ - //Breaks up code - /** - * @attribute Class - * References the static properties of the instance's class. - *

    Quick Example

    - * @codestart - * // a class with a static classProperty property - * $.Class.extend("MyClass", {classProperty : true}, {}); - * - * // a new instance of myClass - * var mc1 = new MyClass(); - * - * // - * mc1.Class.classProperty = false; - * - * // creates a new MyClass - * var mc2 = new mc.Class(); - * @codeend - * Getting static properties via the Class property, such as it's - * [jQuery.Class.static.fullName fullName] is very common. - */ - } - - }) - - - - - - jQuery.Class.prototype. - /** - * @function callback - * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. - * The only difference is this callback 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 - */ - callback = jQuery.Class.callback; - - - -})(true); - -// jquery/lang/lang.js - -(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, - lowerUpper: /([a-z\d])([A-Z])/g, - dash: /([a-z\d])([A-Z])/g - }; - - /** - * @class jQuery.String - */ - var str = ($.String = /* @Static*/ { - /** - * @function strip - * @param {String} s returns a string with leading and trailing whitespace removed. - */ - strip: function( string ) { - return string.replace(/^\s+/, '').replace(/\s+$/, ''); - }, - /** - * Capitalizes a string - * @param {String} s the string to be lowercased. - * @return {String} a string with the first character capitalized, and everything else lowercased - */ - capitalize: function( s, cache ) { - return s.charAt(0).toUpperCase() + s.substr(1); - }, - - /** - * Returns if string ends with another string - * @param {String} s String that is being scanned - * @param {String} pattern What the string might end with - * @return {Boolean} true if the string ends wtih pattern, false if otherwise - */ - endsWith: function( s, pattern ) { - var d = s.length - pattern.length; - return d >= 0 && s.lastIndexOf(pattern) === d; - }, - /** - * 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 ) { - var parts = s.split(regs.undHash), - i = 1; - parts[0] = parts[0].charAt(0).toLowerCase() + parts[0].substr(1); - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(''); - }, - /** - * Like camelize, but the first part is also capitalized - * @param {String} s - * @return {String} the classized string - */ - classize: function( s ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(''); - }, - /** - * Like [jQuery.String.static.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 ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(' '); - }, - - /** - * 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.lowerUpper, '$1_$2'). - replace(regs.dash, '_').toLowerCase() - } - }); - - -})(true); - -// jquery/model/model.js - -(function($){ - - //a cache for attribute capitalization ... slowest part of inti. - var underscore = $.String.underscore, - classize = $.String.classize; - - /** - * @tag core - * @download jquery/dist/jquery.model.js - * @test jquery/model/qunit.html - * @plugin jquery/model - * - * Models wrap an application's data layer. In large applications, a model is critical for: - * - * - Encapsulating services so controllers + views don't care where data comes from. - * - * - Providing helper functions that make manipulating and abstracting raw service data easier. - * - * This is done in two ways: - * - * - Requesting data from and interacting with services - * - * - Converting or wrapping raw service data into a more useful form. - * - * - * ## Basic Use - * - * The [jQuery.Model] class provides a basic skeleton to organize pieces of your application's data layer. - * First, consider doing Ajax without a model. In our imaginary app, you: - * - * - retrieve a list of tasks - * - display the number of days remaining for each task - * - mark tasks as complete after users click them - * - * Let's see how that might look without a model: - * - * @codestart - * $.Controller.extend("MyApp.Controllers.Tasks",{onDocument: true}, - * { - * // get tasks when the page is ready - * ready: function() { - * $.get('/tasks.json', this.callback('gotTasks'), 'json') - * }, - * /* - * * assume json is an array like [{name: "trash", due_date: 1247111409283}, ...] - * *| - * gotTasks: function( json ) { - * for(var i =0; i < json.length; i++){ - * var taskJson = json[i]; - * - * //calculate time remaining - * var remaininTime = new Date() - new Date(taskJson.due_date); - * - * //append some html - * $("#tasks").append("<div class='task' taskid='"+taskJson.id+"'>"+ - * "<label>"+taskJson.name+"</label>"+ - * "Due Date = "+remaininTime+"</div>") - * } - * }, - * // when a task is complete, get the id, make a request, remove it - * ".task click" : function( el ) { - * $.post('/task_complete',{id: el.attr('data-taskid')}, function(){ - * el.remove(); - * }) - * } - * }) - * @codeend - * - * This code might seem fine for right now, but what if: - * - * - The service changes? - * - Other parts of the app want to calculate remaininTime? - * - Other parts of the app want to get tasks? - * - The same task is represented multiple palces on the page? - * - * The solution is of course a strong model layer. Lets look at what a - * a good model does for a controller before we learn how to make one: - * - * @codestart - * $.Controller.extend("MyApp.Controllers.Tasks",{onDocument: true}, - * { - * load: function() { - * Task.findAll({},this.callback('list')) - * }, - * list: function( tasks ) { - * $("#tasks").html(this.view(tasks)) - * }, - * ".task click" : function( el ) { - * el.models()[0].complete(function(){ - * el.remove(); - * }); - * } - * }) - * @codeend - * - * In views/tasks/list.ejs - * - * @codestart html - * <% for(var i =0; i < tasks.length; i++){ %> - * <div class='task <%= tasks[i].identity() %>'> - * <label><%= tasks[i].name %></label> - * <%= tasks[i].timeRemaining() %> - * </div> - * <% } %> - * @codeend - * - * Isn't that better! Granted, some of the improvement comes because we used a view, but we've - * also made our controller completely understandable. Now lets take a look at the model: - * - * @codestart - * $.Model.extend("Task", - * { - * findAll: function( params,success ) { - * $.get("/tasks.json", params, this.callback(["wrapMany",success]),"json"); - * } - * }, - * { - * timeRemaining: function() { - * return new Date() - new Date(this.due_date) - * }, - * complete: function( success ) { - * $.get("/task_complete", {id: this.id }, success,"json"); - * } - * }) - * @codeend - * - * There, much better! Now you have a single place where you can organize Ajax functionality and - * wrap the data that it returned. Lets go through each bolded item in the controller and view.
    - * - * ### Task.findAll - * - * The findAll function requests data from "/tasks.json". When the data is returned, it it is run through - * the "wrapMany" function before being passed to the success callback.
    - * If you don't understand how the callback works, you might want to check out - * [jQuery.Model.static.wrapMany wrapMany] and [jQuery.Class.static.callback callback]. - * - * ### el.models - * - * [jQuery.fn.models models] is a jQuery helper that returns model instances. It uses - * the jQuery's elements' shortNames to find matching model instances. For example: - * - * @codestart html - * <div class='task task_5'> ... </div> - * @codeend - * - * It knows to return a task with id = 5. - * - * ### complete - * - * This should be pretty obvious. - * - * ### identity - * - * [jQuery.Model.prototype.identity Identity] returns a unique identifier that [jQuery.fn.models] can use - * to retrieve your model instance. - * - * ### timeRemaining - * - * timeRemaining is a good example of wrapping your model's raw data with more useful functionality. - * ## Validations - * - * You can validate your model's attributes with another plugin. See [validation]. - */ - - - jQuery.Class.extend("jQuery.Model", - /** - * @Static - */ - { - setup: function(superClass) { - - //we do not inherit attributes (or associations) - if(!this.attributes || superClass.attributes === this.attributes){ - this.attributes = {}; - } - - if(!this.associations || superClass.associations === this.associations){ - this.associations = {}; - } - if(!this.validations || superClass.validations === this.validations){ - this.validations = {}; - } - - //add missing converters - if(superClass.convert != this.convert){ - this.convert = $.extend(superClass.convert, this.convert); - } - - - this._fullName = underscore(this.fullName.replace(/\./g, "_")); - - if ( this.fullName.substr(0, 7) == "jQuery." ) { - return; - } - - //add this to the collection of models - jQuery.Model.models[this._fullName] = this; - - if ( this.listType ) { - this.list = new this.listType([]); - } - - }, - /** - * @attribute attributes - * Attributes contains a list of properties and their types - * for this model. You can use this in conjunction with - * [jQuery.Model.static.convert] to provide automatic - * [jquery.model.typeconversion type conversion]. - * - * The following converts dueDates to JavaScript dates: - * - * @codestart - * $.Model.extend("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; - * } - * } - * } - * },{}) - * @codeend - */ - attributes : {}, - /** - * @attribute defaults - * An object of default values to be set on all instances. This - * is useful if you want some value to be present when new instances are created. - * - * @codestart - * $.Model.extend("Recipe",{ - * defaults : { - * createdAt : new Date(); - * } - * },{}) - * - * var recipe = new Recipe(); - * - * recipe.createdAt //-> date - * - * @codeend - */ - defaults: {}, - /** - * Wrap is used to create a new instance from data returned from the server. - * It is very similar to doing new Model(attributes) - * except that wrap will check if the data passed has an - * - * - attributes, - * - data, or - * - singularName - * - * property. If it does, it will use that objects attributes. - * - * Wrap is really a convience method for servers that don't return just attributes. - * - * @param {Object} attributes - * @return {Model} an instance of the model - */ - wrap: function( attributes ) { - if (!attributes ) { - return null; - } - return new this( - // checks for properties in an object (like rails 2.0 gives); - attributes[this.singularName] || attributes.data || attributes.attributes || attributes); - }, - /** - * Takes raw data from the server, and returns an array of model instances. - * Each item in the raw array becomes an instance of a model class. - * - * @codestart - * $.Model.extend("Recipe",{ - * helper : function(){ - * return i*i; - * } - * }) - * - * var recipes = Recipe.wrapMany([{id: 1},{id: 2}]) - * recipes[0].helper() //-> 1 - * @codeend - * - * If an array is not passed to wrapMany, it will look in the object's .data - * property. - * - * For example: - * - * @codestart - * var recipes = Recipe.wrapMany({data: [{id: 1},{id: 2}]}) - * recipes[0].helper() //-> 1 - * @codeend - * - * @param {Array} instancesRawData an array of raw name - value pairs. - * @return {Array} a JavaScript array of instances or a [jQuery.Model.List list] of instances - * if the model list plugin has been included. - */ - wrapMany: function( instancesRawData ) { - if (!instancesRawData ) return null; - var res = new(this.List || $.Model.List || Array), - arr = $.isArray(instancesRawData), - raw = arr ? instancesRawData : instancesRawData.data, - length = raw.length, - i = 0; - res._use_call = true; //so we don't call next function with all of these - for (; i < length; i++ ) { - res.push(this.wrap(raw[i])); - } - if (!arr ) { //push other stuff onto array - for ( var prop in instancesRawData ) { - if ( prop !== 'data' ) { - res[prop] = instancesRawData[prop]; - } - - } - } - 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.extend("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 ) { - if ( this.associations[property] ) return; - this.attributes[property] || (this.attributes[property] = type); - return type - }, - // a collection of all models - models: {}, - /** - * If OpenAjax is available, - * publishes to OpenAjax.hub. Always adds the shortName.event. - * - * @codestart - * // publishes contact.completed - * Namespace.Contact.publish("completed",contact); - * @codeend - * - * @param {String} event The event name to publish - * @param {Object} data The data to publish - */ - publish: function( event, data ) { - - if ( window.OpenAjax ) { - OpenAjax.hub.publish(underscore(this.shortName) + "." + event, data); - } - - }, - /** - * @hide - * Guesses the type of an object. This is what sets the type if not provided in - * [jQuery.Model.static.attributes]. - * @param {Object} object the object you want to test. - * @return {String} one of string, object, date, array, boolean, number, function - */ - guessType: function( object ) { - if ( typeof object != 'string' ) { - if ( object == null ) return typeof object; - if ( object.constructor == Date ) return 'date'; - if ( $.isArray(object) ) return 'array'; - return typeof object; - } - if ( object == "" ) return 'string'; - //check if true or false - if (object == 'true' || object == 'false') { - return 'boolean'; - } - if (!isNaN(object) && isFinite(+object) ) { - return 'number'; - } - return typeof object; - }, - /** - * @attribute convert - * @type Object - * An object of name-function pairs that are used to convert attributes. - * Check out [jQuery.Model.static.attributes] or - * [jquery.model.typeconversion type conversion] - * for examples. - */ - convert: { - "date": function( str ) { - return typeof str == "string" ? (Date.parse(str) == NaN ? null : Date.parse(str)) : str - }, - "number": function( val ) { - return parseFloat(val) - }, - "boolean": function( val ) { - return Boolean(val) - } - }, - /** - * Implement this function! - * Create is called by save to create a new instance. If you want to be able to call save on an instance - * you have to implement create. - */ - create: function( attrs, success, error ) { - throw "Model: Implement Create" - }, - /** - * Implement this function! - * Update is called by save to update an instance. If you want to be able to call save on an instance - * you have to implement update. - */ - update: function( id, attrs, success, error ) { - throw "Model: Implement " + this.fullName + "'s \"update\"!" - }, - /** - * Implement this function! - * Destroy is called by destroy to remove an instance. If you want to be able to call destroy on an instance - * you have to implement update. - * @param {String|Number} id the id of the instance you want destroyed - */ - destroy: function( id, success, error ) { - throw "Model: Implement " + this.fullName + "'s \"destroy\"!" - }, - /** - * Implement this function! - * @param {Object} params - * @param {Function} success - * @param {Function} error - */ - findAll : function(params, success, error) { - - }, - /** - * Implement this function! - * @param {Object} params - * @param {Function} success - * @param {Function} error - */ - findOne : function(params, success, error){ - - } - }, - /** - * @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.extend("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._initializing = true; - - this.Class.defaults && this.attrs(this.Class.defaults); - - this.attrs(attributes); - delete this._initializing; - }, - /** - * Sets the attributes on this instance and calls save. - * The instance needs to have an id. It will use - * the instance class's [jQuery.Model.static.update update] - * method. - * - * @codestart - * recipe.update({name: "chicken"}, success, error); - * @codeend - * - * If OpenAjax.hub is available, the model will also - * publish a "modelName.updated" message with - * the updated instance. - * - * @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. - * - * @codestart - * $.Model.extend("Task",{ - * init : function(){ - * this.validatePresenceOf("dueDate") - * } - * },{}); - * - * var task = new Task(), - * errors = task.errors() - * - * errors.dueDate[0] //-> "can't be empty" - * @codeend - */ - errors: function(attrs) { - if(attrs){ - attrs = $.isArray(attrs) ? attrs : $.makeArray(arguments); - } - var errors = {}, - self = this, - addErrors = function(attr, funcs){ - $.each(funcs, function(i, func){ - var res = func.call(self); - if(res){ - if(!errors.hasOwnProperty(attr)){ - errors[attr] = []; - } - - errors[attr].push(res); - } - - }) - }; - - $.each(attrs || this.Class.validations || {}, function( attr, funcs ) { - if(typeof attr == 'number'){ - attr = funcs; - funcs = self.Class.validations[attr]; - } - addErrors(attr, funcs || []) - }); - - for(var attr in errors){ - return errors; - } - return null; - }, - /** - * Gets or sets an attribute on the model using setters and - * getters if available. - * - * @codestart - * $.Model.extend("Recipe") - * var recipe = new Recipe(); - * recipe.attr("foo","bar") - * recipe.foo //-> "bar" - * recipe.attr("foo") //-> "bar" - * @codeend - * - * ## Setters - * - * If you add a setAttributeName 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.extend("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.extend("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 [jQuery.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 ) { - var cap = classize(attribute), - get = "get" + cap; - if (value !== undefined) { - this._setProperty(attribute, value, success, error, cap); - return this; - } - return this[get] ? this[get]() : this[attribute]; - }, - /** - * 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.extend("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.extend("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 - */ - bind: function(eventType, handler){ - var wrapped = $(this); - wrapped.bind.apply(wrapped, arguments); - return this; - }, - /** - * Unbinds an event handler from this instance. - * Read [jQuery.Model.prototype.bind] for - * more information. - * @param {String} eventType - * @param {Function} handler - */ - unbind: function(eventType, handler){ - var wrapped = $(this); - wrapped.unbind.apply(wrapped, arguments); - return this; - }, - /** - * Checks if there is a set_property value. If it returns true, lets it handle; otherwise - * saves it. - * @hide - * @param {Object} property - * @param {Object} value - */ - _setProperty: function( property, value, success, error, capitalized ) { - // the potential setter name - var setName = "set" + capitalized, - //the old value - old = this[property], - self = this, - errorCallback = function(errors){ - error && error.call(self,errors); - $(self).triggerHandler("error."+property, errors); - }; - - // if the setter returns nothing, do not set - // we might want to indicate if this was set ok - if (this[setName] && - (value = this[setName]( value, - this.callback('_updateProperty',property, value, old, success, errorCallback), - errorCallback)) === undefined ) { - return ; - } - this._updateProperty(property, value, old, success, errorCallback ) - }, - /** - * Triggers events when a property has been updated - * @hide - * @param {Object} property - * @param {Object} value - * @param {Object} old - * @param {Object} success - */ - _updateProperty : function(property, value, old, success, errorCallback){ - var Class = this.Class, - val, - old, - type = Class.attributes[property] || Class.addAttr(property, Class.guessType(value)), - //the converter - converter = Class.convert[type], - errors = null; - - val = this[property] = - ( value == null ? //if the value is null or undefined - null : // it should be null - (converter ? - converter.call(Class, value) : //convert it to something useful - value) ) //just return it - - - //validate (only if not initializing, this is for performance) - if(!this._initializing){ - errors = this.errors(property); - } - - if(errors){ - errorCallback(errors) - }else{ - if(old !== val && !this._initializing){ - $(this).triggerHandler(property, val); - } - success && success(this); - - } - - //if this class has a global list, add / remove from the list. - if ( property == Class.id && val != null && Class.list ) { - // if we didn't have an old id, add ourselves - if (!old ) { - Class.list.push(this); - } else if ( old != val ) { - // if our id has changed ... well this should be ok - Class.list.remove(old); - Class.list.push(this); - } - } - - }, - /** - * Gets or sets a list of attributes. - * Each attribute is set with [jQuery.Model.prototype.attr attr]. - * - * @codestart - * recipe.attrs({ - * name: "ice water", - * instructions : "put water in a glass" - * }) - * @codeend - * - * @param {Object} [attributes] if present, the list of attributes to send - * @return {Object} the current attributes of the model - */ - attrs: function( attributes ) { - var key; - if (!attributes ) { - attributes = {}; - for ( key in this.Class.attributes ) { - attributes[key] = this.attr(key); - } - } else { - var idName = this.Class.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; - }, - /** - * Returns if the instance is a new object - */ - isNew: function() { - return this[this.Class.id] == null; //if null or undefined - }, - /** - * Saves the instance if there are no errors. - * If the instance is new, [jQuery.Model.static.create] is - * called; otherwise, [jQuery.Model.static.update] is - * called. - * - * @codestart - * recipe.save(success, error); - * @codeend - * - * If OpenAjax.hub is available, after a successful create or update, - * "modelName.created" or "modelName.updated" is published. - * - * @param {Function} [success] called if a successful save. - * @param {Function} [error] called if the save was not successful. - */ - save: function( success, error ) { - - if (this.errors() ) { - //needs to send errors - return false; - } - this.isNew() ? this.Class.create(this.attrs(), this.callback(['created', success]), error) : this.Class.update(this[this.Class.id], this.attrs(), this.callback(['updated', success]), error); - - //this.is_new_record = this.Class.new_record_func; - return true; - }, - - /** - * Destroys the instance by calling - * [jQuery.Model.static.destroy] with the id of the instance. - * - * @codestart - * recipe.destroy(success, error); - * @codeend - * - * If OpenAjax.hub is available, after a successful - * destroy "modelName.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 ) { - this.Class.destroy(this[this.Class.id], this.callback(["destroyed", success]), error); - }, - - - /** - * 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 [jQuery.Model.prototype.elements elements]. - * @return {String} - */ - identity: function() { - var id = this[this.Class.id] - return this.Class._fullName + '_' + (this.Class.escapeIdentity ? encodeURIComponent(id) : id); - }, - /** - * Returns elements that represent this model instance. For this to work, your element's should - * us the [jQuery.Model.prototype.identity identity] function in their class name. Example: - * @codestart html - *
    ...
    - * @codeend - * This function should only rarely be used. It breaks the architecture. - * @param {String|jQuery|element} context - - */ - elements: function( context ) { - return $("." + this.identity(), context); - }, - /** - * Publishes to open ajax hub - * @param {String} event - * @param {Object} [opt6] data if missing, uses the instance in {data: this} - */ - publish: function( event, data ) { - this.Class.publish(event, data || this); - }, - hookup: function( el ) { - var shortName = underscore(this.Class.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" - */ - "destroyed"], function( i, funcName ) { - $.Model.prototype[funcName] = function( attrs ) { - if ( funcName === 'destroyed' && this.Class.list ) { - this.Class.list.remove(this[this.Class.id]); - } - $(this).triggerHandler(funcName) - attrs && typeof attrs == 'object' && this.attrs(attrs.attrs ? attrs.attrs() : attrs); - this.publish(funcName, this) - return [this].concat($.makeArray(arguments)); - } - }) - - /** - * @add jQuery.fn - */ - // break - /** - * @function models - * Returns a list of models. If the models are of the same - * type, and have a [jQuery.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|jQuery.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; - this.each(function() { - $.each($.data(this, "models") || {}, function( name, instance ) { - //either null or the list type shared by all classes - kind = kind === undefined ? instance.Class.List || null : (instance.Class.List === kind ? kind : null) - collection.push(instance) - }) - }); - ret = new(kind || $.Model.List || Array)() - ret.push.apply(ret, $.unique(collection)) - return ret; - } - /** - * @function model - * - * Returns the first model instance found from [jQuery.fn.models]. - * - * @param {Object} type - */ - $.fn.model = function(type) { - if(type && type instanceof $.Model){ - type.hookup(this[0]); - return this; - }else{ - return this.models.apply(this, arguments)[0]; - } - - } - -})(true); - diff --git a/dist/jquery.view.ejs.js b/dist/jquery.view.ejs.js deleted file mode 100644 index 505bb028..00000000 --- a/dist/jquery.view.ejs.js +++ /dev/null @@ -1,1043 +0,0 @@ -// jquery/view/view.js - -(function($){ - - - // converts to an ok dom id - var toId = function( src ) { - return src.replace(/^\/\//, "").replace(/[\/\.]/g, "_") - }, - // used for hookup ids - id = 1; - - /** - * @class jQuery.View - * @tag core - * @plugin jquery/view - * @test jquery/view/qunit.html - * - * 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.replace replace], [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text] like: - * @codestart - * $('.foo').html("//path/to/view.ejs",{}) - * @codeend - *
    • - *
    • Compress processed views with [steal.static.views].
    • - *
    • Use the [jQuery.Controller.prototype.view controller/view] plugin to auto-magically - * lookup views.
    • - *
    • Hookup jquery plugins directly in the template.
    • - * - *
    - * - * ## Supported Template Engines - * - * JavaScriptMVC comes with the following template languages: - * - * - [jQuery.EJS EJS] - provides an ERB like syntax: <%= %> - * - [Jaml] - A functional approach to JS templates. - * - [Micro] - A very lightweight template similar to EJS. - * - [jQuery.tmpl] - A very lightweight template similar to EJS. - * - * There are 3rd party plugins that provide other template - * languages. - * - * ## Use - * - * Views provide client side templating. When you use a view, you're - * almost always wanting to insert the rendered content into the page. - * - * For this reason, the most common way to use a views is through - * jQuery modifier functions like [jQuery.fn.html html]. The view - * plugin overwrites these functions so you can render a view and - * insert its contents into the page with one convenient step. - * - * The following renders the EJS template at - * //app/view/template.ejs with the second parameter used as data. - * It inserts the result of the template into the - * '#foo' element. - * - * @codestart - * $('#foo').html('//app/view/template.ejs', - * {message: "hello world"}) - * @codeend - * - * //app/view/template.ejs might look like: - * - * @codestart xml - * <h2><%= message %></h2></div> - * @codeend - * - * The resulting output would be: - * - * @codestart xml - * <div id='foo'><h2>hello world</h2></div> - * @codeend - * - * The specifics of each templating languages are covered in their - * individual documentation pages. - * - * ### Template Locations - * - * In the example above, we used - * //app/view/template.ejs as the location of - * our template file. Using // at the start of a path - * references the template from the root JavaScriptMVC directory. - * - * If there is no // at the start of the path, the view is looked up - * relative to the current page. - * - * It's recommended that you use paths rooted from the JavaScriptMVC - * directory. This will make your code less likely to change. - * - * You can also use the [jQuery.Controller.prototype.view controller/view] - * plugin to make looking up templates a little easier. - * - * ### Using $.View - * - * Sometimes you want to get the string result of a view and not - * insert it into the page right away. Nested templates are a good - * example of this. For this, you can use $.View. The following - * iterates through a list of contacts, and inserts the result of a - * sub template in each: - * - * @codestart xml - * <% for(var i =0 ; i < contacts.length; i++) { %> - * <%= $.View("//contacts/contact.ejs",contacts[i]) %> - * <% } %> - * @codeend - * - * ## Compress Views with Steal - * - * Steal can package processed views in the production file. Because 'stolen' views are already - * processed, they don't rely on eval. Here's how to steal them: - * - * @codestart - * steal.views('//views/tasks/show.ejs'); - * @codeend - * - * Read more about [steal.static.views steal.views]. - * - * ## 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 - * <ul <%= Mxui.Tabs%>>...<ul> - * @codeend - * - * You can even hook up multiple controllers: - * - * @codestart - * <ul <%= [Mxui.Tabs, Mxui.Filler]%>>...<ul> - * @codeend - * - * @constructor - * - * Looks up a template, processes it, caches it, then renders the template - * with data and optional helpers. - * - * @codestart - * $.View("//myplugin/views/init.ejs",{message: "Hello World"}) - * @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. - * @return {String} The rendered result of the view. - */ - var $view = $.View = function( view, data, helpers ) { - var suffix = view.match(/\.[\w\d]+$/), - type, - el, - url, - id, - renderer, - url = view; - - //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 - if ( url.match(/^\/\//) ) { - url = steal.root.join(url.substr(2)); //can steal be removed? - } - - //get the template engine - type = $.View.types[suffix]; - - //get the renderer function - var renderer = - $.View.cached[id] ? // is it cached? - $.View.cached[id] : // use the cached version - ((el = document.getElementById(view)) ? //is it in the document? - type.renderer(id, el.innerHTML) : //use the innerHTML of the elemnt - get(type, id, url) //do an ajax request for it - ); - - //if we should cache templates - if ( $.View.cache ) { - $.View.cached[id] = renderer; - } - return renderer.call(type, data, helpers) - }, - get = function(type, id, url){ - var text = $.ajax({ - async: false, - url: url, - dataType: "text", - error: function() { - throw "$.View ERROR: There is no template or an empty template at " + url; - } - }).responseText - if (!text.match(/[^\s]/) ) { - throw "$.View ERROR: There is no template or an empty template at " + url; - } - return type.renderer(id, text); - }; - - - $.extend($.View, { - /** - * @attribute hookups - * @hide - * A list of pending 'hookups' - */ - hookups: {}, - /** - * @function hookup - * Registers a hookup function to be called back after the html is put on the page - * @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. - * @param {Object} info a object of method and properties - * that enable template integration: - *
      - *
    • suffix - the view extension. EX: 'ejs'
    • - *
    • 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).
    • - *
    • renderer(id, text) - a function that takes the id of the template and the text of the template and - * returns a render function.
    • - *
    - */ - register: function( info ) { - this.types["." + info.suffix] = info; - }, - 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) - } - } - - }) - - - //---- ADD jQUERY HELPERS ----- - var - //converts jquery functions to use views - convert = function( func_name ) { - var old = jQuery.fn[func_name]; - - jQuery.fn[func_name] = function() { - var args = arguments, - res, hasHookup, secArgType = typeof arguments[1]; - - //check if a template - if ( typeof arguments[0] == "string" && (secArgType == 'object' || secArgType == 'function') && !arguments[1].nodeType && !arguments[1].jquery ) { - args = [$.View.apply($.View, $.makeArray(arguments))]; - } - - //check if there are new hookups - for ( var hasHookups in jQuery.View.hookups ) {}; - - //if there are hookups, get jQuery object - if ( hasHookups ) { - args[0] = $(args[0]) - } - res = old.apply(this, args) - - //now hookup hookups - if ( hasHookups ) { - hookupView(args[0]) - } - return res; - } - }, - hookupView = function( els ) { - //remove all hookups - var hooks = jQuery.View.hookups, - hookupEls, len, i = 0, - id, func; - jQuery.View.hookups = {}; - 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(jQuery.View.hookups, hooks); - }, - /** - * @add jQuery.fn - */ - funcs = [ - /** - * @function prepend - * @parent jQuery.View - * abc - */ - "prepend", - /** - * @function append - * @parent jQuery.View - * abc - */ - "append", - /** - * @function after - * @parent jQuery.View - * abc - */ - "after", - /** - * @function before - * @parent jQuery.View - * abc - */ - "before", - /** - * @function replace - * @parent jQuery.View - * abc - */ - "replace", - /** - * @function text - * @parent jQuery.View - * abc - */ - "text", - /** - * @function html - * @parent jQuery.View - * abc - */ - "html", - /** - * @function replaceWith - * @parent jQuery.View - * abc - */ - "replaceWith"]; - - //go through helper funcs and convert - for ( var i = 0; i < funcs.length; i++ ) { - convert(funcs[i]); - } - - -})(true); - -// jquery/lang/lang.js - -(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, - lowerUpper: /([a-z\d])([A-Z])/g, - dash: /([a-z\d])([A-Z])/g - }; - - /** - * @class jQuery.String - */ - var str = ($.String = /* @Static*/ { - /** - * @function strip - * @param {String} s returns a string with leading and trailing whitespace removed. - */ - strip: function( string ) { - return string.replace(/^\s+/, '').replace(/\s+$/, ''); - }, - /** - * Capitalizes a string - * @param {String} s the string to be lowercased. - * @return {String} a string with the first character capitalized, and everything else lowercased - */ - capitalize: function( s, cache ) { - return s.charAt(0).toUpperCase() + s.substr(1); - }, - - /** - * Returns if string ends with another string - * @param {String} s String that is being scanned - * @param {String} pattern What the string might end with - * @return {Boolean} true if the string ends wtih pattern, false if otherwise - */ - endsWith: function( s, pattern ) { - var d = s.length - pattern.length; - return d >= 0 && s.lastIndexOf(pattern) === d; - }, - /** - * 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 ) { - var parts = s.split(regs.undHash), - i = 1; - parts[0] = parts[0].charAt(0).toLowerCase() + parts[0].substr(1); - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(''); - }, - /** - * Like camelize, but the first part is also capitalized - * @param {String} s - * @return {String} the classized string - */ - classize: function( s ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(''); - }, - /** - * Like [jQuery.String.static.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 ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) - parts[i] = str.capitalize(parts[i]); - return parts.join(' '); - }, - - /** - * 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.lowerUpper, '$1_$2'). - replace(regs.dash, '_').toLowerCase() - } - }); - - -})(true); - -// jquery/lang/rsplit/rsplit.js - -(function($){ - - /** - * @add jQuery.String.static - */ - $.String. - /** - * Splits a string with a regex correctly cross browser - * @param {Object} string - * @param {Object} regex - */ - 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; - } - -})(true); - -// jquery/view/ejs/ejs.js - -(function($){ - - - //helpers we use - var chop = function( string ) { - return string.substr(0, string.length - 1); - }, - extend = $.extend, - isArray = $.isArray, - EJS = function( options ) { - //returns a renderer function - if ( this.constructor != EJS ) { - var ejs = new EJS(options); - return function( data, helpers ) { - return ejs.render(data, helpers) - }; - } - - if ( typeof options == "function" ) { - this.template = {}; - this.template.process = options; - return; - } - //set options on self - $.extend(this, EJS.options, options) - - var template = new EJS.Compiler(this.text, this.type); - - template.compile(options, this.name); - - this.template = template; - }, - defaultSplitter = /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/; - /** - * @class jQuery.EJS - * @plugin jquery/view/ejs - * @parent jQuery.View - * @download jquery/dist/jquery.view.ejs.js - * @test jquery/view/ejs/qunit.html - * Ejs provides ERB - * 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 - * <ul> - * <% for(var i = 0; i < tasks.length; i++){ %> - * <li class="task <%= tasks[i].identity %>"><%= tasks[i].name %></li> - * <% } %> - * </ul> - * @codeend - * For the following examples, we assume this view is in 'views\tasks\list.ejs' - *

    Use

    - * There are 2 common ways to use Views: - *
      - *
    • Controller's [jQuery.Controller.prototype.view view function]
    • - *
    • The jQuery Helpers: [jQuery.fn.after after], - * [jQuery.fn.append append], - * [jQuery.fn.before before], - * [jQuery.fn.before html], - * [jQuery.fn.before prepend], - * [jQuery.fn.before replace], and - * [jQuery.fn.before text].
    • - *
    - *

    View

    - * jQuery.Controller.prototype.view is the preferred way of rendering a view. - * You can find all the options for render in - * its [jQuery.Controller.prototype.view documentation], but here is a brief example of rendering the - * list.ejs view from a controller: - * @codestart - * $.Controller.extend("TasksController",{ - * init: function( el ) { - * Task.findAll({},this.callback('list')) - * }, - * list: function( tasks ) { - * this.element.html( - * this.view("list", {tasks: tasks}) - * ) - * } - * }) - * @codeend - * - * - *

    View Helpers

    - * 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 - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    OptionDefaultDescription
    url loads the template from a file. This path should be relative to [jQuery.root]. - *
    text uses the provided text as the template. Example:
    new View({text: '<%=user%>'}) - *
    element loads a template from the innerHTML or value of the element. - *
    type'<'type of magic tags. Options are '<' or '[' - *
    namethe element ID or url an optional name that is used for caching. - *
    cachetrue in production mode, false in other modestrue to cache template. - *
    - */ - $.EJS = EJS; - /** - * @Prototype - */ - EJS.prototype = { - constructor: EJS, - /** - * Renders an object with extra view helpers attached to the view. - * @param {Object} object data to be rendered - * @param {Object} extra_helpers an object with additonal 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.process.call(object, object, v); - }, - out: function() { - return this.template.out; - } - }; - - - // given a value in <%= %> do something with it - EJS.text = function( input ) { - var myid; - if (input == null || input === undefined) { - return ''; - } - if (input instanceof Date) { - return input.toDateString(); - } - if ( input.hookup ) { - myid = $.View.hookup(function( el, id ) { - input.hookup.call(input, el, id) - }); - return "data-view-id='" + myid + "'" - } - if (typeof input == 'function') { - return "data-view-id='" + $.View.hookup(input) + "'"; - } - - if ( isArray(input) ) { - myid = $.View.hookup(function( el, id ) { - for ( var i = 0; i < input.length; i++ ) { - input[i].hookup ? input[i].hookup(el, id) : input[i](el, id) - } - }); - return "data-view-id='" + myid + "'" - } - if ( input.nodeName || input.jQuery ) { - throw "elements in views are not supported" - } - - if (input.toString) { - return myid ? input.toString(myid) : input.toString(); - } - return ''; - }; - - - /* @Static*/ - - // used to break text into tolkens - EJS.Scanner = function( source, left, right ) { - - // add these properties to the scanner - extend(this, { - leftDelimiter: left + '%', - rightDelimiter: '%' + right, - doubleLeft: left + '%%', - doubleRight: '%%' + right, - leftEqual: left + '%=', - leftComment: left + '%#' - }); - - - // make a regexp that can split on these token - this.splitRegexp = (left == '[' ? - defaultSplitter - : new RegExp("("+ - [this.doubleLeft, - this.doubleRight, - this.leftEqual, - this.leftComment, - this.leftDelimiter, - this.rightDelimiter + '\n', - this.rightDelimiter, - '\n'].join(")|(")+")") ) - - this.source = source; - this.lines = 0; - }; - - - EJS.Scanner.prototype = { - // calls block with each token - scan: function( block ) { - var regex = this.splitRegexp; - if (!this.source == '' ) { - var source_split = $.String.rsplit(this.source, /\n/); - for ( var i = 0; i < source_split.length; i++ ) { - var item = source_split[i]; - this.scanline(item, regex, block); - } - } - }, - scanline: function( line, regex, block ) { - this.lines++; - var line_split = $.String.rsplit(line, regex), - token; - for ( var i = 0; i < line_split.length; i++ ) { - token = line_split[i]; - if ( token != null ) { - try { - block(token, this); - } catch (e) { - throw { - type: 'jQuery.EJS.Scanner', - line: this.lines - }; - } - } - } - } - }; - - // a line and script buffer - // we use this so we know line numbers when there - // is an error. - // pre and post are setup and teardown for the buffer - EJS.Buffer = function( pre_cmd, post_cmd ) { - this.line = []; - this.script = []; - this.post_cmd = post_cmd; - - // add the pre commands to the first line - this.push.apply(this, pre_cmd); - }; - EJS.Buffer.prototype = { - //need to maintain your own semi-colons (for performance) - push: function( ) { - this.line.push.apply(this.line, arguments); - }, - - cr: function() { - this.script.push( this.line.join(''), "\n"); - this.line = []; - }, - //returns the script too - close: function() { - if ( this.line.length > 0 ) { - this.script.push(this.line.join('')) - line = null; - } - this.post_cmd.length && this.push.apply(this, this.post_cmd) - - this.script.push(";"); //makes sure we always have an ending / - return this.script.join("") - } - - }; - // compiles a template - EJS.Compiler = function( source, left ) { - //normalize line endings - this.source = source.replace(/\r\n/g, "\n") - .replace(/\r/g, "\n"); - - left = left || '<'; - var right = '>'; - switch ( left ) { - case '[': - right = ']'; - break; - case '<': - break; - default: - throw left + ' is not a supported deliminator'; - break; - } - this.scanner = new EJS.Scanner(this.source, left, right); - this.out = ''; - }; - EJS.Compiler.prototype = { - compile: function( options, name ) { - - options = options || {}; - - this.out = ''; - - var put_cmd = "___v1ew.push(", - insert_cmd = put_cmd, - buff = new EJS.Buffer(['var ___v1ew = [];'], []), - content = '', - clean = function( content ) { - return content.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\n') - .replace(/"/g, '\\"'); - }, - put = function(content){ - buff.push(put_cmd , '"' , clean(content) , '");'); - } - startTag = null; - - this.scanner.scan(function( token, scanner ) { - // if we don't have a start pair - if ( startTag == null ) { - switch ( token ) { - case '\n': - content = content + "\n"; - put(content); - //buff.push(put_cmd , '"' , clean(content) , '");'); - buff.cr(); - content = ''; - break; - case scanner.leftDelimiter: - case scanner.leftEqual: - case scanner.leftComment: - startTag = token; - if ( content.length > 0 ) { - put(content); - } - content = ''; - break; - - // replace <%% with <% - case scanner.doubleLeft: - content = content + scanner.leftDelimiter; - break; - default: - content = content + token; - break; - } - } - else { - switch ( token ) { - case scanner.rightDelimiter: - switch ( startTag ) { - case scanner.leftDelimiter: - if ( content[content.length - 1] == '\n' ) { - content = chop(content); - buff.push(content,";"); - buff.cr(); - } - else { - buff.push(content,";"); - } - break; - case scanner.leftEqual: - buff.push(insert_cmd , "(jQuery.EJS.text(" , content , ")));"); - break; - } - startTag = null; - content = ''; - break; - case scanner.doubleRight: - content = content + scanner.rightDelimiter; - break; - default: - content = content + token; - break; - } - } - }); - if ( content.length > 0 ) { - // Should be content.dump in Ruby - buff.push(put_cmd , '"' , clean(content) + '");'); - } - this.out = '/*' + name + '*/ try { with(_VIEW) { with (_CONTEXT) {' + buff.close() + " return ___v1ew.join('');}}}catch(e){e.lineNumber=null;throw e;}"; - this.process = new Function("_CONTEXT","_VIEW",this.out) - } - }; - - - //type, cache, folder - /** - * @attribute options - * Sets default options for all views - * - * - * - * - * - * - * - * - * - * - * - * - *
    OptionDefaultDescription
    type'<'type of magic tags. Options are '<' or '[' - *
    cachetrue in production mode, false in other modestrue to cache template. - *
    - * - */ - EJS.options = { - cache: true, - type: '<', - ext: '.ejs' - } - - - - - /** - * @class jQuery.EJS.Helpers - * By adding functions to jQuery.EJS.Helpers.prototype, those functions will be available in the - * views. - * @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 = { - /** - * Makes a plugin - * @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 $.View(). - */ - view: function( url, data, helpers ) { - helpers = helpers || this._extras - data = data || this._data; - return $.View(url, data, helpers) //new EJS(options).render(data, helpers); - } - }; - - - $.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}).out() + - " })"; - }, - renderer: function( id, text ) { - var ejs = new EJS({ - text: text, - name: id - }) - return function( data, helpers ) { - return ejs.render.call(ejs, data, helpers) - } - } - }) - - -})(true); - diff --git a/dom/closest/closest.js b/dom/closest/closest.js index 906e865c..14ac24da 100644 --- a/dom/closest/closest.js +++ b/dom/closest/closest.js @@ -1,14 +1,15 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function(){ +steal('jquery/dom').then(function(){ /** * @function closest * @parent dom - * Overwrites closest to allow open > selectors. This allows controller actions such as: - * @codestart - * ">li click" : function( el, ev ) { ... } - * @codeend + * @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){ diff --git a/dom/compare/compare.html b/dom/compare/compare.html index 4e744c69..83c356a8 100644 --- a/dom/compare/compare.html +++ b/dom/compare/compare.html @@ -60,29 +60,31 @@

    Key

    - \ No newline at end of file diff --git a/dom/compare/compare.js b/dom/compare/compare.js index b2af77a4..400e87c2 100644 --- a/dom/compare/compare.js +++ b/dom/compare/compare.js @@ -1,62 +1,80 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function($){ +steal('jquery/dom').then(function($){ /** * @function compare * @parent dom - * @download jquery/dist/jquery.compare.js + * @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. You can expect it to return the same results as + * 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 * @demo jquery/dom/compare/compare.html * @test jquery/dom/compare/qunit.html * @plugin dom/compare - * @param {HTMLElement} a the first node - * @param {HTMLElement} b the second node - * @return {Number} A bitmap with the following digit values: + * + * + * @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: * * * - * - * - * - * - * + * + * + * + * + * *
    BitsNumberMeaning
    0000000Elements are identical.
    0000011The nodes are in different documents (or one is outside of a document).
    0000102Node B precedes Node A.
    0001004Node A precedes Node B.
    0010008Node B contains Node A.
    01000016Node A contains Node B.
    0000011The nodes are in different + * documents (or one is outside of a document).
    0000102#bar precedes #foo.
    0001004#foo precedes #bar.
    0010008#bar contains #foo.
    01000016#foo contains #bar.
    */ -jQuery.fn.compare = function(b){ //usually - //b is usually a relatedTarget, but b/c it is we have to avoid a few FF errors +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 - b = b.jquery ? b[0] : b; + 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(b) - if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]') return null; + + 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(b); + return this[0].compareDocumentPosition(element); } - if(this[0] == document && b != document) return 8; - var number = (this[0] !== b && this[0].contains(b) && 16) + (this[0] != b && b.contains(this[0]) && 8), + 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 < b.sourceIndex && 4) - number += (this[0].sourceIndex > b.sourceIndex && 2) - number += (this[0].ownerDocument !== b.ownerDocument || + 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 ) || - (b != docEl && b.sourceIndex <= 0 )) && 1 + (element != docEl && element.sourceIndex <= 0 )) && 1 }else{ var range = document.createRange(), sourceRange = document.createRange(), compare; range.selectNode(this[0]); - sourceRange.selectNode(b); + sourceRange.selectNode(element); compare = range.compareBoundaryPoints(Range.START_TO_START, sourceRange); } diff --git a/dom/compare/test/qunit/compare_test.js b/dom/compare/compare_test.js similarity index 89% rename from dom/compare/test/qunit/compare_test.js rename to dom/compare/compare_test.js index 056bf7b5..08ef31a8 100644 --- a/dom/compare/test/qunit/compare_test.js +++ b/dom/compare/compare_test.js @@ -1,3 +1,6 @@ +steal("jquery/dom/compare") //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/dom/compare") test("Compare cases", function(){ $(document.body).append("
    ") @@ -16,4 +19,6 @@ test("Compare cases", function(){ equals(first.compare(second), 4, "A sibling elements"); equals(second.compare(first), 2, "A sibling elements"); outer.remove() -}) \ No newline at end of file +}); + +}); \ No newline at end of file diff --git a/dom/compare/qunit.html b/dom/compare/qunit.html index 1f2208f1..d7c40442 100644 --- a/dom/compare/qunit.html +++ b/dom/compare/qunit.html @@ -15,6 +15,6 @@

      - + \ No newline at end of file diff --git a/dom/compare/test/qunit/qunit.js b/dom/compare/test/qunit/qunit.js deleted file mode 100644 index 8ac2c35f..00000000 --- a/dom/compare/test/qunit/qunit.js +++ /dev/null @@ -1,5 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/dom/compare") //load your app - .plugins('funcunit/qunit') //load qunit - .then("compare_test") \ No newline at end of file diff --git a/dom/cookie/cookie.js b/dom/cookie/cookie.js index 38c03e87..5ca88cb7 100644 --- a/dom/cookie/cookie.js +++ b/dom/cookie/cookie.js @@ -1,15 +1,13 @@ -steal.plugins('jquery/lang/json').then(function() { +steal('jquery/lang/json',function() { // break /** * @function jQuery.cookie * @parent dom + * @plugin jquery/dom/cookie * @author Klaus Hartl/klaus.hartl@stilbuero.de * - *

      Cookie plugin

      - * - * - *

      - * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
      + * JavaScriptMVC's packaged cookie plugin is written by + * Klaus Hartl (stilbuero.de)
      * Dual licensed under the MIT and GPL licenses:
      * http://www.opensource.org/licenses/mit-license.php
      * http://www.gnu.org/licenses/gpl.html @@ -21,9 +19,8 @@ steal.plugins('jquery/lang/json').then(function() { *

      Quick Examples

      * * Set the value of a cookie. - * @codestart - * * $.cookie('the_cookie', 'the_value'); - * @codeend + * + * $.cookie('the_cookie', 'the_value'); * * Create a cookie with all available options. * @codestart @@ -59,7 +56,7 @@ steal.plugins('jquery/lang/json').then(function() { * @param {String} [domain] The value of the domain attribute of the cookie (default: domain of page that created the cookie).
      * @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).
      - * @return {String} or {undefined} when setting the cookie. + * @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 diff --git a/dom/cur_styles/cur_styles.html b/dom/cur_styles/cur_styles.html index 51e11c41..a8cfe5f6 100644 --- a/dom/cur_styles/cur_styles.html +++ b/dom/cur_styles/cur_styles.html @@ -23,12 +23,11 @@

      CurStyles Performance

      - - diff --git a/dom/cur_styles/cur_styles.js b/dom/cur_styles/cur_styles.js index b2554297..a563d5b4 100644 --- a/dom/cur_styles/cur_styles.js +++ b/dom/cur_styles/cur_styles.js @@ -1,120 +1,117 @@ -steal.plugin('jquery/dom').then(function($){ +steal('jquery/dom').then(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 ['marginTop','borderLeft'] - * @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, - 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" + 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 ['marginTop','borderLeft'] + * @return {Object} an object of style:value pairs. Style names are camelCase. + */ + $.curStyles = function( el, styles ) { + if (!el ) { + return null; } - - if (getComputedStyle) { - name = name.replace(rupper, "-$1").toLowerCase(); - val = currentS.getPropertyValue(name); - if ( name === "opacity" && val === "" ) { - val = "1"; + 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"; } - results[oldName] = val; - } else { - var camelCase = name.replace(rdashAlpha, fcamelCase); - results[oldName] = currentS[name] || currentS[camelCase]; + + 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 - var left = style.left, + 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"; + // 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; - } + // Revert the changed values + style.left = left; + el.runtimeStyle.left = rsLeft; + } + } } - } - - return results; -}; -/** - * @add jQuery.fn - */ + return results; + }; + /** + * @add jQuery.fn + */ -$.fn. -/** - * @parent dom - * @plugin jquery/dom/cur_styles - * @download jquery/dist/jquery.curstyles.js - * @test jquery/dom/cur_styles/qunit.html - * Use curStyles to rapidly get a bunch of computed styles from an element. - *

      Quick Example

      - * @codestart - * $("#foo").curStyles('float','display') //-> - * // { - * // cssFloat: "left", display: "block" - * // } - * @codeend - *

      Use

      - *

      An element's computed style is the current calculated style of the property. - * This is different than the values on element.style as - * element.style doesn't reflect styles provided by css or the browser's default - * css properties.

      - *

      Getting computed values individually is expensive! This plugin lets you get all - * the style properties you need all at once.

      - *

      Demo

      - *

      The following demo illustrates the performance improvement curStyle provides by providing - * a faster 'height' jQuery function called 'fastHeight'.

      - * @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)) -} -}) + $.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. + *

      Quick Example

      + * @codestart + * $("#foo").curStyles('float','display') //-> + * // { + * // cssFloat: "left", display: "block" + * // } + * @codeend + *

      Use

      + *

      An element's computed style is the current calculated style of the property. + * This is different than the values on element.style as + * element.style doesn't reflect styles provided by css or the browser's default + * css properties.

      + *

      Getting computed values individually is expensive! This plugin lets you get all + * the style properties you need all at once.

      + *

      Demo

      + *

      The following demo illustrates the performance improvement curStyle provides by providing + * a faster 'height' jQuery function called 'fastHeight'.

      + * @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)); + }; +}); \ No newline at end of file diff --git a/dom/cur_styles/test/qunit/cur_styles_test.js b/dom/cur_styles/cur_styles_test.js similarity index 72% rename from dom/cur_styles/test/qunit/cur_styles_test.js rename to dom/cur_styles/cur_styles_test.js index ce5ac756..d746815e 100644 --- a/dom/cur_styles/test/qunit/cur_styles_test.js +++ b/dom/cur_styles/cur_styles_test.js @@ -1,8 +1,12 @@ +steal("jquery/dom/dimensions",'jquery/view/micro') //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/dom/curStyles"); test("reading", function(){ - $("#qunit-test-area").html("//jquery/dom/cur_styles/test/qunit/curStyles.micro",{}) + + $("#qunit-test-area").html("//jquery/dom/cur_styles/test/curStyles.micro",{}) var res = $.curStyles( $("#styled")[0], ["padding-left", @@ -18,5 +22,7 @@ test("reading", function(){ equals(res.paddingLeft, "5px","padding left"); equals(res.position, "relative","position"); $("#qunit-test-area").html("") +}); + }) diff --git a/dom/cur_styles/qunit.html b/dom/cur_styles/qunit.html index 81341797..736ae9d6 100644 --- a/dom/cur_styles/qunit.html +++ b/dom/cur_styles/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/cur_styles/test/qunit/curStyles.micro b/dom/cur_styles/test/curStyles.micro similarity index 100% rename from dom/cur_styles/test/qunit/curStyles.micro rename to dom/cur_styles/test/curStyles.micro diff --git a/dom/cur_styles/test/qunit/qunit.js b/dom/cur_styles/test/qunit/qunit.js deleted file mode 100644 index e51bcaaf..00000000 --- a/dom/cur_styles/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/dimensions",'jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("cur_styles_test") \ No newline at end of file diff --git a/dom/dimensions/dimensions.html b/dom/dimensions/dimensions.html index 6e2e3150..9c3f09df 100644 --- a/dom/dimensions/dimensions.html +++ b/dom/dimensions/dimensions.html @@ -69,10 +69,8 @@ - \ No newline at end of file diff --git a/dom/dimensions/dimensions.js b/dom/dimensions/dimensions.js index 2d4b0f17..2296b91d 100644 --- a/dom/dimensions/dimensions.js +++ b/dom/dimensions/dimensions.js @@ -1,26 +1,34 @@ -steal.plugins('jquery/dom/cur_styles').then(function($) { +steal('jquery/dom/cur_styles').then(function($) { /** * @page dimensions dimensions * @parent dom - *

      jquery/dom/dimensions Plugin

      + * @plugin jquery/dom/dimensions + * * The dimensions plugin adds support for setting+animating inner+outer height and widths. - *

      Quick Examples

      -@codestart -$('#foo').outerWidth(100).innerHeight(50); -$('#bar').animate({outerWidth: 500}); -@codeend - *

      Use

      - *

      When writing reusable plugins, you often want to + * + * ### 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 + * * @demo jquery/dom/dimensions/dimensions.html */ @@ -39,22 +47,27 @@ var weird = /button|select/i, //margin is inside border */ $.each({ -/* +/** * @function outerWidth * @parent dimensions - * Lets you set the outer height on an object + * Lets you set the outer width on an object * @param {Number} [height] - * @param {Boolean} [includeMargin] + * @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:
      @@ -74,7 +87,7 @@ width: * Otherwise, returns outerHeight in pixels. */ height: -/* +/** * @function innerHeight * @parent dimensions * Lets you set the outer width on an object @@ -104,19 +117,21 @@ height: //getter / setter $.fn["outer" + Upper] = function(v, margin) { - if (typeof v == 'number') { - this[lower](v - getBoxes[lower](this[0], {padding: true, border: true, margin: 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 checks["oldOuter" + Upper].call(this, v) + return first ? checks["oldOuter" + Upper].call(this, v) : null; } } $.fn["inner" + Upper] = function(v) { - if (typeof v == 'number') { - this[lower](v - getBoxes[lower](this[0], { padding: true })) + var first = this[0]; + if (typeof v == 'number') { + first&& this[lower](v - getBoxes[lower](first, { padding: true })) return this; } else { - return checks["oldInner" + Upper].call(this, v) + return first ? checks["oldInner" + Upper].call(this, v) : null; } } //provides animations diff --git a/dom/dimensions/dimensions_test.js b/dom/dimensions/dimensions_test.js new file mode 100644 index 00000000..3f7ad31b --- /dev/null +++ b/dom/dimensions/dimensions_test.js @@ -0,0 +1,14 @@ +steal("jquery/dom/dimensions", + 'jquery/view/micro', + 'funcunit/qunit').then(function(){ + +module("jquery/dom/dimensions"); + + + + +test("outerHeight and width",function(){ + $("#qunit-test-area").html("//jquery/dom/dimensions/test/curStyles.micro",{}) +}) + +}); \ No newline at end of file diff --git a/dom/dimensions/qunit.html b/dom/dimensions/qunit.html index 3636028f..2b0e381c 100644 --- a/dom/dimensions/qunit.html +++ b/dom/dimensions/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/dimensions/test/qunit/curStyles.micro b/dom/dimensions/test/curStyles.micro similarity index 100% rename from dom/dimensions/test/qunit/curStyles.micro rename to dom/dimensions/test/curStyles.micro diff --git a/dom/cur_styles/test/qunit/outer.micro b/dom/dimensions/test/outer.micro similarity index 100% rename from dom/cur_styles/test/qunit/outer.micro rename to dom/dimensions/test/outer.micro diff --git a/dom/dimensions/test/qunit/dimensions_test.js b/dom/dimensions/test/qunit/dimensions_test.js deleted file mode 100644 index 9d34e7e8..00000000 --- a/dom/dimensions/test/qunit/dimensions_test.js +++ /dev/null @@ -1,8 +0,0 @@ -module("jquery/dom/dimensions"); - - - - -test("outerHeight and width",function(){ - $("#qunit-test-area").html("//jquery/dom/dimensions/test/qunit/curStyles.micro",{}) -}) diff --git a/dom/dimensions/test/qunit/qunit.js b/dom/dimensions/test/qunit/qunit.js deleted file mode 100644 index d842c596..00000000 --- a/dom/dimensions/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/dimensions",'jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("dimensions_test") \ No newline at end of file diff --git a/dom/dom.js b/dom/dom.js index f964832f..a47d8438 100644 --- a/dom/dom.js +++ b/dom/dom.js @@ -1,7 +1,82 @@ /** - * @page dom DOM Helpers - * @tag core - * JavaScriptMVC adds a bunch of useful jQuery extensions for the dom. Check them out on the left. - * - */ -steal.plugins('jquery'); \ No newline at end of file +@page dom DOM Helpers +@parent jquerymx +@description jQuery DOM extension. + +JavaScriptMVC adds a bunch of useful +jQuery extensions for the dom. Check them out on the left. + +## [dimensions Dimensions] + +Set and animate the inner and outer height and width of elements. + + $('#foo').outerWidth(100); + $('#bar').animate({innerWidth: 500}); + +This is great when you want to include padding and margin in +setting the dimensions of elements. + +## [jQuery.cookie Cookie] + +Set and get cookie values: + + $.cookie('cookie','value'); + +## [jQuery.fixture Fixture] + +Simulate Ajax responses. + + $.fixture("/services/tasks.php','fixtures/tasks.json'); + +Works with jQuery's Ajax converters! + +## [jQuery.fn.compare Compare] + +Compare the location of two elements rapidly. + + $('#foo').compare($('#bar')) & 2 // true if #bar is before #foo + +## [jQuery.fn.curStyles CurStyles] + +Get multiple css properties quickly. + + $('#foo').curStyles('left','top') //-> {left:'20px',top:'10px'} + +## [jQuery.fn.formParams FormParams] + +Serializes a form into a JSON-like object: + + $('form').formParams() //-> {name: 'Justin', favs: ['JS','Ruby']} + +## [jQuery.fn.selection Selection] + +Gets or sets the current text selection. + + // gets selection info + $('pre').selection() //-> {start: 22, end: 57, range: range} + + // sets the selection + $('div').selection(20,22) + +## [jQuery.fn.within Within] + +Returns elements that have a point within their boundaries. + + $('.drop').within(200,200) //-> drops that touch 200,200 + +## [jQuery.Range Range] + +Text range utilities. + + $('#copy').range() //-> text range that has copy selected + +## [jQuery.route] + +Hash routes mapped to an [jQuery.Observe $.Observe]. + + $.route(':type',{type: 'videos'}) + $.route.delegate('type','set', function(){ ... }) + $.route.attr('type','images'); + +*/ +steal('jquery'); \ No newline at end of file diff --git a/dom/fixture/fixture.html b/dom/fixture/fixture.html index 75a7d6de..d59b1f30 100644 --- a/dom/fixture/fixture.html +++ b/dom/fixture/fixture.html @@ -15,11 +15,13 @@
      - - diff --git a/dom/fixture/fixture.js b/dom/fixture/fixture.js index 6312d856..672b5a7b 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -1,267 +1,547 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom', + 'jquery/lang/object', + 'jquery/lang/string',function( $ ) { + + //used to check urls + - var ajax = $.ajax; + + // 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:"){ + steal.dev.log("ajax request to " + settings.url+", no fixture found"); + } + 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) ) { + var sub = settings.fixture.substr(2) + ''; + url = typeof steal === "undefined" ? + url = "/" + sub : + steal.root.mapJoin(sub) +''; + } + //!steal-remove-start + steal.dev.log("looking for fixture in " + url); + //!steal-remove-end + 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 { + //!steal-remove-start + steal.dev.log("using a dynamic fixture for " +settings.type+" "+ settings.url); + //!steal-remove-end + + //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; + }; /** - * @class jQuery.fixture + * @function jQuery.fixture * @plugin jquery/dom/fixture - * @download jquery/dist/jquery.fixture.js + * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/fixture/fixture.js * @test jquery/dom/fixture/qunit.html * @parent dom * - * Fixtures simulate AJAX responses by overwriting - * [jQuery.ajax $.ajax], - * [jQuery.get $.get], and - * [jQuery.post $.post]. - * Instead of making a request to a server, fixtures simulate - * the repsonse with a file or function. - * - * They are a great technique when you want to develop JavaScript + * $.fixture 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. * - *

      Quick Example

      - *

      Instead of making a request to /tasks.json, - * $.ajax will look in fixtures/tasks.json. - * It's expected that a static fixtures/tasks.json - * file exists relative to the current page. - *

      - * @codestart - * $.ajax({url: "/tasks.json", - * dataType: "json", - * type: "get", - * fixture: "fixtures/tasks.json", - * success: myCallback}); - * @codeend - *

      Using Fixtures

      - * To enable fixtures, you must add this plugin to your page and - * set the fixture property. + * ## Types of Fixtures * - * The fixture property is set as ... - * @codestart - * //... a property with $.ajax - * $.ajax({fixture: FIXTURE_VALUE}) + * There are two common ways of using fixtures. The first is to + * map Ajax requests to another file. The following + * intercepts requests to /tasks.json and directs them + * to fixtures/tasks.json: + * + * $.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 + * /tasks/ID.json 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. * - * //... a parameter in $.get and $.post - * $.get ( url, data, callback, type, FIXTURE_VALUE ) - * $.post( url, data, callback, type, FIXTURE_VALUE ) - * @codeend - *

      Turning Off Fixtures

      - *

      To turn off fixtures, simply remove the fixture plugin from - * your page. The Ajax methods will ignore FIXTURE_VALUE - * and revert to their normal behavior. If you want to ignore a single - * fixture, we suggest commenting it out. - *

      - *
      - * PRO TIP: Don't worry about leaving the fixture values in your source. - * They don't take up many characters and won't impact how jQuery makes - * requests. They can be useful even after the service they simulate - * is created. - *
      - *

      Types of Fixtures

      - *

      There are 2 types of fixtures

      - *
        - *
      • 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 fixture locations can be calculated: - * @codestart - * // looks in test/fixtures/tasks/1.get - * $.ajax({type:"get", - * url: "tasks/1", - * fixture: true}) - * @codeend - * Or provided: - * @codestart - * // looks in fixtures/tasks1.json relative to page - * $.ajax({type:"get", - * url: "tasks/1", - * fixture: "fixtures/task1.json"}) - * - * // looks in fixtures/tasks1.json relative to jmvc root - * // this assumes you are using steal - * $.ajax({type:"get", - * url: "tasks/1", - * fixture: "//fixtures/task1.json"})` - * @codeend - *
      - * PRO TIP: Use provided fixtures. It's easier to understand what it is going. - * Also, create a fixtures folder in your app to hold your fixtures. - *
      - *

      Dynamic Fixtures

      - *

      Dynamic Fixtures are functions that return the arguments the $.ajax callbacks - * (beforeSend, success, complete, - * error) expect.

      - *

      For example, the "success" of a json request is called with - * [data, textStatus, XMLHttpRequest].

      - *

      There are 2 ways to lookup dynamic fixtures.

      - * They can provided: - * @codestart - * //just use a function as the fixture property - * $.ajax({ - * type: "get", - * url: "tasks", - * data: {id: 5}, - * dataType: "json", - * fixture: function( settings, callbackType ) { - * var xhr = {responseText: "{id:"+settings.data.id+"}"} - * switch(callbackType){ - * case "success": - * return [{id: settings.data.id},"success",xhr] - * case "complete": - * return [xhr,"success"] + * + * ## 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 ] * } - * } - * }) - * @codeend - * Or found by name on $.fixture: - * @codestart - * // add your function on $.fixture - * // We use -FUNC by convention - * $.fixture["-myGet"] = function(settings, cbType){...} - * - * // reference it - * $.ajax({ - * type:"get", - * url: "tasks/1", - * dataType: "json", - * fixture: "-myGet"}) - * @codeend - *

      Dynamic fixture functions are called with:

      - *
        - *
      • settings - the settings data passed to $.ajax() - *
      • calbackType - the type of callback about to be called: - * "beforeSend", "success", "complete", - * "error"
      • - *
      - * and should return an array of arguments for the callback.

      - *
      PRO TIP: + * + * 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 completeCallback 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: {id}) is added to the original + * data object. + * + * ## Simulating Errors + * + * The following simulates an unauthorized request + * to /foo. + * + * $.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 null 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. - *
      - *

      Helpers

      - *

      The fixture plugin comes with a few ready-made dynamic fixtures and - * fixture helpers:

      - *
        - *
      • [jQuery.fixture.make] - creates fixtures for findAll, findOne.
      • - *
      • [jQuery.fixture.-restCreate] - a fixture for restful creates.
      • - *
      • [jQuery.fixture.-restDestroy] - a fixture for restful updates.
      • - *
      • [jQuery.fixture.-restUpdate] - a fixture for restful destroys.
      • - *
      + * * @demo jquery/dom/fixture/fixture.html - * @constructor - * Takes an ajax settings and returns a url to look for a fixture. Overwrite this if you want a custom lookup method. - * @param {Object} settings - * @return {String} the url that will be used for the fixture + * + * @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 {NAME} 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. */ - $.fixture = function( settings ) { - var url = settings.url, - match, left, right; - url = url.replace(/%2F/g, "~").replace(/%20/g, "_"); - - if ( settings.data && settings.processData && typeof settings.data !== "string" ) { - settings.data = jQuery.param(settings.data); - } - - - if ( settings.data && settings.type.toLowerCase() == "get" ) { - url += ($.String.include(url, '?') ? '&' : '?') + settings.data; - } - - match = url.match(/^(?:https?:\/\/[^\/]*)?\/?([^\?]*)\??(.*)?/); - left = match[1]; - - right = settings.type ? '.' + settings.type.toLowerCase() : '.post'; - if ( match[2] ) { - left += '/'; - right = match[2].replace(/\#|&/g, '-').replace(/\//g, '~') + right; + 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) } - return left + right; }; - + 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, cbType ) { - switch ( cbType ) { - case "success": - return [$.extend({ - id: parseInt(settings.url, 10) - }, settings.data), "success", $.fixture.xhr()]; - case "complete": - return [$.fixture.xhr(), "success"]; - } + "-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 ) { - switch ( cbType ) { - case "success": - return [true, "success", $.fixture.xhr()]; - case "complete": - return [$.fixture.xhr(), "success"]; - } + return {}; }, + /** + * @hide * Provides a rest create fixture function */ - "-restCreate": function( settings, cbType ) { - switch ( cbType ) { - case "success": - return [{ - id: parseInt(Math.random() * 1000, 10) - }, "success", $.fixture.xhr()]; - case "complete": - return [$.fixture.xhr({ - getResponseHeader: function() { - return settings.url + "/" + parseInt(Math.random() * 1000, 10); - } - }), "success"]; - } - - + "-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. - * @codestart - * //makes a threaded 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 ) { ... } - * }); - * @codeend - * @param {Array} types An array of the fixture names + * + * //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. + * @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 ) { + make: function( types, count, make, filter ) { + if(typeof types === "string"){ + types = [types+"s",types ] + } // make all items - var items = ($.fixture["~" + types[0]] = []); + 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); @@ -273,20 +553,31 @@ steal.plugins('jquery/dom').then(function( $ ) { } //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" ) { - return a[split[0]] < b[split[0]]; + if( a[split[0]] < b[split[0]] ) { + return 1; + } else if(a[split[0]] == b[split[0]]){ + return 0 + } else { + return -1; + } } else { - return a[split[0]] > b[split[0]]; + if( a[split[0]] < b[split[0]] ) { + return -1; + } else if(a[split[0]] == b[split[0]]){ + return 0 + } else { + return 1; + } } }); }); @@ -301,12 +592,14 @@ steal.plugins('jquery/dom').then(function( $ ) { var offset = parseInt(settings.data.offset, 10) || 0, - limit = parseInt(settings.data.limit, 10) || (count - offset), + 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 ) { - if ( param.indexOf("Id") != -1 || param.indexOf("_id") != -1 ) { + 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); @@ -316,6 +609,18 @@ steal.plugins('jquery/dom').then(function( $ ) { } } } + + + 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 [{ @@ -325,19 +630,128 @@ steal.plugins('jquery/dom').then(function( $ ) { "data": retArr.slice(offset, offset + limit) }]; }; - + // findOne $.fixture["-" + types[1]] = function( settings ) { - for ( var i = 0; i < (count); i++ ) { - if ( settings.data.id == items[i].id ) { - return [items[i]]; + 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

      + * + * ## Example + * * The following example shows how the -restCreate fixture uses xhr to return * a simulated xhr object: * @codestart @@ -381,15 +795,26 @@ steal.plugins('jquery/dom').then(function( $ ) { 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 delay + * @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.plugins('jquery/dom/fixtures').then(function(){ + * steal('jquery/dom/fixtures').then(function(){ * $.fixture.delay = 1000; * }) * @codeend @@ -414,105 +839,68 @@ steal.plugins('jquery/dom').then(function( $ ) { return false; }; - /** - * @add jQuery - */ - // break - $. - /** - * Adds the fixture option to settings. If present, loads from fixture location instead - * of provided url. This is useful for simulating ajax responses before the server is done. - * @param {Object} settings - */ - ajax = function( settings ) { - var func = $.fixture; - if (!settings.fixture ) { - return ajax.apply($, arguments); - } - if ( $.fixture["-handleFunction"](settings) ) { - return; - } - if ( typeof settings.fixture == "string" ) { - var url = settings.fixture; - if (/^\/\//.test(url) ) { - url = steal.root.join(settings.fixture.substr(2)); - } - //@steal-remove-start - steal.dev.log("looking for fixture in " + url); - //@steal-remove-end - settings.url = url; - settings.data = null; - settings.type = "GET"; - if (!settings.error ) { - settings.error = function( xhr, error, message ) { - throw "fixtures.js Error " + error + " " + message; - }; - } - return ajax(settings); - - } - settings = jQuery.extend(true, settings, jQuery.extend(true, {}, jQuery.ajaxSettings, settings)); - - settings.url = steal.root.join('test/fixtures/' + func(settings)); // convert settings - settings.data = null; - settings.type = 'GET'; - return ajax(settings); - }; - - $.extend($.ajax, ajax); - - $. - /** - * Adds a fixture param. - * @param {Object} url - * @param {Object} data - * @param {Object} callback - * @param {Object} type - * @param {Object} fixture - */ - get = function( url, data, callback, type, fixture ) { - // shift arguments if data argument was ommited - if ( jQuery.isFunction(data) ) { - fixture = type; - type = callback; - callback = data; - data = null; - } - - return jQuery.ajax({ - type: "GET", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; - - $. - /** - * Adds a fixture param. - * @param {Object} url - * @param {Object} data - * @param {Object} callback - * @param {Object} type - * @param {Object} fixture + + + /** + * @page jquery.fixture.0organizing Organizing Fixtures + * @parent jQuery.fixture + * + * The __best__ way of organizing fixtures is to have a 'fixtures.js' file that steals + * jquery/dom/fixture and defines all your fixtures. For example, + * if you have a 'todo' application, you might + * have todo/fixtures/fixtures.js 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 todo/fixtures/fixtures.js in the + * app file (todo/todo.js) 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) + * } + * */ - post = function( url, data, callback, type, fixture ) { - if ( jQuery.isFunction(data) ) { - fixture = type; - type = callback; - callback = data; - data = {}; - } - - return jQuery.ajax({ - type: "POST", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; -}); \ No newline at end of file + //Expose this for fixture debugging + $.fixture.overwrites = overwrites; +}); diff --git a/dom/fixture/fixture_test.js b/dom/fixture/fixture_test.js new file mode 100644 index 00000000..7559d1ba --- /dev/null +++ b/dom/fixture/fixture_test.js @@ -0,0 +1,332 @@ + +steal("jquery/dom/fixture", "jquery/model",'funcunit/qunit',function(){ + +module("jquery/dom/fixture"); + + +test("static fixtures", function(){ + stop(); + + $.fixture("GET something", "//jquery/dom/fixture/fixtures/test.json"); + $.fixture("POST something", "//jquery/dom/fixture/fixtures/test.json"); + + + $.get("something",function(data){ + equals(data.sweet,"ness","$.get works"); + + $.post("something",function(data){ + equals(data.sweet,"ness","$.post works"); + + + start(); + },'json'); + + },'json'); +}) + +test("dynamic fixtures",function(){ + stop(); + $.fixture.delay = 10; + $.fixture("something", function(){ + return [{sweet: "ness"}] + }) + + $.get("something",function(data){ + equals(data.sweet,"ness","$.get works"); + start(); + + },'json'); +}); + +test("fixture function", 3, function(){ + + stop(); + var url = steal.root.join("jquery/dom/fixture/fixtures/foo.json")+''; + $.fixture(url,"//jquery/dom/fixture/fixtures/foobar.json" ); + + $.get(url,function(data){ + equals(data.sweet,"ner","url passed works"); + + $.fixture(url,"//jquery/dom/fixture/fixtures/test.json" ); + + $.get(url,function(data){ + + equals(data.sweet,"ness","replaced"); + + $.fixture(url, null ); + + $.get(url,function(data){ + + equals(data.a,"b","removed"); + + start(); + + },'json') + + + },'json') + + + + },"json"); + +}); + + +test("fixtures with converters", function(){ + + stop(); + $.ajax( { + url : steal.root.join("jquery/dom/fixture/fixtures/foobar.json")+'', + dataType: "json fooBar", + converters: { + "json fooBar": function( data ) { + // Extract relevant text from the xml document + return "Mr. "+data.name; + } + }, + fixture : function(){ + return { + name : "Justin" + } + }, + success : function(prettyName){ + start(); + equals(prettyName, "Mr. Justin") + } + }); +}) + +test("$.fixture.make fixtures",function(){ + stop(); + $.fixture.make('thing', 1000, function(i){ + return { + id: i, + name: "thing "+i + } + }, + function(item, settings){ + if(settings.data.searchText){ + var regex = new RegExp("^"+settings.data.searchText) + return regex.test(item.name); + } + }) + $.ajax({ + url: "things", + type: "json", + data: { + offset: 100, + limit: 200, + order: ["name ASC"], + searchText: "thing 2" + }, + fixture: "-things", + success: function(things){ + equals(things.data[0].name, "thing 29", "first item is correct") + equals(things.data.length, 11, "there are 11 items") + start(); + } + }) +}); + +test("simulating an error", function(){ + var st = '{type: "unauthorized"}'; + + $.fixture("/foo", function(){ + return [401,st] + }); + stop(); + + $.ajax({ + url : "/foo", + success : function(){ + ok(false, "success called"); + start(); + }, + error : function(jqXHR, status, statusText){ + ok(true, "error called"); + equals(statusText, st); + start(); + } + }) +}) + +test("rand", function(){ + var rand = $.fixture.rand; + var num = rand(5); + equals(typeof num, "number"); + ok(num >= 0 && num < 5, "gets a number" ); + + stop(); + var zero, three, between, next = function(){ + start() + } + // make sure rand can be everything we need + setTimeout(function(){ + var res = rand([1,2,3]); + if(res.length == 0 ){ + zero = true; + } else if(res.length == 3){ + three = true; + } else { + between = true; + } + if(zero && three && between){ + ok(true, "got zero, three, between") + next(); + } else { + setTimeout(arguments.callee, 10) + } + }, 10) + +}); + + +test("_getData", function(){ + var data = $.fixture._getData("/thingers/{id}", "/thingers/5"); + equals(data.id, 5, "gets data"); + var data = $.fixture._getData("/thingers/5?hi.there", "/thingers/5?hi.there"); + deepEqual(data, {}, "gets data"); +}) + +test("_getData with double character value", function(){ + var data = $.fixture._getData("/days/{id}/time_slots.json", "/days/17/time_slots.json"); + equals(data.id, 17, "gets data"); +}); + +test("_compare", function(){ + var same = $.Object.same( + {url : "/thingers/5"}, + {url : "/thingers/{id}"}, $.fixture._compare) + + ok(same, "they are similar"); + + same = $.Object.same( + {url : "/thingers/5"}, + {url : "/thingers"}, $.fixture._compare); + + ok(!same, "they are not the same"); +}) + +test("_similar", function(){ + + var same = $.fixture._similar( + {url : "/thingers/5"}, + {url : "/thingers/{id}"}); + + ok(same, "similar"); + + same = $.fixture._similar( + {url : "/thingers/5", type: "get"}, + {url : "/thingers/{id}"}); + + ok(same, "similar with extra pops on settings"); + + var exact = $.fixture._similar( + {url : "/thingers/5", type: "get"}, + {url : "/thingers/{id}"}, true); + + ok(!exact, "not exact" ) + + var exact = $.fixture._similar( + {url : "/thingers/5"}, + {url : "/thingers/5"}, true); + + ok(exact, "exact" ) +}) + +test("fixture function gets id", function(){ + $.fixture("/thingers/{id}", function(settings){ + return { + id: settings.data.id, + name: "justin" + } + }) + stop(); + $.get("/thingers/5", {}, function(data){ + start(); + ok(data.id) + },'json') +}); + +test("replacing and removing a fixture", function(){ + var url = steal.root.join("jquery/dom/fixture/fixtures/remove.json")+'' + $.fixture("GET "+url, function(){ + return {weird: "ness!"} + }) + stop(); + $.get(url,{}, function(json){ + equals(json.weird,"ness!","fixture set right") + + $.fixture("GET "+url, function(){ + return {weird: "ness?"} + }) + + $.get(url,{}, function(json){ + equals(json.weird,"ness?","fixture set right"); + + $.fixture("GET "+url, null ) + + $.get(url,{}, function(json){ + equals(json.weird,"ness","fixture set right"); + + start(); + },'json'); + + + },'json') + + + + },'json') +}); + +return; // future fixture stuff + +// returning undefined means you want to control timing? +$.fixture('GET /foo', function(orig, settings, headers, cb){ + setTimeout(function(){ + cb(200, "success",{json : "{}"},{}) + },1000); +}) + +// fixture that hooks into model / vice versa? + +// fixture that creates a nice store + +var store = $.fixture.store(1000, function(){ + +}) + +store.find() + +// make cloud + +var clouds = $.fixture.store(1, function(){ + return { + name: "ESCCloud", + DN : "ESCCloud-ESCCloud", + type : "ESCCloud" + } +}); + +var computeCluster = $.fixture.store(5, function(i){ + return { + name : "", + parentDN : clouds.find()[0].DN, + type: "ComputeCluster", + DN : "ComputeCluster-ComputeCluster"+i + } +}); + +$.fixture("GET /computeclusters", function(){ + return [] +}); + +// hacking models? + + + + + +}); diff --git a/dom/fixture/fixtures/foo.json b/dom/fixture/fixtures/foo.json new file mode 100644 index 00000000..9fbc82d0 --- /dev/null +++ b/dom/fixture/fixtures/foo.json @@ -0,0 +1 @@ +{"a" : "b"} diff --git a/dom/fixture/fixtures/foobar.json b/dom/fixture/fixtures/foobar.json new file mode 100644 index 00000000..a50afea1 --- /dev/null +++ b/dom/fixture/fixtures/foobar.json @@ -0,0 +1,3 @@ +{ + "sweet" :"ner" +} diff --git a/dom/fixture/fixtures/remove.json b/dom/fixture/fixtures/remove.json new file mode 100644 index 00000000..1e152b58 --- /dev/null +++ b/dom/fixture/fixtures/remove.json @@ -0,0 +1,3 @@ +{ + "weird" : "ness" +} diff --git a/dom/fixture/qunit.html b/dom/fixture/qunit.html index 9a6ba526..5207a04c 100644 --- a/dom/fixture/qunit.html +++ b/dom/fixture/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/fixture/test/qunit/fixture_test.js b/dom/fixture/test/qunit/fixture_test.js deleted file mode 100644 index a6499ae1..00000000 --- a/dom/fixture/test/qunit/fixture_test.js +++ /dev/null @@ -1,50 +0,0 @@ -module("jquery/dom/fixture"); - - -test("static fixtures", function(){ - stop(); - $.get("something",function(data){ - equals(data.sweet,"ness","$.get works"); - $.post("something",function(data){ - - equals(data.sweet,"ness","$.post works"); - - $.ajax({ - url: "something", - dataType: "json", - success: function( data ) { - equals(data.sweet,"ness","$.ajax works"); - start(); - }, - fixture: "//jquery/dom/fixture/fixtures/test.json" - }) - - },"json","//jquery/dom/fixture/fixtures/test.json"); - },'json',"//jquery/dom/fixture/fixtures/test.json"); -}) - -test("dynamic fixtures",function(){ - stop(); - $.fixture.delay = 10; - var fix = function(){ - return [{sweet: "ness"}] - } - $.get("something",function(data){ - equals(data.sweet,"ness","$.get works"); - $.post("something",function(data){ - - equals(data.sweet,"ness","$.post works"); - - $.ajax({ - url: "something", - dataType: "json", - success: function( data ) { - equals(data.sweet,"ness","$.ajax works"); - start(); - }, - fixture: fix - }) - - },"json",fix); - },'json',fix); -}) diff --git a/dom/fixture/test/qunit/qunit.js b/dom/fixture/test/qunit/qunit.js deleted file mode 100644 index 9750a4ae..00000000 --- a/dom/fixture/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/fixture") //load your app - .plugins('funcunit/qunit') //load qunit - .then("fixture_test") \ No newline at end of file diff --git a/dom/form_params/form_params.html b/dom/form_params/form_params.html index 969307be..650dd9ec 100644 --- a/dom/form_params/form_params.html +++ b/dom/form_params/form_params.html @@ -30,10 +30,8 @@ - \ No newline at end of file diff --git a/dom/form_params/form_params.js b/dom/form_params/form_params.js index 968a0807..0234c4d4 100644 --- a/dom/form_params/form_params.js +++ b/dom/form_params/form_params.js @@ -1,31 +1,72 @@ /** * @add jQuery.fn */ -steal.plugins("jquery/dom").then(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; - } +steal("jquery/dom").then(function( $ ) { + var keyBreaker = /[^\[\]]+/g, + convertValue = function( value ) { + if ( $.isNumeric( value )) { + return parseFloat( value ); + } else if ( value === 'true') { + return true; + } else if ( value === 'false' ) { + return false; + } else if ( value === '' ) { + return undefined; + } + return value; + }, + nestData = function( elem, type, data, parts, value, seen ) { + var name = parts.shift(); - if ( typeof value != 'string' ) { - return false; - } + if ( parts.length ) { + if ( ! data[ name ] ) { + data[ name ] = {}; + } + // Recursive call + nestData( elem, type, data[ name ], parts, value, seen ); + } else { + + // Handle same name case, as well as "last checkbox checked" + // case + if ( name in seen && type != "radio" && ! $.isArray( data[ name ] )) { + if ( name in data ) { + data[ name ] = [ data[name] ]; + } else { + data[ name ] = []; + } + } else { + seen[ name ] = true; + } - return value.match(numberMatcher); - }; + // Finally, assign data + if ( ( type == "radio" || type == "checkbox" ) && ! elem.is(":checked") ) { + return + } + + if ( ! data[ name ] ) { + data[ name ] = value; + } else { + data[ name ].push( value ); + } + + } + + }; + $.fn.extend({ /** * @parent dom - * @download jquery/dist/jquery.formparams.js + * @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.

      + * + * 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 * <form> @@ -34,78 +75,105 @@ steal.plugins("jquery/dom").then(function( $ ) { * <form/> * @codeend * Example code: - * @codestart - * $('form').formParams() //-> { foo:{bar:2, ced: 4} } - * @codeend + * + * $('form').formParams() //-> { foo:{bar:'2', ced: '4'} } + * * * @demo jquery/dom/form_params/form_params.html * - * @param {Boolean} [convert] True if strings that look like numbers and booleans should be converted. Defaults to true. + * @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( convert ) { - if ( this[0].nodeName.toLowerCase() == 'form' && this[0].elements ) { + formParams: function( params ) { + + var convert; - return jQuery(jQuery.makeArray(this[0].elements)).getParams(convert); + // Quick way to determine if something is a boolean + if ( !! params === params ) { + convert = params; + params = null; + } + + if ( params ) { + return this.setParams( params ); + } else { + return this.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 = {}, + // This is used to keep track of the checkbox names that we've + // already seen, so we know that we should return an array if + // we see it multiple times. Fixes last checkbox checked bug. + seen = {}, current; - convert = convert === undefined ? true : convert; - this.each(function() { - var el = this, - type = el.type && el.type.toLowerCase(); - //if we are submit, ignore - if ((type == 'submit') || !el.name ) { + this.find("[name]").each(function() { + var $this = $(this), + type = $this.attr("type"), + name = $this.attr("name"), + value = $this.val(), + parts; + + // Don't accumulate submit buttons and nameless elements + if ( type == "submit" || ! name ) { return; } - var key = el.name, - value = $.fn.val.call([el]) || $.data(el, "value"), - 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 === 'false' ) { - value = Boolean(value); - } - + // Figure out name parts + parts = name.match( keyBreaker ); + if ( ! parts.length ) { + parts = [name]; } - // 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]]; + // Convert the value + if ( convert ) { + value = convertValue( value ); } - lastPart = parts[parts.length - 1]; - //now we are on the last part, set the value - if ( lastPart in current && type === "checkbox" ) { - 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; - } + // Assign data recursively + nestData( $this, type, data, parts, value, seen ); }); + return data; } }); -}); \ No newline at end of file +}); diff --git a/dom/form_params/form_params_test.js b/dom/form_params/form_params_test.js new file mode 100644 index 00000000..2a9e2f3c --- /dev/null +++ b/dom/form_params/form_params_test.js @@ -0,0 +1,84 @@ +steal("jquery/dom/form_params") //load your app + .then('funcunit/qunit','jquery/view/micro') //load qunit + .then(function(){ + +$.ajaxSetup({ + cache : false +}); + +module("jquery/dom/form_params") +test("with a form", function(){ + + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}) + + + var formParams = $("#qunit-test-area form").formParams() ; + ok(formParams.params.one === "1","one is right"); + ok(formParams.params.two === "2","two is right"); + ok(formParams.params.three === "3","three is right"); + same(formParams.params.four,["4","1"],"four is right"); + same(formParams.params.five,["2","3"],"five is right"); + equal(typeof formParams.id , 'string', "Id value is empty"); + + equal( typeof formParams.singleRadio, "string", "Type of single named radio is string" ); + equal( formParams.singleRadio, "2", "Value of single named radio is right" ); + + ok( $.isArray(formParams.lastOneChecked), "Type of checkbox with last option checked is array" ); + equal( formParams.lastOneChecked, "4", "Value of checkbox with the last option checked is 4" ); + +}); + +test("With a non-form element", function() { + + $("#qunit-test-area").html("//jquery/dom/form_params/test/non-form.micro",{}) + + var formParams = $("#divform").formParams() ; + + equal( formParams.id , "foo-bar-baz", "ID input read correctly" ); + +}); + + +test("with true false", function(){ + $("#qunit-test-area").html("//jquery/dom/form_params/test/truthy.micro",{}); + + var formParams = $("#qunit-test-area form").formParams(true); + ok(formParams.foo === undefined, "foo is undefined") + ok(formParams.bar.abc === true, "form bar is true"); + ok(formParams.bar.def === true, "form def is true"); + ok(formParams.bar.ghi === undefined, "form def is undefined"); + ok(formParams.wrong === false, "'false' should become false"); +}); + +test("just strings",function(){ + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}); + var formParams = $("#qunit-test-area form").formParams(false) ; + ok(formParams.params.one === "1","one is right"); + ok(formParams.params.two === '2',"two is right"); + ok(formParams.params.three === '3',"three is right"); + same(formParams.params.four,["4","1"],"four is right"); + same(formParams.params.five,['2','3'],"five is right"); + $("#qunit-test-area").html('') +}); + +test("empty string conversion",function() { + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}); + var formParams = $("#qunit-test-area form").formParams(false) ; + ok('' === formParams.empty, 'Default empty string conversion'); + formParams = $("#qunit-test-area form").formParams(true); + ok(undefined === formParams.empty, 'Default empty string conversion'); +}); + +test("missing names",function(){ + $("#qunit-test-area").html("//jquery/dom/form_params/test/checkbox.micro",{}); + var formParams = $("#qunit-test-area form").formParams() ; + ok(true, "does not break") +}); + +test("same input names to array", function() { + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}); + var formParams = $("#qunit-test-area form").formParams(true); + same(formParams.param1, ['first', 'second', 'third']); +}); + +}); diff --git a/dom/form_params/qunit.html b/dom/form_params/qunit.html index 1223df9f..d1e2f34c 100644 --- a/dom/form_params/qunit.html +++ b/dom/form_params/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/form_params/test/qunit/basics.micro b/dom/form_params/test/basics.micro similarity index 57% rename from dom/form_params/test/qunit/basics.micro rename to dom/form_params/test/basics.micro index 61dea977..14e76957 100644 --- a/dom/form_params/test/qunit/basics.micro +++ b/dom/form_params/test/basics.micro @@ -11,8 +11,12 @@ + + + + - + + + + + + - \ No newline at end of file + + + + + diff --git a/dom/form_params/test/qunit/checkbox.micro b/dom/form_params/test/checkbox.micro similarity index 100% rename from dom/form_params/test/qunit/checkbox.micro rename to dom/form_params/test/checkbox.micro diff --git a/dom/form_params/test/non-form.micro b/dom/form_params/test/non-form.micro new file mode 100644 index 00000000..aa81d984 --- /dev/null +++ b/dom/form_params/test/non-form.micro @@ -0,0 +1,5 @@ +
      + + + +
      diff --git a/dom/form_params/test/qunit/form_params_test.js b/dom/form_params/test/qunit/form_params_test.js deleted file mode 100644 index 18da8cc6..00000000 --- a/dom/form_params/test/qunit/form_params_test.js +++ /dev/null @@ -1,43 +0,0 @@ -module("jquery/dom/form_params") -test("with a form", function(){ - - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/basics.micro",{}) - - var formParams = $("#qunit-test-area form").formParams() ; - ok(formParams.params.one === 1,"one is right"); - ok(formParams.params.two === 2,"two is right"); - ok(formParams.params.three === 3,"three is right"); - same(formParams.params.four,["4","1"],"four is right"); - same(formParams.params.five,[2,3],"five is right"); - - -}); - - -test("with true false", function(){ - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/truthy.micro",{}); - - var formParams = $("#qunit-test-area form").formParams(); - ok(formParams.foo === undefined, "foo is undefined") - ok(formParams.bar.abc === true, "form bar is true"); - ok(formParams.bar.def === true, "form def is true"); - ok(formParams.bar.ghi === undefined, "form def is undefined"); - -}); - -test("just strings",function(){ - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/basics.micro",{}); - var formParams = $("#qunit-test-area form").formParams(false) ; - ok(formParams.params.one === "1","one is right"); - ok(formParams.params.two === '2',"two is right"); - ok(formParams.params.three === '3',"three is right"); - same(formParams.params.four,["4","1"],"four is right"); - same(formParams.params.five,['2','3'],"five is right"); - $("#qunit-test-area").html('') -}) - -test("missing names",function(){ - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/checkbox.micro",{}); - var formParams = $("#qunit-test-area form").formParams() ; - ok(true, "does not break") -}) \ No newline at end of file diff --git a/dom/form_params/test/qunit/qunit.js b/dom/form_params/test/qunit/qunit.js deleted file mode 100644 index 933e2e7c..00000000 --- a/dom/form_params/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/form_params") //load your app - .plugins('funcunit/qunit','jquery/view/micro') //load qunit - .then("form_params_test") \ No newline at end of file diff --git a/dom/form_params/test/qunit/truthy.micro b/dom/form_params/test/truthy.micro similarity index 79% rename from dom/form_params/test/qunit/truthy.micro rename to dom/form_params/test/truthy.micro index f45de9a0..d12fdba5 100644 --- a/dom/form_params/test/qunit/truthy.micro +++ b/dom/form_params/test/truthy.micro @@ -1,7 +1,6 @@
      - - + diff --git a/dom/range/qunit.html b/dom/range/qunit.html new file mode 100644 index 00000000..019896d7 --- /dev/null +++ b/dom/range/qunit.html @@ -0,0 +1,17 @@ + + + + Range QUnit Test + + + +

      Range Test Suite

      +

      +
      +

      +
      +
        +
        + + + \ No newline at end of file diff --git a/dom/range/range.html b/dom/range/range.html new file mode 100644 index 00000000..b5f9d139 --- /dev/null +++ b/dom/range/range.html @@ -0,0 +1,66 @@ + + + + Range + + + +
        +

        The Range Plugin

        +

        Select a range of text on the page and get useful info about it!

        +

        We'll figure out what to do with form elements later.

        +
        
        +		
        + + + + \ No newline at end of file diff --git a/dom/range/range.js b/dom/range/range.js new file mode 100644 index 00000000..7f3db253 --- /dev/null +++ b/dom/range/range.js @@ -0,0 +1,676 @@ +steal('jquery','jquery/dom/compare').then(function($){ +// TODOS ... +// Ad + +/** + * @function jQuery.fn.range + * @parent $.Range + * + * Returns a jQuery.Range for the element selected. + * + * $('#content').range() + */ +$.fn.range = function(){ + return $.Range(this[0]) +} + +var convertType = function(type){ + return type.replace(/([a-z])([a-z]+)/gi, function(all,first, next){ + return first+next.toLowerCase() + }).replace(/_/g,""); +}, +reverse = function(type){ + return type.replace(/^([a-z]+)_TO_([a-z]+)/i, function(all, first, last){ + return last+"_TO_"+first; + }); +}, +getWindow = function( element ) { + return element ? element.ownerDocument.defaultView || element.ownerDocument.parentWindow : window +}, +bisect = function(el, start, end){ + //split the start and end ... figure out who is touching ... + if(end-start == 1){ + return + } +}, +support = {}; +/** + * @Class jQuery.Range + * @parent dom + * @tag alpha + * + * Provides text range helpers for creating, moving, + * and comparing ranges cross browser. + * + * ## Examples + * + * // Get the current range + * var range = $.Range.current() + * + * // move the end of the range 2 characters right + * range.end("+2") + * + * // get the startOffset of the range and the container + * range.start() //-> { offset: 2, container: HTMLELement } + * + * //get the most common ancestor element + * var parent = range.parent() + * + * //select the parent + * var range2 = new $.Range(parent) + * + * @constructor + * + * Returns a jQuery range object. + * + * @param {TextRange|HTMLElement|Point} [range] An object specifiying a + * range. Depending on the object, the selected text will be different. $.Range supports the + * following types + * + * - __undefined or null__ - returns a range with nothing selected + * - __HTMLElement__ - returns a range with the node's text selected + * - __Point__ - returns a range at the point on the screen. The point can be specified like: + * + * //client coordinates + * {clientX: 200, clientY: 300} + * + * //page coordinates + * {pageX: 200, pageY: 300} + * {top: 200, left: 300} + * + * - __TextRange__ a raw text range object. + */ +$.Range = function(range){ + if(this.constructor !== $.Range){ + return new $.Range(range); + } + if(range && range.jquery){ + range = range[0]; + } + // create one + if(!range || range.nodeType){ + this.win = getWindow(range) + if(this.win.document.createRange){ + this.range = this.win.document.createRange() + }else{ + this.range = this.win.document.body.createTextRange() + } + if(range){ + this.select(range) + } + + } else if (range.clientX != null || range.pageX != null || range.left != null) { + this.moveToPoint(range) + } else if (range.originalEvent && range.originalEvent.touches && range.originalEvent.touches.length) { + this.moveToPoint(range.originalEvent.touches[0]) + } else if (range.originalEvent && range.originalEvent.changedTouches && range.originalEvent.changedTouches.length) { + this.moveToPoint(range.originalEvent.changedTouches[0]) + } else { + this.range = range; + } +}; +/** + * @static + */ +$.Range. +/** + * Gets the current range. + * + * $.Range.current() //-> jquery.range + * + * @param {HTMLElement} [el] an optional element used to get selection for a given window. + * @return {jQuery.Range} a jQuery.Range wrapped range. + */ +current = function(el){ + var win = getWindow(el), + selection; + if(win.getSelection){ + selection = win.getSelection() + return new $.Range( selection.rangeCount ? selection.getRangeAt(0) : win.document.createRange()) + }else{ + return new $.Range( win.document.selection.createRange() ); + } +}; + + + + +$.extend($.Range.prototype, +/** @prototype **/ +{ + moveToPoint : function(point){ + var clientX = point.clientX, clientY = point.clientY + if(!clientX){ + var off = scrollOffset(); + clientX = (point.pageX || point.left || 0 ) - off.left; + clientY = (point.pageY || point.top || 0 ) - off.top; + } + if(support.moveToPoint){ + this.range = $.Range().range + this.range.moveToPoint(clientX, clientY); + return this; + } + + + // it's some text node in this range ... + var parent = document.elementFromPoint(clientX, clientY); + + //typically it will be 'on' text + for(var n=0; n < parent.childNodes.length; n++){ + var node = parent.childNodes[n]; + if(node.nodeType === 3 || node.nodeType === 4){ + var range = $.Range(node), + length = range.toString().length; + + + // now lets start moving the end until the boundingRect is within our range + + for(var i = 1; i < length+1; i++){ + var rect = range.end(i).rect(); + if(rect.left <= clientX && rect.left+rect.width >= clientX && + rect.top <= clientY && rect.top+rect.height >= clientY ){ + range.start(i-1); + this.range = range.range; + return; + } + } + } + } + + // if not 'on' text, recursively go through and find out when we shift to next + // 'line' + var previous; + iterate(parent.childNodes, function(textNode){ + var range = $.Range(textNode); + if(range.rect().top > point.clientY){ + return false; + }else{ + previous = range; + } + }); + if(previous){ + previous.start(previous.toString().length); + this.range = previous.range; + }else{ + this.range = $.Range(parent).range + } + + }, + + window : function(){ + return this.win || window; + }, + /** + * Return true if any portion of these two ranges overlap. + * + * var foo = document.getElementById('foo'); + * + * $.Range(foo.childNodes[0]).compare(foo.childNodes[1]) //-> false + * + * @param {jQuery.Range} elRange + * @return {Boolean} true if part of the ranges overlap, false if otherwise. + */ + overlaps : function(elRange){ + if(elRange.nodeType){ + elRange = $.Range(elRange).select(elRange); + } + //if the start is within the element ... + var startToStart = this.compare("START_TO_START", elRange), + endToEnd = this.compare("END_TO_END", elRange) + + // if we wrap elRange + if(startToStart <=0 && endToEnd >=0){ + return true; + } + // if our start is inside of it + if( startToStart >= 0 && + this.compare("START_TO_END", elRange) <= 0 ) { + return true; + } + // if our end is inside of elRange + if(this.compare("END_TO_START", elRange) >= 0 && + endToEnd <= 0 ) { + return true; + } + return false; + }, + /** + * Collapses a range + * + * $('#foo').range().collapse() + * + * @param {Boolean} [toStart] true if to the start of the range, false if to the + * end. Defaults to false. + * @return {jQuery.Range} returns the range for chaining. + */ + collapse : function(toStart){ + this.range.collapse(toStart === undefined ? true : toStart); + return this; + }, + /** + * Returns the text of the range. + * + * currentText = $.Range.current().toString() + * + * @return {String} the text of the range + */ + toString : function(){ + return typeof this.range.text == "string" ? this.range.text : this.range.toString(); + }, + /** + * Gets or sets the start of the range. + * + * If a value is not provided, start returns the range's starting container and offset like: + * + * $('#foo').range().start() //-> {container: fooElement, offset: 0 } + * + * If a set value is provided, it can set the range. The start of the range is set differently + * depending on the type of set value: + * + * - __Object__ - an object with the new starting container and offset is provided like + * + * $.Range().start({container: $('#foo')[0], offset: 20}) + * + * - __Number__ - the new offset value. The container is kept the same. + * + * - __String__ - adjusts the offset by converting the string offset to a number and adding it to the current + * offset. For example, the following moves the offset forward four characters: + * + * $('#foo').range().start("+4") + * + * + * @param {Object|String|Number} [set] a set value if setting the start of the range or nothing if reading it. + * @return {jQuery.Range|Object} if setting the start, the range is returned for chaining, otherwise, the + * start offset and container are returned. + */ + start : function(set){ + if(set === undefined){ + if(this.range.startContainer){ + return { + container : this.range.startContainer, + offset : this.range.startOffset + } + }else{ + var start = this.clone().collapse().parent(); + var startRange = $.Range(start).select(start).collapse(); + startRange.move("END_TO_START", this); + return { + container : start, + offset : startRange.toString().length + } + } + } else { + if (this.range.setStart) { + if(typeof set == 'number'){ + this.range.setStart(this.range.startContainer, set) + } else if(typeof set == 'string') { + this.range.setStart(this.range.startContainer, this.range.startOffset+ parseInt(set,10) ); + } else { + this.range.setStart(set.container, set.offset) + } + } else { + throw 'todo' + } + return this; + } + + + }, + /** + * Sets or gets the end of the range. + * It takes similar options as [jQuery.Range.prototype.start]. + * @param {Object} [set] + */ + end : function(set){ + if (set === undefined) { + if (this.range.startContainer) { + return { + container: this.range.endContainer, + offset: this.range.endOffset + } + } + else { + var end = this.clone().collapse(false).parent(), + endRange = $.Range(end).select(end).collapse(); + endRange.move("END_TO_END", this); + return { + container: end, + offset: endRange.toString().length + } + } + } else { + if (this.range.setEnd) { + if(typeof set == 'number'){ + this.range.setEnd(this.range.endContainer, set) + } else { + this.range.setEnd(set.container, set.offset) + } + } else { + throw 'todo' + } + return this; + } + }, + /** + * Returns the most common ancestor element of + * the endpoints in the range. This will return text elements if the range is + * within a text element. + * @return {HTMLNode} the TextNode or HTMLElement + * that fully contains the range + */ + parent : function(){ + if(this.range.commonAncestorContainer){ + return this.range.commonAncestorContainer; + } else { + + var parentElement = this.range.parentElement(), + range = this.range; + + // IE's parentElement will always give an element, we want text ranges + iterate(parentElement.childNodes, function(txtNode){ + if($.Range(txtNode).range.inRange( range ) ){ + // swap out the parentElement + parentElement = txtNode; + return false; + } + }); + + return parentElement; + } + }, + /** + * Returns the bounding rectangle of this range. + * + * @param {String} [from] - where the coordinates should be + * positioned from. By default, coordinates are given from the client viewport. + * But if 'page' is given, they are provided relative to the page. + * + * @return {TextRectangle} - The client rects. + */ + rect : function(from){ + var rect = this.range.getBoundingClientRect() + if(from === 'page'){ + var off = scrollOffset(); + rect = $.extend({}, rect); + rect.top += off.top; + rect.left += off.left; + } + return rect; + }, + /** + * Returns client rects + * @param {String} [from] how the rects coordinates should be given (viewport or page). Provide 'page' for + * rect coordinates from the page. + */ + rects : function(from){ + var rects = $.makeArray( this.range.getClientRects() ).sort(function(rect1, rect2){ + return rect2.width*rect2.height - rect1.width*rect1.height; + }), + i=0,j, + len = rects.length; + //return rects; + //rects are sorted, largest to smallest + while(i < rects.length){ + var cur = rects[i], + found = false; + + j = i+1; + for(j = i+1; j < rects.length; j++){ + if( withinRect(cur, rects[j] ) ) { + found = rects[j]; + break; + } + } + if(found){ + rects.splice(i,1) + }else{ + i++; + } + + + } + // safari will be return overlapping ranges ... + if(from == 'page'){ + var off = scrollOffset(); + return $.map(rects, function(item){ + var i = $.extend({}, item) + i.top += off.top; + i.left += off.left; + return i; + }) + } + + + return rects; + } + +}); +(function(){ + //method branching .... + var fn = $.Range.prototype, + range = $.Range().range; + + /** + * @function compare + * Compares one range to another range. + * + * ## Example + * + * // compare the highlight element's start position + * // to the start of the current range + * $('#highlight') + * .range() + * .compare('START_TO_START', $.Range.current()) + * + * + * + * @param {Object} type Specifies the boundry of the + * range and the compareRange to compare. + * + * - START\_TO\_START - the start of the range and the start of compareRange + * - START\_TO\_END - the start of the range and the end of compareRange + * - END\_TO\_END - the end of the range and the end of compareRange + * - END\_TO\_START - the end of the range and the start of compareRange + * + * @param {$.Range} compareRange The other range + * to compare against. + * @return {Number} a number indicating if the range + * boundary is before, + * after, or equal to compareRange's + * boundary where: + * + * - -1 - the range boundary comes before the compareRange boundary + * - 0 - the boundaries are equal + * - 1 - the range boundary comes after the compareRange boundary + */ + fn.compare = range.compareBoundaryPoints ? + function(type, range){ + return this.range.compareBoundaryPoints(this.window().Range[reverse( type )], range.range) + }: + function(type, range){ + return this.range.compareEndPoints(convertType(type), range.range) + } + + /** + * @function move + * Move the endpoints of a range relative to another range. + * + * // Move the current selection's end to the + * // end of the #highlight element + * $.Range.current().move('END_TO_END', + * $('#highlight').range() ) + * + * + * @param {String} type a string indicating the ranges boundary point + * to move to which referenceRange boundary point where: + * + * - START\_TO\_START - the start of the range moves to the start of referenceRange + * - START\_TO\_END - the start of the range move to the end of referenceRange + * - END\_TO\_END - the end of the range moves to the end of referenceRange + * - END\_TO\_START - the end of the range moves to the start of referenceRange + * + * @param {jQuery.Range} referenceRange + * @return {jQuery.Range} the original range for chaining + */ + fn.move = range.setStart ? + function(type, range){ + + var rangesRange = range.range; + switch(type){ + case "START_TO_END" : + this.range.setStart(rangesRange.endContainer, rangesRange.endOffset) + break; + case "START_TO_START" : + this.range.setStart(rangesRange.startContainer, rangesRange.startOffset) + break; + case "END_TO_END" : + this.range.setEnd(rangesRange.endContainer, rangesRange.endOffset) + break; + case "END_TO_START" : + this.range.setEnd(rangesRange.startContainer, rangesRange.startOffset) + break; + } + + return this; + }: + function(type, range){ + this.range.setEndPoint(convertType(type), range.range) + return this; + }; + var cloneFunc = range.cloneRange ? "cloneRange" : "duplicate", + selectFunc = range.selectNodeContents ? "selectNodeContents" : "moveToElementText"; + + fn. + /** + * Clones the range and returns a new $.Range + * object. + * + * @return {jQuery.Range} returns the range as a $.Range. + */ + clone = function(){ + return $.Range( this.range[cloneFunc]() ); + }; + + fn. + /** + * @function + * Selects an element with this range. If nothing + * is provided, makes the current + * range appear as if the user has selected it. + * + * This works with text nodes. + * + * @param {HTMLElement} [el] + * @return {jQuery.Range} the range for chaining. + */ + select = range.selectNodeContents ? function(el){ + if(!el){ + this.window().getSelection().addRange(this.range); + }else { + this.range.selectNodeContents(el); + } + return this; + } : function(el){ + if(!el){ + this.range.select() + } else if(el.nodeType === 3){ + //select this node in the element ... + var parent = el.parentNode, + start = 0, + end; + iterate(parent.childNodes, function(txtNode){ + if(txtNode === el){ + end = start + txtNode.nodeValue.length; + return false; + } else { + start = start + txtNode.nodeValue.length + } + }) + this.range.moveToElementText(parent); + + this.range.moveEnd('character', end - this.range.text.length) + this.range.moveStart('character', start); + } else { + this.range.moveToElementText(el); + } + return this; + }; + +})(); + + +// helpers ----------------- + +// iterates through a list of elements, calls cb on every text node +// if cb returns false, exits the iteration +var iterate = function(elems, cb){ + var elem, start; + for (var i = 0; elems[i]; i++) { + elem = elems[i]; + // Get the text from text nodes and CDATA nodes + if (elem.nodeType === 3 || elem.nodeType === 4) { + if (cb(elem) === false) { + return false; + } + // Traverse everything else, except comment nodes + } + else + if (elem.nodeType !== 8) { + if (iterate(elem.childNodes, cb) === false) { + return false; + } + } + } + +}, +supportWhitespace, +isWhitespace = function(el){ + if(supportWhitespace == null){ + supportWhitespace = 'isElementContentWhitespace' in el; + } + return (supportWhitespace? el.isElementContentWhitespace : + (el.nodeType === 3 && '' == el.data.trim())); + +}, +// if a point is within a rectangle +within = function(rect, point){ + + return rect.left <= point.clientX && rect.left + rect.width >= point.clientX && + rect.top <= point.clientY && + rect.top + rect.height >= point.clientY +}, +// if a rectangle is within another rectangle +withinRect = function(outer, inner){ + return within(outer, { + clientX: inner.left, + clientY: inner.top + }) && //top left + within(outer, { + clientX: inner.left + inner.width, + clientY: inner.top + }) && //top right + within(outer, { + clientX: inner.left, + clientY: inner.top + inner.height + }) && //bottom left + within(outer, { + clientX: inner.left + inner.width, + clientY: inner.top + inner.height + }) //bottom right +}, +// gets the scroll offset from a window +scrollOffset = function( win){ + var win = win ||window; + doc = win.document.documentElement, body = win.document.body; + + return { + left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0), + top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0) + }; +}; + + +support.moveToPoint = !!$.Range().range.moveToPoint + + +}); \ No newline at end of file diff --git a/dom/range/range_test.js b/dom/range/range_test.js new file mode 100644 index 00000000..e55246e4 --- /dev/null +++ b/dom/range/range_test.js @@ -0,0 +1,197 @@ +steal("funcunit/qunit", "jquery/dom/range", "jquery/dom/selection").then(function(){ + +module("jquery/dom/range"); + +test("basic range", function(){ + $("#qunit-test-area") + .html("

        0123456789

        "); + $('#1').selection(1,5); + var range = $.Range.current(); + equals(range.start().offset, 1, "start is 1") + equals(range.end().offset, 5, "end is 5") +}); + + +test('jQuery helper', function(){ + + $("#qunit-test-area").html("
        thisTextIsSelected
        ") + + var range = $('#selectMe').range(); + + equals(range.toString(), "thisTextIsSelected") + +}); + +test("constructor with undefined", function(){ + var range = $.Range(); + equals(document, range.start().container, "start is right"); + equals(0, range.start().offset, "start is right"); + equals(document, range.end().container, "end is right"); + equals(0, range.end().offset, "end is right"); +}); + +test("constructor with element", function(){ + + $("#qunit-test-area").html("
        thisTextIsSelected
        ") + + var range = $.Range($('#selectMe')[0]); + + equals(range.toString(), "thisTextIsSelected") + +}); + +test('selecting text nodes and parent', function(){ + $("#qunit-test-area").html("
        thisTextIsSelected
        ") + var txt = $('#selectMe')[0].childNodes[2] + equals(txt.nodeValue,"Is","text is right") + var range = $.Range(); + range.select(txt); + equals( range.parent(), txt, "right parent node" ); +}) + +test('parent', function(){ + $("#qunit-test-area").html("
        thisTextIsSelected
        ") + var txt = $('#selectMe')[0].childNodes[0] + + var range = $.Range(txt); + + equals(range.parent(), txt) +}); + +test("constructor with point", function(){ + + var floater = $("
        thisTextIsSelected
        ").css({ + position: "absolute", + left: "0px", + top: "0px", + border: "solid 1px black" + }) + + $("#qunit-test-area").html(""); + floater.appendTo(document.body); + + + var range = $.Range({pageX: 5, pageY: 5}); + equals(range.start().container.parentNode, floater[0]) + floater.remove() +}); + +test('current', function(){ + $("#qunit-test-area").html("
        thisTextIsSelected
        "); + $('#selectMe').range().select(); + + var range = $.Range.current(); + equals(range.toString(), "thisTextIsSelected" ) +}) + +test('rangeFromPoint', function(){ + +}); + +test('overlaps', function(){}); + +test('collapse', function(){}); + +test('get start', function(){}); + +test('set start to object', function(){}); + +test('set start to number', function(){}); + +test('set start to string', function(){}); + +test('get end', function(){}); + +test('set end to object', function(){}); + +test('set end to number', function(){}); + +test('set end to string', function(){}); + + + +test('rect', function(){}); + +test('rects', function(){}); + +test('compare', function(){}); + +test('move', function(){}); + +test('clone', function(){}); + + +// adding brian's tests + +test("nested range", function(){ + $("#qunit-test-area") + .html("
        012
        345
        "); + $('#2').selection(1,5); + var range = $.Range.current(); + equals(range.start().container.data, "012", "start is 012") + equals(range.end().container.data, "4", "last char is 4") + }); + + test("rect", function(){ + $("#qunit-test-area") + .html("

        0123456789

        "); + $('#1').selection(1,5); + var range = $.Range.current(), + rect = range.rect(); + ok(rect.height, "height non-zero") + ok(rect.width, "width non-zero") + ok(rect.left, "left non-zero") + ok(rect.top, "top non-zero") + }); + + test("collapsed rect", function(){ + $("#qunit-test-area") + .html("

        0123456789

        "); + $('#1').selection(1,5); + var range = $.Range.current(), + start = range.clone().collapse(), + rect = start.rect(); + var r = start.rect(); + ok(rect.height, "height non-zero") + ok(rect.width == 0, "width zero") + ok(rect.left, "left non-zero") + ok(rect.top, "top non-zero") + }); + + test("rects", function(){ + $("#qunit-test-area") + .html("

        0123456789

        "); + $('#1').selection(1,5); + var range = $.Range.current(), + rects = range.rects(); + equals(rects.length, 2, "2 rects found") + }); + + test("multiline rects", function(){ + $("#qunit-test-area") + .html("
        <script type='text/ejs' id='recipes'>\n"+
        +				"<% for(var i=0; i < recipes.length; i++){ %>\n"+
        +				"  <li><%=recipes[i].name %></li>\n"+
        +				"<%} %>\n"+
        +				"</script>
        "); + $('#1').selection(3,56); + var range = $.Range.current(), + rects = range.rects(); + equals(rects.length, 2, "2 rects found") + ok(rects[1].width, "rect has width") + }); + + test("compare", function(){ + $("#qunit-test-area") + .html("

        0123456789

        "); + $('#1').selection(1,5); + var range1 = $.Range.current(); + $('#1').selection(2,3); + var range2 = $.Range.current(); + var pos = range1.compare("START_TO_START", range2) + equals(pos, -1, "pos works") + }); + + +}) + diff --git a/dom/route/qunit.html b/dom/route/qunit.html new file mode 100644 index 00000000..8ccb0a9f --- /dev/null +++ b/dom/route/qunit.html @@ -0,0 +1,15 @@ + + + + Jquery.Dom.Route FuncUnit Test + + + + +

        Jquery.Dom.Route Test Suite

        +

        +
        +

        +
          + + \ No newline at end of file diff --git a/dom/route/route.html b/dom/route/route.html new file mode 100644 index 00000000..84071408 --- /dev/null +++ b/dom/route/route.html @@ -0,0 +1,116 @@ + + + + Jquery.Dom.Route + + + +

          $.Route Demo

          + +
          +
          +
          + Use these links to modify the hash or change it directly in the address bar: +

          + URLs with a registered path: + #!pages/val1/val2/val3 + #!pages/val1// + #!pages/// + #!/val1/val2 +
          + URLs without paths: + #!pages// + #!/// +
          + Empty hash: + #! +
          +

          Hash update events:

          +
          +
          +
          +
          + + Value1:
          + Value2:
          + Value3: + + +
          +

          Data update events:

          +
          +
          + + + + + \ No newline at end of file diff --git a/dom/route/route.js b/dom/route/route.js new file mode 100644 index 00000000..4159adec --- /dev/null +++ b/dom/route/route.js @@ -0,0 +1,471 @@ +steal('jquery/lang/observe', 'jquery/event/hashchange', 'jquery/lang/string/deparam', +function( $ ) { + + // Helper methods used for matching routes. + var + // RegEx used to match route variables of the type ':name'. + // Any word character or a period is matched. + matcher = /\:([\w\.]+)/g, + // Regular expression for identifying &key=value lists. + paramsMatcher = /^(?:&[^=]+=[^&]*)+/, + // Converts a JS Object into a list of parameters that can be + // inserted into an html element tag. + makeProps = function( props ) { + var html = [], + name, val; + each(props, function(name, val){ + if ( name === 'className' ) { + name = 'class' + } + val && html.push(escapeHTML(name), "=\"", escapeHTML(val), "\" "); + }) + return html.join("") + }, + // Escapes ' and " for safe insertion into html tag parameters. + escapeHTML = function( content ) { + return content.replace(/"/g, '"').replace(/'/g, "'"); + }, + // Checks if a route matches the data provided. If any route variable + // is not present in the data the route does not match. If all route + // variables are present in the data the number of matches is returned + // to allow discerning between general and more specific routes. + matchesData = function(route, data) { + var count = 0; + for ( var i = 0; i < route.names.length; i++ ) { + if (!data.hasOwnProperty(route.names[i]) ) { + return -1; + } + count++; + } + return count; + }, + // + onready = true, + location = window.location, + encode = encodeURIComponent, + decode = decodeURIComponent, + each = $.each, + extend = $.extend; + + /** + * @class jQuery.route + * @inherits jQuery.Observe + * @plugin jquery/dom/route + * @parent dom + * @tag 3.2 + * + * jQuery.route helps manage browser history (and + * client state) by + * synchronizing the window.location.hash with + * an [jQuery.Observe]. + * + * ## Background Information + * + * To support the browser's back button and bookmarking + * in an Ajax application, most applications use + * the window.location.hash. By + * changing the hash (via a link or JavaScript), + * one is able to add to the browser's history + * without changing the page. The [jQuery.event.special.hashchange event] allows + * you to listen to when the hash is changed. + * + * Combined, this provides the basics needed to + * create history enabled Ajax websites. However, + * jQuery.Route addresses several other needs such as: + * + * - Pretty Routes + * - Keeping routes independent of application code + * - Listening to specific parts of the history changing + * - Setup / Teardown of widgets. + * + * ## How it works + * + * $.route is a [jQuery.Observe $.Observe] that represents the + * window.location.hash as an + * object. For example, if the hash looks like: + * + * #!type=videos&id=5 + * + * the data in $.route would look like: + * + * { type: 'videos', id: 5 } + * + * + * $.route keeps the state of the hash in-sync with the data in + * $.route. + * + * ## $.Observe + * + * $.route is a [jQuery.Observe $.Observe]. Understanding + * $.Observe is essential for using $.route correctly. + * + * You can + * listen to changes in an Observe with bind and + * delegate and change $.route's properties with + * attr and attrs. + * + * ### Listening to changes in an Observable + * + * Listen to changes in history + * by [jQuery.Observe.prototype.bind bind]ing to + * changes in $.route like: + * + * $.route.bind('change', function(ev, attr, how, newVal, oldVal) { + * + * }) + * + * - attr - the name of the changed attribute + * - how - the type of Observe change event (add, set or remove) + * - newVal/oldVal - the new and old values of the attribute + * + * You can also listen to specific changes + * with [jQuery.Observe.prototype.delegate delegate]: + * + * $.route.delegate('id','change', function(){ ... }) + * + * Observe lets you listen to the following events: + * + * - change - any change to the object + * - add - a property is added + * - set - a property value is added or changed + * - remove - a property is removed + * + * Listening for add is useful for widget setup + * behavior, remove is useful for teardown. + * + * ### Updating an observable + * + * Create changes in the route data like: + * + * $.route.attr('type','images'); + * + * Or change multiple properties at once with + * [jQuery.Observe.prototype.attrs attrs]: + * + * $.route.attr({type: 'pages', id: 5}, true) + * + * When you make changes to $.route, they will automatically + * change the hash. + * + * ## Creating a Route + * + * Use $.route(url, defaults) to create a + * route. A route is a mapping from a url to + * an object (that is the $.route's state). + * + * If no routes are added, or no route is matched, + * $.route's data is updated with the [jQuery.String.deparam deparamed] + * hash. + * + * location.hash = "#!type=videos"; + * // $.route -> {type : "videos"} + * + * Once routes are added and the hash changes, + * $.route looks for matching routes and uses them + * to update $.route's data. + * + * $.route( "content/:type" ); + * location.hash = "#!content/images"; + * // $.route -> {type : "images"} + * + * Default values can also be added: + * + * $.route("content/:type",{type: "videos" }); + * location.hash = "#!content/" + * // $.route -> {type : "videos"} + * + * ## Delay setting $.route + * + * By default, $.route sets its initial data + * on document ready. Sometimes, you want to wait to set + * this data. To wait, call: + * + * $.route.ready(false); + * + * and when ready, call: + * + * $.route.ready(true); + * + * ## Changing the route. + * + * Typically, you never want to set location.hash + * directly. Instead, you can change properties on $.route + * like: + * + * $.route.attr('type', 'videos') + * + * This will automatically look up the appropriate + * route and update the hash. + * + * Often, you want to create links. $.route provides + * the [jQuery.route.link] and [jQuery.route.url] helpers to make this + * easy: + * + * $.route.link("Videos", {type: 'videos'}) + * + * @param {String} url the fragment identifier to match. + * @param {Object} [defaults] an object of default values + * @return {jQuery.route} + */ + $.route = function( url, defaults ) { + // Extract the variable names and replace with regEx that will match an atual URL with values. + var names = [], + test = url.replace(matcher, function( whole, name ) { + names.push(name) + // TODO: I think this should have a + + return "([^\\/\\&]*)" // The '\\' is for string-escaping giving single '\' for regEx escaping + }); + + // Add route in a form that can be easily figured out + $.route.routes[url] = { + // A regular expression that will match the route when variable values + // are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which + // will match for any value of :page and :type (word chars or period). + test: new RegExp("^" + test+"($|&)"), + // The original URL, same as the index for this entry in routes. + route: url, + // An array of all the variable names in this route + names: names, + // Default values provided for the variables. + defaults: defaults || {}, + // The number of parts in the URL separated by '/'. + length: url.split('/').length + } + return $.route; + }; + + extend($.route, { + /** + * Parameterizes the raw JS object representation provided in data. + * If a route matching the provided data is found that URL is built + * from the data. Any remaining data is added at the end of the + * URL as & separated key/value parameters. + * + * @param {Object} data + * @return {String} The route URL and & separated parameters. + */ + param: function( data ) { + // Check if the provided data keys match the names in any routes; + // get the one with the most matches. + var route, + // need it to be at least 1 match + matches = 0, + matchCount, + routeName = data.route; + + delete data.route; + // if we have a route name in our $.route data, use it + if(routeName && (route = $.route.routes[routeName])){ + + } else { + // otherwise find route + each($.route.routes, function(name, temp){ + matchCount = matchesData(temp, data); + if ( matchCount > matches ) { + route = temp; + matches = matchCount + } + }); + } + // if this is match + + if ( route ) { + var cpy = extend({}, data), + // Create the url by replacing the var names with the provided data. + // If the default value is found an empty string is inserted. + res = route.route.replace(matcher, function( whole, name ) { + delete cpy[name]; + return data[name] === route.defaults[name] ? "" : encode( data[name] ); + }), + after; + // remove matching default values + each(route.defaults, function(name,val){ + if(cpy[name] === val) { + delete cpy[name] + } + }) + + // The remaining elements of data are added as + // $amp; separated parameters to the url. + after = $.param(cpy); + return res + (after ? "&" + after : "") + } + // If no route was found there is no hash URL, only paramters. + return $.isEmptyObject(data) ? "" : "&" + $.param(data); + }, + /** + * Populate the JS data object from a given URL. + * + * @param {Object} url + */ + deparam: function( url ) { + // See if the url matches any routes by testing it against the route.test regEx. + // By comparing the URL length the most specialized route that matches is used. + var route = { + length: -1 + }; + each($.route.routes, function(name, temp){ + if ( temp.test.test(url) && temp.length > route.length ) { + route = temp; + } + }); + // If a route was matched + if ( route.length > -1 ) { + var // Since RegEx backreferences are used in route.test (round brackets) + // the parts will contain the full matched string and each variable (backreferenced) value. + parts = url.match(route.test), + // start will contain the full matched string; parts contain the variable values. + start = parts.shift(), + // The remainder will be the &key=value list at the end of the URL. + remainder = url.substr(start.length - (parts[parts.length-1] === "&" ? 1 : 0) ), + // If there is a remainder and it contains a &key=value list deparam it. + obj = (remainder && paramsMatcher.test(remainder)) ? $.String.deparam( remainder.slice(1) ) : {}; + + // Add the default values for this route + obj = extend(true, {}, route.defaults, obj); + // Overwrite each of the default values in obj with those in parts if that part is not empty. + each(parts,function(i, part){ + if ( part && part !== '&') { + obj[route.names[i]] = decode( part ); + } + }); + obj.route = route.route; + return obj; + } + // If no route was matched it is parsed as a &key=value list. + if ( url.charAt(0) !== '&' ) { + url = '&' + url; + } + return paramsMatcher.test(url) ? $.String.deparam( url.slice(1) ) : {}; + }, + /** + * @hide + * A $.Observe that represents the state of the history. + */ + data: new $.Observe({}), + /** + * @attribute + * @type Object + * @hide + * + * A list of routes recognized by the router indixed by the url used to add it. + * Each route is an object with these members: + * + * - test - A regular expression that will match the route when variable values + * are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which + * will match for any value of :page and :type (word chars or period). + * + * - route - The original URL, same as the index for this entry in routes. + * + * - names - An array of all the variable names in this route + * + * - defaults - Default values provided for the variables or an empty object. + * + * - length - The number of parts in the URL separated by '/'. + */ + routes: {}, + /** + * Indicates that all routes have been added and sets $.route.data + * based upon the routes and the current hash. + * + * By default, ready is fired on jQuery's ready event. Sometimes + * you might want it to happen sooner or earlier. To do this call + * + * $.route.ready(false); //prevents firing by the ready event + * $.route.ready(true); // fire the first route change + * + * @param {Boolean} [start] + * @return $.route + */ + ready: function(val) { + if( val === false ) { + onready = false; + } + if( val === true || onready === true ) { + setState(); + } + return $.route; + }, + /** + * Returns a url from the options + * @param {Object} options + * @param {Boolean} merge true if the options should be merged with the current options + * @return {String} + */ + url: function( options, merge ) { + if (merge) { + return "#!" + $.route.param(extend({}, curParams, options)) + } else { + return "#!" + $.route.param(options) + } + }, + /** + * Returns a link + * @param {Object} name The text of the link. + * @param {Object} options The route options (variables) + * @param {Object} props Properties of the <a> other than href. + * @param {Boolean} merge true if the options should be merged with the current options + */ + link: function( name, options, props, merge ) { + return "" + name + ""; + }, + /** + * Returns true if the options represent the current page. + * @param {Object} options + * @return {Boolean} + */ + current: function( options ) { + return location.hash == "#!" + $.route.param(options) + } + }); + // onready + $(function() { + $.route.ready(); + }); + + // The functions in the following list applied to $.route (e.g. $.route.attr('...')) will + // instead act on the $.route.data Observe. + each(['bind','unbind','delegate','undelegate','attr','attrs','serialize','removeAttr'], function(i, name){ + $.route[name] = function(){ + return $.route.data[name].apply($.route.data, arguments) + } + }) + + var // A throttled function called multiple times will only fire once the + // timer runs down. Each call resets the timer. + throttle = function( func ) { + var timer; + return function() { + var args = arguments, + self = this; + clearTimeout(timer); + timer = setTimeout(function(){ + func.apply(self, args) + }, 1); + } + }, + // Intermediate storage for $.route.data. + curParams, + // Deparameterizes the portion of the hash of interest and assign the + // values to the $.route.data removing existing values no longer in the hash. + setState = function() { + var hash = location.hash.substr(1, 1) === '!' ? + location.hash.slice(2) : + location.hash.slice(1); // everything after #! + curParams = $.route.deparam( hash ); + $.route.attrs(curParams, true); + }; + + // If the hash changes, update the $.route.data + $(window).bind('hashchange', setState); + + // If the $.route.data changes, update the hash. + // Using .serialize() retrieves the raw data contained in the observable. + // This function is throttled so it only updates once even if multiple values changed. + $.route.bind("change", throttle(function() { + location.hash = "#!" + $.route.param($.route.serialize()) + })); +}) \ No newline at end of file diff --git a/dom/route/route_test.js b/dom/route/route_test.js new file mode 100644 index 00000000..f487da94 --- /dev/null +++ b/dom/route/route_test.js @@ -0,0 +1,267 @@ +steal('funcunit/qunit').then('./route.js',function(){ + +module("jquery/dom/route") + +test("deparam", function(){ + $.route.routes = {}; + $.route(":page",{ + page: "index" + }) + + var obj = $.route.deparam("jQuery.Controller"); + same(obj, { + page : "jQuery.Controller", + route: ":page" + }); + + obj = $.route.deparam(""); + same(obj, { + page : "index", + route: ":page" + }); + + obj = $.route.deparam("jQuery.Controller&where=there"); + same(obj, { + page : "jQuery.Controller", + where: "there", + route: ":page" + }); + + $.route.routes = {}; + $.route(":page/:index",{ + page: "index", + index: "foo" + }); + + obj = $.route.deparam("jQuery.Controller/&where=there"); + same(obj, { + page : "jQuery.Controller", + index: "foo", + where: "there", + route: ":page/:index" + }); +}) + +test("deparam of invalid url", function(){ + $.route.routes = {}; + $.route("pages/:var1/:var2/:var3", { + var1: 'default1', + var2: 'default2', + var3: 'default3' + }); + + // This path does not match the above route, and since the hash is not + // a &key=value list there should not be data. + obj = $.route.deparam("pages//"); + same(obj, {}); + + // A valid path with invalid parameters should return the path data but + // ignore the parameters. + obj = $.route.deparam("pages/val1/val2/val3&invalid-parameters"); + same(obj, { + var1: 'val1', + var2: 'val2', + var3: 'val3', + route: "pages/:var1/:var2/:var3" + }); +}) + +test("deparam of url with non-generated hash (manual override)", function(){ + $.route.routes = {}; + + // This won't be set like this by route, but it could easily happen via a + // user manually changing the URL or when porting a prior URL structure. + obj = $.route.deparam("page=foo&bar=baz&where=there"); + same(obj, { + page: 'foo', + bar: 'baz', + where: 'there' + }); +}) + +test("param", function(){ + $.route.routes = {}; + $.route("pages/:page",{ + page: "index" + }) + + var res = $.route.param({page: "foo"}); + equals(res, "pages/foo") + + res = $.route.param({page: "foo", index: "bar"}); + equals(res, "pages/foo&index=bar") + + $.route("pages/:page/:foo",{ + page: "index", + foo: "bar" + }) + + res = $.route.param({page: "foo", foo: "bar", where: "there"}); + equals(res, "pages/foo/&where=there") + + // There is no matching route so the hash should be empty. + res = $.route.param({}); + equals(res, "") + + $.route.routes = {}; + + res = $.route.param({page: "foo", bar: "baz", where: "there"}); + equals(res, "&page=foo&bar=baz&where=there") + + res = $.route.param({}); + equals(res, "") +}); + +test("symmetry", function(){ + $.route.routes = {}; + + var obj = {page: "=&[]", nestedArray : ["a"], nested : {a :"b"} } + + var res = $.route.param(obj) + + var o2 = $.route.deparam(res) + same(o2, obj) +}) + +test("light param", function(){ + $.route.routes = {}; + $.route(":page",{ + page: "index" + }) + + var res = $.route.param({page: "index"}); + equals(res, "") + + $.route("pages/:p1/:p2/:p3",{ + p1: "index", + p2: "foo", + p3: "bar" + }) + + res = $.route.param({p1: "index", p2: "foo", p3: "bar"}); + equals(res, "pages///") + + res = $.route.param({p1: "index", p2: "baz", p3: "bar"}); + equals(res, "pages//baz/") +}); + +test('param doesnt add defaults to params', function(){ + $.route.routes = {}; + + $.route("pages/:p1",{ + p2: "foo" + }) + var res = $.route.param({p1: "index", p2: "foo"}); + equals(res, "pages/index") +}) + +test("param-deparam", function(){ + + $.route(":page/:type",{ + page: "index", + type: "foo" + }) + + var data = {page: "jQuery.Controller", + type: "document", + bar: "baz", + where: "there"}; + var res = $.route.param(data); + var obj = $.route.deparam(res); + delete obj.route + same(obj,data ) + return; + data = {page: "jQuery.Controller", type: "foo", bar: "baz", where: "there"}; + res = $.route.param(data); + obj = $.route.deparam(res); + delete obj.route; + same(data, obj) + + data = {page: " a ", type: " / "}; + res = $.route.param(data); + obj = $.route.deparam(res); + delete obj.route; + same(obj ,data ,"slashes and spaces") + + data = {page: "index", type: "foo", bar: "baz", where: "there"}; + res = $.route.param(data); + obj = $.route.deparam(res); + delete obj.route; + same(data, obj) + + $.route.routes = {}; + + data = {page: "foo", bar: "baz", where: "there"}; + res = $.route.param(data); + obj = $.route.deparam(res); + same(data, obj) +}) + +test("precident", function(){ + $.route.routes = {}; + $.route(":who",{who: "index"}); + $.route("search/:search"); + + var obj = $.route.deparam("jQuery.Controller"); + same(obj, { + who : "jQuery.Controller", + route: ":who" + }); + + obj = $.route.deparam("search/jQuery.Controller"); + same(obj, { + search : "jQuery.Controller", + route: "search/:search" + },"bad deparam"); + + equal( $.route.param({ + search : "jQuery.Controller" + }), + "search/jQuery.Controller" , "bad param"); + + equal( $.route.param({ + who : "jQuery.Controller" + }), + "jQuery.Controller" ); +}) + +test("precident2", function(){ + $.route.routes = {}; + $.route(":type",{who: "index"}); + $.route(":type/:id"); + + equal( $.route.param({ + type : "foo", + id: "bar" + }), + "foo/bar" ); +}) + +test("linkTo", function(){ + $.route.routes = {}; + $.route(":foo"); + var res = $.route.link("Hello",{foo: "bar", baz: 'foo'}); + equal( res, 'Hello'); +}) + +test("param with route defined", function(){ + $.route.routes = {}; + $.route("holler") + $.route("foo"); + + var res = $.route.param({foo: "abc",route: "foo"}); + + equal(res, "foo&foo=abc") +}) + +test("route endings", function(){ + $.route.routes = {}; + $.route("foo",{foo: true}); + $.route("food",{food: true}) + + var res = $.route.deparam("food") + ok(res.food, "we get food back") + +}) + +}) diff --git a/dom/selection/qunit.html b/dom/selection/qunit.html new file mode 100644 index 00000000..b99867fd --- /dev/null +++ b/dom/selection/qunit.html @@ -0,0 +1,20 @@ + + + + selection QUnit Test + + + + + +

          selection Test Suite

          +

          +
          +

          +
          +
            +
            + + \ No newline at end of file diff --git a/dom/selection/selection.html b/dom/selection/selection.html new file mode 100644 index 00000000..536b16e5 --- /dev/null +++ b/dom/selection/selection.html @@ -0,0 +1,56 @@ + + + + selection + + + + + + + + + + + + + + + + + + + + +
            Select Textarea
            Select Input
            Select Within One Element

            0123456789

            Select Across Multiple Elements
            012
            345
            + + + + \ No newline at end of file diff --git a/dom/selection/selection.js b/dom/selection/selection.js new file mode 100644 index 00000000..45cc2280 --- /dev/null +++ b/dom/selection/selection.js @@ -0,0 +1,242 @@ +steal('jquery','jquery/dom/range').then(function($){ +var convertType = function(type){ + return type.replace(/([a-z])([a-z]+)/gi, function(all,first, next){ + return first+next.toLowerCase() + }).replace(/_/g,""); +}, +reverse = function(type){ + return type.replace(/^([a-z]+)_TO_([a-z]+)/i, function(all, first, last){ + return last+"_TO_"+first; + }); +}, +getWindow = function( element ) { + return element ? element.ownerDocument.defaultView || element.ownerDocument.parentWindow : window +}, +// A helper that uses range to abstract out getting the current start and endPos. +getElementsSelection = function(el, win){ + var current = $.Range.current(el).clone(), + entireElement = $.Range(el).select(el); + if(!current.overlaps(entireElement)){ + return null; + } + // we need to check if it starts before our element ... + if(current.compare("START_TO_START", entireElement) < 1){ + startPos = 0; + // we should move current ... + current.move("START_TO_START",entireElement); + }else{ + fromElementToCurrent =entireElement.clone(); + fromElementToCurrent.move("END_TO_START", current); + startPos = fromElementToCurrent.toString().length + } + + // now we need to make sure current isn't to the right of us ... + if(current.compare("END_TO_END", entireElement) >= 0){ + endPos = entireElement.toString().length + }else{ + endPos = startPos+current.toString().length + } + return { + start: startPos, + end : endPos + }; +}, +getSelection = function(el){ + // use selectionStart if we can. + var win = getWindow(el); + + if (el.selectionStart !== undefined) { + + if(document.activeElement + && document.activeElement != el + && el.selectionStart == el.selectionEnd + && el.selectionStart == 0){ + return {start: el.value.length, end: el.value.length}; + } + return {start: el.selectionStart, end: el.selectionEnd} + } else if(win.getSelection){ + return getElementsSelection(el, win) + } else{ + + try { + //try 2 different methods that work differently + // one should only work for input elements, but sometimes doesn't + // I don't know why this is, or what to detect + if (el.nodeName.toLowerCase() == 'input') { + var real = getWindow(el).document.selection.createRange(), r = el.createTextRange(); + r.setEndPoint("EndToStart", real); + + var start = r.text.length + return { + start: start, + end: start + real.text.length + } + } + else { + var res = getElementsSelection(el,win) + if(!res){ + return res; + } + // we have to clean up for ie's textareas + var current = $.Range.current().clone(), + r2 = current.clone().collapse().range, + r3 = current.clone().collapse(false).range; + + r2.moveStart('character', -1) + r3.moveStart('character', -1) + // if we aren't at the start, but previous is empty, we are at start of newline + if (res.startPos != 0 && r2.text == "") { + res.startPos += 2; + } + // do a similar thing for the end of the textarea + if (res.endPos != 0 && r3.text == "") { + res.endPos += 2; + } + + return res + } + }catch(e){ + return {start: el.value.length, end: el.value.length}; + } + } +}, +select = function( el, start, end ) { + var win = getWindow(el) + if(el.setSelectionRange){ + if(end === undefined){ + el.focus(); + el.setSelectionRange(start, start); + } else { + el.select(); + el.selectionStart = start; + el.selectionEnd = end; + } + } else if (el.createTextRange) { + //el.focus(); + var r = el.createTextRange(); + r.moveStart('character', start); + end = end || start; + r.moveEnd('character', end - el.value.length); + + r.select(); + } else if(win.getSelection){ + var doc = win.document, + sel = win.getSelection(), + range = doc.createRange(), + ranges = [start, end !== undefined ? end : start]; + getCharElement([el],ranges); + range.setStart(ranges[0].el, ranges[0].count); + range.setEnd(ranges[1].el, ranges[1].count); + + // removeAllRanges is suprisingly necessary for webkit ... BOOO! + sel.removeAllRanges(); + sel.addRange(range); + + } else if(win.document.body.createTextRange){ //IE's weirdness + var range = document.body.createTextRange(); + range.moveToElementText(el); + range.collapse() + range.moveStart('character', start) + range.moveEnd('character', end !== undefined ? end : start) + range.select(); + } + +}, +/* + * If one of the range values is within start and len, replace the range + * value with the element and its offset. + */ +replaceWithLess = function(start, len, range, el){ + if(typeof range[0] === 'number' && range[0] < len){ + range[0] = { + el: el, + count: range[0] - start + }; + } + if(typeof range[1] === 'number' && range[1] <= len){ + range[1] = { + el: el, + count: range[1] - start + };; + } +}, +getCharElement = function( elems , range, len ) { + var elem, + start; + + len = len || 0; + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + start = len + len += elem.nodeValue.length; + //check if len is now greater than what's in counts + replaceWithLess(start, len, range, elem ) + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + len = getCharElement( elem.childNodes, range, len ); + } + } + return len; +}; +/** + * @parent dom + * @tag beta + * + * Gets or sets the current text selection. + * + * ## Getting + * + * Gets the current selection in the context of an element. For example: + * + * $('textarea').selection() // -> { .... } + * + * returns an object with: + * + * - __start__ - The number of characters from the start of the element to the start of the selection. + * - __end__ - The number of characters from the start of the element to the end of the selection. + * - __range__ - A [jQuery.Range $.Range] that represents the current selection. + * + * This lets you get the selected text in a textarea like: + * + * var textarea = $('textarea') + * selection = textarea.selection(), + * selected = textarea.val().substr(selection.start, selection.end); + * + * alert('You selected '+selected+'.'); + * + * Selection works with all elements. If you want to get selection information of the document: + * + * $(document.body).selection(); + * + * ## Setting + * + * By providing a start and end offset, you can select text within a given element. + * + * $('#rte').selection(30, 40) + * + * ## Demo + * + * This demo shows setting the selection in various elements + * + * @demo jquery/dom/selection/selection.html + * + * @param {Number} [start] Start of the range + * @param {Number} [end] End of the range + * @return {Object|jQuery} returns the selection information or the jQuery collection for + * chaining. + */ +$.fn.selection = function(start, end){ + if(start !== undefined){ + return this.each(function(){ + select(this, start, end) + }) + }else{ + return getSelection(this[0]) + } +}; +// for testing +$.fn.selection.getCharElement = getCharElement; + +}); \ No newline at end of file diff --git a/dom/selection/selection_test.js b/dom/selection/selection_test.js new file mode 100644 index 00000000..28886444 --- /dev/null +++ b/dom/selection/selection_test.js @@ -0,0 +1,39 @@ +steal("funcunit/qunit", "jquery/dom/selection").then(function(){ + +module("jquery/dom/selection"); + +test("getCharElement", function(){ + $("#qunit-test-area") + .html(""+ + ""+ + "

            0123456789

            "+ + "
            012
            345
            "); + stop(); + setTimeout(function(){ + var types = ['textarea','#inp','#1','#2']; + for(var i =0; i< types.length; i++){ + //console.log(types[i]) + $(types[i]).selection(1,5); + } + /* + $('textarea').selection(1,5); + $('input').selection(1,5); + $('#1').selection(1,5); + $('#2').selection(1,5); + */ + var res = []; + for(var i =0; i< types.length; i++){ + res.push( $(types[i]).selection() ); + } + + + + for(var i =0; i< res.length; i++){ + same(res[i],{start: 1, end: 5},types[i]) + } + + start(); + },1000) +}); + +}); \ No newline at end of file diff --git a/dom/within/within.js b/dom/within/within.js index b846f4b1..818a18e0 100644 --- a/dom/within/within.js +++ b/dom/within/within.js @@ -1,7 +1,7 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function($){ +steal('jquery/dom').then(function($){ var withinBox = function(x, y, left, top, width, height ){ return (y >= top && y < top + height && @@ -11,33 +11,46 @@ steal.plugins('jquery/dom').then(function($){ /** * @function within * @parent dom - * Returns if the elements are within the position - * @param {Object} x - * @param {Object} y - * @param {Object} cache + * @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(x, y, cache) { +$.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 = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + 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(x, y, - offset.left, offset.top, - this.offsetWidth, this.offsetHeight ); + var res = withinBox(left, top, offset.left, offset.top, + this.offsetWidth, this.offsetHeight ); - if(res) ret.push(this); + if (res) { + ret.push(this); + } }); - return this.pushStack( jQuery.unique( ret ), "within", x+","+y ); + 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 @@ -52,7 +65,11 @@ $.fn.withinBox = function(left, top, width, height, cache){ if(this == document.documentElement) return this.ret.push(this); - var offset = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + var offset = cache ? + jQuery.data(this,"offset") || + jQuery.data(this,"offset", q.offset()) : + q.offset(); + var ew = q.width(), eh = q.height(); diff --git a/download/btn.png b/download/btn.png new file mode 100644 index 00000000..31669823 Binary files /dev/null and b/download/btn.png differ diff --git a/download/download.css b/download/download.css new file mode 100644 index 00000000..4766d661 --- /dev/null +++ b/download/download.css @@ -0,0 +1,88 @@ +body { + margin: 0px; padding: 0px; + overflow: hidden; +} +#plugins .dl_btn_container{ + background: url(btn.png); + border: none; + color: #fff; + font-size: 2em; + float: right; + line-height: 56px; + margin: 20px; + height: 55px; + text-align: center; + width: 171px; +} + +#plugins .dl_btn_container:hover{ + color: #bbb; + cursor: pointer +} + +#plugins #pluginForm { + border-top: dotted 1px #000; + padding: 0px; + width: 100%; + overflow: auto; +} + +#plugins .section{ + clear: both; + overflow: auto; + border-bottom: dotted 1px #000; + padding: 16px 16px 18px; + + background: #eee; +} + +#plugins .section .section-desc{ + float: left; + width: 125px; +} + +#plugins .section .section-desc h3{ + font-size: 1.0em; + margin: 0px 0px 6px; +} + +#plugins .section .section-desc p{ + font-size: 0.7em; +} + +#plugins .plugin{ + float: right; + padding: 0px; + overflow: auto; + width: 422px; + +} + +#plugins .plugin input { + float:left; +} + +#plugins .plugin label{ + color: #5387BD; + display: block; + float: left; + font-size: 0.7em; + font-weight: bold; + margin-left: 10px; + width: 175px; + word-wrap: break-word; +} + +#plugins .plugin .desc{ + float: left; + font-size: 0.7em; + margin: 0px 0px 0px 20px; + width: 194px; +} + +#plugins .select-all-container{ + text-align: right; + font-size: 0.7em; + font-weight: bold; + margin: 0px 6px 12px 0px; +} \ No newline at end of file diff --git a/download/download.html b/download/download.html new file mode 100644 index 00000000..98535d6c --- /dev/null +++ b/download/download.html @@ -0,0 +1,369 @@ + + + + Download Builder + + + + + +
            + + +
            + + + +
            + +
            +

            Class

            +
            + +
            + + + +
            Simulated inheritance in JavaScript
            +
            + + +
            + +
            + +
            +

            Controller

            +

            jQuery widget factory

            +
            + +
            + + + +
            Organize event handlers using event delegation
            +
            + +
            + + +
            + +
            +

            Model

            +

            Wrap an application's data layer

            +
            + +
            + + + +
            A basic skeleton to organize pieces of your application's data layer
            +
            + + +
            + + + +
            Backup and restore instance data
            +
            + +
            + + + +
            Type of model that provides methods for multiple model instances
            + +
            + + + +
            + + + +
            Validate data before sending it to the server
            +
            +
            + + +
            + +
            +

            Event

            +

            Helper functions used for managing events

            +
            + +
            + + + +
            Provides a destroyed event on an element
            +
            + +
            + + + +
            An event when the browser hash changes (for history management)
            +
            + + +
            + + + +
            Allows you to perform default actions as a result of an event
            +
            + +
            + + + +
            Provides drag events as a special events to jQuery
            +
            + +
            + + + +
            Limits the drag to a containing element
            +
            + +
            + + + +
            Will scroll elements with a scroll bar as the drag moves to borders
            +
            + +
            + + + +
            Provides drop events as a special event to jQuery
            +
            + +
            + + + +
            Drag in defined pixel increments
            +
            + +
            + + + +
            Provides delegate-able hover events
            +
            + +
            + + + +
            Normalizes resize events cross browser
            +
            + +
            + + + +
            Adds pauseable/resumeable events to jQuery
            +
            + + +
            + + +
            + +
            +

            View

            +

            Client side template engines with production build support.

            +
            + +
            + + + +
            A uniform interface for using templates with jQuery
            +
            + +
            + + + +
            EJS templates
            +
            + +
            + + + +
            JAML templates
            +
            + +
            + + + +
            Micro templates
            +
            + +
            + + + +
            jQuery tmpl templates
            +
            + +
            + + +
            + +
            +

            DOM

            +

            Useful jQuery extensions for the DOM

            +
            + +
            + + + +
            Compares the position of two nodes
            +
            + +
            + + + +
            Cookie management helpers
            +
            + +
            + + + +
            Rapidly get a bunch of computed styles from an element
            +
            + +
            + + + +
            Support for setting+animating inner+outer height and widths
            +
            + +
            + + + +
            Simulate AJAX responses
            +
            + +
            + + + +
            Name-value pairs that represents values in a form
            +
            + +
            + + + +
            Returns if the elements are within the position
            +
            + +
            + + + +
            jQuery.route helps manage browser history
            +
            + +
            + + + +
            Returns a jQuery.Range for the element selected.
            +
            + +
            + + +
            + +
            +

            Lang

            +

            JavaScript language helpers

            +
            + +
            + + + +
            A vector class
            +
            + +
            + + + +
            Splits a string with a regex correctly cross browser
            +
            + +
            + + + +
            Observe provides observable behavior on JavaScript Objects and Arrays.
            +
            + +
            + +
            + +
            + +
            + + + + + \ No newline at end of file diff --git a/download/download.js b/download/download.js new file mode 100644 index 00000000..0901782b --- /dev/null +++ b/download/download.js @@ -0,0 +1,104 @@ +(function(){ + $.Downloader = { + dependencies: [], + pluginData: null, + ready: function(){ + $.getJSON('../dist/standalone/dependencies.json', + function(data){ + $.Downloader.pluginData = data; + }); + $('#pluginForm').delegate("input[type=checkbox]", "change", + $.proxy($.Downloader.changeHandler, $.Downloader)); + + // append css if necessary + if(location.search && /csspath/.test(location.search)){ + var path = location.search.split("=")[1]; + var headID = document.getElementsByTagName("head")[0], + cssNode = document.createElement('link'); + cssNode.type = 'text/css'; + cssNode.rel = 'stylesheet'; + cssNode.href = path; + cssNode.media = 'screen'; + headID.appendChild(cssNode); + } + + $.Downloader.setupWordbreaks(); + }, + // inject characters in labels + setupWordbreaks: function(){ + var text, newText; + $(".plugin label").each(function(i){ + text = $(this).text(); + newText = text.replace(/\//g, "/") + $(this).html(newText); + }) + }, + changeHandler: function(ev){ + var $target = $(ev.target); + // if they unclicked, ignore it + if(!$target.attr('checked')) { + return; + } + this.dependencies = []; + var $form = $target.closest('form'), + params = $form.formParams(), i, queryVal; + for(i=0; i< lowerDependencies.length; j++) { + // TODO if you find a duplicate, remove the other one first + totalDependencies.push(lowerDependencies[j]) + } + } + } + totalDependencies.push(name) + return totalDependencies; + } + }; + $(document).ready($.Downloader.ready); + $("a.down",top.document.documentElement).click(function(ev){ + ev.preventDefault(); + $('form')[0].submit(); + }) +})() \ No newline at end of file diff --git a/download/test/controllerpage.html b/download/test/controllerpage.html new file mode 100644 index 00000000..ee3f49ab --- /dev/null +++ b/download/test/controllerpage.html @@ -0,0 +1,15 @@ + + + Test + + + + + + + + + + \ No newline at end of file diff --git a/download/test/jquery-1.4.3.js b/download/test/jquery-1.4.3.js new file mode 100644 index 00000000..ad9a79c4 --- /dev/null +++ b/download/test/jquery-1.4.3.js @@ -0,0 +1,6883 @@ +/*! + * jQuery JavaScript Library v1.4.3 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Oct 14 23:10:06 2010 -0400 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, + + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + rwhite = /\s/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Check for non-word characters + rnonword = /\W/, + + // Check for digits + rdigit = /\d/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // Has the ready events already been bound? + readyBound = false, + + // The functions to execute on DOM ready + readyList = [], + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $("TAG") + } else if ( !context && !rnonword.test( selector ) ) { + this.selector = selector; + this.context = document; + selector = document.getElementsByTagName( selector ); + return jQuery.merge( this, selector ); + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return (context || rootjQuery).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return jQuery( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if (selector.selector !== undefined) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.4.3", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + (this.selector ? " " : "") + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // If the DOM is already ready + if ( jQuery.isReady ) { + // Execute the function immediately + fn.call( document, jQuery ); + + // Otherwise, remember the function for later + } else if ( readyList ) { + // Add the function to the wait list + readyList.push( fn ); + } + + return this; + }, + + eq: function( i ) { + return i === -1 ? + this.slice( i ) : + this.slice( i, +i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || jQuery(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy, copyIsArray; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + // A third-party is pushing the ready event forwards + if ( wait === true ) { + jQuery.readyWait--; + } + + // Make sure that the DOM is not already loaded + if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + if ( readyList ) { + // Execute all of them + var fn, i = 0; + while ( (fn = readyList[ i++ ]) ) { + fn.call( document, jQuery ); + } + + // Reset the list of functions + readyList = null; + } + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyBound ) { + return; + } + + readyBound = true; + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNaN: function( obj ) { + return obj == null || !rdigit.test( obj ) || isNaN( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw msg; + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test(data.replace(rvalidescape, "@") + .replace(rvalidtokens, "]") + .replace(rvalidbraces, "")) ) { + + // Try to use the native JSON parser first + return window.JSON && window.JSON.parse ? + window.JSON.parse( data ) : + (new Function("return " + data))(); + + } else { + jQuery.error( "Invalid JSON: " + data ); + } + }, + + noop: function() {}, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && rnotwhite.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + + if ( jQuery.support.scriptEval ) { + script.appendChild( document.createTextNode( data ) ); + } else { + script.text = data; + } + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction(object); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // The extra typeof function check is to prevent crashes + // in Safari 2 (See: #3039) + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type(array); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var ret = [], value; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, length = elems.length; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + proxy: function( fn, proxy, thisObject ) { + if ( arguments.length === 2 ) { + if ( typeof proxy === "string" ) { + thisObject = fn; + fn = thisObject[ proxy ]; + proxy = undefined; + + } else if ( proxy && !jQuery.isFunction( proxy ) ) { + thisObject = proxy; + proxy = undefined; + } + } + + if ( !proxy && fn ) { + proxy = function() { + return fn.apply( thisObject || this, arguments ); + }; + } + + // Set the guid of unique handler to the same of original handler, so it can be removed + if ( fn ) { + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + } + + // So proxy can be declared as an argument + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can be optionally by executed if its a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return (new Date()).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +if ( indexOf ) { + jQuery.inArray = function( elem, array ) { + return indexOf.call( array, elem ); + }; +} + +// Verify that \s matches non-breaking spaces +// (IE fails on this test) +if ( !rwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +// Expose jQuery to the global object +return (window.jQuery = window.$ = jQuery); + +})(); + + +(function() { + + jQuery.support = {}; + + var root = document.documentElement, + script = document.createElement("script"), + div = document.createElement("div"), + id = "script" + jQuery.now(); + + div.style.display = "none"; + div.innerHTML = "
            a"; + + var all = div.getElementsByTagName("*"), + a = div.getElementsByTagName("a")[0], + select = document.createElement("select"), + opt = select.appendChild( document.createElement("option") ); + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return; + } + + jQuery.support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText insted) + style: /red/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55$/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: div.getElementsByTagName("input")[0].value === "on", + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Will be defined later + optDisabled: false, + checkClone: false, + scriptEval: false, + noCloneEvent: true, + boxModel: null, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableHiddenOffsets: true + }; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as diabled) + select.disabled = true; + jQuery.support.optDisabled = !opt.disabled; + + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} + + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support.scriptEval = true; + delete window[ id ]; + } + + root.removeChild( script ); + + if ( div.attachEvent && div.fireEvent ) { + div.attachEvent("onclick", function click() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + jQuery.support.noCloneEvent = false; + div.detachEvent("onclick", click); + }); + div.cloneNode(true).fireEvent("onclick"); + } + + div = document.createElement("div"); + div.innerHTML = ""; + + var fragment = document.createDocumentFragment(); + fragment.appendChild( div.firstChild ); + + // WebKit doesn't clone checked state correctly in fragments + jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + + // Figure out if the W3C box model works as expected + // document.body must exist before we can do this + jQuery(function() { + var div = document.createElement("div"); + div.style.width = div.style.paddingLeft = "1px"; + + document.body.appendChild( div ); + jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + + if ( "zoom" in div.style ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
            "; + jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; + } + + div.innerHTML = "
            t
            "; + var tds = div.getElementsByTagName("td"); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + + tds[0].style.display = ""; + tds[1].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE < 8 fail this test) + jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; + div.innerHTML = ""; + + document.body.removeChild( div ).style.display = "none"; + div = tds = null; + }); + + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); + + // release memory in IE + root = script = div = all = a = null; +})(); + +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; + + + + +var windowData = {}, + rbrace = /^(?:\{.*\}|\[.*\])$/; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + expando: "jQuery" + jQuery.now(), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + data: function( elem, name, data ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + elem = elem == window ? + windowData : + elem; + + var isNode = elem.nodeType, + id = isNode ? elem[ jQuery.expando ] : null, + cache = jQuery.cache, thisCache; + + if ( isNode && !id && typeof name === "string" && data === undefined ) { + return; + } + + // Get the data from the object directly + if ( !isNode ) { + cache = elem; + + // Compute a unique ID for the element + } else if ( !id ) { + elem[ jQuery.expando ] = id = ++jQuery.uuid; + } + + // Avoid generating a new cache unless none exists and we + // want to manipulate it. + if ( typeof name === "object" ) { + if ( isNode ) { + cache[ id ] = jQuery.extend(cache[ id ], name); + + } else { + jQuery.extend( cache, name ); + } + + } else if ( isNode && !cache[ id ] ) { + cache[ id ] = {}; + } + + thisCache = isNode ? cache[ id ] : cache; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) { + thisCache[ name ] = data; + } + + return typeof name === "string" ? thisCache[ name ] : thisCache; + }, + + removeData: function( elem, name ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + elem = elem == window ? + windowData : + elem; + + var isNode = elem.nodeType, + id = isNode ? elem[ jQuery.expando ] : elem, + cache = jQuery.cache, + thisCache = isNode ? cache[ id ] : id; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( thisCache ) { + // Remove the section of cache data + delete thisCache[ name ]; + + // If we've removed all the data, remove the element's cache + if ( isNode && jQuery.isEmptyObject(thisCache) ) { + jQuery.removeData( elem ); + } + } + + // Otherwise, we want to remove all of the element's data + } else { + if ( isNode && jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + + // Completely remove the data cache + } else if ( isNode ) { + delete cache[ id ]; + + // Remove all fields from the object + } else { + for ( var n in elem ) { + delete elem[ n ]; + } + } + } + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + if ( typeof key === "undefined" ) { + return this.length ? jQuery.data( this[0] ) : null; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && this[0].nodeType === 1 ) { + data = this[0].getAttribute( "data-" + key ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + !jQuery.isNaN( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + } else { + data = undefined; + } + } + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var $this = jQuery( this ), args = [ parts[0], value ]; + + $this.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + $this.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + + + + +jQuery.extend({ + queue: function( elem, type, data ) { + if ( !elem ) { + return; + } + + type = (type || "fx") + "queue"; + var q = jQuery.data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( !data ) { + return q || []; + } + + if ( !q || jQuery.isArray(data) ) { + q = jQuery.data( elem, type, jQuery.makeArray(data) ); + + } else { + q.push( data ); + } + + return q; + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), fn = queue.shift(); + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift("inprogress"); + } + + fn.call(elem, function() { + jQuery.dequeue(elem, type); + }); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function( i ) { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + type = type || "fx"; + + return this.queue( type, function() { + var elem = this; + setTimeout(function() { + jQuery.dequeue( elem, type ); + }, time ); + }); + }, + + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + } +}); + + + + +var rclass = /[\n\t]/g, + rspaces = /\s+/, + rreturn = /\r/g, + rspecialurl = /^(?:href|src|style)$/, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rradiocheck = /^(?:radio|checkbox)$/i; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name, fn ) { + return this.each(function(){ + jQuery.attr( this, name, "" ); + if ( this.nodeType === 1 ) { + this.removeAttribute( name ); + } + }); + }, + + addClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.addClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( value && typeof value === "string" ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className ) { + elem.className = value; + + } else { + var className = " " + elem.className + " ", setClass = elem.className; + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { + setClass += " " + classNames[c]; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.removeClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + var className = (" " + elem.className + " ").replace(rclass, " "); + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[c] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this); + self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, i = 0, self = jQuery(this), + state = stateVal, + classNames = value.split( rspaces ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery.data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " "; + for ( var i = 0, l = this.length; i < l; i++ ) { + if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + if ( !arguments.length ) { + var elem = this[0]; + + if ( elem ) { + if ( jQuery.nodeName( elem, "option" ) ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { + return elem.getAttribute("value") === null ? "on" : elem.value; + } + + + // Everything else, we just grab the value + return (elem.value || "").replace(rreturn, ""); + + } + + return undefined; + } + + var isFunction = jQuery.isFunction(value); + + return this.each(function(i) { + var self = jQuery(this), val = value; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call(this, i, self.val()); + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray(val) ) { + val = jQuery.map(val, function (value) { + return value == null ? "" : value + ""; + }); + } + + if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { + this.checked = jQuery.inArray( self.val(), val ) >= 0; + + } else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(val); + + jQuery( "option", this ).each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + this.selectedIndex = -1; + } + + } else { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + // don't set attributes on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery(elem)[name](value); + } + + var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), + // Whether we are setting (or getting) + set = value !== undefined; + + // Try to normalize/fix the name + name = notxml && jQuery.props[ name ] || name; + + // Only do all the following if this is a node (faster for style) + if ( elem.nodeType === 1 ) { + // These attributes require special treatment + var special = rspecialurl.test( name ); + + // Safari mis-reports the default selected property of an option + // Accessing the parent's selectedIndex property fixes it + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + + // If applicable, access the attribute via the DOM 0 way + // 'in' checks fail in Blackberry 4.7 #6931 + if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { + if ( set ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } + + if ( value === null ) { + if ( elem.nodeType === 1 ) { + elem.removeAttribute( name ); + } + + } else { + elem[ name ] = value; + } + } + + // browsers index elements by id/name on forms, give priority to attributes. + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { + return elem.getAttributeNode( name ).nodeValue; + } + + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + if ( name === "tabIndex" ) { + var attributeNode = elem.getAttributeNode( "tabIndex" ); + + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + + return elem[ name ]; + } + + if ( !jQuery.support.style && notxml && name === "style" ) { + if ( set ) { + elem.style.cssText = "" + value; + } + + return elem.style.cssText; + } + + if ( set ) { + // convert the value to a string (all browsers do this but IE) see #1070 + elem.setAttribute( name, "" + value ); + } + + // Ensure that missing attributes return undefined + // Blackberry 4.7 returns "" from getAttribute #6938 + if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { + return undefined; + } + + var attr = !jQuery.support.hrefNormalized && notxml && special ? + // Some attributes require a special call on IE + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return attr === null ? undefined : attr; + } + } +}); + + + + +var rnamespaces = /\.(.*)$/, + rformElems = /^(?:textarea|input|select)$/i, + rperiod = /\./g, + rspace = / /g, + rescape = /[^\w\s.|`]/g, + fcleanup = function( nm ) { + return nm.replace(rescape, "\\$&"); + }, + focusCounts = { focusin: 0, focusout: 0 }; + +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code originated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { + + // Bind an event to an element + // Original by Dean Edwards + add: function( elem, types, handler, data ) { + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { + elem = window; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var handleObjIn, handleObj; + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure + var elemData = jQuery.data( elem ); + + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; + } + + // Use a key less likely to result in collisions for plain JS objects. + // Fixes bug #7150. + var eventKey = elem.nodeType ? "events" : "__events__", + events = elemData[ eventKey ], + eventHandle = elemData.handle; + + if ( typeof events === "function" ) { + // On plain objects events is a fn that holds the the data + // which prevents this data from being JSON serialized + // the function does not need to be called, it just contains the data + eventHandle = events.handle; + events = events.events; + + } else if ( !events ) { + if ( !elem.nodeType ) { + // On plain objects, create a fn that acts as the holder + // of the values to avoid JSON serialization of event data + elemData[ eventKey ] = elemData = function(){}; + } + + elemData.events = events = {}; + } + + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + return typeof jQuery !== "undefined" && !jQuery.event.triggered ? + jQuery.event.handle.apply( eventHandle.elem, arguments ) : + undefined; + }; + } + + // Add elem as a property of the handle function + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = types.split(" "); + + var type, i = 0, namespaces; + + while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + + // Namespaced event handlers + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); + + } else { + namespaces = []; + handleObj.namespace = ""; + } + + handleObj.type = type; + if ( !handleObj.guid ) { + handleObj.guid = handler.guid; + } + + // Get the current list of functions bound to this event + var handlers = events[ type ], + special = jQuery.event.special[ type ] || {}; + + // Init the event handler queue + if ( !handlers ) { + handlers = events[ type ] = []; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add the function to the element's handler list + handlers.push( handleObj ); + + // Keep track of which events have been used, for global triggering + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, pos ) { + // don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + eventKey = elem.nodeType ? "events" : "__events__", + elemData = jQuery.data( elem ), + events = elemData && elemData[ eventKey ]; + + if ( !elemData || !events ) { + return; + } + + if ( typeof events === "function" ) { + elemData = events; + events = events.events; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } + } + + continue; + } + + special = jQuery.event.special[ type ] || {}; + + for ( j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); + } + + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + + if ( pos != null ) { + break; + } + } + } + + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( typeof elemData === "function" ) { + jQuery.removeData( elem, eventKey ); + + } else if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem ); + } + } + }, + + // bubbling is internal + trigger: function( event, data, elem /*, bubbling */ ) { + // Event object or event type + var type = event.type || event, + bubbling = arguments[3]; + + if ( !bubbling ) { + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + jQuery.extend( jQuery.Event(type), event ) : + // Just the event type (string) + jQuery.Event(type); + + if ( type.indexOf("!") >= 0 ) { + event.type = type = type.slice(0, -1); + event.exclusive = true; + } + + // Handle a global trigger + if ( !elem ) { + // Don't bubble custom events when global (to avoid too much overhead) + event.stopPropagation(); + + // Only trigger if we've ever bound an event for it + if ( jQuery.event.global[ type ] ) { + jQuery.each( jQuery.cache, function() { + if ( this.events && this.events[type] ) { + jQuery.event.trigger( event, data, this.handle.elem ); + } + }); + } + } + + // Handle triggering a single element + + // don't do events on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + // Clean up in case it is reused + event.result = undefined; + event.target = elem; + + // Clone the incoming data, if any + data = jQuery.makeArray( data ); + data.unshift( event ); + } + + event.currentTarget = elem; + + // Trigger the event, it is assumed that "handle" is a function + var handle = elem.nodeType ? + jQuery.data( elem, "handle" ) : + (jQuery.data( elem, "__events__" ) || {}).handle; + + if ( handle ) { + handle.apply( elem, data ); + } + + var parent = elem.parentNode || elem.ownerDocument; + + // Trigger an inline bound script + try { + if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { + if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { + event.result = false; + event.preventDefault(); + } + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (inlineError) {} + + if ( !event.isPropagationStopped() && parent ) { + jQuery.event.trigger( event, data, parent, true ); + + } else if ( !event.isDefaultPrevented() ) { + var target = event.target, old, targetType = type.replace(rnamespaces, ""), + isClick = jQuery.nodeName(target, "a") && targetType === "click", + special = jQuery.event.special[ targetType ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + + try { + if ( target[ targetType ] ) { + // Make sure that we don't accidentally re-trigger the onFOO events + old = target[ "on" + targetType ]; + + if ( old ) { + target[ "on" + targetType ] = null; + } + + jQuery.event.triggered = true; + target[ targetType ](); + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (triggerError) {} + + if ( old ) { + target[ "on" + targetType ] = old; + } + + jQuery.event.triggered = false; + } + } + }, + + handle: function( event ) { + var all, handlers, namespaces, namespace_sort = [], namespace_re, events, args = jQuery.makeArray( arguments ); + + event = args[0] = jQuery.event.fix( event || window.event ); + event.currentTarget = this; + + // Namespaced event handlers + all = event.type.indexOf(".") < 0 && !event.exclusive; + + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace_sort = namespaces.slice(0).sort(); + namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.namespace = event.namespace || namespace_sort.join("."); + + events = jQuery.data(this, this.nodeType ? "events" : "__events__"); + + if ( typeof events === "function" ) { + events = events.events; + } + + handlers = (events || {})[ event.type ]; + + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + + // Filter the functions by class + if ( all || namespace_re.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + } + + return event.result; + }, + + props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // store a copy of the original event object + // and "clone" to set read-only properties + var originalEvent = event; + event = jQuery.Event( originalEvent ); + + for ( var i = this.props.length, prop; i; ) { + prop = this.props[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary + if ( !event.target ) { + event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + } + + // check if target is a textnode (safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var doc = document.documentElement, body = document.body; + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Add which for key events + if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { + event.which = event.charCode != null ? event.charCode : event.keyCode; + } + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) { + event.metaKey = event.ctrlKey; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button !== undefined ) { + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } + + return event; + }, + + // Deprecated, use jQuery.guid instead + guid: 1E8, + + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady, + teardown: jQuery.noop + }, + + live: { + add: function( handleObj ) { + jQuery.event.add( this, + liveConvert( handleObj.origType, handleObj.selector ), + jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); + }, + + remove: function( handleObj ) { + jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); + } + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src ) { + // Allow instantiation without the 'new' keyword + if ( !this.preventDefault ) { + return new jQuery.Event( src ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + // Event type + } else { + this.type = src; + } + + // timeStamp is buggy for some events on Firefox(#3843) + // So we won't rely on the native value + this.timeStamp = jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function( event ) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { + parent = parent.parentNode; + } + + if ( parent !== this ) { + // set the correct event type + event.type = event.data; + + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } +}, + +// In case of event delegation, we only need to rename the event.type, +// liveHandler will take care of the rest. +delegate = function( event ) { + event.type = event.data; + jQuery.event.handle.apply( this, arguments ); +}; + +// Create mouseenter and mouseleave events +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + setup: function( data ) { + jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); + }, + teardown: function( data ) { + jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + } + }; +}); + +// submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); + } + }; + +} + +// change delegation, happens here so we have bind. +if ( !jQuery.support.changeBubbles ) { + + var changeFilters, + + getVal = function( elem ) { + var type = elem.type, val = elem.value; + + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; + + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; + + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; + } + + return val; + }, + + testChange = function testChange( e ) { + var elem = e.target, data, val; + + if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { + return; + } + + data = jQuery.data( elem, "_change_data" ); + val = getVal(elem); + + // the current data will be also retrieved by beforeactivate + if ( e.type !== "focusout" || elem.type !== "radio" ) { + jQuery.data( elem, "_change_data", val ); + } + + if ( data === undefined || val === data ) { + return; + } + + if ( data != null || val ) { + e.type = "change"; + e.liveFired = undefined; + return jQuery.event.trigger( e, arguments[1], elem ); + } + }; + + jQuery.event.special.change = { + filters: { + focusout: testChange, + + beforedeactivate: testChange, + + click: function( e ) { + var elem = e.target, type = elem.type; + + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, + + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; + + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, + + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information + beforeactivate: function( e ) { + var elem = e.target; + jQuery.data( elem, "_change_data", getVal(elem) ); + } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return rformElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return rformElems.test( this.nodeName ); + } + }; + + changeFilters = jQuery.event.special.change.filters; + + // Handle when the input is .focus()'d + changeFilters.focus = changeFilters.beforeactivate; +} + +function trigger( type, elem, args ) { + args[0].type = type; + return jQuery.event.handle.apply( elem, args ); +} + +// Create "bubbling" focus and blur events +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + if ( focusCounts[fix]++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --focusCounts[fix] === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.trigger( e, null, e.target ); + } + }); +} + +jQuery.each(["bind", "one"], function( i, name ) { + jQuery.fn[ name ] = function( type, data, fn ) { + // Handle object literals + if ( typeof type === "object" ) { + for ( var key in type ) { + this[ name ](key, data, type[key], fn); + } + return this; + } + + if ( jQuery.isFunction( data ) || data === false ) { + fn = data; + data = undefined; + } + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }) : fn; + + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; + }; +}); + +jQuery.fn.extend({ + unbind: function( type, fn ) { + // Handle object literals + if ( typeof type === "object" && !type.preventDefault ) { + for ( var key in type ) { + this.unbind(key, type[key]); + } + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } + } + + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + + triggerHandler: function( type, data ) { + if ( this[0] ) { + var event = jQuery.Event( type ); + event.preventDefault(); + event.stopPropagation(); + jQuery.event.trigger( event, data, this[0] ); + return event.result; + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, i = 1; + + // link all the functions, so any of them can unbind this click handler + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); + } + + return this.click( jQuery.proxy( fn, function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + })); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + +jQuery.each(["live", "die"], function( i, name ) { + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); + + if ( typeof types === "object" && !types.preventDefault ) { + for ( var key in types ) { + context[ name ]( key, data, types[key], selector ); + } + + return this; + } + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + types = (types || "").split(" "); + + while ( (type = types[ i++ ]) != null ) { + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + + if ( name === "live" ) { + // bind live handler + for ( var j = 0, l = context.length; j < l; j++ ) { + jQuery.event.add( context[j], "live." + liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + } + + } else { + // unbind live handler + context.unbind( "live." + liveConvert( type, selector ), fn ); + } + } + + return this; + }; +}); + +function liveHandler( event ) { + var stop, maxLevel, elems = [], selectors = [], + related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + events = jQuery.data( this, this.nodeType ? "events" : "__events__" ); + + if ( typeof events === "function" ) { + events = events.events; + } + + // Make sure we avoid non-left-click bubbling in Firefox (#3861) + if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { + return; + } + + if ( event.namespace ) { + namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); + + } else { + live.splice( j--, 1 ); + } + } + + match = jQuery( event.target ).closest( selectors, event.currentTarget ); + + for ( i = 0, l = match.length; i < l; i++ ) { + close = match[i]; + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { + elem = close.elem; + related = null; + + // Those two events require additional checking + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + event.type = handleObj.preType; + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; + } + + if ( !related || related !== elem ) { + elems.push({ elem: elem, handleObj: handleObj, level: close.level }); + } + } + } + } + + for ( i = 0, l = elems.length; i < l; i++ ) { + match = elems[i]; + + if ( maxLevel && match.level > maxLevel ) { + break; + } + + event.currentTarget = match.elem; + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + ret = match.handleObj.origHandler.apply( match.elem, arguments ); + + if ( ret === false || event.isPropagationStopped() ) { + maxLevel = match.level; + + if ( ret === false ) { + stop = false; + } + } + } + + return stop; +} + +function liveConvert( type, selector ) { + return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); +} + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.bind( name, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } +}); + +// Prevent memory leaks in IE +// Window isn't included so as not to unbind existing unload events +// More info: +// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ +if ( window.attachEvent && !window.addEventListener ) { + jQuery(window).bind("unload", function() { + for ( var id in jQuery.cache ) { + if ( jQuery.cache[ id ].handle ) { + // Try/Catch is to handle iframes being unloaded, see #4280 + try { + jQuery.event.remove( jQuery.cache[ id ].handle.elem ); + } catch(e) {} + } + } + }); +} + + +/*! + * Sizzle CSS Selector Engine - v1.0 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function(){ + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function(selector, context, results, seed) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), + soFar = selector, ret, cur, pop, i; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec(""); + m = chunker.exec(soFar); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set ); + } + } + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray(set); + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); + } + } + } + } + + return results; +}; + +Sizzle.matches = function(expr, set){ + return Sizzle(expr, null, null, set); +}; + +Sizzle.matchesSelector = function(node, expr){ + return Sizzle(expr, null, null, [node]).length > 0; +}; + +Sizzle.find = function(expr, context, isXML){ + var set; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var type = Expr.order[i], match; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice(1,1); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = context.getElementsByTagName("*"); + } + + return {set: set, expr: expr}; +}; + +Sizzle.filter = function(expr, set, inplace, not){ + var old = expr, result = [], curLoop = set, match, anyFound, + isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var filter = Expr.filter[ type ], found, item, left = match[1]; + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + } else { + curLoop[i] = false; + } + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + leftMatch: {}, + attrMap: { + "class": "className", + "for": "htmlFor" + }, + attrHandle: { + href: function(elem){ + return elem.getAttribute("href"); + } + }, + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test(part), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + ">": function(checkSet, part){ + var isPartStr = typeof part === "string", + elem, i = 0, l = checkSet.length; + + if ( isPartStr && !/\W/.test(part) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + "": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + }, + "~": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + } + }, + find: { + ID: function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + NAME: function(match, context){ + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], results = context.getElementsByName(match[1]); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + TAG: function(match, context){ + return context.getElementsByTagName(match[1]); + } + }, + preFilter: { + CLASS: function(match, curLoop, inplace, result, not, isXML){ + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + ID: function(match){ + return match[1].replace(/\\/g, ""); + }, + TAG: function(match, curLoop){ + return match[1].toLowerCase(); + }, + CHILD: function(match){ + if ( match[1] === "nth" ) { + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + ATTR: function(match, curLoop, inplace, result, not, isXML){ + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + PSEUDO: function(match, curLoop, inplace, result, not){ + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { + result.push.apply( result, ret ); + } + return false; + } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + POS: function(match){ + match.unshift( true ); + return match; + } + }, + filters: { + enabled: function(elem){ + return elem.disabled === false && elem.type !== "hidden"; + }, + disabled: function(elem){ + return elem.disabled === true; + }, + checked: function(elem){ + return elem.checked === true; + }, + selected: function(elem){ + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + return elem.selected === true; + }, + parent: function(elem){ + return !!elem.firstChild; + }, + empty: function(elem){ + return !elem.firstChild; + }, + has: function(elem, i, match){ + return !!Sizzle( match[3], elem ).length; + }, + header: function(elem){ + return (/h\d/i).test( elem.nodeName ); + }, + text: function(elem){ + return "text" === elem.type; + }, + radio: function(elem){ + return "radio" === elem.type; + }, + checkbox: function(elem){ + return "checkbox" === elem.type; + }, + file: function(elem){ + return "file" === elem.type; + }, + password: function(elem){ + return "password" === elem.type; + }, + submit: function(elem){ + return "submit" === elem.type; + }, + image: function(elem){ + return "image" === elem.type; + }, + reset: function(elem){ + return "reset" === elem.type; + }, + button: function(elem){ + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + input: function(elem){ + return (/input|select|textarea|button/i).test(elem.nodeName); + } + }, + setFilters: { + first: function(elem, i){ + return i === 0; + }, + last: function(elem, i, match, array){ + return i === array.length - 1; + }, + even: function(elem, i){ + return i % 2 === 0; + }, + odd: function(elem, i){ + return i % 2 === 1; + }, + lt: function(elem, i, match){ + return i < match[3] - 0; + }, + gt: function(elem, i, match){ + return i > match[3] - 0; + }, + nth: function(elem, i, match){ + return match[3] - 0 === i; + }, + eq: function(elem, i, match){ + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function(elem, match, i, array){ + var name = match[1], filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + } else { + Sizzle.error( "Syntax error, unrecognized expression: " + name ); + } + }, + CHILD: function(elem, match){ + var type = match[1], node = elem; + switch (type) { + case 'only': + case 'first': + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + if ( type === "first" ) { + return true; + } + node = elem; + case 'last': + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + return true; + case 'nth': + var first = match[2], last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + if ( first === 0 ) { + return diff === 0; + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + ID: function(elem, match){ + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + TAG: function(elem, match){ + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + }, + CLASS: function(elem, match){ + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + ATTR: function(elem, match){ + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + POS: function(elem, match, i, array){ + var name = match[2], filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function(array, results) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch(e){ + makeArray = function(array, results) { + var ret = results || [], i = 0; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; +} else { + sortOrder = function( a, b ) { + var ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, + cur = aup, al, bl; + + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // If the nodes are siblings (or identical) we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } + + return ret; +}; + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(); + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + var root = document.documentElement; + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + } + }; + + Expr.filter.ID = function(elem, match){ + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + root = form = null; // release memory in IE +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function(match, context){ + var results = context.getElementsByTagName(match[1]); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + Expr.attrHandle.href = function(elem){ + return elem.getAttribute("href", 2); + }; + } + + div = null; // release memory in IE +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "

            "; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function(query, context, extra, seed){ + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + if ( context.nodeType === 9 ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var old = context.id, id = context.id = "__sizzle__"; + + try { + return makeArray( context.querySelectorAll( "#" + id + " " + query ), extra ); + + } catch(pseudoError) { + } finally { + if ( old ) { + context.id = old; + + } else { + context.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + div = null; // release memory in IE + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, ":sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + if ( matches ) { + Sizzle.matchesSelector = function( node, expr ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) ) { + return matches.call( node, expr ); + } + } catch(e) {} + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
            "; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function(match, context, isXML) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + div = null; // release memory in IE +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +Sizzle.contains = document.documentElement.contains ? function(a, b){ + return a !== b && (a.contains ? a.contains(b) : true); +} : function(a, b){ + return !!(a.compareDocumentPosition(b) & 16); +}; + +Sizzle.isXML = function(elem){ + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function(selector, context){ + var tmpSet = [], later = "", match, + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS; + +jQuery.fn.extend({ + find: function( selector ) { + var ret = this.pushStack( "", "find", selector ), length = 0; + + for ( var i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( var n = length; n < ret.length; n++ ) { + for ( var r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && jQuery.filter( selector, this ).length > 0; + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + if ( jQuery.isArray( selectors ) ) { + var match, matches = {}, selector, level = 1; + + if ( cur && selectors.length ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { + selector = selectors[i]; + + if ( !matches[selector] ) { + matches[selector] = jQuery.expr.match.POS.test( selector ) ? + jQuery( selector, context || this.context ) : + selector; + } + } + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( selector in matches ) { + match = matches[selector]; + + if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { + ret.push({ selector: selector, elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + } + + return ret; + } + + var pos = POS.test( selectors ) ? + jQuery( selectors, context || this.context ) : null; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique(ret) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + if ( !elem || typeof elem === "string" ) { + return jQuery.inArray( this[0], + // If it receives a string, the selector is used + // If it receives nothing, the siblings are used + elem ? jQuery( elem ) : this.parent().children() ); + } + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context || this.context ) : + jQuery.makeArray( selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call(arguments).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], cur = elem[dir]; + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return (elem === qualifier) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + }); +} + + + + +var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /\s]+\/)>/g, + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
            ", "
            " ], + thead: [ 1, "", "
            " ], + tr: [ 2, "", "
            " ], + td: [ 3, "", "
            " ], + col: [ 2, "", "
            " ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and \ No newline at end of file diff --git a/event/default/default.js b/event/default/default.js index f3dd0c2a..050512ff 100644 --- a/event/default/default.js +++ b/event/default/default.js @@ -1,191 +1,154 @@ + +steal('jquery/event').then(function($){ + /** - * @add jQuery.event.special + * @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 */ -steal.plugins('jquery/event').then(function($){ +$.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= /\.(.*)$/; +var types = {}, rnamespaces= /\.(.*)$/, $event = $.event; /** * @attribute default * @parent specialevents * @plugin jquery/event/default - * @download jquery/dist/jquery.event.default.js + * @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.

            - * @codestart - * $("div").bind("default.show", function(ev){ ... }); - * $("ul").delegate("li","default.activate", function(ev){ ... }); - * @codeend - *

            - * The default plugin also adds the [jQuery.fn.triggerDefault triggerDefault] and [jQuery.fn.triggerDefaults triggerDefaults] methods. These are used to trigger - * an event and report back whether preventDefault was called on the event. The only difference is [jQuery.fn.triggerDefault triggerDefault] - * doesn't bubble. - *

            - *

            Example

            - *

            Lets look at how you could build a simple tabs widget with default events. - * First with just jQuery:

            - *

            + * + * 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:

            + * + * Lets see how we would build this with JavaScriptMVC: + * * @demo jquery/event/default/default.html */ -$.event.special["default"] = { +$event.special["default"] = { add: function( handleObj ) { //save the type types[handleObj.namespace.replace(rnamespaces,"")] = true; - //move the handler ... - var origHandler = handleObj.handler; - handleObj.origHandler = origHandler; - handleObj.handler = function(ev, data){ - if(!ev._defaultActions) ev._defaultActions = []; - ev._defaultActions.push({element: this, handler: origHandler, event: ev, data: data, currentTarget: ev.currentTarget}) - } }, setup: function() {return true} } // overwrite trigger to allow default types -var oldTrigger = $.event.trigger; -$.event.trigger = function defaultTriggerer( event, data, elem, bubbling){ - //always need to convert here so we know if we have default actions - var type = event.type || event +var oldTrigger = $event.trigger; - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[$.expando] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); +$event.trigger = function defaultTriggerer( event, data, elem, onlyHandlers){ + // Event object or event type + var type = event.type || event, + namespaces = [], - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } - event._defaultActions = []; //set depth for possibly reused events - } - - var defaultGetter = jQuery.Event("default."+event.type), - res; + // 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 ); - $.extend(defaultGetter,{ - target: elem, - _defaultActions: event._defaultActions, - exclusive : true - }); + //event._defaultActions = []; //set depth for possibly reused events - defaultGetter.stopPropagation(); + var res = oldTrigger.call($.event, event, data, elem, onlyHandlers); - //default events only work on elements - if(elem){ - oldTrigger.call($.event, defaultGetter, [defaultGetter, data], elem, true); - } - //fire old trigger, this will call back here - res = oldTrigger.call($.event, event, data, elem, bubbling); - - //fire if there are default actions to run && - // we have not prevented default && - // propagation has been stopped or we are at the document element - // we have reached the document - if (!event.isDefaultPrevented() && - event._defaultActions && - ( ( event.isPropagationStopped() ) || - ( !elem.parentNode && !elem.ownerDocument ) ) - - ) { - - // put event back - event.namespace= event.type; - event.type = "default"; - event.liveFired = null; - - // call each event handler - for(var i = 0 ; i < event._defaultActions.length; i++){ - var a = event._defaultActions[i], - oldHandle = event.handled; - event.currentTarget = a.currentTarget; - a.handler.call(a.element, event, a.data); - event.handled = event.handled === null ? oldHandle : true; - } - event._defaultActions = null; //set to null so everyone else on this element ignores it - } -} -/** - * @add jQuery.fn - */ -$.fn. -/** - * Triggers the event, stops the event from propagating through the DOM, and - * returns whether or not the event's default action was prevented. - * If true, the default action was not prevented. If false, the - * default action was prevented. This is the same as triggerDefaults, but - * the event doesn't bubble. Use these methods to easily determine if default was - * prevented, and proceed accordingly. - * - *

            Widget developers might use this method to perform additional logic if an event - * handler doesn't prevent the default action. For example, a tabs widget might - * hide the currently shown tab if the application developer doesn't prevent default.

            - * @param {Object} type The type of event to trigger. - * @param {Object} data Some data to pass to callbacks listening to this - * event. - */ -triggerDefault = function(type, data){ - if ( this[0] ) { - var event = $.Event( type ); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return !event.isDefaultPrevented(); + if(!onlyHandlers && !event.isDefaultPrevented() && event.type.indexOf("default") !== 0){ + oldTrigger("default."+event.type, data, elem) + if(event._success){ + event._success(event) + } } - return true; -} -$.fn. -/** - * Triggers the event and returns whether or not the event's - * default action was prevented. If true, the default action was not - * prevented. If false, the default action was prevented. This is the same - * as triggerDefault, but the event bubbles. Use these methods to easily determine if default was - * prevented, and proceed accordingly. - * @param {Object} type The type of event to trigger. - * @param {Object} data Some data to pass to callbacks listening to this - * event. - */ -triggerDefaults = function(type, data){ - if ( this[0] ) { - var event = $.Event( type ); - jQuery.event.trigger( event, data, this[0] ); - return !event.isDefaultPrevented(); + // code for paused + if( event.isPaused && event.isPaused() ){ + // set back original stuff + event.isDefaultPrevented = + event.pausedState.isDefaultPrevented; + event.isPropagationStopped = + event.pausedState.isPropagationStopped; } - return true; -} - - + return res; +}; diff --git a/model/associations/qunit.html b/event/default/default_pause_test.html similarity index 56% rename from model/associations/qunit.html rename to event/default/default_pause_test.html index 605cdfd6..dfc3f0a4 100644 --- a/model/associations/qunit.html +++ b/event/default/default_pause_test.html @@ -1,16 +1,17 @@ - + Default Test Suite + - + -

            associations Test Suite

            +

            Default Test Suite

            diff --git a/event/default/default_pause_test.js b/event/default/default_pause_test.js new file mode 100644 index 00000000..d61f42a6 --- /dev/null +++ b/event/default/default_pause_test.js @@ -0,0 +1,100 @@ +steal('funcunit/qunit','jquery/event/default','jquery/event/pause').then(function(){ + +module("jquery/event/default_pause"); + + +test("default and pause with delegate", function(){ + var order = []; + stop(); + $("#qunit-test-area").html("

            hello

            ") + + $("#foo").delegate("#bar","default.show", function(){ + order.push("default") + }); + + $("#foo").delegate("#bar","show", function(ev){ + order.push('show') + ev.pause(); + + setTimeout(function(){ + ev.resume(); + + setTimeout(function(){ + start(); + same(order,['show','default']) + },30) + + },50) + }); + + + $("#bar").trigger("show") + +}); + +test("default and pause with live", function(){ + $("#qunit-test-area").html("
            hello
            ") + + var order = []; + stop(); + + $("#foo").live("default.show", function(){ + order.push("default") + }); + $("#foo").live("show", function(ev){ + order.push('show') + ev.pause(); + setTimeout(function(){ + ev.resume(); + setTimeout(function(){ + start(); + same(order,['show','default']) + $("#foo").die("show"); + $("#foo").die("default.show"); + },30) + },50) + }); + + + $("#foo").trigger("show") + +}); + + +test("triggerAsync", function(){ + $("#qunit-test-area").html("
            hello
            ") + + var order = []; + stop(); + + $("#foo").live("default.show", function(){ + order.push("default") + }); + + $("#foo").live("show", function(ev){ + order.push('show') + ev.pause(); + setTimeout(function(){ + ev.resume(); + setTimeout(function(){ + start(); + $("#foo").die() + same(order,['show','default','async']) + },30) + },50) + }); + + + $("#foo").triggerAsync("show", function(){ + order.push("async") + }) +}); + +test("triggerAsync with nothing", function(){ + $("#fool").triggerAsync("show", function(){ + ok(true) + }) +}); + + +}); \ No newline at end of file diff --git a/event/default/test/qunit/default_test.js b/event/default/default_test.js similarity index 50% rename from event/default/test/qunit/default_test.js rename to event/default/default_test.js index 269af1c2..13d65c8f 100644 --- a/event/default/test/qunit/default_test.js +++ b/event/default/default_test.js @@ -1,3 +1,5 @@ +steal('funcunit/qunit','jquery/event/default').then(function(){ + module("jquery/event/default") test("namespaced with same function", function(){ @@ -13,9 +15,10 @@ test("namespaced with same function", function(){ test("triggering defaults", function(){ - $("#qunit-test-area").html("//jquery/event/default/test/qunit/html.micro",{}) - + $("#qunit-test-area").html( + "
            ClickMe
            "+ + "
            ClickMe
            ") var count1 = 0, defaultNum, touchNum, num = 0;; @@ -27,17 +30,17 @@ test("triggering defaults", function(){ touchNum = (++num) }) $("#touchme1").trigger("touch") - equals(1, count1, "trigger default event") - equals(1, touchNum, "default called second") - equals(2, defaultNum, "default called second") + equals(count1, 1 , "trigger default event") + equals(touchNum, 1, "default called second") + equals(defaultNum, 2, "default called second") + //now prevent + $("#bigwrapper").bind("touch", function(e){ e.preventDefault()}); - //now prevent + $("#touchme1").trigger("touch"); - $("#bigwrapper").bind("touch", function(e){ e.preventDefault()}) - $("#touchme1").trigger("touch") - equals(1, count1, "default event not called") + equals(count1, 1 , "default event not called again"); // breaking equals(3, touchNum, "touch called again") var count2 = 0; @@ -46,7 +49,6 @@ test("triggering defaults", function(){ }) $(document.body).bind("hide", function(ev){ if(ev.target.id == "clickme1"){ - console.log("stopping and preventing") ev.stopPropagation() ev.preventDefault() } @@ -64,7 +66,10 @@ test("triggering defaults", function(){ test("live on default events", function(){ - $("#qunit-test-area").html("//jquery/event/default/test/qunit/html.micro",{}) + $("#qunit-test-area").html( + + "
            ClickMe
            "+ + "
            ClickMe
            ") var bw = $("#bigwrapper"), count1 = 0, count2 = 0, @@ -101,4 +106,62 @@ test("live on default events", function(){ $("#qunit-test-area").html("") -}) +}); + + +test("default and live order", function(){ + var order = []; + $("#qunit-test-area").html("
            ") + + $("#foo").live("default.show", function(){ + order.push("default") + }); + $("#foo").live("show", function(){ + order.push("show") + }); + + $("#foo").trigger("show") + + same(order, ['show','default'],"show then default") + $("#foo").die() +}); + + +test("type on objects", function(){ + var ev = $.Event('updated'), + obj = {foo: 'bar'}; + + $(obj).trigger(ev) + + equals(ev.type, 'updated') +}); + +test("namespace on objects", function(){ + var ev = $.Event('updated.ns'), + obj = {foo: 'bar'}; + + $(obj).trigger(ev) + equals(ev.namespace, 'ns') +}); + + +test("default events with argument", function(){ + + $("#qunit-test-area").html( + "
            ") + + + var arg = "foobar", touchArg, defaultArg; + $("#touchme").bind("default.touch", function(e, data){ + defaultArg = data; + }) + $("#touchme").bind("touch", function(e, data){ + touchArg = data; + }) + $("#touchme").trigger("touch", arg) + equals(touchArg, arg, "standard event got args") + equals(defaultArg, arg, "default event got args") +}); + + +}); diff --git a/event/default/defaultjquery.html b/event/default/defaultjquery.html index c102d2ad..ebc3167f 100644 --- a/event/default/defaultjquery.html +++ b/event/default/defaultjquery.html @@ -47,10 +47,10 @@

            Default Events

            - - + + \ No newline at end of file diff --git a/event/default/qunit.html b/event/default/qunit.html index 207b0e31..13d29298 100644 --- a/event/default/qunit.html +++ b/event/default/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/event/default/test/qunit/html.micro b/event/default/test/qunit/html.micro deleted file mode 100644 index e10f1199..00000000 --- a/event/default/test/qunit/html.micro +++ /dev/null @@ -1,8 +0,0 @@ -
            -
            -
            ClickMe
            -
            -
            -
            ClickMe -
            -
            \ No newline at end of file diff --git a/event/default/test/qunit/qunit.js b/event/default/test/qunit/qunit.js deleted file mode 100644 index cf8ac90b..00000000 --- a/event/default/test/qunit/qunit.js +++ /dev/null @@ -1,6 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/event/default") //load your app - .plugins('funcunit/qunit','jquery/view/micro') //load qunit - .then("default_test") - diff --git a/event/destroyed/destroyed.html b/event/destroyed/destroyed.html index e486480c..297d6e30 100644 --- a/event/destroyed/destroyed.html +++ b/event/destroyed/destroyed.html @@ -10,15 +10,15 @@ Click here!
            - - - \ No newline at end of file diff --git a/event/destroyed/destroyed.js b/event/destroyed/destroyed.js index 9b2bc1d7..b5a230ee 100644 --- a/event/destroyed/destroyed.js +++ b/event/destroyed/destroyed.js @@ -1,11 +1,11 @@ /** * @add jQuery.event.special */ -steal.plugins('jquery/event').then(function( $ ) { +steal('jquery/event').then(function( $ ) { /** * @attribute destroyed * @parent specialevents - * @download jquery/dist/jquery.event.destroyed.js + * @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. *

            diff --git a/event/destroyed/test/qunit/destroyed_test.js b/event/destroyed/destroyed_test.js similarity index 77% rename from event/destroyed/test/qunit/destroyed_test.js rename to event/destroyed/destroyed_test.js index 17228983..4fb19a96 100644 --- a/event/destroyed/test/qunit/destroyed_test.js +++ b/event/destroyed/destroyed_test.js @@ -1,3 +1,7 @@ +steal("jquery/event/destroyed") //load your app + .then('funcunit/qunit') + .then(function(){ + module("jquery/event/destroyed") test("removing an element", function(){ var div = $("

            ").data("testData",5) @@ -9,4 +13,6 @@ test("removing an element", function(){ }) div.remove(); ok(destroyed, "destroyed called") -}) \ No newline at end of file +}); + +}); \ No newline at end of file diff --git a/event/destroyed/qunit.html b/event/destroyed/qunit.html index f16857d8..f0428050 100644 --- a/event/destroyed/qunit.html +++ b/event/destroyed/qunit.html @@ -6,7 +6,7 @@ margin: 0px; padding: 0px; } - + diff --git a/event/destroyed/test/qunit/qunit.js b/event/destroyed/test/qunit/qunit.js deleted file mode 100644 index 22bc1506..00000000 --- a/event/destroyed/test/qunit/qunit.js +++ /dev/null @@ -1,6 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/event/destroyed") //load your app - .plugins('funcunit/qunit') //load qunit - .then("destroyed_test") - diff --git a/event/drag/drag.html b/event/drag/drag.html index f9437d58..8ad03f3f 100644 --- a/event/drag/drag.html +++ b/event/drag/drag.html @@ -11,6 +11,9 @@ border: dashed 1px red; cursor : pointer; } + .revert { + float: left; + } .big { height: 100px; } @@ -30,6 +33,7 @@ width: 200px; height: 100px; overflow: auto; border: solid 2px black; } + h2 {clear: both;} @@ -47,7 +51,8 @@

            Drag Ghost

            Drag and I get cloned

            Drag Revert

            -
            Drag and let me go
            +
            Drag and let me go
            +
            Drag and let me go

            Limit Drag

            @@ -61,6 +66,10 @@

            Drag Representative

            Drag Horizontal

            I only move horizontal
            +

            Drag Distance

            +
            You have to move me 50 pixels
            + +

            Drag Scrolls

            I move scrollbars
            @@ -76,15 +85,10 @@

            Allow Text Selection

            - - \ No newline at end of file diff --git a/event/drag/drag.js b/event/drag/drag.js index 44048178..78f2dfd2 100644 --- a/event/drag/drag.js +++ b/event/drag/drag.js @@ -1,57 +1,62 @@ - -steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then(function($){ +steal('jquery/event', 'jquery/lang/vector', 'jquery/event/livehack',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, handle = event.handle; - + 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 jquery/dist/jquery.event.drag.js + * @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

            + * ## Drag Events + * * The drag plugin allows you to listen to the following events: + * *
              - *
            • dragdown - the mouse cursor is pressed down
            • + *
            • dragdown - the mouse cursor is pressed down
            • *
            • draginit - the drag motion is started
            • *
            • dragmove - the drag is moved
            • *
            • dragend - the drag has ended
            • *
            • dragover - the drag is over a drop point
            • *
            • dragout - the drag moved out of a drop point
            • *
            - *

            Just by binding or delegating on one of these events, you make + * + * 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

            + * + * ### Example + * * Here's a quick example: - * @codestart - * //makes the drag vertical - * $(".drags").live("draginit", function(event, drag){ - * drag.vertical(); - * }) - * //gets the position of the drag and uses that to set the width - * //of an element - * $(".resize").live("dragmove",function(event, drag){ - * $(this).width(drag.position.left() - $(this).offset().left ) - * }) - * @codeend - *

            Drag Object

            + * + * //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 + * *

            The drag object is passed after the event to drag * event callback functions. By calling methods * and changing the properties of the drag object, @@ -59,7 +64,7 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( *

            *

            The drag properties and methods:

            *
              - *
            • [jQuery.Drag.prototype.cancel cancel] - stops the drag motion from happening
            • + *
            • [jQuery.Drag.prototype.cancel cancel] - stops the drag motion from happening
            • *
            • [jQuery.Drag.prototype.ghost ghost] - copys the draggable and drags the cloned element
            • *
            • [jQuery.Drag.prototype.horizontal horizontal] - limits the scroll to horizontal movement
            • *
            • [jQuery.Drag.prototype.location location] - where the drag should be on the screen
            • @@ -77,37 +82,39 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( * @constructor * The constructor is never called directly. */ - $.Drag = function(){} - + $.Drag = function() {}; + /** * @Static */ - $.extend($.Drag, - { + $.extend($.Drag, { lowerName: "drag", - current : null, + 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 - + 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.liveFired || element, - selector = ev.handleObj.selector, - self = this; + var drag = new $.Drag(), + delegate = ev.delegateTarget || element, + selector = ev.handleObj.selector, + self = this; this.current = drag; drag.setup({ element: element, - delegate: ev.liveFired || 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), @@ -119,115 +126,170 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( destroyed: function() { self.current = null; } - }, ev) + }, ev); } - }) - - - - + }); /** * @Prototype */ - $.extend($.Drag.prototype , { + $.extend($.Drag.prototype, { setup: function( options, ev ) { - //this.noSelection(); - $.extend(this,options); + $.extend(this, options); this.element = $(this.element); this.event = ev; this.moved = false; this.allowOtherDrags = false; - var mousemove = bind(this, this.mousemove); - var mouseup = bind(this, this.mouseup); + var mousemove = bind(this, this.mousemove), + mouseup = bind(this, this.mouseup); this._mousemove = mousemove; this._mouseup = mouseup; - $(document).bind('mousemove' ,mousemove); - $(document).bind('mouseup',mouseup); + this._distance = options.distance ? options.distance : 0; + + this.mouseStartPosition = ev.vector(); //where the mouse is located - if(! this.callEvents('down',this.element, ev) ){ - ev.preventDefault(); + $(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() { + destroy: function() { $(document).unbind('mousemove', this._mousemove); $(document).unbind('mouseup', this._mouseup); - if(!this.moved){ + if (!this.moved ) { this.event = this.element = null; } - //this.selection(); + + this.selection(this.delegate); this.destroyed(); }, mousemove: function( docEl, ev ) { - if(!this.moved){ - this.init(this.element, ev) - this.moved= true; + 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)) { + if ( this._start_position && this._start_position.equals(pointer) ) { return; } //e.preventDefault(); - this.draw(pointer, ev); }, - mouseup: function( docEl,event ) { + + mouseup: function( docEl, event ) { //if there is a current, we should call its dragstop - if(this.moved){ + if ( this.moved ) { this.end(event); } this.destroy(); }, - noSelection: function() { - document.documentElement.onselectstart = function() { return false; }; - document.documentElement.unselectable = "on"; - $(document.documentElement).css('-moz-user-select', 'none'); + + /** + * 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: function() { - document.documentElement.onselectstart = function() { }; - document.documentElement.unselectable = "off"; - $(document.documentElement).css('-moz-user-select', ''); + + /** + * 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 + 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; - this.mouseStartPosition = event.vector(); //where the mouse is located + /** * @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.mouseElementPosition = this.mouseStartPosition.minus(this.element.offsetv()); //where the mouse is on the Element //this.callStart(element, event); - this.callEvents('init',element, event) - + this.callEvents('init', element, event); + //Check what they have set and respond accordingly // if they canceled - if(this._cancelled == true) return; + if ( this._cancelled === true ) { + return; + } //if they set something else as the element - this.startPosition = startElement != this.movingElement ? this.movingElement.offsetv() : this.currentDelta(); - - this.movingElement.makePositioned(); + + this.makePositioned(this.movingElement); this.oldZIndex = this.movingElement.css('zIndex'); - this.movingElement.css('zIndex',1000); - if(!this._only && this.constructor.responder) + 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 cbs = this.callbacks[this.constructor.lowerName+type]; - for(var i=0; i < cbs.length; i++){ - cbs[i].call(element, event, this, 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 + return cbs.length; }, /** * Returns the position of the movingElement by taking its top and left. @@ -235,63 +297,81 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( * @return {Vector} */ currentDelta: function() { - return new $.Vector( parseInt( this.movingElement.css('left') ) || 0 , - parseInt( this.movingElement.css('top') ) || 0 ) ; + 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; + 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 + 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 - this.move( event ); - if(this._cancelled) return; - if(!event.isDefaultPrevented()) + + // 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); + if (!this._only && this.constructor.responder ) { + this.constructor.responder.show(pointer, this, event); + } }, /** - * @hide - * Set the drag to only allow horizontal dragging. + * Sets the position of this drag. * - * @param {Object} offsetPositionv the position of the element (not the mouse) + * 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( offsetPositionv ) { //should draw it on the page - var dragged_element_page_offset = this.movingElement.offsetv(); // the drag element's current page location - - var dragged_element_css_offset = this.currentDelta(); // the drag element's current left + top css attributes - - var dragged_element_position_vector = // the vector between the movingElement's page and css positions - dragged_element_page_offset.minus(dragged_element_css_offset); // this can be thought of as the original offset - - this.required_css_position = offsetPositionv.minus(dragged_element_position_vector) - - + 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); - var style = this.movingElement[0].style; - if(!this._cancelled && !this._horizontal) { - style.top = this.required_css_position.top() + "px" + 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" + if (!this._cancelled && !this._vertical ) { + style.left = this.required_css_position.left() + "px"; } }, move: function( event ) { - this.callEvents('move',this.element, event) + this.callEvents('move', this.element, event); }, over: function( event, drop ) { - this.callEvents('over',this.element, event, drop) + this.callEvents('over', this.element, event, drop); }, out: function( event, drop ) { - this.callEvents('out',this.element, event, drop) + this.callEvents('out', this.element, event, drop); }, /** * Called on drag up @@ -299,25 +379,27 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( * @param {Event} event a mouseup event signalling drag/drop has completed */ end: function( event ) { - if(this._cancelled) return; - if(!this._only && this.constructor.responder) + 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.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; }, /** @@ -325,12 +407,20 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( * @hide */ cleanup: function() { - this.movingElement.css({zIndex: this.oldZIndex}); - if (this.movingElement[0] !== this.element[0]) - this.movingElement.css({ display: 'none' }); - if(this._removeMovingElement) + 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; }, /** @@ -339,10 +429,11 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( 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); + 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. @@ -350,13 +441,15 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( */ 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()) - + 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; }, @@ -366,12 +459,12 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( * @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 ){ + 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", @@ -379,8 +472,8 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( display: 'block', position: 'absolute' }).show(); - - this.mouseElementPosition = new $.Vector(this._offsetX, this._offsetY) + this.noSelection(this.movingElement) + this.mouseElementPosition = new $.Vector(this._offsetX, this._offsetY); }, /** * Makes the movingElement go back to its original position after drop. @@ -392,87 +485,94 @@ steal.plugins('jquery/event','jquery/lang/vector','jquery/event/livehack').then( * @param {Boolean} [val] optional, set to false if you don't want to revert. */ revert: function( val ) { - this._revert = val == null ? true : 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 - *

              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).

              - *

              Why might you not want it?

              - *

              You might want it if you want to allow text selection on element - * within the drag element. Typically these are input elements.

              - *

              Drag events are covered in more detail in [jQuery.Drag].

              - * @codestart - * $(".handles").live("dragdown", function(ev, drag){}) - * @codeend - */ - 'dragdown', - /** - * @attribute draginit - * Called when the drag starts. - *

              Drag events are covered in more detail in [jQuery.Drag].

              - */ - 'draginit', - /** - * @attribute dragover - * Called when the drag is over a drop. - *

              Drag events are covered in more detail in [jQuery.Drag].

              - */ - 'dragover', - /** - * @attribute dragmove - * Called when the drag is moved. - *

              Drag events are covered in more detail in [jQuery.Drag].

              - */ - 'dragmove', - /** - * @attribute dragout - * When the drag leaves a drop point. - *

              Drag events are covered in more detail in [jQuery.Drag].

              - */ - 'dragout', - /** - * @attribute dragend - * Called when the drag is done. - *

              Drag events are covered in more detail in [jQuery.Drag].

              - */ - 'dragend' - ], "mousedown", function(e){ - $.Drag.mousedown.call($.Drag, e, this) - - } ) - - - + event.setupHelper([ + /** + * @attribute dragdown + *

              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).

              + *

              Why might you not want it?

              + *

              You might want it if you want to allow text selection on element + * within the drag element. Typically these are input elements.

              + *

              Drag events are covered in more detail in [jQuery.Drag].

              + * @codestart + * $(".handles").delegate("dragdown", function(ev, drag){}) + * @codeend + */ + 'dragdown', + /** + * @attribute draginit + * Called when the drag starts. + *

              Drag events are covered in more detail in [jQuery.Drag].

              + */ + 'draginit', + /** + * @attribute dragover + * Called when the drag is over a drop. + *

              Drag events are covered in more detail in [jQuery.Drag].

              + */ + 'dragover', + /** + * @attribute dragmove + * Called when the drag is moved. + *

              Drag events are covered in more detail in [jQuery.Drag].

              + */ + 'dragmove', + /** + * @attribute dragout + * When the drag leaves a drop point. + *

              Drag events are covered in more detail in [jQuery.Drag].

              + */ + 'dragout', + /** + * @attribute dragend + * Called when the drag is done. + *

              Drag events are covered in more detail in [jQuery.Drag].

              + */ + 'dragend'], "mousedown", function( e ) { + $.Drag.mousedown.call($.Drag, e, this); + }); }); - - diff --git a/event/drag/test/qunit/drag_test.js b/event/drag/drag_test.js similarity index 79% rename from event/drag/test/qunit/drag_test.js rename to event/drag/drag_test.js index 4e8b8524..5c27d4d4 100644 --- a/event/drag/test/qunit/drag_test.js +++ b/event/drag/drag_test.js @@ -1,3 +1,8 @@ +steal("jquery/event/drop", + 'funcunit/qunit', + 'funcunit/syn') + .then("jquery/event/drop/drop_test.js",function(){ + module("jquery/event/drag",{ makePoints : function(){ var div = $("
              "+ @@ -24,8 +29,7 @@ test("dragging an element", function(){ "
              "+ "
              "+ "
              "); - - div.appendTo($("#qunit-test-area")); + $("#qunit-test-area").html(div); var basicCss = { width: "20px", height: "20px", @@ -181,9 +185,53 @@ test("dragdown" , function(){ var offset2 = $('#dragger').offset(); equals(offset.top+20, offset2.top, "top") equals(offset.left+20, offset2.left, "left") - ok(draginpfocused, "First input was allowed to be focused correctly"); + // IE doesn't respect preventDefault on text inputs (http://www.quirksmode.org/dom/events/click.html) + if(!$.browser.msie) + ok(draginpfocused, "First input was allowed to be focused correctly"); //ok(!dragnopreventfocused, "Second input was not allowed to focus"); start(); }) }) + +test("dragging child element (a handle)" , function(){ + var div = $("
              "+ + "
              "+ + "
              Place to drag
              "+ + "
              "+ + "
              "); + + $("#qunit-test-area").html(div); + $("#dragger").css({ + position: "absolute", + backgroundColor : "blue", + border: "solid 1px black", + top: "0px", + left: "0px", + width: "200px", + height: "200px" + }); + + var dragged = $('#dragged'); + + $('#dragger').bind("draginit", function(ev, drag){ + drag.only(); + drag.representative(dragged); + }) + + stop(); + + var offset = $('#dragger').offset(); + + Syn.drag("+20 +20","dragged", function() { + var offset2 = $('#dragger').offset(); + equals(offset.top, offset2.top, "top") + equals(offset.left, offset2.left, "left") + + ok(dragged.is(':visible'), "Handle should be visible"); + + start(); + }); +}); + +}); \ No newline at end of file diff --git a/event/drag/limit/limit.html b/event/drag/limit/limit.html index 604dbf74..134ebaa5 100644 --- a/event/drag/limit/limit.html +++ b/event/drag/limit/limit.html @@ -38,36 +38,34 @@
              handle
              handle
            - - + + diff --git a/event/drag/limit/limit.js b/event/drag/limit/limit.js index 81d1e7ed..8d7b703a 100644 --- a/event/drag/limit/limit.js +++ b/event/drag/limit/limit.js @@ -2,55 +2,68 @@ * @add jQuery.Drag.prototype */ -steal.plugins('jquery/event/drag').then(function($){ - - - $.Drag.prototype. +steal('jquery/event/drag', 'jquery/dom/cur_styles').then(function( $ ) { + + + $.Drag.prototype /** * @function limit * @plugin jquery/event/drag/limit - * @download jquery/dist/jquery.event.drag.limit.js + * @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){ + .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(), - size : container.dimensionsv() - } - } - + 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){ + $.Drag.prototype.position = function( offsetPositionv ) { //adjust required_css_position accordingly - if(this._limit){ - var movingSize = this.movingElement.dimensionsv(), - lot = this._limit.offset.top(), - lof = this._limit.offset.left() - height = this._limit.size.height(), - width = this._limit.size.width(); - + 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() < lot){ - offsetPositionv.top( lot ) + if ( offsetPositionv.top()+halfHeight < lot ) { + offsetPositionv.top(lot - halfHeight); } //below - if(offsetPositionv.top()+movingSize.height() > lot+ height){ - offsetPositionv.top( lot+ height - movingSize.height() ) + if ( offsetPositionv.top() + movingSize.height() - halfHeight > lot + height ) { + offsetPositionv.top(lot + height - movingSize.height() + halfHeight); } //left - if(offsetPositionv.left() < lof){ - offsetPositionv.left( lof ) + if ( offsetPositionv.left()+halfWidth < lof ) { + offsetPositionv.left(lof - halfWidth); } //right - if(offsetPositionv.left()+movingSize.width() > lof+ width){ - offsetPositionv.left( lof+ width - movingSize.left() ) + if ( offsetPositionv.left() + movingSize.width() -halfWidth > lof + width ) { + offsetPositionv.left(lof + width - movingSize.left()+halfWidth); } } - - oldPosition.call(this, offsetPositionv) - } - -}) \ No newline at end of file + + oldPosition.call(this, offsetPositionv); + }; + +}); \ No newline at end of file diff --git a/event/drag/qunit.html b/event/drag/qunit.html index 4ffe9031..484912f6 100644 --- a/event/drag/qunit.html +++ b/event/drag/qunit.html @@ -11,6 +11,6 @@

              - + \ No newline at end of file diff --git a/event/drag/scroll/scroll.js b/event/drag/scroll/scroll.js index 2a6b6b5a..cd530a66 100644 --- a/event/drag/scroll/scroll.js +++ b/event/drag/scroll/scroll.js @@ -1,4 +1,4 @@ -steal.plugins("jquery/event/drop").then(function($){ //needs drop to determine if respondable +steal("jquery/event/drop").then(function($){ //needs drop to determine if respondable /** * @add jQuery.Drag.prototype @@ -7,17 +7,36 @@ $.Drag.prototype. /** * Will scroll elements with a scroll bar as the drag moves to borders. * @plugin jquery/event/drag/scroll - * @download jquery/dist/jquery.event.drag.scroll.js + * @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){ + scrolls = function(elements, options){ + var elements = $(elements); + for(var i = 0 ; i < elements.length; i++){ - this.constructor.responder._responders.push( new $.Scrollable(elements[i]) ) + this.constructor.responder._elements.push( elements.eq(i).data("_dropData", new $.Scrollable(elements[i], options) )[0] ) } }, -$.Scrollable = function(element){ +$.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 ) { @@ -60,7 +79,7 @@ $.extend($.Scrollable.prototype,{ location_object = $(el == document.documentElement ? window : el), //get the dimension and location of that object - dimensions = location_object.dimensionsv(), + dimensions = location_object.dimensionsv('outer'), position = location_object.offsetv(), //how close our mouse is to the boundries @@ -70,18 +89,18 @@ $.extend($.Scrollable.prototype,{ left = mouse.x() - position.x(), //how far we should scroll - dx =0, dy =0; + dx =0, dy =0, + distance = this.options.distance; - //check if we should scroll - if(bottom < 30) - dy = this.distance(bottom); - else if(top < 30) - dy = -this.distance(top) - if(right < 30) - dx = this.distance(right); - else if(left < 30) - dx = -this.distance(left); + 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){ diff --git a/event/drag/step/step.html b/event/drag/step/step.html index 43b6991b..664375fc 100644 --- a/event/drag/step/step.html +++ b/event/drag/step/step.html @@ -9,8 +9,8 @@ .error_text { color: red; font-size: 10px;} td {padding: 3px;} .handle { - width: 300px; - height: 40px; + width: 98px; + height: 38px; border: dashed 1px red; cursor : pointer; } @@ -23,33 +23,44 @@ margin-top: 50px; } #ondoc { - border: solid 5px red; - padding: 20px; - height: 300px; - width: 600px; - margin: 10px; + border: solid 5px green; + padding: 0px; + height: 240px; + width: 420px; + + } + #demo-html{ + padding: 30px 0px 0px 30px; } - -
              -
              handle
              -
              handle
              -
              - - +
              +
              +
              top left
              +
              horizontal
              +
              vertical
              +
              horizontal vertical
              +
              +
              + diff --git a/event/drag/step/step.js b/event/drag/step/step.js index b1dfd7f9..bece565d 100644 --- a/event/drag/step/step.js +++ b/event/drag/step/step.js @@ -2,58 +2,76 @@ * @add jQuery.Drag.prototype */ -steal.plugins('jquery/event/drag','jquery/dom/cur_styles').then(function($){ - var round = function(x, m){ +steal('jquery/event/drag', 'jquery/dom/cur_styles').then(function( $ ) { + var round = function( x, m ) { return Math.round(x / m) * m; } - + $.Drag.prototype. /** * @function step * @plugin jquery/event/drag/step - * @download jquery/dist/jquery.event.drag.step.js - * makes the drag move in steps - * @codestart - * drag.step({x: 5}, $('foo')) - * @codeend - * @param {number} amount - * @param {jQuery} container + * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/drag/step/step.js + * makes the drag move in steps of amount pixels. + * + * drag.step({x: 5}, $('foo'), "xy") + * + * ## Demo + * + * @demo jquery/event/drag/step/step.html + * + * @param {number|Object} amount make the drag move X amount in pixels from the top-left of container. + * @param {jQuery} [container] the container to move in reference to. If not provided, the document is used. + * @param {String} [center] Indicates how to position the drag element in relationship to the container. + * + * - If nothing is provided, places the top left corner of the drag element at + * 'amount' intervals from the top left corner of the container. + * - If 'x' is provided, it centers the element horizontally on the top-left corner. + * - If 'y' is provided, it centers the element vertically on the top-left corner of the container. + * - If 'xy' is provided, it centers the element on the top-left corner of the container. + * + * @return {jQuery.Drag} the drag object for chaining. */ - step = function(amount, container){ + step = function( amount, container, center ) { //on draws ... make sure this happens - - if(typeof amount == 'number'){ - amount = {x: amount, y:amount} + if ( typeof amount == 'number' ) { + amount = { + x: amount, + y: amount + } } container = container || $(document.body); this._step = amount; - - var styles = container.curStyles("borderTopWidth","paddingTop","borderLeftWidth","paddingLeft"); - var left = parseInt( styles.borderTopWidth ) + parseInt( styles.paddingTop ), - top = parseInt( styles.borderLeftWidth ) + parseInt( styles.paddingLeft ); - - this._step.offset = container.offsetv().plus(left, top); + + var styles = container.curStyles("borderTopWidth", "paddingTop", "borderLeftWidth", "paddingLeft"); + var top = parseInt(styles.borderTopWidth) + parseInt(styles.paddingTop), + left = parseInt(styles.borderLeftWidth) + parseInt(styles.paddingLeft); + + this._step.offset = container.offsetv().plus(left, top); + this._step.center = center; + return this; }; - - + + var oldPosition = $.Drag.prototype.position; - $.Drag.prototype.position = function(offsetPositionv){ + $.Drag.prototype.position = function( offsetPositionv ) { //adjust required_css_position accordingly - if(this._step){ - var movingSize = this.movingElement.dimensionsv(), - lot = this._step.offset.top(), - lof = this._step.offset.left(); - - //console.log(round(offsetPositionv.left() - lof, this._step.x)) - if(this._step.x){ - offsetPositionv.left( lof + round(offsetPositionv.left() - lof, this._step.x) ) + if ( this._step ) { + var step = this._step, + center = step.center && step.center.toLowerCase(), + movingSize = this.movingElement.dimensionsv('outer'), + lot = step.offset.top()- (center && center != 'x' ? movingSize.height() / 2 : 0), + lof = step.offset.left() - (center && center != 'y' ? movingSize.width() / 2 : 0); + + if ( this._step.x ) { + offsetPositionv.left(Math.round(lof + round(offsetPositionv.left() - lof, this._step.x))) } - if(this._step.y){ - offsetPositionv.top( lot + round(offsetPositionv.top() - lot, this._step.y) ) + if ( this._step.y ) { + offsetPositionv.top(Math.round(lot + round(offsetPositionv.top() - lot, this._step.y))) } } - + oldPosition.call(this, offsetPositionv) } - + }) \ No newline at end of file diff --git a/event/drag/test/qunit/qunit.js b/event/drag/test/qunit/qunit.js deleted file mode 100644 index d6804a86..00000000 --- a/event/drag/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/event/drop",'funcunit/synthetic') //load your app - .plugins('funcunit/qunit' ) //load qunit - .then("drag_test") \ No newline at end of file diff --git a/event/drop/drop.html b/event/drop/drop.html index 9a807aa9..439f11f1 100644 --- a/event/drop/drop.html +++ b/event/drop/drop.html @@ -40,35 +40,36 @@

              Dropmove/Dropon

              Drop Count 0
              - - + + \ No newline at end of file diff --git a/event/drop/drop.js b/event/drop/drop.js index d6f7dbda..792c860a 100644 --- a/event/drop/drop.js +++ b/event/drop/drop.js @@ -1,8 +1,5 @@ -steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then(function($){ - var event = $.event, - callHanders = function(){ - - }; +steal('jquery/event/drag','jquery/dom/within','jquery/dom/compare',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 // /** @@ -52,7 +49,7 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then * @class jQuery.Drop * @parent specialevents * @plugin jquery/event/drop - * @download jquery/dist/jquery.event.drop.js + * @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. @@ -72,19 +69,39 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then *

              Examples

              * Here's how to listen for when a drag moves over a drop: * @codestart - * $('.drop').live("dropover", function(ev, drop, drag){ + * $('.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; + this.element = $(element); } + // add the elements ... $.each(eventNames, function(){ event.special[this] = { add: function( handleObj ) { @@ -103,24 +120,27 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then } } } - }) + }); + $.extend($.Drop,{ lowerName: "drop", - _elements: [], //elements that are listening for drops - _responders: [], //potential drop points + _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._elements.length ; i++ ){ - if(el ==this._elements[i]) return; + for(var i =0; i < this._rootElements.length ; i++ ){ + if(el ==this._rootElements[i]) return; } - this._elements.push(el); + this._rootElements.push(el); }, removeElement: function( el ) { - for(var i =0; i < this._elements.length ; i++ ){ - if(el == this._elements[i]){ - this._elements.splice(i,1) + for(var i =0; i < this._rootElements.length ; i++ ){ + if(el == this._rootElements[i]){ + this._rootElements.splice(i,1) return; } } @@ -140,7 +160,7 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then * 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).length == 1)); + return ((responder.element != moveable.element) && (responder.element.within(point[0], point[1], responder._cache).length == 1)); }, /** * @hide @@ -169,60 +189,156 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then responder.callHandlers(this.lowerName+'move',responder.element[0], event, mover) }, /** - * Gets all elements that are droppable, adds them + * 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 ) { - var el, drops, selector, sels; - this.last_active = []; - for(var i=0; i < this._elements.length; i++){ //for each element - el = this._elements[i] + // 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) - for(selector in drops){ //find the selectors - sels = selector ? jQuery(selector, el) : [el]; - for(var e= 0; e < sels.length; e++){ //for each found element, create a drop point - jQuery.removeData(sels[e],"offset"); - this.add(sels[e], new this(drops[selector]), event, drag); + // 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; + } }, - add: function( element, callbacks, event, drag ) { - element = jQuery(element); - var responder = new $.Drop(callbacks, element); - responder.callHandlers(this.lowerName+'init', element[0], event, drag) - if(!responder._canceled){ - this._responders.push(responder); + // 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._responders.length) return; + if(!this._elements.length) return; var respondable, affected = [], propagate = true, - i,j, la, toBeActivated, aff, - oldLastActive = this.last_active; + i = 0, + j, + la, + toBeActivated, + aff, + oldLastActive = this.last_active, + responders = [], + self = this, + drag; - for(var d =0 ; d < this._responders.length; d++ ){ + //what's still affected ... we can also move element out here + while( i < this._elements.length){ + drag = $.data(this._elements[i],"_dropData"); - if(this.isAffected(point, moveable, this._responders[d])){ - affected.push(this._responders[d]); + 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; } - //deactivate everything in last_active that isn't active + 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] + la = oldLastActive[j]; i = 0; while((aff = toBeActivated[i])){ if(la == aff){ @@ -249,10 +365,11 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then } }, end: function( event, moveable ) { - var responder, la; - for(var r =0; r< this.last_active.length; i++){ la = this.last_active[i] @@ -260,8 +377,12 @@ steal.plugins('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then la.callHandlers(this.endName, null, event, moveable); } } - - + // call dropend + for(var r =0; r"+ + "
              "+ + "
              "+ + "
              "+ + "
              "); + + div.appendTo($("#qunit-test-area")); + var basicCss = { + width: "20px", + height: "20px", + position: "absolute", + border: "solid 1px black" + } + $("#drag").css(basicCss).css({top: "0px", left: "0px", zIndex: 1000, backgroundColor: "red"}) + $("#midpoint").css(basicCss).css({top: "0px", left: "30px"}) + $("#drop").css(basicCss).css({top: "0px", left: "60px"}); + + $('#drag').bind("draginit", function(){}); + + $("#midpoint").bind("dropover",function(){ + ok(true, "midpoint called"); + + $("#drop").bind("dropover", function(){ + ok(true, "drop called"); + }); + $.Drop.compile(); + }); + stop(); + Syn.drag({to: "#drop"},"drag", function(){ + start(); + }); +}); + + + +}) diff --git a/event/event.js b/event/event.js index 42422936..c4ed299b 100644 --- a/event/event.js +++ b/event/event.js @@ -1,6 +1,42 @@ /** * @page specialevents Special Events - * @tag core - * JavaScriptMVC adds a bunch of useful jQuery extensions for the dom. Check them out on the left. + * @parent jquerymx + * @description Special events like drag-drop. + * + * JavaScriptMVC provides a bunch of useful special events. Find out more info on the left. The following is a + * brief summary: + * + * ## [jQuery.event.special.default Default Events] + * + * Lets you supply default behavior for an event that is preventable + * with event.preventDefault(). This is extremely useful for providing DOM-like api's for your widgets. + * + * $("#tabs").delegate(".panel","default.open", function(){ + * $(this).show() + * }) + * + * ## [jQuery.event.special.destroyed Destroyed Events] + * + * Know if an element has been removed from the page. + * + * $("#contextMenu").bind("destroyed", function(){ + * // cleanup + * $(document.body).unbind("click.contextMenu"); + * }) + * + * ## [jQuery.Drag Drag] and [jQuery.Drop Drop] Events + * + * Listen to drag-drop events with event delegation. + * + * $(".item").live("dragover", function(ev, drag){ + * // let user know that the item can be dropped + * $(this).addClass("canDrop"); + * }).live("dropover", function(ev, drop, drag){ + * // let user know that the item can be dropped on + * $(this).addClass('drop-able') + * }) + * + * ## + * */ -steal.plugins('jquery'); \ No newline at end of file +steal('jquery'); \ No newline at end of file diff --git a/event/handle/handle.js b/event/handle/handle.js new file mode 100644 index 00000000..49377f8e --- /dev/null +++ b/event/handle/handle.js @@ -0,0 +1,207 @@ +steal("jquery").then(function(){ + +var $event = $.event, + oldTrigger = $event.trigger, + isElement = function(o){ + return ( + typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2 + typeof o === "object" && o.nodeType === 1 && typeof o.nodeName==="string" + ) || (o === window) || (o === document); + }; +$.event.trigger = function(event, data, elem, onlyHandlers){ + // Event object or event type + var type = event.type || event, + namespaces = [], + exclusive; + + if ( type.indexOf("!") >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // 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.type = type; + event.exclusive = exclusive; + event.namespace = namespaces.join("."); + event.namespace_re = new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)"); + + // triggerHandler() and global events don't bubble or run the default action + if ( onlyHandlers || !elem ) { + event.preventDefault(); + event.stopPropagation(); + } + + // Handle a global trigger + if ( !elem ) { + // TODO: Stop taunting the data cache; remove global events and always attach to document + jQuery.each( jQuery.cache, function() { + // internalKey variable is just used to make it easier to find + // and potentially change this stuff later; currently it just + // points to jQuery.expando + var internalKey = jQuery.expando, + internalCache = this[ internalKey ]; + if ( internalCache && internalCache.events && internalCache.events[ type ] ) { + jQuery.event.trigger( event, data, internalCache.handle.elem ); + } + }); + return; + } + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + event.target = elem; + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + var cur = elem, + // IE doesn't like method names with a colon (#3533, #8272) + ontype = type.indexOf(":") < 0 ? "on" + type : ""; + + // Fire event on the current element, then bubble up the DOM tree + do { + var handle = jQuery._data( cur, "handle" ); + + event.currentTarget = cur; + if ( handle ) { + handle.apply( cur, data ); + } + + // Trigger an inline bound script + if ( ontype && jQuery.acceptData( cur ) && cur[ ontype ] && cur[ ontype ].apply( cur, data ) === false ) { + event.result = false; + event.preventDefault(); + } + + // Bubble up to document, then to window + cur = cur.parentNode || cur.ownerDocument || cur === event.target.ownerDocument && window; + } while ( cur && !event.isPropagationStopped() ); + + // If nobody prevented the default action, do it now + if ( !event.isDefaultPrevented() ) { + var old, + special = jQuery.event.special[ type ] || {}; + + if ( (!special._default || special._default.call( elem.ownerDocument, event ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction)() check here because IE6/7 fails that test. + // IE<9 dies on focus to hidden element (#1486), may want to revisit a try/catch. + try { + if ( ontype && elem[ type ] ) { + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + jQuery.event.triggered = type; + elem[ type ](); + } + } catch ( ieError ) {} + + if ( old ) { + elem[ ontype ] = old; + } + + jQuery.event.triggered = undefined; + } + } + + return event.result; +} +// a copy of $'s handle function that goes until it finds +$.event.handle = function( event ) { + + event = jQuery.event.fix( event || window.event ); + // Snapshot the handlers list since a called handler may add/remove events. + var handlers = ((jQuery._data( this, "events" ) || {})[ event.type ] || []).slice(0), + run_all = !event.exclusive && !event.namespace, + args = Array.prototype.slice.call( arguments, 0 ); + + // Use the fix-ed Event rather than the (read-only) native event + args[0] = event; + event.currentTarget = this; + + // JMVC CHANGED + var oldType = event.type, + // run if default is included + runDefault = event.type !== "default" && $event.special['default'] && + // and its not an original event + !event.originalEvent && + // and its an element + isElement(event.target); + if (runDefault) { + $event.special['default'].triggerDefault(event, this, args[1]); + } + event.type = oldType; + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + if( event.firstPass ){ + event.firstPass = false; + continue; + } + + // Triggered event must 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event. + if ( run_all || event.namespace_re.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, args ); + + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + + // JMVC CHANGED + if (runDefault) { + $event.special['default'].checkAndRunDefaults(event, this); + } + return event.result; +} +}) diff --git a/event/hashchange/hashchange.js b/event/hashchange/hashchange.js index fc01306c..2dcea54b 100644 --- a/event/hashchange/hashchange.js +++ b/event/hashchange/hashchange.js @@ -67,8 +67,32 @@ // who want just the basic event & back button support, without all the // extra awesomeness that BBQ provides. This plugin will be included as // part of jQuery BBQ, but also be available separately. - -(function($,window,undefined){ +/** + * @add jQuery.event.special + */ +// +/** + * @attribute hashchange + * + * Documentation on JMVC's hashchange event can + * be found at [http://benalman.com/projects/jquery-hashchange-plugin/]. + * + * ### Examples + * + * // with jQuery + * $(window).bind('hashchange', function(){ + * // do something + * }) + * + * // with $.Controller + * $.Controller('Nav',{ + * "{window} hashchange" : function(){ + * // do something + * } + * }) + * + */ +steal('jquery').then(function($){ '$:nomunge'; // Used by YUI compressor. // Method / object references. @@ -241,5 +265,4 @@ return self; })(); - -})(jQuery,this); \ No newline at end of file +}) \ No newline at end of file diff --git a/event/hover/hover.html b/event/hover/hover.html index a128aa45..d98a8894 100644 --- a/event/hover/hover.html +++ b/event/hover/hover.html @@ -8,8 +8,9 @@ .error {border: solid 1px red;} .error_text { color: red; font-size: 10px;} td {padding: 3px;} - .hover, .hovers { + .hover, .hovers, .hoverleave { border: solid 1px green; + margin:5px; } .hoverstate { background-color: yellow; @@ -27,30 +28,43 @@

              Delegating

              Bound Directly

              hover me for a second
              hover me for a second
              +

              HoverLeave

              +
              Leave and don't return for a half second
              - - + + \ No newline at end of file diff --git a/event/hover/hover.js b/event/hover/hover.js index 1eca9dfe..2cf9f671 100644 --- a/event/hover/hover.js +++ b/event/hover/hover.js @@ -1,8 +1,8 @@ -steal.plugins('jquery/event','jquery/event/livehack').then(function($){ +steal('jquery/event','jquery/event/livehack').then(function($){ /** * @class jQuery.Hover * @plugin jquery/event/hover - * @download jquery/dist/jquery.event.hover.js + * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/event/hover/hover.js * Provides delegate-able hover events. *

              * A hover happens when the mouse stops moving @@ -46,7 +46,7 @@ steal.plugins('jquery/event','jquery/event/livehack').then(function($){ *

              Or you can adjust delay and distance for * an individual element in hoverenter:

              * @codestart - * $(".option").live("hoverinit", function(ev, hover){ + * $(".option").delegate("hoverinit", function(ev, hover){ * //set the distance to 10px * hover.distance(10) * //set the delay to 200ms @@ -59,14 +59,15 @@ steal.plugins('jquery/event','jquery/event/livehack').then(function($){ * @constructor Creates a new hover. This is never * called directly. */ -jQuery.Hover = function(){ - this._delay = jQuery.Hover.delay; - this._distance = jQuery.Hover.distance; +$.Hover = function(){ + this._delay = $.Hover.delay; + this._distance = $.Hover.distance; + this._leave = $.Hover.leave }; /** * @Static */ -$.extend(jQuery.Hover,{ +$.extend($.Hover,{ /** * @attribute delay * A hover is activated if it moves less than distance in this time. @@ -78,13 +79,14 @@ $.extend(jQuery.Hover,{ * A hover is activated if it moves less than this distance in delay time. * Set this value as a global default. */ - distance: 10 + distance: 10, + leave : 0 }) /** * @Prototype */ -$.extend(jQuery.Hover.prototype,{ +$.extend($.Hover.prototype,{ /** * Sets the delay for this hover. This method should * only be used in hoverinit. @@ -93,6 +95,7 @@ $.extend(jQuery.Hover.prototype,{ */ delay: function( delay ) { this._delay = delay; + return this; }, /** * Sets the distance for this hover. This method should @@ -101,60 +104,89 @@ $.extend(jQuery.Hover.prototype,{ */ distance: function( distance ) { this._distance = distance; + return this; + }, + leave : function(leave){ + this._leave = leave; + return this; } }) -var $ = jQuery, - event = jQuery.event, +var event = $.event, handle = event.handle, onmouseenter = function(ev){ //now start checking mousemoves to update location - var delegate = ev.liveFired || ev.currentTarget; + 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, - entered = this, - called = false, + enteredEl = this, + hovered = false, lastEv = ev, - hover = new jQuery.Hover(); - - $(entered).bind("mousemove.specialMouseEnter", {}, function(ev){ - dist += Math.pow( ev.pageX-loc.pageX, 2 ) + Math.pow( ev.pageY-loc.pageY, 2 ); - loc = { - pageX : ev.pageX, - pageY : ev.pageY - } - lastEv = ev - }).bind("mouseleave.specialMouseLeave",{}, function(ev){ - clearTimeout(timer); - if(called){ + hover = new $.Hover(), + leaveTimer, + callHoverLeave = function(){ $.each(event.find(delegate, ["hoverleave"], selector), function(){ - this.call(entered, ev) + this.call(enteredEl, ev, hover) }) - } - $(entered).unbind("mouseleave.specialMouseLeave") - }) + 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(entered, ev, hover) + this.call(enteredEl, ev, hover) }) + timer = setTimeout(function(){ //check that we aren't moveing around - if(dist < hover._distance && $(entered).queue().length == 0){ + if(dist < hover._distance && $(enteredEl).queue().length == 0){ $.each(event.find(delegate, ["hoverenter"], selector), function(){ - this.call(entered, lastEv, hover) + this.call(enteredEl, lastEv, hover) }) - called = true; - $(entered).unbind("mousemove.specialMouseEnter") - + hovered = true; + return; }else{ dist = 0; timer = setTimeout(arguments.callee, hover._delay) } - - }, hover._delay) }; @@ -169,7 +201,7 @@ event.setupHelper( [ * [jQuery.Hover.prototype.delay] and [jQuery.Hover.prototype.distance] * for the current element. Hoverinit is called on mouseenter. * @codestart - * $(".option").live("hoverinit", function(ev, hover){ + * $(".option").delegate("hoverinit", function(ev, hover){ * //set the distance to 10px * hover.distance(10) * //set the delay to 200ms @@ -184,7 +216,7 @@ event.setupHelper( [ * than [jQuery.Hover.prototype.distance] pixels in * [jQuery.Hover.prototype.delay] milliseconds. * @codestart - * $(".option").live("hoverenter", function(ev, hover){ + * $(".option").delegate("hoverenter", function(ev, hover){ * $(this).addClass("hovering"); * }) * @codeend @@ -195,7 +227,7 @@ event.setupHelper( [ * Called when the mouse leaves an element that has been * hovered. * @codestart - * $(".option").live("hoverleave", function(ev, hover){ + * $(".option").delegate("hoverleave", function(ev, hover){ * $(this).removeClass("hovering"); * }) * @codeend @@ -206,7 +238,7 @@ event.setupHelper( [ * Called when the mouse moves on an element that * has been hovered. * @codestart - * $(".option").live("hovermove", function(ev, hover){ + * $(".option").delegate("hovermove", function(ev, hover){ * //not sure why you would want to listen for this * //but we provide it just in case * }) diff --git a/event/hover/test/qunit/hover_test.js b/event/hover/hover_test.js similarity index 91% rename from event/hover/test/qunit/hover_test.js rename to event/hover/hover_test.js index 5b0a2b25..bd58d1f1 100644 --- a/event/hover/test/qunit/hover_test.js +++ b/event/hover/hover_test.js @@ -1,3 +1,7 @@ +steal("jquery/event/hover",'funcunit/syn') //load your app + .then('funcunit/qunit') + .then(function(){ + module("jquery/dom/hover") test("hovering", function(){ @@ -23,7 +27,7 @@ test("hovering", function(){ Syn("mouseover",{pageX: off.top, pageY: off.left}, hover[0]) ok(hoverinits, 'hoverinit'); ok(hoverenters === 0,"hoverinit hasn't been called"); - stop(1000); + stop(); setTimeout(function(){ ok(hoverenters === 1,"hoverenter has been called"); @@ -52,4 +56,6 @@ test("hovering", function(){ },10) },30) -}) +}); + +}); diff --git a/event/hover/qunit.html b/event/hover/qunit.html index 14b67eaa..27ca459a 100644 --- a/event/hover/qunit.html +++ b/event/hover/qunit.html @@ -1,7 +1,7 @@ - + diff --git a/event/hover/test/qunit/qunit.js b/event/hover/test/qunit/qunit.js deleted file mode 100644 index 76d51ade..00000000 --- a/event/hover/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/event/hover",'funcunit/synthetic') //load your app - .plugins('funcunit/qunit') //load qunit - .then("hover_test") \ No newline at end of file diff --git a/event/key/key.html b/event/key/key.html new file mode 100644 index 00000000..a2cda5bb --- /dev/null +++ b/event/key/key.html @@ -0,0 +1,32 @@ + + + + jQuery Event Key + + + +
              + +
              Keydown:
              +
              Keypress:
              +
              Keyup:
              +
              + + + \ No newline at end of file diff --git a/event/key/key.js b/event/key/key.js new file mode 100644 index 00000000..121d40d3 --- /dev/null +++ b/event/key/key.js @@ -0,0 +1,160 @@ +steal('jquery/event').then(function($){ + var keymap = {}, + reverseKeyMap = {}; + + /** + * @function jQuery.event.key + * @parent jQuery.Event.prototype.key + * + * Allows you to set alternate key maps or overwrite existing key codes. + * For example:: + * + * $.event.key({"~" : 177}); + * + * @param {Object} map A map of character - keycode pairs. + */ + $.event.key = function(map){ + $.extend(keymap, map); + for(var name in map){ + reverseKeyMap[map[name]] = name; + } + }; + + $.event.key({ + //backspace + '\b':'8', + + //tab + '\t':'9', + + //enter + '\r':'13', + + //special + 'shift':'16','ctrl':'17','alt':'18', + + //weird + 'pause-break':'19', + 'caps':'20', + 'escape':'27', + 'num-lock':'144', + 'scroll-lock':'145', + 'print' : '44', + + //navigation + 'page-up':'33','page-down':'34','end':'35','home':'36', + 'left':'37','up':'38','right':'39','down':'40','insert':'45','delete':'46', + + //normal characters + ' ':'32', + '0':'48','1':'49','2':'50','3':'51','4':'52','5':'53','6':'54','7':'55','8':'56','9':'57', + 'a':'65','b':'66','c':'67','d':'68','e':'69','f':'70','g':'71','h':'72','i':'73','j':'74','k':'75','l':'76','m':'77', + 'n':'78','o':'79','p':'80','q':'81','r':'82','s':'83','t':'84','u':'85','v':'86','w':'87','x':'88','y':'89','z':'90', + //normal-characters, numpad + 'num0':'96','num1':'97','num2':'98','num3':'99','num4':'100','num5':'101','num6':'102','num7':'103','num8':'104','num9':'105', + '*':'106','+':'107','-':'109','.':'110', + //normal-characters, others + '/':'111', + ';':'186', + '=':'187', + ',':'188', + '-':'189', + '.':'190', + '/':'191', + '`':'192', + '[':'219', + '\\':'220', + ']':'221', + "'":'222', + + //ignore these, you shouldn't use them + 'left window key':'91','right window key':'92','select key':'93', + + + 'f1':'112','f2':'113','f3':'114','f4':'115','f5':'116','f6':'117', + 'f7':'118','f8':'119','f9':'120','f10':'121','f11':'122','f12':'123' + }); + + /** + * @parent specialevents + * @plugin jquery/event/key + * + * Returns a string representation of the key pressed. The following + * listens to and prevents backspaces being pressed in inputs: + * + * $("input").keypress(function(ev){ + * if(ev.keyName() == '\b') { + * ev.preventDefault(); + * } + * }); + * + * ## Keys + * + * The following describes the key values returned by [jQuery.Event.prototype.key]. + * + * - \b - backspace + * - \t - tab + * - \r - enter key + * - shift, ctrl, alt + * - pause-break, caps, escape, num-lock, scroll-loc, print + * - page-up, page-down, end, home, left, up, right, down, insert, delete + * - ' ' - space + * - 0-9 - number key pressed + * - a-z - alpha key pressed + * - num0-9 - number pad key pressed + * - / ; : = , - . / ` [ \\ ] ' " + * - f1-12 - function keys pressed + * + * ## Alternate keys + * + * Use [jQuery.event.key] to set alternate key mappings for other locales. + * + * @return {String} The string representation of of the key pressed. + */ + jQuery.Event.prototype.keyName = function(){ + var event = this, + keycode, + test = /\w/; + + var key_Key = reverseKeyMap[(event.keyCode || event.which)+""], + char_Key = String.fromCharCode(event.keyCode || event.which), + key_Char = event.charCode && reverseKeyMap[event.charCode+""], + char_Char = event.charCode && String.fromCharCode(event.charCode); + + if( char_Char && test.test(char_Char) ) { + return char_Char.toLowerCase() + } + if( key_Char && test.test(key_Char) ) { + return char_Char.toLowerCase() + } + if( char_Key && test.test(char_Key) ) { + return char_Key.toLowerCase() + } + if( key_Key && test.test(key_Key) ) { + return key_Key.toLowerCase() + } + + //if IE + //if ($.browser.msie){ + if (event.type == 'keypress'){ + return event.keyCode ? String.fromCharCode(event.keyCode) : String.fromCharCode(event.which) + } /*else if (event.type == 'keydown') { + // IE only recognizes the backspace and delete keys in the keydown event, not keypress + keycode = reverseKeyMap[event.keyCode]; + + if (keycode === '\b' || keycode === 'delete'){ + return keycode; + } + } */ + //} + + + if (!event.keyCode && event.which) { + return String.fromCharCode(event.which) + } + + return reverseKeyMap[event.keyCode+""] + } + + +}) \ No newline at end of file diff --git a/event/key/key_test.js b/event/key/key_test.js new file mode 100644 index 00000000..60bed3ca --- /dev/null +++ b/event/key/key_test.js @@ -0,0 +1,26 @@ +steal('funcunit/qunit','funcunit/syn','jquery/event/key').then(function(){ + +module('jquery/event/key'); + +test("type some things", function(){ + $("#qunit-test-area").append("") + var keydown, keypress, keyup; + $('#key').keydown(function(ev){ + keydown = ev.keyName(); + }).keypress(function(ev){ + keypress = ev.keyName(); + }).keyup(function(ev){ + keyup = ev.keyName(); + }); + + stop(); + + Syn.key("a","key", function(){ + equals(keydown, "a","keydown"); + equals(keypress,"a","keypress"); + equals(keyup, "a","keyup"); + start(); + }); +}) + +}) diff --git a/event/key/qunit.html b/event/key/qunit.html new file mode 100644 index 00000000..608329d0 --- /dev/null +++ b/event/key/qunit.html @@ -0,0 +1,16 @@ + + + + + + + +

              Key Test Suite

              +

              +
              +

              +
              +
                +
                + + \ No newline at end of file diff --git a/event/livehack/livehack.js b/event/livehack/livehack.js index fee640c8..774edf11 100644 --- a/event/livehack/livehack.js +++ b/event/livehack/livehack.js @@ -1,31 +1,51 @@ -steal.plugins('jquery/event').then(function(){ +steal('jquery/event').then(function() { var event = jQuery.event, - + //helper that finds handlers by type and calls back a function, this is basically handle - findHelper = function(events, types, callback){ - for( var t =0; t< types.length; t++ ) { - var type = types[t], - typeHandlers, - all = type.indexOf(".") < 0, - namespaces, - namespace; - if ( !all ) { + // 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( var 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 @@ -33,135 +53,121 @@ steal.plugins('jquery/event').then(function(){ * @param {String} [selector] optional selector * @return {Array} an array of event handlers */ - event.find = function(el, types, selector){ - var events = $.data(el, "events"), - handlers = []; + event.find = function( el, types, selector ) { + var events = ( $._data(el) || {} ).events, + handlers = [], + t, liver, live; - if( !events ) { + if (!events ) { return handlers; } - - if( selector ) { - if (!events.live) { - return []; - } - var live = events.live; - - for ( var t = 0; t < live.length; t++ ) { - var liver = live[t]; - if( liver.selector === selector && $.inArray(liver.origType, types ) !== -1 ) { - handlers.push(liver.origHandler || liver.handler); - } - } - }else{ - // basically re-create handler's logic - findHelper(events, types, function(type, handler){ - handlers.push(handler); - }) - } + findHelper(events, types, function( type, handler ) { + handlers.push(handler); + }, selector); return handlers; - } + }; /** - * Finds - * @param {HTMLElement} el - * @param {Array} types + * 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 = {}, + 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] = {}), + add = function( selector, event, handler ) { + var select = selectors[selector] || (selectors[selector] = {}), events = select[event] || (select[event] = []); events.push(handler); }; - if ( !events ) { + 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 ); + /*$.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){ - add("", type, handler); - }) - + findHelper(events, types, function( type, handler, selector ) { + add(selector || "", type, handler); + }, null); + return selectors; - } - $.fn.respondsTo = function(events){ - if(!this.length){ + }; + event.supportTouch = "ontouchend" in document; + + $.fn.respondsTo = function( events ) { + if (!this.length ) { return false; - }else{ + } 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); + }; + $.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) { + event.setupHelper = function( types, startingEvent, onFirst ) { + if (!onFirst ) { onFirst = startingEvent; startingEvent = null; } - var add = function(handleObj){ - - var selector = handleObj.selector || ""; - if (selector) { - var bySelector = event.find(this, types, selector); - if (!bySelector.length) { - $(this).delegate(selector,startingEvent, onFirst ); + 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){ + if (!event.find(this, types, selector).length ) { event.add(this, startingEvent, onFirst, { selector: selector, delegate: this }); } - + } - - } - var remove = function(handleObj){ - var selector = handleObj.selector || ""; - if (selector) { - var bySelector = event.find(this, types, selector); - if (!bySelector.length) { - $(this).undelegate(selector,startingEvent, onFirst ); + + }, + 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 - }); + else { + if (!event.find(this, types, selector).length ) { + event.remove(this, startingEvent, onFirst, { + selector: selector, + delegate: this + }); + } } - } - } - $.each(types, function(){ + }; + $.each(types, function() { event.special[this] = { - add: add, + add: add, remove: remove, setup: function() {}, teardown: function() {} }; }); - } -}) \ No newline at end of file + }; +}); \ No newline at end of file diff --git a/event/offline/offline.js b/event/offline/offline.js deleted file mode 100644 index 75229a4d..00000000 --- a/event/offline/offline.js +++ /dev/null @@ -1,36 +0,0 @@ -steal.plugins('jquery/event').then('gears',function($){ - - - - - var support = $.support; - support.online = ("onLine" in window.navigator) - - - - - //support.offlineEvents = eventSupported("online",document.documentElement) - $(function(){ - support.onlineEvents = ("ononline" in document.body) - if(!support.onlineEvents){ - document.body.setAttribute("ononline","") - support.onlineEvents = ("ononline" in window) - } - if(support.onlineEvents){ - return; - } - var lastStatus = navigator.onLine; - setInterval(function(){ - if(lastStatus !== navigator.onLine){ - lastStatus = navigator.onLine - $(document.body).trigger(lastStatus ? "online" : "offline") - $(window).triggerHandle(lastStatus ? "online" : "offline") - } - },100) - - }) - - - - -}) diff --git a/event/pause/pause.html b/event/pause/pause.html new file mode 100644 index 00000000..efafc6c1 --- /dev/null +++ b/event/pause/pause.html @@ -0,0 +1,217 @@ + + + + Pause Example + + + +
                + +
                +
                + Name: + Age: +
                +
                + Yard: + Team: +
                +
                + Price: + Comment: +
                +
                +
                + + + + + + \ No newline at end of file diff --git a/event/pause/pause.js b/event/pause/pause.js new file mode 100644 index 00000000..45d28946 --- /dev/null +++ b/event/pause/pause.js @@ -0,0 +1,169 @@ +steal('jquery/event/default').then(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 pause 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 ? + + +}); \ No newline at end of file diff --git a/event/pause/pause_test.js b/event/pause/pause_test.js new file mode 100644 index 00000000..00eb6b82 --- /dev/null +++ b/event/pause/pause_test.js @@ -0,0 +1,51 @@ +steal('funcunit/qunit','funcunit/syn','jquery/event/pause').then(function(){ + +module("jquery/event/pause", {setup : function(){ + $("#qunit-test-area").html("") + var div = $("
                  "+ + "
                • Hello

                  "+ + "
                  • Foo Bar

                  "+ + "
                ").appendTo($("#qunit-test-area")); + +}}); + +test("basics",3, function(){ + + var calls =0, + lastTime, + space = function(){ + if(lastTime){ + + ok(new Date - lastTime > 35,"space between times "+(new Date - lastTime)) + } + lastTime = new Date() + }; + + $('#ul').delegate("li", "show",function(ev){ + calls++; + space(); + + ev.pause(); + + setTimeout(function(){ + ev.resume(); + },100) + + }) + + $('#wrapper').bind('show', function(){ + space() + equals(calls, 2, "both lis called"); + start() + }); + stop(); + $('#foo').trigger("show") +}); + + + + + + + +}); \ No newline at end of file diff --git a/event/pause/qunit.html b/event/pause/qunit.html new file mode 100644 index 00000000..c9434a5e --- /dev/null +++ b/event/pause/qunit.html @@ -0,0 +1,20 @@ + + + + swipe QUnit Test + + + + + +

                swipe Test Suite

                +

                +
                +

                +
                +
                  +
                  + + \ No newline at end of file diff --git a/event/resize/demo.html b/event/resize/demo.html new file mode 100644 index 00000000..1cdfa568 --- /dev/null +++ b/event/resize/demo.html @@ -0,0 +1,146 @@ + + + + Resize Demo Page + + + + +
                  +

                  My Application's Title (it's long so it will wrap)

                  +
                  + Some Buttons For Folders and Files +
                  +
                  +

                  Folders:

                  +
                    +
                  • Folder One
                  • +
                  • Folder Two
                  • +
                  • Folder Three
                  • +
                  • Folder Four
                  • +
                  • Folder Five
                  • +
                  • Folder Six
                  • +
                  • Folder Seven
                  • +
                  +
                  +
                  +
                  +
                  +
                  +
                  +
                  + Here are some file details +
                  +
                  +
                  + +
                  + A footer +
                  +
                  + + + + + \ No newline at end of file diff --git a/event/resize/qunit.html b/event/resize/qunit.html new file mode 100644 index 00000000..97dcdbc3 --- /dev/null +++ b/event/resize/qunit.html @@ -0,0 +1,17 @@ + + + + Resize QUnit Test + + + + +

                  selection Test Suite

                  +

                  +
                  +

                  +
                  +
                    +
                    + + \ No newline at end of file diff --git a/event/resize/resize.html b/event/resize/resize.html new file mode 100644 index 00000000..6848c248 --- /dev/null +++ b/event/resize/resize.html @@ -0,0 +1,47 @@ + + + + selection + + + +

                    Click an element to see who gets resized as a result. Elements with red borders are + resizable.

                    +
                      +
                    • Hello World
                    • +
                    • Hello World2

                      +
                        +
                      • Another Item
                      • +
                      +
                    • +
                    • JavaScriptMVC is just WAY smart
                    • +
                    + + + + \ No newline at end of file diff --git a/event/resize/resize.js b/event/resize/resize.js index 7c874ba9..db36d1b1 100644 --- a/event/resize/resize.js +++ b/event/resize/resize.js @@ -1,50 +1,159 @@ -steal.plugins('jquery/event').then(function($){ +steal('jquery/event').then(function( $ ) { /** * @add jQuery.event.special */ - var resizeCount = 0, + var resizers = $(), + resizeCount = 0, + // bind on the window window resizes to happen win = $(window), - windowWidth = win.width(), - windowHeight = win.height(), + windowWidth = 0, + windowHeight = 0, timer; + + $(function() { + windowWidth = win.width(); + windowHeight = win.height(); + }) + /** * @attribute resize * @parent specialevents - * Normalizes resize events cross browser. - *

                    This only allows native resize events on the window and prevents them from being called - * indefinitely. - *

                    + * + * 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 resize 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 ) { - //jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); - + // 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){ - if((this !== window) || (resizeCount === 0 && !ev.originalEvent)){ - resizeCount++; - handleObj.origHandler.call(this, ev, data); - resizeCount--; - } - var width = win.width(); - var height = win.height(); - if(resizeCount === 0 && (width != windowWidth ||height != windowHeight)){ - windowWidth = width; - windowHeight = height; - clearTimeout(timer) - timer = setTimeout(function(){ - win.triggerHandler("resize"); - },1) - - } + + 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); + } } - }, - - setup: function() { - return this !== window; } - } -}) + }; + // automatically bind on these + $([document, window]).bind('resize', function() {}) +}) \ No newline at end of file diff --git a/event/resize/resize_test.js b/event/resize/resize_test.js new file mode 100644 index 00000000..1863ec47 --- /dev/null +++ b/event/resize/resize_test.js @@ -0,0 +1,69 @@ + +steal('funcunit/qunit', 'jquery/event/resize').then(function() { + + module("jquery/event/resize") + + + test("resize hits only children in order", function() { + var ids = [] + record = function( ev ) { + ids.push(this.id ? this.id : this) + }, + divs = $("#qunit-test-area").html("
                    ").find('div').bind('resize', record); + + $(document.body).bind('resize', record); + + $("#qunit-test-area").children().eq(0).trigger("resize"); + + same(ids, ['1', '1.1', '1.2']) + + ids = []; + $("#qunit-test-area").trigger("resize"); + same(ids, [document.body, '1', '1.1', '1.2', '2']); + + ids = []; + $(window).trigger("resize"); + same(ids, [document.body, '1', '1.1', '1.2', '2']); + + $(document.body).unbind('resize', record); + }); + + test("resize stopping prop", function() { + var ids = [] + record = function( ev ) { + + ids.push(this.id ? this.id : this) + if ( this.id == '1' ) { + ev.stopPropagation(); + } + }, + divs = $("#qunit-test-area").html("
                    ").find('div').bind('resize', record); + + $(document.body).bind('resize', record); + + $(window).trigger("resize"); + same(ids, [document.body, '1', '2']); + + $(document.body).unbind('resize', record); + }); + + test("resize event cascades from target", function() { + + var ids = [], + record = function( ev ) { + ids.push(this.id ? this.id : this); + }, + + divs = $("#qunit-test-area").html("
                    "); + + divs.find("#1\\.1\\.1").bind("resize", record); + divs.find("#1").bind("resize", record); + + $("#1\\.1").trigger("resize", [false]); + same(ids, ['1.1.1']); + + $("#qunit-test-area").empty(); + }); + + +}) \ No newline at end of file diff --git a/event/select/select.html b/event/select/select.html deleted file mode 100644 index 3cfc3d12..00000000 --- a/event/select/select.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - select - - - -
                    - this is selectable -
                    -
                    - this is selectable -
                    -

                    - This is not selectable -

                    -
                    -
                    - Select me -
                    -
                    - Select me -
                    -
                    - Remove - - - - \ No newline at end of file diff --git a/event/select/select.js b/event/select/select.js deleted file mode 100644 index 00506172..00000000 --- a/event/select/select.js +++ /dev/null @@ -1,65 +0,0 @@ -steal.plugins('jquery/event').then(function($){ - var currentSelected = null, - currentTimer, - pieces, - focusin = function(ev){ - clearTimeout(currentTimer); - ev.stopPropagation(); //prevent others from handling focusin - var so = $.Event('selectout'); - so.relatedTarget = this; - - $(currentSelected).trigger(so); - - var si = $.Event('selectin'); - si.relatedTarget = currentSelected; - si.byFocus = true; - $(ev.target).trigger(si ); - currentSelected = null; - - }, - focusout = function(ev){ - ev.stopPropagation(); - currentSelected = ev.currentTarget; - clearTimeout(currentTimer); - currentTimer = setTimeout(function(){ - $(currentSelected).trigger('selectout'); - currentSelected = null; - }, 100) - }, - focusBubble = 'focusin', - blurBubble = 'focusout'; - - - if(document.addEventListener){ - document.addEventListener('focus', function(ev){ - jQuery.event.trigger( 'focusbubble', null, ev.target ) - },true); - document.addEventListener('blur', function(ev){ - jQuery.event.trigger( 'blurbubble', null, ev.target ) - },true); - focusBubble = 'focusbubble', - blurBubble = 'blurbubble'; - } - - $.event.special.selectin = { - add: function( handleObj ) { - if(handleObj.selector){ - $(this).delegate(handleObj.selector,focusBubble, focusin) - $(this).delegate(handleObj.selector,blurBubble, focusout) - }else{ - $(this).bind(focusBubble, focusin). - bind(blurBubble, focusout) - } - }, - remove: function( handleObj ) { - if(handleObj.selector){ - $(this).undelegate(handleObj.selector,focusBubble, focusin) - $(this).undelegate(handleObj.selector,blurBubble, focusout) - }else{ - $(this).unbind(focusBubble, focusin). - unbind(blurBubble, focusout) - } - } - } - -}) diff --git a/event/selection/qunit.html b/event/selection/qunit.html new file mode 100644 index 00000000..8a555e7b --- /dev/null +++ b/event/selection/qunit.html @@ -0,0 +1,20 @@ + + + + selection QUnit Test + + + + + +

                    selection Test Suite

                    +

                    +
                    +

                    +
                    +
                      +
                      + + \ No newline at end of file diff --git a/event/selection/selection.html b/event/selection/selection.html new file mode 100644 index 00000000..f71dfd2c --- /dev/null +++ b/event/selection/selection.html @@ -0,0 +1,101 @@ + + + + selection + + + + + + + + + + + + + + + + + + + + +
                      Select Textarea
                      Select Input
                      Select Within One Element

                      0123456789

                      Select Across Multiple Elements
                      012
                      345
                      + + +

                      Hello World! how are you today?

                      +

                      I am good, thank you.

                      +
                      + + + + + + \ No newline at end of file diff --git a/event/selection/selection.js b/event/selection/selection.js new file mode 100644 index 00000000..f98c6080 --- /dev/null +++ b/event/selection/selection.js @@ -0,0 +1,96 @@ +// a text selection event that is useful in mobile safari + +steal('jquery/dom/range','jquery/controller','jquery/event/livehack').then(function($){ + + + var event = $.event; + + event.selection = { + delay : 300, + preventDefault : event.supportTouch + }; + + event.setupHelper( ["selectionStart","selectionEnd","selectionEnding","selectionMoving","selectionMove"], "mousedown", function(ev){ + //now start checking mousemoves to update location + var delegate = ev.delegateTarget || ev.currentTarget, + selector = ev.handleObj.selector, + ready = false, + el = this, + startRange = $.Range(ev), + getRange = function(ev){ + var range = $.Range(ev), + pos = startRange.compare("START_TO_START", range), + entire; + if(pos == -1 || pos == 0){ + return startRange.clone().move("END_TO_END", range) + } else { + return range.clone().move("END_TO_END", startRange) + } + }, + cleanUp = function(){ + $(delegate).unbind('mousemove', mousemove) + .unbind('mouseup',mouseup); + clearTimeout(moveTimer); + startRange = null; + }, + mouseup = function(moveev){ + + if(!ready){ + cleanUp(); + return + } + $.each(event.find(delegate, ["selectionMoving"], selector), function(){ + this.call(el, moveev, range) + }); + var range = getRange(moveev); + cleanUp(); + $.each(event.find(delegate, ["selectionEnd"], selector), function(){ + this.call(el, ev, range); + }); + + }, + mousemove = function(moveev){ + // safari keeps triggering moves even if we haven't moved + if(moveev.clientX == ev.clientX && moveev.clientY == ev.clientY){ + return; + } + + if(!ready){ + return cleanUp(); + } + $.each(event.find(delegate, ["selectionMoving"], selector), function(){ + this.call(el, moveev, range) + }); + var range = getRange(moveev); + $.each(event.find(delegate, ["selectionMove"], selector), function(){ + this.call(el, moveev, range) + }); + }, + start = function(){ + ready = true; + var startEv = event.selection.preventDefault ? $.Event('selectionStart') : ev; + var startEv = $.extend(ev, startEv) + $.each(event.find(delegate, ["selectionStart"], selector), function(){ + this.call(el, startEv, startRange) + }); + + if(event.selection.preventDefault && startEv.isDefaultPrevented()){ + ready = false; + cleanUp(); + } + }, + moveTimer; + + if(event.selection.preventDefault){ + ev.preventDefault(); + moveTimer = setTimeout(start, event.selection.delay); + } else { + start(); + } + + + $(delegate).bind('mousemove', mousemove) + .bind('mouseup',mouseup) + }); + +}); \ No newline at end of file diff --git a/event/selection/selection_test.js b/event/selection/selection_test.js new file mode 100644 index 00000000..2a7e5131 --- /dev/null +++ b/event/selection/selection_test.js @@ -0,0 +1,39 @@ +steal("funcunit/qunit", "jquery/dom/selection").then(function(){ + +module("jquery/dom/selection"); + +test("getCharElement", function(){ + $("#qunit-test-area") + .html(""+ + ""+ + "

                      0123456789

                      "+ + "
                      012
                      345
                      "); + stop(); + setTimeout(function(){ + var types = ['textarea','#inp','#1','#2']; + for(var i =0; i< types.length; i++){ + console.log(types[i]) + $(types[i]).selection(1,5); + } + /* + $('textarea').selection(1,5); + $('input').selection(1,5); + $('#1').selection(1,5); + $('#2').selection(1,5); + */ + var res = []; + for(var i =0; i< types.length; i++){ + res.push( $(types[i]).selection() ); + } + + + + for(var i =0; i< res.length; i++){ + same(res[i],{start: 1, end: 5},types[i]) + } + + start(); + },1000) +}); + +}); \ No newline at end of file diff --git a/event/swipe/qunit.html b/event/swipe/qunit.html new file mode 100644 index 00000000..e65f52bd --- /dev/null +++ b/event/swipe/qunit.html @@ -0,0 +1,20 @@ + + + + swipe QUnit Test + + + + + +

                      swipe Test Suite

                      +

                      +
                      +

                      +
                      +
                        +
                        + + \ No newline at end of file diff --git a/event/swipe/swipe.html b/event/swipe/swipe.html new file mode 100644 index 00000000..16aa793f --- /dev/null +++ b/event/swipe/swipe.html @@ -0,0 +1,50 @@ + + + + swipe + + + +
                        Swipe Me
                        +
                        Swipes +
                          +
                        • Swipe Me
                        • +
                        • Swipe Me
                        • +
                        • Swipe Me
                        • +
                        • Swipe Me
                        • +
                        +
                        + + + \ No newline at end of file diff --git a/event/swipe/swipe.js b/event/swipe/swipe.js new file mode 100644 index 00000000..243e278a --- /dev/null +++ b/event/swipe/swipe.js @@ -0,0 +1,103 @@ +steal('jquery/event/livehack').then(function($){ +// TODO remove this, phantom supports touch AND click, but need to make funcunit support touch so its testable +var isPhantom = /Phantom/.test(navigator.userAgent), + supportTouch = !isPhantom && "ontouchend" in document, + scrollEvent = "touchmove scroll", + touchStartEvent = supportTouch ? "touchstart" : "mousedown", + touchStopEvent = supportTouch ? "touchend" : "mouseup", + touchMoveEvent = supportTouch ? "touchmove" : "mousemove", + data = function(event){ + var d = event.originalEvent.touches ? + event.originalEvent.touches[ 0 ] : + event; + return { + time: (new Date).getTime(), + coords: [ d.pageX, d.pageY ], + origin: $( event.target ) + }; + }; + +/** + * @class jQuery.event.swipe + * @parent specialevents + * @plugin jquery/event/swipe + * + * Swipe provides cross browser swipe events. On mobile devices, swipe uses touch events. On desktop browsers, + * swipe uses mouseevents. + * + * A swipe happens when a touch or drag moves + */ +var swipe = $.event.swipe = { + /** + * @attribute delay + * Delay is the upper limit of time the swipe motion can take in milliseconds. This defaults to 1000. + * + * A user must perform the swipe motion in this much time. + */ + delay : 500, + /** + * @attribute max + * The maximum distance the pointer must travel in pixels. The default is 75 pixels. + */ + max : 75, + /** + * @attribute min + * The minimum distance the pointer must travel in pixesl. The default is 30 pixels. + */ + min : 30 +}; + + +$.event.setupHelper( [ + + +"swipe",'swipeleft','swiperight','swipeup','swipedown'], touchStartEvent, function(ev){ + //listen to mouseup + var start = data(ev), + stop, + delegate = ev.delegateTarget || ev.currentTarget, + selector = ev.handleObj.selector, + entered = this; + + function moveHandler(event){ + if ( !start ) { + return; + } + stop = data(event); + + // prevent scrolling + if ( Math.abs( start.coords[0] - stop.coords[0] ) > 10 ) { + event.preventDefault(); + } + }; + $(document.documentElement).bind(touchMoveEvent,moveHandler ) + .one(touchStopEvent, function(event){ + $(this).unbind( touchMoveEvent, moveHandler ); + if ( start && stop ) { + var deltaX = Math.abs(start.coords[0] - stop.coords[0]), + deltaY = Math.abs(start.coords[1] - stop.coords[1]), + distance = Math.sqrt(deltaX*deltaX+deltaY*deltaY); + + if ( stop.time - start.time < swipe.delay && distance >= swipe.min ) { + + var events = ['swipe'] + if( deltaX >= swipe.min && deltaY < swipe.min) { + events.push( start.coords[0] > stop.coords[0] ? "swipeleft" : "swiperight" ); + }else if(deltaY >= swipe.min && deltaX < swipe.min){ + events.push( start.coords[1] < stop.coords[1] ? "swipedown" : "swipeup" ); + } + + + + //trigger swipe events on this guy + $.each($.event.find(delegate, events, selector), function(){ + this.call(entered, ev, {start : start, end: stop}) + }) + + } + } + start = stop = undefined; + }) +}); + +}); diff --git a/event/swipe/swipe_test.js b/event/swipe/swipe_test.js new file mode 100644 index 00000000..3be91c03 --- /dev/null +++ b/event/swipe/swipe_test.js @@ -0,0 +1,110 @@ +steal('funcunit/qunit','funcunit/syn','jquery/event/swipe').then(function(){ + +module("jquery/swipe", {setup : function(){ + $("#qunit-test-area").html("") + var div = $("
                        "+ + "
                        one
                        "+ + "
                        two
                        three
                        "+ + "
                        "); + + div.appendTo($("#qunit-test-area")); + var basicCss = { + position: "absolute", + border: "solid 1px black" + } + $("#outer").css(basicCss).css({top: "10px", left: "10px", + zIndex: 1000, backgroundColor: "green", width: "200px", height: "200px"}) +}}); + +test("swipe right event",2, function(){ + + $("#outer").bind("swipe",function(){ + ok(true,"swipe called"); + }).bind("swipeleft", function(){ + ok(false, "swipe left") + }).bind("swiperight", function(){ + ok(true, "swiperight") + }); + stop(); + Syn.drag({ + from: "20x20", + to: "50x20", + duration: 100, + },"outer", function(){ + start(); + }) + +}); + + +test("swipe left event",2, function(){ + + $("#outer").bind("swipe",function(){ + ok(true,"swipe called"); + }).bind("swipeleft", function(){ + ok(true, "swipe left") + }).bind("swiperight", function(){ + ok(false, "swiperight") + }); + stop(); + Syn.drag({ + from: "50x20", + to: "20x20", + duration: 100, + },"outer", function(){ + start(); + }) + +}); + + +test("swipe up event",2, function(){ + + $("#outer").bind("swipe",function(){ + ok(true,"swipe called"); + }).bind("swipeup", function(){ + ok(true, "swipe left") + }).bind("swiperight", function(){ + ok(false, "swiperight") + }).bind("swipedown", function(){ + ok(false, "swipedown") + }); + stop(); + Syn.drag({ + from: "20x50", + to: "20x20", + duration: 100, + },"outer", function(){ + start(); + }) + +}); + +test("swipe down event",2, function(){ + + $("#outer").bind("swipe",function(){ + ok(true,"swipe called"); + }).bind("swipeup", function(){ + ok(false, "swipe left") + }).bind("swiperight", function(){ + ok(false, "swiperight") + }).bind("swipedown", function(){ + ok(true, "swipedown") + }); + stop(); + Syn.drag({ + from: "20x20", + to: "20x50", + duration: 100, + },"outer", function(){ + start(); + }) + +}); + + + + + + +}) \ No newline at end of file diff --git a/event/offline/offline.html b/event/tap/tap.html similarity index 64% rename from event/offline/offline.html rename to event/tap/tap.html index 280d91ce..6979f127 100644 --- a/event/offline/offline.html +++ b/event/tap/tap.html @@ -2,25 +2,32 @@ "http://www.w3.org/TR/html4/strict.dtd"> - offline + select -
                        + I'd Tap That \ No newline at end of file diff --git a/event/tap/tap.js b/event/tap/tap.js new file mode 100644 index 00000000..356cfd30 --- /dev/null +++ b/event/tap/tap.js @@ -0,0 +1,51 @@ +steal('jquery/event/livehack').then(function($){ +var supportTouch = "ontouchend" in document, + scrollEvent = "touchmove scroll", + touchStartEvent = supportTouch ? "touchstart" : "mousedown", + touchStopEvent = supportTouch ? "touchend" : "mouseup", + touchMoveEvent = supportTouch ? "touchmove" : "mousemove", + data = function(event){ + var d = event.originalEvent.touches ? + event.originalEvent.touches[ 0 ] || event.originalEvent.changedTouches[ 0 ] : + event; + return { + time: (new Date).getTime(), + coords: [ d.pageX, d.pageY ], + origin: $( event.target ) + }; + }; + +/** + * @add jQuery.event.special + */ +$.event.setupHelper( ["tap"], touchStartEvent, function(ev){ + //listen to mouseup + var start = data(ev), + stop, + delegate = ev.delegateTarget || ev.currentTarget, + selector = ev.handleObj.selector, + entered = this, + moved = false, + touching = true, + timer; + + + function upHandler(event){ + stop = data(event); + if ((Math.abs( start.coords[0] - stop.coords[0] ) < 10) && + ( Math.abs( start.coords[1] - stop.coords[1] ) < 10) ){ + $.each($.event.find(delegate, ["tap"], selector), function(){ + this.call(entered, ev, {start : start, end: stop}) + }) + } + }; + + timer = setTimeout(function() { + $(delegate).unbind(touchStopEvent, upHandler); + }, 500 ); + + $(delegate).one(touchStopEvent, upHandler); + +}); + +}); \ No newline at end of file diff --git a/generate/app b/generate/app index e30d2183..aeb906fd 100644 --- a/generate/app +++ b/generate/app @@ -5,17 +5,19 @@ if (!_args[0]) { quit(); } -load('steal/rhino/steal.js'); +load('steal/rhino/rhino.js'); -steal('//steal/generate/generate','//steal/generate/system',function(steal){ - var path = _args[0].toLowerCase().replace('.',"/"), +steal('steal/generate','steal/generate/system.js',function(steal){ + var classed = steal.generate.toClass(_args[0]), + md = steal.generate.convert(classed), data = steal.extend({ - path: path, - application_name: path.match(/[^\/]*$/)[0], + path: _args[0], + application_name: md.underscore, current_path: steal.File.cwdURL(), - path_to_steal : new steal.File(path).pathToRoot() - }, steal.system) + path_to_steal : steal.File(_args[0]).pathToRoot() + }, steal.system); - steal.generate("jquery/generate/templates/app",path,data); + + steal.generate("jquery/generate/templates/app", _args[0], data); }); diff --git a/generate/coffee/controller b/generate/coffee/controller new file mode 100644 index 00000000..a547f8a2 --- /dev/null +++ b/generate/coffee/controller @@ -0,0 +1,35 @@ +if (_args.length < 1) { + print("USAGE : steal/js steal/generate/coffee/controller Company.Widget") + print("EX : steal/js steal/generate/coffee/controller Company.WidgetName"); + print(" > company/widget_name/widget_name.coffee") + print(); + quit(); +} + +load('steal/rhino/rhino.js'); + +steal( '//steal/generate/generate', + '//steal/generate/system', +function(steal){ + var upper = function(parts){ + for(var i =0; i < parts.length; i++){ + parts[i] = parts[i].charAt(0).toUpperCase()+parts[i].substr(1) + } + return parts + } + + if(_args[0].charAt(0) !== _args[0].charAt(0).toUpperCase()){ + var caps = upper( _args[0].split(/_|-/) ).join(''), + name = upper(caps.split("/")).join('.'); + + print(" Creating "+name); + _args[0] = name; + } + + var md = steal.generate.convert(_args[0]), + path = _args[0].toLowerCase().replace('.',"/"); + md.path_to_steal = new steal.File(path).pathToRoot() + steal.generate("jquery/generate/coffee/templates/controller",md.path+"/"+md.underscore,md) + +}); + diff --git a/generate/coffee/templates/controller/(underscore).coffee.ejs b/generate/coffee/templates/controller/(underscore).coffee.ejs new file mode 100644 index 00000000..61c267b7 --- /dev/null +++ b/generate/coffee/templates/controller/(underscore).coffee.ejs @@ -0,0 +1,11 @@ +steal "jquery/controller", +($) -> + + $.Controller "<%= name %>", + { + defaults : {}, + }, + { + init : -> + @element.html "Hello World!" + } diff --git a/generate/coffee/templates/controller/(underscore).html.ejs b/generate/coffee/templates/controller/(underscore).html.ejs new file mode 100644 index 00000000..7ad7f07c --- /dev/null +++ b/generate/coffee/templates/controller/(underscore).html.ejs @@ -0,0 +1,24 @@ + + + + <%= name %> + + + +

                        <%= name %> Demo

                        +
                        + + + + \ No newline at end of file diff --git a/generate/coffee/templates/controller/(underscore)_test.coffee.ejs b/generate/coffee/templates/controller/(underscore)_test.coffee.ejs new file mode 100644 index 00000000..7bf21374 --- /dev/null +++ b/generate/coffee/templates/controller/(underscore)_test.coffee.ejs @@ -0,0 +1,7 @@ +steal "funcunit", + -> + module "<%= fullName %>", setup: -> + S.open "//<%= path %>/<%=underscore%>/<%=underscore%>.html" + + test "Text Test", -> + equals S("h1").text(), "<%= fullName %> Demo", "demo text" \ No newline at end of file diff --git a/generate/templates/plugin/funcunit.html.ejs b/generate/coffee/templates/controller/funcunit.html.ejs similarity index 65% rename from generate/templates/plugin/funcunit.html.ejs rename to generate/coffee/templates/controller/funcunit.html.ejs index 3dc825d5..282c9c9a 100644 --- a/generate/templates/plugin/funcunit.html.ejs +++ b/generate/coffee/templates/controller/funcunit.html.ejs @@ -1,12 +1,13 @@ + - <%= application_name%> FuncUnit Test - + <%= name %> FuncUnit Test + -

                        <%= application_name%> Test Suite

                        +

                        <%= name %> Test Suite

                        diff --git a/generate/controller b/generate/controller index b48b7f16..3fc04a4d 100644 --- a/generate/controller +++ b/generate/controller @@ -1,21 +1,36 @@ if (_args.length < 1) { - print("USAGE : steal/js steal/generate/controller YourController") - print("EX : steal/js steal/generate/model Cookbook.Controllers.Recipe"); - print(" > cookbook/controller/recipe.js") + print("USAGE : steal/js steal/generate/controller Company.Widget") + print("EX : steal/js steal/generate/controller Company.WidgetName"); + print(" > company/widget_name/widget_name.js") print(); quit(); } -load('steal/rhino/steal.js'); +load('steal/rhino/rhino.js'); -steal( '//steal/generate/generate', - '//steal/generate/system', +steal( 'steal/generate', + 'steal/generate/system.js', function(steal){ - var md = steal.generate.convert(_args[0]); + var upper = function(parts){ + for(var i =0; i < parts.length; i++){ + parts[i] = parts[i].charAt(0).toUpperCase()+parts[i].substr(1) + } + return parts + } - md.appPath = md.path.replace(/\/controllers$/,""); + if(_args[0].charAt(0) !== _args[0].charAt(0).toUpperCase()){ + var caps = upper( _args[0].split(/_|-/) ).join(''), + name = upper(caps.split("/")).join('.'); + + print(" Creating "+name); + _args[0] = name; + } + + var md = steal.generate.convert(_args[0]), + path = _args[0].toLowerCase().replace(/\./g,"/"); - steal.generate("jquery/generate/templates/controller",md.appPath,md) + md.path_to_steal = new steal.File(path).pathToRoot() + steal.generate("jquery/generate/templates/controller",md.path+"/"+md.underscore,md) }); diff --git a/generate/model b/generate/model index b9267e8e..b0a9d475 100644 --- a/generate/model +++ b/generate/model @@ -1,26 +1,74 @@ if (_args.length < 1) { - print("USAGE : steal/js jquery/generate/model ClassName Type") - print("TYPES : JsonRest\n") - print("EX : steal/js jquery/generate/model Cashnet.Models.Customer"); - print(" > cashnet/models/customer.js") + print("USAGE : js jquery/generate/model cookbook/models/recipe") print(); quit(); } +load('steal/rhino/rhino.js'); - -load('steal/rhino/steal.js'); - -steal( '//steal/generate/generate', - '//steal/generate/system', - '//steal/generate/inflector' , +steal( 'steal/generate', + 'steal/generate/system.js', + 'steal/generate/inflector.js', function(steal){ - var md = steal.generate.convert(_args[0]); + + var upper = function(parts){ + for(var i =0; i < parts.length; i++){ + parts[i] = parts[i].charAt(0).toUpperCase()+parts[i].substr(1) + } + return parts + } - md.type = _args[1] + if(_args[0].charAt(0) !== _args[0].charAt(0).toUpperCase()){ + var caps = upper( _args[0].split(/_|-/) ).join(''), + name = upper(caps.split("/")).join('.'); + + print(" Creating "+name); + _args[0] = name; + } + var md = steal.generate.convert(_args[0]), + path = _args[0].toLowerCase().replace('.',"/"); + + var folder = md.path.replace(/\/\w+$/, "") + if(!folder){ + print("! Error: Models need to be part of an app"); + quit(); + } + if(!steal.File(folder).exists()){ + print("! Error: folder "+folder+" does not exist!"); + quit(); + } + + md.path_to_steal = new steal.File(path).pathToRoot(); md.appPath = md.path.replace(/\/models$/,""); - - steal.generate("jquery/generate/templates/model",md.appPath,md) + + //check pluralization of last part + if(steal.Inflector.singularize(md.underscore) !== md.underscore){ + print("! Warning: Model names should be singular. I don't think "+md.underscore+" is singular!") + } + + + // generate the files + steal.generate("jquery/generate/templates/model", md.appPath, md) + + try{ + // steal this model in models.js + steal.generate.insertSteal(md.appPath+"/models/models.js", "./"+md.underscore+".js"); + + // steal this model's unit test in qunit.js + steal.generate.insertSteal(md.appPath+"/test/qunit/qunit.js", "./"+md.underscore+"_test.js"); + } catch (e) { + if(e.type && e.type == "DUPLICATE"){ + print("! Error: This model was already created!") + quit(); + } + } + + // $.fixture.make for this model in fixtures.js + var text = readFile("jquery/generate/templates/fixturemake.ejs"); + var fixturetext = new steal.EJS({ + text: text + }).render(md); + steal.generate.insertCode(md.appPath+"/fixtures/fixtures.js", fixturetext); }); \ No newline at end of file diff --git a/generate/page b/generate/page index d1b81e19..a29430b7 100644 --- a/generate/page +++ b/generate/page @@ -5,9 +5,9 @@ if (_args.length < 2) { quit(); } -load('steal/rhino/steal.js'); +load('steal/rhino/rhino.js'); -steal('//steal/generate/generate','//steal/generate/system',function(steal){ +steal('steal/generate','steal/generate/system.js',function(steal){ var path = _args[0].toLowerCase().replace('.',"/") var data = steal.extend({ path: path, diff --git a/generate/plugin b/generate/plugin index e62fdf5e..f0737d94 100644 --- a/generate/plugin +++ b/generate/plugin @@ -4,9 +4,9 @@ if (!_args[0]) { print("Usage: steal/js steal/generate/plugin path"); quit(); } -load('steal/rhino/steal.js'); +load('steal/rhino/rhino.js'); -steal('//steal/generate/generate',function(steal){ +steal('steal/generate',function(steal){ var data = steal.extend({ path: _args[0], application_name: _args[0].match(/[^\/]*$/)[0], diff --git a/generate/scaffold b/generate/scaffold index 2654468d..b2171982 100644 --- a/generate/scaffold +++ b/generate/scaffold @@ -7,21 +7,24 @@ if (_args.length < 1) { quit(); } -load('steal/rhino/steal.js'); +load('steal/rhino/rhino.js'); -steal( '//steal/generate/generate', - '//steal/generate/system', +steal( 'steal/generate', + 'steal/generate/system.js', + 'jquery/generate/templates/model.js', function(steal){ //check capitalization + steal.generate.model(_args[0]); + var parts = _args[0].split("."), part; for(var i=0; i< parts.length;i++){ part = parts[i]; - if(part[0] !== part[0].toUpperCase()){ + if( part[0] !== part[0].toUpperCase() ){ print("! Warning: "+part+" should probably be capitalized. JavaScriptMVC likes capital namespaces and class names.") } - parts[i] = part.toLowerCase(); + parts[i] = steal.generate.underscore(part); } // check folders var folder = parts.slice(0, parts.length-1).join("/"); @@ -44,7 +47,28 @@ function(steal){ md.type = _args[1] md.appPath = md.path.replace(/\/models$/,""); + md.baseName = md.name.replace(/\.Models.*/, "."+md.className); + steal.generate("jquery/generate/templates/scaffold",md.appPath+"/"+md.underscore,md); + + steal.generate.insertSteal( + md.appPath+"/test/funcunit/funcunit.js", + md.appPath+"/"+md.underscore+"/create/create_test.js"); + + steal.generate.insertSteal( + md.appPath+"/test/funcunit/funcunit.js", + md.appPath+"/"+md.underscore+"/list/list_test.js") - steal.generate("jquery/generate/templates/scaffold",md.appPath,md) + steal.generate.insertSteal( + md.appPath+"/"+md.appPath+".js", + md.appPath+"/"+md.underscore+"/create", true) + steal.generate.insertSteal( + md.appPath+"/"+md.appPath+".js", + md.appPath+"/"+md.underscore+"/list", true) + + var text = readFile("jquery/generate/templates/scaffoldHookup.ejs"); + var hookup = new steal.EJS({ + text: text + }).render(md); + steal.generate.insertCode(md.appPath+"/"+md.appPath+".js", hookup ); }); \ No newline at end of file diff --git a/generate/templates/app/(application_name).css.ejs b/generate/templates/app/(application_name).css.ejs index 62aab7eb..73068f1b 100644 --- a/generate/templates/app/(application_name).css.ejs +++ b/generate/templates/app/(application_name).css.ejs @@ -1,2 +1,29 @@ -body {font-family: verdana} -td {padding: 3px;} \ No newline at end of file +body { + font-family:Lucida Sans,Lucida Grande,Arial,sans-serif; + margin:0; + line-height:22px; + width:960px; + margin:0 auto; +} +h1 { + padding:30px 0 10px; + margin-top:0; + line-height:30px; +} + +ul{ + padding:0 0 0 15px; + list-style : none; +} + +a { + color:#ae3d26; + text-decoration:none; +} + +a:hover { text-decoration:underline; } + +hr { + border:none; + border-top:1px dotted #000; +} \ No newline at end of file diff --git a/generate/templates/app/(application_name).html.ejs b/generate/templates/app/(application_name).html.ejs index 36501194..4f049bde 100644 --- a/generate/templates/app/(application_name).html.ejs +++ b/generate/templates/app/(application_name).html.ejs @@ -1,17 +1,28 @@ - + <%= application_name %> -

                        Welcome to JavaScriptMVC 3.0!

                        -
                          -
                        • Include plugins and files in <%= path %>/<%= application_name %>.js.
                        • -
                        • Change to production mode by changing development to production in this file.
                        • -
                        - +

                        Welcome to JavaScriptMVC 3.2!

                        +
                          +
                        • Steal plugins and files in <%= path %>/<%= application_name %>.js.
                        • +
                        • Change to production mode by changing steal.js to steal.production.js in this file.
                        • +
                        +
                        + Here are some links to get you started: + +
                        + Join the community: + + \ No newline at end of file diff --git a/generate/templates/app/(application_name).js.ejs b/generate/templates/app/(application_name).js.ejs index 21bf5926..931b07b7 100644 --- a/generate/templates/app/(application_name).js.ejs +++ b/generate/templates/app/(application_name).js.ejs @@ -1,17 +1,7 @@ -steal.plugins( - 'jquery/controller', // a widget factory - 'jquery/controller/subscribe', // subscribe to OpenAjax.hub - 'jquery/view/ejs', // client side templates - 'jquery/model', // Ajax wrappers - 'jquery/dom/fixture', // simulated Ajax requests - 'jquery/dom/form_params') // form data helper - - .css('<%= application_name %>') // loads styles - - .resources() // 3rd party script's (like jQueryUI), in resources folder - - .models() // loads files in models folder - - .controllers() // loads files in controllers folder - - .views(); // adds views to be added to build \ No newline at end of file +steal( + './<%= application_name %>.css', // application CSS file + './models/models.js', // steals all your models + './fixtures/fixtures.js', // sets up fixtures for your models + function(){ // configure your application + + }) \ No newline at end of file diff --git a/generate/templates/app/(application_name).md.ejs b/generate/templates/app/(application_name).md.ejs new file mode 100644 index 00000000..03151c5c --- /dev/null +++ b/generate/templates/app/(application_name).md.ejs @@ -0,0 +1,3 @@ +@page index <%= application_name %> + +This is a placeholder for the homepage of your documentation. \ No newline at end of file diff --git a/generate/templates/app/fixtures/fixtures.js.ejs b/generate/templates/app/fixtures/fixtures.js.ejs new file mode 100644 index 00000000..1e662acb --- /dev/null +++ b/generate/templates/app/fixtures/fixtures.js.ejs @@ -0,0 +1,5 @@ +// map fixtures for this application + +steal("jquery/dom/fixture", function(){ + +}) \ No newline at end of file diff --git a/generate/templates/app/funcunit.html.ejs b/generate/templates/app/funcunit.html.ejs index 8b261254..19541960 100644 --- a/generate/templates/app/funcunit.html.ejs +++ b/generate/templates/app/funcunit.html.ejs @@ -1,3 +1,4 @@ + diff --git a/generate/templates/app/models/models.js.ejs b/generate/templates/app/models/models.js.ejs new file mode 100644 index 00000000..9224b684 --- /dev/null +++ b/generate/templates/app/models/models.js.ejs @@ -0,0 +1,2 @@ +// steal model files +steal("jquery/model") \ No newline at end of file diff --git a/generate/templates/app/qunit.html.ejs b/generate/templates/app/qunit.html.ejs index 9a2e275b..0264d56c 100644 --- a/generate/templates/app/qunit.html.ejs +++ b/generate/templates/app/qunit.html.ejs @@ -1,3 +1,4 @@ + diff --git a/generate/templates/app/resources/.ignore b/generate/templates/app/resources/.ignore deleted file mode 100644 index e69de29b..00000000 diff --git a/generate/templates/app/scripts/build.js.ejs b/generate/templates/app/scripts/build.js.ejs index cb5672ac..bccd83e5 100644 --- a/generate/templates/app/scripts/build.js.ejs +++ b/generate/templates/app/scripts/build.js.ejs @@ -1,6 +1,6 @@ -//steal/js <%= path %>/scripts/compress.js +//js <%= path %>/scripts/build.js -load("steal/rhino/steal.js"); -steal.plugins('steal/build','steal/build/scripts','steal/build/styles',function(){ +load("steal/rhino/rhino.js"); +steal('steal/build').then('steal/build/scripts','steal/build/styles',function(){ steal.build('<%= path %>/scripts/build.html',{to: '<%= path %>'}); }); diff --git a/generate/templates/app/scripts/clean.js.ejs b/generate/templates/app/scripts/clean.js.ejs index a90472c0..909c5fc3 100644 --- a/generate/templates/app/scripts/clean.js.ejs +++ b/generate/templates/app/scripts/clean.js.ejs @@ -1,7 +1,7 @@ //steal/js <%= path %>/scripts/compress.js -load("steal/rhino/steal.js"); -steal.plugins('steal/clean',function(){ +load("steal/rhino/rhino.js"); +steal('steal/clean',function(){ steal.clean('<%= path %>/<%= application_name %>.html',{ indent_size: 1, indent_char: '\t', diff --git a/generate/templates/app/scripts/crawl.js.ejs b/generate/templates/app/scripts/crawl.js.ejs new file mode 100644 index 00000000..7290b5eb --- /dev/null +++ b/generate/templates/app/scripts/crawl.js.ejs @@ -0,0 +1,7 @@ +// load('<%= path %>/scripts/crawl.js') + +load('steal/rhino/rhino.js') + +steal('steal/html/crawl', function(){ + steal.html.crawl("<%= path %>/<%= application_name %>.html","<%= path %>/out") +}); diff --git a/generate/templates/app/scripts/docs.js.ejs b/generate/templates/app/scripts/docs.js.ejs index 44635db8..78929167 100644 --- a/generate/templates/app/scripts/docs.js.ejs +++ b/generate/templates/app/scripts/docs.js.ejs @@ -1,6 +1,8 @@ //js <%= path %>/scripts/doc.js -load('steal/rhino/steal.js'); -steal.plugins("documentjs").then(function(){ - DocumentJS('<%= path %>/<%= application_name %>.html'); +load('steal/rhino/rhino.js'); +steal("documentjs").then(function(){ + DocumentJS('<%= path %>/<%= application_name %>.html', { + markdown : ['<%= application_name %>'] + }); }); \ No newline at end of file diff --git a/generate/templates/app/test/funcunit/(application_name)_test.js.ejs b/generate/templates/app/test/funcunit/(application_name)_test.js.ejs index afb29a45..bf912289 100644 --- a/generate/templates/app/test/funcunit/(application_name)_test.js.ejs +++ b/generate/templates/app/test/funcunit/(application_name)_test.js.ejs @@ -1,9 +1,11 @@ -module("<%=application_name%> test", { - setup: function(){ - S.open("//<%= path %>/<%=application_name%>.html"); - } -}); - -test("Copy Test", function(){ - equals(S("h1").text(), "Welcome to JavaScriptMVC 3.0!","welcome text"); -}); \ No newline at end of file +steal("funcunit", function(){ + module("<%=application_name%> test", { + setup: function(){ + S.open("//<%= path %>/<%=application_name%>.html"); + } + }); + + test("Copy Test", function(){ + equals(S("h1").text(), "Welcome to JavaScriptMVC 3.2!","welcome text"); + }); +}) \ No newline at end of file diff --git a/generate/templates/app/test/funcunit/funcunit.js.ejs b/generate/templates/app/test/funcunit/funcunit.js.ejs index 760df6b3..adf4980d 100644 --- a/generate/templates/app/test/funcunit/funcunit.js.ejs +++ b/generate/templates/app/test/funcunit/funcunit.js.ejs @@ -1,3 +1,2 @@ -steal - .plugins("funcunit") - .then("<%= application_name %>_test"); \ No newline at end of file +steal("funcunit") + .then("./<%= application_name %>_test.js"); \ No newline at end of file diff --git a/generate/templates/app/test/qunit/(application_name)_test.js.ejs b/generate/templates/app/test/qunit/(application_name)_test.js.ejs index c00d40d7..5ff66a28 100644 --- a/generate/templates/app/test/qunit/(application_name)_test.js.ejs +++ b/generate/templates/app/test/qunit/(application_name)_test.js.ejs @@ -1,5 +1,7 @@ -module("<%= application_name %>"); - -test("<%= application_name %> testing works", function(){ - ok(true,"an assert is run"); -}); \ No newline at end of file +steal("funcunit/qunit", function(){ + module("<%= application_name %>"); + + test("<%= application_name %> testing works", function(){ + ok(true,"an assert is run"); + }); +}) \ No newline at end of file diff --git a/generate/templates/app/test/qunit/qunit.js.ejs b/generate/templates/app/test/qunit/qunit.js.ejs index c901b815..9f532e44 100644 --- a/generate/templates/app/test/qunit/qunit.js.ejs +++ b/generate/templates/app/test/qunit/qunit.js.ejs @@ -1,3 +1 @@ -steal - .plugins("funcunit/qunit", "<%= path %>") - .then("<%= application_name %>_test"); \ No newline at end of file +steal("funcunit/qunit", "./<%= application_name %>_test.js"); \ No newline at end of file diff --git a/generate/templates/app/views/.ignore b/generate/templates/app/views/.ignore deleted file mode 100644 index e69de29b..00000000 diff --git a/generate/templates/controller/(underscore).html.ejs b/generate/templates/controller/(underscore).html.ejs new file mode 100644 index 00000000..60c9942a --- /dev/null +++ b/generate/templates/controller/(underscore).html.ejs @@ -0,0 +1,25 @@ + + + + <%= name %> + + + +

                        <%= name %> Demo

                        +
                        + + + + \ No newline at end of file diff --git a/generate/templates/controller/(underscore).js.ejs b/generate/templates/controller/(underscore).js.ejs new file mode 100644 index 00000000..760ba7a8 --- /dev/null +++ b/generate/templates/controller/(underscore).js.ejs @@ -0,0 +1,21 @@ +steal( 'jquery/controller','jquery/view/ejs' ) + .then( './views/init.ejs', function($){ + +/** + * @class <%= name %> + */ +$.Controller('<%= name %>', +/** @Static */ +{ + defaults : {} +}, +/** @Prototype */ +{ + init : function(){ + this.element.html("//<%= path %>/<%= underscore %>/views/init.ejs",{ + message: "Hello World" + }); + } +}) + +}); \ No newline at end of file diff --git a/generate/templates/controller/(underscore)_test.js.ejs b/generate/templates/controller/(underscore)_test.js.ejs new file mode 100644 index 00000000..c78ce166 --- /dev/null +++ b/generate/templates/controller/(underscore)_test.js.ejs @@ -0,0 +1,14 @@ +steal('funcunit').then(function(){ + +module("<%= fullName %>", { + setup: function(){ + S.open("//<%= path %>/<%=underscore%>/<%=underscore%>.html"); + } +}); + +test("Text Test", function(){ + equals(S("h1").text(), "<%= fullName %> Demo","demo text"); +}); + + +}); \ No newline at end of file diff --git a/generate/templates/controller/controllers/(underscore)_controller.js.ejs b/generate/templates/controller/controllers/(underscore)_controller.js.ejs deleted file mode 100644 index be4e663b..00000000 --- a/generate/templates/controller/controllers/(underscore)_controller.js.ejs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @tag controllers, home - */ -jQuery.Controller.extend('<%=name.replace("Models","Controllers")%>', -/* @Static */ -{ - -}, -/* @Prototype */ -{ - -}); \ No newline at end of file diff --git a/generate/templates/controller/funcunit.html.ejs b/generate/templates/controller/funcunit.html.ejs new file mode 100644 index 00000000..282c9c9a --- /dev/null +++ b/generate/templates/controller/funcunit.html.ejs @@ -0,0 +1,16 @@ + + + + + <%= name %> FuncUnit Test + + + + +

                        <%= name %> Test Suite

                        +

                        +
                        +

                        +
                          + + \ No newline at end of file diff --git a/generate/templates/app/controllers/.ignore b/generate/templates/controller/views/.ignore similarity index 100% rename from generate/templates/app/controllers/.ignore rename to generate/templates/controller/views/.ignore diff --git a/generate/templates/controller/views/init.ejs.ejs b/generate/templates/controller/views/init.ejs.ejs new file mode 100644 index 00000000..5ad93e45 --- /dev/null +++ b/generate/templates/controller/views/init.ejs.ejs @@ -0,0 +1 @@ +<%%= this.message %> \ No newline at end of file diff --git a/generate/templates/fixturemake.ejs b/generate/templates/fixturemake.ejs new file mode 100644 index 00000000..2d0c0c54 --- /dev/null +++ b/generate/templates/fixturemake.ejs @@ -0,0 +1,7 @@ + $.fixture.make("<%= underscore %>", 5, function(i, <%= underscore %>){ + var descriptions = ["grill fish", "make ice", "cut onions"] + return { + name: "<%= underscore %> "+i, + description: $.fixture.rand( descriptions , 1)[0] + } + }) \ No newline at end of file diff --git a/generate/templates/model.js b/generate/templates/model.js new file mode 100644 index 00000000..12908d31 --- /dev/null +++ b/generate/templates/model.js @@ -0,0 +1,75 @@ +steal( 'steal/generate', + 'steal/generate/system.js', + 'steal/generate/inflector.js', + function(steal){ + + + + var upper = function(parts){ + for(var i =0; i < parts.length; i++){ + parts[i] = parts[i].charAt(0).toUpperCase()+parts[i].substr(1) + } + return parts + } + + steal.generate.model = function(arg){ + + if(arg.charAt(0) !== arg.charAt(0).toUpperCase()){ + var caps = upper( arg.split(/_|-/) ).join(''), + name = upper(caps.split("/")).join('.'); + + print(" Creating "+name); + arg = name; + } + + var md = steal.generate.convert(arg), + path = arg.toLowerCase().replace('.',"/"); + + var folder = md.path.replace(/\/\w+$/, "") + if(!folder){ + print("! Error: Models need to be part of an app"); + quit(); + } + if(!steal.File(folder).exists()){ + print("! Error: folder "+folder+" does not exist!"); + quit(); + } + + md.path_to_steal = new steal.File(path).pathToRoot(); + md.appPath = md.path.replace(/\/models$/,""); + + //check pluralization of last part + if(steal.Inflector.singularize(md.underscore) !== md.underscore){ + print("! Warning: Model names should be singular. I don't think "+md.underscore+" is singular!") + } + + // generate the files + steal.generate("jquery/generate/templates/model", md.appPath, md) + + try{ + // steal this model in models.js + steal.generate.insertSteal(md.appPath+"/models/models.js", "./"+md.underscore+".js"); + + // steal this model's unit test in qunit.js + steal.generate.insertSteal(md.appPath+"/test/qunit/qunit.js", "./"+md.underscore+"_test.js"); + } catch (e) { + if(e.type && e.type == "DUPLICATE"){ + print("! Error: This model was already created!") + quit(); + } + } + // $.fixture.make for this model in fixtures.js + + + + var text = readFile("jquery/generate/templates/fixturemake.ejs"); + var fixturetext = new steal.EJS({ + text: text + }).render(md); + steal.generate.insertCode(md.appPath+"/fixtures/fixtures.js", fixturetext); + + } + + + +}); \ No newline at end of file diff --git a/generate/templates/model/fixtures.link b/generate/templates/model/fixtures.link deleted file mode 100644 index 9cb5b4cc..00000000 --- a/generate/templates/model/fixtures.link +++ /dev/null @@ -1 +0,0 @@ -jquery/generate/templates/scaffold/fixtures \ No newline at end of file diff --git a/generate/templates/model/models.link b/generate/templates/model/models.link deleted file mode 100644 index 96e2df12..00000000 --- a/generate/templates/model/models.link +++ /dev/null @@ -1 +0,0 @@ -jquery/generate/templates/scaffold/models \ No newline at end of file diff --git a/generate/templates/model/models/(underscore).js.ejs b/generate/templates/model/models/(underscore).js.ejs new file mode 100644 index 00000000..e877dc2f --- /dev/null +++ b/generate/templates/model/models/(underscore).js.ejs @@ -0,0 +1,21 @@ +steal('jquery/model', function(){ + +/** + * @class <%=name%> + * @parent index + * @inherits jQuery.Model + * Wraps backend <%=underscore%> services. + */ +$.Model('<%=name%>', +/* @Static */ +{ + findAll: "/<%= plural %>.json", + findOne : "/<%= plural %>/{id}.json", + create : "/<%= plural %>.json", + update : "/<%= plural %>/{id}.json", + destroy : "/<%= plural %>/{id}.json" +}, +/* @Prototype */ +{}); + +}) \ No newline at end of file diff --git a/generate/templates/model/test/qunit.link b/generate/templates/model/test/qunit.link deleted file mode 100644 index a3810cd6..00000000 --- a/generate/templates/model/test/qunit.link +++ /dev/null @@ -1 +0,0 @@ -jquery/generate/templates/scaffold/test/qunit \ No newline at end of file diff --git a/generate/templates/model/test/qunit/(underscore)_test.js.ejs b/generate/templates/model/test/qunit/(underscore)_test.js.ejs new file mode 100644 index 00000000..d79dd56b --- /dev/null +++ b/generate/templates/model/test/qunit/(underscore)_test.js.ejs @@ -0,0 +1,51 @@ +steal("funcunit/qunit", "<%= appPath %>/fixtures", "<%= appPath %>/models/<%= underscore %>.js", function(){ + module("Model: <%= name %>") + + test("findAll", function(){ + expect(4); + stop(); + <%= name %>.findAll({}, function(<%= plural %>){ + ok(<%= plural %>) + ok(<%= plural %>.length) + ok(<%= plural %>[0].name) + ok(<%= plural %>[0].description) + start(); + }); + + }) + + test("create", function(){ + expect(3) + stop(); + new <%= name %>({name: "dry cleaning", description: "take to street corner"}).save(function(<%= underscore %>){ + ok(<%= underscore %>); + ok(<%= underscore %>.id); + equals(<%= underscore %>.name,"dry cleaning") + <%= underscore %>.destroy() + start(); + }) + }) + test("update" , function(){ + expect(2); + stop(); + new <%= name %>({name: "cook dinner", description: "chicken"}). + save(function(<%= underscore %>){ + equals(<%= underscore %>.description,"chicken"); + <%= underscore %>.update({description: "steak"},function(<%= underscore %>){ + equals(<%= underscore %>.description,"steak"); + <%= underscore %>.destroy(); + start(); + }) + }) + + }); + test("destroy", function(){ + expect(1); + stop(); + new <%= name %>({name: "mow grass", description: "use riding mower"}). + destroy(function(<%= underscore %>){ + ok( true ,"Destroy called" ) + start(); + }) + }) +}) \ No newline at end of file diff --git a/generate/templates/page.ejs b/generate/templates/page.ejs index 932506c4..85564703 100644 --- a/generate/templates/page.ejs +++ b/generate/templates/page.ejs @@ -1,5 +1,4 @@ - + <%= application_name %> @@ -11,13 +10,13 @@ -

                          Welcome to JavaScriptMVC 3.0!

                          +

                          Welcome to JavaScriptMVC 3.2!

                          • Steal plugins and files in <%= path %>/<%= application_name %>.js.
                          • -
                          • Change to production mode by changing development to production in this file.
                          • +
                          • Change to production mode by changing steal.js to steal.production.js in this file.
                          - \ No newline at end of file + diff --git a/generate/templates/plugin/(application_name).html.ejs b/generate/templates/plugin/(application_name).html.ejs index 94feff6f..76870b96 100644 --- a/generate/templates/plugin/(application_name).html.ejs +++ b/generate/templates/plugin/(application_name).html.ejs @@ -1,5 +1,4 @@ - + <%= application_name %> @@ -11,13 +10,15 @@ -

                          Welcome to JavaScriptMVC 3.0!

                          -
                            -
                          • Include plugins and files in <%= path %>/<%= application_name %>.js.
                          • -
                          • Change to production mode by changing development to production in this file.
                          • -
                          +

                          <%= application_name %> Demo

                          +

                          This is a dummy page to show off your plugin

                          + \ No newline at end of file diff --git a/generate/templates/plugin/(application_name).js.ejs b/generate/templates/plugin/(application_name).js.ejs index f4ca1b5c..2cee54c5 100644 --- a/generate/templates/plugin/(application_name).js.ejs +++ b/generate/templates/plugin/(application_name).js.ejs @@ -1,3 +1,3 @@ -steal.plugins().then(function($){ +steal('jquery',function($){ }); \ No newline at end of file diff --git a/generate/templates/plugin/(application_name)_test.js.ejs b/generate/templates/plugin/(application_name)_test.js.ejs new file mode 100644 index 00000000..a7178a22 --- /dev/null +++ b/generate/templates/plugin/(application_name)_test.js.ejs @@ -0,0 +1,10 @@ +steal('funcunit/qunit','./<%= application_name %>',function(){ + +module("<%= application_name %>"); + +test("<%= application_name %> testing works", function(){ + ok(true,"an assert is run"); +}); + + +}); \ No newline at end of file diff --git a/generate/templates/plugin/docs/.gitignore b/generate/templates/plugin/docs/.gitignore deleted file mode 100644 index e69de29b..00000000 diff --git a/generate/templates/plugin/fixtures/.ignore b/generate/templates/plugin/fixtures/.ignore deleted file mode 100644 index e69de29b..00000000 diff --git a/generate/templates/plugin/qunit.html.ejs b/generate/templates/plugin/qunit.html.ejs index 9a17aead..99d1d6f8 100644 --- a/generate/templates/plugin/qunit.html.ejs +++ b/generate/templates/plugin/qunit.html.ejs @@ -1,11 +1,9 @@ + <%= application_name%> QUnit Test - - + diff --git a/generate/templates/plugin/resources/.ignore b/generate/templates/plugin/resources/.ignore deleted file mode 100644 index e69de29b..00000000 diff --git a/generate/templates/plugin/scripts.link b/generate/templates/plugin/scripts.link deleted file mode 100644 index adb8fab0..00000000 --- a/generate/templates/plugin/scripts.link +++ /dev/null @@ -1 +0,0 @@ -jquery/generate/templates/app/scripts \ No newline at end of file diff --git a/generate/templates/plugin/test.link b/generate/templates/plugin/test.link deleted file mode 100644 index 43220fc2..00000000 --- a/generate/templates/plugin/test.link +++ /dev/null @@ -1 +0,0 @@ -jquery/generate/templates/app/test \ No newline at end of file diff --git a/generate/templates/plugin/views/.ignore b/generate/templates/plugin/views/.ignore deleted file mode 100644 index e69de29b..00000000 diff --git a/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs b/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs deleted file mode 100644 index 89bd6673..00000000 --- a/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs +++ /dev/null @@ -1,100 +0,0 @@ -/*global confirm: true */ - -/** - * @tag controllers, home - * Displays a table of <%= plural %>. Lets the user - * ["<%=name.replace("Models","Controllers")%>.prototype.form submit" create], - * ["<%=name.replace("Models","Controllers")%>.prototype..edit click" edit], - * or ["<%=name.replace("Models","Controllers")%>.prototype..destroy click" destroy] <%= plural %>. - */ -$.Controller.extend('<%=name.replace("Models","Controllers")%>', -/* @Static */ -{ - onDocument: true -}, -/* @Prototype */ -{ - /** - * When the page loads, gets all <%= plural %> to be displayed. - */ - load: function(){ - if(!$("#<%= underscore %>").length){ - $(document.body).append($('
                          ').attr('id','<%= underscore %>')); - <%= name %>.findAll({}, this.callback('list')); - } - }, - /** - * Displays a list of <%= plural %> and the submit form. - * @param {Array} <%= plural %> An array of <%=name%> objects. - */ - list: function( <%= plural %> ){ - $('#<%= underscore %>').html(this.view('init', {<%= plural %>:<%= plural %>} )); - }, - /** - * Responds to the create form being submitted by creating a new <%=name%>. - * @param {jQuery} el A jQuery wrapped element. - * @param {Event} ev A jQuery event whose default action is prevented. - */ -'form submit': function( el, ev ){ - ev.preventDefault(); - new <%= name %>(el.formParams()).save(); -}, -/** - * Listens for <%= plural %> being created. When a <%= underscore %> is created, displays the new <%= underscore %>. - * @param {String} called The open ajax event that was called. - * @param {Event} <%= underscore %> The new <%= underscore %>. - */ -'<%= underscore %>.created subscribe': function( called, <%= underscore %> ){ - $("#<%= underscore %> tbody").append( this.view("list", {<%= plural %>:[<%= underscore %>]}) ); - $("#<%= underscore %> form input[type!=submit]").val(""); //clear old vals -}, - /** - * Creates and places the edit interface. - * @param {jQuery} el The <%= underscore %>'s edit link element. - */ -'.edit click': function( el ){ - var <%= underscore %> = el.closest('.<%= underscore %>').model(); - <%= underscore %>.elements().html(this.view('edit', <%= underscore %>)); -}, - /** - * Removes the edit interface. - * @param {jQuery} el The <%= underscore %>'s cancel link element. - */ -'.cancel click': function( el ){ - this.show(el.closest('.<%= underscore %>').model()); -}, - /** - * Updates the <%= underscore %> from the edit values. - */ -'.update click': function( el ){ - var $<%= underscore %> = el.closest('.<%= underscore %>'); - $<%= underscore %>.model().update($<%= underscore %>.formParams()); -}, - /** - * Listens for updated <%= plural %>. When a <%= underscore %> is updated, - * update's its display. - */ -'<%= underscore %>.updated subscribe': function( called, <%= underscore %> ){ - this.show(<%= underscore %>); -}, - /** - * Shows a <%= underscore %>'s information. - */ -show: function( <%= underscore %> ){ - <%= underscore %>.elements().html(this.view('show',<%= underscore %>)); -}, - /** - * Handle's clicking on a <%= underscore %>'s destroy link. - */ -'.destroy click': function( el ){ - if(confirm("Are you sure you want to destroy?")){ - el.closest('.<%= underscore %>').model().destroy(); - } - }, - /** - * Listens for <%= plural %> being destroyed and removes them from being displayed. - */ -"<%= underscore %>.destroyed subscribe": function(called, <%= underscore %>){ - <%= underscore %>.elements().remove(); //removes ALL elements - } -}); \ No newline at end of file diff --git a/generate/templates/scaffold/create/create.html.ejs b/generate/templates/scaffold/create/create.html.ejs new file mode 100644 index 00000000..c95961cc --- /dev/null +++ b/generate/templates/scaffold/create/create.html.ejs @@ -0,0 +1,29 @@ + + + + <%=baseName %>.Create + + + +

                          <%=baseName %>.Create Demo

                          +
                          +
                          + + + + \ No newline at end of file diff --git a/generate/templates/scaffold/create/create.js.ejs b/generate/templates/scaffold/create/create.js.ejs new file mode 100644 index 00000000..68d4f443 --- /dev/null +++ b/generate/templates/scaffold/create/create.js.ejs @@ -0,0 +1,31 @@ +steal( 'jquery/controller', + 'jquery/view/ejs', + 'jquery/dom/form_params', + 'jquery/controller/view', + '<%= appPath %>/models' ) + .then('./views/init.ejs', function($){ + +/** + * @class <%=baseName%>.Create + * @parent index + * @inherits jQuery.Controller + * Creates <%= plural %> + */ +$.Controller('<%=baseName%>.Create', +/** @Prototype */ +{ + init : function(){ + this.element.html(this.view()); + }, + submit : function(el, ev){ + ev.preventDefault(); + this.element.find('[type=submit]').val('Creating...') + new <%= name %>(el.formParams()).save(this.callback('saved')); + }, + saved : function(){ + this.element.find('[type=submit]').val('Create'); + this.element[0].reset() + } +}) + +}); \ No newline at end of file diff --git a/generate/templates/scaffold/create/create_test.js.ejs b/generate/templates/scaffold/create/create_test.js.ejs new file mode 100644 index 00000000..7c29b735 --- /dev/null +++ b/generate/templates/scaffold/create/create_test.js.ejs @@ -0,0 +1,22 @@ +steal('funcunit',function(){ + +module("<%=baseName%>.Create", { + setup: function(){ + S.open("//<%= appPath %>/<%= underscore %>/create/create.html"); + } +}); + +test("create <%= plural %>", function(){ + S("[name=name]").type("Ice Water"); + S("[name=description]").type("Pour water in a glass. Add ice cubes."); + S("[type=submit]").click(); + S('h3:contains(Ice Water)') + .exists(function(){ + ok(true, "Ice Water added"); + equals(S("[name=name]").val() , "", "form reset"); + equals(S("[name=description]").val() , "", "form reset"); + }) +}); + + +}); \ No newline at end of file diff --git a/generate/templates/scaffold/create/funcunit.html.ejs b/generate/templates/scaffold/create/funcunit.html.ejs new file mode 100644 index 00000000..7e099c0b --- /dev/null +++ b/generate/templates/scaffold/create/funcunit.html.ejs @@ -0,0 +1,15 @@ + + + + <%=baseName%>.Create FuncUnit Test + + + + +

                          <%=baseName%>.Create Test Suite

                          +

                          +
                          +

                          +
                            + + \ No newline at end of file diff --git a/generate/templates/scaffold/create/views/init.ejs.ejs b/generate/templates/scaffold/create/views/init.ejs.ejs new file mode 100644 index 00000000..c137ff81 --- /dev/null +++ b/generate/templates/scaffold/create/views/init.ejs.ejs @@ -0,0 +1,9 @@ +

                            New <%= underscore %>

                            +

                            +
                            + +

                            +


                            + +

                            +

                            \ No newline at end of file diff --git a/generate/templates/scaffold/fixtures/(plural).json.get.ejs b/generate/templates/scaffold/fixtures/(plural).json.get.ejs deleted file mode 100644 index 4f4bef3b..00000000 --- a/generate/templates/scaffold/fixtures/(plural).json.get.ejs +++ /dev/null @@ -1,3 +0,0 @@ -[ - {"name": "Take Out Trash", "description": "To the curb!", "id": 5} -] \ No newline at end of file diff --git a/generate/templates/scaffold/list/funcunit.html.ejs b/generate/templates/scaffold/list/funcunit.html.ejs new file mode 100644 index 00000000..06013025 --- /dev/null +++ b/generate/templates/scaffold/list/funcunit.html.ejs @@ -0,0 +1,15 @@ + + + + <%= baseName %>.List Test + + + + +

                            <%= baseName %>.List Test

                            +

                            +
                            +

                            +
                              + + \ No newline at end of file diff --git a/generate/templates/scaffold/list/list.html.ejs b/generate/templates/scaffold/list/list.html.ejs new file mode 100644 index 00000000..373af0e2 --- /dev/null +++ b/generate/templates/scaffold/list/list.html.ejs @@ -0,0 +1,31 @@ + + + + <%= baseName %>.List Demo + + + +

                              <%= baseName %>.List Demo

                              +
                                + Create <%= className %> + + + + \ No newline at end of file diff --git a/generate/templates/scaffold/list/list.js.ejs b/generate/templates/scaffold/list/list.js.ejs new file mode 100644 index 00000000..48187418 --- /dev/null +++ b/generate/templates/scaffold/list/list.js.ejs @@ -0,0 +1,42 @@ +steal( 'jquery/controller', + 'jquery/view/ejs', + 'jquery/controller/view', + '<%= appPath %>/models' ) +.then( './views/init.ejs', + './views/<%= underscore %>.ejs', + function($){ + +/** + * @class <%=baseName%>.List + * @parent index + * @inherits jQuery.Controller + * Lists <%= plural %> and lets you destroy them. + */ +$.Controller('<%=baseName%>.List', +/** @Static */ +{ + defaults : {} +}, +/** @Prototype */ +{ + init : function(){ + this.element.html(this.view('init',<%= name %>.findAll()) ) + }, + '.destroy click': function( el ){ + if(confirm("Are you sure you want to destroy?")){ + el.closest('.<%= underscore %>').model().destroy(); + } + }, + "{<%= name %>} destroyed" : function(<%= className %>, ev, <%= underscore %>) { + <%= underscore %>.elements(this.element).remove(); + }, + "{<%= name %>} created" : function(<%= className %>, ev, <%= underscore %>){ + this.element.append(this.view('init', [<%= underscore %>])) + }, + "{<%= name %>} updated" : function(<%= className %>, ev, <%= underscore %>){ + <%= underscore %>.elements(this.element) + .html(this.view('<%= underscore %>', <%= underscore %>) ); + } +}); + +}); \ No newline at end of file diff --git a/generate/templates/scaffold/list/list_test.js.ejs b/generate/templates/scaffold/list/list_test.js.ejs new file mode 100644 index 00000000..040fd724 --- /dev/null +++ b/generate/templates/scaffold/list/list_test.js.ejs @@ -0,0 +1,26 @@ +steal('funcunit',function(){ + +module("<%=baseName%>.List", { + setup: function(){ + S.open("//<%= appPath %>/<%= underscore %>/list/list.html"); + } +}); + +test("delete <%= plural %>", function(){ + S('#create').click() + + // wait until grilled cheese has been added + S('h3:contains(Grilled Cheese X)').exists(); + + S.confirm(true); + S('h3:last a').click(); + + + S('h3:contains(Grilled Cheese)').missing(function(){ + ok(true,"Grilled Cheese Removed") + }); + +}); + + +}); \ No newline at end of file diff --git a/generate/templates/scaffold/list/views/(underscore).ejs.ejs b/generate/templates/scaffold/list/views/(underscore).ejs.ejs new file mode 100644 index 00000000..971da5c8 --- /dev/null +++ b/generate/templates/scaffold/list/views/(underscore).ejs.ejs @@ -0,0 +1,2 @@ +

                                <%%= name %> X

                                +

                                <%%= description %>

                                \ No newline at end of file diff --git a/generate/templates/scaffold/list/views/init.ejs.ejs b/generate/templates/scaffold/list/views/init.ejs.ejs new file mode 100644 index 00000000..e3109afa --- /dev/null +++ b/generate/templates/scaffold/list/views/init.ejs.ejs @@ -0,0 +1,5 @@ +<%%for(var i = 0; i < this.length ; i++){%> +
                              • <%%= this[i]%>> + <%%== $.View('//<%= appPath %>/<%= underscore %>/list/views/<%= underscore %>.ejs', this[i] )%> +
                              • +<%%}%> \ No newline at end of file diff --git a/generate/templates/scaffold/models/(underscore).js.ejs b/generate/templates/scaffold/models/(underscore).js.ejs deleted file mode 100644 index 158775e1..00000000 --- a/generate/templates/scaffold/models/(underscore).js.ejs +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @tag models, home - * Wraps backend <%=underscore%> services. Enables - * [<%=name%>.static.findAll retrieving], - * [<%=name%>.static.update updating], - * [<%=name%>.static.destroy destroying], and - * [<%=name%>.static.create creating] <%= plural %>. - */ -$.Model.extend('<%=name%>', -/* @Static */ -{ - /** - * Retrieves <%= plural %> data from your backend services. - * @param {Object} params params that might refine your results. - * @param {Function} success a callback function that returns wrapped <%=underscore%> objects. - * @param {Function} error a callback function for an error in the ajax request. - */ - findAll: function( params, success, error ){ - $.ajax({ - url: '/<%= underscore %>', - type: 'get', - dataType: 'json', - data: params, - success: this.callback(['wrapMany',success]), - error: error, - fixture: "//<%= appPath %>/fixtures/<%= plural %>.json.get" //calculates the fixture path from the url and type. - }); - }, - /** - * Updates a <%= underscore %>'s data. - * @param {String} id A unique id representing your <%= underscore %>. - * @param {Object} attrs Data to update your <%= underscore %> with. - * @param {Function} success a callback function that indicates a successful update. - * @param {Function} error a callback that should be called with an object of errors. - */ - update: function( id, attrs, success, error ){ - $.ajax({ - url: '/<%= plural %>/'+id, - type: 'put', - dataType: 'json', - data: attrs, - success: success, - error: error, - fixture: "-restUpdate" //uses $.fixture.restUpdate for response. - }); - }, - /** - * Destroys a <%= underscore %>'s data. - * @param {String} id A unique id representing your <%= underscore %>. - * @param {Function} success a callback function that indicates a successful destroy. - * @param {Function} error a callback that should be called with an object of errors. - */ - destroy: function( id, success, error ){ - $.ajax({ - url: '/<%= plural %>/'+id, - type: 'delete', - dataType: 'json', - success: success, - error: error, - fixture: "-restDestroy" // uses $.fixture.restDestroy for response. - }); - }, - /** - * Creates a <%= underscore %>. - * @param {Object} attrs A <%= underscore %>'s attributes. - * @param {Function} success a callback function that indicates a successful create. The data that comes back must have an ID property. - * @param {Function} error a callback that should be called with an object of errors. - */ - create: function( attrs, success, error ){ - $.ajax({ - url: '/<%= plural %>', - type: 'post', - dataType: 'json', - success: success, - error: error, - data: attrs, - fixture: "-restCreate" //uses $.fixture.restCreate for response. - }); - } -}, -/* @Prototype */ -{}); \ No newline at end of file diff --git a/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs b/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs deleted file mode 100644 index 346f0209..00000000 --- a/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs +++ /dev/null @@ -1,61 +0,0 @@ -/*global module: true, ok: true, equals: true, S: true, test: true */ -module("<%= underscore %>", { - setup: function () { - // open the page - S.open("//<%= appPath %>/<%= appName %>.html"); - - //make sure there's at least one <%= underscore %> on the page before running a test - S('.<%= underscore %>').exists(); - }, - //a helper function that creates a <%= underscore %> - create: function () { - S("[name=name]").type("Ice"); - S("[name=description]").type("Cold Water"); - S("[type=submit]").click(); - S('.<%= underscore %>:nth-child(2)').exists(); - } -}); - -test("<%= plural %> present", function () { - ok(S('.<%= underscore %>').size() >= 1, "There is at least one <%= underscore %>"); -}); - -test("create <%= plural %>", function () { - - this.create(); - - S(function () { - ok(S('.<%= underscore %>:nth-child(2) td:first').text().match(/Ice/), "Typed Ice"); - }); -}); - -test("edit <%= plural %>", function () { - - this.create(); - - S('.<%= underscore %>:nth-child(2) a.edit').click(); - S(".<%= underscore %> input[name=name]").type(" Water"); - S(".<%= underscore %> input[name=description]").type("\b\b\b\b\bTap Water"); - S(".update").click(); - S('.<%= underscore %>:nth-child(2) .edit').exists(function () { - - ok(S('.<%= underscore %>:nth-child(2) td:first').text().match(/Ice Water/), "Typed Ice Water"); - - ok(S('.<%= underscore %>:nth-child(2) td:nth-child(2)').text().match(/Cold Tap Water/), "Typed Cold Tap Water"); - }); -}); - -test("destroy", function () { - - this.create(); - - S(".<%= underscore %>:nth-child(2) .destroy").click(); - - //makes the next confirmation return true - S.confirm(true); - - S('.<%= underscore %>:nth-child(2)').missing(function () { - ok("destroyed"); - }); - -}); \ No newline at end of file diff --git a/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs b/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs deleted file mode 100644 index 0b5640a4..00000000 --- a/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs +++ /dev/null @@ -1,45 +0,0 @@ -module("Model: <%= name %>") - -test("findAll", function(){ - stop(2000); - <%= name %>.findAll({}, function(<%= plural %>){ - start() - ok(<%= plural %>) - ok(<%= plural %>.length) - ok(<%= plural %>[0].name) - ok(<%= plural %>[0].description) - }); - -}) - -test("create", function(){ - stop(2000); - new <%= name %>({name: "dry cleaning", description: "take to street corner"}).save(function(<%= underscore %>){ - start(); - ok(<%= underscore %>); - ok(<%= underscore %>.id); - equals(<%= underscore %>.name,"dry cleaning") - <%= underscore %>.destroy() - }) -}) -test("update" , function(){ - stop(); - new <%= name %>({name: "cook dinner", description: "chicken"}). - save(function(<%= underscore %>){ - equals(<%= underscore %>.description,"chicken"); - <%= underscore %>.update({description: "steak"},function(<%= underscore %>){ - start() - equals(<%= underscore %>.description,"steak"); - <%= underscore %>.destroy(); - }) - }) - -}); -test("destroy", function(){ - stop(2000); - new <%= name %>({name: "mow grass", description: "use riding mower"}). - destroy(function(<%= underscore %>){ - start(); - ok( true ,"Destroy called" ) - }) -}) \ No newline at end of file diff --git a/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs b/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs deleted file mode 100644 index 3a4acb6e..00000000 --- a/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs +++ /dev/null @@ -1,10 +0,0 @@ -<%%for(var attribute in <%=name%>.attributes){%> - <%%if(attribute == 'id') continue;%> - - - -<%%}%> - - - cancel - \ No newline at end of file diff --git a/generate/templates/scaffold/views/(underscore)/init.ejs.ejs b/generate/templates/scaffold/views/(underscore)/init.ejs.ejs deleted file mode 100644 index 170339a8..00000000 --- a/generate/templates/scaffold/views/(underscore)/init.ejs.ejs +++ /dev/null @@ -1,26 +0,0 @@ -

                                <%=plural%>

                                - - - - <%% for(var attr in <%= name%>.attributes){%> - <%% if(attr == 'id') continue;%> - - <%%}%> - - - - - <%%= $.View('//<%= appPath %>/views/<%= underscore %>/list',{<%=plural%>: <%=plural%>})%> - -
                                <%%= attr%> Options
                                -

                                New <%= underscore %>

                                -
                                - <%% for(var attr in <%= name%>.attributes){ %> - <%% if(attr == 'id') continue;%> -
                                -
                                - -
                                - <%%}%> - -
                                \ No newline at end of file diff --git a/generate/templates/scaffold/views/(underscore)/list.ejs.ejs b/generate/templates/scaffold/views/(underscore)/list.ejs.ejs deleted file mode 100644 index b1b9db53..00000000 --- a/generate/templates/scaffold/views/(underscore)/list.ejs.ejs +++ /dev/null @@ -1,5 +0,0 @@ -<%%for(var i = 0; i < <%=plural%>.length ; i++){%> - <%%= <%=plural%>[i]%>> - <%%= $.View('//<%= appPath %>/views/<%= underscore%>/show',<%=plural%>[i])%> - -<%%}%> \ No newline at end of file diff --git a/generate/templates/scaffold/views/(underscore)/show.ejs.ejs b/generate/templates/scaffold/views/(underscore)/show.ejs.ejs deleted file mode 100644 index 61a50497..00000000 --- a/generate/templates/scaffold/views/(underscore)/show.ejs.ejs +++ /dev/null @@ -1,10 +0,0 @@ -<%%for(var attribute in this.Class.attributes){%> - <%%if(attribute == 'id') continue;%> - - <%%=this[attribute]%> - -<%%}%> - - edit - destroy - \ No newline at end of file diff --git a/generate/templates/scaffoldHookup.ejs b/generate/templates/scaffoldHookup.ejs new file mode 100644 index 00000000..6a094e3e --- /dev/null +++ b/generate/templates/scaffoldHookup.ejs @@ -0,0 +1,2 @@ + $('#<%= plural %>').<%=appPath%>_<%= underscore %>_list(); + $('#create').<%=appPath%>_<%= underscore %>_create(); \ No newline at end of file diff --git a/generate/test/app_plugin_model_controller.js b/generate/test/app_plugin_model_controller.js new file mode 100644 index 00000000..9352a072 --- /dev/null +++ b/generate/test/app_plugin_model_controller.js @@ -0,0 +1,60 @@ +load('steal/rhino/rhino.js') +load('steal/rhino/test.js'); + +(function(rhinoSteal){ + _S = steal.test; + + + _S.module("jquery/generate") + STEALPRINT = false; + + _S.test("app" , function(t){ + _args = ['cnu']; + load('jquery/generate/app'); + _S.clear(); + _S.open('cnu/cnu.html') + t.ok(typeof steal !== 'undefined', "steal is fine") + _S.clear(); + }) + + _S.test("app 2 levels deep" , function(t){ + _args = ['cnu/widget']; + load('jquery/generate/plugin'); + _S.clear(); + _S.open('cnu/widget/widget.html') + t.ok(typeof steal !== 'undefined', "steal is fine") + _S.clear(); + }) + + /** + * Tests generating a very basic controller and model + */ + + _S.test("controller, model, and page" , function(t){ + _args = ['Cnu.Todos']; + load('jquery/generate/controller'); + _S.clear(); + + _args = ['Cnu.Models.Todo']; + load('jquery/generate/model'); + _S.clear(); + cnuContent = readFile('cnu/cnu.js') + +"\n.then('./models/todo.js')" + +"\n.then('./todos/todos.js')"; + load('steal/rhino/rhino.js') + new steal.File('cnu/cnu.js').save( cnuContent ); + + + _args = ['cnu','cnugen.html']; + load('jquery/generate/page'); + _S.clear(); + + _S.open('cnu/cnugen.html'); + + t.ok(typeof Cnu.Todos !== 'undefined',"load Cnu.Controllers.Todos") + t.ok(typeof Cnu.Models.Todo !== 'undefined', "load Cnu.Models.Todo") + + rhinoSteal.File("cnu").removeDir(); + }) + +})(steal); diff --git a/generate/test/run.js b/generate/test/run.js new file mode 100644 index 00000000..1efe5c72 --- /dev/null +++ b/generate/test/run.js @@ -0,0 +1,3 @@ +load("jquery/generate/test/app_plugin_model_controller.js"); + +load("jquery/generate/test/scaffold.js"); diff --git a/generate/test/scaffold.js b/generate/test/scaffold.js new file mode 100644 index 00000000..ecfb9552 --- /dev/null +++ b/generate/test/scaffold.js @@ -0,0 +1,87 @@ + + +load('steal/rhino/rhino.js'); +load('steal/test/test.js'); + +steal('steal/test', function(s){ + + s.test.module("jquery/generate/scaffold") + + STEALPRINT = false; + + s.test.test("make app and scaffold", function(t){ + _args = ['cookbook']; + load('jquery/generate/app'); + _args = ['Cookbook.Models.Recipe']; + load('jquery/generate/scaffold'); + + + load('steal/rhino/rhino.js'); + var cookbookContent = readFile('cookbook/cookbook.js') + +"\n.then('./models/recipe.js')" + +"\n.then('./controllers/recipe_controller.js')"; + new steal.File('cookbook/cookbook.js').save( cookbookContent ); + + var qunitContent = readFile('cookbook/test/qunit/qunit.js'). + replace("cookbook_test", "recipe_test"); + new steal.File('cookbook/test/qunit/qunit.js').save( qunitContent ); + + var funcunitContent = readFile('cookbook/test/funcunit/funcunit.js'). + replace("cookbook_test", "recipe_controller_test"); + new steal.File('cookbook/test/funcunit/funcunit.js').save( funcunitContent ); + + t.clear(); + print('trying to open ...') + t.open('cookbook/cookbook.html', false) + t.ok(Cookbook.Controllers.Recipe, "Recipe Controller") + t.ok(Cookbook.Models.Recipe, "Recipe Controller") + t.clear(); + }); + + //now see if unit and functional run + +// s.test.test("scaffold unit tests", function(t){ +// +// load('steal/rhino/rhino.js'); +// load('funcunit/loader.js'); +// FuncUnit.load('cookbook/qunit.html'); +// }); +// +// s.test.test("scaffold functional tests", function(t){ +// load('steal/rhino/rhino.js'); +// load('funcunit/loader.js'); +// FuncUnit.load('cookbook/funcunit.html'); +// +// }); +// +// s.test.test("documentjs", function(t){ +// t.clear(); +// load('steal/rhino/rhino.js'); +// _args = ['cookbook/cookbook.html'] +// load("documentjs/documentjs.js"); +// DocumentJS('cookbook/cookbook.html'); +// }); + + s.test.test("compress", function(t){ + t.clear(); + load("cookbook/scripts/build.js") + + var cookbookPage = readFile('cookbook/cookbook.html'). + replace("steal.js?cookbook,development", "steal.js?cookbook,production"); + new steal.File('cookbook/cookbook.html').save( cookbookPage ); + + t.clear(); + t.open('cookbook/cookbook.html', false) + t.ok(Cookbook.Controllers.Recipe, "Recipe Controller") + t.ok(Cookbook.Models.Recipe, "Recipe Controller") + t.clear(); + }); + + + //print("-- cleanup --"); +// s.File("cookbook").removeDir(); + +}) + + + diff --git a/jquery.js b/jquery.js index 5614e533..74ce4119 100644 --- a/jquery.js +++ b/jquery.js @@ -1,28 +1,30 @@ /*! - * jQuery JavaScript Library v1.4.3 + * jQuery JavaScript Library v1.7.1 * http://jquery.com/ * - * Copyright 2010, John Resig + * Copyright 2011, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ - * Copyright 2010, The Dojo Foundation + * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * - * Date: Fri Oct 15 00:36:22 2010 -0500 + * Date: Mon Nov 21 21:11:03 2011 -0500 */ (function( window, undefined ) { // Use the correct document accordingly with window argument (sandbox) -var document = window.document; +var document = window.document, + navigator = window.navigator, + location = window.location; var jQuery = (function() { // Define a local copy of jQuery var jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context ); + return new jQuery.fn.init( selector, context, rootjQuery ); }, // Map over jQuery in case of overwrite @@ -35,26 +37,16 @@ var jQuery = function( selector, context ) { rootjQuery, // A simple way to check for HTML strings or ID strings - // (both of which we optimize for) - quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, - - // Is it a simple selector - isSimple = /^.[^:#\[\.,]*$/, + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, // Check if a string has a non-whitespace character in it rnotwhite = /\S/, - rwhite = /\s/, // Used for trimming whitespace trimLeft = /^\s+/, trimRight = /\s+$/, - // Check for non-word characters - rnonword = /\W/, - - // Check for digits - rdigit = /\d/, - // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, @@ -70,17 +62,23 @@ var jQuery = function( selector, context ) { rmsie = /(msie) ([\w.]+)/, rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + // Keep a UserAgent string for use with jQuery.browser userAgent = navigator.userAgent, // For matching the engine and version of the browser browserMatch, - - // Has the ready events already been bound? - readyBound = false, - - // The functions to execute on DOM ready - readyList = [], + + // The deferred used on DOM ready + readyList, // The ready event handler DOMContentLoaded, @@ -92,12 +90,13 @@ var jQuery = function( selector, context ) { slice = Array.prototype.slice, trim = String.prototype.trim, indexOf = Array.prototype.indexOf, - + // [[Class]] -> type pairs class2type = {}; jQuery.fn = jQuery.prototype = { - init: function( selector, context ) { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { var match, elem, ret, doc; // Handle $(""), $(null), or $(undefined) @@ -111,12 +110,12 @@ jQuery.fn = jQuery.prototype = { this.length = 1; return this; } - + // The body element only exists once, optimize finding it if ( selector === "body" && !context && document.body ) { this.context = document; this[0] = document.body; - this.selector = "body"; + this.selector = selector; this.length = 1; return this; } @@ -124,14 +123,21 @@ jQuery.fn = jQuery.prototype = { // Handle HTML strings if ( typeof selector === "string" ) { // Are we dealing with HTML string or an ID? - match = quickExpr.exec( selector ); + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } // Verify a match, and that no context was specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { - doc = (context ? context.ownerDocument || context : document); + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); // If a single string is passed in and it's a single tag // just do a createElement and skip the rest @@ -148,11 +154,11 @@ jQuery.fn = jQuery.prototype = { } else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; } - + return jQuery.merge( this, selector ); - + // HANDLE: $("#id") } else { elem = document.getElementById( match[2] ); @@ -176,21 +182,14 @@ jQuery.fn = jQuery.prototype = { return this; } - // HANDLE: $("TAG") - } else if ( !context && !rnonword.test( selector ) ) { - this.selector = selector; - this.context = document; - selector = document.getElementsByTagName( selector ); - return jQuery.merge( this, selector ); - // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); + return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { - return jQuery( context ).find( selector ); + return this.constructor( context ).find( selector ); } // HANDLE: $(function) @@ -199,7 +198,7 @@ jQuery.fn = jQuery.prototype = { return rootjQuery.ready( selector ); } - if (selector.selector !== undefined) { + if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } @@ -211,7 +210,7 @@ jQuery.fn = jQuery.prototype = { selector: "", // The current version of jQuery being used - jquery: "1.4.4pre", + jquery: "1.7.1", // The default length of a jQuery object is 0 length: 0, @@ -234,18 +233,18 @@ jQuery.fn = jQuery.prototype = { this.toArray() : // Return just the object - ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); + ( num < 0 ? this[ this.length + num ] : this[ num ] ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set - var ret = jQuery(); + var ret = this.constructor(); if ( jQuery.isArray( elems ) ) { push.apply( ret, elems ); - + } else { jQuery.merge( ret, elems ); } @@ -256,7 +255,7 @@ jQuery.fn = jQuery.prototype = { ret.context = this.context; if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; } @@ -271,29 +270,22 @@ jQuery.fn = jQuery.prototype = { each: function( callback, args ) { return jQuery.each( this, callback, args ); }, - + ready: function( fn ) { // Attach the listeners jQuery.bindReady(); - // If the DOM is already ready - if ( jQuery.isReady ) { - // Execute the function immediately - fn.call( document, jQuery ); - - // Otherwise, remember the function for later - } else if ( readyList ) { - // Add the function to the wait list - readyList.push( fn ); - } + // Add the callback + readyList.add( fn ); return this; }, - + eq: function( i ) { + i = +i; return i === -1 ? this.slice( i ) : - this.slice( i, +i + 1 ); + this.slice( i, i + 1 ); }, first: function() { @@ -314,9 +306,9 @@ jQuery.fn = jQuery.prototype = { return callback.call( elem, i, elem ); })); }, - + end: function() { - return this.prevObject || jQuery(null); + return this.prevObject || this.constructor(null); }, // For internal use only. @@ -330,8 +322,11 @@ jQuery.fn = jQuery.prototype = { jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy, copyIsArray; + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { @@ -392,31 +387,37 @@ jQuery.extend = jQuery.fn.extend = function() { jQuery.extend({ noConflict: function( deep ) { - window.$ = _$; + if ( window.$ === jQuery ) { + window.$ = _$; + } - if ( deep ) { + if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }, - + // Is the DOM ready to be used? Set to true once it occurs. isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - // A third-party is pushing the ready event forwards - if ( wait === true ) { - jQuery.readyWait--; + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); } + }, - // Make sure that the DOM is not already loaded - if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( !document.body ) { return setTimeout( jQuery.ready, 1 ); @@ -431,30 +432,21 @@ jQuery.extend({ } // If there are functions bound, to execute - if ( readyList ) { - // Execute all of them - var fn, i = 0; - while ( (fn = readyList[ i++ ]) ) { - fn.call( document, jQuery ); - } - - // Reset the list of functions - readyList = null; - } + readyList.fireWith( document, [ jQuery ] ); // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); } } }, - + bindReady: function() { - if ( readyBound ) { + if ( readyList ) { return; } - readyBound = true; + readyList = jQuery.Callbacks( "once memory" ); // Catch cases where $(document).ready() is called after the // browser event has already occurred. @@ -467,7 +459,7 @@ jQuery.extend({ if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - + // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); @@ -475,8 +467,8 @@ jQuery.extend({ } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes - document.attachEvent("onreadystatechange", DOMContentLoaded); - + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); @@ -510,8 +502,8 @@ jQuery.extend({ return obj && typeof obj === "object" && "setInterval" in obj; }, - isNaN: function( obj ) { - return obj == null || !rdigit.test( obj ) || isNaN( obj ); + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); }, type: function( obj ) { @@ -527,20 +519,25 @@ jQuery.extend({ if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } - - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 return false; } - + // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. - + var key; for ( key in obj ) {} - + return key === undefined || hasOwn.call( obj, key ); }, @@ -550,11 +547,11 @@ jQuery.extend({ } return true; }, - + error: function( msg ) { - throw msg; + throw new Error( msg ); }, - + parseJSON: function( data ) { if ( typeof data !== "string" || !data ) { return null; @@ -562,48 +559,67 @@ jQuery.extend({ // Make sure leading/trailing whitespace is removed (IE can't handle it) data = jQuery.trim( data ); - + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + // Make sure the incoming data is actual JSON // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test(data.replace(rvalidescape, "@") - .replace(rvalidtokens, "]") - .replace(rvalidbraces, "")) ) { + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { - // Try to use the native JSON parser first - return window.JSON && window.JSON.parse ? - window.JSON.parse( data ) : - (new Function("return " + data))(); + return ( new Function( "return " + data ) )(); - } else { - jQuery.error( "Invalid JSON: " + data ); } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; }, noop: function() {}, - // Evalulates a script in a global context + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { - if ( data && rnotwhite.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - - if ( jQuery.support.scriptEval ) { - script.appendChild( document.createTextNode( data ) ); - } else { - script.text = data; - } - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); } }, + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); }, @@ -612,7 +628,7 @@ jQuery.extend({ each: function( object, callback, args ) { var name, i = 0, length = object.length, - isObj = length === undefined || jQuery.isFunction(object); + isObj = length === undefined || jQuery.isFunction( object ); if ( args ) { if ( isObj ) { @@ -638,8 +654,11 @@ jQuery.extend({ } } } else { - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } } } @@ -667,10 +686,8 @@ jQuery.extend({ if ( array != null ) { // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - var type = jQuery.type(array); + var type = jQuery.type( array ); if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { push.call( ret, array ); @@ -682,14 +699,22 @@ jQuery.extend({ return ret; }, - inArray: function( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } + inArray: function( elem, array, i ) { + var len; - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } } } @@ -697,13 +722,14 @@ jQuery.extend({ }, merge: function( first, second ) { - var i = first.length, j = 0; + var i = first.length, + j = 0; if ( typeof second.length === "number" ) { for ( var l = second.length; j < l; j++ ) { first[ i++ ] = second[ j ]; } - + } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; @@ -733,57 +759,72 @@ jQuery.extend({ // arg is for internal usage only map: function( elems, callback, arg ) { - var ret = [], value; + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); - if ( value != null ) { - ret[ ret.length ] = value; + if ( value != null ) { + ret[ ret.length ] = value; + } } } + // Flatten any nested arrays return ret.concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, - proxy: function( fn, proxy, thisObject ) { - if ( arguments.length === 2 ) { - if ( typeof proxy === "string" ) { - thisObject = fn; - fn = thisObject[ proxy ]; - proxy = undefined; + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } - } else if ( proxy && !jQuery.isFunction( proxy ) ) { - thisObject = proxy; - proxy = undefined; - } + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; } - if ( !proxy && fn ) { + // Simulated bind + var args = slice.call( arguments, 2 ), proxy = function() { - return fn.apply( thisObject || this, arguments ); + return fn.apply( context, args.concat( slice.call( arguments ) ) ); }; - } // Set the guid of unique handler to the same of original handler, so it can be removed - if ( fn ) { - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - } + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - // So proxy can be declared as an argument return proxy; }, // Mutifunctional method to get and set values to a collection - // The value/s can be optionally by executed if its a function + // The value/s can optionally be executed if it's a function access: function( elems, key, value, exec, fn, pass ) { var length = elems.length; - + // Setting many attributes if ( typeof key === "object" ) { for ( var k in key ) { @@ -791,25 +832,25 @@ jQuery.extend({ } return elems; } - + // Setting one attribute if ( value !== undefined ) { // Optionally, function values get executed if exec is true exec = !pass && exec && jQuery.isFunction(value); - + for ( var i = 0; i < length; i++ ) { fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); } - + return elems; } - + // Getting an attribute return length ? fn( elems[0], key ) : undefined; }, now: function() { - return (new Date()).getTime(); + return ( new Date() ).getTime(); }, // Use of jQuery.browser is frowned upon. @@ -826,6 +867,27 @@ jQuery.extend({ return { browser: match[1] || "", version: match[2] || "0" }; }, + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + browser: {} }); @@ -845,15 +907,8 @@ if ( jQuery.browser.webkit ) { jQuery.browser.safari = true; } -if ( indexOf ) { - jQuery.inArray = function( elem, array ) { - return indexOf.call( array, elem ); - }; -} - -// Verify that \s matches non-breaking spaces -// (IE fails on this test) -if ( !rwhite.test( "\xA0" ) ) { +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { trimLeft = /^[\s\xA0]+/; trimRight = /[\s\xA0]+$/; } @@ -897,37 +952,420 @@ function doScrollCheck() { jQuery.ready(); } -// Expose jQuery to the global object -return (window.jQuery = window.$ = jQuery); +return jQuery; })(); -(function( jQuery ) { -(function() { - jQuery.support = {}; +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!memory; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + - var root = document.documentElement, - script = document.createElement("script"), - div = document.createElement("div"), - id = "script" + jQuery.now(); - div.style.display = "none"; - div.innerHTML = "
                                a"; - var all = div.getElementsByTagName("*"), - a = div.getElementsByTagName("a")[0], - select = document.createElement("select"), - opt = select.appendChild( document.createElement("option") ); +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + marginDiv, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
                                a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; // Can't get basic test support if ( !all || !all.length || !a ) { - return; + return {}; } - jQuery.support = { + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, + leadingWhitespace: ( div.firstChild.nodeType === 3 ), // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables @@ -938,17 +1376,17 @@ return (window.jQuery = window.$ = jQuery); htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute - // (IE uses .cssText insted) - style: /red/.test( a.getAttribute("style") ), + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", + hrefNormalized: ( a.getAttribute("href") === "/a" ), // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), + opacity: /^0.55/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) @@ -957,91 +1395,151 @@ return (window.jQuery = window.$ = jQuery); // Make sure that if no value is specified for a checkbox // that it defaults to "on". // (WebKit defaults to "" instead) - checkOn: div.getElementsByTagName("input")[0].value === "on", + checkOn: ( input.value === "on" ), // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + // Will be defined later - optDisabled: false, - checkClone: false, - scriptEval: false, + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, noCloneEvent: true, - boxModel: null, inlineBlockNeedsLayout: false, shrinkWrapBlocks: false, - reliableHiddenOffsets: true + reliableMarginRight: true }; + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as diabled) + // (WebKit marks them as disabled) select.disabled = true; - jQuery.support.optDisabled = !opt.disabled; + support.optDisabled = !opt.disabled; - script.type = "text/javascript"; + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e) {} - - root.insertBefore( script, root.firstChild ); - - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support.scriptEval = true; - delete window[ id ]; + delete div.test; + } catch( e ) { + support.deleteExpando = false; } - root.removeChild( script ); - - if ( div.attachEvent && div.fireEvent ) { - div.attachEvent("onclick", function click() { + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { // Cloning a node shouldn't copy over any // bound event handlers (IE does this) - jQuery.support.noCloneEvent = false; - div.detachEvent("onclick", click); + support.noCloneEvent = false; }); - div.cloneNode(true).fireEvent("onclick"); + div.cloneNode( true ).fireEvent( "onclick" ); } - div = document.createElement("div"); - div.innerHTML = ""; + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; - var fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); + input.setAttribute("checked", "checked"); + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); // WebKit doesn't clone checked state correctly in fragments - jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + div.innerHTML = ""; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } - // Figure out if the W3C box model works as expected - // document.body must exist before we can do this - jQuery(function() { - var div = document.createElement("div"); - div.style.width = div.style.paddingLeft = "1px"; + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } - document.body.appendChild( div ); - jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + fragment.removeChild( div ); - if ( "zoom" in div.style ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.style.display = "inline"; - div.style.zoom = 1; - jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + // Null elements to avoid leaks in IE + fragment = select = opt = marginDiv = div = input = null; - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = ""; - div.innerHTML = "
                                "; - jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + conMarginTop, ptlm, vb, style, html, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; } - div.innerHTML = "
                                t
                                "; - var tds = div.getElementsByTagName("td"); + conMarginTop = 1; + ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; + vb = "visibility:hidden;border:0;"; + style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; + html = "
                                " + + "" + + "
                                "; + + container = document.createElement("div"); + container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); // Check if table cells still have offsetWidth/Height when they are set // to display:none and there are still other visible table cells in a @@ -1050,61 +1548,77 @@ return (window.jQuery = window.$ = jQuery); // display:none (it is still safe to use offsets if a parent element is // hidden; don safety goggles and see bug #4512 for more information). // (only IE 8 fails this test) - jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + div.innerHTML = "
                                t
                                "; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); - tds[0].style.display = ""; - tds[1].style.display = "none"; + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; // Check if empty table cells still have offsetWidth/Height - // (IE < 8 fail this test) - jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Figure out if the W3C box model works as expected div.innerHTML = ""; + div.style.width = div.style.paddingLeft = "1px"; + jQuery.boxModel = support.boxModel = div.offsetWidth === 2; - document.body.removeChild( div ).style.display = "none"; - div = tds = null; - }); - - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - var eventSupported = function( eventName ) { - var el = document.createElement("div"); - eventName = "on" + eventName; + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); - var isSupported = (eventName in el); - if ( !isSupported ) { - el.setAttribute(eventName, "return;"); - isSupported = typeof el[eventName] === "function"; + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
                                "; + support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); } - el = null; - return isSupported; - }; + div.style.cssText = ptlm + vb; + div.innerHTML = html; - jQuery.support.submitBubbles = eventSupported("submit"); - jQuery.support.changeBubbles = eventSupported("change"); + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; - // release memory in IE - root = script = div = all = a = null; + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + body.removeChild( container ); + div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; })(); -jQuery.props = { - "for": "htmlFor", - "class": "className", - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - colspan: "colSpan", - tabindex: "tabIndex", - usemap: "useMap", - frameborder: "frameBorder" -}; -})( jQuery ); -(function( jQuery ) { -var windowData = {}, - rbrace = /^(?:\{.*\}|\[.*\])$/; + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; jQuery.extend({ cache: {}, @@ -1112,8 +1626,9 @@ jQuery.extend({ // Please use with caution uuid: 0, - // Unique for each copy of jQuery on the page - expando: "jQuery" + jQuery.now(), + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. @@ -1124,103 +1639,214 @@ jQuery.extend({ "applet": true }, - data: function( elem, name, data ) { + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } - elem = elem == window ? - windowData : - elem; + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", - var isNode = elem.nodeType, - id = isNode ? elem[ jQuery.expando ] : null, - cache = jQuery.cache, thisCache; + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, - if ( isNode && !id && typeof name === "string" && data === undefined ) { - return; - } + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, - // Get the data from the object directly - if ( !isNode ) { - cache = elem; + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; - // Compute a unique ID for the element - } else if ( !id ) { - elem[ jQuery.expando ] = id = ++jQuery.uuid; + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; } - // Avoid generating a new cache unless none exists and we - // want to manipulate it. - if ( typeof name === "object" ) { + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache if ( isNode ) { - cache[ id ] = jQuery.extend(cache[ id ], name); - + elem[ internalKey ] = id = ++jQuery.uuid; } else { - jQuery.extend( cache, name ); + id = internalKey; } + } - } else if ( isNode && !cache[ id ] ) { + if ( !cache[ id ] ) { cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } } - thisCache = isNode ? cache[ id ] : cache; + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } - // Prevent overriding the named cache with undefined values if ( data !== undefined ) { - thisCache[ name ] = data; + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; } - return typeof name === "string" ? thisCache[ name ] : thisCache; + return ret; }, - removeData: function( elem, name ) { + removeData: function( elem, name, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } - elem = elem == window ? - windowData : - elem; + var thisCache, i, l, - var isNode = elem.nodeType, - id = isNode ? elem[ jQuery.expando ] : elem, - cache = jQuery.cache, - thisCache = isNode ? cache[ id ] : id; + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } - // If we want to remove a specific section of the element's data if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + if ( thisCache ) { - // Remove the section of cache data - delete thisCache[ name ]; - // If we've removed all the data, remove the element's cache - if ( isNode && jQuery.isEmptyObject(thisCache) ) { - jQuery.removeData( elem ); + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; } } + } - // Otherwise, we want to remove all of the element's data - } else { - if ( isNode && jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; - } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } - // Completely remove the data cache - } else if ( isNode ) { - delete cache[ id ]; + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } - // Remove all fields from the object + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); } else { - for ( var n in elem ) { - delete elem[ n ]; - } + elem[ internalKey ] = null; } } }, + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + // A method for determining if a DOM node can handle the data expando acceptData: function( elem ) { if ( elem.nodeName ) { @@ -1237,8 +1863,29 @@ jQuery.extend({ jQuery.fn.extend({ data: function( key, value ) { + var parts, attr, name, + data = null; + if ( typeof key === "undefined" ) { - return this.length ? jQuery.data( this[0] ) : null; + if ( this.length ) { + data = jQuery.data( this[0] ); + + if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { + attr = this[0].attributes; + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( this[0], name, data[ name ] ); + } + } + jQuery._data( this[0], "parsedAttrs", true ); + } + } + + return data; } else if ( typeof key === "object" ) { return this.each(function() { @@ -1246,35 +1893,16 @@ jQuery.fn.extend({ }); } - var parts = key.split("."); + parts = key.split("."); parts[1] = parts[1] ? "." + parts[1] : ""; if ( value === undefined ) { - var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); // Try to fetch any internally stored data first if ( data === undefined && this.length ) { data = jQuery.data( this[0], key ); - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && this[0].nodeType === 1 ) { - data = this[0].getAttribute( "data-" + key ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - !jQuery.isNaN( data ) ? parseFloat( data ) : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - } else { - data = undefined; - } - } + data = dataAttr( this[0], key, data ); } return data === undefined && parts[1] ? @@ -1283,11 +1911,12 @@ jQuery.fn.extend({ } else { return this.each(function() { - var $this = jQuery( this ), args = [ parts[0], value ]; + var self = jQuery( this ), + args = [ parts[0], value ]; - $this.triggerHandler( "setData" + parts[1] + "!", args ); + self.triggerHandler( "setData" + parts[1] + "!", args ); jQuery.data( this, key, value ); - $this.triggerHandler( "changeData" + parts[1] + "!", args ); + self.triggerHandler( "changeData" + parts[1] + "!", args ); }); } }, @@ -1299,37 +1928,127 @@ jQuery.fn.extend({ } }); -})( jQuery ); -(function( jQuery ) { +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { -jQuery.extend({ - queue: function( elem, type, data ) { - if ( !elem ) { - return; + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; } + } - type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type ); + return data; +} - // Speed up dequeue by getting out quickly if this is just a lookup - if ( !data ) { - return q || []; +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; } + } - if ( !q || jQuery.isArray(data) ) { - q = jQuery.data( elem, type, jQuery.makeArray(data) ); + return true; +} - } else { - q.push( data ); + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); - return q; + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } }, dequeue: function( elem, type ) { type = type || "fx"; - var queue = jQuery.queue( elem, type ), fn = queue.shift(); + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { @@ -1340,12 +2059,18 @@ jQuery.extend({ // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { - queue.unshift("inprogress"); + queue.unshift( "inprogress" ); } - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); } } }); @@ -1360,7 +2085,7 @@ jQuery.fn.extend({ if ( data === undefined ) { return jQuery.queue( this[0], type ); } - return this.each(function( i ) { + return this.each(function() { var queue = jQuery.queue( this, type, data ); if ( type === "fx" && queue[0] !== "inprogress" ) { @@ -1373,75 +2098,122 @@ jQuery.fn.extend({ jQuery.dequeue( this, type ); }); }, - // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; }); }, - clearQueue: function( type ) { return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise(); } }); -})( jQuery ); -(function( jQuery ) { -var rclass = /[\n\t]/g, - rspaces = /\s+/, + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, rreturn = /\r/g, - rspecialurl = /^(?:href|src|style)$/, rtype = /^(?:button|input)$/i, rfocusable = /^(?:button|input|object|select|textarea)$/i, rclickable = /^a(?:rea)?$/i, - rradiocheck = /^(?:radio|checkbox)$/i; + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; jQuery.fn.extend({ attr: function( name, value ) { return jQuery.access( this, name, value, true, jQuery.attr ); }, - removeAttr: function( name, fn ) { - return this.each(function(){ - jQuery.attr( this, name, "" ); - if ( this.nodeType === 1 ) { - this.removeAttribute( name ); - } + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.prop ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} }); }, addClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.addClass( value.call(this, i, self.attr("class")) ); + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); }); } if ( value && typeof value === "string" ) { - var classNames = (value || "").split( rspaces ); + classNames = value.split( rspace ); - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; if ( elem.nodeType === 1 ) { - if ( !elem.className ) { + if ( !elem.className && classNames.length === 1 ) { elem.className = value; } else { - var className = " " + elem.className + " ", setClass = elem.className; - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { - setClass += " " + classNames[c]; + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; } } elem.className = jQuery.trim( setClass ); @@ -1454,24 +2226,25 @@ jQuery.fn.extend({ }, removeClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.removeClass( value.call(this, i, self.attr("class")) ); + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); }); } if ( (value && typeof value === "string") || value === undefined ) { - var classNames = (value || "").split( rspaces ); + classNames = ( value || "" ).split( rspace ); - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; if ( elem.nodeType === 1 && elem.className ) { if ( value ) { - var className = (" " + elem.className + " ").replace(rclass, " "); - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[c] + " ", " "); + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); } elem.className = jQuery.trim( className ); @@ -1486,21 +2259,23 @@ jQuery.fn.extend({ }, toggleClass: function( value, stateVal ) { - var type = typeof value, isBool = typeof stateVal === "boolean"; + var type = typeof value, + isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { - return this.each(function(i) { - var self = jQuery(this); - self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names - var className, i = 0, self = jQuery(this), + var className, + i = 0, + self = jQuery( this ), state = stateVal, - classNames = value.split( rspaces ); + classNames = value.split( rspace ); while ( (className = classNames[ i++ ]) ) { // check each className given, space seperated list @@ -1511,19 +2286,21 @@ jQuery.fn.extend({ } else if ( type === "undefined" || type === "boolean" ) { if ( this.className ) { // store className if set - jQuery.data( this, "__className__", this.className ); + jQuery._data( this, "__className__", this.className ); } // toggle whole className - this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { - if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { return true; } } @@ -1532,78 +2309,42 @@ jQuery.fn.extend({ }, val: function( value ) { - if ( !arguments.length ) { - var elem = this[0]; + var hooks, ret, isFunction, + elem = this[0]; + if ( !arguments.length ) { if ( elem ) { - if ( jQuery.nodeName( elem, "option" ) ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { - - // Get the specific value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } + hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; - return values; + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; } - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { - return elem.getAttribute("value") === null ? "on" : elem.value; - } - - - // Everything else, we just grab the value - return (elem.value || "").replace(rreturn, ""); + ret = elem.value; + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; } - return undefined; + return; } - var isFunction = jQuery.isFunction(value); + isFunction = jQuery.isFunction( value ); - return this.each(function(i) { - var self = jQuery(this), val = value; + return this.each(function( i ) { + var self = jQuery(this), val; if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { - val = value.call(this, i, self.val()); + val = value.call( this, i, self.val() ); + } else { + val = value; } // Treat null/undefined as ""; convert numbers to string @@ -1611,34 +2352,91 @@ jQuery.fn.extend({ val = ""; } else if ( typeof val === "number" ) { val += ""; - } else if ( jQuery.isArray(val) ) { - val = jQuery.map(val, function (value) { + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { return value == null ? "" : value + ""; }); } - if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { - this.checked = jQuery.inArray( self.val(), val ) >= 0; + hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, - } else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(val); + set: function( elem, value ) { + var values = jQuery.makeArray( value ); - jQuery( "option", this ).each(function() { + jQuery(elem).find("option").each(function() { this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; }); if ( !values.length ) { - this.selectedIndex = -1; + elem.selectedIndex = -1; } - - } else { - this.value = val; + return values; } - }); - } -}); + } + }, -jQuery.extend({ attrFn: { val: true, css: true, @@ -1649,246 +2447,475 @@ jQuery.extend({ height: true, offset: true }, - + attr: function( elem, name, value, pass ) { - // don't set attributes on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; } if ( pass && name in jQuery.attrFn ) { - return jQuery(elem)[name](value); + return jQuery( elem )[ name ]( value ); } - var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), - // Whether we are setting (or getting) - set = value !== undefined; + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } - // Try to normalize/fix the name - name = notxml && jQuery.props[ name ] || name; + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - // Only do all the following if this is a node (faster for style) - if ( elem.nodeType === 1 ) { - // These attributes require special treatment - var special = rspecialurl.test( name ); + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if ( name === "selected" && !jQuery.support.optSelected ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } + if ( value !== undefined ) { - // If applicable, access the attribute via the DOM 0 way - // 'in' checks fail in Blackberry 4.7 #6931 - if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { - if ( set ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; - if ( value === null ) { - if ( elem.nodeType === 1 ) { - elem.removeAttribute( name ); - } + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; - } else { - elem[ name ] = value; + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + + // See #9699 for explanation of this approach (setting first, then removal) + jQuery.attr( elem, name, "" ); + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( rboolean.test( name ) && propName in elem ) { + elem[ propName ] = false; } } + } + } + }, - // browsers index elements by id/name on forms, give priority to attributes. - if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { - return elem.getAttributeNode( name ).nodeValue; + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + propHooks: { + tabIndex: { + get: function( elem ) { // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name === "tabIndex" ) { - var attributeNode = elem.getAttributeNode( "tabIndex" ); + var attributeNode = elem.getAttributeNode("tabindex"); - return attributeNode && attributeNode.specified ? - attributeNode.value : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); - return elem[ name ]; +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); } + return ( ret.nodeValue = value + "" ); + } + }; - if ( !jQuery.support.style && notxml && name === "style" ) { - if ( set ) { - elem.style.cssText = "" + value; + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; } + } + }); + }); - return elem.style.cssText; + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; } + nodeHook.set( elem, value, name ); + } + }; +} - if ( set ) { - // convert the value to a string (all browsers do this but IE) see #1070 - elem.setAttribute( name, "" + value ); + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; - // Ensure that missing attributes return undefined - // Blackberry 4.7 returns "" from getAttribute #6938 - if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { - return undefined; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } } + return null; + } + }); +} - var attr = !jQuery.support.hrefNormalized && notxml && special ? - // Some attributes require a special call on IE - elem.getAttribute( name, 2 ) : - elem.getAttribute( name ); +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} - // Non-existent attributes return null, we normalize to undefined - return attr === null ? undefined : attr; +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } } - } + }); }); -})( jQuery ); -(function( jQuery ) { -var rnamespaces = /\.(.*)$/, - rformElems = /^(?:textarea|input|select)$/i, - rperiod = /\./g, - rspace = / /g, - rescape = /[^\w\s.|`]/g, - fcleanup = function( nm ) { - return nm.replace(rescape, "\\$&"); - }, - focusCounts = { focusin: 0, focusout: 0 }; +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /\bhover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + /* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } + add: function( elem, types, handler, data, selector ) { - // For whatever reason, IE has trouble passing the window object - // around, causing it to be cloned in the process - if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { - elem = window; - } + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; - if ( handler === false ) { - handler = returnFalse; + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; } - var handleObjIn, handleObj; - + // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; } - // Make sure that the function being executed has a unique ID + // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } - // Init the element's event structure - var elemData = jQuery.data( elem ); - - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements - if ( !elemData ) { - return; - } - - // Use a key less likely to result in collisions for plain JS objects. - // Fixes bug #7150. - var eventKey = elem.nodeType ? "events" : "__events__", - events = elemData[ eventKey ], - eventHandle = elemData.handle; - - if ( typeof events === "function" ) { - // On plain objects events is a fn that holds the the data - // which prevents this data from being JSON serialized - // the function does not need to be called, it just contains the data - eventHandle = events.handle; - events = events.events; - - } else if ( !events ) { - if ( !elem.nodeType ) { - // On plain objects, create a fn that acts as the holder - // of the values to avoid JSON serialization of event data - elemData[ eventKey ] = elemData = function(){}; - } - + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { elemData.events = events = {}; } - + eventHandle = elemData.handle; if ( !eventHandle ) { - elemData.handle = eventHandle = function() { - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - return typeof jQuery !== "undefined" && !jQuery.event.triggered ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; } - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; - // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); - - var type, i = 0, namespaces; - - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); - } else { - namespaces = []; - handleObj.namespace = ""; - } + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; - handleObj.type = type; - if ( !handleObj.guid ) { - handleObj.guid = handler.guid; - } + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; - // Init the event handler queue + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; if ( !handlers ) { handlers = events[ type ] = []; + handlers.delegateCount = 0; - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false + // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { @@ -1899,19 +2926,23 @@ jQuery.event = { } } } - - if ( special.add ) { - special.add.call( elem, handleObj ); + + if ( special.add ) { + special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } - // Add the function to the element's handler list - handlers.push( handleObj ); + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } - // Keep track of which events have been used, for global triggering + // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } @@ -1922,294 +2953,307 @@ jQuery.event = { global: {}, // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - if ( handler === false ) { - handler = returnFalse; - } - - var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - eventKey = elem.nodeType ? "events" : "__events__", - elemData = jQuery.data( elem ), - events = elemData && elemData[ eventKey ]; - - if ( !elemData || !events ) { - return; - } - - if ( typeof events === "function" ) { - elemData = events; - events = events.events; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } - - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; + remove: function( elem, types, handler, selector, mappedTypes ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + if ( !elemData || !(events = elemData.events) ) { return; } - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; - - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } - continue; } special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - for ( j = pos || 0; j < eventType.length; j++ ) { + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } + if ( handleObj.selector ) { + eventType.delegateCount--; } - - if ( pos != null ) { - break; + if ( special.remove ) { + special.remove.call( elem, handleObj ); } } } - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } - ret = null; delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; + handle = elemData.handle; if ( handle ) { handle.elem = null; } - delete elemData.events; - delete elemData.handle; + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, - if ( typeof elemData === "function" ) { - jQuery.removeData( elem, eventKey ); + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, - } else if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem ); - } + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; } - }, - // bubbling is internal - trigger: function( event, data, elem /*, bubbling */ ) { // Event object or event type var type = event.type || event, - bubbling = arguments[3]; + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[ jQuery.expando ] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } - // Handle a global trigger - if ( !elem ) { - // Don't bubble custom events when global (to avoid too much overhead) - event.stopPropagation(); - - // Only trigger if we've ever bound an event for it - if ( jQuery.event.global[ type ] ) { - jQuery.each( jQuery.cache, function() { - if ( this.events && this.events[type] ) { - jQuery.event.trigger( event, data, this.handle.elem ); - } - }); - } - } + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } - // Handle triggering a single element + // 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 ); - // don't do events on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; - // Clean up in case it is reused - event.result = undefined; - event.target = elem; + // Handle a global trigger + if ( !elem ) { - // Clone the incoming data, if any - data = jQuery.makeArray( data ); - data.unshift( event ); + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; } - event.currentTarget = elem; + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } - // Trigger the event, it is assumed that "handle" is a function - var handle = elem.nodeType ? - jQuery.data( elem, "handle" ) : - (jQuery.data( elem, "__events__" ) || {}).handle; + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); - if ( handle ) { - handle.apply( elem, data ); + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; } - var parent = elem.parentNode || elem.ownerDocument; + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - // Trigger an inline bound script - try { - if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { - if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { - event.result = false; - event.preventDefault(); - } + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (inlineError) {} + cur = eventPath[i][0]; + event.type = eventPath[i][1]; - if ( !event.isPropagationStopped() && parent ) { - jQuery.event.trigger( event, data, parent, true ); + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; - } else if ( !event.isDefaultPrevented() ) { - var target = event.target, old, targetType = type.replace(rnamespaces, ""), - isClick = jQuery.nodeName(target, "a") && targetType === "click", - special = jQuery.event.special[ targetType ] || {}; + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { - if ( (!special._default || special._default.call( elem, event ) === false) && - !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - try { - if ( target[ targetType ] ) { - // Make sure that we don't accidentally re-trigger the onFOO events - old = target[ "on" + targetType ]; + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { - if ( old ) { - target[ "on" + targetType ] = null; - } + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; - jQuery.event.triggered = true; - target[ targetType ](); + if ( old ) { + elem[ ontype ] = null; } - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (triggerError) {} + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; - if ( old ) { - target[ "on" + targetType ] = old; + if ( old ) { + elem[ ontype ] = old; + } } - - jQuery.event.triggered = false; } } + + return event.result; }, - handle: function( event ) { - var all, handlers, namespaces, namespace_sort = [], namespace_re, events, args = jQuery.makeArray( arguments ); + dispatch: function( event ) { - event = args[0] = jQuery.event.fix( event || window.event ); - event.currentTarget = this; + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); - // Namespaced event handlers - all = event.type.indexOf(".") < 0 && !event.exclusive; + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; - if ( !all ) { - namespaces = event.type.split("."); - event.type = namespaces.shift(); - namespace_sort = namespaces.slice(0).sort(); - namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); - } + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Determine handlers that should run if there are delegated events + // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { - event.namespace = event.namespace || namespace_sort.join("."); + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; - events = jQuery.data(this, this.nodeType ? "events" : "__events__"); + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } - if ( typeof events === "function" ) { - events = events.events; + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); } - handlers = (events || {})[ event.type ]; + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; - if ( events && handlers ) { - // Clone the handlers to prevent manipulation - handlers = handlers.slice(0); + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { - // Filter the functions by class - if ( all || namespace_re.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; event.data = handleObj.data; event.handleObj = handleObj; - - var oldHandle = event.handled, - ret = handleObj.handler.apply( this, args ); - event.handled = event.handled ===null || handleObj.handler === liveHandler ? oldHandle : true + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); if ( ret !== undefined ) { event.result = ret; @@ -2218,10 +3262,6 @@ jQuery.event = { event.stopPropagation(); } } - - if ( event.isImmediatePropagationStopped() ) { - break; - } } } } @@ -2229,87 +3269,109 @@ jQuery.event = { return event.result; }, - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + event = jQuery.Event( originalEvent ); - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; + for ( i = copy.length; i; ) { + prop = copy[ --i ]; event[ prop ] = originalEvent[ prop ]; } - // Fix target property, if necessary + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) if ( !event.target ) { - event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + event.target = originalEvent.srcElement || document; } - // check if target is a textnode (safari) + // Target should not be a text node (#504, Safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; } - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, body = document.body; - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { - event.which = event.charCode != null ? event.charCode : event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { - event.metaKey = event.ctrlKey; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; }, - // Deprecated, use jQuery.guid instead - guid: 1E8, - - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, - special: { ready: { // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop + setup: jQuery.bindReady }, - live: { - add: function( handleObj ) { - jQuery.event.add( this, - liveConvert( handleObj.origType, handleObj.selector ), - jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); - }, + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, - remove: function( handleObj ) { - jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); - } + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" }, beforeunload: { @@ -2326,39 +3388,75 @@ jQuery.event = { } } } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } } }; +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, handle, false ); } - } : + } : function( elem, type, handle ) { if ( elem.detachEvent ) { elem.detachEvent( "on" + type, handle ); } }; -jQuery.Event = function( src ) { +jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { - return new jQuery.Event( src ); + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + // Event type } else { this.type = src; } - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = jQuery.now(); + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; @@ -2381,7 +3479,7 @@ jQuery.Event.prototype = { if ( !e ) { return; } - + // if preventDefault exists run it on the original event if ( e.preventDefault ) { e.preventDefault(); @@ -2414,329 +3512,305 @@ jQuery.Event.prototype = { isImmediatePropagationStopped: returnFalse }; -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - // Check if mouse(over|out) are still within the same parent element - var parent = event.relatedTarget; - - // Firefox sometimes assigns relatedTarget a XUL element - // which we cannot access the parentNode property of - try { - // Traverse up the tree - while ( parent && parent !== this ) { - parent = parent.parentNode; - } - - if ( parent !== this ) { - // set the correct event type - event.type = event.data; - - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } - - // assuming we've left the element since we most likely mousedover a xul element - } catch(e) { } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events +// Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; } }; }); -// submit delegation +// IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( this.nodeName.toLowerCase() !== "form" ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - e.liveFired = undefined; - return trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - e.liveFired = undefined; - return trigger( "submit", this, arguments ); - } - }); + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, - } else { + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { return false; } - }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); } }; - } -// change delegation, happens here so we have bind. +// IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { - var changeFilters, - - getVal = function( elem ) { - var type = elem.type, val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( elem.nodeName.toLowerCase() === "select" ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery.data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery.data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - e.liveFired = undefined; - return jQuery.event.trigger( e, arguments[1], elem ); - } - }; - jQuery.event.special.change = { - filters: { - focusout: testChange, - - beforedeactivate: testChange, - click: function( e ) { - var elem = e.target, type = elem.type; + setup: function() { - if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { - return testChange.call( this, e ); + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = elem.type; + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; - if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - return testChange.call( this, e ); + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information - beforeactivate: function( e ) { - var elem = e.target; - jQuery.data( elem, "_change_data", getVal(elem) ); - } + }); }, - setup: function( data, namespaces ) { - if ( this.type === "file" ) { - return false; - } + handle: function( event ) { + var elem = event.target; - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); } - - return rformElems.test( this.nodeName ); }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); + teardown: function() { + jQuery.event.remove( this, "._change" ); return rformElems.test( this.nodeName ); } }; - - changeFilters = jQuery.event.special.change.filters; - - // Handle when the input is .focus()'d - changeFilters.focus = changeFilters.beforeactivate; -} - -function trigger( type, elem, args ) { - args[0].type = type; - return jQuery.event.handle.apply( elem, args ); } // Create "bubbling" focus and blur events -if ( document.addEventListener ) { +if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + jQuery.event.special[ fix ] = { setup: function() { - if ( focusCounts[fix]++ === 0 ) { + if ( attaches++ === 0 ) { document.addEventListener( orig, handler, true ); } - }, - teardown: function() { - if ( --focusCounts[fix] === 0 ) { + }, + teardown: function() { + if ( --attaches === 0 ) { document.removeEventListener( orig, handler, true ); } } }; - - function handler( e ) { - e = jQuery.event.fix( e ); - e.type = fix; - return jQuery.event.trigger( e, null, e.target ); - } }); } -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); - } - return this; - } - - if ( jQuery.isFunction( data ) || data === false ) { - fn = data; - data = undefined; - } - - var handler = name === "one" ? jQuery.proxy( fn, function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }) : fn; +jQuery.fn.extend({ - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); } + return this; } - return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on.call( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); return this; }, - + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); + return this.on( types, selector, data, fn ); }, - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); }, - + trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, - triggerHandler: function( type, data ) { if ( this[0] ) { - var event = jQuery.Event( type ); - event.preventDefault(); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return event.result; + return jQuery.event.trigger( type, data, this[0], true ); } }, toggle: function( fn ) { // Save reference to arguments for access in closure - var args = arguments, i = 1; + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; while ( i < args.length ) { - jQuery.proxy( fn, args[ i++ ] ); + args[ i++ ].guid = guid; } - return this.click( jQuery.proxy( fn, function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - })); + return this.click( toggler ); }, hover: function( fnOver, fnOut ) { @@ -2744,167 +3818,9 @@ jQuery.fn.extend({ } }); -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; - -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); - - if ( typeof types === "object" && !types.preventDefault ) { - for ( var key in types ) { - context[ name ]( key, data, types[key], selector ); - } - - return this; - } - - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } - - types = (types || "").split(" "); - - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( type === "focus" || type === "blur" ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - for ( var j = 0, l = context.length; j < l; j++ ) { - jQuery.event.add( context[j], "live." + liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - } - - } else { - // unbind live handler - context.unbind( "live." + liveConvert( type, selector ), fn ); - } - } - - return this; - }; -}); - -function liveHandler( event ) { - var stop, maxLevel, elems = [], selectors = [], - related, match, handleObj, elem, j, i, l, data, close, namespace, ret, - events = jQuery.data( this, this.nodeType ? "events" : "__events__" ); - - if ( typeof events === "function" ) { - events = events.events; - } - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) - if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { - return; - } - - if ( event.namespace ) { - namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - close = match[i]; - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { - elem = close.elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - event.type = handleObj.preType; - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj, level: close.level }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - - if ( maxLevel && match.level > maxLevel ) { - break; - } - - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - var oldHandle = event.handled; - ret = match.handleObj.origHandler.apply( match.elem, arguments ); - event.handled = event.handled === null ? oldHandle : true; - - if ( ret === false || event.isPropagationStopped() ) { - maxLevel = match.level; - - if ( ret === false ) { - stop = false; - } - } - } - - return stop; -} - -function liveConvert( type, selector ) { - return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); -} - jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { @@ -2914,57 +3830,53 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl } return arguments.length > 0 ? - this.bind( name, data, fn ) : + this.on( name, null, data, fn ) : this.trigger( name ); }; if ( jQuery.attrFn ) { jQuery.attrFn[ name ] = true; } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } }); -// Prevent memory leaks in IE -// Window isn't included so as not to unbind existing unload events -// More info: -// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ -if ( window.attachEvent && !window.addEventListener ) { - jQuery(window).bind("unload", function() { - for ( var id in jQuery.cache ) { - if ( jQuery.cache[ id ].handle ) { - // Try/Catch is to handle iframes being unloaded, see #4280 - try { - jQuery.event.remove( jQuery.cache[ id ].handle.elem ); - } catch(e) {} - } - } - }); -} -})( jQuery ); + /*! - * Sizzle CSS Selector Engine - v1.0 - * Copyright 2009, The Dojo Foundation + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), done = 0, toString = Object.prototype.toString, hasDuplicate = false, - baseHasDuplicate = true; + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; // Here we check if the JavaScript engine is using some sort of // optimization where it does not always call our comparision // function. If that is the case, discard the hasDuplicate value. // Thus far that includes Google Chrome. -[0, 0].sort(function(){ +[0, 0].sort(function() { baseHasDuplicate = false; return 0; }); -var Sizzle = function(selector, context, results, seed) { +var Sizzle = function( selector, context, results, seed ) { results = results || []; context = context || document; @@ -2978,13 +3890,16 @@ var Sizzle = function(selector, context, results, seed) { return results; } - var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), - soFar = selector, ret, cur, pop, i; + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; // Reset the position of the chunker regexp (start from head) do { - chunker.exec(""); - m = chunker.exec(soFar); + chunker.exec( "" ); + m = chunker.exec( soFar ); if ( m ) { soFar = m[3]; @@ -2999,8 +3914,10 @@ var Sizzle = function(selector, context, results, seed) { } while ( m ); if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); + set = posProcess( parts[0] + parts[1], context, seed ); + } else { set = Expr.relative[ parts[0] ] ? [ context ] : @@ -3013,26 +3930,34 @@ var Sizzle = function(selector, context, results, seed) { selector += parts.shift(); } - set = posProcess( selector, set ); + set = posProcess( selector, set, seed ); } } + } else { // Take a shortcut and set the context if the root selector is an ID // (but not if it'll be faster if the inner selector is an ID) if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; } if ( context ) { ret = seed ? { expr: parts.pop(), set: makeArray(seed) } : Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; if ( parts.length > 0 ) { - checkSet = makeArray(set); + checkSet = makeArray( set ); + } else { prune = false; } @@ -3053,6 +3978,7 @@ var Sizzle = function(selector, context, results, seed) { Expr.relative[ cur ]( checkSet, pop, contextXML ); } + } else { checkSet = parts = []; } @@ -3069,12 +3995,14 @@ var Sizzle = function(selector, context, results, seed) { if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { results.push( set[i] ); } } + } else { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && checkSet[i].nodeType === 1 ) { @@ -3082,6 +4010,7 @@ var Sizzle = function(selector, context, results, seed) { } } } + } else { makeArray( checkSet, results ); } @@ -3094,15 +4023,15 @@ var Sizzle = function(selector, context, results, seed) { return results; }; -Sizzle.uniqueSort = function(results){ +Sizzle.uniqueSort = function( results ) { if ( sortOrder ) { hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); + results.sort( sortOrder ); if ( hasDuplicate ) { for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); } } } @@ -3111,31 +4040,32 @@ Sizzle.uniqueSort = function(results){ return results; }; -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); }; -Sizzle.matchesSelector = function(node, expr){ - return Sizzle(expr, null, null, [node]).length > 0; +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; }; -Sizzle.find = function(expr, context, isXML){ - var set; +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; if ( !expr ) { return []; } - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice(1,1); + left = match[1]; + match.splice( 1, 1 ); if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); + match[1] = (match[1] || "").replace( rBackslash, "" ); set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { expr = expr.replace( Expr.match[ type ], "" ); break; @@ -3145,20 +4075,29 @@ Sizzle.find = function(expr, context, isXML){ } if ( !set ) { - set = context.getElementsByTagName("*"); + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; } - return {set: set, expr: expr}; + return { set: set, expr: expr }; }; -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); while ( expr && set.length ) { - for ( var type in Expr.filter ) { + for ( type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var filter = Expr.filter[ type ], found, item, left = match[1]; + filter = Expr.filter[ type ]; + left = match[1]; + anyFound = false; match.splice(1,1); @@ -3176,23 +4115,26 @@ Sizzle.filter = function(expr, set, inplace, not){ if ( !match ) { anyFound = found = true; + } else if ( match === true ) { continue; } } if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; + pass = not ^ found; if ( inplace && found != null ) { if ( pass ) { anyFound = true; + } else { curLoop[i] = false; } + } else if ( pass ) { result.push( item ); anyFound = true; @@ -3221,6 +4163,7 @@ Sizzle.filter = function(expr, set, inplace, not){ if ( expr === old ) { if ( anyFound == null ) { Sizzle.error( expr ); + } else { break; } @@ -3233,35 +4176,82 @@ Sizzle.filter = function(expr, set, inplace, not){ }; Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; }; var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], + match: { ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ }, + leftMatch: {}, + attrMap: { "class": "className", "for": "htmlFor" }, + attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); } }, + relative: { "+": function(checkSet, part){ var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), + isTag = isPartStr && !rNonWord.test( part ), isPartStrNotTag = isPartStr && !isTag; if ( isTag ) { @@ -3282,23 +4272,29 @@ var Expr = Sizzle.selectors = { Sizzle.filter( part, checkSet, true ); } }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string", - elem, i = 0, l = checkSet.length; - if ( isPartStr && !/\W/.test(part) ) { + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { part = part.toLowerCase(); for ( ; i < l; i++ ) { elem = checkSet[i]; + if ( elem ) { var parent = elem.parentNode; checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; } } + } else { for ( ; i < l; i++ ) { elem = checkSet[i]; + if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : @@ -3311,31 +4307,38 @@ var Expr = Sizzle.selectors = { } } }, + "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; + var nodeCheck, + doneName = done++, + checkFn = dirCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { + if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); } }, + find: { - ID: function(match, context, isXML){ + ID: function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); // Check parentNode to catch when Blackberry 4.6 returns @@ -3343,9 +4346,11 @@ var Expr = Sizzle.selectors = { return m && m.parentNode ? [m] : []; } }, - NAME: function(match, context){ + + NAME: function( match, context ) { if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); + var ret = [], + results = context.getElementsByName( match[1] ); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { @@ -3356,13 +4361,16 @@ var Expr = Sizzle.selectors = { return ret.length === 0 ? null : ret; } }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } } }, preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; if ( isXML ) { return match; @@ -3370,10 +4378,11 @@ var Expr = Sizzle.selectors = { for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { if ( !inplace ) { result.push( elem ); } + } else if ( inplace ) { curLoop[i] = false; } @@ -3382,16 +4391,25 @@ var Expr = Sizzle.selectors = { return false; }, - ID: function(match){ - return match[1].replace(/\\/g, ""); + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); }, - TAG: function(match, curLoop){ - return match[1].toLowerCase(); + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); }, - CHILD: function(match){ + + CHILD: function( match ) { if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); @@ -3399,141 +4417,196 @@ var Expr = Sizzle.selectors = { match[2] = (test[1] + (test[2] || 1)) - 0; match[3] = test[3] - 0; } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } // TODO: Move to normal caching system match[0] = done++; return match; }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; } return match; }, - PSEUDO: function(match, curLoop, inplace, result, not){ + + PSEUDO: function( match, curLoop, inplace, result, not ) { if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); + } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { result.push.apply( result, ret ); } + return false; } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } return match; }, - POS: function(match){ + + POS: function( match ) { match.unshift( true ); + return match; } }, + filters: { - enabled: function(elem){ + enabled: function( elem ) { return elem.disabled === false && elem.type !== "hidden"; }, - disabled: function(elem){ + + disabled: function( elem ) { return elem.disabled === true; }, - checked: function(elem){ + + checked: function( elem ) { return elem.checked === true; }, - selected: function(elem){ + + selected: function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly - elem.parentNode.selectedIndex; + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + return elem.selected === true; }, - parent: function(elem){ + + parent: function( elem ) { return !!elem.firstChild; }, - empty: function(elem){ + + empty: function( elem ) { return !elem.firstChild; }, - has: function(elem, i, match){ + + has: function( elem, i, match ) { return !!Sizzle( match[3], elem ).length; }, - header: function(elem){ + + header: function( elem ) { return (/h\d/i).test( elem.nodeName ); }, - text: function(elem){ - return "text" === elem.type; + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; }, - radio: function(elem){ - return "radio" === elem.type; + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; }, - checkbox: function(elem){ - return "checkbox" === elem.type; + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; }, - file: function(elem){ - return "file" === elem.type; + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; }, - password: function(elem){ - return "password" === elem.type; + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; }, - submit: function(elem){ - return "submit" === elem.type; + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; }, - image: function(elem){ - return "image" === elem.type; + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; }, - reset: function(elem){ - return "reset" === elem.type; + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); }, - input: function(elem){ - return (/input|select|textarea|button/i).test(elem.nodeName); + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; } }, setFilters: { - first: function(elem, i){ + first: function( elem, i ) { return i === 0; }, - last: function(elem, i, match, array){ + + last: function( elem, i, match, array ) { return i === array.length - 1; }, - even: function(elem, i){ + + even: function( elem, i ) { return i % 2 === 0; }, - odd: function(elem, i){ + + odd: function( elem, i ) { return i % 2 === 1; }, - lt: function(elem, i, match){ + + lt: function( elem, i, match ) { return i < match[3] - 0; }, - gt: function(elem, i, match){ + + gt: function( elem, i, match ) { return i > match[3] - 0; }, - nth: function(elem, i, match){ + + nth: function( elem, i, match ) { return match[3] - 0 === i; }, - eq: function(elem, i, match){ + + eq: function( elem, i, match ) { return match[3] - 0 === i; } }, filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); + } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { var not = match[3]; @@ -3544,72 +4617,95 @@ var Expr = Sizzle.selectors = { } return true; + } else { - Sizzle.error( "Syntax error, unrecognized expression: " + name ); + Sizzle.error( name ); } }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) { return false; } } + if ( type === "first" ) { return true; } + node = elem; - case 'last': + + case "last": while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) { return false; } } + return true; - case 'nth': - var first = match[2], last = match[3]; + + case "nth": + first = match[2]; + last = match[3]; if ( first === 1 && last === 0 ) { return true; } - var doneName = match[0], - parent = elem.parentNode; + doneName = match[0]; + parent = elem.parentNode; - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } } - parent.sizcache = doneName; + + parent[ expando ] = doneName; } - var diff = elem.nodeIndex - last; + diff = elem.nodeIndex - last; + if ( first === 0 ) { return diff === 0; + } else { return ( diff % first === 0 && diff / first >= 0 ); } } }, - ID: function(elem, match){ + + ID: function( elem, match ) { return elem.nodeType === 1 && elem.getAttribute("id") === match; }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; }, - CLASS: function(elem, match){ + + CLASS: function( elem, match ) { return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, - ATTR: function(elem, match){ + + ATTR: function( elem, match ) { var name = match[1], - result = Expr.attrHandle[ name ] ? + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : @@ -3620,6 +4716,8 @@ var Expr = Sizzle.selectors = { return result == null ? type === "!=" : + !type && Sizzle.attr ? + result != null : type === "=" ? value === check : type === "*=" ? @@ -3638,8 +4736,10 @@ var Expr = Sizzle.selectors = { value === check || value.substr(0, check.length + 1) === check + "-" : false; }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); @@ -3658,7 +4758,7 @@ for ( var type in Expr.match ) { Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); } -var makeArray = function(array, results) { +var makeArray = function( array, results ) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { @@ -3677,17 +4777,20 @@ try { Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || [], i = 0; +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); + } else { if ( typeof array.length === "number" ) { for ( var l = array.length; i < l; i++ ) { ret.push( array[i] ); } + } else { for ( ; array[i]; i++ ) { ret.push( array[i] ); @@ -3714,18 +4817,28 @@ if ( document.documentElement.compareDocumentPosition ) { return a.compareDocumentPosition(b) & 4 ? -1 : 1; }; + } else { sortOrder = function( a, b ) { - var ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, - cur = aup, al, bl; - // The nodes are identical, we can exit early if ( a === b ) { hasDuplicate = true; return 0; + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + // If the nodes are siblings (or identical) we can do a quick check - } else if ( aup === bup ) { + if ( aup === bup ) { return siblingCheck( a, b ); // If no parents were found then the nodes are disconnected @@ -3785,56 +4898,45 @@ if ( document.documentElement.compareDocumentPosition ) { }; } -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } - - return ret; -}; - // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ // We're going to inject a fake input element with a specified name var form = document.createElement("div"), - id = "script" + (new Date()).getTime(); + id = "script" + (new Date()).getTime(), + root = document.documentElement; + form.innerHTML = ""; // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; root.insertBefore( form, root.firstChild ); // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) if ( document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ + Expr.find.ID = function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; } }; - Expr.filter.ID = function(elem, match){ + Expr.filter.ID = function( elem, match ) { var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); - root = form = null; // release memory in IE + + // release memory in IE + root = form = null; })(); (function(){ @@ -3847,8 +4949,8 @@ Sizzle.getText = function( elems ) { // Make sure no comments are found if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); // Filter out possible comments if ( match[1] === "*" ) { @@ -3869,19 +4971,25 @@ Sizzle.getText = function( elems ) { // Check to see if an attribute returns normalized href attributes div.innerHTML = ""; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); }; } - div = null; // release memory in IE + // release memory in IE + div = null; })(); if ( document.querySelectorAll ) { (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + div.innerHTML = "

                                "; // Safari can't handle uppercase or unicode characters when @@ -3890,35 +4998,85 @@ if ( document.querySelectorAll ) { return; } - Sizzle = function(query, context, extra, seed){ + Sizzle = function( query, context, extra, seed ) { context = context || document; // Only use querySelectorAll on non-XML documents // (ID selectors don't work in non-HTML documents) if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root // and working up from there (Thanks to Andrew Dupont for the technique) - if ( context.nodeType === 1 ) { - var old = context.id, id = context.id = "__sizzle__"; + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } try { - return makeArray( context.querySelectorAll( "#" + id + " " + query ), extra ); + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } } catch(pseudoError) { } finally { - if ( old ) { - context.id = old; - - } else { - context.removeAttribute( "id" ); + if ( !old ) { + oldContext.removeAttribute( "id" ); } } - - } else { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(qsaError) {} } } @@ -3929,33 +5087,51 @@ if ( document.querySelectorAll ) { Sizzle[ prop ] = oldSizzle[ prop ]; } - div = null; // release memory in IE + // release memory in IE + div = null; })(); } (function(){ var html = document.documentElement, - matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, - pseudoWorks = false; + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; - try { - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( document.documentElement, ":sizzle" ); + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); - } catch( pseudoError ) { - pseudoWorks = true; - } + } catch( pseudoError ) { + pseudoWorks = true; + } - if ( matches ) { Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { try { - if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) ) { - return matches.call( node, expr ); + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } } } catch(e) {} + } - return Sizzle(expr, null, null, [node]).length > 0; + return Sizzle(expr, null, null, [node]).length > 0; }; } })(); @@ -3979,30 +5155,33 @@ if ( document.querySelectorAll ) { } Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { + Expr.find.CLASS = function( match, context, isXML ) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; - div = null; // release memory in IE + // release memory in IE + div = null; })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; + if ( elem ) { - elem = elem[dir]; var match = false; + elem = elem[dir]; + while ( elem ) { - if ( elem.sizcache === doneName ) { + if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; + elem[ expando ] = doneName; elem.sizset = i; } @@ -4022,21 +5201,24 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; + if ( elem ) { - elem = elem[dir]; var match = false; + + elem = elem[dir]; while ( elem ) { - if ( elem.sizcache === doneName ) { + if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { - elem.sizcache = doneName; + elem[ expando ] = doneName; elem.sizset = i; } + if ( typeof cur !== "string" ) { if ( elem === cur ) { match = true; @@ -4057,21 +5239,34 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { } } -Sizzle.contains = document.documentElement.contains ? function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -} : function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -}; +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} -Sizzle.isXML = function(elem){ +Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; }; -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", root = context.nodeType ? [context] : context; // Position selectors must be done after the filter @@ -4084,13 +5279,16 @@ var posProcess = function(selector, context){ selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); + Sizzle( selector, root[i], tmpSet, seed ); } return Sizzle.filter( later, tmpSet ); }; // EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; @@ -4101,7 +5299,7 @@ jQuery.contains = Sizzle.contains; })(); -(function( jQuery ) { + var runtil = /Until$/, rparentsprev = /^(?:parents|prevUntil|prevAll)/, @@ -4109,20 +5307,41 @@ var runtil = /Until$/, rmultiselector = /,/, isSimple = /^.[^:#\[\.,]*$/, slice = Array.prototype.slice, - POS = jQuery.expr.match.POS; + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; jQuery.fn.extend({ find: function( selector ) { - var ret = this.pushStack( "", "find", selector ), length = 0; + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } - for ( var i = 0, l = this.length; i < l; i++ ) { + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { length = ret.length; jQuery.find( selector, this[i], ret ); if ( i > 0 ) { // Make sure that the results are unique - for ( var n = length; n < ret.length; n++ ) { - for ( var r = 0; r < length; r++ ) { + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { if ( ret[r] === ret[n] ) { ret.splice(n--, 1); break; @@ -4153,47 +5372,44 @@ jQuery.fn.extend({ filter: function( selector ) { return this.pushStack( winnow(this, selector, true), "filter", selector ); }, - + is: function( selector ) { - return !!selector && jQuery.filter( selector, this ).length > 0; + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { var ret = [], i, l, cur = this[0]; - + + // Array (deprecated as of jQuery 1.7) if ( jQuery.isArray( selectors ) ) { - var match, matches = {}, selector, level = 1; + var level = 1; - if ( cur && selectors.length ) { - for ( i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { - if ( !matches[selector] ) { - matches[selector] = jQuery.expr.match.POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); } } - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[selector]; - - if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { - ret.push({ selector: selector, elem: cur, level: level }); - } - } - - cur = cur.parentNode; - level++; - } + cur = cur.parentNode; + level++; } return ret; } - var pos = POS.test( selectors ) ? - jQuery( selectors, context || this.context ) : null; + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; for ( i = 0, l = this.length; i < l; i++ ) { cur = this[i]; @@ -4205,27 +5421,32 @@ jQuery.fn.extend({ } else { cur = cur.parentNode; - if ( !cur || !cur.ownerDocument || cur === context ) { + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { break; } } } } - ret = ret.length > 1 ? jQuery.unique(ret) : ret; - + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + return this.pushStack( ret, "closest", selectors ); }, - + // Determine the position of an element within // the matched set of elements index: function( elem ) { - if ( !elem || typeof elem === "string" ) { - return jQuery.inArray( this[0], - // If it receives a string, the selector is used - // If it receives nothing, the siblings are used - elem ? jQuery( elem ) : this.parent().children() ); + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); } + // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used @@ -4234,8 +5455,8 @@ jQuery.fn.extend({ add: function( selector, context ) { var set = typeof selector === "string" ? - jQuery( selector, context || this.context ) : - jQuery.makeArray( selector ), + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), all = jQuery.merge( this.get(), set ); return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? @@ -4297,7 +5518,7 @@ jQuery.each({ }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); - + if ( !runtil.test( name ) ) { selector = until; } @@ -4306,13 +5527,13 @@ jQuery.each({ ret = jQuery.filter( selector, ret ); } - ret = this.length > 1 ? jQuery.unique( ret ) : ret; + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { ret = ret.reverse(); } - return this.pushStack( ret, name, slice.call(arguments).join(",") ); + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); }; }); @@ -4326,9 +5547,11 @@ jQuery.extend({ jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : jQuery.find.matches(expr, elems); }, - + dir: function( elem, dir, until ) { - var matched = [], cur = elem[dir]; + var matched = [], + cur = elem[ dir ]; + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); @@ -4366,6 +5589,11 @@ jQuery.extend({ // Implement the identical functionality for filter and not function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep(elements, function( elem, i ) { var retVal = !!qualifier.call( elem, i, elem ); @@ -4374,7 +5602,7 @@ function winnow( elements, qualifier, keep ) { } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; + return ( elem === qualifier ) === keep; }); } else if ( typeof qualifier === "string" ) { @@ -4390,22 +5618,42 @@ function winnow( elements, qualifier, keep ) { } return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; }); } -})( jQuery ); -(function( jQuery ) { -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, rtagName = /<([\w:]+)/, rtbody = /\s]+\/)>/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*", "" ], legend: [ 1, "
                                ", "
                                " ], @@ -4415,7 +5663,8 @@ var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, col: [ 2, "", "
                                " ], area: [ 1, "", "" ], _default: [ 0, "", "" ] - }; + }, + safeFragment = createSafeFragment( document ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; @@ -4430,7 +5679,8 @@ jQuery.fn.extend({ text: function( text ) { if ( jQuery.isFunction(text) ) { return this.each(function(i) { - var self = jQuery(this); + var self = jQuery( this ); + self.text( text.call(this, i, self.text()) ); }); } @@ -4465,7 +5715,7 @@ jQuery.fn.extend({ } return elem; - }).append(this); + }).append( this ); } return this; @@ -4479,7 +5729,8 @@ jQuery.fn.extend({ } return this.each(function() { - var self = jQuery( this ), contents = self.contents(); + var self = jQuery( this ), + contents = self.contents(); if ( contents.length ) { contents.wrapAll( html ); @@ -4491,8 +5742,10 @@ jQuery.fn.extend({ }, wrap: function( html ) { - return this.each(function() { - jQuery( this ).wrapAll( html ); + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); }); }, @@ -4526,7 +5779,7 @@ jQuery.fn.extend({ this.parentNode.insertBefore( elem, this ); }); } else if ( arguments.length ) { - var set = jQuery(arguments[0]); + var set = jQuery.clean( arguments ); set.push.apply( set, this.toArray() ); return this.pushStack( set, "before", arguments ); } @@ -4539,11 +5792,11 @@ jQuery.fn.extend({ }); } else if ( arguments.length ) { var set = this.pushStack( this, "after", arguments ); - set.push.apply( set, jQuery(arguments[0]).toArray() ); + set.push.apply( set, jQuery.clean(arguments) ); return set; } }, - + // keepData is for internal use only--do not document remove: function( selector, keepData ) { for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { @@ -4554,11 +5807,11 @@ jQuery.fn.extend({ } if ( elem.parentNode ) { - elem.parentNode.removeChild( elem ); + elem.parentNode.removeChild( elem ); } } } - + return this; }, @@ -4574,46 +5827,17 @@ jQuery.fn.extend({ elem.removeChild( elem.firstChild ); } } - + return this; }, - clone: function( events ) { - // Do the clone - var ret = this.map(function() { - if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { - // IE copies events bound via attachEvent when - // using cloneNode. Calling detachEvent on the - // clone will also remove the events from the orignal - // In order to get around this, we use innerHTML. - // Unfortunately, this means some modifications to - // attributes in IE that are actually only stored - // as properties will not be copied (such as the - // the name attribute on an input). - var html = this.outerHTML, ownerDocument = this.ownerDocument; - if ( !html ) { - var div = ownerDocument.createElement("div"); - div.appendChild( this.cloneNode(true) ); - html = div.innerHTML; - } - - return jQuery.clean([html.replace(rinlinejQuery, "") - // Handle the case in IE 8 where action=/test/> self-closes a tag - .replace(raction, '="$1">') - .replace(rleadingWhitespace, "")], ownerDocument)[0]; - } else { - return this.cloneNode(true); - } - }); - - // Copy the events from the original to the clone - if ( events === true ) { - cloneCopyEvent( this, ret ); - cloneCopyEvent( this.find("*"), ret.find("*") ); - } + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - // Return the cloned set - return ret; + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); }, html: function( value ) { @@ -4623,7 +5847,7 @@ jQuery.fn.extend({ null; // See if we can take a shortcut and just use innerHTML - } else if ( typeof value === "string" && !rnocache.test( value ) && + } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) && (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { @@ -4645,8 +5869,9 @@ jQuery.fn.extend({ } else if ( jQuery.isFunction( value ) ) { this.each(function(i){ - var self = jQuery(this); - self.html( value.call(this, i, self.html()) ); + var self = jQuery( this ); + + self.html( value.call(this, i, self.html()) ); }); } else { @@ -4668,13 +5893,14 @@ jQuery.fn.extend({ } if ( typeof value !== "string" ) { - value = jQuery(value).detach(); + value = jQuery( value ).detach(); } return this.each(function() { - var next = this.nextSibling, parent = this.parentNode; + var next = this.nextSibling, + parent = this.parentNode; - jQuery(this).remove(); + jQuery( this ).remove(); if ( next ) { jQuery(next).before( value ); @@ -4683,7 +5909,9 @@ jQuery.fn.extend({ } }); } else { - return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ); + return this.length ? + this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : + this; } }, @@ -4692,7 +5920,9 @@ jQuery.fn.extend({ }, domManip: function( args, table, callback ) { - var results, first, value = args[0], scripts = [], fragment, parent; + var results, first, fragment, parent, + value = args[0], + scripts = []; // We can't cloneNode fragments that contain checked, in WebKit if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { @@ -4719,9 +5949,9 @@ jQuery.fn.extend({ } else { results = jQuery.buildFragment( args, this, scripts ); } - + fragment = results.fragment; - + if ( fragment.childNodes.length === 1 ) { first = fragment = fragment.firstChild; } else { @@ -4731,13 +5961,20 @@ jQuery.fn.extend({ if ( first ) { table = table && jQuery.nodeName( first, "tr" ); - for ( var i = 0, l = this.length; i < l; i++ ) { + for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { callback.call( table ? root(this[i], first) : this[i], - i > 0 || results.cacheable || this.length > 1 ? - fragment.cloneNode(true) : + // Make sure that we do not leak memory by inadvertently discarding + // the original fragment (which might have attached data) instead of + // using it; in addition, use the original fragment object for the last + // item instead of first because it can end up being emptied incorrectly + // in certain situations (Bug #8070). + // Fragments from the fragment cache must always be cloned and never used + // in place. + results.cacheable || ( l > 1 && i < lastIndex ) ? + jQuery.clone( fragment, true, true ) : fragment ); } @@ -4759,46 +5996,125 @@ function root( elem, cur ) { elem; } -function cloneCopyEvent(orig, ret) { - var i = 0; +function cloneCopyEvent( src, dest ) { - ret.each(function() { - if ( this.nodeName !== (orig[i] && orig[i].nodeName) ) { - return; - } + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } - var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events; + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; - if ( events ) { - delete curData.handle; - curData.events = {}; + if ( events ) { + delete curData.handle; + curData.events = {}; - for ( var type in events ) { - for ( var handler in events[ type ] ) { - jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); - } + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); } } - }); + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function cloneFixAttributes( src, dest ) { + var nodeName; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + // clearAttributes removes the attributes, which we don't want, + // but also removes the attachEvent events, which we *do* want + if ( dest.clearAttributes ) { + dest.clearAttributes(); + } + + // mergeAttributes, in contrast, only merges back on the + // original attributes, not the events + if ( dest.mergeAttributes ) { + dest.mergeAttributes( src ); + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 fail to clone children inside object elements that use + // the proprietary classid attribute value (rather than the type + // attribute) to identify the type of content to display + if ( nodeName === "object" ) { + dest.outerHTML = src.outerHTML; + + } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + if ( src.checked ) { + dest.defaultChecked = dest.checked = src.checked; + } + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } + + // Event data gets referenced instead of copied if the expando + // gets copied too + dest.removeAttribute( jQuery.expando ); } jQuery.buildFragment = function( args, nodes, scripts ) { - var fragment, cacheable, cacheresults, - doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document); + var fragment, cacheable, cacheresults, doc, + first = args[ 0 ]; + + // nodes may contain either an explicit document object, + // a jQuery collection or context object. + // If nodes[0] contains a valid object to assign to doc + if ( nodes && nodes[0] ) { + doc = nodes[0].ownerDocument || nodes[0]; + } + + // Ensure that an attr object doesn't incorrectly stand in as a document object + // Chrome and Firefox seem to allow this to occur and will throw exception + // Fixes #8950 + if ( !doc.createDocumentFragment ) { + doc = document; + } - // Only cache "small" (1/2 KB) strings that are associated with the main document + // Only cache "small" (1/2 KB) HTML strings that are associated with the main document // Cloning options loses the selected state, so don't cache them // IE 6 doesn't like it when you put or elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache - if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document && - !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) { + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { cacheable = true; - cacheresults = jQuery.fragments[ args[0] ]; - if ( cacheresults ) { - if ( cacheresults !== 1 ) { - fragment = cacheresults; - } + + cacheresults = jQuery.fragments[ first ]; + if ( cacheresults && cacheresults !== 1 ) { + fragment = cacheresults; } } @@ -4808,7 +6124,7 @@ jQuery.buildFragment = function( args, nodes, scripts ) { } if ( cacheable ) { - jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1; + jQuery.fragments[ first ] = cacheresults ? fragment : 1; } return { fragment: fragment, cacheable: cacheable }; @@ -4824,27 +6140,122 @@ jQuery.each({ replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { - var ret = [], insert = jQuery( selector ), + var ret = [], + insert = jQuery( selector ), parent = this.length === 1 && this[0].parentNode; - + if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { insert[ original ]( this[0] ); return this; - + } else { for ( var i = 0, l = insert.length; i < l; i++ ) { - var elems = (i > 0 ? this.clone(true) : this).get(); + var elems = ( i > 0 ? this.clone(true) : this ).get(); jQuery( insert[i] )[ original ]( elems ); ret = ret.concat( elems ); } - + return this.pushStack( ret, name, insert.selector ); } }; }); +function getAll( elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { + return elem.getElementsByTagName( "*" ); + + } else if ( typeof elem.querySelectorAll !== "undefined" ) { + return elem.querySelectorAll( "*" ); + + } else { + return []; + } +} + +// Used in clean, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( elem.type === "checkbox" || elem.type === "radio" ) { + elem.defaultChecked = elem.checked; + } +} +// Finds all inputs and passes them to fixDefaultChecked +function findInputs( elem ) { + var nodeName = ( elem.nodeName || "" ).toLowerCase(); + if ( nodeName === "input" ) { + fixDefaultChecked( elem ); + // Skip scripts, get other children + } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); + } +} + +// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js +function shimCloneNode( elem ) { + var div = document.createElement( "div" ); + safeFragment.appendChild( div ); + + div.innerHTML = elem.outerHTML; + return div.firstChild; +} + jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var srcElements, + destElements, + i, + // IE<=8 does not properly clone detached, unknown element nodes + clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ? + elem.cloneNode( true ) : + shimCloneNode( elem ); + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + // IE copies events bound via attachEvent when using cloneNode. + // Calling detachEvent on the clone will also remove the events + // from the original. In order to get around this, we use some + // proprietary methods to clear the events. Thanks to MooTools + // guys for this hotness. + + cloneFixAttributes( elem, clone ); + + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead + srcElements = getAll( elem ); + destElements = getAll( clone ); + + // Weird iteration because IE will replace the length property + // with an element if you are cloning the body and one of the + // elements on the page has a name or id of "length" + for ( i = 0; srcElements[i]; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + cloneFixAttributes( srcElements[i], destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + cloneCopyEvent( elem, clone ); + + if ( deepDataAndEvents ) { + srcElements = getAll( elem ); + destElements = getAll( clone ); + + for ( i = 0; srcElements[i]; ++i ) { + cloneCopyEvent( srcElements[i], destElements[i] ); + } + } + } + + srcElements = destElements = null; + + // Return the cloned set + return clone; + }, + clean: function( elems, context, fragment, scripts ) { + var checkScriptType; + context = context || document; // !context.createElement fails in IE with an error but returns typeof 'object' @@ -4852,7 +6263,7 @@ jQuery.extend({ context = context.ownerDocument || context[0] && context[0].ownerDocument || document; } - var ret = []; + var ret = [], j; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { if ( typeof elem === "number" ) { @@ -4864,54 +6275,76 @@ jQuery.extend({ } // Convert html string into DOM nodes - if ( typeof elem === "string" && !rhtml.test( elem ) ) { - elem = context.createTextNode( elem ); - - } else if ( typeof elem === "string" ) { - // Fix "XHTML"-style tags in all browsers - elem = elem.replace(rxhtmlTag, "<$1>"); - - // Trim whitespace, otherwise indexOf won't work as expected - var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(), - wrap = wrapMap[ tag ] || wrapMap._default, - depth = wrap[0], - div = context.createElement("div"); + if ( typeof elem === "string" ) { + if ( !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); + } else { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, "<$1>"); + + // Trim whitespace, otherwise indexOf won't work as expected + var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), + wrap = wrapMap[ tag ] || wrapMap._default, + depth = wrap[0], + div = context.createElement("div"); + + // Append wrapper element to unknown element safe doc fragment + if ( context === document ) { + // Use the fragment we've already created for this document + safeFragment.appendChild( div ); + } else { + // Use a fragment created with the owner document + createSafeFragment( context ).appendChild( div ); + } - // Go to html and back, then peel off extra wrappers - div.innerHTML = wrap[1] + elem + wrap[2]; + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + elem + wrap[2]; - // Move to the right depth - while ( depth-- ) { - div = div.lastChild; - } + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } - // Remove IE's autoinserted from table fragments - if ( !jQuery.support.tbody ) { + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { - // String was a , *may* have spurious - var hasBody = rtbody.test(elem), - tbody = tag === "table" && !hasBody ? - div.firstChild && div.firstChild.childNodes : + // String was a
                                , *may* have spurious + var hasBody = rtbody.test(elem), + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : - // String was a bare or - wrap[1] === "
                                " && !hasBody ? - div.childNodes : - []; + // String was a bare or + wrap[1] === "
                                " && !hasBody ? + div.childNodes : + []; - for ( var j = tbody.length - 1; j >= 0 ; --j ) { - if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { - tbody[ j ].parentNode.removeChild( tbody[ j ] ); + for ( j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } } } - } + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } - // IE completely kills leading whitespace when innerHTML is used - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + elem = div.childNodes; } + } - elem = div.childNodes; + // Resets defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + var len; + if ( !jQuery.support.appendChecked ) { + if ( elem[0] && typeof (len = elem.length) === "number" ) { + for ( j = 0; j < len; j++ ) { + findInputs( elem[j] ); + } + } else { + findInputs( elem ); + } } if ( elem.nodeType ) { @@ -4922,13 +6355,18 @@ jQuery.extend({ } if ( fragment ) { + checkScriptType = function( elem ) { + return !elem.type || rscriptType.test( elem.type ); + }; for ( i = 0; ret[i]; i++ ) { if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); - + } else { if ( ret[i].nodeType === 1 ) { - ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) ); + var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); + + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); } fragment.appendChild( ret[i] ); } @@ -4937,40 +6375,47 @@ jQuery.extend({ return ret; }, - + cleanData: function( elems ) { - var data, id, cache = jQuery.cache, + var data, id, + cache = jQuery.cache, special = jQuery.event.special, deleteExpando = jQuery.support.deleteExpando; - + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { continue; } id = elem[ jQuery.expando ]; - + if ( id ) { data = cache[ id ]; - + if ( data && data.events ) { for ( var type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); + // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } } + + // Null the DOM reference to avoid IE6/7/8 leak (#7054) + if ( data.handle ) { + data.handle.elem = null; + } } - + if ( deleteExpando ) { delete elem[ jQuery.expando ]; } else if ( elem.removeAttribute ) { elem.removeAttribute( jQuery.expando ); } - + delete cache[ id ]; } } @@ -4985,7 +6430,7 @@ function evalScript( i, elem ) { dataType: "script" }); } else { - jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) ); } if ( elem.parentNode ) { @@ -4993,27 +6438,24 @@ function evalScript( i, elem ) { } } -})( jQuery ); -(function( jQuery ) { + + var ralpha = /alpha\([^)]*\)/i, ropacity = /opacity=([^)]*)/, - rdashAlpha = /-([a-z])/ig, - rupper = /([A-Z])/g, + // fixed for IE9, see #8346 + rupper = /([A-Z]|^ms)/g, rnumpx = /^-?\d+(?:px)?$/i, rnum = /^-?\d/, + rrelNum = /^([\-+])=([\-+.\de]+)/, cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssWidth = [ "Left", "Right" ], cssHeight = [ "Top", "Bottom" ], curCSS, - // cache check for defaultView.getComputedStyle - getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, - - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; + getComputedStyle, + currentStyle; jQuery.fn.css = function( name, value ) { // Setting 'undefined' is a no-op @@ -5048,11 +6490,14 @@ jQuery.extend({ // Exclude the following css properties to add px cssNumber: { - "zIndex": true, + "fillOpacity": true, "fontWeight": true, + "lineHeight": true, "opacity": true, - "zoom": true, - "lineHeight": true + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true }, // Add in properties whose names you wish to fix before @@ -5070,20 +6515,29 @@ jQuery.extend({ } // Make sure that we're working with the right name - var ret, origName = jQuery.camelCase( name ), + var ret, type, origName = jQuery.camelCase( name ), style = elem.style, hooks = jQuery.cssHooks[ origName ]; name = jQuery.cssProps[ origName ] || origName; // Check if we're setting a value if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + // Make sure that NaN and null values aren't set. See: #7116 - if ( typeof value === "number" && isNaN( value ) || value == null ) { + if ( value == null || type === "number" && isNaN( value ) ) { return; } // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( typeof value === "number" && !jQuery.cssNumber[ origName ] ) { + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { value += "px"; } @@ -5108,11 +6562,17 @@ jQuery.extend({ }, css: function( elem, name, extra ) { + var ret, hooks; + // Make sure that we're working with the right name - var ret, origName = jQuery.camelCase( name ), - hooks = jQuery.cssHooks[ origName ]; + name = jQuery.camelCase( name ); + hooks = jQuery.cssHooks[ name ]; + name = jQuery.cssProps[ name ] || name; - name = jQuery.cssProps[ origName ] || origName; + // cssFloat needs a special treatment + if ( name === "cssFloat" ) { + name = "float"; + } // If a hook was provided get the computed value from there if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) { @@ -5120,7 +6580,7 @@ jQuery.extend({ // Otherwise, if a way to get the computed value exists, use that } else if ( curCSS ) { - return curCSS( elem, name, origName ); + return curCSS( elem, name ); } }, @@ -5140,10 +6600,6 @@ jQuery.extend({ for ( name in options ) { elem.style[ name ] = old[ name ]; } - }, - - camelCase: function( string ) { - return string.replace( rdashAlpha, fcamelCase ); } }); @@ -5157,22 +6613,21 @@ jQuery.each(["height", "width"], function( i, name ) { if ( computed ) { if ( elem.offsetWidth !== 0 ) { - val = getWH( elem, name, extra ); - + return getWH( elem, name, extra ); } else { jQuery.swap( elem, cssShow, function() { val = getWH( elem, name, extra ); }); } - return val + "px"; + return val; } }, set: function( elem, value ) { if ( rnumpx.test( value ) ) { // ignore negative width and height values #1599 - value = parseFloat(value); + value = parseFloat( value ); if ( value >= 0 ) { return value + "px"; @@ -5189,42 +6644,73 @@ if ( !jQuery.support.opacity ) { jQuery.cssHooks.opacity = { get: function( elem, computed ) { // IE uses filters for opacity - return ropacity.test((computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "") ? - (parseFloat(RegExp.$1) / 100) + "" : + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( parseFloat( RegExp.$1 ) / 100 ) + "" : computed ? "1" : ""; }, set: function( elem, value ) { - var style = elem.style; + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level style.zoom = 1; - // Set the alpha filter to set the opacity - var opacity = jQuery.isNaN(value) ? - "" : - "alpha(opacity=" + value * 100 + ")", - filter = style.filter || ""; + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) { + + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there there is no filter style applied in a css rule, we are done + if ( currentStyle && !currentStyle.filter ) { + return; + } + } - style.filter = ralpha.test(filter) ? - filter.replace(ralpha, opacity) : - style.filter + ' ' + opacity; + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; } }; } -if ( getComputedStyle ) { - curCSS = function( elem, newName, name ) { +jQuery(function() { + // This hook cannot be added until DOM ready because the support test + // for it is not run until after DOM ready + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + var ret; + jQuery.swap( elem, { "display": "inline-block" }, function() { + if ( computed ) { + ret = curCSS( elem, "margin-right", "marginRight" ); + } else { + ret = elem.style.marginRight; + } + }); + return ret; + } + }; + } +}); + +if ( document.defaultView && document.defaultView.getComputedStyle ) { + getComputedStyle = function( elem, name ) { var ret, defaultView, computedStyle; name = name.replace( rupper, "-$1" ).toLowerCase(); - if ( !(defaultView = elem.ownerDocument.defaultView) ) { - return undefined; - } - - if ( (computedStyle = defaultView.getComputedStyle( elem, null )) ) { + if ( (defaultView = elem.ownerDocument.defaultView) && + (computedStyle = defaultView.getComputedStyle( elem, null )) ) { ret = computedStyle.getPropertyValue( name ); if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { ret = jQuery.style( elem, name ); @@ -5233,10 +6719,19 @@ if ( getComputedStyle ) { return ret; }; +} + +if ( document.documentElement.currentStyle ) { + currentStyle = function( elem, name ) { + var left, rsLeft, uncomputed, + ret = elem.currentStyle && elem.currentStyle[ name ], + style = elem.style; -} else if ( document.documentElement.currentStyle ) { - curCSS = function( elem, name ) { - var left, rsLeft, ret = elem.currentStyle && elem.currentStyle[ name ], style = elem.style; + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret === null && style && (uncomputed = style[ name ]) ) { + ret = uncomputed; + } // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 @@ -5244,53 +6739,86 @@ if ( getComputedStyle ) { // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { + // Remember the original values left = style.left; - rsLeft = elem.runtimeStyle.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; // Put in the new values to get a computed value out - elem.runtimeStyle.left = elem.currentStyle.left; - style.left = name === "fontSize" ? "1em" : (ret || 0); + if ( rsLeft ) { + elem.runtimeStyle.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ( ret || 0 ); ret = style.pixelLeft + "px"; // Revert the changed values style.left = left; - elem.runtimeStyle.left = rsLeft; + if ( rsLeft ) { + elem.runtimeStyle.left = rsLeft; + } } - return ret; + return ret === "" ? "auto" : ret; }; } -function getWH( elem, name, extra ) { - var which = name === "width" ? cssWidth : cssHeight, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight; +curCSS = getComputedStyle || currentStyle; - if ( extra === "border" ) { - return val; - } +function getWH( elem, name, extra ) { - jQuery.each( which, function() { - if ( !extra ) { - val -= parseFloat(jQuery.css( elem, "padding" + this )) || 0; + // Start with offset property + var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + which = name === "width" ? cssWidth : cssHeight, + i = 0, + len = which.length; + + if ( val > 0 ) { + if ( extra !== "border" ) { + for ( ; i < len; i++ ) { + if ( !extra ) { + val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } else { + val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + } } - if ( extra === "margin" ) { - val += parseFloat(jQuery.css( elem, "margin" + this )) || 0; + return val + "px"; + } - } else { - val -= parseFloat(jQuery.css( elem, "border" + this + "Width" )) || 0; + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, name ); + if ( val < 0 || val == null ) { + val = elem.style[ name ] || 0; + } + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + + // Add padding, border, margin + if ( extra ) { + for ( ; i < len; i++ ) { + val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; + if ( extra !== "padding" ) { + val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; + } + if ( extra === "margin" ) { + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; + } } - }); + } - return val; + return val + "px"; } if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.hidden = function( elem ) { - var width = elem.offsetWidth, height = elem.offsetHeight; + var width = elem.offsetWidth, + height = elem.offsetHeight; - return (width === 0 && height === 0) || (!jQuery.support.reliableHiddenOffsets && (elem.style.display || jQuery.css( elem, "display" )) === "none"); + return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); }; jQuery.expr.filters.visible = function( elem ) { @@ -5298,24 +6826,162 @@ if ( jQuery.expr && jQuery.expr.filters ) { }; } -})( jQuery ); -(function( jQuery ) { -var jsc = jQuery.now(), - rscript = /<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, - rselectTextarea = /^(?:select|textarea)/i, - rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, - rnoContent = /^(?:GET|HEAD|DELETE)$/, + + +var r20 = /%20/g, rbracket = /\[\]$/, - jsre = /\=\?(&|$)/, + rCRLF = /\r?\n/g, + rhash = /#.*$/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, rquery = /\?/, + rscript = /<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, + rselectTextarea = /^(?:select|textarea)/i, + rspacesAjax = /\s+/, rts = /([?&])_=[^&]*/, - rurl = /^(\w+:)?\/\/([^\/?#]+)/, - r20 = /%20/g, - rhash = /#.*$/, + rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/, // Keep a copy of the old load method - _load = jQuery.fn.load; + _load = jQuery.fn.load, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Document location + ajaxLocation, + + // Document location segments + ajaxLocParts, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = ["*/"] + ["*"]; + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + if ( jQuery.isFunction( func ) ) { + var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), + i = 0, + length = dataTypes.length, + dataType, + list, + placeBefore; + + // For each dataType in the dataTypeExpression + for ( ; i < length; i++ ) { + dataType = dataTypes[ i ]; + // We control if we're asked to add before + // any existing element + placeBefore = /^\+/.test( dataType ); + if ( placeBefore ) { + dataType = dataType.substr( 1 ) || "*"; + } + list = structure[ dataType ] = structure[ dataType ] || []; + // then we add to the structure accordingly + list[ placeBefore ? "unshift" : "push" ]( func ); + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, + dataType /* internal */, inspected /* internal */ ) { + + dataType = dataType || options.dataTypes[ 0 ]; + inspected = inspected || {}; + + inspected[ dataType ] = true; + + var list = structure[ dataType ], + i = 0, + length = list ? list.length : 0, + executeOnly = ( structure === prefilters ), + selection; + + for ( ; i < length && ( executeOnly || !selection ); i++ ) { + selection = list[ i ]( options, originalOptions, jqXHR ); + // If we got redirected to another dataType + // we try there if executing only and not done already + if ( typeof selection === "string" ) { + if ( !executeOnly || inspected[ selection ] ) { + selection = undefined; + } else { + options.dataTypes.unshift( selection ); + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, selection, inspected ); + } + } + } + // If we're only executing or nothing was selected + // we try the catchall dataType if not done already + if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, "*", inspected ); + } + // unnecessary when only executing (prefilters) + // but it'll be ignored by the caller in that case + return selection; +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } +} jQuery.fn.extend({ load: function( url, params, callback ) { @@ -5327,10 +6993,10 @@ jQuery.fn.extend({ return this; } - var off = url.indexOf(" "); + var off = url.indexOf( " " ); if ( off >= 0 ) { - var selector = url.slice(off, url.length); - url = url.slice(0, off); + var selector = url.slice( off, url.length ); + url = url.slice( 0, off ); } // Default to a GET request @@ -5342,7 +7008,7 @@ jQuery.fn.extend({ if ( jQuery.isFunction( params ) ) { // We assume that it's the callback callback = params; - params = null; + params = undefined; // Otherwise, build a param string } else if ( typeof params === "object" ) { @@ -5359,26 +7025,34 @@ jQuery.fn.extend({ type: type, dataType: "html", data: params, - complete: function( res, status ) { + // Complete callback (responseText is used internally) + complete: function( jqXHR, status, responseText ) { + // Store the response as specified by the jqXHR object + responseText = jqXHR.responseText; // If successful, inject the HTML into all the matched elements - if ( status === "success" || status === "notmodified" ) { + if ( jqXHR.isResolved() ) { + // #4825: Get the actual response in case + // a dataFilter is present in ajaxSettings + jqXHR.done(function( r ) { + responseText = r; + }); // See if a selector was specified self.html( selector ? // Create a dummy div to hold the results jQuery("
                                ") // inject the contents of the document in, removing the scripts // to avoid any 'Permission Denied' errors in IE - .append(res.responseText.replace(rscript, "")) + .append(responseText.replace(rscript, "")) // Locate the specified elements .find(selector) : // If not, just inject the full result - res.responseText ); + responseText ); } if ( callback ) { - self.each( callback, [res.responseText, status, res] ); + self.each( callback, [ responseText, status, jqXHR ] ); } } }); @@ -5387,88 +7061,87 @@ jQuery.fn.extend({ }, serialize: function() { - return jQuery.param(this.serializeArray()); + return jQuery.param( this.serializeArray() ); }, serializeArray: function() { - return this.map(function() { - return this.elements ? jQuery.makeArray(this.elements) : this; + return this.map(function(){ + return this.elements ? jQuery.makeArray( this.elements ) : this; }) - .filter(function() { + .filter(function(){ return this.name && !this.disabled && - (this.checked || rselectTextarea.test(this.nodeName) || - rinput.test(this.type)); + ( this.checked || rselectTextarea.test( this.nodeName ) || + rinput.test( this.type ) ); }) - .map(function( i, elem ) { - var val = jQuery(this).val(); + .map(function( i, elem ){ + var val = jQuery( this ).val(); return val == null ? null : - jQuery.isArray(val) ? - jQuery.map( val, function( val, i ) { - return { name: elem.name, value: val }; + jQuery.isArray( val ) ? + jQuery.map( val, function( val, i ){ + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }) : - { name: elem.name, value: val }; + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }).get(); } }); // Attach a bunch of functions for handling common AJAX events -jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) { - jQuery.fn[o] = function( f ) { - return this.bind(o, f); +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ + jQuery.fn[ o ] = function( f ){ + return this.on( o, f ); }; }); -jQuery.extend({ - get: function( url, data, callback, type ) { - // shift arguments if data argument was omited +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted if ( jQuery.isFunction( data ) ) { type = type || callback; callback = data; - data = null; + data = undefined; } return jQuery.ajax({ - type: "GET", + type: method, url: url, data: data, success: callback, dataType: type }); - }, + }; +}); + +jQuery.extend({ getScript: function( url, callback ) { - return jQuery.get(url, null, callback, "script"); + return jQuery.get( url, undefined, callback, "script" ); }, getJSON: function( url, data, callback ) { - return jQuery.get(url, data, callback, "json"); + return jQuery.get( url, data, callback, "json" ); }, - post: function( url, data, callback, type ) { - // shift arguments if data argument was omited - if ( jQuery.isFunction( data ) ) { - type = type || callback; - callback = data; - data = {}; + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + if ( settings ) { + // Building a settings object + ajaxExtend( target, jQuery.ajaxSettings ); + } else { + // Extending ajaxSettings + settings = target; + target = jQuery.ajaxSettings; } - - return jQuery.ajax({ - type: "POST", - url: url, - data: data, - success: callback, - dataType: type - }); - }, - - ajaxSetup: function( settings ) { - jQuery.extend( jQuery.ajaxSettings, settings ); + ajaxExtend( target, settings ); + return target; }, ajaxSettings: { - url: location.href, + url: ajaxLocation, + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), global: true, type: "GET", contentType: "application/x-www-form-urlencoded", @@ -5477,551 +7150,1075 @@ jQuery.extend({ /* timeout: 0, data: null, + dataType: null, username: null, password: null, + cache: null, traditional: false, + headers: {}, */ - // This function can be overriden by calling jQuery.ajaxSetup - xhr: function() { - return new window.XMLHttpRequest(); - }, + accepts: { xml: "application/xml, text/xml", html: "text/html", - script: "text/javascript, application/javascript", - json: "application/json, text/javascript", text: "text/plain", - _default: "*/*" - } - }, + json: "application/json, text/javascript", + "*": allTypes + }, - ajax: function( origSettings ) { - var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings), - jsonp, status, data, type = s.type.toUpperCase(), noContent = rnoContent.test(type); + contents: { + xml: /xml/, + html: /html/, + json: /json/ + }, - s.url = s.url.replace( rhash, "" ); + responseFields: { + xml: "responseXML", + text: "responseText" + }, - // Use original (not extended) context object if it was provided - s.context = origSettings && origSettings.context != null ? origSettings.context : s; + // List of data converters + // 1) key format is "source_type destination_type" (a single space in-between) + // 2) the catchall symbol "*" can be used for source_type + converters: { - // convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } + // Convert anything to text + "* text": window.String, - // Handle JSONP Parameter Callbacks - if ( s.dataType === "jsonp" ) { - if ( type === "GET" ) { - if ( !jsre.test( s.url ) ) { - s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?"; - } - } else if ( !s.data || !jsre.test(s.data) ) { - s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?"; - } - s.dataType = "json"; - } + // Text to html (true = no transformation) + "text html": true, - // Build temporary JSONP function - if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) { - jsonp = s.jsonpCallback || ("jsonp" + jsc++); + // Evaluate text as a json expression + "text json": jQuery.parseJSON, - // Replace the =? sequence both in the query string and the data - if ( s.data ) { - s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1"); - } + // Parse text as xml + "text xml": jQuery.parseXML + }, - s.url = s.url.replace(jsre, "=" + jsonp + "$1"); + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + context: true, + url: true + } + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events + // It's the callbackContext if one was provided in the options + // and if it's a DOM node or a jQuery collection + globalEventContext = callbackContext !== s && + ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? + jQuery( callbackContext ) : jQuery.event, + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + // Status-dependent callbacks + statusCode = s.statusCode || {}, + // ifModified key + ifModifiedKey, + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + // Response headers + responseHeadersString, + responseHeaders, + // transport + transport, + // timeout handle + timeoutTimer, + // Cross-domain detection vars + parts, + // The jqXHR state + state = 0, + // To know if global events are to be dispatched + fireGlobals, + // Loop variable + i, + // Fake xhr + jqXHR = { + + readyState: 0, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( !state ) { + var lname = name.toLowerCase(); + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Raw string + getAllResponseHeaders: function() { + return state === 2 ? responseHeadersString : null; + }, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( state === 2 ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match === undefined ? null : match; + }, - // We need to make sure - // that a JSONP style response is executed properly - s.dataType = "script"; + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; + }, - // Handle JSONP-style loading + // Cancel the request + abort: function( statusText ) { + statusText = statusText || "abort"; + if ( transport ) { + transport.abort( statusText ); + } + done( 0, statusText ); + return this; + } + }; - var customJsonp = window[ jsonp ]; + // Callback for when everything is done + // It is defined here because jslint complains if it is declared + // at the end of the function (which would be more logical and readable) + function done( status, nativeStatusText, responses, headers ) { - window[ jsonp ] = function( tmp ) { - if ( jQuery.isFunction( customJsonp ) ) { - customJsonp( tmp ); + // Called once + if ( state === 2 ) { + return; + } - } else { - // Garbage collect - window[ jsonp ] = undefined; + // State is "done" now + state = 2; - try { - delete window[ jsonp ]; - } catch( jsonpError ) {} - } + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout( timeoutTimer ); + } - data = tmp; - jQuery.handleSuccess( s, xhr, status, data ); - jQuery.handleComplete( s, xhr, status, data ); - - if ( head ) { - head.removeChild( script ); - } - }; - } + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; - if ( s.dataType === "script" && s.cache === null ) { - s.cache = false; - } + // Cache response headers + responseHeadersString = headers || ""; - if ( s.cache === false && type === "GET" ) { - var ts = jQuery.now(); + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; - // try replacing _= if it is there - var ret = s.url.replace(rts, "$1_=" + ts); + var isSuccess, + success, + error, + statusText = nativeStatusText, + response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined, + lastModified, + etag; - // if nothing was replaced, add timestamp to the end - s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : ""); - } + // If successful, handle type chaining + if ( status >= 200 && status < 300 || status === 304 ) { - // If data is available, append data to url for get requests - if ( s.data && type === "GET" ) { - s.url += (rquery.test(s.url) ? "&" : "?") + s.data; - } + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { - // Watch for a new set of requests - if ( s.global && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Matches an absolute URL, and saves the domain - var parts = rurl.exec( s.url ), - remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); + if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) { + jQuery.lastModified[ ifModifiedKey ] = lastModified; + } + if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) { + jQuery.etag[ ifModifiedKey ] = etag; + } + } - // If we're requesting a remote document - // and trying to load JSON or Script with a GET - if ( s.dataType === "script" && type === "GET" && remote ) { - var head = document.getElementsByTagName("head")[0] || document.documentElement; - var script = document.createElement("script"); - if ( s.scriptCharset ) { - script.charset = s.scriptCharset; - } - script.src = s.url; + // If not modified + if ( status === 304 ) { - // Handle Script loading - if ( !jsonp ) { - var done = false; + statusText = "notmodified"; + isSuccess = true; - // Attach handlers for all browsers - script.onload = script.onreadystatechange = function() { - if ( !done && (!this.readyState || - this.readyState === "loaded" || this.readyState === "complete") ) { - done = true; - jQuery.handleSuccess( s, xhr, status, data ); - jQuery.handleComplete( s, xhr, status, data ); + // If we have data + } else { - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - if ( head && script.parentNode ) { - head.removeChild( script ); - } + try { + success = ajaxConvert( s, response ); + statusText = "success"; + isSuccess = true; + } catch(e) { + // We have a parsererror + statusText = "parsererror"; + error = e; } - }; + } + } else { + // We extract error from statusText + // then normalize statusText and status for non-aborts + error = statusText; + if ( !statusText || status ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } } - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709 and #4378). - head.insertBefore( script, head.firstChild ); - - // We handle everything using the script element injection - return undefined; - } + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = "" + ( nativeStatusText || statusText ); - var requestDone = false; + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } - // Create the request object - var xhr = s.xhr(); + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; - if ( !xhr ) { - return; - } + if ( fireGlobals ) { + globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), + [ jqXHR, s, isSuccess ? success : error ] ); + } - // Open the socket - // Passing null username, generates a login popup on Opera (#2865) - if ( s.username ) { - xhr.open(type, s.url, s.async, s.username, s.password); - } else { - xhr.open(type, s.url, s.async); - } + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - // Need an extra try/catch for cross domain requests in Firefox 3 - try { - // Set content-type if data specified and content-body is valid for this type - if ( (s.data != null && !noContent) || (origSettings && origSettings.contentType) ) { - xhr.setRequestHeader("Content-Type", s.contentType); + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } } + } - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[s.url] ) { - xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]); - } + // Attach deferreds + deferred.promise( jqXHR ); + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + jqXHR.complete = completeDeferred.add; - if ( jQuery.etag[s.url] ) { - xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]); + // Status-dependent callbacks + jqXHR.statusCode = function( map ) { + if ( map ) { + var tmp; + if ( state < 2 ) { + for ( tmp in map ) { + statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; + } + } else { + tmp = map[ jqXHR.status ]; + jqXHR.then( tmp, tmp ); } } + return this; + }; - // Set header so the called script knows that it's an XMLHttpRequest - // Only send the header if it's not a remote XHR - if ( !remote ) { - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - } + // Remove hash character (#7531: and string promotion) + // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // We also use the url parameter if available + s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); - // Set the Accepts header for the server, depending on the dataType - xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ? - s.accepts[ s.dataType ] + ", */*; q=0.01" : - s.accepts._default ); - } catch( headerError ) {} + // Extract dataTypes list + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax ); - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false ) { - // Handle the global AJAX counter - if ( s.global && jQuery.active-- === 1 ) { - jQuery.event.trigger( "ajaxStop" ); - } + // Determine if a cross-domain request is in order + if ( s.crossDomain == null ) { + parts = rurl.exec( s.url.toLowerCase() ); + s.crossDomain = !!( parts && + ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] || + ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != + ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) + ); + } - // close opended socket - xhr.abort(); - return false; + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); } - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxSend", [xhr, s] ); + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefiler, stop there + if ( state === 2 ) { + return false; } - // Wait for a response to come back - var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) { - // The request was aborted - if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) { - // Opera doesn't call onreadystatechange before this point - // so we simulate the call - if ( !requestDone ) { - jQuery.handleComplete( s, xhr, status, data ); - } + // We can fire global events as of now if asked to + fireGlobals = s.global; - requestDone = true; - if ( xhr ) { - xhr.onreadystatechange = jQuery.noop; - } + // Uppercase the type + s.type = s.type.toUpperCase(); - // The transfer is complete and the data is available, or the request timed out - } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) { - requestDone = true; - xhr.onreadystatechange = jQuery.noop; + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); - status = isTimeout === "timeout" ? - "timeout" : - !jQuery.httpSuccess( xhr ) ? - "error" : - s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? - "notmodified" : - "success"; + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } - var errMsg; + // More options handling for requests with no content + if ( !s.hasContent ) { - if ( status === "success" ) { - // Watch for, and catch, XML document parse errors - try { - // process the data (runs the xml through httpData regardless of callback) - data = jQuery.httpData( xhr, s.dataType, s ); - } catch( parserError ) { - status = "parsererror"; - errMsg = parserError; - } - } + // If data is available, append data to url + if ( s.data ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } - // Make sure that the request was successful or notmodified - if ( status === "success" || status === "notmodified" ) { - // JSONP handles its own success callback - if ( !jsonp ) { - jQuery.handleSuccess( s, xhr, status, data ); - } - } else { - jQuery.handleError( s, xhr, status, errMsg ); - } + // Get ifModifiedKey before adding the anti-cache parameter + ifModifiedKey = s.url; - // Fire the complete handlers - if ( !jsonp ) { - jQuery.handleComplete( s, xhr, status, data ); - } + // Add anti-cache in url if needed + if ( s.cache === false ) { - if ( isTimeout === "timeout" ) { - xhr.abort(); - } + var ts = jQuery.now(), + // try replacing _= if it is there + ret = s.url.replace( rts, "$1_=" + ts ); - // Stop memory leaks - if ( s.async ) { - xhr = null; - } + // if nothing was replaced, add timestamp to the end + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); } - }; + } - // Override the abort handler, if we can (IE 6 doesn't allow it, but that's OK) - // Opera doesn't fire onreadystatechange at all on abort - try { - var oldAbort = xhr.abort; - xhr.abort = function() { - // xhr.abort in IE7 is not a native JS function - // and does not have a call property - if ( xhr && oldAbort.call ) { - oldAbort.call( xhr ); - } + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } - onreadystatechange( "abort" ); - }; - } catch( abortError ) {} + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + ifModifiedKey = ifModifiedKey || s.url; + if ( jQuery.lastModified[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); + } + if ( jQuery.etag[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); + } + } - // Timeout checker - if ( s.async && s.timeout > 0 ) { - setTimeout(function() { - // Check to see if the request is still happening - if ( xhr && !requestDone ) { - onreadystatechange( "timeout" ); - } - }, s.timeout); + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); } - // Send the data - try { - xhr.send( noContent || s.data == null ? null : s.data ); + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { + // Abort if not done already + jqXHR.abort(); + return false; - } catch( sendError ) { - jQuery.handleError( s, xhr, null, sendError ); + } - // Fire the complete handlers - jQuery.handleComplete( s, xhr, status, data ); + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); } - // firefox 1.5 doesn't fire statechange for sync requests - if ( !s.async ) { - onreadystatechange(); + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout( function(){ + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + state = 1; + transport.send( requestHeaders, done ); + } catch (e) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; + } + } } - // return XMLHttpRequest to allow aborting the request etc. - return xhr; + return jqXHR; }, // Serialize an array of form elements or a set of // key/values into a query string param: function( a, traditional ) { - var s = [], add = function( key, value ) { - // If value is a function, invoke it and return its value - value = jQuery.isFunction(value) ? value() : value; - s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value); - }; - + var s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : value; + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; + // Set traditional to true for jQuery <= 1.3.2 behavior. if ( traditional === undefined ) { traditional = jQuery.ajaxSettings.traditional; } - + // If an array was passed in, assume that it is an array of form elements. - if ( jQuery.isArray(a) || a.jquery ) { + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { // Serialize the form elements jQuery.each( a, function() { add( this.name, this.value ); }); - + } else { // If traditional, encode the "old" way (the way 1.3.2 or older // did it), otherwise encode params recursively. for ( var prefix in a ) { - buildParams( prefix, a[prefix], traditional, add ); + buildParams( prefix, a[ prefix ], traditional, add ); } } // Return the resulting serialization - return s.join("&").replace(r20, "+"); + return s.join( "&" ).replace( r20, "+" ); } }); function buildParams( prefix, obj, traditional, add ) { - if ( jQuery.isArray(obj) && obj.length ) { + if ( jQuery.isArray( obj ) ) { // Serialize array item. jQuery.each( obj, function( i, v ) { if ( traditional || rbracket.test( prefix ) ) { // Treat each array item as a scalar. add( prefix, v ); - } else { - // If array item is non-scalar (array or object), encode its - // numeric index to resolve deserialization ambiguity issues. - // Note that rack (as of 1.0.0) can't currently deserialize - // nested arrays properly, and attempting to do so may cause - // a server error. Possible fixes are to modify rack's - // deserialization algorithm or to provide an option or flag - // to force array serialization to be shallow. - buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add ); + } else { + // If array item is non-scalar (array or object), encode its + // numeric index to resolve deserialization ambiguity issues. + // Note that rack (as of 1.0.0) can't currently deserialize + // nested arrays properly, and attempting to do so may cause + // a server error. Possible fixes are to modify rack's + // deserialization algorithm or to provide an option or flag + // to force array serialization to be shallow. + buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && obj != null && typeof obj === "object" ) { + // Serialize object item. + for ( var name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + // Serialize scalar item. + add( prefix, obj ); + } +} + +// This is still on the jQuery object... for now +// Want to move this to jQuery.ajax some day +jQuery.extend({ + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {} + +}); + +/* Handles responses to an ajax request: + * - sets all responseXXX fields accordingly + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var contents = s.contents, + dataTypes = s.dataTypes, + responseFields = s.responseFields, + ct, + type, + finalDataType, + firstDataType; + + // Fill responseXXX fields + for ( type in responseFields ) { + if ( type in responses ) { + jqXHR[ responseFields[type] ] = responses[ type ]; + } + } + + // Remove auto dataType and get content-type in the process + while( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +// Chain conversions given the request and the original response +function ajaxConvert( s, response ) { + + // Apply the dataFilter if provided + if ( s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + var dataTypes = s.dataTypes, + converters = {}, + i, + key, + length = dataTypes.length, + tmp, + // Current and previous dataTypes + current = dataTypes[ 0 ], + prev, + // Conversion expression + conversion, + // Conversion function + conv, + // Conversion functions (transitive conversion) + conv1, + conv2; + + // For each dataType in the chain + for ( i = 1; i < length; i++ ) { + + // Create converters map + // with lowercased keys + if ( i === 1 ) { + for ( key in s.converters ) { + if ( typeof key === "string" ) { + converters[ key.toLowerCase() ] = s.converters[ key ]; + } + } + } + + // Get the dataTypes + prev = current; + current = dataTypes[ i ]; + + // If current is auto dataType, update it to prev + if ( current === "*" ) { + current = prev; + // If no auto and dataTypes are actually different + } else if ( prev !== "*" && prev !== current ) { + + // Get the converter + conversion = prev + " " + current; + conv = converters[ conversion ] || converters[ "* " + current ]; + + // If there is no direct converter, search transitively + if ( !conv ) { + conv2 = undefined; + for ( conv1 in converters ) { + tmp = conv1.split( " " ); + if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { + conv2 = converters[ tmp[1] + " " + current ]; + if ( conv2 ) { + conv1 = converters[ conv1 ]; + if ( conv1 === true ) { + conv = conv2; + } else if ( conv2 === true ) { + conv = conv1; + } + break; + } + } + } + } + // If we found no converter, dispatch an error + if ( !( conv || conv2 ) ) { + jQuery.error( "No conversion from " + conversion.replace(" "," to ") ); + } + // If found converter is not an equivalence + if ( conv !== true ) { + // Convert with 1 or 2 converters accordingly + response = conv ? conv( response ) : conv2( conv1(response) ); + } + } + } + return response; +} + + + + +var jsc = jQuery.now(), + jsre = /(\=)\?(&|$)|\?\?/i; + +// Default jsonp settings +jQuery.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + return jQuery.expando + "_" + ( jsc++ ); + } +}); + +// Detect, normalize options and install callbacks for jsonp requests +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { + + var inspectData = s.contentType === "application/x-www-form-urlencoded" && + ( typeof s.data === "string" ); + + if ( s.dataTypes[ 0 ] === "jsonp" || + s.jsonp !== false && ( jsre.test( s.url ) || + inspectData && jsre.test( s.data ) ) ) { + + var responseContainer, + jsonpCallback = s.jsonpCallback = + jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, + previous = window[ jsonpCallback ], + url = s.url, + data = s.data, + replace = "$1" + jsonpCallback + "$2"; + + if ( s.jsonp !== false ) { + url = url.replace( jsre, replace ); + if ( s.url === url ) { + if ( inspectData ) { + data = data.replace( jsre, replace ); + } + if ( s.data === data ) { + // Add callback manually + url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; + } + } + } + + s.url = url; + s.data = data; + + // Install callback + window[ jsonpCallback ] = function( response ) { + responseContainer = [ response ]; + }; + + // Clean-up function + jqXHR.always(function() { + // Set callback back to previous value + window[ jsonpCallback ] = previous; + // Call if it was a function and we have a response + if ( responseContainer && jQuery.isFunction( previous ) ) { + window[ jsonpCallback ]( responseContainer[ 0 ] ); + } + }); + + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( !responseContainer ) { + jQuery.error( jsonpCallback + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // force json dataType + s.dataTypes[ 0 ] = "json"; + + // Delegate to script + return "script"; + } +}); + + + + +// Install script dataType +jQuery.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /javascript|ecmascript/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +}); + +// Handle cache's special case and global +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + s.global = false; + } +}); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + + var script, + head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; + + return { + + send: function( _, callback ) { + + script = document.createElement( "script" ); + + script.async = "async"; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function( _, isAbort ) { + + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( head && script.parentNode ) { + head.removeChild( script ); + } + + // Dereference the script + script = undefined; + + // Callback if not abort + if ( !isAbort ) { + callback( 200, "success" ); + } + } + }; + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); + }, + + abort: function() { + if ( script ) { + script.onload( 0, 1 ); + } } - }); - - } else if ( !traditional && obj != null && typeof obj === "object" ) { - if ( jQuery.isEmptyObject( obj ) ) { - add( prefix, "" ); - - // Serialize object item. - } else { - jQuery.each( obj, function( k, v ) { - buildParams( prefix + "[" + k + "]", v, traditional, add ); - }); - } - - } else { - // Serialize scalar item. - add( prefix, obj ); + }; } -} +}); -// This is still on the jQuery object... for now -// Want to move this to jQuery.ajax some day -jQuery.extend({ - // Counter for holding the number of active queries - active: 0, - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - handleError: function( s, xhr, status, e ) { - // If a local callback was specified, fire it - if ( s.error ) { - s.error.call( s.context, xhr, status, e ); +var // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject ? function() { + // Abort all pending requests + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ]( 0, 1 ); } + } : false, + xhrId = 0, + xhrCallbacks; - // Fire the global callback - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxError", [xhr, s, e] ); - } - }, +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} - handleSuccess: function( s, xhr, status, data ) { - // If a local callback was specified, fire it and pass it the data - if ( s.success ) { - s.success.call( s.context, data, status, xhr ); - } +function createActiveXHR() { + try { + return new window.ActiveXObject( "Microsoft.XMLHTTP" ); + } catch( e ) {} +} - // Fire the global callback - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxSuccess", [xhr, s] ); - } - }, +// Create the request object +// (This is still attached to ajaxSettings for backward compatibility) +jQuery.ajaxSettings.xhr = window.ActiveXObject ? + /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function() { + return !this.isLocal && createStandardXHR() || createActiveXHR(); + } : + // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +// Determine support properties +(function( xhr ) { + jQuery.extend( jQuery.support, { + ajax: !!xhr, + cors: !!xhr && ( "withCredentials" in xhr ) + }); +})( jQuery.ajaxSettings.xhr() ); - handleComplete: function( s, xhr, status ) { - // Process result - if ( s.complete ) { - s.complete.call( s.context, xhr, status ); - } +// Create transport if the browser can provide an xhr +if ( jQuery.support.ajax ) { - // The request was completed - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxComplete", [xhr, s] ); - } + jQuery.ajaxTransport(function( s ) { + // Cross domain only allowed if supported through XMLHttpRequest + if ( !s.crossDomain || jQuery.support.cors ) { - // Handle the global AJAX counter - if ( s.global && jQuery.active-- === 1 ) { - jQuery.event.trigger( "ajaxStop" ); - } - }, - - triggerGlobal: function( s, type, args ) { - (s.context && s.context.url == null ? jQuery(s.context) : jQuery.event).trigger(type, args); - }, + var callback; - // Determines if an XMLHttpRequest was successful or not - httpSuccess: function( xhr ) { - try { - // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450 - return !xhr.status && location.protocol === "file:" || - xhr.status >= 200 && xhr.status < 300 || - xhr.status === 304 || xhr.status === 1223; - } catch(e) {} + return { + send: function( headers, complete ) { - return false; - }, + // Get a new xhr + var xhr = s.xhr(), + handle, + i; - // Determines if an XMLHttpRequest returns NotModified - httpNotModified: function( xhr, url ) { - var lastModified = xhr.getResponseHeader("Last-Modified"), - etag = xhr.getResponseHeader("Etag"); + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open( s.type, s.url, s.async, s.username, s.password ); + } else { + xhr.open( s.type, s.url, s.async ); + } - if ( lastModified ) { - jQuery.lastModified[url] = lastModified; - } + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } - if ( etag ) { - jQuery.etag[url] = etag; - } + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } - return xhr.status === 304; - }, + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } - httpData: function( xhr, type, s ) { - var ct = xhr.getResponseHeader("content-type") || "", - xml = type === "xml" || !type && ct.indexOf("xml") >= 0, - data = xml ? xhr.responseXML : xhr.responseText; + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + } catch( _ ) {} - if ( xml && data.documentElement.nodeName === "parsererror" ) { - jQuery.error( "parsererror" ); - } + // Do send the request + // This may raise an exception which is actually + // handled in jQuery.ajax (so no try/catch here) + xhr.send( ( s.hasContent && s.data ) || null ); - // Allow a pre-filtering function to sanitize the response - // s is checked to keep backwards compatibility - if ( s && s.dataFilter ) { - data = s.dataFilter( data, type ); - } + // Listener + callback = function( _, isAbort ) { - // The filter can actually parse the response - if ( typeof data === "string" ) { - // Get the JavaScript object, if JSON is used. - if ( type === "json" || !type && ct.indexOf("json") >= 0 ) { - data = jQuery.parseJSON( data ); + var status, + statusText, + responseHeaders, + responses, + xml; - // If the type is "script", eval it in global context - } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) { - jQuery.globalEval( data ); - } - } + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occured + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { - return data; - } + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { -}); + // Only called once + callback = undefined; -/* - * Create the request object; Microsoft failed to properly - * implement the XMLHttpRequest in IE7 (can't request local files), - * so we use the ActiveXObject when it is available - * Additionally XMLHttpRequest can be disabled in IE7/IE8 so - * we need a fallback. - */ -if ( window.ActiveXObject ) { - jQuery.ajaxSettings.xhr = function() { - if ( window.location.protocol !== "file:" ) { - try { - return new window.XMLHttpRequest(); - } catch(xhrError) {} - } + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } + } - try { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } catch(activeError) {} - }; + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + responses = {}; + xml = xhr.responseXML; + + // Construct response list + if ( xml && xml.documentElement /* #4958 */ ) { + responses.xml = xml; + } + responses.text = xhr.responseText; + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } + } + } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); + } + }; + + // if we're in sync mode or it's in cache + // and has been retrieved directly (IE6 & IE7) + // we need to manually fire the callback + if ( !s.async || xhr.readyState === 4 ) { + callback(); + } else { + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } + xhr.onreadystatechange = callback; + } + }, + + abort: function() { + if ( callback ) { + callback(0,1); + } + } + }; + } + }); } -// Does this browser support XHR requests? -jQuery.support.ajax = !!jQuery.ajaxSettings.xhr(); -})( jQuery ); -(function( jQuery ) { + var elemdisplay = {}, + iframe, iframeDoc, rfxtypes = /^(?:toggle|show|hide)$/, - rfxnum = /^([+\-]=)?([\d+.\-]+)(.*)$/, + rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i, timerId, fxAttrs = [ // height animations @@ -6030,32 +8227,50 @@ var elemdisplay = {}, [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ], // opacity animations [ "opacity" ] - ]; + ], + fxNow; jQuery.fn.extend({ show: function( speed, easing, callback ) { + var elem, display; + if ( speed || speed === 0 ) { - return this.animate( genFx("show", 3), speed, easing, callback); + return this.animate( genFx("show", 3), speed, easing, callback ); + } else { for ( var i = 0, j = this.length; i < j; i++ ) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !jQuery.data(this[i], "olddisplay") && this[i].style.display === "none" ) { - this[i].style.display = ""; - } + elem = this[ i ]; - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( this[i].style.display === "" && jQuery.css( this[i], "display" ) === "none" ) { - jQuery.data(this[i], "olddisplay", defaultDisplay(this[i].nodeName)); + if ( elem.style ) { + display = elem.style.display; + + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { + display = elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( display === "" && jQuery.css(elem, "display") === "none" ) { + jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); + } } } // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { - this[i].style.display = jQuery.data(this[i], "olddisplay") || ""; + elem = this[ i ]; + + if ( elem.style ) { + display = elem.style.display; + + if ( display === "" || display === "none" ) { + elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; + } + } } return this; @@ -6067,18 +8282,27 @@ jQuery.fn.extend({ return this.animate( genFx("hide", 3), speed, easing, callback); } else { - for ( var i = 0, j = this.length; i < j; i++ ) { - var display = jQuery.css( this[i], "display" ); + var elem, display, + i = 0, + j = this.length; + + for ( ; i < j; i++ ) { + elem = this[i]; + if ( elem.style ) { + display = jQuery.css( elem, "display" ); - if ( display !== "none" ) { - jQuery.data( this[i], "olddisplay", display ); + if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { + jQuery._data( elem, "olddisplay", display ); + } } } // Set the display of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { - this[i].style.display = "none"; + if ( this[i].style ) { + this[i].style.display = "none"; + } } return this; @@ -6113,35 +8337,57 @@ jQuery.fn.extend({ }, animate: function( prop, speed, easing, callback ) { - var optall = jQuery.speed(speed, easing, callback); + var optall = jQuery.speed( speed, easing, callback ); if ( jQuery.isEmptyObject( prop ) ) { - return this.each( optall.complete ); + return this.each( optall.complete, [ false ] ); } - return this[ optall.queue === false ? "each" : "queue" ](function() { - // XXX ‘this’ does not always have a nodeName when running the + // Do not change referenced properties as per-property easing will be lost + prop = jQuery.extend( {}, prop ); + + function doAnimation() { + // XXX 'this' does not always have a nodeName when running the // test suite - var opt = jQuery.extend({}, optall), p, + if ( optall.queue === false ) { + jQuery._mark( this ); + } + + var opt = jQuery.extend( {}, optall ), isElement = this.nodeType === 1, hidden = isElement && jQuery(this).is(":hidden"), - self = this; + name, val, p, e, + parts, start, end, unit, + method; + + // will store per property easing and be used to determine when an animation is complete + opt.animatedProperties = {}; for ( p in prop ) { - var name = jQuery.camelCase( p ); + // property name normalization + name = jQuery.camelCase( p ); if ( p !== name ) { prop[ name ] = prop[ p ]; delete prop[ p ]; - p = name; } - if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) { - return opt.complete.call(this); + val = prop[ name ]; + + // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default) + if ( jQuery.isArray( val ) ) { + opt.animatedProperties[ name ] = val[ 1 ]; + val = prop[ name ] = val[ 0 ]; + } else { + opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing'; + } + + if ( val === "hide" && hidden || val === "show" && !hidden ) { + return opt.complete.call( this ); } - if ( isElement && ( p === "height" || p === "width" ) ) { + if ( isElement && ( name === "height" || name === "width" ) ) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and @@ -6149,66 +8395,60 @@ jQuery.fn.extend({ opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; // Set display property to inline-block for height/width - // animations on inline elements that are having width/height - // animated + // animations on inline elements that are having width/height animated if ( jQuery.css( this, "display" ) === "inline" && jQuery.css( this, "float" ) === "none" ) { - if ( !jQuery.support.inlineBlockNeedsLayout ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { this.style.display = "inline-block"; } else { - var display = defaultDisplay(this.nodeName); - - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( display === "inline" ) { - this.style.display = "inline-block"; - - } else { - this.style.display = "inline"; - this.style.zoom = 1; - } + this.style.zoom = 1; } } } - - if ( jQuery.isArray( prop[p] ) ) { - // Create (if needed) and add to specialEasing - (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1]; - prop[p] = prop[p][0]; - } } if ( opt.overflow != null ) { this.style.overflow = "hidden"; } - opt.curAnim = jQuery.extend({}, prop); + for ( p in prop ) { + e = new jQuery.fx( this, opt, p ); + val = prop[ p ]; - jQuery.each( prop, function( name, val ) { - var e = new jQuery.fx( self, opt, name ); + if ( rfxtypes.test( val ) ) { - if ( rfxtypes.test(val) ) { - e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop ); + // Tracks whether to show or hide based on private + // data attached to the element + method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); + if ( method ) { + jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); + e[ method ](); + } else { + e[ val ](); + } } else { - var parts = rfxnum.exec(val), - start = e.cur(true) || 0; + parts = rfxnum.exec( val ); + start = e.cur(); if ( parts ) { - var end = parseFloat( parts[2] ), - unit = parts[3] || "px"; + end = parseFloat( parts[2] ); + unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); // We need to compute starting value if ( unit !== "px" ) { - jQuery.style( self, name, (end || 1) + unit); - start = ((end || 1) / e.cur(true)) * start; - jQuery.style( self, name, start + unit); + jQuery.style( this, p, (end || 1) + unit); + start = ( (end || 1) / e.cur() ) * start; + jQuery.style( this, p, start + unit); } // If a +=/-= token was provided, we're doing a relative animation if ( parts[1] ) { - end = ((parts[1] === "-=" ? -1 : 1) * end) + start; + end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start; } e.custom( start, end, unit ); @@ -6217,48 +8457,94 @@ jQuery.fn.extend({ e.custom( start, val, "" ); } } - }); + } // For JS strict compliance return true; - }); - }, + } - stop: function( clearQueue, gotoEnd ) { - var timers = jQuery.timers; + return optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, - if ( clearQueue ) { - this.queue([]); + stop: function( type, clearQueue, gotoEnd ) { + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); } - this.each(function() { - // go in reverse order so anything added to the queue during the loop is ignored - for ( var i = timers.length - 1; i >= 0; i-- ) { - if ( timers[i].elem === this ) { - if (gotoEnd) { - // force the next step to be the last - timers[i](true); - } + return this.each(function() { + var index, + hadTimers = false, + timers = jQuery.timers, + data = jQuery._data( this ); + + // clear marker counters if we know they won't be + if ( !gotoEnd ) { + jQuery._unmark( true, this ); + } + + function stopQueue( elem, data, index ) { + var hooks = data[ index ]; + jQuery.removeData( elem, index, true ); + hooks.stop( gotoEnd ); + } - timers.splice(i, 1); + if ( type == null ) { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { + stopQueue( this, data, index ); + } } + } else if ( data[ index = type + ".run" ] && data[ index ].stop ){ + stopQueue( this, data, index ); } - }); - // start the next in the queue if the last step wasn't forced - if ( !gotoEnd ) { - this.dequeue(); - } + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + if ( gotoEnd ) { - return this; + // force the next step to be the last + timers[ index ]( true ); + } else { + timers[ index ].saveState(); + } + hadTimers = true; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( !( gotoEnd && hadTimers ) ) { + jQuery.dequeue( this, type ); + } + }); } }); +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout( clearFxNow, 0 ); + return ( fxNow = jQuery.now() ); +} + +function clearFxNow() { + fxNow = undefined; +} + +// Generate parameters to create a standard animation function genFx( type, num ) { var obj = {}; - jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() { + jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { obj[ this ] = type; }); @@ -6267,11 +8553,12 @@ function genFx( type, num ) { // Generate shortcuts for custom animations jQuery.each({ - slideDown: genFx("show", 1), - slideUp: genFx("hide", 1), - slideToggle: genFx("toggle", 1), + slideDown: genFx( "show", 1 ), + slideUp: genFx( "hide", 1 ), + slideToggle: genFx( "toggle", 1 ), fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" } + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } }, function( name, props ) { jQuery.fn[ name ] = function( speed, easing, callback ) { return this.animate( props, speed, easing, callback ); @@ -6280,25 +8567,34 @@ jQuery.each({ jQuery.extend({ speed: function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, - easing: fn && easing || easing && !jQuery.isFunction(easing) && easing + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing }; opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default; + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } // Queueing opt.old = opt.complete; - opt.complete = function() { - if ( opt.queue !== false ) { - jQuery(this).dequeue(); - } + + opt.complete = function( noUnmark ) { if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } else if ( noUnmark !== false ) { + jQuery._unmark( this ); + } }; return opt; @@ -6309,7 +8605,7 @@ jQuery.extend({ return firstNum + diff * p; }, swing: function( p, n, firstNum, diff ) { - return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum; + return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum; } }, @@ -6320,9 +8616,7 @@ jQuery.extend({ this.elem = elem; this.prop = prop; - if ( !options.orig ) { - options.orig = {}; - } + options.orig = options.orig || {}; } }); @@ -6334,50 +8628,67 @@ jQuery.fx.prototype = { this.options.step.call( this.elem, this.now, this ); } - (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); + ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); }, // Get the current size cur: function() { - if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) { + if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { return this.elem[ this.prop ]; } - var r = parseFloat( jQuery.css( this.elem, this.prop ) ); - return r && r > -10000 ? r : 0; + var parsed, + r = jQuery.css( this.elem, this.prop ); + // Empty strings, null, undefined and "auto" are converted to 0, + // complex values such as "rotate(1rad)" are returned as is, + // simple values such as "10px" are parsed to Float. + return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed; }, // Start an animation from one number to another custom: function( from, to, unit ) { - this.startTime = jQuery.now(); - this.start = from; + var self = this, + fx = jQuery.fx; + + this.startTime = fxNow || createFxNow(); this.end = to; - this.unit = unit || this.unit || "px"; - this.now = this.start; + this.now = this.start = from; this.pos = this.state = 0; + this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); - var self = this, fx = jQuery.fx; function t( gotoEnd ) { - return self.step(gotoEnd); + return self.step( gotoEnd ); } + t.queue = this.options.queue; t.elem = this.elem; + t.saveState = function() { + if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { + jQuery._data( self.elem, "fxshow" + self.prop, self.start ); + } + }; if ( t() && jQuery.timers.push(t) && !timerId ) { - timerId = setInterval(fx.tick, fx.interval); + timerId = setInterval( fx.tick, fx.interval ); } }, // Simple 'show' function show: function() { + var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); + // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); + this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); this.options.show = true; // Begin the animation - // Make sure that we start at a small width/height to avoid any - // flash of content - this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur()); + // Make sure that we start at a small width/height to avoid any flash of content + if ( dataShow !== undefined ) { + // This show is picking up where a previous hide or show left off + this.custom( this.cur(), dataShow ); + } else { + this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); + } // Start by showing the element jQuery( this.elem ).show(); @@ -6386,67 +8697,84 @@ jQuery.fx.prototype = { // Simple 'hide' function hide: function() { // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); + this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); this.options.hide = true; // Begin the animation - this.custom(this.cur(), 0); + this.custom( this.cur(), 0 ); }, // Each step of an animation step: function( gotoEnd ) { - var t = jQuery.now(), done = true; + var p, n, complete, + t = fxNow || createFxNow(), + done = true, + elem = this.elem, + options = this.options; - if ( gotoEnd || t >= this.options.duration + this.startTime ) { + if ( gotoEnd || t >= options.duration + this.startTime ) { this.now = this.end; this.pos = this.state = 1; this.update(); - this.options.curAnim[ this.prop ] = true; + options.animatedProperties[ this.prop ] = true; - for ( var i in this.options.curAnim ) { - if ( this.options.curAnim[i] !== true ) { + for ( p in options.animatedProperties ) { + if ( options.animatedProperties[ p ] !== true ) { done = false; } } if ( done ) { // Reset the overflow - if ( this.options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { - var elem = this.elem, options = this.options; - jQuery.each( [ "", "X", "Y" ], function (index, value) { - elem.style[ "overflow" + value ] = options.overflow[index]; - } ); + if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { + + jQuery.each( [ "", "X", "Y" ], function( index, value ) { + elem.style[ "overflow" + value ] = options.overflow[ index ]; + }); } // Hide the element if the "hide" operation was done - if ( this.options.hide ) { - jQuery(this.elem).hide(); + if ( options.hide ) { + jQuery( elem ).hide(); } // Reset the properties, if the item has been hidden or shown - if ( this.options.hide || this.options.show ) { - for ( var p in this.options.curAnim ) { - jQuery.style( this.elem, p, this.options.orig[p] ); + if ( options.hide || options.show ) { + for ( p in options.animatedProperties ) { + jQuery.style( elem, p, options.orig[ p ] ); + jQuery.removeData( elem, "fxshow" + p, true ); + // Toggle data is no longer needed + jQuery.removeData( elem, "toggle" + p, true ); } } // Execute the complete function - this.options.complete.call( this.elem ); + // in the event that the complete function throws an exception + // we must ensure it won't be called twice. #5684 + + complete = options.complete; + if ( complete ) { + + options.complete = false; + complete.call( elem ); + } } return false; } else { - var n = t - this.startTime; - this.state = n / this.options.duration; - - // Perform the easing function, defaults to swing - var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop]; - var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear"); - this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration); - this.now = this.start + ((this.end - this.start) * this.pos); + // classical easing cannot be used with an Infinity duration + if ( options.duration == Infinity ) { + this.now = t; + } else { + n = t - this.startTime; + this.state = n / options.duration; + // Perform the easing function, defaults to swing + this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); + this.now = this.start + ( (this.end - this.start) * this.pos ); + } // Perform the next step of the animation this.update(); } @@ -6457,11 +8785,15 @@ jQuery.fx.prototype = { jQuery.extend( jQuery.fx, { tick: function() { - var timers = jQuery.timers; + var timer, + timers = jQuery.timers, + i = 0; - for ( var i = 0; i < timers.length; i++ ) { - if ( !timers[i]() ) { - timers.splice(i--, 1); + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); } } @@ -6491,7 +8823,7 @@ jQuery.extend( jQuery.fx, { _default: function( fx ) { if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { - fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit; + fx.elem.style[ fx.prop ] = fx.now + fx.unit; } else { fx.elem[ fx.prop ] = fx.now; } @@ -6499,6 +8831,14 @@ jQuery.extend( jQuery.fx, { } }); +// Adds width/height step functions +// Do not set anything below 0 +jQuery.each([ "width", "height" ], function( i, prop ) { + jQuery.fx.step[ prop ] = function( fx ) { + jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); + }; +}); + if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.animated = function( elem ) { return jQuery.grep(jQuery.timers, function( fn ) { @@ -6507,25 +8847,53 @@ if ( jQuery.expr && jQuery.expr.filters ) { }; } +// Try to restore the default display value of an element function defaultDisplay( nodeName ) { + if ( !elemdisplay[ nodeName ] ) { - var elem = jQuery("<" + nodeName + ">").appendTo("body"), - display = elem.css("display"); + var body = document.body, + elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), + display = elem.css( "display" ); elem.remove(); + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe if ( display === "none" || display === "" ) { - display = "block"; + // No iframe to use yet, so create it + if ( !iframe ) { + iframe = document.createElement( "iframe" ); + iframe.frameBorder = iframe.width = iframe.height = 0; + } + + body.appendChild( iframe ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "" : "" ) + "" ); + iframeDoc.close(); + } + + elem = iframeDoc.createElement( nodeName ); + + iframeDoc.body.appendChild( elem ); + + display = jQuery.css( elem, "display" ); + body.removeChild( iframe ); } + // Store the correct default display elemdisplay[ nodeName ] = display; } return elemdisplay[ nodeName ]; } -})( jQuery ); -(function( jQuery ) { + + var rtable = /^t(?:able|d|h)$/i, rroot = /^(?:body|html)$/i; @@ -6534,7 +8902,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { jQuery.fn.offset = function( options ) { var elem = this[0], box; - if ( options ) { + if ( options ) { return this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); @@ -6557,15 +8925,15 @@ if ( "getBoundingClientRect" in document.documentElement ) { // Make sure we're not dealing with a disconnected DOM node if ( !box || !jQuery.contains( docElem, elem ) ) { - return box || { top: 0, left: 0 }; + return box ? { top: box.top, left: box.left } : { top: 0, left: 0 }; } var body = doc.body, win = getWindow(doc), clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, - scrollTop = (win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop ), - scrollLeft = (win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft), + scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop, + scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft, top = box.top + scrollTop - clientTop, left = box.left + scrollLeft - clientLeft; @@ -6576,7 +8944,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { jQuery.fn.offset = function( options ) { var elem = this[0]; - if ( options ) { + if ( options ) { return this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); @@ -6590,16 +8958,19 @@ if ( "getBoundingClientRect" in document.documentElement ) { return jQuery.offset.bodyOffset( elem ); } - jQuery.offset.initialize(); - - var offsetParent = elem.offsetParent, prevOffsetParent = elem, - doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement, - body = doc.body, defaultView = doc.defaultView, + var computedStyle, + offsetParent = elem.offsetParent, + prevOffsetParent = elem, + doc = elem.ownerDocument, + docElem = doc.documentElement, + body = doc.body, + defaultView = doc.defaultView, prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, - top = elem.offsetTop, left = elem.offsetLeft; + top = elem.offsetTop, + left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { - if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { break; } @@ -6611,7 +8982,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { top += elem.offsetTop; left += elem.offsetLeft; - if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { + if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } @@ -6620,7 +8991,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { offsetParent = elem.offsetParent; } - if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { + if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } @@ -6633,7 +9004,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { left += body.offsetLeft; } - if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { top += Math.max( docElem.scrollTop, body.scrollTop ); left += Math.max( docElem.scrollLeft, body.scrollLeft ); } @@ -6643,53 +9014,19 @@ if ( "getBoundingClientRect" in document.documentElement ) { } jQuery.offset = { - initialize: function() { - var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.css(body, "marginTop") ) || 0, - html = "
                                "; - - jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } ); - - container.innerHTML = html; - body.insertBefore( container, body.firstChild ); - innerDiv = container.firstChild; - checkDiv = innerDiv.firstChild; - td = innerDiv.nextSibling.firstChild.firstChild; - - this.doesNotAddBorder = (checkDiv.offsetTop !== 5); - this.doesAddBorderForTableAndCells = (td.offsetTop === 5); - - checkDiv.style.position = "fixed"; - checkDiv.style.top = "20px"; - - // safari subtracts parent border width here which is 5px - this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15); - checkDiv.style.position = checkDiv.style.top = ""; - - innerDiv.style.overflow = "hidden"; - innerDiv.style.position = "relative"; - - this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5); - - this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop); - - body.removeChild( container ); - body = container = innerDiv = checkDiv = table = td = null; - jQuery.offset.initialize = jQuery.noop; - }, bodyOffset: function( body ) { - var top = body.offsetTop, left = body.offsetLeft; + var top = body.offsetTop, + left = body.offsetLeft; - jQuery.offset.initialize(); - - if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) { + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { top += parseFloat( jQuery.css(body, "marginTop") ) || 0; left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; } return { top: top, left: left }; }, - + setOffset: function( elem, options, i ) { var position = jQuery.css( elem, "position" ); @@ -6702,28 +9039,30 @@ jQuery.offset = { curOffset = curElem.offset(), curCSSTop = jQuery.css( elem, "top" ), curCSSLeft = jQuery.css( elem, "left" ), - calculatePosition = (position === "absolute" && jQuery.inArray('auto', [curCSSTop, curCSSLeft]) > -1), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, props = {}, curPosition = {}, curTop, curLeft; - // need to be able to calculate position if either top or left is auto and position is absolute + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed if ( calculatePosition ) { curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; } - curTop = calculatePosition ? curPosition.top : parseInt( curCSSTop, 10 ) || 0; - curLeft = calculatePosition ? curPosition.left : parseInt( curCSSLeft, 10 ) || 0; - if ( jQuery.isFunction( options ) ) { options = options.call( elem, i, curOffset ); } - props = (!curElem.is(":visible") && !curOffset.top && !curOffset.left) || isNaN(curTop) || isNaN(curLeft) ? - { top : options.top, left: options.left } - : { - top: (options.top - curOffset.top) + curTop, - left: (options.left - curOffset.left) + curLeft - }; - + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + if ( "using" in options ) { options.using.call( elem, props ); } else { @@ -6734,6 +9073,7 @@ jQuery.offset = { jQuery.fn.extend({ + position: function() { if ( !this[0] ) { return null; @@ -6781,29 +9121,16 @@ jQuery.fn.extend({ jQuery.each( ["Left", "Top"], function( i, name ) { var method = "scroll" + name; - jQuery.fn[ method ] = function(val) { - var elem = this[0], win; - - if ( !elem ) { - return null; - } + jQuery.fn[ method ] = function( val ) { + var elem, win; - if ( val !== undefined ) { - // Set the scroll offset - return this.each(function() { - win = getWindow( this ); + if ( val === undefined ) { + elem = this[ 0 ]; - if ( win ) { - win.scrollTo( - !i ? val : jQuery(win).scrollLeft(), - i ? val : jQuery(win).scrollTop() - ); + if ( !elem ) { + return null; + } - } else { - this[ method ] = val; - } - }); - } else { win = getWindow( elem ); // Return the scroll offset @@ -6812,6 +9139,21 @@ jQuery.each( ["Left", "Top"], function( i, name ) { win.document.body[ method ] : elem[ method ]; } + + // Set the scroll offset + return this.each(function() { + win = getWindow( this ); + + if ( win ) { + win.scrollTo( + !i ? val : jQuery( win ).scrollLeft(), + i ? val : jQuery( win ).scrollTop() + ); + + } else { + this[ method ] = val; + } + }); }; }); @@ -6823,25 +9165,31 @@ function getWindow( elem ) { false; } -})( jQuery ); -(function( jQuery ) { -// Create innerHeight, innerWidth, outerHeight and outerWidth methods + + +// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods jQuery.each([ "Height", "Width" ], function( i, name ) { var type = name.toLowerCase(); // innerHeight and innerWidth - jQuery.fn["inner" + name] = function() { - return this[0] ? - parseFloat( jQuery.css( this[0], type, "padding" ) ) : + jQuery.fn[ "inner" + name ] = function() { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, "padding" ) ) : + this[ type ]() : null; }; // outerHeight and outerWidth - jQuery.fn["outer" + name] = function( margin ) { - return this[0] ? - parseFloat( jQuery.css( this[0], type, margin ? "margin" : "border" ) ) : + jQuery.fn[ "outer" + name ] = function( margin ) { + var elem = this[0]; + return elem ? + elem.style ? + parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) : + this[ type ]() : null; }; @@ -6851,7 +9199,7 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { if ( !elem ) { return size == null ? null : this; } - + if ( jQuery.isFunction( size ) ) { return this.each(function( i ) { var self = jQuery( this ); @@ -6859,30 +9207,60 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { }); } - return jQuery.isWindow( elem ) ? + if ( jQuery.isWindow( elem ) ) { // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode - elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] || - elem.document.body[ "client" + name ] : - - // Get document width or height - (elem.nodeType === 9) ? // is it a document - // Either scroll[Width/Height] or offset[Width/Height], whichever is greater - Math.max( - elem.documentElement["client" + name], - elem.body["scroll" + name], elem.documentElement["scroll" + name], - elem.body["offset" + name], elem.documentElement["offset" + name] - ) : - - // Get or set width or height on the element - size === undefined ? - // Get width or height on the element - parseFloat( jQuery.css( elem, type ) ) : - - // Set the width or height on the element (default to pixels if value is unitless) - this.css( type, typeof size === "string" ? size : size + "px" ); + // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat + var docElemProp = elem.document.documentElement[ "client" + name ], + body = elem.document.body; + return elem.document.compatMode === "CSS1Compat" && docElemProp || + body && body[ "client" + name ] || docElemProp; + + // Get document width or height + } else if ( elem.nodeType === 9 ) { + // Either scroll[Width/Height] or offset[Width/Height], whichever is greater + return Math.max( + elem.documentElement["client" + name], + elem.body["scroll" + name], elem.documentElement["scroll" + name], + elem.body["offset" + name], elem.documentElement["offset" + name] + ); + + // Get or set width or height on the element + } else if ( size === undefined ) { + var orig = jQuery.css( elem, type ), + ret = parseFloat( orig ); + + return jQuery.isNumeric( ret ) ? ret : orig; + + // Set the width or height on the element (default to pixels if value is unitless) + } else { + return this.css( type, typeof size === "string" ? size : size + "px" ); + } }; }); -})( jQuery ); -})(window); + + + +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + + + +})( window ); \ No newline at end of file diff --git a/jquerymx.md b/jquerymx.md new file mode 100644 index 00000000..3d4f316c --- /dev/null +++ b/jquerymx.md @@ -0,0 +1,346 @@ +@page jquerymx jQueryMX +@parent index 0 +@description jQuery Model View Controller and extensions. + +jQueryMX is a collection of useful jQuery libraries that provide +the missing functionality necessary to +implement and organize large-scale jQuery applications. + +Every part of jQueryMX can be used stand-alone which keeps your +app super light. $.Model, $.View, and $.Controller are only 7kb minified and gzipped. This +includs their $.String, $.Class, and destroyed-event dependencies. + +If you are using [steal], simply steal the plugin you need like: + + steal('jquery/controller', function(){ + $.Controller('Tabs'); + }) + +Or, use the [http://javascriptmvc.com/builder.html download builder] to select +the files you need. + +jQueryMX is divided into four core areas: + + - DOM Helpers + - Language Helpers + - Special events + - Model, View, Controller and Class ( Read [mvc the walkthrough] ) + +The following highlights jQueryMX's functionality. + +## DOM Helpers + +[dom DOM helpers] extend jQuery with extra functionality for +manipulating the DOM. For example, [dimensions] lets you set the +outer width and height of elements like: + + $('#foo').outerWidth(500); + +THe following are the other dom plugins: + + - [jQuery.cookie Cookie] - Set and get cookie values. + - [jQuery.fixture Fixture] - Simulate Ajax responses. + - [jQuery.fn.closest Closest] - Use the open child selector in event delegation. + - [jQuery.fn.compare Compare] - Compare the location of two elements rapidly. + - [jQuery.fn.curStyles CurStyles] - Get multiple css properties quickly. + - [jQuery.fn.formParams FormParams] - Serializes a form into a JSON-like object. + - [jQuery.fn.selection Selection] - Gets or sets the current text selection. + - [jQuery.fn.within Within] - Returns elements that have a point within their boundaries. + - [jQuery.Range Range] - Text range utilities. + - [jQuery.route] - Routes for history-enabled ajax apps. + +## Special Events + +jQueryMX comes packed with jQuery [specialevents special events] and event helpers. + + - [jQuery.Drag Drag] - Delegatable drag events. + - [jQuery.Drop Drop] - Delegatable drop events. + - [jQuery.Hover Hover] - Delegatable hover events. + - [jQuery.event.special.destroyed Destroyed] - Know when an element is removed from the page. + - [jQuery.event.special.resize Resize] - Listen to resize events on any element. + - [jQuery.event.swipe Swipe] - Delegatable swipe events. + - [jQuery.Event.prototype.key Key] - Get the character from a key event. + - [jQuery.event.special.default Default] - Provide default behaviors for events. + - [jquery.event.pause Pause-Resume] - Pause and resume event propagation. + + +## Language Helpers + +Language helpers make it easy to perform various functions on +JavaScript data. + + - [jQuery.Object Object] - compare objects and sets + - [jQuery.Observe Observe] - Listen to changes in JS Objects and Arrays + - [jQuery.String String] - String helpers + - [jQuery.toJSON toJSON] - create and convert JSON strings + - [jQuery.Vector Vector] - vector math + +## $.Class + +[jQuery.Class $.Class] provides simple prototypal +inheritance. It's used by [jQuery.Controller $.Controller] and +[jQuery.Model $.Model]. + + // create a Monster Class + $.Class("Monster", + // static methods + { + + // a list of all monsters + monsters : [] + }, + // prototype methods + { + + // called when a new monster is created + init : function(name){ + + // stores a reference to the name + this.name = name; + + // adds this monster to the collection of monsters + this.Class.monsters.push(this); + }, + + // a method on monsters + speak : function(){ + alert(this.name + " says hello."); + } + }); + + // create a monster + var hydra = new Monster("hydra"); + + // call a method on a monster + hydra.speak(); + +## $.Model + +[jQuery.Model $.Model] encapsulates the service and data layer. The following connects to a JSON REST service +and adds a helper to let us know if we can destroy a task: + + $.Model("Task",{ + findAll : "GET /tasks.json", + findOne : "GET /tasks/{id}.json", + create : "POST /tasks.json", + update : "PUT /tasks/{id}.json", + destroy : "DELETE /tasks/{id}.json" + },{ + canDestroy : function(){ + return this.acl.indexOf('w') > -1 + } + }); + +Assuming '/tasks.json' returns a JSON array like ... + + [{ + "id" : 1, + "name" : "take out trash", + "acl" : "rw", + "createdAt": 1303000731164 // April 16 2011 + }, + { + "id" : 2, + "name" : "do the dishes", + "acl" : "r" , + "createdAt": 1303087131164 // April 17 2011 + }] + +... the following will retrieve all tasks from the server and +then destroy tasks that the user is able to destroy: + + Task.findAll({}, function(tasks){ + for(var i =0; i < tasks.length; i++){ + + var task = tasks[i]; + + if( task.canDestroy() ){ + task.destroy(); + } + } + }); + +Model has a number of other useful features such as: + +
                                  +
                                • Listening to [jquery.model.events events].

                                  +@codestart +// listen to name changes on a task +task.bind("name", function(ev, newName){ + alert('task name = '+newName); +}); + +//change the task's name +task.attr('name', "laundry"); + +//listen for Tasks being created: +Task.bind("created", function(ev, newTask){ + // create newTask's html and add it to the page +}); +@codeend +
                                • +
                                • [jquery.model.typeconversion Converting] raw data into more useful objects.

                                  +@codestart +$.Model('Task', { + convert : { + 'date' : function(raw){ + return new Date(raw) + } + }, + attributes : { + 'createdAt' : 'date' + } +},{}); + +var task = new Task({ createdAt : 1303087131164}); + +// createdAt is now a date. +task.createdAt.getFullYear() // -> 2011 +@codeend +
                                • +
                                • Methods and utilities on [jQuery.Model.List lists] of instances.

                                  +@codestart +// define a task list +$.Model.List('Task.List',{ + + // add a helper method to a collection of tasks + canDestroyAll : function(){ + + return this.grep(function(task){ + return task.canDestroy(); + }).length === this.length + } +}); + +Task.findAll({}, function(tasks){ + + //tasks is a Task.List + tasks.canDestroyAll() //-> boolean +}) +@codeend +
                                • +
                                • [http://api.jquery.com/category/deferred-object/ Deferreds]

                                  +@codestart +// make 2 requests, and do something when they are +// both complete + +$.when( Task.findAll(), People.findAll() ) + .done(function(tasks, people){ + + // do something cool! +}) +@codeend +
                                • +
                                + +## $.View + +[jQuery.View $.View] is a template framework. It allows +you to use different template engines in the same way. + +The following requests tasks from the model, then +loads a template at "task/views/tasks.ejs", +renders it with tasks, and +inserts the result in the #tasks element. + + Task.findAll( {}, function(tasks){ + + $('#tasks').html( 'task/views/tasks.ejs', tasks ); + }); + +tasks.ejs might look like: + + <% $.each(this, function(task){ %> +
                              • <%= task.name %>
                              • + <% }) %> + +$.View understands [http://api.jquery.com/category/deferred-object/ deferreds] so the following does the exact same thing! + + $('#tasks').html( 'task/views/tasks.ejs', Task.findAll() ); + +Any template engine can be used with $.View. JavaScriptMVC comes with: + + - [jQuery.EJS] + - [Jaml] + - [Micro] + - [jQuery.tmpl] + +## $.Controller + +[jQuery.Controller $.Controller] is a jQuery widget factory. The +following creates a $.fn.list [jquery.controller.plugin plugin] that writes +a message into an element: + + $.Controller( "List", { + init: function( ) { + this.element.text( this.options.message ); + } + }); + + // create the list + $('#list').list({message: "Hello World"}); + +$.Controller lets you define [jQuery.Controller.static.defaults default options]: + + $.Controller( "List", { + defaults: { + message : "I am list" + } + },{ + init: function( ) { + this.element.text( this.options.message ); + } + }); + + // create's a list that writes "I am list" + $('#list').list(); + +Controller's best feature is that it organizes your event handlers, and +makes [jquery.controller.listening binding and unbinding] event +handlers extremely easy. The following listens for clicks on an +LI elements and alerts the element's text: + + $.Controller( "TaskList", { + init: function(){ + // uses a view to render tasks + this.element.html( "tasks.ejs", Task.findAll() ); + }, + "li click": function(el){ + alert( el.text() ); + } + }); + +Controller makes it easy to parameterize event binding. The following +listens for tasks being created and inserts them into the list: + + $.Controller( "TaskList", { + init: function( ) { + // uses a view to render tasks + this.element.html("tasks.ejs", Task.findAll()); + }, + "{Task} created": function( Task, ev, newTask ) { + this.element.append( "tasks.ejs", [newTask] ); + } + }); + +Finally, this makes it very easy to create widgets that work with any model: + + $.Controller( "List", { + init: function(){ + // uses a view to render tasks + this.element.html( this.options.view, + this.options.model.findAll( )); + }, + "{model} created": function( Model, ev, instance ){ + this.element.append( this.options.view, [instance] ); + } + }); + + $("#tasks").list({ model: Task, view: 'tasks.ejs' }); + $("#people").list({model: Person, view: 'people.ejs' }); + + + + + + \ No newline at end of file diff --git a/lang/json/json.js b/lang/json/json.js index 20803485..3c6457d8 100644 --- a/lang/json/json.js +++ b/lang/json/json.js @@ -13,17 +13,25 @@ * It is also influenced heavily by MochiKit's serializeJSON, which is * copyrighted 2005 by Bob Ippolito. */ - steal.plugins('jquery').then(function(){ -(function($) { - /** 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. - **/ + steal('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) @@ -131,8 +139,9 @@ } }; - /** jQuery.evalJSON(src) - Evaluates a given piece of json source. + /** + * @function jQuery.evalJSON + * Evaluates a given piece of json source. **/ $.evalJSON = function(src) { @@ -141,9 +150,10 @@ return eval("(" + src + ")"); }; - /** jQuery.secureEvalJSON(src) - Evals JSON in a way that is *more* secure. - **/ + /** + * @function jQuery.secureEvalJSON + * Evals JSON in a way that is *more* secure. + **/ $.secureEvalJSON = function(src) { if (typeof(JSON) == 'object' && JSON.parse) @@ -160,16 +170,18 @@ throw new SyntaxError("Error parsing JSON, source is not valid."); }; - /** jQuery.quoteString(string) - 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." + /** + * @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) { @@ -197,5 +209,4 @@ '"' : '\\"', '\\': '\\\\' }; -})(jQuery); }) \ No newline at end of file diff --git a/lang/lang.html b/lang/lang.html deleted file mode 100644 index 66ea99af..00000000 --- a/lang/lang.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - Lang Performance Test - - - -
                                -
                                - - - - - \ No newline at end of file diff --git a/lang/lang.js b/lang/lang.js deleted file mode 100644 index 680a8011..00000000 --- a/lang/lang.js +++ /dev/null @@ -1,109 +0,0 @@ -steal.plugins('jquery').then(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, - lowerUpper: /([a-z\d])([A-Z])/g, - dash: /([a-z\d])([A-Z])/g - }; - - /** - * @class jQuery.String - */ - var str = ($.String = - /* @Static*/ - { - /** - * @function strip - * @param {String} s returns a string with leading and trailing whitespace removed. - */ - strip: function( string ) { - return string.replace(/^\s+/, '').replace(/\s+$/, ''); - }, - /** - * Capitalizes a string - * @param {String} s the string to be lowercased. - * @return {String} a string with the first character capitalized, and everything else lowercased - */ - capitalize: function( s, cache ) { - return s.charAt(0).toUpperCase() + s.substr(1); - }, - - /** - * Returns if string ends with another string - * @param {String} s String that is being scanned - * @param {String} pattern What the string might end with - * @return {Boolean} true if the string ends wtih pattern, false if otherwise - */ - endsWith: function( s, pattern ) { - var d = s.length - pattern.length; - return d >= 0 && s.lastIndexOf(pattern) === d; - }, - /** - * 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 ) { - var parts = s.split(regs.undHash), - i = 1; - parts[0] = parts[0].charAt(0).toLowerCase() + parts[0].substr(1); - for (; i < parts.length; i++ ) { - parts[i] = str.capitalize(parts[i]); - } - - return parts.join(''); - }, - /** - * Like camelize, but the first part is also capitalized - * @param {String} s - * @return {String} the classized string - */ - classize: function( s ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) { - parts[i] = str.capitalize(parts[i]); - } - - return parts.join(''); - }, - /** - * Like [jQuery.String.static.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 ) { - var parts = s.split(regs.undHash), - i = 0; - for (; i < parts.length; i++ ) { - parts[i] = str.capitalize(parts[i]); - } - - return parts.join(' '); - }, - - /** - * 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.lowerUpper, '$1_$2').replace(regs.dash, '_').toLowerCase(); - } - }); - -}); \ No newline at end of file diff --git a/lang/lang_test.js b/lang/lang_test.js new file mode 100644 index 00000000..d80a9d96 --- /dev/null +++ b/lang/lang_test.js @@ -0,0 +1 @@ +steal('./object/object_test','./observe/observe_test','./string/string_test') diff --git a/lang/object/object.html b/lang/object/object.html new file mode 100644 index 00000000..7d4be073 --- /dev/null +++ b/lang/object/object.html @@ -0,0 +1,25 @@ + + + + object + + + +

                                object Demo

                                +

                                This is a dummy page to show off your plugin

                                + + + + \ No newline at end of file diff --git a/lang/object/object.js b/lang/object/object.js new file mode 100644 index 00000000..d81f23c7 --- /dev/null +++ b/lang/object/object.js @@ -0,0 +1,225 @@ +steal('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() + } +} + + +}); \ No newline at end of file diff --git a/lang/object/object_test.js b/lang/object/object_test.js new file mode 100644 index 00000000..c1952713 --- /dev/null +++ b/lang/object/object_test.js @@ -0,0 +1,105 @@ +steal('funcunit/qunit','./object',function(){ + +module("object"); + +test("same", function(){ + + + ok( $.Object.same({type: "FOLDER"},{type: "FOLDER", count: 5}, { + count: null + }), "count ignored" ); + + ok( $.Object.same({type: "folder"},{type: "FOLDER"}, { + type: "i" + }), "folder case ignored" ); +}) + +test("subsets", function(){ + + var res1 = $.Object.subsets({parentId: 5, type: "files"}, + [{parentId: 6}, {type: "folders"}, {type: "files"}]); + + same(res1,[{type: "files"}]) + + var res2 = $.Object.subsets({parentId: 5, type: "files"}, + [{}, {type: "folders"}, {type: "files"}]); + + same(res2,[{},{type: "files"}]); + + var res3 = $.Object.subsets({parentId: 5, type: "folders"}, + [{parentId: 5},{type: "files"}]); + + same(res3,[{parentId: 5}]) +}); + +test("subset compare", function(){ + + ok( $.Object.subset( + {type: "FOLDER"}, + {type: "FOLDER"}), + + "equal sets" ); + + ok( $.Object.subset( + {type: "FOLDER", parentId: 5}, + {type: "FOLDER"}), + + "sub set" ); + + ok(! $.Object.subset( + {type: "FOLDER"}, + {type: "FOLDER", parentId: 5}), + + "wrong way" ); + + + ok(! $.Object.subset( + {type: "FOLDER", parentId: 7}, + {type: "FOLDER", parentId: 5}), + + "different values" ); + + ok( $.Object.subset( + {type: "FOLDER", count: 5}, // subset + {type: "FOLDER"}, + {count: null} ), + + "count ignored" ); + + + ok( $.Object.subset( + {type: "FOLDER", kind: "tree"}, // subset + {type: "FOLDER", foo: true, bar: true }, + {foo: null, bar: null} ), + + "understands a subset" ); + ok( $.Object.subset( + {type: "FOLDER", foo: true, bar: true }, + {type: "FOLDER", kind: "tree"}, // subset + + {foo: null, bar: null, kind : null} ), + + "ignores nulls" ); +}); + +test("searchText", function(){ + var item = { + id: 1, + name: "thinger" + }, + searchText = { + searchText : "foo" + }, + compare = { + searchText : function(items, paramsText, itemr, params){ + equals(item,itemr); + equals(searchText, params) + return true; + } + }; + + ok( $.Object.subset( item, searchText, compare ), "searchText" ); +}); + + +}); \ No newline at end of file diff --git a/lang/object/qunit.html b/lang/object/qunit.html new file mode 100644 index 00000000..d661e08c --- /dev/null +++ b/lang/object/qunit.html @@ -0,0 +1,17 @@ + + + + object QUnit Test + + + + +

                                object Test Suite

                                +

                                +
                                +

                                +
                                +
                                  +
                                  + + \ No newline at end of file diff --git a/lang/observe/delegate/delegate.js b/lang/observe/delegate/delegate.js new file mode 100644 index 00000000..385ac26e --- /dev/null +++ b/lang/observe/delegate/delegate.js @@ -0,0 +1,313 @@ +steal('jquery/lang/observe',function(){ + + /** + * @add jQuery.Observe.prototype + */ + + // ** - 'this' will be the deepest item changed + // * - 'this' will be any changes within *, but * will be the + // this returned + + // tells if the parts part of a delegate matches the broken up props of the event + // gives the prop to use as 'this' + // - parts - the attribute name of the delegate split in parts ['foo','*'] + // - props - the split props of the event that happened ['foo','bar','0'] + // - returns - the attribute to delegate too ('foo.bar'), or null if not a match + var matches = function(parts, props){ + //check props parts are the same or + var len = parts.length, + i =0, + // keeps the matched props we will use + matchedProps = [], + prop; + + // if the event matches + for(i; i< len; i++){ + prop = props[i] + // if no more props (but we should be matching them) + // return null + if( typeof prop !== 'string' ) { + return null; + } else + // if we have a "**", match everything + if( parts[i] == "**" ) { + return props.join("."); + } else + // a match, but we want to delegate to "*" + if (parts[i] == "*"){ + // only do this if there is nothing after ... + matchedProps.push(prop); + } + else if( prop === parts[i] ) { + matchedProps.push(prop); + } else { + return null; + } + } + return matchedProps.join("."); + }, + // gets a change event and tries to figure out which + // delegates to call + delegate = function(event, prop, how, newVal, oldVal){ + // pre-split properties to save some regexp time + var props = prop.split("."), + delegates = ($.data(this,"_observe_delegates") || []).slice(0), + delegate, + attr, + matchedAttr, + hasMatch, + valuesEqual; + event.attr = prop; + event.lastAttr = props[props.length -1 ]; + // for each delegate + for(var i =0; delegate = delegates[i++];){ + + // if there is a batchNum, this means that this + // event is part of a series of events caused by a single + // attrs call. We don't want to issue the same event + // multiple times + // setting the batchNum happens later + if((event.batchNum && delegate.batchNum === event.batchNum) || delegate.undelegated ){ + continue; + } + + // reset match and values tests + hasMatch = undefined; + valuesEqual = true; + + // for each attr in a delegate + for(var a =0 ; a < delegate.attrs.length; a++){ + + attr = delegate.attrs[a]; + + // check if it is a match + if(matchedAttr = matches(attr.parts, props)){ + hasMatch = matchedAttr; + } + // if it has a value, make sure it's the right value + // if it's set, we should probably check that it has a + // value no matter what + if(attr.value && valuesEqual /* || delegate.hasValues */){ + valuesEqual = attr.value === ""+this.attr(attr.attr) + } else if (valuesEqual && delegate.attrs.length > 1){ + // if there are multiple attributes, each has to at + // least have some value + valuesEqual = this.attr(attr.attr) !== undefined + } + } + + // if there is a match and valuesEqual ... call back + + if(hasMatch && valuesEqual) { + // how to get to the changed property from the delegate + var from = prop.replace(hasMatch+".",""); + + // if this event is part of a batch, set it on the delegate + // to only send one event + if(event.batchNum ){ + delegate.batchNum = event.batchNum + } + + // if we listen to change, fire those with the same attrs + // TODO: the attrs should probably be using from + if( delegate.event === 'change' ){ + arguments[1] = from; + event.curAttr = hasMatch; + delegate.callback.apply(this.attr(hasMatch), $.makeArray( arguments)); + } else if(delegate.event === how ){ + + // if it's a match, callback with the location of the match + delegate.callback.apply(this.attr(hasMatch), [event,newVal, oldVal, from]); + } else if(delegate.event === 'set' && + how == 'add' ) { + // if we are listening to set, we should also listen to add + delegate.callback.apply(this.attr(hasMatch), [event,newVal, oldVal, from]); + } + } + + } + }; + + $.extend($.Observe.prototype,{ + /** + * @plugin jquery/lang/observe/delegate + * Listen for changes in a child attribute from the parent. The child attribute + * does not have to exist. + * + * + * // create an observable + * var observe = $.O({ + * foo : { + * bar : "Hello World" + * } + * }) + * + * //listen to changes on a property + * observe.delegate("foo.bar","change", function(ev, prop, how, newVal, oldVal){ + * // foo.bar has been added, set, or removed + * this //-> + * }); + * + * // change the property + * observe.attr('foo.bar',"Goodbye Cruel World") + * + * ## Types of events + * + * Delegate lets you listen to add, set, remove, and change events on property. + * + * __add__ + * + * An add event is fired when a new property has been added. + * + * var o = new $.Observe({}); + * o.delegate("name","add", function(ev, value){ + * // called once + * $('#name').show() + * }) + * o.attr('name',"Justin") + * o.attr('name',"Brian"); + * + * Listening to add events is useful for 'setup' functionality (in this case + * showing the #name element. + * + * __set__ + * + * Set events are fired when a property takes on a new value. set events are + * always fired after an add. + * + * o.delegate("name","set", function(ev, value){ + * // called twice + * $('#name').text(value) + * }) + * o.attr('name',"Justin") + * o.attr('name',"Brian"); + * + * __remove__ + * + * Remove events are fired after a property is removed. + * + * o.delegate("name","remove", function(ev){ + * // called once + * $('#name').text(value) + * }) + * o.attr('name',"Justin"); + * o.removeAttr('name'); + * + * ## Wildcards - matching multiple properties + * + * Sometimes, you want to know when any property within some part + * of an observe has changed. Delegate lets you use wildcards to + * match any property name. The following listens for any change + * on an attribute of the params attribute: + * + * var o = $.Observe({ + * options : { + * limit : 100, + * offset: 0, + * params : { + * parentId: 5 + * } + * } + * }) + * o.delegate('options.*','change', function(){ + * alert('1'); + * }) + * o.delegate('options.**','change', function(){ + * alert('2'); + * }) + * + * // alerts 1 + * // alerts 2 + * o.attr('options.offset',100) + * + * // alerts 2 + * o.attr('options.params.parentId',6); + * + * Using a single wildcard (*) matches single level + * properties. Using a double wildcard (**) matches + * any deep property. + * + * ## Listening on multiple properties and values + * + * Delegate lets you listen on multiple values at once, for example, + * + * @param {String} selector the attributes you want to listen for changes in. + * @param {String} event the event name + * @param {Function} cb the callback handler + * @return {jQuery.Delegate} the delegate for chaining + */ + delegate : function(selector, event, cb){ + selector = $.trim(selector); + var delegates = $.data(this, "_observe_delegates") || + $.data(this, "_observe_delegates", []), + attrs = []; + + // split selector by spaces + selector.replace(/([^\s=]+)=?([^\s]+)?/g, function(whole, attr, value){ + attrs.push({ + // the attribute name + attr: attr, + // the attribute's pre-split names (for speed) + parts: attr.split('.'), + // the value associated with this prop + value: value + }) + }); + + // delegates has pre-processed info about the event + delegates.push({ + // the attrs name for unbinding + selector : selector, + // an object of attribute names and values {type: 'recipe',id: undefined} + // undefined means a value was not defined + attrs : attrs, + callback : cb, + event: event + }); + if(delegates.length === 1){ + this.bind("change",delegate) + } + return this; + }, + /** + * @plugin jquery/lang/observe/delegate + * Removes a delegate event handler. + * + * observe.undelegate("name","set", function(){ ... }) + * + * @param {String} selector the attribute name of the object you want to undelegate from. + * @param {String} event the event name + * @param {Function} cb the callback handler + * @return {jQuery.Delegate} the delegate for chaining + */ + undelegate : function(selector, event, cb){ + selector = $.trim(selector); + + var i =0, + delegates = $.data(this, "_observe_delegates") || [], + delegateOb; + if(selector){ + while(i < delegates.length){ + delegateOb = delegates[i]; + if( delegateOb.callback === cb || + (!cb && delegateOb.selector === selector) ){ + delegateOb.undelegated = true; + delegates.splice(i,1) + } else { + i++; + } + } + } else { + // remove all delegates + delegates = []; + } + if(!delegates.length){ + $.removeData(this, "_observe_delegates"); + this.unbind("change",delegate) + } + return this; + } + }); + // add helpers for testing .. + $.Observe.prototype.delegate.matches = matches; +}) diff --git a/lang/observe/delegate/delegate_test.js b/lang/observe/delegate/delegate_test.js new file mode 100644 index 00000000..c938e285 --- /dev/null +++ b/lang/observe/delegate/delegate_test.js @@ -0,0 +1,253 @@ +steal('funcunit/qunit','jquery/lang/observe',function(){ + + +module('jquery/lang/observe/delegate') + +var matches = $.Observe.prototype.delegate.matches; + +test("matches", function(){ + + equals( matches(['**'], ['foo','bar','0']) , + 'foo.bar.0' , "everything" ); + + equals( matches(['*.**'], ['foo']) , + null , "everything at least one level deep" ) + + equals( matches(['foo','*'], ['foo','bar','0']) , + 'foo.bar' ) + + equals(matches(['*'], + ['foo','bar','0']) , + 'foo' ); + + equals( matches([ '*', 'bar' ], + ['foo','bar','0']) , + 'foo.bar' ) + // - props - + // - returns - 'foo.bar' +}) + +test("list events", function(){ + + var list = new $.Observe.List([ + {name: 'Justin'}, + {name: 'Brian'}, + {name: 'Austin'}, + {name: 'Mihael'}]) + list.comparator = 'name'; + list.sort(); + // events on a list + // - move - item from one position to another + // due to changes in elements that change the sort order + // - add (items added to a list) + // - remove (items removed from a list) + // - reset (all items removed from the list) + // - change something happened + + // a move directly on this list + list.bind('move', function(ev, item, newPos, oldPos){ + ok(true,"move called"); + equals(item.name, "Zed"); + equals(newPos, 3); + equals(oldPos, 0); + }); + + // a remove directly on this list + list.bind('remove', function(ev, items, oldPos){ + ok(true,"remove called"); + equals(items.length,1); + equals(items[0].name, 'Alexis'); + equals(oldPos, 0, "put in right spot") + }) + list.bind('add', function(ev, items, newPos){ + ok(true,"add called"); + equals(items.length,1); + equals(items[0].name, 'Alexis'); + equals(newPos, 0, "put in right spot") + }); + + list.push({name: 'Alexis'}); + + // now lets remove alexis ... + list.splice(0,1); + list[0].attr('name',"Zed") +}) + + +test("delegate", 4,function(){ + + var state = new $.Observe({ + properties : { + prices : [] + } + }); + var prices = state.attr('properties.prices'); + + state.delegate("properties.prices","change", function(ev, attr, how, val, old){ + equals(attr, "0", "correct change name") + equals(how, "add") + equals(val[0].attr("foo"),"bar", "correct") + ok(this === prices, "rooted element") + }); + + prices.push({foo: "bar"}); + + state.undelegate(); + +}) +test("delegate on add", 2, function(){ + + var state = new $.Observe({}); + + state.delegate("foo","add", function(ev, newVal){ + ok(true, "called"); + equals(newVal, "bar","got newVal") + }).delegate("foo","remove", function(){ + ok(false,"remove should not be called") + }); + + state.attr("foo","bar") + +}) + +test("delegate set is called on add", 2, function(){ + var state = new $.Observe({}); + + state.delegate("foo","set", function(ev, newVal){ + ok(true, "called"); + equals(newVal, "bar","got newVal") + }); + state.attr("foo","bar") +}); + +test("delegate's this", 5, function(){ + var state = new $.Observe({ + person : { + name : { + first : "justin", + last : "meyer" + } + }, + prop : "foo" + }); + var n = state.attr('person.name'), + check + + // listen to person name changes + state.delegate("person.name","set", check = function(ev, newValue, oldVal, from){ + // make sure we are getting back the person.name + equals(this, n) + equals(newValue, "Brian"); + equals(oldVal, "justin"); + // and how to get there + equals(from,"first") + }); + n.attr('first',"Brian"); + state.undelegate("person.name",'set',check) + // stop listening + + // now listen to changes in prop + state.delegate("prop","set", function(){ + equals(this, 'food'); + }); // this is weird, probably need to support direct bind ... + + // update the prop + state.attr('prop','food') +}) + + +test("delegate on deep properties with *", function(){ + var state = new $.Observe({ + person : { + name : { + first : "justin", + last : "meyer" + } + } + }); + + state.delegate("person","set", function(ev, newVal, oldVal, attr){ + equals(this, state.attr('person'), "this is set right") + equals(attr, "name.first") + }); + state.attr("person.name.first","brian") +}); + +test("compound sets", function(){ + + var state = new $.Observe({ + type : "person", + id: "5" + }); + var count = 0; + state.delegate("type=person id","set", function(){ + equals(state.type, "person","type is person") + ok(state.id !== undefined, "id has value"); + count++; + }) + + // should trigger a change + state.attr("id",0); + equals(count, 1, "changing the id to 0 caused a change"); + + // should not fire a set + state.removeAttr("id") + equals(count, 1, "removing the id changed nothing"); + + state.attr("id",3) + equals(count, 2, "adding an id calls callback"); + + state.attr("type","peter") + equals(count, 2, "changing the type does not fire callback"); + + state.removeAttr("type"); + state.removeAttr("id"); + + equals(count, 2, ""); + + state.attrs({ + type : "person", + id: "5" + }); + + equals(count, 3, "setting person and id only fires 1 event"); + + state.removeAttr("type"); + state.removeAttr("id"); + + state.attrs({ + type : "person" + }); + equals(count, 3, "setting person does not fire anything"); +}) + +test("undelegate within event loop",1, function(){ + + var state = new $.Observe({ + type : "person", + id: "5" + }); + var f1 = function(){ + state.undelegate("type","add",f2); + }, + f2 = function(){ + ok(false,"I am removed, how am I called") + }, + f3 = function(){ + state.undelegate("type","add",f1); + }, + f4 = function(){ + ok(true,"f4 called") + }; + state.delegate("type", "set", f1); + state.delegate("type","set",f2); + state.delegate("type","set",f3); + state.delegate("type","set",f4); + state.attr("type","other"); + +}) + + + + +}); \ No newline at end of file diff --git a/lang/observe/demo.html b/lang/observe/demo.html new file mode 100644 index 00000000..301be853 --- /dev/null +++ b/lang/observe/demo.html @@ -0,0 +1,140 @@ + + + + object + + + +

                                  My Unsorted Todo List

                                  +
                                    + +
                                  +

                                  My Sorted Todo List

                                  +
                                    + +
                                  + +
                                  + + + + + \ No newline at end of file diff --git a/lang/observe/observe.html b/lang/observe/observe.html new file mode 100644 index 00000000..4a9d0a8f --- /dev/null +++ b/lang/observe/observe.html @@ -0,0 +1,83 @@ + + + + object + + + +

                                  $.Observe Demo

                                  +

                                  Make changes to the JSON in the following textarea. + Click Update Attrs to merge the JSON and see the events produced.

                                  +
                                  o = new $.Observe({})
                                  +
                                  o.attrs(
                                  +, true) +
                                  
                                  +	
                                  o.bind('change', function( ev, attr, how, newValue, prevValue, where ) { 
                                  + + + + + + + + + +
                                  attrhownewValueprevValuewhere
                                  + + + + + + \ No newline at end of file diff --git a/lang/observe/observe.js b/lang/observe/observe.js new file mode 100644 index 00000000..e018cc07 --- /dev/null +++ b/lang/observe/observe.js @@ -0,0 +1,1021 @@ +steal('jquery/class').then(function() { + + // Alias helpful methods from jQuery + var isArray = $.isArray, + isObject = function( obj ) { + return typeof obj === 'object' && obj !== null && obj; + }, + makeArray = $.makeArray, + each = $.each, + // listens to changes on val and 'bubbles' the event up + // - val the object to listen to changes on + // - prop the property name val is at on + // - parent the parent object of prop + hookup = function( val, prop, parent ) { + // if it's an array make a list, otherwise a val + if (val instanceof $.Observe){ + // we have an observe already + // make sure it is not listening to this already + unhookup([val], parent._namespace) + } else if ( isArray(val) ) { + val = new $.Observe.List(val) + } else { + val = new $.Observe(val) + } + // attr (like target, how you (delegate) to get to the target) + // currentAttr (how to get to you) + // delegateAttr (hot to get to the delegated Attr) + + // + // + //listen to all changes and trigger upwards + val.bind("change" + parent._namespace, function( ev, attr ) { + // trigger the type on this ... + var args = $.makeArray(arguments), + ev = args.shift(); + if(prop === "*"){ + args[0] = parent.indexOf(val)+"." + args[0] + } else { + args[0] = prop + "." + args[0] + } + // change the attr + //ev.origTarget = ev.origTarget || ev.target; + // the target should still be the original object ... + $.event.trigger(ev, args, parent) + }); + + return val; + }, + unhookup = function(items, namespace){ + var item; + for(var i =0; i < items.length; i++){ + item = items[i] + if( item && item.unbind ){ + item.unbind("change" + namespace) + } + } + }, + // an id to track events for a given observe + id = 0, + collecting = null, + // call to start collecting events (Observe sends all events at once) + collect = function() { + if (!collecting ) { + collecting = []; + return true; + } + }, + // creates an event on item, but will not send immediately + // if collecting events + // - item - the item the event should happen on + // - event - the event name ("change") + // - args - an array of arguments + trigger = function( item, event, args ) { + // send no events if initalizing + if (item._init) { + return; + } + if (!collecting ) { + return $.event.trigger(event, args, item, true) + } else { + collecting.push({ + t: item, + ev: event, + args: args + }) + } + }, + // which batch of events this is for, might not want to send multiple + // messages on the same batch. This is mostly for + // event delegation + batchNum = 0, + // sends all pending events + sendCollection = function() { + var len = collecting.length, + items = collecting.slice(0), + cur; + collecting = null; + batchNum ++; + for ( var i = 0; i < len; i++ ) { + cur = items[i]; + // batchNum + $.event.trigger({ + type: cur.ev, + batchNum : batchNum + }, cur.args, cur.t) + } + + }, + // a helper used to serialize an Observe or Observe.List where: + // observe - the observable + // how - to serialize with 'attrs' or 'serialize' + // where - to put properties, in a {} or []. + serialize = function( observe, how, where ) { + // go through each property + observe.each(function( name, val ) { + // if the value is an object, and has a attrs or serialize function + where[name] = isObject(val) && typeof val[how] == 'function' ? + // call attrs or serialize to get the original data back + val[how]() : + // otherwise return the value + val + }) + return where; + }; + + /** + * @class jQuery.Observe + * @parent jquerymx.lang + * @test jquery/lang/observe/qunit.html + * + * Observe provides the awesome observable pattern for + * JavaScript Objects and Arrays. It lets you + * + * - Set and remove property or property values on objects and arrays + * - Listen for changes in objects and arrays + * - Work with nested properties + * + * ## Creating an $.Observe + * + * To create an $.Observe, or $.Observe.List, you can simply use + * the `$.O(data)` shortcut like: + * + * var person = $.O({name: 'justin', age: 29}), + * hobbies = $.O(['programming', 'basketball', 'nose picking']) + * + * Depending on the type of data passed to $.O, it will create an instance of either: + * + * - $.Observe, which is used for objects like: `{foo: 'bar'}`, and + * - [jQuery.Observe.List $.Observe.List], which is used for arrays like `['foo','bar']` + * + * $.Observe.List and $.Observe are very similar. In fact, + * $.Observe.List inherits $.Observe and only adds a few extra methods for + * manipulating arrays like [jQuery.Observe.List.prototype.push push]. Go to + * [jQuery.Observe.List $.Observe.List] for more information about $.Observe.List. + * + * You can also create a `new $.Observe` simply by pass it the data you want to observe: + * + * var data = { + * addresses : [ + * { + * city: 'Chicago', + * state: 'IL' + * }, + * { + * city: 'Boston', + * state : 'MA' + * } + * ], + * name : "Justin Meyer" + * }, + * o = new $.Observe(data); + * + * _o_ now represents an observable copy of _data_. + * + * ## Getting and Setting Properties + * + * Use [jQuery.Observe.prototype.attr attr] and [jQuery.Observe.prototype.attr attrs] + * to get and set properties. + * + * For example, you can read the property values of _o_ with + * `observe.attr( name )` like: + * + * // read name + * o.attr('name') //-> Justin Meyer + * + * And set property names of _o_ with + * `observe.attr( name, value )` like: + * + * // update name + * o.attr('name', "Brian Moschel") //-> o + * + * Observe handles nested data. Nested Objects and + * Arrays are converted to $.Observe and + * $.Observe.Lists. This lets you read nested properties + * and use $.Observe methods on them. The following + * updates the second address (Boston) to 'New York': + * + * o.attr('addresses.1').attrs({ + * city: 'New York', + * state: 'NY' + * }) + * + * `attrs()` can be used to get all properties back from the observe: + * + * o.attrs() // -> + * { + * addresses : [ + * { + * city: 'Chicago', + * state: 'IL' + * }, + * { + * city: 'New York', + * state : 'MA' + * } + * ], + * name : "Brian Moschel" + * } + * + * ## Listening to property changes + * + * When a property value is changed, it creates events + * that you can listen to. There are two ways to listen + * for events: + * + * - [jQuery.Observe.prototype.bind bind] - listen for any type of change + * - [jQuery.Observe.prototype.delegate delegate] - listen to a specific type of change + * + * With `bind( "change" , handler( ev, attr, how, newVal, oldVal ) )`, you can listen + * to any change that happens within the + * observe. The handler gets called with the property name that was + * changed, how it was changed ['add','remove','set'], the new value + * and the old value. + * + * o.bind('change', function( ev, attr, how, nevVal, oldVal ) { + * + * }) + * + * `delegate( attr, event, handler(ev, newVal, oldVal ) )` lets you listen + * to a specific event on a specific attribute. + * + * // listen for name changes + * o.delegate("name","set", function(){ + * + * }) + * + * Delegate lets you specify multiple attributes and values to match + * for the callback. For example, + * + * r = $.O({type: "video", id : 5}) + * r.delegate("type=images id","set", function(){}) + * + * This is used heavily by [jQuery.route $.route]. + * + * @constructor + * + * @param {Object} obj a JavaScript Object that will be + * converted to an observable + */ + $.Class('jQuery.Observe', + /** + * @prototype + */ + { + init: function( obj ) { + // _data is where we keep the properties + this._data = {}; + // the namespace this object uses to listen to events + this._namespace = ".observe" + (++id); + // sets all attrs + this._init = true; + this.attrs(obj); + delete this._init; + }, + /** + * Get or set an attribute on the observe. + * + * o = new $.Observe({}); + * + * // sets a user property + * o.attr('user',{name: 'hank'}); + * + * // read the user's name + * o.attr('user.name') //-> 'hank' + * + * If a value is set for the first time, it will trigger + * an `'add'` and `'set'` change event. Once + * the value has been added. Any future value changes will + * trigger only `'set'` events. + * + * + * @param {String} attr the attribute to read or write. + * + * o.attr('name') //-> reads the name + * o.attr('name', 'Justin') //-> writes the name + * + * You can read or write deep property names. For example: + * + * o.attr('person', {name: 'Justin'}) + * o.attr('person.name') //-> 'Justin' + * + * @param {Object} [val] if provided, sets the value. + * @return {Object} the observable or the attribute property. + * + * If you are reading, the property value is returned: + * + * o.attr('name') //-> Justin + * + * If you are writing, the observe is returned for chaining: + * + * o.attr('name',"Brian").attr('name') //-> Justin + */ + attr: function( attr, val ) { + + if ( val === undefined ) { + // if we are getting a value + return this._get(attr) + } else { + // otherwise we are setting + this._set(attr, val); + return this; + } + }, + /** + * Iterates through each attribute, calling handler + * with each attribute name and value. + * + * new Observe({foo: 'bar'}) + * .each(function(name, value){ + * equals(name, 'foo') + * equals(value,'bar') + * }) + * + * @param {function} handler(attrName,value) A function that will get + * called back with the name and value of each attribute on the observe. + * + * Returning `false` breaks the looping. The following will never + * log 3: + * + * new Observe({a : 1, b : 2, c: 3}) + * .each(function(name, value){ + * console.log(value) + * if(name == 2){ + * return false; + * } + * }) + * + * @return {jQuery.Observe} the original observable. + */ + each: function() { + return each.apply(null, [this.__get()].concat(makeArray(arguments))) + }, + /** + * Removes a property + * + * o = new $.Observe({foo: 'bar'}); + * o.removeAttr('foo'); //-> 'bar' + * + * This creates a `'remove'` change event. Learn more about events + * in [jQuery.Observe.prototype.bind bind] and [jQuery.Observe.prototype.delegate delegate]. + * + * @param {String} attr the attribute name to remove. + * @return {Object} the value that was removed. + */ + removeAttr: function( attr ) { + // convert the attr into parts (if nested) + var parts = isArray(attr) ? attr : attr.split("."), + // the actual property to remove + prop = parts.shift(), + // the current value + current = this._data[prop]; + + // if we have more parts, call removeAttr on that part + if ( parts.length ) { + return current.removeAttr(parts) + } else { + // otherwise, delete + delete this._data[prop]; + // create the event + trigger(this, "change", [prop, "remove", undefined, current]); + return current; + } + }, + // reads a property from the object + _get: function( attr ) { + var parts = isArray(attr) ? attr : (""+attr).split("."), + current = this.__get(parts.shift()); + if ( parts.length ) { + return current ? current._get(parts) : undefined + } else { + return current; + } + }, + // reads a property directly if an attr is provided, otherwise + // returns the 'real' data object itself + __get: function( attr ) { + return attr ? this._data[attr] : this._data; + }, + // sets attr prop as value on this object where + // attr - is a string of properties or an array of property values + // value - the raw value to set + // description - an object with converters / serializers / defaults / getterSetters? + _set: function( attr, value ) { + // convert attr to attr parts (if it isn't already) + var parts = isArray(attr) ? attr : ("" + attr).split("."), + // the immediate prop we are setting + prop = parts.shift(), + // its current value + current = this.__get(prop); + + // if we have an object and remaining parts + if ( isObject(current) && parts.length ) { + // that object should set it (this might need to call attr) + current._set(parts, value) + } else if (!parts.length ) { + // otherwise, we are setting it on this object + // todo: check if value is object and transform + // are we changing the value + if ( value !== current ) { + + // check if we are adding this for the first time + // if we are, we need to create an 'add' event + var changeType = this.__get().hasOwnProperty(prop) ? "set" : "add"; + + // set the value on data + this.__set(prop, + // if we are getting an object + isObject(value) ? + // hook it up to send event to us + hookup(value, prop, this) : + // value is normal + value); + + + + // trigger the change event + trigger(this, "change", [prop, changeType, value, current]); + + // if we can stop listening to our old value, do it + current && unhookup([current], this._namespace); + } + + } else { + throw "jQuery.Observe: set a property on an object that does not exist" + } + }, + // directly sets a property on this object + __set: function( prop, val ) { + this._data[prop] = val; + // add property directly for easy writing + // check if its on the prototype so we don't overwrite methods like attrs + if (!(prop in this.constructor.prototype)) { + this[prop] = val + } + }, + /** + * Listens to changes on a jQuery.Observe. + * + * When attributes of an observe change, including attributes on nested objects, + * a `'change'` event is triggered on the observe. These events come + * in three flavors: + * + * - `add` - a attribute is added + * - `set` - an existing attribute's value is changed + * - `remove` - an attribute is removed + * + * The change event is fired with: + * + * - the attribute changed + * - how it was changed + * - the newValue of the attribute + * - the oldValue of the attribute + * + * Example: + * + * o = new $.Observe({name : "Payal"}); + * o.bind('change', function(ev, attr, how, newVal, oldVal){ + * // ev -> {type: 'change'} + * // attr -> "name" + * // how -> "add" + * // newVal-> "Justin" + * // oldVal-> undefined + * }) + * + * o.attr('name', 'Justin') + * + * Listening to `change` is only useful for when you want to + * know every change on an Observe. For most applications, + * [jQuery.Observe.prototype.delegate delegate] is + * much more useful as it lets you listen to specific attribute + * changes and sepecific types of changes. + * + * + * @param {String} eventType the event name. Currently, + * only 'change' events are supported. For more fine + * grained control, use [jQuery.Observe.prototype.delegate]. + * + * @param {Function} handler(event, attr, how, newVal, oldVal) A + * callback function where + * + * - event - the event + * - attr - the name of the attribute changed + * - how - how the attribute was changed (add, set, remove) + * - newVal - the new value of the attribute + * - oldVal - the old value of the attribute + * + * @return {$.Observe} the observe for chaining. + */ + bind: function( eventType, handler ) { + $.fn.bind.apply($([this]), arguments); + return this; + }, + /** + * Unbinds a listener. This uses [http://api.jquery.com/unbind/ jQuery.unbind] + * and works very similar. This means you can + * use namespaces or unbind all event handlers for a given event: + * + * // unbind a specific event handler + * o.unbind('change', handler) + * + * // unbind all change event handlers bound with the + * // foo namespace + * o.unbind('change.foo') + * + * // unbind all change event handlers + * o.unbind('change') + * + * @param {String} eventType - the type of event with + * any optional namespaces. Currently, only `change` events + * are supported with bind. + * + * @param {Function} [handler] - The original handler function passed + * to [jQuery.Observe.prototype.bind bind]. + * + * @return {jQuery.Observe} the original observe for chaining. + */ + unbind: function( eventType, handler ) { + $.fn.unbind.apply($([this]), arguments); + return this; + }, + /** + * Get the serialized Object form of the observe. Serialized + * data is typically used to send back to a server. + * + * o.serialize() //-> { name: 'Justin' } + * + * Serialize currently returns the same data + * as [jQuery.Observe.prototype.attrs]. However, in future + * versions, serialize will be able to return serialized + * data similar to [jQuery.Model]. The following will work: + * + * new Observe({time: new Date()}) + * .serialize() //-> { time: 1319666613663 } + * + * @return {Object} a JavaScript Object that can be + * serialized with `JSON.stringify` or other methods. + * + */ + serialize: function() { + return serialize(this, 'serialize', {}); + }, + /** + * Set multiple properties on the observable + * @param {Object} props + * @param {Boolean} remove true if you should remove properties that are not in props + */ + attrs: function( props, remove ) { + if ( props === undefined ) { + return serialize(this, 'attrs', {}) + } + + props = $.extend(true, {}, props); + var prop, collectingStarted = collect(); + + for ( prop in this._data ) { + var curVal = this._data[prop], + newVal = props[prop]; + + // if we are merging ... + if ( newVal === undefined ) { + remove && this.removeAttr(prop); + continue; + } + if ( isObject(curVal) && isObject(newVal) ) { + curVal.attrs(newVal, remove) + } else if ( curVal != newVal ) { + this._set(prop, newVal) + } else { + + } + delete props[prop]; + } + // add remaining props + for ( var prop in props ) { + newVal = props[prop]; + this._set(prop, newVal) + } + if ( collectingStarted ) { + sendCollection(); + } + } + }); + // Helpers for list + /** + * @class jQuery.Observe.List + * @inherits jQuery.Observe + * @parent jQuery.Observe + * + * An observable list. You can listen to when items are push, popped, + * spliced, shifted, and unshifted on this array. + * + * + */ + var list = jQuery.Observe('jQuery.Observe.List', + /** + * @prototype + */ + { + init: function( instances, options ) { + this.length = 0; + this._namespace = ".list" + (++id); + this._init = true; + this.bind('change',this.proxy('_changes')); + this.push.apply(this, makeArray(instances || [])); + $.extend(this, options); + if(this.comparator){ + this.sort() + } + delete this._init; + }, + _changes : function(ev, attr, how, newVal, oldVal){ + // detects an add, sorts it, re-adds? + //console.log("") + + + + // if we are sorting, and an attribute inside us changed + if(this.comparator && /^\d+./.test(attr) ) { + + // get the index + var index = +(/^\d+/.exec(attr)[0]), + // and item + item = this[index], + // and the new item + newIndex = this.sortedIndex(item); + + if(newIndex !== index){ + // move ... + [].splice.call(this, index, 1); + [].splice.call(this, newIndex, 0, item); + + trigger(this, "move", [item, newIndex, index]); + ev.stopImmediatePropagation(); + trigger(this,"change", [ + attr.replace(/^\d+/,newIndex), + how, + newVal, + oldVal + ]); + return; + } + } + + + // if we add items, we need to handle + // sorting and such + + // trigger direct add and remove events ... + if(attr.indexOf('.') === -1){ + + if( how === 'add' ) { + trigger(this, how, [newVal,+attr]); + } else if( how === 'remove' ) { + trigger(this, how, [oldVal, +attr]) + } + + } + // issue add, remove, and move events ... + }, + sortedIndex : function(item){ + var itemCompare = item.attr(this.comparator), + equaled = 0, + i; + for(var i =0; i < this.length; i++){ + if(item === this[i]){ + equaled = -1; + continue; + } + if(itemCompare <= this[i].attr(this.comparator) ) { + return i+equaled; + } + } + return i+equaled; + }, + __get : function(attr){ + return attr ? this[attr] : this; + }, + __set : function(attr, val){ + this[attr] = val; + }, + /** + * Returns the serialized form of this list. + */ + serialize: function() { + return serialize(this, 'serialize', []); + }, + /** + * Iterates through each item of the list, calling handler + * with each index and value. + * + * new Observe.List(['a']) + * .each(function(index, value){ + * equals(index, 1) + * equals(value,'a') + * }) + * + * @param {function} handler(index,value) A function that will get + * called back with the index and value of each item on the list. + * + * Returning `false` breaks the looping. The following will never + * log 'c': + * + * new Observe(['a','b','c']) + * .each(function(index, value){ + * console.log(value) + * if(index == 1){ + * return false; + * } + * }) + * + * @return {jQuery.Observe.List} the original observable. + */ + // placeholder for each + /** + * Remove items or add items from a specific point in the list. + * + * ### Example + * + * The following creates a list of numbers and replaces 2 and 3 with + * "a", and "b". + * + * var l = new $.Observe.List([0,1,2,3]); + * + * l.bind('change', function( ev, attr, how, newVals, oldVals, where ) { ... }) + * + * l.splice(1,2, "a", "b"); // results in [0,"a","b",3] + * + * This creates 2 change events. The first event is the removal of + * numbers one and two where it's callback values will be: + * + * - attr - "1" - indicates where the remove event took place + * - how - "remove" + * - newVals - undefined + * - oldVals - [1,2] -the array of removed values + * - where - 1 - the location of where these items where removed + * + * The second change event is the addition of the "a", and "b" values where + * the callback values will be: + * + * - attr - "1" - indicates where the add event took place + * - how - "added" + * - newVals - ["a","b"] + * - oldVals - [1, 2] - the array of removed values + * - where - 1 - the location of where these items where added + * + * @param {Number} index where to start removing or adding items + * @param {Object} count the number of items to remove + * @param {Object} [added] an object to add to + */ + splice: function( index, count ) { + var args = makeArray(arguments), + i; + + for ( i = 2; i < args.length; i++ ) { + var val = args[i]; + if ( isObject(val) ) { + args[i] = hookup(val, "*", this) + } + } + if ( count === undefined ) { + count = args[1] = this.length - index; + } + var removed = [].splice.apply(this, args); + if ( count > 0 ) { + trigger(this, "change", [""+index, "remove", undefined, removed]); + unhookup(removed, this._namespace); + } + if ( args.length > 2 ) { + trigger(this, "change", [""+index, "add", args.slice(2), removed]); + } + return removed; + }, + /** + * Updates an array with a new array. It is able to handle + * removes in the middle of the array. + * + * @param {Array} props + * @param {Boolean} remove + */ + attrs: function( props, remove ) { + if ( props === undefined ) { + return serialize(this, 'attrs', []); + } + + // copy + props = props.slice(0); + + var len = Math.min(props.length, this.length), + collectingStarted = collect(); + for ( var prop = 0; prop < len; prop++ ) { + var curVal = this[prop], + newVal = props[prop]; + + if ( isObject(curVal) && isObject(newVal) ) { + curVal.attrs(newVal, remove) + } else if ( curVal != newVal ) { + this._set(prop, newVal) + } else { + + } + } + if ( props.length > this.length ) { + // add in the remaining props + this.push(props.slice(this.length)) + } else if ( props.length < this.length && remove ) { + this.splice(props.length) + } + //remove those props didn't get too + if ( collectingStarted ) { + sendCollection() + } + }, + sort: function(method, silent){ + var comparator = this.comparator, + args = comparator ? [function(a, b){ + a = a[comparator] + b = b[comparator] + return a === b ? 0 : (a < b ? -1 : 1); + }] : [], + res = [].sort.apply(this, args); + + !silent && trigger(this, "reset"); + + } + }), + + + // create push, pop, shift, and unshift + // converts to an array of arguments + getArgs = function( args ) { + if ( args[0] && ($.isArray(args[0])) ) { + return args[0] + } + else { + return makeArray(args) + } + }; + // describes the method and where items should be added + each({ + /** + * @function push + * Add items to the end of the list. + * + * var l = new $.Observe.List([]); + * + * l.bind('change', function( + * ev, // the change event + * attr, // the attr that was changed, for multiple items, "*" is used + * how, // "add" + * newVals, // an array of new values pushed + * oldVals, // undefined + * where // the location where these items where added + * ) { + * + * }) + * + * l.push('0','1','2'); + * + * @return {Number} the number of items in the array + */ + push: "length", + /** + * @function unshift + * Add items to the start of the list. This is very similar to + * [jQuery.Observe.prototype.push]. + */ + unshift: 0 + }, + // adds a method where + // - name - method name + // - where - where items in the array should be added + + + function( name, where ) { + list.prototype[name] = function() { + // get the items being added + var args = getArgs(arguments), + // where we are going to add items + len = where ? this.length : 0; + + // go through and convert anything to an observe that needs to be converted + for ( var i = 0; i < args.length; i++ ) { + var val = args[i]; + if ( isObject(val) ) { + args[i] = hookup(val, "*", this) + } + } + + // if we have a sort item, add that + if( args.length == 1 && this.comparator ) { + // add each item ... + // we could make this check if we are already adding in order + // but that would be confusing ... + var index = this.sortedIndex(args[0]); + this.splice(index, 0, args[0]); + return this.length; + } + + // call the original method + var res = [][name].apply(this, args) + + // cause the change where the args are: + // len - where the additions happened + // add - items added + // args - the items added + // undefined - the old value + if ( this.comparator && args.length > 1) { + this.sort(null, true); + trigger(this,"reset", [args]) + } else { + trigger(this, "change", [""+len, "add", args, undefined]) + } + + + return res; + } + }); + + each({ + /** + * @function pop + * + * Removes an item from the end of the list. + * + * var l = new $.Observe.List([0,1,2]); + * + * l.bind('change', function( + * ev, // the change event + * attr, // the attr that was changed, for multiple items, "*" is used + * how, // "remove" + * newVals, // undefined + * oldVals, // 2 + * where // the location where these items where added + * ) { + * + * }) + * + * l.pop(); + * + * @return {Object} the element at the end of the list + */ + pop: "length", + /** + * @function shift + * Removes an item from the start of the list. This is very similar to + * [jQuery.Observe.prototype.pop]. + * + * @return {Object} the element at the start of the list + */ + shift: 0 + }, + // creates a 'remove' type method + + + function( name, where ) { + list.prototype[name] = function() { + + var args = getArgs(arguments), + len = where && this.length ? this.length - 1 : 0; + + + var res = [][name].apply(this, args) + + // create a change where the args are + // "*" - change on potentially multiple properties + // "remove" - items removed + // undefined - the new values (there are none) + // res - the old, removed values (should these be unbound) + // len - where these items were removed + trigger(this, "change", [""+len, "remove", undefined, [res]]) + + if ( res && res.unbind ) { + res.unbind("change" + this._namespace) + } + return res; + } + }); + + list.prototype. + /** + * @function indexOf + * Returns the position of the item in the array. Returns -1 if the + * item is not in the array. + * @param {Object} item + * @return {Number} + */ + indexOf = [].indexOf || function(item){ + return $.inArray(item, this) + } + + /** + * @class $.O + */ + $.O = function(data, options){ + if(isArray(data) || data instanceof $.Observe.List){ + return new $.Observe.List(data, options) + } else { + return new $.Observe(data, options) + } + } +}); diff --git a/lang/observe/observe.md b/lang/observe/observe.md new file mode 100644 index 00000000..12d9302d --- /dev/null +++ b/lang/observe/observe.md @@ -0,0 +1,86 @@ +## 3.1 Backlog - Deferreds + +jQuery 1.6 brought Deferred support. They are a great feature +that promise to make a lot of asynchronous functionality +easier to write and manage. But, many people struggle +with uses other than 'waiting for a bunch of Ajax requests to complete'. For 3.1, we +identified an extremely common, but annoying, practice that becomes +a one-liner with deferreds: loading data and a template and rendering the +result into an element. + +## Templates Consume Deferreds + +Here's what that looks like in 3.1: + + $('#todos').html('temps/todos.ejs', $.get('/todos',{},'json') ); + +This will make two parallel ajax requests. One request +is made for the template at `temps/todos.ejs` which might look like: + +
                                  <% for(var i =0; i< this.length; i++) { %>
                                  +  <li><%= this[i].name %></li>
                                  +<% } %>
                                  +
                                  + +The second request loads `/todos` which might look like: + + [ + {"id" : 1, "name": "Take out the Trash"}, + {"id" : 2, "name": "Do the Laundry"} + ] + +When both have been loaded, the template is rendered with the todos data and +the result set as the `#todos` element's innerHTML. + +This is fab fast! The AJAX and template request are made in parallel and rendered +when both are complete. I am too lazy to write +out what this would look like before deferreds. Actually, I'm not too lazy: + + var template, + data, + done = function(){ + if( template && data ) { + var html = new EJS({text: template}) + .render(data); + $('#todos').html( html ) + } + } + $.get('temps/todos.ejs', function(text){ + template = text; + done(); + },'text') + $.get('/todos',{}, function(json){ + data = json + done(); + },'json') + +## Models Return Deferreds + +Model AJAX functions now return deferreds. Creating models like: + + $.Model('User',{ + findAll: '/users' + },{}); + + $.Model('Todo',{ + findAll: '/todos' + },{}) + +Lets you request todos and users and get back a deferred that can be +used in functions that accept deferreds like $.when: + + $.when( User.findAll(), + Todo.findAll() ) + +Or $.View: + + $('#content').html('temps/content.ejs',{ + users : User.findAll(), + todos: Todo.findAll() + }) + +## Conclusion + +Despite using templates, this is +still 'waiting for a bunch of Ajax requests to complete'. Does +anyone have other good uses for deferreds? diff --git a/lang/observe/observe_test.js b/lang/observe/observe_test.js new file mode 100644 index 00000000..86c2ffe4 --- /dev/null +++ b/lang/observe/observe_test.js @@ -0,0 +1,312 @@ +steal('funcunit/qunit','jquery/lang/observe/delegate',function(){ + +module('jquery/lang/observe') + +test("Basic Observe",9,function(){ + + var state = new $.O({ + category : 5, + productType : 4, + properties : { + brand: [], + model : [], + price : [] + } + }); + + var added; + + state.bind("change", function(ev, attr, how, val, old){ + equals(attr, "properties.brand.0", "correct change name") + equals(how, "add") + equals(val[0].attr("foo"),"bar", "correct") + + added = val[0]; + }); + + + + state.attr("properties.brand").push({foo: "bar"}); + state.unbind("change"); + + added.bind("change", function(ev, attr, how, val, old){ + equals(attr, "foo") + equals(how, "set") + equals(val, "zoo") + }) + state.bind("change", function(ev, attr, how, val, old){ + equals(attr, "properties.brand.0.foo") + equals(how, "set") + equals(val,"zoo") + }); + added.attr("foo", "zoo"); + +}); + +test("list splice", function(){ + var l = new $.Observe.List([0,1,2,3]), + first = true; + + l.bind('change', function( ev, attr, how, newVals, oldVals ) { + equals (attr, "1") + // where comes from the attr ... + //equals(where, 1) + if(first){ + equals( how, "remove", "removing items" ) + equals( newVals, undefined, "no new Vals" ) + } else { + same( newVals, ["a","b"] , "got the right newVals") + equals( how, "add", "adding items" ) + } + + first = false; + }) + + l.splice(1,2, "a", "b"); + same(l.serialize(), [0,"a","b", 3], "serialized") +}); + +test("list pop", function(){ + var l = new $.Observe.List([0,1,2,3]); + + l.bind('change', function( ev, attr, how, newVals, oldVals ) { + equals (attr, "3") + + equals( how, "remove" ) + equals( newVals, undefined ) + same( oldVals, [3] ) + }) + + l.pop(); + same(l.serialize(), [0,1,2]) +}) + +test("changing an object unbinds", function(){ + var state = new $.Observe({ + category : 5, + productType : 4, + properties : { + brand: [], + model : [], + price : [] + } + }), + count = 0; + + var brand = state.attr("properties.brand"); + + state.bind("change", function(ev, attr, how, val, old){ + equals(attr,"properties.brand"); + + equals(count, 0, "count called once"); + count++; + equals(how, "set") + equals(val[0], "hi") + }); + if (typeof console != "undefined") console.log("before") + state.attr("properties.brand",["hi"]); + if (typeof console != "undefined") console.log("after") + + brand.push(1,2,3); + +}); + +test("replacing with an object that object becomes observable",function(){ + var state = new $.Observe({ + properties : { + brand: [], + model : [], + price : [] + } + }); + + ok(state.attr("properties").bind, "has bind function"); + + state.attr("properties",{}); + + ok(state.attr("properties").bind, "has bind function"); +}); + +test("remove attr", function(){ + var state = new $.Observe({ + properties : { + brand: [], + model : [], + price : [] + } + }); + + state.bind("change", function(ev, attr, how, newVal, old){ + equals(attr, "properties"); + equals(how, "remove") + same(old.serialize() ,{ + brand: [], + model : [], + price : [] + } ); + }) + + state.removeAttr("properties"); + equals(undefined, state.attr("properties") ); +}); + +test("attrs", function(){ + var state = new $.Observe({ + properties : { + foo: "bar", + brand: [] + } + }); + + state.bind("change", function(ev, attr, how, newVal){ + equals(attr, "properties.foo") + equals(newVal, "bad") + }) + + state.attrs({ + properties : { + foo: "bar", + brand: [] + } + }) + + state.attrs({ + properties : { + foo: "bad", + brand: [] + } + }); + + state.unbind("change"); + + state.bind("change", function(ev, attr, how, newVal){ + equals(attr, "properties.brand.0") + equals(how,"add") + same(newVal, ["bad"]) + }); + + state.attrs({ + properties : { + foo: "bad", + brand: ["bad"] + } + }); + +}); + +test("empty get", function(){ + var state = new $.Observe({}); + + equals(state.attr('foo.bar'), undefined) +}); + +test("attrs deep array ", function(){ + var state = new $.Observe({}); + var arr = [{ + foo: "bar" + }], + thing = { + arr: arr + }; + + state.attrs({ + thing: thing + }, true); + + ok(thing.arr === arr, "thing unmolested"); +}); + +test('attrs semi-serialize', function(){ + var first = { + foo : {bar: 'car'}, + arr: [1,2,3, {four: '5'} + ] + }, + compare = $.extend(true, {}, first); + var res = new $.Observe(first).attrs(); + same(res,compare, "test") +}) + +test("attrs sends events after it is done", function(){ + var state = new $.Observe({foo: 1, bar: 2}) + state.bind('change', function(){ + equals(state.attr('foo'), -1, "foo set"); + equals(state.attr('bar'), -2, "bar set") + }) + state.attrs({foo: -1, bar: -2}); +}) + +test("direct property access", function(){ + var state = new $.Observe({foo: 1, attrs: 2}); + equals(state.foo,1); + equals(typeof state.attrs, 'function') +}) + +test("pop unbinds", function(){ + var l = new $.O([{foo: 'bar'}]); + var o = l.attr(0), + count = 0; + l.bind('change', function(ev, attr, how, newVal, oldVal){ + count++; + if(count == 1){ + // the prop change + equals(attr, '0.foo', "count is set"); + } else if(count === 2 ){ + equals(how, "remove"); + equals(attr, "0") + } else { + ok(false, "called too many times") + } + + }) + + equals( o.attr('foo') , 'bar'); + + o.attr('foo','car') + l.pop(); + o.attr('foo','bad') +}) + +test("splice unbinds", function(){ + var l = new $.Observe.List([{foo: 'bar'}]); + var o = l.attr(0), + count = 0; + l.bind('change', function(ev, attr, how, newVal, oldVal){ + count++; + if(count == 1){ + // the prop change + equals(attr, '0.foo', "count is set"); + } else if(count === 2 ){ + equals(how, "remove"); + equals(attr, "0") + } else { + ok(false, "called too many times") + } + + }) + + equals( o.attr('foo') , 'bar'); + + o.attr('foo','car') + l.splice(0,1); + o.attr('foo','bad') +}); + + +test("always gets right attr even after moving array items", function(){ + var l = new $.Observe.List([{foo: 'bar'}]); + var o = l.attr(0); + l.unshift("A new Value") + + + l.bind('change', function(ev, attr, how){ + equals(attr, "1.foo") + }) + + + o.attr('foo','led you') +}) + + + +}).then('./delegate/delegate_test.js'); diff --git a/lang/observe/qunit.html b/lang/observe/qunit.html new file mode 100644 index 00000000..dfd2ef53 --- /dev/null +++ b/lang/observe/qunit.html @@ -0,0 +1,17 @@ + + + + + + +

                                  Observe Test Suite

                                  +

                                  +
                                  +

                                  +
                                    +
                                    + associations + list + + + \ No newline at end of file diff --git a/lang/openajax/openajax.html b/lang/openajax/openajax.html index 2f5c2b78..b2c08e0c 100644 --- a/lang/openajax/openajax.html +++ b/lang/openajax/openajax.html @@ -13,12 +13,12 @@ \ No newline at end of file diff --git a/lang/openajax/openajax.js b/lang/openajax/openajax.js index 72370351..cc48f88f 100644 --- a/lang/openajax/openajax.js +++ b/lang/openajax/openajax.js @@ -1,4 +1,4 @@ -//@steal-clean +//!steal-clean /******************************************************************************* * OpenAjax.js * @@ -7,7 +7,7 @@ * * http://www.openajax.org/member/wiki/OpenAjax_Hub_Specification * - * Copyright 2006-2008 OpenAjax Alliance + * Copyright 2006-2009 OpenAjax Alliance * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy @@ -22,10 +22,10 @@ steal.then(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(){ + * @class OpenAjax + * Use OpenAjax.hub to publish and subscribe to messages. + */ + OpenAjax = new function(){ var t = true; var f = false; var g = window; @@ -34,8 +34,8 @@ if(!window["OpenAjax"]){ var h = {}; this.hub = h; h.implementer = "http://openajax.org"; - h.implVersion = "1.0"; - h.specVersion = "1.0"; + h.implVersion = "2.0"; + h.specVersion = "2.0"; h.implExtraData = {}; var libs = {}; h.libraries = libs; @@ -109,14 +109,14 @@ if(!window["OpenAjax"]){ } } - h._publish = function(tree, path, index, name, msg, pcb, pcid) { + 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, pcb, pcid); - this._publish(tree.c["*"], path, index + 1, name, msg, pcb, pcid); + 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") { @@ -128,8 +128,6 @@ if(!window["OpenAjax"]){ var cb = callbacks[i].cb; var fcb = callbacks[i].fcb; var d = callbacks[i].data; - var sid = callbacks[i].sid; - var scid = callbacks[i].cid; if(typeof cb == "string"){ // get a function object cb = sc[cb]; @@ -139,9 +137,7 @@ if(!window["OpenAjax"]){ fcb = sc[fcb]; } if((!fcb) || (fcb.call(sc, name, msg, d))) { - if((!pcb) || (pcb(name, msg, pcid, scid))) { - cb.call(sc, name, msg, d, sid); - } + cb.call(sc, name, msg, d, pid); } } } @@ -198,5 +194,5 @@ if(!window["OpenAjax"]){ OpenAjax.hub.registerLibrary("OpenAjax", "http://openajax.org/hub", "1.0", {}); } -OpenAjax.hub.registerLibrary("JavaScriptMVC", "http://JavaScriptMVC.com", "1.5", {}); -}); \ No newline at end of file +OpenAjax.hub.registerLibrary("JavaScriptMVC", "http://JavaScriptMVC.com", "3.1", {}); +}); diff --git a/lang/qunit.html b/lang/qunit.html new file mode 100644 index 00000000..0cec0f90 --- /dev/null +++ b/lang/qunit.html @@ -0,0 +1,17 @@ + + + + + + +

                                    Lang Test Suite

                                    +

                                    +
                                    +

                                    +
                                      +
                                      + associations + list + + + \ No newline at end of file diff --git a/lang/string/deparam/deparam.js b/lang/string/deparam/deparam.js new file mode 100644 index 00000000..0f9dd0bd --- /dev/null +++ b/lang/string/deparam/deparam.js @@ -0,0 +1,71 @@ +steal('jquery', function($){ + + var digitTest = /^\d+$/, + keyBreaker = /([^\[\]]+)|(\[\])/g, + plus = /\+/g, + paramTest = /([^?#]*)(#.*)?$/; + + /** + * @add jQuery.String + */ + $.String = $.extend($.String || {}, { + + /** + * @function deparam + * + * Takes a string of name value pairs and returns a Object literal that represents those params. + * + * @param {String} params a string like "foo=bar&person[age]=3" + * @return {Object} A JavaScript Object that represents the params: + * + * { + * foo: "bar", + * person: { + * age: "3" + * } + * } + */ + deparam: function(params){ + + if(! params || ! paramTest.test(params) ) { + return {}; + } + + + var data = {}, + pairs = params.split('&'), + current; + + for(var i=0; i < pairs.length; i++){ + current = data; + var pair = pairs[i].split('='); + + // if we find foo=1+1=2 + if(pair.length != 2) { + pair = [pair[0], pair.slice(1).join("=")] + } + + var key = decodeURIComponent(pair[0].replace(plus, " ")), + value = decodeURIComponent(pair[1].replace(plus, " ")), + parts = key.match(keyBreaker); + + for ( var j = 0; j < parts.length - 1; j++ ) { + var part = parts[j]; + if (!current[part] ) { + // if what we are pointing to looks like an array + current[part] = digitTest.test(parts[j+1]) || parts[j+1] == "[]" ? [] : {} + } + current = current[part]; + } + lastPart = parts[parts.length - 1]; + if(lastPart == "[]"){ + current.push(value) + }else{ + current[lastPart] = value; + } + } + return data; + } + }); + +}) diff --git a/lang/string/deparam/deparam_test.js b/lang/string/deparam/deparam_test.js new file mode 100644 index 00000000..5a40a261 --- /dev/null +++ b/lang/string/deparam/deparam_test.js @@ -0,0 +1,50 @@ +steal('funcunit/qunit','./deparam').then(function(){ + +module('jquery/lang/deparam') + +test("Basic deparam",function(){ + + var data = $.String.deparam("a=b"); + equals(data.a,"b") + + var data = $.String.deparam("a=b&c=d"); + equals(data.a,"b") + equals(data.c,"d") +}) +test("Nested deparam",function(){ + + var data = $.String.deparam("a[b]=1&a[c]=2"); + equals(data.a.b,1) + equals(data.a.c,2) + + var data = $.String.deparam("a[]=1&a[]=2"); + equals(data.a[0],1) + equals(data.a[1],2) + + var data = $.String.deparam("a[b][]=1&a[b][]=2"); + equals(data.a.b[0],1) + equals(data.a.b[1],2) + + var data = $.String.deparam("a[0]=1&a[1]=2"); + equals(data.a[0],1) + equals(data.a[1],2) +}); + + +test("deparam an array", function(){ + var data = $.String.deparam("a[0]=1&a[1]=2"); + + ok($.isArray(data.a), "is array") + + equals(data.a[0],1) + equals(data.a[1],2) +}) + +test("deparam object with spaces", function(){ + var data = $.String.deparam("a+b=c+d&+e+f+=+j+h+"); + + equals(data["a b"], "c d") + equals(data[" e f "], " j h ") +}) + +}) diff --git a/lang/string/deparam/qunit.html b/lang/string/deparam/qunit.html new file mode 100644 index 00000000..c44742f4 --- /dev/null +++ b/lang/string/deparam/qunit.html @@ -0,0 +1,17 @@ + + + + + + +

                                      Deparam Test Suite

                                      +

                                      +
                                      +

                                      +
                                        +
                                        + associations + list + + + \ No newline at end of file diff --git a/lang/string/qunit.html b/lang/string/qunit.html new file mode 100644 index 00000000..4388f99b --- /dev/null +++ b/lang/string/qunit.html @@ -0,0 +1,17 @@ + + + + string QUnit Test + + + + +

                                        string Test Suite

                                        +

                                        +
                                        +

                                        +
                                        +
                                          +
                                          + + \ No newline at end of file diff --git a/lang/rsplit/rsplit.js b/lang/string/rsplit/rsplit.js similarity index 68% rename from lang/rsplit/rsplit.js rename to lang/string/rsplit/rsplit.js index f9bb5642..8b60bc7c 100644 --- a/lang/rsplit/rsplit.js +++ b/lang/string/rsplit/rsplit.js @@ -1,12 +1,16 @@ -steal.plugins('jquery/lang').then(function( $ ) { +steal('jquery/lang/string',function( $ ) { /** - * @add jQuery.String.static + * @add jQuery.String */ $.String. /** * Splits a string with a regex correctly cross browser - * @param {Object} string - * @param {Object} regex + * + * $.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), diff --git a/lang/string/string.html b/lang/string/string.html new file mode 100644 index 00000000..c84d332a --- /dev/null +++ b/lang/string/string.html @@ -0,0 +1,25 @@ + + + + string + + + +

                                          string Demo

                                          +

                                          This is a dummy page to show off your plugin

                                          + + + + \ No newline at end of file diff --git a/lang/string/string.js b/lang/string/string.js new file mode 100644 index 00000000..e29fb807 --- /dev/null +++ b/lang/string/string.js @@ -0,0 +1,236 @@ +/** + * @page jquerymx.lang Language Helpers + * @parent jquerymx + * @description A collection of language helpers for things like String, Objects, etc. + * + * JavaScriptMVC has several lightweight language helper plugins. + * + * ## [jQuery.Object Object] + * + * Methods useful for comparing Objects. For example, if two + * objects are the same: + * + * $.Object.same({foo: "bar"}, {foo: "bar"}); + * + * ## [jQuery.Observe Observe] + * + * Makes an Object's properties observable: + * + * var person = new $.Observe({ name: "Justin" }) + * person.bind('change', function(){ ... }) + * person.attr('name', "Brian"); + * + * ## [jQuery.String String] + * + * String helpers capitalize, underscore, and perform similar manipulations + * on strings. They can also lookup a value in an object: + * + * $.String.getObject("foo.bar",{foo: {bar: "car"}}) + * + * ## [jQuery.toJSON toJSON] + * + * Used to create or consume JSON strings. + * + * ## [jQuery.Vector Vector] + * + * Used for vector math. + */ +//string helpers +steal('jquery').then(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: + *
                                            + *
                                          • [jQuery.String.capitalize|capitalize]: Capitalizes a string (some_string » Some_string)
                                          • + *
                                          • [jQuery.String.camelize|camelize]: Capitalizes a string from something undercored + * (some_string » someString, some-string » someString)
                                          • + *
                                          • [jQuery.String.classize|classize]: Like [jQuery.String.camelize|camelize], + * but the first part is also capitalized (some_string » SomeString)
                                          • + *
                                          • [jQuery.String.niceName|niceName]: Like [jQuery.String.classize|classize], but a space separates each 'word' (some_string » Some String)
                                          • + *
                                          • [jQuery.String.underscore|underscore]: Underscores a string (SomeString » some_string)
                                          • + *
                                          • [jQuery.String.sub|sub]: Returns a string with {param} replaced values from data. + *
                                            +		 *       $.String.sub("foo {bar}",{bar: "far"})
                                            +		 *       //-> "foo far"
                                            + *
                                          • + *
                                          + * + */ + 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 + }); +}); \ No newline at end of file diff --git a/lang/string/string_test.js b/lang/string/string_test.js new file mode 100644 index 00000000..9d345984 --- /dev/null +++ b/lang/string/string_test.js @@ -0,0 +1,46 @@ +steal('funcunit/qunit','./string').then(function(){ + +module("jquery/lang/string") + +test("$.String.sub", function(){ + equals($.String.sub("a{b}",{b: "c"}),"ac") + + var foo = {b: "c"}; + + equals($.String.sub("a{b}",foo,true),"ac"); + + ok(!foo.b, "removed this b"); + + +}); + +test("$.String.sub double", function(){ + equals($.String.sub("{b} {d}",[{b: "c", d: "e"}]),"c e"); +}) + +test("String.underscore", function(){ + equals($.String.underscore("Foo.Bar.ZarDar"),"foo.bar.zar_dar") +}) + + +test("$.String.getObject", function(){ + var obj = $.String.getObject("foo", [{a: 1}, {foo: 'bar'}]); + + equals(obj,'bar', 'got bar') + + + // test null data + + var obj = $.String.getObject("foo", [{a: 1}, {foo: 0}]); + + equals(obj,0, 'got 0 (falsey stuff)') +}); + +test("$.String.niceName", function(){ + var str = "some_underscored_string"; + var niceStr = $.String.niceName(str); + equals(niceStr, 'Some Underscored String', 'got correct niceName'); +}) + + +}).then('./deparam/deparam_test'); diff --git a/lang/vector/vector.js b/lang/vector/vector.js index 745b1ad3..a1cc1501 100644 --- a/lang/vector/vector.js +++ b/lang/vector/vector.js @@ -1,8 +1,9 @@ -steal.then(function(){ +steal('jquery').then(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 + * @class jQuery.Vector + * @parent jquerymx.lang * A vector class * @constructor creates a new vector instance from the arguments. Example: * @codestart @@ -10,158 +11,153 @@ steal.then(function(){ * @codeend * */ -jQuery.Vector = function(){ - this.update( jQuery.makeArray(arguments) ); -}; -jQuery.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 newArr = []; - - for(var i=0; i < this.array.length; i++) - newArr.push( f( this.array[i] ) ); - var vec = new jQuery.Vector(); - return vec.update(newArr); - }, - /** - * Adds two vectors together. Example: - * @codestart - * new Vector(1,2).plus(2,3) //-> <3,5> - * new Vector(3,5).plus(new Vector(4,5)) //-> <7,10> - * @codeend - * @return {jQuery.Vector} - */ - plus: function() { - var args = arguments[0] instanceof jQuery.Vector ? - arguments[0].array : - jQuery.makeArray(arguments), - arr=this.array.slice(0), - vec = new jQuery.Vector(); - for(var 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 args = arguments[0] instanceof jQuery.Vector ? - arguments[0].array : - jQuery.makeArray(arguments), - arr=this.array.slice(0), vec = new jQuery.Vector(); - for(var 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 args = arguments[0] instanceof jQuery.Vector ? - arguments[0].array : - jQuery.makeArray(arguments), - arr=this.array.slice(0), vec = new jQuery.Vector(); - for(var i=0; i < args.length; i++) - if(arr[i] != args[i]) return null; - return vec.update(arr); - }, - /* - * Returns the 2nd value of the vector - * @return {Number} - */ - x : getSetZero, - width : getSetZero, - /** - * Returns the first value of the vector - * @return {Number} - */ - y : getSetOne, - height : getSetOne, - /** - * Same as x() - * @return {Number} - */ - top : getSetOne, - /** - * same as y() - * @return {Number} - */ - left : getSetZero, - /** - * returns (x,y) - * @return {String} - */ - toString: function() { - return "("+this.array[0]+","+this.array[1]+")"; - }, - /** - * Replaces the vectors contents - * @param {Object} array - */ - update: function( array ) { - if(this.array){ - for(var i =0; i < this.array.length; i++) delete this.array[i]; - } - this.array = array; - for(var i =0; i < array.length; i++) this[i]= this.array[i]; - return this; - } -}; - -jQuery.Event.prototype.vector = function(){ - if(this.originalEvent.synthetic){ - var doc = document.documentElement, body = document.body; - return new jQuery.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 jQuery.Vector(this.pageX, this.pageY); - } -} + $.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 = []; -jQuery.fn.offsetv = function() { - if(this[0] == window){ - return new jQuery.Vector(window.pageXOffset ? window.pageXOffset : document.documentElement.scrollLeft, - window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop) - }else{ - var offset = this.offset(); - return new jQuery.Vector(offset.left, offset.top); - } -}; - -jQuery.fn.dimensionsv = function(){ - if(this[0] == window) - return new jQuery.Vector(this.width(), this.height()); - else - return new jQuery.Vector(this.outerWidth(), this.outerHeight()); -} -jQuery.fn.centerv = function(){ - return this.offsetv().plus( this.dimensionsv().app(function(u){return u /2;}) ) -} + 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) //-> <3,5> + * new Vector(3,5).plus(new Vector(4,5)) //-> <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; + } + }; -jQuery.fn.makePositioned = function() { - return this.each(function(){ - var that = jQuery(this); - var pos = that.css('position'); + $.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); + } + }; - if (!pos || pos == 'static') { - var style = { position: 'relative' }; + $.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); + } + }; - if (window.opera) { - style.top = '0px'; - style.left = '0px'; - } - that.css(style); + $.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"]()); } - }); -}; - -}) + }; +}); diff --git a/model/associations/associations.html b/model/associations/associations.html deleted file mode 100644 index 71e72c85..00000000 --- a/model/associations/associations.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - Model Events Demo - - - -
                                          -

                                          Model Associations Demo

                                          -

                                          This demo shows how you can setup associations.

                                          -
                                          -
                                          -
                                          -
                                          - - - - - - - \ No newline at end of file diff --git a/model/associations/associations.js b/model/associations/associations.js deleted file mode 100644 index a4469195..00000000 --- a/model/associations/associations.js +++ /dev/null @@ -1,191 +0,0 @@ -steal.plugins('jquery/model').then(function($){ -/** -@page jquery.model.associations Associations -@parent jQuery.Model -@download jquery/dist/jquery.model.associations.js -@test jquery/model/associations/qunit.html -@plugin jquery/model/associations - -For efficiency, you often want to get data for related -records at the same time. The jquery.model.assocations.js -plugin lets you do this. - -Lets say we wanted to list tasks for contacts. When we request our contacts, -the JSON data will come back like: - -@codestart -[ - {'id': 1, - 'name' : 'Justin Meyer', - 'birthday': '1982-10-20', - 'tasks' : [ - {'id': 1, - 'title': "write up model layer", - 'due': "2010-10-5" }, - {'id': 1, - 'title': "document models", - 'due': "2010-10-8"}]}, - ... -] -@codeend - -We want to be able to do something like: - -@codestart -var tasks = contact.attr("tasks"); - -tasks[0].due //-> date -@codeend - -Basically, we want attr("tasks") to -return a list of task instances. - -Associations let you do this. Here's how: - -First, create a Task model: - -@codestart -$.Model.extend("Task",{ - convert : { - date : function(date){ ... } - }, - attributes : { - due : 'date' - } -},{ - weeksPastDue : function(){ - return Math.round( (new Date() - this.due) / - (1000*60*60*24*7 ) ); - } -}) -@codeend - -Then create a Contact model that 'hasMany' tasks: - -@codestart -$.Model.extend("Contact",{ - associations : { - hasMany : "Task" - }, - ... -},{ - ... -}); -@codeend - -Here's a demo of this in action: - -@demo jquery/model/associations/associations.html - -You can customize associations with -the [jQuery.Model.static.belongsTo belongsTo] -and [jQuery.Model.static.belongsTo hasMany] methods. - */ - - - //overwrite model's setup to provide associations - - var oldSetup = $.Model.setup, - associate = function(hasMany, Class, type){ - hasMany = hasMany || []; - hasMany = typeof hasMany == 'string' ? [hasMany] : hasMany; - for(var i=0; i < hasMany.length;i++){ - Class[type].call(Class, hasMany[i]) - } - }; - // this provides associations on the has many - $.Model.setup = function(){ - oldSetup.apply(this, arguments); - associate( this.associations.hasMany, this, "hasMany"); - associate(this.associations.belongsTo, this, "belongsTo"); - delete this.associations.hasMany; - delete this.associations.belongsTo; - } - - - $.Model. - /** - * @function jQuery.Model.static.belongsTo - * @parent jquery.model.associations - * @plugin jquery/model/associations - * Use to convert values on attribute name to - * instances of model type. - * @codestart - * $.Model.extend("Task",{ - * init : function(){ - * this.belongsTo("Person","assignedTo"); - * } - * },{}) - * @codeend - * - * @param {String} type The string name of the model. - * @param {String} [name] The name of the property. Defaults to the shortName of the model. - */ - belongsTo = function(type, name){ - name = name || $.String.camelize( type.match(/\w+$/)[0] ); - var cap = $.String.capitalize(name), - set = function(v){ - return ( this[name] = (v == v.Class ? v : $.Class.getObject(type).wrap(v)) ) - }, - get = function(){ - return this[name]; - } - - set.doNotInhert = true; - get.doNotInherit = true; - - if(!this.prototype["set"+cap]){ - this.prototype["set"+cap] = set; - } - if(!this.prototype["get"+cap]){ - this.prototype["get"+cap] = get - } - this.associations[name] = { - belongsTo: type - }; - return this; - } - $.Model. - /** - * @function jQuery.Model.static.hasMany - * @parent jquery.model.associations - * @plugin jquery/model/associations - * Converts values on attribute name to - * instances of model type. - * @codestart - * $.Model.extend("Task",{ - * init : function(){ - * this.hasMany("Person","people"); - * } - * },{}) - * @codeend - * - * @param {String} type The string name of the model. - * @param {String} [name] The name of the property. - * Defaults to the shortName of the model with an "s" at the end. - */ - hasMany = function(type, name){ - name = name || $.String.camelize( type.match(/\w+$/)[0] )+"s"; - - var cap = $.String.capitalize(name) - if(!this.prototype["set"+cap]){ - this.prototype["set"+cap] = function(v){ - // should probably check instanceof - return this[name] = (v == v.Class ? v : $.Class.getObject(type).wrapMany(v)) - } - } - if(!this.prototype["get"+cap]){ - this.prototype["get"+cap] = function(){ - return this[name] || $.Class.getObject(type).wrapMany([]); - } - } - this.associations[name] = { - hasMany: type - }; - return this; - } - - - -}) - diff --git a/model/associations/test/qunit/associations_test.js b/model/associations/test/qunit/associations_test.js deleted file mode 100644 index d787e87e..00000000 --- a/model/associations/test/qunit/associations_test.js +++ /dev/null @@ -1,51 +0,0 @@ -module("jquery/model/associations",{ - setup: function() { - - $.Model.extend("MyTest.Person"); - $.Model.extend("MyTest.Loan"); - $.Model.extend("MyTest.Issues"); - - $.Model.extend("MyTest.Customer", - { - init: function() { - this.belongsTo("MyTest.Person") - this.hasMany("MyTest.Loan") - this.hasMany("MyTest.Issues") - } - }, - {}); - } -}) - - - - - -test("associations work", function(){ - var c = new MyTest.Customer({ - id: 5, - person : { - id: 1, - name: "Justin" - }, - issues : [], - loans : [ - { - amount : 1000, - id: 2 - }, - { - amount : 19999, - id: 3 - } - ] - }) - equals(c.person.name, "Justin", "association present"); - equals(c.person.Class, MyTest.Person, "belongs to association typed"); - - equals(c.issues.length, 0); - - equals(c.loans.length, 2); - - equals(c.loans[0].Class, MyTest.Loan); -}) \ No newline at end of file diff --git a/model/associations/test/qunit/qunit.js b/model/associations/test/qunit/qunit.js deleted file mode 100644 index 5dba306c..00000000 --- a/model/associations/test/qunit/qunit.js +++ /dev/null @@ -1,9 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/model/associations") //load your app - .plugins('funcunit/qunit') //load qunit - .then("associations_test") - -if(steal.browser.rhino){ - steal.plugins('funcunit/qunit/env') -} \ No newline at end of file diff --git a/model/backup/backup.html b/model/backup/backup.html index 7a6280ff..89e67b03 100644 --- a/model/backup/backup.html +++ b/model/backup/backup.html @@ -27,13 +27,19 @@

                                          Model Backup Demo

                                          src='../../../steal/steal.js'> - - \ No newline at end of file diff --git a/model/backup/backup.js b/model/backup/backup.js index da89f959..e4c8c481 100644 --- a/model/backup/backup.js +++ b/model/backup/backup.js @@ -1,12 +1,60 @@ //allows you to backup and restore a model instance -steal.plugins('jquery/model').then(function(){ - +steal('jquery/model').then(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 jquery.model.backup Backup / Restore +@page jquerymx.model.backup Backup / Restore @parent jQuery.Model @plugin jquery/model/backup @test jquery/model/backup/qunit.html -@download jquery/dist/jquery.model.backup.js +@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. @@ -38,101 +86,45 @@ See this in action: @demo jquery/model/backup/backup.html */ - // a helper to iterate through the associations - var associations = function(instance, func){ - var name, - res; - - for(name in instance.Class.associations){ - association = instance.Class.associations[name]; - if("belongsTo" in association){ - if(instance[name] && (res = func(instance[name]) ) ){ - return res; - } - } - if("hasMany" in association){ - if(instance[name]){ - for(var i =0 ; i < instance[name].length; i++){ - if( (res = func(instance[name][i]) ) ){ - return res; - } - } - } - } - } - } - - $.extend($.Model.prototype,{ /** * @function jQuery.Model.prototype.backup - * @plugin jquery/model/backup - * @parent jquery.model.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() { - associations(this, function(associated){ - associated.backup(); - }) - this._backupStore = $.extend(true, {},this.attrs()); + this._backupStore = this.serialize(); return this; }, - - _backup: function() { - this._backupStore = $.extend(true, {},this.attrs()); - }, + /** * @function jQuery.Model.prototype.isDirty * @plugin jquery/model/backup - * @parent jquery.model.backup + * @parent jquerymx.model.backup * Returns if the instance needs to be saved. This will go * through associations too. - * @param {Boolean} [checkAssociations=false] true if associations should be checked. Defaults to false. - * be checked, false if otherwise * @return {Boolean} true if there are changes, false if otherwise */ isDirty: function(checkAssociations) { - if(!this._backupStore) return false; - //go through attrs and compare ... - var current = this.attrs(), - name, - association, - res; - for(name in current){ - if(current[name] !== this._backupStore[name]){ - return true; - } - - } - if( checkAssociations ){ - res = associations(this, function(associated){ - return associated.isDirty(); - }) - if(res === true){ - return true; - } + // check if it serializes the same + if(!this._backupStore){ + return false; + } else { + return !same(this.serialize(), this._backupStore, !!checkAssociations); } - - return false; }, /** * @function jQuery.Model.prototype.restore - * @plugin jquery/model/backup * @parent jquery.model.backup * restores this instance to its backup data. - * @param {Boolean} [restoreAssociations=false] if true, restores associations. * @return {model} the instance (for chaining) */ restore: function(restoreAssociations) { - this.attrs(this._backupStore); + var props = restoreAssociations ? this._backupStore : flatProps(this._backupStore) + this.attrs(props); - if( restoreAssociations ){ - associations(this, function(associated){ - associated.restore(); - }) - } return this; } diff --git a/model/backup/qunit/qunit.js b/model/backup/qunit/qunit.js index a30ca1a4..4869d98d 100644 --- a/model/backup/qunit/qunit.js +++ b/model/backup/qunit/qunit.js @@ -1,5 +1,5 @@ -steal.plugins('funcunit/qunit') - .plugins("jquery/model/backup","jquery/model/associations").then(function(){ +steal('funcunit/qunit') + .then("jquery/model/backup").then(function(){ module("jquery/model/backup",{ @@ -28,11 +28,18 @@ test("backing up", function(){ }); test("backup / restore with associations", function(){ - $.Model.extend("Instruction") - Recipe.hasMany("Instruction") + $.Model("Instruction"); + $.Model("Cookbook"); - $.Model.extend("Cookbook") - Recipe.belongsTo("Cookbook") + $.Model("Recipe",{ + attributes : { + instructions : "Instruction.models", + cookbook: "Cookbook.model" + } + },{}); + + + var recipe = new Recipe({ name: "cheese burger", diff --git a/model/demo-convert.html b/model/demo-convert.html index f9ad7428..73dcf41e 100644 --- a/model/demo-convert.html +++ b/model/demo-convert.html @@ -1,20 +1,20 @@ - - Model Convert Demo + + Model Convert Demo - - + +
                                          -

                                          Model Convert Demo

                                          -

                                          This demo shows converting date strings sent by the - server to JavaScript dates with attributes and convert.

                                          +

                                          Model Convert Demo

                                          +

                                          This demo shows converting date strings sent by the + server to JavaScript dates with attributes and convert.

                                            @@ -22,58 +22,56 @@

                                            Model Convert Demo

                                            - - + \ No newline at end of file diff --git a/model/demo-dom.html b/model/demo-dom.html index 23e94506..f0958e6d 100644 --- a/model/demo-dom.html +++ b/model/demo-dom.html @@ -21,44 +21,29 @@

                                            Model DOM Helpers Demo

                                            - - - \ No newline at end of file diff --git a/model/demo-encapsulate.html b/model/demo-encapsulate.html index d6a4ffa1..3bd632c4 100644 --- a/model/demo-encapsulate.html +++ b/model/demo-encapsulate.html @@ -25,20 +25,13 @@

                                            Work Item Grid

                                            - + - \ No newline at end of file diff --git a/model/demo-events.html b/model/demo-events.html index 2fc7bf53..9c12d940 100644 --- a/model/demo-events.html +++ b/model/demo-events.html @@ -28,31 +28,47 @@

                                            Direct Binding

                                            -

                                            Subscribing

                                            +

                                            Model Binding

                                            - The following list subscribes - to "contact.updated" events. + The following list binds + to "updated" events on Contact.

                                            - - - diff --git a/model/demo-setter.html b/model/demo-setter.html index 18f8f3dc..6d0d58e8 100644 --- a/model/demo-setter.html +++ b/model/demo-setter.html @@ -22,26 +22,25 @@

                                            Model Setter Demo

                                            - + \ No newline at end of file diff --git a/model/fixtures/school.json b/model/fixtures/school.json new file mode 100644 index 00000000..734e4916 --- /dev/null +++ b/model/fixtures/school.json @@ -0,0 +1,4 @@ +{ + "id": 4, + "name" : "Highland" +} \ No newline at end of file diff --git a/model/fixtures/schools.json b/model/fixtures/schools.json new file mode 100644 index 00000000..1b596fa0 --- /dev/null +++ b/model/fixtures/schools.json @@ -0,0 +1,4 @@ +[{ + "id": 1, + "name" : "adler" +}] diff --git a/model/list/cookie/cookie.html b/model/list/cookie/cookie.html index 69024d3a..f94eae02 100644 --- a/model/list/cookie/cookie.html +++ b/model/list/cookie/cookie.html @@ -34,13 +34,11 @@

                                            List of Contacts

                                            src='../../../../steal/steal.js'> - diff --git a/model/list/cookie/cookie.js b/model/list/cookie/cookie.js index 8e276a0b..e28b98e1 100644 --- a/model/list/cookie/cookie.js +++ b/model/list/cookie/cookie.js @@ -1,9 +1,10 @@ -steal.plugins('jquery/dom/cookie','jquery/model/list').then(function($){ +steal('jquery/dom/cookie','jquery/model/list').then(function($){ /** + * @class jQuery.Model.List.Cookie * @plugin jquery/model/list/cookie * @test jquery/model/list/cookie/qunit.html - * @download jquery/dist/jquery.model.list.cookie.js + * @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 @@ -42,7 +43,7 @@ steal.plugins('jquery/dom/cookie','jquery/model/list').then(function($){ * * @demo jquery/model/list/cookie/cookie.html */ -$.Model.List.extend("jQuery.Model.List.Cookie", +$.Model.List("jQuery.Model.List.Cookie", /** * @Prototype */ @@ -57,7 +58,7 @@ $.Model.List.extend("jQuery.Model.List.Cookie", // each also needs what they are referencd by ? var props = $.cookie( name ) || {type : null, ids : []}, instances = [], - Class = props.type ? $.Class.getObject(props.type) : null; + 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 ); @@ -80,7 +81,7 @@ $.Model.List.extend("jQuery.Model.List.Cookie", }); $.cookie(name, $.toJSON({ - type: this[0] && this[0].Class.fullName, + type: this[0] && this[0].constructor.fullName, ids: ids }), { expires: this.days }); return this; diff --git a/model/list/cookie/qunit/qunit.js b/model/list/cookie/qunit/qunit.js index 6dc4b6ae..dc7c7deb 100644 --- a/model/list/cookie/qunit/qunit.js +++ b/model/list/cookie/qunit/qunit.js @@ -1,4 +1,4 @@ -steal.plugins('funcunit/qunit','jquery/model/list/cookie').then(function($){ +steal('funcunit/qunit','jquery/model/list/cookie').then(function($){ module("jquery/model/list/cookie",{ setup: function(){ diff --git a/model/list/list-insert.html b/model/list/list-insert.html index 5fba5ea6..3e941291 100644 --- a/model/list/list-insert.html +++ b/model/list/list-insert.html @@ -25,18 +25,31 @@

                                            Model List Demo

                                            - - - diff --git a/model/list/list.html b/model/list/list.html index 9bb252a8..abb3395a 100644 --- a/model/list/list.html +++ b/model/list/list.html @@ -25,44 +25,32 @@

                                            Model List Helper Demo

                                            - - - diff --git a/model/list/list.js b/model/list/list.js index ff7babc7..6996b8db 100644 --- a/model/list/list.js +++ b/model/list/list.js @@ -1,285 +1,825 @@ -steal.plugins('jquery/model').then(function($){ +steal('jquery/model').then(function( $ ) { -var add = function(data, inst){ - var id = inst.Class.id; - data[inst[id]] = inst; - }, - getArgs = function(args){ - if(args[0] !== undefined && args[0].length && typeof args[0] != 'string'){ + var getArgs = function( args ) { + if ( args[0] && ($.isArray(args[0])) ) { return args[0] - }else{ + } else if ( args[0] instanceof $.Model.List ) { + return $.makeArray(args[0]) + } else { return $.makeArray(args) } - } -/** - * @parent jQuery.Model - * @download jquery/dist/jquery.model.list.js - * @test jquery/model/list/qunit.html - * @plugin jquery/model/list - * Model lists are useful for: - * - * - Adding helpers for multiple model instances. - * - Faster HTML inserts. - * - Storing and retrieving multiple instances. - * - * ## List Helpers - * - * It's pretty common to deal with multiple items at a time. - * List helpers provide methods for multiple model instances. - * - * For example, if we wanted to be able to destroy multiple - * contacts, we could add a destroyAll method to a Contact - * list: - * - * @codestart - * $.Model.List.extend("Contact.List",{ - * destroyAll : function(){ - * $.post("/destroy", - * this.map(function(contact){ - * return contact.id - * }), - * this.callback('destroyed'), - * 'json') - * }, - * destroyed : function(){ - * this.each(function(){ - * this.destroyed(); - * }) - * } - * }); - * @codeend - * - * The following demo illustrates this. Check - * multiple Contacts and click "DESTROY ALL" - * - * @demo jquery/model/list/list.html - * - * ## Faster Inserts - * - * The 'easy' way to add a model to an element is simply inserting - * the model into the view like: - * - * @codestart xml - * <div <%= task %>> A task </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.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 - * <div class='task <%= task.identity() %>'> A task </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 - */ -$.Class.extend("jQuery.Model.List", -/** - * @Prototype - */ -{ - init: function( instances ) { - this.length = 0; - this._data = {}; - this.push.apply(this, $.makeArray(instances || [] ) ); - }, - /** - * Slice works just like an array's slice, except this - * returns another instance of this model list's class. - */ - 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. - * @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; - }); - }, - /** - * Returns a model list of elements where callback returns true. - * @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.Class.id]] = inst; - }) }, - /** - * Gets a list of elements by ID or element. - */ - get: function() { - if(!this.length){ - return new this.Class([]); - } - if(this._changed){ - this._makeData(); - } - var list = [], - underscored = this[0].Class._fullName, - idName = this[0].Class.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) )){ - val = this._data[matches[1]] - }else{ - val = this._data[typeof args[i] == 'string' ? args[i] : args[i][idName] ] + //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 + * <div <%= task %>> A task </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 + * <div class='task <%= task.identity() %>'> A task </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]); + } } - val && list.push(val) } - return new this.Class(list) }, /** - * Removes instances from this list by id or by an - * element. - * @param {Object} args + * @Prototype */ - remove: function( args ) { - if(!this.length){ - return []; - } - var list = [], - underscored = this[0].Class._fullName, - idName = this[0].Class.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' ? - args[a] : - args[a][idName] ); - if(inst[idName] == id){ - list.push.apply(list, this.splice(i, 1) ); - args.splice(a,1); - found = true; - break; + { + 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) } - if(!found){ - i++; + 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: + * + *
                                            ...
                                            + * + * This also works if you hooked up the model: + * + *
                                            <%= todo %>> ...
                                            + * + * 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() + }); } - return new this.Class(list); - }, - publish: function( name, data ) { - OpenAjax.hub.publish(this.Class.shortName+"."+name, data) - }, - /** - * Gets all the elements that represent this list. - * @param {Object} context - */ - elements: function( context ) { - // TODO : this can probably be done with 1 query. - var jq = $(); - this.each(function(){ - jq.add("."+this.identity(), context) - }) - return jq; - } -}); + }); -var modifiers = { - /** - * @function push - * Pushs an instance onto the list - */ - push: [].push, - /** - * @function pop - * Pops the last instance off the list - */ - pop: [].pop, - /** - * @function shift - * Shifts the first instance off the list - */ - shift: [].shift, - /** - * @function unshift - * Adds an instance to the start of the list. - */ - unshift: [].unshift, - /** - * @function splice - * Splices items from the list - */ - splice: [].splice, - /** - * @function sort - * sorts the list - */ - sort : [].sort -} + var push = [].push, + modifiers = { -$.each(modifiers, function(name, func){ - $.Model.List.prototype[name] = function(){ - this._changed = true; - return func.apply( this, arguments ); - } -}) + /** + * @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 indexOf + * Finds the index of the item in the list. Returns -1 if not found. + * + * list.indexOf(item) + * + */ + indexOf: [].indexOf, + /** + * @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([ -/** - * @function each - * Iterates through the list, calling callback on each item in the list. - * @param {Function} callback - */ -'each', -/** - * @function map - * Iterates through the list, calling callback on each item in the list. - * It returns an array of the items each call to callback returned. - * @param {Function} callback - */ -'map'], function(i, name){ - $.Model.List.prototype[name] = function(callback, args){ - return $[name]( this, callback, args ); - } -}) + 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); + } + }) }) \ No newline at end of file diff --git a/model/list/list_test.js b/model/list/list_test.js new file mode 100644 index 00000000..9a94ec23 --- /dev/null +++ b/model/list/list_test.js @@ -0,0 +1,199 @@ +steal("jquery/model/list",'funcunit/qunit', 'jquery/dom/fixture', function(){ + +module("jquery/model/list", { + setup: function() { + $.Model.extend("Person") + + $.Model.List("Person.List",{ + destroy : "DELETE /person/destroyAll", + update : "PUT /person/updateAll" + },{}); + var people = [] + for(var i =0; i < 20; i++){ + people.push( new Person({id: "a"+i}) ) + } + this.people = new $.Model.List(people); + } +}) + +test("hookup with list", function(){ + + + + var div = $("
                                            ") + + for(var i =0; i < 20 ; i ++){ + var child = $("
                                            "); + var p = new Person({foo: "bar"+i, id: i}); + p.hookup( child[0] ); + div.append(child) + } + var models = div.children().models(); + ok(models.Class === Person.List, "correct type"); + equals(models.length, 20, "Got 20 people") + + +}) + +test("create", function(){ + + equals(this.people.length, 20) + + equals(this.people.get("a2")[0].id,"a2" , "get works") +}) + + +test("splice", function(){ + ok(this.people.get("a1").length,"something where a1 is") + this.people.splice(1,1) + equals(this.people.length, 19) + ok(!this.people.get("a1").length,"nothing where a1 is") + +}) + +test("remove", function(){ + var res = this.people.remove("a1") + ok(!this.people.get("a1").length,"nothing where a1 is") + ok(res.length, "got something array like") + equals(res[0].id, "a1") +}) + + +test("list from models", function(){ + var people = Person.models([{id: 1}, {id: 2}]); + ok(people.elements, "we can find elements from a list") +}); + +test("destroy a list", function(){ + var people = Person.models([{id: 1}, {id: 2}]); + stop(); + // make sure a request is made + $.fixture('DELETE /person/destroyAll', function(){ + + ok(true, "called right fixture"); + return true; + }) + // make sure the people have a destroyed event + people[0].bind('destroyed', function(){ + ok(true, "destroyed event called") + }) + + people.destroy(function(deleted){ + ok(true, "destroy callback called"); + ok(people.length, 0, "objects removed"); + ok(deleted.length, 2, "got back deleted items") + start() + // make sure the list is empty + + }) + +}); + +test("destroy a list with nothing in it", function(){ + var people = Person.models([]); + stop(); + + // make sure a request is made + $.fixture('DELETE /person/destroyAll', function(){ + ok(true, "called right fixture"); + return true; + }); + + people.destroy(function(deleted){ + ok(true, "destroy callback called"); + equal(deleted.length, people.length, "got back deleted items") + start(); + }); +}); + +test("update a list", function(){ + var people = Person.models([{id: 1}, {id: 2}]), + updateWith = { + name: "Justin", + age : 29 + }, + newProps = { + newProp : "yes" + }; + stop(); + + // make sure a request is made + $.fixture('PUT /person/updateAll', function(orig){ + ok(true, "called right fixture"); + ok(orig.data.ids.length, 2, "got 2 ids") + same(orig.data.attrs, updateWith, "got the same attrs") + return newProps; + }) + + // make sure the people have a destroyed event + people[0].bind('updated', function(){ + ok(true, "updated event called") + }) + + people.update(updateWith,function(updated){ + ok(true, "updated callback called"); + ok(updated.length, 2, "got back deleted items"); + same(updated[0].attrs(),$.extend({id : 1},newProps, updateWith )); + start(); + }); +}) + +test("update a list with nothing in it", function(){ + var people = Person.models([]), + updateWith = { + name: "Justin", + age : 29 + }; + stop(); + + // make sure a request is made + $.fixture('PUT /person/updateAll', function(orig){ + ok(true, "called right fixture"); + return newProps; + }); + + people.update(updateWith,function(updated){ + ok(true, "updated callback called"); + equal(updated.length, people.length, "nothing updated"); + start(); + }); +}) + +test("events - add", 4, function(){ + var list = new Person.List; + list.bind("add", function(ev, items){ + ok(1, "add called"); + equals(items.length, 1, "we get an array") + }); + + var person = new Person({id: 1, name: "alex"}); + + + list.push(person); + + // check that we are listening to updates on person ... + + ok( $(person).data("events"), "person has events" ); + + list.unbind("add"); + + ok( !$(person).data("events"), "person has no events" ); + +}); + +test("events - update", function(){ + var list = new Person.List; + list.bind("update", function(ev, updated){ + ok(1, "update called"); + ok(person === updated, "we get the person back"); + + equals(updated.name, "Alex", "got the right name") + }); + + var person = new Person({id: 1, name: "justin"}); + list.push(person); + + person.updated({name: "Alex"}) +}); + +}) diff --git a/model/list/local/local.js b/model/list/local/local.js index 53ddf97b..eb9bfb0f 100644 --- a/model/list/local/local.js +++ b/model/list/local/local.js @@ -1,18 +1,19 @@ -steal.plugins('jquery/dom/cookie','jquery/model/list').then(function($){ +steal('jquery/dom/cookie','jquery/model/list').then(function($){ /** + * @class jQuery.Model.List.Local * @plugin jquery/model/list/local - * @download jquery/dist/jquery.model.list.local.js + * @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.extend("jQuery.Model.List.Local", +$.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 ? $.Class.getObject(props.type) : null; + 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 ]; @@ -25,11 +26,11 @@ $.Model.List.extend("jQuery.Model.List.Local", // go through and listen to instance updating var ids = [], days = this.days; this.each(function(i, inst){ - window.localStorage[inst.identity()] = instance.attrs(); + window.localStorage[inst.identity()] = inst.attrs(); ids.push(inst.identity()); }); window.localStorage[name] = { - type: this[0] && this[0].Class.fullName, + type: this[0] && this[0].constructor.fullName, ids: ids }; return this; diff --git a/model/list/memory.html b/model/list/memory.html new file mode 100644 index 00000000..94fbc973 --- /dev/null +++ b/model/list/memory.html @@ -0,0 +1,87 @@ + + + + List Helper Demo + + + + Start +
                                            + + + + + + \ No newline at end of file diff --git a/model/list/qunit.html b/model/list/qunit.html index 81fe09fa..1ea6a1eb 100644 --- a/model/list/qunit.html +++ b/model/list/qunit.html @@ -1,7 +1,7 @@ - + diff --git a/model/list/test/qunit/list_test.js b/model/list/test/qunit/list_test.js deleted file mode 100644 index 7e94fd66..00000000 --- a/model/list/test/qunit/list_test.js +++ /dev/null @@ -1,64 +0,0 @@ - -module("jquery/model/list", { - setup: function() { - $.Model.extend("Person") - - $.Model.List.extend("Person.List",{ - destroy: function() { - equals(this.length, 20, "Got 20 people") - } - }); - var people = [] - for(var i =0; i < 20; i++){ - people.push( new Person({id: "a"+i}) ) - } - this.people = new $.Model.List(people); - } -}) - -test("hookup with list", function(){ - - - - var div = $("
                                            ") - - for(var i =0; i < 20 ; i ++){ - var child = $("
                                            "); - var p = new Person({foo: "bar"+i, id: i}); - p.hookup( child[0] ); - div.append(child) - } - var models = div.children().models(); - ok(models.Class === Person.List, "correct type"); - models.destroy(); - -}) - -test("create", function(){ - - equals(this.people.length, 20) - - equals(this.people.get("a2")[0].id,"a2" , "get works") -}) - - -test("splice", function(){ - ok(this.people.get("a1").length,"something where a1 is") - this.people.splice(1,1) - equals(this.people.length, 19) - ok(!this.people.get("a1").length,"nothing where a1 is") - -}) - -test("remove", function(){ - var res = this.people.remove("a1") - ok(!this.people.get("a1").length,"nothing where a1 is") - ok(res.length, "got something array like") - equals(res[0].id, "a1") -}) - - -test("list from wrapMany", function(){ - var people = Person.wrapMany([{id: 1}, {id: 2}]); - ok(people.destroy, "we can destroy a list") -}) diff --git a/model/list/test/qunit/qunit.js b/model/list/test/qunit/qunit.js deleted file mode 100644 index 43a3fb32..00000000 --- a/model/list/test/qunit/qunit.js +++ /dev/null @@ -1,6 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/model/list") //load your app - .plugins('funcunit/qunit') //load qunit - .then("list_test") - diff --git a/model/model.js b/model/model.js index b0c2d44f..9e0e589d 100644 --- a/model/model.js +++ b/model/model.js @@ -1,332 +1,1029 @@ /*global OpenAjax: true */ -steal.plugins('jquery/class', 'jquery/lang').then(function() { - //a cache for attribute capitalization ... slowest part of init. - var underscore = $.String.underscore, - classize = $.String.classize; +steal('jquery/class', 'jquery/lang/string', 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'; /** - * @tag core - * @download jquery/dist/jquery.model.js + * @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 wrap an application's data layer. In large applications, a model is critical for: + * Models super-charge an application's + * data layer, making it easy to: * - * - Encapsulating services so controllers + views don't care where data comes from. - * - * - Providing helper functions that make manipulating and abstracting raw service data easier. + * - 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 * - * This is done in two ways: + * 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. * - * - Requesting data from and interacting with services - * - * - Converting or wrapping raw service data into a more useful form. * + * ## Get and modify data from the server * - * ## Basic Use + * $.Model makes connecting to a JSON REST service + * really easy. The following models todos by + * describing the services that can create, retrieve, + * update, and delete todos. * - * The [jQuery.Model] class provides a basic skeleton to organize pieces of your application's data layer. - * First, consider doing Ajax without a model. In our imaginary app, you: + * $.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' + * },{}); * - * - retrieve a list of tasks - * - display the number of days remaining for each task - * - mark tasks as complete after users click them + * This lets you create, retrieve, update, and delete + * todos programatically: * - * Let's see how that might look without a model: + * __Create__ * - * @codestart - * $.Controller.extend("MyApp.Controllers.Tasks",{onDocument: true}, - * { - * // get tasks when the page is ready - * ready: function() { - * $.get('/tasks.json', this.callback('gotTasks'), 'json') - * }, - * |* - * * assume json is an array like [{name: "trash", due_date: 1247111409283}, ...] - * *| - * gotTasks: function( json ) { - * for(var i =0; i < json.length; i++){ - * var taskJson = json[i]; + * Create a todo instance and + * call [$.Model::save save]\(success, error\) + * 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 + * [$.Model.findAll findAll]\(params, callback(items)\): + * + * 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 + * [$.Model.findOne findOne]\(params, callback(item)\): + * + * 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 + * save to update it on the server. + * + * // update the todos' name + * todo.attr('name','Take out the trash') * - * //calculate time remaining - * var remaininTime = new Date() - new Date(taskJson.due_date); + * // update it on the server + * todo.save() * - * //append some html - * $("#tasks").append("<div class='task' taskid='"+taskJson.id+"'>"+ - * "<label>"+taskJson.name+"</label>"+ - * "Due Date = "+remaininTime+"</div>") - * } - * }, - * // when a task is complete, get the id, make a request, remove it - * ".task click" : function( el ) { - * $.post('/task_complete',{id: el.attr('data-taskid')}, function(){ - * el.remove(); + * + * __Destroy__ + * + * Call [$.Model.prototype.destroy destroy]\(success, error\) + * 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 + * Model.[$.Model.bind bind]\(eventType, handler(event, model)\) + * to listen to all events of type on a model and + * model.[$.Model.prototype.bind bind]\(eventType, handler(event)\) + * 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 ) {...}) * }) - * } - * }) - * @codeend + * + * __Destroy__ * - * This code might seem fine for right now, but what if: + * // 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 ) {...}) * - * - The service changes? - * - Other parts of the app want to calculate remaininTime? - * - Other parts of the app want to get tasks? - * - The same task is represented multiple palces on the page? + * __Property Changes__ * - * The solution is of course a strong model layer. Lets look at what a - * a good model does for a controller before we learn how to make one: + * // listen for when the name property changes + * todo.bind('name', function(ev){ }) * - * @codestart - * $.Controller.extend("MyApp.Controllers.Tasks",{onDocument: true}, - * { - * load: function() { - * Task.findAll({},this.callback('list')) - * }, - * list: function( tasks ) { - * $("#tasks").html(this.view(tasks)) - * }, - * ".task click" : function( el ) { - * el.models()[0].complete(function(){ - * el.remove(); + * __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. + * + * [jQuery.fn.model $.fn.model]\(item\) lets you set or read a model + * instance from an element: + * + * Todo.findAll({}, function( todos ) { + * + * $.each(todos, function(todo) { + * $('
                                          • ').model(todo) + * .text(todo.name) + * .appendTo('#todos') + * }); * }); - * } - * }) - * @codeend * - * In views/tasks/list.ejs + * When a todo is deleted, get its element with + * item.[$.Model.prototype.elements elements]\(context\) + * 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 - * <% for(var i =0; i < tasks.length; i++){ %> - * <div class='task <%= tasks[i].identity() %>'> - * <label><%= tasks[i].name %></label> - * <%= tasks[i].timeRemaining() %> - * </div> + * <% for(var i =0; i < todos.length; i++){ %> + * <li <%= todos[i] %>><%= todos[i].name %></li> * <% } %> * @codeend * - * Isn't that better! Granted, some of the improvement comes because we used a view, but we've - * also made our controller completely understandable. Now lets take a look at the model: + * Notice how you can add a model to an element with <%= model %> * - * @codestart - * $.Model.extend("Task", - * { - * findAll: function( params,success ) { - * $.get("/tasks.json", params, this.callback(["wrapMany",success]),"json"); - * } - * }, - * { - * timeRemaining: function() { - * return new Date() - new Date(this.due_date) - * }, - * complete: function( success ) { - * $.get("/task_complete", {id: this.id }, success,"json"); - * } - * }) - * @codeend + * ## Lists * - * There, much better! Now you have a single place where you can organize Ajax functionality and - * wrap the data that it returned. Lets go through each bolded item in the controller and view.
                                            + * [$.Model.List $.Model.List] lets you handle multiple model instances + * with ease. A List acts just like an Array, but you can add special properties + * to it and listen to events on it. * - * ### Task.findAll + * $.Model.List has become its own plugin, read about it + * [$.Model.List here]. * - * The findAll function requests data from "/tasks.json". When the data is returned, it it is run through - * the "wrapMany" function before being passed to the success callback.
                                            - * If you don't understand how the callback works, you might want to check out - * [jQuery.Model.static.wrapMany wrapMany] and [jQuery.Class.static.callback callback]. + * ## Other Good Stuff * - * ### el.models + * Model can make a lot of other common tasks much easier. * - * [jQuery.fn.models models] is a jQuery helper that returns model instances. It uses - * the jQuery's elements' shortNames to find matching model instances. For example: + * ### Type Conversion * - * @codestart html - * <div class='task task_5'> ... </div> - * @codeend + * 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: * - * It knows to return a task with id = 5. + * { name: 'take out trash', + * id: 1, + * dueDate: 1303173531164 } * - * ### complete + * But instead, you want a JavaScript date object: * - * This should be pretty obvious. + * 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: * - * ### identity + * $.Model('Todo',{ + * attributes : { + * dueDate: 'date' + * } + * },{}) * - * [jQuery.Model.prototype.identity Identity] returns a unique identifier that [jQuery.fn.models] can use - * to retrieve your model instance. + * ### Associations * - * ### timeRemaining + * The [$.Model.attributes attributes] property also + * supports associations. For example, todo data might come back with + * User data as an owner property like: * - * timeRemaining is a good example of wrapping your model's raw data with more useful functionality. - * ## Validations + * { name: 'take out trash', + * id: 1, + * owner: { name: 'Justin', id: 3} } * - * You can validate your model's attributes with another plugin. See [validation]. + * To convert owner into a User model, set the owner type as the User's + * [$.Model.model model]( data ) 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 timeRemaining 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" + * } + * }) + * } + * ,{}); + * + * */ - - - jQuery.Class.extend("jQuery.Model", + // methods that we'll weave into model if provided + ajaxMethods = /** * @Static */ { - setup: function( superClass ) { - - //we do not inherit attributes (or associations) - if (!this.attributes || superClass.attributes === this.attributes ) { - this.attributes = {}; - } - - if (!this.associations || superClass.associations === this.associations ) { - this.associations = {}; + 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") } - if (!this.validations || superClass.validations === this.validations ) { - this.validations = {}; + }, + 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 [$.Model.models]\(raw\). 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"); + }; + } + }; + - //add missing converters - if ( superClass.convert != this.convert ) { - this.convert = $.extend(superClass.convert, this.convert); - } - this._fullName = underscore(this.fullName.replace(/\./g, "_")); - if ( this.fullName.substr(0, 7) == "jQuery." ) { + 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 - jQuery.Model.models[this._fullName] = this; - + //$.Model.models[this._fullName] = this; if ( this.listType ) { this.list = new this.listType([]); } + //!steal-remove-start + if (!proto ) { + steal.dev.warn("model.js " + fullName + " has no static properties. You probably need ,{} ") + } + //!steal-remove-end + 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 list of properties and their types - * for this model. You can use this in conjunction with - * [jQuery.Model.static.convert] to provide automatic - * [jquery.model.typeconversion type conversion]. + * 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: * - * @codestart - * $.Model.extend("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; + * + * $.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" * } - * } - * },{}) - * @codeend + * + * This points tasks and owner properties to use + * Task.models and Person.model + * to convert the raw data into an array of Tasks and a Person. + * + * Note that the full names of the models themselves are App.Models.Task + * and App.Models.Person. 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: {}, /** - * @attribute defaults - * An object of default values to be set on all instances. This - * is useful if you want some value to be present when new instances are created. + * $.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. * - * @codestart - * $.Model.extend("Recipe",{ - * defaults : { - * createdAt : new Date(); - * } - * },{}) + * You will never call this method directly. Instead, you tell $.ajax about it in findOne: * - * var recipe = new Recipe(); + * $.Model('Recipe',{ + * findOne : function(params, success, error ){ + * return $.ajax({ + * url: '/services/recipes/'+params.id+'.json', + * type: 'get', + * + * dataType : 'json recipe.model' //LOOK HERE! + * }); + * } + * },{}) * - * recipe.createdAt //-> date + * This makes the result of findOne a [http://api.jquery.com/category/deferred-object/ $.Deferred] + * that resolves to a model instance: * - * @codeend - */ - defaults: {}, - /** - * Wrap is used to create a new instance from data returned from the server. - * It is very similar to doing new Model(attributes) - * except that wrap will check if the data passed has an + * 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"} } * - * - attributes, - * - data, or - * - singularName * - * property. If it does, it will use that objects attributes. + * ### Overwriting Model * - * Wrap is really a convience method for servers that don't return just attributes. + * If your service returns data like: * - * @param {Object} attributes + * {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 */ - wrap: function( attributes ) { + 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); - attributes[this.singularName] || attributes.data || attributes.attributes || attributes); + isObject(attributes[this._shortName]) || isObject(attributes.data) || isObject(attributes.attributes) || attributes); }, /** - * Takes raw data from the server, and returns an array of model instances. - * Each item in the raw array becomes an instance of a model class. + * $.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. * - * @codestart - * $.Model.extend("Recipe",{ - * helper : function(){ - * return i*i; - * } - * }) + * You will never call this method directly. Instead, you tell $.ajax about it in findAll: * - * var recipes = Recipe.wrapMany([{id: 1},{id: 2}]) - * recipes[0].helper() //-> 1 - * @codeend + * $.Model('Recipe',{ + * findAll : function(params, success, error ){ + * return $.ajax({ + * url: '/services/recipes.json', + * type: 'get', + * data: params + * + * dataType : 'json recipe.models' //LOOK HERE! + * }); + * } + * },{}) * - * If an array is not passed to wrapMany, it will look in the object's .data - * property. + * This makes the result of findAll a [http://api.jquery.com/category/deferred-object/ $.Deferred] + * that resolves to a list of model instances: * - * For example: + * var deferredRecipes = Recipe.findAll({}); + * + * deferredRecipes.then(function(recipes){ + * console.log('I have '+recipes.length+'recipes.'); + * }) * - * @codestart - * var recipes = Recipe.wrapMany({data: [{id: 1},{id: 2}]}) - * recipes[0].helper() //-> 1 - * @codeend + * ## 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 [jQuery.Model.List list] of instances + * @return {Array} a JavaScript array of instances or a [$.Model.List list] of instances * if the model list plugin has been included. */ - wrapMany: function( instancesRawData ) { + models: function( instancesRawData ) { if (!instancesRawData ) { return null; } - var listType = this.List || $.Model.List || Array, - res = new listType(), - arr = $.isArray(instancesRawData), - raw = arr ? instancesRawData : instancesRawData.data, - length = raw.length, + // 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; - res._use_call = true; //so we don't call next function with all of these + //!steal-remove-start + if (!length ) { + steal.dev.warn("model.js models has no data. If you have one item, use model") + } + //!steal-remove-end for (; i < length; i++ ) { - res.push(this.wrap(raw[i])); + res.push(this.model(raw[i])); } if (!arr ) { //push other stuff onto array - for ( var prop in instancesRawData ) { + each(instancesRawData, function(prop, val){ if ( prop !== 'data' ) { - res[prop] = instancesRawData[prop]; + res[prop] = val; } - - } + }) } return res; }, @@ -336,7 +1033,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * For example, it's common in .NET to use Id. Your model might look like: * * @codestart - * $.Model.extend("Friends",{ + * $.Model("Friends",{ * id: "Id" * },{}); * @codeend @@ -350,131 +1047,98 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * @param {String} type */ addAttr: function( property, type ) { - var stub; + var stub, attrs = this.attributes; - if ( this.associations[property] ) { - return; - } - stub = this.attributes[property] || (this.attributes[property] = type); + stub = attrs[property] || (attrs[property] = type); return type; }, - // a collection of all models - models: {}, - /** - * If OpenAjax is available, - * publishes to OpenAjax.hub. Always adds the shortName.event. - * - * @codestart - * // publishes contact.completed - * Namespace.Contact.publish("completed",contact); - * @codeend - * - * @param {String} event The event name to publish - * @param {Object} data The data to publish - */ - publish: function( event, data ) { - //@steal-remove-start - steal.dev.log("Model.js - publishing " + underscore(this.shortName) + "." + event); - //@steal-remove-end - if ( window.OpenAjax ) { - OpenAjax.hub.publish(underscore(this.shortName) + "." + event, data); - } - - }, - /** - * @hide - * Guesses the type of an object. This is what sets the type if not provided in - * [jQuery.Model.static.attributes]. - * @param {Object} object the object you want to test. - * @return {String} one of string, object, date, array, boolean, number, function - */ - guessType: function( object ) { - if ( typeof object != 'string' ) { - if ( object === null ) { - return typeof object; - } - if ( object.constructor == Date ) { - return 'date'; - } - if ( $.isArray(object) ) { - return 'array'; - } - return typeof object; - } - if ( object === "" ) { - return 'string'; - } - //check if true or false - if ( object == 'true' || object == 'false' ) { - return 'boolean'; - } - if (!isNaN(object) && isFinite(+object) ) { - return 'number'; - } - return typeof object; - }, /** * @attribute convert * @type Object * An object of name-function pairs that are used to convert attributes. - * Check out [jQuery.Model.static.attributes] or + * 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 ) { - return typeof str === "string" ? (isNaN(Date.parse(str)) ? null : Date.parse(str)) : 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); + 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; } }, /** - * Implement this function! - * Create is called by save to create a new instance. If you want to be able to call save on an instance - * you have to implement create. - */ - create: function( attrs, success, error ) { - throw "Model: Implement Create"; - }, - /** - * Implement this function! - * Update is called by save to update an instance. If you want to be able to call save on an instance - * you have to implement update. - */ - update: function( id, attrs, success, error ) { - throw "Model: Implement " + this.fullName + "'s \"update\"!"; - }, - /** - * Implement this function! - * Destroy is called by destroy to remove an instance. If you want to be able to call destroy on an instance - * you have to implement update. - * @param {String|Number} id the id of the instance you want destroyed + * @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" } + * */ - destroy: function( id, success, error ) { - throw "Model: Implement " + this.fullName + "'s \"destroy\"!"; + serialize: { + "default": function( val, type ) { + return isObject(val) && val.serialize ? val.serialize() : val; + }, + "date": function( val ) { + return val && val.getTime() + } }, /** - * Implement this function! - * @param {Object} params - * @param {Function} success - * @param {Function} error + * @function bind */ - findAll: function( params, success, error ) { - - }, + bind: bind, /** - * Implement this function! - * @param {Object} params - * @param {Function} success - * @param {Function} error + * @function unbind */ - findOne: function( params, success, error ) { - - } + unbind: unbind, + _ajax: ajax }, /** * @Prototype @@ -487,7 +1151,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * Setup should never be called directly. * * @codestart - * $.Model.extend("Recipe") + * $.Model("Recipe") * var recipe = new Recipe({foo: "bar"}); * recipe.foo //-> "bar" * recipe.attr("foo") //-> "bar" @@ -496,29 +1160,22 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * @param {Object} attributes a hash of attributes */ setup: function( attributes ) { - var stub; - // so we know not to fire events - this._initializing = true; - - stub = this.Class.defaults && this.attrs(this.Class.defaults); - - this.attrs(attributes); - delete this._initializing; + 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 [jQuery.Model.static.update update] + * the instance class's [$.Model.update update] * method. * * @codestart * recipe.update({name: "chicken"}, success, error); * @codeend * - * If OpenAjax.hub is available, the model will also - * publish a "modelName.updated" message with - * the updated instance. + * 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 @@ -537,60 +1194,72 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * To use validations, it's suggested you use the * model/validations plugin. * - * @codestart - * $.Model.extend("Task",{ - * init : function(){ - * this.validatePresenceOf("dueDate") - * } - * },{}); + * $.Model("Task",{ + * init : function(){ + * this.validatePresenceOf("dueDate") + * } + * },{}); * - * var task = new Task(), - * errors = task.errors() + * var task = new Task(), + * errors = task.errors() * - * errors.dueDate[0] //-> "can't be empty" - * @codeend + * 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); + 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 ) { + each(funcs, function( i, func ) { var res = func.call(self); if ( res ) { - if (!errors.hasOwnProperty(attr) ) { + if (!errors[attr] ) { errors[attr] = []; } - errors[attr].push(res); } }); - }; + }, + validations = this.constructor.validations; - $.each(attrs || this.Class.validations || {}, function( attr, funcs ) { + // 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 = self.Class.validations[attr]; + funcs = validations[attr]; } + // add errors to the addErrors(attr, funcs || []); }); + // return errors as long as we have one + return $.isEmptyObject(errors) ? null : errors; - for ( var attr in errors ) { - if ( errors.hasOwnProperty(attr) ) { - return errors; - } - } - return null; }, /** * Gets or sets an attribute on the model using setters and * getters if available. * * @codestart - * $.Model.extend("Recipe") + * $.Model("Recipe") * var recipe = new Recipe(); * recipe.attr("foo","bar") * recipe.foo //-> "bar" @@ -604,7 +1273,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * with the value and is expected to return the converted value. * * @codestart - * $.Model.extend("Recipe",{ + * $.Model("Recipe",{ * setCreatedAt : function(raw){ * return Date.parse(raw) * } @@ -623,7 +1292,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * call success with the converted value. For example: * * @codestart - * $.Model.extend("Recipe",{ + * $.Model("Recipe",{ * setTitle : function(title, success, error){ * $.post( * "recipe/update/"+this.id+"/title", @@ -641,7 +1310,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * ## Events * * When you use attr, it can also trigger events. This is - * covered in [jQuery.Model.prototype.bind]. + * 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. @@ -651,21 +1320,52 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * 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 ) { - this._setProperty(attribute, value, success, error, cap); + // 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.extend("School") + * $.Model("School") * var school = new School(); * school.bind("address", function(ev, address){ * alert('address changed to '+address); @@ -676,7 +1376,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * You can also bind to attribute errors. * * @codestart - * $.Model.extend("School",{ + * $.Model("School",{ * setName : function(name, success, error){ * if(!name){ * error("no name"); @@ -697,100 +1397,122 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * @param {Function} handler a function to call back when an event happens on this model. * @return {model} the model instance for chaining */ - bind: function( eventType, handler ) { - var wrapped = $(this); - wrapped.bind.apply(wrapped, arguments); - return this; - }, + bind: bind, /** + * @function unbind * Unbinds an event handler from this instance. - * Read [jQuery.Model.prototype.bind] for + * Read [$.Model.prototype.bind] for * more information. * @param {String} eventType * @param {Function} handler */ - unbind: function( eventType, handler ) { - var wrapped = $(this); - wrapped.unbind.apply(wrapped, arguments); - return this; - }, - /** - * Checks if there is a set_property value. If it returns true, lets it handle; otherwise - * saves it. - * @hide - * @param {Object} property - * @param {Object} value - */ - _setProperty: function( property, value, success, error, capitalized ) { - // the potential setter name - var setName = "set" + capitalized, - //the old value - old = this[property], - self = this, - errorCallback = function( errors ) { - var stub; - stub = error && error.call(self, errors); - $(self).triggerHandler("error." + property, errors); - }; - - // if the setter returns nothing, do not set - // we might want to indicate if this was set ok - if ( this[setName] && (value = this[setName](value, this.callback('_updateProperty', property, value, old, success, errorCallback), errorCallback)) === undefined ) { - return; - } - this._updateProperty(property, value, old, success, errorCallback); - }, - /** - * Triggers events when a property has been updated - * @hide - * @param {Object} property - * @param {Object} value - * @param {Object} old - * @param {Object} success - */ + 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.Class, - val, type = Class.attributes[property] || Class.addAttr(property, Class.guessType(value)), + 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], + converter = Class.convert[type] || Class.convert['default'], + // errors for this property errors = null, - stub; + // 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) ); - val = this[property] = (value === null ? //if the value is null or undefined - null : // it should be null - (converter ? converter.call(Class, value) : //convert it to something useful - value)); //just return it //validate (only if not initializing, this is for performance) - if (!this._initializing ) { + 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 ) { - errorCallback(errors); - } else { - if ( old !== val && !this._initializing ) { - $(this).triggerHandler(property, val); - } - stub = success && success(this); - + 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 && Class.list ) { + if ( property === Class.id && val !== null && list ) { // if we didn't have an old id, add ourselves if (!old ) { - Class.list.push(this); + list.push(this); } else if ( old != val ) { // if our id has changed ... well this should be ok - Class.list.remove(old); - Class.list.push(this); + 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 [jQuery.Model.prototype.attr attr]. + * Each attribute is set with [$.Model.prototype.attr attr]. * * @codestart * recipe.attrs({ @@ -799,20 +1521,23 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * }) * @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; + var key, constructor = this.constructor, + attrs = constructor.attributes; if (!attributes ) { attributes = {}; - for ( key in this.Class.attributes ) { - if ( this.Class.attributes.hasOwnProperty(key) ) { + for ( key in attrs ) { + if ( attrs.hasOwnProperty(key) ) { attributes[key] = this.attr(key); } } } else { - var idName = this.Class.id; + var idName = constructor.id; //always set the id last for ( key in attributes ) { if ( key != idName ) { @@ -827,43 +1552,91 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { return attributes; }, /** - * Returns if the instance is a new object + * 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() { - return (this[this.Class.id] === undefined); //if null or undefined + var id = getId(this); + return (id === undefined || id === null || id === ''); //if null or undefined }, /** - * Saves the instance if there are no errors. - * If the instance is new, [jQuery.Model.static.create] is - * called; otherwise, [jQuery.Model.static.update] is - * called. + * Creates or updates the instance using [$.Model.create] or + * [$.Model.update] depending if the instance + * [$.Model.prototype.isNew has an id or not]. * - * @codestart - * recipe.save(success, error); - * @codeend + * 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); * - * If OpenAjax.hub is available, after a successful create or update, - * "modelName.created" or "modelName.updated" is published. * - * @param {Function} [success] called if a successful save. - * @param {Function} [error] called if the save was not successful. + * @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 ) { - var stub; - - if ( this.errors() ) { - //needs to send errors - return false; - } - stub = this.isNew() ? this.Class.create(this.attrs(), this.callback(['created', success]), error) : this.Class.update(this[this.Class.id], this.attrs(), this.callback(['updated', success]), error); - - //this.is_new_record = this.Class.new_record_func; - return true; + return makeRequest(this, this.isNew() ? 'create' : 'update', success, error); }, /** * Destroys the instance by calling - * [jQuery.Model.static.destroy] with the id of the instance. + * [$.Model.destroy] with the id of the instance. * * @codestart * recipe.destroy(success, error); @@ -877,7 +1650,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * @param {Function} [error] called if an unsuccessful destroy */ destroy: function( success, error ) { - this.Class.destroy(this[this.Class.id], this.callback(["destroyed", success]), error); + return makeRequest(this, 'destroy', success, error, 'destroyed'); }, @@ -887,42 +1660,55 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * 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 [jQuery.Model.prototype.elements elements]. + * for a model with [$.Model.prototype.elements elements]. * @return {String} */ identity: function() { - var id = this[this.Class.id]; - return this.Class._fullName + '_' + (this.Class.escapeIdentity ? encodeURIComponent(id) : id); + 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 [jQuery.Model.prototype.identity identity] function in their class name. Example: - * @codestart html - *
                                            ...
                                            - * @codeend - * This function should only rarely be used. It breaks the architecture. - * @param {String|jQuery|element} context - + * us the [$.Model.prototype.identity identity] function in their class name. Example: + * + *
                                            ...
                                            + * + * This also works if you hooked up the model: + * + *
                                            <%= todo %>> ...
                                            + * + * 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 ) { - return $("." + this.identity(), context); - }, - /** - * Publishes to open ajax hub - * @param {String} event - * @param {Object} [opt6] data if missing, uses the instance in {data: this} - */ - publish: function( event, data ) { - this.Class.publish(event, data || this); + var id = this.identity(); + if( this.constructor.escapeIdentity ) { + id = id.replace(/([ #;&,.+*~\'%:"!^$[\]()=>|\/])/g,'\\$1') + } + + return $("." + id, context); }, hookup: function( el ) { - var shortName = underscore(this.Class.shortName), + var shortName = this.constructor._shortName, models = $.data(el, "models") || $.data(el, "models", {}); $(el).addClass(shortName + " " + this.identity()); models[shortName] = this; } }); - $.each([ + + each([ /** * @function created * @hide @@ -940,20 +1726,34 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { /** * @function destroyed * @hide - * Called after an instance is destroyed. Publishes - * "shortName.destroyed" + * 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; + var stub, constructor = this.constructor; - if ( funcName === 'destroyed' && this.Class.list ) { - this.Class.list.remove(this[this.Class.id]); + // remove from the list if instance is destroyed + if ( funcName === 'destroyed' && constructor.list ) { + constructor.list.remove(getId(this)); } - $(this).triggerHandler(funcName); + + // update attributes if attributes have been passed stub = attrs && typeof attrs == 'object' && this.attrs(attrs.attrs ? attrs.attrs() : attrs); - this.publish(funcName, this); - return [this].concat($.makeArray(arguments)); + + // call event on the instance + trigger(this,funcName); + + //!steal-remove-start + steal.dev.log("Model.js - "+ constructor.shortName+" "+ funcName); + //!steal-remove-end + + // call event on the instance's Class + trigger(constructor,funcName, this); + return [this].concat(makeArray(arguments)); // return like this for this.proxy chains }; }); @@ -964,7 +1764,7 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { /** * @function models * Returns a list of models. If the models are of the same - * type, and have a [jQuery.Model.List], it will return + * type, and have a [$.Model.List], it will return * the models wrapped with the list. * * @codestart @@ -972,32 +1772,46 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { * @codeend * * @param {jQuery.Class} [type] if present only returns models of the provided type. - * @return {Array|jQuery.Model.List} returns an array of model instances that are represented by the contained elements. + * @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 ) { + each($.data(this, "models") || {}, function( name, instance ) { //either null or the list type shared by all classes - kind = kind === undefined ? instance.Class.List || null : (instance.Class.List === kind ? kind : null); + kind = kind === undefined ? instance.constructor.List || null : (instance.constructor.List === kind ? kind : null); collection.push(instance); }); }); - retType = kind || $.Model.List || Array; - ret = new retType(); + ret = getList(kind); - ret.push.apply(ret, $.unique(collection)); + ret.push.apply(ret, unique(collection)); return ret; }; /** * @function model * - * Returns the first model instance found from [jQuery.fn.models]. + * 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){ + * $('
                                            ').model(item) + * .appendTo(el) + * }) + * } * - * @param {Object} type + * @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 ) { @@ -1008,4 +1822,4 @@ steal.plugins('jquery/class', 'jquery/lang').then(function() { } }; -}); \ No newline at end of file +}); diff --git a/model/modelBinder.html b/model/modelBinder.html index 3dee5ddc..cba4a743 100644 --- a/model/modelBinder.html +++ b/model/modelBinder.html @@ -44,10 +44,10 @@ - + \ No newline at end of file diff --git a/model/pages/associations.html b/model/pages/associations.html new file mode 100644 index 00000000..68dc048f --- /dev/null +++ b/model/pages/associations.html @@ -0,0 +1,106 @@ + + + + Model Events Demo + + + +
                                            +

                                            Model Associations Demo

                                            +

                                            This demo shows how you can setup associations.

                                            +
                                            +
                                            +
                                            +
                                            + + + + + \ No newline at end of file diff --git a/model/pages/deferreds.md b/model/pages/deferreds.md new file mode 100644 index 00000000..eaf4da9a --- /dev/null +++ b/model/pages/deferreds.md @@ -0,0 +1,48 @@ +@page jquery.model.deferreds Deferreds +@parent jQuery.Model + +Models (and views) work +with [http://api.jquery.com/category/deferred-object/ jQuery.Deferred]. If +you properly fill out a model's [jquery.model.services service API], asynchronous +requests done via the model will return a jQuery.Deferred. + +## findAll example + +The following example, requests tasks and people and waits for both requests +to be complete before alerting the user: + + var tasksDef = Task.findAll(), + peopleDef = People.findAll(); + + $.when(tasksDef,peopleDef).done(function(taskResponse, peopleResponse){ + alert("There are "+taskRespone[0].length+" tasks and "+ + peopleResponse[0].length+" people."); + }); + +__Note__ taskResponse[0] is an Array of tasks. + +## save and destroy example + +Calls to [jQuery.Model.prototype.save save] and [jQuery.Model.prototype.destroy] also +return a deferred. The deferred is resolved to the newly created, destroyed, or updated +model instance. + +The following creates a task, updates it, and destroys it: + + var taskD = new Task({name: "dishes"}).save(); + + taskD.done(function(task){ + + var taskD2 = task.update({name: "all the dishes"}) + + taskD2.done(function(task){ + + var taskD3 = task.destroy(); + + taskD3.done(function(){ + console.log("task destroyed"); + }) + + }) + + }); diff --git a/model/pages/encapsulate.js b/model/pages/encapsulate.md similarity index 50% rename from model/pages/encapsulate.js rename to model/pages/encapsulate.md index b6300a6a..4b8546cc 100644 --- a/model/pages/encapsulate.js +++ b/model/pages/encapsulate.md @@ -1,10 +1,8 @@ -/* @page jquery.model.encapsulate Service Encapsulation @parent jQuery.Model -

                                            Service / Ajax Encapsulation

                                            - -Models encapsulate your application's raw data. +Models encapsulate your application's raw data. This promotes reuse and provide a +standard interface for widgets to talk to services. The majority of the time, the raw data comes from services your server provides. For example, @@ -28,18 +26,14 @@ The server might return something like: In most jQuery code, you'll see something like the following to retrieve contacts data: -@codestart -$.get('/contacts.json', - {type: 'tasty'}, - successCallback, - 'json') -@codeend + $.get('/contacts.json', + {type: 'tasty'}, + successCallback, + 'json') Instead, model encapsulates (wraps) this request so you call it like: -@codestart -Contact.findAll({type: 'old'}, successCallback); -@codeend + Contact.findAll({type: 'old'}, successCallback); And instead of raw data, findAll returns contact instances that let you do things like: @@ -62,7 +56,8 @@ The Grid demo shows using two different models with the same widget. ## How to Encapsulate -Think of models as a contract for creating, reading, updating, and deleting data. +Think of models as a contract for creating, reading, updating, and deleting data. + By filling out a model, you can pass that model to a widget and the widget will use the model as a proxy for your data. @@ -70,7 +65,7 @@ The following chart shows the methods most models provide: - +
                                            Create
                                            Contact.create(attrs, success, error
                                            Create
                                            Contact.create(attrs, success, error)
                                            Read
                                            Contact.findAll(params,success,error)
                                            @@ -85,32 +80,88 @@ Contact.findOne(params, success, error)
                                            By filling out these methods, you get the benefits of encapsulation, -AND all the other magic Model provides. Lets see how we might fill out the -Contact.findAll function: +AND all the other magic Model provides. + +There are two ways to fill out these methods: + + - providing templated service urls + - implementing the method + +## Using Templated Service URLS + +If your server is REST-ish, you can simply provide +urls to your services. + +The following shows filling out a +Task model's urls. For each method it shows calling the function, +how the service request is made, and what the server's response +should look like: + + $.Model("Task",{ + + // Task.findAll({foo: "bar"}) + // -> GET /tasks.json?foo=bar + // <- [{id: 1, name: "foo"},{ ... }] + findAll : "/tasks.json", + + // Task.findOne({id: 5}) + // -> GET /tasks/5.json + findOne : "/tasks/{id}.json", + + // new Task({name: 'justin'}).save() + // -> POST /tasks.json id=5 + // <- {id : 5} + create : "/tasks.json", + + // task.update({name: 'justin'}) + // -> PUT /tasks/5.json name=justin + // <- {} + update : "/tasks/{id}.json", + + // task.destroy() + // -> DESTROY /tasks/5.json + // <- {} + destroy : "/tasks/{id}.json" + },{}) + +You can change the HTTP request type by putting a GET, POST, DELETE, PUT like: + + $.Model("Todo",{ + destroy: "POST /task/delete/{id}.json + },{}) + +Note: Even if your server doesn't respond with service data +in the same way, it's likely that $.Model will be able to figure it out. If not, +you can probably +overwrite [jQuery.Model.static.models models] +or [jQuery.Model.static.model model]. If that doesn't work, you can +always implement it yourself. + +## Implement Service Methods + +If providing a url doesn't work for you, you +might need to fill out the +service method yourself. Before doing this, it's good +to have an understanding of jQuery's Ajax converters and +deferreds. + + + + + +Lets see how we might fill out the +Contact.findAll function to work with JSONP: @codestart -$.Model.extend('Contact', +$.Model('Contact', { findAll : function(params, success, error){ // do the ajax request - $.get('/contacts.json', + return $.get('/contacts.jsonp', params, - function( json ){ - - // on success, create new Contact - // instances for each contact - var wrapped = []; - - for(var i =0; i< json.length;i++){ - wrapped.push( new Contact(json[i] ) ); - } - - //call success with the contacts - success( wrapped ); - - }, - 'json'); + success, + 'jsonp contact.models'); } }, { @@ -119,31 +170,7 @@ $.Model.extend('Contact', }); @codeend -Well, that would be annoying to write out every time. Fortunately, models have -the wrapMany method which will make it easier: -@codestart -findAll : function(params, success, error){ - $.get('/contacts.json', - params, - function( json ){ - success(Contact.wrapMany(json)); - }, - 'json'); - } -@codeend - -Model is based off JavaScriptMVC's jQuery.Class. It's callback allows us to pipe -wrapMany into the success handler and make our code even shorter: - -@codestart -findAll : function(params, success, error){ - $.get('/contacts.json', - params, - this.callback(['wrapMany', success]), - 'json') - } -@codeend If we wanted to make a list of contacts, we could do it like: @@ -156,7 +183,3 @@ Contact.findAll({},function(contacts){ $('#contacts').html( html.join('') ); }); @codeend - - - */ -//s \ No newline at end of file diff --git a/model/pages/events.js b/model/pages/events.js deleted file mode 100644 index acfc5d71..00000000 --- a/model/pages/events.js +++ /dev/null @@ -1,78 +0,0 @@ -/* -@page jquery.model.events Events -@parent jQuery.Model - -Models produce events that you can listen to. This is -useful when there are multiple representations of the same instance on the page. -If one representation is updated, the other representation -should be updated. - -Events also provide a more traditional MVC approach. View-Controllers -bind to a specific property. If that property changes, the -View-Controller updates itself. - -Model provides two ways to listen for events on model instances: - -## Way 1: Bind - -You can [jQuery.Model.prototype.bind bind] to attribute changes in a model instance -just like you would with events in jQuery. - -The following listens for contact birthday changes. - -@codestart -contact.bind("birthday", function(ev, birthday){ - // do something -}) -@codeend - -The 'birthday' event is triggered whenever an attribute is -successfully changed: - -@codestart -contact.attr('birthday', "10-20-1982"); -@codeend - -Bind is the prefered approach if you're favoring a more -traditional MVC architecture. However, this can sometimes -be more complex than the subscribe method because of -maintaining additional event handlers. - -## Way 2: Subscribe - -If OpenAjax.hub is available, Models also publish events when -an instance is created, updated, or destroyed. - -You can subscribe to these events with OpenAjax.hub like: - -@codestart -OpenAjax.hub.subscribe( - "contact.updated", - function(called, contact){ - //do something ... -}) -@codeend - -Typically, you'll subscribe with the -jquery/controller/subscribe plugin like: - -@codestart -$.Controller.extend("Subscriber",{ - - ... - - "todo.destroyed subscribe" : function(called, todo){ - - //find the contact in this widget: - var el = todo.elements(this.element) - - //remove element - el.remove(); - }, - - ... -}) -@codeend - - - */ \ No newline at end of file diff --git a/model/pages/typeconversion.js b/model/pages/typeconversion.md similarity index 61% rename from model/pages/typeconversion.js rename to model/pages/typeconversion.md index 2aea6aa1..b32ac105 100644 --- a/model/pages/typeconversion.js +++ b/model/pages/typeconversion.md @@ -1,9 +1,6 @@ -/** @page jquery.model.typeconversion Type Conversion @parent jQuery.Model -# Type Conversion - You often want to convert from what the model sends you to a form more useful to JavaScript. For example, contacts might be returned from the server with dates that look like: @@ -14,7 +11,7 @@ to new Date(1982,10,20). We can do this in two ways: The [jQuery.Model.prototype.attrs attrs] and [jQuery.Model.prototype.attr attr] function look for -a setATTRNAME function to handle setting the +a setATTRNAME function to handle setting the date property. By providing a function that takes the raw data and returns @@ -36,7 +33,7 @@ an attribute and provide a function to convert that type. The following sets the birthday attribute to "date" and provides a date conversion function: @codestart -$.Model.extend("Contact", +$.Model("Contact", { attributes : { birthday : 'date' @@ -61,4 +58,31 @@ $.Model.extend("Contact", @codeend @demo jquery/model/demo-convert.html - */ \ No newline at end of file + +# Serialization + +Serialization occurs before the model is saved. This allows you to prepare your model's attributes before they're sent to the server. + +By default every attribute will be passed through the 'default' serialization method that will return the value if the property holds a primitive value (string, number, ...), or it will call the "serialize" method if the property holds an object with the "serialize" method set. + +You can set the serialization methods similar to the convert methods: + +@codestart +$.Model("Contact", +{ + attributes : { + birthday : 'date' + }, + serialize : { + date : function( val, type ){ + return val.getYear() + "-" + (val.getMonth() + 1) + "-" + val.getDate(); + } + }, + findAll : function( ... ){ ... } +}, +{ + // No prototype properties necessary +}) +@codeend + +This code will format the 'birthday' attribute as '2011-11-24' before it will be sent to the server. \ No newline at end of file diff --git a/model/qunit.html b/model/qunit.html index abca295f..218735de 100644 --- a/model/qunit.html +++ b/model/qunit.html @@ -12,6 +12,6 @@

                                            associations list - + \ No newline at end of file diff --git a/model/service/json_rest/json_rest.js b/model/service/json_rest/json_rest.js index 6813d7e6..433c3412 100644 --- a/model/service/json_rest/json_rest.js +++ b/model/service/json_rest/json_rest.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/model/service').then(function(){ +steal('jquery/model/service').then(function(){ $.Model.service.jsonRest = $.Model.service({ url : "", @@ -22,7 +22,7 @@ $.Model.service.jsonRest = $.Model.service({ type: 'get', dataType: 'json', data: params, - success: this.callback(['wrapMany',success]), + success: this.proxy(['wrapMany',success]), error: error, fixture: true }) diff --git a/model/service/service.js b/model/service/service.js index 6efbf197..2f4ab662 100644 --- a/model/service/service.js +++ b/model/service/service.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/model').then(function(){ +steal('jquery/model').then(function(){ var convert = function(method, func){ return typeof method == 'function' ? function(){ diff --git a/model/service/twitter/twitter.js b/model/service/twitter/twitter.js index 8974e6fe..8aaa6740 100644 --- a/model/service/twitter/twitter.js +++ b/model/service/twitter/twitter.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/model/service').then(function(){ +steal('jquery/model/service').then(function(){ $.Model.service.twitter = $.Model.service({ url : "http://api.twitter.com/1/", diff --git a/model/service/yql/yql.js b/model/service/yql/yql.js index 895dfc19..d10a2ea7 100644 --- a/model/service/yql/yql.js +++ b/model/service/yql/yql.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/model/service').then(function(){ +steal('jquery/model/service').then(function(){ $.Model.service.yql = $.Model.service({ select : "*", diff --git a/model/store/qunit.html b/model/store/qunit.html new file mode 100644 index 00000000..d8a863b6 --- /dev/null +++ b/model/store/qunit.html @@ -0,0 +1,17 @@ + + + + store QUnit Test + + + + +

                                            store Test Suite

                                            +

                                            +
                                            +

                                            +
                                            +
                                              +
                                              + + \ No newline at end of file diff --git a/model/store/store.html b/model/store/store.html new file mode 100644 index 00000000..12b928e0 --- /dev/null +++ b/model/store/store.html @@ -0,0 +1,188 @@ + + + + store + + + +

                                              store Demo

                                              +
                                                + +
                                              +
                                                + +
                                              + + + + + + \ No newline at end of file diff --git a/model/store/store.js b/model/store/store.js new file mode 100644 index 00000000..ab73370a --- /dev/null +++ b/model/store/store.js @@ -0,0 +1,546 @@ +steal('jquery/model/list','jquery/lang/object', function($){ + +var same = $.Object.same, + trigger = function(obj, event, args){ + $.event.trigger(event, args, obj, true) + }, + $method = function( name ) { + return function( eventType, handler ) { + return $.fn[name].apply($([this]), arguments); + } + }, + bind = $method('bind'), + unbind = $method('unbind'); + + +$.Class('jQuery.Model.Store', +{ + id: "id", + bind: bind, + unbind: unbind, + compare : {}, + + init : function(){ + if(this.fullName === 'jQuery.Model.Store'){ + return; + } + + this.sets = []; + this.data = {}; + + // listen on create and add ... listen on destroy and remove + this.namespace.bind('destroyed', this.proxy('destroyed')); + this.namespace.bind('updated', this.proxy('updated')); + this.namespace.bind("created", this.proxy('created')); + }, + + /** + * Internal compare method. + * + * @param {Object} prop + * @param {Object} itemData + * @param {Object} paramData + */ + _compare : function(prop, itemData, paramData){ + return same(itemData, paramData, this.compare[prop]); + }, + + /** + * Creates an item in the sets. Triggered from a model + * event indicating an item was created. + * + * @param {Object} event + * @param {Object} item + */ + created: function(ev,item){ + this.add([item]); + }, + + /** + * Updates an item in the sets. Triggered from a model + * event indicating an item was updated. + * + * @param {Object} event + * @param {Object} item + */ + updated: function(ev, item){ + // go through lists and remove this guy if he is in the list and should not be ... + var sets = this.sets.slice(0), + report = ["Store - updating "]; + + for(var i =0, length = this.sets.length; i < length; i++){ + var set = sets[i], + inSet = this.filter(item, set.params) !== false, + inList = set.list.get(item)[0]; + + if(inSet && !inList){ + report.push("adding to", set.params, "; "); + set.list.push(item) + } else if(!inSet && inList) { + report.push("removing from", set.params, "; "); + set.list.remove(item.id) + } + } + }, + + /** + * Destroy triggered by model event. + * Calls remove function to remove item from lists. + * + * @param {Object} event + * @param {Object} id + */ + destroyed : function(ev,id){ + this.remove(id); + }, + + /** + * @function remove + * + * Removes an item from the sets. + * + * @param {Object} id + */ + remove:function(id){ + var idProp = this.id; + + if(id[idProp] !== undefined){ + id = id[idProp]; + } + + var item = this.data[id]; + + if(!item){ + return; + } + + delete this.data[id]; + }, + + /** + * @function removeSet + * + * Removes a set given a parms object and + * removes each one of the items from the data. + * + * @param {Object} params + */ + removeSet: function(params){ + var matchIdx; + + $.each(this.sets, this.proxy(function(i,set){ + if($.Object.same(params, set.params, this.compare)){ + set.list.each(this.proxy(function(i,item){ + delete this.data[item[this.id]]; + })); + matchIdx = i; + return false; + } + })); + + matchIdx != undefined && this.sets.splice(matchIdx, 1); + }, + + /** + * @function add + * + * Adds items to the set(s) given the matching params. + * + * @param {Object} items + * @param {Object} params + */ + add : function(items, params){ + // need to check the filter rules, if we can even add this ... + + var len = items.length, + i=0, + item, + idProp = this.id, + id, + added = []; + + for(; i< len; i++){ + item = items[i]; + id = item[idProp]; + + if( this.data[id] ){ + + // if there is something there ... take care of it .. + this.update(this.data[id], item); + + // if the item already exists from say a 'findOne' call + // the item will already exist in 'data' but not the 'list' + added.push(item) + } else { + added.push(this.data[id] = item); + } + } + + // go through sets and add to them ... + // slice so that if in callback, the number of sets increases, you are ok + var sets = this.sets.slice(0), + report = ["Store - adding "]; + + for(var i=0, iLength = sets.length; i < iLength; i++){ + var set = sets[i], + itemsForSet = []; + + for(var j =0, jLength = added.length; j< jLength; j++){ + item = added[j] + if( this.filter(item, set.params) !== false) { + itemsForSet.push(item) + } + } + + if(itemsForSet.length) { + report.push(itemsForSet.length,"to", set.params, "; "); + set.list.push(itemsForSet); + } + } + }, + + /** + * @function update + * + * Updates the properties of currentItem + * + * @param {Object} currentItem + * @param {Object} newItem + */ + update : function(currentItem, newItem){ + currentItem.attrs(newItem.serialize()); + }, + + /** + * @function sort + * + * Returns if a set contains the parameters. + * + * @param {Object} params + **/ + has : function(params){ + // check if it has an evil param ... + return $.Object.subsets(params, this.sets).length + }, + + /** + * @function filter + * + * Called with the item and the current params. + * Should return __false__ if the item should be filtered out of the result. + * + * By default this goes through each param in params and see if it matches the + * same property in item (if item has the property defined). + * + * @param {Object} item + * @param {Object} params + */ + filter : function(item, params){ + // go through each param in params + var param, paramValue + for ( var param in params ) { + i=0; + paramValue = params[param]; + + // in fixtures we ignore null, I don't want to now + if ( paramValue !== undefined && item[param] !== undefined + && !this._compare(param, item[param], paramValue) ) { + return false; + } + } + }, + + /** + * @function sort + * + * Sorts the object in place. By default uses an order + * property in the param of the class. + * + * @codestart + * var models = $.Model.Store.sort(myModelListInstance); + * @codeend + * + * @param {Object} items + * @param {Object} params + */ + sort : function(items, params){ + $.each((params.order || []).slice(0).reverse(), function( i, name ) { + var split = name.split(" "); + items = items.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; + } + } + }); + }); + return items + }, + + /** + * @function pagination + * + * Paginates the item in place. By default uses an order + * property in the param of the class. + * + * @codestart + * var models = $.Model.Store.pagination(myModelListInstance); + * @codeend + * + * @param {Object} items + * @param {Object} params + */ + pagination : function(items, params){ + var offset = parseInt(params.offset, 10) || 0, + limit = parseInt(params.limit, 10) || (items.length - offset); + + return items.slice(offset, offset + limit); + }, + + /** + * @function get + * + * Retrieves an item(s) given an id or array of ids from the global data + * set of the model store. If the item is not returned yet, it will return + * the deffered. + * + * @codestart + * var model = $.Model.Store.get(222); + * @codeend + * + * @codestart + * var models = $.Model.Store.get([222, 223, 224]); + * @codeend + * + * @param {Object} id int or array of ints + */ + get : function(id){ + if($.isArray(id)) { + var returnArr = []; + + $.each(id, this.proxy(function(i,d){ + var m = this.data[d]; + m && returnArr.push(m); + })); + + return returnArr; + } else { + return this.data[id]; + } + }, + + /** + * @function findOne + * + * FindOne attempts to retrieve an individual model + * from the sets of currently fetched data. If the model + * was not previously fetched, it will then execute a request on the + * static 'findOne' method of the model. It returns + * the deffered object. + * + * @codestart + * $.Model.Store.findOne(222).done(success); + * @codeend + * + * + * You can listen for 'findOne' to be triggered by + * binding to the 'findOne' event on the class. + * + * @codestart + * $.Model.Store.bind('findOne', function(id){ ... }); + * @codeend + * + * + * @param {Object} id of item + * @param {Function} success handler + * @param {Function} error handler + **/ + findOne : function(id, success, error){ + var data = this.data[id], + def; + + if(data){ + if(data.isRejected){ + return data; + } else { + def = $.Deferred(); + def.resolve(data); + } + } else { + this.data[id] = def = this.namespace.findOne({ id: id }); + + def.done(this.proxy(function(item){ + this.data[id] = item; + })); + } + + def.done(success); + trigger(this, 'findOne', id); + + return def; + }, + + /** + * @function findAll + * + * FindAll attempts to retrieve a list of model(s) + * from the sets of currently fetched data. If the model(s) + * were not previously fetched, it will then execute a request on the + * static 'findAll' method of the model. It returns + * the deffered object. + * + * @codestart + * $.Model.Store.findAll({ parentId: 2222 }).done(success); + * @codeend + * + * + * You can listen for 'findAll' to be triggered by + * binding to the 'findAll' event on the class. + * + * @codestart + * $.Model.Store.bind('findAll', function(params){ ... }); + * @codeend + * + * + * @param {Object} params + * @param {Boolean} register registers this list as owning some content, but does not + * @param {Boolean} ready + **/ + findAll : function(params, register, ready){ + // find the first set that is the same + // or is a subset with a def + var parentLoadedSet, + self = this, + list, + cb = function(){ + ready(list) + }; + + if(typeof register === 'function' ){ + ready = register; + register = false; + } + ready = ready || function(){}; + + for(var i =0, length = this.sets.length; i < length; i++){ + var set = this.sets[i]; + if( $.Object.subset(params, set.params, this.compare) ){ + parentLoadedSet = set; + + if( $.Object.same(set.params, params, this.compare) ){ + // what if it's not loaded + if(!set.def){ + var def = this.namespace.findAll(params); + set.def = def; + def.done(function(items){ + list = items; + self.add(items, params) + cb && cb(); + }) + } else { + list = set.list; + if(set.def.isResolved()){ + setTimeout(cb, 1); + } else { + set.def.done(cb); + } + } + + return set.list; + } + } + } + + // create a list, a set and add the set to our list of sets + list = new this.namespace.List(); + var sameSet = { + params: $.extend({},params), + list: list + }; + + this.sets.push(sameSet); + + // we have loaded or are loading what we need + if( parentLoadedSet ) { + // find the first set with a deferred + if( !parentLoadedSet.def ) { + + // we need to load this ... + + } else if( parentLoadedSet.def.isResolved() ){ + // add right away + var items = self.findAllCached(params); + list.push(items); + setTimeout(cb, 1);; + } else { + // this will be filled when add is called ... + parentLoadedSet.def.done(function(){ + var items = self.findAllCached(params); + list.push(items); + cb && cb(); + }) + } + + } else { + + if( !register ) { + // we need to load it + var def = sameSet.def = this.namespace.findAll(params); + + def.done(function(items){ + self.add(items, params); + cb && cb(); + }); + } + } + + trigger(this, 'findAll', params); + + return list; + }, + + /** + * @function findAllCached + * + * FindAll attempts to retrieve a list of model(s) + * only from the cache. + * + * @param {Object} params + **/ + findAllCached : function(params){ + // remove anything not filtering .... + // - sorting, grouping, limit, and offset + + var list = [], + data = this.data, + item; + + for(var id in data){ + item = data[id]; + if( this.filter(item, params) !== false) { + list.push(item) + } + } + + // do sorting / grouping + list = this.pagination(this.sort(list, params), params); + + // take limit and offset ... + return list; + } +},{ }); + +}); \ No newline at end of file diff --git a/model/store/store_test.js b/model/store/store_test.js new file mode 100644 index 00000000..4cccdefd --- /dev/null +++ b/model/store/store_test.js @@ -0,0 +1,201 @@ +steal('funcunit/qunit','./store.js', + 'jquery/model', + 'jquery/model/list', + 'jquery/dom/fixture',function(){ + +module("store", { + setup : function(){ + + } +}); + + + +/* +test("smart findAll", function(){ + + $.Model('Item'); + + + + ok( this.store.has({parentId: 7}) , "store has everything with parentId 7"); + + + var items = this.store.findAll({parentId: 7}); + equals( items.length, 2 , "got the wrong number of items"); + $.each(items, function(i, item){ + if(item.parentId != 7){ + ok(false,"got a bad parentId") + } + }) +})*/ + +test("store findAll", 5, function(){ + + $.fixture.make('item',40, function(i){ + return { + name: "Name "+i, + parentId: i%4+1 + } + }) + + $.Model('Item',{},{}); + $.Model.List('Item.List'); + $.Model.Store('Item.Store'); + + + var list = Item.Store.findAll({}); + stop(); + list.bind("add", function(ev, items){ + console.log("here ...") + start(); + + ok(items, "add called with items"); + + equal( items.length,40, "add called with items"); + + var list2 = Item.Store.findAll({parentId: 2}); + + equal( list2.length , 10, "immediately loaded"); + + + list.unbind('add',arguments.callee); + + list.bind('add', function(){ + ok(true, "big list added to") + }) + + list2.bind('add', function(){ + ok(true, "small list added too") + }) + + Item.Store.add([new Item({id: 100, parentId: 2})]); + + }) + +}) + +test("Store Compare", function(){ + + + $.fixture.make('item',40, function(i){ + return { + name: "Name "+i, + parentId: i%4+1 + } + }) + + $.Model('Item',{},{}); + $.Model.List('Item.List'); + $.Model.Store('Item.Store',{ + compare : { + count : null + } + },{}); + + + var list = Item.Store.findAll({count: 2}); + stop(); + list.bind("add", function(ev, items){ + ok(items.length); + ok(list.length) + start() + var list2 = Item.Store.findAll({count: 500}); + equals(list2.length, list.length, "lists have the same items"); + ok(list2 === list,"lists are equal") + }) +}) + +test("Store Remove", function(){ + $.fixture.make('item',40, function(i){ + return { + name: "Name "+i, + parentId: i%4+1 + } + }) + + $.Model('Item',{},{}); + $.Model.List('Item.List'); + $.Model.Store('Item.Store',{ + compare : { + count : null + } + },{}); + + var list = Item.Store.findAll({parentId: 1}), + len = 0, + first; + stop(); + list.bind("add", function(ev, items){ + ok(items.length, "there should be items"); + len = items.length; + first = items[0] + first.destroy(); + }) + list.bind("remove", function(ev, items){ + ok(items[0] === first, "removed first item"); + equals(list.length, len - 1, "length adjusted") + var list2 = Item.Store.findAll({parentId: 1}); + ok(list2.get(first.id)[0] === undefined, "Model Store remove callback"); + start(); + }) +}); + +test("Store Update", function(){ + $.fixture.make('item',40, function(i){ + return { + name: "Name "+i, + parentId: i%4+1 + } + }) + + $.Model('Item',{},{}); + $.Model.List('Item.List'); + $.Model.Store('Item.Store',{ + compare : { + count : null + } + },{}); + + var list1 = Item.Store.findAll({parentId: 1}), + list2 = Item.Store.findAll({parentId: 2}), + len = 0, + first; + + stop(); + var def1 = $.Deferred(), + def2 = $.Deferred(), + first, + updating; + + list1.bind("add", function(ev, items){ + console.log("1 added") + def1.resolve(true) + first = items[0] + }); + list1.bind("remove", function(ev, items){ + console.log("1 removed") + equals(items[0].id, first.id, "first removed") + }) + list2.bind("add", function(ev, items){ + console.log("2 added") + if(!updating){ + def2.resolve(true); + } else { + equals(items[0].id, first.id, "item added to second list") + start(); + } + }); + + $.when(def1, def2).then(function(){ + console.log('both ready') + updating = true; + first.updated({parentId: 2}) + }); + +}); + + + +}); + diff --git a/model/test/4.json b/model/test/4.json new file mode 100644 index 00000000..d05cb425 --- /dev/null +++ b/model/test/4.json @@ -0,0 +1,4 @@ +{ + "id": 4, + "name" : "adler" +} \ No newline at end of file diff --git a/model/test/create.json b/model/test/create.json new file mode 100644 index 00000000..734e4916 --- /dev/null +++ b/model/test/create.json @@ -0,0 +1,4 @@ +{ + "id": 4, + "name" : "Highland" +} \ No newline at end of file diff --git a/model/test/people.json b/model/test/people.json new file mode 100644 index 00000000..b5f86a2e --- /dev/null +++ b/model/test/people.json @@ -0,0 +1,4 @@ +[{ + "id" : 5, + "name" : "Justin" +}] diff --git a/model/test/person.json b/model/test/person.json new file mode 100644 index 00000000..ea58f4ea --- /dev/null +++ b/model/test/person.json @@ -0,0 +1,4 @@ +{ + "id" : 5, + "name" : "Justin" +} diff --git a/model/test/qunit/associations_test.js b/model/test/qunit/associations_test.js new file mode 100644 index 00000000..7e8a8559 --- /dev/null +++ b/model/test/qunit/associations_test.js @@ -0,0 +1,121 @@ +module("jquery/model/associations",{ + setup: function() { + + $.Model("MyTest.Person", { + serialize: function() { + return "My name is " + this.name; + } + }); + $.Model("MyTest.Loan"); + $.Model("MyTest.Issue"); + + $.Model("MyTest.Customer", + { + attributes : { + person : "MyTest.Person.model", + loans : "MyTest.Loan.models", + issues : "MyTest.Issue.models" + }, + + update : function(id, attrs, success, error){ + return $.ajax({ + url : "/people/"+id, + data : attrs, + type : 'post', + dataType : "json", + fixture: function(){ + return [{ + loansAttr: attrs.loans, + personAttr: attrs.person + }] + }, + success : success + }) + } + }, + {}); + } +}); + +test("associations work", function(){ + var c = new MyTest.Customer({ + id: 5, + person : { + id: 1, + name: "Justin" + }, + issues : [], + loans : [ + { + amount : 1000, + id: 2 + }, + { + amount : 19999, + id: 3 + } + ] + }) + equals(c.person.name, "Justin", "association present"); + equals(c.person.Class, MyTest.Person, "belongs to association typed"); + + equals(c.issues.length, 0); + + equals(c.loans.length, 2); + + equals(c.loans[0].Class, MyTest.Loan); +}); + +test("Model association serialize on save", function(){ + var c = new MyTest.Customer({ + id: 5, + person : { + id: 1, + name: "thecountofzero" + }, + issues : [], + loans : [] + }), + cSave = c.save(); + + stop(); + cSave.then(function(customer){ + start() + equals(customer.personAttr, "My name is thecountofzero", "serialization works"); + + }); + +}); + +test("Model.List association serialize on save", function(){ + var c = new MyTest.Customer({ + id: 5, + person : { + id: 1, + name: "thecountofzero" + }, + issues : [], + loans : [ + { + amount : 1000, + id: 2 + }, + { + amount : 19999, + id: 3 + } + ] + }), + cSave = c.save(); + + stop(); + cSave.then(function(customer){ + start() + ok(customer.loansAttr._namespace === undefined, "_namespace does not exist"); + ok(customer.loansAttr._data === undefined, "_data does not exist"); + ok(customer.loansAttr._use_call === undefined, "_use_call does not exist"); + ok(customer.loansAttr._changed === undefined, "_changed does not exist"); + + }); + +}); \ No newline at end of file diff --git a/model/test/qunit/findAll.json b/model/test/qunit/findAll.json new file mode 100644 index 00000000..8dc242fa --- /dev/null +++ b/model/test/qunit/findAll.json @@ -0,0 +1,4 @@ +[{ + "id" : 1, + "name" : "Thing 1" +}] diff --git a/model/test/qunit/model_test.js b/model/test/qunit/model_test.js index 782d0e6f..0719d4e3 100644 --- a/model/test/qunit/model_test.js +++ b/model/test/qunit/model_test.js @@ -1,7 +1,7 @@ module("jquery/model", { setup: function() { var ids = 0; - $.Model.extend("Person",{ + $.Model("Person",{ findAll: function( params, success, error ) { success("findAll"); }, @@ -46,7 +46,135 @@ test("CRUD", function(){ equals(inst, person, "we get back the same instance"); equals(person.zoo, "monkeys", "updated to monkeys zoo! This tests that you callback with the attrs") }) -}) +}); + +test("findAll deferred", function(){ + $.Model.extend("Person",{ + findAll : function(params, success, error){ + return $.ajax({ + url : "/people", + data : params, + dataType : "json person.models", + fixture: "//jquery/model/test/people.json" + }) + } + },{}); + stop(); + var people = Person.findAll({}); + people.then(function(people){ + equals(people.length, 1, "we got a person back"); + equals(people[0].name, "Justin", "Got a name back"); + equals(people[0].constructor.shortName, "Person", "got a class back"); + start(); + }) +}); + +test("findOne deferred", function(){ + $.Model.extend("Person",{ + findOne : function(params, success, error){ + return $.ajax({ + url : "/people/5", + data : params, + dataType : "json person.model", + fixture: "//jquery/model/test/person.json" + }) + } + },{}); + stop(); + var person = Person.findOne({}); + person.then(function(person){ + equals(person.name, "Justin", "Got a name back"); + equals(person.constructor.shortName, "Person", "got a class back"); + start(); + }) +}); + +test("save deferred", function(){ + + $.Model("Person",{ + create : function(attrs, success, error){ + return $.ajax({ + url : "/people", + data : attrs, + type : 'post', + dataType : "json", + fixture: function(){ + return [{id: 5}] + }, + success : success + }) + } + },{}); + + var person = new Person({name: "Justin"}), + personD = person.save(); + + stop(); + personD.then(function(person){ + start() + equals(person.id, 5, "we got an id") + + }); + +}); + +test("update deferred", function(){ + + $.Model("Person",{ + update : function(id, attrs, success, error){ + return $.ajax({ + url : "/people/"+id, + data : attrs, + type : 'post', + dataType : "json", + fixture: function(){ + return [{thing: "er"}] + }, + success : success + }) + } + },{}); + + var person = new Person({name: "Justin", id:5}), + personD = person.save(); + + stop(); + personD.then(function(person){ + start() + equals(person.thing, "er", "we got updated") + + }); + +}); + +test("destroy deferred", function(){ + + $.Model("Person",{ + destroy : function(id, success, error){ + return $.ajax({ + url : "/people/"+id, + type : 'post', + dataType : "json", + fixture: function(){ + return [{thing: "er"}] + }, + success : success + }) + } + },{}); + + var person = new Person({name: "Justin", id:5}), + personD = person.destroy(); + + stop(); + personD.then(function(person){ + start() + equals(person.thing, "er", "we got destroyed") + + }); +}); + + test("hookup and model", function(){ var div = $("
                                              ") var p = new Person({foo: "bar2", id: 5}); @@ -55,24 +183,66 @@ test("hookup and model", function(){ ok(div.hasClass("person_5"), "has person_5"); equals(p, div.model(),"gets model" ) }) -test("guess type", function(){ - equals("array", $.Model.guessType( [] ) ); - equals("date", $.Model.guessType( new Date() ) ); - equals("boolean", $.Model.guessType( true ) ); - equals("number", $.Model.guessType( "1" ) ); - equals("string", $.Model.guessType( "a" ) ); - - equals("string", $.Model.guessType( "1e234234324234" ) ); - equals("string", $.Model.guessType( "-1e234234324234" ) ); +// test that models returns an array of unique instances +test("unique models", function(){ + var div1 = $("
                                              ") + var div2 = $("
                                              ") + var div3 = $("
                                              ") + var p = new Person({foo: "bar2", id: 5}); + var p2 = new Person({foo: "bar3", id: 4}); + p.hookup( div1[0] ); + p.hookup( div2[0] ); + p2.hookup( div3[0] ); + var models = div1.add(div2).add(div3).models(); + equals(p, models[0], "gets models" ) + equals(p2, models[1], "gets models" ) + equals(2, models.length, "gets models" ) }) -test("wrapMany", function(){ - var people = Person.wrapMany([ + +test("models", function(){ + var people = Person.models([ {id: 1, name: "Justin"} ]) equals(people[0].prettyName(),"Mr. Justin","wraps wrapping works") }); + + +test("async setters", function(){ + + /* + $.Model("Test.AsyncModel",{ + setName : function(newVal, success, error){ + + + setTimeout(function(){ + success(newVal) + }, 100) + } + }); + + var model = new Test.AsyncModel({ + name : "justin" + }); + equals(model.name, "justin","property set right away") + + //makes model think it is no longer new + model.id = 1; + + var count = 0; + + model.bind('name', function(ev, newName){ + equals(newName, "Brian",'new name'); + equals(++count, 1, "called once"); + ok(new Date() - now > 0, "time passed") + start(); + }) + var now = new Date(); + model.attr('name',"Brian"); + stop();*/ +}) + test("binding", 2,function(){ var inst = new Person({foo: "bar"}); @@ -103,4 +273,319 @@ test("error binding", 1, function(){ }) +test("auto methods",function(){ + //turn off fixtures + $.fixture.on = false; + var School = $.Model.extend("Jquery.Model.Models.School",{ + findAll : steal.root.join("jquery/model/test")+"/{type}.json", + findOne : steal.root.join("jquery/model/test")+"/{id}.json", + create : steal.root.join("jquery/model/test")+"/create.json", + update : "POST "+steal.root.join("jquery/model/test")+"/update{id}.json" + },{}) + stop(); + School.findAll({type:"schools"}, function(schools){ + ok(schools,"findAll Got some data back"); + equals(schools[0].constructor.shortName,"School","there are schools") + + School.findOne({id : "4"}, function(school){ + ok(school,"findOne Got some data back"); + equals(school.constructor.shortName,"School","a single school"); + + + new School({name: "Highland"}).save(function(){ + equals(this.name,"Highland","create gets the right name") + this.update({name: "LHS"}, function(){ + start(); + equals(this.name,"LHS","create gets the right name") + + $.fixture.on = true; + }) + }) + + }) + + }) +}) + +test("isNew", function(){ + var p = new Person(); + ok(p.isNew(), "nothing provided is new"); + var p2 = new Person({id: null}) + ok(p2.isNew(), "null id is new"); + var p3 = new Person({id: 0}) + ok(!p3.isNew(), "0 is not new"); +}); +test("findAll string", function(){ + $.fixture.on = false; + $.Model("Test.Thing",{ + findAll : steal.root.join("jquery/model/test/qunit/findAll.json")+'' + },{}); + stop(); + Test.Thing.findAll({},function(things){ + equals(things.length, 1, "got an array"); + equals(things[0].id, 1, "an array of things"); + start(); + $.fixture.on = true; + }) +}) +test("Empty uses fixtures", function(){ + $.Model("Test.Things"); + $.fixture.make("thing", 10, function(i){ + return { + id: i + } + }); + stop(); + Test.Thing.findAll({}, function(things){ + start(); + equals(things.length, 10,"got 10 things") + }) +}); + +test("Model events" , function(){ + var order = 0; + $.Model("Test.Event",{ + create : function(attrs, success){ + success({id: 1}) + }, + update : function(id, attrs, success){ + success(attrs) + }, + destroy : function(id, success){ + success() + } + },{}); + + stop(); + $([Test.Event]).bind('created',function(ev, passedItem){ + + ok(this === Test.Event, "got model") + ok(passedItem === item, "got instance") + equals(++order, 1, "order"); + passedItem.update({}); + + }).bind('updated', function(ev, passedItem){ + equals(++order, 2, "order"); + ok(this === Test.Event, "got model") + ok(passedItem === item, "got instance") + + passedItem.destroy({}); + + }).bind('destroyed', function(ev, passedItem){ + equals(++order, 3, "order"); + ok(this === Test.Event, "got model") + ok(passedItem === item, "got instance") + + start(); + + }) + + var item = new Test.Event(); + item.save(); + +}); + + +test("converters and serializes", function(){ + $.Model("Task1",{ + attributes: { + createdAt: "date" + }, + convert: { + date: function(d){ + var months = ["jan", "feb", "mar"] + return months[d.getMonth()] + } + }, + serialize: { + date: function(d){ + var months = {"jan":0, "feb":1, "mar":2} + return months[d] + } + } + },{}); + $.Model("Task2",{ + attributes: { + createdAt: "date" + }, + convert: { + date: function(d){ + var months = ["apr", "may", "jun"] + return months[d.getMonth()] + } + }, + serialize: { + date: function(d){ + var months = {"apr":0, "may":1, "jun":2} + return months[d] + } + } + },{}); + var d = new Date(); + d.setDate(1); + d.setMonth(1); + var task1=new Task1({ + createdAt: d, + name:"Task1" + }); + d.setMonth(2) + var task2=new Task2({ + createdAt: d, + name:"Task2" + }); + equals(task1.createdAt, "feb", "Task1 model convert"); + equals(task2.createdAt, "jun", "Task2 model convert"); + equals(task1.serialize().createdAt, 1, "Task1 model serialize"); + equals(task2.serialize().createdAt, 2, "Task2 model serialize"); + equals(task1.serialize().name, "Task1", "Task1 model default serialized"); + equals(task2.serialize().name, "Task2", "Task2 model default serialized"); +}); + +test("default converters", function(){ + var num = 1318541064012; + equals( $.Model.convert.date(num).getTime(), num, "converted to a date with a number" ); +}) + +test("removeAttr test", function(){ + var person = new Person({foo: "bar"}) + equals(person.foo, "bar", "property set"); + person.removeAttr('foo') + + equals(person.foo, undefined, "property removed"); + var attrs = person.attrs() + equals(attrs.foo, undefined, "attrs removed"); +}); + +test("identity should replace spaces with underscores", function(){ + $.Model("Task",{},{}); + t = new Task({ + id: "id with spaces" + }); + equals(t.identity(), "task_id_with_spaces") +}); + +test("save error args", function(){ + var Foo = $.Model('Testin.Models.Foo',{ + create : "/testinmodelsfoos.json" + },{ + + }) + var st = '{type: "unauthorized"}'; + + $.fixture("/testinmodelsfoos.json", function(){ + return [401,st] + }); + stop(); + var inst = new Foo({}).save(function(){ + ok(false, "success should not be called") + }, function(jQXHR){ + ok(true, "error called") + ok(jQXHR.getResponseHeader,"jQXHR object") + start() + }) + + + +}); + +test("hookup and elements", function(){ + $.Model('Escaper',{ + escapeIdentity : true + },{}); + + var ul = $('
                                              '), + li = ul.find('li'); + + var esc = new Escaper({id: " some crazy #/ %ing stuff"}); + + li.model(esc); + + var res = esc.elements(ul); + + equals(res.length,1, "1 item") + ok(res[0] === li[0], "items are equal") +}) + +test('aborting create update and destroy', function(){ + stop(); + var delay = $.fixture.delay; + $.fixture.delay = 1000; + + $.fixture("POST /abort", function(){ + ok(false, "we should not be calling the fixture"); + return {}; + }) + + $.Model('Abortion',{ + create : "POST /abort", + update : "POST /abort", + destroy: "POST /abort" + },{}); + + var deferred = new Abortion({name: "foo"}).save(function(){ + ok(false, "success create") + }, function(){ + ok(true, "create error called"); + + + deferred = new Abortion({name: "foo",id: 5}) + .save(function(){},function(){ + ok(true, "error called in update") + + deferred = new Abortion({name: "foo",id: 5}).destroy(function(){}, + function(){ + ok(true,"destroy error called") + $.fixture.delay = delay; + start(); + }) + + setTimeout(function(){ + deferred.abort(); + },10) + + }) + + setTimeout(function(){ + deferred.abort(); + },10) + }); + setTimeout(function(){ + deferred.abort(); + },10) + + +}); + +test("object definitions", function(){ + + $.Model('ObjectDef',{ + findAll : { + url : "/test/place" + }, + findOne : { + url : "/objectdef/{id}", + timeout : 1000 + }, + create : { + + }, + update : { + + }, + destroy : { + + } + },{}) + + $.fixture("GET /objectdef/{id}", function(original){ + equals(original.timeout,1000,"timeout set"); + return {yes: true} + }); + stop(); + ObjectDef.findOne({id: 5}, function(){ + start(); + }) +}) + + diff --git a/model/test/qunit/qunit.js b/model/test/qunit/qunit.js index 6284acce..e1117ddb 100644 --- a/model/test/qunit/qunit.js +++ b/model/test/qunit/qunit.js @@ -1,11 +1,9 @@ //we probably have to have this only describing where the tests are -steal - .plugins("jquery/model") //load your app - .plugins('funcunit/qunit') //load qunit - .then("model_test") - .plugins( - "jquery/model/associations/test/qunit", +steal("jquery/model","jquery/dom/fixture") //load your app + .then('funcunit/qunit') //load qunit + .then("./model_test.js")//,"./associations_test.js") + .then( "jquery/model/backup/qunit", - "jquery/model/list/test/qunit" - - ).then("//jquery/model/validations/qunit/validations_test") + "jquery/model/list/list_test.js" + ) + .then("jquery/model/validations/qunit/validations_test.js") diff --git a/model/test/schools.json b/model/test/schools.json new file mode 100644 index 00000000..1b596fa0 --- /dev/null +++ b/model/test/schools.json @@ -0,0 +1,4 @@ +[{ + "id": 1, + "name" : "adler" +}] diff --git a/model/test/update4.json b/model/test/update4.json new file mode 100644 index 00000000..9f705c98 --- /dev/null +++ b/model/test/update4.json @@ -0,0 +1,4 @@ +{ + "id": 4, + "name" : "LHS" +} \ No newline at end of file diff --git a/model/validations/demo.html b/model/validations/demo.html new file mode 100644 index 00000000..20cb9c0b --- /dev/null +++ b/model/validations/demo.html @@ -0,0 +1,105 @@ + + + + Model Validations Demo + + + +
                                              + +
                                              +
                                              + + + + \ No newline at end of file diff --git a/model/validations/qunit/validations_test.js b/model/validations/qunit/validations_test.js index 29487526..ec7447aa 100644 --- a/model/validations/qunit/validations_test.js +++ b/model/validations/qunit/validations_test.js @@ -1,4 +1,4 @@ -steal.plugins('funcunit/qunit','jquery/model/validations').then(function(){ +steal('funcunit/qunit','jquery/model/validations').then(function(){ module("jquery/model/validations",{ setup : function(){ @@ -22,23 +22,28 @@ test("models can validate, events, callbacks", 11,function(){ equals(errors.age[0], "it's a date type", "error message is right"); task.bind("error.age", function(ev, errs){ - ok(this === task, "we get task back"); + ok(this === task, "we get task back by binding"); ok(errs, "There are errors"); equals(errs.age.length, 1, "there is one error"); equals(errs.age[0], "it's a date type", "error message is right"); }) - task.attr("age","blah") + task.attr("age","blah"); + + task.unbind("error.age"); + + task.attr("age", "blaher", function(){}, function(errs){ - ok(this === task, "we get task back"); + ok(this === task, "we get task back in error handler"); ok(errs, "There are errors"); equals(errs.age.length, 1, "there is one error"); equals(errs.age[0], "it's a date type", "error message is right"); - }) + }); + }) test("validatesFormatOf", function(){ @@ -61,13 +66,42 @@ test("validatesFormatOf", function(){ }); test("validatesInclusionOf", function(){ + Person.validateInclusionOf("thing", ["yes", "no", "maybe"]); + + ok(!new Person({thing: "yes"}).errors(),"no errors"); + + var errors = new Person({thing: "foobar"}).errors(); + + ok(errors, "there are errors"); + equals(errors.thing.length,1,"one error on thing"); + + equals(errors.thing[0],"is not a valid option (perhaps out of range)","basic message"); + + Person.validateInclusionOf("otherThing", ["yes", "no", "maybe"],{message: "not a valid option"}); + var errors2 = new Person({thing: "yes", otherThing: "maybe not"}).errors(); -}) + equals(errors2.otherThing[0],"not a valid option", "can supply a custom message"); +}); test("validatesLengthOf", function(){ + Person.validateLengthOf("thing", 2, 5); + + ok(!new Person({thing: "yes"}).errors(),"no errors"); -}) + var errors = new Person({thing: "foobar"}).errors(); + + ok(errors, "there are errors"); + equals(errors.thing.length,1,"one error on thing"); + + equals(errors.thing[0],"is too long (max=5)","basic message"); + + Person.validateLengthOf("otherThing", 2, 5, {message: "invalid length"}); + + var errors2 = new Person({thing: "yes", otherThing: "too long"}).errors(); + + equals(errors2.otherThing[0],"invalid length", "can supply a custom message"); +}); test("validatesPresenceOf", function(){ $.Model.extend("Task",{ @@ -76,16 +110,65 @@ test("validatesPresenceOf", function(){ } },{}); + //test for undefined var task = new Task(), errors = task.errors(); ok(errors) ok(errors.dueDate) - equals(errors.dueDate[0], "can't be empty" , "right message") -}) + equals(errors.dueDate[0], "can't be empty" , "right message"); + + //test for null + task = new Task({dueDate: null}); + errors = task.errors(); + + ok(errors) + ok(errors.dueDate) + equals(errors.dueDate[0], "can't be empty" , "right message"); -test("validatesRangeOf", function(){ + //test for "" + task = new Task({dueDate: ""}); + errors = task.errors(); -}) + ok(errors) + ok(errors.dueDate) + equals(errors.dueDate[0], "can't be empty" , "right message"); + //Affirmative test + task = new Task({dueDate : "yes"}); + errors = task.errors();; + + ok(!errors, "no errors "+typeof errors); + + $.Model.extend("Task",{ + init : function(){ + this.validatePresenceOf("dueDate",{message : "You must have a dueDate"}) + } + },{}); + + task = new Task({dueDate : "yes"}); + errors = task.errors();; + + ok(!errors, "no errors "+typeof errors); }) + +test("validatesRangeOf", function(){ + Person.validateRangeOf("thing", 2, 5); + + ok(!new Person({thing: 4}).errors(),"no errors"); + + var errors = new Person({thing: 6}).errors(); + + ok(errors, "there are errors") + equals(errors.thing.length,1,"one error on thing"); + + equals(errors.thing[0],"is out of range [2,5]","basic message"); + + Person.validateRangeOf("otherThing", 2, 5, {message: "value out of range"}); + + var errors2 = new Person({thing: 4, otherThing: 6}).errors(); + + equals(errors2.otherThing[0],"value out of range", "can supply a custom message"); +}); + +}); diff --git a/model/validations/validations.html b/model/validations/validations.html index 59278d5f..0eeb932c 100644 --- a/model/validations/validations.html +++ b/model/validations/validations.html @@ -26,36 +26,40 @@

                                              Model Validations Demo

                                              - - - - - \ No newline at end of file diff --git a/model/validations/validations.js b/model/validations/validations.js index 024e24aa..92291a53 100644 --- a/model/validations/validations.js +++ b/model/validations/validations.js @@ -1,8 +1,8 @@ -steal.plugins('jquery/model').then(function($){ +steal('jquery/model').then(function($){ /** @page jquery.model.validations Validations -@plugin jquery/mode/validations -@download jquery/dist/jquery.model.validations.js +@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 @@ -15,7 +15,7 @@ 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.extend("Contact",{ +$.Model("Contact",{ init : function(){ // validates that birthday is in the future this.validate("birthday",function(){ @@ -45,14 +45,12 @@ var validate = function(attrNames, options, proc) { } options = options || {}; attrNames = $.makeArray(attrNames) - var customMsg = options.message, - self = this; 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]){ @@ -60,29 +58,61 @@ var validate = function(attrNames, options, proc) { } self.validations[attrName].push(function(){ var res = proc.call(this, this[attrName]); - return options.message || res; + 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 [validation] for more on 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 true for valid and false otherwise. Function is called in the instance context and takes the value 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.<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 [validation] for more on validations. + * 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'. @@ -93,7 +123,7 @@ $.extend($.Model, { if( (typeof value != 'undefined' && value != '') && String(value).match(regexp) == null ) { - return "is invalid"; + return this.Class.validationMessages.format; } }); }, @@ -102,7 +132,7 @@ $.extend($.Model, { * @function jQuery.Model.static.validateInclusionOf * @parent jquery.model.validations * Validates whether the values of the specified attributes are available in a particular - * array. See [validation] for more on validations. + * 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'. @@ -114,14 +144,14 @@ $.extend($.Model, { return; if($.grep(inArray, function(elm) { return (elm == value);}).length == 0) - return "is not a valid option (perhaps out of range)"; + 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 [validation] for more on 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) @@ -131,31 +161,31 @@ $.extend($.Model, { validateLengthOf: function(attrNames, min, max, options) { validate.call(this, attrNames, options, function(value) { if((typeof value == 'undefined' && min > 0) || value.length < min) - return "is too short (min=" + min + ")"; + return this.Class.validationMessages.lengthShort + " (min=" + min + ")"; else if(typeof value != 'undefined' && value.length > max) - return "is too long (max=" + 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 [validation] for more on 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 == "") - return "can't be empty"; + 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 [validation] for more on 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) @@ -165,14 +195,9 @@ $.extend($.Model, { validateRangeOf: function(attrNames, low, hi, options) { validate.call(this, attrNames, options, function(value) { if(typeof value != 'undefined' && value < low || value > hi) - return "is out of range [" + low + "," + hi + "]"; + return this.Class.validationMessages.range + " [" + low + "," + hi + "]"; }); } }); - - - - - }); diff --git a/qunit.html b/qunit.html index f5d06870..f67f1712 100644 --- a/qunit.html +++ b/qunit.html @@ -1,6 +1,9 @@ + + jQueryMX Test @@ -10,6 +13,6 @@

                                                - + \ No newline at end of file diff --git a/test/qunit/integration.js b/test/qunit/integration.js new file mode 100644 index 00000000..722e654c --- /dev/null +++ b/test/qunit/integration.js @@ -0,0 +1,69 @@ +steal('funcunit/qunit', + 'jquery/model', + 'jquery/controller', + 'jquery/view/ejs', + 'jquery/dom/fixture') + .then(function(){ + +module('integration',{ + setup : function(){ + $("#qunit-test-area").html("") + } +}); + +test("controller can listen to model instances and model classes", function(){ + + + $("#qunit-test-area").html(""); + + + + $.Controller("Test.BinderThing",{ + "{model} created" : function(){ + ok(true,"model called"); + start(); + }, + "{instance} created" : function(){ + ok(true, "instance updated") + } + }); + + $.Model("Test.ModelThing",{ + create : function(attrs, success){ + success({id: 1}) + } + }); + + + var inst = new Test.ModelThing(); + + $("
                                                ").appendTo( $("#qunit-test-area") ) + .test_binder_thing({ + model : Test.ModelThing, + instance: inst + }); + + inst.save(); + stop(); +}) + + +test("Model and Views", function(){ + stop(); + + $.Model("Test.Thing",{ + findOne : "/thing" + },{}) + + $.fixture("/thing","//jquery/test/thing.json") + + var res = $.View("//jquery/test/template.ejs", + Test.Thing.findOne()); + + res.done(function(resolved){ + equals(resolved,"foo","works") + start() + }) +}) + +}) diff --git a/test/qunit/qunit.js b/test/qunit/qunit.js index 57c41cd7..6b478bbf 100644 --- a/test/qunit/qunit.js +++ b/test/qunit/qunit.js @@ -1,21 +1,41 @@ +(function(){ + var isReady, + stateAfterScript; + //we probably have to have this only describing where the tests are -steal.plugins( -'jquery/class/test/qunit', -'jquery/controller/test/qunit', -'jquery/controller/view/test/qunit', +steal('jquery').then(function(){ + $(function(){ + isReady = true; + }) +},'jquery/class/class_test.js') +.then('jquery/controller/controller_test.js') +.then('jquery/dom/compare/compare_test.js') +.then('jquery/dom/cur_styles/cur_styles_test.js') +.then('jquery/dom/dimensions/dimensions_test.js') +.then('jquery/dom/form_params/form_params_test.js') +.then('jquery/dom/route/route_test.js') +.then('jquery/lang/lang_test.js') +.then('jquery/dom/fixture/fixture_test.js') +.then('jquery/event/default/default_test.js') +.then('jquery/event/destroyed/destroyed_test.js') +.then('jquery/event/drag/drag_test.js') +.then('jquery/event/hover/hover_test.js') +.then('jquery/event/key/key_test.js') +.then('jquery/tie/tie_test.js') +.then('jquery/controller/view/test/qunit') +.then('jquery/model/test/qunit') +.then('jquery/view/test/qunit') +.then('./integration.js') +.then('jquery/event/default/default_pause_test.js',function(){ + + stateAfterScript = isReady; + module('jquery v steal'); -'jquery/dom/compare/test/qunit', -'jquery/dom/cur_styles/test/qunit', -'jquery/dom/dimensions/test/qunit', -'jquery/dom/fixture/test/qunit', -'jquery/dom/form_params/test/qunit', -'jquery/event/default/test/qunit', -'jquery/event/destroyed/test/qunit', -'jquery/event/hover/test/qunit', -'jquery/event/drag/test/qunit', -'jquery/model/test/qunit', + test("jquery isn't ready", function(){ + ok(!stateAfterScript, "jQuery isn't ready yet") + }) + +}); -'jquery/view/test/qunit', - 'jquery/view/ejs/test/qunit' -) +})(); diff --git a/test/run.js b/test/run.js new file mode 100644 index 00000000..e6924913 --- /dev/null +++ b/test/run.js @@ -0,0 +1,8 @@ +// loads all of jquerymx's command line tests + +//load("jquery/download/test/run.js"); + +load('jquery/view/test/compression/run.js'); + +load("jquery/generate/test/run.js"); + diff --git a/test/template.ejs b/test/template.ejs new file mode 100644 index 00000000..36f1f1b5 --- /dev/null +++ b/test/template.ejs @@ -0,0 +1 @@ +<%= name %> \ No newline at end of file diff --git a/test/thing.json b/test/thing.json new file mode 100644 index 00000000..b3fa01ab --- /dev/null +++ b/test/thing.json @@ -0,0 +1,4 @@ +{ + "id" :1, + "name": "foo" +} diff --git a/tie/qunit.html b/tie/qunit.html index 853cfb51..58385408 100644 --- a/tie/qunit.html +++ b/tie/qunit.html @@ -5,7 +5,7 @@ - + diff --git a/tie/test/qunit/qunit.js b/tie/test/qunit/qunit.js deleted file mode 100644 index 4ffe60a2..00000000 --- a/tie/test/qunit/qunit.js +++ /dev/null @@ -1,3 +0,0 @@ -steal - .plugins("funcunit/qunit", "jquery/tie") - .then("tie_test"); \ No newline at end of file diff --git a/tie/test/qunit/tie_test.js b/tie/test/qunit/tie_test.js deleted file mode 100644 index 4fe53dc6..00000000 --- a/tie/test/qunit/tie_test.js +++ /dev/null @@ -1,5 +0,0 @@ -module("tie"); - -test("tie testing works", function(){ - ok(true,"an assert is run"); -}); \ No newline at end of file diff --git a/tie/tie.html b/tie/tie.html index f471f552..16d9be57 100644 --- a/tie/tie.html +++ b/tie/tie.html @@ -19,12 +19,17 @@ height: 30px; background-color: green; } + .rating .selected { + font-size: 20px; + font-weight: bold; + color: red; + }
                                                - +
                                                @@ -46,27 +51,64 @@ - - \ No newline at end of file diff --git a/tie/tie.js b/tie/tie.js index 1000ab8d..1a36416c 100644 --- a/tie/tie.js +++ b/tie/tie.js @@ -1,8 +1,24 @@ -steal.plugins('jquery/controller').then(function($){ - -$.Controller.extend("Tie",{ - nameOk : true -},{ +steal('jquery/controller').then(function($){ + +/** + * @class jQuery.Tie + * @core + * + * The $.fn.tie plugin binds form elements and controllers with + * models and vice versa. The result is that a change in + * a model will automatically update the form element or controller + * AND a change event on the element will update the model. + * + * + * + * + * + */ +$.Controller("jQuery.Tie",{ + setup : function(el){ + this._super(el,{}) + return $.makeArray(arguments); + }, init : function(el, inst, attr, type){ // if there's a controller if(!type){ @@ -22,6 +38,9 @@ $.Controller.extend("Tie",{ this.inst = inst; this.bind(inst, attr, "attrChanged"); + //destroy this controller if the model instance is destroyed + this.bind(inst, "destroyed", "modelDestroyed"); + var value = inst.attr(attr); //set the value this.lastValue = value; @@ -41,6 +60,9 @@ $.Controller.extend("Tie",{ this.lastValue = val; } }, + modelDestroyed : function(){ + this.destroy() + }, setVal : function(val){ if (this.type) { this.element[this.type]("val", val) @@ -50,11 +72,11 @@ $.Controller.extend("Tie",{ } }, change : function(el, ev, val){ - if(!this.type){ + if(!this.type && val === undefined){ val = this.element.val(); } - this.inst.attr(this.attr, val, null, this.callback('setBack')) + this.inst.attr(this.attr, val, null, this.proxy('setBack')) }, setBack : function(){ @@ -62,7 +84,12 @@ $.Controller.extend("Tie",{ }, destroy : function(){ this.inst = null; - this._super(); + if(! this._destroyed ){ + // assume it's because of the https://github.com/jupiterjs/jquerymx/pull/20 + // problem and don't throw an error + this._super(); + } + } }); diff --git a/tie/tie_test.js b/tie/tie_test.js new file mode 100644 index 00000000..a4892441 --- /dev/null +++ b/tie/tie_test.js @@ -0,0 +1,125 @@ +steal + .then("funcunit/qunit", "jquery/tie",'jquery/model') + .then(function(){ + + + module("jquery/tie",{ + setup : function(){ + $.Model("Person",{ + setAge : function(age, success, error){ + age = +(age); + if(isNaN(age) || !isFinite(age) || age < 1 || age > 10){ + error() + }else{ + return age; + } + } + }); + } + }); + + test("sets age on tie", function(){ + + var person1 = new Person({age: 5}); + var inp = $("").appendTo( $("#qunit-test-area") ); + + inp.tie(person1, 'age'); + + equals(inp.val(), "5", "sets age"); + + var person2 = new Person(); + var inp2 = $("").appendTo( $("#qunit-test-area") ); + inp2.tie(person2, 'age'); + equals(inp2.val(), "", "nothing set"); + + person2.attr("age",6); + + equals(inp2.val(), "6", "nothing set"); + + + }); + + test("removing the controller, removes the tie ", 3, function(){ + var person1 = new Person({age: 5}); + var inp = $("
                                                ").appendTo( $("#qunit-test-area") ); + + $.Controller("Foo",{ + val : function(value){ + equals(value, 5, "Foo got the value correct") + } + }); + + inp.foo().tie(person1,"age"); + var foo = inp.controller('foo'), + tie = inp.controller('tie'); + inp.foo("destroy"); + + person1.attr("age",7) + ok(foo._destroyed, "Foo is destroyed"); + ok(tie._destroyed, "Tie is destroyed") + }) + + test("destroying the person, removes the tie", function(){ + var person1 = new Person({age: 5}); + var inp = $("
                                                ").appendTo( $("#qunit-test-area") ); + + $.Controller("Foo",{ + val : function(value){ + equals(value, 5, "Foo got the value correct") + } + }); + + inp.foo().tie(person1,"age"); + var foo = inp.controller('foo'), + tie = inp.controller('tie'); + + person1.destroyed(); + + person1.attr("age",7) + ok(!foo._destroyed, "Foo is not destroyed"); + ok(tie._destroyed, "Tie is destroyed") + }) + + test("removing html element removes the tie", function() { + var person1 = new Person({age: 5}); + var inp = $("
                                                ").appendTo( $("#qunit-test-area") ); + + $.Controller("Foo",{ + val : function(value) {} + }); + + inp.foo().tie(person1,"age"); + var foo = inp.controller('foo'), + tie = inp.controller('tie'); + + inp.remove(); // crashes here + + ok(foo._destroyed, "Foo is destroyed"); + ok(tie._destroyed, "Tie is destroyed") + }); + + test("tie on a specific controller", function(){}); + + test("no controller with val, only listen", function(){ + var person1 = new Person({age: 5}); + var inp = $("
                                                ").appendTo( $("#qunit-test-area") ); + + inp.tie(person1,"age"); + + inp.trigger("change",7); + equals(7, person1.attr('age'), "persons age set on change event"); + }); + + test("input error recovery", function(){ + var person1 = new Person({age: 5}); + var inp = $("").appendTo( $("#qunit-test-area") ); + + inp.tie(person1, 'age'); + + inp.val(100).trigger('change'); + + equals(inp.val(), "5", "input value stays the same"); + equals(person1.attr('age'), "5", "persons age stays the same"); + }) + + }); \ No newline at end of file diff --git a/update b/update index bd181bb1..57b1a5be 100644 --- a/update +++ b/update @@ -1,5 +1,5 @@ -load('steal/rhino/steal.js') +load('steal/rhino/rhino.js') -steal('//steal/get/get', function(s) { +steal('steal/get', function(s) { s.get('http://github.com/jupiterjs/jquerymx/', {name: 'jquery'}); }) diff --git a/view/ejs/easyhookup.ejs b/view/ejs/easyhookup.ejs new file mode 100644 index 00000000..82aef45b --- /dev/null +++ b/view/ejs/easyhookup.ejs @@ -0,0 +1 @@ +
                                                <%= (el)-> el.addClass(text) %>> \ No newline at end of file diff --git a/view/ejs/ejs.html b/view/ejs/ejs.html deleted file mode 100644 index 81c8cf92..00000000 --- a/view/ejs/ejs.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - ejs - - - -

                                                Welcome to JavaScriptMVC 3.0!

                                                -
                                                  -
                                                • Include plugins and files in jquery/view/ejs/ejs.js.
                                                • -
                                                • Change to production mode by changing development to production in this file.
                                                • -
                                                - - - \ No newline at end of file diff --git a/view/ejs/ejs.js b/view/ejs/ejs.js index 026d4fbc..e5132159 100644 --- a/view/ejs/ejs.js +++ b/view/ejs/ejs.js @@ -1,494 +1,630 @@ /*jslint evil: true */ -steal.plugins('jquery/view', 'jquery/lang/rsplit').then(function( $ ) { +steal('jquery/view', 'jquery/lang/string/rsplit').then(function( $ ) { - //helpers we use - var chop = function( string ) { - return string.substr(0, string.length - 1); + // 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, '&').replace(//g, '>').replace(quoteReg, '"').replace(singleQuoteReg, "'"); + }, + $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 ERB + * 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 + * <ul> + * <% for(var i = 0; i < tasks.length; i++){ %> + * <li class="task <%= tasks[i].identity %>"><%= tasks[i].name %></li> + * <% } %> + * </ul> + * @codeend + * + * For the following examples, we assume this view is in 'views\tasks\list.ejs'. + * + * + * ## 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 %> - Runs JS Code. + * For example: + * + * <% alert('hello world') %> + * + * - <%= CODE %> - Runs JS Code and writes the _escaped_ result into the result of the template. + * For example: + * + *

                                                <%= 'hello world' %>

                                                + * + * - <%== CODE %> - Runs JS Code and writes the _unescaped_ result into the result of the template. + * For example: + * + *

                                                <%== 'hello world' %>

                                                + * + * - <%%= CODE %> - Writes <%= CODE %> to the result of the template. This is very useful for generators. + * + * <%%= 'hello world' %> + * + * - <%# 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 + * <ul <%= Mxui.Tabs%>>...<ul> + * @codeend + * + * You can even hook up multiple controllers: + * + * @codestart + * <ul <%= [Mxui.Tabs, Mxui.Filler]%>>...<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 + * <ul <%= plugin('mxui_tabs', { option: 'value' }) %>>...<ul> + * @codeend + * + * Don't add a semicolon when using view helpers. + * + * + *

                                                View Helpers

                                                + * 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 + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
                                                OptionDefaultDescription
                                                text uses the provided text as the template. Example:
                                                new View({text: '<%=user%>'}) + *
                                                type'<'type of magic tags. Options are '<' or '[' + *
                                                namethe element ID or url an optional name that is used for caching. + *
                                                + */ EJS = function( options ) { - //returns a renderer function + // 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 = {}; - this.template.process = options; + this.template = { + fn: options + }; return; } //set options on self - $.extend(this, EJS.options, options); - - var template = new EJS.Compiler(this.text, this.type); - - template.compile(options, this.name); - - this.template = template; - }, - defaultSplitter = /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/; + 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. /** - * @class jQuery.EJS - * - * @plugin jquery/view/ejs - * @parent jQuery.View - * @download jquery/dist/jquery.view.ejs.js - * @test jquery/view/ejs/qunit.html - * - * - * Ejs provides ERB - * 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 - * <ul> - * <% for(var i = 0; i < tasks.length; i++){ %> - * <li class="task <%= tasks[i].identity %>"><%= tasks[i].name %></li> - * <% } %> - * </ul> - * @codeend - * For the following examples, we assume this view is in 'views\tasks\list.ejs' - *

                                                Use

                                                - * There are 2 common ways to use Views: - *
                                                  - *
                                                • Controller's [jQuery.Controller.prototype.view view function]
                                                • - *
                                                • The jQuery Helpers: [jQuery.fn.after after], - * [jQuery.fn.append append], - * [jQuery.fn.before before], - * [jQuery.fn.before html], - * [jQuery.fn.before prepend], - * [jQuery.fn.before replace], and - * [jQuery.fn.before text].
                                                • - *
                                                - *

                                                View

                                                - * jQuery.Controller.prototype.view is the preferred way of rendering a view. - * You can find all the options for render in - * its [jQuery.Controller.prototype.view documentation], but here is a brief example of rendering the - * list.ejs view from a controller: - * @codestart - * $.Controller.extend("TasksController",{ - * init: function( el ) { - * Task.findAll({},this.callback('list')) - * }, - * list: function( tasks ) { - * this.element.html( - * this.view("list", {tasks: tasks}) - * ) - * } - * }) - * @codeend - * - * ## 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 - * <ul <%= Mxui.Tabs%>>...<ul> - * @codeend - * - * You can even hook up multiple controllers: + * Renders an object with view helpers attached to the view. * - * @codestart - * <ul <%= [Mxui.Tabs, Mxui.Filler]%>>...<ul> - * @codeend - * - *

                                                View Helpers

                                                - * 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 - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * + * new EJS({text: "<%= message %>"}).render({ + * message: "foo" + * },{helper: function(){ ... }}) * - *
                                                OptionDefaultDescription
                                                url loads the template from a file. This path should be relative to [jQuery.root]. - *
                                                text uses the provided text as the template. Example:
                                                new View({text: '<%=user%>'}) - *
                                                element loads a template from the innerHTML or value of the element. - *
                                                type'<'type of magic tags. Options are '<' or '[' - *
                                                namethe element ID or url an optional name that is used for caching. - *
                                                cachetrue in production mode, false in other modestrue to cache template. - *
                                                + * @param {Object} object data to be rendered + * @param {Object} [extraHelpers] an object with view helpers + * @return {String} returns the result of the string */ - $.EJS = EJS; - /** - * @Prototype + 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 */ - EJS.prototype = { - constructor: EJS, + + extend(EJS, { /** - * Renders an object with extra view helpers attached to the view. - * @param {Object} object data to be rendered - * @param {Object} extra_helpers an object with additonal view helpers - * @return {String} returns the result of the string + * Used to convert what's in <%= %> 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 + * <%= function(el){$(el).fadeIn()} %> + * @codeend + * + * @param {String|Object|Function} input the value in between the + * write magic tags: <%= %> + * @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 */ - render: function( object, extraHelpers ) { - object = object || {}; - this._extra_helpers = extraHelpers; - var v = new EJS.Helpers(object, extraHelpers || {}); - return this.template.process.call(object, object, v); - }, - out: function() { - return this.template.out; - } - }; - /* @Static */ + 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 - EJS. - /** - * Used to convert what's in <%= %> 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 - * <%= function(el){$(el).fadeIn()} %> - * @codeend - * - * @param {String|Object|Function} input the value in between the - * write majic tags: <%= %> - * @return {String} returns the content to be added to the rendered - * output. The content is different depending on the type: - * - * * string - a bac - * * foo - bar - */ - text = function( input ) { - if ( typeof input == 'string' ) { - return input; - } - var myid; - if ( input === null || input === undefined ) { - return ''; - } - if ( input instanceof Date ) { - return input.toDateString(); - } - if ( input.hookup ) { - myid = $.View.hookup(function( el, id ) { + function( el, id ) { input.hookup.call(input, el, id); - }); - return "data-view-id='" + myid + "'"; - } - if ( typeof input == 'function' ) { - return "data-view-id='" + $.View.hookup(input) + "'"; - } - - if ( isArray(input) ) { - myid = $.View.hookup(function( 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++ ) { - var stub; - stub = input[i].hookup ? input[i].hookup(el, id) : input[i](el, id); + input[i].hookup ? input[i].hookup(el, id) : input[i](el, id); } }); - return "data-view-id='" + myid + "'"; - } - if ( input.nodeName || input.jQuery ) { - throw "elements in views are not supported"; + // 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' } - - if ( input.toString ) { - return myid ? input.toString(myid) : input.toString(); + }); + // ========= 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); } - return ''; - }; - - - - // used to break text into tolkens - EJS.Scanner = function( source, left, right ) { - - // add these properties to the scanner - extend(this, { - leftDelimiter: left + '%', - rightDelimiter: '%' + right, - doubleLeft: left + '%%', - doubleRight: '%%' + right, - leftEqual: left + '%=', - leftComment: left + '%#' - }); - - - // make a regexp that can split on these token - this.splitRegexp = (left == '[' ? defaultSplitter : new RegExp("(" + [this.doubleLeft, this.doubleRight, this.leftEqual, this.leftComment, this.leftDelimiter, this.rightDelimiter + '\n', this.rightDelimiter, '\n'].join(")|(") + ")")); - - this.source = source; - this.lines = 0; - }; - - - EJS.Scanner.prototype = { - // calls block with each token - scan: function( block ) { - var regex = this.splitRegexp; - if ( this.source ) { - var source_split = $.String.rsplit(this.source, /\n/); - for ( var i = 0; i < source_split.length; i++ ) { - var item = source_split[i]; - this.scanline(item, regex, block); - } - } - }, - scanline: function( line, regex, block ) { - this.lines++; - var line_split = $.String.rsplit(line, regex), + }, + 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 ) { - try { - block(token, this); - } catch (e) { - throw { - type: 'jQuery.EJS.Scanner', - line: this.lines - }; - } + block(token, scanner); } } - } - }; - - // a line and script buffer - // we use this so we know line numbers when there - // is an error. - // pre and post are setup and teardown for the buffer - EJS.Buffer = function( pre_cmd, post_cmd ) { - this.line = []; - this.script = []; - this.post_cmd = post_cmd; - - // add the pre commands to the first line - this.push.apply(this, pre_cmd); - }; - EJS.Buffer.prototype = { - //need to maintain your own semi-colons (for performance) - push: function() { - this.line.push.apply(this.line, arguments); }, - - cr: function() { - this.script.push(this.line.join(''), "\n"); - this.line = []; + // 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; }, - //returns the script too - close: function() { - var stub; - - if ( this.line.length > 0 ) { - this.script.push(this.line.join('')); - this.line = []; - } - - stub = this.post_cmd.length && this.push.apply(this, this.post_cmd); - - this.script.push(";"); //makes sure we always have an ending / - return this.script.join(""); - } - - }; - // compiles a template - EJS.Compiler = function( source, left ) { - //normalize line endings - this.source = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - left = left || '<'; - var right = '>'; - switch ( left ) { - case '[': - right = ']'; - break; - case '<': - break; - default: - throw left + ' is not a supported deliminator'; - } - this.scanner = new EJS.Scanner(this.source, left, right); - this.out = ''; - }; - EJS.Compiler.prototype = { - compile: function( options, name ) { - - options = options || {}; - - this.out = ''; - + // 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, - buff = new EJS.Buffer(['var ___v1ew = [];'], []), + // 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 = '', - clean = function( content ) { - return content.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"'); - }, + // 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), '");'); }, - startTag = null; - - this.scanner.scan(function( token, scanner ) { + // 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.push(put_cmd , '"' , clean(content) , '");'); buff.cr(); - content = ''; + empty(); break; - case scanner.leftDelimiter: - case scanner.leftEqual: - case scanner.leftComment: + // 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); } - content = ''; + empty(); break; + case scanner.dLeft: // replace <%% with <% - case scanner.doubleLeft: - content = content + scanner.leftDelimiter; + content += scanner.left; break; default: - content = content + token; + content += token; break; } } else { + //we have a start tag switch ( token ) { - case scanner.rightDelimiter: + case scanner.right: + // %> switch ( startTag ) { - case scanner.leftDelimiter: - if ( content[content.length - 1] == '\n' ) { - content = chop(content); - buff.push(content, ";"); - buff.cr(); + 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) } - else { - buff.push(content, ";"); + // add the remaining content + buff.push(content, last); + + // if we have a block, start counting + if(bn === 1 ){ + endStack.push(";") } break; - case scanner.leftEqual: - buff.push(insert_cmd, "(jQuery.EJS.text(", content, ")));"); + 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; - content = ''; + empty(); break; - case scanner.doubleRight: - content = content + scanner.rightDelimiter; + case scanner.dRight: + content += scanner.right; break; default: - content = content + token; + content += token; break; } } - }); + }) if ( content.length > 0 ) { // Should be content.dump in Ruby buff.push(put_cmd, '"', clean(content) + '");'); } - var template = buff.close(); - this.out = '/*' + name + '*/ try { with(_VIEW) { with (_CONTEXT) {' + template + " return ___v1ew.join('');}}}catch(e){e.lineNumber=null;throw e;}"; + 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 - eval('this.process = (function(_CONTEXT,_VIEW){' + this.out + '})'); //new Function("_CONTEXT","_VIEW",this.out) - } - }; + myEval.call(out, 'this.fn = (function(_CONTEXT,_VIEW){' + out.out + '});\r\n//@ sourceURL="' + name + '.js"'); - - //type, cache, folder - /** - * @attribute options - * Sets default options for all views - * - * - * - * - * - * - * - * - * - * - * - * - *
                                                OptionDefaultDescription
                                                type'<'type of magic tags. Options are '<' or '[' - *
                                                cachetrue in production mode, false in other modestrue to cache template. - *
                                                - * - */ - EJS.options = { - cache: true, - type: '<', - ext: '.ejs' - }; + 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. - * @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 + * + * 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: + * + *
                                                <%= upperHtml('javascriptmvc') %>>
                                                + * + * + * @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*/ + /** + * @prototype + */ EJS.Helpers.prototype = { /** - * Makes a plugin + * Hooks up a jQuery plugin on. * @param {String} name the plugin name */ plugin: function( name ) { @@ -505,27 +641,25 @@ steal.plugins('jquery/view', 'jquery/lang/rsplit').then(function( $ ) { view: function( url, data, helpers ) { helpers = helpers || this._extras; data = data || this._data; - return $.View(url, data, helpers); //new EJS(options).render(data, helpers); + return $View(url, data, helpers); //new EJS(options).render(data, helpers); } }; - - $.View.register({ + // 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 - }).out() + " })"; + text: src, + name: id + }).template.out + " })"; }, renderer: function( id, text ) { - var ejs = new EJS({ + return EJS({ text: text, name: id }); - return function( data, helpers ) { - return ejs.render.call(ejs, data, helpers); - }; } }); }); \ No newline at end of file diff --git a/view/ejs/ejs_test.js b/view/ejs/ejs_test.js new file mode 100644 index 00000000..195625d1 --- /dev/null +++ b/view/ejs/ejs_test.js @@ -0,0 +1,129 @@ +steal('funcunit/qunit','jquery/view/ejs', function(){ +module("jquery/view/ejs, rendering",{ + setup : function(){ + + this.animals = ['sloth', 'bear', 'monkey'] + if(!this.animals.each){ + this.animals.each = function(func){ + for(var i =0; i < this.length; i++){ + func(this[i]) + } + } + } + + this.squareBrackets = "
                                                  [% this.animals.each(function(animal){%]" + + "
                                                • [%= animal %]
                                                • " + + "[%});%]
                                                " + this.squareBracketsNoThis = "
                                                  [% animals.each(function(animal){%]" + + "
                                                • [%= animal %]
                                                • " + + "[%});%]
                                                " + this.angleBracketsNoThis = "
                                                  <% animals.each(function(animal){%>" + + "
                                                • <%= animal %>
                                                • " + + "<%});%>
                                                "; + + } +}) +test("render with left bracket", function(){ + var compiled = new $.EJS({text: this.squareBrackets, type: '['}).render({animals: this.animals}) + equals(compiled, "
                                                • sloth
                                                • bear
                                                • monkey
                                                ", "renders with bracket") +}) +test("render with with", function(){ + var compiled = new $.EJS({text: this.squareBracketsNoThis, type: '['}).render({animals: this.animals}) ; + equals(compiled, "
                                                • sloth
                                                • bear
                                                • monkey
                                                ", "renders bracket with no this") +}) +test("default carrot", function(){ + var compiled = new $.EJS({text: this.angleBracketsNoThis}).render({animals: this.animals}) ; + + equals(compiled, "
                                                • sloth
                                                • bear
                                                • monkey
                                                ") +}) +test("render with double angle", function(){ + var text = "<%% replace_me %>"+ + "
                                                  <% animals.each(function(animal){%>" + + "
                                                • <%= animal %>
                                                • " + + "<%});%>
                                                "; + var compiled = new $.EJS({text: text}).render({animals: this.animals}) ; + equals(compiled, "<% replace_me %>
                                                • sloth
                                                • bear
                                                • monkey
                                                ", "works") +}); + +test("comments", function(){ + var text = "<%# replace_me %>"+ + "
                                                  <% animals.each(function(animal){%>" + + "
                                                • <%= animal %>
                                                • " + + "<%});%>
                                                "; + var compiled = new $.EJS({text: text}).render({animals: this.animals}) ; + equals(compiled,"
                                                • sloth
                                                • bear
                                                • monkey
                                                " ) +}); + +test("multi line", function(){ + var text = "a \n b \n c", + result = new $.EJS({text: text}).render({}) ; + + equals(result, text) +}) + +test("escapedContent", function(){ + var text = "<%= tags %><%= number %>"; + var compiled = new $.EJS({text: text}).render({tags: "foo < bar < car > zar > poo", + quotes : "I use 'quote' fingers \"a lot\"", + number : 123}) ; + + var div = $('
                                                ').html(compiled) + equals(div.find('span').text(), "foo < bar < car > zar > poo" ); + equals(div.find('strong').text(), 123 ); + equals(div.find('input').val(), "I use 'quote' fingers \"a lot\"" ); + equals(div.find('label').html(), "&" ); +}) + +test("unescapedContent", function(){ + var text = "<%== tags %>
                                                <%= tags %>
                                                "; + var compiled = new $.EJS({text: text}).render({tags: "foobar", + quotes : "I use 'quote' fingers "a lot""}) ; + + var div = $('
                                                ').html(compiled) + equals(div.find('span').text(), "foobar" ); + equals(div.find('div').text().toLowerCase(), "foobar" ); + equals(div.find('span').html().toLowerCase(), "foobar" ); + equals(div.find('input').val(), "I use 'quote' fingers \"a lot\"" ); +}); + +test("returning blocks", function(){ + var somethingHelper = function(cb){ + return cb([1,2,3,4]) + } + + var res = $.View("//jquery/view/ejs/test_template.ejs",{something: somethingHelper, + items: ['a','b']}); + // make sure expected values are in res + ok(/\s4\s/.test(res), "first block called" ); + equals(res.match(/ItemsLength4/g).length, 4, "innerBlock and each") +}); + +test("easy hookup", function(){ + var div = $('
                                                ').html("//jquery/view/ejs/easyhookup.ejs",{text: "yes"}) + ok( div.find('div').hasClass('yes'), "has yes" ) +}); + +test("helpers", function() { + $.EJS.Helpers.prototype.simpleHelper = function() + { + return 'Simple'; + } + + $.EJS.Helpers.prototype.elementHelper = function() + { + return function(el) { + el.innerHTML = 'Simple'; + } + } + + var text = "
                                                <%= simpleHelper() %>
                                                "; + var compiled = new $.EJS({text: text}).render() ; + equals(compiled, "
                                                Simple
                                                "); + + text = "
                                                <%= elementHelper() %>>
                                                "; + compiled = new $.EJS({text: text}).render() ; + $('#qunit-test-area').append($(compiled)); + equals($('#hookup').html(), "Simple"); +}); + +}) diff --git a/view/ejs/funcunit.html b/view/ejs/funcunit.html deleted file mode 100644 index 18e35cd8..00000000 --- a/view/ejs/funcunit.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - -

                                                ejs Test Suite

                                                -

                                                -
                                                -

                                                -
                                                -
                                                  -
                                                  - - \ No newline at end of file diff --git a/view/ejs/other.js b/view/ejs/other.js deleted file mode 100644 index fec1636f..00000000 --- a/view/ejs/other.js +++ /dev/null @@ -1,47 +0,0 @@ -var ___v1ew = []; - var previous = "", res, current, title;; -___v1ew.push("\n"); -___v1ew.push("\n"); - if(selected && selected.length) { ;___v1ew.push("\n"); -___v1ew.push("
                                                  \n"); -___v1ew.push(" "); for(var i =0; i < selected.length; i++){;___v1ew.push("\n"); -___v1ew.push(" "); current = selected[i]; - title = (current.title ? current.title: current.name); - res = calculateDisplay(previous, title); - name = normalizeName(current.name) ;___v1ew.push("\n"); -___v1ew.push("
                                                   
                                                  \n"); -___v1ew.push(" \n"); -___v1ew.push("
                                                   
                                                  \n"); -___v1ew.push(" "); if(i<(selected.length-1)){ ;___v1ew.push("\n"); -___v1ew.push("
                                                   
                                                  \n"); -___v1ew.push(" ");};___v1ew.push("\n"); -___v1ew.push(" ");};___v1ew.push("\n"); -___v1ew.push("
                                                  \n"); -};___v1ew.push("\n"); -___v1ew.push("
                                                  \n"); -___v1ew.push("
                                                   
                                                  \n"); -___v1ew.push("
                                                  \n"); -___v1ew.push(" "); for(var i =0; i < list.length; i++){;___v1ew.push("\n"); -___v1ew.push(" "); current = list[i]; - if(current.hide){ continue; } - title = (current.title ? current.title: current.name); - res = calculateDisplay(previous, title); - name = normalizeName(current.name) ;___v1ew.push("\n"); -___v1ew.push(" \n"); -___v1ew.push(" ");___v1ew.push((jQuery.View.EJS.text(res.name.replace("jQuery","$"))));___v1ew.push("\n"); -___v1ew.push(" \n"); -___v1ew.push(" "); previous = title;___v1ew.push("\n"); -___v1ew.push(" ");};___v1ew.push("\n"); -___v1ew.push("
                                                  \n"); -___v1ew.push("
                                                   
                                                  \n"); -___v1ew.push("
                                                  \n"); -___v1ew.push("\n"); -___v1ew.push("\n"); -; \ No newline at end of file diff --git a/view/ejs/qunit.html b/view/ejs/qunit.html index 140f12e9..3d65b557 100644 --- a/view/ejs/qunit.html +++ b/view/ejs/qunit.html @@ -6,7 +6,7 @@ margin: 0px; padding: 0px; } - + diff --git a/view/ejs/test/qunit/ejs_test.js b/view/ejs/test/qunit/ejs_test.js deleted file mode 100644 index 42d8b983..00000000 --- a/view/ejs/test/qunit/ejs_test.js +++ /dev/null @@ -1,62 +0,0 @@ -module("jquery/view/ejs, rendering",{ - setup : function(){ - - this.animals = ['sloth', 'bear', 'monkey'] - if(!this.animals.each){ - this.animals.each = function(func){ - for(var i =0; i < this.length; i++){ - func(this[i]) - } - } - } - - this.squareBrackets = "
                                                    [% this.animals.each(function(animal){%]" + - "
                                                  • [%= animal %]
                                                  • " + - "[%});%]
                                                  " - this.squareBracketsNoThis = "
                                                    [% animals.each(function(animal){%]" + - "
                                                  • [%= animal %]
                                                  • " + - "[%});%]
                                                  " - this.angleBracketsNoThis = "
                                                    <% animals.each(function(animal){%>" + - "
                                                  • <%= animal %>
                                                  • " + - "<%});%>
                                                  "; - - } -}) -test("render with left bracket", function(){ - var compiled = new $.EJS({text: this.squareBrackets, type: '['}).render({animals: this.animals}) - equals(compiled, "
                                                  • sloth
                                                  • bear
                                                  • monkey
                                                  ", "renders with bracket") -}) -test("render with with", function(){ - var compiled = new $.EJS({text: this.squareBracketsNoThis, type: '['}).render({animals: this.animals}) ; - equals(compiled, "
                                                  • sloth
                                                  • bear
                                                  • monkey
                                                  ", "renders bracket with no this") -}) -test("default carrot", function(){ - var compiled = new $.EJS({text: this.angleBracketsNoThis}).render({animals: this.animals}) ; - - equals(compiled, "
                                                  • sloth
                                                  • bear
                                                  • monkey
                                                  ") -}) -test("render with double angle", function(){ - var text = "<%% replace_me %>"+ - "
                                                    <% animals.each(function(animal){%>" + - "
                                                  • <%= animal %>
                                                  • " + - "<%});%>
                                                  "; - var compiled = new $.EJS({text: text}).render({animals: this.animals}) ; - equals(compiled, "<% replace_me %>
                                                  • sloth
                                                  • bear
                                                  • monkey
                                                  ", "works") -}); - -test("comments", function(){ - var text = "<%# replace_me %>"+ - "
                                                    <% animals.each(function(animal){%>" + - "
                                                  • <%= animal %>
                                                  • " + - "<%});%>
                                                  "; - var compiled = new $.EJS({text: text}).render({animals: this.animals}) ; - equals(compiled,"
                                                  • sloth
                                                  • bear
                                                  • monkey
                                                  " ) -}); - -test("multi line", function(){ - var text = "a \n b \n c", - result = new $.EJS({text: text}).render({}) ; - - equals(result, text) -}) -//test("multi line sourc") diff --git a/view/ejs/test/qunit/qunit.js b/view/ejs/test/qunit/qunit.js deleted file mode 100644 index 3383d1c0..00000000 --- a/view/ejs/test/qunit/qunit.js +++ /dev/null @@ -1,6 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/view/ejs") //load your app - .plugins('funcunit/qunit') //load qunit - .then("ejs_test") - \ No newline at end of file diff --git a/view/ejs/test_template.ejs b/view/ejs/test_template.ejs new file mode 100644 index 00000000..23347b00 --- /dev/null +++ b/view/ejs/test_template.ejs @@ -0,0 +1,8 @@ +<%# Test Something Produces Items%> +<%== something(function(items){ %> +<%== items.length%> +<% $.each(items, function(){ %><%# Test Something Produces Items%> +<%== something(function(items){ %>ItemsLength<%== items.length %><% }) %> +<% }) %> +<% }) %> +<% for( var i =0; i < items.length; i++) { %>for <%= items[i] %><% } %> \ No newline at end of file diff --git a/view/helpers/helpers.js b/view/helpers/helpers.js index f12e01f1..478046eb 100644 --- a/view/helpers/helpers.js +++ b/view/helpers/helpers.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/view/ejs').then(function($){ +steal('jquery/view/ejs').then(function($){ /** * @add jQuery.EJS.Helpers.prototype @@ -315,7 +315,7 @@ $.extend($.EJS.Helpers.prototype, { */ img_tag: function( image_location, options ) { options = options || {}; - options.src = steal.root.join("resources/images/"+image_location); + options.src = steal.root.join("resources/images/"+image_location)+''; return this.single_tag_for('img', options); } diff --git a/view/jaml/jaml.js b/view/jaml/jaml.js index 91748ecf..65513ef4 100644 --- a/view/jaml/jaml.js +++ b/view/jaml/jaml.js @@ -1,4 +1,4 @@ -steal.plugins("jquery/view").then(function(){ +steal("jquery/view").then(function(){ diff --git a/view/micro/micro.js b/view/micro/micro.js index 891fb2aa..581c5ff0 100644 --- a/view/micro/micro.js +++ b/view/micro/micro.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/view').then(function(){ +steal('jquery/view').then(function(){ // Simple JavaScript Templating // John Resig - http://ejohn.org/ - MIT Licensed diff --git a/view/pages/deferreds.md b/view/pages/deferreds.md new file mode 100644 index 00000000..a65400bc --- /dev/null +++ b/view/pages/deferreds.md @@ -0,0 +1,73 @@ +@page view.deferreds Using Deferreds with Views +@parent jQuery.View 1 + +jQuery 1.6 brought [http://api.jquery.com/category/deferred-object/ Deferred] support. They are a great feature that promise to make a lot of asynchronous functionality easier to write and manage. jQuery.View uses Deferreds to simplify a common but annoying task into a one-liner: loading data and a template and rendering the result into an element. + +## Templates Consume Deferreds + +Here's what rendering templates looks like with deferreds: + + $('#todos').html('temps/todos.ejs', $.get('/todos',{},'json') ); + +This will make two parallel ajax requests. One request +is made for the template at `temps/todos.ejs`: + +
                                                  <% for(var i =0; i < this.length; i++) { %>
                                                  +  <li><%= this[i].name %></li>
                                                  +<% } %>
                                                  +
                                                  + +The second request for `/todos` might respond with a JSON array: + + [ + {"id" : 1, "name": "Take out the Trash"}, + {"id" : 2, "name": "Do the Laundry"} + ] + +When both have been loaded, the template is rendered with the todos data. The resulting HTML is placed in the `#todos` element. + +This is fab fast! The AJAX and template request are made in parallel and rendered +when both are complete. Before deferreds, this was a lot uglier: + + var template, + data, + done = function(){ + if( template && data ) { + var html = new EJS({text: template}) + .render(data); + $('#todos').html( html ) + } + } + $.get('temps/todos.ejs', function(text){ + template = text; + done(); + },'text') + $.get('/todos',{}, function(json){ + data = json + done(); + },'json') + +## Models Return Deferreds + +Model AJAX functions now return deferreds. Creating models like: + + $.Model('User',{ + findAll: '/users' + },{}); + + $.Model('Todo',{ + findAll: '/todos' + },{}) + +Lets you request todos and users and get back a deferred that can be +used in functions that accept deferreds like $.when: + + $.when( User.findAll(), + Todo.findAll() ) + +Or $.View: + + $('#content').html('temps/content.ejs',{ + users : User.findAll(), + todos: Todo.findAll() + }) \ No newline at end of file diff --git a/view/qunit.html b/view/qunit.html index 26f38aba..c8e18a46 100644 --- a/view/qunit.html +++ b/view/qunit.html @@ -1,7 +1,7 @@ - + diff --git a/view/test/compression/compression.js b/view/test/compression/compression.js index e1d5babb..59901998 100644 --- a/view/test/compression/compression.js +++ b/view/test/compression/compression.js @@ -1,10 +1,12 @@ -steal.plugins('jquery/view/ejs', 'jquery/view/ejs', 'jquery/view/tmpl') - .views('relative.ejs', - '//jquery/view/test/compression/views/absolute.ejs', - 'tmplTest.tmpl') - .then(function(){ - $(function(){ - $("#target").append($.View('//jquery/view/test/compression/views/relative.ejs', {} )) +steal('jquery/view/ejs', 'jquery/view/tmpl') + .then('./views/relative.ejs', + 'jquery/view/test/compression/views/absolute.ejs', + './views/tmplTest.tmpl', + './views/test.ejs', + function(){ + + $(document).ready(function(){ + $("#target").append('//jquery/view/test/compression/views/relative.ejs', {}) .append($.View('//jquery/view/test/compression/views/absolute.ejs', {} )) .append($.View('//jquery/view/test/compression/views/tmplTest.tmpl', {message: "Jquery Tmpl"} )) }) diff --git a/view/test/compression/run.js b/view/test/compression/run.js index 5467e340..d3c7d20a 100644 --- a/view/test/compression/run.js +++ b/view/test/compression/run.js @@ -4,32 +4,48 @@ * Tests compressing a very basic page and one that is using steal */ -load('steal/rhino/steal.js') -steal('//steal/test/test', function(s){ +load('steal/rhino/rhino.js') +steal('steal/test', function(s){ - steal.File("jquery/view/test/compression/views/absolute.ejs").save("

                                                  Absolute

                                                  "); - steal.File("jquery/view/test/compression/views/relative.ejs").save("

                                                  Relative

                                                  "); - steal.File("jquery/view/test/compression/views/tmplTest.tmpl").save("

                                                  ${message}

                                                  "); - s.test.clear(); + s.test.module("jquery/view/compression") + STEALPRINT = false; - load("steal/rhino/steal.js"); - steal.plugins('steal/build','steal/build/scripts','steal/build/styles',function(){ - steal.build('jquery/view/test/compression/compression.html',{to: 'jquery/view/test/compression'}); - }); - s.test.clear(); - s.test.remove("jquery/view/test/compression/views/absolute.ejs") - s.test.remove("jquery/view/test/compression/views/relative.ejs") - s.test.remove("jquery/view/test/compression/views/tmplTest.tmpl") + s.test.test("templates" , function(t){ + + + steal.File("jquery/view/test/compression/views/absolute.ejs").save("

                                                  Absolute

                                                  \n

                                                  ok

                                                  "); + steal.File("jquery/view/test/compression/views/relative.ejs").save("

                                                  Relative

                                                  "); + steal.File("jquery/view/test/compression/views/tmplTest.tmpl").save("

                                                  ${message}

                                                  "); + s.test.clear(); + + load("steal/rhino/rhino.js"); + steal('steal/build','steal/build/scripts',function(){ + steal.build('jquery/view/test/compression/compression.html',{to: 'jquery/view/test/compression'}); + }); + s.test.clear(); + s.test.remove("jquery/view/test/compression/views/absolute.ejs") + s.test.remove("jquery/view/test/compression/views/relative.ejs") + s.test.remove("jquery/view/test/compression/views/tmplTest.tmpl") + + + steal = {env: "production"}; + + s.test.open('jquery/view/test/compression/compression.html') + s.test.ok( /Relative/i.test( $(document.body).text() ), "Relative not in page!" ); + + s.test.ok( /Absolute/i.test( $(document.body).text() ), "Absolute not in page!" ); + + s.test.ok( /Jquery Tmpl/i.test( $(document.body).text() ), "Jquery Tmpl not in page!" ); + + s.test.clear(); + s.test.remove("jquery/view/test/compression/production.js") + + }) + + - steal = {env: "production"}; - s.test.open('jquery/view/test/compression/compression.html') - s.test.ok( /Relative/i.test( $(document.body).text() ), "Relative not in page!" ); - s.test.ok( /Absolute/i.test( $(document.body).text() ), "Absolute not in page!" ); - s.test.ok( /Jquery Tmpl/i.test( $(document.body).text() ), "Jquery Tmpl not in page!" ); - s.test.clear(); - s.test.remove("jquery/view/test/compression/production.js") }); \ No newline at end of file diff --git a/dom/dimensions/test/qunit/outer.micro b/view/test/compression/views/keep.me similarity index 100% rename from dom/dimensions/test/qunit/outer.micro rename to view/test/compression/views/keep.me diff --git a/view/test/compression/views/test.ejs b/view/test/compression/views/test.ejs new file mode 100644 index 00000000..dff991ff --- /dev/null +++ b/view/test/compression/views/test.ejs @@ -0,0 +1,3 @@ +<%for(var i = 0; i < 5 ; i++){%> +
                                                1. hi
                                                2. +<%}%> \ No newline at end of file diff --git a/view/test/qunit/deferred.ejs b/view/test/qunit/deferred.ejs new file mode 100644 index 00000000..75a4bdd5 --- /dev/null +++ b/view/test/qunit/deferred.ejs @@ -0,0 +1 @@ +<%= foo %> \ No newline at end of file diff --git a/view/test/qunit/deferreds.ejs b/view/test/qunit/deferreds.ejs new file mode 100644 index 00000000..884157f5 --- /dev/null +++ b/view/test/qunit/deferreds.ejs @@ -0,0 +1 @@ +<%= foo %> and <%= bar %> \ No newline at end of file diff --git a/view/test/qunit/hookup.ejs b/view/test/qunit/hookup.ejs new file mode 100644 index 00000000..ab7cb687 --- /dev/null +++ b/view/test/qunit/hookup.ejs @@ -0,0 +1 @@ +
                                                  <%= function(){} %> /> \ No newline at end of file diff --git a/view/test/qunit/hookupvalcall.ejs b/view/test/qunit/hookupvalcall.ejs new file mode 100644 index 00000000..7e0a8d49 --- /dev/null +++ b/view/test/qunit/hookupvalcall.ejs @@ -0,0 +1,4 @@ + +
                                                  <%= function(){}%>>
                                                  \ No newline at end of file diff --git a/view/test/qunit/large.ejs b/view/test/qunit/large.ejs index 671ef3b7..75636104 100644 --- a/view/test/qunit/large.ejs +++ b/view/test/qunit/large.ejs @@ -173,7 +173,7 @@ wrapMany into the success handler and make our code even shorter:

                                                    findAll : function(params, success, error){
                                                       $.get('/contacts.json',
                                                       params, 
                                                  -    this.callback(['wrapMany', success]),
                                                  +    this.proxy(['wrapMany', success]),
                                                       'json')
                                                     }
                                                   
                                                  diff --git a/view/test/qunit/qunit.js b/view/test/qunit/qunit.js index d4e14416..a0d926a1 100644 --- a/view/test/qunit/qunit.js +++ b/view/test/qunit/qunit.js @@ -1,9 +1,8 @@ //we probably have to have this only describing where the tests are -steal - .plugins("jquery/view","jquery/view/micro","jquery/view/ejs","jquery/view/jaml","jquery/view/tmpl") //load your app - .plugins('funcunit/qunit') //load qunit - .then("view_test") +steal("jquery/view","jquery/view/micro","jquery/view/ejs/ejs_test.js","jquery/view/jaml","jquery/view/tmpl") //load your app + .then('funcunit/qunit') //load qunit + .then("./view_test.js","jquery/view/tmpl/tmpl_test.js") -if(steal.browser.rhino){ - steal.plugins('funcunit/qunit/env') +if(steal.isRhino){ + steal('funcunit/qunit/rhino') } \ No newline at end of file diff --git a/view/test/qunit/view_test.js b/view/test/qunit/view_test.js index eec9748f..ef917d84 100644 --- a/view/test/qunit/view_test.js +++ b/view/test/qunit/view_test.js @@ -1,6 +1,22 @@ -module("jquery/view") -test("multipel template types work", function(){ +module("jquery/view"); + +test("Ajax transport", function(){ + var order = 0; + $.ajax({ + url: "//jquery/view/test/qunit/template.ejs", + dataType : "view", + async : false + }).done(function(view){ + equals(++order,1, "called synchronously"); + equals(view({message: "hi"}).indexOf("

                                                  hi

                                                  "), 0, "renders stuff!") + }); + + equals(++order,2, "called synchronously"); +}) + + +test("multiple template types work", function(){ $.each(["micro","ejs","jaml", "tmpl"], function(){ $("#qunit-test-area").html(""); @@ -52,14 +68,122 @@ test("caching works", function(){ $("#qunit-test-area").html(""); $("#qunit-test-area").html("//jquery/view/test/qunit/large.ejs",{"message" :"helloworld"}, function(text){ - var lap2 = new Date - first , + var lap2 = (new Date()) - first, lap1 = first-startT; - - ok(lap2 < lap1, "faster this time "+(lap1 - lap2) ) + // ok( lap1 > lap2, "faster this time "+(lap1 - lap2) ) start(); $("#qunit-test-area").html(""); }) }) -}) \ No newline at end of file +}) +test("hookup", function(){ + $("#qunit-test-area").html(""); + + $("#qunit-test-area").html("//jquery/view/test/qunit/hookup.ejs",{}); //makes sure no error happens +}) + +test("inline templates other than 'tmpl' like ejs", function(){ + $("#qunit-test-area").html(""); + + $("#qunit-test-area").html($('')); + + $("#qunit-test-area").html('test_ejs', {name: 'Henry'}); + equal( $("#new_name").text(), 'Henry'); + $("#qunit-test-area").html(""); +}); + +test("object of deferreds", function(){ + var foo = $.Deferred(), + bar = $.Deferred(); + stop(); + $.View("//jquery/view/test/qunit/deferreds.ejs",{ + foo : foo.promise(), + bar : bar + }).then(function(result){ + equals(result, "FOO and BAR"); + start(); + }); + setTimeout(function(){ + foo.resolve("FOO"); + },100); + bar.resolve("BAR"); + +}); + +test("deferred", function(){ + var foo = $.Deferred(); + stop(); + $.View("//jquery/view/test/qunit/deferred.ejs",foo).then(function(result){ + equals(result, "FOO"); + start(); + }); + setTimeout(function(){ + foo.resolve({ + foo: "FOO" + }); + },100); + +}); + + +test("modifier with a deferred", function(){ + $("#qunit-test-area").html(""); + stop(); + + var foo = $.Deferred(); + $("#qunit-test-area").html("//jquery/view/test/qunit/deferred.ejs", foo ); + setTimeout(function(){ + foo.resolve({ + foo: "FOO" + }); + start(); + equals($("#qunit-test-area").html(), "FOO", "worked!"); + },100); + +}); + +test("jQuery.fn.hookup", function(){ + $("#qunit-test-area").html(""); + var els = $($.View("//jquery/view/test/qunit/hookup.ejs",{})).hookup(); + $("#qunit-test-area").html(els); //makes sure no error happens +}); + +test("non-HTML content in hookups", function(){ + $("#qunit-test-area").html(""); + $.View.hookup(function(){}); + $("#qunit-test-area textarea").val("asdf"); + equals($("#qunit-test-area textarea").val(), "asdf"); +}); + +test("html takes promise", function(){ + var d = $.Deferred(); + $("#qunit-test-area").html(d); + stop(); + d.done(function(){ + equals($("#qunit-test-area").html(), "Hello World", "deferred is working"); + start(); + }) + setTimeout(function(){ + d.resolve("Hello World") + },10) +}); + +test("val set with a template within a hookup within another template", function(){ + $("#qunit-test-area").html("//jquery/view/test/qunit/hookupvalcall.ejs",{}); +}) + +/*test("bad url", function(){ + $.View("//asfdsaf/sadf.ejs") +});*/ + +test("hyphen in type", function(){ + $(document.body).append("") + + $("#qunit-test-area").html('hyphenEjs',{}); + + ok( /Hyphen/.test( $("#qunit-test-area").html() ), "has hyphen" ); +}) + + diff --git a/view/tmpl/test.tmpl b/view/tmpl/test.tmpl new file mode 100644 index 00000000..f06c7690 --- /dev/null +++ b/view/tmpl/test.tmpl @@ -0,0 +1 @@ +{{if 1}}

                                                  Hello World

                                                  {{/if}} \ No newline at end of file diff --git a/view/tmpl/tmpl.js b/view/tmpl/tmpl.js index 391df6a3..377474df 100644 --- a/view/tmpl/tmpl.js +++ b/view/tmpl/tmpl.js @@ -1,6 +1,9 @@ -// jQuery Templating Plugin -// Copyright 2010, John Resig +// jQuery Templates Plugin +// http://github.com/jquery/jquery-tmpl +// +// Copyright Software Freedom Conservancy, Inc. // Dual licensed under the MIT or GPL Version 2 licenses. +// http://jquery.org/license /** * @class jQuery.tmpl @@ -20,137 +23,500 @@ * For more information on jQuery.tmpl read * [http://api.jquery.com/category/plugins/templates/ it's documentation]. */ -steal.plugins('jquery/view').then(function(){ +steal('jquery/view').then(function(){ // Override the DOM manipulation function - var oldManip = jQuery.fn.domManip; - + 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({ - render: function( data ) { - return this.map(function(i, tmpl){ - return jQuery.render( tmpl, data ); - }); + // 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] ); }, - - // This will allow us to do: .append( "template", dataObject ) - domManip: function( args ) { + + // 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.length > 1 && args[0].nodeType ) { - arguments[0] = [ jQuery.makeArray(args) ]; + 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 ); } - - if ( args.length === 2 && typeof args[0] === "string" && typeof args[1] !== "string" ) { - arguments[0] = [ jQuery.render( args[0], args[1] ) ]; + cloneIndex = 0; + if ( !appendToTmplItems ) { + jQuery.tmpl.complete( newTmplItems ); } - - return oldManip.apply( this, arguments ); + return this; } }); - + jQuery.extend({ - render: function( tmpl, data ) { - var fn; - - // Use a pre-defined template, if available - if ( jQuery.templates[ tmpl ] ) { - fn = jQuery.templates[ tmpl ]; - - // We're pulling from a script node - } else if ( tmpl.nodeType ) { - var node = tmpl, elemData = jQuery.data( node ); - fn = elemData.tmpl || jQuery.tmpl( node.innerHTML ); - } - - fn = fn || jQuery.tmpl( tmpl ); - - // We assume that if the template string is being passed directly - // in the user doesn't want it cached. They can stick it in - // jQuery.templates to cache it. - - if ( jQuery.isArray( data ) ) { - return jQuery.map( data, function( data, i ) { - return fn.call( data, jQuery, data, i ); - }); + // 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; + }, - } else { - return fn.call( data, jQuery, data, 0 ); + // 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; }, - - // You can stick pre-built template functions here - templates: {}, - + // 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("<").split(">").join(">").split('"').join(""").split("'").join("'"); + } + }); - tmplcmd: { - each: { - _default: [ null, "$i" ], - prefix: "jQuery.each($1,function($2){with(this){", - suffix: "}});" + 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': { - prefix: "if($1){", - suffix: "}" + "if": { + open: "if(($notnull_1) && $1a){", + close: "}" }, - 'else': { - prefix: "}else{" + "else": { + _default: { $1: "true" }, + open: "}else if(($notnull_1) && $1a){" }, - html: { - prefix: "_.push(typeof $1==='function'?$1.call(this):$1);" + "html": { + // Unecoded expression evaluation. + open: "if($notnull_1){_.push($1a);}" }, "=": { - _default: [ "this" ], - prefix: "_.push($.encode(typeof $1==='function'?$1.call(this):$1));" + // Encoded expression evaluation. Abbreviated form is ${}. + _default: { $1: "$data" }, + open: "if($notnull_1){_.push($.encode($1a));}" + }, + "!": { + // Comment tag. Skipped by parser + open: "" } }, - encode: function( text ) { - return text != null ? document.createTextNode( text.toString() ).nodeValue : ""; + // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events + complete: function( items ) { + newTmplItems = {}; }, - tmpl: function(str, data, i) { - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - var fn = new Function("jQuery","$data","$i", - "var $=jQuery,_=[];_.data=$data;_.index=$i;" + + // 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 ); + } - // Introduce the data as local variables using with(){} - "with($data){_.push('" + + 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); + } - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .replace(/\${([^}]*)}/g, "{{= $1}}") - .replace(/{{(\/?)(\w+|.)(?:\((.*?)\))?(?: (.*?))?}}/g, function(all, slash, type, fnargs, args) { - var tmpl = jQuery.tmplcmd[ type ]; + // 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;" + - if ( !tmpl ) { - throw "Template not found: " + type; - } + // Introduce the data as local variables using with(){} + "with($data){_.push('" + - var def = tmpl._default; + // 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(""); + } - return "');" + tmpl[slash ? "suffix" : "prefix"] - .split("$1").join(args || def[0]) - .split("$2").join(fnargs || def[1]) + "_.push('"; - }) - + "');}return $(_.join('')).get();"); + 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; + } - // Provide some basic currying to the user - return data ? fn.call( this, jQuery, data, i ) : fn; + // 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 jQuery.render( text, data ); - } + return tmpl.call($, $, {data: data}).join(''); + //$(text).tmpl(data);//jQuery.render( text, data ); + }; }, script: function( id, str ) { - var tmpl = $.tmpl(str).toString(); - return "function(data){return ("+tmpl+").call(jQuery, jQuery, data); }"; + var tmpl = $.template( null, str ); + return "function(data){return ("+tmpl+").call(jQuery, jQuery, {data: data}).join(''); }"; } }) jQuery.View.ext = ".tmpl" diff --git a/view/tmpl/tmpl_test.js b/view/tmpl/tmpl_test.js new file mode 100644 index 00000000..92ac9a7a --- /dev/null +++ b/view/tmpl/tmpl_test.js @@ -0,0 +1,11 @@ +steal('funcunit/qunit','jquery/view/tmpl').then(function(){ +// use the view/qunit.html test to run this test script +module("jquery/view/tmpl") + +test("ifs work", function(){ + $("#qunit-test-area").html(""); + + $("#qunit-test-area").html("//jquery/view/tmpl/test.tmpl",{}); + ok($("#qunit-test-area").find('h1').length, "There's an h1") +}) +}); diff --git a/view/view.html b/view/view.html index 0332f33c..da9669b5 100644 --- a/view/view.html +++ b/view/view.html @@ -47,7 +47,7 @@

                                                  TMPL

                                                  \ No newline at end of file diff --git a/view/view.js b/view/view.js index 36a0075e..069e8899 100644 --- a/view/view.js +++ b/view/view.js @@ -1,132 +1,197 @@ -if ( window.jQuery && jQuery.Controller ) { - steal.plugins("jquery/controller/view"); -} -steal.plugins("jquery").then(function( $ ) { +steal("jquery").then(function( $ ) { - // converts to an ok dom id + // 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 - * @tag core + * @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], + * - 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.replace replace], [jQuery.fn.replaceWith replaceWith], [jQuery.fn.text text] like: - * @codestart - * $('.foo').html("//path/to/view.ejs",{}) - * @codeend - *
                                                  • - *
                                                  • Compress processed views with [steal.static.views].
                                                  • - *
                                                  • Use the [jQuery.Controller.prototype.view controller/view] plugin to auto-magically - * lookup views.
                                                  • - *
                                                  • Hookup jquery plugins directly in the template.
                                                  • + * [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: + *
                                                    <h2><%= message %></h2>
                                                    + * + * - Renders it with {message: 'hello world'}, resulting in: + *
                                                    <div id='foo'>"<h2>hello world</h2></div>
                                                    * - *
                                                  + * - Inserts the result into the foo element. Foo might look like: + *
                                                  <div id='foo'><h2>hello world</h2></div>
                                                  * - * ## Supported Template Engines + * ## jQuery Modifiers * - * JavaScriptMVC comes with the following template languages: + * You can use a template with the following jQuery modifiers: * - * - [jQuery.EJS EJS] - provides an ERB like syntax: <%= %> - * - [Jaml] - A functional approach to JS templates. - * - [Micro] - A very lightweight template similar to EJS. - * - [jQuery.tmpl] - A very lightweight template similar to EJS. + * + * + * + * + * + * + * + * + *
                                                  [jQuery.fn.after after] $('#bar').after('temp.jaml',{});
                                                  [jQuery.fn.append append] $('#bar').append('temp.jaml',{});
                                                  [jQuery.fn.before before] $('#bar').before('temp.jaml',{});
                                                  [jQuery.fn.html html] $('#bar').html('temp.jaml',{});
                                                  [jQuery.fn.prepend prepend] $('#bar').prepend('temp.jaml',{});
                                                  [jQuery.fn.replaceWith replaceWith] $('#bar').replaceWith('temp.jaml',{});
                                                  [jQuery.fn.text text] $('#bar').text('temp.jaml',{});
                                                  * - * There are 3rd party plugins that provide other template - * languages. + * You always have to pass a string and an object (or function) for the jQuery modifier + * to user a template. * - * ## Use + * ## Template Locations + * + * View can load from script tags or from files. * - * Views provide client side templating. When you use a view, you're - * almost always wanting to insert the rendered content into the page. + * ## From Script Tags * - * For this reason, the most common way to use a views is through - * jQuery modifier functions like [jQuery.fn.html html]. The view - * plugin overwrites these functions so you can render a view and - * insert its contents into the page with one convenient step. + * To load from a script tag, create a script tag with your template and an id like: * - * The following renders the EJS template at - * //app/view/template.ejs with the second parameter used as data. - * It inserts the result of the template into the - * '#foo' element. + *
                                                  <script type='text/ejs' id='recipes'>
                                                  +	 * <% for(var i=0; i < recipes.length; i++){ %>
                                                  +	 *   <li><%=recipes[i].name %></li>
                                                  +	 * <%} %>
                                                  +	 * </script>
                                                  + * + * Render with this template like: * * @codestart - * $('#foo').html('//app/view/template.ejs', - * {message: "hello world"}) + * $("#foo").html('recipes',recipeData) * @codeend * - * //app/view/template.ejs might look like: + * Notice we passed the id of the element we want to render. * - * @codestart xml - * <h2><%= message %></h2></div> - * @codeend + * ## From File * - * The resulting output would be: + * You can pass the path of a template file location like: * - * @codestart xml - * <div id='foo'><h2>hello world</h2></div> - * @codeend + * $("#foo").html('templates/recipes.ejs',recipeData) * - * The specifics of each templating languages are covered in their - * individual documentation pages. + * 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: * - * ### Template Locations + * $("#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: * - * In the example above, we used - * //app/view/template.ejs as the location of - * our template file. Using // at the start of a path - * references the template from the root JavaScriptMVC directory. + * $("#foo").html( this.view('recipes', recipeData) ) * - * If there is no // at the start of the path, the view is looked up - * relative to the current page. + * ## Packaging Templates * - * It's recommended that you use paths rooted from the JavaScriptMVC - * directory. This will make your code less likely to change. + * If you're making heavy use of templates, you want to organize + * them in files so they can be reused between pages and applications. * - * You can also use the [jQuery.Controller.prototype.view controller/view] - * plugin to make looking up templates a little easier. + * 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. * - * ### Using $.View + * Fortunately, [steal.static.views steal.views] can build templates + * into your production files. You just have to point to the view file like: * - * Sometimes you want to get the string result of a view and not - * insert it into the page right away. Nested templates are a good - * example of this. For this, you can use $.View. The following - * iterates through a list of contacts, and inserts the result of a - * sub template in each: + * steal.views('path/to/the/view.ejs'); + * + * ## Asynchronous * - * @codestart xml - * <% for(var i =0 ; i < contacts.length; i++) { %> - * <%= $.View("//contacts/contact.ejs",contacts[i]) %> - * <% } %> - * @codeend + * By default, retrieving requests is done synchronously. This is + * fine because StealJS packages view templates with your JS download. * - * ## Compress Views with Steal + * 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: * - * Steal can package processed views in the production - * file. Because 'stolen' views are already - * processed, they don't rely on eval and are much faster. Here's - * how to steal them: + * $("#foo").html('recipes',recipeData, function(result){ + * this.fadeIn() + * }); * - * @codestart - * steal.views('//views/tasks/show.ejs'); - * @codeend + * 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: * - * Read more about [steal.static.views steal.views]. + * - EmbeddedJS + *
                                                  <h2><%= message %></h2>
                                                  + * + * - JAML + *
                                                  h2(data.message);
                                                  + * + * - Micro + *
                                                  <h2>{%= message %}</h2>
                                                  + * + * - jQuery.Tmpl + *
                                                  <h2>${message}</h2>
                                                  + * + * The popular Mustache + * template engine is supported in a 2nd party plugin. * - *

                                                  $.View

                                                  + * ## 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. @@ -141,7 +206,8 @@ steal.plugins("jquery").then(function( $ ) { * 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){ + * $.View("//myplugin/views/init.ejs", + * {message: "Hello World"}, function(result){ * // do something with result * }) * @codeend @@ -153,93 +219,239 @@ steal.plugins("jquery").then(function( $ ) { * @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. + * @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, render, checkText, get; - - $view = $.View = function( view, data, helpers, callback ) { - var suffix = view.match(/\.[\w\d]+$/), - type, el, id, renderer, url = 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; } - //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); + // see if we got passed any deferreds + var deferreds = getDeferreds(data); - //if a absolute path, use steal to get it - if ( url.match(/^\/\//) ) { - url = steal.root.join(url.substr(2)); //can steal be removed? - } - //get the template engine - type = $.View.types[suffix]; - - //get the renderer function - renderer = - $.View.cached[id] ? // is it cached? - $.View.cached[id] : // use the cached version - ((el = document.getElementById(view)) ? //is it in the document? - type.renderer(id, el.innerHTML) : //use the innerHTML of the elemnt - get(type, id, url, data, helpers, callback) //do an ajax request for it - ); - // we won't always get a renderer (if async ajax) - return renderer && render(renderer, type, id, data, helpers, callback); - }; - // caches the template, renders the content, and calls back if it should - render = function( renderer, type, id, data, helpers, callback ) { - var res, stub; - if ( $.View.cache ) { - $.View.cached[id] = renderer; + 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(); } - res = renderer.call(type, data, helpers); - stub = callback && callback(res); - return res; - }; - // makes sure there's a template - checkText = function( text, url ) { - if (!text.match(/[^\s]/) ) { - throw "$.View ERROR: There is no template or an empty template at " + url; + 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; } - }; - // gets a template, if there's a callback, renders and calls back its;ef - get = function( type, id, url, data, helpers, callback ) { - if ( callback ) { - $.ajax({ + }, + // makes sure there's a template, if not, has steal provide a warning + checkText = function( text, url ) { + if (!text.match(/[^\s]/) ) { + steal.dev.log("There is no template or an empty template at " + url) + 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: "text", - error: function() { - checkText("", url); - }, - success: function( text ) { - checkText(text, url); - render(type.renderer(id, text), type, id, data, helpers, callback); - } + dataType: "view", + async: async }); - } else { - var text = $.ajax({ - async: false, - url: url, - dataType: "text", - error: function() { - checkText("", url); + }, + // 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; } - }).responseText; - checkText(text, url); - return type.renderer(id, text); + // 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 '' 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) +''; + } - $.extend($.View, { + //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 @@ -248,7 +460,17 @@ steal.plugins("jquery").then(function( $ ) { hookups: {}, /** * @function hookup - * Registers a hookup function to be called back after the html is put on the page + * 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 = "
                                                  " + * $('.foo').html(html); + * + * * @param {Function} cb a callback function to be called with the element * @param {Number} the hookup number */ @@ -272,9 +494,38 @@ steal.plugins("jquery").then(function( $ ) { * @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: *
                                                    + *
                                                  • plugin - the location of the plugin. EX: 'jquery/view/ejs'
                                                  • *
                                                  • suffix - the view extension. EX: 'ejs'
                                                  • *
                                                  • 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).
                                                  • @@ -284,6 +535,16 @@ steal.plugins("jquery").then(function( $ ) { */ 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: {}, /** @@ -300,7 +561,7 @@ steal.plugins("jquery").then(function( $ ) { * @param {Object} src */ registerScript: function( type, id, src ) { - return "$.View.preload('" + id + "'," + $.View.types["." + type].script(id, src) + ");"; + return "$.View.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");"; }, /** * @hide @@ -310,27 +571,52 @@ steal.plugins("jquery").then(function( $ ) { * @param {Function} renderer */ preload: function( id, renderer ) { - $.View.cached[id] = function( data, helpers ) { + $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, getCallback, hookupView, funcs; + 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 ) { - var old = jQuery.fn[func_name]; - - jQuery.fn[func_name] = function() { - var args = $.makeArray(arguments), - callbackNum, callback, self = this; + // 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 - if ( isTemplate(args) ) { + else if ( isTemplate(args) ) { // if we should operate async if ((callbackNum = getCallback(args))) { @@ -339,60 +625,102 @@ steal.plugins("jquery").then(function( $ ) { modify.call(self, [result], old); callback.call(self, result); }; - $.View.apply($.View, args); + $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; } - - //otherwise do the template now - args = [$.View.apply($.View, args)]; } - - return modify.call(this, args, old); + return noHookup[func_name] ? old.apply(this,args) : + modify.call(this, args, old); }; }; - // modifies the html of the element + + // modifies the content of the element + // but also will run any hookup modify = function( args, old ) { - var res, stub; + var res, stub, hooks; //check if there are new hookups - for ( var hasHookups in jQuery.View.hookups ) { - // Having arbitrary code in here makes JSLint pass - if ( true ) { - stub = true; - } + for ( var hasHookups in $view.hookups ) { + break; } //if there are hookups, get jQuery object - if ( hasHookups ) { + if ( hasHookups && args[0] && isHTML(args[0]) ) { + hooks = $view.hookups; + $view.hookups = {}; args[0] = $(args[0]); } res = old.apply(this, args); - //now hookup hookups - if ( hasHookups ) { - hookupView(args[0]); + //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]; - - return typeof args[0] == "string" && (secArgType == 'object' || secArgType == 'function') && !args[1].nodeType && !args[1].jquery; + + // 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 if there is one (for async view use) + //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 ) { + hookupView = function( els, hooks ) { //remove all hookups - var hooks = jQuery.View.hookups, - hookupEls, len, i = 0, + var hookupEls, len, i = 0, id, func; - jQuery.View.hookups = {}; + 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++ ) { @@ -403,65 +731,150 @@ steal.plugins("jquery").then(function( $ ) { } } //copy remaining hooks back - $.extend(jQuery.View.hookups, hooks); + $.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 */ - funcs = [ + $.fn.hookup = function() { + var hooks = $view.hookups; + $view.hookups = {}; + hookupView(this, hooks); + return this; + }; + + /** + * @add jQuery.fn + */ + $.each([ /** * @function prepend * @parent jQuery.View - * abc + * + * 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 - * abc + * + * 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 - * abc + * + * 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 - * abc + * + * 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 replace - * @parent jQuery.View - * abc - */ - "replace", /** * @function text * @parent jQuery.View - * abc + * + * 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 - * abc + * + * 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 - * abc + * + * 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"]; + "replaceWith", "val"],function(i, func){ + convert(func); + }); //go through helper funcs and convert - for ( var i = 0; i < funcs.length; i++ ) { - convert(funcs[i]); - } + }); \ No newline at end of file diff --git a/view/vieww.html b/view/vieww.html deleted file mode 100644 index 35717704..00000000 --- a/view/vieww.html +++ /dev/null @@ -1,290 +0,0 @@ - -

                                                    - Everyone loves client side templates. - They are a great way to create html - which is something JavaScript apps do all the time. -

                                                    - -

                                                    In February, a jQuery templating system was -proposed and resulted in a tremendous amount of -discussion, followed by an -official templating engine for jQuery - jquery-tmpl. -

                                                    - -

                                                    -Although jquery-tmpl is a solid templating engine, -the discussion highlighted three -extremely important facts about developers and client side templates: -

                                                    - -

                                                    -Fact 1: Everyone has their favorite templating engine -

                                                    - -

                                                    -There's a whole slew of templating languages people like: -

                                                    - - - -

                                                    -Most of these template engines have distinct advantages and dissadvantages. -It's impossible to expect a single template engine to meet everyone's needs. -

                                                    -

                                                    Fact 2: Most templating engines provide the exact same features

                                                    -

                                                    - I've yet to encounter a template that does't provide: -

                                                    -
                                                      -
                                                    • A way of loading templates (typically from elements or files)
                                                    • -
                                                    • A way of caching processed templates.
                                                    • -
                                                    • An interface to render the template with arbitrary data.
                                                    • -
                                                    -

                                                    Fact 3: Very few people are familiar with -the complexities of using templates

                                                    -

                                                    There's more than just syntax and magic tag preference that goes -into a templating system, consider:

                                                    -
                                                      -
                                                    • How can I build and share plugin that uses templates?
                                                    • -
                                                    • How can I share templates across pages / apps?
                                                    • -
                                                    • How can I organize template files?
                                                    • -
                                                    -

                                                    jQuery.View

                                                    - -

                                                    -jQuery.View -is a templating interface that takes -care of the complexities of using templates, -while being completely template agnostic. -

                                                    -

                                                    This means that you can use any templating language in the exact -same way and get all the additional features that jQuery.View provides.

                                                    - -

                                                    Features

                                                    -
                                                      -
                                                    • Convenient syntax.
                                                    • -
                                                    • Template loading from html elements and external files.
                                                    • -
                                                    • Synchronous and asynchronous template loading.
                                                    • -
                                                    • Template preloading.
                                                    • -
                                                    • Caching of processed templates.
                                                    • -
                                                    • Bundling of processed templates in production builds.
                                                    • -
                                                    -

                                                    Downloads

                                                    -
                                                      -
                                                    • jquery.view.js
                                                    • -
                                                    • jquery.view.ejs.js
                                                    • -
                                                    • jquery.view.jaml.js
                                                    • -
                                                    • jquery.view.micro.js
                                                    • -
                                                    • jquery.view.tmpl.js
                                                    • -
                                                    -

                                                    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:

                                                    -
                                                      -
                                                    1. -

                                                      Loads the template a 'mytemplate.ejs'. It might look like:

                                                      -
                                                      <h2><%= message %></h2>
                                                      -
                                                    2. -
                                                    3. -

                                                      renders it with {message: 'hello world'}, resulting in:

                                                      -
                                                      "<h2>hello world</h2>"
                                                      -
                                                    4. -
                                                    5. -

                                                      Inserts the result into the foo element. Foo might look like:

                                                      -
                                                      <div id='foo'>"<h2>hello world</h2></div>
                                                      -
                                                    6. -
                                                    - - - -

                                                    jQuery Modifiers

                                                    -

                                                    -You can use a template with the following jQuery modifier methods: -

                                                    - - - - - - - - - -
                                                    after $('#bar').after('temp.jaml',{});
                                                    append $('#bar').append('temp.jaml',{});
                                                    before $('#bar').before('temp.jaml',{});
                                                    html $('#bar').html('temp.jaml',{});
                                                    prepend $('#bar').prepend('temp.jaml',{});
                                                    replace $('#bar').replace('temp.jaml',{});
                                                    replaceWidth $('#bar').replaceWidth('temp.jaml',{});
                                                    text $('#bar').text('temp.jaml',{});
                                                    -

                                                    -Template Locations -

                                                    - -

                                                    -View can load from script tags or from files. To load from a script tag, create a script tag with your template and an id like: -

                                                    - -

                                                    -

                                                    <script type='text/ejs' id='recipes'>
                                                    -<% for(var i=0; i < recipes.length; i++){ %>
                                                    -  <li><%=recipes[i].name %></li>
                                                    -<%} %>
                                                    -</script>
                                                    -

                                                    - -

                                                    -Render with this template like: -

                                                    -
                                                    $("#foo").html('recipes',recipeData)
                                                    -

                                                    Notice we passed the id of the element we want to render.

                                                    - -

                                                    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 would have -to retrieve each template individually. The additional HTTP requests would slow -down your app. -

                                                    - -

                                                    -Use StealJS to build templates into your production files. -You just have to point to the view file like: -

                                                    - -
                                                    steal.views('path/to/the/view.ejs');
                                                    -
                                                    -

                                                    This will pre-process the view and insert it into a compressed single file with -your other code.

                                                    -

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

                                                    - -

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

                                                    - -
                                                    $.view('path/to/template.jaml',{}, function(){});
                                                    -

                                                    -When it comes time to use them in your app, they will be ready for the user. -

                                                    -

                                                    -Supported Templates -

                                                    -

                                                    -JavaScriptMVC comes with the following templates: -

                                                    -
                                                      -
                                                    • EmbeddedJS

                                                      -
                                                      <h2><%= message %></h2>
                                                    • -
                                                    • JAML

                                                      -
                                                      h2(data.message);
                                                    • -
                                                    • Micro

                                                      -
                                                      <h2>{%= message %}</h2>
                                                    • -
                                                    • jQuery.Tmpl

                                                      -
                                                      <h2>${message}</h2>
                                                    • -
                                                    -

                                                    Mustache is supported in a 2nd party plugin. - -

                                                    - -

                                                    -Using Other Templates: -

                                                    - -

                                                    -Integrating into $.View (and StealJS's build process) is easy, you just have to register your script like: -

                                                    - -
                                                    $.View.register({
                                                    -	suffix : "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); }";
                                                    -	}
                                                    -})
                                                    - -

                                                    -Here's what each property does:

                                                    -
                                                      -
                                                    • 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.
                                                    • -
                                                    - -

                                                    Conclusion

                                                    -

                                                    - Templates are great, but there's a lot of extra work that goes into - making a template engine useful. - But, almost all of that extra work can be abstracted and reused. -

                                                    -

                                                    This is exactly what jQuery.View is! It's a tool so future template -engines don't have to worry about loading, caching, and bundling templates.

                                                    -

                                                    Even better, as their is a uniform template API, it enables plugin authors -to write widgets that accept arbitrary template types.

                                                    - -

                                                    I personally feel like this would be a good canidate for jQuery an -official jQuery plugin of its own. Imagine customizing the layout of a -widget by passing it a template: - -

                                                    - -
                                                    $("#upcoming").srchr_search_result({
                                                    -	modelType : Srchr.Models.Upcoming,
                                                    -	resultView : "//srchr/views/upcoming.ejs"
                                                    -});
                                                    -

                                                    P.S. This is actual code from our -JavaScriptMVC version of Srchr. -We customize search results panels with a Model used to retrieve -searches and a view to output the results. - -

                                                    \ No newline at end of file