-
Notifications
You must be signed in to change notification settings - Fork 2
/
backbone.canal.js
374 lines (305 loc) · 10.6 KB
/
backbone.canal.js
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
/*
* backbone.canal v0.1.0
* Copyright 2013, Matt Morgan (@mlmorg)
* MIT license
*/
(function () {
"use strict";
// Alias libraries
var root = this;
var _ = root._;
var Backbone = root.Backbone;
// References for overridden methods
var _constructor = Backbone.Router.prototype.constructor;
var _extractParameters = Backbone.Router.prototype._extractParameters;
var _getFragment = Backbone.History.prototype.getFragment;
// Useful regex patters
var paramNamesPattern = /(\(.*?)?[:\*]\w+\)?/g;
var optionalParamPattern = /\(.*?[:\*](\w+)\)/;
var queryStringPattern = /\?.*$/;
var plusPattern = /\+/g;
var r20Pattern = /%20/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Backbone.Canal
// --------------
var Canal = Backbone.Canal = {
configure: function (options) {
return _.extend(this.options, options);
},
options: {
deparam: function (string) {
// Extract the key/value pairs
var items = string.replace(plusPattern, ' ').split('&');
// Build hash
var obj = {};
_.each(items, function (params) {
var param = params.split('=');
obj[param[0]] = decodeURIComponent(param[1]);
});
return obj;
},
param: function (obj) {
// Build param strings
var params = _.map(obj, function (value, key) {
return key + '=' + encodeURIComponent(value);
});
// Create full query param string
return params.join('&').replace(r20Pattern, '+');
}
}
};
// Extend Backbone.Router
// ----------------------
Backbone.Router = Backbone.Router.extend({
constructor: function () {
this._routes = {};
this._named = {};
return _constructor.apply(this, arguments);
},
route: function (route, name, callback) {
// Store information about the route on the router
route = this._storeRoute(route, name);
// Default callback to the method present on the router
if (!callback) {
callback = this[name];
}
// Add the handler to Backbone.History
Backbone.history.route(route, _.bind(function (fragment) {
// Extract route and query parameters
var params = this._extractParameters(route, fragment);
// Create argument array
var args = [params];
// Call any before filters
var halt = _.find(this._getFilters(name, 'before'), function (filter) {
return filter.apply(this, [name].concat(args)) === false;
}, this);
// Stop if any before filters return false
if (halt) {
return;
}
// Create a function to call the route method and trigger events
var cb = _.bind(function () {
// Run the callback
if (callback) {
callback.apply(this, args);
}
// Trigger events
this.trigger.apply(this, ['route:' + name].concat(args));
this.trigger.apply(this, ['route', name].concat(args));
Backbone.history.trigger('route', this, name, args);
// Call any after filters
_.each(this._getFilters(name, 'after'), function (filter) {
filter.apply(this, [name].concat(args));
}, this);
}, this);
// Build the around filters around the function
// (reversed for correct order)
_.each(this._getFilters(name, 'around').reverse(), function (filter) {
var lastCb = cb;
cb = _.bind(function () {
filter.apply(this, [lastCb, name].concat(args));
}, this);
}, this);
// Call the function
cb();
}, this));
return this;
},
url: function (name, params) {
params = params || {};
// Determine the best match for this route with the passed params
var routes = this._named[name] || [];
var keys = _.keys(params);
var route = _.find(routes, function (route) {
var names = _.difference(route.parameters, route.optionalParameters);
var diff = _.difference(names, keys);
return !diff.length;
});
// If we have a matching route, build the URL for it
if (route) {
return this._buildUrl(route.route, params);
}
},
go: function (name, params, options) {
// Get the associated route URL
var url = this.url(name, params);
// If a URL exists, navigate to it
if (_.isString(url)) {
return this.navigate(url, _.extend({ trigger: true }, options));
}
// Otherwise, call the router method, if it exists
else if (this[name]) {
return this[name](params);
}
// When no method or matching route exists, throw an error
else {
throw new Error('No method or matching route exists');
}
},
_extractParameters: function (route, fragment) {
var params = {};
// Remove any query parameters from fragment
var query = getQueryString(fragment);
fragment = fragment.replace(query, '');
// Parse query parameters and merge onto hash
if (query) {
_.extend(params, Canal.options.deparam(query.substr(1)));
}
// Extract named/splat parameters and merge onto hash
var paramNames = this._routes[route].parameters;
var paramValues = _extractParameters.call(this, route, fragment);
if (paramValues) {
_.each(paramValues, function (param, i) {
// Set the parameter key to the name in the names array or, if no
// name is in the array, the index value
var key = paramNames[i] || i;
params[key] = param;
});
}
return params;
},
_getFilters: function (name, type) {
var filters = [];
_.each(this[type], function (options, filterName) {
var add = true;
options = options || {};
// Determine whether this filter should be used based on only option
if (options.only) {
add = false;
options.only = _.isArray(options.only) ?
options.only : [options.only];
if (_.contains(options.only, name)) {
add = true;
}
}
// Determine whether this filter should be used based on except option
if (options.except) {
options.except = _.isArray(options.except) ?
options.except : [options.except];
if (_.contains(options.except, name)) {
add = false;
}
}
// Upon determination, add the filter to the filters array
if (add && this[filterName]) {
filters.push(this[filterName]);
}
}, this);
return filters;
},
_storeRoute: function (route, name) {
var url = route;
// Create a regular expression of the route
if (!_.isRegExp(route)) {
route = this._routeToRegExp(route);
}
// Get route parameter names and any optionals
var optionals = [];
var parameters = _.map(url.match(paramNamesPattern), function (name) {
var optional = name.match(optionalParamPattern);
if (optional) {
name = optional[1];
optionals.push(name);
} else {
name = name.substr(1);
}
return name;
});
// Store hash of route information on the router
this._routes[route] = {
route: route,
url: url !== route ? url : undefined,
parameters: parameters,
optionalParameters: optionals
};
// Add route information to an array of routes associated with a method
this._named[name] = this._named[name] || [];
this._named[name].push(this._routes[route]);
// Sort the named route array by number of parameters
this._named[name] = _.sortBy(this._named[name], function (route) {
return -route.parameters.length;
});
return route;
},
_buildUrl: function (route, params) {
params = _.clone(params || {});
// Get route information
route = this._routes[route];
// Start with the base and leading slash
var url = '/' + (route.url || '');
// Replace named/splat parts with their parameter equivalent
_.each(route.parameters, function (name) {
// Find named/splat parts that match the passed parameter
var match = url.match(new RegExp('(\\(.*?)?[:\\*]' + name + '\\)?'));
if (match) {
var value = params[name];
// Optional parameters that don't exist on the params hash should
// be removed
if (!_.isString(value)) {
value = '';
}
// Add any leading characters stripped out of optional parts
else {
if (match[1]) {
value = match[1].substring(1) + value;
}
}
// Replace url part with the parameter value
url = url.replace(match[0], value);
// Remove this parameter from the hash
delete params[name];
}
});
// Add any extra items as query parameters to url
var query = Canal.options.param(params);
if (query) {
url += '?' + query;
}
return url;
}
});
// Extend Backbone.History
// -----------------------
_.extend(Backbone.History.prototype, {
getFragment: function (fragment, forcePushState) {
// Get query string from current URL, if necessary
var query, queryRegExp;
if (!_.isString(fragment)) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
query = this.location.search;
queryRegExp = query.replace(escapeRegExp, '\\$&');
}
}
// Get fragment from original method
fragment = _getFragment.call(this, fragment, forcePushState);
// Add the query string, if not already present
if (query && !fragment.match(queryRegExp)) {
fragment += query;
}
return fragment;
},
loadUrl: function (fragmentOverride) {
// Get fragment
var fragment = this.fragment = this.getFragment(fragmentOverride);
// Create fragment base without a query string
var query = getQueryString(fragment);
var fragmentBase = query ? fragment.replace(query, '') : fragment;
// If a handler matches the base, run callback
var matched = _.any(this.handlers, function (handler) {
if (handler.route.test(fragmentBase)) {
handler.callback(fragment);
return true;
}
});
return matched;
}
});
// Helper functions
// ----------------
var getQueryString = function (url) {
var query = url.match(queryStringPattern);
if (query) {
return query[0];
}
};
}).call(this);