diff --git a/README b/README index 12846a17..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_/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 60737856..1d840746 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,6 @@ // load('jquery/build.js') -load('steal/rhino/steal.js') +load('steal/rhino/rhino.js') var i, fileName, cmd, plugins = [ @@ -16,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"]}, @@ -38,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", @@ -96,7 +90,8 @@ var i, fileName, cmd, ] -steal.plugins('steal/build/pluginify').then( function(s){ +steal.File('jquery/dist').mkdir(); +steal('steal/build/pluginify').then( function(s){ var plugin, exclude, fileDest, fileName; for(i=0; i< 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, - //hash of names to steals + files = {}, depends = function(stl, steals){ if(stl.dependencies){ @@ -41,7 +88,7 @@ steal.plugins('steal/build/pluginify','steal/build/apps','steal/build/scripts'). var depend = stl.dependencies[d]; if(!steals[depend.path]){ steals[depend.path] = true; - print(" " + depend.path); + print("123 " + depend.path); //depends(depend, steals); } @@ -70,34 +117,14 @@ steal.plugins('steal/build/pluginify','steal/build/apps','steal/build/scripts'). if(stl.dependencies){ for (var d = 0; d < stl.dependencies.length; d++) { var depend = stl.dependencies[d]; - dependencies.push(depend.path); + if (depend.path !== "jquery/jquery.js") { + dependencies.push(depend.path); + } } } - }) + })*/ + - steal.File("jquery/dist/standalone").mkdir(); - steal.File("jquery/dist/standalone/dependencies.json").save($.toJSON(files)); - //get each file ... - print("Creating jquery/dist/standalone/") - var compressor = steal.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 = steal.build.builders.scripts.clean(content); - print(" "+out+""); - content = steal.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)); - } }) \ 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 9433e9b7..b48d1d00 100644 --- a/class/class.js +++ b/class/class.js @@ -2,26 +2,31 @@ // 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.plugins("jquery","jquery/lang").then(function( $ ) { +//!steal-clean +steal("jquery","jquery/lang/string",function( $ ) { - // if we are initializing a new class + // =============== 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 + // 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 ) { @@ -46,16 +51,17 @@ steal.plugins("jquery","jquery/lang").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 + * @description Easy inheritance in JavaScript. * - * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between + * 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: @@ -67,6 +73,8 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * - 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 @@ -93,7 +101,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * count is incremented. * * @codestart - * $.Class.extend('Monster', + * $.Class('Monster', * /* @static *| * { * count: 0 @@ -109,7 +117,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * this.health = 10; * * // increments count - * this.Class.count++; + * this.constructor.count++; * }, * eat: function( smallChildren ){ * this.health += smallChildren; @@ -145,7 +153,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * efficient at eating small children, but more powerful fighters. * * - * Monster.extend("SeaMonster",{ + * Monster("SeaMonster",{ * eat: function( smallChildren ) { * this._super(smallChildren / 2); * }, @@ -162,12 +170,12 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * * You can also inherit static properties in the same way: * - * $.Class.extend("First", + * $.Class("First", * { * staticMethod: function() { return 1;} * },{}) * - * First.extend("Second",{ + * First("Second",{ * staticMethod: function() { return this._super()+1;} * },{}) * @@ -179,26 +187,29 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * 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. @@ -210,7 +221,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * * * @codestart - * $.Class.extend("MyClass", + * $.Class("MyClass", * { * setup: function() {} //static setup * init: function() {} //static constructor @@ -221,97 +232,138 @@ steal.plugins("jquery","jquery/lang").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. */ clss = $.Class = function() { if (arguments.length) { - clss.extend.apply(clss, arguments); + return clss.extend.apply(clss, arguments); } }; /* @Static*/ 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; @@ -320,11 +372,11 @@ steal.plugins("jquery","jquery/lang").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; @@ -334,14 +386,15 @@ steal.plugins("jquery","jquery/lang").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; @@ -355,44 +408,55 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * next function. * @return {Function} the callback function. */ - callback: function( funcs ) { + proxy: function( funcs ) { //args that should be curried var args = makeArray(arguments), self; + // get the functions to callback funcs = args.shift(); + // if there is only one function, make funcs into an array if (!isArray(funcs) ) { funcs = [funcs]; } - + + // keep a reference to us in self self = this; - //@steal-remove-start + + //!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 + //!steal-remove-end return function class_cb() { + // add the arguments after the curried args var cur = concatArgs(args, arguments), isString, length = funcs.length, f = 0, func; - + + // go through each function to call back for (; f < length; f++ ) { func = funcs[f]; if (!func ) { continue; } - + + // set called with the name of the function on self (this is how this.view works) isString = typeof func == "string"; if ( isString && self._set_called ) { self.called = func; } + + // call the function cur = (isString ? self[func] : func).apply(self, cur || []); + + // pass the result to the next function (if there is a next function) if ( f < length - 1 ) { cur = !isArray(cur) || cur._use_call ? [cur] : cur } @@ -400,37 +464,27 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { 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: $.String.getObject, /** * @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); } @@ -463,33 +517,47 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * @param {Object} protoProps the prototype properties of the new class */ setup: function( baseClass, fullName ) { + // set defaults as the merger of the parent defaults and this object's defaults this.defaults = extend(true, {}, baseClass.defaults, this.defaults); return arguments; }, rawInstance: function() { + // prevent running init initializing = true; var inst = new this(); initializing = false; + // allow running init return inst; }, /** * Extends a class with new static and prototype functions. There are a variety of ways * to use extend: - * @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; @@ -502,7 +570,7 @@ steal.plugins("jquery","jquery/lang").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, @@ -510,16 +578,17 @@ steal.plugins("jquery","jquery/lang").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 + // 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) @@ -532,7 +601,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { } } - // copy new props on class + // copy new static props on class inheritProps(klass, this, Class); // do namespace stuff @@ -540,47 +609,66 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { var parts = fullName.split(/\./), shortName = parts.pop(), - current = clss.getObject(parts.join('.'), window, true), + current = getObject(parts.join('.'), window, true), namespace = current; - //@steal-remove-start + //!steal-remove-start if (!Class.nameOk ) { //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, { 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 - */ + + // 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*/ @@ -651,24 +739,25 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { */ //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. */ } @@ -678,18 +767,19 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { - clss.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 = clss.callback; + proxy = clss.proxy; -})(); \ No newline at end of file +})(); diff --git a/class/class_test.js b/class/class_test.js index 89dc0495..ec95179c 100644 --- a/class/class_test.js +++ b/class/class_test.js @@ -1,6 +1,5 @@ -steal - .plugins("jquery/class") //load your app - .plugins('funcunit/qunit').then(function(){ +steal("jquery/class") //load your app + .then('funcunit/qunit').then(function(){ module("jquery/class"); diff --git a/controller/controller.html b/controller/controller.html index 43604741..a76e2ace 100644 --- a/controller/controller.html +++ b/controller/controller.html @@ -50,9 +50,7 @@ - \ No newline at end of file diff --git a/controller/controller.js b/controller/controller.js index 3f42a763..f3bd8476 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -1,6 +1,6 @@ -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, @@ -27,23 +27,33 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func 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.nodeName ? $(this) : 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 @@ -64,50 +74,57 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func return $.data(el, "controllers", data) }; /** - * @tag core + * @class jQuery.Controller + * @parent jquerymx * @plugin jquery/controller * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/controller/controller.js * @test jquery/controller/qunit.html + * @inherits jQuery.Class + * @description jQuery widget factory. * - * 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. + * + * This means it is used to + * create things like tabs, grids, and contextmenus as well as + * organizing them into higher-order business rules. * * Controllers make your code deterministic, reusable, organized and can tear themselves * down auto-magically. Read about [http://jupiterjs.com/news/writing-the-perfect-jquery-plugin * the theory behind controller] and * a [http://jupiterjs.com/news/organize-jquery-widgets-with-jquery-controller walkthrough of its features] - * on Jupiter's blog. + * on Jupiter's blog. [mvc.controller Get Started with jQueryMX] also has a great walkthrough. * + * Controller inherits from [jQuery.Class $.Class] and makes heavy use of + * [http://api.jquery.com/delegate/ event delegation]. Make sure + * you understand these concepts before using it. * * ## Basic Example * * Instead of * - * @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('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 Controller * * Controller helps you build and organize jQuery plugins. It can be used @@ -137,8 +154,10 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * } * }) * - * This creates a $.fn.my_widget [jquery.controller.plugin jQuery helper function] - * that can be used to create a new controller instance on an element. + * 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 * @@ -196,19 +215,17 @@ 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("
  • New Todo
  • "); + * } + * }) * * Now that you've created the controller class, you've must attach the event handlers on the '#todos' div by * creating [jQuery.Controller.prototype.setup|a new controller instance]. There are 2 ways of doing this. @@ -227,15 +244,13 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * * 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 * @@ -297,6 +312,9 @@ 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("jQuery.Controller", /** @@ -304,19 +322,22 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func */ { /** - * 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; @@ -326,6 +347,18 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func this._shortName = underscoreAndRemoveController(this.shortName); var controller = this, + /** + * @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; @@ -335,7 +368,7 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func 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]; return this.each(function() { //check if created @@ -361,30 +394,22 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func } // make sure listensTo is an array - //@steal-remove-start + //!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 (funcName == 'constructor' || !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._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); @@ -405,19 +430,41 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func }, /** * @hide + * This takes a method name and the options passed to a controller + * and tries to return the data necessary to pass to a processor + * (something that binds things). + * + * For performance reasons, this called twice. First, it is called when + * the Controller class is created. If the methodName is templated + * like : "{window} foo", it returns null. If it is not templated + * it returns event binding data. + * + * The resulting data is added to this.actions. + * + * When a controller instance is created, _action is called again, but only + * on templated actions. + * * @param {Object} methodName the method that will be bound * @param {Object} [options] first param merged with class default options * @return {Object} null or the processor and pre-split parts. * The processor is what does the binding/subscribing. */ _action: function( methodName, options ) { - //if we don't have a controller instance, we'll break this guy up later + // reset the test index parameterReplacer.lastIndex = 0; + + //if we don't have options (a controller instance), we'll run this later if (!options && parameterReplacer.test(methodName) ) { return null; } + // If we have options, run sub to replace templates "{}" with a value from the options + // or the window var convertedName = options ? Str.sub(methodName, [options, window]) : methodName, + + // If a "{}" resolves to an object, convertedName will be an array arr = isArray(convertedName), + + // get the parts of the function = [convertedName, delegatePart, eventPart] parts = (arr ? convertedName[1] : convertedName).match(breaker), event = parts[2], processor = processors[event] || basicProcessor; @@ -470,19 +517,12 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * - select * - submit * - * The following processors always listen on the window or document: - * - * - windowresize - * - windowscroll - * - load - * - unload - * - hashchange - * - ready - * - * Which means anytime the window is resized, the following controller will listen to it: - * + * Listen to events on the document or window + * with templated event handlers: + * + * * $.Controller('Sized',{ - * windowresize : function(){ + * "{window} resize" : function(){ * this.element.width(this.element.parent().width() / 2); * } * }); @@ -492,7 +532,8 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func 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',{ @@ -504,6 +545,7 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * }) * * $('.foo').tab_panel().trigger("show"); + * */ listensTo: [], /** @@ -524,6 +566,9 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * * $("#el1").message(); //writes "Hello World" * $("#el12").message({message: "hi"}); //writes hi + * + * In [jQuery.Controller.prototype.setup setup] the options passed to the controller + * are merged with defaults. This is not a deep merge. */ defaults: {} }, @@ -534,19 +579,19 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func /** * Setup is where most of controller's magic happens. It does the following: * - * ### Sets this.element + * ### 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]. * - * ### Adds the controller's name to the element's className. + * ### 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. * - * ### Saves the controller in $.data + * ### 3. Saves the controller in $.data * * A reference to the controller instance is saved in $.data. You can find * instances of "Foo.Bar" like: @@ -557,33 +602,53 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * * Setup does the event binding described in [jquery.controller.listening Listening To Events]. * - * ## API * @param {HTMLElement} element the element this instance operates on. * @param {Object} [options] option values for the controller. These get added to - * this.options. + * 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[0] || element; + element = (typeof element == 'string' ? $(element) : + (element.jquery ? element : [element]) )[0]; + + //set element and className on element + var pluginname = cls.pluginName || cls._fullName; //set element and className on element - this.element = $(element).addClass(cls._fullName); + this.element = $(element).addClass(pluginname); //set in data - (data(element) || data(element, {}))[cls._fullName] = this; + (data(element) || data(element, {}))[pluginname] = this; - //adds bindings - this._bindings = []; + /** * @attribute options - * Options is [jQuery.Controller.static.defaults] merged with the 2nd argument + * + * 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 : { @@ -599,19 +664,13 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * $("#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 ) { - if ( cls.actions.hasOwnProperty(funcName) ) { - ready = cls.actions[funcName] || cls._action(funcName, this.options); - this._bindings.push( - ready.processor(ready.delegate || element, ready.parts[2], ready.parts[1], this.callback(funcName), this)); - } - } - + /** * @attribute called @@ -621,14 +680,8 @@ 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 @@ -675,28 +728,40 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * } * } */ - return this.element; + return [this.element, this.options].concat(makeArray(arguments).slice(2)); + /** + * @function init + * + * Implement this. + */ }, /** - * Bind attaches event handlers that will be removed when the controller is removed. - * This 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} [el=this.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 @@ -704,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; @@ -713,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.
    @@ -746,27 +850,105 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func return this._binder(element, eventName, func, selector); }, /** - * Called if an controller's [jquery.controller.plugin jQuery helper] is called on an element that already has a controller instance - * of the same type. Extends [jQuery.Controller.prototype.options 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); + this._unbind(); + this.bind(); }, /** * Destroy unbinds and undelegates all event handlers on this controller, @@ -784,32 +966,39 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * 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"); - * - * ### API + * */ 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 ) { - value(self.element[0]); - }); - + // unbind bindings + this._unbind(); + // clean up delete this._actions; delete this.element.data("controllers")[fname]; $(this).triggerHandler("destroyed"); //in case we want to know if the controller is removed + this.element = null; }, /** @@ -835,21 +1024,15 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func //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) && el === controller.element[0]) { //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); }; - //set commong 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 ) { + //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; }); /** @@ -860,45 +1043,47 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func //controllers can be strings or classes var i, isAControllerOf = function( instance, controllers ) { for ( i = 0; i < controllers.length; i++ ) { - if ( typeof controllers[i] == 'string' ? instance.Class._shortName == controllers[i] : instance instanceof controllers[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, 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); + $.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 index 19062eec..d94799e9 100644 --- a/controller/controller_test.js +++ b/controller/controller_test.js @@ -1,6 +1,5 @@ -steal - .plugins("jquery/controller",'jquery/controller/subscribe') //load your app - .plugins('funcunit/qunit') //load qunit +steal("jquery/controller",'jquery/controller/subscribe') //load your app + .then('funcunit/qunit') //load qunit .then(function(){ module("jquery/controller") @@ -60,54 +59,6 @@ test("subscribe testing works", function(){ }) -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 = { @@ -196,7 +147,7 @@ test("objects in action", function(){ "{item} someEvent" : function(thing, ev){ ok(true, "called"); equals(ev.type, "someEvent","correct event") - equals(this.Class.fullName, "Thing", "This is a controller isntance") + equals(this.constructor.fullName, "Thing", "This is a controller isntance") equals(thing.name,"Justin","Raw, not jQuery wrapped thing") } }); @@ -224,4 +175,95 @@ test("dot",function(){ $("#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 95befeb1..00000000 --- a/controller/history/history.js +++ /dev/null @@ -1,187 +0,0 @@ -steal.plugins('jquery/controller/subscribe', - 'jquery/event/hashchange', - 'jquery/lang/deparam').then(function($){ - -/** - * @page jquery.controller.history History Events - * @parent jQuery.Controller - * @plugin jquery/controller/history - * The jquery/controller/history plugin adds - * browser hash (#) based history support. - * - * It allows you to listen to hashchange events with OpenAjax.hub. - * - * Typically you subscribe to a history event in your controllers: - * - * $.Controller("MyHistory",{ - * "history.pagename subscribe" : function(called, data){ - * //called when hash = #pagename - * } - * }) - * - * ## Event Names - * - * When a history event happens, an OpenAjax message is produced that - * starts with "history.". The remainder of the message name depends on the - * value of the "hash". - * - * The following shows hash values and - * the corresponding published message and data. - * - * "#foo=bar" -> "history.index" {foo: bar} - * "#foo/bar" -> "history.foo.bar" {} - * "#foo&bar=baz" -> "history.foo" {bar: baz} - * - * Essentially, if the hash starts with something like #foo/bar, this gets - * added to the message name as "foo.bar". Once "&" is found, it adds the remainder - * as name-value pairs to the message data. - * - * ## Controller Helper Functions - * - * The methods on the left are added to Controller.prototype and make it easier to - * make changes to history. - * - */ - -var keyBreaker = /([^\[\]]+)|(\[\])/g; - -$.Controller.History = { - /** - * @hide - * returns the pathname part - * - * // if the url is "#foo/bar&foo=bar" - * $.Controller.History.pathname() -> 'foo/bar' - * - */ - pathname : function(path) { - var parts = path.match(/#([^&]*)/); - return parts ? parts[1] : null - }, - /** - * @hide - * returns the search part, but without the first & - * - * // if the url is "#foo/bar&foo=bar" - * $.Controller.History.search() -> 'foo=bar' - */ - search : function(path) { - var parts = path.match(/#[^&]*&(.*)/); - return parts ? parts[1] : null - }, - /** - * @hide - * Returns the data - * @param {Object} path - */ - 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") - return $.String.deparam(search); - } -}; - - - - - -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 -}) -/** - * @add jQuery.Controller.prototype - */ - -$.extend($.Controller.prototype, { - /** - * @parent jquery.controller.history - * 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; - }, - /** - * @parent jquery.controller.history - * 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); - }, - /** - * @parent jquery.controller.history - * 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; - }, - /** - * @hide - * @parent jquery.controller.history - * 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; - }, - - /** - * @parent jquery.controller.history - * 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/html5/html5.js b/controller/history/html5/html5.js deleted file mode 100644 index c2c94075..00000000 --- a/controller/history/html5/html5.js +++ /dev/null @@ -1,31 +0,0 @@ -steal.plugins('jquery/controller/subscribe').then(function($){ - - var hasHistoryManagementSupport = !!(window.history && history.pushState); - - if (hasHistoryManagementSupport) { - steal.dev.log("WARNING: The current browser does not support HTML5 History Management."); - } else { - window.onpopstate = function(event) { - OpenAjax.hub.publish("history."+location.href, (event && event.state) || {}); - }; - - setTimeout(function(){ - window.onpopstate(); - }, 1); // immediately after ready - - $.extend($.Controller.prototype, { - redirectTo: function(url, data, title) { - data = data || {}; - window.history.pushState(data, title, url); - this.publish("history." + url, data); - } - }); - - $.Controller.processors["windowpopstate"] = function(el, event, selector, cb) { - $(window).bind("popstate", cb); - return function(){ - $(window).unbind("popstate", cb); - } - }; - } -}) diff --git a/controller/history/html5/qunit.html b/controller/history/html5/qunit.html deleted file mode 100644 index 7274e702..00000000 --- a/controller/history/html5/qunit.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - -

    HTML5 History Test Suite

    -

    -
    -

    -
    -
      -
      - - diff --git a/controller/history/html5/qunit/qunit.js b/controller/history/html5/qunit/qunit.js deleted file mode 100755 index 1687eaef..00000000 --- a/controller/history/html5/qunit/qunit.js +++ /dev/null @@ -1,82 +0,0 @@ -steal.plugins("funcunit/qunit", "jquery/controller/history/html5").then(function($){ - -module("jquery/controller/history/html5",{ - setup: function(){ - - } -}) - - -$.Controller.extend("HTML5HistoryTestController", { -}, -{ - "history.** subscribe": function(event_name, params) { - this["gotHistory"](event_name.replace("history.", ""), params); - }, - - gotHistory: $.noop, - - "window windowpopstate": function(ev) { - this["gotPopState"](location.href, (ev.originalEvent && ev.originalEvent.state) || {}); - }, - - gotPopState: $.noop -}); - -var originalLocation = location.href; - -asyncTest("Controller redirect should work", function(){ - expect(1); - var testController = new HTML5HistoryTestController($("
      ").get(0)); - - var testLocation = "/test/location"; - testController["gotHistory"] = function(location, state) { - start(); - equals(location, testLocation); - testController["gotHistory"] = $.noop; - testController.redirectTo(originalLocation); - }; - - stop(); - testController.redirectTo(testLocation); -}); - -asyncTest("State data should persist", function(){ - expect(1); - var testController = new HTML5HistoryTestController($("
      ").get(0)); - - testController["gotHistory"] = function(location, state) { - start(); - equals(state.hi, "mom"); - testController["gotHistory"] = $.noop; - testController.redirectTo(originalLocation); - }; - - stop(); - testController.redirectTo("/test/location", { hi: "mom" }); -}); - -asyncTest("Should listen to windowpopstate", function(){ - expect(2); - var testController = new HTML5HistoryTestController($("
      ").get(0)); - - testController["gotPopState"] = function(location, state) { - start(); - ok(location.indexOf("/test/location") !== -1); - equals(state.hi, "mom"); - testController["gotPopState"] = $.noop; - testController.redirectTo(originalLocation); - }; - - stop(); - testController.redirectTo("/test/location", { hi: "mom" }); - - testController["gotHistory"] = function(location, state) { - testController["gotHistory"] = $.noop; - window.history.back(); - }; - - testController.redirectTo("/test/location2", { hi: "mom2" }); -}); - -}); 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/document.js b/controller/pages/document.js deleted file mode 100644 index fc3fd328..00000000 --- a/controller/pages/document.js +++ /dev/null @@ -1,65 +0,0 @@ -/** -@page jquery.controller.documentcontrollers Document Controllers -@parent jQuery.Controller - -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 should be used sparingly. They are not very reusable. -They should only be used for glueing together other controllers and page -layout. - -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() - }, - "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 - */ -// \ No newline at end of file diff --git a/controller/pages/listening.js b/controller/pages/listening.js deleted file mode 100644 index cbd8bf67..00000000 --- a/controller/pages/listening.js +++ /dev/null @@ -1,114 +0,0 @@ -/** -@page jquery.controller.listening Listening To Events -@parent jQuery.Controller - -Controllers organize event handlers and make listening to -events really easy. - -## Automatic Binding - -When a [jQuery.Controller.prototype.setup new controller is created], -contoller checks its methods for functions that are named like -an event handler. It automatically 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 all 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(){}, - - // listens to all mouseovers on - // li elements withing this controller - "li mouseover" : function(){} - - // listens to the window being resized - "windowresize" : function(){} - }) - -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 listensTo property. Here's how: - - $.Controller.extend("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")) - } - }) - -## Parameterized Event Bindings - -Controller lets you parameterize event names and selectors. The following -makes 2 buttons. One says hello on click, the other on mouseenter. - - $.Controller("Hello",{ - "{helloEvent}" : function(){ - alert('hello') - } - }) - - $("#clickMe").hello({helloEvent : "click"}); - $("#touchMe").hello({helloEvent : "mouseenter"}); - -You can parameterize any part of the method name. The following makes two -lists. One listens for clicks on divs, the other on lis. - - $.Controller("List",{ - "{listItem} click" : function(){ - //do something! - } - }) - - $("#divs").list({listItem : "div"}); - $("#lis").list({listItem : "li"}); - -## 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. - - */ -// \ No newline at end of file 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.js b/controller/pages/plugin.md similarity index 99% rename from controller/pages/plugin.js rename to controller/pages/plugin.md index 6c21eb1e..d3ee520f 100644 --- a/controller/pages/plugin.js +++ b/controller/pages/plugin.md @@ -1,4 +1,3 @@ -/** @page jquery.controller.plugin The generated jQuery plugin @parent jQuery.Controller @@ -100,5 +99,3 @@ You can overwrite the Controller's default name by setting a static pluginName p { ... }) $("#tabs").tabs() - */ -// \ No newline at end of file diff --git a/model/associations/qunit.html b/controller/route/qunit.html similarity index 56% rename from model/associations/qunit.html rename to controller/route/qunit.html index 605cdfd6..25edc491 100644 --- a/model/associations/qunit.html +++ b/controller/route/qunit.html @@ -1,16 +1,13 @@ + - - + route QUnit Test + -

      associations 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 29b9b03f..26280343 100644 --- a/controller/subscribe/subscribe.js +++ b/controller/subscribe/subscribe.js @@ -1,5 +1,5 @@ /*global OpenAjax: true */ -steal.plugins('jquery/controller', 'jquery/lang/openajax').then(function() { +steal('jquery/controller', 'jquery/lang/openajax').then(function() { /** * @function jQuery.Controller.static.processors.subscribe @@ -26,13 +26,14 @@ steal.plugins('jquery/controller', 'jquery/lang/openajax').then(function() { * @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 {Function} cb the callback function + * @param {String} cb the callback function's name */ - jQuery.Controller.processors.subscribe = function( el, event, selector, cb ) { - var subscription = OpenAjax.hub.subscribe(selector, cb); + jQuery.Controller.processors.subscribe = function( el, event, selector, cb, controller ) { + var subscription = OpenAjax.hub.subscribe(selector, function(){ + return controller[cb].apply(controller, arguments) + }); return function() { - var sub = subscription; - OpenAjax.hub.unsubscribe(sub); + OpenAjax.hub.unsubscribe(subscription); }; }; diff --git a/controller/view/test/qunit/controller_view_test.js b/controller/view/test/qunit/controller_view_test.js index 82188116..506da897 100644 --- a/controller/view/test/qunit/controller_view_test.js +++ b/controller/view/test/qunit/controller_view_test.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/controller/view','jquery/view/micro','funcunit/qunit') //load qunit +steal('jquery/controller/view','jquery/view/micro','funcunit/qunit') //load qunit .then(function(){ module("jquery/controller/view"); @@ -18,5 +18,33 @@ steal.plugins('jquery/controller/view','jquery/view/micro','funcunit/qunit') // ok(/Hello World/i.test($('#cont_view').text()),"view rendered") }); + 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") + }); + + 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/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 a7e80ddf..400e87c2 100644 --- a/dom/compare/compare.js +++ b/dom/compare/compare.js @@ -1,7 +1,7 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function($){ +steal('jquery/dom').then(function($){ /** * @function compare * @parent dom @@ -50,8 +50,12 @@ jQuery.fn.compare = function(element){ //usually return null; } if (window.HTMLElement) { //make sure we aren't coming from XUL element + var s = HTMLElement.prototype.toString.call(element) - if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]') return null; + if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]' || s === '[object Window]') { + return null; + } + } if(this[0].compareDocumentPosition){ return this[0].compareDocumentPosition(element); diff --git a/dom/compare/compare_test.js b/dom/compare/compare_test.js index f005176c..08ef31a8 100644 --- a/dom/compare/compare_test.js +++ b/dom/compare/compare_test.js @@ -1,6 +1,5 @@ -steal - .plugins("jquery/dom/compare") //load your app - .plugins('funcunit/qunit').then(function(){ +steal("jquery/dom/compare") //load your app + .then('funcunit/qunit').then(function(){ module("jquery/dom/compare") test("Compare cases", function(){ 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 b44590e5..a563d5b4 100644 --- a/dom/cur_styles/cur_styles.js +++ b/dom/cur_styles/cur_styles.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom').then(function( $ ) { var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, rupper = /([A-Z])/g, diff --git a/dom/cur_styles/cur_styles_test.js b/dom/cur_styles/cur_styles_test.js index e279172a..d746815e 100644 --- a/dom/cur_styles/cur_styles_test.js +++ b/dom/cur_styles/cur_styles_test.js @@ -1,6 +1,5 @@ -steal - .plugins("jquery/dom/dimensions",'jquery/view/micro') //load your app - .plugins('funcunit/qunit').then(function(){ +steal("jquery/dom/dimensions",'jquery/view/micro') //load your app + .then('funcunit/qunit').then(function(){ module("jquery/dom/curStyles"); 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/dimensions/test/qunit/outer.micro b/dom/dimensions/test/outer.micro similarity index 100% rename from dom/dimensions/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 e2204e8a..a47d8438 100644 --- a/dom/dom.js +++ b/dom/dom.js @@ -1,6 +1,8 @@ /** @page dom DOM Helpers -@tag core +@parent jquerymx +@description jQuery DOM extension. + JavaScriptMVC adds a bunch of useful jQuery extensions for the dom. Check them out on the left. @@ -68,6 +70,13 @@ 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.plugins('jquery'); \ No newline at end of file +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 f1ceb37a..672b5a7b 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -1,6 +1,13 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom', + 'jquery/lang/object', + 'jquery/lang/string',function( $ ) { + + //used to check urls + + // the pre-filter needs to re-route the url + $.ajaxPrefilter( function( settings, originalOptions, jqXHR ) { // if fixtures are on if(! $.fixture.on) { @@ -8,10 +15,13 @@ steal.plugins('jquery/dom').then(function( $ ) { } // add the fixture option if programmed in - overwrite(settings); + 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; } @@ -25,11 +35,14 @@ steal.plugins('jquery/dom').then(function( $ ) { var url = settings.fixture; if (/^\/\//.test(url) ) { - url = steal.root.join(settings.fixture.substr(2)); + var sub = settings.fixture.substr(2) + ''; + url = typeof steal === "undefined" ? + url = "/" + sub : + steal.root.mapJoin(sub) +''; } - //@steal-remove-start + //!steal-remove-start steal.dev.log("looking for fixture in " + url); - //@steal-remove-end + //!steal-remove-end settings.url = url; settings.data = null; settings.type = "GET"; @@ -40,13 +53,19 @@ steal.plugins('jquery/dom').then(function( $ ) { } }else { - //@steal-remove-start - steal.dev.log("using a dynamic fixture for " + settings.url); - //@steal-remove-end + //!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") + settings.dataTypes.splice(0,0,"fixture"); + + if(data){ + $.extend(originalOptions.data, data) + } + // add to settings data from fixture ... + } }); @@ -105,32 +124,10 @@ steal.plugins('jquery/dom').then(function( $ ) { var typeTest = /^(script|json|test|jsonp)$/, // a list of 'overwrite' settings object overwrites = [], - // checks if an overwrite matches ajax settings - isSimilar = function(settings, overwrite, exact){ - - settings = $.extend({}, settings) - - for(var prop in overwrite){ - if(prop === 'fixture'){ - - } else if(overwrite[prop] !== settings[prop]){ - return false; - } - if(exact){ - delete settings[prop] - } - } - if(exact){ - for(var name in settings){ - return false - } - } - return true; - }, // returns the index of an overwrite function find = function(settings, exact){ for(var i =0; i < overwrites.length; i++){ - if(isSimilar(settings, overwrites[i], exact)){ + if($fixture._similar(settings, overwrites[i], exact)){ return i; } } @@ -141,6 +138,7 @@ steal.plugins('jquery/dom').then(function( $ ) { var index = find(settings); if(index > -1){ settings.fixture = overwrites[index].fixture; + return $fixture._getData(overwrites[index].url, settings.url) } }, @@ -151,14 +149,26 @@ steal.plugins('jquery/dom').then(function( $ ) { 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){ + settings.url.replace(/\/(\d+)(\/|$|\.)/g, function(all, num){ id = num; }); } if(id === undefined){ - id = settings.url.replace(/\/(\w+)(\/|$)/g, function(all, num){ + id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function(all, num){ if(num != 'update'){ id = num; } @@ -173,174 +183,277 @@ steal.plugins('jquery/dom').then(function( $ ) { }; /** - * @class jQuery.fixture + * @function jQuery.fixture * @plugin jquery/dom/fixture * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/fixture/fixture.js * @test jquery/dom/fixture/qunit.html * @parent dom * - * Fixtures simulate AJAX responses. Instead of making - * a request to a server, fixtures simulate - * the response 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. * - * ### Two Quick Examples + * ## Types of Fixtures * * There are two common ways of using fixtures. The first is to - * map Ajax requests to another file or function. The following + * 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"); * - * You can also add a fixture option directly to $.ajax like: - * - * $.ajax({url: "/tasks.json", - * dataType: "json", - * type: "get", - * fixture: "fixtures/tasks.json", - * success: myCallback - * }); - * - * The first technique keeps fixture logic out of your Ajax - * requests. However, if your service urls are changing __a lot__ - * the second technique means you only have to change the service - * url in one spot. - * + * 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() } + * }) * - * ## Types of Fixtures + * We categorize fixtures into the following types: * - * 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 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"); * - * $.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 * $.fixture("tasks/1", "//fixtures/task1.json"); - * - * $.ajax({type:"get", - * url: "tasks/1", - * fixture: "//fixtures/task1.json"})` * - * ### Dynamic 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]. + * Dynamic Fixtures are functions that get the details of + * the Ajax request and return the result of the mocked service + * request from your server. * - * There are 2 ways to lookup dynamic fixtures. They can provided: + * For example, the following returns a successful response + * with JSON data from the server: * - * //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"] - * } + * $.fixture("/foobar.json", function(orig, settings, headers){ + * return [200, "success", {json: {foo: "bar" } }, {} ] + * }) + * + * The fixture function has the following signature: + * + * function( originalOptions, options, headers ) { + * return [ status, statusText, responses, responseHeaders ] + * } + * + * where the fixture function is called with: + * + * - originalOptions - are the options provided to the ajax method, unmodified, + * and thus, without defaults from ajaxSettings + * - options - are the request options + * - headers - a map of key/value request headers + * + * and the fixture function returns an array as arguments for ajaxTransport's 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. * - * Or found by name on $.fixture: + * $.fixture("/foo", function(){ + * return [401,"{type: 'unauthorized'}"] + * }); * - * // add your function on $.fixture - * // We use -FUNC by convention - * $.fixture["-myGet"] = function(settings, cbType){...} + * This could be received by the following Ajax request: * - * // reference it * $.ajax({ - * type:"get", - * url: "tasks/1", - * dataType: "json", - * fixture: "-myGet"}) - * - *

      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: + * 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 + * @demo jquery/dom/fixture/fixture.html * - * The fixture plugin comes with a few ready-made dynamic fixtures and - * fixture helpers:

      + * @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. * - *
        - *
      • [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.
      • - *
      + * If a __string__ is passed, it can be used to match the url and type. Urls + * can be templated, using {NAME} as wildcards. * - * @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 {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 , fixture) { + var $fixture = $.fixture = function( settings , fixture ){ // if we provide a fixture ... if(fixture !== undefined){ if(typeof settings == 'string'){ // handle url strings - settings ={ - url : settings - }; + 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){ + if(index > -1){ overwrites.splice(index,1) } if(fixture == null){ return } - settings.fixture = fixture; overwrites.push(settings) } }; - - $.extend($.fixture, { + var replacer = $.String._regs.replacer; + + $.extend($.fixture, { + // given ajax settings, find an overwrite + _similar : function(settings, overwrite, exact){ + if(exact){ + return $.Object.same(settings , overwrite, {fixture : null}) + } else { + return $.Object.subset(settings, overwrite, $.fixture._compare) + } + }, + _compare : { + url : function(a, b){ + return !! $fixture._getData(b, a) + }, + fixture : null, + type : "i" + }, + // gets data from a url like "/todo/{id}" given "todo/5" + _getData : function(fixtureUrl, url){ + var order = [], + fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.').replace('?', '\\?'), + res = new RegExp(fixtureUrlAdjusted.replace(replacer, function(whole, part){ + order.push(part) + return "([^\/]+)" + })+"$").exec(url), + data = {}; + + if(!res){ + return null; + } + res.shift(); + $.each(order, function(i, name){ + data[name] = res.shift() + }) + return data; + }, /** + * @hide * Provides a rest update fixture function */ "-restUpdate": function( settings ) { - return [{ + return [200,"succes",{ id: getId(settings) },{ location: settings.url+"/"+getId(settings) @@ -348,6 +461,7 @@ steal.plugins('jquery/dom').then(function( $ ) { }, /** + * @hide * Provides a rest destroy fixture function */ "-restDestroy": function( settings, cbType ) { @@ -355,11 +469,12 @@ steal.plugins('jquery/dom').then(function( $ ) { }, /** + * @hide * Provides a rest create fixture function */ - "-restCreate": function( settings, cbType ) { - var id = parseInt(Math.random() * 100000, 10); - return [{ + "-restCreate": function( settings, cbType, nul, id ) { + var id = id || parseInt(Math.random() * 100000, 10); + return [200,"succes",{ id: id },{ location: settings.url+"/"+id @@ -367,30 +482,32 @@ steal.plugins('jquery/dom').then(function( $ ) { }, /** + * @function jQuery.fixture.make + * @parent jQuery.fixture * Used to make fixtures for findAll / findOne style requests. - * @codestart - * //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 ) { ... } - * }); - * @codeend + * + * //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 @@ -402,14 +519,14 @@ steal.plugins('jquery/dom').then(function( $ ) { * server params like searchText or startDate. The function should return true if the item passes the filter, * false otherwise. For example: * - * @codestart - * function(item, settings){ - if(settings.data.searchText){ - var regex = new RegExp("^"+settings.data.searchText) - return regex.test(item.name); - } - * } - * @codeend + * + * function(item, settings){ + * if(settings.data.searchText){ + * var regex = new RegExp("^"+settings.data.searchText) + * return regex.test(item.name); + * } + * } + * */ make: function( types, count, make, filter ) { if(typeof types === "string"){ @@ -436,10 +553,9 @@ 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 ) { @@ -476,13 +592,13 @@ 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 ) { i=0; - if ( settings.data[param] && // don't do this if the value of the param is null (ignore it) + 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] ) { @@ -516,7 +632,8 @@ steal.plugins('jquery/dom').then(function( $ ) { }; // findOne $.fixture["-" + types[1]] = function( settings ) { - return [findOne(settings.data.id)]; + var item = findOne(getId(settings)); + return item ? [item] : []; }; // update $.fixture["-" + types[1]+"Update"] = function( settings, cbType ) { @@ -541,6 +658,7 @@ steal.plugins('jquery/dom').then(function( $ ) { }; $.fixture["-" + types[1]+"Create"] = function( settings, cbType ) { var item = make(items.length, items); + $.extend(item, settings.data); if(!item.id){ @@ -548,10 +666,88 @@ steal.plugins('jquery/dom').then(function( $ ) { } items.push(item); - return $.fixture["-restCreate"](settings, cbType) + + 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 @@ -612,12 +808,13 @@ steal.plugins('jquery/dom').then(function( $ ) { 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 @@ -642,73 +839,7 @@ steal.plugins('jquery/dom').then(function( $ ) { return false; }; - /** - * @add jQuery - */ - $. - /** - * 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) ) { - if(!typeTest.test(type||"")){ - fixture = type; - type = callback; - } - callback = data; - data = null; - } - 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 - */ - post = function( url, data, callback, type, fixture ) { - if ( jQuery.isFunction(data) ) { - if(!typeTest.test(type||"")){ - fixture = type; - type = callback; - } - callback = data; - data = {}; - } - - return jQuery.ajax({ - type: "POST", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; + /** * @page jquery.fixture.0organizing Organizing Fixtures @@ -752,7 +883,7 @@ steal.plugins('jquery/dom').then(function( $ ) { * steal({path: '//todo/fixtures/fixtures.js',ignore: true}); * * //start of your app's steals - * steal.plugins( ... ) + * steal( ... ) * * We typically keep it a one liner so it's easy to comment out. * @@ -770,31 +901,6 @@ steal.plugins('jquery/dom').then(function( $ ) { * } * */ - // - /** - * @add jQuery.fixture - */ - // - /** - * @page jquery.fixture.1errors Simulating Errors - * @parent jQuery.fixture - * - * 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'}" - * } - * }) - * - */ -}); \ 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 index fb08fe99..7559d1ba 100644 --- a/dom/fixture/fixture_test.js +++ b/dom/fixture/fixture_test.js @@ -1,62 +1,47 @@ -steal - .plugins("jquery/dom/fixture", "jquery/model") - .plugins('funcunit/qunit').then(function(){ + +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"); - $.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"); + start(); + },'json'); + + },'json'); }) test("dynamic fixtures",function(){ stop(); $.fixture.delay = 10; - var fix = function(){ + $.fixture("something", 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); + start(); + + },'json'); }); test("fixture function", 3, function(){ stop(); - var url = steal.root.join("jquery/dom/fixture/fixtures/foo.json"); + var url = steal.root.join("jquery/dom/fixture/fixtures/foo.json")+''; $.fixture(url,"//jquery/dom/fixture/fixtures/foobar.json" ); $.get(url,function(data){ @@ -92,7 +77,7 @@ test("fixtures with converters", function(){ stop(); $.ajax( { - url : steal.root.join("jquery/dom/fixture/fixtures/foobar.json"), + url : steal.root.join("jquery/dom/fixture/fixtures/foobar.json")+'', dataType: "json fooBar", converters: { "json fooBar": function( data ) { @@ -166,5 +151,182 @@ test("simulating an error", function(){ }) }) +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/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/form_params/form_params.html b/dom/form_params/form_params.html index 59b9a754..650dd9ec 100644 --- a/dom/form_params/form_params.html +++ b/dom/form_params/form_params.html @@ -25,16 +25,13 @@ 1 min
      5 min
      10 min
      - 10 min
      - \ No newline at end of file diff --git a/dom/form_params/form_params.js b/dom/form_params/form_params.js index 1ae3319d..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 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 = $.data(el, "value") || $.fn.val.call([el]), - isRadioCheck = radioCheck.test(el.type), - parts = key.match(keyBreaker), - write = !isRadioCheck || !! el.checked, - //make an array of values - lastPart; - - if ( convert ) { - if ( isNumber(value) ) { - value = parseFloat(value); - } else if ( value === 'true' || value === '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 index c437ef77..019896d7 100644 --- a/dom/range/qunit.html +++ b/dom/range/qunit.html @@ -1,20 +1,17 @@ - selection QUnit Test - - + Range QUnit Test -

      selection Test Suite

      +

      Range Test Suite

        + \ No newline at end of file diff --git a/dom/range/range.html b/dom/range/range.html index f076f196..b5f9d139 100644 --- a/dom/range/range.html +++ b/dom/range/range.html @@ -30,33 +30,37 @@

        The Range Plugin

        
         		
        \ No newline at end of file diff --git a/dom/range/range.js b/dom/range/range.js index 29307367..7f3db253 100644 --- a/dom/range/range.js +++ b/dom/range/range.js @@ -1,11 +1,14 @@ -steal.plugins('jquery','jquery/dom/compare').then(function($){ +steal('jquery','jquery/dom/compare').then(function($){ // TODOS ... // Ad /** * @function jQuery.fn.range + * @parent $.Range * - * Returns a jQuery.Range. + * Returns a jQuery.Range for the element selected. + * + * $('#content').range() */ $.fn.range = function(){ return $.Range(this[0]) @@ -29,24 +32,43 @@ bisect = function(el, start, end){ if(end-start == 1){ return } -} +}, +support = {}; /** * @Class jQuery.Range * @parent dom * @tag alpha * - * Provides text range helpers for creating, moving, and comparing ranges. + * 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|Node|Point} [range] An object specifiying a + * @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 - * - __Node__ - returns a range with the node's text 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 @@ -77,8 +99,12 @@ $.Range = function(range){ this.select(range) } - } else if (range.clientX || range.pageX || range.left) { - this.rangeFromPoint(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; } @@ -86,7 +112,7 @@ $.Range = function(range){ /** * @static */ -// +$.Range. /** * Gets the current range. * @@ -95,7 +121,7 @@ $.Range = function(range){ * @param {HTMLElement} [el] an optional element used to get selection for a given window. * @return {jQuery.Range} a jQuery.Range wrapped range. */ -$.Range.current = function(el){ +current = function(el){ var win = getWindow(el), selection; if(win.getSelection){ @@ -109,14 +135,22 @@ $.Range.current = function(el){ -$.extend($.Range.prototype,{ - rangeFromPoint : function(point){ +$.extend($.Range.prototype, +/** @prototype **/ +{ + moveToPoint : function(point){ var clientX = point.clientX, clientY = point.clientY if(!clientX){ var off = scrollOffset(); - clientX = (off.pageX || off.left || 0 ) - off.left; - clientY = (off.pageY || off.top || 0 ) - off.top; + 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); @@ -207,12 +241,19 @@ $.extend($.Range.prototype,{ * * @param {Boolean} [toStart] true if to the start of the range, false if to the * end. Defaults to false. - * @return {Range} returns the range for chaining. + * @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(); }, @@ -276,7 +317,8 @@ $.extend($.Range.prototype,{ }, /** - * Sets or gets the end of the range. It takes similar options as [jQuery.Range.prototype.get]. + * Sets or gets the end of the range. + * It takes similar options as [jQuery.Range.prototype.start]. * @param {Object} [set] */ end : function(set){ @@ -288,8 +330,8 @@ $.extend($.Range.prototype,{ } } else { - var end = this.clone().collapse(false).parent(); - var endRange = $.Range(end).select(end).collapse(); + var end = this.clone().collapse(false).parent(), + endRange = $.Range(end).select(end).collapse(); endRange.move("END_TO_END", this); return { container: end, @@ -310,10 +352,31 @@ $.extend($.Range.prototype,{ } }, /** - * returns the most common ancestor element of the endpoints in the range. + * 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(){ - return this.range.parentElement || this.range.commonAncestorContainer + 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. @@ -389,13 +452,36 @@ $.extend($.Range.prototype,{ /** * @function compare - * Compares one range to another range. This is different from the spec b/c the spec is confusing. + * 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()) * - * source.compare("START_TO_END", toRange); * - * This returns -1 if source's start is before toRange's end. - * @param {Object} type - * @param {Object} range + * + * @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){ @@ -407,9 +493,24 @@ $.extend($.Range.prototype,{ /** * @function move - * Move the endpoints of a range - * @param {Object} type - * @param {Object} range + * 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){ @@ -439,19 +540,59 @@ $.extend($.Range.prototype,{ var cloneFunc = range.cloneRange ? "cloneRange" : "duplicate", selectFunc = range.selectNodeContents ? "selectNodeContents" : "moveToElementText"; + fn. /** - * Clones the range and returns a new $.Range object. + * Clones the range and returns a new $.Range + * object. + * + * @return {jQuery.Range} returns the range as a $.Range. */ - fn.clone = function(){ + clone = function(){ return $.Range( this.range[cloneFunc]() ); }; + fn. /** - * Selects an element with this range - * @param {HTMLElement} el + * @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. */ - fn.select = function(el){ - this.range[selectFunc](el); + 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; }; @@ -480,6 +621,16 @@ var iterate = function(elems, cb){ } } } + +}, +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){ @@ -519,5 +670,7 @@ scrollOffset = function( win){ }; +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 index e69de29b..e55246e4 100644 --- a/dom/range/range_test.js +++ 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

          +
          View qunit test results
          +
          +
          +
          + 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/selection.html b/dom/selection/selection.html index 20840c29..536b16e5 100644 --- a/dom/selection/selection.html +++ b/dom/selection/selection.html @@ -11,68 +11,46 @@ - - - - - - - - - - - - - - - - - -
          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.

          -
          - - + + + Select Input + + + + Select Within One Element +

          0123456789

          + + + Select Across Multiple Elements +
          012
          345
          + + + src='../../../steal/steal.js'> \ No newline at end of file diff --git a/dom/selection/selection.js b/dom/selection/selection.js index 7a224c51..45cc2280 100644 --- a/dom/selection/selection.js +++ b/dom/selection/selection.js @@ -1,4 +1,4 @@ -steal.plugins('jquery','jquery/dom/range').then(function($){ +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() @@ -195,10 +195,10 @@ getCharElement = function( elems , range, len ) { * 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 teh start of the element to the end of the selection. - * _ __range__ - A [jQuery.Range] that represents the current 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 do: + * This lets you get the selected text in a textarea like: * * var textarea = $('textarea') * selection = textarea.selection(), @@ -206,7 +206,7 @@ getCharElement = function( elems , range, len ) { * * alert('You selected '+selected+'.'); * - * Selection works with all elements. If you want to get selection information on the page: + * Selection works with all elements. If you want to get selection information of the document: * * $(document.body).selection(); * @@ -216,9 +216,15 @@ getCharElement = function( elems , range, len ) { * * $('#rte').selection(30, 40) * - * @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 + * ## 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){ diff --git a/dom/selection/selection_test.js b/dom/selection/selection_test.js index d6ccd747..28886444 100644 --- a/dom/selection/selection_test.js +++ b/dom/selection/selection_test.js @@ -1,5 +1,4 @@ -steal - .plugins("funcunit/qunit", "jquery/dom/selection").then(function(){ +steal("funcunit/qunit", "jquery/dom/selection").then(function(){ module("jquery/dom/selection"); @@ -13,7 +12,7 @@ test("getCharElement", function(){ setTimeout(function(){ var types = ['textarea','#inp','#1','#2']; for(var i =0; i< types.length; i++){ - console.log(types[i]) + //console.log(types[i]) $(types[i]).selection(1,5); } /* diff --git a/dom/within/within.js b/dom/within/within.js index bea9a8be..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,10 +11,18 @@ steal.plugins('jquery/dom').then(function($){ /** * @function within * @parent dom - * Returns if the elements are within the position - * @param {Number} left the position - * @param {Number} top - * @param {Boolean} [useOffsetCache] + * @plugin jquery/dom/within + * + * Returns the elements are within the position. + * + * // get all elements that touch 200x200. + * $('*').within(200, 200); + * + * @param {Number} left the position from the left of the page + * @param {Number} top the position from the top of the page + * @param {Boolean} [useOffsetCache] cache the dimensions and offset of the elements. + * @return {jQuery} a jQuery collection of elements whos area + * overlaps the element position. */ $.fn.within= function(left, top, useOffsetCache) { var ret = [] @@ -42,6 +50,7 @@ $.fn.within= function(left, top, useOffsetCache) { /** * @function withinBox + * @parent jQuery.fn.within * returns if elements are within the box * @param {Object} left * @param {Object} top @@ -56,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/download.html b/download/download.html index 89b112e1..98535d6c 100644 --- a/download/download.html +++ b/download/download.html @@ -45,26 +45,6 @@

          Controller

          Organize event handlers using event delegation
          -
          - - - -
          Add page history support to Controller
          -
          - -
          - - - -
          Add pub/sub support to Controller
          -
          - -
          - - - -
          Helpers that tie view templates to a controller instance
          -
          @@ -82,12 +62,6 @@

          Model

          A basic skeleton to organize pieces of your application's data layer
          -
          - - - -
          Get data for related records
          -
          @@ -104,13 +78,6 @@

          Model

          -
          - - - -
          A storeable list of model instances
          -
          -