diff --git a/Gruntfile.js b/Gruntfile.js index 9217dcc0481..365c8af75b3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -151,8 +151,8 @@ module.exports = function (grunt) { steal: { options: { urls: [ - 'http://localhost:8000/test/dojo.html', 'http://localhost:8000/test/jquery.html', + 'http://localhost:8000/test/dojo.html', //'http://localhost:8000/can/test/zepto.html', 'http://localhost:8000/test/mootools.html', 'http://localhost:8000/test/yui.html' diff --git a/builder.json b/builder.json index c17bd4f7f9c..b4b94dbd946 100644 --- a/builder.json +++ b/builder.json @@ -56,6 +56,11 @@ "type": "core", "isDefault": true }, + "can/route/pushstate": { + "name": "can.route.pushstate", + "description": "can.route with pushstate", + "type": "plugin" + }, "can/control/route": { "name": "can.Control.route", "description": "Declare routes in your Control", diff --git a/control/route/route_test.js b/control/route/route_test.js index cd46edac2cb..925217fcad6 100644 --- a/control/route/route_test.js +++ b/control/route/route_test.js @@ -3,19 +3,24 @@ module("can/control/route",{ setup : function(){ stop(); + can.route.routes = {}; + can.route._teardown(); + can.route.defaultBinding = "hashchange"; + can.route.ready(); window.location.hash = ""; setTimeout(function(){ + start(); - },13) + },13); + } }); test("routes changed", function () { - can.route.ready(); expect(3); //setup controller - can.Control("Router", { + can.Control.extend("Router", { "foo/:bar route" : function () { ok(true, 'route updated to foo/:bar') }, @@ -44,9 +49,8 @@ test("routes changed", function () { }); test("route pointers", function(){ - can.route.ready(); expect(1); - var Tester = can.Control({ + var Tester = can.Control.extend({ "lol/:wat route" : "meth", meth : function(){ ok(true, "method pointer called") diff --git a/map/elements/elements.js b/map/elements/elements.js deleted file mode 100644 index 977673fa9f8..00000000000 --- a/map/elements/elements.js +++ /dev/null @@ -1,152 +0,0 @@ -steal('can/util', 'can/map', function(can, Observe) { - -var unique = function( items ) { - var collect = []; - // check unique property, if it isn't there, add to collect - can.each(items, function( item ) { - if (!item["__u Nique"] ) { - collect.push(item); - item["__u Nique"] = 1; - } - }); - // remove unique - return can.each(collect, function( item ) { - delete item["__u Nique"]; - }); - } - - can.extend(can.Map.prototype,{ - /** - * Returns a unique identifier for the observe instance. For example: - * - * @codestart - * 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 observe with [$.Observe.prototype.elements elements]. - * - * If your observe id has special characters that are not permitted as CSS class names, - * you can set the `escapeIdentity` on the observe instance's constructor - * which will `encodeURIComponent` the `id` of the observe. - * - * @return {String} The unique identifier for this instance. - */ - identity: function() { - var constructor = this.constructor, - id = this[constructor.id] || this._cid.replace(/./, ''), - name = constructor._fullName ? constructor._fullName + '_' : ''; - - return (name + (constructor.escapeIdentity ? encodeURIComponent(id) : id)).replace(/ /g, '_'); - }, - /** - * Returns elements that represent this observe instance. For this to work, your element should - * use the [$.Observe.prototype.identity identity] function in their class name. Example: - * - *
...
- * - * This also works if you hooked up the observe: - * - *
> ...
- * - * Typically, you'll use this as a response to a Observe Event: - * - * "{Todo} destroyed": function(Todo, event, todo){ - * todo.elements(this.element).remove(); - * } - * - * - * @param {String|jQuery|element} context If provided, only elements inside this element - * that represent this observe will be returned. - * - * @return {jQuery} Returns a jQuery wrapped nodelist of elements that have this observe instances - * identity in their class name. - */ - elements: function( context ) { - var id = this.identity(); - if( this.constructor.escapeIdentity ) { - id = id.replace(/([ #;&,.+*~\'%:"!^$[\]()=>|\/])/g,'\\$1') - } - - return can.$("." + id, context); - }, - hookup: function( el ) { - var shortName = this.constructor._shortName || '', - $el = can.$(el), - observes; - - (observes = can.data($el, "instances") )|| can.data($el, "instances", observes = {}); - can.addClass($el,shortName + " " + this.identity()); - observes[shortName] = this; - } - }); - - - /** - * @add jQuery.fn - */ - // break - /** - * @function instances - * Returns a list of observes. If the observes are of the same - * type, and have a [$.Observe.List], it will return - * the observes wrapped with the list. - * - * @codestart - * $(".recipes").instances() //-> [recipe, ...] - * @codeend - * - * @param {jQuery.Class} [type] if present only returns observes of the provided type. - * @return {Array|$.Observe.List} returns an array of observes instances that are represented by the contained elements. - */ - $.fn.instances = function( type ) { - //get it from the data - var collection = [], - kind, ret, retType; - this.each(function() { - can.each($.data(this, "instances") || {}, function( instance, name ) { - //either null or the list type shared by all classes - kind = kind === undefined ? instance.constructor.List || null : (instance.constructor.List === kind ? kind : null); - collection.push(instance); - }); - }); - - ret = kind ? new kind : new can.List; - - ret.push.apply(ret, unique(collection)); - return ret; - }; - /** - * @function instance - * - * Returns the first observe instance found from [jQuery.fn.instances] or - * sets the instance on an element. - * - * //gets an instance - * ".edit click" : function(el) { - * el.closest('.todo').instance().destroy() - * }, - * // sets an instance - * list : function(items){ - * var el = this.element; - * $.each(item, function(item){ - * $('
').instance(item) - * .appendTo(el) - * }) - * } - * - * @param {Object} [type] The type of instance to return. If a instance is provided - * it will add the instance to the element. - */ - $.fn.instance = function( type ) { - if ( type && type instanceof can.Map ) { - type.hookup(this[0]); - return this; - } else { - return this.instances.apply(this, arguments)[0]; - } - - }; - - return can.Map; -}) diff --git a/map/elements/elements_test.js b/map/elements/elements_test.js deleted file mode 100644 index a5978ef632b..00000000000 --- a/map/elements/elements_test.js +++ /dev/null @@ -1,15 +0,0 @@ -(function() { - -module("can/map/elements") - -test("identity uses the real id", function(){ - var Person = can.Model.extend({ - id:'ssn', - _fullName: 'Person' - },{ - }) - - equal(new Person({ssn:'987-65-4321'}).identity(),'Person_987-65-4321'); -}) - -})() diff --git a/map/elements/test.html b/map/elements/test.html deleted file mode 100644 index cdb80bac73b..00000000000 --- a/map/elements/test.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - -

can.Map.elements Test Suite

- -

- -
-

-
    -
    - - - - - - \ No newline at end of file diff --git a/map/map.js b/map/map.js index 76779a087b9..e65181be5f9 100644 --- a/map/map.js +++ b/map/map.js @@ -440,7 +440,7 @@ steal('can/util','can/util/bind','can/construct', 'can/util/batch',function(can, */ removeAttr: function( attr ) { // Info if this is List or not - var isList = this instanceof can.List, + var isList = can.List && this instanceof can.List, // Convert the `attr` into parts (if nested). parts = attrParts(attr), // The actual property to remove. diff --git a/map/map_test.js b/map/map_test.js index 94f12f8b18e..4aff18d9c56 100644 --- a/map/map_test.js +++ b/map/map_test.js @@ -47,6 +47,13 @@ test("Nested Map", 5, function(){ }) - +test("remove attr", function(){ + var state = new can.Map({ + category : 5, + productType : 4 + }); + state.removeAttr("category"); + deepEqual( can.Map.keys(state), ["productType"], "one property" ); +}) })(); diff --git a/map/transaction/qunit.html b/map/transaction/qunit.html deleted file mode 100644 index b5441542d6d..00000000000 --- a/map/transaction/qunit.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - -

    Observe Compute Test Suite

    -

    -
    -

    -
      -
      - - - - \ No newline at end of file diff --git a/map/transaction/transaction.js b/map/transaction/transaction.js deleted file mode 100644 index 45098c4149d..00000000000 --- a/map/transaction/transaction.js +++ /dev/null @@ -1,52 +0,0 @@ -steal('can', function(can){ - - - - - var events = [], - transactionCount = 0, - originalBatchTrigger = can.batch.trigger, - changedBatchTrigger = function(obj, ev){ - originalBatchTrigger.apply(this, arguments); - if(ev === "change"){ - var args = can.makeArray(arguments); - args[1] = "changed"; - originalBatchTrigger.apply(this, args); - } - }, - recordingBatchTrigger = function(obj, ev){ - originalBatchTrigger.apply(this, arguments); - if(ev === "change"){ - var args = can.makeArray(arguments); - args[1] = "changed"; - events.push( args ); - } - }; - - can.batch.trigger = changedBatchTrigger; - - can.transaction = function(){ - if( transactionCount === 0 ) { - can.batch.trigger = recordingBatchTrigger; - } - - - transactionCount++; - - - return function(){ - transactionCount--; - if( transactionCount === 0 ) { - var myEvents = events.slice(0) - events = []; - can.batch.trigger = changedBatchTrigger; - can.each(myEvents, function(eventArgs){ - originalBatchTrigger.apply(can, eventArgs); - }); - } - } - }; - - return can.Map; - -}); diff --git a/map/transaction/transaction_test.js b/map/transaction/transaction_test.js deleted file mode 100644 index 9cceff4f7e2..00000000000 --- a/map/transaction/transaction_test.js +++ /dev/null @@ -1,41 +0,0 @@ -module('can/map/transaction') - -test("Basic Transaction",function(){ - stop(); - var obs = new can.Map({ - first: "justin", - last: "meyer" - }); - var count = 0, - ready = false; - - obs.bind("changed", function(ev, attr, how, newVal, oldVal){ - - ok(ready, "event is ready"); - - if(count == 0){ - equal(attr,"first") - equal(newVal, "Justin") - } else if(count == 1){ - equal(attr,"last") - equal(newVal, "Meyer") - } else { - ok(false,"too many events") - } - count++; - }); - - var end = can.transaction(); - - obs.attr("first","Justin"); - setTimeout(function(){ - obs.attr("last","Meyer") - ready = true; - end(); - start(); - },30) - - -}); - - diff --git a/model/list/cookie/cookie.html b/model/list/cookie/cookie.html deleted file mode 100644 index f94eae02fae..00000000000 --- a/model/list/cookie/cookie.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - Model List Cookie Demo - - - -
      -

      Cookie List Demo

      -

      This demo show keeping data stored in a cookie. Create a few contacts, - refresh the page, and they should still be present.

      -
      -
      -

      Create A Contact

      -
      - -
      - - - (must be like 1982-10-20)
      - -
      -

      List of Contacts

      -
      -
      - - - - - \ No newline at end of file diff --git a/model/list/cookie/cookie.js b/model/list/cookie/cookie.js deleted file mode 100644 index adb352521f1..00000000000 --- a/model/list/cookie/cookie.js +++ /dev/null @@ -1,92 +0,0 @@ -steal('jquery/dom/cookie','jquery/model/list').then(function($){ - -/** - * @constructor jQuery.Model.List.Cookie - * @plugin jquery/model/list/cookie - * @test jquery/model/list/cookie/qunit.html - * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/list/cookie/cookie.js - * @parent jQuery.Model.List - * - * Provides a store-able list of model instances. The following - * retrieves and saves a list of contacts: - * - * @codestart - * var contacts = new Contact.List([]).retrieve("contacts"); - * - * // add each contact to the page - * contacts.each(function(){ - addContact(this); - * }); - * - * // when a new cookie is crated - * $("#contact").submit(function(ev){ - * ev.preventDefault(); - * var data = $(this).formParams(); - * - * // gives it a random id - * data.id = +new Date(); - * var contact = new Contact(data); - * - * //add it to the list of contacts - * contacts.push(contact); - * - * //store the current list - * contacts.store("contacts"); - * - * //show the contact - * addContact(contact); - * }) - * @codeend - * - * You can see this in action in the following demo. Create a contact, then - * refresh the page. - * - * @demo jquery/model/list/cookie/cookie.html - */ -$.Model.List("jQuery.Model.List.Cookie", -/** - * @Prototype - */ -{ - days : null, - /** - * Deserializes a list of instances in the cookie with the provided name - * @param {String} name the name of the cookie to use. - * @return {jQuery.Model} returns this model instance. - */ - retrieve : function(name){ - // each also needs what they are referencd by ? - var props = $.cookie( name ) || {type : null, ids : []}, - instances = [], - Class = props.type ? $.String.getObject(props.type) : null; - for(var i =0; i < props.ids.length;i++){ - var identity = props.ids[i], - instanceData = $.cookie( identity ); - instances.push( new Class(instanceData) ) - } - this.push.apply(this,instances); - return this; - }, - /** - * Serializes and saves this list of model instances to the cookie in name. - * @param {String} name the name of the cookie - * @return {jQuery.Model} returns this model instance. - */ - store : function(name){ - // go through and listen to instance updating - var ids = [], days = this.days; - this.each(function(i, inst){ - $.cookie(inst.identity(), $.toJSON(inst.attrs()), { expires: days }); - ids.push(inst.identity()); - }); - - $.cookie(name, $.toJSON({ - type: this[0] && this[0].constructor.fullName, - ids: ids - }), { expires: this.days }); - return this; - } -}) - -}) - diff --git a/model/list/cookie/qunit.html b/model/list/cookie/qunit.html deleted file mode 100644 index 3cf020d043b..00000000000 --- a/model/list/cookie/qunit.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - -

      Model List Cookie Test Suite

      -

      -
      -

      -
      -
        -
        - - \ No newline at end of file diff --git a/model/list/cookie/qunit/qunit.js b/model/list/cookie/qunit/qunit.js deleted file mode 100644 index 890a2a04d91..00000000000 --- a/model/list/cookie/qunit/qunit.js +++ /dev/null @@ -1,27 +0,0 @@ -steal('funcunit/qunit','jquery/model/list/cookie').then(function($){ - -module("jquery/model/list/cookie",{ - setup: function(){ - // clear any existing cookie ... - $.cookie("list", "", {expires: -1}) - $.Model.extend("Search", {}, {}); - - $.Model.List.Cookie.extend("Search.Store") - } -}) - -test("storing and retrieving",function(){ - - var store = new Search.Store([]) //should be able to look up by namespace .... - - ok(!store.length, "empty list"); - - store.push( new Search({id: 1}), new Search({id: 2}) ) - store.store("list"); - - var store2 = new Search.Store([]).retrieve("list"); - equal(store2.length, 2, "there are 2 items") - -}) - -}) diff --git a/model/list/local/local.js b/model/list/local/local.js deleted file mode 100644 index 1d79b85c462..00000000000 --- a/model/list/local/local.js +++ /dev/null @@ -1,42 +0,0 @@ -steal('jquery/dom/cookie','jquery/model/list').then(function($){ -/** - * @constructor jQuery.Model.List.Local - * @plugin jquery/model/list/local - * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/model/list/local/local.js - * @parent jQuery.Model.List - * Works exactly the same as [jQuery.Model.List.Cookie] except uses - * a local store instead of cookies. - */ -$.Model.List("jQuery.Model.List.Local", -{ - retrieve : function(name){ - // each also needs what they are referencd by ? - var props = window.localStorage[ name ] || "[]", - instances = [], - Class = props.type ? $.String.getObject(props.type) : null; - for(var i =0; i < props.ids.length;i++){ - var identity = props.ids[i], - instanceData = window.localStorage[ identity ]; - instances.push( new Class(instanceData) ) - } - this.push.apply(this,instances); - return this; - }, - store : function(name){ - // go through and listen to instance updating - var ids = [], days = this.days; - this.each(function(i, inst){ - window.localStorage[inst.identity()] = inst.attrs(); - ids.push(inst.identity()); - }); - window.localStorage[name] = { - type: this[0] && this[0].constructor.fullName, - ids: ids - }; - return this; - } - -}); - -}) - diff --git a/model/service/json_rest/json_rest.js b/model/service/json_rest/json_rest.js deleted file mode 100644 index 433c3412214..00000000000 --- a/model/service/json_rest/json_rest.js +++ /dev/null @@ -1,109 +0,0 @@ -steal('jquery/model/service').then(function(){ - -$.Model.service.jsonRest = $.Model.service({ - url : "", - type : ".json", - name : "", - getSingularUrl : function(Class, id){ - return this.singularUrl ? - this.singularUrl+"/"+id+this.type : - this.url+this.getName(Class)+"s/"+id+this.type - }, - getPluralUrl : function(Class, id){ - return this.pluralUrl || this.url+this.getName(Class)+"s"+this.type; - }, - getName : function(Class){ - return this.name || Class.name - }, - findAll : function(params){ - var plural = this._service.getPluralUrl(this); - $.ajax({ - url: plural, - type: 'get', - dataType: 'json', - data: params, - success: this.proxy(['wrapMany',success]), - error: error, - fixture: true - }) - }, - getParams : function(attrs){ - var name = this.getName(this), - params = {}; - for(var n in attrs){ - params[name+"["+n+"]"] = attrs[n]; - } - return params; - }, - update : function( id, attrs, success, error ) { - var params = this._service.getParams(attrs), - singular = this._service.getSingularUrl(this, id), - plural = this._service.getPluralUrl(this), - self = this; - - - - $.ajax({ - url: singular, - type: 'put', - dataType: 'text', - data: params, - complete: function(xhr, status ){ - if (/\w+/.test(xhr.responseText)) { - return error( eval('('+xhr.responseText+')') ); - } - success({}) - }, - fixture: "-restUpdate" - - }) - }, - destroy : function(id, success, error){ - var singular = this._service.getSingularUrl(this,id); - $.ajax({ - url: singular, - type: 'delete', - dataType: 'text', - success: success, - error: error, - fixture: "-restDestroy" - }) - }, - create: function( attrs, success, error ) { - var params = this._service.getParams(attrs), - plural = this._service.getPluralUrl(this), - self = this, - name = this._service.getName(this); - - $.ajax({ - url: plural, - type: 'post', - dataType: 'text', - complete: function(xhr, status){ - if (status != "success") { - error(xhr, status) - } - if (/\w+/.test(xhr.responseText)) { - var res = eval('('+xhr.responseText+')'); - if(res[name]){ - success(res[name]); - return; - } - return error( res ); - } - var loc = xhr.responseText; - try{loc = xhr.getResponseHeader("location");}catch(e){}; - if (loc) { - //todo check this with prototype - var mtcs = loc.match(/\/[^\/]*?(\w+)?$/); - if(mtcs) return success({id: parseInt(mtcs[1])}); - } - success({}); - }, - data: params, - fixture: "-restCreate" - }) - } -}); - -}); \ No newline at end of file diff --git a/model/service/service.js b/model/service/service.js deleted file mode 100644 index 2f4ab662c79..00000000000 --- a/model/service/service.js +++ /dev/null @@ -1,30 +0,0 @@ -steal('jquery/model').then(function(){ - var convert = function(method, func){ - - return typeof method == 'function' ? function(){ - var old = this._service, - ret; - this._service = func; - ret = method.apply(this, arguments); - this._service = old; - return ret; - } : method - } - /** - * Creates a service - * @param {Object} defaults - * @param {Object} methods - */ - $.Model.service = function(properties){ - - var func = function(newProps){ - return $.Model.service( $.extend({}, properties, newProps) ); - }; - - for(var name in properties){ - func[name] = convert(properties[name], func) - } - - return func; - } -}); diff --git a/model/service/twitter/twitter.html b/model/service/twitter/twitter.html deleted file mode 100644 index 37ee00942d5..00000000000 --- a/model/service/twitter/twitter.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - associations - - - -

        JavaScriptMVC Tweets

        - - - - - \ No newline at end of file diff --git a/model/service/twitter/twitter.js b/model/service/twitter/twitter.js deleted file mode 100644 index 8aaa6740b78..00000000000 --- a/model/service/twitter/twitter.js +++ /dev/null @@ -1,43 +0,0 @@ -steal('jquery/model/service').then(function(){ - - $.Model.service.twitter = $.Model.service({ - url : "http://api.twitter.com/1/", - select : "*", - from : "statuses/user_timeline.json", - where : {screen_name : "javascriptmvc"}, - /** - * - * @param {Object} params - */ - findAll : function(params, success, error){ - - - var url = (params.url || this._service.url)+(params.from || this._service.from), - self = this; - - var twitterJson = { - url: url, - dataType: "jsonp", - data: params.where || this._service.where, - error : error - } - - if(this.wrapMany){ - twitterJson.success = function (data) { - if(data.results){ - data = data.results - } - success(self.wrapMany(data)) - - } - }else{ - twitterJson.success = success; - } - - $.ajax(twitterJson); - } - }); - -}) - - diff --git a/model/service/yql/yql.html b/model/service/yql/yql.html deleted file mode 100644 index b398bde2974..00000000000 --- a/model/service/yql/yql.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - associations - - - - - - - \ No newline at end of file diff --git a/model/service/yql/yql.js b/model/service/yql/yql.js deleted file mode 100644 index d10a2ea7b45..00000000000 --- a/model/service/yql/yql.js +++ /dev/null @@ -1,66 +0,0 @@ -steal('jquery/model/service').then(function(){ - - $.Model.service.yql = $.Model.service({ - select : "*", - from : "flickr.photos.search", - convert : function (query, params) { - $.each( params, function (key) { - var name = new RegExp( "#\{" + key + "\}","g" ); - var value = $.trim(this); - //if (!value.match(/^[0-9]+$/)) { - // value = '"' + value + '"'; - //} - query = query.replace(name, value); - } - ); - return query; - }, - /** - * - * @param {Object} params - */ - findAll : function(params, success, error){ - params = $.extend({}, this._service, params); - var query = ["SELECT",params.select,"FROM",params.from]; - - - if(params.where){ - query.push("WHERE",typeof params.where == "string" || this._service.convert(params.where[0],params.where[1])) - } - var self = this; - - - var yqlJson = { - url: "http://query.yahooapis.com/v1/public/yql", - dataType: "jsonp", - data: { - q: query.join(" "), - format: "json", - env: 'store://datatables.org/alltableswithkeys', - callback: "?" - } - } - if (error) { - yqlJson.error = error; - } - if(this.wrapMany){ - yqlJson.success = function (data) { - var results = data.query.results - if(results){ - for(var name in results){ - success(self.wrapMany(data.query.results[name])); - break; - } - }else{ - success([]); - } - } - }else{ - yqlJson.success = success; - } - - $.ajax(yqlJson); - } - }); - -}) \ No newline at end of file diff --git a/route/pushstate/pushstate.html b/route/pushstate/pushstate.html new file mode 100644 index 00000000000..59126f2fa3a --- /dev/null +++ b/route/pushstate/pushstate.html @@ -0,0 +1,82 @@ + + + + + + + + + + \ No newline at end of file diff --git a/route/pushstate/pushstate.js b/route/pushstate/pushstate.js index 1ace2bafe2c..52c72eddea4 100644 --- a/route/pushstate/pushstate.js +++ b/route/pushstate/pushstate.js @@ -2,86 +2,88 @@ steal('can/util', 'can/route', function(can) { "use strict"; if(window.history && history.pushState) { - - var getPath = function() { - return location.pathname + location.search; - }; - - // popstate only fires on back/forward. - // To detect when someone calls push/replaceState, we need to wrap each method. - can.each(['pushState','replaceState'],function(method) { - var orig = history[method]; - history[method] = function(state) { - var result = orig.apply(history, arguments); - can.route.history.attr('path',getPath()); - can.route.history.attr('type',method); - return result; - }; - }); - // Bind to popstate for back/forward - can.bind.call(window, 'popstate', function() { - can.route.history.attr('path',getPath()); - can.route.history.attr('type','popState'); - }); - - - var param = can.route.param, - paramsMatcher = /^\?(?:[^=]+=[^&]*&)*[^=]+=[^&]*/; - can.extend(can.route, { - history: new can.Map({path:getPath()}), - _paramsMatcher: paramsMatcher, - _querySeparator: '?', - _setup: function() { + can.route.bindings.pushstate = { + /** + * @property can.route.pushstate.root + * @parent can.route.pushstate + * + */ + root: "/", + paramsMatcher: /^\?(?:[^=]+=[^&]*&)*[^=]+=[^&]*/, + querySeparator: '?', + bind: function() { // intercept routable links - can.$('body').on('click', 'a', function(e) { - if(!e.isDefaultPrevented()) { - // Fix for ie showing blank host, but blank host means current host. - if(!this.host) { - this.host = window.location.host; - } - // HTML5 pushstate requires host to be the same. Don't prevent default for other hosts. - if(can.route.updateWith(this.pathname+this.search) && window.location.host == this.host) { - e.preventDefault(); - } - } - }); - can.route.history.bind('path',can.route.setState); - }, - updateWith: function(pathname) { - var curParams = can.route.deparam(pathname); - - if(curParams.route) { - can.route.attr(curParams, true); - return true; - } - return false; + can.delegate.call(can.$(document.documentElement),'click', 'a', anchorClickFix); + + // popstate only fires on back/forward. + // To detect when someone calls push/replaceState, we need to wrap each method. + can.each(['pushState','replaceState'],function(method) { + originalMethods[method] = window.history[method]; + window.history[method] = function(state) { + var result = originalMethods[method].apply(window.history, arguments); + can.route.setState(); + return result; + }; + }); + + // Bind to popstate for back/forward + can.bind.call(window, 'popstate', can.route.setState); }, - _getHash: getPath, - _setHash: function(serialized) { - var path = can.route.param(serialized, true); - if(path !== can.route._getHash()) { - can.route.updateLocation(path); - } - return path; + unbind: function(){ + can.undelegate.call(can.$(document.documentElement),'click', 'a', anchorClickFix); + + can.each(['pushState','replaceState'],function(method) { + window.history[method] = originalMethods[method]; + }); + can.unbind.call(window, 'popstate', can.route.setState); }, - current: function( options ) { - return this._getHash() === can.route.param(options); + matchingPartOfURL: function(){ + var root = cleanRoot(), + loc = (location.pathname + location.search), + index = loc.indexOf(root); + + return loc.substr(index+root.length); }, - /** - * This is a blunt hook for updating the window.location. - * You may prefer to use replaceState instead of pushState in some circumstances, - * in which case you can overwrite this method and handle the change yourself. - */ - updateLocation: function(path) { - history.pushState(null, null, path); - }, - url: function( options, merge ) { - if (merge) { - options = can.extend({}, can.route.deparam( this._getHash()), options); - } - return can.route.param(options); + setURL: function(path) { + window.history.pushState(null, null, can.route._call("root")+path); + return path; } - }); + } + + + var anchorClickFix = function(e) { + if(!e.isDefaultPrevented()) { + // Fix for ie showing blank host, but blank host means current host. + if(!this.host) { + this.host = window.location.host; + } + // if link is within the same domain + if(window.location.host == this.host){ + // check if a route matches + var curParams = can.route.deparam(this.pathname+this.search); + // if a route matches + if(curParams.route) { + // update the data + can.route.attr(curParams, true); + e.preventDefault(); + } + } + } + }, + cleanRoot = function(){ + var domain = location.protocol+"//"+location.host, + root = can.route._call("root"), + index = root.indexOf( domain ); + if( index == 0 ) { + return can.route.root.substr(domain.length) + } + return root + }, + // a collection of methods on history that we are overwriting + originalMethods = {}; + + can.route.defaultBinding = "pushstate"; + } return can; diff --git a/route/pushstate/pushstate.md b/route/pushstate/pushstate.md new file mode 100644 index 00000000000..9092a87b272 --- /dev/null +++ b/route/pushstate/pushstate.md @@ -0,0 +1,26 @@ +@page can.route.pushstate +@download can/route/pushstate +@test can/route/pushstate/test.html +@parent can.route.plugins + +@description Changes [can.route] to use +[push state and pop state](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history) +to change the window's [pathname](https://developer.mozilla.org/en-US/docs/Web/API/URLUtils.pathname) instead +of the [hash](https://developer.mozilla.org/en-US/docs/Web/API/URLUtils.hash). + +@body + +## Use + +The pushstate plugin uses the same API as [can.route] with only one additional +property - [can.route.root]. `can.route.root` specifies the part of that pathname that +should not change. For example, if we only want to have pathnames within `app.com/contacts/`, +we can specify a root like: + + can.route.root = "/contacts/" + can.route(":page\\.html"); + can.route.url({page: "list"}) //-> "/contacts/list.html" + +Now, all routes will start with "/contacts/". The default [can.root.route] +is "/". + diff --git a/route/pushstate/pushstate_test.js b/route/pushstate/pushstate_test.js index 02bf539ee63..603981ffcca 100644 --- a/route/pushstate/pushstate_test.js +++ b/route/pushstate/pushstate_test.js @@ -1,7 +1,13 @@ +(function(){ + var originalPath = location.pathname; module("can/route/pushstate",{ + setup: function(){ + can.route._teardown(); + can.route.defaultBinding = "pushstate"; + }, teardown: function() { - history.replaceState(null,null,originalPath); + } }); @@ -286,85 +292,111 @@ test("strange characters", function(){ equal(res, "bar/"+encodeURIComponent("\/")) }); -test("updating the url", function(){ - stop(); - window.routeTestReady = function(iCanRoute, loc){ - iCanRoute("/:type/:id"); - iCanRoute.attr({type: "bar", id: "\/"}); - - setTimeout(function(){ - var after = loc.pathname; - equal(after,"/bar/"+encodeURIComponent("\/")); - start(); - - can.remove(can.$(iframe)) - - },30); - } - var iframe = document.createElement('iframe'); - iframe.src = can.test.path("route/pushstate/testing.html"); - can.$("#qunit-test-area")[0].appendChild(iframe); -}); - -test("sticky enough routes", function(){ - stop(); - window.routeTestReady = function(iCanRoute, loc, history){ - iCanRoute("/active"); - iCanRoute(""); - history.pushState(null,null,"/active"); - - setTimeout(function(){ - var after = loc.pathname; - equal(after,"/active"); - start(); - - can.remove(can.$(iframe)) - - },30); - } - var iframe = document.createElement('iframe'); - iframe.src = can.test.path("route/pushstate/testing.html?2"); - can.$("#qunit-test-area")[0].appendChild(iframe); -}); - -test("unsticky routes", function(){ - stop(); - window.routeTestReady = function(iCanRoute, loc){ - iCanRoute("/:type") - iCanRoute("/:type/:id"); - iCanRoute.attr({type: "bar"}); - - setTimeout(function(){ - var after = loc.pathname; - equal(after,"/bar"); - iCanRoute.attr({type: "bar", id: "\/"}); +if( window.history && history.pushState) { + test("updating the url", function(){ + stop(); + window.routeTestReady = function(iCanRoute, loc){ + iCanRoute.ready() + iCanRoute("/:type/:id"); + iCanRoute.attr({type: "bar", id: "5"}); - // check for 1 second - var time = new Date() + setTimeout(function(){ var after = loc.pathname; - if(after == "/bar/"+encodeURIComponent("\/")){ - equal(after,"/bar/"+encodeURIComponent("\/"),"should go to type/id"); - can.remove(can.$(iframe)) - start(); - } else if( new Date() - time > 2000){ - ok(false, "hash is "+after); + equal(after,"/bar/5", "path is "+after); + start(); + + can.remove(can.$(iframe)) + + },100); + } + var iframe = document.createElement('iframe'); + iframe.src = can.test.path("route/pushstate/testing.html"); + can.$("#qunit-test-area")[0].appendChild(iframe); + }); + + test("sticky enough routes", function(){ + stop(); + window.routeTestReady = function(iCanRoute, loc, history){ + iCanRoute("/active"); + iCanRoute(""); + history.pushState(null,null,"/active"); + + setTimeout(function(){ + var after = loc.pathname; + equal(after,"/active"); + start(); + + can.remove(can.$(iframe)) + },30); + } + var iframe = document.createElement('iframe'); + iframe.src = can.test.path("route/pushstate/testing.html?2"); + can.$("#qunit-test-area")[0].appendChild(iframe); + }); + + test("unsticky routes", function(){ + + stop(); + window.routeTestReady = function(iCanRoute, loc, iframeHistory){ + // check if we can even test this + iframeHistory.pushState(null,null,"/bar/"+encodeURIComponent("\/")); + setTimeout(function(){ + + if( "/bar/"+encodeURIComponent("\/") === loc.pathname ){ + runTest(); + + } else if(loc.pathname.indexOf("/bar/") >=0 ){ + // encoding doesn't actually work + console.log("can/route/pushstate/pushstate_test.js: win.location is automatically unescaping, can not test") + ok(true,"can't test!"); can.remove(can.$(iframe)) + start() } else { setTimeout(arguments.callee, 30) } + },30) + var runTest = function(){ + iCanRoute.ready(); + iCanRoute("/:type"); + iCanRoute("/:type/:id"); + iCanRoute.attr({type: "bar"}); - },1) + setTimeout(function(){ + var after = loc.pathname; + equal(after,"/bar","only type is set"); + iCanRoute.attr({type: "bar", id: "\/"}); + + // check for 1 second + var time = new Date() + setTimeout(function(){ + var after = loc.pathname; + + if(after == "/bar/"+encodeURIComponent("\/")){ + equal(after,"/bar/"+encodeURIComponent("\/"),"should go to type/id"); + can.remove(can.$(iframe)) + start(); + } else if( new Date() - time > 2000){ + ok(false, "hash is "+after); + can.remove(can.$(iframe)) + } else { + setTimeout(arguments.callee, 30) + } + + },30) + + },30) + } - },1) - - - } - var iframe = document.createElement('iframe'); - iframe.src = can.test.path("route/pushstate/testing.html?1"); - can.$("#qunit-test-area")[0].appendChild(iframe); -}); + + + } + var iframe = document.createElement('iframe'); + iframe.src = can.test.path("route/pushstate/testing.html?1"); + can.$("#qunit-test-area")[0].appendChild(iframe); + }); +} test("empty default is matched even if last", function(){ @@ -432,3 +464,5 @@ test("dashes in routes", function(){ route: ":foo-:bar" }); }) + +})(); \ No newline at end of file diff --git a/route/pushstate/testing.html b/route/pushstate/testing.html index 26d82622c0d..c46938d05c4 100644 --- a/route/pushstate/testing.html +++ b/route/pushstate/testing.html @@ -1,7 +1,7 @@ - can.route pushstate test page + can.route test page

        This is a dummy page to use
        for testing route goodness

        @@ -22,12 +22,14 @@ }; - steal.config(configuration); - steal('can/route/pushstate', function (route) { + + steal(function(){ + steal.config(configuration); + }).then('can/route/pushstate', function () { // make sure it's after ready setTimeout(function () { - window.parent.routeTestReady(can.route, window.location, window.history) + window.parent.routeTestReady && window.parent.routeTestReady(can.route, window.location, window.history) }, 30) }) diff --git a/route/route.js b/route/route.js index d5df3d3643a..e1b819fbf1e 100644 --- a/route/route.js +++ b/route/route.js @@ -54,29 +54,77 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { return (str+'').replace(/([.?*+\^$\[\]\\(){}|\-])/g, "\\$1"); }, each = can.each, - extend = can.extend; + extend = can.extend, + removeBackslash = function(str){ + return str.replace(/\\/g,"") + }, + // A ~~throttled~~ debounced function called multiple times will only fire once the + // timer runs down. Each call resets the timer. + timer, + // Intermediate storage for `can.route.data`. + curParams, + // The last hash caused by a data change + lastHash, + // Are data changes pending that haven't yet updated the hash + changingData, + // If the `can.route.data` changes, update the hash. + // Using `.serialize()` retrieves the raw data contained in the `observable`. + // This function is ~~throttled~~ debounced so it only updates once even if multiple values changed. + // This might be able to use batchNum and avoid this. + onRouteDataChange = function(ev, attr, how, newval) { + // indicate that data is changing + changingData = 1; + clearTimeout( timer ); + timer = setTimeout(function() { + // indicate that the hash is set to look like the data + changingData = 0; + var serialized = can.route.data.serialize(), + path = can.route.param(serialized, true); + can.route._call("setURL",path); + + lastHash = path + }, 10); + }; can.route = function( url, defaults ) { + // if route ends with a / and url starts with a /, remove the leading / of the url + var root = can.route._call("root"); + + if(root.lastIndexOf("/") == root.length - 1 && + url.indexOf("/") === 0) { + url = url.substr(1); + } + + defaults = defaults || {}; // Extract the variable names and replace with `RegExp` that will match // an atual URL with values. var names = [], - test = url.replace(matcher, function( whole, name, i ) { - names.push(name); - var next = "\\"+( url.substr(i+whole.length,1) || can.route._querySeparator ); - // a name without a default value HAS to have a value - // a name that has a default value can be empty - // The `\\` is for string-escaping giving single `\` for `RegExp` escaping. - return "([^" +next+"]"+(defaults[name] ? "*" : "+")+")"; - }); - + res, + test = "", + lastIndex = matcher.lastIndex = 0, + next, + querySeparator = can.route._call("querySeparator"); + + // res will be something like [":foo","foo"] + while(res = matcher.exec(url)){ + names.push(res[1]); + test += removeBackslash( url.substring(lastIndex, matcher.lastIndex - res[0].length) ); + next = "\\"+( removeBackslash(url.substr(matcher.lastIndex,1)) || querySeparator ); + // a name without a default value HAS to have a value + // a name that has a default value can be empty + // The `\\` is for string-escaping giving single `\` for `RegExp` escaping. + test += "([^" +next+"]"+(defaults[res[1]] ? "*" : "+")+")"; + lastIndex = matcher.lastIndex; + } + test += url.substr(lastIndex).replace("\\","") // Add route in a form that can be easily figured out. can.route.routes[url] = { // A regular expression that will match the route when variable values // are present; i.e. for `:page/:type` the `RegExp` is `/([\w\.]*)/([\w\.]*)/` which // will match for any value of `:page` and `:type` (word chars or period). - test: new RegExp("^" + test+"($|"+wrapQuote(can.route._querySeparator)+")"), + test: new RegExp("^" + test+"($|"+wrapQuote(querySeparator)+")"), // The original URL, same as the index for this entry in routes. route: url, // An `array` of all the variable names in this route. @@ -93,10 +141,7 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { * @static */ extend(can.route, { - - _querySeparator: '&', - _paramsMatcher: paramsMatcher, - + /** * @function can.route.param param * @parent can.route.static @@ -163,7 +208,7 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { res = route.route.replace(matcher, function( whole, name ) { delete cpy[name]; return data[name] === route.defaults[name] ? "" : encodeURIComponent( data[name] ); - }), + }).replace("\\",""), after; // Remove matching default values each(route.defaults, function(val,name){ @@ -180,10 +225,10 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { if(_setRoute){ can.route.attr('route',route.route); } - return res + (after ? can.route._querySeparator + after : ""); + return res + (after ? can.route._call("querySeparator") + after : ""); } // If no route was found, there is no hash URL, only paramters. - return can.isEmptyObject(data) ? "" : can.route._querySeparator + can.param(data); + return can.isEmptyObject(data) ? "" : can.route._call("querySeparator") + can.param(data); }, /** * @function can.route.deparam deparam @@ -224,7 +269,10 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { // By comparing the URL length the most specialized route that matches is used. var route = { length: -1 - }; + }, + querySeparator = can.route._call("querySeparator"), + paramsMatcher = can.route._call("paramsMatcher"); + each(can.route.routes, function(temp, name){ if ( temp.test.test(url) && temp.length > route.length ) { route = temp; @@ -239,16 +287,16 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { // Start will contain the full matched string; parts contain the variable values. start = parts.shift(), // The remainder will be the `&key=value` list at the end of the URL. - remainder = url.substr(start.length - (parts[parts.length-1] === can.route._querySeparator ? 1 : 0) ), + remainder = url.substr(start.length - (parts[parts.length-1] === querySeparator ? 1 : 0) ), // If there is a remainder and it contains a `&key=value` list deparam it. - obj = (remainder && can.route._paramsMatcher.test(remainder)) ? can.deparam( remainder.slice(1) ) : {}; + obj = (remainder && paramsMatcher.test(remainder)) ? can.deparam( remainder.slice(1) ) : {}; // Add the default values for this route. obj = extend(true, {}, route.defaults, obj); // Overwrite each of the default values in `obj` with those in // parts if that part is not empty. each(parts,function(part, i){ - if ( part && part !== can.route._querySeparator) { + if ( part && part !== querySeparator ) { obj[route.names[i]] = decodeURIComponent( part ); } }); @@ -256,10 +304,10 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { return obj; } // If no route was matched, it is parsed as a `&key=value` list. - if ( url.charAt(0) !== can.route._querySeparator ) { - url = can.route._querySeparator + url; + if ( url.charAt(0) !== querySeparator ) { + url = querySeparator + url; } - return can.route._paramsMatcher.test(url) ? can.deparam( url.slice(1) ) : {}; + return paramsMatcher.test(url) ? can.deparam( url.slice(1) ) : {}; }, /** * @hide @@ -308,12 +356,9 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { * can.route.ready(true); // fire the first route change */ ready: function(val) { - if( val === false ) { - onready = val; - } - if( val === true || onready === true ) { + if( val !== true ) { can.route._setup(); - setState(); + can.route.setState(); } return can.route; }, @@ -346,10 +391,11 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { * // -> "#!video/5&isNew=false" */ url: function( options, merge ) { + if (merge) { - options = extend({}, curParams, options) - } - return "#!" + can.route.param(options); + options = can.extend({}, can.route.deparam( can.route._call("matchingPartOfURL")), options); + } + return can.route._call("root") + can.route.param(options); }, /** * @function can.route.link link @@ -426,19 +472,73 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { * can.route.current({ id: 5, type: 'videos' }) // -> true */ current: function( options ) { - return location.hash == "#!" + can.route.param(options) + return this._call("matchingPartOfURL") === can.route.param(options); }, + bindings: { + hashchange : { + paramsMatcher: paramsMatcher, + querySeparator: "&", + bind: function(){ + can.bind.call(window,'hashchange', setState); + }, + unbind: function(){ + can.unbind.call(window,'hashchange', setState); + }, + // Gets the part of the url we are determinging the route from. + // For hashbased routing, it's everything after the #, for + // pushState it's configurable + matchingPartOfURL: function() { + return location.href.split(/#!?/)[1] || ""; + }, + // gets called with the serialized can.route data after a route has changed + // returns what the url has been updated to (for matching purposes) + setURL: function(path) { + location.hash = "#!" + path; + return path; + }, + root: "#!", + } + }, + defaultBinding: "hashchange", + currentBinding: null, + // ready calls setup + // setup binds and listens to data changes + // bind listens to whatever you should be listening to + // data changes tries to set the path + + // we need to be able to + // easily kick off calling setState + // teardown whatever is there + // turn on a particular binding + + // called when the route is ready _setup: function() { - // If the hash changes, update the `can.route.data`. - can.bind.call(window,'hashchange', setState); + if(!can.route.currentBinding){ + can.route._call("bind"); + can.route.bind("change", onRouteDataChange); + can.route.currentBinding = can.route.defaultBinding; + } }, - _getHash: function() { - return location.href.split(/#!?/)[1] || ""; + _teardown: function(){ + if( can.route.currentBinding ) { + can.route._call("unbind"); + can.route.unbind("change", onRouteDataChange); + can.route.currentBinding = null; + } + clearTimeout(timer); + changingData = 0; }, - _setHash: function(serialized) { - var path = (can.route.param(serialized, true)); - location.hash = "#!" + path; - return path; + // a helper to get stuff from the current or default bindings + _call: function(){ + var args = can.makeArray(arguments), + prop = args.shift(), + binding = can.route.bindings[can.route.currentBinding || can.route.defaultBinding] + method = binding[prop]; + if(typeof method === "function"){ + return method.apply(binding,args) + } else { + return method; + } } }); @@ -457,19 +557,14 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { } }) - var // A ~~throttled~~ debounced function called multiple times will only fire once the - // timer runs down. Each call resets the timer. - timer, - // Intermediate storage for `can.route.data`. - curParams, - // Deparameterizes the portion of the hash of interest and assign the + var // Deparameterizes the portion of the hash of interest and assign the // values to the `can.route.data` removing existing values no longer in the hash. // setState is called typically by hashchange which fires asynchronously // So it's possible that someone started changing the data before the // hashchange event fired. For this reason, it will not set the route data // if the data is changing or the hash already matches the hash that was set. setState = can.route.setState = function() { - var hash = can.route._getHash(); + var hash = can.route._call("matchingPartOfURL"); curParams = can.route.deparam( hash ); // if the hash data is currently changing, or @@ -477,36 +572,11 @@ steal('can/util','can/map', 'can/util/string/deparam', function(can) { if(!changingData || hash !== lastHash){ can.route.attr(curParams, true); } - }, - // The last hash caused by a data change - lastHash, - // Are data changes pending that haven't yet updated the hash - changingData; - - // If the `can.route.data` changes, update the hash. - // Using `.serialize()` retrieves the raw data contained in the `observable`. - // This function is ~~throttled~~ debounced so it only updates once even if multiple values changed. - // This might be able to use batchNum and avoid this. - can.route.bind("change", function(ev, attr) { - // indicate that data is changing - changingData = 1; - clearTimeout( timer ); - timer = setTimeout(function() { - // indicate that the hash is set to look like the data - changingData = 0; - var serialized = can.route.data.serialize(); - - lastHash = can.route._setHash(serialized); - }, 1); - }); - // `onready` event... - can.bind.call(document,"ready",can.route.ready); + }; + + - // Libraries other than jQuery don't execute the document `ready` listener - // if we are already DOM ready - if( (document.readyState === 'complete' || document.readyState === "interactive") && onready) { - can.route.ready(); - } + return can.route; }); diff --git a/route/route.md b/route/route.md index 0b4584e80e6..4e01019bb73 100644 --- a/route/route.md +++ b/route/route.md @@ -4,6 +4,7 @@ @download can/route @test can/route/test.html @parent canjs +@group can.route.plugins plugins @description Manage browser history and client state by synchronizing the window.location.hash with diff --git a/route/route_test.js b/route/route_test.js index a052b105714..7dd8a21235b 100644 --- a/route/route_test.js +++ b/route/route_test.js @@ -1,5 +1,10 @@ (function() { -module("can/route") +module("can/route",{ + setup: function(){ + can.route._teardown(); + can.route.defaultBinding = "hashchange"; + } +}) test("deparam", function(){ can.route.routes = {}; @@ -38,7 +43,7 @@ test("deparam", function(){ index: "foo", where: "there", route: ":page/:index" - }); + }, "default value and queryparams"); }) test("deparam of invalid url", function(){ @@ -365,7 +370,7 @@ if(typeof steal !== 'undefined') { testarea.innerHTML = ''; start(); }); - + iCanRoute.ready() setTimeout(function() { iframe.src = iframe.src + '#!bla=blu'; }, 100); @@ -378,6 +383,7 @@ if(typeof steal !== 'undefined') { test("updating the hash", function(){ stop(); window.routeTestReady = function(iCanRoute, loc){ + iCanRoute.ready() iCanRoute(":type/:id"); iCanRoute.attr({type: "bar", id: "\/"}); @@ -398,6 +404,7 @@ if(typeof steal !== 'undefined') { test("sticky enough routes", function(){ stop(); window.routeTestReady = function(iCanRoute, loc){ + iCanRoute.ready() iCanRoute("active"); iCanRoute(""); loc.hash = "#!active" @@ -419,6 +426,7 @@ if(typeof steal !== 'undefined') { test("unsticky routes", function(){ stop(); window.routeTestReady = function(iCanRoute, loc){ + iCanRoute.ready() iCanRoute(":type") iCanRoute(":type/:id"); iCanRoute.attr({type: "bar"}); @@ -455,4 +463,22 @@ if(typeof steal !== 'undefined') { }); } + +test("escaping periods", function(){ + + can.route.routes = {}; + can.route(":page\\.html",{ + page: "index" + }); + + var obj = can.route.deparam("can.Control.html"); + deepEqual(obj, { + page : "can.Control", + route: ":page\\.html" + }); + + equal( can.route.param({page: "can.Control"}), "can.Control.html"); + +}) + })(); \ No newline at end of file diff --git a/test/amd/dojo.html b/test/amd/dojo.html index fec8a5c1259..6f4bca74862 100644 --- a/test/amd/dojo.html +++ b/test/amd/dojo.html @@ -66,6 +66,9 @@

        "test/route/route_test": ["can/route", "test/test/test" ], + "test/route/pushstate/pushstate_test": [ + "can/route/pushstate", "test/test/test" + ], "test/control/route/route_test": [ "can/control/route", "test/test/test" ], @@ -123,6 +126,7 @@

        "test/view/ejs/ejs_test", "test/control/control_test", "test/route/route_test", + "test/route/pushstate/pushstate_test", "test/control/route/route_test", "test/view/mustache/mustache_test", "test/view/bindings/bindings_test", diff --git a/test/amd/jquery.html b/test/amd/jquery.html index 693d539be2f..0a0475dbdb0 100644 --- a/test/amd/jquery.html +++ b/test/amd/jquery.html @@ -66,6 +66,9 @@

        "test/route/route_test": ["can/route", "test/test/test" ], + "test/route/pushstate/pushstate_test": [ + "can/route/pushstate", "test/test/test" + ], "test/control/route/route_test": [ "can/control/route", "test/test/test" ], @@ -129,6 +132,7 @@

        "test/view/ejs/ejs_test", "test/control/control_test", "test/route/route_test", + "test/route/pushstate/pushstate_test", "test/control/route/route_test", "test/view/mustache/mustache_test", "test/view/bindings/bindings_test", diff --git a/test/amd/mootools.html b/test/amd/mootools.html index 1deb6b1ef51..989ecbe1ed4 100644 --- a/test/amd/mootools.html +++ b/test/amd/mootools.html @@ -66,6 +66,9 @@

        "test/route/route_test": ["can/route", "test/test/test" ], + "test/route/pushstate/pushstate_test": [ + "can/route/pushstate", "test/test/test" + ], "test/control/route/route_test": [ "can/control/route", "test/test/test" ], @@ -123,6 +126,7 @@

        "test/view/ejs/ejs_test", "test/control/control_test", "test/route/route_test", + "test/route/pushstate/pushstate_test", "test/control/route/route_test", "test/view/mustache/mustache_test", "test/view/bindings/bindings_test", diff --git a/test/amd/yui.html b/test/amd/yui.html index 4b2107e2399..989eea48b6f 100644 --- a/test/amd/yui.html +++ b/test/amd/yui.html @@ -66,6 +66,9 @@

        "test/route/route_test": ["can/route", "test/test/test" ], + "test/route/pushstate/pushstate_test": [ + "can/route/pushstate", "test/test/test" + ], "test/control/route/route_test": [ "can/control/route", "test/test/test" ], @@ -123,6 +126,7 @@

        "test/view/ejs/ejs_test", "test/control/control_test", "test/route/route_test", + "test/route/pushstate/pushstate_test", "test/control/route/route_test", "test/view/mustache/mustache_test", "test/view/bindings/bindings_test", diff --git a/test/amd/zepto.html b/test/amd/zepto.html index 24ed7ecd1aa..c36f0ca5b9e 100644 --- a/test/amd/zepto.html +++ b/test/amd/zepto.html @@ -66,6 +66,9 @@

        "test/route/route_test": ["can/route", "test/test/test" ], + "test/route/pushstate/pushstate_test": [ + "can/route/pushstate", "test/test/test" + ], "test/control/route/route_test": [ "can/control/route", "test/test/test" ], @@ -123,6 +126,7 @@

        "test/view/ejs/ejs_test", "test/control/control_test", "test/route/route_test", + "test/route/pushstate/pushstate_test", "test/control/route/route_test", "test/view/mustache/mustache_test", "test/view/bindings/bindings_test", diff --git a/test/dojo.html b/test/dojo.html index 5f9eabdc602..ddbb726ce1c 100644 --- a/test/dojo.html +++ b/test/dojo.html @@ -36,14 +36,15 @@

        }).then("can/util/fixture", "can/component", "can/construct", "can/observe", "can/compute", "can/model", "can/view", "can/view/ejs", - "can/control", "can/route", "can/control/route", - "can/view/mustache", "can/view/bindings", - "can/view/scope", "can/model/queue", - "can/construct/super", "can/construct/proxy", - "can/map/delegate", "can/map/setter", - "can/map/attributes", "can/map/validations", - "can/map/backup", "can/util/object", - "can/util/string", "can/util/fixture") + "can/control", "can/route", "can/route/pushstate", + "can/control/route", "can/view/mustache", + "can/view/bindings", "can/view/scope", + "can/model/queue", "can/construct/super", + "can/construct/proxy", "can/map/delegate", + "can/map/setter", "can/map/attributes", + "can/map/validations", "can/map/backup", + "can/util/object", "can/util/string", + "can/util/fixture") .then("can/test").then( "can/component/component_test.js") .then("can/construct/construct_test.js") @@ -54,6 +55,7 @@

        .then("can/view/ejs/ejs_test.js") .then("can/control/control_test.js") .then("can/route/route_test.js") + .then("can/route/pushstate/pushstate_test.js") .then("can/control/route/route_test.js") .then("can/view/mustache/mustache_test.js") .then("can/view/bindings/bindings_test.js") diff --git a/test/jquery.html b/test/jquery.html index 723d7f81dd3..9c2c3122222 100644 --- a/test/jquery.html +++ b/test/jquery.html @@ -36,15 +36,16 @@

        }).then("can/util/fixture", "can/component", "can/construct", "can/observe", "can/compute", "can/model", "can/view", "can/view/ejs", - "can/control", "can/route", "can/control/route", - "can/view/mustache", "can/view/bindings", - "can/view/scope", "can/model/queue", - "can/construct/super", "can/construct/proxy", - "can/map/delegate", "can/map/setter", - "can/map/attributes", "can/map/validations", - "can/map/backup", "can/control/plugin", - "can/view/modifiers", "can/util/object", - "can/util/string", "can/util/fixture") + "can/control", "can/route", "can/route/pushstate", + "can/control/route", "can/view/mustache", + "can/view/bindings", "can/view/scope", + "can/model/queue", "can/construct/super", + "can/construct/proxy", "can/map/delegate", + "can/map/setter", "can/map/attributes", + "can/map/validations", "can/map/backup", + "can/control/plugin", "can/view/modifiers", + "can/util/object", "can/util/string", + "can/util/fixture") .then("can/test").then( "can/component/component_test.js") .then("can/construct/construct_test.js") @@ -55,6 +56,7 @@

        .then("can/view/ejs/ejs_test.js") .then("can/control/control_test.js") .then("can/route/route_test.js") + .then("can/route/pushstate/pushstate_test.js") .then("can/control/route/route_test.js") .then("can/view/mustache/mustache_test.js") .then("can/view/bindings/bindings_test.js") diff --git a/test/mootools.html b/test/mootools.html index cd186fe81f1..95b6fd872be 100644 --- a/test/mootools.html +++ b/test/mootools.html @@ -36,14 +36,15 @@

        }).then("can/util/fixture", "can/component", "can/construct", "can/observe", "can/compute", "can/model", "can/view", "can/view/ejs", - "can/control", "can/route", "can/control/route", - "can/view/mustache", "can/view/bindings", - "can/view/scope", "can/model/queue", - "can/construct/super", "can/construct/proxy", - "can/map/delegate", "can/map/setter", - "can/map/attributes", "can/map/validations", - "can/map/backup", "can/util/object", - "can/util/string", "can/util/fixture") + "can/control", "can/route", "can/route/pushstate", + "can/control/route", "can/view/mustache", + "can/view/bindings", "can/view/scope", + "can/model/queue", "can/construct/super", + "can/construct/proxy", "can/map/delegate", + "can/map/setter", "can/map/attributes", + "can/map/validations", "can/map/backup", + "can/util/object", "can/util/string", + "can/util/fixture") .then("can/test").then( "can/component/component_test.js") .then("can/construct/construct_test.js") @@ -54,6 +55,7 @@

        .then("can/view/ejs/ejs_test.js") .then("can/control/control_test.js") .then("can/route/route_test.js") + .then("can/route/pushstate/pushstate_test.js") .then("can/control/route/route_test.js") .then("can/view/mustache/mustache_test.js") .then("can/view/bindings/bindings_test.js") diff --git a/test/yui.html b/test/yui.html index d81466edfca..da60c0eab68 100644 --- a/test/yui.html +++ b/test/yui.html @@ -36,14 +36,15 @@

        }).then("can/util/fixture", "can/component", "can/construct", "can/observe", "can/compute", "can/model", "can/view", "can/view/ejs", - "can/control", "can/route", "can/control/route", - "can/view/mustache", "can/view/bindings", - "can/view/scope", "can/model/queue", - "can/construct/super", "can/construct/proxy", - "can/map/delegate", "can/map/setter", - "can/map/attributes", "can/map/validations", - "can/map/backup", "can/util/object", - "can/util/string", "can/util/fixture") + "can/control", "can/route", "can/route/pushstate", + "can/control/route", "can/view/mustache", + "can/view/bindings", "can/view/scope", + "can/model/queue", "can/construct/super", + "can/construct/proxy", "can/map/delegate", + "can/map/setter", "can/map/attributes", + "can/map/validations", "can/map/backup", + "can/util/object", "can/util/string", + "can/util/fixture") .then("can/test").then( "can/component/component_test.js") .then("can/construct/construct_test.js") @@ -54,6 +55,7 @@

        .then("can/view/ejs/ejs_test.js") .then("can/control/control_test.js") .then("can/route/route_test.js") + .then("can/route/pushstate/pushstate_test.js") .then("can/control/route/route_test.js") .then("can/view/mustache/mustache_test.js") .then("can/view/bindings/bindings_test.js") diff --git a/test/zepto.html b/test/zepto.html index 1e650f41290..e37bea6ae4b 100644 --- a/test/zepto.html +++ b/test/zepto.html @@ -36,14 +36,15 @@

        }).then("can/util/fixture", "can/component", "can/construct", "can/observe", "can/compute", "can/model", "can/view", "can/view/ejs", - "can/control", "can/route", "can/control/route", - "can/view/mustache", "can/view/bindings", - "can/view/scope", "can/model/queue", - "can/construct/super", "can/construct/proxy", - "can/map/delegate", "can/map/setter", - "can/map/attributes", "can/map/validations", - "can/map/backup", "can/util/object", - "can/util/string", "can/util/fixture") + "can/control", "can/route", "can/route/pushstate", + "can/control/route", "can/view/mustache", + "can/view/bindings", "can/view/scope", + "can/model/queue", "can/construct/super", + "can/construct/proxy", "can/map/delegate", + "can/map/setter", "can/map/attributes", + "can/map/validations", "can/map/backup", + "can/util/object", "can/util/string", + "can/util/fixture") .then("can/test").then( "can/component/component_test.js") .then("can/construct/construct_test.js") @@ -54,6 +55,7 @@

        .then("can/view/ejs/ejs_test.js") .then("can/control/control_test.js") .then("can/route/route_test.js") + .then("can/route/pushstate/pushstate_test.js") .then("can/control/route/route_test.js") .then("can/view/mustache/mustache_test.js") .then("can/view/bindings/bindings_test.js") diff --git a/util/event.js b/util/event.js index a4c802de1e1..02a7039aaf6 100644 --- a/util/event.js +++ b/util/event.js @@ -23,7 +23,7 @@ can.removeEvent = function(event, fn){ return; } var i =0, - events = this.__bindEvents[event.split(".")[0]], + events = this.__bindEvents[event.split(".")[0]] || [], ev; while(i < events.length){ ev = events[i]