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,33 +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+""); - content = steal.build.builders.scripts.clean(content); - s.File("jquery/dist/standalone/"+out+".js").save(content); - s.File("jquery/dist/standalone/"+out+".min.js").save(compressor(content)); - } }) \ No newline at end of file diff --git a/class/class.html b/class/class.html index a4386612..ef16143d 100644 --- a/class/class.html +++ b/class/class.html @@ -61,11 +61,9 @@

History Tabs

- \ No newline at end of file diff --git a/class/class.js b/class/class.js index f4bc4866..b48d1d00 100644 --- a/class/class.js +++ b/class/class.js @@ -2,23 +2,31 @@ // This is a modified version of John Resig's class // http://ejohn.org/blog/simple-javascript-inheritance/ // It provides class level inheritance and callbacks. -//@steal-clean -steal.plugins("jquery","jquery/lang").then(function( $ ) { +//!steal-clean +steal("jquery","jquery/lang/string",function( $ ) { - // if we are initializing a new class + // =============== HELPERS ================= + + // if we are initializing a new class var initializing = false, makeArray = $.makeArray, isFunction = $.isFunction, isArray = $.isArray, + extend = $.extend, + getObject = $.String.getObject, concatArgs = function(arr, args){ return arr.concat(makeArray(args)); }, + // tests if we can get super in .toString() fnTest = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/, - + // overwrites an object with methods, sets up _super + // newProps - new properties + // oldProps - where the old properties might be + // addTo - what we are adding to inheritProps = function( newProps, oldProps, addTo ) { addTo = addTo || newProps for ( var name in newProps ) { @@ -43,52 +51,57 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { })(name, newProps[name]) : newProps[name]; } }, - + STR_PROTOTYPE = 'prototype' /** * @class jQuery.Class * @plugin jquery/class - * @tag core + * @parent jquerymx * @download dist/jquery/jquery.class.js * @test jquery/class/qunit.html - * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between - * jQuery's functional programming style and Object Oriented Programming. - * It is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] + * @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 @@ -104,7 +117,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * this.health = 10; * * // increments count - * this.Class.count++; + * this.constructor.count++; * }, * eat: function( smallChildren ){ * this.health += smallChildren; @@ -128,68 +141,75 @@ steal.plugins("jquery","jquery/lang").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. @@ -201,7 +221,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * * * @codestart - * $.Class.extend("MyClass", + * $.Class("MyClass", * { * setup: function() {} //static setup * init: function() {} //static constructor @@ -212,97 +232,138 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * }) * @codeend * - *

Setup

- *

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

- *

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

- *

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

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

Init

+ * + * ### Init * - *

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

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

Callbacks

- *

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

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

Callback is available as a static and prototype method.

- *

Demo

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

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

+ *
+ *
{optional:Object} + *

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

+ *
+ *
{Object} + *

Creates prototype methods on the class.

+ *
+ *
+ * + * When a Class is created, the static [jQuery.Class.static.setup setup] + * and [jQuery.Class.static.init init] methods are called. + * + * To create an instance of a Class, call: + * + * new Class([args ... ]) -> instance + * + * The created instance will have all the + * prototype properties and methods defined by the PROTOTYPE object. + * + * When an instance is created, the prototype [jQuery.Class.prototype.setup setup] + * and [jQuery.Class.prototype.init init] methods + * are called. */ clss = $.Class = function() { if (arguments.length) { - clss.extend.apply(clss, arguments); + return clss.extend.apply(clss, arguments); } }; /* @Static*/ - $.extend(clss, { + 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; @@ -311,11 +372,11 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * MyClass.showData(); * @codeend *

Currying Arguments

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

Nesting Functions

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

Example

* @codestart - * $.Class.extend("MyClass",{},{}) + * $.Class("MyClass",{},{}) * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) * @codeend * @return {class} instance of the class */ newInstance: function() { + // get a raw instance objet (init is not called) var inst = this.rawInstance(), args; + + // call setup if there is a setup if ( inst.setup ) { args = inst.setup.apply(inst, arguments); } + // call init if there is an init, if setup returned args, use those as the arguments if ( inst.init ) { inst.init.apply(inst, isArray(args) ? args : arguments); } 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; @@ -474,7 +570,7 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { proto = proto || {}; var _super_class = this, - _super = this.prototype, + _super = this[STR_PROTOTYPE], name, shortName, namespace, prototype; // Instantiate a base class (but only create the instance, @@ -482,16 +578,17 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { initializing = true; prototype = new this(); initializing = false; + // Copy the properties over onto the new prototype inheritProps(proto, _super, prototype); // The dummy class constructor - function Class() { // All construction is actually done in the init method if ( initializing ) return; - if ( this.constructor !== Class && arguments.length ) { //we are being called w/o new + // we are being called w/o new, we are extending + if ( this.constructor !== Class && arguments.length ) { return arguments.callee.extend.apply(arguments.callee, arguments) } else { //we are being called w/ new return this.Class.newInstance.apply(this.Class, arguments) @@ -499,12 +596,12 @@ steal.plugins("jquery","jquery/lang").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 @@ -512,64 +609,94 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { var parts = fullName.split(/\./), shortName = parts.pop(), - current = clss.getObject(parts.join('.'), window, true), + current = getObject(parts.join('.'), window, true), namespace = current; - //@steal-remove-start + //!steal-remove-start if (!Class.nameOk ) { //steal.dev.isHappyName(fullName) } if(current[shortName]){ steal.dev.warn("class.js There's already something called "+fullName) } - //@steal-remove-end + //!steal-remove-end current[shortName] = Class; } // set things that can't be overwritten - $.extend(Class, { + extend(Class, { prototype: prototype, + /** + * @attribute namespace + * The namespaces object + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.namespace //-> MyOrg + * + */ namespace: namespace, + /** + * @attribute shortName + * The name of the class without its namespace, provided for introspection purposes. + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * + */ shortName: shortName, constructor: Class, + /** + * @attribute fullName + * The full name of the class, including namespace, provided for introspection purposes. + * + * $.Class("MyOrg.MyClass",{},{}) + * MyOrg.MyClass.shortName //-> 'MyClass' + * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' + * + */ fullName: fullName }); //make sure our prototype looks nice - Class.prototype.Class = Class.prototype.constructor = Class; + Class[STR_PROTOTYPE].Class = Class[STR_PROTOTYPE].constructor = Class; + - /** - * @attribute fullName - * The full name of the class, including namespace, provided for introspection purposes. - * @codestart - * $.Class.extend("MyOrg.MyClass",{},{}) - * MyOrg.MyClass.shortName //-> 'MyClass' - * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' - * @codeend - */ - + // call the class setup var args = Class.setup.apply(Class, concatArgs([_super_class],arguments)); - + + // call the class init if ( Class.init ) { - Class.init.apply(Class, args || []); + Class.init.apply(Class, args || concatArgs([_super_class],arguments)); } /* @Prototype*/ 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 @@ -577,44 +704,60 @@ steal.plugins("jquery","jquery/lang").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. */ } @@ -624,18 +767,19 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { - clss.prototype. + clss.callback = clss[STR_PROTOTYPE].callback = clss[STR_PROTOTYPE]. /** - * @function callback - * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. - * The only difference is this callback works + * @function proxy + * Returns a method that sets 'this' to the current instance. This does the same thing as + * and is described better in [jQuery.Class.static.proxy]. + * The only difference is this proxy works * on a instance instead of a class. * @param {String|Array} fname If a string, it represents the function to be called. * If it is an array, it will call each function in order and pass the return value of the prior function to the * next function. * @return {Function} the callback function */ - callback = clss.callback; + proxy = clss.proxy; -})(); \ No newline at end of file +})(); diff --git a/class/test/qunit/class_test.js b/class/class_test.js similarity index 89% rename from class/test/qunit/class_test.js rename to class/class_test.js index 1d02219c..ec95179c 100644 --- a/class/test/qunit/class_test.js +++ b/class/class_test.js @@ -1,4 +1,6 @@ -console.log("class ..."); +steal("jquery/class") //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/class"); test("Creating", function(){ @@ -179,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 300f8144..f3bd8476 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -1,19 +1,17 @@ -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, - binder = el.bind && el.unbind ? el : $(el); + 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; - } + } }; } binder.bind(ev, wrappedCallback || callback); @@ -25,24 +23,37 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func }; }, 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.nodeName ? $(this) : this].concat(Array.prototype.slice.call(arguments, 0))); + context.called = name; + return method.apply(context, [this.nodeName ? $(this) : this].concat( slice.call(arguments, 0) ) ); }; }, // matches dots @@ -51,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 @@ -135,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 * @@ -194,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. @@ -225,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 * @@ -295,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; @@ -324,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; @@ -333,11 +368,11 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func var args = makeArray(arguments), //if the arg is a method on this controller - isMethod = typeof options == "string" && isFunction(controller.prototype[options]), + isMethod = typeof options == "string" && isFunction(controller[STR_PROTOTYPE][options]), meth = args[0]; - 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]; @@ -355,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); @@ -399,29 +424,50 @@ 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, - arr = $.isArray(convertedName), - parts = (arr ? convertedName[1] :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, @@ -471,19 +517,12 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * - select * - submit * - * The following processors always listen on the window or document: - * - * - windowresize - * - windowscroll - * - load - * - unload - * - hashchange - * - ready - * - * Which means anytime the window is resized, the following controller will listen to it: - * + * Listen to events on the document or window + * with templated event handlers: + * + * * $.Controller('Sized',{ - * windowresize : function(){ + * "{window} resize" : function(){ * this.element.width(this.element.parent().width() / 2); * } * }); @@ -493,7 +532,8 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func processors: {}, /** * @attribute listensTo - * A list of special events this controller listens too. You only need to add event names that + * An array of special events this controller + * listens too. You only need to add event names that * are whole words (ie have no special characters). * * $.Controller('TabPanel',{ @@ -505,6 +545,7 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * }) * * $('.foo').tab_panel().trigger("show"); + * */ listensTo: [], /** @@ -525,6 +566,9 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * * $("#el1").message(); //writes "Hello World" * $("#el12").message({message: "hi"}); //writes hi + * + * In [jQuery.Controller.prototype.setup setup] the options passed to the controller + * are merged with defaults. This is not a deep merge. */ defaults: {} }, @@ -535,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: @@ -558,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 : { @@ -600,19 +664,13 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * $("#tabs1").tabs() // adds 'ui-active-state' * $("#tabs2").tabs({activeClass : 'active'}) // adds 'active' * - * + * Options are typically updated by calling + * [jQuery.Controller.prototype.update update]; + * */ - this.options = $.extend($.extend(true, {}, cls.defaults), options); - - //go through the cached list of actions and use the processor to bind - for ( funcName in cls.actions ) { - if ( cls.actions.hasOwnProperty(funcName) ) { - ready = cls.actions[funcName] || cls._getAction(funcName, this.options); - this._bindings.push( - ready.processor(ready.delegate || element, ready.parts[2], ready.parts[1], this.callback(funcName), this)); - } - } + this.options = extend( extend(true, {}, cls.defaults), options); + /** * @attribute called @@ -622,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 @@ -676,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 @@ -705,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; @@ -714,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.
    @@ -747,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, @@ -785,35 +966,39 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * this._super(); //Always call this! * }) * + * Make sure you always call _super when overwriting + * controller's destroy event. The base destroy functionality unbinds + * all event handlers the controller has created. + * * You could call destroy manually on an element with ChangeText * added like: * * $("#changed").change_text("destroy"); - * - * ### API + * */ destroy: function() { if ( this._destroyed ) { - throw this.Class.shortName + " controller instance has been deleted"; + throw this[STR_CONSTRUCTOR].shortName + " controller already deleted"; } var self = this, - fname = this.Class._fullName, + fname = this[STR_CONSTRUCTOR].pluginName || this[STR_CONSTRUCTOR]._fullName, controllers; + + // mark as destroyed this._destroyed = true; + + // remove the className this.element.removeClass(fname); - $.each(this._bindings, function( key, value ) { - if ( isFunction(value) ) { - value(self.element[0]); - } - }); - + // unbind bindings + this._unbind(); + // clean up delete this._actions; - 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; }, /** @@ -833,39 +1018,23 @@ 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 resize scroll select submit focusin focusout mouseenter mouseleave".split(" "), function( i, v ) { + + //set common events to be processed as a basicProcessor + each("change click contextmenu dblclick keydown keyup keypress mousedown mousemove mouseout mouseover mouseup reset resize scroll select submit focusin focusout mouseenter mouseleave".split(" "), function( i, v ) { processors[v] = basicProcessor; }); - $.each(["windowresize", "windowscroll", "load", "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 */ @@ -874,49 +1043,47 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func //controllers can be strings or classes var i, isAControllerOf = function( instance, controllers ) { for ( i = 0; i < controllers.length; i++ ) { - if ( typeof controllers[i] == 'string' ? instance.Class._shortName == controllers[i] : instance instanceof controllers[i] ) { + if ( typeof controllers[i] == 'string' ? instance[STR_CONSTRUCTOR]._shortName == controllers[i] : instance instanceof controllers[i] ) { return true; } } return false; }; - - /** - * @function controllers - * Gets all controllers in the jQuery element. - * @return {Array} an array of controller instances. - */ - $.fn.controllers = function() { - var controllerNames = makeArray(arguments), - instances = [], - controllers; - //check if arguments - this.each(function() { - var c, cname; - - controllers = $.data(this, "controllers"); - if (!controllers ) { - return; - } - for ( cname in controllers ) { - if ( controllers.hasOwnProperty(cname) ) { - c = controllers[cname]; - if (!controllerNames.length || isAControllerOf(c, controllerNames) ) { - instances.push(c); + $.fn.extend({ + /** + * @function controllers + * Gets all controllers in the jQuery element. + * @return {Array} an array of controller instances. + */ + controllers: function() { + var controllerNames = makeArray(arguments), + instances = [], + controllers, c, cname; + //check if arguments + this.each(function() { + + controllers = $.data(this, "controllers"); + for ( cname in controllers ) { + if ( controllers.hasOwnProperty(cname) ) { + c = controllers[cname]; + if (!controllerNames.length || isAControllerOf(c, controllerNames) ) { + instances.push(c); + } } } - } - }); - return instances; - }; - /** - * @function controller - * Gets a controller in the jQuery element. With no arguments, returns the first one found. - * @param {Object} controller (optional) if exists, the first controller instance with this class type will be returned. - * @return {jQuery.Controller} the first controller. - */ - $.fn.controller = function( controller ) { - return this.controllers.apply(this, arguments)[0]; - }; + }); + return instances; + }, + /** + * @function controller + * Gets a controller in the jQuery element. With no arguments, returns the first one found. + * @param {Object} controller (optional) if exists, the first controller instance with this class type will be returned. + * @return {jQuery.Controller} the first controller. + */ + controller: function( controller ) { + return this.controllers.apply(this, arguments)[0]; + } + }); + -}); \ No newline at end of file +}); diff --git a/controller/test/qunit/controller_test.js b/controller/controller_test.js similarity index 56% rename from controller/test/qunit/controller_test.js rename to controller/controller_test.js index 6c481294..d94799e9 100644 --- a/controller/test/qunit/controller_test.js +++ b/controller/controller_test.js @@ -1,3 +1,7 @@ +steal("jquery/controller",'jquery/controller/subscribe') //load your app + .then('funcunit/qunit') //load qunit + .then(function(){ + module("jquery/controller") test("subscribe testing works", function(){ @@ -55,54 +59,6 @@ test("subscribe testing works", function(){ }) -test("document and main controllers", function(){ - var a = $("
    ").appendTo($("#qunit-test-area")), - a_inner = a.find('span'), - b = $("
    ").appendTo($("#qunit-test-area")), - b_inner = b.find('span'), - doc_outer_clicks = 0, - doc_inner_clicks = 0, - main_outer_clicks = 0, - main_inner_clicks = 0; - - $.Controller.extend("TestController", { onDocument: true }, { - click: function() { - doc_outer_clicks++; - }, - "span click" : function() { - doc_inner_clicks++; - } - }) - - a_inner.trigger("click"); - equals(doc_outer_clicks,1,"document controller handled (no-selector) click inside listening element"); - equals(doc_inner_clicks,1,"document controller handled (selector) click inside listening element"); - - b_inner.trigger("click"); - equals(doc_outer_clicks,1,"document controller ignored (no-selector) click outside listening element"); - equals(doc_inner_clicks,1,"document controller ignored (selector) click outside listening element"); - - $(document.documentElement).controller('test').destroy(); - - $.Controller.extend("MainController", { onDocument: true }, { - click: function() { - main_outer_clicks++; - }, - "span click" : function() { - main_inner_clicks++; - } - }) - - b_inner.trigger("click"); - equals(main_outer_clicks,1,"main controller handled (no-selector) click"); - equals(main_inner_clicks,1,"main controller handled (selector) click"); - - $(document.documentElement).controller('main').destroy(); - - a.remove(); - b.remove(); -}) - test("bind to any special", function(){ jQuery.event.special.crazyEvent = { @@ -141,7 +97,7 @@ test("parameterized actions", function(){ test("windowresize", function(){ var called = false; jQuery.Controller.extend("WindowBind",{ - "windowresize" : function() { + "{window} resize" : function() { called = true; } }) @@ -191,7 +147,7 @@ test("objects in action", function(){ "{item} someEvent" : function(thing, ev){ ok(true, "called"); equals(ev.type, "someEvent","correct event") - equals(this.Class.fullName, "Thing", "This is a controller isntance") + equals(this.constructor.fullName, "Thing", "This is a controller isntance") equals(thing.name,"Justin","Raw, not jQuery wrapped thing") } }); @@ -205,4 +161,109 @@ test("objects in action", function(){ $("#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 07f6d2b3..00000000 --- a/controller/history/history.js +++ /dev/null @@ -1,218 +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. - * - * It allows you to listen to hashchange events with OpenAjax.hub. - * - * Typically you subscribe to a history event in your controllers: - * - * $.Controller("MyHistory",{ - * "history.pagename subscribe" : function(called, data){ - * //called when hash = #pagename - * } - * }) - * - * ## Event Names - * - * When a history event happens, an OpenAjax message is produced that - * starts with "history.". The remainder of the message name depends on the - * value of the "hash". - * - * The following shows hash values and - * the corresponding published message and data. - * - * "#foo=bar" -> "history.index" {foo: bar} - * "#foo/bar" -> "history.foo.bar" {} - * "#foo&bar=baz" -> "history.foo" {bar: baz} - * - * Essentially, if the hash starts with something like #foo/bar, this gets - * added to the message name as "foo.bar". Once "&" is found, it adds the remainder - * as name-value pairs to the message data. - * - * ## Controller Helper Functions - * - * The methods on the left are added to Controller.prototype and make it easier to - * make changes to history. - * - */ - -var keyBreaker = /([^\[\]]+)|(\[\])/g; - -$.Controller.History = { - /** - * @hide - * returns the pathname part - * - * // if the url is "#foo/bar&foo=bar" - * $.Controller.History.pathname() -> 'foo/bar' - * - */ - pathname : function(path) { - var parts = path.match(/#([^&]*)/); - return parts ? parts[1] : null - }, - /** - * @hide - * returns the search part, but without the first & - * - * // if the url is "#foo/bar&foo=bar" - * $.Controller.History.search() -> 'foo=bar' - */ - search : function(path) { - var parts = path.match(/#[^&]*&(.*)/); - return parts ? parts[1] : null - }, - /** - * @hide - * Returns the data - * @param {Object} path - */ - getData: function(path) { - var search = $.Controller.History.search(path), - digitTest = /^\d+$/; - if(! search || ! search.match(/([^?#]*)(#.*)?$/) ) { - return {}; - } - - // Support the legacy format that used MVC.Object.to_query_string that used %20 for - // spaces and not the '+' sign; - search = search.replace(/\+/g,"%20") - - 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 -}) -/** - * @add jQuery.Controller.prototype - */ - -$.extend($.Controller.prototype, { - /** - * @parent jquery.controller.history - * Redirects to another page. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - */ - redirectTo: function(options){ - var point = this._get_history_point(options); - location.hash = point; - }, - /** - * @parent jquery.controller.history - * Redirects to another page by replacing current URL with the given one. This - * call will not create a new entry in the history. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - */ - replaceWith: function(options){ - var point = this._get_history_point(options); - location.replace(location.href.split('#')[0] + point); - }, - /** - * @parent jquery.controller.history - * Adds history point to browser history. - * @plugin 'dom/history' - * @param {Object} options an object that will turned into a url like #controller/action¶m1=value1 - * @param {Object} data extra data saved in history -- NO LONGER SUPPORTED - */ - historyAdd : function(options, data) { - var point = this._get_history_point(options); - location.hash = point; - }, - /** - * @hide - * @parent jquery.controller.history - * Creates a history point from given options. Resultant history point is like #controller/action¶m1=value1 - * @plugin 'dom/history' - * @param {Object} options an object that will turned into history point - */ - _get_history_point: function(options) { - var controller_name = options.controller || this.Class.underscoreName; - var action_name = options.action || 'index'; - - /* Convert the options to parameters (removing controller and action if needed) */ - if(options.controller) - delete options.controller; - if(options.action) - delete options.action; - - var paramString = (options) ? $.param(options) : ''; - if(paramString.length) - paramString = '&' + paramString; - - return '#' + controller_name + '/' + action_name + paramString; - }, - - /** - * @parent jquery.controller.history - * Provides current window.location parameters as object properties. - * @plugin 'dom/history' - */ - pathData :function() { - return $.Controller.History.getData(location.href); - } -}); - - - - - -}); \ No newline at end of file diff --git a/controller/history/html5/html5.js b/controller/history/html5/html5.js deleted file mode 100644 index c2c94075..00000000 --- a/controller/history/html5/html5.js +++ /dev/null @@ -1,31 +0,0 @@ -steal.plugins('jquery/controller/subscribe').then(function($){ - - var hasHistoryManagementSupport = !!(window.history && history.pushState); - - if (hasHistoryManagementSupport) { - steal.dev.log("WARNING: The current browser does not support HTML5 History Management."); - } else { - window.onpopstate = function(event) { - OpenAjax.hub.publish("history."+location.href, (event && event.state) || {}); - }; - - setTimeout(function(){ - window.onpopstate(); - }, 1); // immediately after ready - - $.extend($.Controller.prototype, { - redirectTo: function(url, data, title) { - data = data || {}; - window.history.pushState(data, title, url); - this.publish("history." + url, data); - } - }); - - $.Controller.processors["windowpopstate"] = function(el, event, selector, cb) { - $(window).bind("popstate", cb); - return function(){ - $(window).unbind("popstate", cb); - } - }; - } -}) diff --git a/controller/history/html5/qunit.html b/controller/history/html5/qunit.html deleted file mode 100644 index 7274e702..00000000 --- a/controller/history/html5/qunit.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - -

    HTML5 History Test Suite

    -

    -
    -

    -
    -
      -
      - - diff --git a/controller/history/html5/qunit/qunit.js b/controller/history/html5/qunit/qunit.js deleted file mode 100755 index 1687eaef..00000000 --- a/controller/history/html5/qunit/qunit.js +++ /dev/null @@ -1,82 +0,0 @@ -steal.plugins("funcunit/qunit", "jquery/controller/history/html5").then(function($){ - -module("jquery/controller/history/html5",{ - setup: function(){ - - } -}) - - -$.Controller.extend("HTML5HistoryTestController", { -}, -{ - "history.** subscribe": function(event_name, params) { - this["gotHistory"](event_name.replace("history.", ""), params); - }, - - gotHistory: $.noop, - - "window windowpopstate": function(ev) { - this["gotPopState"](location.href, (ev.originalEvent && ev.originalEvent.state) || {}); - }, - - gotPopState: $.noop -}); - -var originalLocation = location.href; - -asyncTest("Controller redirect should work", function(){ - expect(1); - var testController = new HTML5HistoryTestController($("
      ").get(0)); - - var testLocation = "/test/location"; - testController["gotHistory"] = function(location, state) { - start(); - equals(location, testLocation); - testController["gotHistory"] = $.noop; - testController.redirectTo(originalLocation); - }; - - stop(); - testController.redirectTo(testLocation); -}); - -asyncTest("State data should persist", function(){ - expect(1); - var testController = new HTML5HistoryTestController($("
      ").get(0)); - - testController["gotHistory"] = function(location, state) { - start(); - equals(state.hi, "mom"); - testController["gotHistory"] = $.noop; - testController.redirectTo(originalLocation); - }; - - stop(); - testController.redirectTo("/test/location", { hi: "mom" }); -}); - -asyncTest("Should listen to windowpopstate", function(){ - expect(2); - var testController = new HTML5HistoryTestController($("
      ").get(0)); - - testController["gotPopState"] = function(location, state) { - start(); - ok(location.indexOf("/test/location") !== -1); - equals(state.hi, "mom"); - testController["gotPopState"] = $.noop; - testController.redirectTo(originalLocation); - }; - - stop(); - testController.redirectTo("/test/location", { hi: "mom" }); - - testController["gotHistory"] = function(location, state) { - testController["gotHistory"] = $.noop; - window.history.back(); - }; - - testController.redirectTo("/test/location2", { hi: "mom2" }); -}); - -}); diff --git a/controller/history/qunit/qunit.js b/controller/history/qunit/qunit.js deleted file mode 100644 index 9a6d5e71..00000000 --- a/controller/history/qunit/qunit.js +++ /dev/null @@ -1,38 +0,0 @@ -steal.plugins('funcunit/qunit','jquery/controller/history').then(function($){ - -module("jquery/controller/history",{ - setup: function(){ - - } -}) - -test("Basic getData",function(){ - - var data = $.Controller.History.getData("#foo/bar&a=b"); - equals(data.a,"b") - - var data = $.Controller.History.getData("#foo/bar&a=b&c=d"); - equals(data.a,"b") - equals(data.c,"d") -}) -test("Nested getData",function(){ - - var data = $.Controller.History.getData("#foo/bar&a[b]=1&a[c]=2"); - equals(data.a.b,1) - equals(data.a.c,2) - - var data = $.Controller.History.getData("#foo/bar&a[]=1&a[]=2"); - equals(data.a[0],1) - equals(data.a[1],2) - - var data = $.Controller.History.getData("#foo/bar&a[b][]=1&a[b][]=2"); - equals(data.a.b[0],1) - equals(data.a.b[1],2) - - var data = $.Controller.History.getData("#foo/bar&a[0]=1&a[1]=2"); - equals(data.a[0],1) - equals(data.a[1],2) -}) - - -}) diff --git a/controller/pages/document.js b/controller/pages/document.js deleted file mode 100644 index fc3fd328..00000000 --- a/controller/pages/document.js +++ /dev/null @@ -1,65 +0,0 @@ -/** -@page jquery.controller.documentcontrollers Document Controllers -@parent jQuery.Controller - -Document Controllers delegate on the -documentElement. You don't have to attach an instance as this will be done -for you when the controller class is created. Document Controllers, with the -exception of MainControllers, -add an implicit '#CONTROLLERNAME' before every selector. - -To create a document controller, you just have to set -the controller's [jQuery.Controller.static.onDocument static onDocument] -property to true. - -@codestart -$.Controller.extend('TodosController', -{onDocument: true}, -{ - ".todo mouseover" : function( el, ev ) { //matches #todos .todo - el.css("backgroundColor","red") - }, - ".todo mouseout" : function( el, ev ) { //matches #todos .todo - el.css("backgroundColor","") - }, - ".create click" : function() { //matches #todos .create - this.find("ol").append("<li class='todo'>New Todo</li>"); - } -}) -@codeend - -DocumentControllers should be used sparingly. They are not very reusable. -They should only be used for glueing together other controllers and page -layout. - -Often, a Document Controller's "ready" event will be used to create -necessary Element Controllers. - -@codestart -$.Controller.extend('SidebarController', -{onDocument: true}, -{ - ready : function() { - $(".slider").slider() - }, - "a.tag click" : function() {..} -}) -@codeend - -## MainControllers - -MainControllers are documentControllers that do not add '#CONTROLLERNAME' before every selector. This controller -should only be used for page wide functionality and setup. - -@codestart -$.Controller.extend("MainController",{ - hasActiveElement : document.activeElement || false -},{ - focus : funtion(el){ - if(!this.Class.hasActiveElement) - document.activeElement = el[0] //tracks active element - } -}) -@codeend - */ -// \ No newline at end of file diff --git a/controller/pages/listening.js b/controller/pages/listening.js deleted file mode 100644 index cbd8bf67..00000000 --- a/controller/pages/listening.js +++ /dev/null @@ -1,114 +0,0 @@ -/** -@page jquery.controller.listening Listening To Events -@parent jQuery.Controller - -Controllers organize event handlers and make listening to -events really easy. - -## Automatic Binding - -When a [jQuery.Controller.prototype.setup new controller is created], -contoller checks its methods for functions that are named like -an event handler. It automatically binds these functions to the -controller's [jQuery.Controller.prototype.element element] with event delegation. When -the controller is destroyed (or it's element is removed from the page), controller -will unbind all its event handlers automatically. - -For example, each of the following controller's functions will automatically -bound: - - $.Controller("Crazy",{ - - // listens to all clicks on this element - "click" : function(){}, - - // listens to all mouseovers on - // li elements withing this controller - "li mouseover" : function(){} - - // listens to the window being resized - "windowresize" : function(){} - }) - -Controller will bind function names with spaces, standard DOM events, and -event names in $.event.special. - -In general, Controller will know automatically when to bind event handler functions except for -one case - event names without selectors that are not in $.event.special. - -But to correct for this, you just need to add the -function to the listensTo property. Here's how: - - $.Controller.extend("MyShow",{ - listensTo: ["show"] - },{ - show: function( el, ev ) { - el.show(); - } - }) - $('.show').my_show().trigger("show"); - -## Callback parameters - -Event handlers bound with controller are called back with the element and the event -as parameters. this refers to the controller instance. For example: - - $.Controller("Tabs",{ - - // li - the list element that was clicked - // ev - the click event - "li click" : function(li, ev){ - this.tab(li).hide() - }, - tab : function(li){ - return $(li.find("a").attr("href")) - } - }) - -## Parameterized Event Bindings - -Controller lets you parameterize event names and selectors. The following -makes 2 buttons. One says hello on click, the other on mouseenter. - - $.Controller("Hello",{ - "{helloEvent}" : function(){ - alert('hello') - } - }) - - $("#clickMe").hello({helloEvent : "click"}); - $("#touchMe").hello({helloEvent : "mouseenter"}); - -You can parameterize any part of the method name. The following makes two -lists. One listens for clicks on divs, the other on lis. - - $.Controller("List",{ - "{listItem} click" : function(){ - //do something! - } - }) - - $("#divs").list({listItem : "div"}); - $("#lis").list({listItem : "li"}); - -## Subscribing to OpenAjax messages and custom bindings - -The jquery/controller/subscribe plugin allows controllers to listen -to OpenAjax.hub messages like: - - $.Controller("Listener",{ - "something.updated subscribe" : function(called, data){ - - } - }) - -You can create your own binders by adding to [jQuery.Controller.static.processors]. - -## Manually binding to events. - -The [jQuery.Controller.prototype.bind] and [jQuery.Controller.prototype.delegate] -methods let you listen to events on other elements. These event handlers will -be unbound when the controller instance is destroyed. - - */ -// \ No newline at end of file diff --git a/controller/pages/listening.md b/controller/pages/listening.md new file mode 100644 index 00000000..c27ab175 --- /dev/null +++ b/controller/pages/listening.md @@ -0,0 +1,189 @@ +@page jquery.controller.listening Listening To Events +@parent jQuery.Controller + +Controllers make creating and tearing down event handlers extremely +easy. The tearingdown of event handlers is especially important +in preventing memory leaks in long lived applications. + +## Automatic Binding + +When a [jQuery.Controller.prototype.setup new controller is created], +contoller checks its prototype methods for functions that are named like +event handlers. It binds these functions to the +controller's [jQuery.Controller.prototype.element element] with +event delegation. When +the controller is destroyed (or it's element is removed from the page), controller +will unbind its event handlers automatically. + +For example, each of the following controller's functions will automatically +bound: + + $.Controller("Crazy",{ + + // listens to all clicks on this element + "click" : function(el, ev){}, + + // listens to all mouseovers on + // li elements withing this controller + "li mouseover" : function(el, ev){} + + // listens to the window being resized + "{window} resize" : function(window, ev){} + }) + +Controller will bind function names with spaces, standard DOM events, and +event names in $.event.special. + +In general, Controller will know automatically when to bind event handler functions except for +one case - event names without selectors that are not in $.event.special. + +But to correct for this, you just need to add the +function to the [jQuery.Controller.static.listensTo listensTo] +property. Here's how: + + $.Controller("MyShow",{ + listensTo: ["show"] + },{ + show: function( el, ev ) { + el.show(); + } + }) + $('.show').my_show().trigger("show"); + +## Callback parameters + +Event handlers bound with controller are called back with the element and the event +as parameters. this refers to the controller instance. For example: + + $.Controller("Tabs",{ + + // li - the list element that was clicked + // ev - the click event + "li click" : function(li, ev){ + this.tab(li).hide() + }, + tab : function(li){ + return $(li.find("a").attr("href")) + } + }) + +## Templated Event Bindings + +One of Controller's most powerful features is templated event +handlers. You can parameterize the event name, +the selector, or event the root element. + +### Templating event names and selectors: + +Often, you want to make a widget's behavior +configurable. A common example is configuring which event +a menu should show a sub-menu (ex: on click or mouseenter). The +following controller lets you configure when a menu should show +sub-menus: + +The following makes two buttons. One says hello on click, +the other on a 'tap' event. + + $.Controller("Menu",{ + "li {showEvent}" : function(el){ + el.children('ul').show() + } + }) + + $("#clickMe").menu({showEvent : "click"}); + $("#touchMe").menu({showEvent : "mouseenter"}); + +$.Controller replaces value in {} with +values in a +controller's [jQuery.Controller.prototype.options options]. This means +we can easily provide a default showEvent value and create +a menu without providing a value like: + + $.Controller("Menu", + { + defaults : { + showEvent : "click" + } + }, + { + "li {showEvent}" : function(el){ + el.children('ul').show() + } + }); + + $("#clickMe").menu(); //defaults to using click + +Sometimes, we might might want to configure our widget to +use different elements. The following makes the menu widget's +button elements configurable: + + $.Controller("Menu",{ + "{button} {showEvent}" : function(el){ + el.children('ul').show() + } + }) + + $('#buttonMenu').menu({button: "button"}); + +### Templating the root element. + +Finally, controller lets you bind to objects outside +of the [jQuery.Controller.prototype.element controller's element]. + +The following listens to clicks on the window: + + $.Controller("HideOnClick",{ + "{window} click" : function(){ + this.element.hide() + } + }) + +The following listens to Todos being created: + + $.Controller("NewTodos",{ + "{App.Models.Todo} created" : function(Todo, ev, newTodo){ + this.element.append("newTodos.ejs", newTodo) + } + }); + +But instead of making NewTodos only work with the Todo model, +we can make it configurable: + + $.Controller("Newbie",{ + "{model} created" : function(Model, ev, newItem){ + this.element.append(this.options.view, newItem) + } + }); + + $('#newItems').newbie({ + model: App.Models.Todo, + view: "newTodos.ejs" + }) + +### How Templated events work + +When looking up a value to replace {}, +controller first looks up the item in the options, then it looks +up the value in the window object. It does not use eval to look up the +object. Instead it uses [jQuery.String.getObject]. + + +## Subscribing to OpenAjax messages and custom bindings + +The jquery/controller/subscribe plugin allows controllers to listen +to OpenAjax.hub messages like: + + $.Controller("Listener",{ + "something.updated subscribe" : function(called, data){ + + } + }) + +You can create your own binders by adding to [jQuery.Controller.static.processors]. + +## Manually binding to events. + +The [jQuery.Controller.prototype.bind] and [jQuery.Controller.prototype.delegate] +methods let you listen to events on other elements. These event handlers will +be unbound when the controller instance is destroyed. + diff --git a/controller/pages/plugin.js b/controller/pages/plugin.md similarity index 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/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/test/qunit/controller_view_test.js b/controller/view/test/qunit/controller_view_test.js index 82188116..506da897 100644 --- a/controller/view/test/qunit/controller_view_test.js +++ b/controller/view/test/qunit/controller_view_test.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/controller/view','jquery/view/micro','funcunit/qunit') //load qunit +steal('jquery/controller/view','jquery/view/micro','funcunit/qunit') //load qunit .then(function(){ module("jquery/controller/view"); @@ -18,5 +18,33 @@ steal.plugins('jquery/controller/view','jquery/view/micro','funcunit/qunit') // ok(/Hello World/i.test($('#cont_view').text()),"view rendered") }); + test("test.suffix.doubling", function(){ + + $.Controller.extend("jquery.Controller.View.Test.Qunit",{ + init: function() { + this.element.html(this.view('init.micro')) + } + }) + + jQuery.View.ext = ".ejs"; // Reset view extension to default + equal(".ejs", jQuery.View.ext); + + $("#qunit-test-area").append("
      "); + + new jquery.Controller.View.Test.Qunit( $('#suffix_test_cont_view') ); + + ok(/Hello World/i.test($('#suffix_test_cont_view').text()),"view rendered") + }); + + test("complex paths nested inside a controller directory", function(){ + $.Controller.extend("Myproject.Controllers.Foo.Bar"); + + var path = jQuery.Controller._calculatePosition(Myproject.Controllers.Foo.Bar, "init.ejs", "init") + equals(path, "//myproject/views/foo/bar/init.ejs", "view path is correct") + + $.Controller.extend("Myproject.Controllers.FooBar"); + path = jQuery.Controller._calculatePosition(Myproject.Controllers.FooBar, "init.ejs", "init") + equals(path, "//myproject/views/foo_bar/init.ejs", "view path is correct") + }) }); diff --git a/controller/view/test/qunit/qunit.js b/controller/view/test/qunit/qunit.js index 38255d8c..7873c312 100644 --- a/controller/view/test/qunit/qunit.js +++ b/controller/view/test/qunit/qunit.js @@ -1,6 +1,5 @@ //we probably have to have this only describing where the tests are -steal - .plugins('jquery/controller/view','jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("controller_view_test") +steal('jquery/controller/view','jquery/view/micro') //load your app + .then('funcunit/qunit') //load qunit + .then("./controller_view_test.js") diff --git a/controller/view/view.js b/controller/view/view.js index 8872d138..c248fa5c 100644 --- a/controller/view/view.js +++ b/controller/view/view.js @@ -1,23 +1,30 @@ -steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { +steal('jquery/controller', 'jquery/view').then(function( $ ) { + var URI = steal.URI || steal.File; + jQuery.Controller.getFolder = function() { return jQuery.String.underscore(this.fullName.replace(/\./g, "/")).replace("/Controllers", ""); }; - var calculatePosition = function( Class, view, action_name ) { - var slashes = Class.fullName.replace(/\./g, "/"), - hasControllers = slashes.indexOf("/Controllers/" + Class.shortName) != -1, - path = jQuery.String.underscore(slashes.replace("/Controllers/" + Class.shortName, "")), - controller_name = Class._shortName, - suffix = (typeof view == "string" && view.match(/\.[\w\d]+$/)) || jQuery.View.ext; + jQuery.Controller._calculatePosition = function( Class, view, action_name ) { + + var classParts = Class.fullName.split('.'), + classPartsWithoutPrefix = classParts.slice(0); + classPartsWithoutPrefix.splice(0, 2); // Remove prefix (usually 2 elements) + var classPartsWithoutPrefixSlashes = classPartsWithoutPrefix.join('/'), + hasControllers = (classParts.length > 2) && classParts[1] == 'Controllers', + path = hasControllers? jQuery.String.underscore(classParts[0]): jQuery.String.underscore(classParts.join("/")), + controller_name = jQuery.String.underscore(classPartsWithoutPrefix.join('/')).toLowerCase(), + suffix = (typeof view == "string" && /\.[\w\d]+$/.test(view)) ? "" : jQuery.View.ext; + //calculate view if ( typeof view == "string" ) { if ( view.substr(0, 2) == "//" ) { //leave where it is } else { - view = "//" + new steal.File('views/' + (view.indexOf('/') !== -1 ? view : (hasControllers ? controller_name + '/' : "") + view)).joinFrom(path) + suffix; + view = "//" + URI(path).join( 'views/' + (view.indexOf('/') !== -1 ? view : (hasControllers ? controller_name + '/' : "") + view)) + suffix; } } else if (!view ) { - view = "//" + new steal.File('views/' + (hasControllers ? controller_name + '/' : "") + action_name.replace(/\.|#/g, '').replace(/ /g, '_')).joinFrom(path) + suffix; + view = "//" + URI(path).join('views/' + (hasControllers ? controller_name + '/' : "") + action_name.replace(/\.|#/g, '').replace(/ /g, '_'))+ suffix; } return view; }; @@ -38,14 +45,16 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { } //load from name var current = window; - var parts = this.Class.fullName.split(/\./); + var parts = this.constructor.fullName.split(/\./); for ( var i = 0; i < parts.length; i++ ) { - if ( typeof current.Helpers == 'object' ) { - jQuery.extend(helpers, current.Helpers); + if(current){ + if ( typeof current.Helpers == 'object' ) { + jQuery.extend(helpers, current.Helpers); + } + current = current[parts[i]]; } - current = current[parts[i]]; } - if ( typeof current.Helpers == 'object' ) { + if (current && typeof current.Helpers == 'object' ) { jQuery.extend(helpers, current.Helpers); } this._default_helpers = helpers; @@ -71,18 +80,20 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { * el.html( this.view() ) * // renders with views/tasks/under.ejs * el.after( this.view("under", [1,2]) ); + * // renders with views/tasks/under.micro + * el.after( this.view("under.micro", [1,2]) ); * // renders with views/shared/top.ejs * el.before( this.view("shared/top", {phrase: "hi"}) ); * } * }) * @codeend - * @plugin controller/view + * @plugin jquery/controller/view * @return {String} the rendered result of the view. - * @param {String} [optional1] view The view you are going to render. If a view isn't explicity given + * @param {String} [view] The view you are going to render. If a view isn't explicity given * this function will try to guess at the correct view as show in the example code above. - * @param {Object} [optional2] data data to be provided to the view. If not present, the controller instance + * @param {Object} [data] data to be provided to the view. If not present, the controller instance * is used. - * @param {Object} [optional3] myhelpers an object of helpers that will be available in the view. If not present + * @param {Object} [myhelpers] an object of helpers that will be available in the view. If not present * this controller class's "Helpers" property will be used. * */ @@ -94,7 +105,7 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { view = null; } //guess from controller name - view = calculatePosition(this.Class, view, this.called); + view = jQuery.Controller._calculatePosition(this.Class, view, this.called); //calculate data data = data || this; diff --git a/dom/closest/closest.js b/dom/closest/closest.js index 906e865c..14ac24da 100644 --- a/dom/closest/closest.js +++ b/dom/closest/closest.js @@ -1,14 +1,15 @@ /** * @add jQuery.fn */ -steal.plugins('jquery/dom').then(function(){ +steal('jquery/dom').then(function(){ /** * @function closest * @parent dom - * Overwrites closest to allow open > selectors. This allows controller actions such as: - * @codestart - * ">li click" : function( el, ev ) { ... } - * @codeend + * @plugin jquery/dom/closest + * Overwrites closest to allow open > selectors. This allows controller + * actions such as: + * + * ">li click" : function( el, ev ) { ... } */ var oldClosest = jQuery.fn.closest; jQuery.fn.closest = function(selectors, context){ diff --git a/dom/compare/compare.html b/dom/compare/compare.html index 4e744c69..83c356a8 100644 --- a/dom/compare/compare.html +++ b/dom/compare/compare.html @@ -60,29 +60,31 @@

      Key

      - \ No newline at end of file diff --git a/dom/compare/compare.js b/dom/compare/compare.js index 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 b44590e5..a563d5b4 100644 --- a/dom/cur_styles/cur_styles.js +++ b/dom/cur_styles/cur_styles.js @@ -1,4 +1,4 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom').then(function( $ ) { var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle, rupper = /([A-Z])/g, diff --git a/dom/cur_styles/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 73107512..672b5a7b 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -1,25 +1,133 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom', + 'jquery/lang/object', + 'jquery/lang/string',function( $ ) { + + //used to check urls + - var ajax = $.ajax, - typeTest = /^(script|json|test|jsonp)$/, + + // 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 = [], - // checks if an overwrite matches ajax settings - isTheSame = function(settings, overwrite){ - for(var prop in overwrite){ - if(prop === 'fixture'){ - continue; - } - if(overwrite[prop] !== settings[prop]){ - return false; - } - } - return true; - }, // returns the index of an overwrite function - find = function(settings){ + find = function(settings, exact){ for(var i =0; i < overwrites.length; i++){ - if(isTheSame(settings, overwrites[i])){ + if($fixture._similar(settings, overwrites[i], exact)){ return i; } } @@ -30,286 +138,376 @@ steal.plugins('jquery/dom').then(function( $ ) { var index = find(settings); if(index > -1){ settings.fixture = overwrites[index].fixture; + return $fixture._getData(overwrites[index].url, settings.url) + } + + }, + /** + * 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; } + //*/ - }; // by url + 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 , fixture) { + var $fixture = $.fixture = function( settings , fixture ){ // if we provide a fixture ... if(fixture !== undefined){ if(typeof settings == 'string'){ // handle url strings - settings ={ - url : settings - }; + var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i); + if(!matches){ + settings = { + url : settings + }; + } else { + settings = { + url : matches[2], + type: matches[1] + }; + } + + } + + //handle removing. An exact match if fixture was provided, otherwise, anything similar + var index = find(settings, !!fixture); + if(index > -1){ + overwrites.splice(index,1) } - //handle removing if(fixture == null){ - var index = find(settings); - if(index >= -1){ - return overwrites.splice(index,1) - } return } - settings.fixture = fixture; overwrites.push(settings) - return; - } - - - 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; } - 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 nested list of messages - * $.fixture.make(["messages","message"],1000, function(i, messages){ - * return { - * subject: "This is message "+i, - * body: "Here is some text for this message", - * date: Math.floor( new Date().getTime() ), - * parentId : i < 100 ? null : Math.floor(Math.random()*i) - * } - * }) - * //uses the message fixture to return messages limited by offset, limit, order, etc. - * $.ajax({ - * url: "messages", - * data:{ - * offset: 100, - * limit: 50, - * order: "date ASC", - * parentId: 5}, - * }, - * fixture: "-messages", - * success: function( messages ) { ... } - * }); - * @codeend + * + * //makes a nested list of messages + * $.fixture.make(["messages","message"],1000, function(i, messages){ + * return { + * subject: "This is message "+i, + * body: "Here is some text for this message", + * date: Math.floor( new Date().getTime() ), + * parentId : i < 100 ? null : Math.floor(Math.random()*i) + * } + * }) + * //uses the message fixture to return messages limited by offset, limit, order, etc. + * $.ajax({ + * url: "messages", + * data:{ + * offset: 100, + * limit: 50, + * order: ["date ASC"], + * parentId: 5}, + * }, + * fixture: "-messages", + * success: function( messages ) { ... } + * }); + * * @param {Array|String} types An array of the fixture names or the singular fixture name. * If an array, the first item is the plural fixture name (prefixed with -) and the second * item is the singular name. If a string, it's assumed to be the singular fixture name. Make @@ -317,8 +515,20 @@ steal.plugins('jquery/dom').then(function( $ ) { * @param {Number} count the number of items to create * @param {Function} make a function that will return json data representing the object. The * make function is called back with the id and the current array of items. + * @param {Function} filter (optional) a function used to further filter results. Used for to simulate + * server params like searchText or startDate. The function should return true if the item passes the filter, + * false otherwise. For example: + * + * + * function(item, settings){ + * if(settings.data.searchText){ + * var regex = new RegExp("^"+settings.data.searchText) + * return regex.test(item.name); + * } + * } + * */ - make: function( types, count, make ) { + make: function( types, count, make, filter ) { if(typeof types === "string"){ types = [types+"s",types ] } @@ -330,25 +540,6 @@ steal.plugins('jquery/dom').then(function( $ ) { return items[i]; } } - }, - getId = function(settings){ - var id = settings.data.id; - - 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; - } - }) - } - - return id; }; for ( var i = 0; i < (count); i++ ) { @@ -362,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; + } } }); }); @@ -390,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); @@ -405,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 [{ @@ -416,7 +632,8 @@ steal.plugins('jquery/dom').then(function( $ ) { }; // findOne $.fixture["-" + types[1]] = function( settings ) { - return [findOne(settings.data.id)]; + var item = findOne(getId(settings)); + return item ? [item] : []; }; // update $.fixture["-" + types[1]+"Update"] = function( settings, cbType ) { @@ -441,6 +658,7 @@ steal.plugins('jquery/dom').then(function( $ ) { }; $.fixture["-" + types[1]+"Create"] = function( settings, cbType ) { var item = make(items.length, items); + $.extend(item, settings.data); if(!item.id){ @@ -448,12 +666,92 @@ steal.plugins('jquery/dom').then(function( $ ) { } items.push(item); - return $.fixture["-restCreate"](settings, cbType) + + return $.fixture["-restCreate"](settings, cbType, undefined, item.id ); }; + + + return { + getId: getId, + findOne : findOne, + find : function(settings){ + return findOne( getId(settings) ); + } + } }, /** + * @function jQuery.fixture.rand + * @parent jQuery.fixture + * + * Creates random integers or random arrays of + * other arrays. + * + * ## Examples + * + * var rand = $.fixture.rand; + * + * // get a random integer between 0 and 10 (inclusive) + * rand(11); + * + * // get a random number between -5 and 5 (inclusive) + * rand(-5, 6); + * + * // pick a random item from an array + * rand(["j","m","v","c"],1)[0] + * + * // pick a random number of items from an array + * rand(["j","m","v","c"]) + * + * // pick 2 items from an array + * rand(["j","m","v","c"],2) + * + * // pick between 2 and 3 items at random + * rand(["j","m","v","c"],2,3) + * + * + * @param {Array|Number} arr An array of items to select from. + * If a number is provided, a random number is returned. + * If min and max are not provided, a random number of items are selected + * from this array. + * @param {Number} [min] If only min is provided, min items + * are selected. + * @param {Number} [max] If min and max are provided, a random number of + * items between min and max (inclusive) is selected. + */ + rand : function(arr, min, max){ + if(typeof arr == 'number'){ + if(typeof min == 'number'){ + return arr+ Math.floor(Math.random() * (min - arr) ); + } else { + return Math.floor(Math.random() * arr); + } + + } + var rand = arguments.callee; + // get a random set + if(min === undefined){ + return rand(arr, rand(arr.length+1)) + } + // get a random selection of arr + var res = []; + arr = arr.slice(0); + // set max + if(!max){ + max = min; + } + //random max + max = min + Math.round( rand(max - min) ) + for(var i=0; i < max; i++){ + res.push(arr.splice( rand(arr.length), 1 )[0]) + } + return res; + }, + /** + * @hide * Use $.fixture.xhr to create an object that looks like an xhr object. - *

        Example

        + * + * ## Example + * * The following example shows how the -restCreate fixture uses xhr to return * a simulated xhr object: * @codestart @@ -510,12 +808,13 @@ steal.plugins('jquery/dom').then(function( $ ) { on : true }); /** - * @attribute delay + * @attribute $.fixture.delay + * @parent $.fixture * Sets the delay in milliseconds between an ajax request is made and * the success and complete handlers are called. This only sets * functional fixtures. By default, the delay is 200ms. * @codestart - * steal.plugins('jquery/dom/fixtures').then(function(){ + * steal('jquery/dom/fixtures').then(function(){ * $.fixture.delay = 1000; * }) * @codeend @@ -540,119 +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; - - //lets look for the fixture setting ... - overwrite(settings); - - if (!settings.fixture || ! $.fixture.on ) { - 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) ) { - if(!typeTest.test(type||"")){ - fixture = type; - type = callback; - } - callback = data; - data = null; - } - if ( jQuery.isFunction(data) ) { - fixture = type; - type = callback; - callback = data; - data = null; - } - - return jQuery.ajax({ - type: "GET", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; - - $. - /** - * Adds a fixture param. - * @param {Object} url - * @param {Object} data - * @param {Object} callback - * @param {Object} type - * @param {Object} fixture + + + /** + * @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) ) { - if(!typeTest.test(type||"")){ - fixture = type; - type = callback; - } - callback = data; - data = {}; - } - - return jQuery.ajax({ - type: "POST", - url: url, - data: data, - success: callback, - dataType: type, - fixture: fixture - }); - }; -}); \ No newline at end of file + //Expose this for fixture debugging + $.fixture.overwrites = overwrites; +}); diff --git a/dom/fixture/fixture_test.js b/dom/fixture/fixture_test.js index 0dcd0bf3..7559d1ba 100644 --- a/dom/fixture/fixture_test.js +++ b/dom/fixture/fixture_test.js @@ -1,72 +1,68 @@ -steal - .plugins("jquery/dom/fixture") //load your app - .plugins('funcunit/qunit').then(function(){ + +steal("jquery/dom/fixture", "jquery/model",'funcunit/qunit',function(){ module("jquery/dom/fixture"); + test("static fixtures", function(){ stop(); + + $.fixture("GET something", "//jquery/dom/fixture/fixtures/test.json"); + $.fixture("POST something", "//jquery/dom/fixture/fixtures/test.json"); + + $.get("something",function(data){ equals(data.sweet,"ness","$.get works"); + $.post("something",function(data){ - equals(data.sweet,"ness","$.post works"); - $.ajax({ - url: "something", - dataType: "json", - success: function( data ) { - equals(data.sweet,"ness","$.ajax works"); - start(); - }, - fixture: "//jquery/dom/fixture/fixtures/test.json" - }) - },"json","//jquery/dom/fixture/fixtures/test.json"); - },'json',"//jquery/dom/fixture/fixtures/test.json"); + start(); + },'json'); + + },'json'); }) test("dynamic fixtures",function(){ stop(); $.fixture.delay = 10; - var fix = function(){ + $.fixture("something", function(){ return [{sweet: "ness"}] - } + }) + $.get("something",function(data){ equals(data.sweet,"ness","$.get works"); - $.post("something",function(data){ - - equals(data.sweet,"ness","$.post works"); - - $.ajax({ - url: "something", - dataType: "json", - success: function( data ) { - equals(data.sweet,"ness","$.ajax works"); - start(); - }, - fixture: fix - }) - - },"json",fix); - },'json',fix); + start(); + + },'json'); }); -test("fixture function", function(){ +test("fixture function", 3, function(){ stop(); - var url = steal.root.join("jquery/dom/fixture/fixtures/foo.json"); + var url = steal.root.join("jquery/dom/fixture/fixtures/foo.json")+''; $.fixture(url,"//jquery/dom/fixture/fixtures/foobar.json" ); $.get(url,function(data){ equals(data.sweet,"ner","url passed works"); - $.fixture(url,null ); + $.fixture(url,"//jquery/dom/fixture/fixtures/test.json" ); $.get(url,function(data){ - equals(data.a,"b","removed"); - start(); + equals(data.sweet,"ness","replaced"); + + $.fixture(url, null ); + + $.get(url,function(data){ + + equals(data.a,"b","removed"); + + start(); + + },'json') + },'json') @@ -76,4 +72,261 @@ test("fixture function", function(){ }); + +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/remove.json b/dom/fixture/fixtures/remove.json new file mode 100644 index 00000000..1e152b58 --- /dev/null +++ b/dom/fixture/fixtures/remove.json @@ -0,0 +1,3 @@ +{ + "weird" : "ness" +} diff --git a/dom/form_params/form_params.html b/dom/form_params/form_params.html index 59b9a754..650dd9ec 100644 --- a/dom/form_params/form_params.html +++ b/dom/form_params/form_params.html @@ -25,16 +25,13 @@ 1 min
        5 min
        10 min
        - 10 min
        - \ No newline at end of file diff --git a/dom/form_params/form_params.js b/dom/form_params/form_params.js index 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/dom/range/qunit.html b/dom/range/qunit.html index c437ef77..019896d7 100644 --- a/dom/range/qunit.html +++ b/dom/range/qunit.html @@ -1,20 +1,17 @@ - selection QUnit Test - - + Range QUnit Test -

        selection Test Suite

        +

        Range Test Suite

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

          The Range Plugin

          
           		
          \ No newline at end of file diff --git a/dom/range/range.js b/dom/range/range.js index c8672582..7f3db253 100644 --- a/dom/range/range.js +++ b/dom/range/range.js @@ -1,7 +1,19 @@ -steal.plugins('jquery','jquery/dom/compare').then(function($){ +steal('jquery','jquery/dom/compare').then(function($){ // TODOS ... // Ad +/** + * @function jQuery.fn.range + * @parent $.Range + * + * Returns a jQuery.Range 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() @@ -14,19 +26,67 @@ reverse = function(type){ }, 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 - * A range helper for jQuery - * @param {Object} range + * @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) @@ -35,25 +95,120 @@ $.Range = function(range){ }else{ this.range = this.win.document.body.createTextRange() } - }else{ + 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; - } + } }; -$.Range.current = function(el){ - var win = getWindow(el) +/** + * @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){ - return new $.Range( win.getSelection().getRangeAt(0) ) + 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,{ + + + + +$.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. - * @param {Object} elRange + * + * 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){ @@ -79,47 +234,214 @@ $.extend($.Range.prototype,{ } 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); + 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(); }, - start : function(){ - if(this.range.startContainer){ - return { - container : this.range.startContainer, - offset : this.range.startOffset + /** + * 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{ - 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; } + + }, - end : function(){ - if(this.range.startContainer){ - return { - container : this.range.endContainer, - offset : this.range.endOffset + /** + * 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(); - var endRange = $.Range(end).select(end).collapse(); - endRange.move("END_TO_END", this); - return { - container : end, - offset : endRange.toString().length + 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(){ - return this.range.parentElement || this.range.commonAncestorContainer + if(this.range.commonAncestorContainer){ + return this.range.commonAncestorContainer; + } else { + + var parentElement = this.range.parentElement(), + range = this.range; + + // IE's parentElement will always give an element, we want text ranges + iterate(parentElement.childNodes, function(txtNode){ + if($.Range(txtNode).range.inRange( range ) ){ + // swap out the parentElement + parentElement = txtNode; + return false; + } + }); + + return parentElement; + } + }, + /** + * Returns the bounding rectangle of this range. + * + * @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; } }); @@ -130,13 +452,36 @@ $.extend($.Range.prototype,{ /** * @function compare - * Compares one range to another range. This is different from the spec b/c the spec is confusing. + * Compares one range to another range. + * + * ## Example + * + * // compare the highlight element's start position + * // to the start of the current range + * $('#highlight') + * .range() + * .compare('START_TO_START', $.Range.current()) + * * - * source.compare("START_TO_END", toRange); + * + * @param {Object} type Specifies the boundry of the + * range and the compareRange to compare. * - * This returns -1 if source's start is before toRange's end. - * @param {Object} type - * @param {Object} range + * - 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){ @@ -148,9 +493,24 @@ $.extend($.Range.prototype,{ /** * @function move - * Move the endpoints of a range - * @param {Object} type - * @param {Object} range + * Move the endpoints of a range relative to another range. + * + * // Move the current selection's end to the + * // end of the #highlight element + * $.Range.current().move('END_TO_END', + * $('#highlight').range() ) + * + * + * @param {String} type a string indicating the ranges boundary point + * to move to which referenceRange boundary point where: + * + * - START\_TO\_START - the start of the range moves to the start of referenceRange + * - START\_TO\_END - the start of the range move to the end of referenceRange + * - END\_TO\_END - the end of the range moves to the end of referenceRange + * - END\_TO\_START - the end of the range moves to the start of referenceRange + * + * @param {jQuery.Range} referenceRange + * @return {jQuery.Range} the original range for chaining */ fn.move = range.setStart ? function(type, range){ @@ -180,26 +540,137 @@ $.extend($.Range.prototype,{ var cloneFunc = range.cloneRange ? "cloneRange" : "duplicate", selectFunc = range.selectNodeContents ? "selectNodeContents" : "moveToElementText"; + fn. /** - * Clones the range and returns a new $.Range object. + * Clones the range and returns a new $.Range + * object. + * + * @return {jQuery.Range} returns the range as a $.Range. */ - fn.clone = function(){ + clone = function(){ return $.Range( this.range[cloneFunc]() ); }; + fn. /** - * Selects an element with this range - * @param {HTMLElement} el + * @function + * Selects an element with this range. If nothing + * is provided, makes the current + * range appear as if the user has selected it. + * + * This works with text nodes. + * + * @param {HTMLElement} [el] + * @return {jQuery.Range} the range for chaining. */ - fn.select = function(el){ - this.range[selectFunc](el); + select = range.selectNodeContents ? function(el){ + if(!el){ + this.window().getSelection().addRange(this.range); + }else { + this.range.selectNodeContents(el); + } + return this; + } : function(el){ + if(!el){ + this.range.select() + } else if(el.nodeType === 3){ + //select this node in the element ... + var parent = el.parentNode, + start = 0, + end; + iterate(parent.childNodes, function(txtNode){ + if(txtNode === el){ + end = start + txtNode.nodeValue.length; + return false; + } else { + start = start + txtNode.nodeValue.length + } + }) + this.range.moveToElementText(parent); + + this.range.moveEnd('character', end - this.range.text.length) + this.range.moveStart('character', start); + } else { + this.range.moveToElementText(el); + } return this; }; })(); +// 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 index e69de29b..e55246e4 100644 --- a/dom/range/range_test.js +++ b/dom/range/range_test.js @@ -0,0 +1,197 @@ +steal("funcunit/qunit", "jquery/dom/range", "jquery/dom/selection").then(function(){ + +module("jquery/dom/range"); + +test("basic range", function(){ + $("#qunit-test-area") + .html("

          0123456789

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

          0123456789

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

          0123456789

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

          0123456789

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

          0123456789

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

          Jquery.Dom.Route Test Suite

          +

          +
          +

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

            $.Route Demo

            + +
            +
            +
            + 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 index c437ef77..b99867fd 100644 --- a/dom/selection/qunit.html +++ b/dom/selection/qunit.html @@ -5,7 +5,7 @@ - + diff --git a/dom/selection/scripts/build.html b/dom/selection/scripts/build.html deleted file mode 100644 index 0a3e73f9..00000000 --- a/dom/selection/scripts/build.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - selection Build Page - - -

            selection Build Page

            -

            This is a dummy page that loads your app so steal can - get all the files. -

            -

            If you built your app - to depend on HTML in the page before DOMContent loaded or - onload, you can add the HTML here, or you can change the - build.js script to point to a better html file. -

            - - - \ No newline at end of file diff --git a/dom/selection/scripts/build.js b/dom/selection/scripts/build.js deleted file mode 100644 index 4ae102c6..00000000 --- a/dom/selection/scripts/build.js +++ /dev/null @@ -1,6 +0,0 @@ -//steal/js jquery/dom/selection/scripts/compress.js - -load("steal/rhino/steal.js"); -steal.plugins('steal/build','steal/build/scripts','steal/build/styles',function(){ - steal.build('jquery/dom/selection/scripts/build.html',{to: 'jquery/dom/selection'}); -}); diff --git a/dom/selection/scripts/clean.js b/dom/selection/scripts/clean.js deleted file mode 100644 index e42fce23..00000000 --- a/dom/selection/scripts/clean.js +++ /dev/null @@ -1,17 +0,0 @@ -//steal/js jquery/dom/selection/scripts/compress.js - -load("steal/rhino/steal.js"); -steal.plugins('steal/clean',function(){ - steal.clean('jquery/dom/selection/selection.html',{ - indent_size: 1, - indent_char: '\t', - jslint : false, - ignore: /jquery\/jquery.js/, - predefined: { - steal: true, - jQuery: true, - $ : true, - window : true - } - }); -}); diff --git a/dom/selection/scripts/docs.js b/dom/selection/scripts/docs.js deleted file mode 100644 index cd647e01..00000000 --- a/dom/selection/scripts/docs.js +++ /dev/null @@ -1,6 +0,0 @@ -//js jquery/dom/selection/scripts/doc.js - -load('steal/rhino/steal.js'); -steal.plugins("documentjs").then(function(){ - DocumentJS('jquery/dom/selection/selection.html'); -}); \ No newline at end of file diff --git a/dom/selection/selection.html b/dom/selection/selection.html index 20840c29..536b16e5 100644 --- a/dom/selection/selection.html +++ b/dom/selection/selection.html @@ -11,68 +11,46 @@ - - - - - - - - - - - - - - - - - -
            Select Textarea
            Select Input
            Select Within One Element

            0123456789

            Select Across Multiple Elements
            012
            345
            - - -

            Hello World! how are you today?

            -

            I am good, thank you.

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

            0123456789

            + + + Select Across Multiple Elements +
            012
            345
            + + + src='../../../steal/steal.js'> \ No newline at end of file diff --git a/dom/selection/selection.js b/dom/selection/selection.js index 702961f9..45cc2280 100644 --- a/dom/selection/selection.js +++ b/dom/selection/selection.js @@ -1,4 +1,4 @@ -steal.plugins('jquery','jquery/dom/range').then(function($){ +steal('jquery','jquery/dom/range').then(function($){ var convertType = function(type){ return type.replace(/([a-z])([a-z]+)/gi, function(all,first, next){ return first+next.toLowerCase() @@ -181,9 +181,51 @@ getCharElement = function( elems , range, len ) { return len; }; /** - * Gets or sets the current text selection - * @param {Object} start - * @param {Object} end + * @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){ 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/selection/test/funcunit/funcunit.js b/dom/selection/test/funcunit/funcunit.js deleted file mode 100644 index 9c1125d6..00000000 --- a/dom/selection/test/funcunit/funcunit.js +++ /dev/null @@ -1,3 +0,0 @@ -steal - .plugins("funcunit") - .then("selection_test"); \ No newline at end of file diff --git a/dom/selection/test/funcunit/selection_test.js b/dom/selection/test/funcunit/selection_test.js deleted file mode 100644 index 1c70ed57..00000000 --- a/dom/selection/test/funcunit/selection_test.js +++ /dev/null @@ -1,9 +0,0 @@ -module("selection test", { - setup: function(){ - S.open("//jquery/dom/selection/selection.html"); - } -}); - -test("Copy Test", function(){ - equals(S("h1").text(), "Welcome to JavaScriptMVC 3.0!","welcome text"); -}); \ No newline at end of file diff --git a/dom/selection/test/qunit/qunit.js b/dom/selection/test/qunit/qunit.js deleted file mode 100644 index 91c01a46..00000000 --- a/dom/selection/test/qunit/qunit.js +++ /dev/null @@ -1,3 +0,0 @@ -steal - .plugins("funcunit/qunit", "jquery/dom/selection") - .then("selection_test"); \ No newline at end of file diff --git a/dom/within/within.js b/dom/within/within.js index 40eced62..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,12 +11,20 @@ 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, useOffsetCache) { +$.fn.within= function(left, top, useOffsetCache) { var ret = [] this.each(function(){ var q = jQuery(this); @@ -28,7 +36,7 @@ $.fn.within= function(x, y, useOffsetCache) { jQuery.data(this,"offsetCache") || jQuery.data(this,"offsetCache", q.offset()) : q.offset(); - var res = withinBox(x, y, offset.left, offset.top, + var res = withinBox(left, top, offset.left, offset.top, this.offsetWidth, this.offsetHeight ); if (res) { @@ -36,12 +44,13 @@ $.fn.within= function(x, y, useOffsetCache) { } }); - 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 @@ -56,7 +65,11 @@ $.fn.withinBox = function(left, top, width, height, cache){ if(this == document.documentElement) return this.ret.push(this); - var offset = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + var offset = cache ? + jQuery.data(this,"offset") || + jQuery.data(this,"offset", q.offset()) : + q.offset(); + var ew = q.width(), eh = q.height(); diff --git a/download/download.html b/download/download.html index 89b112e1..98535d6c 100644 --- a/download/download.html +++ b/download/download.html @@ -45,26 +45,6 @@

            Controller

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

            Model

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

            Model

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