diff --git a/.gitignore b/.gitignore index 0514b94f..96adf415 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .tmp* -dist \ No newline at end of file +dist +*.swp diff --git a/README b/README index 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,32 +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/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+""); - 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 31db76ee..b48d1d00 100644 --- a/class/class.js +++ b/class/class.js @@ -2,23 +2,38 @@ // This is a modified version of John Resig's class // http://ejohn.org/blog/simple-javascript-inheritance/ // It provides class level inheritance and callbacks. -//@steal-clean -steal.plugins("jquery").then(function( $ ) { +//!steal-clean +steal("jquery","jquery/lang/string",function( $ ) { - // if we are initializing a new class - var initializing = false, + // =============== HELPERS ================= + // if we are initializing a new class + var initializing = false, + makeArray = $.makeArray, + isFunction = $.isFunction, + isArray = $.isArray, + extend = $.extend, + getObject = $.String.getObject, + concatArgs = function(arr, args){ + return arr.concat(makeArray(args)); + }, + // tests if we can get super in .toString() fnTest = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/, - + // overwrites an object with methods, sets up _super + // newProps - new properties + // oldProps - where the old properties might be + // addTo - what we are adding to inheritProps = function( newProps, oldProps, addTo ) { addTo = addTo || newProps for ( var name in newProps ) { // Check if we're overwriting an existing function - addTo[name] = typeof newProps[name] == "function" && typeof oldProps[name] == "function" && fnTest.test(newProps[name]) ? (function( name, fn ) { + addTo[name] = isFunction(newProps[name]) && + isFunction(oldProps[name]) && + fnTest.test(newProps[name]) ? (function( name, fn ) { return function() { var tmp = this._super, ret; @@ -35,53 +50,58 @@ steal.plugins("jquery").then(function( $ ) { }; })(name, newProps[name]) : newProps[name]; } - }; - + }, + STR_PROTOTYPE = 'prototype' /** * @class jQuery.Class * @plugin jquery/class - * @tag core + * @parent jquerymx * @download dist/jquery/jquery.class.js * @test jquery/class/qunit.html - * Class provides simulated inheritance in JavaScript. Use $.Class to bridge the gap between - * jQuery's functional programming style and Object Oriented Programming. - * It is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] + * @description Easy inheritance in JavaScript. + * + * Class provides simulated inheritance in JavaScript. Use Class to bridge the gap between + * jQuery's functional programming style and Object Oriented Programming. It + * is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] * Inheritance library. Besides prototypal inheritance, it includes a few important features: - * - *

Static v. Prototype

- *

Before learning about Class, it's important to + * + * - Static inheritance + * - Introspection + * - Namespaces + * - Setup and initialization methods + * - Easy callback function creation + * + * + * The [mvc.class Get Started with jQueryMX] has a good walkthrough of $.Class. + * + * ## Static v. Prototype + * + * Before learning about Class, it's important to * understand the difference between - * a class's static and prototype properties. - *

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

A static (or class) property is on the Class constructor + * a class's __static__ and __prototype__ properties. + * + * //STATIC + * MyClass.staticProperty //shared property + * + * //PROTOTYPE + * myclass = new MyClass() + * myclass.prototypeMethod() //instance method + * + * A static (or class) property is on the Class constructor * function itself - * and can be thought of being shared by all instances of the Class. - * Prototype propertes are available only on instances of the Class. - *

- *

A Basic Class

- *

The following creates a Monster class with a + * and can be thought of being shared by all instances of the + * Class. Prototype propertes are available only on instances of the Class. + * + * ## A Basic Class + * + * The following creates a Monster class with a * name (for introspection), static, and prototype members. * Every time a monster instance is created, the static * count is incremented. * - *

* @codestart - * $.Class.extend('Monster', + * $.Class('Monster', * /* @static *| * { * count: 0 @@ -97,7 +117,7 @@ steal.plugins("jquery").then(function( $ ) { * this.health = 10; * * // increments count - * this.Class.count++; + * this.constructor.count++; * }, * eat: function( smallChildren ){ * this.health += smallChildren; @@ -121,68 +141,75 @@ steal.plugins("jquery").then(function( $ ) { * * @codeend * - *

+ * * Notice that the prototype init function is called when a new instance of Monster is created. - *

- *

Inheritance

- *

When a class is extended, all static and prototype properties are available on the new class. + * + * + * ## Inheritance + * + * When a class is extended, all static and prototype properties are available on the new class. * If you overwrite a function, you can call the base class's function by calling * this._super. Lets create a SeaMonster class. SeaMonsters are less * efficient at eating small children, but more powerful fighters. - *

- * @codestart - * Monster.extend("SeaMonster",{ - * eat: function( smallChildren ) { - * this._super(smallChildren / 2); - * }, - * fight: function() { - * this.health -= 1; - * } - * }); - * - * lockNess = new SeaMonster('Lock Ness'); - * lockNess.eat(4); //health = 12 - * lockNess.fight(); //health = 11 - * @codeend - *

Static property inheritance

+ * + * + * Monster("SeaMonster",{ + * eat: function( smallChildren ) { + * this._super(smallChildren / 2); + * }, + * fight: function() { + * this.health -= 1; + * } + * }); + * + * lockNess = new SeaMonster('Lock Ness'); + * lockNess.eat(4); //health = 12 + * lockNess.fight(); //health = 11 + * + * ### Static property inheritance + * * You can also inherit static properties in the same way: - * @codestart - * $.Class.extend("First", - * { - * staticMethod: function() { return 1;} - * },{}) + * + * $.Class("First", + * { + * staticMethod: function() { return 1;} + * },{}) * - * First.extend("Second",{ - * staticMethod: function() { return this._super()+1;} - * },{}) + * First("Second",{ + * staticMethod: function() { return this._super()+1;} + * },{}) * - * Second.staticMethod() // -> 2 - * @codeend - *

Namespaces

- *

Namespaces are a good idea! We encourage you to namespace all of your code. + * Second.staticMethod() // -> 2 + * + * ## Namespaces + * + * Namespaces are a good idea! We encourage you to namespace all of your code. * It makes it possible to drop your code into another app without problems. * Making a namespaced class is easy: - *

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

Introspection

+ * * Often, it's nice to create classes whose name helps determine functionality. Ruby on * Rails's [http://api.rubyonrails.org/classes/ActiveRecord/Base.html|ActiveRecord] ORM class * is a great example of this. Unfortunately, JavaScript doesn't have a way of determining * an object's name, so the developer must provide a name. Class fixes this by taking a String name for the class. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's * static properties. * * - *

Setup and initialization methods

+ * ## Setup and initialization methods + * *

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

Setup

- *

Setup functions are called before init functions. Static setup functions are passed + * ### Setup + * + * Setup functions are called before init functions. Static setup functions are passed * the base class followed by arguments passed to the extend function. - * Prototype static functions are passed the Class constructor function arguments.

- *

If a setup function returns an array, that array will be used as the arguments + * Prototype static functions are passed the Class constructor + * function arguments. + * + * If a setup function returns an array, that array will be used as the arguments * for the following init method. This provides setup functions the ability to normalize * arguments passed to the init constructors. They are also excellent places - * to put setup code you want to almost always run.

- *

+ * to put setup code you want to almost always run. + * + * * The following is similar to how [jQuery.Controller.prototype.setup] * makes sure init is always called with a jQuery element and merged options * even if it is passed a raw * HTMLElement and no second parameter. - *

- * @codestart - * $.Class.extend("jQuery.Controller",{ - * ... - * },{ - * setup: function( el, options ) { - * ... - * return [$(el), - * $.extend(true, - * this.Class.defaults, - * options || {} ) ] - * } - * }) - * @codeend + * + * $.Class("jQuery.Controller",{ + * ... + * },{ + * setup: function( el, options ) { + * ... + * return [$(el), + * $.extend(true, + * this.Class.defaults, + * options || {} ) ] + * } + * }) + * * Typically, you won't need to make or overwrite setup functions. - *

Init

+ * + * ### Init * - *

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

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

Callbacks

- *

Similar to jQuery's proxy method, Class provides a - * [jQuery.Class.static.callback callback] + * + * $.Class("Foo", { + * init: function( arg1, arg2, arg3 ) { + * this.sum = arg1+arg2+arg3; + * } + * }) + * var foo = new Foo(1,2,3); + * foo.sum //-> 6 + * + * ## Proxies + * + * Similar to jQuery's proxy method, Class provides a + * [jQuery.Class.static.proxy proxy] * function that returns a callback to a method that will always * have * this set to the class or instance of the class. - *

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

Callback is available as a static and prototype method.

- *

Demo

+ * + * $.Class("Todo",{ + * init: function( name ) { + * this.name = name + * }, + * get: function() { + * $.get("/stuff",this.proxy('show')) + * }, + * show: function( txt ) { + * alert(this.name+txt) + * } + * }) + * new Todo("Trash").get() + * + * Callback is available as a static and prototype method. + * + * ## Demo + * * @demo jquery/class/class.html - * - * @constructor Creating a new instance of an object that has extended jQuery.Class - * calls the init prototype function and returns a new instance of the class. - * + * + * + * @constructor + * + * To create a Class call: + * + * $.Class( [NAME , STATIC,] PROTOTYPE ) -> Class + * + *
+ *
{optional:String} + *

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

+ *
+ *
{optional:Object} + *

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

+ *
+ *
{Object} + *

Creates prototype methods on the class.

+ *
+ *
+ * + * When a Class is created, the static [jQuery.Class.static.setup setup] + * and [jQuery.Class.static.init init] methods are called. + * + * To create an instance of a Class, call: + * + * new Class([args ... ]) -> instance + * + * The created instance will have all the + * prototype properties and methods defined by the PROTOTYPE object. + * + * When an instance is created, the prototype [jQuery.Class.prototype.setup setup] + * and [jQuery.Class.prototype.init init] methods + * are called. */ - jQuery.Class = function() { + clss = $.Class = function() { if (arguments.length) { - jQuery.Class.extend.apply(jQuery.Class, arguments); + return clss.extend.apply(clss, arguments); } }; /* @Static*/ - $.extend($.Class, { + extend(clss, { /** - * @function callback + * @function proxy * Returns a callback function for a function on this Class. - * The callback function ensures that 'this' is set appropriately. + * Proxy ensures that 'this' is set appropriately. * @codestart - * $.Class.extend("MyClass",{ + * $.Class("MyClass",{ * getData: function() { * this.showing = null; - * $.get("data.json",this.callback('gotData'),'json') + * $.get("data.json",this.proxy('gotData'),'json') * }, * gotData: function( data ) { * this.showing = data; @@ -304,11 +372,11 @@ steal.plugins("jquery").then(function( $ ) { * MyClass.showData(); * @codeend *

Currying Arguments

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

Nesting Functions

- * Callback can take an array of functions to call as the first argument. When the returned callback function + * Proxy can take an array of functions to call as + * the first argument. When the returned callback function * is called each function in the array is passed the return value of the prior function. This is often used * to eliminate currying initial arguments. * @codestart - * $.Class.extend("MyClass",{ + * $.Class("MyClass",{ * getData: function( callback ) { * //calls process, then callback with value from process - * $.get("data.json",this.callback(['process2',callback]),'json') + * $.get("data.json",this.proxy(['process2',callback]),'json') * }, * process2: function( type,jsonData ) { * jsonData.processed = true; @@ -339,130 +408,156 @@ steal.plugins("jquery").then(function( $ ) { * next function. * @return {Function} the callback function. */ - callback: function( funcs ) { + proxy: function( funcs ) { //args that should be curried - var args = jQuery.makeArray(arguments), + var args = makeArray(arguments), self; + // get the functions to callback funcs = args.shift(); - if (!jQuery.isArray(funcs) ) { + // if there is only one function, make funcs into an array + if (!isArray(funcs) ) { funcs = [funcs]; } - + + // keep a reference to us in self self = this; - //@steal-remove-start + + //!steal-remove-start for( var i =0; i< funcs.length;i++ ) { - if(typeof funcs[i] == "string" && typeof this[funcs[i]] !== 'function'){ + 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() { - var cur = args.concat(jQuery.makeArray(arguments)), + // add the arguments after the curried args + var cur = concatArgs(args, arguments), isString, length = funcs.length, f = 0, func; - + + // go through each function to call back for (; f < length; f++ ) { func = funcs[f]; if (!func ) { continue; } - + + // set called with the name of the function on self (this is how this.view works) isString = typeof func == "string"; if ( isString && self._set_called ) { self.called = func; } + + // call the function cur = (isString ? self[func] : func).apply(self, cur || []); + + // pass the result to the next function (if there is a next function) if ( f < length - 1 ) { - cur = !jQuery.isArray(cur) || cur._use_call ? [cur] : cur + cur = !isArray(cur) || cur._use_call ? [cur] : cur } } return cur; } }, - /** - * @function getObject - * Gets an object from a String. - * If the object or namespaces the string represent do not - * exist it will create them. - * @codestart - * Foo = {Bar: {Zar: {"Ted"}}} - * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" - * @codeend - * @param {String} objectName the object you want to get - * @param {Object} [current=window] the object you want to look in. - * @return {Object} the object you are looking for. - */ - getObject: function( objectName, current ) { - var current = current || window, - parts = objectName ? objectName.split(/\./) : [], - i = 0; - for (; i < parts.length; i++ ) { - current = current[parts[i]] || (current[parts[i]] = {}) - } - return current; - }, /** * @function newInstance * Creates a new instance of the class. This method is useful for creating new instances * with arbitrary parameters. *

Example

* @codestart - * $.Class.extend("MyClass",{},{}) + * $.Class("MyClass",{},{}) * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) * @codeend * @return {class} instance of the class */ newInstance: function() { + // get a raw instance objet (init is not called) var inst = this.rawInstance(), args; + + // call setup if there is a setup if ( inst.setup ) { args = inst.setup.apply(inst, arguments); } + // call init if there is an init, if setup returned args, use those as the arguments if ( inst.init ) { - inst.init.apply(inst, $.isArray(args) ? args : arguments); + inst.init.apply(inst, isArray(args) ? args : arguments); } return inst; }, /** - * Copy and overwrite options from old class - * @param {Object} oldClass - * @param {String} fullName - * @param {Object} staticProps - * @param {Object} protoProps + * Setup gets called on the inherting class with the base class followed by the + * inheriting class's raw properties. + * + * Setup will deeply extend a static defaults property on the base class with + * properties on the base class. For example: + * + * $.Class("MyBase",{ + * defaults : { + * foo: 'bar' + * } + * },{}) + * + * MyBase("Inheriting",{ + * defaults : { + * newProp : 'newVal' + * } + * },{} + * + * Inheriting.defaults -> {foo: 'bar', 'newProp': 'newVal'} + * + * @param {Object} baseClass the base class that is being inherited from + * @param {String} fullName the name of the new class + * @param {Object} staticProps the static properties of the new class + * @param {Object} protoProps the prototype properties of the new class */ - setup: function( oldClass, fullName ) { - this.defaults = $.extend(true, {}, oldClass.defaults, this.defaults); + setup: function( baseClass, fullName ) { + // set defaults as the merger of the parent defaults and this object's defaults + this.defaults = extend(true, {}, baseClass.defaults, this.defaults); return arguments; }, rawInstance: function() { + // prevent running init initializing = true; var inst = new this(); initializing = false; + // allow running init return inst; }, /** * Extends a class with new static and prototype functions. There are a variety of ways * to use extend: - * @codestart - * //with className, static and prototype functions - * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) - * //with just classname and prototype functions - * $.Class.extend('Task',{ PROTOTYPE }) - * //With just a className - * $.Class.extend('Task') - * @codeend + * + * // with className, static and prototype functions + * $.Class('Task',{ STATIC },{ PROTOTYPE }) + * // with just classname and prototype functions + * $.Class('Task',{ PROTOTYPE }) + * // with just a className + * $.Class('Task') + * + * You no longer have to use .extend. Instead, you can pass those options directly to + * $.Class (and any inheriting classes): + * + * // with className, static and prototype functions + * $.Class('Task',{ STATIC },{ PROTOTYPE }) + * // with just classname and prototype functions + * $.Class('Task',{ PROTOTYPE }) + * // with just a className + * $.Class('Task') + * * @param {String} [fullName] the classes name (used for classes w/ introspection) * @param {Object} [klass] the new classes static/class functions * @param {Object} [proto] the new classes prototype functions + * * @return {jQuery.Class} returns the new class */ extend: function( fullName, klass, proto ) { - // figure out what was passed + // figure out what was passed and normalize it if ( typeof fullName != 'string' ) { proto = klass; klass = fullName; @@ -475,7 +570,7 @@ steal.plugins("jquery").then(function( $ ) { proto = proto || {}; var _super_class = this, - _super = this.prototype, + _super = this[STR_PROTOTYPE], name, shortName, namespace, prototype; // Instantiate a base class (but only create the instance, @@ -483,16 +578,17 @@ steal.plugins("jquery").then(function( $ ) { initializing = true; prototype = new this(); initializing = false; + // Copy the properties over onto the new prototype inheritProps(proto, _super, prototype); // The dummy class constructor - function Class() { // All construction is actually done in the init method if ( initializing ) return; - if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new + // 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) @@ -500,12 +596,12 @@ steal.plugins("jquery").then(function( $ ) { } // Copy old stuff onto class for ( name in this ) { - if ( this.hasOwnProperty(name) && $.inArray(name, ['prototype', 'defaults', 'getObject']) == -1 ) { + if ( this.hasOwnProperty(name) ) { Class[name] = this[name]; } } - // do static inheritance + // copy new static props on class inheritProps(klass, this, Class); // do namespace stuff @@ -513,64 +609,94 @@ steal.plugins("jquery").then(function( $ ) { var parts = fullName.split(/\./), shortName = parts.pop(), - current = $.Class.getObject(parts.join('.')), + current = getObject(parts.join('.'), window, true), namespace = current; - //@steal-remove-start + //!steal-remove-start if (!Class.nameOk ) { - steal.dev.isHappyName(fullName) + //steal.dev.isHappyName(fullName) } if(current[shortName]){ steal.dev.warn("class.js There's already something called "+fullName) } - //@steal-remove-end + //!steal-remove-end current[shortName] = Class; } // set things that can't be overwritten - $.extend(Class, { + extend(Class, { prototype: prototype, + /** + * @attribute namespace + * The namespaces object + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.namespace //-> MyOrg + * + */ namespace: namespace, + /** + * @attribute shortName + * The name of the class without its namespace, provided for introspection purposes. + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * + */ shortName: shortName, constructor: Class, + /** + * @attribute fullName + * The full name of the class, including namespace, provided for introspection purposes. + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * + */ fullName: fullName }); //make sure our prototype looks nice - Class.prototype.Class = Class.prototype.constructor = Class; + Class[STR_PROTOTYPE].Class = Class[STR_PROTOTYPE].constructor = Class; + - /** - * @attribute fullName - * The full name of the class, including namespace, provided for introspection purposes. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - */ - - var args = Class.setup.apply(Class, [_super_class].concat($.makeArray(arguments))); - + // call the class setup + var args = Class.setup.apply(Class, concatArgs([_super_class],arguments)); + + // call the class init if ( Class.init ) { - Class.init.apply(Class, args || []); + Class.init.apply(Class, args || concatArgs([_super_class],arguments)); } /* @Prototype*/ return Class; /** * @function setup - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * setup: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend + * If a setup method is provided, it is called when a new + * instances is created. It gets passed the same arguments that + * were given to the Class constructor function ( new Class( arguments ... )). + * + * $.Class("MyClass", + * { + * setup: function( val ) { + * this.val = val; + * } + * }) + * var mc = new MyClass("Check Check") + * mc.val //-> 'Check Check' + * + * Setup is called before [jQuery.Class.prototype.init init]. If setup + * return an array, those arguments will be used for init. + * + * $.Class("jQuery.Controller",{ + * setup : function(htmlElement, rawOptions){ + * return [$(htmlElement), + * $.extend({}, this.Class.defaults, rawOptions )] + * } + * }) * *
PRO TIP: * Setup functions are used to normalize constructor arguments and provide a place for @@ -578,44 +704,60 @@ steal.plugins("jquery").then(function( $ ) { * run. *
* + * Setup is not defined on $.Class itself, so calling super in inherting classes + * will break. Don't do the following: + * + * $.Class("Thing",{ + * setup : function(){ + * this._super(); // breaks! + * } + * }) + * * @return {Array|undefined} If an array is return, [jQuery.Class.prototype.init] is * called with those arguments; otherwise, the original arguments are used. */ //break up /** * @function init - * Called with the same arguments as new Class(arguments ...) when a new instance is created. - * @codestart - * $.Class.extend("MyClass", - * { - * init: function( val ) { - * this.val = val; - * } - * }) - * var mc = new MyClass("Check Check") - * mc.val //-> 'Check Check' - * @codeend + * If an init method is provided, it gets called when a new instance + * is created. Init gets called after [jQuery.Class.prototype.setup setup], typically with the + * same arguments passed to the Class + * constructor: ( new Class( arguments ... )). + * + * $.Class("MyClass", + * { + * init: function( val ) { + * this.val = val; + * } + * }) + * var mc = new MyClass(1) + * mc.val //-> 1 + * + * [jQuery.Class.prototype.setup Setup] is able to modify the arguments passed to init. Read + * about it there. + * */ //Breaks up code /** - * @attribute Class - * References the static properties of the instance's class. - *

Quick Example

- * @codestart - * // a class with a static classProperty property - * $.Class.extend("MyClass", {classProperty : true}, {}); + * @attribute constructor + * + * A reference to the Class (or constructor function). This allows you to access + * a class's static properties from an instance. + * + * ### Quick Example * - * // a new instance of myClass - * var mc1 = new MyClass(); + * // a class with a static property + * $.Class("MyClass", {staticProperty : true}, {}); + * + * // a new instance of myClass + * var mc1 = new MyClass(); * - * // - * mc1.Class.classProperty = false; + * // read the static property from the instance: + * mc1.constructor.staticProperty //-> true + * + * Getting static properties with the constructor property, like + * [jQuery.Class.static.fullName fullName], is very common. * - * // creates a new MyClass - * var mc2 = new mc.Class(); - * @codeend - * Getting static properties via the Class property, such as it's - * [jQuery.Class.static.fullName fullName] is very common. */ } @@ -625,18 +767,19 @@ steal.plugins("jquery").then(function( $ ) { - jQuery.Class.prototype. + clss.callback = clss[STR_PROTOTYPE].callback = clss[STR_PROTOTYPE]. /** - * @function callback - * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. - * The only difference is this callback works + * @function proxy + * Returns a method that sets 'this' to the current instance. This does the same thing as + * and is described better in [jQuery.Class.static.proxy]. + * The only difference is this proxy works * on a instance instead of a class. * @param {String|Array} fname If a string, it represents the function to be called. * If it is an array, it will call each function in order and pass the return value of the prior function to the * next function. * @return {Function} the callback function */ - callback = jQuery.Class.callback; + proxy = clss.proxy; -})(); \ No newline at end of file +})(); diff --git a/class/test/qunit/class_test.js b/class/class_test.js similarity index 88% rename from class/test/qunit/class_test.js rename to class/class_test.js index e3c86cce..ec95179c 100644 --- a/class/test/qunit/class_test.js +++ b/class/class_test.js @@ -1,3 +1,6 @@ +steal("jquery/class") //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/class"); test("Creating", function(){ @@ -46,7 +49,7 @@ test("Creating", function(){ new Dog(); new Animal(); new Animal(); - ajax = new Ajax(1000); + var ajax = new Ajax(1000); equals(2, Animal.count, "right number of animals"); equals(1, Dog.count, "right number of animals") @@ -178,3 +181,25 @@ test("Creating without extend", function(){ }); new Foo().dude(true); }) + + +/* Not sure I want to fix this yet. +test("Super in derived when parent doesn't have init", function(){ + $.Class("Parent",{ + }); + + Parent("Derived",{ + init : function(){ + this._super(); + } + }); + + try { + new Derived(); + ok(true, "Can call super in init safely") + } catch (e) { + ok(false, "Failed to call super in init with error: " + e) + } +})*/ + +}); \ No newline at end of file diff --git a/class/qunit.html b/class/qunit.html index 255812e4..503f699f 100644 --- a/class/qunit.html +++ b/class/qunit.html @@ -10,6 +10,6 @@

    - + \ No newline at end of file diff --git a/class/test/qunit/qunit.js b/class/test/qunit/qunit.js deleted file mode 100644 index 3a2d2068..00000000 --- a/class/test/qunit/qunit.js +++ /dev/null @@ -1,5 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/class") //load your app - .plugins('funcunit/qunit') //load qunit - .then("class_test") \ No newline at end of file diff --git a/controller/controller.html b/controller/controller.html index e2253166..a76e2ace 100644 --- a/controller/controller.html +++ b/controller/controller.html @@ -50,11 +50,9 @@ - \ No newline at end of file diff --git a/controller/controller.js b/controller/controller.js index 20f3f280..f3bd8476 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -1,45 +1,59 @@ -steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(function( $ ) { - - // ------- helpers ------ +steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( $ ) { + // ------- HELPER FUNCTIONS ------ + // Binds an element, returns a function that unbinds var bind = function( el, ev, callback ) { - var wrappedCallback; + var wrappedCallback, + binder = el.bind && el.unbind ? el : $(isFunction(el) ? [el] : el); //this is for events like >click. if ( ev.indexOf(">") === 0 ) { ev = ev.substr(1); wrappedCallback = function( event ) { if ( event.target === el ) { callback.apply(this, arguments); - } else { - event.handled = null; - } + } }; } - $(el).bind(ev, wrappedCallback || callback); + binder.bind(ev, wrappedCallback || callback); // if ev name has >, change the name and bind // in the wrapped callback, check that the element matches the actual element return function() { - $(el).unbind(ev, wrappedCallback || callback); + binder.unbind(ev, wrappedCallback || callback); el = ev = callback = wrappedCallback = null; }; }, + makeArray = $.makeArray, + isArray = $.isArray, + isFunction = $.isFunction, + extend = $.extend, + Str = $.String, + each = $.each, + + STR_PROTOTYPE = 'prototype', + STR_CONSTRUCTOR = 'constructor', + slice = Array[STR_PROTOTYPE].slice, + // Binds an element, returns a function that unbinds delegate = function( el, selector, ev, callback ) { - $(el).delegate(selector, ev, callback); + var binder = el.delegate && el.undelegate ? el : $(isFunction(el) ? [el] : el) + binder.delegate(selector, ev, callback); return function() { - $(el).undelegate(selector, ev, callback); - el = ev = callback = selector = null; + binder.undelegate(selector, ev, callback); + binder = el = ev = callback = selector = null; }; }, + + // calls bind or unbind depending if there is a selector binder = function( el, ev, callback, selector ) { return selector ? delegate(el, selector, ev, callback) : bind(el, ev, callback); }, - /** - * moves 'this' to the first argument - */ - shifter = function shifter(cb) { + + // moves 'this' to the first argument, wraps it with jQuery if it's an element + shifter = function shifter(context, name) { + var method = typeof name == "string" ? context[name] : name; return function() { - return cb.apply(null, [$(this)].concat(Array.prototype.slice.call(arguments, 0))); + context.called = name; + return method.apply(context, [this.nodeName ? $(this) : this].concat( slice.call(arguments, 0) ) ); }; }, // matches dots @@ -48,61 +62,69 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func controllersReg = /_?controllers?/ig, //used to remove the controller from the name underscoreAndRemoveController = function( className ) { - return $.String.underscore(className.replace("jQuery.","").replace(dotsReg, '_').replace(controllersReg, "")); + return Str.underscore(className.replace("jQuery.", "").replace(dotsReg, '_').replace(controllersReg, "")); }, // checks if it looks like an action actionMatcher = /[^\w]/, - // gets jus the event - eventCleaner = /^(>?default\.)|(>)/, // handles parameterized action names parameterReplacer = /\{([^\}]+)\}/g, breaker = /^(?:(.*?)\s)?([\w\.\:>]+)$/, - basicProcessor; + basicProcessor, + data = function(el, data){ + return $.data(el, "controllers", data) + }; /** - * @tag core + * @class jQuery.Controller + * @parent jquerymx * @plugin jquery/controller * @download 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 @@ -132,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 * @@ -191,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("
  1. New Todo
  2. "); + * } + * }) * * Now that you've created the controller class, you've must attach the event handlers on the '#todos' div by * creating [jQuery.Controller.prototype.setup|a new controller instance]. There are 2 ways of doing this. @@ -222,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 * @@ -292,26 +312,32 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * //calls FooController.prototype.bar * $(".special").foo("bar","something I want to pass") * @codeend + * + * These methods let you call one controller from another controller. + * */ - $.Class.extend("jQuery.Controller", + $.Class("jQuery.Controller", /** * @Static */ { /** - * Does 3 things: - *
      - *
    1. Creates a jQuery helper for this controller.
    2. - *
    3. Calculates and caches which functions listen for events.
    4. - *
    5. and attaches this element to the documentElement if onDocument is true.
    6. - *
    - *

    jQuery Helper Naming Examples

    - * @codestart - * "TaskController" -> $().task_controller() - * "Controllers.Task" -> $().controllers_task() - * @codeend + * Does 2 things: + * + * - Creates a jQuery helper for this controller. + * - Calculates and caches which functions listen for events. + * + * ### jQuery Helper Naming Examples + * + * + * "TaskController" -> $().task_controller() + * "Controllers.Task" -> $().controllers_task() + * */ - init: function() { + setup: function() { + // Allow contollers to inherit "defaults" from superclasses as it done in $.Class + this._super.apply(this, arguments); + // if you didn't provide a name, or are controller, don't do anything if (!this.shortName || this.fullName == "jQuery.Controller" ) { return; @@ -321,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; @@ -328,13 +366,13 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func if (!$.fn[pluginname] ) { $.fn[pluginname] = function( options ) { - var args = $.makeArray(arguments), + var args = makeArray(arguments), //if the arg is a method on this controller - isMethod = typeof options == "string" && $.isFunction(controller.prototype[options]), + isMethod = typeof options == "string" && isFunction(controller[STR_PROTOTYPE][options]), meth = args[0]; - this.each(function() { + return this.each(function() { //check if created - var controllers = $.data(this, "controllers"), + var controllers = data(this), //plugin is actually the controller instance plugin = controllers && controllers[pluginname]; @@ -352,36 +390,26 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func controller.newInstance.apply(controller, [this].concat(args)); } }); - //always return the element - return this; }; } // make sure listensTo is an array - //@steal-remove-start - if (!$.isArray(this.listensTo) ) { + //!steal-remove-start + if (!isArray(this.listensTo) ) { throw "listensTo is not an array in " + this.fullName; } - //@steal-remove-end + //!steal-remove-end // calculate and cache actions this.actions = {}; - for ( funcName in this.prototype ) { - if (!$.isFunction(this.prototype[funcName]) ) { + for ( funcName in this[STR_PROTOTYPE] ) { + if (funcName == 'constructor' || !isFunction(this[STR_PROTOTYPE][funcName]) ) { continue; } if ( this._isAction(funcName) ) { - this.actions[funcName] = this._getAction(funcName); + this.actions[funcName] = this._action(funcName); } } - - /** - * @attribute onDocument - * Set to true if you want to automatically attach this element to the documentElement. - */ - if ( this.onDocument ) { - forLint = new controller(document.documentElement); - } }, hookup: function( el ) { return new this(el); @@ -396,31 +424,54 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func if ( actionMatcher.test(methodName) ) { return true; } else { - var cleanedEvent = methodName.replace(eventCleaner, ""); - return $.inArray(cleanedEvent, this.listensTo) > -1 || $.event.special[cleanedEvent] || $.Controller.processors[cleanedEvent]; + return $.inArray(methodName, this.listensTo) > -1 || $.event.special[methodName] || processors[methodName]; } }, /** * @hide + * This takes a method name and the options passed to a controller + * and tries to return the data necessary to pass to a processor + * (something that binds things). + * + * For performance reasons, this called twice. First, it is called when + * the Controller class is created. If the methodName is templated + * like : "{window} foo", it returns null. If it is not templated + * it returns event binding data. + * + * The resulting data is added to this.actions. + * + * When a controller instance is created, _action is called again, but only + * on templated actions. + * * @param {Object} methodName the method that will be bound * @param {Object} [options] first param merged with class default options * @return {Object} null or the processor and pre-split parts. * The processor is what does the binding/subscribing. */ - _getAction: function( methodName, options ) { - //if we don't have a controller instance, we'll break this guy up later + _action: function( methodName, options ) { + // reset the test index parameterReplacer.lastIndex = 0; + + //if we don't have options (a controller instance), we'll run this later if (!options && parameterReplacer.test(methodName) ) { return null; } - var convertedName = options ? $.String.sub(methodName, options) : methodName, - parts = convertedName.match(breaker), + // If we have options, run sub to replace templates "{}" with a value from the options + // or the window + var convertedName = options ? Str.sub(methodName, [options, window]) : methodName, + + // If a "{}" resolves to an object, convertedName will be an array + arr = isArray(convertedName), + + // get the parts of the function = [convertedName, delegatePart, eventPart] + parts = (arr ? convertedName[1] : convertedName).match(breaker), event = parts[2], - processor = this.processors[event] || basicProcessor; + processor = processors[event] || basicProcessor; return { processor: processor, - parts: parts + parts: parts, + delegate : arr ? convertedName[0] : undefined }; }, /** @@ -435,19 +486,66 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * //selector - the left of the selector * //cb - the function to call * //controller - the binding controller - * }; + * }; * * This would bind anything like: "foo~3242 myprocessor". * * The processor must return a function that when called, * unbinds the event handler. * + * Controller already has processors for the following events: + * + * - change + * - click + * - contextmenu + * - dblclick + * - focusin + * - focusout + * - keydown + * - keyup + * - keypress + * - mousedown + * - mouseenter + * - mouseleave + * - mousemove + * - mouseout + * - mouseover + * - mouseup + * - reset + * - resize + * - scroll + * - select + * - submit + * + * Listen to events on the document or window + * with templated event handlers: + * + * + * $.Controller('Sized',{ + * "{window} resize" : function(){ + * this.element.width(this.element.parent().width() / 2); + * } + * }); + * + * $('.foo').sized(); */ processors: {}, /** * @attribute listensTo - * A list of special events this controller listens too. You only need to add event names that + * An array of special events this controller + * listens too. You only need to add event names that * are whole words (ie have no special characters). + * + * $.Controller('TabPanel',{ + * listensTo : ['show'] + * },{ + * 'show' : function(){ + * this.element.show(); + * } + * }) + * + * $('.foo').tab_panel().trigger("show"); + * */ listensTo: [], /** @@ -468,8 +566,11 @@ 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 : {} + defaults: {} }, /** * @Prototype @@ -478,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: @@ -501,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.jquery ? 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, "controllers") || $.data(element, "controllers", {}))[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 : { @@ -543,18 +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 ) { - ready = cls.actions[funcName] || cls._getAction(funcName, this.options); - - this._bindings.push( - ready.processor(element, ready.parts[2], ready.parts[1], this.callback(funcName), this)); - } + this.options = extend( extend(true, {}, cls.defaults), options); + /** * @attribute called @@ -564,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 @@ -618,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 @@ -647,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; @@ -656,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.
    @@ -689,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); + extend(this.options, options); + this._unbind(); + this.bind(); }, /** * Destroy unbinds and undelegates all event handlers on this controller, @@ -727,36 +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( ) { + destroy: function() { if ( this._destroyed ) { - throw this.Class.shortName + " controller instance has been deleted"; + throw this[STR_CONSTRUCTOR].shortName + " controller already deleted"; } var self = this, - fname = this.Class._fullName; + fname = this[STR_CONSTRUCTOR].pluginName || this[STR_CONSTRUCTOR]._fullName, + controllers; + + // mark as destroyed this._destroyed = true; + + // remove the className this.element.removeClass(fname); - $.each(this._bindings, function( key, value ) { - if ( $.isFunction(value) ) { - value(self.element[0]); - } - }); - + // unbind bindings + this._unbind(); + // clean up delete this._actions; - - var controllers = this.element.data("controllers"); - if ( controllers && controllers[fname] ) { - delete controllers[fname]; - } + delete this.element.data("controllers")[fname]; + $(this).triggerHandler("destroyed"); //in case we want to know if the controller is removed + this.element = null; }, /** @@ -776,97 +1018,72 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func _set_called: true }); + var processors = $.Controller.processors, //------------- PROCESSSORS ----------------------------- //processors do the binding. They return a function that //unbinds when called. //the basic processor that binds events - basicProcessor = function( el, event, selector, cb, controller ) { - var c = controller.Class; - - // document controllers use their name as an ID prefix. - if ( c.onDocument && !/^Main(Controller)?$/.test(c.shortName) ) { //prepend underscore name if necessary - selector = selector ? "#" + c._shortName + " " + selector : "#" + c._shortName; - } - return binder(el, event, shifter(cb), selector); + basicProcessor = function( el, event, selector, methodName, controller ) { + return binder(el, event, shifter(controller, methodName), selector); }; - var processors = $.Controller.processors, - //a window event only happens on the window - windowEvent = function( el, event, selector, cb ) { - return binder(window, event.replace(/window/, ""), shifter(cb)); - }; - //set commong events to be processed as a basicProcessor - $.each("change click contextmenu dblclick keydown keyup keypress mousedown mousemove mouseout mouseover mouseup reset windowresize resize windowscroll scroll select submit dblclick focusin focusout load unload ready hashchange mouseenter mouseleave".split(" "), function( i, v ) { + + //set common events to be processed as a basicProcessor + each("change click contextmenu dblclick keydown keyup keypress mousedown mousemove mouseout mouseover mouseup reset resize scroll select submit focusin focusout mouseenter mouseleave".split(" "), function( i, v ) { processors[v] = basicProcessor; }); - $.each(["windowresize", "windowscroll", "load", "ready", "unload", "hashchange"], function( i, v ) { - processors[v] = windowEvent; - }); - //the ready processor happens on the document - processors.ready = function( el, event, selector, cb ) { - $(shifter(cb)); //cant really unbind - }; /** * @add jQuery.fn */ - $.fn.mixin = function() { - //create a bunch of controllers - var controllers = $.makeArray(arguments), - forLint; - return this.each(function() { - for ( var i = 0; i < controllers.length; i++ ) { - forLint = new controllers[i](this); - } - - }); - }; //used to determine if a controller instance is one of controllers //controllers can be strings or classes - var isAControllerOf = function( instance, controllers ) { - for ( var i = 0; i < controllers.length; i++ ) { - if ( typeof controllers[i] == 'string' ? instance.Class._shortName == controllers[i] : instance instanceof controllers[i] ) { + var i, isAControllerOf = function( instance, controllers ) { + for ( i = 0; i < controllers.length; i++ ) { + if ( typeof controllers[i] == 'string' ? instance[STR_CONSTRUCTOR]._shortName == controllers[i] : instance instanceof controllers[i] ) { return true; } } return false; }; - - /** - * @function controllers - * Gets all controllers in the jQuery element. - * @return {Array} an array of controller instances. - */ - $.fn.controllers = function() { - var controllerNames = $.makeArray(arguments), - instances = [], - controllers; - //check if arguments - this.each(function() { - controllers = $.data(this, "controllers"); - if (!controllers ) { - return; - } - for ( var cname in controllers ) { - var c = controllers[cname]; - if (!controllerNames.length || isAControllerOf(c, controllerNames) ) { - instances.push(c); + $.fn.extend({ + /** + * @function controllers + * Gets all controllers in the jQuery element. + * @return {Array} an array of controller instances. + */ + controllers: function() { + var controllerNames = makeArray(arguments), + instances = [], + controllers, c, cname; + //check if arguments + this.each(function() { + + controllers = $.data(this, "controllers"); + for ( cname in controllers ) { + if ( controllers.hasOwnProperty(cname) ) { + c = controllers[cname]; + if (!controllerNames.length || isAControllerOf(c, controllerNames) ) { + instances.push(c); + } + } } - } - }); - return instances; - }; - /** - * @function controller - * Gets a controller in the jQuery element. With no arguments, returns the first one found. - * @param {Object} controller (optional) if exists, the first controller instance with this class type will be returned. - * @return {jQuery.Controller} the first controller. - */ - $.fn.controller = function( controller ) { - return this.controllers.apply(this, arguments)[0]; - }; + }); + return instances; + }, + /** + * @function controller + * Gets a controller in the jQuery element. With no arguments, returns the first one found. + * @param {Object} controller (optional) if exists, the first controller instance with this class type will be returned. + * @return {jQuery.Controller} the first controller. + */ + controller: function( controller ) { + return this.controllers.apply(this, arguments)[0]; + } + }); + -}); \ No newline at end of file +}); diff --git a/controller/controller_test.js b/controller/controller_test.js new file mode 100644 index 00000000..d94799e9 --- /dev/null +++ b/controller/controller_test.js @@ -0,0 +1,269 @@ +steal("jquery/controller",'jquery/controller/subscribe') //load your app + .then('funcunit/qunit') //load qunit + .then(function(){ + +module("jquery/controller") +test("subscribe testing works", function(){ + + var ta = $("
    ").appendTo( $("#qunit-test-area") ) + + ta.html("click here") + + var clicks = 0, destroys = 0; + var subscribes = 0; + $.Controller.extend("MyTest",{ + click: function() { + clicks++ + }, + "a.b subscribe" : function() { + subscribes++ + }, + destroy: function() { + + this._super() + destroys++; + } + }) + ta.my_test(); + ta.trigger("click") + equals(clicks,1, "can listen to clicks") + + OpenAjax.hub.publish("a.b",{}) + equals(subscribes,1, "can subscribe") + var controllerInstance = ta.controller('my_test') + ok( controllerInstance.Class == MyTest, "can get controller" ) + controllerInstance.destroy() + + equals(destroys,1, "destroy called once") + ok(!ta.controller(), "controller is removed") + + OpenAjax.hub.publish("a.b",{}) + equals(subscribes,1, "subscription is torn down") + ta.trigger("click") + equals(clicks,1, "No longer listening") + + + + ta.my_test(); + ta.trigger("click") + OpenAjax.hub.publish("a.b",{}) + equals(clicks,2, "can listen again to clicks") + equals(subscribes,2, "can listen again to subscription") + + ta.remove(); + + ta.trigger("click") + OpenAjax.hub.publish("a.b",{}) + equals(clicks,2, "Clicks stopped") + equals(subscribes,2, "Subscribes stopped") +}) + + + +test("bind to any special", function(){ + jQuery.event.special.crazyEvent = { + + } + var called = false; + jQuery.Controller.extend("WeirdBind",{ + crazyEvent: function() { + called = true; + } + }) + var a = $("
    ").appendTo($("#qunit-test-area")) + a.weird_bind(); + a.trigger("crazyEvent") + ok(called, "heard the trigger"); + + $("#qunit-test-area").html("") + +}) + +test("parameterized actions", function(){ + var called = false; + jQuery.Controller.extend("WeirderBind",{ + "{parameterized}" : function() { + called = true; + } + }) + var a = $("
    ").appendTo($("#qunit-test-area")) + a.weirder_bind({parameterized: "sillyEvent"}); + a.trigger("sillyEvent") + ok(called, "heard the trigger") + + $("#qunit-test-area").html("") +}) + +test("windowresize", function(){ + var called = false; + jQuery.Controller.extend("WindowBind",{ + "{window} resize" : function() { + called = true; + } + }) + $("#qunit-test-area").html("
    ") + $("#weird").window_bind(); + $(window).trigger('resize') + ok(called,"got window resize event"); + + $("#qunit-test-area").html("") +}) + +// this.delegate(this.cached.header.find('tr'), "th", "mousemove", "th_mousemove"); +test("delegate", function(){ + var called = false; + jQuery.Controller.extend("DelegateTest",{ + click: function() {} + }) + var els = $("
    click me
    ").appendTo($("#qunit-test-area")) + var c = els.delegate_test(); + c.controller().delegate(els.find("span"), "a", "click", function(){ + called = true; + }) + els.find("a").trigger('click') + ok(called, "delegate works") + $("#qunit-test-area").html("") +}) + +test("inherit", function(){ + var called = false; + $.Controller.extend( "Parent", { + click: function(){ + called = true; + } + }) + Parent.extend( "Child", { + + }) + var els = $("
    click me
    ").appendTo($("#qunit-test-area")) + els.child(); + els.find("a").trigger('click') + ok(called, "inherited the click method") + $("#qunit-test-area").html("") +}); + +test("objects in action", function(){ + $.Controller('Thing',{ + "{item} someEvent" : function(thing, ev){ + ok(true, "called"); + equals(ev.type, "someEvent","correct event") + equals(this.constructor.fullName, "Thing", "This is a controller isntance") + equals(thing.name,"Justin","Raw, not jQuery wrapped thing") + } + }); + + var thing1 = {name: "Justin"}; + + var ta = $("
    ").appendTo( $("#qunit-test-area") ) + ta.thing({item : thing1}); + + $(thing1).trigger("someEvent"); + + $("#qunit-test-area").html(""); + +}); + +test("dot",function(){ + $.Controller("Dot",{ + "foo.bar" : function(){ + ok(true,'called') + } + }); + + var ta = $("
    ").appendTo( $("#qunit-test-area") ); + ta.dot().trigger("foo.bar"); + $("#qunit-test-area").html(""); +}) + +// HTMLFormElement[0] breaks +test("the right element", 1, function(){ + $.Controller('FormTester',{ + init : function(){ + equals(this.element[0].nodeName.toLowerCase(), "form" ) + } + }) + $("
    ").appendTo( $("#qunit-test-area") ) + .form_tester(); + $("#qunit-test-area").html("") +}) + +test("pluginName", function() { + // Testing for controller pluginName fixes as reported in + // http://forum.javascriptmvc.com/#topic/32525000000253001 + // http://forum.javascriptmvc.com/#topic/32525000000488001 + expect(6); + + $.Controller("PluginName", { + pluginName : "my_plugin" + }, { + method : function(arg) { + ok(true, "Method called"); + }, + + update : function(options) { + this._super(options); + ok(true, "Update called"); + }, + + destroy : function() { + ok(true, "Destroyed"); + this._super(); + } + }); + + var ta = $("
    ").addClass('existing_class').appendTo( $("#qunit-test-area") ); + ta.my_plugin(); // Init + ok(ta.hasClass("my_plugin"), "Should have class my_plugin"); + ta.my_plugin(); // Update + ta.my_plugin("method"); // method() + ta.controller().destroy(); // destroy + ok(!ta.hasClass("my_plugin"), "Shouldn't have class my_plugin after being destroyed"); + ok(ta.hasClass("existing_class"), "Existing class should still be there"); +}) + +test("inherit defaults", function() { + $.Controller.extend("BaseController", { + defaults : { + foo: 'bar' + } + }, {}); + + BaseController.extend("InheritingController", { + defaults : { + newProp : 'newVal' + } + }, {}); + + ok(InheritingController.defaults.foo === 'bar', 'Class must inherit defaults from the parent class'); + ok(InheritingController.defaults.newProp == 'newVal', 'Class must have own defaults'); + var inst = new InheritingController($('
    '), {}); + ok(inst.options.foo === 'bar', 'Instance must inherit defaults from the parent class'); + ok(inst.options.newProp == 'newVal', 'Instance must have defaults of it`s class'); +}); + +test("update rebinding", 2, function(){ + var first = true; + $.Controller("Rebinder", { + "{item} foo" : function(item, ev){ + if(first){ + equals(item.id, 1, "first item"); + first = false; + } else { + equals(item.id, 2, "first item"); + } + } + }); + + var item1 = {id: 1}, + item2 = {id: 2}, + el = $('
    ').rebinder({item: item1}) + + $(item1).trigger("foo") + + el.rebinder({item: item2}); + + $(item2).trigger("foo") +}) + + +}); diff --git a/controller/demo-update.html b/controller/demo-update.html new file mode 100644 index 00000000..cb5e936b --- /dev/null +++ b/controller/demo-update.html @@ -0,0 +1,54 @@ + + + + Controller Example + + + +
    +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/controller/history/history.html b/controller/history/history.html deleted file mode 100644 index 3bc6b0a9..00000000 --- a/controller/history/history.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - hover - - - - - - - - diff --git a/controller/history/history.js b/controller/history/history.js deleted file mode 100644 index c17d8526..00000000 --- a/controller/history/history.js +++ /dev/null @@ -1,187 +0,0 @@ -steal.plugins('jquery/controller/subscribe', - 'jquery/event/hashchange').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. - * - * Typically you subscribe to a history event in your controllers: - * - * $.Controller("MyHistory",{ - * "history.pagename subscribe" : function(called, data){ - * //called when hash = #pagename - * } - * }) - * - * 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} - * - */ - -var keyBreaker = /([^\[\]]+)|(\[\])/g; - -$.Controller.History = { - /** - * - * returns the pathname part - * - * @codestart - * "#foo/bar&foo=bar" -> 'foo/bar' - * @codeend - */ - pathname : function(path) { - var parts = path.match(/#([^&]*)/); - return parts ? parts[1] : null - }, - /** - * returns the search part, but without the first & - * @codestart - * "#foo/bar&foo=bar" -> 'foo=barr' - * @codeend - */ - search : function(path) { - var parts = path.match(/#[^&]*&(.*)/); - return parts ? parts[1] : null - }, - getData: function(path) { - var search = $.Controller.History.search(path), - digitTest = /^\d+$/; - if(! search || ! search.match(/([^?#]*)(#.*)?$/) ) { - return {}; - } - - // Support the legacy format that used MVC.Object.to_query_string that used %20 for - // spaces and not the '+' sign; - search = search.replace(/\+/g,"%20") - - var data = {}, - pairs = search.split('&'), - current; - - for(var i=0; i < pairs.length; i++){ - current = data; - var pair = pairs[i].split('='); - - // if we find foo=1+1=2 - if(pair.length != 2) { - pair = [pair[0], pair.slice(1).join("=")] - } - - var key = decodeURIComponent(pair[0]), - value = decodeURIComponent(pair[1]), - parts = key.match(keyBreaker); - - for ( var j = 0; j < parts.length - 1; j++ ) { - var part = parts[j]; - if (!current[part] ) { - current[part] = digitTest.test(part) || parts[j+1] == "[]" ? [] : {} - } - current = current[part]; - } - lastPart = parts[parts.length - 1]; - if(lastPart == "[]"){ - current.push(value) - }else{ - current[lastPart] = value; - } - } - return data; - } -}; - - - - - -jQuery(function($) { - $(window).bind('hashchange',function() { - var data = $.Controller.History.getData(location.href), - folders = $.Controller.History.pathname(location.href) || 'index', - hasSlash = (folders.indexOf('/') != -1); - - if( !hasSlash && folders != 'index' ) { - folders += '/index'; - } - - OpenAjax.hub.publish("history."+folders.replace("/","."), data); - }); - - setTimeout(function(){ - $(window).trigger('hashchange') - },1) //immediately after ready -}) - - -$.extend($.Controller.prototype, { - /** - * Redirects to another page. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - */ - redirectTo: function(options){ - var point = this._get_history_point(options); - location.hash = point; - }, - /** - * Redirects to another page by replacing current URL with the given one. This - * call will not create a new entry in the history. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - */ - replaceWith: function(options){ - var point = this._get_history_point(options); - location.replace(location.href.split('#')[0] + point); - }, - /** - * Adds history point to browser history. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - * @param {Object} data extra data saved in history -- NO LONGER SUPPORTED - */ - historyAdd : function(options, data) { - var point = this._get_history_point(options); - location.hash = point; - }, - /** - * Creates a history point from given options. Resultant history point is like #controller/action¶m1=value1 - * @plugin 'dom/history' - * @param {Object} options an object that will turned into history point - */ - _get_history_point: function(options) { - var controller_name = options.controller || this.Class.underscoreName; - var action_name = options.action || 'index'; - - /* Convert the options to parameters (removing controller and action if needed) */ - if(options.controller) - delete options.controller; - if(options.action) - delete options.action; - - var paramString = (options) ? $.param(options) : ''; - if(paramString.length) - paramString = '&' + paramString; - - return '#' + controller_name + '/' + action_name + paramString; - }, - - /** - * Provides current window.location parameters as object properties. - * @plugin 'dom/history' - */ - pathData :function() { - return $.Controller.History.getData(location.href); - } -}); - - - - - -}); \ No newline at end of file diff --git a/controller/history/qunit/qunit.js b/controller/history/qunit/qunit.js deleted file mode 100644 index 9a6d5e71..00000000 --- a/controller/history/qunit/qunit.js +++ /dev/null @@ -1,38 +0,0 @@ -steal.plugins('funcunit/qunit','jquery/controller/history').then(function($){ - -module("jquery/controller/history",{ - setup: function(){ - - } -}) - -test("Basic getData",function(){ - - var data = $.Controller.History.getData("#foo/bar&a=b"); - equals(data.a,"b") - - var data = $.Controller.History.getData("#foo/bar&a=b&c=d"); - equals(data.a,"b") - equals(data.c,"d") -}) -test("Nested getData",function(){ - - var data = $.Controller.History.getData("#foo/bar&a[b]=1&a[c]=2"); - equals(data.a.b,1) - equals(data.a.c,2) - - var data = $.Controller.History.getData("#foo/bar&a[]=1&a[]=2"); - equals(data.a[0],1) - equals(data.a[1],2) - - var data = $.Controller.History.getData("#foo/bar&a[b][]=1&a[b][]=2"); - equals(data.a.b[0],1) - equals(data.a.b[1],2) - - var data = $.Controller.History.getData("#foo/bar&a[0]=1&a[1]=2"); - equals(data.a[0],1) - equals(data.a[1],2) -}) - - -}) diff --git a/controller/pages/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 97% rename from controller/pages/plugin.js rename to controller/pages/plugin.md index d82e324a..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 @@ -17,7 +16,7 @@ For example, the following controller: } }) -creates a jQuery.fn.my_tabs method that you can use like: +creates a jQuery.fn.my_widget method that you can use like: // create my_widget on each .thing $(".thing").my_widget({message : "Hello"}) @@ -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/controller/qunit.html b/controller/qunit.html index 909f3130..ae8fad54 100644 --- a/controller/qunit.html +++ b/controller/qunit.html @@ -6,7 +6,7 @@ margin: 0px; padding: 0px; } - + diff --git a/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/test/qunit/controller_test.js b/controller/test/qunit/controller_test.js deleted file mode 100644 index 210685a0..00000000 --- a/controller/test/qunit/controller_test.js +++ /dev/null @@ -1,170 +0,0 @@ -module("jquery/controller") -test("subscribe testing works", function(){ - - var ta = $("
    ").appendTo( $("#qunit-test-area") ) - - ta.html("click here") - - var clicks = 0, destroys = 0; - var subscribes = 0; - $.Controller.extend("MyTest",{ - click: function() { - clicks++ - }, - "a.b subscribe" : function() { - subscribes++ - }, - destroy: function() { - - this._super() - destroys++; - } - }) - ta.my_test(); - ta.trigger("click") - equals(clicks,1, "can listen to clicks") - - OpenAjax.hub.publish("a.b",{}) - equals(subscribes,1, "can subscribe") - var controllerInstance = ta.controller('my_test') - ok( controllerInstance.Class == MyTest, "can get controller" ) - controllerInstance.destroy() - - equals(destroys,1, "destroy called once") - ok(!ta.controller(), "controller is removed") - - OpenAjax.hub.publish("a.b",{}) - equals(subscribes,1, "subscription is torn down") - ta.trigger("click") - equals(clicks,1, "No longer listening") - - - - ta.my_test(); - ta.trigger("click") - OpenAjax.hub.publish("a.b",{}) - equals(clicks,2, "can listen again to clicks") - equals(subscribes,2, "can listen again to subscription") - - ta.remove(); - - ta.trigger("click") - OpenAjax.hub.publish("a.b",{}) - equals(clicks,2, "Clicks stopped") - equals(subscribes,2, "Subscribes stopped") -}) - - -test("document and main controllers", function(){ - var a = $("
    ").appendTo($("#qunit-test-area")), - a_inner = a.find('span'), - b = $("
    ").appendTo($("#qunit-test-area")), - b_inner = b.find('span'), - doc_outer_clicks = 0, - doc_inner_clicks = 0, - main_outer_clicks = 0, - main_inner_clicks = 0; - - $.Controller.extend("TestController", { onDocument: true }, { - click: function() { - doc_outer_clicks++; - }, - "span click" : function() { - doc_inner_clicks++; - } - }) - - a_inner.trigger("click"); - equals(doc_outer_clicks,1,"document controller handled (no-selector) click inside listening element"); - equals(doc_inner_clicks,1,"document controller handled (selector) click inside listening element"); - - b_inner.trigger("click"); - equals(doc_outer_clicks,1,"document controller ignored (no-selector) click outside listening element"); - equals(doc_inner_clicks,1,"document controller ignored (selector) click outside listening element"); - - $(document.documentElement).controller('test').destroy(); - - $.Controller.extend("MainController", { onDocument: true }, { - click: function() { - main_outer_clicks++; - }, - "span click" : function() { - main_inner_clicks++; - } - }) - - b_inner.trigger("click"); - equals(main_outer_clicks,1,"main controller handled (no-selector) click"); - equals(main_inner_clicks,1,"main controller handled (selector) click"); - - $(document.documentElement).controller('main').destroy(); - - a.remove(); - b.remove(); -}) - - -test("bind to any special", function(){ - jQuery.event.special.crazyEvent = { - - } - var called = false; - jQuery.Controller.extend("WeirdBind",{ - crazyEvent: function() { - called = true; - } - }) - var a = $("
    ").appendTo($("#qunit-test-area")) - a.weird_bind(); - a.trigger("crazyEvent") - ok(called, "heard the trigger"); - - $("#qunit-test-area").html("") - -}) - -test("parameterized actions", function(){ - var called = false; - jQuery.Controller.extend("WeirderBind",{ - "{parameterized}" : function() { - called = true; - } - }) - var a = $("
    ").appendTo($("#qunit-test-area")) - a.weirder_bind({parameterized: "sillyEvent"}); - a.trigger("sillyEvent") - ok(called, "heard the trigger") - - $("#qunit-test-area").html("") -}) - -test("windowresize", function(){ - var called = false; - jQuery.Controller.extend("WindowBind",{ - "windowresize" : function() { - called = true; - } - }) - $("#qunit-test-area").html("
    ") - $("#weird").window_bind(); - $(window).trigger('resize') - ok(called,"got window resize event"); - - $("#qunit-test-area").html("") -}) - -// this.delegate(this.cached.header.find('tr'), "th", "mousemove", "th_mousemove"); -test("delegate", function(){ - var called = false; - jQuery.Controller.extend("DelegateTest",{ - click: function() {} - }) - var els = $("").appendTo($("#qunit-test-area")) - var c = els.delegate_test(); - c.controller().delegate(els.find("span"), "a", "click", function(){ - called = true; - }) - els.find("a").trigger('click') - ok(called, "delegate works") - $("#qunit-test-area").html("") -}) diff --git a/controller/test/qunit/qunit.js b/controller/test/qunit/qunit.js deleted file mode 100644 index 611a8519..00000000 --- a/controller/test/qunit/qunit.js +++ /dev/null @@ -1,9 +0,0 @@ -//we probably have to have this only describing where the tests are -steal - .plugins("jquery/controller",'jquery/controller/subscribe') //load your app - .plugins('funcunit/qunit') //load qunit - .then("controller_test") - -if(steal.browser.rhino){ - steal.plugins('funcunit/qunit/env') -} \ No newline at end of file diff --git a/controller/view/qunit.html b/controller/view/qunit.html index cfb5649a..cc57a785 100644 --- a/controller/view/qunit.html +++ b/controller/view/qunit.html @@ -6,7 +6,7 @@ margin: 0px; padding: 0px; } - + diff --git a/controller/view/test/qunit/controller_view_test.js b/controller/view/test/qunit/controller_view_test.js index d35ce47c..506da897 100644 --- a/controller/view/test/qunit/controller_view_test.js +++ b/controller/view/test/qunit/controller_view_test.js @@ -1,15 +1,50 @@ -module("jquery/controller/view") -test("this.view", function(){ +steal('jquery/controller/view','jquery/view/micro','funcunit/qunit') //load qunit + .then(function(){ - $.Controller.extend("jquery.Controller.View.Test.Qunit",{ - init: function() { - this.element.html(this.view()) - } - }) - jQuery.View.ext = ".micro"; - $("#qunit-test-area").append("
    "); + module("jquery/controller/view"); + + test("this.view", function(){ + + $.Controller.extend("jquery.Controller.View.Test.Qunit",{ + init: function() { + this.element.html(this.view()) + } + }) + jQuery.View.ext = ".micro"; + $("#qunit-test-area").append("
    "); + + new jquery.Controller.View.Test.Qunit( $('#cont_view') ); + + ok(/Hello World/i.test($('#cont_view').text()),"view rendered") + }); - new jquery.Controller.View.Test.Qunit( $('#cont_view') ); + test("test.suffix.doubling", function(){ + + $.Controller.extend("jquery.Controller.View.Test.Qunit",{ + init: function() { + this.element.html(this.view('init.micro')) + } + }) + + jQuery.View.ext = ".ejs"; // Reset view extension to default + equal(".ejs", jQuery.View.ext); + + $("#qunit-test-area").append("
    "); + + new jquery.Controller.View.Test.Qunit( $('#suffix_test_cont_view') ); + + ok(/Hello World/i.test($('#suffix_test_cont_view').text()),"view rendered") + }); - ok(/Hello World/i.test($('#cont_view').text()),"view rendered") -}); \ No newline at end of file + test("complex paths nested inside a controller directory", function(){ + $.Controller.extend("Myproject.Controllers.Foo.Bar"); + + var path = jQuery.Controller._calculatePosition(Myproject.Controllers.Foo.Bar, "init.ejs", "init") + equals(path, "//myproject/views/foo/bar/init.ejs", "view path is correct") + + $.Controller.extend("Myproject.Controllers.FooBar"); + path = jQuery.Controller._calculatePosition(Myproject.Controllers.FooBar, "init.ejs", "init") + equals(path, "//myproject/views/foo_bar/init.ejs", "view path is correct") + }) +}); + diff --git a/controller/view/test/qunit/qunit.js b/controller/view/test/qunit/qunit.js index 38255d8c..7873c312 100644 --- a/controller/view/test/qunit/qunit.js +++ b/controller/view/test/qunit/qunit.js @@ -1,6 +1,5 @@ //we probably have to have this only describing where the tests are -steal - .plugins('jquery/controller/view','jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("controller_view_test") +steal('jquery/controller/view','jquery/view/micro') //load your app + .then('funcunit/qunit') //load qunit + .then("./controller_view_test.js") diff --git a/controller/view/view.js b/controller/view/view.js index 8872d138..c248fa5c 100644 --- a/controller/view/view.js +++ b/controller/view/view.js @@ -1,23 +1,30 @@ -steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { +steal('jquery/controller', 'jquery/view').then(function( $ ) { + var URI = steal.URI || steal.File; + jQuery.Controller.getFolder = function() { return jQuery.String.underscore(this.fullName.replace(/\./g, "/")).replace("/Controllers", ""); }; - var calculatePosition = function( Class, view, action_name ) { - var slashes = Class.fullName.replace(/\./g, "/"), - hasControllers = slashes.indexOf("/Controllers/" + Class.shortName) != -1, - path = jQuery.String.underscore(slashes.replace("/Controllers/" + Class.shortName, "")), - controller_name = Class._shortName, - suffix = (typeof view == "string" && view.match(/\.[\w\d]+$/)) || jQuery.View.ext; + jQuery.Controller._calculatePosition = function( Class, view, action_name ) { + + var classParts = Class.fullName.split('.'), + classPartsWithoutPrefix = classParts.slice(0); + classPartsWithoutPrefix.splice(0, 2); // Remove prefix (usually 2 elements) + var classPartsWithoutPrefixSlashes = classPartsWithoutPrefix.join('/'), + hasControllers = (classParts.length > 2) && classParts[1] == 'Controllers', + path = hasControllers? jQuery.String.underscore(classParts[0]): jQuery.String.underscore(classParts.join("/")), + controller_name = jQuery.String.underscore(classPartsWithoutPrefix.join('/')).toLowerCase(), + suffix = (typeof view == "string" && /\.[\w\d]+$/.test(view)) ? "" : jQuery.View.ext; + //calculate view if ( typeof view == "string" ) { if ( view.substr(0, 2) == "//" ) { //leave where it is } else { - view = "//" + new steal.File('views/' + (view.indexOf('/') !== -1 ? view : (hasControllers ? controller_name + '/' : "") + view)).joinFrom(path) + suffix; + view = "//" + URI(path).join( 'views/' + (view.indexOf('/') !== -1 ? view : (hasControllers ? controller_name + '/' : "") + view)) + suffix; } } else if (!view ) { - view = "//" + new steal.File('views/' + (hasControllers ? controller_name + '/' : "") + action_name.replace(/\.|#/g, '').replace(/ /g, '_')).joinFrom(path) + suffix; + view = "//" + URI(path).join('views/' + (hasControllers ? controller_name + '/' : "") + action_name.replace(/\.|#/g, '').replace(/ /g, '_'))+ suffix; } return view; }; @@ -38,14 +45,16 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { } //load from name var current = window; - var parts = this.Class.fullName.split(/\./); + var parts = this.constructor.fullName.split(/\./); for ( var i = 0; i < parts.length; i++ ) { - if ( typeof current.Helpers == 'object' ) { - jQuery.extend(helpers, current.Helpers); + if(current){ + if ( typeof current.Helpers == 'object' ) { + jQuery.extend(helpers, current.Helpers); + } + current = current[parts[i]]; } - current = current[parts[i]]; } - if ( typeof current.Helpers == 'object' ) { + if (current && typeof current.Helpers == 'object' ) { jQuery.extend(helpers, current.Helpers); } this._default_helpers = helpers; @@ -71,18 +80,20 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { * el.html( this.view() ) * // renders with views/tasks/under.ejs * el.after( this.view("under", [1,2]) ); + * // renders with views/tasks/under.micro + * el.after( this.view("under.micro", [1,2]) ); * // renders with views/shared/top.ejs * el.before( this.view("shared/top", {phrase: "hi"}) ); * } * }) * @codeend - * @plugin controller/view + * @plugin jquery/controller/view * @return {String} the rendered result of the view. - * @param {String} [optional1] view The view you are going to render. If a view isn't explicity given + * @param {String} [view] The view you are going to render. If a view isn't explicity given * this function will try to guess at the correct view as show in the example code above. - * @param {Object} [optional2] data data to be provided to the view. If not present, the controller instance + * @param {Object} [data] data to be provided to the view. If not present, the controller instance * is used. - * @param {Object} [optional3] myhelpers an object of helpers that will be available in the view. If not present + * @param {Object} [myhelpers] an object of helpers that will be available in the view. If not present * this controller class's "Helpers" property will be used. * */ @@ -94,7 +105,7 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { view = null; } //guess from controller name - view = calculatePosition(this.Class, view, this.called); + view = jQuery.Controller._calculatePosition(this.Class, view, this.called); //calculate data data = data || this; diff --git a/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 76813043..400e87c2 100644 --- a/dom/compare/compare.js +++ b/dom/compare/compare.js @@ -1,62 +1,80 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function($){ +steal('jquery/dom').then(function($){ /** * @function compare * @parent dom * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/compare/compare.js + * * Compares the position of two nodes and returns a bitmask detailing how they are positioned - * relative to each other. You can expect it to return the same results as + * relative to each other. + * + * $('#foo').compare($('#bar')) //-> Number + * + * You can expect it to return the same results as * [http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition | compareDocumentPosition]. * Parts of this documentation and source come from [http://ejohn.org/blog/comparing-document-position | John Resig]. - *

    Demo

    + * + * ## Demo * @demo jquery/dom/compare/compare.html * @test jquery/dom/compare/qunit.html * @plugin dom/compare - * @param {HTMLElement} a the first node - * @param {HTMLElement} b the second node - * @return {Number} A bitmap with the following digit values: + * + * + * @param {HTMLElement|jQuery} element an element or jQuery collection to compare against. + * @return {Number} A bitmap number representing how the elements are positioned from each other. + * + * If the code looks like: + * + * $('#foo').compare($('#bar')) //-> Number + * + * Number is a bitmap with with the following values: * * * - * - * - * - * - * + * + * + * + * + * *
    BitsNumberMeaning
    0000000Elements are identical.
    0000011The nodes are in different documents (or one is outside of a document).
    0000102Node B precedes Node A.
    0001004Node A precedes Node B.
    0010008Node B contains Node A.
    01000016Node A contains Node B.
    0000011The nodes are in different + * documents (or one is outside of a document).
    0000102#bar precedes #foo.
    0001004#foo precedes #bar.
    0010008#bar contains #foo.
    01000016#foo contains #bar.
    */ -jQuery.fn.compare = function(b){ //usually - //b is usually a relatedTarget, but b/c it is we have to avoid a few FF errors +jQuery.fn.compare = function(element){ //usually + //element is usually a relatedTarget, but element/c it is we have to avoid a few FF errors try{ //FF3 freaks out with XUL - b = b.jquery ? b[0] : b; + element = element.jquery ? element[0] : element; }catch(e){ return null; } if (window.HTMLElement) { //make sure we aren't coming from XUL element - var s = HTMLElement.prototype.toString.call(b) - if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]') return null; + + var s = HTMLElement.prototype.toString.call(element) + if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]' || s === '[object Window]') { + return null; + } + } if(this[0].compareDocumentPosition){ - return this[0].compareDocumentPosition(b); + return this[0].compareDocumentPosition(element); } - if(this[0] == document && b != document) return 8; - var number = (this[0] !== b && this[0].contains(b) && 16) + (this[0] != b && b.contains(this[0]) && 8), + if(this[0] == document && element != document) return 8; + var number = (this[0] !== element && this[0].contains(element) && 16) + (this[0] != element && element.contains(this[0]) && 8), docEl = document.documentElement; if(this[0].sourceIndex){ - number += (this[0].sourceIndex < b.sourceIndex && 4) - number += (this[0].sourceIndex > b.sourceIndex && 2) - number += (this[0].ownerDocument !== b.ownerDocument || + number += (this[0].sourceIndex < element.sourceIndex && 4) + number += (this[0].sourceIndex > element.sourceIndex && 2) + number += (this[0].ownerDocument !== element.ownerDocument || (this[0] != docEl && this[0].sourceIndex <= 0 ) || - (b != docEl && b.sourceIndex <= 0 )) && 1 + (element != docEl && element.sourceIndex <= 0 )) && 1 }else{ var range = document.createRange(), sourceRange = document.createRange(), compare; range.selectNode(this[0]); - sourceRange.selectNode(b); + sourceRange.selectNode(element); compare = range.compareBoundaryPoints(Range.START_TO_START, sourceRange); } diff --git a/dom/compare/test/qunit/compare_test.js b/dom/compare/compare_test.js similarity index 89% rename from dom/compare/test/qunit/compare_test.js rename to dom/compare/compare_test.js index 056bf7b5..08ef31a8 100644 --- a/dom/compare/test/qunit/compare_test.js +++ b/dom/compare/compare_test.js @@ -1,3 +1,6 @@ +steal("jquery/dom/compare") //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/dom/compare") test("Compare cases", function(){ $(document.body).append("
    ") @@ -16,4 +19,6 @@ test("Compare cases", function(){ equals(first.compare(second), 4, "A sibling elements"); equals(second.compare(first), 2, "A sibling elements"); outer.remove() -}) \ No newline at end of file +}); + +}); \ No newline at end of file diff --git a/dom/compare/qunit.html b/dom/compare/qunit.html index 1f2208f1..d7c40442 100644 --- a/dom/compare/qunit.html +++ b/dom/compare/qunit.html @@ -15,6 +15,6 @@

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

      Cookie plugin

      - * - * - *

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

      Quick Examples

      * * Set the value of a cookie. - * @codestart - * * $.cookie('the_cookie', 'the_value'); - * @codeend + * + * $.cookie('the_cookie', 'the_value'); * * Create a cookie with all available options. * @codestart @@ -59,7 +56,7 @@ steal.plugins('jquery/lang/json').then(function() { * @param {String} [domain] The value of the domain attribute of the cookie (default: domain of page that created the cookie).
      * @param {Boolean} secure If true, the secure attribute of the cookie will be set and the cookie transmission will * require a secure protocol (like HTTPS).
      - * @return {String} or {undefined} when setting the cookie. + * @return {String} the value of the cookie or {undefined} when setting the cookie. */ jQuery.cookie = function(name, value, options) { if (typeof value != 'undefined') { // name and value given, set cookie diff --git a/dom/cur_styles/cur_styles.html b/dom/cur_styles/cur_styles.html index 51e11c41..a8cfe5f6 100644 --- a/dom/cur_styles/cur_styles.html +++ b/dom/cur_styles/cur_styles.html @@ -23,12 +23,11 @@

      CurStyles Performance

      - - diff --git a/dom/cur_styles/cur_styles.js b/dom/cur_styles/cur_styles.js index ecab26d4..a563d5b4 100644 --- a/dom/cur_styles/cur_styles.js +++ b/dom/cur_styles/cur_styles.js @@ -1,120 +1,117 @@ -steal.plugins('jquery/dom').then(function($){ +steal('jquery/dom').then(function( $ ) { -var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, - rupper = /([A-Z])/g, - rdashAlpha = /-([a-z])/ig, - fcamelCase = function(all, letter) { - return letter.toUpperCase(); - }, - getStyle = function(elem) { - if (getComputedStyle) { - return getComputedStyle(elem, null); - } - else if (elem.currentStyle) { - return elem.currentStyle - } - }, - rfloat = /float/i, - rnumpx = /^-?\d+(?:px)?$/i, - rnum = /^-?\d/; -/** - * @add jQuery - */ -// -/** - * @function curStyles - * @param {HTMLElement} el - * @param {Array} styles An array of style names like ['marginTop','borderLeft'] - * @return {Object} an object of style:value pairs. Style names are camelCase. - */ -$.curStyles = function(el, styles) { - if(!el){ - return null; - } - var currentS = getStyle(el), - oldName, - val, - style = el.style, - results = {}, - i=0, - name; - - for(; i < styles.length; i++){ - name = styles[i]; - oldName = name.replace(rdashAlpha, fcamelCase); - - if ( rfloat.test( name ) ) { - name = jQuery.support.cssFloat ? "float" : "styleFloat"; - oldName = "cssFloat" + var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, + rupper = /([A-Z])/g, + rdashAlpha = /-([a-z])/ig, + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }, + getStyle = function( elem ) { + if ( getComputedStyle ) { + return getComputedStyle(elem, null); + } + else if ( elem.currentStyle ) { + return elem.currentStyle; + } + }, + rfloat = /float/i, + rnumpx = /^-?\d+(?:px)?$/i, + rnum = /^-?\d/; + /** + * @add jQuery + */ + // + /** + * @function curStyles + * @param {HTMLElement} el + * @param {Array} styles An array of style names like ['marginTop','borderLeft'] + * @return {Object} an object of style:value pairs. Style names are camelCase. + */ + $.curStyles = function( el, styles ) { + if (!el ) { + return null; } - - if (getComputedStyle) { - name = name.replace(rupper, "-$1").toLowerCase(); - val = currentS.getPropertyValue(name); - if ( name === "opacity" && val === "" ) { - val = "1"; + var currentS = getStyle(el), + oldName, val, style = el.style, + results = {}, + i = 0, + left, rsLeft, camelCase, name; + + for (; i < styles.length; i++ ) { + name = styles[i]; + oldName = name.replace(rdashAlpha, fcamelCase); + + if ( rfloat.test(name) ) { + name = jQuery.support.cssFloat ? "float" : "styleFloat"; + oldName = "cssFloat"; } - results[oldName] = val; - } else { - var camelCase = name.replace(rdashAlpha, fcamelCase); - results[oldName] = currentS[name] || currentS[camelCase]; + + if ( getComputedStyle ) { + name = name.replace(rupper, "-$1").toLowerCase(); + val = currentS.getPropertyValue(name); + if ( name === "opacity" && val === "" ) { + val = "1"; + } + results[oldName] = val; + } else { + camelCase = name.replace(rdashAlpha, fcamelCase); + results[oldName] = currentS[name] || currentS[camelCase]; - if (!rnumpx.test(results[oldName]) && rnum.test(results[oldName])) { //convert to px - // Remember the original values - var left = style.left, + if (!rnumpx.test(results[oldName]) && rnum.test(results[oldName]) ) { //convert to px + // Remember the original values + left = style.left; rsLeft = el.runtimeStyle.left; - // Put in the new values to get a computed value out - el.runtimeStyle.left = el.currentStyle.left; - style.left = camelCase === "fontSize" ? "1em" : (results[oldName] || 0); - results[oldName] = style.pixelLeft + "px"; + // Put in the new values to get a computed value out + el.runtimeStyle.left = el.currentStyle.left; + style.left = camelCase === "fontSize" ? "1em" : (results[oldName] || 0); + results[oldName] = style.pixelLeft + "px"; - // Revert the changed values - style.left = left; - el.runtimeStyle.left = rsLeft; - } + // Revert the changed values + style.left = left; + el.runtimeStyle.left = rsLeft; + } + } } - } - - return results; -}; -/** - * @add jQuery.fn - */ + return results; + }; + /** + * @add jQuery.fn + */ -$.fn. -/** - * @parent dom - * @plugin jquery/dom/cur_styles - * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/cur_styles/cur_styles.js - * @test jquery/dom/cur_styles/qunit.html - * Use curStyles to rapidly get a bunch of computed styles from an element. - *

      Quick Example

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

      Use

      - *

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

      - *

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

      - *

      Demo

      - *

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

      - * @demo jquery/dom/cur_styles/cur_styles.html - * @param {String} style pass style names as arguments - * @return {Object} an object of style:value pairs - */ -curStyles = function(){ - return $.curStyles(this[0], $.makeArray(arguments)) -} -}) + $.fn + /** + * @parent dom + * @plugin jquery/dom/cur_styles + * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/cur_styles/cur_styles.js + * @test jquery/dom/cur_styles/qunit.html + * Use curStyles to rapidly get a bunch of computed styles from an element. + *

      Quick Example

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

      Use

      + *

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

      + *

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

      + *

      Demo

      + *

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

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

      jquery/dom/dimensions Plugin

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

      Quick Examples

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

      Use

      - *

      When writing reusable plugins, you often want to + * + * ### Quick Examples + * + * $('#foo').outerWidth(100).innerHeight(50); + * $('#bar').animate({outerWidth: 500}); + * + * ## Use + * + * When writing reusable plugins, you often want to * set or animate an element's width and height that include its padding, * border, or margin. This is especially important in plugins that * allow custom styling. + * * The dimensions plugin overwrites [jQuery.fn.outerHeight outerHeight], * [jQuery.fn.outerWidth outerWidth], [jQuery.fn.innerHeight innerHeight] * and [jQuery.fn.innerWidth innerWidth] * to let you set and animate these properties. - *

      - *

      Demo

      + * + * + * + * + * ## Demo + * * @demo jquery/dom/dimensions/dimensions.html */ @@ -39,22 +47,27 @@ var weird = /button|select/i, //margin is inside border */ $.each({ -/* +/** * @function outerWidth * @parent dimensions - * Lets you set the outer height on an object + * Lets you set the outer width on an object * @param {Number} [height] - * @param {Boolean} [includeMargin] + * @param {Boolean} [includeMargin=false] Makes setting the outerWidth adjust + * for margin. Defaults to false. + * + * $('#hasMargin').outerWidth(50, true); + * + * @return {jQuery|Number} If you are setting the value, returns the jQuery wrapped elements. */ width: -/* +/** * @function innerWidth * @parent dimensions * Lets you set the inner height of an object * @param {Number} [height] */ "Width", -/* +/** * @function outerHeight * @parent dimensions * Lets you set the outer height of an object where:
      @@ -74,7 +87,7 @@ width: * Otherwise, returns outerHeight in pixels. */ height: -/* +/** * @function innerHeight * @parent dimensions * Lets you set the outer width on an object @@ -104,19 +117,21 @@ height: //getter / setter $.fn["outer" + Upper] = function(v, margin) { - if (typeof v == 'number') { - this[lower](v - getBoxes[lower](this[0], {padding: true, border: true, margin: margin})) + var first = this[0]; + if (typeof v == 'number') { + first && this[lower](v - getBoxes[lower](first, {padding: true, border: true, margin: margin})) return this; } else { - return checks["oldOuter" + Upper].call(this, v) + return first ? checks["oldOuter" + Upper].call(this, v) : null; } } $.fn["inner" + Upper] = function(v) { - if (typeof v == 'number') { - this[lower](v - getBoxes[lower](this[0], { padding: true })) + var first = this[0]; + if (typeof v == 'number') { + first&& this[lower](v - getBoxes[lower](first, { padding: true })) return this; } else { - return checks["oldInner" + Upper].call(this, v) + return first ? checks["oldInner" + Upper].call(this, v) : null; } } //provides animations diff --git a/dom/dimensions/dimensions_test.js b/dom/dimensions/dimensions_test.js new file mode 100644 index 00000000..3f7ad31b --- /dev/null +++ b/dom/dimensions/dimensions_test.js @@ -0,0 +1,14 @@ +steal("jquery/dom/dimensions", + 'jquery/view/micro', + 'funcunit/qunit').then(function(){ + +module("jquery/dom/dimensions"); + + + + +test("outerHeight and width",function(){ + $("#qunit-test-area").html("//jquery/dom/dimensions/test/curStyles.micro",{}) +}) + +}); \ No newline at end of file diff --git a/dom/dimensions/qunit.html b/dom/dimensions/qunit.html index 3636028f..2b0e381c 100644 --- a/dom/dimensions/qunit.html +++ b/dom/dimensions/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/dimensions/test/qunit/curStyles.micro b/dom/dimensions/test/curStyles.micro similarity index 100% rename from dom/dimensions/test/qunit/curStyles.micro rename to dom/dimensions/test/curStyles.micro diff --git a/dom/cur_styles/test/qunit/outer.micro b/dom/dimensions/test/outer.micro similarity index 100% rename from dom/cur_styles/test/qunit/outer.micro rename to dom/dimensions/test/outer.micro diff --git a/dom/dimensions/test/qunit/dimensions_test.js b/dom/dimensions/test/qunit/dimensions_test.js deleted file mode 100644 index 9d34e7e8..00000000 --- a/dom/dimensions/test/qunit/dimensions_test.js +++ /dev/null @@ -1,8 +0,0 @@ -module("jquery/dom/dimensions"); - - - - -test("outerHeight and width",function(){ - $("#qunit-test-area").html("//jquery/dom/dimensions/test/qunit/curStyles.micro",{}) -}) diff --git a/dom/dimensions/test/qunit/outer.micro b/dom/dimensions/test/qunit/outer.micro deleted file mode 100644 index e69de29b..00000000 diff --git a/dom/dimensions/test/qunit/qunit.js b/dom/dimensions/test/qunit/qunit.js deleted file mode 100644 index d842c596..00000000 --- a/dom/dimensions/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/dimensions",'jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("dimensions_test") \ No newline at end of file diff --git a/dom/dom.js b/dom/dom.js index f964832f..a47d8438 100644 --- a/dom/dom.js +++ b/dom/dom.js @@ -1,7 +1,82 @@ /** - * @page dom DOM Helpers - * @tag core - * JavaScriptMVC adds a bunch of useful jQuery extensions for the dom. Check them out on the left. - * - */ -steal.plugins('jquery'); \ No newline at end of file +@page dom DOM Helpers +@parent jquerymx +@description jQuery DOM extension. + +JavaScriptMVC adds a bunch of useful +jQuery extensions for the dom. Check them out on the left. + +## [dimensions Dimensions] + +Set and animate the inner and outer height and width of elements. + + $('#foo').outerWidth(100); + $('#bar').animate({innerWidth: 500}); + +This is great when you want to include padding and margin in +setting the dimensions of elements. + +## [jQuery.cookie Cookie] + +Set and get cookie values: + + $.cookie('cookie','value'); + +## [jQuery.fixture Fixture] + +Simulate Ajax responses. + + $.fixture("/services/tasks.php','fixtures/tasks.json'); + +Works with jQuery's Ajax converters! + +## [jQuery.fn.compare Compare] + +Compare the location of two elements rapidly. + + $('#foo').compare($('#bar')) & 2 // true if #bar is before #foo + +## [jQuery.fn.curStyles CurStyles] + +Get multiple css properties quickly. + + $('#foo').curStyles('left','top') //-> {left:'20px',top:'10px'} + +## [jQuery.fn.formParams FormParams] + +Serializes a form into a JSON-like object: + + $('form').formParams() //-> {name: 'Justin', favs: ['JS','Ruby']} + +## [jQuery.fn.selection Selection] + +Gets or sets the current text selection. + + // gets selection info + $('pre').selection() //-> {start: 22, end: 57, range: range} + + // sets the selection + $('div').selection(20,22) + +## [jQuery.fn.within Within] + +Returns elements that have a point within their boundaries. + + $('.drop').within(200,200) //-> drops that touch 200,200 + +## [jQuery.Range Range] + +Text range utilities. + + $('#copy').range() //-> text range that has copy selected + +## [jQuery.route] + +Hash routes mapped to an [jQuery.Observe $.Observe]. + + $.route(':type',{type: 'videos'}) + $.route.delegate('type','set', function(){ ... }) + $.route.attr('type','images'); + +*/ +steal('jquery'); \ No newline at end of file diff --git a/dom/fixture/fixture.html b/dom/fixture/fixture.html index 75a7d6de..d59b1f30 100644 --- a/dom/fixture/fixture.html +++ b/dom/fixture/fixture.html @@ -15,11 +15,13 @@
      - - diff --git a/dom/fixture/fixture.js b/dom/fixture/fixture.js index 787fae00..672b5a7b 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -1,267 +1,547 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom', + 'jquery/lang/object', + 'jquery/lang/string',function( $ ) { + + //used to check urls + - var ajax = $.ajax; + + // the pre-filter needs to re-route the url + + $.ajaxPrefilter( function( settings, originalOptions, jqXHR ) { + // if fixtures are on + if(! $.fixture.on) { + return; + } + + // add the fixture option if programmed in + var data = overwrite(settings); + + // if we don't have a fixture, do nothing + if(!settings.fixture){ + if(window.location.protocol === "file:"){ + steal.dev.log("ajax request to " + settings.url+", no fixture found"); + } + return; + } + + //if referencing something else, update the fixture option + if ( typeof settings.fixture === "string" && $.fixture[settings.fixture] ) { + settings.fixture = $.fixture[settings.fixture]; + } + + // if a string, we just point to the right url + if ( typeof settings.fixture == "string" ) { + var url = settings.fixture; + + if (/^\/\//.test(url) ) { + var sub = settings.fixture.substr(2) + ''; + url = typeof steal === "undefined" ? + url = "/" + sub : + steal.root.mapJoin(sub) +''; + } + //!steal-remove-start + steal.dev.log("looking for fixture in " + url); + //!steal-remove-end + settings.url = url; + settings.data = null; + settings.type = "GET"; + if (!settings.error ) { + settings.error = function( xhr, error, message ) { + throw "fixtures.js Error " + error + " " + message; + }; + } + + }else { + //!steal-remove-start + steal.dev.log("using a dynamic fixture for " +settings.type+" "+ settings.url); + //!steal-remove-end + + //it's a function ... add the fixture datatype so our fixture transport handles it + // TODO: make everything go here for timing and other fun stuff + settings.dataTypes.splice(0,0,"fixture"); + + if(data){ + $.extend(originalOptions.data, data) + } + // add to settings data from fixture ... + + } + + }); + + + $.ajaxTransport( "fixture", function( s, original ) { + + // remove the fixture from the datatype + s.dataTypes.shift(); + + //we'll return the result of the next data type + var next = s.dataTypes[0], + timeout; + + return { + + send: function( headers , callback ) { + + // callback after a timeout + timeout = setTimeout(function() { + + // get the callback data from the fixture function + var response = s.fixture(original, s, headers); + + // normalize the fixture data into a response + if(!$.isArray(response)){ + var tmp = [{}]; + tmp[0][next] = response + response = tmp; + } + if(typeof response[0] != 'number'){ + response.unshift(200,"success") + } + + // make sure we provide a response type that matches the first datatype (typically json) + if(!response[2] || !response[2][next]){ + var tmp = {} + tmp[next] = response[2]; + response[2] = tmp; + } + + // pass the fixture data back to $.ajax + callback.apply(null, response ); + }, $.fixture.delay); + }, + + abort: function() { + clearTimeout(timeout) + } + }; + + }); + + + + var typeTest = /^(script|json|test|jsonp)$/, + // a list of 'overwrite' settings object + overwrites = [], + // returns the index of an overwrite function + find = function(settings, exact){ + for(var i =0; i < overwrites.length; i++){ + if($fixture._similar(settings, overwrites[i], exact)){ + return i; + } + } + return -1; + }, + // overwrites the settings fixture if an overwrite matches + overwrite = function(settings){ + var index = find(settings); + if(index > -1){ + settings.fixture = overwrites[index].fixture; + return $fixture._getData(overwrites[index].url, settings.url) + } + + }, + /** + * Makes an attempt to guess where the id is at in the url and returns it. + * @param {Object} settings + */ + getId = function(settings){ + var id = settings.data.id; + + if(id === undefined && typeof settings.data === "number") { + id = settings.data; + } + + /* + Check for id in params(if query string) + If this is just a string representation of an id, parse + if(id === undefined && typeof settings.data === "string") { + id = settings.data; + } + //*/ + + if(id === undefined){ + settings.url.replace(/\/(\d+)(\/|$|\.)/g, function(all, num){ + id = num; + }); + } + + if(id === undefined){ + id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function(all, num){ + if(num != 'update'){ + id = num; + } + }) + } + + if(id === undefined){ // if still not set, guess a random number + id = Math.round(Math.random()*1000) + } + + return id; + }; /** - * @class jQuery.fixture + * @function jQuery.fixture * @plugin jquery/dom/fixture * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/fixture/fixture.js * @test jquery/dom/fixture/qunit.html * @parent dom * - * Fixtures simulate AJAX responses by overwriting - * [jQuery.ajax $.ajax], - * [jQuery.get $.get], and - * [jQuery.post $.post]. - * Instead of making a request to a server, fixtures simulate - * the repsonse with a file or function. - * - * They are a great technique when you want to develop JavaScript + * $.fixture intercepts a AJAX request and simulates + * the response with a file or function. They are a great technique + * when you want to develop JavaScript * independently of the backend. * - *

      Quick Example

      - *

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

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

      Using Fixtures

      - * To enable fixtures, you must add this plugin to your page and - * set the fixture property. + * ## Types of Fixtures * - * The fixture property is set as ... - * @codestart - * //... a property with $.ajax - * $.ajax({fixture: FIXTURE_VALUE}) + * There are two common ways of using fixtures. The first is to + * map Ajax requests to another file. The following + * intercepts requests to /tasks.json and directs them + * to fixtures/tasks.json: + * + * $.fixture("/tasks.json","fixtures/tasks.json"); + * + * The other common option is to generate the Ajax response with + * a function. The following intercepts updating tasks at + * /tasks/ID.json and responds with updated data: + * + * $.fixture("PUT /tasks/{id}.json", function(original, settings, headers){ + * return { updatedAt : new Date().getTime() } + * }) + * + * We categorize fixtures into the following types: + * + * - __Static__ - the response is in a file. + * - __Dynamic__ - the response is generated by a function. * - * //... a parameter in $.get and $.post - * $.get ( url, data, callback, type, FIXTURE_VALUE ) - * $.post( url, data, callback, type, FIXTURE_VALUE ) - * @codeend - *

      Turning Off Fixtures

      - *

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

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

      Types of Fixtures

      - *

      There are 2 types of fixtures

      - *
        - *
      • Static - the response is in a file. - *
      • - *
      • - * Dynamic - the response is generated by a function. - *
      • - *
      * There are different ways to lookup static and dynamic fixtures. - *

      Static Fixtures

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

      Dynamic Fixtures

      - *

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

      - *

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

      - *

      There are 2 ways to lookup dynamic fixtures.

      - * They can provided: - * @codestart - * //just use a function as the fixture property - * $.ajax({ - * type: "get", - * url: "tasks", - * data: {id: 5}, - * dataType: "json", - * fixture: function( settings, callbackType ) { - * var xhr = {responseText: "{id:"+settings.data.id+"}"} - * switch(callbackType){ - * case "success": - * return [{id: settings.data.id},"success",xhr] - * case "complete": - * return [xhr,"success"] + * + * ## Static Fixtures + * + * Static fixtures use an alternate url as the response of the Ajax request. + * + * // looks in fixtures/tasks1.json relative to page + * $.fixture("tasks/1", "fixtures/task1.json"); + * + * $.fixture("tasks/1", "//fixtures/task1.json"); + * + * ## Dynamic Fixtures + * + * Dynamic Fixtures are functions that get the details of + * the Ajax request and return the result of the mocked service + * request from your server. + * + * For example, the following returns a successful response + * with JSON data from the server: + * + * $.fixture("/foobar.json", function(orig, settings, headers){ + * return [200, "success", {json: {foo: "bar" } }, {} ] + * }) + * + * The fixture function has the following signature: + * + * function( originalOptions, options, headers ) { + * return [ status, statusText, responses, responseHeaders ] * } - * } - * }) - * @codeend - * Or found by name on $.fixture: - * @codestart - * // add your function on $.fixture - * // We use -FUNC by convention - * $.fixture["-myGet"] = function(settings, cbType){...} - * - * // reference it - * $.ajax({ - * type:"get", - * url: "tasks/1", - * dataType: "json", - * fixture: "-myGet"}) - * @codeend - *

      Dynamic fixture functions are called with:

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

      - *
      PRO TIP: + * + * where the fixture function is called with: + * + * - originalOptions - are the options provided to the ajax method, unmodified, + * and thus, without defaults from ajaxSettings + * - options - are the request options + * - headers - a map of key/value request headers + * + * and the fixture function returns an array as arguments for ajaxTransport's completeCallback with: + * + * - status - is the HTTP status code of the response. + * - statusText - the status text of the response + * - responses - a map of dataType/value that contains the responses for each data format supported + * - headers - response headers + * + * However, $.fixture handles the + * common case where you want a successful response with JSON data. The + * previous can be written like: + * + * $.fixture("/foobar.json", function(orig, settings, headers){ + * return {foo: "bar" }; + * }) + * + * If you want to return an array of data, wrap your array in another array: + * + * $.fixture("/tasks.json", function(orig, settings, headers){ + * return [ [ "first","second","third"] ]; + * }) + * + * $.fixture works closesly with jQuery's + * ajaxTransport system. Understanding it is the key to creating advanced + * fixtures. + * + * ### Templated Urls + * + * Often, you want a dynamic fixture to handle urls + * for multiple resources (for example a REST url scheme). $.fixture's + * templated urls allow you to match urls with a wildcard. + * + * The following example simulates services that get and update 100 todos. + * + * // create todos + * var todos = {}; + * for(var i = 0; i < 100; i++) { + * todos[i] = { + * id: i, + * name: "Todo "+i + * } + * } + * $.fixture("GET /todos/{id}", function(orig){ + * // return the JSON data + * // notice that id is pulled from the url and added to data + * return todos[orig.data.id] + * }) + * $.fixture("PUT /todos/{id}", function(orig){ + * // update the todo's data + * $.extend( todos[orig.data.id], orig.data ); + * + * // return data + * return {}; + * }) + * + * Notice that data found in templated urls (ex: {id}) is added to the original + * data object. + * + * ## Simulating Errors + * + * The following simulates an unauthorized request + * to /foo. + * + * $.fixture("/foo", function(){ + * return [401,"{type: 'unauthorized'}"] + * }); + * + * This could be received by the following Ajax request: + * + * $.ajax({ + * url: '/foo', + * error : function(jqXhr, status, statusText){ + * // status === 'error' + * // statusText === "{type: 'unauthorized'}" + * } + * }) + * + * ## Turning off Fixtures + * + * You can remove a fixture by passing null for the fixture option: + * + * // add a fixture + * $.fixture("GET todos.json","//fixtures/todos.json"); + * + * // remove the fixture + * $.fixture("GET todos.json", null) + * + * You can also set [jQuery.fixture.on $.fixture.on] to false: + * + * $.fixture.on = false; + * + * ## Make + * + * [jQuery.fixture.make $.fixture.make] makes a CRUD service layer that handles sorting, grouping, + * filtering and more. + * + * ## Testing Performance + * * Dynamic fixtures are awesome for performance testing. Want to see what * 10000 files does to your app's performance? Make a fixture that returns 10000 items. * * What to see what the app feels like when a request takes 5 seconds to return? Set * [jQuery.fixture.delay] to 5000. - *
      - *

      Helpers

      - *

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

      - *
        - *
      • [jQuery.fixture.make] - creates fixtures for findAll, findOne.
      • - *
      • [jQuery.fixture.-restCreate] - a fixture for restful creates.
      • - *
      • [jQuery.fixture.-restDestroy] - a fixture for restful updates.
      • - *
      • [jQuery.fixture.-restUpdate] - a fixture for restful destroys.
      • - *
      + * * @demo jquery/dom/fixture/fixture.html - * @constructor - * Takes an ajax settings and returns a url to look for a fixture. Overwrite this if you want a custom lookup method. - * @param {Object} settings - * @return {String} the url that will be used for the fixture + * + * @param {Object|String} settings Configures the AJAX requests the fixture should + * intercept. If an __object__ is passed, the object's properties and values + * are matched against the settings passed to $.ajax. + * + * If a __string__ is passed, it can be used to match the url and type. Urls + * can be templated, using {NAME} as wildcards. + * + * @param {Function|String} fixture The response to use for the AJAX + * request. If a __string__ url is passed, the ajax request is redirected + * to the url. If a __function__ is provided, it looks like: + * + * fixture( originalSettings, settings, headers ) + * + * where: + * + * - originalSettings - the orignal settings passed to $.ajax + * - settings - the settings after all filters have run + * - headers - request headers + * + * If __null__ is passed, and there is a fixture at settings, that fixture will be removed, + * allowing the AJAX request to behave normally. */ - $.fixture = function( settings ) { - var url = settings.url, - match, left, right; - url = url.replace(/%2F/g, "~").replace(/%20/g, "_"); - - if ( settings.data && settings.processData && typeof settings.data !== "string" ) { - settings.data = jQuery.param(settings.data); - } - - - if ( settings.data && settings.type.toLowerCase() == "get" ) { - url += ($.String.include(url, '?') ? '&' : '?') + settings.data; - } - - match = url.match(/^(?:https?:\/\/[^\/]*)?\/?([^\?]*)\??(.*)?/); - left = match[1]; - - right = settings.type ? '.' + settings.type.toLowerCase() : '.post'; - if ( match[2] ) { - left += '/'; - right = match[2].replace(/\#|&/g, '-').replace(/\//g, '~') + right; + var $fixture = $.fixture = function( settings , fixture ){ + // if we provide a fixture ... + if(fixture !== undefined){ + if(typeof settings == 'string'){ + // handle url strings + var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i); + if(!matches){ + settings = { + url : settings + }; + } else { + settings = { + url : matches[2], + type: matches[1] + }; + } + + } + + //handle removing. An exact match if fixture was provided, otherwise, anything similar + var index = find(settings, !!fixture); + if(index > -1){ + overwrites.splice(index,1) + } + if(fixture == null){ + return + } + settings.fixture = fixture; + overwrites.push(settings) } - return left + right; }; - + var replacer = $.String._regs.replacer; + $.extend($.fixture, { + // given ajax settings, find an overwrite + _similar : function(settings, overwrite, exact){ + if(exact){ + return $.Object.same(settings , overwrite, {fixture : null}) + } else { + return $.Object.subset(settings, overwrite, $.fixture._compare) + } + }, + _compare : { + url : function(a, b){ + return !! $fixture._getData(b, a) + }, + fixture : null, + type : "i" + }, + // gets data from a url like "/todo/{id}" given "todo/5" + _getData : function(fixtureUrl, url){ + var order = [], + fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.').replace('?', '\\?'), + res = new RegExp(fixtureUrlAdjusted.replace(replacer, function(whole, part){ + order.push(part) + return "([^\/]+)" + })+"$").exec(url), + data = {}; + + if(!res){ + return null; + } + res.shift(); + $.each(order, function(i, name){ + data[name] = res.shift() + }) + return data; + }, /** + * @hide * Provides a rest update fixture function */ - "-restUpdate": function( settings, cbType ) { - switch ( cbType ) { - case "success": - return [$.extend({ - id: parseInt(settings.url, 10) - }, settings.data), "success", $.fixture.xhr()]; - case "complete": - return [$.fixture.xhr(), "success"]; - } + "-restUpdate": function( settings ) { + return [200,"succes",{ + id: getId(settings) + },{ + location: settings.url+"/"+getId(settings) + }]; }, + /** + * @hide * Provides a rest destroy fixture function */ "-restDestroy": function( settings, cbType ) { - switch ( cbType ) { - case "success": - return [true, "success", $.fixture.xhr()]; - case "complete": - return [$.fixture.xhr(), "success"]; - } + return {}; }, + /** + * @hide * Provides a rest create fixture function */ - "-restCreate": function( settings, cbType ) { - switch ( cbType ) { - case "success": - return [{ - id: parseInt(Math.random() * 1000, 10) - }, "success", $.fixture.xhr()]; - case "complete": - return [$.fixture.xhr({ - getResponseHeader: function() { - return settings.url + "/" + parseInt(Math.random() * 1000, 10); - } - }), "success"]; - } - - + "-restCreate": function( settings, cbType, nul, id ) { + var id = id || parseInt(Math.random() * 100000, 10); + return [200,"succes",{ + id: id + },{ + location: settings.url+"/"+id + }]; }, + /** + * @function jQuery.fixture.make + * @parent jQuery.fixture * Used to make fixtures for findAll / findOne style requests. - * @codestart - * //makes a threaded list of messages - * $.fixture.make(["messages","message"],1000, function(i, messages){ - * return { - * subject: "This is message "+i, - * body: "Here is some text for this message", - * date: Math.floor( new Date().getTime() ), - * parentId : i < 100 ? null : Math.floor(Math.random()*i) - * } - * }) - * //uses the message fixture to return messages limited by offset, limit, order, etc. - * $.ajax({ - * url: "messages", - * data:{ - * offset: 100, - * limit: 50, - * order: "date ASC", - * parentId: 5}, - * }, - * fixture: "-messages", - * success: function( messages ) { ... } - * }); - * @codeend - * @param {Array} types An array of the fixture names + * + * //makes a nested list of messages + * $.fixture.make(["messages","message"],1000, function(i, messages){ + * return { + * subject: "This is message "+i, + * body: "Here is some text for this message", + * date: Math.floor( new Date().getTime() ), + * parentId : i < 100 ? null : Math.floor(Math.random()*i) + * } + * }) + * //uses the message fixture to return messages limited by offset, limit, order, etc. + * $.ajax({ + * url: "messages", + * data:{ + * offset: 100, + * limit: 50, + * order: ["date ASC"], + * parentId: 5}, + * }, + * fixture: "-messages", + * success: function( messages ) { ... } + * }); + * + * @param {Array|String} types An array of the fixture names or the singular fixture name. + * If an array, the first item is the plural fixture name (prefixed with -) and the second + * item is the singular name. If a string, it's assumed to be the singular fixture name. Make + * will simply add s to the end of it for the plural name. * @param {Number} count the number of items to create - * @param {Function} make a function that will return json data representing the object. + * @param {Function} make a function that will return json data representing the object. The + * make function is called back with the id and the current array of items. + * @param {Function} filter (optional) a function used to further filter results. Used for to simulate + * server params like searchText or startDate. The function should return true if the item passes the filter, + * false otherwise. For example: + * + * + * function(item, settings){ + * if(settings.data.searchText){ + * var regex = new RegExp("^"+settings.data.searchText) + * return regex.test(item.name); + * } + * } + * */ - make: function( types, count, make ) { + make: function( types, count, make, filter ) { + if(typeof types === "string"){ + types = [types+"s",types ] + } // make all items - var items = ($.fixture["~" + types[0]] = []); + var items = ($.fixture["~" + types[0]] = []), // TODO: change this to a hash + findOne = function(id){ + for ( var i = 0; i < items.length; i++ ) { + if ( id == items[i].id ) { + return items[i]; + } + } + }; + for ( var i = 0; i < (count); i++ ) { //call back provided make var item = make(i, items); @@ -273,20 +553,31 @@ steal.plugins('jquery/dom').then(function( $ ) { } //set plural fixture for findAll $.fixture["-" + types[0]] = function( settings ) { - //copy array of items var retArr = items.slice(0); - + settings.data = settings.data || {}; //sort using order //order looks like ["age ASC","gender DESC"] $.each((settings.data.order || []).slice(0).reverse(), function( i, name ) { var split = name.split(" "); retArr = retArr.sort(function( a, b ) { if ( split[1].toUpperCase() !== "ASC" ) { - return a[split[0]] < b[split[0]]; + if( a[split[0]] < b[split[0]] ) { + return 1; + } else if(a[split[0]] == b[split[0]]){ + return 0 + } else { + return -1; + } } else { - return a[split[0]] > b[split[0]]; + if( a[split[0]] < b[split[0]] ) { + return -1; + } else if(a[split[0]] == b[split[0]]){ + return 0 + } else { + return 1; + } } }); }); @@ -301,12 +592,14 @@ steal.plugins('jquery/dom').then(function( $ ) { var offset = parseInt(settings.data.offset, 10) || 0, - limit = parseInt(settings.data.limit, 10) || (count - offset), + limit = parseInt(settings.data.limit, 10) || (items.length - offset), i = 0; //filter results if someone added an attr like parentId for ( var param in settings.data ) { - if ( param.indexOf("Id") != -1 || param.indexOf("_id") != -1 ) { + i=0; + if ( settings.data[param] !== undefined && // don't do this if the value of the param is null (ignore it) + (param.indexOf("Id") != -1 || param.indexOf("_id") != -1) ) { while ( i < retArr.length ) { if ( settings.data[param] != retArr[i][param] ) { retArr.splice(i, 1); @@ -316,6 +609,18 @@ steal.plugins('jquery/dom').then(function( $ ) { } } } + + + if( filter ) { + i = 0; + while (i < retArr.length) { + if (!filter(retArr[i], settings)) { + retArr.splice(i, 1); + } else { + i++; + } + } + } //return data spliced with limit and offset return [{ @@ -325,19 +630,128 @@ steal.plugins('jquery/dom').then(function( $ ) { "data": retArr.slice(offset, offset + limit) }]; }; - + // findOne $.fixture["-" + types[1]] = function( settings ) { - for ( var i = 0; i < (count); i++ ) { - if ( settings.data.id == items[i].id ) { - return [items[i]]; + var item = findOne(getId(settings)); + return item ? [item] : []; + }; + // update + $.fixture["-" + types[1]+"Update"] = function( settings, cbType ) { + var id = getId(settings); + + // TODO: make it work with non-linear ids .. + $.extend(findOne(id), settings.data); + return $.fixture["-restUpdate"](settings, cbType) + }; + $.fixture["-" + types[1]+"Destroy"] = function( settings, cbType ) { + var id = getId(settings); + for(var i = 0; i < items.length; i ++ ){ + if(items[i].id == id){ + items.splice(i, 1); + break; } } + + // TODO: make it work with non-linear ids .. + $.extend(findOne(id), settings.data); + return $.fixture["-restDestroy"](settings, cbType) }; - + $.fixture["-" + types[1]+"Create"] = function( settings, cbType ) { + var item = make(items.length, items); + + $.extend(item, settings.data); + + if(!item.id){ + item.id = items.length; + } + + items.push(item); + + return $.fixture["-restCreate"](settings, cbType, undefined, item.id ); + }; + + + return { + getId: getId, + findOne : findOne, + find : function(settings){ + return findOne( getId(settings) ); + } + } + }, + /** + * @function jQuery.fixture.rand + * @parent jQuery.fixture + * + * Creates random integers or random arrays of + * other arrays. + * + * ## Examples + * + * var rand = $.fixture.rand; + * + * // get a random integer between 0 and 10 (inclusive) + * rand(11); + * + * // get a random number between -5 and 5 (inclusive) + * rand(-5, 6); + * + * // pick a random item from an array + * rand(["j","m","v","c"],1)[0] + * + * // pick a random number of items from an array + * rand(["j","m","v","c"]) + * + * // pick 2 items from an array + * rand(["j","m","v","c"],2) + * + * // pick between 2 and 3 items at random + * rand(["j","m","v","c"],2,3) + * + * + * @param {Array|Number} arr An array of items to select from. + * If a number is provided, a random number is returned. + * If min and max are not provided, a random number of items are selected + * from this array. + * @param {Number} [min] If only min is provided, min items + * are selected. + * @param {Number} [max] If min and max are provided, a random number of + * items between min and max (inclusive) is selected. + */ + rand : function(arr, min, max){ + if(typeof arr == 'number'){ + if(typeof min == 'number'){ + return arr+ Math.floor(Math.random() * (min - arr) ); + } else { + return Math.floor(Math.random() * arr); + } + + } + var rand = arguments.callee; + // get a random set + if(min === undefined){ + return rand(arr, rand(arr.length+1)) + } + // get a random selection of arr + var res = []; + arr = arr.slice(0); + // set max + if(!max){ + max = min; + } + //random max + max = min + Math.round( rand(max - min) ) + for(var i=0; i < max; i++){ + res.push(arr.splice( rand(arr.length), 1 )[0]) + } + return res; }, /** + * @hide * Use $.fixture.xhr to create an object that looks like an xhr object. - *

      Example

      + * + * ## Example + * * The following example shows how the -restCreate fixture uses xhr to return * a simulated xhr object: * @codestart @@ -381,15 +795,26 @@ steal.plugins('jquery/dom').then(function( $ ) { status: 200, statusText: "OK" }, xhr); - } + }, + /** + * @attribute on + * On lets you programatically turn off fixtures. This is mostly used for testing. + * + * $.fixture.on = false + * Task.findAll({}, function(){ + * $.fixture.on = true; + * }) + */ + on : true }); /** - * @attribute delay + * @attribute $.fixture.delay + * @parent $.fixture * Sets the delay in milliseconds between an ajax request is made and * the success and complete handlers are called. This only sets * functional fixtures. By default, the delay is 200ms. * @codestart - * steal.plugins('jquery/dom/fixtures').then(function(){ + * steal('jquery/dom/fixtures').then(function(){ * $.fixture.delay = 1000; * }) * @codeend @@ -414,105 +839,68 @@ steal.plugins('jquery/dom').then(function( $ ) { return false; }; - /** - * @add jQuery - */ - // break - $. - /** - * Adds the fixture option to settings. If present, loads from fixture location instead - * of provided url. This is useful for simulating ajax responses before the server is done. - * @param {Object} settings - */ - ajax = function( settings ) { - var func = $.fixture; - if (!settings.fixture ) { - return ajax.apply($, arguments); - } - if ( $.fixture["-handleFunction"](settings) ) { - return; - } - if ( typeof settings.fixture == "string" ) { - var url = settings.fixture; - if (/^\/\//.test(url) ) { - url = steal.root.join(settings.fixture.substr(2)); - } - //@steal-remove-start - steal.dev.log("looking for fixture in " + url); - //@steal-remove-end - settings.url = url; - settings.data = null; - settings.type = "GET"; - if (!settings.error ) { - settings.error = function( xhr, error, message ) { - throw "fixtures.js Error " + error + " " + message; - }; - } - return ajax(settings); - - } - settings = jQuery.extend(true, settings, jQuery.extend(true, {}, jQuery.ajaxSettings, settings)); - - settings.url = steal.root.join('test/fixtures/' + func(settings)); // convert settings - settings.data = null; - settings.type = 'GET'; - return ajax(settings); - }; - - $.extend($.ajax, ajax); - - $. - /** - * Adds a fixture param. - * @param {Object} url - * @param {Object} data - * @param {Object} callback - * @param {Object} type - * @param {Object} fixture - */ - get = function( url, data, callback, type, fixture ) { - // shift arguments if data argument was ommited - if ( jQuery.isFunction(data) ) { - fixture = type; - type = callback; - callback = data; - data = null; - } - - return jQuery.ajax({ - type: "GET", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; - - $. - /** - * Adds a fixture param. - * @param {Object} url - * @param {Object} data - * @param {Object} callback - * @param {Object} type - * @param {Object} fixture + + + /** + * @page jquery.fixture.0organizing Organizing Fixtures + * @parent jQuery.fixture + * + * The __best__ way of organizing fixtures is to have a 'fixtures.js' file that steals + * jquery/dom/fixture and defines all your fixtures. For example, + * if you have a 'todo' application, you might + * have todo/fixtures/fixtures.js look like: + * + * steal({ + * path: '//jquery/dom/fixture.js', + * ignore: true + * }) + * .then(function(){ + * + * $.fixture({ + * type: 'get', + * url: '/services/todos.json' + * }, + * '//todo/fixtures/todos.json'); + * + * $.fixture({ + * type: 'post', + * url: '/services/todos.json' + * }, + * function(settings){ + * return {id: Math.random(), + * name: settings.data.name} + * }); + * + * }) + * + * __Notice__: We used steal's ignore option to prevent + * loading the fixture plugin in production. + * + * Finally, we steal todo/fixtures/fixtures.js in the + * app file (todo/todo.js) like: + * + * + * steal({path: '//todo/fixtures/fixtures.js',ignore: true}); + * + * //start of your app's steals + * steal( ... ) + * + * We typically keep it a one liner so it's easy to comment out. + * + * ## Switching Between Sets of Fixtures + * + * If you are using fixtures for testing, you often want to use different + * sets of fixtures. You can add something like the following to your fixtures.js file: + * + * if( /fixtureSet1/.test( window.location.search) ){ + * $.fixture("/foo","//foo/fixtures/foo1.json'); + * } else if(/fixtureSet2/.test( window.location.search)){ + * $.fixture("/foo","//foo/fixtures/foo1.json'); + * } else { + * // default fixtures (maybe no fixtures) + * } + * */ - post = function( url, data, callback, type, fixture ) { - if ( jQuery.isFunction(data) ) { - fixture = type; - type = callback; - callback = data; - data = {}; - } - - return jQuery.ajax({ - type: "POST", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; -}); \ No newline at end of file + //Expose this for fixture debugging + $.fixture.overwrites = overwrites; +}); diff --git a/dom/fixture/fixture_test.js b/dom/fixture/fixture_test.js new file mode 100644 index 00000000..7559d1ba --- /dev/null +++ b/dom/fixture/fixture_test.js @@ -0,0 +1,332 @@ + +steal("jquery/dom/fixture", "jquery/model",'funcunit/qunit',function(){ + +module("jquery/dom/fixture"); + + +test("static fixtures", function(){ + stop(); + + $.fixture("GET something", "//jquery/dom/fixture/fixtures/test.json"); + $.fixture("POST something", "//jquery/dom/fixture/fixtures/test.json"); + + + $.get("something",function(data){ + equals(data.sweet,"ness","$.get works"); + + $.post("something",function(data){ + equals(data.sweet,"ness","$.post works"); + + + start(); + },'json'); + + },'json'); +}) + +test("dynamic fixtures",function(){ + stop(); + $.fixture.delay = 10; + $.fixture("something", function(){ + return [{sweet: "ness"}] + }) + + $.get("something",function(data){ + equals(data.sweet,"ness","$.get works"); + start(); + + },'json'); +}); + +test("fixture function", 3, function(){ + + stop(); + var url = steal.root.join("jquery/dom/fixture/fixtures/foo.json")+''; + $.fixture(url,"//jquery/dom/fixture/fixtures/foobar.json" ); + + $.get(url,function(data){ + equals(data.sweet,"ner","url passed works"); + + $.fixture(url,"//jquery/dom/fixture/fixtures/test.json" ); + + $.get(url,function(data){ + + equals(data.sweet,"ness","replaced"); + + $.fixture(url, null ); + + $.get(url,function(data){ + + equals(data.a,"b","removed"); + + start(); + + },'json') + + + },'json') + + + + },"json"); + +}); + + +test("fixtures with converters", function(){ + + stop(); + $.ajax( { + url : steal.root.join("jquery/dom/fixture/fixtures/foobar.json")+'', + dataType: "json fooBar", + converters: { + "json fooBar": function( data ) { + // Extract relevant text from the xml document + return "Mr. "+data.name; + } + }, + fixture : function(){ + return { + name : "Justin" + } + }, + success : function(prettyName){ + start(); + equals(prettyName, "Mr. Justin") + } + }); +}) + +test("$.fixture.make fixtures",function(){ + stop(); + $.fixture.make('thing', 1000, function(i){ + return { + id: i, + name: "thing "+i + } + }, + function(item, settings){ + if(settings.data.searchText){ + var regex = new RegExp("^"+settings.data.searchText) + return regex.test(item.name); + } + }) + $.ajax({ + url: "things", + type: "json", + data: { + offset: 100, + limit: 200, + order: ["name ASC"], + searchText: "thing 2" + }, + fixture: "-things", + success: function(things){ + equals(things.data[0].name, "thing 29", "first item is correct") + equals(things.data.length, 11, "there are 11 items") + start(); + } + }) +}); + +test("simulating an error", function(){ + var st = '{type: "unauthorized"}'; + + $.fixture("/foo", function(){ + return [401,st] + }); + stop(); + + $.ajax({ + url : "/foo", + success : function(){ + ok(false, "success called"); + start(); + }, + error : function(jqXHR, status, statusText){ + ok(true, "error called"); + equals(statusText, st); + start(); + } + }) +}) + +test("rand", function(){ + var rand = $.fixture.rand; + var num = rand(5); + equals(typeof num, "number"); + ok(num >= 0 && num < 5, "gets a number" ); + + stop(); + var zero, three, between, next = function(){ + start() + } + // make sure rand can be everything we need + setTimeout(function(){ + var res = rand([1,2,3]); + if(res.length == 0 ){ + zero = true; + } else if(res.length == 3){ + three = true; + } else { + between = true; + } + if(zero && three && between){ + ok(true, "got zero, three, between") + next(); + } else { + setTimeout(arguments.callee, 10) + } + }, 10) + +}); + + +test("_getData", function(){ + var data = $.fixture._getData("/thingers/{id}", "/thingers/5"); + equals(data.id, 5, "gets data"); + var data = $.fixture._getData("/thingers/5?hi.there", "/thingers/5?hi.there"); + deepEqual(data, {}, "gets data"); +}) + +test("_getData with double character value", function(){ + var data = $.fixture._getData("/days/{id}/time_slots.json", "/days/17/time_slots.json"); + equals(data.id, 17, "gets data"); +}); + +test("_compare", function(){ + var same = $.Object.same( + {url : "/thingers/5"}, + {url : "/thingers/{id}"}, $.fixture._compare) + + ok(same, "they are similar"); + + same = $.Object.same( + {url : "/thingers/5"}, + {url : "/thingers"}, $.fixture._compare); + + ok(!same, "they are not the same"); +}) + +test("_similar", function(){ + + var same = $.fixture._similar( + {url : "/thingers/5"}, + {url : "/thingers/{id}"}); + + ok(same, "similar"); + + same = $.fixture._similar( + {url : "/thingers/5", type: "get"}, + {url : "/thingers/{id}"}); + + ok(same, "similar with extra pops on settings"); + + var exact = $.fixture._similar( + {url : "/thingers/5", type: "get"}, + {url : "/thingers/{id}"}, true); + + ok(!exact, "not exact" ) + + var exact = $.fixture._similar( + {url : "/thingers/5"}, + {url : "/thingers/5"}, true); + + ok(exact, "exact" ) +}) + +test("fixture function gets id", function(){ + $.fixture("/thingers/{id}", function(settings){ + return { + id: settings.data.id, + name: "justin" + } + }) + stop(); + $.get("/thingers/5", {}, function(data){ + start(); + ok(data.id) + },'json') +}); + +test("replacing and removing a fixture", function(){ + var url = steal.root.join("jquery/dom/fixture/fixtures/remove.json")+'' + $.fixture("GET "+url, function(){ + return {weird: "ness!"} + }) + stop(); + $.get(url,{}, function(json){ + equals(json.weird,"ness!","fixture set right") + + $.fixture("GET "+url, function(){ + return {weird: "ness?"} + }) + + $.get(url,{}, function(json){ + equals(json.weird,"ness?","fixture set right"); + + $.fixture("GET "+url, null ) + + $.get(url,{}, function(json){ + equals(json.weird,"ness","fixture set right"); + + start(); + },'json'); + + + },'json') + + + + },'json') +}); + +return; // future fixture stuff + +// returning undefined means you want to control timing? +$.fixture('GET /foo', function(orig, settings, headers, cb){ + setTimeout(function(){ + cb(200, "success",{json : "{}"},{}) + },1000); +}) + +// fixture that hooks into model / vice versa? + +// fixture that creates a nice store + +var store = $.fixture.store(1000, function(){ + +}) + +store.find() + +// make cloud + +var clouds = $.fixture.store(1, function(){ + return { + name: "ESCCloud", + DN : "ESCCloud-ESCCloud", + type : "ESCCloud" + } +}); + +var computeCluster = $.fixture.store(5, function(i){ + return { + name : "", + parentDN : clouds.find()[0].DN, + type: "ComputeCluster", + DN : "ComputeCluster-ComputeCluster"+i + } +}); + +$.fixture("GET /computeclusters", function(){ + return [] +}); + +// hacking models? + + + + + +}); diff --git a/dom/fixture/fixtures/foo.json b/dom/fixture/fixtures/foo.json new file mode 100644 index 00000000..9fbc82d0 --- /dev/null +++ b/dom/fixture/fixtures/foo.json @@ -0,0 +1 @@ +{"a" : "b"} diff --git a/dom/fixture/fixtures/foobar.json b/dom/fixture/fixtures/foobar.json new file mode 100644 index 00000000..a50afea1 --- /dev/null +++ b/dom/fixture/fixtures/foobar.json @@ -0,0 +1,3 @@ +{ + "sweet" :"ner" +} diff --git a/dom/fixture/fixtures/remove.json b/dom/fixture/fixtures/remove.json new file mode 100644 index 00000000..1e152b58 --- /dev/null +++ b/dom/fixture/fixtures/remove.json @@ -0,0 +1,3 @@ +{ + "weird" : "ness" +} diff --git a/dom/fixture/qunit.html b/dom/fixture/qunit.html index 9a6ba526..5207a04c 100644 --- a/dom/fixture/qunit.html +++ b/dom/fixture/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/fixture/test/qunit/fixture_test.js b/dom/fixture/test/qunit/fixture_test.js deleted file mode 100644 index a6499ae1..00000000 --- a/dom/fixture/test/qunit/fixture_test.js +++ /dev/null @@ -1,50 +0,0 @@ -module("jquery/dom/fixture"); - - -test("static fixtures", function(){ - stop(); - $.get("something",function(data){ - equals(data.sweet,"ness","$.get works"); - $.post("something",function(data){ - - equals(data.sweet,"ness","$.post works"); - - $.ajax({ - url: "something", - dataType: "json", - success: function( data ) { - equals(data.sweet,"ness","$.ajax works"); - start(); - }, - fixture: "//jquery/dom/fixture/fixtures/test.json" - }) - - },"json","//jquery/dom/fixture/fixtures/test.json"); - },'json',"//jquery/dom/fixture/fixtures/test.json"); -}) - -test("dynamic fixtures",function(){ - stop(); - $.fixture.delay = 10; - var fix = function(){ - return [{sweet: "ness"}] - } - $.get("something",function(data){ - equals(data.sweet,"ness","$.get works"); - $.post("something",function(data){ - - equals(data.sweet,"ness","$.post works"); - - $.ajax({ - url: "something", - dataType: "json", - success: function( data ) { - equals(data.sweet,"ness","$.ajax works"); - start(); - }, - fixture: fix - }) - - },"json",fix); - },'json',fix); -}) diff --git a/dom/fixture/test/qunit/qunit.js b/dom/fixture/test/qunit/qunit.js deleted file mode 100644 index 9750a4ae..00000000 --- a/dom/fixture/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/fixture") //load your app - .plugins('funcunit/qunit') //load qunit - .then("fixture_test") \ No newline at end of file diff --git a/dom/form_params/form_params.html b/dom/form_params/form_params.html index 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 7197c7f1..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 = $.fn.val.call([el]) || $.data(el, "value"), - isRadioCheck = radioCheck.test(el.type), - parts = key.match(keyBreaker), - write = !isRadioCheck || !! el.checked, - //make an array of values - lastPart; - - if ( convert ) { - if ( isNumber(value) ) { - value = parseFloat(value); - } else if ( value === 'true' || value === 'false' ) { - value = Boolean(value); - } - + // Figure out name parts + parts = name.match( keyBreaker ); + if ( ! parts.length ) { + parts = [name]; } - // go through and create nested objects - current = data; - for ( var i = 0; i < parts.length - 1; i++ ) { - if (!current[parts[i]] ) { - current[parts[i]] = {}; - } - current = current[parts[i]]; + // Convert the value + if ( convert ) { + value = convertValue( value ); } - lastPart = parts[parts.length - 1]; - //now we are on the last part, set the value - if ( lastPart in current && type === "checkbox" ) { - if (!$.isArray(current[lastPart]) ) { - current[lastPart] = current[lastPart] === undefined ? [] : [current[lastPart]]; - } - if ( write ) { - current[lastPart].push(value); - } - } else if ( write || !current[lastPart] ) { - current[lastPart] = write ? value : undefined; - } + // Assign data recursively + nestData( $this, type, data, parts, value, seen ); }); + return data; } }); -}); \ No newline at end of file +}); diff --git a/dom/form_params/form_params_test.js b/dom/form_params/form_params_test.js new file mode 100644 index 00000000..2a9e2f3c --- /dev/null +++ b/dom/form_params/form_params_test.js @@ -0,0 +1,84 @@ +steal("jquery/dom/form_params") //load your app + .then('funcunit/qunit','jquery/view/micro') //load qunit + .then(function(){ + +$.ajaxSetup({ + cache : false +}); + +module("jquery/dom/form_params") +test("with a form", function(){ + + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}) + + + var formParams = $("#qunit-test-area form").formParams() ; + ok(formParams.params.one === "1","one is right"); + ok(formParams.params.two === "2","two is right"); + ok(formParams.params.three === "3","three is right"); + same(formParams.params.four,["4","1"],"four is right"); + same(formParams.params.five,["2","3"],"five is right"); + equal(typeof formParams.id , 'string', "Id value is empty"); + + equal( typeof formParams.singleRadio, "string", "Type of single named radio is string" ); + equal( formParams.singleRadio, "2", "Value of single named radio is right" ); + + ok( $.isArray(formParams.lastOneChecked), "Type of checkbox with last option checked is array" ); + equal( formParams.lastOneChecked, "4", "Value of checkbox with the last option checked is 4" ); + +}); + +test("With a non-form element", function() { + + $("#qunit-test-area").html("//jquery/dom/form_params/test/non-form.micro",{}) + + var formParams = $("#divform").formParams() ; + + equal( formParams.id , "foo-bar-baz", "ID input read correctly" ); + +}); + + +test("with true false", function(){ + $("#qunit-test-area").html("//jquery/dom/form_params/test/truthy.micro",{}); + + var formParams = $("#qunit-test-area form").formParams(true); + ok(formParams.foo === undefined, "foo is undefined") + ok(formParams.bar.abc === true, "form bar is true"); + ok(formParams.bar.def === true, "form def is true"); + ok(formParams.bar.ghi === undefined, "form def is undefined"); + ok(formParams.wrong === false, "'false' should become false"); +}); + +test("just strings",function(){ + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}); + var formParams = $("#qunit-test-area form").formParams(false) ; + ok(formParams.params.one === "1","one is right"); + ok(formParams.params.two === '2',"two is right"); + ok(formParams.params.three === '3',"three is right"); + same(formParams.params.four,["4","1"],"four is right"); + same(formParams.params.five,['2','3'],"five is right"); + $("#qunit-test-area").html('') +}); + +test("empty string conversion",function() { + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}); + var formParams = $("#qunit-test-area form").formParams(false) ; + ok('' === formParams.empty, 'Default empty string conversion'); + formParams = $("#qunit-test-area form").formParams(true); + ok(undefined === formParams.empty, 'Default empty string conversion'); +}); + +test("missing names",function(){ + $("#qunit-test-area").html("//jquery/dom/form_params/test/checkbox.micro",{}); + var formParams = $("#qunit-test-area form").formParams() ; + ok(true, "does not break") +}); + +test("same input names to array", function() { + $("#qunit-test-area").html("//jquery/dom/form_params/test/basics.micro",{}); + var formParams = $("#qunit-test-area form").formParams(true); + same(formParams.param1, ['first', 'second', 'third']); +}); + +}); diff --git a/dom/form_params/qunit.html b/dom/form_params/qunit.html index 1223df9f..d1e2f34c 100644 --- a/dom/form_params/qunit.html +++ b/dom/form_params/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/form_params/test/qunit/basics.micro b/dom/form_params/test/basics.micro similarity index 57% rename from dom/form_params/test/qunit/basics.micro rename to dom/form_params/test/basics.micro index 61dea977..14e76957 100644 --- a/dom/form_params/test/qunit/basics.micro +++ b/dom/form_params/test/basics.micro @@ -11,8 +11,12 @@ + + + + - + + + + + + - \ No newline at end of file + + + + + diff --git a/dom/form_params/test/qunit/checkbox.micro b/dom/form_params/test/checkbox.micro similarity index 100% rename from dom/form_params/test/qunit/checkbox.micro rename to dom/form_params/test/checkbox.micro diff --git a/dom/form_params/test/non-form.micro b/dom/form_params/test/non-form.micro new file mode 100644 index 00000000..aa81d984 --- /dev/null +++ b/dom/form_params/test/non-form.micro @@ -0,0 +1,5 @@ +
      + + + +
      diff --git a/dom/form_params/test/qunit/form_params_test.js b/dom/form_params/test/qunit/form_params_test.js deleted file mode 100644 index 18da8cc6..00000000 --- a/dom/form_params/test/qunit/form_params_test.js +++ /dev/null @@ -1,43 +0,0 @@ -module("jquery/dom/form_params") -test("with a form", function(){ - - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/basics.micro",{}) - - var formParams = $("#qunit-test-area form").formParams() ; - ok(formParams.params.one === 1,"one is right"); - ok(formParams.params.two === 2,"two is right"); - ok(formParams.params.three === 3,"three is right"); - same(formParams.params.four,["4","1"],"four is right"); - same(formParams.params.five,[2,3],"five is right"); - - -}); - - -test("with true false", function(){ - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/truthy.micro",{}); - - var formParams = $("#qunit-test-area form").formParams(); - ok(formParams.foo === undefined, "foo is undefined") - ok(formParams.bar.abc === true, "form bar is true"); - ok(formParams.bar.def === true, "form def is true"); - ok(formParams.bar.ghi === undefined, "form def is undefined"); - -}); - -test("just strings",function(){ - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/basics.micro",{}); - var formParams = $("#qunit-test-area form").formParams(false) ; - ok(formParams.params.one === "1","one is right"); - ok(formParams.params.two === '2',"two is right"); - ok(formParams.params.three === '3',"three is right"); - same(formParams.params.four,["4","1"],"four is right"); - same(formParams.params.five,['2','3'],"five is right"); - $("#qunit-test-area").html('') -}) - -test("missing names",function(){ - $("#qunit-test-area").html("//jquery/dom/form_params/test/qunit/checkbox.micro",{}); - var formParams = $("#qunit-test-area form").formParams() ; - ok(true, "does not break") -}) \ No newline at end of file diff --git a/dom/form_params/test/qunit/qunit.js b/dom/form_params/test/qunit/qunit.js deleted file mode 100644 index 933e2e7c..00000000 --- a/dom/form_params/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/form_params") //load your app - .plugins('funcunit/qunit','jquery/view/micro') //load qunit - .then("form_params_test") \ No newline at end of file diff --git a/dom/form_params/test/qunit/truthy.micro b/dom/form_params/test/truthy.micro similarity index 79% rename from dom/form_params/test/qunit/truthy.micro rename to dom/form_params/test/truthy.micro index f45de9a0..d12fdba5 100644 --- a/dom/form_params/test/qunit/truthy.micro +++ b/dom/form_params/test/truthy.micro @@ -1,7 +1,6 @@
      - - + diff --git a/controller/history/qunit.html b/dom/range/qunit.html similarity index 55% rename from controller/history/qunit.html rename to dom/range/qunit.html index d4edfd1e..019896d7 100644 --- a/controller/history/qunit.html +++ b/dom/range/qunit.html @@ -1,21 +1,17 @@ - - + Range QUnit Test -

      Model Store Cookie Test Suite

      +

      Range Test Suite

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

        The Range Plugin

        +

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

        +

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

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

        0123456789

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

        0123456789

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

        0123456789

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

        0123456789

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

        0123456789

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

        Jquery.Dom.Route Test Suite

        +

        +
        +

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

          $.Route Demo

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

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

          Hash update events:

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

          Data update events:

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

          selection Test Suite

          +

          +
          +

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

            0123456789

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

            0123456789

            "+ + "
            012
            345
            "); + stop(); + setTimeout(function(){ + var types = ['textarea','#inp','#1','#2']; + for(var i =0; i< types.length; i++){ + //console.log(types[i]) + $(types[i]).selection(1,5); + } + /* + $('textarea').selection(1,5); + $('input').selection(1,5); + $('#1').selection(1,5); + $('#2').selection(1,5); + */ + var res = []; + for(var i =0; i< types.length; i++){ + res.push( $(types[i]).selection() ); + } + + + + for(var i =0; i< res.length; i++){ + same(res[i],{start: 1, end: 5},types[i]) + } + + start(); + },1000) +}); + +}); \ No newline at end of file diff --git a/dom/within/within.js b/dom/within/within.js index b846f4b1..818a18e0 100644 --- a/dom/within/within.js +++ b/dom/within/within.js @@ -1,7 +1,7 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function($){ +steal('jquery/dom').then(function($){ var withinBox = function(x, y, left, top, width, height ){ return (y >= top && y < top + height && @@ -11,33 +11,46 @@ steal.plugins('jquery/dom').then(function($){ /** * @function within * @parent dom - * Returns if the elements are within the position - * @param {Object} x - * @param {Object} y - * @param {Object} cache + * @plugin jquery/dom/within + * + * Returns the elements are within the position. + * + * // get all elements that touch 200x200. + * $('*').within(200, 200); + * + * @param {Number} left the position from the left of the page + * @param {Number} top the position from the top of the page + * @param {Boolean} [useOffsetCache] cache the dimensions and offset of the elements. + * @return {jQuery} a jQuery collection of elements whos area + * overlaps the element position. */ -$.fn.within= function(x, y, cache) { +$.fn.within= function(left, top, useOffsetCache) { var ret = [] this.each(function(){ var q = jQuery(this); - if(this == document.documentElement) return ret.push(this); - - var offset = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + if (this == document.documentElement) { + return ret.push(this); + } + var offset = useOffsetCache ? + jQuery.data(this,"offsetCache") || jQuery.data(this,"offsetCache", q.offset()) : + q.offset(); - var res = withinBox(x, y, - offset.left, offset.top, - this.offsetWidth, this.offsetHeight ); + var res = withinBox(left, top, offset.left, offset.top, + this.offsetWidth, this.offsetHeight ); - if(res) ret.push(this); + if (res) { + ret.push(this); + } }); - return this.pushStack( jQuery.unique( ret ), "within", x+","+y ); + return this.pushStack( jQuery.unique( ret ), "within", left+","+top ); } /** * @function withinBox + * @parent jQuery.fn.within * returns if elements are within the box * @param {Object} left * @param {Object} top @@ -52,7 +65,11 @@ $.fn.withinBox = function(left, top, width, height, cache){ if(this == document.documentElement) return this.ret.push(this); - var offset = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + var offset = cache ? + jQuery.data(this,"offset") || + jQuery.data(this,"offset", q.offset()) : + q.offset(); + var ew = q.width(), eh = q.height(); diff --git a/download/download.css b/download/download.css index 3e8df59b..4766d661 100644 --- a/download/download.css +++ b/download/download.css @@ -52,7 +52,7 @@ body { #plugins .plugin{ float: right; - padding: 0px 0px 18px 0px; + padding: 0px; overflow: auto; width: 422px; diff --git a/download/download.html b/download/download.html index 5631e5ac..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
            -
            -