Coverage

98%
184
181
3
98%
184
181
3
LineHitsSource
1/* jshint es3: true */
2/*
3 * Navstack
4 * https://github.com/rstacruz/navstack
5 *
6 * Manages a stack of multiple views.
7 */
8
91(function(root, factory) {
10
111 if (typeof define === 'function' && define.amd) {
120 define(['jquery'], factory); /* AMD */
131 } else if (typeof exports === 'object') {
140 factory(jquery()); /* CommonJS */
15 } else {
161 root.Navstack = factory(jquery()); /* Globals */
17 }
18
19 function jquery() {
201 var $ = (typeof require === 'function' && require('jquery')) || root.jQuery || root.$ || null;
211 if (!$) throw new Error("Navstack: jQuery not found.");
221 return $;
23 }
24
25})(this, function ($) {
26
271 var Navstack, Pane;
28
29 /** Navstack:
30 * A stack.
31 *
32 * nav = new Navstack();
33 */
34
351 Navstack = function (options) {
36 /*** transitions:
37 * Registry of pane transitions.
38 * A local version of `Navstack.transitions`. */
3987 this.transitions = {};
40
41 /*** adaptors:
42 * Registry of suitable adaptors.
43 * A local version of `Navstack.adaptors`. */
4487 this.adaptors = {};
45
4687 $.extend(this, options);
47
48 /***
49 * panes:
50 * Index of panes that have been registered with this Navstack.
51 * Object with pane names as keys and `Pane` instances as values.
52 *
53 * nav.push('home', function () { ... });
54 *
55 * nav.panes['home']
56 * nav.panes['home'].name
57 * nav.panes['home'].el
58 * nav.panes['home'].view
59 */
6087 this.panes = {};
61
62 /*** active:
63 * Alias for the active pane. This is
64 * a `Pane` instance. */
6587 this.active = null;
66
67 /*** stack:
68 * Ordered array of pane names of what are the actively. */
6987 this.stack = [];
70
71 /*** emitter:
72 * (Internal) event emitter. */
7387 this.emitter = $({});
74
75 /*** el:
76 * The DOM element.
77 *
78 * $(nav.el).show()
79 */
8087 this.el = (options && options.el) ? $(options.el) : $('<div>');
81
8287 $(this.el)
83 .attr('data-stack', true)
84 .addClass('-navstack');
85
8687 this.init(options);
87 };
88
891 Navstack.prototype = {
90 /***
91 * init:
92 * Constructor. Override me.
93 *
94 * var MyStack = Navstack.extend({
95 * init: function() {
96 * // initialize here
97 * }
98 * });
99 */
100
101 init: function () {},
102
103 /***
104 * register:
105 * Registers a pane `name` with initializer function `fn`, allowing you to
106 * use `.go()` on the registered pane later.
107 *
108 * This is called on `.push`.
109 */
110
111 register: function (name, fn) {
112127 this.panes[name] = new Pane(name, fn, this);
113 },
114
115 /***
116 * Events:
117 * There's events. Available events are:
118 *
119 * - 'remove'
120 */
121
122 /***
123 * on: .on(event, function)
124 * Binds an event handler.
125 *
126 * nav.on('remove', function() {
127 * // do things
128 * });
129 */
130
131 on: function (event, handler) {
1323 this.emitter.on(event, $.proxy(handler, this));
1333 return this;
134 },
135
136 /***
137 * off: (event, callback)
138 * Removes an event handler.
139 *
140 * nav.off('remove', myfunction);
141 */
142
143 off: function (event, handler) {
1441 this.emitter.off(event, $.proxy(handler, this));
1451 return this;
146 },
147
148 /***
149 * one: (event, callback)
150 * Works like `.on`, except it unbinds itself right after.
151 */
152
153 one: function (event, handler) {
1549 this.emitter.one(event, $.proxy(handler, this));
1559 return this;
156 },
157
158 /***
159 * push: .push(name, [fn])
160 * Registers a pane.
161 *
162 * nav.push('home', function() {
163 * return $("<div>...</div>");
164 * });
165 */
166
167 push: function (name, fn) {
168103 if (!this.panes[name]) {
169103 if (!fn) throw new Error("Navstack: unknown pane '" + name + "'");
170101 this.register(name, fn);
171 }
172
173102 return this.go(name);
174 },
175
176 /***
177 * go: (name)
178 * (Internal) Switches to a given pane `name`.
179 */
180
181 go: function (name) {
182 // Switching to the same thing? No need to do anything
183116 if (this.active && this.active.name === name)
1841 return this.active.view;
185
186115 if (!this.panes[name])
1871 throw new Error("Navstack: unknown pane '"+name+"'");
188
189 // Get the current pane so we can transition later
190114 var previous = this.active;
191
192 // Spawn the pane if it hasn't been spawned before
193114 if (!this.panes[name].el) {
194111 this.spawnPane(name);
195 }
196
197112 var current = this.panes[name];
198
199 // Insert into stack
200112 if (this.stack.indexOf(name) === -1) {
201109 this.purgeObsolete();
202109 this.insertIntoStack(current);
203 }
204
205 // Register a new 'active' pane
206112 this.active = current;
207
208 // Perform the transition
209112 var direction = this.getDirection(previous, current);
210112 var transition = this.getTransition(this.transition);
211111 this.runTransition(transition, direction, current, previous);
212
213 // Event
214111 this.emitter.trigger($.Event('transition', {
215 direction: direction,
216 current: current,
217 previous: previous
218 }));
219
220111 return (current && current.view);
221 },
222
223 /***
224 * transition: Object
225 * Pane transition.
226 */
227
228 transition: {
229 before: function (direction, current, previous, next) {
230168 if (current) $(current.el).hide();
23184 return next();
232 },
233 run: function (direction, current, previous, next) {
234156 if (current) $(current.el).show();
235115 if (previous) $(previous.el).hide();
23678 return next();
237 },
238 after: function (direction, current, previous, next) {
23978 return next();
240 }
241 },
242
243 /***
244 * remove:
245 * Removes and destroys the Navstack.
246 */
247
248 remove: function () {
249 // TODO: destroy each pane
25024 this.emitter.trigger('remove');
25124 $(this.el).remove();
252 },
253
254 /***
255 * teardown:
256 * Alias for `remove` (to make Navstack behave a bit more like Ractive
257 * components).
258 */
259
260 teardown: function () {
2611 return this.remove.apply(this, arguments);
262 },
263
264 /***
265 * getAdaptors:
266 * Returns the adaptors available.
267 */
268
269 getAdaptors: function () {
270115 var adapt = this.adapt || Navstack.adapt;
271115 var nav = this;
272
273115 return map(adapt, function(name) {
274417 var adaptor = (nav.adaptors && nav.adaptors[name]) ||
275 Navstack.adaptors[name];
276
277417 if (!adaptor)
2781 console.warn("Navstack: unknown adaptor '" + name + "'");
279
280417 return adaptor;
281 });
282 },
283
284 /***
285 * getAdaptorFor: .getAdaptorFor(obj)
286 * Wraps the given `obj` object with a suitable adaptor.
287 *
288 * view = new Backbone.View({ ... });
289 * adaptor = nav.getAdaptorFor(view);
290 *
291 * adaptor.el()
292 * adaptor.remove()
293 */
294
295 getAdaptorFor: function (obj) {
296113 var adaptors = this.getAdaptors();
297
298113 for (var i=0; i < adaptors.length; ++i) {
299365 var adaptor = adaptors[i];
300
301365 if (adaptor.filter(obj)) {
302112 var wrapped = adaptor.wrap(obj, this);
303112 wrapped.adaptor = adaptor;
304112 return wrapped;
305 }
306 }
307
3081 throw new Error("Navstack: no adaptor found");
309 },
310
311 /***
312 * purgePane:
313 * (Internal) Purges a given pane.
314 *
315 * this.purgePane('home');
316 * this.purgePane(this.panes['home']);
317 */
318
319 purgePane: function (name, options) {
32062 var pane = typeof name === 'string' ?
321 this.panes[name] : name;
322
323 // if pane doesn't exist: act like it was removed
324112 if (!pane) return;
325
32612 name = pane.name;
327
328 // emit events
32912 this.emitter.trigger('purge', pane);
33012 this.emitter.trigger('purge:'+name, pane);
331
332 // remove from DOM
33312 this.panes[name].adaptor.remove();
33412 delete this.panes[name];
335
336 // remove from stack
33712 var idx = this.stack.indexOf(name);
33824 if (idx > -1) this.stack.splice(idx, 1);
339 },
340
341 /*
342 * (Internal) Purges any panes that are not needed.
343 */
344
345 purgeObsolete: function () {
346168 if (!this.active) return;
347
34850 var idx = this.stack.indexOf(this.active.name);
349
35050 for (var i = this.stack.length; i>idx; i--) {
35154 this.purgePane(this.stack[i]);
352 }
353 },
354
355 /*** getDirection: (from, to)
356 * (Internal) Returns the direction of animation based on the
357 * indices of panes `from` and `to`.
358 *
359 * // Going to a pane
360 * this.getDirection('home', 'timeline')
361 * => 'forward'
362 *
363 * // Going from a pane
364 * this.getDirection('timeline', 'home')
365 * => 'backward'
366 *
367 * // Pane objects are ok too
368 * this.getDirection(this.pane['home'], this.pane['timeline']);
369 */
370
371 getDirection: function (from, to) {
372175 if (!from) return 'first';
373
37455 var idx = {
375 from: this.stack.indexOf((from && from.name) || from),
376 to: this.stack.indexOf((to && to.name) || to)
377 };
378
37955 if (idx.to < idx.from)
3804 return 'backward';
381 else
38251 return 'forward';
383 },
384
385 /*** spawnPane: .spawnPane(name)
386 * (Internal) Spawns the pane of a given `name`.
387 * Returns the pane instance.
388 */
389
390 spawnPane: function (name) {
391 // Get the pane (previously .register()'ed) and initialize it.
392111 var current = this.panes[name];
393111 if (!current) throw new Error("Navstack: Unknown pane: "+name);
394111 current.init();
395
396109 return current;
397 },
398
399 /*** getTransition: .getTransition(transition)
400 * (Internal) get the transition object for the given string `transition`.
401 * Throws an error if it's invalid.
402 */
403
404 getTransition: function (transition) {
405116 if (typeof transition === 'string') {
40631 transition = (this.transitions && this.transitions[transition]) ||
407 Navstack.transitions[transition];
408 }
409
410116 if (typeof transition !== 'object')
4111 throw new Error("Navstack: invalid 'transition' value");
412
413115 return transition;
414 },
415
416 /**
417 * (Internal) performs a transition with the given `transition` object.
418 */
419
420 runTransition: function (transition, direction, current, previous) {
421111 transition.before(direction, current, previous, function () {
422111 Navstack.queue(function (next) {
423101 transition.run(direction, current, previous, function () {
42496 transition.after(direction, current, previous, next);
425 });
426 });
427 });
428 },
429
430 /**
431 * (Internal) updates `this.stack` to include `pane`, taking into
432 * account Z indices.
433 *
434 * pane = this.pane['home'];
435 * this.insertIntoStack(pane);
436 */
437
438 insertIntoStack: function (pane) {
439109 var name = pane.name;
440109 if (this.stack.indexOf(name) > -1) return;
441
442109 this.stack.push(pane.name);
443 }
444
445 };
446
447 /*** extend
448 * Subclasses Navstack to create your new Navstack class.
449 */
450
4511 Navstack.extend = function (proto) {
4524 var klass = function() { Navstack.apply(this, arguments); };
4532 $.extend(klass.prototype, Navstack.prototype, proto);
4542 return klass;
455 };
456
457
458 /**
459 * Pane.
460 *
461 * pane.name
462 * pane.initializer // function
463 * pane.el
464 * pane.view
465 */
466
4671 Pane = Navstack.Pane = function (name, initializer, parent) {
468 /** The identification `name` of this pane, as passed to `register()`. */
469127 this.name = name;
470
471 /** Function to create the view. */
472127 this.initializer = initializer;
473
474 /** Reference to `Navstack`. */
475127 this.parent = parent;
476
477 /** DOM element. Created on `init()`. */
478127 this.el = null;
479
480 /** View instance as created by initializer. Created on `init()`. */
481127 this.view = null;
482
483 /** A wrapped version of the `view` */
484127 this.adaptor = null;
485 };
486
4871 Pane.prototype = {
488 /**
489 * Initializes the pane's view if needed.
490 */
491
492 init: function () {
493222 if (!this.isInitialized()) this.forceInit();
494 },
495
496 /**
497 * Forces initialization even if it hasn't been yet.
498 */
499
500 forceInit: function () {
501111 var fn = this.initializer;
502
503111 if (typeof fn !== 'function')
5041 throw new Error("Navstack: pane initializer is not a function");
505
506 // Let the initializer create the element, just use it afterwards.
507110 this.view = this.initializer.call(this.parent);
508110 this.adaptor = this.parent.getAdaptorFor(this.view);
509110 this.el = this.adaptor.el();
510
511110 if (!this.el)
5121 throw new Error("Navstack: no element found");
513
514109 $(this.el)
515 .attr('data-stack-pane', this.name)
516 .addClass('-navstack-pane')
517 .appendTo(this.parent.el);
518 },
519
520 isInitialized: function () {
521111 return !! this.el;
522 }
523 };
524
5251 var filterSupported = (function () {
5261 var e = document.createElement("div");
5271 e.style.webkitFilter = "grayscale(1)";
5281 return (window.getComputedStyle(e).webkitFilter === "grayscale(1)");
529 })();
530
531 /**
532 * For transitions
533 */
534
5351 Navstack.buildTransition = function (prefix) {
5363 return {
537 before: function (direction, current, previous, next) {
538
53926 $(current && current.el)
540 .add(previous && previous.el)
541 .toggleClass('-navstack-with-filter', filterSupported)
542 .toggleClass('-navstack-no-filter', !filterSupported);
543
54426 if (direction !== 'first' && current)
54513 $(current.el).addClass(prefix+'-hide');
546
547 // Do transitions on next browser tick so that any DOM elements that
548 // need rendering will take its time
54926 return setTimeout(next, 0);
550 },
551
552 after: function (direction, current, previous, next) {
55317 $(current && current.el)
554 .add(previous && previous.el)
555 .removeClass('-navstack-with-filter -navstack-no-filter');
556
55717 return next();
558 },
559
560 run: function (direction, current, previous, next) {
56135 if (direction === 'first') return next();
562
5639 var $parent =
564 current ? $(current.el).parent() :
565 previous ? $(previous.el).parent() : null;
566
5679 $parent.addClass(prefix+'-container');
568
5699 if (previous)
5709 $(previous.el)
571 .removeClass(prefix+'-hide')
572 .addClass(prefix+'-exit-'+direction);
573
5749 $(current.el)
575 .removeClass(prefix+'-hide')
576 .addClass(prefix+'-enter-'+direction)
577 .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() {
5784 $parent.removeClass(prefix+'-container');
579
5804 if (previous)
5814 $(previous.el)
582 .addClass(prefix+'-hide')
583 .removeClass(prefix+'-exit-'+direction);
584
5854 $(current.el)
586 .removeClass(prefix+'-enter-'+direction);
587
5884 next();
589 });
590 }
591 };
592 };
593
594 /**
595 * Transitions
596 */
597
5981 Navstack.transitions = {
599 slide: Navstack.buildTransition('slide'),
600 modal: Navstack.buildTransition('modal'),
601 flip: Navstack.buildTransition('flip')
602 };
603
604 /**
605 * Adaptors
606 */
607
6081 Navstack.adaptors = {};
609
610 /**
611 * (Internal) Helper for building a generic filter
612 */
613
614 function buildAdaptor (options) {
6154 return {
616 filter: function (obj) {
617363 return typeof obj === 'object' && options.check(obj);
618 },
619
620 wrap: function (obj, nav) {
621110 return {
622110 el: function () { return options.el(obj); },
62316 remove: function () { return options.remove(obj); }
624 };
625 }
626 };
627 }
628
629 /*
630 * Backbone adaptor
631 */
632
6331 Navstack.adaptors.backbone = buildAdaptor({
63418 el: function (obj) { return obj.el; },
635 check: function (obj) {
636101 return (typeof obj.remove === 'function') && (typeof obj.el === 'object');
637 },
6381 remove: function (obj) { return obj.remove(); }
639 });
640
641 /*
642 * Ractive adaptor
643 */
644
6451 Navstack.adaptors.ractive = buildAdaptor({
6463 el: function (obj) { return obj.find('*'); },
647 check: function (obj) {
64886 return (typeof obj.teardown === 'function') && (typeof obj.el === 'object');
649 },
6501 remove: function (obj) { return obj.teardown(); }
651 });
652
653 /*
654 * React.js adaptor
655 */
656
6571 Navstack.adaptors.react = buildAdaptor({
6583 el: function (obj) { return obj.getDOMNode(); },
65986 check: function (obj) { return (typeof obj.getDOMNode === 'function'); },
6601 remove: function (obj) { return window.React.unmountComponentAtNode(obj.getDOMNode()); }
661 });
662
663
664 /*
665 * Generic adaptor. Accounts for any object that gives off an `el` property.
666 */
667
6681 Navstack.adaptors.jquery = buildAdaptor({
66986 el: function (obj) { return $(obj); },
67086 check: function (obj) { return $(obj)[0].nodeType === 1; },
67113 remove: function (obj) { return $(obj).remove(); }
672 });
673
6741 Navstack.adapt = ['backbone', 'ractive', 'react', 'jquery'];
675
676 /*
677 * Helpers
678 */
679
680 function map (obj, fn) {
681230 if (obj.map) return obj.map(fn);
6820 else throw new Error("Todo: implement map shim");
683 }
684
685 /**
686 * (Internal) Queues animations.
687 */
688
6891 Navstack.queue = function (fn) {
69089 $(document).queue(fn);
691 };
692
6931 Navstack.version = '0.1.1';
694
6951 return Navstack;
696
697});
698