/gbone.js
JavaScript | 497 lines | 264 code | 94 blank | 139 comment | 43 complexity | a0652275838c3dc474d3b14f70fba5bf MD5 | raw file
- // Gbone.js 0.1.0
- // (c) 2011 Gobhi Theivendran
- // Gbone.js may be freely distributed under the MIT license.
- // For all details and documentation:
- // https://github.com/gobhi/gbone.js
- (function( root, factory ) {
- // Set up Gbone appropriately for the environment.
- if ( typeof exports !== 'undefined' ) {
- // Node/CommonJS, no need for jQuery/Zepto in that case.
- factory( root, require('backbone'), exports, require('underscore') );
- } else if ( typeof define === 'function' && define.amd ) {
- // AMD
- define('gbone', ['underscore', 'backbone', 'zepto', 'exports'], function( _, Backbone, $, exports ) {
- // Export global even in AMD case in case this script is loaded with
- // others that may still expect a global GBone.
- root.Gbone = factory( root, Backbone, exports, _, $ );
- });
- } else {
- // Browser globals
- root.Gbone = factory( root, Backbone, {}, root._, ( root.jQuery || root.Zepto || root.ender ) );
- }
- }(this, function( root, Backbone, Gbone, _, $ ) {
-
- // Initial Setup
- // -------------
-
- // Mixins
- var observer, cleanup, transitions, state,
- // State machine for Panel Views.
- Manager;
- // Current version of the library.
- Gbone.VERSION = '0.1.0';
- // Mixins
- // -----------------
- // Each mixin operates on an object's `prototype`.
- // The observer mixin contains behavior for binding to events in a fashion
- // that can be cleaned up later.
- // `this.bindTo(this.collection, 'change', this.render);`
- // `this.unbindFromAll();`
- //
- observer = function (obj) {
- // On top of binding `event` to `source`, keeps track of all the event
- // handlers that are bound. A single call to `unbindFromAll()` will
- // unbind them.
- obj.bindTo = function (source, event, callback) {
- source.bind(event, callback, this);
- this.bindings = this.bindings || [];
- this.bindings.push({ source: source, event: event, callback: callback });
- };
- // Unbind all events.
- obj.unbindFromAll = function () {
- _.each(this.bindings, function (binding) {
- binding.source.unbind(binding.event, binding.callback);
- });
- this.bindings = [];
- };
- };
-
-
- // The cleanup mixin contains set of helpers for adding/managing
- // immediate child Views, cleaning up and housekeeping. Used with the
- // observer mixin. Maintains an internal array of child Views.
- //
- cleanup = function (obj) {
-
- // Cleanup child Views, DOM events, Model/Collection events
- // and events from this View.
- obj.cleanup = function () {
- this.unbind();
- if (this.unbindFromAll) this.unbindFromAll();
- this._cleanupChildren();
- this.removeFromParent();
- this.remove();
- };
-
- // Append a child View into the given `view`.
- obj.appendChild = function (view) {
- this._addChild(view);
- $(this.el).append(view.el);
- };
- // Append a child View into a specific `container` element in the
- // given `view`.
- obj.appendChildInto = function (view, container) {
- this._addChild(view);
- this.$(container).append(view.el);
- };
- obj._addChild = function (view) {
- this.children = this.children || [];
- this.children.push(view);
- view.parent = this;
- };
- obj._cleanupChildren = function () {
- _.each(this.children, function (view) {
- if (view.cleanup) view.cleanup();
- });
- };
- // Remove this View from its parent View.
- obj.removeFromParent = function () {
- this.parent && this.parent.removeChild(this);
- };
- // Remove the given child View `view` from this View.
- obj.removeChild = function (view) {
- var index = _.indexOf(this.children, view);
- this.children.splice(index, 1);
- };
- };
-
-
- // The transitions mixin contains functions and objects needed for
- // doing transition effects when Panel Views are activated/deactivated.
- // There are default transitions provided, but you can add your own
- // by using the `addTransition` method. When adding a new transition,
- // it must have a definition under `effects` and `reverseEffects` objects
- // of the Panel. It must also take in an arugment `callback`, which
- // is a function that will be called once the transition is complete.
- // Check out the default transitions code below for an example of how to setup
- // your own transitions.
- // Note that if jQuery is used, the default transitions
- // require GFX (http://maccman.github.com/gfx/), or if Zepto is used then
- // Zepto-GFX (https://github.com/gobhi/zepto-gfx).
- //
- transitions = function (obj) {
-
- // Animation options for the default transitions.
- var effectOptions = {
- duration: 450,
- easing: 'cubic-bezier(.25, .1, .25, 1)'
- },
-
- // Helper function to handle the transitions for the default
- // effects.
- handleTransition = function (that, anim, options, callback) {
-
- var l = that.transitionBindings.length,
- // Helper function to animate a single element.
- animate = function (container, ops, index) {
- if (!$.fn[anim]) throw new Error('$.fn.' + anim + ' is not available');
-
- if ($.fn.gfx) {
- // Using GFX.
- container[anim](ops);
- // Only call the callback function if this is the last animation.
- if (index === l-1) container.queueNext(callback);
- } else {
- // Using Zepto-GFX. Only call the callback function if this is
- // the last animation.
- (index === l-1) ? container[anim](ops, callback) : container[anim](ops);
- }
- };
-
- // Animate each element.
- _.each(that.transitionBindings, function(elm, index) {
- var container = that.$(elm);
- if (container.length === 0)
- throw new Error('The container element to animate is not \
- availabe in this view.');
-
- animate(container, options, index);
- });
- };
-
- // The default element(s) in the Panel to animate for the transitions.
- // An array of elements/selectors of the form
- // `['.header', '.container', '.footer', ...]`. Each element/selector
- // in the `transitionBindings` array represents a child DOM element
- // within the Panel that is to be animated. If `transitionBindings`
- // is not overridden, the default child element that will be animated
- // in the Panel View is `.container`.
- obj.transitionBindings = ['.container'];
-
- // Transition effects for activation.
- obj.effects = {
-
- // Slide in from the left.
- left: function (callback) {
- var $el = $(this.el),
- options = _.extend({}, effectOptions, {direction: 'left'})
-
- handleTransition(this, 'gfxSlideIn', options, callback);
- },
- // Slide in from the right.
- right: function (callback) {
- var $el = $(this.el),
- options = _.extend({}, effectOptions, {direction: 'right'});
-
- handleTransition(this, 'gfxSlideIn', options, callback);
- }
- };
-
-
- // Transition effects for deactivation.
- obj.reverseEffects = {
- // Reverse transition for the slide in from
- // left: slide out to the right.
- left: function (callback) {
- var $el = $(this.el),
- options = _.extend({}, effectOptions, {direction: 'right'});
-
- handleTransition(this, 'gfxSlideOut', options, callback);
- },
-
- // Reverse transition for the slide in from
- // right: slide out to the left.
- right: function (callback) {
- var $el = $(this.el),
- options = _.extend({}, effectOptions, {direction: 'left'});
-
- handleTransition(this, 'gfxSlideOut', options, callback);
- }
- };
-
- // Add a new transition. The `transition` argument is an object as follows:
- // `transition.effects` - Object that contains the activation transitions to be added.
- // `transition.reverseEffects` - Object that contains the deactivation transitions.
- // See the default transition effects defined above for an example.
- obj.addTransition = function (transition) {
- if (!transition.effects) throw new Error('transition.effects is not set.');
- if (!transition.reverseEffects) throw new Error('transition.reverseEffects \
- is not set.');
- _.extend(this.effects, transition.effects);
- _.extend(this.reverseEffects, transition.reverseEffects);
- };
-
- };
-
-
- // The state mixin contains methods used by the Manager to handle
- // activating/deactivating the Views it manages.
- //
- state = function (obj) {
-
- obj.active = function () {
- // Add in `active` as the first argument.
- Array.prototype.unshift.call(arguments, 'active');
- this.trigger.apply(this, arguments);
- };
- obj.isActive = function () {
- return $(this.el).hasClass('active');
- };
- obj._activate = function (params) {
- var that = this;
-
- $(this.el).addClass('active').show();
- // Once the transition is completed (if any), trigger the activated
- // event.
- if (params && params.trans && this.effects &&
- this.effects[params.trans]) {
- this.effects[params.trans].call(this, function() {
- that.trigger('activated');
- });
- } else {
- this.trigger('activated');
- }
- };
- obj._deactivate = function (params) {
- if (!this.isActive()) return;
- var that = this,
- callback = function () {
- $(that.el).removeClass('active').hide();
- that.trigger('deactivated');
- };
- if (params && params.trans && this.reverseEffects &&
- this.reverseEffects[params.trans]) {
- this.reverseEffects[params.trans].call(this, callback);
- } else {
- callback();
- }
- };
- };
-
-
- // Manager
- // -----------------
- // The Manager class is a state machine for managing Views.
- //
- Manager = function () {
- this._setup.apply(this, arguments);
- };
- _.extend(Manager.prototype, Backbone.Events, {
-
- _setup: function () {
- this.views = [];
- this.bind('change', this._change, this);
- this.add.apply(this, arguments);
- },
- // Add one or more Views.
- // `add(panel1, panel2, ...)`
- add: function () {
- _.each(Array.prototype.slice.call(arguments), function (view) {
- this.addOne(view);
- }, this);
- },
- // Add a View.
- addOne: function (view) {
- view.bind('active', function () {
- Array.prototype.unshift.call(arguments, view);
- Array.prototype.unshift.call(arguments, 'change');
- this.trigger.apply(this, arguments);
- }, this);
- this.views.push(view);
- },
-
- // Deactivate all managed Views.
- deactivateAll: function () {
- Array.prototype.unshift.call(arguments, false);
- Array.prototype.unshift.call(arguments, 'change');
- this.trigger.apply(this, arguments);
- },
-
- // For the View passed in - `current`, check if it's available in the
- // internal Views array, activate it and deactivate the others.
- _change: function (current) {
- var args = Array.prototype.slice.call(arguments, 1);
- _.each(this.views, function (view) {
- if (view === current) {
- view._activate.apply(view, args);
- } else {
- view._deactivate.apply(view, args);
- }
- }, this);
- }
- });
-
-
- // Gbone.Stage
- // -----------------
- // A Stage is a essentially a View that covers the
- // entire viewport. It has a default `template` (that can be
- // overridden), transition support and contains Panel views
- // that it manages using Manager.
- // Stages generally cover the entire viewport. Panels are nested
- // in a Stage and can be transitioned.
- // An application usually displays one Stage and Panel at a time.
- // The Stage's Panels can then transition in and out to show
- // different parts of the application.
- //
- Gbone.Stage = function (options) {
- this._setup(options, 'stage');
- Backbone.View.call(this, options);
- };
- _.extend(Gbone.Stage.prototype,
- Backbone.View.prototype, {
-
- // The default html `skeleton` template to be used by the Stage.
- // It's important that the class `viewport` be set in an element
- // in the `skeleton`. This element will be used by the Stage to append its
- // Panel Views.
- skeleton: _.template('<header></header><article class="viewport"> \
- </article><footer></footer>'),
-
-
- // Add Panel(s) to this Stage.
- add: function () {
- this._manager = this._manager || new Manager();
- this._manager.add.apply(this._manager, arguments);
- this._append.apply(this, arguments);
- },
-
- // Retrieve a Panel with a name of `name` in this Stage (if any).
- getPanel: function(name) {
- // This Stage doesn't have any Panels.
- if (!this._manager) return null;
- var views = this._manager.views;
- return _.find(this._manager.views, function (panel) {
- return panel.name === name;
- });
- },
- // Append Panel(s) to this Stage.
- _append: function () {
- if (this.$('.viewport').length === 0) {
- throw new Error('The Stage must have an element with \
- class \'viewport\' that will be used to append the Panels to.');
- }
-
- _.each(Array.prototype.slice.call(arguments), function (panel) {
- if (panel.stage !== this) panel.stage = this;
- this.appendChildInto(panel, '.viewport');
- }, this);
-
- },
-
- // Called in the constructor during initialization.
- _setup: function (options) {
- _.bindAll(this);
- options.el ? this.el = options.el : this._ensureElement();
-
- // If a `name` is not provided, create one. The name is used
- // primarily for setting up the routes.
- this.name = options.name || _.uniqueId('stage-');
- $(this.el).addClass('stage').html(this.skeleton());
-
- // Create a Router if one is not provided.
- this.router = options.router || Backbone.Router.extend();
- }
-
- });
-
- observer(Gbone.Stage.prototype);
- cleanup(Gbone.Stage.prototype);
-
-
- // Gbone.Panel
- // -----------------
- // Similar to a Stage, a Panel is just a View with transition
- // support whenever it is activated/deactivated. A Panel's
- // parent is a Stage and that Stage is responsible for
- // managing and activating/deactivating the Panel.
- // Usually only one Panel is shown in the application at one time.
- //
- Gbone.Panel = function (options) {
- this._setup(options, 'panel');
- Backbone.View.call(this, options);
- };
- _.extend(Gbone.Panel.prototype,
- Backbone.View.prototype, {
-
- // The default html `skeleton` to be used by the Panel. This can be overridden
- // when extending the Panel View.
- skeleton: _.template('<div class="container"><header></header><article></article></div>'),
- // Setup the routing for the Panel.
- // The route for a Panel is as follows: `[stage name]/[panel name]/trans-:trans`
- // where `trans-:trans` is optional and is used to set the transition effect.
- // The `callback` gets called after the routing happens. Within the callback you
- // should activate the Panel by calling the `active` method on it and/or
- // `render`etc...
- routePanel: function (callback) {
- if (this.stage) {
- this.stage.router.route(this.stage.name + '/' + this.name + '/trans-:trans', this.name, callback);
- this.stage.router.route(this.stage.name + '/' + this.name, this.name, callback);
- } else {
- throw new Error('A Stage for this Panel is not available.');
- }
- },
-
- // Called in the constructor during initialization.
- _setup: function (options) {
- _.bindAll(this);
- options.el ? this.el = options.el : this._ensureElement();
- // If a `name` is not provided, create one. The `name` is used
- // primarily for setting up the routes.
- this.name = options.name || _.uniqueId('panel-');
- $(this.el).addClass('panel').html(this.skeleton());
- if (options.stage) {
- this.stage = options.stage;
- this.stage.add(this);
- }
- }
- });
- observer(Gbone.Panel.prototype);
- state(Gbone.Panel.prototype);
- cleanup(Gbone.Panel.prototype);
- transitions(Gbone.Panel.prototype);
-
- Gbone.Stage.extend = Gbone.Panel.extend = Backbone.View.extend;
- return Gbone;
- }));