UNPKG

20.3 kBJavaScriptView Raw
1
2
3//
4// Generated on Tue Dec 16 2014 12:13:47 GMT+0100 (CET) by Charlie Robbins, Paolo Fragomeni & the Contributors (Using Codesurgeon).
5// Version 1.2.6
6//
7
8(function (exports) {
9
10/*
11 * browser.js: Browser specific functionality for director.
12 *
13 * (C) 2011, Charlie Robbins, Paolo Fragomeni, & the Contributors.
14 * MIT LICENSE
15 *
16 */
17
18var dloc = document.location;
19
20function dlocHashEmpty() {
21 // Non-IE browsers return '' when the address bar shows '#'; Director's logic
22 // assumes both mean empty.
23 return dloc.hash === '' || dloc.hash === '#';
24}
25
26var listener = {
27 mode: 'modern',
28 hash: dloc.hash,
29 history: false,
30
31 check: function () {
32 var h = dloc.hash;
33 if (h != this.hash) {
34 this.hash = h;
35 this.onHashChanged();
36 }
37 },
38
39 fire: function () {
40 if (this.mode === 'modern') {
41 this.history === true ? window.onpopstate() : window.onhashchange();
42 }
43 else {
44 this.onHashChanged();
45 }
46 },
47
48 init: function (fn, history) {
49 var self = this;
50 this.history = history;
51
52 if (!Router.listeners) {
53 Router.listeners = [];
54 }
55
56 function onchange(onChangeEvent) {
57 for (var i = 0, l = Router.listeners.length; i < l; i++) {
58 Router.listeners[i](onChangeEvent);
59 }
60 }
61
62 //note IE8 is being counted as 'modern' because it has the hashchange event
63 if ('onhashchange' in window && (document.documentMode === undefined
64 || document.documentMode > 7)) {
65 // At least for now HTML5 history is available for 'modern' browsers only
66 if (this.history === true) {
67 // There is an old bug in Chrome that causes onpopstate to fire even
68 // upon initial page load. Since the handler is run manually in init(),
69 // this would cause Chrome to run it twise. Currently the only
70 // workaround seems to be to set the handler after the initial page load
71 // http://code.google.com/p/chromium/issues/detail?id=63040
72 setTimeout(function() {
73 window.onpopstate = onchange;
74 }, 500);
75 }
76 else {
77 window.onhashchange = onchange;
78 }
79 this.mode = 'modern';
80 }
81 else {
82 //
83 // IE support, based on a concept by Erik Arvidson ...
84 //
85 var frame = document.createElement('iframe');
86 frame.id = 'state-frame';
87 frame.style.display = 'none';
88 document.body.appendChild(frame);
89 this.writeFrame('');
90
91 if ('onpropertychange' in document && 'attachEvent' in document) {
92 document.attachEvent('onpropertychange', function () {
93 if (event.propertyName === 'location') {
94 self.check();
95 }
96 });
97 }
98
99 window.setInterval(function () { self.check(); }, 50);
100
101 this.onHashChanged = onchange;
102 this.mode = 'legacy';
103 }
104
105 Router.listeners.push(fn);
106
107 return this.mode;
108 },
109
110 destroy: function (fn) {
111 if (!Router || !Router.listeners) {
112 return;
113 }
114
115 var listeners = Router.listeners;
116
117 for (var i = listeners.length - 1; i >= 0; i--) {
118 if (listeners[i] === fn) {
119 listeners.splice(i, 1);
120 }
121 }
122 },
123
124 setHash: function (s) {
125 // Mozilla always adds an entry to the history
126 if (this.mode === 'legacy') {
127 this.writeFrame(s);
128 }
129
130 if (this.history === true) {
131 window.history.pushState({}, document.title, s);
132 // Fire an onpopstate event manually since pushing does not obviously
133 // trigger the pop event.
134 this.fire();
135 } else {
136 dloc.hash = (s[0] === '/') ? s : '/' + s;
137 }
138 return this;
139 },
140
141 writeFrame: function (s) {
142 // IE support...
143 var f = document.getElementById('state-frame');
144 var d = f.contentDocument || f.contentWindow.document;
145 d.open();
146 d.write("<script>_hash = '" + s + "'; onload = parent.listener.syncHash;<script>");
147 d.close();
148 },
149
150 syncHash: function () {
151 // IE support...
152 var s = this._hash;
153 if (s != dloc.hash) {
154 dloc.hash = s;
155 }
156 return this;
157 },
158
159 onHashChanged: function () {}
160};
161
162var Router = exports.Router = function (routes) {
163 if (!(this instanceof Router)) return new Router(routes);
164
165 this.params = {};
166 this.routes = {};
167 this.methods = ['on', 'once', 'after', 'before'];
168 this.scope = [];
169 this._methods = {};
170
171 this._insert = this.insert;
172 this.insert = this.insertEx;
173
174 this.historySupport = (window.history != null ? window.history.pushState : null) != null
175
176 this.configure();
177 this.mount(routes || {});
178};
179
180Router.prototype.init = function (r) {
181 var self = this
182 , routeTo;
183 this.handler = function(onChangeEvent) {
184 var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash;
185 var url = self.history === true ? self.getPath() : newURL.replace(/.*#/, '');
186 self.dispatch('on', url.charAt(0) === '/' ? url : '/' + url);
187 };
188
189 listener.init(this.handler, this.history);
190
191 if (this.history === false) {
192 if (dlocHashEmpty() && r) {
193 dloc.hash = r;
194 } else if (!dlocHashEmpty()) {
195 self.dispatch('on', '/' + dloc.hash.replace(/^(#\/|#|\/)/, ''));
196 }
197 }
198 else {
199 if (this.convert_hash_in_init) {
200 // Use hash as route
201 routeTo = dlocHashEmpty() && r ? r : !dlocHashEmpty() ? dloc.hash.replace(/^#/, '') : null;
202 if (routeTo) {
203 window.history.replaceState({}, document.title, routeTo);
204 }
205 }
206 else {
207 // Use canonical url
208 routeTo = this.getPath();
209 }
210
211 // Router has been initialized, but due to the chrome bug it will not
212 // yet actually route HTML5 history state changes. Thus, decide if should route.
213 if (routeTo || this.run_in_init === true) {
214 this.handler();
215 }
216 }
217
218 return this;
219};
220
221Router.prototype.explode = function () {
222 var v = this.history === true ? this.getPath() : dloc.hash;
223 if (v.charAt(1) === '/') { v=v.slice(1) }
224 return v.slice(1, v.length).split("/");
225};
226
227Router.prototype.setRoute = function (i, v, val) {
228 var url = this.explode();
229
230 if (typeof i === 'number' && typeof v === 'string') {
231 url[i] = v;
232 }
233 else if (typeof val === 'string') {
234 url.splice(i, v, s);
235 }
236 else {
237 url = [i];
238 }
239
240 listener.setHash(url.join('/'));
241 return url;
242};
243
244//
245// ### function insertEx(method, path, route, parent)
246// #### @method {string} Method to insert the specific `route`.
247// #### @path {Array} Parsed path to insert the `route` at.
248// #### @route {Array|function} Route handlers to insert.
249// #### @parent {Object} **Optional** Parent "routes" to insert into.
250// insert a callback that will only occur once per the matched route.
251//
252Router.prototype.insertEx = function(method, path, route, parent) {
253 if (method === "once") {
254 method = "on";
255 route = function(route) {
256 var once = false;
257 return function() {
258 if (once) return;
259 once = true;
260 return route.apply(this, arguments);
261 };
262 }(route);
263 }
264 return this._insert(method, path, route, parent);
265};
266
267Router.prototype.getRoute = function (v) {
268 var ret = v;
269
270 if (typeof v === "number") {
271 ret = this.explode()[v];
272 }
273 else if (typeof v === "string"){
274 var h = this.explode();
275 ret = h.indexOf(v);
276 }
277 else {
278 ret = this.explode();
279 }
280
281 return ret;
282};
283
284Router.prototype.destroy = function () {
285 listener.destroy(this.handler);
286 return this;
287};
288
289Router.prototype.getPath = function () {
290 var path = window.location.pathname;
291 if (path.substr(0, 1) !== '/') {
292 path = '/' + path;
293 }
294 return path;
295};
296function _every(arr, iterator) {
297 for (var i = 0; i < arr.length; i += 1) {
298 if (iterator(arr[i], i, arr) === false) {
299 return;
300 }
301 }
302}
303
304function _flatten(arr) {
305 var flat = [];
306 for (var i = 0, n = arr.length; i < n; i++) {
307 flat = flat.concat(arr[i]);
308 }
309 return flat;
310}
311
312function _asyncEverySeries(arr, iterator, callback) {
313 if (!arr.length) {
314 return callback();
315 }
316 var completed = 0;
317 (function iterate() {
318 iterator(arr[completed], function(err) {
319 if (err || err === false) {
320 callback(err);
321 callback = function() {};
322 } else {
323 completed += 1;
324 if (completed === arr.length) {
325 callback();
326 } else {
327 iterate();
328 }
329 }
330 });
331 })();
332}
333
334function paramifyString(str, params, mod) {
335 mod = str;
336 for (var param in params) {
337 if (params.hasOwnProperty(param)) {
338 mod = params[param](str);
339 if (mod !== str) {
340 break;
341 }
342 }
343 }
344 return mod === str ? "([._a-zA-Z0-9-%()]+)" : mod;
345}
346
347function regifyString(str, params) {
348 var matches, last = 0, out = "";
349 while (matches = str.substr(last).match(/[^\w\d\- %@&]*\*[^\w\d\- %@&]*/)) {
350 last = matches.index + matches[0].length;
351 matches[0] = matches[0].replace(/^\*/, "([_.()!\\ %@&a-zA-Z0-9-]+)");
352 out += str.substr(0, matches.index) + matches[0];
353 }
354 str = out += str.substr(last);
355 var captures = str.match(/:([^\/]+)/ig), capture, length;
356 if (captures) {
357 length = captures.length;
358 for (var i = 0; i < length; i++) {
359 capture = captures[i];
360 if (capture.slice(0, 2) === "::") {
361 str = capture.slice(1);
362 } else {
363 str = str.replace(capture, paramifyString(capture, params));
364 }
365 }
366 }
367 return str;
368}
369
370function terminator(routes, delimiter, start, stop) {
371 var last = 0, left = 0, right = 0, start = (start || "(").toString(), stop = (stop || ")").toString(), i;
372 for (i = 0; i < routes.length; i++) {
373 var chunk = routes[i];
374 if (chunk.indexOf(start, last) > chunk.indexOf(stop, last) || ~chunk.indexOf(start, last) && !~chunk.indexOf(stop, last) || !~chunk.indexOf(start, last) && ~chunk.indexOf(stop, last)) {
375 left = chunk.indexOf(start, last);
376 right = chunk.indexOf(stop, last);
377 if (~left && !~right || !~left && ~right) {
378 var tmp = routes.slice(0, (i || 1) + 1).join(delimiter);
379 routes = [ tmp ].concat(routes.slice((i || 1) + 1));
380 }
381 last = (right > left ? right : left) + 1;
382 i = 0;
383 } else {
384 last = 0;
385 }
386 }
387 return routes;
388}
389
390var QUERY_SEPARATOR = /\?.*/;
391
392Router.prototype.configure = function(options) {
393 options = options || {};
394 for (var i = 0; i < this.methods.length; i++) {
395 this._methods[this.methods[i]] = true;
396 }
397 this.recurse = options.recurse || this.recurse || false;
398 this.async = options.async || false;
399 this.delimiter = options.delimiter || "/";
400 this.strict = typeof options.strict === "undefined" ? true : options.strict;
401 this.notfound = options.notfound;
402 this.resource = options.resource;
403 this.history = options.html5history && this.historySupport || false;
404 this.run_in_init = this.history === true && options.run_handler_in_init !== false;
405 this.convert_hash_in_init = this.history === true && options.convert_hash_in_init !== false;
406 this.every = {
407 after: options.after || null,
408 before: options.before || null,
409 on: options.on || null
410 };
411 return this;
412};
413
414Router.prototype.param = function(token, matcher) {
415 if (token[0] !== ":") {
416 token = ":" + token;
417 }
418 var compiled = new RegExp(token, "g");
419 this.params[token] = function(str) {
420 return str.replace(compiled, matcher.source || matcher);
421 };
422 return this;
423};
424
425Router.prototype.on = Router.prototype.route = function(method, path, route) {
426 var self = this;
427 if (!route && typeof path == "function") {
428 route = path;
429 path = method;
430 method = "on";
431 }
432 if (Array.isArray(path)) {
433 return path.forEach(function(p) {
434 self.on(method, p, route);
435 });
436 }
437 if (path.source) {
438 path = path.source.replace(/\\\//ig, "/");
439 }
440 if (Array.isArray(method)) {
441 return method.forEach(function(m) {
442 self.on(m.toLowerCase(), path, route);
443 });
444 }
445 path = path.split(new RegExp(this.delimiter));
446 path = terminator(path, this.delimiter);
447 this.insert(method, this.scope.concat(path), route);
448};
449
450Router.prototype.path = function(path, routesFn) {
451 var self = this, length = this.scope.length;
452 if (path.source) {
453 path = path.source.replace(/\\\//ig, "/");
454 }
455 path = path.split(new RegExp(this.delimiter));
456 path = terminator(path, this.delimiter);
457 this.scope = this.scope.concat(path);
458 routesFn.call(this, this);
459 this.scope.splice(length, path.length);
460};
461
462Router.prototype.dispatch = function(method, path, callback) {
463 var self = this, fns = this.traverse(method, path.replace(QUERY_SEPARATOR, ""), this.routes, ""), invoked = this._invoked, after;
464 this._invoked = true;
465 if (!fns || fns.length === 0) {
466 this.last = [];
467 if (typeof this.notfound === "function") {
468 this.invoke([ this.notfound ], {
469 method: method,
470 path: path
471 }, callback);
472 }
473 return false;
474 }
475 if (this.recurse === "forward") {
476 fns = fns.reverse();
477 }
478 function updateAndInvoke() {
479 self.last = fns.after;
480 self.invoke(self.runlist(fns), self, callback);
481 }
482 after = this.every && this.every.after ? [ this.every.after ].concat(this.last) : [ this.last ];
483 if (after && after.length > 0 && invoked) {
484 if (this.async) {
485 this.invoke(after, this, updateAndInvoke);
486 } else {
487 this.invoke(after, this);
488 updateAndInvoke();
489 }
490 return true;
491 }
492 updateAndInvoke();
493 return true;
494};
495
496Router.prototype.invoke = function(fns, thisArg, callback) {
497 var self = this;
498 var apply;
499 if (this.async) {
500 apply = function(fn, next) {
501 if (Array.isArray(fn)) {
502 return _asyncEverySeries(fn, apply, next);
503 } else if (typeof fn == "function") {
504 fn.apply(thisArg, (fns.captures || []).concat(next));
505 }
506 };
507 _asyncEverySeries(fns, apply, function() {
508 if (callback) {
509 callback.apply(thisArg, arguments);
510 }
511 });
512 } else {
513 apply = function(fn) {
514 if (Array.isArray(fn)) {
515 return _every(fn, apply);
516 } else if (typeof fn === "function") {
517 return fn.apply(thisArg, fns.captures || []);
518 } else if (typeof fn === "string" && self.resource) {
519 self.resource[fn].apply(thisArg, fns.captures || []);
520 }
521 };
522 _every(fns, apply);
523 }
524};
525
526Router.prototype.traverse = function(method, path, routes, regexp, filter) {
527 var fns = [], current, exact, match, next, that;
528 function filterRoutes(routes) {
529 if (!filter) {
530 return routes;
531 }
532 function deepCopy(source) {
533 var result = [];
534 for (var i = 0; i < source.length; i++) {
535 result[i] = Array.isArray(source[i]) ? deepCopy(source[i]) : source[i];
536 }
537 return result;
538 }
539 function applyFilter(fns) {
540 for (var i = fns.length - 1; i >= 0; i--) {
541 if (Array.isArray(fns[i])) {
542 applyFilter(fns[i]);
543 if (fns[i].length === 0) {
544 fns.splice(i, 1);
545 }
546 } else {
547 if (!filter(fns[i])) {
548 fns.splice(i, 1);
549 }
550 }
551 }
552 }
553 var newRoutes = deepCopy(routes);
554 newRoutes.matched = routes.matched;
555 newRoutes.captures = routes.captures;
556 newRoutes.after = routes.after.filter(filter);
557 applyFilter(newRoutes);
558 return newRoutes;
559 }
560 if (path === this.delimiter && routes[method]) {
561 next = [ [ routes.before, routes[method] ].filter(Boolean) ];
562 next.after = [ routes.after ].filter(Boolean);
563 next.matched = true;
564 next.captures = [];
565 return filterRoutes(next);
566 }
567 for (var r in routes) {
568 if (routes.hasOwnProperty(r) && (!this._methods[r] || this._methods[r] && typeof routes[r] === "object" && !Array.isArray(routes[r]))) {
569 current = exact = regexp + this.delimiter + r;
570 if (!this.strict) {
571 exact += "[" + this.delimiter + "]?";
572 }
573 match = path.match(new RegExp("^" + exact));
574 if (!match) {
575 continue;
576 }
577 if (match[0] && match[0] == path && routes[r][method]) {
578 next = [ [ routes[r].before, routes[r][method] ].filter(Boolean) ];
579 next.after = [ routes[r].after ].filter(Boolean);
580 next.matched = true;
581 next.captures = match.slice(1);
582 if (this.recurse && routes === this.routes) {
583 next.push([ routes.before, routes.on ].filter(Boolean));
584 next.after = next.after.concat([ routes.after ].filter(Boolean));
585 }
586 return filterRoutes(next);
587 }
588 next = this.traverse(method, path, routes[r], current);
589 if (next.matched) {
590 if (next.length > 0) {
591 fns = fns.concat(next);
592 }
593 if (this.recurse) {
594 fns.push([ routes[r].before, routes[r].on ].filter(Boolean));
595 next.after = next.after.concat([ routes[r].after ].filter(Boolean));
596 if (routes === this.routes) {
597 fns.push([ routes["before"], routes["on"] ].filter(Boolean));
598 next.after = next.after.concat([ routes["after"] ].filter(Boolean));
599 }
600 }
601 fns.matched = true;
602 fns.captures = next.captures;
603 fns.after = next.after;
604 return filterRoutes(fns);
605 }
606 }
607 }
608 return false;
609};
610
611Router.prototype.insert = function(method, path, route, parent) {
612 var methodType, parentType, isArray, nested, part;
613 path = path.filter(function(p) {
614 return p && p.length > 0;
615 });
616 parent = parent || this.routes;
617 part = path.shift();
618 if (/\:|\*/.test(part) && !/\\d|\\w/.test(part)) {
619 part = regifyString(part, this.params);
620 }
621 if (path.length > 0) {
622 parent[part] = parent[part] || {};
623 return this.insert(method, path, route, parent[part]);
624 }
625 if (!part && !path.length && parent === this.routes) {
626 methodType = typeof parent[method];
627 switch (methodType) {
628 case "function":
629 parent[method] = [ parent[method], route ];
630 return;
631 case "object":
632 parent[method].push(route);
633 return;
634 case "undefined":
635 parent[method] = route;
636 return;
637 }
638 return;
639 }
640 parentType = typeof parent[part];
641 isArray = Array.isArray(parent[part]);
642 if (parent[part] && !isArray && parentType == "object") {
643 methodType = typeof parent[part][method];
644 switch (methodType) {
645 case "function":
646 parent[part][method] = [ parent[part][method], route ];
647 return;
648 case "object":
649 parent[part][method].push(route);
650 return;
651 case "undefined":
652 parent[part][method] = route;
653 return;
654 }
655 } else if (parentType == "undefined") {
656 nested = {};
657 nested[method] = route;
658 parent[part] = nested;
659 return;
660 }
661 throw new Error("Invalid route context: " + parentType);
662};
663
664
665
666Router.prototype.extend = function(methods) {
667 var self = this, len = methods.length, i;
668 function extend(method) {
669 self._methods[method] = true;
670 self[method] = function() {
671 var extra = arguments.length === 1 ? [ method, "" ] : [ method ];
672 self.on.apply(self, extra.concat(Array.prototype.slice.call(arguments)));
673 };
674 }
675 for (i = 0; i < len; i++) {
676 extend(methods[i]);
677 }
678};
679
680Router.prototype.runlist = function(fns) {
681 var runlist = this.every && this.every.before ? [ this.every.before ].concat(_flatten(fns)) : _flatten(fns);
682 if (this.every && this.every.on) {
683 runlist.push(this.every.on);
684 }
685 runlist.captures = fns.captures;
686 runlist.source = fns.source;
687 return runlist;
688};
689
690Router.prototype.mount = function(routes, path) {
691 if (!routes || typeof routes !== "object" || Array.isArray(routes)) {
692 return;
693 }
694 var self = this;
695 path = path || [];
696 if (!Array.isArray(path)) {
697 path = path.split(self.delimiter);
698 }
699 function insertOrMount(route, local) {
700 var rename = route, parts = route.split(self.delimiter), routeType = typeof routes[route], isRoute = parts[0] === "" || !self._methods[parts[0]], event = isRoute ? "on" : rename;
701 if (isRoute) {
702 rename = rename.slice((rename.match(new RegExp("^" + self.delimiter)) || [ "" ])[0].length);
703 parts.shift();
704 }
705 if (isRoute && routeType === "object" && !Array.isArray(routes[route])) {
706 local = local.concat(parts);
707 self.mount(routes[route], local);
708 return;
709 }
710 if (isRoute) {
711 local = local.concat(rename.split(self.delimiter));
712 local = terminator(local, self.delimiter);
713 }
714 self.insert(event, local, routes[route]);
715 }
716 for (var route in routes) {
717 if (routes.hasOwnProperty(route)) {
718 insertOrMount(route, path.slice(0));
719 }
720 }
721};
722
723
724
725}(typeof exports === "object" ? exports : window));
\No newline at end of file