1 /**
  2  * Olives http://flams.github.com/olives
  3  * The MIT License (MIT)
  4  * Copyright (c) 2012-2014 Olivier Scherrer <pode.fr@gmail.com> - Olivier Wietrich <olivier.wietrich@gmail.com>
  5  */
  6 "use strict";
  7 
  8 var Router = require("emily").Router,
  9     Tools = require("emily").Tools;
 10 
 11 /**
 12  * @class
 13  * A locationRouter is a router which navigates to the route defined in the URL and updates this URL
 14  * while navigating. It's a subtype of Emily's Router
 15  */
 16 function LocationRouterConstructor() {
 17 
 18     /**
 19      * The handle on the watch
 20      * @private
 21      */
 22     var _watchHandle,
 23 
 24     /**
 25      * The default route to navigate to when nothing is supplied in the url
 26      * @private
 27      */
 28     _defaultRoute = "",
 29 
 30     /**
 31      * The last route that was navigated to
 32      * @private
 33      */
 34     _lastRoute = window.location.hash;
 35 
 36     /**
 37      * Navigates to the current hash or to the default route if none is supplied in the url
 38      * @private
 39      */
 40      /*jshint validthis:true*/
 41     function doNavigate() {
 42         if (window.location.hash) {
 43             var parsedHash = this.parse(window.location.hash);
 44             this.navigate.apply(this, parsedHash);
 45         } else {
 46             this.navigate(_defaultRoute);
 47         }
 48     }
 49 
 50     /**
 51      * Set the default route to navigate to when nothing is defined in the url
 52      * @param {String} defaultRoute the defaultRoute to navigate to
 53      * @returns {Boolean} true if it's not an empty string
 54      */
 55     this.setDefaultRoute = function setDefaultRoute(defaultRoute) {
 56         if (defaultRoute && typeof defaultRoute == "string") {
 57             _defaultRoute = defaultRoute;
 58             return true;
 59         } else {
 60             return false;
 61         }
 62     };
 63 
 64     /**
 65      * Get the currently set default route
 66      * @returns {String} the default route
 67      */
 68     this.getDefaultRoute = function getDefaultRoute() {
 69         return _defaultRoute;
 70     };
 71 
 72     /**
 73      * The function that parses the url to determine the route to navigate to.
 74      * It has a default behavior explained below, but can be overriden as long as
 75      * it has the same contract.
 76      * @param {String} hash the hash coming from window.location.has
 77      * @returns {Array} has to return an array with the list of arguments to call
 78      *    navigate with. The first item of the array must be the name of the route.
 79      *
 80      * Example: #album/holiday/2013
 81      *      will navigate to the route "album" and give two arguments "holiday" and "2013"
 82      */
 83     this.parse = function parse(hash) {
 84         return hash.split("#").pop().split("/");
 85     };
 86 
 87     /**
 88      * The function that converts, or serialises the route and its arguments to a valid URL.
 89      * It has a default behavior below, but can be overriden as long as it has the same contract.
 90      * @param {Array} args the list of arguments to serialize
 91      * @returns {String} the serialized arguments to add to the url hashmark
 92      *
 93      * Example:
 94      *      ["album", "holiday", "2013"];
 95      *      will give "album/holiday/2013"
 96      *
 97      */
 98     this.toUrl = function toUrl(args) {
 99         return args.join("/");
100     };
101 
102     /**
103      * When all the routes and handlers have been defined, start the location router
104      * so it parses the URL and navigates to the corresponding route.
105      * It will also start listening to route changes and hashmark changes to navigate.
106      * While navigating, the hashmark itself will also change to reflect the current route state
107      */
108     this.start = function start(defaultRoute) {
109         this.setDefaultRoute(defaultRoute);
110         doNavigate.call(this);
111         this.bindOnHashChange();
112         this.bindOnRouteChange();
113     };
114 
115     /**
116      * Remove the events handler for cleaning.
117      */
118     this.destroy = function destroy() {
119         this.unwatch(_watchHandle);
120         window.removeEventListener("hashchange", this.boundOnHashChange, true);
121     };
122 
123     /**
124      * Parse the hash and navigate to the corresponding url
125      * @private
126      */
127     this.onHashChange  = function onHashChange() {
128         if (window.location.hash != _lastRoute) {
129             doNavigate.call(this);
130         }
131     };
132 
133     /**
134      * The bound version of onHashChange for add/removeEventListener
135      * @private
136      */
137     this.boundOnHashChange = this.onHashChange.bind(this);
138 
139     /**
140      * Add an event listener to hashchange to navigate to the corresponding route
141      * when it changes
142      * @private
143      */
144     this.bindOnHashChange = function bindOnHashChange() {
145         window.addEventListener("hashchange", this.boundOnHashChange, true);
146     };
147 
148     /**
149      * Watch route change events from the router to update the location
150      * @private
151      */
152     this.bindOnRouteChange = function bindOnRouteChange() {
153         _watchHandle = this.watch(this.onRouteChange, this);
154     };
155 
156     /**
157      * The handler for when the route changes
158      * It updates the location
159      * @private
160      */
161     this.onRouteChange = function onRouteChange() {
162         window.location.hash = this.toUrl(Tools.toArray(arguments));
163         _lastRoute = window.location.hash;
164     };
165 
166     this.getLastRoute = function getLastRoute() {
167         return _lastRoute;
168     };
169 
170 }
171 
172 module.exports = function LocationRouterFactory() {
173     LocationRouterConstructor.prototype = new Router();
174     return new LocationRouterConstructor();
175 };
176