// TODO: catch refreshes?.?.?
// TODO:
(function(){
var _historyAPI = window.history;
function arrayFindString(str, strArray) {
var matches = {
values:[],
index:[]
};
for (var j=0; j<strArray.length; j++) {
if (strArray[j].match(str)) {
matches.index.push(j);
matches.values.push(strArray[j]);
}
}
if(matches.index.length) return matches;
return -1;
}
function callRouteFn(routeKey, slugs, stateData) {
var routes = this.routes;
if( routes.before ) routes.before( stateData );
routes[routeKey].enter(slugs, stateData);
if( routes.after ) routes.after( stateData );
}
// TODO: split this up in smaller more specific tasks - it's too large
function parseURLroute (routeString, stateData){
routeString = routeString || '';
// get all the route keys
var keys = Object.keys(this.routes);
var routeKeys = [];
var wildcardKeys = keys.filter(function(item){
if(item.substr(item.length -1) === '*') {
return true;
} else {
routeKeys.push(item);
return false;
}
});
// test exact routes
var exact = routeKeys.indexOf(routeString);
var routeKey;
if(exact >= 0) {
routeKey = routeKeys[exact];
} else {
// not exact
var routeArray = routeString.split('/');
routeArray.shift();
var matches = arrayFindString(routeArray[0], routeKeys);
var fnIndexMatches = matches.index;
var fnKeyMatches = matches.values;
// if theres more than one match refine by string comparison
if(fnIndexMatches.length > 1){
var refineMatch = {
index:[],
values:[]
};
var mostPoints = 0;
var mostSimilar;
for (var i = 0; i < fnKeyMatches.length; i++) {
var newArr = fnKeyMatches[i].split('/');
newArr.shift();
var points = 0;
if( newArr.length === routeArray.length){
for (var val = 0; val < newArr.length; val++) {
if(newArr[val] === routeArray[val]){
points++;
}
if(points > mostPoints) {
mostPoints = points;
mostSimilar = i;
}
}
}
}
if(matches.index[mostSimilar]){
refineMatch.index.push(matches.index[mostSimilar]);
refineMatch.values.push(matches.values[mostSimilar]);
}
matches = refineMatch;
}
//console.log( 'routeString ', routeString, matches);
// only 1 match should exisit in the matches object by now.
if (matches.index.length === 0) {
this.routes.notFound(routeString);
this.emit('ROUTE_NOT_FOUND', { route: routeString });
return;
} else if (matches.index.length > 1) {
console.error('More than 1 route found');
}
// set the current slugs and the routeKey to call
this.slugs = getSlugs(matches, routeArray);
routeKey = matches.values[0];
}
// call previous onLeave handler
var routeFnObj = this.routes[this.current];
if(routeFnObj && routeFnObj.leave) routeFnObj.leave();
if(this.current){
// call previous onLeave handler for wildcards
for (var e = 0; e < wildcardKeys.length; e++) {
if(this.current.indexOf( wildcardKeys[e].replace('*','') ) === 0 ){
var leaveFn = this.routes[ wildcardKeys[e] ].leave;
if(leaveFn) leaveFn();
}
}
}
// test and call wildcards
for (var d = 0; d < wildcardKeys.length; d++) {
if(routeString.indexOf( wildcardKeys[d].replace('*','') ) === 0 ){
callRouteFn.call(this, wildcardKeys[d], this.slugs, stateData);
}
}
// call the singel route found in the first tests
callRouteFn.call(this, routeKey, this.slugs, stateData);
this.current = routeString;
var returnVal = {route: routeString, previousRoute: this.current, slugs: this.slugs };
return returnVal;
}
function getSlugs(matches, routeArray){
var hasSlugs = false;
// get any slug values
var splitMatchArray = matches.values[0].split('/');
splitMatchArray.shift();
//find slugs
var slugs = {};
for (var match = 1; match < splitMatchArray.length; match++) {
// is it a slug?
var isSlug = splitMatchArray[match].charAt(0) + splitMatchArray[match].charAt(splitMatchArray[match].length-1);
if(isSlug === '{}') {
hasSlugs = true;
// it's a slug!
var slugName = splitMatchArray[match].slice(1,-1);
var slugValue = routeArray[match];
slugs[slugName] = slugValue;
}
}
return hasSlugs ? slugs : null;
}
function setState() {
// store the state datat in localStorage, history.state has a 640kB limit.
var stateString = JSON.stringify({data:$.store.data, states: $.store.states});
localStorage.setItem(this.current, stateString);
}
$.fn.router = {
/**
* Add a route to the router instance.
* @memberOf ish.router
* @param {String} route The route path you're adding.
* @param {Object} fn The routing object conatining 'enter' and 'leave' functions keyed by their respective name.
* @return {ish.router} Chainable, returns its own instance.
*/
add: function(route, fn){
this.routes[route] = fn;
return this;
},
/**
* Remove a route from the router instance.
* @param {String} route The route path to remove.
* @return {ish.router} Chainable, returns its own instance.
*/
remove: function(route){
delete this.routes[route];
return this;
},
/**
* Flush/remove all routes from the router instance.
* @return {ish.router} Chainable, returns its own instance.
*/
flush: function(){
this.routes = {};
return this;
},
/**
* Navigate to the specified route path.
* @param {String} route The route path to remove.
* @return {ish.router} Chainable, returns its own instance.
*/
navigate: function(route){
setState.call(this); // set state of the current page
var routeData = parseURLroute.call(this, route); // parse url route and switch pages
_historyAPI.pushState(routeData, '', this.current);
this.emit('ON_NAVIGATE', routeData);
return this;
},
/**
* Destroy the router instance.
* @return {null}
*/
destroy: function(){
$(window).off('popstate', this.popHandler);
return null;
}
};
var popHandler = function(evt){
setState.call(this);
//get state
var route;
var stateData;
if (history.state){
var historyState = history.state;
stateData = JSON.parse(localStorage.getItem(historyState.route));
route = parseURLroute.call(this, historyState.route, stateData);
} else {
currentLocation = document.URL.replace(this.baseURL,'');
route = parseURLroute.call(this, currentLocation);
}
this.emit('ON_POP', route);
return stateData;
};
/**
* An application router.
* @name ish.router
* @constructor
* @extends {ish.emitter}
* @param {Object} options The utilities options object.
* @param {String} options.baseURL The base URL of the application.
* @param {Object} options.routes The routing object.
* @return {$.router} The router instance
*
* @example
* var router = ish.router({
* baseURL: 'http://ish.stateful.local',
* routes: {
* notFound: function(url){
* },
* before: function(history){
*
* },
* after: function(history){
*
* },
* '/': {
* enter: function(slugs, history){
*
* },
* leave: function(slugs, history){
* }
* }
* }
* };
*/
$.router = function(options){
var factory = Object.create($.fn.router);
ish.extend(factory, ish.emitter(), options);
var currentLocation = document.URL.replace(factory.baseURL,'');
//console.log('currentLocation ',currentLocation,factory.baseURL);
factory.popHandler = popHandler.bind(factory);
$(window).on('popstate', factory.popHandler);
//parses the inital route
parseURLroute.call(factory, currentLocation);
// add history state for the current page
_historyAPI.replaceState({route: factory.current, slugs: factory.slugs }, "", factory.current);
return factory;
};
})();