├── LICENSE ├── NOTICE ├── README.md ├── backstack-min.js ├── backstack.js ├── build ├── almond.js ├── build.js ├── build.sh ├── r.js ├── wrap-end.frag └── wrap-start.frag └── src ├── StackNavigator.js └── effects ├── Effect.js ├── FadeEffect.js ├── NoEffect.js ├── SlideEffect.js └── vendorPrefix.js /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Piotr Walczyszyn (http://outof.me | @pwalczyszyn) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

What is BackStack?

3 |

4 | BackStack is a JavaScript component/extension for Backbone.js that allows you to create nice view 5 | transitions in your HTML5 apps. By default it comes with an implementation of mobile-style slide transitions, fade 6 | transitions, and no-effect transitions. 7 |

8 |

9 | It is conceptually based on the ViewNavigator API from the mobile SDK of the Apache Flex framework. It enables developers to manage a stack of views that can be pushed, popped, or replaced. 10 |

11 | 12 |

Why should I use it?

13 |

Although you can use it for web development (as I did to create this site) it is especially useful when building mobile apps with PhoneGap/Cordova framework. 14 | Of course, if you are using one of the dozen or so available mobile UI frameworks like jQuery Mobile, 15 | Sencha Touch, or jQTouch you don't need it. 16 |

17 | 18 |

Are there other alternatives?

19 |

You can use the ViewNavigator implementation from the app-UI framework built by fellow Adobe Evangelist 20 | Andy Trice. Alternatively, you can build the transitions yourself; it is not really hard! Or, use one of the frameworks mentioned above. 21 |

22 | 23 |

How was it built?

24 |

This may not be particularly interesting to everyone but I used the almond library to develop/package BackStack. Almond is a replacement AMD loader for RequireJS. It is a smaller "shim" loader, providing a minimal AMD API footprint that includes loader plugin support.

25 |
26 | 27 | ### Where can I find more info about it? 28 | Here is a demo site that is actually built with BackStack. Be aware of a disclaimer that I only tested it with WebKit based browsers as I mainly used it for mobile apps development. So if you are on IE this site many not work for you, sorry for that ;) 29 | 30 | You can also checkout this simple example that is available on jsFiddle. 31 | 32 | Watch an intro on YouTube with step-by-step instructions how to get started: http://www.youtube.com/watch?v=sm5DI-iYark 33 | 34 | ### BackStack.StackNavigator class API 35 |
36 |

Constructor

37 | 38 | 39 | 47 | 48 | 49 |

Events

50 |

51 | BackStack.StackNavigator eventing model is based on Backbone's events implementation. 52 |

83 |

84 | 85 |

Fields

86 | 92 | 93 |

Functions

94 | 136 |
-------------------------------------------------------------------------------- /backstack-min.js: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2012 Piotr Walczyszyn (http://outof.me | @pwalczyszyn) 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | ////////////////////////////////////////////////////////////////////////////////////// 18 | 19 | /** 20 | * almond 0.1.1 Copyright (c) 2011, The Dojo Foundation All Rights Reserved. 21 | * Available via the MIT or new BSD license. 22 | * see: http://github.com/jrburke/almond for details 23 | */ 24 | 25 | (function(a,b){typeof define=="function"&&define.amd?define(["jquery","underscore","Backbone"],b):a.BackStack=b(a.jQuery||a.Zepto||a.ender,a._,a.Backbone)})(this,function(a,b,c){var d,e,f;return function(a){function l(a,b){var c=b&&b.split("/"),d=g.map,e=d&&d["*"]||{},f,h,i,j,k,l,m;if(a&&a.charAt(0)==="."&&b){c=c.slice(0,c.length-1),a=c.concat(a.split("/"));for(k=0;m=a[k];k++)if(m===".")a.splice(k,1),k-=1;else if(m===".."){if(k===1&&(a[2]===".."||a[0]===".."))return!0;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}if((c||e)&&d){f=a.split("/");for(k=f.length;k>0;k-=1){h=f.slice(0,k).join("/");if(c)for(l=c.length;l>0;l-=1){i=d[c.slice(0,l).join("/")];if(i){i=i[h];if(i){j=i;break}}}j=j||e[h];if(j){f.splice(0,k,j),a=f.join("/");break}}}return a}function m(b,c){return function(){return k.apply(a,i.call(arguments,0).concat([b,c]))}}function n(a){return function(b){return l(b,a)}}function o(a){return function(c){b[a]=c}}function p(d){if(c.hasOwnProperty(d)){var e=c[d];delete c[d],h[d]=!0,j.apply(a,e)}if(!b.hasOwnProperty(d))throw new Error("No "+d);return b[d]}function q(a,b){var c,d,e=a.indexOf("!");return e!==-1?(c=l(a.slice(0,e),b),a=a.slice(e+1),d=p(c),d&&d.normalize?a=d.normalize(a,n(b)):a=l(a,b)):a=l(a,b),{f:c?c+"!"+a:a,n:a,p:d}}function r(a){return function(){return g&&g.config&&g.config[a]||{}}}var b={},c={},g={},h={},i=[].slice,j,k;j=function(d,e,f,g){var i=[],j,k,l,n,s,t;g=g||d;if(typeof f=="function"){e=!e.length&&f.length?["require","exports","module"]:e;for(t=0;t=0){h--;var g=a(b.target);g.css(j,""),g.css(k,""),c&&c[0]==b.target&&c.css("left",0),h==0&&d&&(f&&clearTimeout(f),d.call(e))}};b&&(h++,b.one(g.transitionEndEvent,l),b.css("left",0),b.css(k,[j," ",g.fromViewTransitionProps.duration,"s ",g.fromViewTransitionProps.easing," ",g.fromViewTransitionProps.delay,"s"].join(""))),c&&(h++,c.one(g.transitionEndEvent,l),c.css("left",g.direction=="left"?e.$el.width():-e.$el.width()),c.css(k,[j," ",g.toViewTransitionProps.duration,"s ",g.toViewTransitionProps.easing," ",g.toViewTransitionProps.delay,"s"].join("")),c.css("visibility","visible"));if(b||c)e.$el.css("width"),i="translate3d("+(g.direction=="left"?-e.$el.width():e.$el.width())+"px, 0, 0)";var m=Math.max(g.fromViewTransitionProps.duration,g.toViewTransitionProps.duration)+Math.max(g.fromViewTransitionProps.delay,g.toViewTransitionProps.delay);f=setTimeout(function(){h>0&&(h=-1,console.log("Warning "+g.transitionEndEvent+" didn't trigger in expected time!"),c&&(c.off(g.transitionEndEvent,l),c.css(k,""),c.css(j,""),c.css("left",0)),b&&(b.off(g.transitionEndEvent,l),b.css(k,""),b.css(j,"")),d.call(e))},m*1.5*1e3);var n;b&&c?n=b.add(c):c?n=c:b&&(n=b),n&&n.css(j,i)}});return c}),f("StackNavigator",["effects/SlideEffect"],function(a){function d(a,b){a.__backStackRendered__?a.$el.css({visibility:"hidden"}):(a.stackNavigator=b,typeof a.destructionPolicy=="undefined"&&(a.destructionPolicy="auto"),a.$el.css({position:"absolute",visibility:"hidden",overflow:"hidden",width:"100%",height:"100%"})),b.$el.append(a.el),a.__backStackRendered__||(a.render.call(a),a.__backStackRendered__=!0)}function e(a,c,d){return b.extend({type:a,cancelable:b.isUndefined(d)?!1:d,preventDefault:function(){this.cancelable&&(this.isDefaultPrevented=function(){return!0})},isDefaultPrevented:function(){return!1},trigger:function(a){return a.trigger(this.type,this),this}},c)}function f(c,f,g,h){d(f.instance,this),h=h||this.defaultPushTransition||(this.defaultPushTransition=new a({direction:"left"})),h.play(c?c.instance.$el:null,f.instance.$el,function(){var a=g>0?this.viewsStack.splice(this.viewsStack.length-g,g):c?[c]:null;a!=null&&b.each(a,function(a){e("viewDeactivate",{target:a.instance}).trigger(a.instance),a.instance.destructionPolicy=="never"?a.instance.$el.detach():(a.instance.remove(),a.instance=null)},this),this.viewsStack.push(f),this.activeView=f.instance,e("viewActivate",{target:f.instance}).trigger(f.instance),e("viewChanged",{target:this}).trigger(this),m.call(this)},this)}function g(c,f,g,h){f&&(f.instance=f.instance?f.instance:new f.viewClass(f.options),d(f.instance,this)),h=h||this.defaultPopTransition||(this.defaultPopTransition=new a({direction:"right"})),h.play(c.instance.$el,f?f.instance.$el:null,function(){var a=this.viewsStack.splice(this.viewsStack.length-g,g);b.each(a,function(a){e("viewDeactivate",{target:a.instance}).trigger(a.instance),a.instance.destructionPolicy=="never"?a.instance.$el.detach():(a.instance.remove(),a.instance=null)},this),f?(this.activeView=f.instance,e("viewActivate",{target:f.instance}).trigger(f.instance)):this.activeView=null,e("viewChanged",{target:this}).trigger(this),m.call(this)},this)}function h(a,c,d){var g=b.last(this.viewsStack),h=b.isFunction(a)?new a(c):a,i={instance:h,viewClass:h.constructor,options:c},j=e("viewChanging",{action:"push",fromViewClass:g?g.viewClass:null,fromView:g?g.instance:null,toViewClass:i.viewClass,toView:i.instance},!0).trigger(this);if(j.isDefaultPrevented())return null;f.call(this,g,i,0,d)}function i(a){if(this.viewsStack.length==0)throw new Error("Popping from an empty stack!");var c=b.last(this.viewsStack),d=this.viewsStack.length>1?this.viewsStack[this.viewsStack.length-2]:null,f=e("viewChanging",{action:"pop",fromViewClass:c.viewClass,fromView:c.instance,toViewClass:d?d.viewClass:null,toView:d?d.instance:null},!0).trigger(this);if(f.isDefaultPrevented())return;g.call(this,c,d,1,a)}function j(a){if(this.viewsStack.length==0)throw new Error("Popping from an empty stack!");var c=b.last(this.viewsStack),d=e("viewChanging",{action:"popAll",fromViewClass:c.viewClass,fromView:c.instance,toViewClass:null,toView:null},!0).trigger(this);if(d.isDefaultPrevented())return;g.call(this,c,null,this.viewsStack.length,a)}function k(a,c,d){if(this.viewsStack.length==0)throw new Error("Replacing on an empty stack!");var g=b.last(this.viewsStack),h=b.isFunction(a)?new a(c):a,i={instance:h,viewClass:h.constructor,options:c},j=e("viewChanging",{action:"replace",fromViewClass:g.viewClass,fromView:g.instance,toViewClass:i.viewClass,toView:i.instance},!0).trigger(this);if(j.isDefaultPrevented())return null;f.call(this,g,i,1,d)}function l(a,c,d){if(this.viewsStack.length==0)throw new Error("Replacing on an empty stack!");var g=b.last(this.viewsStack),h=b.isFunction(a)?new a(c):a,i={instance:h,viewClass:h.constructor,options:c},j=e("viewChanging",{action:"replaceAll",fromViewClass:g.viewClass,fromView:g.instance,toViewClass:i.viewClass,toView:i.instance},!0).trigger(this);if(j.isDefaultPrevented())return null;f.call(this,g,i,this.viewsStack.length,d)}function m(){this.actionsQueue.splice(0,1);if(this.actionsQueue.length>0){var a=this.actionsQueue[0],b=Array.prototype.slice.call(a.args);switch(a.fn){case"pushView":h.apply(this,b);break;case"popView":i.apply(this,b);break;case"popAll":j.apply(this,b);break;case"replaceView":k.apply(this,b);break;case"replaceAll":l.apply(this,b)}}}var n=c.View.extend({viewsStack:null,activeView:null,defaultPushTransition:null,defaultPopTransition:null,actionsQueue:null,initialize:function(a){this.$el.css({overflow:"hidden"}),this.viewsStack=[],this.actionsQueue=[],a.popTransition&&(this.defaultPopTransition=a.popTransition),a.pushTransition&&(this.defaultPushTransition=a.pushTransition)},pushView:function(a,b,c){this.actionsQueue.push({fn:"pushView",args:arguments}),this.actionsQueue.length==1&&h.call(this,a,b,c)},popView:function(a){this.actionsQueue.push({fn:"popView",args:arguments}),this.actionsQueue.length==1&&i.call(this,a)},popAll:function(a){this.actionsQueue.push({fn:"popAll",args:arguments}),this.actionsQueue.length==1&&j.call(this,a)},replaceView:function(a,b,c){this.actionsQueue.push({fn:"replaceView",args:arguments}),this.actionsQueue.length==1&&k.call(this,a,b,c)},replaceAll:function(a,b,c){this.actionsQueue.push({fn:"replaceAll",args:arguments}),this.actionsQueue.length==1&&l.call(this,a,b,c)}});return n}),f("effects/FadeEffect",["effects/Effect"],function(b){var c=b.extend({fromViewTransitionProps:{duration:.4,easing:"linear",delay:.1},toViewTransitionProps:{duration:.4,easing:"linear",delay:.1},play:function(b,c,d,e){var f=this,g,h=0,i=f.vendorPrefix==""?"transition":["-"+f.vendorPrefix.toLowerCase(),"-","transition"].join(""),j=function(b){h>=0&&(h--,a(b.target).css(i,""),h==0&&d&&(g&&clearTimeout(g),d.call(e)))};b&&(h++,b.one(f.transitionEndEvent,j),b.css(i,["opacity ",f.fromViewTransitionProps.duration,"s ",f.fromViewTransitionProps.easing," ",f.fromViewTransitionProps.delay,"s"].join(""))),c&&(h++,c.one(f.transitionEndEvent,j),c.css("opacity",0),c.css(i,["opacity ",f.toViewTransitionProps.duration,"s ",f.toViewTransitionProps.easing," ",f.toViewTransitionProps.delay,"s"].join("")),c.css("visibility","visible")),e.$el.css("width");var k=Math.max(f.fromViewTransitionProps.duration,f.toViewTransitionProps.duration)+Math.max(f.fromViewTransitionProps.delay,f.toViewTransitionProps.delay);g=setTimeout(function(){h>0&&(h=-1,console.log("Warning "+f.transitionEndEvent+" didn't trigger in expected time!"),c&&(c.off(f.transitionEndEvent,j),c.css(i,"")),b&&(b.off(f.transitionEndEvent,j),b.css(i,"")),d.call(e))},k*1.5*1e3),c&&c.css("opacity",1),b&&b.css("opacity",0)}});return c}),f("effects/NoEffect",["effects/Effect"],function(a){var b=a.extend();return b.prototype.play=function(a,b,c,d){b&&b.css("visibility","visible"),c.call(d)},b}),{StackNavigator:e("StackNavigator"),Effect:e("effects/Effect"),NoEffect:e("effects/NoEffect"),SlideEffect:e("effects/SlideEffect"),FadeEffect:e("effects/FadeEffect")}}) -------------------------------------------------------------------------------- /backstack.js: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2012 Piotr Walczyszyn (http://outof.me | @pwalczyszyn) 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | ////////////////////////////////////////////////////////////////////////////////////// 18 | 19 | // BackStack version 1.1.2 20 | 21 | (function (root, factory) { 22 | // Set up BackStack appropriately for the environment. 23 | if (typeof define === 'function' && define.amd) { 24 | // AMD 25 | define(['jquery', 'underscore', 'Backbone'], factory); 26 | } else { 27 | // Browser globals 28 | root.BackStack = factory((root.jQuery || root.Zepto || root.ender), root._, root.Backbone); 29 | } 30 | }(this, function ($, _, Backbone) { 31 | 32 | /** 33 | * almond 0.1.1 Copyright (c) 2011, The Dojo Foundation All Rights Reserved. 34 | * Available via the MIT or new BSD license. 35 | * see: http://github.com/jrburke/almond for details 36 | */ 37 | //Going sloppy to avoid 'use strict' string cost, but strict practices should 38 | //be followed. 39 | /*jslint sloppy: true */ 40 | /*global setTimeout: false */ 41 | 42 | var requirejs, require, define; 43 | (function (undef) { 44 | var defined = {}, 45 | waiting = {}, 46 | config = {}, 47 | defining = {}, 48 | aps = [].slice, 49 | main, req; 50 | 51 | /** 52 | * Given a relative module name, like ./something, normalize it to 53 | * a real name that can be mapped to a path. 54 | * @param {String} name the relative name 55 | * @param {String} baseName a real name that the name arg is relative 56 | * to. 57 | * @returns {String} normalized name 58 | */ 59 | function normalize(name, baseName) { 60 | var baseParts = baseName && baseName.split("/"), 61 | map = config.map, 62 | starMap = (map && map['*']) || {}, 63 | nameParts, nameSegment, mapValue, foundMap, i, j, part; 64 | 65 | //Adjust any relative paths. 66 | if (name && name.charAt(0) === ".") { 67 | //If have a base name, try to normalize against it, 68 | //otherwise, assume it is a top-level require that will 69 | //be relative to baseUrl in the end. 70 | if (baseName) { 71 | //Convert baseName to array, and lop off the last part, 72 | //so that . matches that "directory" and not name of the baseName's 73 | //module. For instance, baseName of "one/two/three", maps to 74 | //"one/two/three.js", but we want the directory, "one/two" for 75 | //this normalization. 76 | baseParts = baseParts.slice(0, baseParts.length - 1); 77 | 78 | name = baseParts.concat(name.split("/")); 79 | 80 | //start trimDots 81 | for (i = 0; (part = name[i]); i++) { 82 | if (part === ".") { 83 | name.splice(i, 1); 84 | i -= 1; 85 | } else if (part === "..") { 86 | if (i === 1 && (name[2] === '..' || name[0] === '..')) { 87 | //End of the line. Keep at least one non-dot 88 | //path segment at the front so it can be mapped 89 | //correctly to disk. Otherwise, there is likely 90 | //no path mapping for a path starting with '..'. 91 | //This can still fail, but catches the most reasonable 92 | //uses of .. 93 | return true; 94 | } else if (i > 0) { 95 | name.splice(i - 1, 2); 96 | i -= 2; 97 | } 98 | } 99 | } 100 | //end trimDots 101 | 102 | name = name.join("/"); 103 | } 104 | } 105 | 106 | //Apply map config if available. 107 | if ((baseParts || starMap) && map) { 108 | nameParts = name.split('/'); 109 | 110 | for (i = nameParts.length; i > 0; i -= 1) { 111 | nameSegment = nameParts.slice(0, i).join("/"); 112 | 113 | if (baseParts) { 114 | //Find the longest baseName segment match in the config. 115 | //So, do joins on the biggest to smallest lengths of baseParts. 116 | for (j = baseParts.length; j > 0; j -= 1) { 117 | mapValue = map[baseParts.slice(0, j).join('/')]; 118 | 119 | //baseName segment has config, find if it has one for 120 | //this name. 121 | if (mapValue) { 122 | mapValue = mapValue[nameSegment]; 123 | if (mapValue) { 124 | //Match, update name to the new value. 125 | foundMap = mapValue; 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | 132 | foundMap = foundMap || starMap[nameSegment]; 133 | 134 | if (foundMap) { 135 | nameParts.splice(0, i, foundMap); 136 | name = nameParts.join('/'); 137 | break; 138 | } 139 | } 140 | } 141 | 142 | return name; 143 | } 144 | 145 | function makeRequire(relName, forceSync) { 146 | return function () { 147 | //A version of a require function that passes a moduleName 148 | //value for items that may need to 149 | //look up paths relative to the moduleName 150 | return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync])); 151 | }; 152 | } 153 | 154 | function makeNormalize(relName) { 155 | return function (name) { 156 | return normalize(name, relName); 157 | }; 158 | } 159 | 160 | function makeLoad(depName) { 161 | return function (value) { 162 | defined[depName] = value; 163 | }; 164 | } 165 | 166 | function callDep(name) { 167 | if (waiting.hasOwnProperty(name)) { 168 | var args = waiting[name]; 169 | delete waiting[name]; 170 | defining[name] = true; 171 | main.apply(undef, args); 172 | } 173 | 174 | if (!defined.hasOwnProperty(name)) { 175 | throw new Error('No ' + name); 176 | } 177 | return defined[name]; 178 | } 179 | 180 | /** 181 | * Makes a name map, normalizing the name, and using a plugin 182 | * for normalization if necessary. Grabs a ref to plugin 183 | * too, as an optimization. 184 | */ 185 | function makeMap(name, relName) { 186 | var prefix, plugin, 187 | index = name.indexOf('!'); 188 | 189 | if (index !== -1) { 190 | prefix = normalize(name.slice(0, index), relName); 191 | name = name.slice(index + 1); 192 | plugin = callDep(prefix); 193 | 194 | //Normalize according 195 | if (plugin && plugin.normalize) { 196 | name = plugin.normalize(name, makeNormalize(relName)); 197 | } else { 198 | name = normalize(name, relName); 199 | } 200 | } else { 201 | name = normalize(name, relName); 202 | } 203 | 204 | //Using ridiculous property names for space reasons 205 | return { 206 | f: prefix ? prefix + '!' + name : name, //fullName 207 | n: name, 208 | p: plugin 209 | }; 210 | } 211 | 212 | function makeConfig(name) { 213 | return function () { 214 | return (config && config.config && config.config[name]) || {}; 215 | }; 216 | } 217 | 218 | main = function (name, deps, callback, relName) { 219 | var args = [], 220 | usingExports, 221 | cjsModule, depName, ret, map, i; 222 | 223 | //Use name if no relName 224 | relName = relName || name; 225 | 226 | //Call the callback to define the module, if necessary. 227 | if (typeof callback === 'function') { 228 | 229 | //Pull out the defined dependencies and pass the ordered 230 | //values to the callback. 231 | //Default to [require, exports, module] if no deps 232 | deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; 233 | for (i = 0; i < deps.length; i++) { 234 | map = makeMap(deps[i], relName); 235 | depName = map.f; 236 | 237 | //Fast path CommonJS standard dependencies. 238 | if (depName === "require") { 239 | args[i] = makeRequire(name); 240 | } else if (depName === "exports") { 241 | //CommonJS module spec 1.1 242 | args[i] = defined[name] = {}; 243 | usingExports = true; 244 | } else if (depName === "module") { 245 | //CommonJS module spec 1.1 246 | cjsModule = args[i] = { 247 | id: name, 248 | uri: '', 249 | exports: defined[name], 250 | config: makeConfig(name) 251 | }; 252 | } else if (defined.hasOwnProperty(depName) || waiting.hasOwnProperty(depName)) { 253 | args[i] = callDep(depName); 254 | } else if (map.p) { 255 | map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); 256 | args[i] = defined[depName]; 257 | } else if (!defining[depName]) { 258 | throw new Error(name + ' missing ' + depName); 259 | } 260 | } 261 | 262 | ret = callback.apply(defined[name], args); 263 | 264 | if (name) { 265 | //If setting exports via "module" is in play, 266 | //favor that over return value and exports. After that, 267 | //favor a non-undefined return value over exports use. 268 | if (cjsModule && cjsModule.exports !== undef && 269 | cjsModule.exports !== defined[name]) { 270 | defined[name] = cjsModule.exports; 271 | } else if (ret !== undef || !usingExports) { 272 | //Use the return value from the function. 273 | defined[name] = ret; 274 | } 275 | } 276 | } else if (name) { 277 | //May just be an object definition for the module. Only 278 | //worry about defining if have a module name. 279 | defined[name] = callback; 280 | } 281 | }; 282 | 283 | requirejs = require = req = function (deps, callback, relName, forceSync) { 284 | if (typeof deps === "string") { 285 | //Just return the module wanted. In this scenario, the 286 | //deps arg is the module name, and second arg (if passed) 287 | //is just the relName. 288 | //Normalize module name, if it contains . or .. 289 | return callDep(makeMap(deps, callback).f); 290 | } else if (!deps.splice) { 291 | //deps is a config object, not an array. 292 | config = deps; 293 | if (callback.splice) { 294 | //callback is an array, which means it is a dependency list. 295 | //Adjust args if there are dependencies 296 | deps = callback; 297 | callback = relName; 298 | relName = null; 299 | } else { 300 | deps = undef; 301 | } 302 | } 303 | 304 | //Support require(['a']) 305 | callback = callback || function () {}; 306 | 307 | //Simulate async callback; 308 | if (forceSync) { 309 | main(undef, deps, callback, relName); 310 | } else { 311 | setTimeout(function () { 312 | main(undef, deps, callback, relName); 313 | }, 15); 314 | } 315 | 316 | return req; 317 | }; 318 | 319 | /** 320 | * Just drops the config on the floor, but returns req in case 321 | * the config return value is used. 322 | */ 323 | req.config = function (cfg) { 324 | config = cfg; 325 | return req; 326 | }; 327 | 328 | define = function (name, deps, callback) { 329 | 330 | //This module may not have dependencies 331 | if (!deps.splice) { 332 | //deps is not an array, so probably means 333 | //an object literal or factory function for 334 | //the value. Adjust args. 335 | callback = deps; 336 | deps = []; 337 | } 338 | 339 | waiting[name] = [name, deps, callback]; 340 | }; 341 | 342 | define.amd = { 343 | jQuery: true 344 | }; 345 | }()); 346 | 347 | define("almond", function(){}); 348 | 349 | define('effects/vendorPrefix',[], function () { 350 | 351 | /** 352 | * Helper function to detect browser vendor prefix. 353 | * Thanks to Lea Verou: http://lea.verou.me/2009/02/find-the-vendor-prefix-of-the-current-browser/ 354 | * I just modified it slightly as I expect it to be used in mobile/WebKit scenarios mostly. 355 | */ 356 | var vendorPrefix, 357 | regex = /^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/, 358 | someScript = document.getElementsByTagName('script')[0]; 359 | 360 | // Exception for WebKit based browsers 361 | if ('WebkitOpacity' in someScript.style) { 362 | vendorPrefix = 'Webkit'; 363 | } else if ('KhtmlOpacity' in someScript.style) { 364 | vendorPrefix = 'Khtml'; 365 | } else { 366 | for (var prop in someScript.style) { 367 | if (regex.test(prop)) { 368 | // test is faster than match, so it's better to perform 369 | // that on the lot and match only when necessary 370 | vendorPrefix = prop.match(regex)[0]; 371 | break; 372 | } 373 | } 374 | } 375 | 376 | return (vendorPrefix.toLowerCase() || ''); 377 | }); 378 | define('effects/Effect',['effects/vendorPrefix'], function (vendorPrefix) { 379 | 380 | var Effect = function Effect(params) { 381 | 382 | if (params) _.extend(this, params); 383 | 384 | this.vendorPrefix = vendorPrefix; 385 | 386 | if (this.vendorPrefix == 'moz' || this.vendorPrefix == '') this.transitionEndEvent = 'transitionend'; 387 | else if (this.vendorPrefix == 'ms') this.transitionEndEvent = 'MSTransitionEnd'; 388 | else this.transitionEndEvent = this.vendorPrefix + 'TransitionEnd'; 389 | 390 | }; 391 | 392 | // Shared empty constructor function to aid in prototype-chain creation. 393 | var ctor = function () { 394 | }; 395 | 396 | Effect.extend = function (protoProps, staticProps) { 397 | var child = function () { 398 | Effect.apply(this, arguments); 399 | }; 400 | 401 | // Inherit class (static) properties from parent. 402 | _.extend(child, Effect); 403 | 404 | // Set the prototype chain to inherit from `parent`, without calling 405 | // `parent`'s constructor function. 406 | ctor.prototype = Effect.prototype; 407 | child.prototype = new ctor(); 408 | 409 | // Add prototype properties (instance properties) to the subclass, 410 | // if supplied. 411 | if (protoProps) _.extend(child.prototype, protoProps); 412 | 413 | // Add static properties to the constructor function, if supplied. 414 | if (staticProps) _.extend(child, staticProps); 415 | 416 | // Correctly set child's `prototype.constructor`. 417 | child.prototype.constructor = child; 418 | 419 | // Set a convenience property in case the parent's prototype is needed later. 420 | child.__super__ = Effect.prototype; 421 | 422 | return child; 423 | }; 424 | 425 | return Effect; 426 | }); 427 | define('effects/SlideEffect',['effects/Effect'], function (Effect) { 428 | 429 | var SlideEffect = Effect.extend({ 430 | 431 | direction:'left', 432 | 433 | fromViewTransitionProps:{duration:0.4, easing:'ease-out', delay:0}, 434 | 435 | toViewTransitionProps:{duration:0.4, easing:'ease-out', delay:0}, 436 | 437 | play:function ($fromView, $toView, callback, context) { 438 | 439 | var timeout, 440 | that = this, 441 | activeTransitions = 0, 442 | transformParams, 443 | transformProp = that.vendorPrefix == '' ? 'transform' : 444 | ['-' + that.vendorPrefix, '-', 'transform'].join(''), 445 | transitionProp = that.vendorPrefix == '' ? 'transition' : 446 | ['-' + that.vendorPrefix, '-', 'transition'].join(''); 447 | 448 | var transitionEndHandler = function (event) { 449 | if (activeTransitions >= 0) { 450 | activeTransitions--; 451 | 452 | var $target = $(event.target); 453 | $target.css(transformProp, ''); 454 | $target.css(transitionProp, ''); 455 | 456 | if ($toView && $toView[0] == event.target) $toView.css('left', 0); 457 | 458 | if (activeTransitions == 0 && callback) { 459 | if (timeout) clearTimeout(timeout); 460 | callback.call(context); 461 | } 462 | } 463 | }; 464 | 465 | if ($fromView) { 466 | activeTransitions++; 467 | 468 | $fromView.one(that.transitionEndEvent, transitionEndHandler); 469 | 470 | $fromView.css('left', 0); 471 | $fromView.css(transitionProp, [transformProp, ' ', 472 | that.fromViewTransitionProps.duration, 's ', 473 | that.fromViewTransitionProps.easing, ' ', 474 | that.fromViewTransitionProps.delay, 's'].join('')); 475 | } 476 | 477 | if ($toView) { 478 | activeTransitions++; 479 | 480 | $toView.one(that.transitionEndEvent, transitionEndHandler); 481 | 482 | $toView.css('left', that.direction == 'left' ? context.$el.width() : -context.$el.width()); 483 | $toView.css(transitionProp, [transformProp, ' ', 484 | that.toViewTransitionProps.duration, 's ', 485 | that.toViewTransitionProps.easing, ' ', 486 | that.toViewTransitionProps.delay, 's'].join('')); 487 | 488 | // Showing the view 489 | $toView.css('visibility', 'visible'); 490 | } 491 | 492 | if ($fromView || $toView) { 493 | // This is a hack to force DOM reflow before transition starts 494 | context.$el.css('width'); 495 | 496 | transformParams = 'translate3d(' + (that.direction == 'left' ? -context.$el.width() : context.$el.width()) + 'px, 0, 0)'; 497 | } 498 | 499 | // This is a fallback for situations when TransitionEnd event doesn't get triggered 500 | var transDuration = Math.max(that.fromViewTransitionProps.duration, that.toViewTransitionProps.duration) + 501 | Math.max(that.fromViewTransitionProps.delay, that.toViewTransitionProps.delay); 502 | 503 | timeout = setTimeout(function () { 504 | if (activeTransitions > 0) { 505 | activeTransitions = -1; 506 | 507 | console.log('Warning ' + that.transitionEndEvent + ' didn\'t trigger in expected time!'); 508 | 509 | if ($toView) { 510 | $toView.off(that.transitionEndEvent, transitionEndHandler); 511 | $toView.css(transitionProp, ''); 512 | $toView.css(transformProp, ''); 513 | $toView.css('left', 0); 514 | } 515 | 516 | if ($fromView) { 517 | $fromView.off(that.transitionEndEvent, transitionEndHandler); 518 | $fromView.css(transitionProp, ''); 519 | $fromView.css(transformProp, ''); 520 | } 521 | 522 | callback.call(context); 523 | } 524 | }, transDuration * 1.5 * 1000); 525 | 526 | var $views; 527 | if ($fromView && $toView) $views = $fromView.add($toView); 528 | else if ($toView) $views = $toView; 529 | else if ($fromView) $views = $fromView; 530 | 531 | if ($views) $views.css(transformProp, transformParams); 532 | } 533 | }); 534 | 535 | return SlideEffect; 536 | }); 537 | define('StackNavigator',['effects/SlideEffect'], function (SlideEffect) { 538 | 539 | /** 540 | * Rendering the view and setting props required by StackNavigator. 541 | * @private 542 | * @ignore 543 | * 544 | * @param {View} view View to be rendered. 545 | * @param {StackNavigator} stackNavigator View StackNavigator instance. 546 | */ 547 | function appendView(view, stackNavigator) { 548 | 549 | if (!view.__backStackRendered__) { 550 | 551 | // Setting ref to parent StackNavigator 552 | view.stackNavigator = stackNavigator; 553 | 554 | // Setting default destructionPolicy if it's not set 555 | if (typeof view.destructionPolicy === 'undefined') view.destructionPolicy = 'auto'; 556 | 557 | // Setting default styles 558 | view.$el.css({position:'absolute', visibility:'hidden', overflow:'hidden', width:'100%', height:'100%'}); 559 | 560 | } else { 561 | // Resetting visibility to hidden 562 | view.$el.css({visibility:'hidden'}); 563 | } 564 | 565 | // Adding view to the DOM 566 | stackNavigator.$el.append(view.el); 567 | 568 | if (!view.__backStackRendered__) { 569 | // Rendering the view 570 | view.render.call(view); 571 | 572 | // Setting default of __backStackRendered__ property 573 | view.__backStackRendered__ = true; 574 | } 575 | } 576 | 577 | /** 578 | * Creates event objects triggered by BackStack. 579 | * @private 580 | * @ignore 581 | * 582 | * @param {string} type Event type name. 583 | * @param {*} args Event args. 584 | * @param {boolean} cancelable Flag indicating if event is cancelable. 585 | * @return {event} The new object. 586 | */ 587 | function createEvent(type, args, cancelable) { 588 | return _.extend({ 589 | 590 | type:type, 591 | 592 | cancelable:_.isUndefined(cancelable) ? false : cancelable, 593 | 594 | preventDefault:function () { 595 | if (this.cancelable) 596 | this.isDefaultPrevented = function () { 597 | return true; 598 | }; 599 | }, 600 | 601 | isDefaultPrevented:function () { 602 | return false; 603 | }, 604 | 605 | trigger:function (target) { 606 | target.trigger(this.type, this); 607 | return this; 608 | } 609 | }, args); 610 | } 611 | 612 | /** 613 | * Private common push method. 614 | * @private 615 | * @ignore 616 | * 617 | * @param {object} fromViewRef Reference to from view. 618 | * @param {object} toViewRef Reference to to view. 619 | * @param {number} replaceHowMany Number of views to replace with pushed view. 620 | * @param {Effect} transition Transition to played during push. 621 | */ 622 | function push(fromViewRef, toViewRef, replaceHowMany, transition) { 623 | 624 | // Rendering view if required 625 | appendView(toViewRef.instance, this); 626 | 627 | transition = transition || this.defaultPushTransition || (this.defaultPushTransition = new SlideEffect({direction:'left'})); 628 | transition.play(fromViewRef ? fromViewRef.instance.$el : null, toViewRef.instance.$el, 629 | function () { // Callback function 630 | 631 | var remove = replaceHowMany > 0 ? this.viewsStack.splice(this.viewsStack.length - replaceHowMany, replaceHowMany) 632 | : (fromViewRef ? [fromViewRef] : null); 633 | 634 | if (remove != null) { 635 | _.each(remove, function (ref) { 636 | 637 | // Triggering viewDeactivate event 638 | createEvent('viewDeactivate', {target:ref.instance}).trigger(ref.instance); 639 | 640 | if (ref.instance.destructionPolicy == 'never') { // Detaching if destructionPolicy == 'never' 641 | ref.instance.$el.detach(); 642 | } else { // Removing if destructionPolicy == 'auto' 643 | ref.instance.remove(); 644 | ref.instance = null; 645 | } 646 | }, this); 647 | } 648 | 649 | // Adding view to the stack internal array 650 | this.viewsStack.push(toViewRef); 651 | 652 | // Setting activeView property 653 | this.activeView = toViewRef.instance; 654 | 655 | // Triggering viewActivate event 656 | createEvent('viewActivate', {target:toViewRef.instance}).trigger(toViewRef.instance); 657 | 658 | // Triggering viewChanged event 659 | createEvent('viewChanged', {target:this}).trigger(this); 660 | 661 | // Popping item from actions queue 662 | popActionsQueue.call(this); 663 | 664 | }, this); 665 | } 666 | 667 | /** 668 | * Private common pop method. 669 | * @private 670 | * @ignore 671 | * 672 | * @param {object} fromViewRef Reference to from view. 673 | * @param {object} toViewRef Reference to to view. 674 | * @param {number} howMany Number of views to pop from the stack. 675 | * @param {Effect} transition Transition to played during pop. 676 | */ 677 | function pop(fromViewRef, toViewRef, howMany, transition) { 678 | 679 | if (toViewRef) { 680 | // Recreating view instance if necessary 681 | toViewRef.instance = toViewRef.instance ? toViewRef.instance : new toViewRef.viewClass(toViewRef.options); 682 | // Rendering view if required 683 | appendView(toViewRef.instance, this); 684 | } 685 | 686 | transition = transition || this.defaultPopTransition || (this.defaultPopTransition = new SlideEffect({direction:'right'})); 687 | transition.play(fromViewRef.instance.$el, toViewRef ? toViewRef.instance.$el : null, 688 | function () { // Callback function 689 | 690 | // Popping views from a stack 691 | var remove = this.viewsStack.splice(this.viewsStack.length - howMany, howMany); 692 | _.each(remove, function (ref) { 693 | 694 | // Triggering viewDeactivate event 695 | createEvent('viewDeactivate', {target:ref.instance}).trigger(ref.instance); 696 | 697 | if (ref.instance.destructionPolicy == 'never') { // Detaching if destructionPolicy == 'never' 698 | ref.instance.$el.detach(); 699 | } else { // Removing if destructionPolicy == 'auto' 700 | ref.instance.remove(); 701 | ref.instance = null; 702 | } 703 | }, this); 704 | 705 | if (toViewRef) { // If toViewRef exists activating it 706 | 707 | // Setting activeView property 708 | this.activeView = toViewRef.instance; 709 | 710 | // Triggering viewActivate event 711 | createEvent('viewActivate', {target:toViewRef.instance}).trigger(toViewRef.instance); 712 | 713 | } else { // Nulling activeView property 714 | this.activeView = null; 715 | } 716 | 717 | // Triggering viewChanged event 718 | createEvent('viewChanged', {target:this}).trigger(this); 719 | 720 | // Popping item from actions queue 721 | popActionsQueue.call(this); 722 | }, this); 723 | } 724 | 725 | function pushView(view, viewOptions, transition) { 726 | // Getting ref of the view on top of the stack 727 | var fromViewRef = _.last(this.viewsStack), 728 | // Creating new view instance if it is necessary 729 | toView = _.isFunction(view) ? new view(viewOptions) : view, 730 | // Creating new view ref 731 | toViewRef = {instance:toView, viewClass:toView.constructor, options:viewOptions}, 732 | // Creating viewChanging event object and triggering it 733 | event = createEvent('viewChanging', 734 | { 735 | action:'push', 736 | fromViewClass:fromViewRef ? fromViewRef.viewClass : null, 737 | fromView:fromViewRef ? fromViewRef.instance : null, 738 | toViewClass:toViewRef.viewClass, 739 | toView:toViewRef.instance 740 | }, 741 | true).trigger(this); 742 | 743 | // Checking if event wasn't cancelled 744 | if (event.isDefaultPrevented()) return null; 745 | 746 | push.call(this, fromViewRef, toViewRef, 0, transition); 747 | } 748 | 749 | function popView(transition) { 750 | if (this.viewsStack.length == 0) throw new Error('Popping from an empty stack!'); 751 | 752 | // Getting ref of the view on top of the stack 753 | var fromViewRef = _.last(this.viewsStack), 754 | // Getting ref of the view below current one 755 | toViewRef = this.viewsStack.length > 1 ? this.viewsStack[this.viewsStack.length - 2] : null, 756 | // Creating viewChanging event object and triggering it 757 | event = createEvent('viewChanging', 758 | { 759 | action:'pop', 760 | fromViewClass:fromViewRef.viewClass, 761 | fromView:fromViewRef.instance, 762 | toViewClass:toViewRef ? toViewRef.viewClass : null, 763 | toView:toViewRef ? toViewRef.instance : null 764 | }, 765 | true).trigger(this); 766 | 767 | // Checking if event wasn't cancelled 768 | if (event.isDefaultPrevented()) return; 769 | 770 | // Popping top view 771 | pop.call(this, fromViewRef, toViewRef, 1, transition); 772 | } 773 | 774 | function popAll(transition) { 775 | if (this.viewsStack.length == 0) throw new Error('Popping from an empty stack!'); 776 | 777 | // Getting ref of the view on top of the stack 778 | var fromViewRef = _.last(this.viewsStack), 779 | // Creating viewChanging event object and triggering it 780 | event = createEvent('viewChanging', 781 | { 782 | action:'popAll', 783 | fromViewClass:fromViewRef.viewClass, 784 | fromView:fromViewRef.instance, 785 | toViewClass:null, 786 | toView:null 787 | }, 788 | true).trigger(this); 789 | 790 | // Checking if event wasn't cancelled 791 | if (event.isDefaultPrevented()) return; 792 | 793 | // Popping top view 794 | pop.call(this, fromViewRef, null, this.viewsStack.length, transition); 795 | } 796 | 797 | function replaceView(view, viewOptions, transition) { 798 | if (this.viewsStack.length == 0) throw new Error('Replacing on an empty stack!'); 799 | 800 | // Getting ref of the view on top of the stack 801 | var fromViewRef = _.last(this.viewsStack), 802 | // Creating new view instance if it is necessary 803 | toView = _.isFunction(view) ? new view(viewOptions) : view, 804 | // Creating new view ref 805 | toViewRef = {instance:toView, viewClass:toView.constructor, options:viewOptions}, 806 | // Creating viewChanging event object and triggering it 807 | event = createEvent('viewChanging', 808 | { 809 | action:'replace', 810 | fromViewClass:fromViewRef.viewClass, 811 | fromView:fromViewRef.instance, 812 | toViewClass:toViewRef.viewClass, 813 | toView:toViewRef.instance 814 | }, 815 | true).trigger(this); 816 | 817 | // Checking if event wasn't cancelled 818 | if (event.isDefaultPrevented()) return null; 819 | 820 | // Pushing new view on top 821 | push.call(this, fromViewRef, toViewRef, 1, transition); 822 | } 823 | 824 | function replaceAll(view, viewOptions, transition) { 825 | if (this.viewsStack.length == 0) throw new Error('Replacing on an empty stack!'); 826 | 827 | // Getting ref of the view on top of the stack 828 | var fromViewRef = _.last(this.viewsStack), 829 | // Creating new view instance if it is necessary 830 | toView = _.isFunction(view) ? new view(viewOptions) : view, 831 | // Creating new view ref 832 | toViewRef = {instance:toView, viewClass:toView.constructor, options:viewOptions}, 833 | // Creating viewChanging event object and triggering it 834 | event = createEvent('viewChanging', 835 | { 836 | action:'replaceAll', 837 | fromViewClass:fromViewRef.viewClass, 838 | fromView:fromViewRef.instance, 839 | toViewClass:toViewRef.viewClass, 840 | toView:toViewRef.instance 841 | }, 842 | true).trigger(this); 843 | 844 | // Checking if event wasn't cancelled 845 | if (event.isDefaultPrevented()) return null; 846 | 847 | // Pushing new view on top 848 | push.call(this, fromViewRef, toViewRef, this.viewsStack.length, transition); 849 | } 850 | 851 | function popActionsQueue() { 852 | this.actionsQueue.splice(0, 1); 853 | if (this.actionsQueue.length > 0) { 854 | var action = this.actionsQueue[0], 855 | args = Array.prototype.slice.call(action.args); 856 | switch (action.fn) { 857 | case 'pushView': 858 | pushView.apply(this, args); 859 | break; 860 | case 'popView': 861 | popView.apply(this, args); 862 | break; 863 | case 'popAll': 864 | popAll.apply(this, args); 865 | break; 866 | case 'replaceView': 867 | replaceView.apply(this, args); 868 | break; 869 | case 'replaceAll': 870 | replaceAll.apply(this, args); 871 | break; 872 | } 873 | } 874 | } 875 | 876 | var StackNavigator = Backbone.View.extend( 877 | /** @lends BackStack.StackNavigator */ 878 | { 879 | /** 880 | * @name StackNavigator#viewChanging 881 | * @event 882 | * @param {Object} e 883 | * @param {Boolean} [e.cancelable=true] 884 | */ 885 | 886 | /** 887 | * An array with all the view refs on the stack. 888 | */ 889 | viewsStack:null, 890 | 891 | /** 892 | * View on top of the stack. 893 | */ 894 | activeView:null, 895 | 896 | /** 897 | * Default push transition effect. 898 | */ 899 | defaultPushTransition:null, 900 | 901 | /** 902 | * Default pop transition effect. 903 | */ 904 | defaultPopTransition:null, 905 | 906 | /** 907 | * Queue of actions to be executed on the stack. 908 | */ 909 | actionsQueue:null, 910 | 911 | /** 912 | * Initializes StackNavigator. 913 | * 914 | * @param {Object} options This is a Backbone.View options hash that can have popTransition and pushTransition 915 | * properties that can be initiated for this instance of navigator. 916 | * 917 | * @constructs 918 | * */ 919 | initialize:function (options) { 920 | // Setting default styles 921 | this.$el.css({overflow:'hidden'}); 922 | 923 | // Setting new viewsStack array 924 | this.viewsStack = []; 925 | 926 | // Setting new queue of actions 927 | this.actionsQueue = []; 928 | 929 | // Setting default pop transition 930 | if (options.popTransition) this.defaultPopTransition = options.popTransition; 931 | 932 | // Setting default push transition 933 | if (options.pushTransition) this.defaultPushTransition = options.pushTransition; 934 | }, 935 | 936 | /** 937 | * Pushes new view to the stack. 938 | * 939 | * @param {Backbone.View || Backbone.ViewClass} view View class or view instance to be pushed to the stack. 940 | * @param {Object} viewOptions Options to be passed if view is contructed by StackNavigator. 941 | * @param {Effect} transition Transition effect to be played when pushing new view. 942 | */ 943 | pushView:function (view, viewOptions, transition) { 944 | // Pushing current action to the queue 945 | this.actionsQueue.push({fn:'pushView', args:arguments}); 946 | 947 | if (this.actionsQueue.length == 1) pushView.call(this, view, viewOptions, transition); 948 | }, 949 | 950 | /** 951 | * Pops an active view from a stack and displays one below. 952 | * 953 | * @param {Effect} transition Transition effect to be played when popping new view. 954 | */ 955 | popView:function (transition) { 956 | // Pushing current action to the queue 957 | this.actionsQueue.push({fn:'popView', args:arguments}); 958 | 959 | if (this.actionsQueue.length == 1) popView.call(this, transition); 960 | }, 961 | 962 | /** 963 | * Pops all views from a stack and leaves empty stack. 964 | * 965 | * @param {Effect} transition Transition effect to be played when popping views. 966 | */ 967 | popAll:function (transition) { 968 | // Pushing current action to the queue 969 | this.actionsQueue.push({fn:'popAll', args:arguments}); 970 | 971 | if (this.actionsQueue.length == 1) popAll.call(this, transition); 972 | }, 973 | 974 | /** 975 | * Replaces view on top of the stack, with the one passed as a view param. 976 | * 977 | * @param {Backbone.View || Backbone.ViewClass} view View class or view instance to be pushed on top of the stack instead of current one. 978 | * @param {Object} viewOptions Hash with options to be passed to the view, if view param is not an instance. 979 | * @param {Effect} transition Transition effect to be played when replacing views. 980 | */ 981 | replaceView:function (view, viewOptions, transition) { 982 | // Pushing current action to the queue 983 | this.actionsQueue.push({fn:'replaceView', args:arguments}); 984 | 985 | if (this.actionsQueue.length == 1) replaceView.call(this, view, viewOptions, transition); 986 | }, 987 | 988 | /** 989 | * Replaces all of the views on the stack, with the one passed as a view param. 990 | * 991 | * @param {Backbone.View || Backbone.ViewClass} view View class or view instance to be pushed on top of the stack. 992 | * @param {Object} viewOptions Hash with options to be passed to the view, if view param is not an instance. 993 | * @param {Effect} transition Transition effect to be played when replacing views. 994 | */ 995 | replaceAll:function (view, viewOptions, transition) { 996 | // Pushing current action to the queue 997 | this.actionsQueue.push({fn:'replaceAll', args:arguments}); 998 | 999 | if (this.actionsQueue.length == 1) replaceAll.call(this, view, viewOptions, transition); 1000 | } 1001 | }); 1002 | 1003 | return StackNavigator; 1004 | }); 1005 | define('effects/FadeEffect',['effects/Effect'], function (Effect) { 1006 | 1007 | var FadeEffect = Effect.extend({ 1008 | 1009 | fromViewTransitionProps:{duration:0.4, easing:'linear', delay:0.1}, 1010 | 1011 | toViewTransitionProps:{duration:0.4, easing:'linear', delay:0.1}, 1012 | 1013 | play:function ($fromView, $toView, callback, context) { 1014 | 1015 | var that = this, 1016 | timeout, 1017 | activeTransitions = 0, 1018 | transitionProp = that.vendorPrefix == '' ? 'transition' 1019 | : ['-' + that.vendorPrefix.toLowerCase(), '-', 'transition'].join(''); 1020 | 1021 | var transitionEndHandler = function (event) { 1022 | if (activeTransitions >= 0) { 1023 | activeTransitions--; 1024 | 1025 | $(event.target).css(transitionProp, ''); 1026 | 1027 | if (activeTransitions == 0 && callback) { 1028 | if (timeout) clearTimeout(timeout); 1029 | callback.call(context); 1030 | } 1031 | } 1032 | }; 1033 | 1034 | if ($fromView) { 1035 | activeTransitions++; 1036 | 1037 | // Registering transition end handler 1038 | $fromView.one(that.transitionEndEvent, transitionEndHandler); 1039 | 1040 | // Setting transition css props 1041 | $fromView.css(transitionProp, ['opacity ', that.fromViewTransitionProps.duration, 's ', 1042 | that.fromViewTransitionProps.easing, ' ', 1043 | that.fromViewTransitionProps.delay, 's'].join('')); 1044 | } 1045 | 1046 | if ($toView) { 1047 | activeTransitions++; 1048 | 1049 | $toView.one(that.transitionEndEvent, transitionEndHandler); 1050 | 1051 | // Setting initial opacity 1052 | $toView.css('opacity', 0); 1053 | 1054 | // Setting transition css props 1055 | $toView.css(transitionProp, ['opacity ', that.toViewTransitionProps.duration, 's ', 1056 | that.toViewTransitionProps.easing, ' ', 1057 | that.toViewTransitionProps.delay, 's'].join('')); 1058 | 1059 | // Showing the view 1060 | $toView.css('visibility', 'visible'); 1061 | } 1062 | 1063 | // This is a hack to force DOM reflow before transition starts 1064 | context.$el.css('width'); 1065 | 1066 | // This is a fallback for situations when TransitionEnd event doesn't get triggered 1067 | var transDuration = Math.max(that.fromViewTransitionProps.duration, that.toViewTransitionProps.duration) + 1068 | Math.max(that.fromViewTransitionProps.delay, that.toViewTransitionProps.delay); 1069 | 1070 | timeout = setTimeout(function () { 1071 | if (activeTransitions > 0) { 1072 | activeTransitions = -1; 1073 | 1074 | console.log('Warning ' + that.transitionEndEvent + ' didn\'t trigger in expected time!'); 1075 | 1076 | if ($toView) { 1077 | $toView.off(that.transitionEndEvent, transitionEndHandler); 1078 | $toView.css(transitionProp, ''); 1079 | } 1080 | 1081 | if ($fromView) { 1082 | $fromView.off(that.transitionEndEvent, transitionEndHandler); 1083 | $fromView.css(transitionProp, ''); 1084 | } 1085 | 1086 | callback.call(context); 1087 | } 1088 | }, transDuration * 1.5 * 1000); 1089 | 1090 | if ($toView) $toView.css('opacity', 1); 1091 | if ($fromView) $fromView.css('opacity', 0); 1092 | } 1093 | }); 1094 | 1095 | return FadeEffect; 1096 | }); 1097 | define('effects/NoEffect',['effects/Effect'], function (Effect) { 1098 | 1099 | var NoEffect = Effect.extend(); 1100 | NoEffect.prototype.play = function ($fromView, $toView, callback, context) { 1101 | if ($toView) { 1102 | // Showing the view 1103 | $toView.css('visibility', 'visible'); 1104 | } 1105 | callback.call(context); 1106 | }; 1107 | 1108 | return NoEffect; 1109 | }); 1110 | return { 1111 | StackNavigator : require('StackNavigator'), 1112 | Effect : require('effects/Effect'), 1113 | NoEffect : require('effects/NoEffect'), 1114 | SlideEffect : require('effects/SlideEffect'), 1115 | FadeEffect : require('effects/FadeEffect') 1116 | }; 1117 | })); -------------------------------------------------------------------------------- /build/almond.js: -------------------------------------------------------------------------------- 1 | /** 2 | * almond 0.1.1 Copyright (c) 2011, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/jrburke/almond for details 5 | */ 6 | //Going sloppy to avoid 'use strict' string cost, but strict practices should 7 | //be followed. 8 | /*jslint sloppy: true */ 9 | /*global setTimeout: false */ 10 | 11 | var requirejs, require, define; 12 | (function (undef) { 13 | var defined = {}, 14 | waiting = {}, 15 | config = {}, 16 | defining = {}, 17 | aps = [].slice, 18 | main, req; 19 | 20 | /** 21 | * Given a relative module name, like ./something, normalize it to 22 | * a real name that can be mapped to a path. 23 | * @param {String} name the relative name 24 | * @param {String} baseName a real name that the name arg is relative 25 | * to. 26 | * @returns {String} normalized name 27 | */ 28 | function normalize(name, baseName) { 29 | var baseParts = baseName && baseName.split("/"), 30 | map = config.map, 31 | starMap = (map && map['*']) || {}, 32 | nameParts, nameSegment, mapValue, foundMap, i, j, part; 33 | 34 | //Adjust any relative paths. 35 | if (name && name.charAt(0) === ".") { 36 | //If have a base name, try to normalize against it, 37 | //otherwise, assume it is a top-level require that will 38 | //be relative to baseUrl in the end. 39 | if (baseName) { 40 | //Convert baseName to array, and lop off the last part, 41 | //so that . matches that "directory" and not name of the baseName's 42 | //module. For instance, baseName of "one/two/three", maps to 43 | //"one/two/three.js", but we want the directory, "one/two" for 44 | //this normalization. 45 | baseParts = baseParts.slice(0, baseParts.length - 1); 46 | 47 | name = baseParts.concat(name.split("/")); 48 | 49 | //start trimDots 50 | for (i = 0; (part = name[i]); i++) { 51 | if (part === ".") { 52 | name.splice(i, 1); 53 | i -= 1; 54 | } else if (part === "..") { 55 | if (i === 1 && (name[2] === '..' || name[0] === '..')) { 56 | //End of the line. Keep at least one non-dot 57 | //path segment at the front so it can be mapped 58 | //correctly to disk. Otherwise, there is likely 59 | //no path mapping for a path starting with '..'. 60 | //This can still fail, but catches the most reasonable 61 | //uses of .. 62 | return true; 63 | } else if (i > 0) { 64 | name.splice(i - 1, 2); 65 | i -= 2; 66 | } 67 | } 68 | } 69 | //end trimDots 70 | 71 | name = name.join("/"); 72 | } 73 | } 74 | 75 | //Apply map config if available. 76 | if ((baseParts || starMap) && map) { 77 | nameParts = name.split('/'); 78 | 79 | for (i = nameParts.length; i > 0; i -= 1) { 80 | nameSegment = nameParts.slice(0, i).join("/"); 81 | 82 | if (baseParts) { 83 | //Find the longest baseName segment match in the config. 84 | //So, do joins on the biggest to smallest lengths of baseParts. 85 | for (j = baseParts.length; j > 0; j -= 1) { 86 | mapValue = map[baseParts.slice(0, j).join('/')]; 87 | 88 | //baseName segment has config, find if it has one for 89 | //this name. 90 | if (mapValue) { 91 | mapValue = mapValue[nameSegment]; 92 | if (mapValue) { 93 | //Match, update name to the new value. 94 | foundMap = mapValue; 95 | break; 96 | } 97 | } 98 | } 99 | } 100 | 101 | foundMap = foundMap || starMap[nameSegment]; 102 | 103 | if (foundMap) { 104 | nameParts.splice(0, i, foundMap); 105 | name = nameParts.join('/'); 106 | break; 107 | } 108 | } 109 | } 110 | 111 | return name; 112 | } 113 | 114 | function makeRequire(relName, forceSync) { 115 | return function () { 116 | //A version of a require function that passes a moduleName 117 | //value for items that may need to 118 | //look up paths relative to the moduleName 119 | return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync])); 120 | }; 121 | } 122 | 123 | function makeNormalize(relName) { 124 | return function (name) { 125 | return normalize(name, relName); 126 | }; 127 | } 128 | 129 | function makeLoad(depName) { 130 | return function (value) { 131 | defined[depName] = value; 132 | }; 133 | } 134 | 135 | function callDep(name) { 136 | if (waiting.hasOwnProperty(name)) { 137 | var args = waiting[name]; 138 | delete waiting[name]; 139 | defining[name] = true; 140 | main.apply(undef, args); 141 | } 142 | 143 | if (!defined.hasOwnProperty(name)) { 144 | throw new Error('No ' + name); 145 | } 146 | return defined[name]; 147 | } 148 | 149 | /** 150 | * Makes a name map, normalizing the name, and using a plugin 151 | * for normalization if necessary. Grabs a ref to plugin 152 | * too, as an optimization. 153 | */ 154 | function makeMap(name, relName) { 155 | var prefix, plugin, 156 | index = name.indexOf('!'); 157 | 158 | if (index !== -1) { 159 | prefix = normalize(name.slice(0, index), relName); 160 | name = name.slice(index + 1); 161 | plugin = callDep(prefix); 162 | 163 | //Normalize according 164 | if (plugin && plugin.normalize) { 165 | name = plugin.normalize(name, makeNormalize(relName)); 166 | } else { 167 | name = normalize(name, relName); 168 | } 169 | } else { 170 | name = normalize(name, relName); 171 | } 172 | 173 | //Using ridiculous property names for space reasons 174 | return { 175 | f: prefix ? prefix + '!' + name : name, //fullName 176 | n: name, 177 | p: plugin 178 | }; 179 | } 180 | 181 | function makeConfig(name) { 182 | return function () { 183 | return (config && config.config && config.config[name]) || {}; 184 | }; 185 | } 186 | 187 | main = function (name, deps, callback, relName) { 188 | var args = [], 189 | usingExports, 190 | cjsModule, depName, ret, map, i; 191 | 192 | //Use name if no relName 193 | relName = relName || name; 194 | 195 | //Call the callback to define the module, if necessary. 196 | if (typeof callback === 'function') { 197 | 198 | //Pull out the defined dependencies and pass the ordered 199 | //values to the callback. 200 | //Default to [require, exports, module] if no deps 201 | deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; 202 | for (i = 0; i < deps.length; i++) { 203 | map = makeMap(deps[i], relName); 204 | depName = map.f; 205 | 206 | //Fast path CommonJS standard dependencies. 207 | if (depName === "require") { 208 | args[i] = makeRequire(name); 209 | } else if (depName === "exports") { 210 | //CommonJS module spec 1.1 211 | args[i] = defined[name] = {}; 212 | usingExports = true; 213 | } else if (depName === "module") { 214 | //CommonJS module spec 1.1 215 | cjsModule = args[i] = { 216 | id: name, 217 | uri: '', 218 | exports: defined[name], 219 | config: makeConfig(name) 220 | }; 221 | } else if (defined.hasOwnProperty(depName) || waiting.hasOwnProperty(depName)) { 222 | args[i] = callDep(depName); 223 | } else if (map.p) { 224 | map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); 225 | args[i] = defined[depName]; 226 | } else if (!defining[depName]) { 227 | throw new Error(name + ' missing ' + depName); 228 | } 229 | } 230 | 231 | ret = callback.apply(defined[name], args); 232 | 233 | if (name) { 234 | //If setting exports via "module" is in play, 235 | //favor that over return value and exports. After that, 236 | //favor a non-undefined return value over exports use. 237 | if (cjsModule && cjsModule.exports !== undef && 238 | cjsModule.exports !== defined[name]) { 239 | defined[name] = cjsModule.exports; 240 | } else if (ret !== undef || !usingExports) { 241 | //Use the return value from the function. 242 | defined[name] = ret; 243 | } 244 | } 245 | } else if (name) { 246 | //May just be an object definition for the module. Only 247 | //worry about defining if have a module name. 248 | defined[name] = callback; 249 | } 250 | }; 251 | 252 | requirejs = require = req = function (deps, callback, relName, forceSync) { 253 | if (typeof deps === "string") { 254 | //Just return the module wanted. In this scenario, the 255 | //deps arg is the module name, and second arg (if passed) 256 | //is just the relName. 257 | //Normalize module name, if it contains . or .. 258 | return callDep(makeMap(deps, callback).f); 259 | } else if (!deps.splice) { 260 | //deps is a config object, not an array. 261 | config = deps; 262 | if (callback.splice) { 263 | //callback is an array, which means it is a dependency list. 264 | //Adjust args if there are dependencies 265 | deps = callback; 266 | callback = relName; 267 | relName = null; 268 | } else { 269 | deps = undef; 270 | } 271 | } 272 | 273 | //Support require(['a']) 274 | callback = callback || function () {}; 275 | 276 | //Simulate async callback; 277 | if (forceSync) { 278 | main(undef, deps, callback, relName); 279 | } else { 280 | setTimeout(function () { 281 | main(undef, deps, callback, relName); 282 | }, 15); 283 | } 284 | 285 | return req; 286 | }; 287 | 288 | /** 289 | * Just drops the config on the floor, but returns req in case 290 | * the config return value is used. 291 | */ 292 | req.config = function (cfg) { 293 | config = cfg; 294 | return req; 295 | }; 296 | 297 | define = function (name, deps, callback) { 298 | 299 | //This module may not have dependencies 300 | if (!deps.splice) { 301 | //deps is not an array, so probably means 302 | //an object literal or factory function for 303 | //the value. Adjust args. 304 | callback = deps; 305 | deps = []; 306 | } 307 | 308 | waiting[name] = [name, deps, callback]; 309 | }; 310 | 311 | define.amd = { 312 | jQuery: true 313 | }; 314 | }()); 315 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | ({ 2 | baseUrl:"../src", 3 | paths:{ 4 | "almond":"../build/almond" 5 | }, 6 | include:["almond", "StackNavigator", "effects/Effect", "effects/FadeEffect", "effects/NoEffect", "effects/SlideEffect"], 7 | preserveLicenseComments:true, 8 | out:"backstack-built.js", 9 | wrap:{ 10 | startFile:"wrap-start.frag", 11 | endFile:"wrap-end.frag" 12 | }, 13 | optimize:"none" 14 | }) -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/local/bin/node r.js -o build.js out=../backstack.js 4 | /usr/local/bin/node r.js -o build.js out=../backstack-min.js optimize=uglify 5 | -------------------------------------------------------------------------------- /build/wrap-end.frag: -------------------------------------------------------------------------------- 1 | 2 | return { 3 | StackNavigator : require('StackNavigator'), 4 | Effect : require('effects/Effect'), 5 | NoEffect : require('effects/NoEffect'), 6 | SlideEffect : require('effects/SlideEffect'), 7 | FadeEffect : require('effects/FadeEffect') 8 | }; 9 | })); -------------------------------------------------------------------------------- /build/wrap-start.frag: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2012 Piotr Walczyszyn (http://outof.me | @pwalczyszyn) 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | ////////////////////////////////////////////////////////////////////////////////////// 18 | 19 | // BackStack version 1.1.2 20 | 21 | (function (root, factory) { 22 | // Set up BackStack appropriately for the environment. 23 | if (typeof define === 'function' && define.amd) { 24 | // AMD 25 | define(['jquery', 'underscore', 'Backbone'], factory); 26 | } else { 27 | // Browser globals 28 | root.BackStack = factory((root.jQuery || root.Zepto || root.ender), root._, root.Backbone); 29 | } 30 | }(this, function ($, _, Backbone) { 31 | -------------------------------------------------------------------------------- /src/StackNavigator.js: -------------------------------------------------------------------------------- 1 | define(['effects/SlideEffect'], function (SlideEffect) { 2 | 3 | /** 4 | * Rendering the view and setting props required by StackNavigator. 5 | * @private 6 | * @ignore 7 | * 8 | * @param {View} view View to be rendered. 9 | * @param {StackNavigator} stackNavigator View StackNavigator instance. 10 | */ 11 | function appendView(view, stackNavigator) { 12 | 13 | if (!view.__backStackRendered__) { 14 | 15 | // Setting ref to parent StackNavigator 16 | view.stackNavigator = stackNavigator; 17 | 18 | // Setting default destructionPolicy if it's not set 19 | if (typeof view.destructionPolicy === 'undefined') view.destructionPolicy = 'auto'; 20 | 21 | // Setting default styles 22 | view.$el.css({position:'absolute', visibility:'hidden', overflow:'hidden', width:'100%', height:'100%'}); 23 | 24 | } else { 25 | // Resetting visibility to hidden 26 | view.$el.css({visibility:'hidden'}); 27 | } 28 | 29 | // Adding view to the DOM 30 | stackNavigator.$el.append(view.el); 31 | 32 | if (!view.__backStackRendered__) { 33 | // Rendering the view 34 | view.render.call(view); 35 | 36 | // Setting default of __backStackRendered__ property 37 | view.__backStackRendered__ = true; 38 | } 39 | } 40 | 41 | /** 42 | * Creates event objects triggered by BackStack. 43 | * @private 44 | * @ignore 45 | * 46 | * @param {string} type Event type name. 47 | * @param {*} args Event args. 48 | * @param {boolean} cancelable Flag indicating if event is cancelable. 49 | * @return {event} The new object. 50 | */ 51 | function createEvent(type, args, cancelable) { 52 | return _.extend({ 53 | 54 | type:type, 55 | 56 | cancelable:_.isUndefined(cancelable) ? false : cancelable, 57 | 58 | preventDefault:function () { 59 | if (this.cancelable) 60 | this.isDefaultPrevented = function () { 61 | return true; 62 | }; 63 | }, 64 | 65 | isDefaultPrevented:function () { 66 | return false; 67 | }, 68 | 69 | trigger:function (target) { 70 | target.trigger(this.type, this); 71 | return this; 72 | } 73 | }, args); 74 | } 75 | 76 | /** 77 | * Private common push method. 78 | * @private 79 | * @ignore 80 | * 81 | * @param {object} fromViewRef Reference to from view. 82 | * @param {object} toViewRef Reference to to view. 83 | * @param {number} replaceHowMany Number of views to replace with pushed view. 84 | * @param {Effect} transition Transition to played during push. 85 | */ 86 | function push(fromViewRef, toViewRef, replaceHowMany, transition) { 87 | 88 | // Rendering view if required 89 | appendView(toViewRef.instance, this); 90 | 91 | transition = transition || this.defaultPushTransition || (this.defaultPushTransition = new SlideEffect({direction:'left'})); 92 | transition.play(fromViewRef ? fromViewRef.instance.$el : null, toViewRef.instance.$el, 93 | function () { // Callback function 94 | 95 | var remove = replaceHowMany > 0 ? this.viewsStack.splice(this.viewsStack.length - replaceHowMany, replaceHowMany) 96 | : (fromViewRef ? [fromViewRef] : null); 97 | 98 | if (remove != null) { 99 | _.each(remove, function (ref) { 100 | 101 | // Triggering viewDeactivate event 102 | createEvent('viewDeactivate', {target:ref.instance}).trigger(ref.instance); 103 | 104 | if (ref.instance.destructionPolicy == 'never') { // Detaching if destructionPolicy == 'never' 105 | ref.instance.$el.detach(); 106 | } else { // Removing if destructionPolicy == 'auto' 107 | ref.instance.remove(); 108 | ref.instance = null; 109 | } 110 | }, this); 111 | } 112 | 113 | // Adding view to the stack internal array 114 | this.viewsStack.push(toViewRef); 115 | 116 | // Setting activeView property 117 | this.activeView = toViewRef.instance; 118 | 119 | // Triggering viewActivate event 120 | createEvent('viewActivate', {target:toViewRef.instance}).trigger(toViewRef.instance); 121 | 122 | // Triggering viewChanged event 123 | createEvent('viewChanged', {target:this}).trigger(this); 124 | 125 | // Popping item from actions queue 126 | popActionsQueue.call(this); 127 | 128 | }, this); 129 | } 130 | 131 | /** 132 | * Private common pop method. 133 | * @private 134 | * @ignore 135 | * 136 | * @param {object} fromViewRef Reference to from view. 137 | * @param {object} toViewRef Reference to to view. 138 | * @param {number} howMany Number of views to pop from the stack. 139 | * @param {Effect} transition Transition to played during pop. 140 | */ 141 | function pop(fromViewRef, toViewRef, howMany, transition) { 142 | 143 | if (toViewRef) { 144 | // Recreating view instance if necessary 145 | toViewRef.instance = toViewRef.instance ? toViewRef.instance : new toViewRef.viewClass(toViewRef.options); 146 | // Rendering view if required 147 | appendView(toViewRef.instance, this); 148 | } 149 | 150 | transition = transition || this.defaultPopTransition || (this.defaultPopTransition = new SlideEffect({direction:'right'})); 151 | transition.play(fromViewRef.instance.$el, toViewRef ? toViewRef.instance.$el : null, 152 | function () { // Callback function 153 | 154 | // Popping views from a stack 155 | var remove = this.viewsStack.splice(this.viewsStack.length - howMany, howMany); 156 | _.each(remove, function (ref) { 157 | 158 | // Triggering viewDeactivate event 159 | createEvent('viewDeactivate', {target:ref.instance}).trigger(ref.instance); 160 | 161 | if (ref.instance.destructionPolicy == 'never') { // Detaching if destructionPolicy == 'never' 162 | ref.instance.$el.detach(); 163 | } else { // Removing if destructionPolicy == 'auto' 164 | ref.instance.remove(); 165 | ref.instance = null; 166 | } 167 | }, this); 168 | 169 | if (toViewRef) { // If toViewRef exists activating it 170 | 171 | // Setting activeView property 172 | this.activeView = toViewRef.instance; 173 | 174 | // Triggering viewActivate event 175 | createEvent('viewActivate', {target:toViewRef.instance}).trigger(toViewRef.instance); 176 | 177 | } else { // Nulling activeView property 178 | this.activeView = null; 179 | } 180 | 181 | // Triggering viewChanged event 182 | createEvent('viewChanged', {target:this}).trigger(this); 183 | 184 | // Popping item from actions queue 185 | popActionsQueue.call(this); 186 | }, this); 187 | } 188 | 189 | function pushView(view, viewOptions, transition) { 190 | // Getting ref of the view on top of the stack 191 | var fromViewRef = _.last(this.viewsStack), 192 | // Creating new view instance if it is necessary 193 | toView = _.isFunction(view) ? new view(viewOptions) : view, 194 | // Creating new view ref 195 | toViewRef = {instance:toView, viewClass:toView.constructor, options:viewOptions}, 196 | // Creating viewChanging event object and triggering it 197 | event = createEvent('viewChanging', 198 | { 199 | action:'push', 200 | fromViewClass:fromViewRef ? fromViewRef.viewClass : null, 201 | fromView:fromViewRef ? fromViewRef.instance : null, 202 | toViewClass:toViewRef.viewClass, 203 | toView:toViewRef.instance 204 | }, 205 | true).trigger(this); 206 | 207 | // Checking if event wasn't cancelled 208 | if (event.isDefaultPrevented()) return null; 209 | 210 | push.call(this, fromViewRef, toViewRef, 0, transition); 211 | } 212 | 213 | function popView(transition) { 214 | if (this.viewsStack.length == 0) throw new Error('Popping from an empty stack!'); 215 | 216 | // Getting ref of the view on top of the stack 217 | var fromViewRef = _.last(this.viewsStack), 218 | // Getting ref of the view below current one 219 | toViewRef = this.viewsStack.length > 1 ? this.viewsStack[this.viewsStack.length - 2] : null, 220 | // Creating viewChanging event object and triggering it 221 | event = createEvent('viewChanging', 222 | { 223 | action:'pop', 224 | fromViewClass:fromViewRef.viewClass, 225 | fromView:fromViewRef.instance, 226 | toViewClass:toViewRef ? toViewRef.viewClass : null, 227 | toView:toViewRef ? toViewRef.instance : null 228 | }, 229 | true).trigger(this); 230 | 231 | // Checking if event wasn't cancelled 232 | if (event.isDefaultPrevented()) return; 233 | 234 | // Popping top view 235 | pop.call(this, fromViewRef, toViewRef, 1, transition); 236 | } 237 | 238 | function popAll(transition) { 239 | if (this.viewsStack.length == 0) throw new Error('Popping from an empty stack!'); 240 | 241 | // Getting ref of the view on top of the stack 242 | var fromViewRef = _.last(this.viewsStack), 243 | // Creating viewChanging event object and triggering it 244 | event = createEvent('viewChanging', 245 | { 246 | action:'popAll', 247 | fromViewClass:fromViewRef.viewClass, 248 | fromView:fromViewRef.instance, 249 | toViewClass:null, 250 | toView:null 251 | }, 252 | true).trigger(this); 253 | 254 | // Checking if event wasn't cancelled 255 | if (event.isDefaultPrevented()) return; 256 | 257 | // Popping top view 258 | pop.call(this, fromViewRef, null, this.viewsStack.length, transition); 259 | } 260 | 261 | function replaceView(view, viewOptions, transition) { 262 | if (this.viewsStack.length == 0) throw new Error('Replacing on an empty stack!'); 263 | 264 | // Getting ref of the view on top of the stack 265 | var fromViewRef = _.last(this.viewsStack), 266 | // Creating new view instance if it is necessary 267 | toView = _.isFunction(view) ? new view(viewOptions) : view, 268 | // Creating new view ref 269 | toViewRef = {instance:toView, viewClass:toView.constructor, options:viewOptions}, 270 | // Creating viewChanging event object and triggering it 271 | event = createEvent('viewChanging', 272 | { 273 | action:'replace', 274 | fromViewClass:fromViewRef.viewClass, 275 | fromView:fromViewRef.instance, 276 | toViewClass:toViewRef.viewClass, 277 | toView:toViewRef.instance 278 | }, 279 | true).trigger(this); 280 | 281 | // Checking if event wasn't cancelled 282 | if (event.isDefaultPrevented()) return null; 283 | 284 | // Pushing new view on top 285 | push.call(this, fromViewRef, toViewRef, 1, transition); 286 | } 287 | 288 | function replaceAll(view, viewOptions, transition) { 289 | if (this.viewsStack.length == 0) throw new Error('Replacing on an empty stack!'); 290 | 291 | // Getting ref of the view on top of the stack 292 | var fromViewRef = _.last(this.viewsStack), 293 | // Creating new view instance if it is necessary 294 | toView = _.isFunction(view) ? new view(viewOptions) : view, 295 | // Creating new view ref 296 | toViewRef = {instance:toView, viewClass:toView.constructor, options:viewOptions}, 297 | // Creating viewChanging event object and triggering it 298 | event = createEvent('viewChanging', 299 | { 300 | action:'replaceAll', 301 | fromViewClass:fromViewRef.viewClass, 302 | fromView:fromViewRef.instance, 303 | toViewClass:toViewRef.viewClass, 304 | toView:toViewRef.instance 305 | }, 306 | true).trigger(this); 307 | 308 | // Checking if event wasn't cancelled 309 | if (event.isDefaultPrevented()) return null; 310 | 311 | // Pushing new view on top 312 | push.call(this, fromViewRef, toViewRef, this.viewsStack.length, transition); 313 | } 314 | 315 | function popActionsQueue() { 316 | this.actionsQueue.splice(0, 1); 317 | if (this.actionsQueue.length > 0) { 318 | var action = this.actionsQueue[0], 319 | args = Array.prototype.slice.call(action.args); 320 | switch (action.fn) { 321 | case 'pushView': 322 | pushView.apply(this, args); 323 | break; 324 | case 'popView': 325 | popView.apply(this, args); 326 | break; 327 | case 'popAll': 328 | popAll.apply(this, args); 329 | break; 330 | case 'replaceView': 331 | replaceView.apply(this, args); 332 | break; 333 | case 'replaceAll': 334 | replaceAll.apply(this, args); 335 | break; 336 | } 337 | } 338 | } 339 | 340 | var StackNavigator = Backbone.View.extend( 341 | /** @lends BackStack.StackNavigator */ 342 | { 343 | /** 344 | * @name StackNavigator#viewChanging 345 | * @event 346 | * @param {Object} e 347 | * @param {Boolean} [e.cancelable=true] 348 | */ 349 | 350 | /** 351 | * An array with all the view refs on the stack. 352 | */ 353 | viewsStack:null, 354 | 355 | /** 356 | * View on top of the stack. 357 | */ 358 | activeView:null, 359 | 360 | /** 361 | * Default push transition effect. 362 | */ 363 | defaultPushTransition:null, 364 | 365 | /** 366 | * Default pop transition effect. 367 | */ 368 | defaultPopTransition:null, 369 | 370 | /** 371 | * Queue of actions to be executed on the stack. 372 | */ 373 | actionsQueue:null, 374 | 375 | /** 376 | * Initializes StackNavigator. 377 | * 378 | * @param {Object} options This is a Backbone.View options hash that can have popTransition and pushTransition 379 | * properties that can be initiated for this instance of navigator. 380 | * 381 | * @constructs 382 | * */ 383 | initialize:function (options) { 384 | // Setting default styles 385 | this.$el.css({overflow:'hidden'}); 386 | 387 | // Setting new viewsStack array 388 | this.viewsStack = []; 389 | 390 | // Setting new queue of actions 391 | this.actionsQueue = []; 392 | 393 | // Setting default pop transition 394 | if (options.popTransition) this.defaultPopTransition = options.popTransition; 395 | 396 | // Setting default push transition 397 | if (options.pushTransition) this.defaultPushTransition = options.pushTransition; 398 | }, 399 | 400 | /** 401 | * Pushes new view to the stack. 402 | * 403 | * @param {Backbone.View || Backbone.ViewClass} view View class or view instance to be pushed to the stack. 404 | * @param {Object} viewOptions Options to be passed if view is contructed by StackNavigator. 405 | * @param {Effect} transition Transition effect to be played when pushing new view. 406 | */ 407 | pushView:function (view, viewOptions, transition) { 408 | // Pushing current action to the queue 409 | this.actionsQueue.push({fn:'pushView', args:arguments}); 410 | 411 | if (this.actionsQueue.length == 1) pushView.call(this, view, viewOptions, transition); 412 | }, 413 | 414 | /** 415 | * Pops an active view from a stack and displays one below. 416 | * 417 | * @param {Effect} transition Transition effect to be played when popping new view. 418 | */ 419 | popView:function (transition) { 420 | // Pushing current action to the queue 421 | this.actionsQueue.push({fn:'popView', args:arguments}); 422 | 423 | if (this.actionsQueue.length == 1) popView.call(this, transition); 424 | }, 425 | 426 | /** 427 | * Pops all views from a stack and leaves empty stack. 428 | * 429 | * @param {Effect} transition Transition effect to be played when popping views. 430 | */ 431 | popAll:function (transition) { 432 | // Pushing current action to the queue 433 | this.actionsQueue.push({fn:'popAll', args:arguments}); 434 | 435 | if (this.actionsQueue.length == 1) popAll.call(this, transition); 436 | }, 437 | 438 | /** 439 | * Replaces view on top of the stack, with the one passed as a view param. 440 | * 441 | * @param {Backbone.View || Backbone.ViewClass} view View class or view instance to be pushed on top of the stack instead of current one. 442 | * @param {Object} viewOptions Hash with options to be passed to the view, if view param is not an instance. 443 | * @param {Effect} transition Transition effect to be played when replacing views. 444 | */ 445 | replaceView:function (view, viewOptions, transition) { 446 | // Pushing current action to the queue 447 | this.actionsQueue.push({fn:'replaceView', args:arguments}); 448 | 449 | if (this.actionsQueue.length == 1) replaceView.call(this, view, viewOptions, transition); 450 | }, 451 | 452 | /** 453 | * Replaces all of the views on the stack, with the one passed as a view param. 454 | * 455 | * @param {Backbone.View || Backbone.ViewClass} view View class or view instance to be pushed on top of the stack. 456 | * @param {Object} viewOptions Hash with options to be passed to the view, if view param is not an instance. 457 | * @param {Effect} transition Transition effect to be played when replacing views. 458 | */ 459 | replaceAll:function (view, viewOptions, transition) { 460 | // Pushing current action to the queue 461 | this.actionsQueue.push({fn:'replaceAll', args:arguments}); 462 | 463 | if (this.actionsQueue.length == 1) replaceAll.call(this, view, viewOptions, transition); 464 | } 465 | }); 466 | 467 | return StackNavigator; 468 | }); -------------------------------------------------------------------------------- /src/effects/Effect.js: -------------------------------------------------------------------------------- 1 | define(['effects/vendorPrefix'], function (vendorPrefix) { 2 | 3 | var Effect = function Effect(params) { 4 | 5 | if (params) _.extend(this, params); 6 | 7 | this.vendorPrefix = vendorPrefix; 8 | 9 | if (this.vendorPrefix == 'moz' || this.vendorPrefix == '') this.transitionEndEvent = 'transitionend'; 10 | else if (this.vendorPrefix == 'ms') this.transitionEndEvent = 'MSTransitionEnd'; 11 | else this.transitionEndEvent = this.vendorPrefix + 'TransitionEnd'; 12 | 13 | }; 14 | 15 | // Shared empty constructor function to aid in prototype-chain creation. 16 | var ctor = function () { 17 | }; 18 | 19 | Effect.extend = function (protoProps, staticProps) { 20 | var child = function () { 21 | Effect.apply(this, arguments); 22 | }; 23 | 24 | // Inherit class (static) properties from parent. 25 | _.extend(child, Effect); 26 | 27 | // Set the prototype chain to inherit from `parent`, without calling 28 | // `parent`'s constructor function. 29 | ctor.prototype = Effect.prototype; 30 | child.prototype = new ctor(); 31 | 32 | // Add prototype properties (instance properties) to the subclass, 33 | // if supplied. 34 | if (protoProps) _.extend(child.prototype, protoProps); 35 | 36 | // Add static properties to the constructor function, if supplied. 37 | if (staticProps) _.extend(child, staticProps); 38 | 39 | // Correctly set child's `prototype.constructor`. 40 | child.prototype.constructor = child; 41 | 42 | // Set a convenience property in case the parent's prototype is needed later. 43 | child.__super__ = Effect.prototype; 44 | 45 | return child; 46 | }; 47 | 48 | return Effect; 49 | }); -------------------------------------------------------------------------------- /src/effects/FadeEffect.js: -------------------------------------------------------------------------------- 1 | define(['effects/Effect'], function (Effect) { 2 | 3 | var FadeEffect = Effect.extend({ 4 | 5 | fromViewTransitionProps:{duration:0.4, easing:'linear', delay:0.1}, 6 | 7 | toViewTransitionProps:{duration:0.4, easing:'linear', delay:0.1}, 8 | 9 | play:function ($fromView, $toView, callback, context) { 10 | 11 | var that = this, 12 | timeout, 13 | activeTransitions = 0, 14 | transitionProp = that.vendorPrefix == '' ? 'transition' 15 | : ['-' + that.vendorPrefix.toLowerCase(), '-', 'transition'].join(''); 16 | 17 | var transitionEndHandler = function (event) { 18 | if (activeTransitions >= 0) { 19 | activeTransitions--; 20 | 21 | $(event.target).css(transitionProp, ''); 22 | 23 | if (activeTransitions == 0 && callback) { 24 | if (timeout) clearTimeout(timeout); 25 | callback.call(context); 26 | } 27 | } 28 | }; 29 | 30 | if ($fromView) { 31 | activeTransitions++; 32 | 33 | // Registering transition end handler 34 | $fromView.one(that.transitionEndEvent, transitionEndHandler); 35 | 36 | // Setting transition css props 37 | $fromView.css(transitionProp, ['opacity ', that.fromViewTransitionProps.duration, 's ', 38 | that.fromViewTransitionProps.easing, ' ', 39 | that.fromViewTransitionProps.delay, 's'].join('')); 40 | } 41 | 42 | if ($toView) { 43 | activeTransitions++; 44 | 45 | $toView.one(that.transitionEndEvent, transitionEndHandler); 46 | 47 | // Setting initial opacity 48 | $toView.css('opacity', 0); 49 | 50 | // Setting transition css props 51 | $toView.css(transitionProp, ['opacity ', that.toViewTransitionProps.duration, 's ', 52 | that.toViewTransitionProps.easing, ' ', 53 | that.toViewTransitionProps.delay, 's'].join('')); 54 | 55 | // Showing the view 56 | $toView.css('visibility', 'visible'); 57 | } 58 | 59 | // This is a hack to force DOM reflow before transition starts 60 | context.$el.css('width'); 61 | 62 | // This is a fallback for situations when TransitionEnd event doesn't get triggered 63 | var transDuration = Math.max(that.fromViewTransitionProps.duration, that.toViewTransitionProps.duration) + 64 | Math.max(that.fromViewTransitionProps.delay, that.toViewTransitionProps.delay); 65 | 66 | timeout = setTimeout(function () { 67 | if (activeTransitions > 0) { 68 | activeTransitions = -1; 69 | 70 | console.log('Warning ' + that.transitionEndEvent + ' didn\'t trigger in expected time!'); 71 | 72 | if ($toView) { 73 | $toView.off(that.transitionEndEvent, transitionEndHandler); 74 | $toView.css(transitionProp, ''); 75 | } 76 | 77 | if ($fromView) { 78 | $fromView.off(that.transitionEndEvent, transitionEndHandler); 79 | $fromView.css(transitionProp, ''); 80 | } 81 | 82 | callback.call(context); 83 | } 84 | }, transDuration * 1.5 * 1000); 85 | 86 | if ($toView) $toView.css('opacity', 1); 87 | if ($fromView) $fromView.css('opacity', 0); 88 | } 89 | }); 90 | 91 | return FadeEffect; 92 | }); -------------------------------------------------------------------------------- /src/effects/NoEffect.js: -------------------------------------------------------------------------------- 1 | define(['effects/Effect'], function (Effect) { 2 | 3 | var NoEffect = Effect.extend(); 4 | NoEffect.prototype.play = function ($fromView, $toView, callback, context) { 5 | if ($toView) { 6 | // Showing the view 7 | $toView.css('visibility', 'visible'); 8 | } 9 | callback.call(context); 10 | }; 11 | 12 | return NoEffect; 13 | }); -------------------------------------------------------------------------------- /src/effects/SlideEffect.js: -------------------------------------------------------------------------------- 1 | define(['effects/Effect'], function (Effect) { 2 | 3 | var SlideEffect = Effect.extend({ 4 | 5 | direction:'left', 6 | 7 | fromViewTransitionProps:{duration:0.4, easing:'ease-out', delay:0}, 8 | 9 | toViewTransitionProps:{duration:0.4, easing:'ease-out', delay:0}, 10 | 11 | play:function ($fromView, $toView, callback, context) { 12 | 13 | var timeout, 14 | that = this, 15 | activeTransitions = 0, 16 | transformParams, 17 | transformProp = that.vendorPrefix == '' ? 'transform' : 18 | ['-' + that.vendorPrefix, '-', 'transform'].join(''), 19 | transitionProp = that.vendorPrefix == '' ? 'transition' : 20 | ['-' + that.vendorPrefix, '-', 'transition'].join(''); 21 | 22 | var transitionEndHandler = function (event) { 23 | if (activeTransitions >= 0) { 24 | activeTransitions--; 25 | 26 | var $target = $(event.target); 27 | $target.css(transformProp, ''); 28 | $target.css(transitionProp, ''); 29 | 30 | if ($toView && $toView[0] == event.target) $toView.css('left', 0); 31 | 32 | if (activeTransitions == 0 && callback) { 33 | if (timeout) clearTimeout(timeout); 34 | callback.call(context); 35 | } 36 | } 37 | }; 38 | 39 | if ($fromView) { 40 | activeTransitions++; 41 | 42 | $fromView.one(that.transitionEndEvent, transitionEndHandler); 43 | 44 | $fromView.css('left', 0); 45 | $fromView.css(transitionProp, [transformProp, ' ', 46 | that.fromViewTransitionProps.duration, 's ', 47 | that.fromViewTransitionProps.easing, ' ', 48 | that.fromViewTransitionProps.delay, 's'].join('')); 49 | } 50 | 51 | if ($toView) { 52 | activeTransitions++; 53 | 54 | $toView.one(that.transitionEndEvent, transitionEndHandler); 55 | 56 | $toView.css('left', that.direction == 'left' ? context.$el.width() : -context.$el.width()); 57 | $toView.css(transitionProp, [transformProp, ' ', 58 | that.toViewTransitionProps.duration, 's ', 59 | that.toViewTransitionProps.easing, ' ', 60 | that.toViewTransitionProps.delay, 's'].join('')); 61 | 62 | // Showing the view 63 | $toView.css('visibility', 'visible'); 64 | } 65 | 66 | if ($fromView || $toView) { 67 | // This is a hack to force DOM reflow before transition starts 68 | context.$el.css('width'); 69 | 70 | transformParams = 'translate3d(' + (that.direction == 'left' ? -context.$el.width() : context.$el.width()) + 'px, 0, 0)'; 71 | } 72 | 73 | // This is a fallback for situations when TransitionEnd event doesn't get triggered 74 | var transDuration = Math.max(that.fromViewTransitionProps.duration, that.toViewTransitionProps.duration) + 75 | Math.max(that.fromViewTransitionProps.delay, that.toViewTransitionProps.delay); 76 | 77 | timeout = setTimeout(function () { 78 | if (activeTransitions > 0) { 79 | activeTransitions = -1; 80 | 81 | console.log('Warning ' + that.transitionEndEvent + ' didn\'t trigger in expected time!'); 82 | 83 | if ($toView) { 84 | $toView.off(that.transitionEndEvent, transitionEndHandler); 85 | $toView.css(transitionProp, ''); 86 | $toView.css(transformProp, ''); 87 | $toView.css('left', 0); 88 | } 89 | 90 | if ($fromView) { 91 | $fromView.off(that.transitionEndEvent, transitionEndHandler); 92 | $fromView.css(transitionProp, ''); 93 | $fromView.css(transformProp, ''); 94 | } 95 | 96 | callback.call(context); 97 | } 98 | }, transDuration * 1.5 * 1000); 99 | 100 | var $views; 101 | if ($fromView && $toView) $views = $fromView.add($toView); 102 | else if ($toView) $views = $toView; 103 | else if ($fromView) $views = $fromView; 104 | 105 | if ($views) $views.css(transformProp, transformParams); 106 | } 107 | }); 108 | 109 | return SlideEffect; 110 | }); -------------------------------------------------------------------------------- /src/effects/vendorPrefix.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | 3 | /** 4 | * Helper function to detect browser vendor prefix. 5 | * Thanks to Lea Verou: http://lea.verou.me/2009/02/find-the-vendor-prefix-of-the-current-browser/ 6 | * I just modified it slightly as I expect it to be used in mobile/WebKit scenarios mostly. 7 | */ 8 | var vendorPrefix, 9 | regex = /^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/, 10 | someScript = document.getElementsByTagName('script')[0]; 11 | 12 | // Exception for WebKit based browsers 13 | if ('WebkitOpacity' in someScript.style) { 14 | vendorPrefix = 'Webkit'; 15 | } else if ('KhtmlOpacity' in someScript.style) { 16 | vendorPrefix = 'Khtml'; 17 | } else { 18 | for (var prop in someScript.style) { 19 | if (regex.test(prop)) { 20 | // test is faster than match, so it's better to perform 21 | // that on the lot and match only when necessary 22 | vendorPrefix = prop.match(regex)[0]; 23 | break; 24 | } 25 | } 26 | } 27 | 28 | return (vendorPrefix.toLowerCase() || ''); 29 | }); --------------------------------------------------------------------------------