PageRenderTime 90ms CodeModel.GetById 18ms RepoModel.GetById 3ms app.codeStats 0ms

/gbone.js

http://github.com/gobhi/gbone.js
JavaScript | 497 lines | 264 code | 94 blank | 139 comment | 43 complexity | a0652275838c3dc474d3b14f70fba5bf MD5 | raw file
  1. // Gbone.js 0.1.0
  2. // (c) 2011 Gobhi Theivendran
  3. // Gbone.js may be freely distributed under the MIT license.
  4. // For all details and documentation:
  5. // https://github.com/gobhi/gbone.js
  6. (function( root, factory ) {
  7. // Set up Gbone appropriately for the environment.
  8. if ( typeof exports !== 'undefined' ) {
  9. // Node/CommonJS, no need for jQuery/Zepto in that case.
  10. factory( root, require('backbone'), exports, require('underscore') );
  11. } else if ( typeof define === 'function' && define.amd ) {
  12. // AMD
  13. define('gbone', ['underscore', 'backbone', 'zepto', 'exports'], function( _, Backbone, $, exports ) {
  14. // Export global even in AMD case in case this script is loaded with
  15. // others that may still expect a global GBone.
  16. root.Gbone = factory( root, Backbone, exports, _, $ );
  17. });
  18. } else {
  19. // Browser globals
  20. root.Gbone = factory( root, Backbone, {}, root._, ( root.jQuery || root.Zepto || root.ender ) );
  21. }
  22. }(this, function( root, Backbone, Gbone, _, $ ) {
  23. // Initial Setup
  24. // -------------
  25. // Mixins
  26. var observer, cleanup, transitions, state,
  27. // State machine for Panel Views.
  28. Manager;
  29. // Current version of the library.
  30. Gbone.VERSION = '0.1.0';
  31. // Mixins
  32. // -----------------
  33. // Each mixin operates on an object's `prototype`.
  34. // The observer mixin contains behavior for binding to events in a fashion
  35. // that can be cleaned up later.
  36. // `this.bindTo(this.collection, 'change', this.render);`
  37. // `this.unbindFromAll();`
  38. //
  39. observer = function (obj) {
  40. // On top of binding `event` to `source`, keeps track of all the event
  41. // handlers that are bound. A single call to `unbindFromAll()` will
  42. // unbind them.
  43. obj.bindTo = function (source, event, callback) {
  44. source.bind(event, callback, this);
  45. this.bindings = this.bindings || [];
  46. this.bindings.push({ source: source, event: event, callback: callback });
  47. };
  48. // Unbind all events.
  49. obj.unbindFromAll = function () {
  50. _.each(this.bindings, function (binding) {
  51. binding.source.unbind(binding.event, binding.callback);
  52. });
  53. this.bindings = [];
  54. };
  55. };
  56. // The cleanup mixin contains set of helpers for adding/managing
  57. // immediate child Views, cleaning up and housekeeping. Used with the
  58. // observer mixin. Maintains an internal array of child Views.
  59. //
  60. cleanup = function (obj) {
  61. // Cleanup child Views, DOM events, Model/Collection events
  62. // and events from this View.
  63. obj.cleanup = function () {
  64. this.unbind();
  65. if (this.unbindFromAll) this.unbindFromAll();
  66. this._cleanupChildren();
  67. this.removeFromParent();
  68. this.remove();
  69. };
  70. // Append a child View into the given `view`.
  71. obj.appendChild = function (view) {
  72. this._addChild(view);
  73. $(this.el).append(view.el);
  74. };
  75. // Append a child View into a specific `container` element in the
  76. // given `view`.
  77. obj.appendChildInto = function (view, container) {
  78. this._addChild(view);
  79. this.$(container).append(view.el);
  80. };
  81. obj._addChild = function (view) {
  82. this.children = this.children || [];
  83. this.children.push(view);
  84. view.parent = this;
  85. };
  86. obj._cleanupChildren = function () {
  87. _.each(this.children, function (view) {
  88. if (view.cleanup) view.cleanup();
  89. });
  90. };
  91. // Remove this View from its parent View.
  92. obj.removeFromParent = function () {
  93. this.parent && this.parent.removeChild(this);
  94. };
  95. // Remove the given child View `view` from this View.
  96. obj.removeChild = function (view) {
  97. var index = _.indexOf(this.children, view);
  98. this.children.splice(index, 1);
  99. };
  100. };
  101. // The transitions mixin contains functions and objects needed for
  102. // doing transition effects when Panel Views are activated/deactivated.
  103. // There are default transitions provided, but you can add your own
  104. // by using the `addTransition` method. When adding a new transition,
  105. // it must have a definition under `effects` and `reverseEffects` objects
  106. // of the Panel. It must also take in an arugment `callback`, which
  107. // is a function that will be called once the transition is complete.
  108. // Check out the default transitions code below for an example of how to setup
  109. // your own transitions.
  110. // Note that if jQuery is used, the default transitions
  111. // require GFX (http://maccman.github.com/gfx/), or if Zepto is used then
  112. // Zepto-GFX (https://github.com/gobhi/zepto-gfx).
  113. //
  114. transitions = function (obj) {
  115. // Animation options for the default transitions.
  116. var effectOptions = {
  117. duration: 450,
  118. easing: 'cubic-bezier(.25, .1, .25, 1)'
  119. },
  120. // Helper function to handle the transitions for the default
  121. // effects.
  122. handleTransition = function (that, anim, options, callback) {
  123. var l = that.transitionBindings.length,
  124. // Helper function to animate a single element.
  125. animate = function (container, ops, index) {
  126. if (!$.fn[anim]) throw new Error('$.fn.' + anim + ' is not available');
  127. if ($.fn.gfx) {
  128. // Using GFX.
  129. container[anim](ops);
  130. // Only call the callback function if this is the last animation.
  131. if (index === l-1) container.queueNext(callback);
  132. } else {
  133. // Using Zepto-GFX. Only call the callback function if this is
  134. // the last animation.
  135. (index === l-1) ? container[anim](ops, callback) : container[anim](ops);
  136. }
  137. };
  138. // Animate each element.
  139. _.each(that.transitionBindings, function(elm, index) {
  140. var container = that.$(elm);
  141. if (container.length === 0)
  142. throw new Error('The container element to animate is not \
  143. availabe in this view.');
  144. animate(container, options, index);
  145. });
  146. };
  147. // The default element(s) in the Panel to animate for the transitions.
  148. // An array of elements/selectors of the form
  149. // `['.header', '.container', '.footer', ...]`. Each element/selector
  150. // in the `transitionBindings` array represents a child DOM element
  151. // within the Panel that is to be animated. If `transitionBindings`
  152. // is not overridden, the default child element that will be animated
  153. // in the Panel View is `.container`.
  154. obj.transitionBindings = ['.container'];
  155. // Transition effects for activation.
  156. obj.effects = {
  157. // Slide in from the left.
  158. left: function (callback) {
  159. var $el = $(this.el),
  160. options = _.extend({}, effectOptions, {direction: 'left'})
  161. handleTransition(this, 'gfxSlideIn', options, callback);
  162. },
  163. // Slide in from the right.
  164. right: function (callback) {
  165. var $el = $(this.el),
  166. options = _.extend({}, effectOptions, {direction: 'right'});
  167. handleTransition(this, 'gfxSlideIn', options, callback);
  168. }
  169. };
  170. // Transition effects for deactivation.
  171. obj.reverseEffects = {
  172. // Reverse transition for the slide in from
  173. // left: slide out to the right.
  174. left: function (callback) {
  175. var $el = $(this.el),
  176. options = _.extend({}, effectOptions, {direction: 'right'});
  177. handleTransition(this, 'gfxSlideOut', options, callback);
  178. },
  179. // Reverse transition for the slide in from
  180. // right: slide out to the left.
  181. right: function (callback) {
  182. var $el = $(this.el),
  183. options = _.extend({}, effectOptions, {direction: 'left'});
  184. handleTransition(this, 'gfxSlideOut', options, callback);
  185. }
  186. };
  187. // Add a new transition. The `transition` argument is an object as follows:
  188. // `transition.effects` - Object that contains the activation transitions to be added.
  189. // `transition.reverseEffects` - Object that contains the deactivation transitions.
  190. // See the default transition effects defined above for an example.
  191. obj.addTransition = function (transition) {
  192. if (!transition.effects) throw new Error('transition.effects is not set.');
  193. if (!transition.reverseEffects) throw new Error('transition.reverseEffects \
  194. is not set.');
  195. _.extend(this.effects, transition.effects);
  196. _.extend(this.reverseEffects, transition.reverseEffects);
  197. };
  198. };
  199. // The state mixin contains methods used by the Manager to handle
  200. // activating/deactivating the Views it manages.
  201. //
  202. state = function (obj) {
  203. obj.active = function () {
  204. // Add in `active` as the first argument.
  205. Array.prototype.unshift.call(arguments, 'active');
  206. this.trigger.apply(this, arguments);
  207. };
  208. obj.isActive = function () {
  209. return $(this.el).hasClass('active');
  210. };
  211. obj._activate = function (params) {
  212. var that = this;
  213. $(this.el).addClass('active').show();
  214. // Once the transition is completed (if any), trigger the activated
  215. // event.
  216. if (params && params.trans && this.effects &&
  217. this.effects[params.trans]) {
  218. this.effects[params.trans].call(this, function() {
  219. that.trigger('activated');
  220. });
  221. } else {
  222. this.trigger('activated');
  223. }
  224. };
  225. obj._deactivate = function (params) {
  226. if (!this.isActive()) return;
  227. var that = this,
  228. callback = function () {
  229. $(that.el).removeClass('active').hide();
  230. that.trigger('deactivated');
  231. };
  232. if (params && params.trans && this.reverseEffects &&
  233. this.reverseEffects[params.trans]) {
  234. this.reverseEffects[params.trans].call(this, callback);
  235. } else {
  236. callback();
  237. }
  238. };
  239. };
  240. // Manager
  241. // -----------------
  242. // The Manager class is a state machine for managing Views.
  243. //
  244. Manager = function () {
  245. this._setup.apply(this, arguments);
  246. };
  247. _.extend(Manager.prototype, Backbone.Events, {
  248. _setup: function () {
  249. this.views = [];
  250. this.bind('change', this._change, this);
  251. this.add.apply(this, arguments);
  252. },
  253. // Add one or more Views.
  254. // `add(panel1, panel2, ...)`
  255. add: function () {
  256. _.each(Array.prototype.slice.call(arguments), function (view) {
  257. this.addOne(view);
  258. }, this);
  259. },
  260. // Add a View.
  261. addOne: function (view) {
  262. view.bind('active', function () {
  263. Array.prototype.unshift.call(arguments, view);
  264. Array.prototype.unshift.call(arguments, 'change');
  265. this.trigger.apply(this, arguments);
  266. }, this);
  267. this.views.push(view);
  268. },
  269. // Deactivate all managed Views.
  270. deactivateAll: function () {
  271. Array.prototype.unshift.call(arguments, false);
  272. Array.prototype.unshift.call(arguments, 'change');
  273. this.trigger.apply(this, arguments);
  274. },
  275. // For the View passed in - `current`, check if it's available in the
  276. // internal Views array, activate it and deactivate the others.
  277. _change: function (current) {
  278. var args = Array.prototype.slice.call(arguments, 1);
  279. _.each(this.views, function (view) {
  280. if (view === current) {
  281. view._activate.apply(view, args);
  282. } else {
  283. view._deactivate.apply(view, args);
  284. }
  285. }, this);
  286. }
  287. });
  288. // Gbone.Stage
  289. // -----------------
  290. // A Stage is a essentially a View that covers the
  291. // entire viewport. It has a default `template` (that can be
  292. // overridden), transition support and contains Panel views
  293. // that it manages using Manager.
  294. // Stages generally cover the entire viewport. Panels are nested
  295. // in a Stage and can be transitioned.
  296. // An application usually displays one Stage and Panel at a time.
  297. // The Stage's Panels can then transition in and out to show
  298. // different parts of the application.
  299. //
  300. Gbone.Stage = function (options) {
  301. this._setup(options, 'stage');
  302. Backbone.View.call(this, options);
  303. };
  304. _.extend(Gbone.Stage.prototype,
  305. Backbone.View.prototype, {
  306. // The default html `skeleton` template to be used by the Stage.
  307. // It's important that the class `viewport` be set in an element
  308. // in the `skeleton`. This element will be used by the Stage to append its
  309. // Panel Views.
  310. skeleton: _.template('<header></header><article class="viewport"> \
  311. </article><footer></footer>'),
  312. // Add Panel(s) to this Stage.
  313. add: function () {
  314. this._manager = this._manager || new Manager();
  315. this._manager.add.apply(this._manager, arguments);
  316. this._append.apply(this, arguments);
  317. },
  318. // Retrieve a Panel with a name of `name` in this Stage (if any).
  319. getPanel: function(name) {
  320. // This Stage doesn't have any Panels.
  321. if (!this._manager) return null;
  322. var views = this._manager.views;
  323. return _.find(this._manager.views, function (panel) {
  324. return panel.name === name;
  325. });
  326. },
  327. // Append Panel(s) to this Stage.
  328. _append: function () {
  329. if (this.$('.viewport').length === 0) {
  330. throw new Error('The Stage must have an element with \
  331. class \'viewport\' that will be used to append the Panels to.');
  332. }
  333. _.each(Array.prototype.slice.call(arguments), function (panel) {
  334. if (panel.stage !== this) panel.stage = this;
  335. this.appendChildInto(panel, '.viewport');
  336. }, this);
  337. },
  338. // Called in the constructor during initialization.
  339. _setup: function (options) {
  340. _.bindAll(this);
  341. options.el ? this.el = options.el : this._ensureElement();
  342. // If a `name` is not provided, create one. The name is used
  343. // primarily for setting up the routes.
  344. this.name = options.name || _.uniqueId('stage-');
  345. $(this.el).addClass('stage').html(this.skeleton());
  346. // Create a Router if one is not provided.
  347. this.router = options.router || Backbone.Router.extend();
  348. }
  349. });
  350. observer(Gbone.Stage.prototype);
  351. cleanup(Gbone.Stage.prototype);
  352. // Gbone.Panel
  353. // -----------------
  354. // Similar to a Stage, a Panel is just a View with transition
  355. // support whenever it is activated/deactivated. A Panel's
  356. // parent is a Stage and that Stage is responsible for
  357. // managing and activating/deactivating the Panel.
  358. // Usually only one Panel is shown in the application at one time.
  359. //
  360. Gbone.Panel = function (options) {
  361. this._setup(options, 'panel');
  362. Backbone.View.call(this, options);
  363. };
  364. _.extend(Gbone.Panel.prototype,
  365. Backbone.View.prototype, {
  366. // The default html `skeleton` to be used by the Panel. This can be overridden
  367. // when extending the Panel View.
  368. skeleton: _.template('<div class="container"><header></header><article></article></div>'),
  369. // Setup the routing for the Panel.
  370. // The route for a Panel is as follows: `[stage name]/[panel name]/trans-:trans`
  371. // where `trans-:trans` is optional and is used to set the transition effect.
  372. // The `callback` gets called after the routing happens. Within the callback you
  373. // should activate the Panel by calling the `active` method on it and/or
  374. // `render`etc...
  375. routePanel: function (callback) {
  376. if (this.stage) {
  377. this.stage.router.route(this.stage.name + '/' + this.name + '/trans-:trans', this.name, callback);
  378. this.stage.router.route(this.stage.name + '/' + this.name, this.name, callback);
  379. } else {
  380. throw new Error('A Stage for this Panel is not available.');
  381. }
  382. },
  383. // Called in the constructor during initialization.
  384. _setup: function (options) {
  385. _.bindAll(this);
  386. options.el ? this.el = options.el : this._ensureElement();
  387. // If a `name` is not provided, create one. The `name` is used
  388. // primarily for setting up the routes.
  389. this.name = options.name || _.uniqueId('panel-');
  390. $(this.el).addClass('panel').html(this.skeleton());
  391. if (options.stage) {
  392. this.stage = options.stage;
  393. this.stage.add(this);
  394. }
  395. }
  396. });
  397. observer(Gbone.Panel.prototype);
  398. state(Gbone.Panel.prototype);
  399. cleanup(Gbone.Panel.prototype);
  400. transitions(Gbone.Panel.prototype);
  401. Gbone.Stage.extend = Gbone.Panel.extend = Backbone.View.extend;
  402. return Gbone;
  403. }));