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 | | |