diff --git a/README b/README index 12846a17..f5535b09 100644 --- a/README +++ b/README @@ -10,15 +10,16 @@ A. How to get (and contribute) JMVC http://github.com/jupiterjs/steal and http://github.com/jupiterjs/jquerymx - 3. Add steal and javascriptmvc as submodules of your project... + 3. Add steal and jquerymx as submodules of your project... git submodule add git@github.com:_YOU_/steal.git steal git submodule add git@github.com:_YOU_/jquerymx.git jquery - * Notice javascriptmvc is under the jquery folder + * Notice jquerymx is under the jquery folder 4. Learn a little more about submodules ... http://johnleach.co.uk/words/archives/2008/10/12/323/git-submodules-in-n-easy-steps - 5. Make changes in steal or jmvc, and push them back to your fork. + 5. Make changes in steal or jquerymx, and push them back to your fork. 6. Make a pull request to your fork. + diff --git a/class/class.js b/class/class.js index fd4cd611..b48d1d00 100644 --- a/class/class.js +++ b/class/class.js @@ -2,7 +2,7 @@ // 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-clean steal("jquery","jquery/lang/string",function( $ ) { // =============== HELPERS ================= @@ -59,8 +59,9 @@ steal("jquery","jquery/lang/string",function( $ ) { * @parent jquerymx * @download dist/jquery/jquery.class.js * @test jquery/class/qunit.html + * @description Easy inheritance in JavaScript. * - * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between + * Class provides simulated inheritance in JavaScript. Use Class to bridge the gap between * jQuery's functional programming style and Object Oriented Programming. It * is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] * Inheritance library. Besides prototypal inheritance, it includes a few important features: @@ -348,7 +349,7 @@ steal("jquery","jquery/lang/string",function( $ ) { clss = $.Class = function() { if (arguments.length) { - clss.extend.apply(clss, arguments); + return clss.extend.apply(clss, arguments); } }; @@ -424,13 +425,13 @@ steal("jquery","jquery/lang/string",function( $ ) { // 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), @@ -611,14 +612,14 @@ steal("jquery","jquery/lang/string",function( $ ) { current = getObject(parts.join('.'), window, true), namespace = current; - //@steal-remove-start + //!steal-remove-start if (!Class.nameOk ) { //steal.dev.isHappyName(fullName) } if(current[shortName]){ steal.dev.warn("class.js There's already something called "+fullName) } - //@steal-remove-end + //!steal-remove-end current[shortName] = Class; } @@ -667,7 +668,7 @@ steal("jquery","jquery/lang/string",function( $ ) { // call the class init if ( Class.init ) { - Class.init.apply(Class, args || []); + Class.init.apply(Class, args || concatArgs([_super_class],arguments)); } /* @Prototype*/ @@ -781,4 +782,4 @@ steal("jquery","jquery/lang/string",function( $ ) { proxy = clss.proxy; -})(); \ No newline at end of file +})(); diff --git a/controller/controller.js b/controller/controller.js index 82c4c290..f3bd8476 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -80,6 +80,7 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/controller/controller.js * @test jquery/controller/qunit.html * @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 @@ -222,7 +223,7 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( * el.css("backgroundColor","") * }, * ".create click" : function() { - * this.find("ol").append("<li class='todo'>New Todo</li>"); + * this.find("ol").append("
  • New Todo
  • "); * } * }) * @@ -393,11 +394,11 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( } // make sure listensTo is an array - //@steal-remove-start + //!steal-remove-start if (!isArray(this.listensTo) ) { throw "listensTo is not an array in " + this.fullName; } - //@steal-remove-end + //!steal-remove-end // calculate and cache actions this.actions = {}; @@ -611,7 +612,8 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( var funcName, ready, cls = this[STR_CONSTRUCTOR]; //want the raw element here - element = element.jquery ? element[0] : element; + element = (typeof element == 'string' ? $(element) : + (element.jquery ? element : [element]) )[0]; //set element and className on element var pluginname = cls.pluginName || cls._fullName; @@ -726,7 +728,12 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( * } * } */ - return this.element; + return [this.element, this.options].concat(makeArray(arguments).slice(2)); + /** + * @function init + * + * Implement this. + */ }, /** * Bind attaches event handlers that will be @@ -1079,4 +1086,4 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( }); -}); \ No newline at end of file +}); diff --git a/controller/route/qunit.html b/controller/route/qunit.html new file mode 100644 index 00000000..25edc491 --- /dev/null +++ b/controller/route/qunit.html @@ -0,0 +1,18 @@ + + + + + route QUnit Test + + + + +

    route Test Suite

    +

    +
    +

    +
    +
      +
      + + \ No newline at end of file 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 index 0e0228c4..bc0f1c67 100644 --- a/controller/route/route.js +++ b/controller/route/route.js @@ -8,7 +8,24 @@ steal('jquery/dom/route','jquery/controller', function(){ * @param {Object} selector * @param {Object} cb */ - jQuery.Controller.processors.route = function(el, event, selector, cb){ - + jQuery.Controller.processors.route = function(el, event, selector, funcName, controller){ + $.route(selector||"") + var batchNum; + var check = function(ev, attr, how){ + if($.route.attr('route') === (selector||"") && + (ev.batchNum === undefined || ev.batchNum !== batchNum ) ){ + + batchNum = ev.batchNum; + + var d = $.route.attrs(); + delete d.route; + + controller[funcName](d) + } + } + $.route.bind('change',check); + return function(){ + $.route.unbind('change',check) + } } }) diff --git a/controller/route/route_test.js b/controller/route/route_test.js new file mode 100644 index 00000000..6a140d6b --- /dev/null +++ b/controller/route/route_test.js @@ -0,0 +1,10 @@ +steal('funcunit/qunit','./route',function(){ + +module("route"); + +test("route testing works", function(){ + ok(true,"an assert is run"); +}); + + +}); \ No newline at end of file diff --git a/controller/view/view.js b/controller/view/view.js index 5c05345f..c248fa5c 100644 --- a/controller/view/view.js +++ b/controller/view/view.js @@ -1,4 +1,6 @@ steal('jquery/controller', 'jquery/view').then(function( $ ) { + var URI = steal.URI || steal.File; + jQuery.Controller.getFolder = function() { return jQuery.String.underscore(this.fullName.replace(/\./g, "/")).replace("/Controllers", ""); }; @@ -19,10 +21,10 @@ steal('jquery/controller', 'jquery/view').then(function( $ ) { 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; }; @@ -45,12 +47,14 @@ steal('jquery/controller', 'jquery/view').then(function( $ ) { var current = window; var parts = this.constructor.fullName.split(/\./); for ( var i = 0; i < parts.length; i++ ) { - if ( typeof current.Helpers == 'object' ) { - jQuery.extend(helpers, current.Helpers); + if(current){ + if ( typeof current.Helpers == 'object' ) { + jQuery.extend(helpers, current.Helpers); + } + current = current[parts[i]]; } - current = current[parts[i]]; } - if ( typeof current.Helpers == 'object' ) { + if (current && typeof current.Helpers == 'object' ) { jQuery.extend(helpers, current.Helpers); } this._default_helpers = helpers; diff --git a/dom/dom.js b/dom/dom.js index 9343b5c9..a47d8438 100644 --- a/dom/dom.js +++ b/dom/dom.js @@ -1,6 +1,8 @@ /** @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. diff --git a/dom/fixture/fixture.js b/dom/fixture/fixture.js index 07df8d8d..672b5a7b 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -35,11 +35,14 @@ steal('jquery/dom', var url = settings.fixture; if (/^\/\//.test(url) ) { - url = steal.root.join(settings.fixture.substr(2)); + var sub = settings.fixture.substr(2) + ''; + url = typeof steal === "undefined" ? + url = "/" + sub : + steal.root.mapJoin(sub) +''; } - //@steal-remove-start + //!steal-remove-start steal.dev.log("looking for fixture in " + url); - //@steal-remove-end + //!steal-remove-end settings.url = url; settings.data = null; settings.type = "GET"; @@ -50,9 +53,9 @@ steal('jquery/dom', } }else { - //@steal-remove-start + //!steal-remove-start steal.dev.log("using a dynamic fixture for " +settings.type+" "+ settings.url); - //@steal-remove-end + //!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 @@ -429,9 +432,10 @@ steal('jquery/dom', // gets data from a url like "/todo/{id}" given "todo/5" _getData : function(fixtureUrl, url){ var order = [], - res = new RegExp(fixtureUrl.replace(replacer, function(whole, part){ + fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.').replace('?', '\\?'), + res = new RegExp(fixtureUrlAdjusted.replace(replacer, function(whole, part){ order.push(part) - return "([^\/])+" + return "([^\/]+)" })+"$").exec(url), data = {}; @@ -897,4 +901,6 @@ steal('jquery/dom', * } * */ -}); \ 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 581355c1..7559d1ba 100644 --- a/dom/fixture/fixture_test.js +++ b/dom/fixture/fixture_test.js @@ -1,10 +1,11 @@ + steal("jquery/dom/fixture", "jquery/model",'funcunit/qunit',function(){ module("jquery/dom/fixture"); test("static fixtures", function(){ - stop(3000); + stop(); $.fixture("GET something", "//jquery/dom/fixture/fixtures/test.json"); $.fixture("POST something", "//jquery/dom/fixture/fixtures/test.json"); @@ -40,7 +41,7 @@ test("dynamic fixtures",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){ @@ -76,7 +77,7 @@ test("fixtures with converters", function(){ stop(); $.ajax( { - url : steal.root.join("jquery/dom/fixture/fixtures/foobar.json"), + url : steal.root.join("jquery/dom/fixture/fixtures/foobar.json")+'', dataType: "json fooBar", converters: { "json fooBar": function( data ) { @@ -184,8 +185,15 @@ test("rand", function(){ 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"}, @@ -234,7 +242,7 @@ test("fixture function gets id", function(){ name: "justin" } }) - stop(3000); + stop(); $.get("/thingers/5", {}, function(data){ start(); ok(data.id) @@ -242,7 +250,7 @@ test("fixture function gets id", function(){ }); test("replacing and removing a fixture", function(){ - var url = steal.root.join("jquery/dom/fixture/fixtures/remove.json") + var url = steal.root.join("jquery/dom/fixture/fixtures/remove.json")+'' $.fixture("GET "+url, function(){ return {weird: "ness!"} }) @@ -263,7 +271,7 @@ test("replacing and removing a fixture", function(){ equals(json.weird,"ness","fixture set right"); start(); - }); + },'json'); },'json') @@ -271,8 +279,51 @@ test("replacing and removing a fixture", function(){ },'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/form_params/form_params.js b/dom/form_params/form_params.js index a47ca517..0234c4d4 100644 --- a/dom/form_params/form_params.js +++ b/dom/form_params/form_params.js @@ -2,22 +2,58 @@ * @add jQuery.fn */ steal("jquery/dom").then(function( $ ) { - var radioCheck = /radio|checkbox/i, - keyBreaker = /[^\[\]]+/g, - numberMatcher = /^[\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?$/; + var keyBreaker = /[^\[\]]+/g, + convertValue = function( value ) { + if ( $.isNumeric( value )) { + return parseFloat( value ); + } else if ( value === 'true') { + return true; + } else if ( value === 'false' ) { + return false; + } else if ( value === '' ) { + return undefined; + } + return value; + }, + nestData = function( elem, type, data, parts, value, seen ) { + var name = parts.shift(); - var isNumber = function( value ) { - if ( typeof value == 'number' ) { - return true; - } + if ( parts.length ) { + if ( ! data[ name ] ) { + data[ name ] = {}; + } + // Recursive call + nestData( elem, type, data[ name ], parts, value, seen ); + } else { - if ( typeof value != 'string' ) { - return false; - } + // Handle same name case, as well as "last checkbox checked" + // case + if ( name in seen && type != "radio" && ! $.isArray( data[ name ] )) { + if ( name in data ) { + data[ name ] = [ data[name] ]; + } else { + data[ name ] = []; + } + } else { + seen[ name ] = true; + } + + // Finally, assign data + if ( ( type == "radio" || type == "checkbox" ) && ! elem.is(":checked") ) { + return + } - return value.match(numberMatcher); - }; + if ( ! data[ name ] ) { + data[ name ] = value; + } else { + data[ name ].push( value ); + } + + + } + }; + $.fn.extend({ /** * @parent dom @@ -28,6 +64,9 @@ steal("jquery/dom").then(function( $ ) { * 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> @@ -42,76 +81,99 @@ steal("jquery/dom").then(function( $ ) { * * @demo jquery/dom/form_params/form_params.html * - * @param {Boolean} [convert=false] 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 ) { - return jQuery(jQuery.makeArray(this[0].elements)).getParams(convert); + var convert; + + // Quick way to determine if something is a boolean + if ( !! params === params ) { + convert = params; + params = null; + } + + if ( params ) { + return this.setParams( params ); + } else { + return this.getParams( convert ); } - return jQuery("input[name], textarea[name], select[name]", this[0]).getParams(convert); + }, + setParams: function( params ) { + + // Find all the inputs + this.find("[name]").each(function() { + + var value = params[ $(this).attr("name") ], + $this; + + // Don't do all this work if there's no value + if ( value !== undefined ) { + $this = $(this); + + // Nested these if statements for performance + if ( $this.is(":radio") ) { + if ( $this.val() == value ) { + $this.attr("checked", true); + } + } else if ( $this.is(":checkbox") ) { + // Convert single value to an array to reduce + // complexity + value = $.isArray( value ) ? value : [value]; + if ( $.inArray( $this.val(), value ) > -1) { + $this.attr("checked", true); + } + } else { + $this.val( value ); + } + } + }); }, getParams: function( convert ) { var data = {}, + // This is used to keep track of the checkbox names that we've + // already seen, so we know that we should return an array if + // we see it multiple times. Fixes last checkbox checked bug. + seen = {}, current; - convert = convert === undefined ? false : convert; - this.each(function() { - var el = this, - type = el.type && el.type.toLowerCase(); - //if we are submit, ignore - if ((type == 'submit') || !el.name ) { + this.find("[name]").each(function() { + var $this = $(this), + type = $this.attr("type"), + name = $this.attr("name"), + value = $this.val(), + parts; + + // Don't accumulate submit buttons and nameless elements + if ( type == "submit" || ! name ) { return; } - var key = el.name, - value = $.data(el, "value") || $.fn.val.call([el]), - isRadioCheck = radioCheck.test(el.type), - parts = key.match(keyBreaker), - write = !isRadioCheck || !! el.checked, - //make an array of values - lastPart; - - if ( convert ) { - if ( isNumber(value) ) { - value = parseFloat(value); - } else if ( value === 'true') { - value = true; - } else if ( value === 'false' ) { - value = false; - } - if(value === '') { - value = undefined; - } + // Figure out name parts + parts = name.match( keyBreaker ); + if ( ! parts.length ) { + parts = [name]; } - // go through and create nested objects - current = data; - for ( var i = 0; i < parts.length - 1; i++ ) { - if (!current[parts[i]] ) { - current[parts[i]] = {}; - } - current = current[parts[i]]; + // Convert the value + if ( convert ) { + value = convertValue( value ); } - lastPart = parts[parts.length - 1]; - //now we are on the last part, set the value - if ( lastPart in current && type === "checkbox" ) { - if (!$.isArray(current[lastPart]) ) { - current[lastPart] = current[lastPart] === undefined ? [] : [current[lastPart]]; - } - if ( write ) { - current[lastPart].push(value); - } - } else if ( write || !current[lastPart] ) { - current[lastPart] = write ? value : undefined; - } + // Assign data recursively + nestData( $this, type, data, parts, value, seen ); }); + return data; } }); -}); \ No newline at end of file +}); diff --git a/dom/form_params/form_params_test.js b/dom/form_params/form_params_test.js index 44c2c2b1..2a9e2f3c 100644 --- a/dom/form_params/form_params_test.js +++ b/dom/form_params/form_params_test.js @@ -10,6 +10,7 @@ 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"); @@ -17,12 +18,26 @@ test("with a form", function(){ ok(formParams.params.three === "3","three is right"); same(formParams.params.four,["4","1"],"four is right"); same(formParams.params.five,["2","3"],"five is right"); - equal(typeof formParams.id , 'string', "Id value is empty"); + equal( typeof formParams.singleRadio, "string", "Type of single named radio is string" ); + equal( formParams.singleRadio, "2", "Value of single named radio is right" ); + + ok( $.isArray(formParams.lastOneChecked), "Type of checkbox with last option checked is array" ); + equal( formParams.lastOneChecked, "4", "Value of checkbox with the last option checked is 4" ); }); +test("With a non-form element", function() { + + $("#qunit-test-area").html("//jquery/dom/form_params/test/non-form.micro",{}) + + var formParams = $("#divform").formParams() ; + + equal( formParams.id , "foo-bar-baz", "ID input read correctly" ); + +}); + test("with true false", function(){ $("#qunit-test-area").html("//jquery/dom/form_params/test/truthy.micro",{}); @@ -44,7 +59,15 @@ test("just strings",function(){ 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",{}); @@ -52,4 +75,10 @@ test("missing names",function(){ ok(true, "does not break") }); -}); \ No newline at end of file +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/test/basics.micro b/dom/form_params/test/basics.micro index 5c5b5be8..14e76957 100644 --- a/dom/form_params/test/basics.micro +++ b/dom/form_params/test/basics.micro @@ -11,8 +11,12 @@ + + + + - + + + + + + + - \ No newline at end of file + + + + diff --git a/dom/form_params/test/non-form.micro b/dom/form_params/test/non-form.micro new file mode 100644 index 00000000..aa81d984 --- /dev/null +++ b/dom/form_params/test/non-form.micro @@ -0,0 +1,5 @@ +
      + + + +
      diff --git a/dom/route/route.js b/dom/route/route.js index ef70387f..4159adec 100644 --- a/dom/route/route.js +++ b/dom/route/route.js @@ -1,4 +1,4 @@ -steal('jquery/lang/observe', 'jquery/event/hashchange', 'jquery/lang/string/deparam', 'jquery/lang/observe/delegate', +steal('jquery/lang/observe', 'jquery/event/hashchange', 'jquery/lang/string/deparam', function( $ ) { // Helper methods used for matching routes. @@ -13,13 +13,12 @@ function( $ ) { makeProps = function( props ) { var html = [], name, val; - for ( name in props ) { - val = props[name] + 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. @@ -42,7 +41,11 @@ function( $ ) { }, // onready = true, - location = window.location; + location = window.location, + encode = encodeURIComponent, + decode = decodeURIComponent, + each = $.each, + extend = $.extend; /** * @class jQuery.route @@ -139,7 +142,7 @@ function( $ ) { * Or change multiple properties at once with * [jQuery.Observe.prototype.attrs attrs]: * - * $.route.attrs({type: 'pages', id: 5}, true) + * $.route.attr({type: 'pages', id: 5}, true) * * When you make changes to $.route, they will automatically * change the hash. @@ -204,20 +207,21 @@ function( $ ) { * @param {Object} [defaults] an object of default values * @return {jQuery.route} */ - var $route = $.route = function( url, defaults ) { + $.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) - return "([\\w\\.]*)" // The '\\' is for string-escaping giving single '\' for regEx escaping + // 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] = { + $.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), + 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 @@ -227,10 +231,10 @@ function( $ ) { // The number of parts in the URL separated by '/'. length: url.split('/').length } - return $route; + return $.route; }; - $.extend($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 @@ -244,33 +248,42 @@ function( $ ) { // Check if the provided data keys match the names in any routes; // get the one with the most matches. var route, - matches = -1, - temp, - matchCount; - for ( var name in $route.routes ) { - temp = $route.routes[name], - matchCount = matchesData(temp, data); - if ( matchCount > matches ) { - route = temp; - matches = matchCount - } + // 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), + 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] ? "" : data[name]; + return data[name] === route.defaults[name] ? "" : encode( data[name] ); }), after; - // remove matching default values - for(name in route.defaults) { - if(cpy[name] === route.defaults[name]) { + 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. @@ -291,35 +304,38 @@ function( $ ) { var route = { length: -1 }; - for ( var name in $route.routes ) { - var temp = $route.routes[name] + 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 mathced string; parts contain the variable values. + // 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), + 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); + obj = extend(true, {}, route.defaults, obj); // Overwrite each of the default values in obj with those in parts if that part is not empty. - for ( var p = 0; p < parts.length; p++ ) { - if ( parts[p] ) { - obj[route.names[p]] = parts[p] + 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) ) : {}; }, /** @@ -351,8 +367,15 @@ function( $ ) { /** * 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 + * @return $.route */ ready: function(val) { if( val === false ) { @@ -361,7 +384,7 @@ function( $ ) { if( val === true || onready === true ) { setState(); } - return $route; + return $.route; }, /** * Returns a url from the options @@ -371,9 +394,9 @@ function( $ ) { */ url: function( options, merge ) { if (merge) { - return "#!" + $route.param($.extend({}, curParams, options)) + return "#!" + $.route.param(extend({}, curParams, options)) } else { - return "#!" + $route.param(options) + return "#!" + $.route.param(options) } }, /** @@ -385,35 +408,18 @@ function( $ ) { */ link: function( name, options, props, merge ) { return "" + name + ""; }, /** - * Returns if the options represent the current page. + * Returns true if the options represent the current page. * @param {Object} options + * @return {Boolean} */ current: function( options ) { - return location.hash == "#!" + $route.param(options) - }, - /** - * Change the current page using either a data object or a url string. - * @param {Object|String} loc The object with attributes or hash string. - * @param {Boolean} remove true to remove properties not in loc, only if loc === Object, default true. - * @return $.route Fluent interface. - */ - set: function(loc, remove) { - if ($.isPlainObject( loc )) { - $route.attrs( loc, (typeof remove == "undefined") ? true : remove ); - } else if (typeof loc == "string") { - var pre = ""; - if (loc[0] != '!' && loc[1] != '!') { - pre = '#!'; - } - location.hash = pre + loc; - } - return $route; - } + return location.hash == "#!" + $.route.param(options) + } }); // onready $(function() { @@ -422,19 +428,23 @@ function( $ ) { // 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) + 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, time ) { + throttle = function( func ) { var timer; return function() { + var args = arguments, + self = this; clearTimeout(timer); - timer = setTimeout(func, time || 1); + timer = setTimeout(function(){ + func.apply(self, args) + }, 1); } }, // Intermediate storage for $.route.data. @@ -442,17 +452,11 @@ function( $ ) { // 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() { - // commented out code handles people setting attrs before onready - //if( $.isEmptyObject( $route.data.serialize() ) ) { - 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); - //} else { - // window.location.hash = "#!" + $route.param($route.data.serialize()) - //} - + 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 @@ -461,7 +465,7 @@ function( $ ) { // 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.data.bind("change", throttle(function() { - location.hash = "#!" + $route.param($route.data.serialize()) + $.route.bind("change", throttle(function() { + location.hash = "#!" + $.route.param($.route.serialize()) })); }) \ No newline at end of file diff --git a/dom/route/route_test.js b/dom/route/route_test.js index db048125..f487da94 100644 --- a/dom/route/route_test.js +++ b/dom/route/route_test.js @@ -10,18 +10,21 @@ test("deparam", function(){ var obj = $.route.deparam("jQuery.Controller"); same(obj, { - page : "jQuery.Controller" + page : "jQuery.Controller", + route: ":page" }); obj = $.route.deparam(""); same(obj, { - page : "index" + page : "index", + route: ":page" }); obj = $.route.deparam("jQuery.Controller&where=there"); same(obj, { page : "jQuery.Controller", - where: "there" + where: "there", + route: ":page" }); $.route.routes = {}; @@ -34,7 +37,8 @@ test("deparam", function(){ same(obj, { page : "jQuery.Controller", index: "foo", - where: "there" + where: "there", + route: ":page/:index" }); }) @@ -57,10 +61,24 @@ test("deparam of invalid url", function(){ same(obj, { var1: 'val1', var2: 'val2', - var3: 'val3' + 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",{ @@ -144,19 +162,31 @@ test("param-deparam", function(){ type: "foo" }) - var data = {page: "jQuery.Controller", type: "document", bar: "baz", where: "there"}; + var data = {page: "jQuery.Controller", + type: "document", + bar: "baz", + where: "there"}; var res = $.route.param(data); var obj = $.route.deparam(res); - same(data, obj) - + 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 = {}; @@ -174,12 +204,14 @@ test("precident", function(){ var obj = $.route.deparam("jQuery.Controller"); same(obj, { - who : "jQuery.Controller" + who : "jQuery.Controller", + route: ":who" }); obj = $.route.deparam("search/jQuery.Controller"); same(obj, { - search : "jQuery.Controller" + search : "jQuery.Controller", + route: "search/:search" },"bad deparam"); equal( $.route.param({ @@ -212,4 +244,24 @@ test("linkTo", function(){ equal( res, 'Hello'); }) +test("param with route defined", function(){ + $.route.routes = {}; + $.route("holler") + $.route("foo"); + + var res = $.route.param({foo: "abc",route: "foo"}); + + equal(res, "foo&foo=abc") +}) + +test("route endings", function(){ + $.route.routes = {}; + $.route("foo",{foo: true}); + $.route("food",{food: true}) + + var res = $.route.deparam("food") + ok(res.food, "we get food back") + +}) + }) diff --git a/dom/selection/selection.js b/dom/selection/selection.js index 40d78ce6..45cc2280 100644 --- a/dom/selection/selection.js +++ b/dom/selection/selection.js @@ -195,7 +195,7 @@ getCharElement = function( elems , range, len ) { * returns an object with: * * - __start__ - The number of characters from the start of the element to the start of the selection. - * - __end__ - The number of characters from teh start of the element to the end of the selection. + * - __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: diff --git a/dom/within/within.js b/dom/within/within.js index ca0f4401..818a18e0 100644 --- a/dom/within/within.js +++ b/dom/within/within.js @@ -65,7 +65,11 @@ $.fn.withinBox = function(left, top, width, height, cache){ if(this == document.documentElement) return this.ret.push(this); - var offset = cache ? jQuery.data(this,"offset", q.offset()) : q.offset(); + var offset = cache ? + jQuery.data(this,"offset") || + jQuery.data(this,"offset", q.offset()) : + q.offset(); + var ew = q.width(), eh = q.height(); diff --git a/download/download.html b/download/download.html index 3905efe4..98535d6c 100644 --- a/download/download.html +++ b/download/download.html @@ -343,8 +343,8 @@

      Lang

      - - + +
      Splits a string with a regex correctly cross browser
      diff --git a/event/default/default.js b/event/default/default.js index 4ac8100b..050512ff 100644 --- a/event/default/default.js +++ b/event/default/default.js @@ -1,5 +1,5 @@ -steal('jquery/event', 'jquery/event/handle').then(function($){ +steal('jquery/event').then(function($){ /** * @function jQuery.fn.triggerAsync @@ -20,7 +20,8 @@ steal('jquery/event', 'jquery/event/handle').then(function($){ * $('#other').addClass('error'); * }); * - * triggerAsync is design to work with the [jquery.event.pause] plugin although it is defined in _jquery/event/default_. + * triggerAsync is design to work with the [jquery.event.pause] + * plugin although it is defined in _jquery/event/default_. * * @param {String} type The type of event * @param {Object} data The data for the event @@ -105,114 +106,13 @@ $event.special["default"] = { //save the type types[handleObj.namespace.replace(rnamespaces,"")] = true; - //move the handler ... - var origHandler = handleObj.handler; - handleObj.origHandler = origHandler; - handleObj.handler = function(ev, data){ - if(!ev._defaultActions) ev._defaultActions = []; - ev._defaultActions.push({element: this, handler: origHandler, event: ev, data: data, currentTarget: ev.currentTarget}) - } - }, - setup: function() {return true}, - triggerDefault : function(event, elem, data){ - - var defaultGetter = jQuery.Event("default."+event.type); - - $.extend(defaultGetter,{ - target: elem, - _defaultActions: event._defaultActions, - exclusive : true - }); - - defaultGetter.stopPropagation(); - - //default events only work on elements - if(elem){ - // Event object or event type - var type = defaultGetter.type || event, namespaces = [], exclusive; - - if (type.indexOf("!") >= 0) { - // Exclusive events trigger only for the exact event (no namespaces) - type = type.slice(0, -1); - exclusive = true; - } - - if (type.indexOf(".") >= 0) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - defaultGetter.type = type; - defaultGetter.exclusive = exclusive; - - $event.handle.call(elem, defaultGetter, data); - } }, - checkAndRunDefaults : function(event, elem){ - //fire if there are default actions to run && - // we have not prevented default && - // propagation has been stopped or we are at the document element - // we have reached the document - if (!event.isDefaultPrevented() && - (!event.isPaused || !event.isPaused()) && // no paused function or it's not paused - event._defaultActions && - ( ( event.isPropagationStopped() ) || - ( !elem.parentNode && !elem.ownerDocument ) ) - - ) { - var origNamespace = event.namespace, - origType = event.type, - origLiveFired = event.liveFired; - // put event back - event.namespace= event.type; - event.type = "default"; - event.liveFired = null; - - // call each event handler - for(var i = 0 ; i < event._defaultActions.length; i++){ - var a = event._defaultActions[i], - oldHandle = event.handled; - event.currentTarget = a.currentTarget; - a.handler.call(a.element, event, a.data); - event.handled = event.handled === null ? oldHandle : true; - } - - event._defaultActions = null; //set to null so everyone else on this element ignores it - - if(event._success){ - event._success(event); - } - - event.namespace= origNamespace; - event.type = origType; - event.liveFired = origLiveFired; - - } - } + setup: function() {return true} } // overwrite trigger to allow default types -var oldTrigger = $event.trigger, - triggerDefault = $event.special['default'].triggerDefault, - checkAndRunDefaults = $event.special['default'].checkAndRunDefaults, - oldData = jQuery._data; - -$._data = function(elem, name, data){ - // always need to supply a function to call for handle - if(!data && name === "handle"){ - var func = oldData.apply(this, arguments); - return function(e){ - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.handle.apply( this, arguments ) : - undefined; - } - } - return oldData.apply(this, arguments) -} +var oldTrigger = $event.trigger; $event.trigger = function defaultTriggerer( event, data, elem, onlyHandlers){ // Event object or event type @@ -228,9 +128,26 @@ $event.trigger = function defaultTriggerer( event, data, elem, onlyHandlers){ // Just the event type (string) new jQuery.Event( type ); - event._defaultActions = []; //set depth for possibly reused events + //event._defaultActions = []; //set depth for possibly reused events + + var res = oldTrigger.call($.event, event, data, elem, onlyHandlers); - oldTrigger.call($.event, event, data, elem, onlyHandlers); + + if(!onlyHandlers && !event.isDefaultPrevented() && event.type.indexOf("default") !== 0){ + oldTrigger("default."+event.type, data, elem) + if(event._success){ + event._success(event) + } + } + // code for paused + if( event.isPaused && event.isPaused() ){ + // set back original stuff + event.isDefaultPrevented = + event.pausedState.isDefaultPrevented; + event.isPropagationStopped = + event.pausedState.isPropagationStopped; + } + return res; }; diff --git a/event/default/default_pause_test.js b/event/default/default_pause_test.js index 6b49f415..d61f42a6 100644 --- a/event/default/default_pause_test.js +++ b/event/default/default_pause_test.js @@ -15,6 +15,7 @@ test("default and pause with delegate", function(){ $("#foo").delegate("#bar","show", function(ev){ order.push('show') ev.pause(); + setTimeout(function(){ ev.resume(); @@ -69,6 +70,7 @@ test("triggerAsync", function(){ $("#foo").live("default.show", function(){ order.push("default") }); + $("#foo").live("show", function(ev){ order.push('show') ev.pause(); diff --git a/event/default/default_test.js b/event/default/default_test.js index f593686e..13d65c8f 100644 --- a/event/default/default_test.js +++ b/event/default/default_test.js @@ -30,17 +30,17 @@ test("triggering defaults", function(){ touchNum = (++num) }) $("#touchme1").trigger("touch") - equals(1, count1, "trigger default event") - equals(1, touchNum, "default called second") - equals(2, defaultNum, "default called second") + equals(count1, 1 , "trigger default event") + equals(touchNum, 1, "default called second") + equals(defaultNum, 2, "default called second") + //now prevent + $("#bigwrapper").bind("touch", function(e){ e.preventDefault()}); - //now prevent + $("#touchme1").trigger("touch"); - $("#bigwrapper").bind("touch", function(e){ e.preventDefault()}) - $("#touchme1").trigger("touch") - equals(1, count1, "default event not called") + equals(count1, 1 , "default event not called again"); // breaking equals(3, touchNum, "touch called again") var count2 = 0; diff --git a/event/drag/drag.js b/event/drag/drag.js index d3fba2f5..78f2dfd2 100644 --- a/event/drag/drag.js +++ b/event/drag/drag.js @@ -104,14 +104,14 @@ steal('jquery/event', 'jquery/lang/vector', 'jquery/event/livehack',function( $ //ev.preventDefault(); //create Drag var drag = new $.Drag(), - delegate = ev.liveFired || element, + delegate = ev.delegateTarget || element, selector = ev.handleObj.selector, self = this; this.current = drag; drag.setup({ element: element, - delegate: ev.liveFired || element, + delegate: ev.delegateTarget || element, selector: ev.handleObj.selector, moved: false, _distance: this.distance, diff --git a/event/drop/drop.html b/event/drop/drop.html index 5d4af6c6..439f11f1 100644 --- a/event/drop/drop.html +++ b/event/drop/drop.html @@ -41,33 +41,35 @@

      Dropmove/Dropon

      Drop Count 0 + \ No newline at end of file diff --git a/event/drop/drop.js b/event/drop/drop.js index df624632..792c860a 100644 --- a/event/drop/drop.js +++ b/event/drop/drop.js @@ -1,8 +1,5 @@ -steal('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then(function($){ - var event = $.event, - callHanders = function(){ - - }; +steal('jquery/event/drag','jquery/dom/within','jquery/dom/compare',function($){ + var event = $.event; //somehow need to keep track of elements with selectors on them. When element is removed, somehow we need to know that // /** @@ -368,7 +365,9 @@ steal('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then(functio } }, end: function( event, moveable ) { - var responder, la, endName = this.lowerName+'end'; + var responder, la, + endName = this.lowerName+'end', + dropData; // call dropon //go through the actives ... if you are over one, call dropped on it @@ -380,7 +379,8 @@ steal('jquery/event/drag','jquery/dom/within','jquery/dom/compare').then(functio } // call dropend for(var r =0; r

      HoverLeave

      Leave and don't return for a half second
      + src='../../../steal/steal.js'> \ No newline at end of file diff --git a/event/hover/hover.js b/event/hover/hover.js index 7503455d..2cf9f671 100644 --- a/event/hover/hover.js +++ b/event/hover/hover.js @@ -115,7 +115,7 @@ var event = $.event, handle = event.handle, onmouseenter = function(ev){ //now start checking mousemoves to update location - var delegate = ev.liveFired || ev.currentTarget; + var delegate = ev.delegateTarget || ev.currentTarget; var selector = ev.handleObj.selector; //prevents another mouseenter until current has run its course if($.data(delegate,"_hover"+selector)){ diff --git a/event/hover/hover_test.js b/event/hover/hover_test.js index 322a3343..bd58d1f1 100644 --- a/event/hover/hover_test.js +++ b/event/hover/hover_test.js @@ -27,7 +27,7 @@ test("hovering", function(){ Syn("mouseover",{pageX: off.top, pageY: off.left}, hover[0]) ok(hoverinits, 'hoverinit'); ok(hoverenters === 0,"hoverinit hasn't been called"); - stop(1000); + stop(); setTimeout(function(){ ok(hoverenters === 1,"hoverenter has been called"); diff --git a/event/key/key.js b/event/key/key.js index 386847e3..121d40d3 100644 --- a/event/key/key.js +++ b/event/key/key.js @@ -83,7 +83,7 @@ steal('jquery/event').then(function($){ * listens to and prevents backspaces being pressed in inputs: * * $("input").keypress(function(ev){ - * if(ev.key() == '\b') { + * if(ev.keyName() == '\b') { * ev.preventDefault(); * } * }); @@ -111,7 +111,7 @@ steal('jquery/event').then(function($){ * * @return {String} The string representation of of the key pressed. */ - jQuery.Event.prototype.key = function(){ + jQuery.Event.prototype.keyName = function(){ var event = this, keycode, test = /\w/; diff --git a/event/key/key_test.js b/event/key/key_test.js index 30da8d88..60bed3ca 100644 --- a/event/key/key_test.js +++ b/event/key/key_test.js @@ -6,11 +6,11 @@ test("type some things", function(){ $("#qunit-test-area").append("") var keydown, keypress, keyup; $('#key').keydown(function(ev){ - keydown = ev.key(); + keydown = ev.keyName(); }).keypress(function(ev){ - keypress = ev.key(); + keypress = ev.keyName(); }).keyup(function(ev){ - keyup = ev.key(); + keyup = ev.keyName(); }); stop(); diff --git a/event/livehack/livehack.js b/event/livehack/livehack.js index ec8282ec..774edf11 100644 --- a/event/livehack/livehack.js +++ b/event/livehack/livehack.js @@ -3,8 +3,15 @@ steal('jquery/event').then(function() { var event = jQuery.event, //helper that finds handlers by type and calls back a function, this is basically handle - findHelper = function( events, types, callback ) { - var t, type, typeHandlers, all, h, handle, namespaces, namespace; + // events - the events object + // types - an array of event types to look for + // callback(type, handlerFunc, selector) - a callback + // selector - an optional selector to filter with, if there, matches by selector + // if null, matches anything, otherwise, matches with no selector + findHelper = function( events, types, callback, selector ) { + var t, type, typeHandlers, all, h, handle, + namespaces, namespace, + match; for ( t = 0; t < types.length; t++ ) { type = types[t]; all = type.indexOf(".") < 0; @@ -17,9 +24,24 @@ steal('jquery/event').then(function() { for ( h = 0; h < typeHandlers.length; h++ ) { handle = typeHandlers[h]; - if (!handle.selector && (all || namespace.test(handle.namespace)) ) { - callback(type, handle.origHandler || handle.handler); + + match = (all || namespace.test(handle.namespace)); + + if(match){ + if(selector){ + if (handle.selector === selector ) { + callback(type, handle.origHandler || handle.handler); + } + } else if (selector === null){ + callback(type, handle.origHandler || handle.handler, handle.selector); + } + else if (!handle.selector ) { + callback(type, handle.origHandler || handle.handler); + + } } + + } } }; @@ -32,32 +54,16 @@ steal('jquery/event').then(function() { * @return {Array} an array of event handlers */ event.find = function( el, types, selector ) { - var events = $.data(el, "events"), + var events = ( $._data(el) || {} ).events, handlers = [], t, liver, live; if (!events ) { return handlers; } - - if ( selector ) { - if (!events.live ) { - return []; - } - live = events.live; - - for ( t = 0; t < live.length; t++ ) { - liver = live[t]; - if ( liver.selector === selector && $.inArray(liver.origType, types) !== -1 ) { - handlers.push(liver.origHandler || liver.handler); - } - } - } else { - // basically re-create handler's logic - findHelper(events, types, function( type, handler ) { - handlers.push(handler); - }); - } + findHelper(events, types, function( type, handler ) { + handlers.push(handler); + }, selector); return handlers; }; /** @@ -66,7 +72,7 @@ steal('jquery/event').then(function() { * @param {Array} types event types */ event.findBySelector = function( el, types ) { - var events = $.data(el, "events"), + var events = $._data(el).events, selectors = {}, //adds a handler for a given selector and event add = function( selector, event, handler ) { @@ -79,15 +85,15 @@ steal('jquery/event').then(function() { return selectors; } //first check live: - $.each(events.live || [], function( i, live ) { + /*$.each(events.live || [], function( i, live ) { if ( $.inArray(live.origType, types) !== -1 ) { add(live.selector, live.origType, live.origHandler || live.handler); } - }); + });*/ //then check straight binds - findHelper(events, types, function( type, handler ) { - add("", type, handler); - }); + findHelper(events, types, function( type, handler, selector ) { + add(selector || "", type, handler); + }, null); return selectors; }; diff --git a/event/pause/pause.js b/event/pause/pause.js index f0ded8e0..45d28946 100644 --- a/event/pause/pause.js +++ b/event/pause/pause.js @@ -1,4 +1,4 @@ -steal('jquery/event/livehack', 'jquery/event/handle').then(function($){ +steal('jquery/event/default').then(function($){ var current, @@ -99,134 +99,71 @@ $.Event.prototype.isPaused = returnFalse $.Event.prototype.pause = function(){ - current = this; + // stop the event from continuing temporarily + // keep the current state of the event ... + this.pausedState = { + isDefaultPrevented : this.isDefaultPrevented() ? + returnTrue : returnFalse, + isPropagationStopped : this.isPropagationStopped() ? + returnTrue : returnFalse + }; + this.stopImmediatePropagation(); + this.preventDefault(); this.isPaused = returnTrue; + + + + }; $.Event.prototype.resume = function(){ - this.isPaused = this.isImmediatePropagationStopped = this.isPropagationStopped = returnFalse; - - var el = this.liveFired || this.currentTarget || this.target, - defult = $.event.special['default'], - oldType = this.type; - - // if we were in a 'live' -> run our liveHandler - if(this.handleObj.origHandler){ - var cur = this.currentTarget; - this.currentTarget = this.liveFired; - this.liveFired = undefined; + // temporarily remove all event handlers of this type + var handleObj = this.handleObj, + currentTarget = this.currentTarget; + // temporarily overwrite special handle + var origType = jQuery.event.special[ handleObj.origType ], + origHandle = origType && origType.handle; - liveHandler.call(el, this, cur ); - el = cur; + if(!origType){ + jQuery.event.special[ handleObj.origType ] = {}; } - if(this.isImmediatePropagationStopped()){ - return false; + jQuery.event.special[ handleObj.origType ].handle = function(ev){ + // remove this once we have passed the handleObj + if(ev.handleObj === handleObj && ev.currentTarget === currentTarget){ + if(!origType){ + delete jQuery.event.special[ handleObj.origType ]; + } else { + jQuery.event.special[ handleObj.origType ].handle = origHandle; + } + } } + delete this.pausedState; + // reset stuff + this.isPaused = this.isImmediatePropagationStopped = returnFalse; - // skip the event the first pass because we've already handled it - this.firstPass = true; + + // re-run dispatch + //$.event.dispatch.call(currentTarget, this) + + // with the events removed, dispatch if(!this.isPropagationStopped()){ - $.event.trigger(this, [this.handleObj], el, false); + // fire the event again, no events will get fired until + // same currentTarget / handler + $.event.trigger(this, [], this.target); } }; +/*var oldDispatch = $.event.dispatch; +$.event.dispatch = function(){ + +}*/ +// we need to finish handling -function liveHandler( event, after ) { - var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, - elems = [], - selectors = [], - events = jQuery._data( this, "events" ); - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) - if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { - return; - } - - if ( event.namespace ) { - namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - close = match[i]; - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) && !close.elem.disabled ) { - elem = close.elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - event.type = handleObj.preType; - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - - // Make sure not to accidentally match a child element with the same selector - if ( related && jQuery.contains( elem, related ) ) { - related = elem; - } - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj, level: close.level }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - // inserted to only call elements after this point ... - if(after) { - if(after === match.elem){ - after = undefined; - } - continue; - } - - if ( maxLevel && match.level > maxLevel ) { - break; - } - - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - ret = match.handleObj.origHandler.apply( match.elem, arguments ); - - if ( ret === false || event.isPropagationStopped() ) { - maxLevel = match.level; - - if ( ret === false ) { - stop = false; - } - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } +// and then trigger on next element ... +// can we fake the target ? - return stop; -} }); \ No newline at end of file diff --git a/event/resize/demo.html b/event/resize/demo.html index 00571d78..1cdfa568 100644 --- a/event/resize/demo.html +++ b/event/resize/demo.html @@ -87,19 +87,16 @@

      Folders:

      steal('mxui/data/grid','mxui/layout/split', 'jquery/dom/fixture').then(function(){ - $('#content').mxui_layout_fill({ - parent : $(document.body) - }) + $('#content').mxui_layout_fill(document.body) - $('#split').mxui_layout_fill({ - parent : $('#content') - }).mxui_layout_split({ direction: "vertical", panelClass: "panel" }) + $('#split').mxui_layout_fill('#content') + .mxui_layout_split({ direction: "vertical", panelClass: "panel" }) - $('#vsplit').mxui_layout_fill({ - parent : $('#leftPanel') - }).mxui_layout_split({ direction: "horizontal" }); + + $('#vsplit').mxui_layout_fill($('#leftPanel')) + .mxui_layout_split({ direction: "horizontal" }); diff --git a/event/selection/selection.js b/event/selection/selection.js index eab88323..f98c6080 100644 --- a/event/selection/selection.js +++ b/event/selection/selection.js @@ -12,7 +12,7 @@ steal('jquery/dom/range','jquery/controller','jquery/event/livehack').then(funct event.setupHelper( ["selectionStart","selectionEnd","selectionEnding","selectionMoving","selectionMove"], "mousedown", function(ev){ //now start checking mousemoves to update location - var delegate = ev.liveFired || ev.currentTarget, + var delegate = ev.delegateTarget || ev.currentTarget, selector = ev.handleObj.selector, ready = false, el = this, diff --git a/event/swipe/swipe.js b/event/swipe/swipe.js index 8185b3d7..243e278a 100644 --- a/event/swipe/swipe.js +++ b/event/swipe/swipe.js @@ -1,5 +1,7 @@ steal('jquery/event/livehack').then(function($){ -var supportTouch = "ontouchend" in document, +// TODO remove this, phantom supports touch AND click, but need to make funcunit support touch so its testable +var isPhantom = /Phantom/.test(navigator.userAgent), + supportTouch = !isPhantom && "ontouchend" in document, scrollEvent = "touchmove scroll", touchStartEvent = supportTouch ? "touchstart" : "mousedown", touchStopEvent = supportTouch ? "touchend" : "mouseup", @@ -53,7 +55,7 @@ $.event.setupHelper( [ //listen to mouseup var start = data(ev), stop, - delegate = ev.liveFired || ev.currentTarget, + delegate = ev.delegateTarget || ev.currentTarget, selector = ev.handleObj.selector, entered = this; diff --git a/event/tap/tap.js b/event/tap/tap.js index e0aa2e6f..356cfd30 100644 --- a/event/tap/tap.js +++ b/event/tap/tap.js @@ -22,7 +22,7 @@ $.event.setupHelper( ["tap"], touchStartEvent, function(ev){ //listen to mouseup var start = data(ev), stop, - delegate = ev.liveFired || ev.currentTarget, + delegate = ev.delegateTarget || ev.currentTarget, selector = ev.handleObj.selector, entered = this, moved = false, @@ -32,7 +32,7 @@ $.event.setupHelper( ["tap"], touchStartEvent, function(ev){ function upHandler(event){ stop = data(event); - if ((Math.abs( start.coords[0] - stop.coords[0] ) < 10) || + if ((Math.abs( start.coords[0] - stop.coords[0] ) < 10) && ( Math.abs( start.coords[1] - stop.coords[1] ) < 10) ){ $.each($.event.find(delegate, ["tap"], selector), function(){ this.call(entered, ev, {start : start, end: stop}) diff --git a/generate/app b/generate/app index 3e372c46..aeb906fd 100644 --- a/generate/app +++ b/generate/app @@ -8,14 +8,16 @@ if (!_args[0]) { load('steal/rhino/rhino.js'); steal('steal/generate','steal/generate/system.js',function(steal){ - var md = steal.generate.convert(_args[0]), + var classed = steal.generate.toClass(_args[0]), + md = steal.generate.convert(classed), data = steal.extend({ - path: md.path, - application_name: md.appName, + path: _args[0], + application_name: md.underscore, current_path: steal.File.cwdURL(), - path_to_steal : steal.File(md.path).pathToRoot() - }, steal.system) + path_to_steal : steal.File(_args[0]).pathToRoot() + }, steal.system); - steal.generate("jquery/generate/templates/app", md.path, data); + + steal.generate("jquery/generate/templates/app", _args[0], data); }); diff --git a/generate/templates/app/scripts/build.js.ejs b/generate/templates/app/scripts/build.js.ejs index f7a7d819..bccd83e5 100644 --- a/generate/templates/app/scripts/build.js.ejs +++ b/generate/templates/app/scripts/build.js.ejs @@ -1,4 +1,4 @@ -//steal/js <%= path %>/scripts/compress.js +//js <%= path %>/scripts/build.js load("steal/rhino/rhino.js"); steal('steal/build').then('steal/build/scripts','steal/build/styles',function(){ diff --git a/jquery.js b/jquery.js index 719e1d4e..74ce4119 100644 --- a/jquery.js +++ b/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v1.6.4 + * jQuery JavaScript Library v1.7.1 * http://jquery.com/ * * Copyright 2011, John Resig @@ -11,7 +11,7 @@ * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * - * Date: Mon Sep 12 18:54:48 2011 -0400 + * Date: Mon Nov 21 21:11:03 2011 -0500 */ (function( window, undefined ) { @@ -47,9 +47,6 @@ var jQuery = function( selector, context ) { trimLeft = /^\s+/, trimRight = /\s+$/, - // Check for digits - rdigit = /\d/, - // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, @@ -140,7 +137,7 @@ jQuery.fn = jQuery.prototype = { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; - doc = (context ? context.ownerDocument || context : document); + doc = ( context ? context.ownerDocument || context : document ); // If a single string is passed in and it's a single tag // just do a createElement and skip the rest @@ -157,7 +154,7 @@ jQuery.fn = jQuery.prototype = { } else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes; + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; } return jQuery.merge( this, selector ); @@ -187,7 +184,7 @@ jQuery.fn = jQuery.prototype = { // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); + return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) @@ -201,7 +198,7 @@ jQuery.fn = jQuery.prototype = { return rootjQuery.ready( selector ); } - if (selector.selector !== undefined) { + if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } @@ -213,7 +210,7 @@ jQuery.fn = jQuery.prototype = { selector: "", // The current version of jQuery being used - jquery: "1.6.4", + jquery: "1.7.1", // The default length of a jQuery object is 0 length: 0, @@ -258,7 +255,7 @@ jQuery.fn = jQuery.prototype = { ret.context = this.context; if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; } @@ -279,15 +276,16 @@ jQuery.fn = jQuery.prototype = { jQuery.bindReady(); // Add the callback - readyList.done( fn ); + readyList.add( fn ); return this; }, eq: function( i ) { + i = +i; return i === -1 ? this.slice( i ) : - this.slice( i, +i + 1 ); + this.slice( i, i + 1 ); }, first: function() { @@ -434,11 +432,11 @@ jQuery.extend({ } // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); + readyList.fireWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.trigger ) { - jQuery( document ).trigger( "ready" ).unbind( "ready" ); + jQuery( document ).trigger( "ready" ).off( "ready" ); } } }, @@ -448,7 +446,7 @@ jQuery.extend({ return; } - readyList = jQuery._Deferred(); + readyList = jQuery.Callbacks( "once memory" ); // Catch cases where $(document).ready() is called after the // browser event has already occurred. @@ -504,8 +502,8 @@ jQuery.extend({ return obj && typeof obj === "object" && "setInterval" in obj; }, - isNaN: function( obj ) { - return obj == null || !rdigit.test( obj ) || isNaN( obj ); + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); }, type: function( obj ) { @@ -551,7 +549,7 @@ jQuery.extend({ }, error: function( msg ) { - throw msg; + throw new Error( msg ); }, parseJSON: function( data ) { @@ -573,7 +571,7 @@ jQuery.extend({ .replace( rvalidtokens, "]" ) .replace( rvalidbraces, "")) ) { - return (new Function( "return " + data ))(); + return ( new Function( "return " + data ) )(); } jQuery.error( "Invalid JSON: " + data ); @@ -688,8 +686,6 @@ jQuery.extend({ if ( array != null ) { // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 var type = jQuery.type( array ); @@ -703,18 +699,22 @@ jQuery.extend({ return ret; }, - inArray: function( elem, array ) { - if ( !array ) { - return -1; - } + inArray: function( elem, array, i ) { + var len; - if ( indexOf ) { - return indexOf.call( array, elem ); - } + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } } } @@ -850,7 +850,7 @@ jQuery.extend({ }, now: function() { - return (new Date()).getTime(); + return ( new Date() ).getTime(); }, // Use of jQuery.browser is frowned upon. @@ -957,188 +957,360 @@ return jQuery; })(); -var // Promise methods - promiseMethods = "done fail isResolved isRejected promise then always pipe".split( " " ), - // Static reference to slice - sliceDeferred = [].slice; +// String to Object flags format cache +var flagsCache = {}; -jQuery.extend({ - // Create a simple deferred (one callbacks list) - _Deferred: function() { - var // callbacks list - callbacks = [], - // stored [ context , args ] - fired, - // to avoid firing when already doing so - firing, - // flag to know if the deferred has been cancelled - cancelled, - // the deferred itself - deferred = { - - // done( f1, f2, ...) - done: function() { - if ( !cancelled ) { - var args = arguments, - i, - length, - elem, - type, - _fired; - if ( fired ) { - _fired = fired; - fired = 0; - } - for ( i = 0, length = args.length; i < length; i++ ) { - elem = args[ i ]; - type = jQuery.type( elem ); - if ( type === "array" ) { - deferred.done.apply( deferred, elem ); - } else if ( type === "function" ) { - callbacks.push( elem ); +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } } } - if ( _fired ) { - deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); - } } - return this; - }, - - // resolve with given context and args - resolveWith: function( context, args ) { - if ( !cancelled && !fired && !firing ) { - // make sure args are available (#8421) - args = args || []; - firing = 1; - try { - while( callbacks[ 0 ] ) { - callbacks.shift().apply( context, args ); - } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; } - finally { - fired = [ context, args ]; - firing = 0; + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); } - return this; - }, + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!memory; + } + }; - // resolve with this as context and given arguments - resolve: function() { - deferred.resolveWith( this, arguments ); - return this; - }, + return self; +}; - // Has this deferred been resolved? - isResolved: function() { - return !!( firing || fired ); - }, - // Cancel - cancel: function() { - cancelled = 1; - callbacks = []; - return this; - } - }; - return deferred; - }, - // Full fledged deferred (two callbacks list) +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + Deferred: function( func ) { - var deferred = jQuery._Deferred(), - failDeferred = jQuery._Deferred(), - promise; - // Add errorDeferred methods, then and promise - jQuery.extend( deferred, { - then: function( doneCallbacks, failCallbacks ) { - deferred.done( doneCallbacks ).fail( failCallbacks ); - return this; - }, - always: function() { - return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments ); + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList }, - fail: failDeferred.done, - rejectWith: failDeferred.resolveWith, - reject: failDeferred.resolve, - isRejected: failDeferred.isResolved, - pipe: function( fnDone, fnFail ) { - return jQuery.Deferred(function( newDefer ) { - jQuery.each( { - done: [ fnDone, "resolve" ], - fail: [ fnFail, "reject" ] - }, function( handler, data ) { - var fn = data[ 0 ], - action = data[ 1 ], - returned; - if ( jQuery.isFunction( fn ) ) { - deferred[ handler ](function() { - returned = fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise().then( newDefer.resolve, newDefer.reject ); - } else { - newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); - } - }); - } else { - deferred[ handler ]( newDefer[ action ] ); + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; } - }); - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - if ( obj == null ) { - if ( promise ) { - return promise; } - promise = obj = {}; - } - var i = promiseMethods.length; - while( i-- ) { - obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ]; + return obj; } - return obj; - } - }); - // Make sure only one callback list will be used - deferred.done( failDeferred.cancel ).fail( deferred.cancel ); - // Unexpose cancel - delete deferred.cancel; + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + // Call given func if any if ( func ) { func.call( deferred, deferred ); } + + // All done! return deferred; }, // Deferred helper when: function( firstParam ) { - var args = arguments, + var args = sliceDeferred.call( arguments, 0 ), i = 0, length = args.length, + pValues = new Array( length ), count = length, + pCount = length, deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? firstParam : - jQuery.Deferred(); + jQuery.Deferred(), + promise = deferred.promise(); function resolveFunc( i ) { return function( value ) { args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; if ( !( --count ) ) { - // Strange bug in FF4: - // Values changed onto the arguments object sometimes end up as undefined values - // outside the $.when method. Cloning the object into a fresh array solves the issue - deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) ); + deferred.resolveWith( deferred, args ); } }; } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } if ( length > 1 ) { - for( ; i < length; i++ ) { - if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) { - args[ i ].promise().then( resolveFunc(i), deferred.reject ); + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); } else { --count; } @@ -1149,39 +1321,35 @@ jQuery.extend({ } else if ( deferred !== firstParam ) { deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); } - return deferred.promise(); + return promise; } }); + jQuery.support = (function() { - var div = document.createElement( "div" ), - documentElement = document.documentElement, + var support, all, a, select, opt, input, marginDiv, - support, fragment, - body, - testElementParent, - testElement, - testElementStyle, tds, events, eventName, i, - isSupported; + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; // Preliminary tests div.setAttribute("className", "t"); div.innerHTML = "
      a"; - all = div.getElementsByTagName( "*" ); a = div.getElementsByTagName( "a" )[ 0 ]; @@ -1201,11 +1369,11 @@ jQuery.support = (function() { // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables - tbody: !div.getElementsByTagName( "tbody" ).length, + tbody: !div.getElementsByTagName("tbody").length, // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName( "link" ).length, + htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute // (IE uses .cssText instead) @@ -1213,12 +1381,12 @@ jQuery.support = (function() { // Make sure that URLs aren't manipulated // (IE normalizes it by default) - hrefNormalized: ( a.getAttribute( "href" ) === "/a" ), + hrefNormalized: ( a.getAttribute("href") === "/a" ), // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), + opacity: /^0.55/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) @@ -1236,6 +1404,13 @@ jQuery.support = (function() { // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) getSetAttribute: div.className !== "t", + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + // Will be defined later submitBubbles: true, changeBubbles: true, @@ -1273,7 +1448,7 @@ jQuery.support = (function() { div.cloneNode( true ).fireEvent( "onclick" ); } - // Check if a radio maintains it's value + // Check if a radio maintains its value // after being appended to the DOM input = document.createElement("input"); input.value = "t"; @@ -1283,82 +1458,18 @@ jQuery.support = (function() { input.setAttribute("checked", "checked"); div.appendChild( input ); fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); + fragment.appendChild( div.lastChild ); // WebKit doesn't clone checked state correctly in fragments support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - div.innerHTML = ""; - - // Figure out if the W3C box model works as expected - div.style.width = div.style.paddingLeft = "1px"; - - body = document.getElementsByTagName( "body" )[ 0 ]; - // We use our own, invisible, body unless the body is already present - // in which case we use a div (#9239) - testElement = document.createElement( body ? "div" : "body" ); - testElementStyle = { - visibility: "hidden", - width: 0, - height: 0, - border: 0, - margin: 0, - background: "none" - }; - if ( body ) { - jQuery.extend( testElementStyle, { - position: "absolute", - left: "-1000px", - top: "-1000px" - }); - } - for ( i in testElementStyle ) { - testElement.style[ i ] = testElementStyle[ i ]; - } - testElement.appendChild( div ); - testElementParent = body || documentElement; - testElementParent.insertBefore( testElement, testElementParent.firstChild ); - // Check if a disconnected checkbox will retain its checked // value of true after appended to the DOM (IE6/7) support.appendChecked = input.checked; - support.boxModel = div.offsetWidth === 2; - - if ( "zoom" in div.style ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.style.display = "inline"; - div.style.zoom = 1; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); - - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = ""; - div.innerHTML = "
      "; - support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); - } - - div.innerHTML = "
      t
      "; - tds = div.getElementsByTagName( "td" ); - - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - // (only IE 8 fails this test) - isSupported = ( tds[ 0 ].offsetHeight === 0 ); + fragment.removeChild( input ); + fragment.appendChild( div ); - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Check if empty table cells still have offsetWidth/Height - // (IE < 8 fail this test) - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); div.innerHTML = ""; // Check if div with explicit width and no margin-right incorrectly @@ -1366,21 +1477,18 @@ jQuery.support = (function() { // info see bug #3333 // Fails in WebKit before Feb 2011 nightlies // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - if ( document.defaultView && document.defaultView.getComputedStyle ) { + if ( window.getComputedStyle ) { marginDiv = document.createElement( "div" ); marginDiv.style.width = "0"; marginDiv.style.marginRight = "0"; + div.style.width = "2px"; div.appendChild( marginDiv ); support.reliableMarginRight = - ( parseInt( ( document.defaultView.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; } - // Remove the body element we added - testElement.innerHTML = ""; - testElementParent.removeChild( testElement ); - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ // We only care about the case where non-standard event systems // are used, namely in IE. Short-circuiting here helps us to // avoid an eval call (in setAttribute) which can cause CSP @@ -1390,7 +1498,7 @@ jQuery.support = (function() { submit: 1, change: 1, focusin: 1 - } ) { + }) { eventName = "on" + i; isSupported = ( eventName in div ); if ( !isSupported ) { @@ -1401,15 +1509,111 @@ jQuery.support = (function() { } } - // Null connected elements to avoid leaks in IE - testElement = fragment = select = opt = body = marginDiv = div = input = null; + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = marginDiv = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + conMarginTop, ptlm, vb, style, html, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; + vb = "visibility:hidden;border:0;"; + style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; + html = "
      " + + "" + + "
      "; + + container = document.createElement("div"); + container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
      t
      "; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Figure out if the W3C box model works as expected + div.innerHTML = ""; + div.style.width = div.style.paddingLeft = "1px"; + jQuery.boxModel = support.boxModel = div.offsetWidth === 2; + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
      "; + support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); + } + + div.style.cssText = ptlm + vb; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + body.removeChild( container ); + div = container = null; + + jQuery.extend( support, offsetSupport ); + }); return support; })(); -// Keep track of boxModel -jQuery.boxModel = jQuery.support.boxModel; - @@ -1437,7 +1641,6 @@ jQuery.extend({ hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); }, @@ -1446,7 +1649,7 @@ jQuery.extend({ return; } - var thisCache, ret, + var privateCache, thisCache, ret, internalKey = jQuery.expando, getByName = typeof name === "string", @@ -1460,11 +1663,12 @@ jQuery.extend({ // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all - if ( (!id || (pvt && id && (cache[ id ] && !cache[ id ][ internalKey ]))) && getByName && data === undefined ) { + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { return; } @@ -1472,18 +1676,17 @@ jQuery.extend({ // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { - elem[ jQuery.expando ] = id = ++jQuery.uuid; + elem[ internalKey ] = id = ++jQuery.uuid; } else { - id = jQuery.expando; + id = internalKey; } } if ( !cache[ id ] ) { cache[ id ] = {}; - // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery - // metadata on plain JS objects when the object is serialized using - // JSON.stringify + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify if ( !isNode ) { cache[ id ].toJSON = jQuery.noop; } @@ -1493,34 +1696,33 @@ jQuery.extend({ // shallow copied over onto the existing cache if ( typeof name === "object" || typeof name === "function" ) { if ( pvt ) { - cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); + cache[ id ] = jQuery.extend( cache[ id ], name ); } else { - cache[ id ] = jQuery.extend(cache[ id ], name); + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } - thisCache = cache[ id ]; + privateCache = thisCache = cache[ id ]; - // Internal jQuery data is stored in a separate object inside the object's data + // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined - // data - if ( pvt ) { - if ( !thisCache[ internalKey ] ) { - thisCache[ internalKey ] = {}; + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; } - thisCache = thisCache[ internalKey ]; + thisCache = thisCache.data; } if ( data !== undefined ) { thisCache[ jQuery.camelCase( name ) ] = data; } - // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should - // not attempt to inspect the internal events object using jQuery.data, as this - // internal data object is undocumented and subject to change. - if ( name === "events" && !thisCache[name] ) { - return thisCache[ internalKey ] && thisCache[ internalKey ].events; + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; } // Check for both converted-to-camel and non-converted data property names @@ -1548,7 +1750,7 @@ jQuery.extend({ return; } - var thisCache, + var thisCache, i, l, // Reference to internal data cache key internalKey = jQuery.expando, @@ -1559,7 +1761,7 @@ jQuery.extend({ cache = isNode ? jQuery.cache : elem, // See jQuery.data for more information - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + id = isNode ? elem[ internalKey ] : internalKey; // If there is already no cache entry for this object, there is no // purpose in continuing @@ -1569,28 +1771,43 @@ jQuery.extend({ if ( name ) { - thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; + thisCache = pvt ? cache[ id ] : cache[ id ].data; if ( thisCache ) { - // Support interoperable removal of hyphenated or camelcased keys - if ( !thisCache[ name ] ) { - name = jQuery.camelCase( name ); + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } } - delete thisCache[ name ]; + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } // If there is no data left in the cache, we want to continue // and let the cache object itself get destroyed - if ( !isEmptyDataObject(thisCache) ) { + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { return; } } } // See jQuery.data for more information - if ( pvt ) { - delete cache[ id ][ internalKey ]; + if ( !pvt ) { + delete cache[ id ].data; // Don't destroy the parent cache unless the internal data object // had been the only thing left in it @@ -1599,8 +1816,6 @@ jQuery.extend({ } } - var internalCache = cache[ id ][ internalKey ]; - // Browsers that fail expando deletion also refuse to delete expandos on // the window, but it will allow it on all other JS objects; other browsers // don't care @@ -1611,32 +1826,18 @@ jQuery.extend({ cache[ id ] = null; } - // We destroyed the entire user cache at once because it's faster than - // iterating through each key, but we need to continue to persist internal - // data if it existed - if ( internalCache ) { - cache[ id ] = {}; - // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery - // metadata on plain JS objects when the object is serialized using - // JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - - cache[ id ][ internalKey ] = internalCache; - - // Otherwise, we need to eliminate the expando on the node to avoid + // We destroyed the cache and need to eliminate the expando on the node to avoid // false lookups in the cache for entries that no longer exist - } else if ( isNode ) { + if ( isNode ) { // IE does not allow us to delete expando properties from nodes, // nor does it have a removeAttribute function on Document nodes; // we must handle all of these cases if ( jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; + delete elem[ internalKey ]; } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); + elem.removeAttribute( internalKey ); } else { - elem[ jQuery.expando ] = null; + elem[ internalKey ] = null; } } }, @@ -1662,14 +1863,15 @@ jQuery.extend({ jQuery.fn.extend({ data: function( key, value ) { - var data = null; + var parts, attr, name, + data = null; if ( typeof key === "undefined" ) { if ( this.length ) { data = jQuery.data( this[0] ); - if ( this[0].nodeType === 1 ) { - var attr = this[0].attributes, name; + if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { + attr = this[0].attributes; for ( var i = 0, l = attr.length; i < l; i++ ) { name = attr[i].name; @@ -1679,6 +1881,7 @@ jQuery.fn.extend({ dataAttr( this[0], name, data[ name ] ); } } + jQuery._data( this[0], "parsedAttrs", true ); } } @@ -1690,7 +1893,7 @@ jQuery.fn.extend({ }); } - var parts = key.split("."); + parts = key.split("."); parts[1] = parts[1] ? "." + parts[1] : ""; if ( value === undefined ) { @@ -1708,12 +1911,12 @@ jQuery.fn.extend({ } else { return this.each(function() { - var $this = jQuery( this ), + var self = jQuery( this ), args = [ parts[0], value ]; - $this.triggerHandler( "setData" + parts[1] + "!", args ); + self.triggerHandler( "setData" + parts[1] + "!", args ); jQuery.data( this, key, value ); - $this.triggerHandler( "changeData" + parts[1] + "!", args ); + self.triggerHandler( "changeData" + parts[1] + "!", args ); }); } }, @@ -1739,7 +1942,7 @@ function dataAttr( elem, key, data ) { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : - !jQuery.isNaN( data ) ? parseFloat( data ) : + jQuery.isNumeric( data ) ? parseFloat( data ) : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} @@ -1755,11 +1958,14 @@ function dataAttr( elem, key, data ) { return data; } -// TODO: This is a hack for 1.5 ONLY to allow objects with a single toJSON -// property to be considered empty objects; this property always exists in -// order to make sure JSON.stringify does not expose internal metadata +// checks a cache object for emptiness function isEmptyDataObject( obj ) { for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } if ( name !== "toJSON" ) { return false; } @@ -1775,17 +1981,17 @@ function handleQueueMarkDefer( elem, type, src ) { var deferDataKey = type + "defer", queueDataKey = type + "queue", markDataKey = type + "mark", - defer = jQuery.data( elem, deferDataKey, undefined, true ); + defer = jQuery._data( elem, deferDataKey ); if ( defer && - ( src === "queue" || !jQuery.data( elem, queueDataKey, undefined, true ) ) && - ( src === "mark" || !jQuery.data( elem, markDataKey, undefined, true ) ) ) { + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { // Give room for hard-coded callbacks to fire first // and eventually mark/queue something else on the element setTimeout( function() { - if ( !jQuery.data( elem, queueDataKey, undefined, true ) && - !jQuery.data( elem, markDataKey, undefined, true ) ) { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { jQuery.removeData( elem, deferDataKey, true ); - defer.resolve(); + defer.fire(); } }, 0 ); } @@ -1795,8 +2001,8 @@ jQuery.extend({ _mark: function( elem, type ) { if ( elem ) { - type = (type || "fx") + "mark"; - jQuery.data( elem, type, (jQuery.data(elem,type,undefined,true) || 0) + 1, true ); + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); } }, @@ -1809,9 +2015,9 @@ jQuery.extend({ if ( elem ) { type = type || "fx"; var key = type + "mark", - count = force ? 0 : ( (jQuery.data( elem, key, undefined, true) || 1 ) - 1 ); + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); if ( count ) { - jQuery.data( elem, key, count, true ); + jQuery._data( elem, key, count ); } else { jQuery.removeData( elem, key, true ); handleQueueMarkDefer( elem, type, "mark" ); @@ -1820,13 +2026,15 @@ jQuery.extend({ }, queue: function( elem, type, data ) { + var q; if ( elem ) { - type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type, undefined, true ); + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !q || jQuery.isArray(data) ) { - q = jQuery.data( elem, type, jQuery.makeArray(data), true ); + q = jQuery._data( elem, type, jQuery.makeArray(data) ); } else { q.push( data ); } @@ -1840,7 +2048,7 @@ jQuery.extend({ var queue = jQuery.queue( elem, type ), fn = queue.shift(), - defer; + hooks = {}; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { @@ -1851,16 +2059,17 @@ jQuery.extend({ // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { - queue.unshift("inprogress"); + queue.unshift( "inprogress" ); } - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); } if ( !queue.length ) { - jQuery.removeData( elem, type + "queue", true ); + jQuery.removeData( elem, type + "queue " + type + ".run", true ); handleQueueMarkDefer( elem, type, "queue" ); } } @@ -1892,14 +2101,14 @@ jQuery.fn.extend({ // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; }); }, clearQueue: function( type ) { @@ -1930,9 +2139,9 @@ jQuery.fn.extend({ if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && - jQuery.data( elements[ i ], deferDataKey, jQuery._Deferred(), true ) )) { + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { count++; - tmp.done( resolve ); + tmp.add( resolve ); } } resolve(); @@ -1950,7 +2159,8 @@ var rclass = /[\n\t\r]/g, rfocusable = /^(?:button|input|object|select|textarea)$/i, rclickable = /^a(?:rea)?$/i, rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, - nodeHook, boolHook; + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; jQuery.fn.extend({ attr: function( name, value ) { @@ -1962,11 +2172,11 @@ jQuery.fn.extend({ jQuery.removeAttr( this, name ); }); }, - + prop: function( name, value ) { return jQuery.access( this, name, value, true, jQuery.prop ); }, - + removeProp: function( name ) { name = jQuery.propFix[ name ] || name; return this.each(function() { @@ -2025,7 +2235,7 @@ jQuery.fn.extend({ } if ( (value && typeof value === "string") || value === undefined ) { - classNames = (value || "").split( rspace ); + classNames = ( value || "" ).split( rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; @@ -2086,8 +2296,10 @@ jQuery.fn.extend({ }, hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { return true; } @@ -2097,9 +2309,9 @@ jQuery.fn.extend({ }, val: function( value ) { - var hooks, ret, + var hooks, ret, isFunction, elem = this[0]; - + if ( !arguments.length ) { if ( elem ) { hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; @@ -2110,17 +2322,17 @@ jQuery.fn.extend({ ret = elem.value; - return typeof ret === "string" ? + return typeof ret === "string" ? // handle most common string cases - ret.replace(rreturn, "") : + ret.replace(rreturn, "") : // handle cases where value is null/undef or number ret == null ? "" : ret; } - return undefined; + return; } - var isFunction = jQuery.isFunction( value ); + isFunction = jQuery.isFunction( value ); return this.each(function( i ) { var self = jQuery(this), val; @@ -2168,7 +2380,7 @@ jQuery.extend({ }, select: { get: function( elem ) { - var value, + var value, i, max, option, index = elem.selectedIndex, values = [], options = elem.options, @@ -2180,8 +2392,10 @@ jQuery.extend({ } // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; // Don't return options that are disabled or in a disabled optgroup if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && @@ -2233,18 +2447,14 @@ jQuery.extend({ height: true, offset: true }, - - attrFix: { - // Always normalize to ensure hook usage - tabindex: "tabIndex" - }, - + attr: function( elem, name, value, pass ) { - var nType = elem.nodeType; - + var ret, hooks, notxml, + nType = elem.nodeType; + // don't get/set attributes on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return undefined; + return; } if ( pass && name in jQuery.attrFn ) { @@ -2252,36 +2462,24 @@ jQuery.extend({ } // Fallback to prop when attributes are not supported - if ( !("getAttribute" in elem) ) { + if ( typeof elem.getAttribute === "undefined" ) { return jQuery.prop( elem, name, value ); } - var ret, hooks, - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - // Normalize the name if needed + // All attributes are lowercase + // Grab necessary hook if one is defined if ( notxml ) { - name = jQuery.attrFix[ name ] || name; - - hooks = jQuery.attrHooks[ name ]; - - if ( !hooks ) { - // Use boolHook for boolean attributes - if ( rboolean.test( name ) ) { - hooks = boolHook; - - // Use nodeHook if available( IE6/7 ) - } else if ( nodeHook ) { - hooks = nodeHook; - } - } + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); - return undefined; + return; } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; @@ -2305,17 +2503,29 @@ jQuery.extend({ } }, - removeAttr: function( elem, name ) { - var propName; - if ( elem.nodeType === 1 ) { - name = jQuery.attrFix[ name ] || name; + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; - jQuery.attr( elem, name, "" ); - elem.removeAttribute( name ); + // See #9699 for explanation of this approach (setting first, then removal) + jQuery.attr( elem, name, "" ); + elem.removeAttribute( getSetAttribute ? name : propName ); - // Set corresponding property to false for boolean attributes - if ( rboolean.test( name ) && (propName = jQuery.propFix[ name ] || name) in elem ) { - elem[ propName ] = false; + // Set corresponding property to false for boolean attributes + if ( rboolean.test( name ) && propName in elem ) { + elem[ propName ] = false; + } + } } } }, @@ -2374,17 +2584,17 @@ jQuery.extend({ frameborder: "frameBorder", contenteditable: "contentEditable" }, - + prop: function( elem, name, value ) { - var nType = elem.nodeType; + var ret, hooks, notxml, + nType = elem.nodeType; // don't get/set properties on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return undefined; + return; } - var ret, hooks, - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); if ( notxml ) { // Fix name and attach hooks @@ -2397,7 +2607,7 @@ jQuery.extend({ return ret; } else { - return (elem[ name ] = value); + return ( elem[ name ] = value ); } } else { @@ -2409,7 +2619,7 @@ jQuery.extend({ } } }, - + propHooks: { tabIndex: { get: function( elem ) { @@ -2427,16 +2637,17 @@ jQuery.extend({ } }); -// Add the tabindex propHook to attrHooks for back-compat -jQuery.attrHooks.tabIndex = jQuery.propHooks.tabIndex; +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; // Hook for boolean attributes boolHook = { get: function( elem, name ) { // Align boolean attributes with corresponding properties // Fall back to attribute presence where some booleans are not supported - var attrNode; - return jQuery.prop( elem, name ) === true || ( attrNode = elem.getAttributeNode( name ) ) && attrNode.nodeValue !== false ? + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? name.toLowerCase() : undefined; }, @@ -2461,16 +2672,20 @@ boolHook = { }; // IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !jQuery.support.getSetAttribute ) { - +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true + }; + // Use this for any attribute in IE6/7 // This fixes almost every IE6/7 issue nodeHook = jQuery.valHooks.button = { get: function( elem, name ) { var ret; ret = elem.getAttributeNode( name ); - // Return undefined if nodeValue is empty string - return ret && ret.nodeValue !== "" ? + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? ret.nodeValue : undefined; }, @@ -2481,10 +2696,13 @@ if ( !jQuery.support.getSetAttribute ) { ret = document.createAttribute( name ); elem.setAttributeNode( ret ); } - return (ret.nodeValue = value + ""); + return ( ret.nodeValue = value + "" ); } }; + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) // This is for removals jQuery.each([ "width", "height" ], function( i, name ) { @@ -2497,6 +2715,18 @@ if ( !jQuery.support.getSetAttribute ) { } }); }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; } @@ -2520,7 +2750,7 @@ if ( !jQuery.support.style ) { return elem.style.cssText.toLowerCase() || undefined; }, set: function( elem, value ) { - return (elem.style.cssText = "" + value); + return ( elem.style.cssText = "" + value ); } }; } @@ -2545,6 +2775,11 @@ if ( !jQuery.support.optSelected ) { }); } +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + // Radios and checkboxes getter/setter if ( !jQuery.support.checkOn ) { jQuery.each([ "radio", "checkbox" ], function() { @@ -2560,7 +2795,7 @@ jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { - return (elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0); + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } }); @@ -2569,116 +2804,118 @@ jQuery.each([ "radio", "checkbox" ], function() { -var rnamespaces = /\.(.*)$/, - rformElems = /^(?:textarea|input|select)$/i, - rperiod = /\./g, - rspaces = / /g, - rescape = /[^\w\s.|`]/g, - fcleanup = function( nm ) { - return nm.replace(rescape, "\\$&"); +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /\bhover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); }; /* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; - if ( handler === false ) { - handler = returnFalse; - } else if ( !handler ) { - // Fixes bug #7229. Fix recommended by jdalton + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { return; } - var handleObjIn, handleObj; - + // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; } - // Make sure that the function being executed has a unique ID + // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } - // Init the element's event structure - var elemData = jQuery._data( elem ); - - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements - if ( !elemData ) { - return; - } - - var events = elemData.events, - eventHandle = elemData.handle; - + // Init the element's event structure and main handler, if this is the first + events = elemData.events; if ( !events ) { elemData.events = events = {}; } - + eventHandle = elemData.handle; if ( !eventHandle ) { elemData.handle = eventHandle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; } - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; - // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { - var type, i = 0, namespaces; + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; - - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); - - } else { - namespaces = []; - handleObj.namespace = ""; - } + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; - handleObj.type = type; - if ( !handleObj.guid ) { - handleObj.guid = handler.guid; - } + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; - // Init the event handler queue + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; if ( !handlers ) { handlers = events[ type ] = []; + handlers.delegateCount = 0; - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false + // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { @@ -2698,10 +2935,14 @@ jQuery.event = { } } - // Add the function to the element's handler list - handlers.push( handleObj ); + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } - // Keep track of which events have been used, for event optimization + // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } @@ -2712,129 +2953,80 @@ jQuery.event = { global: {}, // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - if ( handler === false ) { - handler = returnFalse; - } - - var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ), - events = elemData && elemData.events; - - if ( !elemData || !events ) { - return; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } + remove: function( elem, types, handler, selector, mappedTypes ) { - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; - - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + if ( !elemData || !(events = elemData.events) ) { return; } - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } - continue; } special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - for ( j = pos || 0; j < eventType.length; j++ ) { + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } + if ( handleObj.selector ) { + eventType.delegateCount--; } - - if ( pos != null ) { - break; + if ( special.remove ) { + special.remove.call( elem, handleObj ); } } } - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } - ret = null; delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; + handle = elemData.handle; if ( handle ) { handle.elem = null; } - delete elemData.events; - delete elemData.handle; - - if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem, undefined, true ); - } + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); } }, - + // Events that are safe to short-circuit if no handlers are attached. // Native DOM events should not be added, they may have inline handlers. customEvent: { @@ -2844,18 +3036,28 @@ jQuery.event = { }, trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + // Event object or event type var type = event.type || event, namespaces = [], - exclusive; + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } - if ( type.indexOf("!") >= 0 ) { + if ( type.indexOf( "!" ) >= 0 ) { // Exclusive events trigger only for the exact event (no namespaces) type = type.slice(0, -1); exclusive = true; } - if ( type.indexOf(".") >= 0 ) { + if ( type.indexOf( "." ) >= 0 ) { // Namespaced trigger; create a regexp to match event type in handle() namespaces = type.split("."); type = namespaces.shift(); @@ -2877,230 +3079,299 @@ jQuery.event = { new jQuery.Event( type ); event.type = type; + event.isTrigger = true; event.exclusive = exclusive; - event.namespace = namespaces.join("."); - event.namespace_re = new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)"); - - // triggerHandler() and global events don't bubble or run the default action - if ( onlyHandlers || !elem ) { - event.preventDefault(); - event.stopPropagation(); - } + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; // Handle a global trigger if ( !elem ) { + // TODO: Stop taunting the data cache; remove global events and always attach to document - jQuery.each( jQuery.cache, function() { - // internalKey variable is just used to make it easier to find - // and potentially change this stuff later; currently it just - // points to jQuery.expando - var internalKey = jQuery.expando, - internalCache = this[ internalKey ]; - if ( internalCache && internalCache.events && internalCache.events[ type ] ) { - jQuery.event.trigger( event, data, internalCache.handle.elem ); + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); } - }); - return; - } - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + } return; } // Clean up the event in case it is being reused event.result = undefined; - event.target = elem; + if ( !event.target ) { + event.target = elem; + } // Clone any incoming data and prepend the event, creating the handler arg list data = data != null ? jQuery.makeArray( data ) : []; data.unshift( event ); - var cur = elem, - // IE doesn't like method names with a colon (#3533, #8272) - ontype = type.indexOf(":") < 0 ? "on" + type : ""; + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { - // Fire event on the current element, then bubble up the DOM tree - do { - var handle = jQuery._data( cur, "handle" ); + cur = eventPath[i][0]; + event.type = eventPath[i][1]; - event.currentTarget = cur; + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } - - // Trigger an inline bound script - if ( ontype && jQuery.acceptData( cur ) && cur[ ontype ] && cur[ ontype ].apply( cur, data ) === false ) { - event.result = false; + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { event.preventDefault(); } - - // Bubble up to document, then to window - cur = cur.parentNode || cur.ownerDocument || cur === event.target.ownerDocument && window; - } while ( cur && !event.isPropagationStopped() ); + } + event.type = type; // If nobody prevented the default action, do it now - if ( !event.isDefaultPrevented() ) { - var old, - special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && !event.isDefaultPrevented() ) { - if ( (!special._default || special._default.call( elem.ownerDocument, event ) === false) && + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction)() check here because IE6/7 fails that test. - // IE<9 dies on focus to hidden element (#1486), may want to revisit a try/catch. - try { - if ( ontype && elem[ type ] ) { - // Don't re-trigger an onFOO event when we call its FOO() method - old = elem[ ontype ]; + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { - if ( old ) { - elem[ ontype ] = null; - } + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; - jQuery.event.triggered = type; - elem[ type ](); + if ( old ) { + elem[ ontype ] = null; } - } catch ( ieError ) {} - if ( old ) { - elem[ ontype ] = old; - } + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; - jQuery.event.triggered = undefined; + if ( old ) { + elem[ ontype ] = old; + } + } } } - + return event.result; }, - handle: function( event ) { + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event || window.event ); - // Snapshot the handlers list since a called handler may add/remove events. - var handlers = ((jQuery._data( this, "events" ) || {})[ event.type ] || []).slice(0), + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), run_all = !event.exclusive && !event.namespace, - args = Array.prototype.slice.call( arguments, 0 ); + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; - // Use the fix-ed Event rather than the (read-only) native event + // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; - event.currentTarget = this; - - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; - - // Triggered event must 1) be non-exclusive and have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event. - if ( run_all || event.namespace_re.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; - event.data = handleObj.data; - event.handleObj = handleObj; - - var ret = handleObj.handler.apply( this, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); + event.delegateTarget = this; + + // Determine handlers that should run if there are delegated events + // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); } } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } - if ( event.isImmediatePropagationStopped() ) { - break; + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } } } } + return event.result; }, - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + event = jQuery.Event( originalEvent ); - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; + for ( i = copy.length; i; ) { + prop = copy[ --i ]; event[ prop ] = originalEvent[ prop ]; } - // Fix target property, if necessary + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) if ( !event.target ) { - // Fixes #1925 where srcElement might not be defined either - event.target = event.srcElement || document; + event.target = originalEvent.srcElement || document; } - // check if target is a textnode (safari) + // Target should not be a text node (#504, Safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var eventDocument = event.target.ownerDocument || document, - doc = eventDocument.documentElement, - body = eventDocument.body; - - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { - event.which = event.charCode != null ? event.charCode : event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { event.metaKey = event.ctrlKey; } - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; }, - // Deprecated, use jQuery.guid instead - guid: 1E8, - - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, - special: { ready: { // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop + setup: jQuery.bindReady }, - live: { - add: function( handleObj ) { - jQuery.event.add( this, - liveConvert( handleObj.origType, handleObj.selector ), - jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); - }, + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, - remove: function( handleObj ) { - jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); - } + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" }, beforeunload: { @@ -3117,9 +3388,35 @@ jQuery.event = { } } } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } } }; +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { @@ -3134,7 +3431,7 @@ jQuery.removeEvent = document.removeEventListener ? jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { + if ( !(this instanceof jQuery.Event) ) { return new jQuery.Event( src, props ); } @@ -3145,8 +3442,8 @@ jQuery.Event = function( src, props ) { // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; // Event type } else { @@ -3158,9 +3455,8 @@ jQuery.Event = function( src, props ) { jQuery.extend( this, props ); } - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = jQuery.now(); + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; @@ -3216,216 +3512,130 @@ jQuery.Event.prototype = { isImmediatePropagationStopped: returnFalse }; -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - - // Check if mouse(over|out) are still within the same parent element - var related = event.relatedTarget, - inside = false, - eventType = event.type; - - event.type = event.data; - - if ( related !== this ) { - - if ( related ) { - inside = jQuery.contains( this, related ); - } - - if ( !inside ) { - - jQuery.event.handle.apply( this, arguments ); - - event.type = eventType; - } - } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events +// Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; } }; }); -// submit delegation +// IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( !jQuery.nodeName( this, "form" ) ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - // Avoid triggering error on non-existent type attribute in IE VML (#7071) - var elem = e.target, - type = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.type : ""; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, - type = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.type : ""; + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - trigger( "submit", this, arguments ); - } - }); + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, - } else { + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { return false; } - }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); } }; - } -// change delegation, happens here so we have bind. +// IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { - var changeFilters, - - getVal = function( elem ) { - var type = jQuery.nodeName( elem, "input" ) ? elem.type : "", - val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( jQuery.nodeName( elem, "select" ) ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery._data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery._data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - e.liveFired = undefined; - jQuery.event.trigger( e, arguments[1], elem ); - } - }; - jQuery.event.special.change = { - filters: { - focusout: testChange, - - beforedeactivate: testChange, - click: function( e ) { - var elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : ""; + setup: function() { - if ( type === "radio" || type === "checkbox" || jQuery.nodeName( elem, "select" ) ) { - testChange.call( this, e ); + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : ""; + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; - if ( (e.keyCode === 13 && !jQuery.nodeName( elem, "textarea" ) ) || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - testChange.call( this, e ); + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information - beforeactivate: function( e ) { - var elem = e.target; - jQuery._data( elem, "_change_data", getVal(elem) ); - } + }); }, - setup: function( data, namespaces ) { - if ( this.type === "file" ) { - return false; - } + handle: function( event ) { + var elem = event.target; - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); } - - return rformElems.test( this.nodeName ); }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); + teardown: function() { + jQuery.event.remove( this, "._change" ); return rformElems.test( this.nodeName ); } }; - - changeFilters = jQuery.event.special.change.filters; - - // Handle when the input is .focus()'d - changeFilters.focus = changeFilters.beforeactivate; -} - -function trigger( type, elem, args ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - // Don't pass args or remember liveFired; they apply to the donor event. - var event = jQuery.extend( {}, args[ 0 ] ); - event.type = type; - event.originalEvent = {}; - event.liveFired = undefined; - jQuery.event.handle.call( elem, event ); - if ( event.isDefaultPrevented() ) { - args[ 0 ].preventDefault(); - } } // Create "bubbling" focus and blur events @@ -3433,7 +3643,10 @@ if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0; + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; jQuery.event.special[ fix ] = { setup: function() { @@ -3447,89 +3660,120 @@ if ( !jQuery.support.focusinBubbles ) { } } }; - - function handler( donor ) { - // Donor event is always a native one; fix it and switch its type. - // Let focusin/out handler cancel the donor focus/blur event. - var e = jQuery.event.fix( donor ); - e.type = fix; - e.originalEvent = {}; - jQuery.event.trigger( e, null, e.target ); - if ( e.isDefaultPrevented() ) { - donor.preventDefault(); - } - } }); } -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - var handler; +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); } return this; } - if ( arguments.length === 2 || data === false ) { - fn = data; - data = undefined; + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } } - - if ( name === "one" ) { - handler = function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }; - handler.guid = fn.guid || jQuery.guid++; - } else { - handler = fn; + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; } - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); - } + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } - - return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); - } - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on.call( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); } + return this; } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); return this; }, delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); + return this.on( types, selector, data, fn ); }, - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); }, trigger: function( type, data ) { @@ -3537,7 +3781,6 @@ jQuery.fn.extend({ jQuery.event.trigger( type, data, this ); }); }, - triggerHandler: function( type, data ) { if ( this[0] ) { return jQuery.event.trigger( type, data, this[0], true ); @@ -3551,8 +3794,8 @@ jQuery.fn.extend({ i = 0, toggler = function( event ) { // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); // Make sure that clicks stop event.preventDefault(); @@ -3575,178 +3818,9 @@ jQuery.fn.extend({ } }); -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; - -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); - - if ( typeof types === "object" && !types.preventDefault ) { - for ( var key in types ) { - context[ name ]( key, data, types[key], selector ); - } - - return this; - } - - if ( name === "die" && !types && - origSelector && origSelector.charAt(0) === "." ) { - - context.unbind( origSelector ); - - return this; - } - - if ( data === false || jQuery.isFunction( data ) ) { - fn = data || returnFalse; - data = undefined; - } - - types = (types || "").split(" "); - - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( liveMap[ type ] ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - for ( var j = 0, l = context.length; j < l; j++ ) { - jQuery.event.add( context[j], "live." + liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - } - - } else { - // unbind live handler - context.unbind( "live." + liveConvert( type, selector ), fn ); - } - } - - return this; - }; -}); - -function liveHandler( event ) { - var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, - elems = [], - selectors = [], - events = jQuery._data( this, "events" ); - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) - if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { - return; - } - - if ( event.namespace ) { - namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - close = match[i]; - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) && !close.elem.disabled ) { - elem = close.elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - event.type = handleObj.preType; - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - - // Make sure not to accidentally match a child element with the same selector - if ( related && jQuery.contains( elem, related ) ) { - related = elem; - } - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj, level: close.level }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - - if ( maxLevel && match.level > maxLevel ) { - break; - } - - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - ret = match.handleObj.origHandler.apply( match.elem, arguments ); - - if ( ret === false || event.isPropagationStopped() ) { - maxLevel = match.level; - - if ( ret === false ) { - stop = false; - } - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - - return stop; -} - -function liveConvert( type, selector ) { - return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspaces, "&"); -} - jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { @@ -3756,13 +3830,21 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl } return arguments.length > 0 ? - this.bind( name, data, fn ) : + this.on( name, null, data, fn ) : this.trigger( name ); }; if ( jQuery.attrFn ) { jQuery.attrFn[ name ] = true; } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } }); @@ -3776,11 +3858,13 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), done = 0, toString = Object.prototype.toString, hasDuplicate = false, baseHasDuplicate = true, rBackslash = /\\/g, + rReturn = /\r\n/g, rNonWord = /\W/; // Here we check if the JavaScript engine is using some sort of @@ -3832,7 +3916,7 @@ var Sizzle = function( selector, context, results, seed ) { if ( parts.length > 1 && origPOS.exec( selector ) ) { if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); + set = posProcess( parts[0] + parts[1], context, seed ); } else { set = Expr.relative[ parts[0] ] ? @@ -3846,7 +3930,7 @@ var Sizzle = function( selector, context, results, seed ) { selector += parts.shift(); } - set = posProcess( selector, set ); + set = posProcess( selector, set, seed ); } } @@ -3965,18 +4049,17 @@ Sizzle.matchesSelector = function( node, expr ) { }; Sizzle.find = function( expr, context, isXML ) { - var set; + var set, i, len, match, type, left; if ( !expr ) { return []; } - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var match, - type = Expr.order[i]; + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; + left = match[1]; match.splice( 1, 1 ); if ( left.substr( left.length - 1 ) !== "\\" ) { @@ -4002,17 +4085,18 @@ Sizzle.find = function( expr, context, isXML ) { Sizzle.filter = function( expr, set, inplace, not ) { var match, anyFound, + type, found, item, filter, left, + i, pass, old = expr, result = [], curLoop = set, isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); while ( expr && set.length ) { - for ( var type in Expr.filter ) { + for ( type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var found, item, - filter = Expr.filter[ type ], - left = match[1]; + filter = Expr.filter[ type ]; + left = match[1]; anyFound = false; @@ -4038,10 +4122,10 @@ Sizzle.filter = function( expr, set, inplace, not ) { } if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; + pass = not ^ found; if ( inplace && found != null ) { if ( pass ) { @@ -4092,7 +4176,46 @@ Sizzle.filter = function( expr, set, inplace, not ) { }; Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; }; var Expr = Sizzle.selectors = { @@ -4482,7 +4605,7 @@ var Expr = Sizzle.selectors = { return filter( elem, i, match, array ); } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; @@ -4501,7 +4624,10 @@ var Expr = Sizzle.selectors = { }, CHILD: function( elem, match ) { - var type = match[1], + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], node = elem; switch ( type ) { @@ -4529,18 +4655,18 @@ var Expr = Sizzle.selectors = { return true; case "nth": - var first = match[2], - last = match[3]; + first = match[2]; + last = match[3]; if ( first === 1 && last === 0 ) { return true; } - var doneName = match[0], - parent = elem.parentNode; + doneName = match[0]; + parent = elem.parentNode; - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { @@ -4548,10 +4674,10 @@ var Expr = Sizzle.selectors = { } } - parent.sizcache = doneName; + parent[ expando ] = doneName; } - var diff = elem.nodeIndex - last; + diff = elem.nodeIndex - last; if ( first === 0 ) { return diff === 0; @@ -4567,7 +4693,7 @@ var Expr = Sizzle.selectors = { }, TAG: function( elem, match ) { - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; }, CLASS: function( elem, match ) { @@ -4577,7 +4703,9 @@ var Expr = Sizzle.selectors = { ATTR: function( elem, match ) { var name = match[1], - result = Expr.attrHandle[ name ] ? + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : @@ -4588,6 +4716,8 @@ var Expr = Sizzle.selectors = { return result == null ? type === "!=" : + !type && Sizzle.attr ? + result != null : type === "=" ? value === check : type === "*=" ? @@ -4768,26 +4898,6 @@ if ( document.documentElement.compareDocumentPosition ) { }; } -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } - - return ret; -}; - // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ @@ -5065,13 +5175,13 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { elem = elem[dir]; while ( elem ) { - if ( elem.sizcache === doneName ) { + if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; + elem[ expando ] = doneName; elem.sizset = i; } @@ -5098,14 +5208,14 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { elem = elem[dir]; while ( elem ) { - if ( elem.sizcache === doneName ) { + if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { - elem.sizcache = doneName; + elem[ expando ] = doneName; elem.sizset = i; } @@ -5153,7 +5263,7 @@ Sizzle.isXML = function( elem ) { return documentElement ? documentElement.nodeName !== "HTML" : false; }; -var posProcess = function( selector, context ) { +var posProcess = function( selector, context, seed ) { var match, tmpSet = [], later = "", @@ -5169,13 +5279,16 @@ var posProcess = function( selector, context ) { selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); + Sizzle( selector, root[i], tmpSet, seed ); } return Sizzle.filter( later, tmpSet ); }; // EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; @@ -5261,43 +5374,33 @@ jQuery.fn.extend({ }, is: function( selector ) { - return !!selector && ( typeof selector === "string" ? - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { var ret = [], i, l, cur = this[0]; - // Array + // Array (deprecated as of jQuery 1.7) if ( jQuery.isArray( selectors ) ) { - var match, selector, - matches = {}, - level = 1; - - if ( cur && selectors.length ) { - for ( i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; - - if ( !matches[ selector ] ) { - matches[ selector ] = POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; - } - } + var level = 1; - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[ selector ]; + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { - if ( match.jquery ? match.index( cur ) > -1 : jQuery( cur ).is( match ) ) { - ret.push({ selector: selector, elem: cur, level: level }); - } + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); } - - cur = cur.parentNode; - level++; } + + cur = cur.parentNode; + level++; } return ret; @@ -5414,12 +5517,7 @@ jQuery.each({ } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ), - // The variable 'args' was introduced in - // https://github.com/jquery/jquery/commit/52a0238 - // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed. - // http://code.google.com/p/v8/issues/detail?id=1050 - args = slice.call(arguments); + var ret = jQuery.map( this, fn, until ); if ( !runtil.test( name ) ) { selector = until; @@ -5435,7 +5533,7 @@ jQuery.each({ ret = ret.reverse(); } - return this.pushStack( ret, name, args.join(",") ); + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); }; }); @@ -5504,7 +5602,7 @@ function winnow( elements, qualifier, keep ) { } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; + return ( elem === qualifier ) === keep; }); } else if ( typeof qualifier === "string" ) { @@ -5520,20 +5618,38 @@ function winnow( elements, qualifier, keep ) { } return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; }); } -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, rtagName = /<([\w:]+)/, rtbody = /<(?:" + nodeNames + ")", "i"), // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rscriptType = /\/(java|ecma)script/i, @@ -5547,7 +5663,8 @@ var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, col: [ 2, "", "
      " ], area: [ 1, "", "" ], _default: [ 0, "", "" ] - }; + }, + safeFragment = createSafeFragment( document ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; @@ -5625,8 +5742,10 @@ jQuery.fn.extend({ }, wrap: function( html ) { - return this.each(function() { - jQuery( this ).wrapAll( html ); + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); }); }, @@ -5660,7 +5779,7 @@ jQuery.fn.extend({ this.parentNode.insertBefore( elem, this ); }); } else if ( arguments.length ) { - var set = jQuery(arguments[0]); + var set = jQuery.clean( arguments ); set.push.apply( set, this.toArray() ); return this.pushStack( set, "before", arguments ); } @@ -5673,7 +5792,7 @@ jQuery.fn.extend({ }); } else if ( arguments.length ) { var set = this.pushStack( this, "after", arguments ); - set.push.apply( set, jQuery(arguments[0]).toArray() ); + set.push.apply( set, jQuery.clean(arguments) ); return set; } }, @@ -5728,7 +5847,7 @@ jQuery.fn.extend({ null; // See if we can take a shortcut and just use innerHTML - } else if ( typeof value === "string" && !rnocache.test( value ) && + } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) && (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { @@ -5854,7 +5973,7 @@ jQuery.fn.extend({ // in certain situations (Bug #8070). // Fragments from the fragment cache must always be cloned and never used // in place. - results.cacheable || (l > 1 && i < lastIndex) ? + results.cacheable || ( l > 1 && i < lastIndex ) ? jQuery.clone( fragment, true, true ) : fragment ); @@ -5883,27 +6002,26 @@ function cloneCopyEvent( src, dest ) { return; } - var internalKey = jQuery.expando, - oldData = jQuery.data( src ), - curData = jQuery.data( dest, oldData ); + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; - // Switch to use the internal data object, if it exists, for the next - // stage of data copying - if ( (oldData = oldData[ internalKey ]) ) { - var events = oldData.events; - curData = curData[ internalKey ] = jQuery.extend({}, oldData); + if ( events ) { + delete curData.handle; + curData.events = {}; - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( var type in events ) { - for ( var i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); - } + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); } } } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } } function cloneFixAttributes( src, dest ) { @@ -5965,16 +6083,17 @@ function cloneFixAttributes( src, dest ) { } jQuery.buildFragment = function( args, nodes, scripts ) { - var fragment, cacheable, cacheresults, doc; - - // nodes may contain either an explicit document object, - // a jQuery collection or context object. - // If nodes[0] contains a valid object to assign to doc - if ( nodes && nodes[0] ) { - doc = nodes[0].ownerDocument || nodes[0]; - } + var fragment, cacheable, cacheresults, doc, + first = args[ 0 ]; + + // nodes may contain either an explicit document object, + // a jQuery collection or context object. + // If nodes[0] contains a valid object to assign to doc + if ( nodes && nodes[0] ) { + doc = nodes[0].ownerDocument || nodes[0]; + } - // Ensure that an attr object doesn't incorrectly stand in as a document object + // Ensure that an attr object doesn't incorrectly stand in as a document object // Chrome and Firefox seem to allow this to occur and will throw exception // Fixes #8950 if ( !doc.createDocumentFragment ) { @@ -5985,12 +6104,15 @@ jQuery.buildFragment = function( args, nodes, scripts ) { // Cloning options loses the selected state, so don't cache them // IE 6 doesn't like it when you put or elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache - if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document && - args[0].charAt(0) === "<" && !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) { + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { cacheable = true; - cacheresults = jQuery.fragments[ args[0] ]; + cacheresults = jQuery.fragments[ first ]; if ( cacheresults && cacheresults !== 1 ) { fragment = cacheresults; } @@ -6002,7 +6124,7 @@ jQuery.buildFragment = function( args, nodes, scripts ) { } if ( cacheable ) { - jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1; + jQuery.fragments[ first ] = cacheresults ? fragment : 1; } return { fragment: fragment, cacheable: cacheable }; @@ -6028,7 +6150,7 @@ jQuery.each({ } else { for ( var i = 0, l = insert.length; i < l; i++ ) { - var elems = (i > 0 ? this.clone(true) : this).get(); + var elems = ( i > 0 ? this.clone(true) : this ).get(); jQuery( insert[i] )[ original ]( elems ); ret = ret.concat( elems ); } @@ -6039,10 +6161,10 @@ jQuery.each({ }); function getAll( elem ) { - if ( "getElementsByTagName" in elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { return elem.getElementsByTagName( "*" ); - } else if ( "querySelectorAll" in elem ) { + } else if ( typeof elem.querySelectorAll !== "undefined" ) { return elem.querySelectorAll( "*" ); } else { @@ -6058,19 +6180,33 @@ function fixDefaultChecked( elem ) { } // Finds all inputs and passes them to fixDefaultChecked function findInputs( elem ) { - if ( jQuery.nodeName( elem, "input" ) ) { + var nodeName = ( elem.nodeName || "" ).toLowerCase(); + if ( nodeName === "input" ) { fixDefaultChecked( elem ); - } else if ( "getElementsByTagName" in elem ) { + // Skip scripts, get other children + } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); } } +// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js +function shimCloneNode( elem ) { + var div = document.createElement( "div" ); + safeFragment.appendChild( div ); + + div.innerHTML = elem.outerHTML; + return div.firstChild; +} + jQuery.extend({ clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var clone = elem.cloneNode(true), - srcElements, - destElements, - i; + var srcElements, + destElements, + i, + // IE<=8 does not properly clone detached, unknown element nodes + clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ? + elem.cloneNode( true ) : + shimCloneNode( elem ); if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { @@ -6082,8 +6218,7 @@ jQuery.extend({ cloneFixAttributes( elem, clone ); - // Using Sizzle here is crazy slow, so we use getElementsByTagName - // instead + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead srcElements = getAll( elem ); destElements = getAll( clone ); @@ -6148,11 +6283,20 @@ jQuery.extend({ elem = elem.replace(rxhtmlTag, "<$1>"); // Trim whitespace, otherwise indexOf won't work as expected - var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(), + var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), wrap = wrapMap[ tag ] || wrapMap._default, depth = wrap[0], div = context.createElement("div"); + // Append wrapper element to unknown element safe doc fragment + if ( context === document ) { + // Use the fragment we've already created for this document + safeFragment.appendChild( div ); + } else { + // Use a fragment created with the owner document + createSafeFragment( context ).appendChild( div ); + } + // Go to html and back, then peel off extra wrappers div.innerHTML = wrap[1] + elem + wrap[2]; @@ -6233,7 +6377,9 @@ jQuery.extend({ }, cleanData: function( elems ) { - var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special, + var data, id, + cache = jQuery.cache, + special = jQuery.event.special, deleteExpando = jQuery.support.deleteExpando; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { @@ -6244,7 +6390,7 @@ jQuery.extend({ id = elem[ jQuery.expando ]; if ( id ) { - data = cache[ id ] && cache[ id ][ internalKey ]; + data = cache[ id ]; if ( data && data.events ) { for ( var type in data.events ) { @@ -6506,7 +6652,7 @@ if ( !jQuery.support.opacity ) { set: function( elem, value ) { var style = elem.style, currentStyle = elem.currentStyle, - opacity = jQuery.isNaN( value ) ? "" : "alpha(opacity=" + value * 100 + ")", + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout @@ -6563,11 +6709,8 @@ if ( document.defaultView && document.defaultView.getComputedStyle ) { name = name.replace( rupper, "-$1" ).toLowerCase(); - if ( !(defaultView = elem.ownerDocument.defaultView) ) { - return undefined; - } - - if ( (computedStyle = defaultView.getComputedStyle( elem, null )) ) { + if ( (defaultView = elem.ownerDocument.defaultView) && + (computedStyle = defaultView.getComputedStyle( elem, null )) ) { ret = computedStyle.getPropertyValue( name ); if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { ret = jQuery.style( elem, name ); @@ -6580,25 +6723,32 @@ if ( document.defaultView && document.defaultView.getComputedStyle ) { if ( document.documentElement.currentStyle ) { currentStyle = function( elem, name ) { - var left, + var left, rsLeft, uncomputed, ret = elem.currentStyle && elem.currentStyle[ name ], - rsLeft = elem.runtimeStyle && elem.runtimeStyle[ name ], style = elem.style; + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret === null && style && (uncomputed = style[ name ]) ) { + ret = uncomputed; + } + // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { + // Remember the original values left = style.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; // Put in the new values to get a computed value out if ( rsLeft ) { elem.runtimeStyle.left = elem.currentStyle.left; } - style.left = name === "fontSize" ? "1em" : (ret || 0); + style.left = name === "fontSize" ? "1em" : ( ret || 0 ); ret = style.pixelLeft + "px"; // Revert the changed values @@ -6618,20 +6768,22 @@ function getWH( elem, name, extra ) { // Start with offset property var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - which = name === "width" ? cssWidth : cssHeight; + which = name === "width" ? cssWidth : cssHeight, + i = 0, + len = which.length; if ( val > 0 ) { if ( extra !== "border" ) { - jQuery.each( which, function() { + for ( ; i < len; i++ ) { if ( !extra ) { - val -= parseFloat( jQuery.css( elem, "padding" + this ) ) || 0; + val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; } if ( extra === "margin" ) { - val += parseFloat( jQuery.css( elem, extra + this ) ) || 0; + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; } else { - val -= parseFloat( jQuery.css( elem, "border" + this + "Width" ) ) || 0; + val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; } - }); + } } return val + "px"; @@ -6647,15 +6799,15 @@ function getWH( elem, name, extra ) { // Add padding, border, margin if ( extra ) { - jQuery.each( which, function() { - val += parseFloat( jQuery.css( elem, "padding" + this ) ) || 0; + for ( ; i < len; i++ ) { + val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; if ( extra !== "padding" ) { - val += parseFloat( jQuery.css( elem, "border" + this + "Width" ) ) || 0; + val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; } if ( extra === "margin" ) { - val += parseFloat( jQuery.css( elem, extra + this ) ) || 0; + val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; } - }); + } } return val + "px"; @@ -6666,7 +6818,7 @@ if ( jQuery.expr && jQuery.expr.filters ) { var width = elem.offsetWidth, height = elem.offsetHeight; - return (width === 0 && height === 0) || (!jQuery.support.reliableHiddenOffsets && (elem.style.display || jQuery.css( elem, "display" )) === "none"); + return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); }; jQuery.expr.filters.visible = function( elem ) { @@ -6720,7 +6872,7 @@ var r20 = /%20/g, // Document location segments ajaxLocParts, - + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression allTypes = ["*/"] + ["*"]; @@ -6759,7 +6911,7 @@ function addToPrefiltersOrTransports( structure ) { placeBefore; // For each dataType in the dataTypeExpression - for(; i < length; i++ ) { + for ( ; i < length; i++ ) { dataType = dataTypes[ i ]; // We control if we're asked to add before // any existing element @@ -6790,7 +6942,7 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jqX executeOnly = ( structure === prefilters ), selection; - for(; i < length && ( executeOnly || !selection ); i++ ) { + for ( ; i < length && ( executeOnly || !selection ); i++ ) { selection = list[ i ]( options, originalOptions, jqXHR ); // If we got redirected to another dataType // we try there if executing only and not done already @@ -6821,7 +6973,7 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jqX function ajaxExtend( target, src ) { var key, deep, flatOptions = jQuery.ajaxSettings.flatOptions || {}; - for( key in src ) { + for ( key in src ) { if ( src[ key ] !== undefined ) { ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; } @@ -6938,7 +7090,7 @@ jQuery.fn.extend({ // Attach a bunch of functions for handling common AJAX events jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ jQuery.fn[ o ] = function( f ){ - return this.bind( o, f ); + return this.on( o, f ); }; }); @@ -7080,7 +7232,7 @@ jQuery.extend({ jQuery( callbackContext ) : jQuery.event, // Deferreds deferred = jQuery.Deferred(), - completeDeferred = jQuery._Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), // Status-dependent callbacks statusCode = s.statusCode || {}, // ifModified key @@ -7230,7 +7382,7 @@ jQuery.extend({ // We extract error from statusText // then normalize statusText and status for non-aborts error = statusText; - if( !statusText || status ) { + if ( !statusText || status ) { statusText = "error"; if ( status < 0 ) { status = 0; @@ -7259,7 +7411,7 @@ jQuery.extend({ } // Complete - completeDeferred.resolveWith( callbackContext, [ jqXHR, statusText ] ); + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); if ( fireGlobals ) { globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); @@ -7274,14 +7426,14 @@ jQuery.extend({ deferred.promise( jqXHR ); jqXHR.success = jqXHR.done; jqXHR.error = jqXHR.fail; - jqXHR.complete = completeDeferred.done; + jqXHR.complete = completeDeferred.add; // Status-dependent callbacks jqXHR.statusCode = function( map ) { if ( map ) { var tmp; if ( state < 2 ) { - for( tmp in map ) { + for ( tmp in map ) { statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; } } else { @@ -7358,7 +7510,7 @@ jQuery.extend({ ret = s.url.replace( rts, "$1_=" + ts ); // if nothing was replaced, add timestamp to the end - s.url = ret + ( (ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); } } @@ -7432,7 +7584,7 @@ jQuery.extend({ done( -1, e ); // Simply rethrow otherwise } else { - jQuery.error( e ); + throw e; } } } @@ -7536,7 +7688,7 @@ function ajaxHandleResponses( s, jqXHR, responses ) { firstDataType; // Fill responseXXX fields - for( type in responseFields ) { + for ( type in responseFields ) { if ( type in responses ) { jqXHR[ responseFields[type] ] = responses[ type ]; } @@ -7615,13 +7767,13 @@ function ajaxConvert( s, response ) { conv2; // For each dataType in the chain - for( i = 1; i < length; i++ ) { + for ( i = 1; i < length; i++ ) { // Create converters map // with lowercased keys if ( i === 1 ) { - for( key in s.converters ) { - if( typeof key === "string" ) { + for ( key in s.converters ) { + if ( typeof key === "string" ) { converters[ key.toLowerCase() ] = s.converters[ key ]; } } @@ -7632,7 +7784,7 @@ function ajaxConvert( s, response ) { current = dataTypes[ i ]; // If current is auto dataType, update it to prev - if( current === "*" ) { + if ( current === "*" ) { current = prev; // If no auto and dataTypes are actually different } else if ( prev !== "*" && prev !== current ) { @@ -7644,7 +7796,7 @@ function ajaxConvert( s, response ) { // If there is no direct converter, search transitively if ( !conv ) { conv2 = undefined; - for( conv1 in converters ) { + for ( conv1 in converters ) { tmp = conv1.split( " " ); if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { conv2 = converters[ tmp[1] + " " + current ]; @@ -8083,11 +8235,11 @@ jQuery.fn.extend({ var elem, display; if ( speed || speed === 0 ) { - return this.animate( genFx("show", 3), speed, easing, callback); + return this.animate( genFx("show", 3), speed, easing, callback ); } else { for ( var i = 0, j = this.length; i < j; i++ ) { - elem = this[i]; + elem = this[ i ]; if ( elem.style ) { display = elem.style.display; @@ -8101,8 +8253,8 @@ jQuery.fn.extend({ // Set elements which have been overridden with display: none // in a stylesheet to whatever the default browser style is // for such an element - if ( display === "" && jQuery.css( elem, "display" ) === "none" ) { - jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName)); + if ( display === "" && jQuery.css(elem, "display") === "none" ) { + jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); } } } @@ -8110,13 +8262,13 @@ jQuery.fn.extend({ // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { - elem = this[i]; + elem = this[ i ]; if ( elem.style ) { display = elem.style.display; if ( display === "" || display === "none" ) { - elem.style.display = jQuery._data(elem, "olddisplay") || ""; + elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; } } } @@ -8130,12 +8282,17 @@ jQuery.fn.extend({ return this.animate( genFx("hide", 3), speed, easing, callback); } else { - for ( var i = 0, j = this.length; i < j; i++ ) { - if ( this[i].style ) { - var display = jQuery.css( this[i], "display" ); + var elem, display, + i = 0, + j = this.length; + + for ( ; i < j; i++ ) { + elem = this[i]; + if ( elem.style ) { + display = jQuery.css( elem, "display" ); - if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) { - jQuery._data( this[i], "olddisplay", display ); + if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { + jQuery._data( elem, "olddisplay", display ); } } } @@ -8180,7 +8337,7 @@ jQuery.fn.extend({ }, animate: function( prop, speed, easing, callback ) { - var optall = jQuery.speed(speed, easing, callback); + var optall = jQuery.speed( speed, easing, callback ); if ( jQuery.isEmptyObject( prop ) ) { return this.each( optall.complete, [ false ] ); @@ -8189,7 +8346,7 @@ jQuery.fn.extend({ // Do not change referenced properties as per-property easing will be lost prop = jQuery.extend( {}, prop ); - return this[ optall.queue === false ? "each" : "queue" ](function() { + function doAnimation() { // XXX 'this' does not always have a nodeName when running the // test suite @@ -8200,9 +8357,9 @@ jQuery.fn.extend({ var opt = jQuery.extend( {}, optall ), isElement = this.nodeType === 1, hidden = isElement && jQuery(this).is(":hidden"), - name, val, p, - display, e, - parts, start, end, unit; + name, val, p, e, + parts, start, end, unit, + method; // will store per property easing and be used to determine when an animation is complete opt.animatedProperties = {}; @@ -8238,25 +8395,17 @@ jQuery.fn.extend({ opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; // Set display property to inline-block for height/width - // animations on inline elements that are having width/height - // animated + // animations on inline elements that are having width/height animated if ( jQuery.css( this, "display" ) === "inline" && jQuery.css( this, "float" ) === "none" ) { - if ( !jQuery.support.inlineBlockNeedsLayout ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { this.style.display = "inline-block"; } else { - display = defaultDisplay( this.nodeName ); - - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( display === "inline" ) { - this.style.display = "inline-block"; - - } else { - this.style.display = "inline"; - this.style.zoom = 1; - } + this.style.zoom = 1; } } } @@ -8270,8 +8419,17 @@ jQuery.fn.extend({ e = new jQuery.fx( this, opt, p ); val = prop[ p ]; - if ( rfxtypes.test(val) ) { - e[ val === "toggle" ? hidden ? "show" : "hide" : val ](); + if ( rfxtypes.test( val ) ) { + + // Tracks whether to show or hide based on private + // data attached to the element + method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); + if ( method ) { + jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); + e[ method ](); + } else { + e[ val ](); + } } else { parts = rfxnum.exec( val ); @@ -8284,7 +8442,7 @@ jQuery.fn.extend({ // We need to compute starting value if ( unit !== "px" ) { jQuery.style( this, p, (end || 1) + unit); - start = ((end || 1) / e.cur()) * start; + start = ( (end || 1) / e.cur() ) * start; jQuery.style( this, p, start + unit); } @@ -8303,39 +8461,71 @@ jQuery.fn.extend({ // For JS strict compliance return true; - }); + } + + return optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); }, - stop: function( clearQueue, gotoEnd ) { - if ( clearQueue ) { - this.queue([]); + stop: function( type, clearQueue, gotoEnd ) { + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var index, + hadTimers = false, + timers = jQuery.timers, + data = jQuery._data( this ); - this.each(function() { - var timers = jQuery.timers, - i = timers.length; // clear marker counters if we know they won't be if ( !gotoEnd ) { jQuery._unmark( true, this ); } - while ( i-- ) { - if ( timers[i].elem === this ) { - if (gotoEnd) { - // force the next step to be the last - timers[i](true); - } - timers.splice(i, 1); + function stopQueue( elem, data, index ) { + var hooks = data[ index ]; + jQuery.removeData( elem, index, true ); + hooks.stop( gotoEnd ); + } + + if ( type == null ) { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { + stopQueue( this, data, index ); + } } + } else if ( data[ index = type + ".run" ] && data[ index ].stop ){ + stopQueue( this, data, index ); } - }); - // start the next in the queue if the last step wasn't forced - if ( !gotoEnd ) { - this.dequeue(); - } + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + if ( gotoEnd ) { - return this; + // force the next step to be the last + timers[ index ]( true ); + } else { + timers[ index ].saveState(); + } + hadTimers = true; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( !( gotoEnd && hadTimers ) ) { + jQuery.dequeue( this, type ); + } + }); } }); @@ -8354,7 +8544,7 @@ function clearFxNow() { function genFx( type, num ) { var obj = {}; - jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() { + jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { obj[ this ] = type; }); @@ -8363,9 +8553,9 @@ function genFx( type, num ) { // Generate shortcuts for custom animations jQuery.each({ - slideDown: genFx("show", 1), - slideUp: genFx("hide", 1), - slideToggle: genFx("toggle", 1), + slideDown: genFx( "show", 1 ), + slideUp: genFx( "hide", 1 ), + slideToggle: genFx( "toggle", 1 ), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } @@ -8377,25 +8567,31 @@ jQuery.each({ jQuery.extend({ speed: function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, - easing: fn && easing || easing && !jQuery.isFunction(easing) && easing + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing }; opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default; + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } // Queueing opt.old = opt.complete; + opt.complete = function( noUnmark ) { if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } - if ( opt.queue !== false ) { - jQuery.dequeue( this ); + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); } else if ( noUnmark !== false ) { jQuery._unmark( this ); } @@ -8409,7 +8605,7 @@ jQuery.extend({ return firstNum + diff * p; }, swing: function( p, n, firstNum, diff ) { - return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum; + return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum; } }, @@ -8432,12 +8628,12 @@ jQuery.fx.prototype = { this.options.step.call( this.elem, this.now, this ); } - (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); + ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); }, // Get the current size cur: function() { - if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) { + if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { return this.elem[ this.prop ]; } @@ -8455,17 +8651,22 @@ jQuery.fx.prototype = { fx = jQuery.fx; this.startTime = fxNow || createFxNow(); - this.start = from; this.end = to; - this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); - this.now = this.start; + this.now = this.start = from; this.pos = this.state = 0; + this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); function t( gotoEnd ) { - return self.step(gotoEnd); + return self.step( gotoEnd ); } + t.queue = this.options.queue; t.elem = this.elem; + t.saveState = function() { + if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { + jQuery._data( self.elem, "fxshow" + self.prop, self.start ); + } + }; if ( t() && jQuery.timers.push(t) && !timerId ) { timerId = setInterval( fx.tick, fx.interval ); @@ -8474,14 +8675,20 @@ jQuery.fx.prototype = { // Simple 'show' function show: function() { + var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); + // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); + this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); this.options.show = true; // Begin the animation - // Make sure that we start at a small width/height to avoid any - // flash of content - this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur()); + // Make sure that we start at a small width/height to avoid any flash of content + if ( dataShow !== undefined ) { + // This show is picking up where a previous hide or show left off + this.custom( this.cur(), dataShow ); + } else { + this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); + } // Start by showing the element jQuery( this.elem ).show(); @@ -8490,20 +8697,20 @@ jQuery.fx.prototype = { // Simple 'hide' function hide: function() { // Remember where we started, so that we can go back to it later - this.options.orig[this.prop] = jQuery.style( this.elem, this.prop ); + this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); this.options.hide = true; // Begin the animation - this.custom(this.cur(), 0); + this.custom( this.cur(), 0 ); }, // Each step of an animation step: function( gotoEnd ) { - var t = fxNow || createFxNow(), + var p, n, complete, + t = fxNow || createFxNow(), done = true, elem = this.elem, - options = this.options, - i, n; + options = this.options; if ( gotoEnd || t >= options.duration + this.startTime ) { this.now = this.end; @@ -8512,8 +8719,8 @@ jQuery.fx.prototype = { options.animatedProperties[ this.prop ] = true; - for ( i in options.animatedProperties ) { - if ( options.animatedProperties[i] !== true ) { + for ( p in options.animatedProperties ) { + if ( options.animatedProperties[ p ] !== true ) { done = false; } } @@ -8522,25 +8729,36 @@ jQuery.fx.prototype = { // Reset the overflow if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { - jQuery.each( [ "", "X", "Y" ], function (index, value) { - elem.style[ "overflow" + value ] = options.overflow[index]; + jQuery.each( [ "", "X", "Y" ], function( index, value ) { + elem.style[ "overflow" + value ] = options.overflow[ index ]; }); } // Hide the element if the "hide" operation was done if ( options.hide ) { - jQuery(elem).hide(); + jQuery( elem ).hide(); } // Reset the properties, if the item has been hidden or shown if ( options.hide || options.show ) { - for ( var p in options.animatedProperties ) { - jQuery.style( elem, p, options.orig[p] ); + for ( p in options.animatedProperties ) { + jQuery.style( elem, p, options.orig[ p ] ); + jQuery.removeData( elem, "fxshow" + p, true ); + // Toggle data is no longer needed + jQuery.removeData( elem, "toggle" + p, true ); } } // Execute the complete function - options.complete.call( elem ); + // in the event that the complete function throws an exception + // we must ensure it won't be called twice. #5684 + + complete = options.complete; + if ( complete ) { + + options.complete = false; + complete.call( elem ); + } } return false; @@ -8554,8 +8772,8 @@ jQuery.fx.prototype = { this.state = n / options.duration; // Perform the easing function, defaults to swing - this.pos = jQuery.easing[ options.animatedProperties[ this.prop ] ]( this.state, n, 0, 1, options.duration ); - this.now = this.start + ((this.end - this.start) * this.pos); + this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); + this.now = this.start + ( (this.end - this.start) * this.pos ); } // Perform the next step of the animation this.update(); @@ -8567,9 +8785,15 @@ jQuery.fx.prototype = { jQuery.extend( jQuery.fx, { tick: function() { - for ( var timers = jQuery.timers, i = 0 ; i < timers.length ; ++i ) { - if ( !timers[i]() ) { - timers.splice(i--, 1); + var timer, + timers = jQuery.timers, + i = 0; + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); } } @@ -8599,7 +8823,7 @@ jQuery.extend( jQuery.fx, { _default: function( fx ) { if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { - fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit; + fx.elem.style[ fx.prop ] = fx.now + fx.unit; } else { fx.elem[ fx.prop ] = fx.now; } @@ -8607,6 +8831,14 @@ jQuery.extend( jQuery.fx, { } }); +// Adds width/height step functions +// Do not set anything below 0 +jQuery.each([ "width", "height" ], function( i, prop ) { + jQuery.fx.step[ prop ] = function( fx ) { + jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); + }; +}); + if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.animated = function( elem ) { return jQuery.grep(jQuery.timers, function( fn ) { @@ -8623,7 +8855,6 @@ function defaultDisplay( nodeName ) { var body = document.body, elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), display = elem.css( "display" ); - elem.remove(); // If the simple way fails, @@ -8651,7 +8882,6 @@ function defaultDisplay( nodeName ) { iframeDoc.body.appendChild( elem ); display = jQuery.css( elem, "display" ); - body.removeChild( iframe ); } @@ -8728,8 +8958,6 @@ if ( "getBoundingClientRect" in document.documentElement ) { return jQuery.offset.bodyOffset( elem ); } - jQuery.offset.initialize(); - var computedStyle, offsetParent = elem.offsetParent, prevOffsetParent = elem, @@ -8742,7 +8970,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { - if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { break; } @@ -8754,7 +8982,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { top += elem.offsetTop; left += elem.offsetLeft; - if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { + if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } @@ -8763,7 +8991,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { offsetParent = elem.offsetParent; } - if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { + if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } @@ -8776,7 +9004,7 @@ if ( "getBoundingClientRect" in document.documentElement ) { left += body.offsetLeft; } - if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { + if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { top += Math.max( docElem.scrollTop, body.scrollTop ); left += Math.max( docElem.scrollLeft, body.scrollLeft ); } @@ -8786,46 +9014,12 @@ if ( "getBoundingClientRect" in document.documentElement ) { } jQuery.offset = { - initialize: function() { - var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.css(body, "marginTop") ) || 0, - html = "
      "; - - jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } ); - - container.innerHTML = html; - body.insertBefore( container, body.firstChild ); - innerDiv = container.firstChild; - checkDiv = innerDiv.firstChild; - td = innerDiv.nextSibling.firstChild.firstChild; - - this.doesNotAddBorder = (checkDiv.offsetTop !== 5); - this.doesAddBorderForTableAndCells = (td.offsetTop === 5); - - checkDiv.style.position = "fixed"; - checkDiv.style.top = "20px"; - - // safari subtracts parent border width here which is 5px - this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15); - checkDiv.style.position = checkDiv.style.top = ""; - - innerDiv.style.overflow = "hidden"; - innerDiv.style.position = "relative"; - - this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5); - - this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop); - - body.removeChild( container ); - jQuery.offset.initialize = jQuery.noop; - }, bodyOffset: function( body ) { var top = body.offsetTop, left = body.offsetLeft; - jQuery.offset.initialize(); - - if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) { + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { top += parseFloat( jQuery.css(body, "marginTop") ) || 0; left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; } @@ -8845,7 +9039,7 @@ jQuery.offset = { curOffset = curElem.offset(), curCSSTop = jQuery.css( elem, "top" ), curCSSLeft = jQuery.css( elem, "left" ), - calculatePosition = (position === "absolute" || position === "fixed") && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, props = {}, curPosition = {}, curTop, curLeft; // need to be able to calculate position if either top or left is auto and position is either absolute or fixed @@ -8862,11 +9056,11 @@ jQuery.offset = { options = options.call( elem, i, curOffset ); } - if (options.top != null) { - props.top = (options.top - curOffset.top) + curTop; + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; } - if (options.left != null) { - props.left = (options.left - curOffset.left) + curLeft; + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; } if ( "using" in options ) { @@ -8879,6 +9073,7 @@ jQuery.offset = { jQuery.fn.extend({ + position: function() { if ( !this[0] ) { return null; @@ -8981,16 +9176,20 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { // innerHeight and innerWidth jQuery.fn[ "inner" + name ] = function() { var elem = this[0]; - return elem && elem.style ? + return elem ? + elem.style ? parseFloat( jQuery.css( elem, type, "padding" ) ) : + this[ type ]() : null; }; // outerHeight and outerWidth jQuery.fn[ "outer" + name ] = function( margin ) { var elem = this[0]; - return elem && elem.style ? + return elem ? + elem.style ? parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) : + this[ type ]() : null; }; @@ -9030,7 +9229,7 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { var orig = jQuery.css( elem, type ), ret = parseFloat( orig ); - return jQuery.isNaN( ret ) ? orig : ret; + return jQuery.isNumeric( ret ) ? ret : orig; // Set the width or height on the element (default to pixels if value is unitless) } else { @@ -9041,6 +9240,27 @@ jQuery.each([ "Height", "Width" ], function( i, name ) { }); + + // Expose jQuery to the global object window.jQuery = window.$ = jQuery; -})(window); \ No newline at end of file + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + + + +})( window ); \ No newline at end of file diff --git a/jquerymx.md b/jquerymx.md index d5862d79..3d4f316c 100644 --- a/jquerymx.md +++ b/jquerymx.md @@ -1,5 +1,6 @@ @page jquerymx jQueryMX @parent index 0 +@description jQuery Model View Controller and extensions. jQueryMX is a collection of useful jQuery libraries that provide the missing functionality necessary to diff --git a/lang/observe/delegate/delegate.js b/lang/observe/delegate/delegate.js index 8fadccf3..385ac26e 100644 --- a/lang/observe/delegate/delegate.js +++ b/lang/observe/delegate/delegate.js @@ -4,43 +4,127 @@ steal('jquery/lang/observe',function(){ * @add jQuery.Observe.prototype */ + // ** - 'this' will be the deepest item changed + // * - 'this' will be any changes within *, but * will be the + // this returned + // tells if the parts part of a delegate matches the broken up props of the event - var matches = function(delegate, props){ + // gives the prop to use as 'this' + // - parts - the attribute name of the delegate split in parts ['foo','*'] + // - props - the split props of the event that happened ['foo','bar','0'] + // - returns - the attribute to delegate too ('foo.bar'), or null if not a match + var matches = function(parts, props){ //check props parts are the same or - var parts = delegate.parts, - len = parts.length, - i =0; + var len = parts.length, + i =0, + // keeps the matched props we will use + matchedProps = [], + prop; // if the event matches for(i; i< len; i++){ - if(parts[i] == "**") { - return true; - } else if( typeof props[i] == 'string' && ( props[i] === parts[i] || parts[i] === "*" ) ) { - + prop = props[i] + // if no more props (but we should be matching them) + // return null + if( typeof prop !== 'string' ) { + return null; + } else + // if we have a "**", match everything + if( parts[i] == "**" ) { + return props.join("."); + } else + // a match, but we want to delegate to "*" + if (parts[i] == "*"){ + // only do this if there is nothing after ... + matchedProps.push(prop); + } + else if( prop === parts[i] ) { + matchedProps.push(prop); } else { - return false; + return null; } } - return len === props.length; + return matchedProps.join("."); }, + // gets a change event and tries to figure out which + // delegates to call delegate = function(event, prop, how, newVal, oldVal){ + // pre-split properties to save some regexp time var props = prop.split("."), - delegates = $.data(this,"_observe_delegates") || [], - delegate; + delegates = ($.data(this,"_observe_delegates") || []).slice(0), + delegate, + attr, + matchedAttr, + hasMatch, + valuesEqual; event.attr = prop; event.lastAttr = props[props.length -1 ]; - - for(var i =0; i < delegates.length; i++){ - // check delegate.event - delegate = delegates[i]; + // for each delegate + for(var i =0; delegate = delegates[i++];){ + + // if there is a batchNum, this means that this + // event is part of a series of events caused by a single + // attrs call. We don't want to issue the same event + // multiple times + // setting the batchNum happens later + if((event.batchNum && delegate.batchNum === event.batchNum) || delegate.undelegated ){ + continue; + } + + // reset match and values tests + hasMatch = undefined; + valuesEqual = true; + + // for each attr in a delegate + for(var a =0 ; a < delegate.attrs.length; a++){ + + attr = delegate.attrs[a]; + + // check if it is a match + if(matchedAttr = matches(attr.parts, props)){ + hasMatch = matchedAttr; + } + // if it has a value, make sure it's the right value + // if it's set, we should probably check that it has a + // value no matter what + if(attr.value && valuesEqual /* || delegate.hasValues */){ + valuesEqual = attr.value === ""+this.attr(attr.attr) + } else if (valuesEqual && delegate.attrs.length > 1){ + // if there are multiple attributes, each has to at + // least have some value + valuesEqual = this.attr(attr.attr) !== undefined + } + } - if( delegate.event === 'change' && matches(delegate, props) ){ - delegate.callback.apply(this.attr(prop), arguments); - } else if(delegate.event === how && matches(delegate, props) ){ - delegate.callback.apply(this.attr(prop), [event,newVal, oldVal]); - } else if(delegate.event === 'set' && how == 'add' && matches(delegates[i], props)) { - delegate.callback.apply(this.attr(prop), [event,newVal, oldVal]); + // if there is a match and valuesEqual ... call back + + if(hasMatch && valuesEqual) { + // how to get to the changed property from the delegate + var from = prop.replace(hasMatch+".",""); + + // if this event is part of a batch, set it on the delegate + // to only send one event + if(event.batchNum ){ + delegate.batchNum = event.batchNum + } + + // if we listen to change, fire those with the same attrs + // TODO: the attrs should probably be using from + if( delegate.event === 'change' ){ + arguments[1] = from; + event.curAttr = hasMatch; + delegate.callback.apply(this.attr(hasMatch), $.makeArray( arguments)); + } else if(delegate.event === how ){ + + // if it's a match, callback with the location of the match + delegate.callback.apply(this.attr(hasMatch), [event,newVal, oldVal, from]); + } else if(delegate.event === 'set' && + how == 'add' ) { + // if we are listening to set, we should also listen to add + delegate.callback.apply(this.attr(hasMatch), [event,newVal, oldVal, from]); + } } + } }; @@ -52,7 +136,7 @@ steal('jquery/lang/observe',function(){ * * * // create an observable - * var observe = new $.Observe({ + * var observe = $.O({ * foo : { * bar : "Hello World" * } @@ -61,6 +145,7 @@ steal('jquery/lang/observe',function(){ * //listen to changes on a property * observe.delegate("foo.bar","change", function(ev, prop, how, newVal, oldVal){ * // foo.bar has been added, set, or removed + * this //-> * }); * * // change the property @@ -142,19 +227,40 @@ steal('jquery/lang/observe',function(){ * properties. Using a double wildcard (**) matches * any deep property. * - * @param {String} attr the attribute you want to listen for changes in. + * ## Listening on multiple properties and values + * + * Delegate lets you listen on multiple values at once, for example, + * + * @param {String} selector the attributes you want to listen for changes in. * @param {String} event the event name * @param {Function} cb the callback handler * @return {jQuery.Delegate} the delegate for chaining */ - delegate : function(attr, event, cb){ - attr = $.trim(attr); + delegate : function(selector, event, cb){ + selector = $.trim(selector); var delegates = $.data(this, "_observe_delegates") || - $.data(this, "_observe_delegates", []); - attr = $.trim(attr); + $.data(this, "_observe_delegates", []), + attrs = []; + + // split selector by spaces + selector.replace(/([^\s=]+)=?([^\s]+)?/g, function(whole, attr, value){ + attrs.push({ + // the attribute name + attr: attr, + // the attribute's pre-split names (for speed) + parts: attr.split('.'), + // the value associated with this prop + value: value + }) + }); + + // delegates has pre-processed info about the event delegates.push({ - attr : attr, - parts : attr.split('.'), + // the attrs name for unbinding + selector : selector, + // an object of attribute names and values {type: 'recipe',id: undefined} + // undefined means a value was not defined + attrs : attrs, callback : cb, event: event }); @@ -169,28 +275,30 @@ steal('jquery/lang/observe',function(){ * * observe.undelegate("name","set", function(){ ... }) * - * @param {String} attr the attribute name of the object you want to undelegate from. + * @param {String} selector the attribute name of the object you want to undelegate from. * @param {String} event the event name * @param {Function} cb the callback handler * @return {jQuery.Delegate} the delegate for chaining */ - undelegate : function(attr, event, cb){ - attr = $.trim(attr); + undelegate : function(selector, event, cb){ + selector = $.trim(selector); var i =0, delegates = $.data(this, "_observe_delegates") || [], - delegate; - if(attr){ + delegateOb; + if(selector){ while(i < delegates.length){ - delegate = delegates[i]; - if( delegate.callback === cb || - (!cb && delegate.attr === attr) ){ + delegateOb = delegates[i]; + if( delegateOb.callback === cb || + (!cb && delegateOb.selector === selector) ){ + delegateOb.undelegated = true; delegates.splice(i,1) } else { i++; } } } else { + // remove all delegates delegates = []; } if(!delegates.length){ @@ -199,5 +307,7 @@ steal('jquery/lang/observe',function(){ } return this; } - }) + }); + // add helpers for testing .. + $.Observe.prototype.delegate.matches = matches; }) diff --git a/lang/observe/delegate/delegate_test.js b/lang/observe/delegate/delegate_test.js index 9ff04408..c938e285 100644 --- a/lang/observe/delegate/delegate_test.js +++ b/lang/observe/delegate/delegate_test.js @@ -3,28 +3,99 @@ steal('funcunit/qunit','jquery/lang/observe',function(){ module('jquery/lang/observe/delegate') -test("delegate", function(){ +var matches = $.Observe.prototype.delegate.matches; + +test("matches", function(){ + + equals( matches(['**'], ['foo','bar','0']) , + 'foo.bar.0' , "everything" ); + + equals( matches(['*.**'], ['foo']) , + null , "everything at least one level deep" ) + + equals( matches(['foo','*'], ['foo','bar','0']) , + 'foo.bar' ) + + equals(matches(['*'], + ['foo','bar','0']) , + 'foo' ); + + equals( matches([ '*', 'bar' ], + ['foo','bar','0']) , + 'foo.bar' ) + // - props - + // - returns - 'foo.bar' +}) + +test("list events", function(){ + + var list = new $.Observe.List([ + {name: 'Justin'}, + {name: 'Brian'}, + {name: 'Austin'}, + {name: 'Mihael'}]) + list.comparator = 'name'; + list.sort(); + // events on a list + // - move - item from one position to another + // due to changes in elements that change the sort order + // - add (items added to a list) + // - remove (items removed from a list) + // - reset (all items removed from the list) + // - change something happened + + // a move directly on this list + list.bind('move', function(ev, item, newPos, oldPos){ + ok(true,"move called"); + equals(item.name, "Zed"); + equals(newPos, 3); + equals(oldPos, 0); + }); + + // a remove directly on this list + list.bind('remove', function(ev, items, oldPos){ + ok(true,"remove called"); + equals(items.length,1); + equals(items[0].name, 'Alexis'); + equals(oldPos, 0, "put in right spot") + }) + list.bind('add', function(ev, items, newPos){ + ok(true,"add called"); + equals(items.length,1); + equals(items[0].name, 'Alexis'); + equals(newPos, 0, "put in right spot") + }); + + list.push({name: 'Alexis'}); + + // now lets remove alexis ... + list.splice(0,1); + list[0].attr('name',"Zed") +}) + + +test("delegate", 4,function(){ var state = new $.Observe({ properties : { - price : [] + prices : [] } }); - var price = state.attr('properties.price'); + var prices = state.attr('properties.prices'); - state.delegate("properties.price","change", function(ev, attr, how, val, old){ - equals(attr, "properties.price", "correct change name") + state.delegate("properties.prices","change", function(ev, attr, how, val, old){ + equals(attr, "0", "correct change name") equals(how, "add") equals(val[0].attr("foo"),"bar", "correct") - ok(this === price, "rooted element") + ok(this === prices, "rooted element") }); - price.push({foo: "bar"}); + prices.push({foo: "bar"}); state.undelegate(); }) -test("delegate on add", function(){ +test("delegate on add", 2, function(){ var state = new $.Observe({}); @@ -39,7 +110,7 @@ test("delegate on add", function(){ }) -test("delegate set is called on add", function(){ +test("delegate set is called on add", 2, function(){ var state = new $.Observe({}); state.delegate("foo","set", function(ev, newVal){ @@ -47,7 +118,136 @@ test("delegate set is called on add", function(){ equals(newVal, "bar","got newVal") }); state.attr("foo","bar") +}); + +test("delegate's this", 5, function(){ + var state = new $.Observe({ + person : { + name : { + first : "justin", + last : "meyer" + } + }, + prop : "foo" + }); + var n = state.attr('person.name'), + check + + // listen to person name changes + state.delegate("person.name","set", check = function(ev, newValue, oldVal, from){ + // make sure we are getting back the person.name + equals(this, n) + equals(newValue, "Brian"); + equals(oldVal, "justin"); + // and how to get there + equals(from,"first") + }); + n.attr('first',"Brian"); + state.undelegate("person.name",'set',check) + // stop listening + + // now listen to changes in prop + state.delegate("prop","set", function(){ + equals(this, 'food'); + }); // this is weird, probably need to support direct bind ... + + // update the prop + state.attr('prop','food') }) +test("delegate on deep properties with *", function(){ + var state = new $.Observe({ + person : { + name : { + first : "justin", + last : "meyer" + } + } + }); + + state.delegate("person","set", function(ev, newVal, oldVal, attr){ + equals(this, state.attr('person'), "this is set right") + equals(attr, "name.first") + }); + state.attr("person.name.first","brian") +}); + +test("compound sets", function(){ + + var state = new $.Observe({ + type : "person", + id: "5" + }); + var count = 0; + state.delegate("type=person id","set", function(){ + equals(state.type, "person","type is person") + ok(state.id !== undefined, "id has value"); + count++; + }) + + // should trigger a change + state.attr("id",0); + equals(count, 1, "changing the id to 0 caused a change"); + + // should not fire a set + state.removeAttr("id") + equals(count, 1, "removing the id changed nothing"); + + state.attr("id",3) + equals(count, 2, "adding an id calls callback"); + + state.attr("type","peter") + equals(count, 2, "changing the type does not fire callback"); + + state.removeAttr("type"); + state.removeAttr("id"); + + equals(count, 2, ""); + + state.attrs({ + type : "person", + id: "5" + }); + + equals(count, 3, "setting person and id only fires 1 event"); + + state.removeAttr("type"); + state.removeAttr("id"); + + state.attrs({ + type : "person" + }); + equals(count, 3, "setting person does not fire anything"); +}) + +test("undelegate within event loop",1, function(){ + + var state = new $.Observe({ + type : "person", + id: "5" + }); + var f1 = function(){ + state.undelegate("type","add",f2); + }, + f2 = function(){ + ok(false,"I am removed, how am I called") + }, + f3 = function(){ + state.undelegate("type","add",f1); + }, + f4 = function(){ + ok(true,"f4 called") + }; + state.delegate("type", "set", f1); + state.delegate("type","set",f2); + state.delegate("type","set",f3); + state.delegate("type","set",f4); + state.attr("type","other"); + +}) + + + + }); \ No newline at end of file diff --git a/lang/observe/demo.html b/lang/observe/demo.html new file mode 100644 index 00000000..301be853 --- /dev/null +++ b/lang/observe/demo.html @@ -0,0 +1,140 @@ + + + + object + + + +

      My Unsorted Todo List

      + +

      My Sorted Todo List

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

      Model DOM Helpers Demo

      steal('jquery/model','jquery/dom/fixture').then(function(){ $.fixture("GET /contacts.json", function(){ - return [[{id: 1, name: 'Justin Meyer', birthday: 403938000000}, - {id: 2, name: 'Brian Moschel', birthday: 437205600000}, - {id: 3, name: 'Mihael Konjevic', birthday: 483771600000}]]; + return [[{id: 1, name: 'Justin Meyer', + birthday: 403938000000}, + {id: 2, name: 'Brian Moschel', + birthday: 437205600000}, + {id: 3, name: 'Mihael Konjevic', + birthday: 483771600000}]]; }); $.fixture("DELETE /contacts.json", function(){ diff --git a/model/demo-setter.html b/model/demo-setter.html index 3fca9a5d..6d0d58e8 100644 --- a/model/demo-setter.html +++ b/model/demo-setter.html @@ -30,9 +30,12 @@

      Model Setter Demo

      function(){ $.fixture("/contacts.json", function(orig, settings, headers){ - return [[{'id': 1,'name' : 'Justin Meyer','birthday': '1982-10-20'}, - {'id': 2,'name' : 'Brian Moschel','birthday': '1983-11-10'}, - {'id': 3,'name' : 'Alex Gomes','birthday': '1980-2-10'}]]; + return [[{'id': 1,'name' : 'Justin Meyer', + 'birthday': '1982-10-20'}, + {'id': 2,'name' : 'Brian Moschel', + 'birthday': '1983-11-10'}, + {'id': 3,'name' : 'Alex Gomes', + 'birthday': '1980-2-10'}]]; }); // A contact model diff --git a/model/guesstype/guesstype.js b/model/guesstype/guesstype.js deleted file mode 100644 index e29f0b17..00000000 --- a/model/guesstype/guesstype.js +++ /dev/null @@ -1,37 +0,0 @@ -steal('jquery/model').then(function(){ - - - /** - * @hide - * Guesses the type of an object. This is what sets the type if not provided in - * [jQuery.Model.static.attributes]. - * @param {Object} object the object you want to test. - * @return {String} one of string, object, date, array, boolean, number, function - */ - $.Model.guessType= function( object ) { - if ( typeof object != 'string' ) { - if ( object === null ) { - return typeof object; - } - if ( object.constructor == Date ) { - return 'date'; - } - if ( isArray(object) ) { - return 'array'; - } - return typeof object; - } - if ( object === "" ) { - return 'string'; - } - //check if true or false - if ( object == 'true' || object == 'false' ) { - return 'boolean'; - } - if (!isNaN(object) && isFinite(+object) ) { - return 'number'; - } - return typeof object; - }; - -}); diff --git a/model/guesstype/guesstype_test.js b/model/guesstype/guesstype_test.js deleted file mode 100644 index 54b4bf1d..00000000 --- a/model/guesstype/guesstype_test.js +++ /dev/null @@ -1,12 +0,0 @@ -module("jquery/model/guesstype") - -test("guess type", function(){ - equals("array", $.Model.guessType( [] ) ); - equals("date", $.Model.guessType( new Date() ) ); - equals("boolean", $.Model.guessType( true ) ); - equals("number", $.Model.guessType( "1" ) ); - equals("string", $.Model.guessType( "a" ) ); - - equals("string", $.Model.guessType( "1e234234324234" ) ); - equals("string", $.Model.guessType( "-1e234234324234" ) ); -}) diff --git a/model/list/list.js b/model/list/list.js index ed694d63..6996b8db 100644 --- a/model/list/list.js +++ b/model/list/list.js @@ -5,8 +5,7 @@ steal('jquery/model').then(function( $ ) { return args[0] } else if ( args[0] instanceof $.Model.List ) { return $.makeArray(args[0]) - } - else { + } else { return $.makeArray(args) } }, @@ -447,7 +446,8 @@ steal('jquery/model').then(function( $ ) { * * var match = list.get($('#content')[0]) * - * @param {Object} args element or id to remove + * @param {Object} args elements or ids to retrieve. + * @return {$.Model.List} A sub-Model.List with the elements that were queried. */ get: function() { if (!this.length ) { @@ -465,8 +465,10 @@ steal('jquery/model').then(function( $ ) { for ( var i = 0; i < args.length; i++ ) { if ( args[i].nodeName && (matches = args[i].className.match(test)) ) { + // If this is a dom element val = this._data[matches[1]] } else { + // Else an id was provided as a number or string. val = this._data[typeof args[i] == 'string' || typeof args[i] == 'number' ? args[i] : args[i][idName]] } val && list.push(val) @@ -478,13 +480,14 @@ steal('jquery/model').then(function( $ ) { * * To remove by id: * - * var match = list.get(23); + * var match = list.remove(23); * * or to remove by element: * - * var match = list.get($('#content')[0]) + * var match = list.remove($('#content')[0]) * - * @param {Object} args element or id to remove + * @param {Object} args elements or ids to remove. + * @return {$.Model.List} A Model.List of the elements that were removed. */ remove: function( args ) { if (!this.length ) { @@ -691,13 +694,15 @@ steal('jquery/model').then(function( $ ) { }, /** * @function push - * Adds a instance or instances to the list + * Adds an instance or instances to the list * * list.push(new Recipe({id: 5, name: "Water"})) + * + * @param args {Object} The instance(s) to push onto the list. + * @return {Number} The number of elements in the list after the new element was pushed in. */ push: function() { - var args = getArgs(arguments), - self = this; + var args = getArgs(arguments); //listen to events on this only if someone is listening on us, this means remove won't //be called if we aren't listening for removes if ( this[expando] !== undefined ) { @@ -755,6 +760,14 @@ steal('jquery/model').then(function( $ ) { * */ splice: [].splice, + /** + * @function indexOf + * Finds the index of the item in the list. Returns -1 if not found. + * + * list.indexOf(item) + * + */ + indexOf: [].indexOf, /** * @function sort * Sorts the instances in the list. @@ -762,7 +775,15 @@ steal('jquery/model').then(function( $ ) { * list.sort(sortfunc) * */ - sort: [].sort + sort: [].sort, + /** + * @function reverse + * Reverse the list in place + * + * list.reverse() + * + */ + reverse: [].reverse } each(modifiers, function( name, func ) { diff --git a/model/list/local/local.js b/model/list/local/local.js index 568a63cb..eb9bfb0f 100644 --- a/model/list/local/local.js +++ b/model/list/local/local.js @@ -26,7 +26,7 @@ $.Model.List("jQuery.Model.List.Local", // go through and listen to instance updating var ids = [], days = this.days; this.each(function(i, inst){ - window.localStorage[inst.identity()] = instance.attrs(); + window.localStorage[inst.identity()] = inst.attrs(); ids.push(inst.identity()); }); window.localStorage[name] = { diff --git a/model/model.js b/model/model.js index c576c7ab..9e0e589d 100644 --- a/model/model.js +++ b/model/model.js @@ -13,55 +13,49 @@ steal('jquery/class', 'jquery/lang/string', function() { extend = $.extend, each = $.each, trigger = function(obj, event, args){ - $(obj).triggerHandler( event, args ); + $.event.trigger(event, args, obj, true) }, - reqType = /GET|POST|PUT|DELETE/i, // used to make an ajax request where - // ajaxOb - a string ajax name - // attrs - the attributes or data that will be sent + // ajaxOb - a bunch of options + // data - the attributes or data that will be sent // success - callback function // error - error callback // fixture - the name of the fixture (typically a path or something on $.fixture // type - the HTTP request type (defaults to "post") // dataType - how the data should return (defaults to "json") - ajax = function( ajaxOb, attrs, success, error, fixture, type, dataType ) { - // set the dataType - var dataType = dataType || "json", - // - src = "", - tmp; + ajax = function(ajaxOb, data, success, error, fixture, type, dataType ) { + + + // if we get a string, handle it if ( typeof ajaxOb == "string" ) { // if there's a space, it's probably the type var sp = ajaxOb.indexOf(" ") - if ( sp > 2 && sp < 7 ) { - tmp = ajaxOb.substr(0, sp); - if ( reqType.test(tmp) ) { - type = tmp; - } else { - dataType = tmp; + if ( sp > -1 ) { + ajaxOb = { + url: ajaxOb.substr(sp + 1), + type: ajaxOb.substr(0, sp) } - src = ajaxOb.substr(sp + 1) } else { - src = ajaxOb; + ajaxOb = {url : ajaxOb} } } // if we are a non-array object, copy to a new attrs - typeof attrs == "object" && (!isArray(attrs)) && (attrs = extend({}, attrs)); + ajaxOb.data = typeof data == "object" && !isArray(data) ? + extend(ajaxOb.data || {}, data) : data; + // get the url with any templated values filled out - var url = $String.sub(src, attrs, true); + ajaxOb.url = $String.sub(ajaxOb.url, ajaxOb.data, true); - return $.ajax({ - url: url, - data: attrs, - success: success, - error: error, + return $.ajax(extend({ type: type || "post", - dataType: dataType, - fixture: fixture - }); + dataType: dataType ||"json", + fixture: fixture, + success : success, + error: error + },ajaxOb)); }, // guesses at a fixture name where // extra - where to look for 'MODELNAME'+extra fixtures (ex: "Create" -> '-recipeCreate') @@ -111,7 +105,7 @@ steal('jquery/class', 'jquery/lang/string', function() { each(items, function( i, item ) { if (!item["__u Nique"] ) { collect.push(item); - item["__u Nique"] = true; + item["__u Nique"] = 1; } }); // remove unique @@ -140,7 +134,9 @@ steal('jquery/class', 'jquery/lang/string', function() { // the args to pass to the ajax method args = [self.serialize(), resolve, reject], // the Model - model = self.constructor; + model = self.constructor, + jqXHR, + promise = deferred.promise(); // destroy does not need data if ( type == 'destroy' ) { @@ -156,10 +152,15 @@ steal('jquery/class', 'jquery/lang/string', function() { deferred.then(success); deferred.fail(error); - // call the - model[type].apply(model, args); - - return deferred.promise(); + // call the model's function and hook up + // abort + jqXHR = model[type].apply(model, args); + if(jqXHR && jqXHR.abort){ + promise.abort = function(){ + jqXHR.abort(); + } + } + return promise; }, // a quick way to tell if it's an object and not some string isObject = function( obj ) { @@ -167,8 +168,7 @@ steal('jquery/class', 'jquery/lang/string', function() { }, $method = function( name ) { return function( eventType, handler ) { - $.fn[name].apply($([this]), arguments); - return this; + return $.fn[name].apply($([this]), arguments); } }, bind = $method('bind'), @@ -180,6 +180,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/model.js * @test jquery/model/qunit.html * @plugin jquery/model + * @description Models and apps data layer. * * Models super-charge an application's * data layer, making it easy to: @@ -217,7 +218,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * __Create__ * * Create a todo instance and - * call [jQuery.Model.prototype.save save]( success, error ) + * call [$.Model::save save]\(success, error\) * to create the todo on the server. * * // create a todo instance @@ -229,7 +230,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * __Retrieve__ * * Retrieve a list of todos from the server with - * findAll( params, callback( items ) ): + * [$.Model.findAll findAll]\(params, callback(items)\): * * Todo.findAll({}, function( todos ){ * @@ -240,7 +241,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * }); * * Retrieve a single todo from the server with - * findOne( params, callback( item ) ): + * [$.Model.findOne findOne]\(params, callback(item)\): * * Todo.findOne({id: 5}, function( todo ){ * @@ -263,7 +264,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * * __Destroy__ * - * Call [jQuery.Model.prototype.destroy destroy]( success, error ) + * Call [$.Model.prototype.destroy destroy]\(success, error\) * to delete an item on the server. * * todo.destroy() @@ -273,8 +274,11 @@ steal('jquery/class', 'jquery/lang/string', function() { * Listening to changes in data is a critical part of * the [http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller Model-View-Controller] * architecture. $.Model lets you listen to when an item is created, updated, destroyed - * and its properties are changed by creating [jquery.model.events events] - * on the Model and on instances of the model. + * or its properties are changed. Use + * Model.[$.Model.bind bind]\(eventType, handler(event, model)\) + * to listen to all events of type on a model and + * model.[$.Model.prototype.bind bind]\(eventType, handler(event)\) + * to listen to events on a specific instance. * * __Create__ * @@ -331,7 +335,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * that lists each todo in the page and when a todo is * deleted, removes it. * - * [jQuery.fn.model $.fn.model]( item ) lets you set or read a model + * [jQuery.fn.model $.fn.model]\(item\) lets you set or read a model * instance from an element: * * Todo.findAll({}, function( todos ) { @@ -344,7 +348,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * }); * * When a todo is deleted, get its element with - * item.[jQuery.Model.prototype.elements elements]( context ) + * item.[$.Model.prototype.elements elements]\(context\) * and remove it from the page. * * Todo.bind('destroyed', function( ev, todo ) { @@ -377,12 +381,12 @@ steal('jquery/class', 'jquery/lang/string', function() { * * ## Lists * - * [jQuery.Model.List $.Model.List] lets you handle multiple model instances + * [$.Model.List $.Model.List] lets you handle multiple model instances * with ease. A List acts just like an Array, but you can add special properties * to it and listen to events on it. * * $.Model.List has become its own plugin, read about it - * [jQuery.Model.List here]. + * [$.Model.List here]. * * ## Other Good Stuff * @@ -403,7 +407,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * * date.attr('dueDate') //-> new Date(1303173531164) * - * By defining property-type pairs in [jQuery.Model.static.attributes attributes], + * By defining property-type pairs in [$.Model.attributes attributes], * you can have model auto-convert values from the server into more useful types: * * $.Model('Todo',{ @@ -414,7 +418,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * * ### Associations * - * The [jQuery.Model.static.attributes attributes] property also + * The [$.Model.attributes attributes] property also * supports associations. For example, todo data might come back with * User data as an owner property like: * @@ -423,7 +427,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * owner: { name: 'Justin', id: 3} } * * To convert owner into a User model, set the owner type as the User's - * [jQuery.Model.static.model model]( data ) method: + * [$.Model.model model]( data ) method: * * $.Model('Todo',{ * attributes : { @@ -457,8 +461,8 @@ steal('jquery/class', 'jquery/lang/string', function() { * ### Deferreds * * Model methods that make requests to the server such as: - * [jQuery.Model.static.findAll findAll], [jQuery.Model.static.findOne findOne], - * [jQuery.Model.prototype.save save], and [jQuery.Model.prototype.destroy destroy] return a + * [$.Model.findAll findAll], [$.Model.findOne findOne], + * [$.Model.prototype.save save], and [$.Model.prototype.destroy destroy] return a * [jquery.model.deferreds deferred] that resolves to the item(s) * being retrieved or modified. * @@ -493,14 +497,9 @@ steal('jquery/class', 'jquery/lang/string', function() { create: function( str ) { /** * @function create - * Create is used to create a model instance on the server. By implementing - * create along with the rest of the [jquery.model.services service api], your models provide an abstract - * API for services. + * Create is used by [$.Model.prototype.save save] to create a model instance on the server. * - * Create is called by save to create a new instance. If you want to be able to call save on an instance - * you have to implement create. - * - * The easiest way to implement create is to just give it the url to post data to: + * The easiest way to implement create is to give it the url to post data to: * * $.Model("Recipe",{ * create: "/recipes" @@ -508,21 +507,25 @@ steal('jquery/class', 'jquery/lang/string', function() { * * This lets you create a recipe like: * - * new Recipe({name: "hot dog"}).save(function(){ - * this.name //this is the new recipe - * }).save(callback) + * new Recipe({name: "hot dog"}).save(); * - * You can also implement create by yourself. You just need to call success back with + * You can also implement create by yourself. Create gets called with: + * + * - `attrs` - the [$.Model.serialize serialized] model attributes. + * - `success(newAttrs)` - a success handler. + * - `error` - an error handler. + * + * You just need to call success back with * an object that contains the id of the new instance and any other properties that should be * set on the instance. * * For example, the following code makes a request - * to '/recipes.json?name=hot+dog' and gets back + * to `POST /recipes.json {'name': 'hot+dog'}` and gets back * something that looks like: * * { - * id: 5, - * createdAt: 2234234329 + * "id": 5, + * "createdAt": 2234234329 * } * * The code looks like: @@ -535,25 +538,21 @@ steal('jquery/class', 'jquery/lang/string', function() { * * * @param {Object} attrs Attributes on the model instance - * @param {Function} success(attrs) the callback function, it must be called with an object + * @param {Function} success(newAttrs) the callback function, it must be called with an object * that has the id of the new instance and any other attributes the service needs to add. * @param {Function} error a function to callback if something goes wrong. */ return function( attrs, success, error ) { - return ajax(str, attrs, success, error, fixture(this, "Create", "-restCreate")) + return ajax(str || this._shortName, attrs, success, error, fixture(this, "Create", "-restCreate")) }; }, update: function( str ) { /** * @function update - * Update is used to update a model instance on the server. By implementing - * update along with the rest of the [jquery.model.services service api], your models provide an abstract - * API for services. - * - * Update is called by [jQuery.Model.prototype.save] or [jQuery.Model.prototype.update] - * on an existing model instance. + * Update is used by [$.Model.prototype.save save] to + * update a model instance on the server. * - * The easist way to implement update is to just give it the url to put data to: + * The easist way to implement update is to just give it the url to `PUT` data to: * * $.Model("Recipe",{ * update: "/recipes/{id}" @@ -615,15 +614,13 @@ steal('jquery/class', 'jquery/lang/string', function() { * @param {Function} error a function to callback if something goes wrong. */ return function( id, attrs, success, error ) { - return ajax(str, addId(this, attrs, id), success, error, fixture(this, "Update", "-restUpdate"), "put") + return ajax( str || this._shortName+"/{"+this.id+"}", addId(this, attrs, id), success, error, fixture(this, "Update", "-restUpdate"), "put") } }, destroy: function( str ) { /** * @function destroy - * Destroy is used to remove a model instance from the server. By implementing - * destroy along with the rest of the [jquery.model.services service api], your models provide an abstract - * service API. + * Destroy is used to remove a model instance from the server. * * You can implement destroy with a string like: * @@ -649,17 +646,15 @@ steal('jquery/class', 'jquery/lang/string', function() { return function( id, success, error ) { var attrs = {}; attrs[this.id] = id; - return ajax(str, attrs, success, error, fixture(this, "Destroy", "-restDestroy"), "delete") + return ajax( str || this._shortName+"/{"+this.id+"}", attrs, success, error, fixture(this, "Destroy", "-restDestroy"), "delete") } }, findAll: function( str ) { /** * @function findAll - * FindAll is used to retrive a model instances from the server. By implementing - * findAll along with the rest of the [jquery.model.services service api], your models provide an abstract - * service API. - * findAll returns a deferred ($.Deferred) + * FindAll is used to retrive a model instances from the server. + * findAll returns a deferred ($.Deferred). * * You can implement findAll with a string: * @@ -667,13 +662,14 @@ steal('jquery/class', 'jquery/lang/string', function() { * findAll : "/things.json" * },{}) * - * Or you can implement it yourself. The 'dataType' attribute is used to convert a JSON array of attributes - * to an array of instances. For example: + * Or you can implement it yourself. The `dataType` attribute + * is used to convert a JSON array of attributes + * to an array of instances. It calls [$.Model.models]\(raw\). For example: * * $.Model("Thing",{ * findAll : function(params, success, error){ * return $.ajax({ - * url: '/things.json', + * url: '/things.json', * type: 'get', * dataType: 'json thing.models', * data: params, @@ -689,7 +685,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * @param {Function} error */ return function( params, success, error ) { - return ajax(str || this.shortName + "s.json", params, success, error, fixture(this, "s"), "get", "json " + this._shortName + ".models"); + return ajax( str || this._shortName, params, success, error, fixture(this, "s"), "get", "json " + this._shortName + ".models"); }; }, findOne: function( str ) { @@ -725,7 +721,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * @param {Function} error */ return function( params, success, error ) { - return ajax(str, params, success, error, fixture(this), "get", "json " + this._shortName + ".model"); + return ajax(str || this._shortName+"/{"+this.id+"}", params, success, error, fixture(this), "get", "json " + this._shortName + ".model"); }; } }; @@ -756,25 +752,26 @@ steal('jquery/class', 'jquery/lang/string', function() { this._fullName = underscore(fullName.replace(/\./g, "_")); this._shortName = underscore(this.shortName); - if ( fullName.substr(0, 7) == "jQuery." ) { + if ( fullName.indexOf("jQuery") == 0 ) { return; } //add this to the collection of models - //jQuery.Model.models[this._fullName] = this; + //$.Model.models[this._fullName] = this; if ( this.listType ) { this.list = new this.listType([]); } - //@steal-remove-start + //!steal-remove-start if (!proto ) { steal.dev.warn("model.js " + fullName + " has no static properties. You probably need ,{} ") } - //@steal-remove-end - for ( var name in ajaxMethods ) { - if ( typeof this[name] !== 'function' ) { - this[name] = ajaxMethods[name](this[name]); + //!steal-remove-end + each(ajaxMethods, function(name, method){ + var prop = self[name]; + if ( typeof prop !== 'function' ) { + self[name] = method(prop); } - } + }); //add ajax converters var converters = {}, @@ -791,7 +788,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * @attribute attributes * Attributes contains a map of attribute names/types. * You can use this in conjunction with - * [jQuery.Model.static.convert] to provide automatic + * [$.Model.convert] to provide automatic * [jquery.model.typeconversion type conversion] (including * associations). * @@ -835,13 +832,18 @@ steal('jquery/class', 'jquery/lang/string', function() { * Task.models and Person.model * to convert the raw data into an array of Tasks and a Person. * + * Note that the full names of the models themselves are App.Models.Task + * and App.Models.Person. The _.model_ and _.models_ parts are appended + * for the benefit of [$.Model.convert convert] to identify the types as + * models. + * * @demo jquery/model/pages/associations.html * */ attributes: {}, /** * $.Model.model is used as a [http://api.jquery.com/extending-ajax/#Converters Ajax converter] - * to convert the response of a [jQuery.Model.static.findOne] request + * to convert the response of a [$.Model.findOne] request * into a model instance. * * You will never call this method directly. Instead, you tell $.ajax about it in findOne: @@ -915,8 +917,8 @@ steal('jquery/class', 'jquery/lang/string', function() { }, /** * $.Model.models is used as a [http://api.jquery.com/extending-ajax/#Converters Ajax converter] - * to convert the response of a [jQuery.Model.static.findAll] request - * into an array (or [jQuery.Model.List $.Model.List]) of model instances. + * to convert the response of a [$.Model.findAll] request + * into an array (or [$.Model.List $.Model.List]) of model instances. * * You will never call this method directly. Instead, you tell $.ajax about it in findAll: * @@ -974,12 +976,12 @@ steal('jquery/class', 'jquery/lang/string', function() { * * $.Model('Person',{ * models : function(data){ - * this._super(data.ballers); + * return this._super(data.ballers); * } * },{}) * * @param {Array} instancesRawData an array of raw name - value pairs. - * @return {Array} a JavaScript array of instances or a [jQuery.Model.List list] of instances + * @return {Array} a JavaScript array of instances or a [$.Model.List list] of instances * if the model list plugin has been included. */ models: function( instancesRawData ) { @@ -1005,25 +1007,23 @@ steal('jquery/class', 'jquery/lang/string', function() { // get the object's data instancesRawData.data), // the number of items - length = raw.length, + length = raw ? raw.length : null, i = 0; - //@steal-remove-start + //!steal-remove-start if (!length ) { steal.dev.warn("model.js models has no data. If you have one item, use model") } - //@steal-remove-end - // res._use_call = true; //so we don't call next function with all of these + //!steal-remove-end for (; i < length; i++ ) { res.push(this.model(raw[i])); } if (!arr ) { //push other stuff onto array - for ( var prop in instancesRawData ) { + each(instancesRawData, function(prop, val){ if ( prop !== 'data' ) { - res[prop] = instancesRawData[prop]; + res[prop] = val; } - - } + }) } return res; }, @@ -1052,16 +1052,19 @@ steal('jquery/class', 'jquery/lang/string', function() { stub = attrs[property] || (attrs[property] = type); return type; }, - guessType: function() { - return "string" - }, /** * @attribute convert * @type Object * An object of name-function pairs that are used to convert attributes. - * Check out [jQuery.Model.static.attributes] or + * Check out [$.Model.attributes] or * [jquery.model.typeconversion type conversion] * for examples. + * + * Convert comes with the following types: + * + * - date - Converts to a JS date. Accepts integers or strings that work with Date.parse + * - number - an integer or number that can be passed to parseFloat + * - boolean - converts "false" to false, and puts everything else through Boolean() */ convert: { "date": function( str ) { @@ -1078,7 +1081,7 @@ steal('jquery/class', 'jquery/lang/string', function() { return parseFloat(val); }, "boolean": function( val ) { - return Boolean(val); + return Boolean(val === "false" ? 0 : val); }, "default": function( val, error, type ) { var construct = getObject(type), @@ -1094,6 +1097,31 @@ steal('jquery/class', 'jquery/lang/string', function() { return typeof construct == "function" ? construct.call(context, val) : val; } }, + /** + * @attribute serialize + * @type Object + * An object of name-function pairs that are used to serialize attributes. + * Similar to [$.Model.convert], in that the keys of this object + * correspond to the types specified in [$.Model.attributes]. + * + * For example, to serialize all dates to ISO format: + * + * + * $.Model("Contact",{ + * attributes : { + * birthday : 'date' + * }, + * serialize : { + * date : function(val, type){ + * return new Date(val).toISOString(); + * } + * } + * },{}) + * + * new Contact({ birthday: new Date("Oct 25, 1973") }).serialize() + * // { "birthday" : "1973-10-25T05:00:00.000Z" } + * + */ serialize: { "default": function( val, type ) { return isObject(val) && val.serialize ? val.serialize() : val; @@ -1102,7 +1130,13 @@ steal('jquery/class', 'jquery/lang/string', function() { return val && val.getTime() } }, + /** + * @function bind + */ bind: bind, + /** + * @function unbind + */ unbind: unbind, _ajax: ajax }, @@ -1134,7 +1168,7 @@ steal('jquery/class', 'jquery/lang/string', function() { /** * Sets the attributes on this instance and calls save. * The instance needs to have an id. It will use - * the instance class's [jQuery.Model.static.update update] + * the instance class's [$.Model.update update] * method. * * @codestart @@ -1171,7 +1205,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * * errors.dueDate[0] //-> "can't be empty" * - * @params {Array} [attrs] an optional list of attributes to get errors for: + * @param {Array} [attrs] an optional list of attributes to get errors for: * * task.errors(['dueDate']); * @@ -1276,7 +1310,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * ## Events * * When you use attr, it can also trigger events. This is - * covered in [jQuery.Model.prototype.bind]. + * covered in [$.Model.prototype.bind]. * * @param {String} attribute the attribute you want to set or get * @param {String|Number|Boolean} [value] value the value you want to set. @@ -1325,6 +1359,7 @@ steal('jquery/class', 'jquery/lang/string', function() { }, /** + * @function bind * Binds to events on this model instance. Typically * you'll bind to an attribute name. Handler will be called * every time the attribute value changes. For example: @@ -1364,23 +1399,14 @@ steal('jquery/class', 'jquery/lang/string', function() { */ bind: bind, /** + * @function unbind * Unbinds an event handler from this instance. - * Read [jQuery.Model.prototype.bind] for + * Read [$.Model.prototype.bind] for * more information. * @param {String} eventType * @param {Function} handler */ unbind: unbind, - // Checks if there is a setProperty value. - // If it returns true, lets it handle; otherwise - // property - the attribute name - // value - the value to set - // success - a successful callback - // error - an error callback - // capitalized - the clasized property value (expensive to recalculate) - _setProperty: function( property, value, success, error, capitalized ) { - - }, // Actually updates a property on a model. This // - Triggers events when a property has been updated // - uses converters to change the data type @@ -1393,7 +1419,7 @@ steal('jquery/class', 'jquery/lang/string', function() { // the value that we will set val, // the type of the attribute - type = Class.attributes[property] || Class.addAttr(property, Class.guessType(value)), + type = Class.attributes[property] || Class.addAttr(property, "string"), //the converter converter = Class.convert[type] || Class.convert['default'], // errors for this property @@ -1454,7 +1480,7 @@ steal('jquery/class', 'jquery/lang/string', function() { /** * Removes an attribute from the list existing of attributes. - * Each attribute is set with [jQuery.Model.prototype.attr attr]. + * Each attribute is set with [$.Model.prototype.attr attr]. * * @codestart * recipe.removeAttr('name') @@ -1486,7 +1512,7 @@ steal('jquery/class', 'jquery/lang/string', function() { /** * Gets or sets a list of attributes. - * Each attribute is set with [jQuery.Model.prototype.attr attr]. + * Each attribute is set with [$.Model.prototype.attr attr]. * * @codestart * recipe.attrs({ @@ -1525,6 +1551,15 @@ steal('jquery/class', 'jquery/lang/string', function() { } return attributes; }, + /** + * Get a serialized object for the model. Serialized data is typically + * used to send back to a server. See [$.Model.serialize]. + * + * model.serialize() // -> { name: 'Fred' } + * + * @return {Object} a JavaScript object that can be serialized with + * `JSON.stringify` or other methods. + */ serialize: function() { var Class = this.constructor, attrs = Class.attributes, @@ -1536,8 +1571,9 @@ steal('jquery/class', 'jquery/lang/string', function() { for ( attr in attrs ) { if ( attrs.hasOwnProperty(attr) ) { type = attrs[attr]; + // the attribute's converter or the default converter for the class converter = Class.serialize[type] || Class.serialize['default']; - data[attr] = converter(this[attr], type); + data[attr] = converter.call(Class, this[attr], type); } } return data; @@ -1551,12 +1587,12 @@ steal('jquery/class', 'jquery/lang/string', function() { */ isNew: function() { var id = getId(this); - return (id === undefined || id === null); //if null or undefined + return (id === undefined || id === null || id === ''); //if null or undefined }, /** - * Creates or updates the instance using [jQuery.Model.static.create] or - * [jQuery.Model.static.update] depending if the instance - * [jQuery.Model.prototype.isNew has an id or not]. + * Creates or updates the instance using [$.Model.create] or + * [$.Model.update] depending if the instance + * [$.Model.prototype.isNew has an id or not]. * * When a save is successful, `success` is called and depending if the * instance was created or updated, a created or updated event is fired. @@ -1588,8 +1624,8 @@ steal('jquery/class', 'jquery/lang/string', function() { * }, error); * * - * @param {Function} [success(instance,data)] called if a successful save. - * @param {Function} [error(jqXHR)] error handler function called if the + * @param {Function} [success] called with (instance,data) if a successful save. + * @param {Function} [error] error handler function called with (jqXHR) if the * save was not successful. It is passed the ajax request's jQXHR object. * @return {$.Deferred} a jQuery deferred that resolves to the instance, but * after it has been created or updated. @@ -1600,7 +1636,7 @@ steal('jquery/class', 'jquery/lang/string', function() { /** * Destroys the instance by calling - * [jQuery.Model.static.destroy] with the id of the instance. + * [$.Model.destroy] with the id of the instance. * * @codestart * recipe.destroy(success, error); @@ -1624,7 +1660,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * new Todo({id: 5}).identity() //-> 'todo_5' * @codeend * Typically this is used in an element's shortName property so you can find all elements - * for a model with [jQuery.Model.prototype.elements elements]. + * for a model with [$.Model.prototype.elements elements]. * @return {String} */ identity: function() { @@ -1634,7 +1670,7 @@ steal('jquery/class', 'jquery/lang/string', function() { }, /** * Returns elements that represent this model instance. For this to work, your element's should - * us the [jQuery.Model.prototype.identity identity] function in their class name. Example: + * us the [$.Model.prototype.identity identity] function in their class name. Example: * *
      ...
      * @@ -1656,7 +1692,12 @@ steal('jquery/class', 'jquery/lang/string', function() { * identity in their class name. */ elements: function( context ) { - return $("." + this.identity(), context); + var id = this.identity(); + if( this.constructor.escapeIdentity ) { + id = id.replace(/([ #;&,.+*~\'%:"!^$[\]()=>|\/])/g,'\\$1') + } + + return $("." + id, context); }, hookup: function( el ) { var shortName = this.constructor._shortName, @@ -1706,12 +1747,12 @@ steal('jquery/class', 'jquery/lang/string', function() { // call event on the instance trigger(this,funcName); - //@steal-remove-start + //!steal-remove-start steal.dev.log("Model.js - "+ constructor.shortName+" "+ funcName); - //@steal-remove-end + //!steal-remove-end // call event on the instance's Class - trigger([constructor],funcName, this); + trigger(constructor,funcName, this); return [this].concat(makeArray(arguments)); // return like this for this.proxy chains }; }); @@ -1723,7 +1764,7 @@ steal('jquery/class', 'jquery/lang/string', function() { /** * @function models * Returns a list of models. If the models are of the same - * type, and have a [jQuery.Model.List], it will return + * type, and have a [$.Model.List], it will return * the models wrapped with the list. * * @codestart @@ -1731,7 +1772,7 @@ steal('jquery/class', 'jquery/lang/string', function() { * @codeend * * @param {jQuery.Class} [type] if present only returns models of the provided type. - * @return {Array|jQuery.Model.List} returns an array of model instances that are represented by the contained elements. + * @return {Array|$.Model.List} returns an array of model instances that are represented by the contained elements. */ $.fn.models = function( type ) { //get it from the data @@ -1781,71 +1822,4 @@ steal('jquery/class', 'jquery/lang/string', function() { } }; - /** - * @page jquery.model.services Service APIs - * @parent jQuery.Model - * - * Models provide an abstract API for connecting to your Services. - * By implementing static: - * - * - [jQuery.Model.static.findAll] - * - [jQuery.Model.static.findOne] - * - [jQuery.Model.static.create] - * - [jQuery.Model.static.update] - * - [jQuery.Model.static.destroy] - * - * You can find more details on how to implement each method. - * Typically, you can just use templated service urls. But if you need to - * implement these methods yourself, the following - * is a useful quick reference: - * - * ### create(attrs, success([attrs]), error()) -> deferred - * - * - attrs - an Object of attribute / value pairs - * - success([attrs]) - Create calls success when the request has completed - * successfully. Success can be called back with an object that represents - * additional properties that will be set on the instance. For example, the server might - * send back an updatedAt date. - * - error - Create should callback error if an error happens during the request - * - deferred - A deferred that gets resolved to any additional attrs - * that might need to be set on the model instance. - * - * - * ### findAll( params, success(items), error) -> deferred - * - * - * - params - an Object that filters the items returned - * - success(items) - success should be called with an Array of Model instances. - * - error - called if an error happens during the request - * - deferred - A deferred that gets resolved to the list of items - * - * ### findOne(params, success(items), error) -> deferred - * - * - params - an Object that filters the item returned - * - success(item) - success should be called with a model instance. - * - error - called if an error happens during the request - * - deferred - A deferred that gets resolved to a model instance - * - * ### update(id, attrs, success([attrs]), error()) -> deferred - * - * - id - the id of the instance you are updating - * - attrs - an Object of attribute / value pairs - * - success([attrs]) - Call success when the request has completed - * successfully. Success can be called back with an object that represents - * additional properties that will be set on the instance. For example, the server might - * send back an updatedAt date. - * - error - Callback error if an error happens during the request - * - deferred - A deferred that gets resolved to any additional attrs - * that might need to be set on the model instance. - * - * ### destroy(id, success([attrs]), error()) -> deferred - * - * - id - the id of the instance you are destroying - * - success([attrs]) - Calls success when the request has completed - * successfully. Success can be called back with an object that represents - * additional properties that will be set on the instance. - * - error - Create should callback error if an error happens during the request - * - deferred - A deferred that gets resolved to any additional attrs - * that might need to be set on the model instance. - */ -}); \ No newline at end of file +}); diff --git a/model/modelBinder.html b/model/modelBinder.html index 5da061ba..cba4a743 100644 --- a/model/modelBinder.html +++ b/model/modelBinder.html @@ -46,7 +46,7 @@ + diff --git a/view/ejs/test/qunit/qunit.js b/view/ejs/test/qunit/qunit.js deleted file mode 100644 index 65f03ff3..00000000 --- a/view/ejs/test/qunit/qunit.js +++ /dev/null @@ -1,5 +0,0 @@ -//we probably have to have this only describing where the tests are -steal("jquery/view/ejs") //load your app - .then('funcunit/qunit') //load qunit - .then("./ejs_test.js") - \ No newline at end of file diff --git a/view/ejs/test_template.ejs b/view/ejs/test_template.ejs new file mode 100644 index 00000000..23347b00 --- /dev/null +++ b/view/ejs/test_template.ejs @@ -0,0 +1,8 @@ +<%# Test Something Produces Items%> +<%== something(function(items){ %> +<%== items.length%> +<% $.each(items, function(){ %><%# Test Something Produces Items%> +<%== something(function(items){ %>ItemsLength<%== items.length %><% }) %> +<% }) %> +<% }) %> +<% for( var i =0; i < items.length; i++) { %>for <%= items[i] %><% } %> \ No newline at end of file diff --git a/view/helpers/helpers.js b/view/helpers/helpers.js index 75b3f91b..478046eb 100644 --- a/view/helpers/helpers.js +++ b/view/helpers/helpers.js @@ -315,7 +315,7 @@ $.extend($.EJS.Helpers.prototype, { */ img_tag: function( image_location, options ) { options = options || {}; - options.src = steal.root.join("resources/images/"+image_location); + options.src = steal.root.join("resources/images/"+image_location)+''; return this.single_tag_for('img', options); } diff --git a/view/test/qunit/qunit.js b/view/test/qunit/qunit.js index 4c155d23..a0d926a1 100644 --- a/view/test/qunit/qunit.js +++ b/view/test/qunit/qunit.js @@ -1,5 +1,5 @@ //we probably have to have this only describing where the tests are -steal("jquery/view","jquery/view/micro","jquery/view/ejs","jquery/view/jaml","jquery/view/tmpl") //load your app +steal("jquery/view","jquery/view/micro","jquery/view/ejs/ejs_test.js","jquery/view/jaml","jquery/view/tmpl") //load your app .then('funcunit/qunit') //load qunit .then("./view_test.js","jquery/view/tmpl/tmpl_test.js") diff --git a/view/test/qunit/view_test.js b/view/test/qunit/view_test.js index 128e0fb4..ef917d84 100644 --- a/view/test/qunit/view_test.js +++ b/view/test/qunit/view_test.js @@ -68,10 +68,9 @@ test("caching works", function(){ $("#qunit-test-area").html(""); $("#qunit-test-area").html("//jquery/view/test/qunit/large.ejs",{"message" :"helloworld"}, function(text){ - var lap2 = new Date - first , + var lap2 = (new Date()) - first, lap1 = first-startT; - - ok( lap1 - lap2 > -20, "faster this time "+(lap1 - lap2) ) + // ok( lap1 > lap2, "faster this time "+(lap1 - lap2) ) start(); $("#qunit-test-area").html(""); @@ -98,7 +97,7 @@ test("inline templates other than 'tmpl' like ejs", function(){ test("object of deferreds", function(){ var foo = $.Deferred(), bar = $.Deferred(); - stop(1000); + stop(); $.View("//jquery/view/test/qunit/deferreds.ejs",{ foo : foo.promise(), bar : bar @@ -178,3 +177,13 @@ test("val set with a template within a hookup within another template", function /*test("bad url", function(){ $.View("//asfdsaf/sadf.ejs") });*/ + +test("hyphen in type", function(){ + $(document.body).append("") + + $("#qunit-test-area").html('hyphenEjs',{}); + + ok( /Hyphen/.test( $("#qunit-test-area").html() ), "has hyphen" ); +}) + + diff --git a/view/view.js b/view/view.js index b2f10ce4..069e8899 100644 --- a/view/view.js +++ b/view/view.js @@ -16,6 +16,8 @@ steal("jquery").then(function( $ ) { * @test jquery/view/qunit.html * @download dist/jquery.view.js * + * @description A JavaScript template framework. + * * View provides a uniform interface for using templates with * jQuery. When template engines [jQuery.View.register register] * themselves, you are able to: @@ -62,7 +64,7 @@ steal("jquery").then(function( $ ) { * [jQuery.fn.before before] $('#bar').before('temp.jaml',{}); * [jQuery.fn.html html] $('#bar').html('temp.jaml',{}); * [jQuery.fn.prepend prepend] $('#bar').prepend('temp.jaml',{}); - * [jQuery.fn.replaceWith replaceWith] $('#bar').replaceWidth('temp.jaml',{}); + * [jQuery.fn.replaceWith replaceWith] $('#bar').replaceWith('temp.jaml',{}); * [jQuery.fn.text text] $('#bar').text('temp.jaml',{}); * * @@ -387,7 +389,7 @@ steal("jquery").then(function( $ ) { // if we have an inline template, derive the suffix from the 'text/???' part // this only supports '' tags if ( el = document.getElementById(url) ) { - suffix = el.type.match(/\/[\d\w]+$/)[0].replace(/^\//, '.'); + suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2]; } // if there is no suffix, add one @@ -405,7 +407,7 @@ steal("jquery").then(function( $ ) { var sub = url.substr(2); url = typeof steal === "undefined" ? url = "/" + sub : - steal.root.mapJoin(sub); + steal.root.mapJoin(sub) +''; } //set the template engine type @@ -535,9 +537,9 @@ steal("jquery").then(function( $ ) { this.types["." + info.suffix] = info; if ( window.steal ) { - steal.type(info.suffix + " view js", function( options, orig, success, error ) { + steal.type(info.suffix + " view js", function( options, success, error ) { var type = $view.types["." + options.type], - id = toId(options.rootSrc); + id = toId(options.rootSrc+''); options.text = type.script(id, options.text) success(); @@ -576,9 +578,9 @@ steal("jquery").then(function( $ ) { }); if ( window.steal ) { - steal.type("view js", function( options, orig, success, error ) { + steal.type("view js", function( options, success, error ) { var type = $view.types["." + options.type], - id = toId(options.rootSrc); + id = toId(options.rootSrc+''); options.text = "steal('" + (type.plugin || "jquery/view/" + options.type) + "').then(function($){" + "$.View.preload('" + id + "'," + options.text + ");\n})"; success(); @@ -587,7 +589,9 @@ steal("jquery").then(function( $ ) { //---- ADD jQUERY HELPERS ----- //converts jquery functions to use views - var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs; + var convert, modify, isTemplate, isHTML, isDOM, getCallback, hookupView, funcs, + // text and val cannot produce an element, so don't run hookups on them + noHookup = {'val':true,'text':true}; convert = function( func_name ) { // save the old jQuery helper @@ -639,8 +643,8 @@ steal("jquery").then(function( $ ) { return this; } } - return modify.call(this, args, old); - + return noHookup[func_name] ? old.apply(this,args) : + modify.call(this, args, old); }; };