# Coverage

98%
184
181
3

## navstack.js

98%
184
181
3

Line | Hits | Source
--- | --- | ---
1 |  | /* jshint es3: true */
2 |  | /*
3 |  | * Navstack
4 |  | * https://github.com/rstacruz/navstack
5 |  | *
6 |  | * Manages a stack of multiple views.
7 |  | */
8 |  |
9 | 1 | (function(root, factory) {
10 |  |
11 | 1 | if (typeof define === 'function' && define.amd) {
12 | 0 | define(['jquery'], factory); /* AMD */
13 | 1 | } else if (typeof exports === 'object') {
14 | 0 | factory(jquery()); /* CommonJS */
15 |  | } else {
16 | 1 | root.Navstack = factory(jquery()); /* Globals */
17 |  | }
18 |  |
19 |  | function jquery() {
20 | 1 | var $ = (typeof require === 'function' && require('jquery')) || root.jQuery || root.$ || null;
21 | 1 | if (!$) throw new Error("Navstack: jQuery not found.");
22 | 1 | return $;
23 |  | }
24 |  |
25 |  | })(this, function ($) {
26 |  |
27 | 1 | var Navstack, Pane;
28 |  |
29 |  | /** Navstack:
30 |  | * A stack.
31 |  | *
32 |  | * nav = new Navstack();
33 |  | */
34 |  |
35 | 1 | Navstack = function (options) {
36 |  | /*** transitions:
37 |  | * Registry of pane transitions.
38 |  | * A local version of `Navstack.transitions`. */
39 | 87 | this.transitions = {};
40 |  |
41 |  | /*** adaptors:
42 |  | * Registry of suitable adaptors.
43 |  | * A local version of `Navstack.adaptors`. */
44 | 87 | this.adaptors = {};
45 |  |
46 | 87 | $.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 |  | */
60 | 87 | this.panes = {};
61 |  |
62 |  | /*** active:
63 |  | * Alias for the active pane. This is
64 |  | * a `Pane` instance. */
65 | 87 | this.active = null;
66 |  |
67 |  | /*** stack:
68 |  | * Ordered array of pane names of what are the actively. */
69 | 87 | this.stack = [];
70 |  |
71 |  | /*** emitter:
72 |  | * (Internal) event emitter. */
73 | 87 | this.emitter = $({});
74 |  |
75 |  | /*** el:
76 |  | * The DOM element.
77 |  | *
78 |  | * $(nav.el).show()
79 |  | */
80 | 87 | this.el = (options && options.el) ? $(options.el) : $('<div>');
81 |  |
82 | 87 | $(this.el)
83 |  | .attr('data-stack', true)
84 |  | .addClass('-navstack');
85 |  |
86 | 87 | this.init(options);
87 |  | };
88 |  |
89 | 1 | 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) {
112 | 127 | 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) {
132 | 3 | this.emitter.on(event, $.proxy(handler, this));
133 | 3 | 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) {
144 | 1 | this.emitter.off(event, $.proxy(handler, this));
145 | 1 | 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) {
154 | 9 | this.emitter.one(event, $.proxy(handler, this));
155 | 9 | 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) {
168 | 103 | if (!this.panes[name]) {
169 | 103 | if (!fn) throw new Error("Navstack: unknown pane '" + name + "'");
170 | 101 | this.register(name, fn);
171 |  | }
172 |  |
173 | 102 | 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
183 | 116 | if (this.active && this.active.name === name)
184 | 1 | return this.active.view;
185 |  |
186 | 115 | if (!this.panes[name])
187 | 1 | throw new Error("Navstack: unknown pane '"+name+"'");
188 |  |
189 |  | // Get the current pane so we can transition later
190 | 114 | var previous = this.active;
191 |  |
192 |  | // Spawn the pane if it hasn't been spawned before
193 | 114 | if (!this.panes[name].el) {
194 | 111 | this.spawnPane(name);
195 |  | }
196 |  |
197 | 112 | var current = this.panes[name];
198 |  |
199 |  | // Insert into stack
200 | 112 | if (this.stack.indexOf(name) === -1) {
201 | 109 | this.purgeObsolete();
202 | 109 | this.insertIntoStack(current);
203 |  | }
204 |  |
205 |  | // Register a new 'active' pane
206 | 112 | this.active = current;
207 |  |
208 |  | // Perform the transition
209 | 112 | var direction = this.getDirection(previous, current);
210 | 112 | var transition = this.getTransition(this.transition);
211 | 111 | this.runTransition(transition, direction, current, previous);
212 |  |
213 |  | // Event
214 | 111 | this.emitter.trigger($.Event('transition', {
215 |  | direction: direction,
216 |  | current: current,
217 |  | previous: previous
218 |  | }));
219 |  |
220 | 111 | return (current && current.view);
221 |  | },
222 |  |
223 |  | /***
224 |  | * transition: Object
225 |  | * Pane transition.
226 |  | */
227 |  |
228 |  | transition: {
229 |  | before: function (direction, current, previous, next) {
230 | 168 | if (current) $(current.el).hide();
231 | 84 | return next();
232 |  | },
233 |  | run: function (direction, current, previous, next) {
234 | 156 | if (current) $(current.el).show();
235 | 115 | if (previous) $(previous.el).hide();
236 | 78 | return next();
237 |  | },
238 |  | after: function (direction, current, previous, next) {
239 | 78 | return next();
240 |  | }
241 |  | },
242 |  |
243 |  | /***
244 |  | * remove:
245 |  | * Removes and destroys the Navstack.
246 |  | */
247 |  |
248 |  | remove: function () {
249 |  | // TODO: destroy each pane
250 | 24 | this.emitter.trigger('remove');
251 | 24 | $(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 () {
261 | 1 | return this.remove.apply(this, arguments);
262 |  | },
263 |  |
264 |  | /***
265 |  | * getAdaptors:
266 |  | * Returns the adaptors available.
267 |  | */
268 |  |
269 |  | getAdaptors: function () {
270 | 115 | var adapt = this.adapt || Navstack.adapt;
271 | 115 | var nav = this;
272 |  |
273 | 115 | return map(adapt, function(name) {
274 | 417 | var adaptor = (nav.adaptors && nav.adaptors[name]) ||
275 |  | Navstack.adaptors[name];
276 |  |
277 | 417 | if (!adaptor)
278 | 1 | console.warn("Navstack: unknown adaptor '" + name + "'");
279 |  |
280 | 417 | 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) {
296 | 113 | var adaptors = this.getAdaptors();
297 |  |
298 | 113 | for (var i=0; i < adaptors.length; ++i) {
299 | 365 | var adaptor = adaptors[i];
300 |  |
301 | 365 | if (adaptor.filter(obj)) {
302 | 112 | var wrapped = adaptor.wrap(obj, this);
303 | 112 | wrapped.adaptor = adaptor;
304 | 112 | return wrapped;
305 |  | }
306 |  | }
307 |  |
308 | 1 | 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) {
320 | 62 | var pane = typeof name === 'string' ?
321 |  | this.panes[name] : name;
322 |  |
323 |  | // if pane doesn't exist: act like it was removed
324 | 112 | if (!pane) return;
325 |  |
326 | 12 | name = pane.name;
327 |  |
328 |  | // emit events
329 | 12 | this.emitter.trigger('purge', pane);
330 | 12 | this.emitter.trigger('purge:'+name, pane);
331 |  |
332 |  | // remove from DOM
333 | 12 | this.panes[name].adaptor.remove();
334 | 12 | delete this.panes[name];
335 |  |
336 |  | // remove from stack
337 | 12 | var idx = this.stack.indexOf(name);
338 | 24 | 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 () {
346 | 168 | if (!this.active) return;
347 |  |
348 | 50 | var idx = this.stack.indexOf(this.active.name);
349 |  |
350 | 50 | for (var i = this.stack.length; i>idx; i--) {
351 | 54 | 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) {
372 | 175 | if (!from) return 'first';
373 |  |
374 | 55 | var idx = {
375 |  | from: this.stack.indexOf((from && from.name) || from),
376 |  | to: this.stack.indexOf((to && to.name) || to)
377 |  | };
378 |  |
379 | 55 | if (idx.to < idx.from)
380 | 4 | return 'backward';
381 |  | else
382 | 51 | 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.
392 | 111 | var current = this.panes[name];
393 | 111 | if (!current) throw new Error("Navstack: Unknown pane: "+name);
394 | 111 | current.init();
395 |  |
396 | 109 | 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) {
405 | 116 | if (typeof transition === 'string') {
406 | 31 | transition = (this.transitions && this.transitions[transition]) ||
407 |  | Navstack.transitions[transition];
408 |  | }
409 |  |
410 | 116 | if (typeof transition !== 'object')
411 | 1 | throw new Error("Navstack: invalid 'transition' value");
412 |  |
413 | 115 | return transition;
414 |  | },
415 |  |
416 |  | /**
417 |  | * (Internal) performs a transition with the given `transition` object.
418 |  | */
419 |  |
420 |  | runTransition: function (transition, direction, current, previous) {
421 | 111 | transition.before(direction, current, previous, function () {
422 | 111 | Navstack.queue(function (next) {
423 | 101 | transition.run(direction, current, previous, function () {
424 | 96 | 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) {
439 | 109 | var name = pane.name;
440 | 109 | if (this.stack.indexOf(name) > -1) return;
441 |  |
442 | 109 | this.stack.push(pane.name);
443 |  | }
444 |  |
445 |  | };
446 |  |
447 |  | /*** extend
448 |  | * Subclasses Navstack to create your new Navstack class.
449 |  | */
450 |  |
451 | 1 | Navstack.extend = function (proto) {
452 | 4 | var klass = function() { Navstack.apply(this, arguments); };
453 | 2 | $.extend(klass.prototype, Navstack.prototype, proto);
454 | 2 | return klass;
455 |  | };
456 |  |
457 |  |
458 |  | /**
459 |  | * Pane.
460 |  | *
461 |  | * pane.name
462 |  | * pane.initializer // function
463 |  | * pane.el
464 |  | * pane.view
465 |  | */
466 |  |
467 | 1 | Pane = Navstack.Pane = function (name, initializer, parent) {
468 |  | /** The identification `name` of this pane, as passed to `register()`. */
469 | 127 | this.name = name;
470 |  |
471 |  | /** Function to create the view. */
472 | 127 | this.initializer = initializer;
473 |  |
474 |  | /** Reference to `Navstack`. */
475 | 127 | this.parent = parent;
476 |  |
477 |  | /** DOM element. Created on `init()`. */
478 | 127 | this.el = null;
479 |  |
480 |  | /** View instance as created by initializer. Created on `init()`. */
481 | 127 | this.view = null;
482 |  |
483 |  | /** A wrapped version of the `view` */
484 | 127 | this.adaptor = null;
485 |  | };
486 |  |
487 | 1 | Pane.prototype = {
488 |  | /**
489 |  | * Initializes the pane's view if needed.
490 |  | */
491 |  |
492 |  | init: function () {
493 | 222 | if (!this.isInitialized()) this.forceInit();
494 |  | },
495 |  |
496 |  | /**
497 |  | * Forces initialization even if it hasn't been yet.
498 |  | */
499 |  |
500 |  | forceInit: function () {
501 | 111 | var fn = this.initializer;
502 |  |
503 | 111 | if (typeof fn !== 'function')
504 | 1 | throw new Error("Navstack: pane initializer is not a function");
505 |  |
506 |  | // Let the initializer create the element, just use it afterwards.
507 | 110 | this.view = this.initializer.call(this.parent);
508 | 110 | this.adaptor = this.parent.getAdaptorFor(this.view);
509 | 110 | this.el = this.adaptor.el();
510 |  |
511 | 110 | if (!this.el)
512 | 1 | throw new Error("Navstack: no element found");
513 |  |
514 | 109 | $(this.el)
515 |  | .attr('data-stack-pane', this.name)
516 |  | .addClass('-navstack-pane')
517 |  | .appendTo(this.parent.el);
518 |  | },
519 |  |
520 |  | isInitialized: function () {
521 | 111 | return !! this.el;
522 |  | }
523 |  | };
524 |  |
525 | 1 | var filterSupported = (function () {
526 | 1 | var e = document.createElement("div");
527 | 1 | e.style.webkitFilter = "grayscale(1)";
528 | 1 | return (window.getComputedStyle(e).webkitFilter === "grayscale(1)");
529 |  | })();
530 |  |
531 |  | /**
532 |  | * For transitions
533 |  | */
534 |  |
535 | 1 | Navstack.buildTransition = function (prefix) {
536 | 3 | return {
537 |  | before: function (direction, current, previous, next) {
538 |  |
539 | 26 | $(current && current.el)
540 |  | .add(previous && previous.el)
541 |  | .toggleClass('-navstack-with-filter', filterSupported)
542 |  | .toggleClass('-navstack-no-filter', !filterSupported);
543 |  |
544 | 26 | if (direction !== 'first' && current)
545 | 13 | $(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
549 | 26 | return setTimeout(next, 0);
550 |  | },
551 |  |
552 |  | after: function (direction, current, previous, next) {
553 | 17 | $(current && current.el)
554 |  | .add(previous && previous.el)
555 |  | .removeClass('-navstack-with-filter -navstack-no-filter');
556 |  |
557 | 17 | return next();
558 |  | },
559 |  |
560 |  | run: function (direction, current, previous, next) {
561 | 35 | if (direction === 'first') return next();
562 |  |
563 | 9 | var $parent =
564 |  | current ? $(current.el).parent() :
565 |  | previous ? $(previous.el).parent() : null;
566 |  |
567 | 9 | $parent.addClass(prefix+'-container');
568 |  |
569 | 9 | if (previous)
570 | 9 | $(previous.el)
571 |  | .removeClass(prefix+'-hide')
572 |  | .addClass(prefix+'-exit-'+direction);
573 |  |
574 | 9 | $(current.el)
575 |  | .removeClass(prefix+'-hide')
576 |  | .addClass(prefix+'-enter-'+direction)
577 |  | .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() {
578 | 4 | $parent.removeClass(prefix+'-container');
579 |  |
580 | 4 | if (previous)
581 | 4 | $(previous.el)
582 |  | .addClass(prefix+'-hide')
583 |  | .removeClass(prefix+'-exit-'+direction);
584 |  |
585 | 4 | $(current.el)
586 |  | .removeClass(prefix+'-enter-'+direction);
587 |  |
588 | 4 | next();
589 |  | });
590 |  | }
591 |  | };
592 |  | };
593 |  |
594 |  | /**
595 |  | * Transitions
596 |  | */
597 |  |
598 | 1 | Navstack.transitions = {
599 |  | slide: Navstack.buildTransition('slide'),
600 |  | modal: Navstack.buildTransition('modal'),
601 |  | flip: Navstack.buildTransition('flip')
602 |  | };
603 |  |
604 |  | /**
605 |  | * Adaptors
606 |  | */
607 |  |
608 | 1 | Navstack.adaptors = {};
609 |  |
610 |  | /**
611 |  | * (Internal) Helper for building a generic filter
612 |  | */
613 |  |
614 |  | function buildAdaptor (options) {
615 | 4 | return {
616 |  | filter: function (obj) {
617 | 363 | return typeof obj === 'object' && options.check(obj);
618 |  | },
619 |  |
620 |  | wrap: function (obj, nav) {
621 | 110 | return {
622 | 110 | el: function () { return options.el(obj); },
623 | 16 | remove: function () { return options.remove(obj); }
624 |  | };
625 |  | }
626 |  | };
627 |  | }
628 |  |
629 |  | /*
630 |  | * Backbone adaptor
631 |  | */
632 |  |
633 | 1 | Navstack.adaptors.backbone = buildAdaptor({
634 | 18 | el: function (obj) { return obj.el; },
635 |  | check: function (obj) {
636 | 101 | return (typeof obj.remove === 'function') && (typeof obj.el === 'object');
637 |  | },
638 | 1 | remove: function (obj) { return obj.remove(); }
639 |  | });
640 |  |
641 |  | /*
642 |  | * Ractive adaptor
643 |  | */
644 |  |
645 | 1 | Navstack.adaptors.ractive = buildAdaptor({
646 | 3 | el: function (obj) { return obj.find('*'); },
647 |  | check: function (obj) {
648 | 86 | return (typeof obj.teardown === 'function') && (typeof obj.el === 'object');
649 |  | },
650 | 1 | remove: function (obj) { return obj.teardown(); }
651 |  | });
652 |  |
653 |  | /*
654 |  | * React.js adaptor
655 |  | */
656 |  |
657 | 1 | Navstack.adaptors.react = buildAdaptor({
658 | 3 | el: function (obj) { return obj.getDOMNode(); },
659 | 86 | check: function (obj) { return (typeof obj.getDOMNode === 'function'); },
660 | 1 | 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 |  |
668 | 1 | Navstack.adaptors.jquery = buildAdaptor({
669 | 86 | el: function (obj) { return $(obj); },
670 | 86 | check: function (obj) { return $(obj)[0].nodeType === 1; },
671 | 13 | remove: function (obj) { return $(obj).remove(); }
672 |  | });
673 |  |
674 | 1 | Navstack.adapt = ['backbone', 'ractive', 'react', 'jquery'];
675 |  |
676 |  | /*
677 |  | * Helpers
678 |  | */
679 |  |
680 |  | function map (obj, fn) {
681 | 230 | if (obj.map) return obj.map(fn);
682 | 0 | else throw new Error("Todo: implement map shim");
683 |  | }
684 |  |
685 |  | /**
686 |  | * (Internal) Queues animations.
687 |  | */
688 |  |
689 | 1 | Navstack.queue = function (fn) {
690 | 89 | $(document).queue(fn);
691 |  | };
692 |  |
693 | 1 | Navstack.version = '0.1.1';
694 |  |
695 | 1 | return Navstack;
696 |  |
697 |  | });
698 |  |