forked from jupiterjs/jquerymx
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroute.js
More file actions
446 lines (435 loc) · 14.9 KB
/
route.js
File metadata and controls
446 lines (435 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
steal('jquery/lang/observe', 'jquery/event/hashchange', 'jquery/lang/string/deparam',
function( $ ) {
// Helper methods used for matching routes.
var
// RegEx used to match route variables of the type ':name'.
// Any word character or a period is matched.
matcher = /\:([\w\.]+)/g,
// Regular expression for identifying &key=value lists.
paramsMatcher = /^(?:&[^=]+=[^&]*)+/,
// Converts a JS Object into a list of parameters that can be
// inserted into an html element tag.
makeProps = function( props ) {
var html = [],
name, val;
each(props, function(name, val){
if ( name === 'className' ) {
name = 'class'
}
val && html.push(escapeHTML(name), "=\"", escapeHTML(val), "\" ");
})
return html.join("")
},
// Escapes ' and " for safe insertion into html tag parameters.
escapeHTML = function( content ) {
return content.replace(/"/g, '"').replace(/'/g, "'");
},
// Checks if a route matches the data provided. If any route variable
// is not present in the data the route does not match. If all route
// variables are present in the data the number of matches is returned
// to allow discerning between general and more specific routes.
matchesData = function(route, data) {
var count = 0;
for ( var i = 0; i < route.names.length; i++ ) {
if (!data.hasOwnProperty(route.names[i]) ) {
return -1;
}
count++;
}
return count;
},
//
onready = true,
location = window.location,
encode = encodeURIComponent,
decode = decodeURIComponent,
each = $.each,
extend = $.extend;
/**
* @class jQuery.route
* @inherits jQuery.Observe
* @plugin jquery/dom/route
* @parent dom
* @tag 3.2
*
* jQuery.route helps manage browser history (and
* client state) by
* synchronizing the window.location.hash with
* an [jQuery.Observe].
*
* ## Background Information
*
* To support the browser's back button and bookmarking
* in an Ajax application, most applications use
* the <code>window.location.hash</code>. By
* changing the hash (via a link or JavaScript),
* one is able to add to the browser's history
* without changing the page. The [jQuery.event.special.hashchange event] allows
* you to listen to when the hash is changed.
*
* Combined, this provides the basics needed to
* create history enabled Ajax websites. However,
* jQuery.Route addresses several other needs such as:
*
* - Pretty Routes
* - Keeping routes independent of application code
* - Listening to specific parts of the history changing
* - Setup / Teardown of widgets.
*
* ## How it works
*
* <code>$.route</code> is a [jQuery.Observe $.Observe] that represents the
* <code>window.location.hash</code> as an
* object. For example, if the hash looks like:
*
* #!type=videos&id=5
*
* the data in <code>$.route</code> would look like:
*
* { type: 'videos', id: 5 }
*
*
* $.route keeps the state of the hash in-sync with the data in
* $.route.
*
* ## $.Observe
*
* $.route is a [jQuery.Observe $.Observe]. Understanding
* $.Observe is essential for using $.route correctly.
*
* You can
* listen to changes in an Observe with bind and
* delegate and change $.route's properties with
* attr and attrs.
*
* ### Listening to changes in an Observable
*
* Listen to changes in history
* by [jQuery.Observe.prototype.bind bind]ing to
* changes in <code>$.route</code> like:
*
* $.route.bind('change', function(ev, attr, how, newVal, oldVal) {
*
* })
*
* - attr - the name of the changed attribute
* - how - the type of Observe change event (add, set or remove)
* - newVal/oldVal - the new and old values of the attribute
*
* You can also listen to specific changes
* with [jQuery.Observe.prototype.delegate delegate]:
*
* $.route.delegate('id','change', function(){ ... })
*
* Observe lets you listen to the following events:
*
* - change - any change to the object
* - add - a property is added
* - set - a property value is added or changed
* - remove - a property is removed
*
* Listening for <code>add</code> is useful for widget setup
* behavior, <code>remove</code> is useful for teardown.
*
* ### Updating an observable
*
* Create changes in the route data like:
*
* $.route.attr('type','images');
*
* Or change multiple properties at once with
* [jQuery.Observe.prototype.attrs attrs]:
*
* $.route.attr({type: 'pages', id: 5}, true)
*
* When you make changes to $.route, they will automatically
* change the <code>hash</code>.
*
* ## Creating a Route
*
* Use <code>$.route(url, defaults)</code> to create a
* route. A route is a mapping from a url to
* an object (that is the $.route's state).
*
* If no routes are added, or no route is matched,
* $.route's data is updated with the [jQuery.String.deparam deparamed]
* hash.
*
* location.hash = "#!type=videos";
* // $.route -> {type : "videos"}
*
* Once routes are added and the hash changes,
* $.route looks for matching routes and uses them
* to update $.route's data.
*
* $.route( "content/:type" );
* location.hash = "#!content/images";
* // $.route -> {type : "images"}
*
* Default values can also be added:
*
* $.route("content/:type",{type: "videos" });
* location.hash = "#!content/"
* // $.route -> {type : "videos"}
*
* ## Delay setting $.route
*
* By default, <code>$.route</code> sets its initial data
* on document ready. Sometimes, you want to wait to set
* this data. To wait, call:
*
* $.route.ready(false);
*
* and when ready, call:
*
* $.route.ready(true);
*
* ## Changing the route.
*
* Typically, you never want to set <code>location.hash</code>
* directly. Instead, you can change properties on <code>$.route</code>
* like:
*
* $.route.attr('type', 'videos')
*
* This will automatically look up the appropriate
* route and update the hash.
*
* Often, you want to create links. <code>$.route</code> provides
* the [jQuery.route.link] and [jQuery.route.url] helpers to make this
* easy:
*
* $.route.link("Videos", {type: 'videos'})
*
* @param {String} url the fragment identifier to match.
* @param {Object} [defaults] an object of default values
* @return {jQuery.route}
*/
$.route = function( url, defaults ) {
// Extract the variable names and replace with regEx that will match an atual URL with values.
var names = [],
test = url.replace(matcher, function( whole, name ) {
names.push(name)
return "([^\\/\\&]*)" // The '\\' is for string-escaping giving single '\' for regEx escaping
});
// Add route in a form that can be easily figured out
$.route.routes[url] = {
// A regular expression that will match the route when variable values
// are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which
// will match for any value of :page and :type (word chars or period).
test: new RegExp("^" + test),
// The original URL, same as the index for this entry in routes.
route: url,
// An array of all the variable names in this route
names: names,
// Default values provided for the variables.
defaults: defaults || {},
// The number of parts in the URL separated by '/'.
length: url.split('/').length
}
return $.route;
};
extend($.route, {
/**
* Parameterizes the raw JS object representation provided in data.
* If a route matching the provided data is found that URL is built
* from the data. Any remaining data is added at the end of the
* URL as & separated key/value parameters.
*
* @param {Object} data
* @return {String} The route URL and & separated parameters.
*/
param: function( data ) {
// Check if the provided data keys match the names in any routes;
// get the one with the most matches.
delete data.route;
var route,
matches = -1,
matchCount;
each($.route.routes, function(name, temp){
matchCount = matchesData(temp, data);
if ( matchCount > matches ) {
route = temp;
matches = matchCount
}
});
if ( route ) {
var cpy = extend({}, data),
// Create the url by replacing the var names with the provided data.
// If the default value is found an empty string is inserted.
res = route.route.replace(matcher, function( whole, name ) {
delete cpy[name];
return data[name] === route.defaults[name] ? "" : encode( data[name] );
}),
after;
// remove matching default values
each(route.defaults, function(name,val){
if(cpy[name] === val) {
delete cpy[name]
}
})
// The remaining elements of data are added as
// $amp; separated parameters to the url.
after = $.param(cpy);
return res + (after ? "&" + after : "")
}
// If no route was found there is no hash URL, only paramters.
return $.isEmptyObject(data) ? "" : "&" + $.param(data);
},
/**
* Populate the JS data object from a given URL.
*
* @param {Object} url
*/
deparam: function( url ) {
// See if the url matches any routes by testing it against the route.test regEx.
// By comparing the URL length the most specialized route that matches is used.
var route = {
length: -1
};
each($.route.routes, function(name, temp){
if ( temp.test.test(url) && temp.length > route.length ) {
route = temp;
}
});
// If a route was matched
if ( route.length > -1 ) {
var // Since RegEx backreferences are used in route.test (round brackets)
// the parts will contain the full matched string and each variable (backreferenced) value.
parts = url.match(route.test),
// start will contain the full matched string; parts contain the variable values.
start = parts.shift(),
// The remainder will be the &key=value list at the end of the URL.
remainder = url.substr(start.length),
// If there is a remainder and it contains a &key=value list deparam it.
obj = (remainder && paramsMatcher.test(remainder)) ? $.String.deparam( remainder.slice(1) ) : {};
// Add the default values for this route
obj = extend(true, {}, route.defaults, obj);
// Overwrite each of the default values in obj with those in parts if that part is not empty.
each(parts,function(i, part){
if ( part ) {
obj[route.names[i]] = decode( part );
}
});
obj.route = route.route;
return obj;
}
// If no route was matched it is parsed as a &key=value list.
return paramsMatcher.test(url) ? $.String.deparam( url.slice(1) ) : {};
},
/**
* @hide
* A $.Observe that represents the state of the history.
*/
data: new $.Observe({}),
/**
* @attribute
* @type Object
* @hide
*
* A list of routes recognized by the router indixed by the url used to add it.
* Each route is an object with these members:
*
* - test - A regular expression that will match the route when variable values
* are present; i.e. for :page/:type the regEx is /([\w\.]*)/([\w\.]*)/ which
* will match for any value of :page and :type (word chars or period).
*
* - route - The original URL, same as the index for this entry in routes.
*
* - names - An array of all the variable names in this route
*
* - defaults - Default values provided for the variables or an empty object.
*
* - length - The number of parts in the URL separated by '/'.
*/
routes: {},
/**
* Indicates that all routes have been added and sets $.route.data
* based upon the routes and the current hash.
* @param {Boolean} [start]
* @return
*/
ready: function(val) {
if( val === false ) {
onready = false;
}
if( val === true || onready === true ) {
setState();
}
return $.route;
},
/**
* Returns a url from the options
* @param {Object} options
* @param {Boolean} merge true if the options should be merged with the current options
* @return {String}
*/
url: function( options, merge ) {
if (merge) {
return "#!" + $.route.param(extend({}, curParams, options))
} else {
return "#!" + $.route.param(options)
}
},
/**
* Returns a link
* @param {Object} name The text of the link.
* @param {Object} options The route options (variables)
* @param {Object} props Properties of the <a> other than href.
* @param {Boolean} merge true if the options should be merged with the current options
*/
link: function( name, options, props, merge ) {
return "<a " + makeProps(
extend({
href: $.route.url(options, merge)
}, props)) + ">" + name + "</a>";
},
/**
* Returns true if the options represent the current page.
* @param {Object} options
* @return {Boolean}
*/
current: function( options ) {
return location.hash == "#!" + $.route.param(options)
}
});
// onready
$(function() {
$.route.ready();
});
// The functions in the following list applied to $.route (e.g. $.route.attr('...')) will
// instead act on the $.route.data Observe.
each(['bind','unbind','delegate','undelegate','attr','serialize','removeAttr'], function(i, name){
$.route[name] = function(){
return $.route.data[name].apply($.route.data, arguments)
}
})
var // A throttled function called multiple times will only fire once the
// timer runs down. Each call resets the timer.
throttle = function( func ) {
var timer;
return function() {
clearTimeout(timer);
timer = setTimeout(func, 1);
}
},
// Intermediate storage for $.route.data.
curParams,
// Deparameterizes the portion of the hash of interest and assign the
// values to the $.route.data removing existing values no longer in the hash.
setState = function() {
var hash = location.hash.substr(1, 1) === '!' ?
location.hash.slice(2) :
location.hash.slice(1); // everything after #!
curParams = $.route.deparam( hash );
$.route.attr(curParams, true);
};
// If the hash changes, update the $.route.data
$(window).bind('hashchange', setState);
// If the $.route.data changes, update the hash.
// Using .serialize() retrieves the raw data contained in the observable.
// This function is throttled so it only updates once even if multiple values changed.
$.route.bind("change", throttle(function() {
location.hash = "#!" + $.route.param($.route.serialize())
}));
})