/*
* router.js
* Fabian Irsara
* Copyright 2015, Licensed GPL & MIT
*/
define(['./uri'], function(uri){
// cache current domain for link checks
var currentDomain = uri(window.location.href.toString()).join(['protocol', 'domain', 'port']);
/**
* @example
*
* var controller = {layout: ...};
* var router = new Router(controller, {...routes})
* router.parse(controller.layout);
* router.route();
*
* @class Router
* @param {Object} controller
* @param {Object} routes
**/
function Router(controller, routes){
// store controller instance
this.controller = controller;
// store routes
this.routes = routes;
// store link elements
this.linkElements = [];
// bind url change function to instance
this._urlChange = _urlChange.bind(this);
// bind clicked function to instance
this._clicked = _clicked.bind(this);
// add event listener to window, listen for url changes
window.addEventListener('popstate', this._urlChange);
}
/**
* wheter urls should be rewritten or not (if not uses query string ?route=path/to/route)
* @memberof Router
* @instance
* @static
* @var {Boolean} rewrite
*/
Router.rewrite = true;
/**
* Base subpath if app is not running on root
* @memberof Router
* @instance
* @static
* @var {String} base
*/
Router.base = '/';
/**
* destroy current router instance
* @method destroy
* @memberof Router
* @instance
**/
Router.prototype.destroy = function(){
window.removeEventListener('popstate', this._urlChange);
for (var i = 0, _len = this.linkElements.length; i < _len; i++) {
this.linkElements[i].removeEventListener('click', this._clicked);
}
};
/**
* parses a dom container for href's, and binds click events to internal links
* @method parse
* @memberof Router
* @instance
* @param {DOMElement} container
**/
Router.prototype.parse = function(container){
var elements = container.querySelectorAll('a[href]');
for (var i = 0, _len = elements.length; i < _len; i++) {
var internalLink = true;
var itemHref = elements[i].getAttribute('href');
if (elements[i].getAttribute('target') === '_blank') internalLink = false;
if (itemHref.indexOf('http://') !== -1 || itemHref.indexOf('https://') !== -1) {
if (itemHref.indexOf(currentDomain) === -1) internalLink = false;
}
if (internalLink) {
this.linkElements.push(elements[i]);
elements[i].removeEventListener('click', this._clicked);
elements[i].addEventListener('click', this._clicked);
}
}
};
/**
* fetches route and calls controller, should be called when first instantiating the router
* @method route
* @memberof Router
* @instance
**/
Router.prototype.route = function(){
_urlChange.call(this);
};
/**
* switches to a specific path
* @method to
* @memberof Router
* @instance
* @param {String} path that should be switched to
**/
Router.prototype.to = function(path){
history.pushState(null, null, path);
this._urlChange();
};
/**
* prevents click events if not a new window needs to be shown
* @method _clicked
* @memberof Router
* @instance
* @private
* @param {MouseEvent} event to prevent clicks
**/
var _clicked = function(event){
if (event.ctrlKey ||
event.shiftKey ||
event.metaKey || // apple
(event.button && event.button === 1) // middle click, >IE9 + everyone else
){
event.stopPropagation();
return false;
}
event.preventDefault();
var itemHref = event.currentTarget.getAttribute('href');
var path = Router.get(itemHref);
var route = Router.fetch(this.routes, path);
if (route.path && this.routes[route.path] && this.routes[route.path].overwrite === false) {
_route.call(this, path);
} else {
this.to(itemHref);
}
};
/**
* calls route when a url change has been detected
* @method _urlChange
* @memberof Router
* @instance
* @private
**/
var _urlChange = function(){
_route.call(this, Router.get());
};
/**
* finds current url path and calls controller's according action
* @method _route
* @memberof Router
* @instance
* @private
**/
var _route = function(path){
var route = Router.fetch(this.routes, path);
var definition = this.routes[route.path];
if (! route.path) {
if (this.routes.defaults) {
if (this.routes.defaults.path) {
route = Router.fetch(this.routes, this.routes.defaults.path);
definition = this.routes[route.path];
} else {
definition = this.routes.defaults;
}
}
}
if (definition && definition.controller) {
var InstanceClass = definition.controller;
var instance = new InstanceClass(this.controller);
var action = definition.action || 'index';
action = 'action' + action.substring(0, 1).toUpperCase() + action.substring(1);
if (instance[action]) {
instance[action].apply(instance, route.params);
}
}
};
/**
* generates a full url to a specific route taking rewrite rules into account
* @method generate
* @memberof Router
* @instance
* @public
* @static
* @param {String} path for which a full url should be generated
*/
Router.generate = function(path){
var current = uri(window.location.href.toString());
if (Router.rewrite) {
current.path = Router.base + path;
return current.join(['protocol', 'domain', 'port', 'path']);
}
current.path = Router.base;
current.query = {path: path};
return current.join(['protocol', 'domain', 'port', 'path', 'query']);
};
/**
* generates the corresponding path of the current or a specified url, stripping out the domain and base path
* @method get
* @memberof Router
* @instance
* @public
* @static
* @param {String} url (optional)
*/
Router.get = function(url){
if (! url) url = window.location.href.toString();
var current = uri(url);
var path = current.query.path;
if (Router.rewrite) {
path = current.path.replace(Router.base, '');
if (Router.base === '/') path = current.path.substring(1);
}
if (! path) path = '';
return path;
};
/**
* finds a specific path in defined routes
* @method fetch
* @memberof Router
* @instance
* @public
* @static
* @param {Object} routes
* @param {String} path
*/
Router.fetch = function(routes, path){
var foundPath = null;
var params = [];
if (! path) path = Router.get();
path = path.split('/');
for (var k in routes) {
if (routes.hasOwnProperty(k)) {
var routePath = k.split('/');
params = [];
if (path.length !== routePath.length) continue;
var allValid = true;
for (var i = 0, _len = path.length; i < _len; i++) {
if (routePath[i].indexOf('{') >= 0 && routePath[i].indexOf('}') >= 0) {
params.push(path[i]);
} else if (path[i] !== routePath[i]) {
allValid = false;
}
}
if (allValid) {
foundPath = k;
break;
}
}
}
return {path: foundPath, params: params};
};
return Router;
});