source: Dev/trunk/src/client/dojo/router/RouterBase.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 10.3 KB
Line 
1define([
2        "dojo/_base/declare",
3        "dojo/hash",
4        "dojo/topic"
5], function(declare, hash, topic){
6
7        // module:
8        //              dojo/router/RouterBase
9
10        // Creating a basic trim to avoid needing the full dojo/string module
11        // similarly to dojo/_base/lang's trim
12        var trim;
13        if(String.prototype.trim){
14                trim = function(str){ return str.trim(); };
15        }else{
16                trim = function(str){ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); };
17        }
18
19        // Firing of routes on the route object is always the same,
20        // no clean way to expose this on the prototype since it's for the
21        // internal router objects.
22        function fireRoute(params, currentPath, newPath){
23                var queue, isStopped, isPrevented, eventObj, callbackArgs, i, l;
24
25                queue = this.callbackQueue;
26                isStopped = false;
27                isPrevented = false;
28                eventObj = {
29                        stopImmediatePropagation: function(){ isStopped = true; },
30                        preventDefault: function(){ isPrevented = true; },
31                        oldPath: currentPath,
32                        newPath: newPath,
33                        params: params
34                };
35
36                callbackArgs = [eventObj];
37
38                if(params instanceof Array){
39                        callbackArgs = callbackArgs.concat(params);
40                }else{
41                        for(var key in params){
42                                callbackArgs.push(params[key]);
43                        }
44                }
45
46                for(i=0, l=queue.length; i<l; ++i){
47                        if(!isStopped){
48                                queue[i].apply(null, callbackArgs);
49                        }
50                }
51
52                return !isPrevented;
53        }
54
55        // Our actual class-like object
56        var RouterBase = declare(null, {
57                // summary:
58                //              A module that allows one to easily map hash-based structures into
59                //              callbacks. The router module is a singleton, offering one central
60                //              point for all registrations of this type.
61                // example:
62                //      |       var router = new RouterBase({});
63                //      |       router.register("/widgets/:id", function(evt){
64                //      |               // If "/widgets/3" was matched,
65                //      |               // evt.params.id === "3"
66                //      |               xhr.get({
67                //      |                       url: "/some/path/" + evt.params.id,
68                //      |                       load: function(data){
69                //      |                               // ...
70                //      |                       }
71                //      |               });
72                //      |       });
73
74                _routes: null,
75                _routeIndex: null,
76                _started: false,
77                _currentPath: "",
78
79                idMatch: /:(\w[\w\d]*)/g,
80                idReplacement: "([^\\/]+)",
81                globMatch: /\*(\w[\w\d]*)/,
82                globReplacement: "(.+)",
83
84                constructor: function(kwArgs){
85                        // A couple of safety initializations
86                        this._routes = [];
87                        this._routeIndex = {};
88
89                        // Simple constructor-style "Decorate myself all over" for now
90                        for(var i in kwArgs){
91                                if(kwArgs.hasOwnProperty(i)){
92                                        this[i] = kwArgs[i];
93                                }
94                        }
95                },
96
97                register: function(/*String|RegExp*/ route, /*Function*/ callback){
98                        // summary:
99                        //              Registers a route to a handling callback
100                        // description:
101                        //              Given either a string or a regular expression, the router
102                        //              will monitor the page's hash and respond to changes that
103                        //              match the string or regex as provided.
104                        //
105                        //              When provided a regex for the route:
106                        //
107                        //              - Matching is performed, and the resulting capture groups
108                        //              are passed through to the callback as an array.
109                        //
110                        //              When provided a string for the route:
111                        //
112                        //              - The string is parsed as a URL-like structure, like
113                        //              "/foo/bar"
114                        //              - If any portions of that URL are prefixed with a colon
115                        //              (:), they will be parsed out and provided to the callback
116                        //              as properties of an object.
117                        //              - If the last piece of the URL-like structure is prefixed
118                        //              with a star (*) instead of a colon, it will be replaced in
119                        //              the resulting regex with a greedy (.+) match and
120                        //              anything remaining on the hash will be provided as a
121                        //              property on the object passed into the callback. Think of
122                        //              it like a basic means of globbing the end of a route.
123                        // example:
124                        //      |       router.register("/foo/:bar/*baz", function(object){
125                        //      |               // If the hash was "/foo/abc/def/ghi",
126                        //      |               // object.bar === "abc"
127                        //      |               // object.baz === "def/ghi"
128                        //      |       });
129                        // returns: Object
130                        //              A plain JavaScript object to be used as a handle for
131                        //              either removing this specific callback's registration, as
132                        //              well as to add new callbacks with the same route initially
133                        //              used.
134                        // route: String|RegExp
135                        //              A string or regular expression which will be used when
136                        //              monitoring hash changes.
137                        // callback: Function
138                        //              When the hash matches a pattern as described in the route,
139                        //              this callback will be executed. It will receive an event
140                        //              object that will have several properties:
141                        //
142                        //              - params: Either an array or object of properties pulled
143                        //              from the new hash
144                        //              - oldPath: The hash in its state before the change
145                        //              - newPath: The new hash being shifted to
146                        //              - preventDefault: A method that will stop hash changes
147                        //              from being actually applied to the active hash. This only
148                        //              works if the hash change was initiated using `router.go`,
149                        //              as changes initiated more directly to the location.hash
150                        //              property will already be in place
151                        //              - stopImmediatePropagation: When called, will stop any
152                        //              further bound callbacks on this particular route from
153                        //              being executed. If two distinct routes are bound that are
154                        //              different, but both happen to match the current hash in
155                        //              some way, this will *not* keep other routes from receiving
156                        //              notice of the change.
157
158                        return this._registerRoute(route, callback);
159                },
160
161                registerBefore: function(/*String|RegExp*/ route, /*Function*/ callback){
162                        // summary:
163                        //              Registers a route to a handling callback, except before
164                        //              any previously registered callbacks
165                        // description:
166                        //              Much like the `register` method, `registerBefore` allows
167                        //              us to register route callbacks to happen before any
168                        //              previously registered callbacks. See the documentation for
169                        //              `register` for more details and examples.
170
171                        return this._registerRoute(route, callback, true);
172                },
173
174                go: function(path, replace){
175                        // summary:
176                        //              A simple pass-through to make changing the hash easy,
177                        //              without having to require dojo/hash directly. It also
178                        //              synchronously fires off any routes that match.
179                        // example:
180                        //      |       router.go("/foo/bar");
181
182                        var applyChange;
183
184                        if(typeof path !== "string"){return false;}
185
186                        path = trim(path);
187                        applyChange = this._handlePathChange(path);
188
189                        if(applyChange){
190                                hash(path, replace);
191                        }
192
193                        return applyChange;
194                },
195
196                startup: function(defaultPath){
197                        // summary:
198                        //              This method must be called to activate the router. Until
199                        //              startup is called, no hash changes will trigger route
200                        //              callbacks.
201
202                        if(this._started){ return; }
203
204                        var self = this,
205                                startingPath = hash();
206
207                        this._started = true;
208                        this._hashchangeHandle = topic.subscribe("/dojo/hashchange", function(){
209                                self._handlePathChange.apply(self, arguments);
210                        });
211
212                        if(!startingPath){
213                                // If there is no initial starting point, push our defaultPath into our
214                                // history as the starting point
215                                this.go(defaultPath, true);
216                        }else{
217                                // Handle the starting path
218                                this._handlePathChange(startingPath);
219                        }
220                },
221
222                destroy: function(){
223                        if(this._hashchangeHandle){
224                                this._hashchangeHandle.remove();
225                        }                       
226                        this._routes = null;
227                        this._routeIndex = null;
228                },
229
230                _handlePathChange: function(newPath){
231                        var i, j, li, lj, routeObj, result,
232                                allowChange, parameterNames, params,
233                                routes = this._routes,
234                                currentPath = this._currentPath;
235
236                        if(!this._started || newPath === currentPath){ return allowChange; }
237
238                        allowChange = true;
239
240                        for(i=0, li=routes.length; i<li; ++i){
241                                routeObj = routes[i];
242                                result = routeObj.route.exec(newPath);
243
244                                if(result){
245                                        if(routeObj.parameterNames){
246                                                parameterNames = routeObj.parameterNames;
247                                                params = {};
248
249                                                for(j=0, lj=parameterNames.length; j<lj; ++j){
250                                                        params[parameterNames[j]] = result[j+1];
251                                                }
252                                        }else{
253                                                params = result.slice(1);
254                                        }
255                                        allowChange = routeObj.fire(params, currentPath, newPath);
256                                }
257                        }
258
259                        if(allowChange){
260                                this._currentPath = newPath;
261                        }
262
263                        return allowChange;
264                },
265
266                _convertRouteToRegExp: function(route){
267                        // Sub in based on IDs and globs
268                        route = route.replace(this.idMatch, this.idReplacement);
269                        route = route.replace(this.globMatch, this.globReplacement);
270                        // Make sure it's an exact match
271                        route = "^" + route + "$";
272
273                        return new RegExp(route);
274                },
275
276                _getParameterNames: function(route){
277                        var idMatch = this.idMatch,
278                                globMatch = this.globMatch,
279                                parameterNames = [], match;
280
281                        idMatch.lastIndex = 0;
282
283                        while((match = idMatch.exec(route)) !== null){
284                                parameterNames.push(match[1]);
285                        }
286                        if((match = globMatch.exec(route)) !== null){
287                                parameterNames.push(match[1]);
288                        }
289
290                        return parameterNames.length > 0 ? parameterNames : null;
291                },
292
293                _indexRoutes: function(){
294                        var i, l, route, routeIndex, routes = this._routes;
295
296                        // Start a new route index
297                        routeIndex = this._routeIndex = {};
298
299                        // Set it up again
300                        for(i=0, l=routes.length; i<l; ++i){
301                                route = routes[i];
302                                routeIndex[route.route] = i;
303                        }
304                },
305
306                _registerRoute: function(/*String|RegExp*/route, /*Function*/callback, /*Boolean?*/isBefore){
307                        var index, exists, routeObj, callbackQueue, removed,
308                                self = this, routes = this._routes,
309                                routeIndex = this._routeIndex;
310
311                        // Try to fetch the route if it already exists.
312                        // This works thanks to stringifying of regex
313                        index = this._routeIndex[route];
314                        exists = typeof index !== "undefined";
315                        if(exists){
316                                routeObj = routes[index];
317                        }
318
319                        // If we didn't get one, make a default start point
320                        if(!routeObj){
321                                routeObj = {
322                                        route: route,
323                                        callbackQueue: [],
324                                        fire: fireRoute
325                                };
326                        }
327
328                        callbackQueue = routeObj.callbackQueue;
329
330                        if(typeof route == "string"){
331                                routeObj.parameterNames = this._getParameterNames(route);
332                                routeObj.route = this._convertRouteToRegExp(route);
333                        }
334
335                        if(isBefore){
336                                callbackQueue.unshift(callback);
337                        }else{
338                                callbackQueue.push(callback);
339                        }
340
341                        if(!exists){
342                                index = routes.length;
343                                routeIndex[route] = index;
344                                routes.push(routeObj);
345                        }
346
347                        // Useful in a moment to keep from re-removing routes
348                        removed = false;
349
350                        return { // Object
351                                remove: function(){
352                                        var i, l;
353
354                                        if(removed){ return; }
355
356                                        for(i=0, l=callbackQueue.length; i<l; ++i){
357                                                if(callbackQueue[i] === callback){
358                                                        callbackQueue.splice(i, 1);
359                                                }
360                                        }
361
362
363                                        if(callbackQueue.length === 0){
364                                                routes.splice(index, 1);
365                                                self._indexRoutes();
366                                        }
367
368                                        removed = true;
369                                },
370                                register: function(callback, isBefore){
371                                        return self.register(route, callback, isBefore);
372                                }
373                        };
374                }
375        });
376
377        return RouterBase;
378});
Note: See TracBrowser for help on using the repository browser.