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
-
-
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]