diff --git a/README b/README index 12846a17..19b1dfba 100644 --- a/README +++ b/README @@ -10,15 +10,15 @@ 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..b19adbda 100644 --- a/build.js +++ b/build.js @@ -1,13 +1,13 @@ // load('jquery/build.js') -load('steal/rhino/steal.js') +load('steal/rhino/rhino.js') -var i, fileName, cmd, +var i, fileName, cmd, plugins = [ - "class" , + "class" , "controller", { - plugin: "controller/subscribe", + plugin: "controller/subscribe", exclude: ["jquery/controller/controller.js", "jquery/class/class.js", "jquery/lang/lang.js", @@ -16,17 +16,19 @@ var i, fileName, cmd, "event/default", "event/destroyed", "event/drag", + "event/pause", + "event/resize", { - plugin: "event/drag/limit", + plugin: "event/drag/limit", exclude: ["jquery/lang/vector/vector.js", "jquery/event/livehack/livehack.js", "jquery/event/drag/drag.js"]}, { - plugin: "event/drag/scroll", + plugin: "event/drag/scroll", exclude: ["jquery/dom/within/within.js", "jquery/dom/compare/compare.js", "jquery/event/drop/drop.js","jquery/lang/vector/vector.js", "jquery/event/livehack/livehack.js", "jquery/event/drag/drag.js"]}, { plugin: "event/drop", exclude: ["jquery/lang/vector/vector.js", "jquery/event/livehack/livehack.js", "jquery/event/drag/drag.js"]}, "event/hover", - "view/ejs", + "view/ejs", "dom/closest", "dom/compare", { @@ -35,17 +37,9 @@ var i, fileName, cmd, }, "dom/fixture", "dom/form_params", - "dom/within", + "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< plugins.length; i++) { } fileName = fileName || "jquery." + plugin.replace(/\//g, ".").replace(/dom\./, "").replace(/\_/, "") + ".js"; fileDest = "jquery/dist/" + fileName - // compress + // compress var outBaos = new java.io.ByteArrayOutputStream(); var output = new java.io.PrintStream(outBaos); runCommand("java", "-jar", "steal/build/scripts/compiler.jar", "--compilation_level", "SIMPLE_OPTIMIZATIONS", "--warning_level", "QUIET", "--js", fileDest, { output: output }); - + var minFileDest = fileDest.replace(".js", ".min.js") new steal.File(minFileDest).save(outBaos.toString()); print("***" + fileName + " pluginified and compressed") diff --git a/buildAll.js b/buildAll.js index 06cbf45e..ba130056 100644 --- a/buildAll.js +++ b/buildAll.js @@ -1,39 +1,86 @@ -// load('jquery/build.js') +// load('jquery/buildAll.js') -load('steal/rhino/steal.js') +load('steal/rhino/rhino.js') // load every plugin in a single app // get dependency graph // generate single script -steal.plugins('steal/build/pluginify','steal/build/apps','steal/build/scripts').then( function(s){ +steal('steal/build/pluginify','steal/build/apps','steal/build/scripts').then( function(s){ var ignore = /\.\w+|test|generate|dist|qunit|fixtures|pages/ - - var plugins = []; - + + var plugins = [], + /** + * {"path/to/file.js" : ["file2/thing.js", ...]} + */ + files = {}; + s.File('jquery').contents(function( name, type, current ) { if (type !== 'file' && !ignore.test(name)) { var folder = current+"/"+name; - print(folder); - plugins.push(folder); - steal.File(folder).contents(arguments.callee, folder) + if(readFile(folder+"/"+name+".js")){ + print(folder); + plugins.push(folder); + steal.File(folder).contents(arguments.callee, folder) + } + //steal.File(path + "/" + (current ? current + "/" : "") + name).contents(arguments.callee, (current ? current + "/" : "") + name); } },"jquery"); - + // tell it to load all plugins into this page - rhinoLoader = { - callback: function( s ) { - s.plugins.apply(s,plugins); - } - }; - - steal.win().build_in_progress = true; + + + //steal.win().build_in_progress = true; print(" LOADING APP ") + steal.build.open('steal/rhino/blank.html', { + startFiles: plugins + }, function(opener){ + + opener.each('js', function(options, text, stl){ + print(options.rootSrc) + var dependencies = files[options.rootSrc] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.options.rootSrc !== "jquery/jquery.js") { + dependencies.push(depend.options.rootSrc); + } + } + } + }) + + s.File("jquery/dist/standalone").mkdirs(); + s.File("jquery/dist/standalone/dependencies.json").save($.toJSON(files)); + //get each file ... + print("Creating jquery/dist/standalone/") + var compressor = s.build.builders.scripts.compressors[ "localClosure"]() + for(var path in files){ + if(path == "jquery/jquery.js"){ + continue; + } + var content = readFile(path); + var funcContent = s.build.pluginify.getFunction(content); + if(typeof funcContent == "undefined"){ + content = ""; + } else { + content = "("+s.build.pluginify.getFunction(content)+")(Foundry);"; + } + 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,24 +88,24 @@ 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); } - - + + } } }, all = function(c){ for(var i =0; i < steals.length; i++){ var pSteal =steals[i]; - + if(!pSteal.func){ c(pSteal) } - + } - + }; print(" LOADED, GETTING DEPENDS"); all(function(stl){ @@ -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/buildModule.js b/buildModule.js new file mode 100644 index 00000000..5c813136 --- /dev/null +++ b/buildModule.js @@ -0,0 +1,179 @@ +// load('jquery/buildAll.js') + +load('steal/rhino/rhino.js') + + +// load every plugin in a single app +// get dependency graph +// generate single script + +steal('steal/build/pluginify','steal/build/apps','steal/build/scripts').then( function(s){ + var ignore = /\.\w+|test|generate|dist|qunit|fixtures|pages/ + + var plugins = [], + /** + * {"path/to/file.js" : ["file2/thing.js", ...]} + */ + files = {}; + + s.File('jquery').contents(function( name, type, current ) { + if (type !== 'file' && !ignore.test(name)) { + var folder = current+"/"+name; + if(readFile(folder+"/"+name+".js")){ + print(folder); + plugins.push(folder); + steal.File(folder).contents(arguments.callee, folder) + } + + //steal.File(path + "/" + (current ? current + "/" : "") + name).contents(arguments.callee, (current ? current + "/" : "") + name); + } + },"jquery"); + + // tell it to load all plugins into this page + + + //steal.win().build_in_progress = true; + print(" LOADING APP ") + steal.build.open('steal/rhino/blank.html', { + startFiles: plugins + }, function(opener){ + + opener.each('js', function(options, text, stl){ + print(options.rootSrc) + var dependencies = files[options.rootSrc] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.options.rootSrc !== "jquery/jquery.js") { + dependencies.push(depend.options.rootSrc); + } + } + } + }); + + var toModuleName = function(path) { + return path.replace(/\/\w+\.js/,"").replace(/\//g,".").replace("jquery.",""); + }; + + var excludeModule = ""; + + s.File("jquery/dist/modules/mvc").mkdirs(); + + //get each file ... + print("Creating jquery/dist/modules/") + + for (var path in files) { + + if (path=="jquery/jquery.js") { + continue; + } + + var name = toModuleName(path), + content = readFile(path), + deps = files[path], + exports = s.build.pluginify.getFunctionBody(content); + + var module = "", + moduleName = "mvc/" + name; + + // Do not build empty scripts. + if(typeof exports == "undefined") { + excludeModule += moduleName + ","; + continue; + } + + // Translate dependencies + var moduleDeps = []; + for (var i in deps) { + var moduleDep = "mvc/" + toModuleName(deps[i]); + + if (moduleDep == moduleName || excludeModule.indexOf(moduleDep) > -1) { + continue; + } + + moduleDeps.push(moduleDep); + } + + var moduleFile = "jquery/dist/modules/"+moduleName+".js", + rawModuleFile = "jquery/dist/modules/"+moduleName+".js.raw" + + s.File(rawModuleFile).save(exports); + + var args = ["-n", moduleName]; + + if (moduleDeps.length > 0) { + args.push("-d") + args.push(moduleDeps.join(",")); + } + + args.push(rawModuleFile); + + var result = { + args: args, + input: "", + output: "", + err: "" + }; + + runCommand("../../build/modularize", result); + + module = s.build.builders.scripts.clean(result.output); + + print(" "+moduleName+""); + + s.File("jquery/dist/modules/"+moduleName+".js").save(module); + } + + }) + + /* + var pageSteal = steal.build.open("steal/rhino/empty.html").steal, + steals = pageSteal.total, + + files = {}, + depends = function(stl, steals){ + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if(!steals[depend.path]){ + steals[depend.path] = true; + print("123 " + depend.path); + //depends(depend, steals); + } + + + } + } + }, + all = function(c){ + for(var i =0; i < steals.length; i++){ + var pSteal =steals[i]; + + if(!pSteal.func){ + c(pSteal) + } + + } + + }; + print(" LOADED, GETTING DEPENDS"); + all(function(stl){ + files[stl.path] = stl; + }) + all(function(stl){ + print(stl.path) + var dependencies = files[stl.path] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.path !== "jquery/jquery.js") { + dependencies.push(depend.path); + } + } + } + })*/ + + + + +}) diff --git a/buildScripts.js b/buildScripts.js new file mode 100644 index 00000000..681de0e0 --- /dev/null +++ b/buildScripts.js @@ -0,0 +1,179 @@ +// load('jquery/buildAll.js') + +load('steal/rhino/rhino.js') + + +// load every plugin in a single app +// get dependency graph +// generate single script + +steal('steal/build/pluginify','steal/build/apps','steal/build/scripts').then( function(s){ + var ignore = /\.\w+|test|generate|dist|qunit|fixtures|pages/ + + var plugins = [], + /** + * {"path/to/file.js" : ["file2/thing.js", ...]} + */ + files = {}; + + s.File('jquery').contents(function( name, type, current ) { + if (type !== 'file' && !ignore.test(name)) { + var folder = current+"/"+name; + if(readFile(folder+"/"+name+".js")){ + print(folder); + plugins.push(folder); + steal.File(folder).contents(arguments.callee, folder) + } + + //steal.File(path + "/" + (current ? current + "/" : "") + name).contents(arguments.callee, (current ? current + "/" : "") + name); + } + },"jquery"); + + // tell it to load all plugins into this page + + + //steal.win().build_in_progress = true; + print(" LOADING APP ") + steal.build.open('steal/rhino/blank.html', { + startFiles: plugins + }, function(opener){ + + opener.each('js', function(options, text, stl){ + print(options.rootSrc) + var dependencies = files[options.rootSrc] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.options.rootSrc !== "jquery/jquery.js") { + dependencies.push(depend.options.rootSrc); + } + } + } + }); + + var toModuleName = function(path) { + return path.replace(/\/\w+\.js/,"").replace(/\//g,".").replace("jquery.",""); + }; + + var excludeModule = ""; + + s.File("jquery/dist/modules/mvc").mkdirs(); + + //get each file ... + print("Creating jquery/dist/modules/") + + for (var path in files) { + + if (path=="jquery/jquery.js") { + continue; + } + + var name = toModuleName(path), + content = readFile(path), + deps = files[path], + exports = s.build.pluginify.getFunctionBody(content); + + var module = "", + moduleName = "mvc/" + name; + + // Do not build empty scripts. + if(typeof exports == "undefined") { + excludeModule += moduleName + ","; + continue; + } + + // Translate dependencies + var moduleDeps = []; + for (var i in deps) { + var moduleDep = "mvc/" + toModuleName(deps[i]); + + if (moduleDep == moduleName || excludeModule.indexOf(moduleDep) > -1) { + continue; + } + + moduleDeps.push(moduleDep); + } + + // var moduleFile = "jquery/dist/modules/"+moduleName+".js", + // rawModuleFile = "jquery/dist/modules/"+moduleName+".js.raw" + + // s.File(rawModuleFile).save(exports); + + // var args = ["-n", moduleName]; + + // if (moduleDeps.length > 0) { + // args.push("-d") + // args.push(moduleDeps.join(",")); + // } + + // args.push(rawModuleFile); + + // var result = { + // args: args, + // input: "", + // output: "", + // err: "" + // }; + + // runCommand("../../build/modularize", result); + + module = s.build.builders.scripts.clean("(function(){" + exports + "})();"); + + print(" "+moduleName+""); + + s.File("jquery/dist/modules/"+moduleName+".js").save(module); + } + + }) + + /* + var pageSteal = steal.build.open("steal/rhino/empty.html").steal, + steals = pageSteal.total, + + files = {}, + depends = function(stl, steals){ + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if(!steals[depend.path]){ + steals[depend.path] = true; + print("123 " + depend.path); + //depends(depend, steals); + } + + + } + } + }, + all = function(c){ + for(var i =0; i < steals.length; i++){ + var pSteal =steals[i]; + + if(!pSteal.func){ + c(pSteal) + } + + } + + }; + print(" LOADED, GETTING DEPENDS"); + all(function(stl){ + files[stl.path] = stl; + }) + all(function(stl){ + print(stl.path) + var dependencies = files[stl.path] = []; + if(stl.dependencies){ + for (var d = 0; d < stl.dependencies.length; d++) { + var depend = stl.dependencies[d]; + if (depend.path !== "jquery/jquery.js") { + dependencies.push(depend.path); + } + } + } + })*/ + + + + +}) 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..03048209 100644 --- a/class/class.js +++ b/class/class.js @@ -1,30 +1,38 @@ -//jQuery.Class +//jQuery.Class // 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 ) { // Check if we're overwriting an existing function - addTo[name] = isFunction(newProps[name]) && - isFunction(oldProps[name]) && + addTo[name] = isFunction(newProps[name]) && + isFunction(oldProps[name]) && fnTest.test(newProps[name]) ? (function( name, fn ) { return function() { var tmp = this._super, @@ -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 + * @description Easy inheritance in JavaScript. + * * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between - * jQuery's functional programming style and Object Oriented Programming. - * It is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] + * 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 + * a class's __static__ and __prototype__ properties. * - * //PROTOTYPE - * myclass = new MyClass() - * myclass.prototypeMethod() //instance method - * @codeend - *

A static (or class) property is on the Class constructor + * //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;} - * },{}) * - * First.extend("Second",{ - * staticMethod: function() { return this._super()+1;} - * },{}) + * $.Class("First", + * { + * staticMethod: function() { return 1;} + * },{}) * - * Second.staticMethod() // -> 2 - * @codeend - *

Namespaces

- *

Namespaces are a good idea! We encourage you to namespace all of your code. + * First("Second",{ + * staticMethod: function() { return this._super()+1;} + * },{}) + * + * Second.staticMethod() // -> 2 + * + * ## Namespaces + * + * Namespaces are a good idea! We encourage you to namespace all of your code. * It makes it possible to drop your code into another app without problems. * Making a namespaced class is easy: - *

- * @codestart - * $.Class.extend("MyNamespace.MyClass",{},{}); * - * new MyNamespace.MyClass() - * @codeend + * + * $.Class("MyNamespace.MyClass",{},{}); + * + * 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,78 +232,119 @@ 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 functions are called after setup functions. + * ### Init + * + * Init functions are called after setup functions. * Typically, they receive the same arguments * as their preceding setup function. The Foo class's init method * gets called in the following example: - *

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

Callbacks

- *

Similar to jQuery's proxy method, Class provides a - * [jQuery.Class.static.callback callback] + * + * $.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() { @@ -293,16 +354,16 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { }; /* @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; @@ -341,49 +403,60 @@ steal.plugins("jquery","jquery/lang").then(function( $ ) { * },{}); * MyClass.getData(showDataFunc); * @codeend - * @param {String|Array} fname If a string, it represents the function to be called. + * @param {String|Array} fname If a string, it represents the function to be called. * If it is an array, it will call each function in order and pass the return value of the prior function to the * next function. * @return {Function} the callback function. */ - callback: function( funcs ) { + 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, + 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,122 +596,181 @@ 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 if ( fullName ) { + var root; + if (klass && klass.root) { + root = klass.root; + if ($.isString(root)) { + root = getObject(root, window, true); + } + } + var parts = fullName.split(/\./), shortName = parts.pop(), - current = clss.getObject(parts.join('.'), window, true), + current = getObject(parts.join('.'), root || 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 + + // !-- FOUNDRY HACK --! // + // Inherit any existing properties from the namespace where Class is being assigned to. + extend(true, Class, current[shortName]); + 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 - * - *
PRO TIP: + * 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 * setup code that extending classes don't have to remember to call _super to * run. *
- * - * @return {Array|undefined} If an array is return, [jQuery.Class.prototype.init] is + * + * 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}, {}); - * - * // a new instance of myClass - * var mc1 = new MyClass(); - * - * // - * mc1.Class.classProperty = false; - * - * // creates a new MyClass - * var mc2 = new mc.Class(); - * @codeend - * Getting static properties via the Class property, such as it's - * [jQuery.Class.static.fullName fullName] is very common. + * @attribute constructor + * + * A reference to the Class (or constructor function). This allows you to access + * a class's static properties from an instance. + * + * ### Quick Example + * + * // a class with a static property + * $.Class("MyClass", {staticProperty : true}, {}); + * + * // a new instance of myClass + * var mc1 = new MyClass(); + * + * // read the static property from the instance: + * mc1.constructor.staticProperty //-> true + * + * Getting static properties with the constructor property, like + * [jQuery.Class.static.fullName fullName], is very common. + * */ } @@ -624,18 +780,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. + * @param {String|Array} fname If a string, it represents the function to be called. * If it is an array, it will call each function in order and pass the return value of the prior function to the * next function. * @return {Function} the callback function */ - callback = clss.callback; + proxy = clss.proxy; -})(); \ No newline at end of file +}); diff --git a/class/class_test.js b/class/class_test.js index 0447e74c..ec95179c 100644 --- a/class/class_test.js +++ b/class/class_test.js @@ -1,8 +1,6 @@ -//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(function(){ +steal("jquery/class") //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/class"); test("Creating", function(){ @@ -183,4 +181,25 @@ test("Creating without extend", function(){ }); new Foo().dude(true); }) -}) \ No newline at end of file + + +/* 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/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..30ab9d4c 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -1,22 +1,26 @@ -steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(function( $ ) { +steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function($) { + // ------- HELPER FUNCTIONS ------ - // ------- helpers ------ // Binds an element, returns a function that unbinds - var bind = function( el, ev, callback ) { + var bind = function( el, ev, callback, eventData ) { 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); + // !-- FOUNDRY HACK --! // + // Support for passing event data + if (eventData) { + binder.bind(ev, eventData, wrappedCallback || callback); + } else { + binder.bind(ev, wrappedCallback || callback); + } // if ev name has >, change the name and bind // in the wrapped callback, check that the element matches the actual element return function() { @@ -25,24 +29,63 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func }; }, makeArray = $.makeArray, + isArray = $.isArray, isFunction = $.isFunction, + isString = $.isString, + extend = $.extend, + Str = $.String, + each = $.each, + getObject = Str.getObject, + + 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); + delegate = function( el, selector, ev, callback, eventData ) { + + // !-- FOUNDRY HACK --! // + // Make event delegation work with direct child selector + if ( selector.indexOf(">") === 0 ) { + selector = (el.data("directSelector") + " " || "") + selector; + } + + var binder = el.delegate && el.undelegate ? el : $(isFunction(el) ? [el] : el) + + // !-- FOUNDRY HACK --! // + // Support for passing event data + if (eventData) { + binder.delegate(selector, ev, eventData, callback); + } else { + 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; }; }, - binder = function( el, ev, callback, selector ) { - return selector ? delegate(el, selector, ev, callback) : bind(el, ev, callback); + + // calls bind or unbind depending if there is a selector + binder = function( el, ev, callback, selector, eventData ) { + // !-- FOUNDRY HACK --! // + // Support for passing event data + return selector ? delegate(el, selector, ev, callback, eventData) : bind(el, ev, callback, eventData); }, - /** - * 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; + + // !-- FOUNDRY HACK --! // + // Support for passing event data + if (isArray(method) && isFunction(method[1])) { + method = method[1]; + } + 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,73 +94,88 @@ 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($.globalNamespace + ".", "").replace(dotsReg, '_').replace(controllersReg, "")); }, // checks if it looks like an action - actionMatcher = /[^\w]/, - // gets jus the event - eventCleaner = /^(>?default\.)|(>)/, + // actionMatcher = /[^\w]/, + + // !-- FOUNDRY HACK --! // + // Prevent inclusion of single word property name that starts with a symbol, e.g. $family from MooTools. + // This is coming from an environment where jQuery and MooTools may coexist. + actionMatcher = /^\S(.*)\s(.*)/, + // handles parameterized action names parameterReplacer = /\{([^\}]+)\}/g, + controllerReplacer = /\{([^\.]+[\.][^\.]+)\}/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 - * - * 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. - * - * 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 + * @inherits jQuery.Class + * @description jQuery widget factory. + * + * jQuery.Controller helps create organized, memory-leak free, rapidly performing + * jQuery widgets. Its extreme flexibility allows it to serve as both + * a traditional View and a traditional Controller. + * + * This means it is used to + * create things like tabs, grids, and contextmenus as well as + * organizing them into higher-order business rules. + * + * Controllers make your code deterministic, reusable, organized and can tear themselves + * down auto-magically. Read about [http://jupiterjs.com/news/writing-the-perfect-jquery-plugin + * the theory behind controller] and * a [http://jupiterjs.com/news/organize-jquery-widgets-with-jquery-controller walkthrough of its features] - * on Jupiter's blog. - * - * + * 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 * to build simple widgets, like a slider, or organize multiple * widgets into something greater. - * - * To understand how to use Controller, you need to understand + * + * To understand how to use Controller, you need to understand * the typical lifecycle of a jQuery widget and how that maps to * controller's functionality: - * + * * ### A controller class is created. - * + * * $.Controller("MyWidget", * { * defaults : { @@ -125,62 +183,64 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * } * }, * { - * init : function(rawEl, rawOptions){ + * init : function(rawEl, rawOptions){ * this.element.append( * "
"+this.options.message+"
" * ); * }, - * "div click" : function(div, ev){ + * "div click" : function(div, ev){ * div.remove(); - * } - * }) - * - * 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 - * + * * $('.thing').my_widget(options) // calls new MyWidget(el, options) - * - * This calls new MyWidget(el, options) on - * each '.thing' element. - * + * + * This calls new MyWidget(el, options) on + * each '.thing' element. + * * When a new [jQuery.Class Class] instance is created, it calls the class's * prototype setup and init methods. Controller's [jQuery.Controller.prototype.setup setup] * method: - * + * * - Sets [jQuery.Controller.prototype.element this.element] and adds the controller's name to element's className. * - Merges passed in options with defaults object and sets it as [jQuery.Controller.prototype.options this.options] * - Saves a reference to the controller in $.data. * - [jquery.controller.listening Binds all event handler methods]. - * - * + * + * * ### The controller responds to events - * + * * Typically, Controller event handlers are automatically bound. However, there are * multiple ways to [jquery.controller.listening listen to events] with a controller. - * - * Once an event does happen, the callback function is always called with 'this' + * + * Once an event does happen, the callback function is always called with 'this' * referencing the controller instance. This makes it easy to use helper functions and * save state on the controller. - * - * + * + * * ### The widget is destroyed - * - * If the element is removed from the page, the + * + * If the element is removed from the page, the * controller's [jQuery.Controller.prototype.destroy] method is called. * This is a great place to put any additional teardown functionality. - * + * * You can also teardown a controller programatically like: - * + * * $('.thing').my_widget('destroy'); - * + * * ## Todos Example - * - * Lets look at a very basic example - + * + * Lets look at a very basic example - * a list of todos and a button you want to click to create a new todo. * Your HTML might look like: - * + * * @codestart html * <div id='todos'> * <ol> @@ -191,201 +251,327 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * <a class="create">Create</a> * </div> * @codeend - * + * * To add a mousover effect and create todos, your controller might look like: - * - * @codestart - * $.Controller.extend('Todos',{ - * ".todo mouseover" : function( el, ev ) { - * el.css("backgroundColor","red") - * }, - * ".todo mouseout" : function( el, ev ) { - * el.css("backgroundColor","") - * }, - * ".create click" : function() { - * this.find("ol").append("<li class='todo'>New Todo</li>"); - * } - * }) - * @codeend - * + * + * $.Controller('Todos',{ + * ".todo mouseover" : function( el, ev ) { + * el.css("backgroundColor","red") + * }, + * ".todo mouseout" : function( el, ev ) { + * el.css("backgroundColor","") + * }, + * ".create click" : function() { + * this.find("ol").append("
  • New Todo
  • "); + * } + * }) + * * Now that you've created the controller class, you've must attach the event handlers on the '#todos' div by * creating [jQuery.Controller.prototype.setup|a new controller instance]. There are 2 ways of doing this. - * + * * @codestart * //1. Create a new controller directly: * new Todos($('#todos')); * //2. Use jQuery function * $('#todos').todos(); * @codeend - * + * * ## Controller Initialization - * - * It can be extremely useful to add an init method with + * + * It can be extremely useful to add an init method with * setup functionality for your widget. - * + * * In the following example, I create a controller that when created, will put a message as the content of the element: - * - * @codestart - * $.Controller.extend("SpecialController", - * { - * init: function( el, message ) { - * this.element.html(message) - * } - * }) - * $(".special").special("Hello World") - * @codeend - * + * + * $.Controller("SpecialController", + * { + * init: function( el, message ) { + * this.element.html(message) + * } + * }) + * $(".special").special("Hello World") + * * ## Removing Controllers - * + * * Controller removal is built into jQuery. So to remove a controller, you just have to remove its element: - * + * * @codestart * $(".special_controller").remove() * $("#containsControllers").html("") * @codeend - * + * * It's important to note that if you use raw DOM methods (innerHTML, removeChild), the controllers won't be destroyed. - * + * * If you just want to remove controller functionality, call destroy on the controller instance: - * + * * @codestart * $(".special_controller").controller().destroy() * @codeend - * + * * ## Accessing Controllers - * - * Often you need to get a reference to a controller, there are a few ways of doing that. For the + * + * Often you need to get a reference to a controller, there are a few ways of doing that. For the * following example, we assume there are 2 elements with className="special". - * + * * @codestart * //creates 2 foo controllers * $(".special").foo() - * + * * //creates 2 bar controllers * $(".special").bar() - * + * * //gets all controllers on all elements: * $(".special").controllers() //-> [foo, bar, foo, bar] - * + * * //gets only foo controllers * $(".special").controllers(FooController) //-> [foo, foo] - * + * * //gets all bar controllers * $(".special").controllers(BarController) //-> [bar, bar] - * + * * //gets first controller * $(".special").controller() //-> foo - * + * * //gets foo controller via data * $(".special").data("controllers")["FooController"] //-> foo * @codeend - * + * * ## Calling methods on Controllers - * + * * Once you have a reference to an element, you can call methods on it. However, Controller has * a few shortcuts: - * + * * @codestart * //creates foo controller * $(".special").foo({name: "value"}) - * + * * //calls FooController.prototype.update * $(".special").foo({name: "value2"}) - * + * * //calls FooController.prototype.bar * $(".special").foo("bar","something I want to pass") * @codeend + * + * These methods let you call one controller from another controller. + * */ - $.Class.extend("jQuery.Controller", - /** + var controllerRoot = $.globalNamespace + ".Controller"; + + $.Controller = function(name) { + + // !-- FOUNDRY HACK --! // + // By default, all controllers are created under the + // $.Controller root namespace. + var args = makeArray(arguments), + _static = { + root: controllerRoot + }, + _prototype; + + if (args.length > 2) { + // Namespace can be overriden + _static = $.extend(_static, args[1]); + _prototype = args[2]; + } else { + _prototype = args[1]; + } + + if (_static.namespace) { + name = _static.namespace + "." + name; + } + + return $.Controller.Class(name, _static, _prototype); + } + + var controllerClass = controllerRoot + ".Class"; + + $.Class(controllerClass, + /** * @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(baseClass, name) { + + // 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" ) { + if (!this.shortName || this.fullName == controllerClass) { return; } + + // !-- FOUNDRY HACK --! // + // Added support for expandable elements. + var elements = this.elements || [], + i = 0, + defaults = this.defaults; + + while (element = elements[i++]) { + + var start = element.indexOf("{"), + end = element.indexOf("}"), + length = element.length, + prefix = element.slice(0, start), + suffix = element.slice(end + 1), + names = element.slice(start + 1, end).split("|"), + j = 0; + + // "^width [data-eb{label|slider}]" turns into + // widthLabel => [data-eb-label] + // widthSlider => [data-eb-slider] + + // "^width [data-eb".match(/^\^(\S*)\s(.*)/); + // 0 ==> "^width [data-eb" + // 1 ==> "width", + // 2 ==> "[data-eb" + var parts = prefix.match(/^\^(\S*)\s(.*)/), + propPrefix = ""; + + if (parts) { + propPrefix = parts[1] + "-"; + prefix = parts[2]; + } + + while (name = names[j++]) { + var prop = "{" + $.camelize(propPrefix + name) + "}"; + + !$.has(defaults, prop) && + (defaults[prop] = prefix + name + suffix); + } + } + // cache the underscored names this._fullName = underscoreAndRemoveController(this.fullName); this._shortName = underscoreAndRemoveController(this.shortName); var controller = this, - pluginname = this.pluginName || this._fullName, + /** + * @attribute pluginName + * Setting the pluginName property allows you + * to change the jQuery plugin helper name from its + * default value. + * + * $.Controller("Mxui.Layout.Fill",{ + * pluginName: "fillWith" + * },{}); + * + * $("#foo").fillWith(); + */ funcName, forLint; - // create jQuery plugin - if (!$.fn[pluginname] ) { - $.fn[pluginname] = function( options ) { - - var args = makeArray(arguments), - //if the arg is a method on this controller - isMethod = typeof options == "string" && isFunction(controller.prototype[options]), - meth = args[0]; - this.each(function() { - //check if created - var controllers = $.data(this, "controllers"), - //plugin is actually the controller instance - plugin = controllers && controllers[pluginname]; - - if ( plugin ) { - if ( isMethod ) { + // !-- FOUNDRY HACK --! // + // Make creation of jQuery plugin by testing the existence of pluginName. + if (isString(this.pluginName)) { + + // !-- FOUNDRY HACK --! // + // Add a reference to the fullname + var _fullName = this._fullName; + var pluginname = this.pluginName; + + // create jQuery plugin + if (!$.fn[pluginname] ) { + $.fn[pluginname] = function( options ) { + + var args = makeArray(arguments); + + // Returning controller instance if it exists + if ($.isString(options) && options==="controller") { + + var controllers = data(this[0]), + instance = controllers && controllers[_fullName]; + + return instance; + } + + return this.each(function() { + //check if created + var controllers = data(this), + //plugin is actually the controller instance + //plugin = controllers && controllers[pluginname]; + + // !-- FOUNDRY HACK --! // + // Check using controller full name + instance = controllers && controllers[_fullName]; + + if (instance) { + // call a method on the controller with the remaining args - plugin[meth].apply(plugin, args.slice(1)); - } else { + if ($.isString(options)) { + var method = instance[options]; + $.isFunction(method) && method.apply(instance, args.slice(1)); + return; + } + // call the plugin's update method - plugin.update.apply(plugin, args); - } + instance.update.apply(instance, args); - } else { - //create a new controller instance - controller.newInstance.apply(controller, [this].concat(args)); - } - }); - //always return the element - return this; - }; + } else { + //create a new controller instance + controller.newInstance.apply(controller, [this].concat(args)); + } + }); + }; + } } - // make sure listensTo is an array - //@steal-remove-start - if (!$.isArray(this.listensTo) ) { - throw "listensTo is not an array in " + this.fullName; + // !-- FOUNDRY HACK --! // + // If a prototype factory function was given instead of a prototype object, + // we expect the factory function to return the prototype object upon execution + // of the factory function. This factory function gets executed during the + // instantiation of the controller. + + var args = makeArray(arguments), + prototype = this[STR_PROTOTYPE], + protoFactory = args[(args.length > 3) ? 3 : 2]; + + if (isFunction(protoFactory)) { + + // Remap the factory function + this.protoFactory = protoFactory; + + // Attempt to execute the prototype factory once to get + // a list of actions that we can cache first. + prototype = this.protoFactory.call(this, null); } - //@steal-remove-end + // calculate and cache actions this.actions = {}; - for ( funcName in this.prototype ) { - if (!isFunction(this.prototype[funcName]) ) { - continue; - } - if ( this._isAction(funcName) ) { - this.actions[funcName] = this._getAction(funcName); + // !-- FOUNDRY HACK --! // + // Support for handlers that also pass in event data + for (funcName in prototype) { + + if (funcName=='constructor') continue; + + if (this._isAction(funcName)) { + + var method = prototype[funcName], + isMethod = isFunction(method) || (isArray(method) && isFunction(method[1])); + + if (!isMethod) continue; + + 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); - } + // !-- FOUNDRY HACK --! // + // Controller has been created. Resolve module. + $.module("$:/Controllers/" + this.fullName).resolve(this); }, + hookup: function( el ) { return new this(el); }, @@ -399,40 +585,100 @@ 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. + * @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), + + // !-- FOUNDRY HACK --! // + // Ability to bind custom event to self. + // "{self} customEvent" + methodName = methodName.replace("{self} ", ""); + + // If we have options, run sub to replace templates "{}" with a value from the options + // or the window + var convertedName = methodName; + + if (options) { + + var bindingOtherController = false; + + if (controllerReplacer.test(methodName)) { + + var controller, selector = ""; + convertedName = + methodName + .replace(controllerReplacer, function(whole, inside){ + var parts = inside.split("."); + controller = options["{"+parts[0]+"}"] || {}; + if ($.isControllerInstance(controller)) { + selector = (controller[parts[1]] || {})["selector"]; + } + return selector; + }) + .match(breaker); + + // If there is a selector, this will be true. + bindingOtherController = !!selector; + + convertedName = [controller.element].concat(convertedName || []); + } + + if (!bindingOtherController) { + + convertedName = Str.sub(methodName, [options, window]); + } + } + + // If a "{}" resolves to an object, convertedName will be an array + var 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, delegate : arr ? convertedName[0] : undefined }; }, + /** * @attribute processors * An object of {eventName : function} pairs that Controller uses to hook up events * auto-magically. A processor function looks like: - * + * * jQuery.Controller.processors. * myprocessor = function( el, event, selector, cb, controller ) { * //el - the controller's element @@ -441,61 +687,55 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * //cb - the function to call * //controller - the binding controller * }; - * + * * This would bind anything like: "foo~3242 myprocessor". - * - * The processor must return a function that when called, + * + * The processor must return a function that when called, * unbinds the event handler. - * + * * Controller already has processors for the following events: - * - * - change - * - click - * - contextmenu - * - dblclick + * + * - change + * - click + * - contextmenu + * - dblclick * - focusin * - focusout - * - keydown - * - keyup - * - keypress - * - mousedown + * - keydown + * - keyup + * - keypress + * - mousedown * - mouseenter * - mouseleave - * - mousemove - * - mouseout - * - mouseover - * - mouseup - * - reset - * - resize - * - scroll - * - 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: - * + * - mousemove + * - mouseout + * - mouseover + * - mouseup + * - reset + * - resize + * - scroll + * - select + * - submit + * + * 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); * } * }); - * + * * $('.foo').sized(); */ processors: {}, /** * @attribute listensTo - * A list of special events this controller listens too. You only need to add event names that + * An array of special events this controller + * listens too. You only need to add event names that * are whole words (ie have no special characters). - * + * * $.Controller('TabPanel',{ * listensTo : ['show'] * },{ @@ -503,15 +743,16 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * this.element.show(); * } * }) - * + * * $('.foo').tab_panel().trigger("show"); + * */ listensTo: [], /** * @attribute defaults - * A object of name-value pairs that act as default values for a controller's + * A object of name-value pairs that act as default values for a controller's * [jQuery.Controller.prototype.options options]. - * + * * $.Controller("Message", * { * defaults : { @@ -522,140 +763,282 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * this.element.text(this.options.message); * } * }) - * + * * $("#el1").message(); //writes "Hello World" * $("#el12").message({message: "hi"}); //writes hi + * + * In [jQuery.Controller.prototype.setup setup] the options passed to the controller + * are merged with defaults. This is not a deep merge. */ - defaults: {} + defaults: {}, + + hostname: "parent" }, - /** + /** * @Prototype */ { /** * Setup is where most of controller's magic happens. It does the following: - * - * ### Sets this.element - * - * The first parameter passed to new Controller(el, options) is expected to be + * + * ### 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. - * - * Controller adds it's plugin name to the element's className for easier + * + * ### 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 - * - * A reference to the controller instance is saved in $.data. You can find - * instances of "Foo.Bar" like: - * + * + * ### 3. Saves the controller in $.data + * + * A reference to the controller instance is saved in $.data. You can find + * instances of "Foo.Bar" like: + * * $("#el").data("controllers")['foo_bar']. - * + * * ### Binds event handlers - * + * * Setup does the event binding described in [jquery.controller.listening Listening To Events]. - * - * ## 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; + setup: function(elem, options) { - //want the raw element here - element = element.jquery ? element[0] : element; + var instance = this, + Class = instance[STR_CONSTRUCTOR], + prototype = instance[STR_PROTOTYPE]; - //set element and className on element - this.element = $(element).addClass(cls._fullName); + var _fullName = Class._fullName; - //set in data - ($.data(element, "controllers") || $.data(element, "controllers", {}))[cls._fullName] = this; + // !-- FOUNDRY HACK --! // + // Unique id for every controller instance. + instance.instanceId = $.uid(_fullName + '_'); - //adds bindings - this._bindings = []; - /** - * @attribute options - * Options is [jQuery.Controller.static.defaults] merged with the 2nd argument - * passed to a controller (or the first argument passed to the - * [jquery.controller.plugin controller's jQuery plugin]). - * - * For example: - * - * $.Controller("Tabs", - * { - * defaults : { - * activeClass: "ui-active-state" - * } - * }, - * { - * init : function(){ - * this.element.addClass(this.options.activeClass); - * } - * }) - * - * $("#tabs1").tabs() // adds 'ui-active-state' - * $("#tabs2").tabs({activeClass : 'active'}) // adds 'active' - * - * - */ - 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)); + // !-- FOUNDRY HACK --! // + // Added defaultOptions as an alternative to defaults + var instanceOptions = instance.options + = extend(true, {}, Class.defaults, Class.defaultOptions, options); + + // Convert HTML element into a jQuery element + // and store it inside instance.element. + var element = instance.element + = $(elem); + + // !-- FOUNDRY HACK --! // + // Execute factory function if exists, extends the properties + // of the returned object onto the instance. + if (Class.protoFactory) { + + // This is where "self" keyword is passed as first argument. + prototype = Class.protoFactory.apply(Class, [instance, instanceOptions, element]); + + // Extend the properties of the prototype object onto the instance. + extend(true, instance, prototype); + } + + // !-- FOUNDRY HACK --! // + // Use _fullName instead + // This actually does $(e).data("controllers", _fullName); + (data(elem) || data(elem, {}))[_fullName] = instance; + + // !-- FOUNDRY HACK --~ // + // Add a unique direct selector for every controller instance. + if (!element.data("directSelector")) { + var selector = $.uid("DS"); + element + .addClass(selector) + .data("directSelector", "." + selector); + } + + // !-- FOUNDRY HACK --! // + // Augment selector properties into selector functions. + // The rest are passed in as controller properties. + instance.selectors = {}; + + for (var name in instanceOptions) { + + if (!name.match(/^\{.+\}$/)) continue; + + var key = name.replace(/^\{|\}$/g,''), + val = instanceOptions[name]; + + // Augmented selector function + if (isString(val)) { + + var selectorFuncExtension = instance[key]; + + instance[key] = instance.selectors[key] = (function(instance, selector, funcName) { + + // Selector shorthand for controllers + selector = /^(\.|\#)$/.test(selector) ? selector + funcName : selector; + + // Create selector function + var selectorFunc = function(filter) { + + var elements = (selectorFunc.baseElement || instance.element).find(selector); + + if ($.isString(filter)) { + elements = elements.filter(filter); + } + + if ($.isPlainObject(filter)) { + $.each(filter, function(key, val){ + elements = elements.filterBy(key, val); + }); + } + + return elements; + }; + + // Keep the selector as a property of the function + selectorFunc.selector = selector; + + selectorFunc.css = function() { + + var cssRule = selectorFunc.cssRule; + + if (!cssRule) { + + var directSelector = element.data("directSelector"), + + ruleSelector = $.map(selector.split(","), function(selector) { + return directSelector + " " + selector + }); + + cssRule = selectorFunc.cssRule = $.cssRule(ruleSelector); + cssRule.important = true; + } + + return (arguments.length) ? cssRule.css.apply(cssRule, arguments) : cssRule; + }; + + selectorFunc.inside = function(el) { + return $(el).find(selector); + }; + + selectorFunc.of = function(el) { + return $(el).parents(selector).eq(0); + }; + + selectorFunc.under = function(el) { + + var nodes = []; + + selectorFunc().each(function(){ + if ($(this).parents().filter(el).length) { + nodes.push(this); + } + }); + + return $(nodes); + }; + + if ($.isPlainObject(selectorFuncExtension)) { + $.extend(selectorFunc, selectorFuncExtension); + } + + return selectorFunc; + + })(instance, val, key); + + // Else just reference it, e.g. controller instance + } else { + + instance[key] = val; } } + // !-- FOUNDRY HACK --! // + // Augment view properties into view functions. + // self.view.listItem(useHtml, data, callback); + var views = instanceOptions.view; + + // Prevent augmented functions from being + // extended onto the prototype view function. + var __view = instance.view; + + instance.view = function() { + return __view.apply(this, arguments); + }; + + each(views || {}, function(name, view){ + + instance.view[name] = function(useHtml) { + + var args = makeArray(arguments); + + if ($.isBoolean(useHtml)) { + args = args.slice(1); + } else { + useHtml = false; + } + + return instance.view.apply(instance, [useHtml, name].concat(args)); + } + }); + + // !-- FOUNDRY HACK --! // + // Instance property override + $.extend(instance, instanceOptions.controller); + + // !--- FOUNDRY HACK --! // + instance.pluginInstances = {}; /** * @attribute called - * String name of current function being called on controller instance. This is + * String name of current function being called on controller instance. This is * used for picking the right view in render. * @hide */ - this.called = "init"; + instance.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 + instance._bind(); + + var __init = instance.init || $.noop; + + // !-- FOUNDRY HACK --! // + // Trigger init event when controller is created. + instance.init = function(){ + instance.init = __init; + result = __init.apply(instance, arguments); + instance.trigger("init." + Class.fullName.toLowerCase(), [instance]); + return result; + } /** * @attribute element - * The controller instance's delegated element. This - * is set by [jQuery.Controller.prototype.setup setup]. It + * The controller instance's delegated element. This + * is set by [jQuery.Controller.prototype.setup setup]. It * is a jQuery wrapped element. - * + * * For example, if I add MyWidget to a '#myelement' element like: - * + * * $.Controller("MyWidget",{ * init : function(){ * this.element.css("color","red") * } * }) - * + * * $("#myelement").my_widget() - * + * * MyWidget will turn #myelement's font color red. - * + * * ## Using a different element. - * + * * Sometimes, you want a different element to be this.element. A * very common example is making progressively enhanced form widgets. - * + * * To change this.element, overwrite Controller's setup method like: - * + * * $.Controller("Combobox",{ * setup : function(el, options){ * this.oldElement = $(el); @@ -676,49 +1059,144 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * } * } */ - return this.element; + return [element, instanceOptions].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 * and second parameter. Otherwise the function is called back like a normal bind. * @return {Integer} The id of the binding in this._bindings */ - bind: function( el, eventName, func ) { - if ( typeof el == 'string' ) { - func = eventName; - eventName = el; - el = this.element; + + on: function(eventName) { + + var args = makeArray(arguments), + element = this.element, + length = args.length; + + // Listen to the controller's element + // on(eventName, eventHandler); + if (length==2) { + return this._binder(element, eventName, args[1]); + } + + // Listen to controller's child elements matching the selector + // on(eventName, selector, eventHandler); + // args[1] == selector, jquery collection or dom node. + // args[2] == eventHandler. + if (length==3 && isString(args[1])) { + return this._binder(element, eventName, args[2], args[1]); + } else { + return this._binder(args[1], eventName, args[2]); + } + + // Listen to an element from another element + // on(eventName, element, selector, eventHandler); + if (length==4) { + return this._binder($(args[1]), eventName, args[3], args[2]); } - return this._binder(el, eventName, func); + }, + + // !-- FOUNDRY HACK --! // + // Rename this.bind from this_bind. Conflict with mootools. + // _bind: function( el, eventName, func ) { + _bind: function() { + + var instance = this, + Class = instance[STR_CONSTRUCTOR], + actions = Class.actions, + bindings = instance._bindings = [], + element = instance.element; + + each(actions || {}, function(name, action){ + + if (!actions.hasOwnProperty(name)) return; + + var ready = Class.actions[name] || Class._action(name, instance.options); + + // Translate to the controller element first + if ($.isControllerInstance(ready.delegate)) { + ready.delegate = ready.delegate.element; + } + + bindings.push( + ready.processor( + ready.delegate || element, + ready.parts[2], + ready.parts[1], + name, + instance + ) + ); + }); + + //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; }, _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 = []; + }, + // !-- FOUNDRY HACK --! // + // Element event triggering + trigger: function(name) { + + var el = this.element; + if (!el) return; + + var event = $.Event(name); + el.trigger.apply(el, [event].concat($.makeArray(arguments).slice(1))); + + return event; + }, /** * 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,34 +1225,112 @@ 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, + * Destroy unbinds and undelegates all event handlers on this controller, * and prevents memory leaks. This is called automatically * if the element is removed. You can overwrite it to add your own * teardown functionality: - * + * * $.Controller("ChangeText",{ * init : function(){ * this.oldText = this.element.text(); @@ -784,37 +1340,48 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func * this.element.text(this.oldText); * this._super(); //Always call this! * }) - * + * + * Make sure you always call _super when overwriting + * controller's destroy event. The base destroy functionality unbinds + * all event handlers the controller has created. + * * You could call destroy manually on an element with ChangeText * added like: - * + * * $("#changed").change_text("destroy"); - * - * ### API + * */ destroy: function() { + if ( this._destroyed ) { - throw this.Class.shortName + " controller instance has been deleted"; + return; } - var self = this, - fname = this.Class._fullName, + var fname = this[STR_CONSTRUCTOR]._fullName, controllers; + + // remove all plugins + for (pname in this.pluginInstances) { + this.removePlugin(pname); + } + + // mark as destroyed this._destroyed = true; - this.element.removeClass(fname); - $.each(this._bindings, function( key, value ) { - if ( isFunction(value) ) { - value(self.element[0]); - } - }); + // remove the className + this.element.removeClass(fname); + // 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; + + // !-- FOUNDRY HACK --! // + // Reassign this.element to an empty jQuery element instead. + this.element = $(); }, /** * Queries from the controller's element. @@ -829,94 +1396,498 @@ steal.plugins('jquery/class', 'jquery/lang', 'jquery/event/destroyed').then(func find: function( selector ) { return this.element.find(selector); }, + + // !-- FOUNDRY HACK --! // + // Quick acccess to views. + view: function() { + + var args = makeArray(arguments), + name, + options = args, + useHtml = false, + context = this[STR_CONSTRUCTOR].component || $, + html = "", + view = this.options.view || {}; + + if (typeof args[0] == "boolean") { + useHtml = args[0]; + options = args.slice(1); + } + + name = options[0] = view[options[0]]; + + // If view is not assigned, return empty string. + if (name==undefined) { + return (useHtml) ? "" : $(""); + } + + html = context.View.apply(context, options); + + return (useHtml) ? html : $($.parseHTML($.trim(html))); + }, + + getPlugin: function(name) { + + return this.pluginInstances[name]; + }, + + addSubscriber: function(instance) { + + var instances = ($.isArray(instance)) ? instance : [instance || {}]; + + // Prep options + var host = this, + hostname = this.Class.hostname, + options = {}; + options["{" + hostname + "}"] = host; + + $.map(instances, function(instance, i){ + + // If this is not a controller instance. + if (!$.isControllerInstance(instance)) return false; + + // If instance is already a subscriber,skip. + if (instance.options[hostname]===this) return instance; + + // Also map itself as a method name + instance[hostname] = host; + + // Attach publisher to subscriber + return instance.update(options); + }); + + return instances; + }, + + // addPlugin(name, object, [options]); + // The object should consist of a method called destroy(); + + // addPlugin(name, function, [options]); + // The function should return an object with a method called destroy(); + + addPlugin: function(name, plugin, options) { + + if (!name) return; + + // This means we are working with plugin shorthand + if ((!plugin && !options) || $.isPlainObject(plugin)) { + options = plugin; + plugin = [this.Class.root, this.Class.fullName, $.String.capitalize(name)].join("."); + } + + // If plugin is a string, get the controller from it. + if ($.isString(plugin)) { + plugin = $.getController(plugin); + } + + var isPluginInstance = $.isControllerInstance(plugin); + + // Controller class are also functions, + // so this simple test is good enough. + if (!isFunction(plugin) && !isPluginInstance) return; + + // Normalize plugin options + var pluginOptions = + this.Class.pluginExtendsInstance ? + this.options[name] : + (this.options.plugin || {})[name]; + + options = $.extend(true, {element: this.element}, options, pluginOptions); + + // Determine plugin type + var type = + ((isPluginInstance) ? "instance" : + (($.isController(plugin)) ? "controller" : "function")); + + // Trigger addPlugin event so controller can decorate the options + this.trigger("addPlugin", [name, plugin, options, type]); + + var hostname = this.Class.hostname; + + // Subcontrollers should have a way to listen back to host controller + options["{" + hostname + "}"] = this; + + var pluginInstance; + + switch(type) { + + // Plugin instance + case "instance": + + pluginInstance = plugin; + + // Update child plugin with custom plugin options from host + plugin.update(options); + + plugin[hostname] = this; + break; + + // Plugin controller + case "controller": + pluginInstance = options.element.addController(plugin, options); + break; + + // Plugin function + case "function": + pluginInstance = plugin(this, options); + break; + } + + // If pluginInstance could not be created, stop. + if (!pluginInstance) return; + + // Register plugin + this.pluginInstances[name] = pluginInstance; + + // Also extend instance with a property point to the plugin + if (this.Class.pluginExtendsInstance) { + this[name] = pluginInstance; + } + + // Host controller should also have a way to listen back to the child controller + if (type!=="function") { + + var hostOptions = {}; + hostOptions["{" + name + "}"] = pluginInstance; + + this.update(hostOptions); + } + + // Trigger registerPlugin + this.trigger("registerPlugin", [name, pluginInstance, options, type]); + + return pluginInstance; + }, + + removePlugin: function(name) { + + var plugin = this.getPlugin(name); + + if (!plugin) return; + + // Trigger removePlugin + this.trigger("removePlugin", [name, plugin]); + + delete this.pluginInstances[name]; + + return $.isFunction(plugin.destroy) ? plugin.destroy() : null; + }, + + invokePlugin: function(name, method, args) { + + var plugin = this.getPlugin(name); + + // If plugin not exist, stop. + if (!plugin) return; + + // If plugin method not exist, stop. + if (!$.isFunction(plugin[method])) return; + + // Let any third party modify the arguments if required + this.trigger("invokePlugin", [name, plugin, args]); + + return plugin[method].apply(this, args); + }, + + getMessageGroup: function() { + + // Find parent element + var messageGroup = ($.isFunction(this.messageGroup)) ? this.messageGroup() : this.element.find("[data-message-group]"); + + if (messageGroup.length < 1) { + messageGroup = $("
    ").prependTo(this.element); + } + + return messageGroup; + }, + + setMessage: function(message, type) { + + // Normalize arguments + var defaultOptions = { + type : "warning", // type: info, error, success + message: "", + parent : this.getMessageGroup(), + element: $('
    ') + }, + userOptions = {}, + isDeferred = $.isDeferred(message); + + // Normalize user options + if ($.isPlainObject(message) && !isDeferred) { + userOptions = message; + } else { + userOptions = { + message: message, + type : type || "warning" + } + } + + var options = $.extend({}, defaultOptions, userOptions), + element = options.element; + + if ($.isDeferred(message)) { + + var myself = arguments.callee, + context = this; + + message.done(function(message, type) { + options.message = message; + options.type = type || "warning"; + myself.call(context, options); + element.show(); + }); + + } else { + + element + .addClass("alert-" + options.type) + .append(options.message); + + if ($('html').has(element).length < 1) { + element.appendTo(options.parent); + } + } + + return element; + }, + + clearMessage: function() { + + this.getMessageGroup().empty(); + }, + //tells callback to set called on this. I hate this. _set_called: true }); + var processors = $.Controller.Class.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; + basicProcessor = function( el, event, selector, methodName, controller ) { - // 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; + // !-- FOUNDRY HACK --! // + // Support for passing event data + + var method = controller[methodName], + eventData; + + if (isArray(method) && isFunction(method[1])) { + eventData = method[0]; } - return binder(el, event, shifter(cb), selector); - }; - var processors = $.Controller.processors, + return binder(el, event, shifter(controller, methodName), selector, eventData); + }; - //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 */ //used to determine if a controller instance is one of controllers //controllers can be strings or classes - var i, isAControllerOf = function( instance, controllers ) { - for ( i = 0; i < controllers.length; i++ ) { - if ( typeof controllers[i] == 'string' ? instance.Class._shortName == controllers[i] : instance instanceof controllers[i] ) { - return true; - } + + var normalizeController = function(controller) { + return controller.replace("$.Controller", controllerRoot); + } + + var getController = function(controller) { + if (isString(controller)) { + controller = normalizeController(controller); + controller = getObject(controller) || getObject(controllerRoot + "." + controller); + }; + if (isController(controller)) { + return controller; + }; + } + + var isController = function(controller) { + return isFunction(controller) && controller.hasOwnProperty("_fullName"); + } + + var flattenControllers = function(controllers) { + return $.map(controllers, function(controller){ + return (isArray(controller)) ? flattenControllers(controller) : getController(controller); + }); + }; + + $.getController = getController; + + $.isController = function(controller) { + return !!getController(controller); + } + + $.isControllerInstance = function(instance) { + return instance && instance[STR_CONSTRUCTOR] && isController(instance[STR_CONSTRUCTOR]); + } + + $.isControllerOf = function(instance, controllers) { + + if (!controllers) return false; + + if (!isArray(controllers)) { + controllers = [controllers]; } + + for (var i=0; i 1) ? instances : instances[0]; + }, + + removeController: function(controller) { + this.each(function(){ + var instances = $(this).controllers(controller); + while (instances.length) { + instances.shift().destroy(); + } + }); + return this; + }, + + addControllerWhenAvailable: function(controller) { + + var elements = this, + args = arguments, + task = $.Deferred(); + + if ($.isController(controller)) { + controller = controller.fullName; } - }); - 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]; - }; -}); \ No newline at end of file + if (!isString(controller)) { + return task.reject(); + } + + $.module("$:/Controllers/" + controller) + .pipe( + function(){ + var instance = elements.addController.apply(elements, args); + task.resolveWith(instance, [elements, instance]); + }, + task.reject, + task.fail + ); + + return task; + }, + + // @deprecated 2.2 + implement: function() { + this.addController.apply(this, arguments); + return this; + } + + }); + + // !-- FOUNDRY HACK --! // + // Add support for augmented selector function on jQuery's DOM traversal/filtering methods. + (function(){ + var fns = ["is", "find"], + _fns = {}, + fn; + + while (fn = fns.shift()) { + _fns[fn] = $.fn[fn]; + $.fn[fn] = (function(fn) { + return function(obj) { + return _fns[fn].apply(this, (obj || {}).hasOwnProperty("of") ? [obj.selector] : arguments); + } + })(fn); + } + })(); + +}); 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..67c7cefb --- /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 + */ + $.Controller.Class.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..689b7199 100644 --- a/controller/subscribe/subscribe.js +++ b/controller/subscribe/subscribe.js @@ -1,38 +1,39 @@ /*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 * @parent jQuery.Controller.static.processors * @plugin jquery/controller/subscribe * Adds OpenAjax.Hub subscribing to controllers. - * + * * $.Controller("Subscriber",{ * "recipe.updated subscribe" : function(called, recipe){ - * + * * }, * "todo.* subscribe" : function(called, todo){ - * + * * } * }) - * + * * You should typically be listening to jQuery triggered events when communicating between * controllers. Subscribe should be used for listening to model changes. - * + * * ### API - * + * * This is the call signiture for the processor, not the controller subscription callbacks. - * + * * @param {HTMLElement} el the element being bound. This isn't used. * @param {String} event the event type (subscribe). * @param {String} selector the subscription name - * @param {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); + $.Controller.Class.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); }; }; @@ -47,7 +48,7 @@ steal.plugins('jquery/controller', 'jquery/lang/openajax').then(function() { * @param {String} message Message name, ex: "Something.Happened". * @param {Object} data The data sent. */ - jQuery.Controller.prototype.publish = function() { + $.Controller.Class.prototype.publish = function() { OpenAjax.hub.publish.apply(OpenAjax.hub, arguments); }; -}); \ No newline at end of file +}); 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..c7bf5c19 100644 --- a/controller/view/view.js +++ b/controller/view/view.js @@ -1,36 +1,43 @@ -steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { - jQuery.Controller.getFolder = function() { - return jQuery.String.underscore(this.fullName.replace(/\./g, "/")).replace("/Controllers", ""); +steal('jquery/controller', 'jquery/view').then(function($) { + var URI = steal.URI || steal.File; + + $.Controller.getFolder = function() { + return $.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; + $.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? $.String.underscore(classParts[0]): $.String.underscore(classParts.join("/")), + controller_name = $.String.underscore(classPartsWithoutPrefix.join('/')).toLowerCase(), + suffix = (typeof view == "string" && /\.[\w\d]+$/.test(view)) ? "" : $.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; }; var calculateHelpers = function( myhelpers ) { var helpers = {}; if ( myhelpers ) { - if ( jQuery.isArray(myhelpers) ) { + if ( $.isArray(myhelpers) ) { for ( var h = 0; h < myhelpers.length; h++ ) { - jQuery.extend(helpers, myhelpers[h]); + $.extend(helpers, myhelpers[h]); } } else { - jQuery.extend(helpers, myhelpers); + $.extend(helpers, myhelpers); } } else { if ( this._default_helpers ) { @@ -38,15 +45,17 @@ 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' ) { + $.extend(helpers, current.Helpers); + } + current = current[parts[i]]; } - current = current[parts[i]]; } - if ( typeof current.Helpers == 'object' ) { - jQuery.extend(helpers, current.Helpers); + if (current && typeof current.Helpers == 'object' ) { + $.extend(helpers, current.Helpers); } this._default_helpers = helpers; } @@ -57,32 +66,34 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { * @add jQuery.Controller.prototype */ - jQuery.Controller.prototype. + $.Controller.prototype. /** * @tag view * Renders a View template with the controller instance. If the first argument - * is not supplied, + * is not supplied, * it looks for a view in /views/controller_name/action_name.ejs. * If data is not provided, it uses the controller instance as data. * @codestart * TasksController = $.Controller.extend('TasksController',{ * click: function( el ) { * // renders with views/tasks/click.ejs - * el.html( this.view() ) + * 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 = $.Controller._calculatePosition(this.Class, view, this.called); //calculate data data = data || this; @@ -103,8 +114,8 @@ steal.plugins('jquery/controller', 'jquery/view').then(function( $ ) { var helpers = calculateHelpers.call(this, myhelpers); - return jQuery.View(view, data, helpers); //what about controllers in other folders? + return $.View(view, data, helpers); //what about controllers in other folders? }; -}); \ No newline at end of file +}); diff --git a/dom/closest/closest.js b/dom/closest/closest.js index 906e865c..8dbd9860 100644 --- a/dom/closest/closest.js +++ b/dom/closest/closest.js @@ -1,20 +1,28 @@ /** * @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){ + var oldClosest = $.fn._closest = $.fn.closest; + $.fn.closest = function(selectors, context){ + + // FOUNDRY_HACK + // If a jQuery or node element was passed in, use original closest method. + if (selectors instanceof $ || $.isElement(selectors)) { + return oldClosest.call(this, arguments); + } + var rooted = {}, res, result, thing, i, j, selector, rootedIsEmpty = true, selector, selectorsArr = selectors; if(typeof selectors == "string") selectorsArr = [selectors]; - + $.each(selectorsArr, function(i, selector){ if(selector.indexOf(">") == 0 ){ if(selector.indexOf(" ") != -1){ @@ -25,9 +33,9 @@ steal.plugins('jquery/dom').then(function(){ rootedIsEmpty = false; } }) - + res = oldClosest.call(this, selectors, context); - + if(rootedIsEmpty) return res; i =0; while(i < res.length){ @@ -44,4 +52,4 @@ steal.plugins('jquery/dom').then(function(){ } return res; } -}) \ No newline at end of file +}) 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..58b7a2cd 100644 --- a/dom/compare/compare.js +++ b/dom/compare/compare.js @@ -1,67 +1,85 @@ /** * @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 + * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/dom/compare/compare.js + * + * Compares the position of two nodes and returns a bitmask detailing how they are positioned + * relative to each other. + * + * $('#foo').compare($('#bar')) //-> Number + * + * You can expect it to return the same results as * [http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition | compareDocumentPosition]. * Parts of this documentation and source come from [http://ejohn.org/blog/comparing-document-position | John Resig]. - *

      Demo

      + * + * ## Demo * @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 - +$.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(), + 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); - + } return number; } -}); \ No newline at end of file +}); 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..78039249 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 @@ -19,34 +17,33 @@ steal.plugins('jquery/lang/json').then(function() { * / Get the value of a cookie with the given name. *

        *

        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 * $.cookie('the_cookie', 'the_value', * { expires: 7, path: '/', domain: 'jquery.com', secure: true }); * @codeend - * + * * Create a session cookie. * @codestart * $.cookie('the_cookie', 'the_value'); * @codeend - * + * * Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain * used when the cookie was set. * @codestart * $.cookie('the_cookie', null); * @codeend - * + * * Get the value of a cookie. * @codestart * $.cookie('the_cookie'); * @codeend - * + * * * @param {String} [name] The name of the cookie. * @param {String} [value] The value of the cookie. @@ -59,9 +56,9 @@ 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) { + $.cookie = function(name, value, options) { if (typeof value != 'undefined') { // name and value given, set cookie options = options || {}; 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..41985262 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, @@ -42,7 +42,7 @@ steal.plugins('jquery/dom').then(function( $ ) { oldName = name.replace(rdashAlpha, fcamelCase); if ( rfloat.test(name) ) { - name = jQuery.support.cssFloat ? "float" : "styleFloat"; + name = $.support.cssFloat ? "float" : "styleFloat"; oldName = "cssFloat"; } @@ -114,4 +114,4 @@ steal.plugins('jquery/dom').then(function( $ ) { .curStyles = function() { return $.curStyles(this[0], $.makeArray(arguments)); }; -}); \ No newline at end of file +}); diff --git a/dom/cur_styles/test/qunit/cur_styles_test.js b/dom/cur_styles/cur_styles_test.js similarity index 72% rename from dom/cur_styles/test/qunit/cur_styles_test.js rename to dom/cur_styles/cur_styles_test.js index ce5ac756..d746815e 100644 --- a/dom/cur_styles/test/qunit/cur_styles_test.js +++ b/dom/cur_styles/cur_styles_test.js @@ -1,8 +1,12 @@ +steal("jquery/dom/dimensions",'jquery/view/micro') //load your app + .then('funcunit/qunit').then(function(){ + module("jquery/dom/curStyles"); test("reading", function(){ - $("#qunit-test-area").html("//jquery/dom/cur_styles/test/qunit/curStyles.micro",{}) + + $("#qunit-test-area").html("//jquery/dom/cur_styles/test/curStyles.micro",{}) var res = $.curStyles( $("#styled")[0], ["padding-left", @@ -18,5 +22,7 @@ test("reading", function(){ equals(res.paddingLeft, "5px","padding left"); equals(res.position, "relative","position"); $("#qunit-test-area").html("") +}); + }) diff --git a/dom/cur_styles/qunit.html b/dom/cur_styles/qunit.html index 81341797..736ae9d6 100644 --- a/dom/cur_styles/qunit.html +++ b/dom/cur_styles/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/cur_styles/test/qunit/curStyles.micro b/dom/cur_styles/test/curStyles.micro similarity index 100% rename from dom/cur_styles/test/qunit/curStyles.micro rename to dom/cur_styles/test/curStyles.micro diff --git a/dom/cur_styles/test/qunit/qunit.js b/dom/cur_styles/test/qunit/qunit.js deleted file mode 100644 index e51bcaaf..00000000 --- a/dom/cur_styles/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/dimensions",'jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("cur_styles_test") \ No newline at end of file diff --git a/dom/dimensions/dimensions.html b/dom/dimensions/dimensions.html index 6e2e3150..9c3f09df 100644 --- a/dom/dimensions/dimensions.html +++ b/dom/dimensions/dimensions.html @@ -69,10 +69,8 @@ - \ No newline at end of file diff --git a/dom/dimensions/dimensions.js b/dom/dimensions/dimensions.js index 2d4b0f17..2296b91d 100644 --- a/dom/dimensions/dimensions.js +++ b/dom/dimensions/dimensions.js @@ -1,26 +1,34 @@ -steal.plugins('jquery/dom/cur_styles').then(function($) { +steal('jquery/dom/cur_styles').then(function($) { /** * @page dimensions dimensions * @parent dom - *

        jquery/dom/dimensions Plugin

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

        Quick Examples

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

        Use

        - *

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

        - *

        Demo

        + * + * + * + * + * ## Demo + * * @demo jquery/dom/dimensions/dimensions.html */ @@ -39,22 +47,27 @@ var weird = /button|select/i, //margin is inside border */ $.each({ -/* +/** * @function outerWidth * @parent dimensions - * Lets you set the outer height on an object + * Lets you set the outer width on an object * @param {Number} [height] - * @param {Boolean} [includeMargin] + * @param {Boolean} [includeMargin=false] Makes setting the outerWidth adjust + * for margin. Defaults to false. + * + * $('#hasMargin').outerWidth(50, true); + * + * @return {jQuery|Number} If you are setting the value, returns the jQuery wrapped elements. */ width: -/* +/** * @function innerWidth * @parent dimensions * Lets you set the inner height of an object * @param {Number} [height] */ "Width", -/* +/** * @function outerHeight * @parent dimensions * Lets you set the outer height of an object where:
        @@ -74,7 +87,7 @@ width: * Otherwise, returns outerHeight in pixels. */ height: -/* +/** * @function innerHeight * @parent dimensions * Lets you set the outer width on an object @@ -104,19 +117,21 @@ height: //getter / setter $.fn["outer" + Upper] = function(v, margin) { - if (typeof v == 'number') { - this[lower](v - getBoxes[lower](this[0], {padding: true, border: true, margin: margin})) + var first = this[0]; + if (typeof v == 'number') { + first && this[lower](v - getBoxes[lower](first, {padding: true, border: true, margin: margin})) return this; } else { - return checks["oldOuter" + Upper].call(this, v) + return first ? checks["oldOuter" + Upper].call(this, v) : null; } } $.fn["inner" + Upper] = function(v) { - if (typeof v == 'number') { - this[lower](v - getBoxes[lower](this[0], { padding: true })) + var first = this[0]; + if (typeof v == 'number') { + first&& this[lower](v - getBoxes[lower](first, { padding: true })) return this; } else { - return checks["oldInner" + Upper].call(this, v) + return first ? checks["oldInner" + Upper].call(this, v) : null; } } //provides animations diff --git a/dom/dimensions/dimensions_test.js b/dom/dimensions/dimensions_test.js new file mode 100644 index 00000000..3f7ad31b --- /dev/null +++ b/dom/dimensions/dimensions_test.js @@ -0,0 +1,14 @@ +steal("jquery/dom/dimensions", + 'jquery/view/micro', + 'funcunit/qunit').then(function(){ + +module("jquery/dom/dimensions"); + + + + +test("outerHeight and width",function(){ + $("#qunit-test-area").html("//jquery/dom/dimensions/test/curStyles.micro",{}) +}) + +}); \ No newline at end of file diff --git a/dom/dimensions/qunit.html b/dom/dimensions/qunit.html index 3636028f..2b0e381c 100644 --- a/dom/dimensions/qunit.html +++ b/dom/dimensions/qunit.html @@ -7,7 +7,7 @@ margin: 0px; padding: 0px; } - + diff --git a/dom/dimensions/test/qunit/curStyles.micro b/dom/dimensions/test/curStyles.micro similarity index 100% rename from dom/dimensions/test/qunit/curStyles.micro rename to dom/dimensions/test/curStyles.micro diff --git a/dom/cur_styles/test/qunit/outer.micro b/dom/dimensions/test/outer.micro similarity index 100% rename from dom/cur_styles/test/qunit/outer.micro rename to dom/dimensions/test/outer.micro diff --git a/dom/dimensions/test/qunit/dimensions_test.js b/dom/dimensions/test/qunit/dimensions_test.js deleted file mode 100644 index 9d34e7e8..00000000 --- a/dom/dimensions/test/qunit/dimensions_test.js +++ /dev/null @@ -1,8 +0,0 @@ -module("jquery/dom/dimensions"); - - - - -test("outerHeight and width",function(){ - $("#qunit-test-area").html("//jquery/dom/dimensions/test/qunit/curStyles.micro",{}) -}) diff --git a/dom/dimensions/test/qunit/outer.micro b/dom/dimensions/test/qunit/outer.micro deleted file mode 100644 index e69de29b..00000000 diff --git a/dom/dimensions/test/qunit/qunit.js b/dom/dimensions/test/qunit/qunit.js deleted file mode 100644 index d842c596..00000000 --- a/dom/dimensions/test/qunit/qunit.js +++ /dev/null @@ -1,4 +0,0 @@ -steal - .plugins("jquery/dom/dimensions",'jquery/view/micro') //load your app - .plugins('funcunit/qunit') //load qunit - .then("dimensions_test") \ No newline at end of file diff --git a/dom/dom.js b/dom/dom.js index f964832f..a47d8438 100644 --- a/dom/dom.js +++ b/dom/dom.js @@ -1,7 +1,82 @@ /** - * @page dom DOM Helpers - * @tag core - * JavaScriptMVC adds a bunch of useful jQuery extensions for the dom. Check them out on the left. - * - */ -steal.plugins('jquery'); \ No newline at end of file +@page dom DOM Helpers +@parent jquerymx +@description jQuery DOM extension. + +JavaScriptMVC adds a bunch of useful +jQuery extensions for the dom. Check them out on the left. + +## [dimensions Dimensions] + +Set and animate the inner and outer height and width of elements. + + $('#foo').outerWidth(100); + $('#bar').animate({innerWidth: 500}); + +This is great when you want to include padding and margin in +setting the dimensions of elements. + +## [jQuery.cookie Cookie] + +Set and get cookie values: + + $.cookie('cookie','value'); + +## [jQuery.fixture Fixture] + +Simulate Ajax responses. + + $.fixture("/services/tasks.php','fixtures/tasks.json'); + +Works with jQuery's Ajax converters! + +## [jQuery.fn.compare Compare] + +Compare the location of two elements rapidly. + + $('#foo').compare($('#bar')) & 2 // true if #bar is before #foo + +## [jQuery.fn.curStyles CurStyles] + +Get multiple css properties quickly. + + $('#foo').curStyles('left','top') //-> {left:'20px',top:'10px'} + +## [jQuery.fn.formParams FormParams] + +Serializes a form into a JSON-like object: + + $('form').formParams() //-> {name: 'Justin', favs: ['JS','Ruby']} + +## [jQuery.fn.selection Selection] + +Gets or sets the current text selection. + + // gets selection info + $('pre').selection() //-> {start: 22, end: 57, range: range} + + // sets the selection + $('div').selection(20,22) + +## [jQuery.fn.within Within] + +Returns elements that have a point within their boundaries. + + $('.drop').within(200,200) //-> drops that touch 200,200 + +## [jQuery.Range Range] + +Text range utilities. + + $('#copy').range() //-> text range that has copy selected + +## [jQuery.route] + +Hash routes mapped to an [jQuery.Observe $.Observe]. + + $.route(':type',{type: 'videos'}) + $.route.delegate('type','set', function(){ ... }) + $.route.attr('type','images'); + +*/ +steal('jquery'); \ No newline at end of file diff --git a/dom/fixture/fixture.html b/dom/fixture/fixture.html index 75a7d6de..d59b1f30 100644 --- a/dom/fixture/fixture.html +++ b/dom/fixture/fixture.html @@ -15,11 +15,13 @@
        - - diff --git a/dom/fixture/fixture.js b/dom/fixture/fixture.js index 5ce2a0ae..3cc4322b 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -1,25 +1,130 @@ -steal.plugins('jquery/dom').then(function( $ ) { +steal('jquery/dom', + 'jquery/lang/object', + 'jquery/lang/string',function($) { - var ajax = $.ajax, - typeTest = /^(script|json|test|jsonp)$/, + //used to check urls + + + + // the pre-filter needs to re-route the url + + $.ajaxPrefilter( function( settings, originalOptions, jqXHR ) { + // if fixtures are on + if(! $.fixture.on) { + return; + } + + // add the fixture option if programmed in + var data = overwrite(settings); + + // if we don't have a fixture, do nothing + if(!settings.fixture){ + if(window.location.protocol === "file:"){ + 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) ) { + url = steal.root.mapJoin(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; + }; + } + + }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 +135,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 - * 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. - * - * The fixture property is set as ... - * @codestart - * //... a property with $.ajax - * $.ajax({fixture: FIXTURE_VALUE}) - * - * //... 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. - *
        • - *
        + * + * $.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. + * + * ## Types of Fixtures + * + * There are two common ways of using fixtures. The first is to + * map Ajax requests to another file. The following + * intercepts requests to /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. + * * 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: - * Dynamic fixtures are awesome for performance testing. Want to see what + * + * 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 + 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,6 +512,18 @@ 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, filter ) { if(typeof types === "string"){ @@ -330,27 +537,8 @@ 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++ ) { //call back provided make var item = make(i, items); @@ -362,20 +550,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 +589,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,7 +606,8 @@ steal.plugins('jquery/dom').then(function( $ ) { } } } - + + if( filter ) { i = 0; while (i < retArr.length) { @@ -427,7 +629,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 ) { @@ -445,40 +648,121 @@ steal.plugins('jquery/dom').then(function( $ ) { break; } } - + // TODO: make it work with non-linear ids .. $.extend(findOne(id), settings.data); return $.fixture["-restDestroy"](settings, cbType) }; $.fixture["-" + types[1]+"Create"] = function( settings, cbType ) { var item = make(items.length, items); + $.extend(item, settings.data); - + if(!item.id){ item.id = items.length; } - + items.push(item); - return $.fixture["-restCreate"](settings, cbType) + + 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; }, /** - * Use $.fixture.xhr to create an object that looks like an xhr object. - *

        Example

        - * The following example shows how the -restCreate fixture uses xhr to return + * @hide + * Use $.fixture.xhr to create an object that looks like an xhr object. + * + * ## Example + * + * The following example shows how the -restCreate fixture uses xhr to return * a simulated xhr object: * @codestart * "-restCreate" : function( settings, cbType ) { * switch(cbType){ - * case "success": + * case "success": * return [ - * {id: parseInt(Math.random()*1000)}, - * "success", + * {id: parseInt(Math.random()*1000)}, + * "success", * $.fixture.xhr()]; * case "complete": - * return [ + * return [ * $.fixture.xhr({ - * getResponseHeader: function() { + * getResponseHeader: function() { * return settings.url+"/"+parseInt(Math.random()*1000); * } * }), @@ -512,7 +796,7 @@ steal.plugins('jquery/dom').then(function( $ ) { /** * @attribute on * On lets you programatically turn off fixtures. This is mostly used for testing. - * + * * $.fixture.on = false * Task.findAll({}, function(){ * $.fixture.on = true; @@ -521,12 +805,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 @@ -551,119 +836,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..800c4e2f 100644 --- a/dom/form_params/form_params.js +++ b/dom/form_params/form_params.js @@ -1,7 +1,7 @@ /** * @add jQuery.fn */ -steal.plugins("jquery/dom").then(function( $ ) { +steal("jquery/dom").then(function($) { var radioCheck = /radio|checkbox/i, keyBreaker = /[^\[\]]+/g, numberMatcher = /^[\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?$/; @@ -24,8 +24,13 @@ steal.plugins("jquery/dom").then(function( $ ) { * @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,27 +39,70 @@ 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, convert ) { + + // Quick way to determine if something is a boolean + if ( !! params === params ) { + convert = params; + params = null; + } - return jQuery(jQuery.makeArray(this[0].elements)).getParams(convert); + if ( params ) { + return this.setParams( params ); + } else if ( this[0].nodeName.toLowerCase() == 'form' && this[0].elements ) { + return $($.makeArray(this[0].elements)).getParams(convert); } - return jQuery("input[name], textarea[name], select[name]", this[0]).getParams(convert); + return $("input[name], textarea[name], select[name]", this[0]).getParams(convert); + }, + setParams: function( params ) { + + // Find all the inputs + this.find("[name]").each(function() { + + var value = params[ $(this).attr("name") ], + $this; + + // Don't do all this work if there's no value + if ( value !== undefined ) { + $this = $(this); + + // Nested these if statements for performance + if ( $this.is(":radio") ) { + if ( $this.val() == value ) { + $this.attr("checked", true); + } + } else if ( $this.is(":checkbox") ) { + // Convert single value to an array to reduce + // complexity + value = $.isArray( value ) ? value : [value]; + if ( $.inArray( $this.val(), value ) > -1) { + $this.attr("checked", true); + } + } else { + $this.val( value ); + } + } + }); }, getParams: function( convert ) { var data = {}, current; - convert = convert === undefined ? true : convert; + convert = convert === undefined ? false : convert; this.each(function() { var el = this, @@ -65,7 +113,7 @@ steal.plugins("jquery/dom").then(function( $ ) { } var key = el.name, - value = $.fn.val.call([el]) || $.data(el, "value"), + value = $.data(el, "value") || $.fn.val.call([el]), isRadioCheck = radioCheck.test(el.type), parts = key.match(keyBreaker), write = !isRadioCheck || !! el.checked, @@ -75,10 +123,14 @@ steal.plugins("jquery/dom").then(function( $ ) { if ( convert ) { if ( isNumber(value) ) { value = parseFloat(value); - } else if ( value === 'true' || value === 'false' ) { - value = Boolean(value); + } else if ( value === 'true') { + value = true; + } else if ( value === 'false' ) { + value = false; + } + if(value === '') { + value = undefined; } - } // go through and create nested objects @@ -92,7 +144,7 @@ steal.plugins("jquery/dom").then(function( $ ) { lastPart = parts[parts.length - 1]; //now we are on the last part, set the value - if ( lastPart in current && type === "checkbox" ) { + if (current[lastPart]) { if (!$.isArray(current[lastPart]) ) { current[lastPart] = current[lastPart] === undefined ? [] : [current[lastPart]]; } @@ -100,6 +152,7 @@ steal.plugins("jquery/dom").then(function( $ ) { current[lastPart].push(value); } } else if ( write || !current[lastPart] ) { + current[lastPart] = write ? value : undefined; } @@ -108,4 +161,4 @@ steal.plugins("jquery/dom").then(function( $ ) { } }); -}); \ 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..93d50fd1 --- /dev/null +++ b/dom/form_params/form_params_test.js @@ -0,0 +1,69 @@ +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"); + + +}); + + +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 77% rename from dom/form_params/test/qunit/basics.micro rename to dom/form_params/test/basics.micro index 61dea977..dfc247ff 100644 --- a/dom/form_params/test/qunit/basics.micro +++ b/dom/form_params/test/basics.micro @@ -12,7 +12,7 @@ - + - - \ 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/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 37e22ad1..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 54dc1c2b..b5f9d139 100644 --- a/dom/range/range.html +++ b/dom/range/range.html @@ -24,7 +24,6 @@
          -

          The Range Plugin

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

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

          @@ -35,28 +34,33 @@

          The Range Plugin

          \ No newline at end of file diff --git a/dom/range/range.js b/dom/range/range.js index 4c9a8965..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) @@ -39,44 +99,58 @@ $.Range = function(range){ this.select(range) } - } else if (range.clientX || range.pageX || range.left) { - this.rangeFromPoint(range) + } else if (range.clientX != null || range.pageX != null || range.left != null) { + this.moveToPoint(range) } else if (range.originalEvent && range.originalEvent.touches && range.originalEvent.touches.length) { - this.rangeFromPoint(range.originalEvent.touches[0]) + this.moveToPoint(range.originalEvent.touches[0]) } else if (range.originalEvent && range.originalEvent.changedTouches && range.originalEvent.changedTouches.length) { - this.rangeFromPoint(range.originalEvent.changedTouches[0]) + 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() ); } }; -var bisect = function(el, start, end){ - //split the start and end ... figure out who is touching ... - if(end-start == 1){ - return - } - - - -} -$.extend($.Range.prototype,{ - rangeFromPoint : function(point){ + +$.extend($.Range.prototype, +/** @prototype **/ +{ + moveToPoint : function(point){ var clientX = point.clientX, clientY = point.clientY if(!clientX){ var off = scrollOffset(); - clientX = (off.pageX || off.left || 0 ) - off.left; - clientY = (off.pageY || off.top || 0 ) - off.top; + clientX = (point.pageX || point.left || 0 ) - off.left; + clientY = (point.pageY || point.top || 0 ) - off.top; } + if(support.moveToPoint){ + this.range = $.Range().range + this.range.moveToPoint(clientX, clientY); + return this; + } + // it's some text node in this range ... var parent = document.elementFromPoint(clientX, clientY); @@ -128,7 +202,13 @@ $.extend($.Range.prototype,{ }, /** * 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){ @@ -156,16 +236,53 @@ $.extend($.Range.prototype,{ }, /** * Collapses a range - * @param {Boolean} [toStart] true if to the start of the range, false if to the end. Defaults to false. - * @return {Range} returns the range for chaining. + * + * $('#foo').range().collapse() + * + * @param {Boolean} [toStart] true if to the start of the range, false if to the + * end. Defaults to false. + * @return {jQuery.Range} returns the range for chaining. */ collapse : function(toStart){ this.range.collapse(toStart === undefined ? true : toStart); return this; }, + /** + * Returns the text of the range. + * + * currentText = $.Range.current().toString() + * + * @return {String} the text of the range + */ toString : function(){ return typeof this.range.text == "string" ? this.range.text : this.range.toString(); }, + /** + * Gets or sets the start of the range. + * + * If a value is not provided, start returns the range's starting container and offset like: + * + * $('#foo').range().start() //-> {container: fooElement, offset: 0 } + * + * If a set value is provided, it can set the range. The start of the range is set differently + * depending on the type of set value: + * + * - __Object__ - an object with the new starting container and offset is provided like + * + * $.Range().start({container: $('#foo')[0], offset: 20}) + * + * - __Number__ - the new offset value. The container is kept the same. + * + * - __String__ - adjusts the offset by converting the string offset to a number and adding it to the current + * offset. For example, the following moves the offset forward four characters: + * + * $('#foo').range().start("+4") + * + * + * @param {Object|String|Number} [set] a set value if setting the start of the range or nothing if reading it. + * @return {jQuery.Range|Object} if setting the start, the range is returned for chaining, otherwise, the + * start offset and container are returned. + */ start : function(set){ if(set === undefined){ if(this.range.startContainer){ @@ -199,6 +316,11 @@ $.extend($.Range.prototype,{ }, + /** + * 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) { @@ -208,8 +330,8 @@ $.extend($.Range.prototype,{ } } else { - var end = this.clone().collapse(false).parent(); - var endRange = $.Range(end).select(end).collapse(); + var end = this.clone().collapse(false).parent(), + endRange = $.Range(end).select(end).collapse(); endRange.move("END_TO_END", this); return { container: end, @@ -229,31 +351,44 @@ $.extend($.Range.prototype,{ 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(); - // collapsed ranges don't provide meaningful bounding client rect in safari - // so we'll uncollapse it and get the dimensions - if(!rect.left && !rect.top){ - var range = this.clone(), - end = range.end(), - start = range.start(), - correction = 0; - if (start.offset > 0) { - range.start(start.offset - 1); - rect = range.range.getBoundingClientRect(); - correction = rect.width; - } - else if(end.offset < end.container.length - 1){ - range.end(end.offset+1) - rect = range.range.getBoundingClientRect(); - } - rect = $.extend({}, rect); - rect.left += correction; - rect.width = 0; - } + var rect = this.range.getBoundingClientRect() if(from === 'page'){ var off = scrollOffset(); rect = $.extend({}, rect); @@ -317,13 +452,36 @@ $.extend($.Range.prototype,{ /** * @function compare - * Compares one range to another range. This is different from the spec b/c the spec is confusing. + * Compares one range to another range. + * + * ## Example + * + * // compare the highlight element's start position + * // to the start of the current range + * $('#highlight') + * .range() + * .compare('START_TO_START', $.Range.current()) * - * source.compare("START_TO_END", toRange); * - * This returns -1 if source's start is before toRange's end. - * @param {Object} type - * @param {Object} range + * + * @param {Object} type Specifies the boundry of the + * range and the compareRange to compare. + * + * - START\_TO\_START - the start of the range and the start of compareRange + * - START\_TO\_END - the start of the range and the end of compareRange + * - END\_TO\_END - the end of the range and the end of compareRange + * - END\_TO\_START - the end of the range and the start of compareRange + * + * @param {$.Range} compareRange The other range + * to compare against. + * @return {Number} a number indicating if the range + * boundary is before, + * after, or equal to compareRange's + * boundary where: + * + * - -1 - the range boundary comes before the compareRange boundary + * - 0 - the boundaries are equal + * - 1 - the range boundary comes after the compareRange boundary */ fn.compare = range.compareBoundaryPoints ? function(type, range){ @@ -335,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){ @@ -367,27 +540,69 @@ $.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 +// 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++) { @@ -406,12 +621,26 @@ var iterate = function(elems, cb){ } } } -}, within = function(rect, point){ + +}, +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 -}, withinRect = function(outer, inner){ +}, +// if a rectangle is within another rectangle +withinRect = function(outer, inner){ return within(outer, { clientX: inner.left, clientY: inner.top @@ -428,7 +657,9 @@ var iterate = function(elems, cb){ clientX: inner.left + inner.width, clientY: inner.top + inner.height }) //bottom right -}, scrollOffset = function( win){ +}, +// gets the scroll offset from a window +scrollOffset = function( win){ var win = win ||window; doc = win.document.documentElement, body = win.document.body; @@ -438,6 +669,8 @@ var iterate = function(elems, cb){ }; }; -$("
          ").appendTo(document.body) + +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 a7e0824b..e55246e4 100644 --- a/dom/range/range_test.js +++ b/dom/range/range_test.js @@ -1,5 +1,4 @@ -steal - .plugins("funcunit/qunit", "jquery/dom/range", "jquery/dom/selection").then(function(){ +steal("funcunit/qunit", "jquery/dom/range", "jquery/dom/selection").then(function(){ module("jquery/dom/range"); @@ -12,7 +11,119 @@ test("basic range", function(){ equals(range.end().offset, 5, "end is 5") }); - test("nested range", function(){ + +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); @@ -22,31 +133,29 @@ test("basic range", function(){ }); 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") - console.log(rect) + $("#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") - console.log(start, rect) + $("#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(){ @@ -56,7 +165,20 @@ test("basic range", function(){ var range = $.Range.current(), rects = range.rects(); equals(rects.length, 2, "2 rects found") - console.log(rects) + }); + + 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(){ @@ -69,4 +191,7 @@ test("basic range", function(){ var pos = range1.compare("START_TO_START", range2) equals(pos, -1, "pos works") }); -}); \ No newline at end of file + + +}) + 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..d3edb406 --- /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()) + })); +}); 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/event/selection/test/qunit/selection_test.js b/dom/selection/selection_test.js similarity index 88% rename from event/selection/test/qunit/selection_test.js rename to dom/selection/selection_test.js index 267d05bd..28886444 100644 --- a/event/selection/test/qunit/selection_test.js +++ b/dom/selection/selection_test.js @@ -1,3 +1,5 @@ +steal("funcunit/qunit", "jquery/dom/selection").then(function(){ + module("jquery/dom/selection"); test("getCharElement", function(){ @@ -10,7 +12,7 @@ test("getCharElement", function(){ setTimeout(function(){ var types = ['textarea','#inp','#1','#2']; for(var i =0; i< types.length; i++){ - console.log(types[i]) + //console.log(types[i]) $(types[i]).selection(1,5); } /* @@ -32,4 +34,6 @@ test("getCharElement", function(){ 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/selection/test/qunit/selection_test.js b/dom/selection/test/qunit/selection_test.js deleted file mode 100644 index 267d05bd..00000000 --- a/dom/selection/test/qunit/selection_test.js +++ /dev/null @@ -1,35 +0,0 @@ -module("jquery/dom/selection"); - -test("getCharElement", function(){ - $("#qunit-test-area") - .html(""+ - ""+ - "

            0123456789

            "+ - "
            012
            345
            "); - stop(); - setTimeout(function(){ - var types = ['textarea','#inp','#1','#2']; - for(var i =0; i< types.length; i++){ - console.log(types[i]) - $(types[i]).selection(1,5); - } - /* - $('textarea').selection(1,5); - $('input').selection(1,5); - $('#1').selection(1,5); - $('#2').selection(1,5); - */ - var res = []; - for(var i =0; i< types.length; i++){ - res.push( $(types[i]).selection() ); - } - - - - for(var i =0; i< res.length; i++){ - same(res[i],{start: 1, end: 5},types[i]) - } - - start(); - },1000) -}); \ No newline at end of file diff --git a/dom/within/within.js b/dom/within/within.js index 40eced62..1fc22d47 100644 --- a/dom/within/within.js +++ b/dom/within/within.js @@ -1,47 +1,56 @@ /** * @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 && x >= left && x < left + width); - } + } /** * @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); + var q = $(this); if (this == document.documentElement) { return ret.push(this); } - var offset = useOffsetCache ? - jQuery.data(this,"offsetCache") || jQuery.data(this,"offsetCache", q.offset()) : + var offset = useOffsetCache ? + $.data(this,"offsetCache") || $.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) { ret.push(this); } }); - - return this.pushStack( jQuery.unique( ret ), "within", x+","+y ); + + return this.pushStack( $.unique( ret ), "within", left+","+top ); } /** * @function withinBox + * @parent jQuery.fn.within * returns if elements are within the box * @param {Object} left * @param {Object} top @@ -52,11 +61,15 @@ $.fn.within= function(x, y, useOffsetCache) { $.fn.withinBox = function(left, top, width, height, cache){ var ret = [] this.each(function(){ - var q = jQuery(this); + var q = $(this); if(this == document.documentElement) return this.ret.push(this); - var offset = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + var offset = cache ? + $.data(this,"offset") || + $.data(this,"offset", q.offset()) : + q.offset(); + var ew = q.width(), eh = q.height(); @@ -65,7 +78,7 @@ $.fn.withinBox = function(left, top, width, height, cache){ if(res) ret.push(this); }); - return this.pushStack( jQuery.unique( ret ), "withinBox", jQuery.makeArray(arguments).join(",") ); + return this.pushStack( $.unique( ret ), "withinBox", $.makeArray(arguments).join(",") ); } - -}) \ No newline at end of file + +}); 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
            -
            -