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