(function($) { // Initial Setup // ------------- var root = this, // The top-level namespace activities; if (typeof exports !== 'undefined') { activities = exports; } else { activities = root.activities = {}; } // Current version of the library. activities.VERSION = '0.3.1'; // Require jquery. if (!$ && (typeof require !== 'undefined')) { root.jQuery = $ = require('jquery'); } // Require Backbone. if (!root.Backbone && (typeof require !== 'undefined')) { root.Backbone = require('backbone'); } // Require Underscore. if (!root._ && (typeof require !== 'undefined')) { root._ = require('underscore')._; } root.Backbone.activities = activities; // Helpers // ------- activities.helpers = {}; var getPlaces = function(ActivityClass) { var place = ActivityClass.prototype.place; if (!place) { throw new Error("Activity must have a `place` property"); } if (_.isArray(place)) { return place; } else { return [ place ]; } }; var isArray = function(obj) { return _.isArray(obj); } activities.helpers.extend = Backbone.View.extend; activities.helpers.getPlaces = getPlaces; // jQuery's `$.when` method treates any non deferred objects that it's passed // as a resolved deferred. activities.helpers.resolvedPromise = null; // Event Bus // --------- // // Global eventBus used to communicate between modules. var eventBus = {}; _.extend(eventBus, Backbone.Events); activities.getEventBus = function() { return eventBus; }; // activities.History // ------------------ // Wrapper for Backbone.History, keeps it as it is only overriding the // `loadUrl` method to trigger the 'historyChange' event. function History() { return Backbone.History.apply(this, arguments); } _.extend(History.prototype, Backbone.History.prototype, { eventBus: eventBus, loadUrl: function(fragmentOverride) { var ret = Backbone.History.prototype.loadUrl.apply(this, arguments); var path = this.getFragment(); this.eventBus.trigger("historyChange", path); return true; } }); activities.History = History; activities.history || (activities.history = new History()); // Place Controller // ---------------- var placeController = { eventBus: activities.getEventBus(), goTo: function(place) { this.eventBus.trigger("placeChangeRequest", place); } }; activities.getPlaceController = function() { return placeController; } /** * Initialize `Route` with the given `path` and `options`. * * Options: * * - `sensitive` enable case-sensitive routes * - `strict` enable strict matching for trailing slashes * * @param {String} path * @param {Object} options. */ function Route(path, options) { options = options || {}; this.path = path; this.params = {}; this.regexp = this.pathRegexp(path , this.keys = [] , options.sensitive , options.strict); } /** * Check if this route matches `path`, if so * populate `.params`. * * @param {String} path * @return {Boolean} */ Route.prototype.test = function(path){ var keys = this.keys , params = this.params = [] , m = this.regexp.exec(path); if (!m) return false; for (var i = 1, len = m.length; i < len; ++i) { var key = keys[i - 1]; var val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i]; if (key) { params[key.name] = val; } else { params.push(val); } } return true; }; Route.prototype.extractParameters = function(path) { return this.params; }; Route.prototype.pathRegexp = function(path, keys, sensitive, strict) { if (path instanceof RegExp) return path; if (Array.isArray(path)) path = '(' + path.join('|') + ')'; path = path .concat(strict ? '' : '/?') .replace(/\/\(/g, '(?:/') .replace(/\+/g, '__plus__') .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ keys.push({ name: key, optional: !! optional }); slash = slash || ''; return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || ''); }) .replace(/([\/.])/g, '\\$1') .replace(/__plus__/g, '(.+)') .replace(/\*/g, '(.*)'); return new RegExp('^' + path + '$', sensitive ? '' : 'i'); } activities.Route = Route; // activities.Place // ---------------- function Place(params) { this.params = params || {}; this.initialize.apply(this, arguments); } _.extend(Place.prototype, { pattern: null, initialize: function() {}, // Builds a route from our `string` pattern and params `object`. getRoute: function() { var p, path = this.pattern; for (p in this.params) { path = path.replace(":" + p, this.params[p]); } return path; }, getParams: function() { return this.params; }, equals: function(place) { if (place instanceof this.constructor && this._equalParams(place)) { return true; } return false; }, _equalParams: function(place) { var params = place.getParams(); return _.isEqual(params, this.params); } }); Place.extend = activities.helpers.extend; activities.Place = Place; // activities.Activity // ------------------- function Activity(place) { this.currentPlace = place; this.initialize.apply(this, arguments); } _.extend(Activity.prototype, { placeController: activities.getPlaceController(), eventBus: activities.getEventBus(), // override initialize: function(place) {}, // override start: function(panel) {}, // override stop: function() {}, // override cancel: function() {}, // override mayStop: function() { return true; }, goTo: function(place) { this.placeController.goTo(place); } }); Activity.extend = activities.helpers.extend; activities.Activity = Activity; // activities.Match // ---------------- // Relates an ´Activity´ subclass to a ´Place´ subclass. function Match(Place, Activity) { this.Place = Place; this.Activity = Activity; // every place must have a ´pattern´ this.pattern = Place.prototype.pattern; } _.extend(Match.prototype, { // Tests a if a place is instance of our ´Place´ class. test: function(place) { var route, params; if (place instanceof activities.Place) { if(place instanceof this.Place) { return true; } else { return false; } } this.route = new activities.Route(this.pattern); return this.route.test(place); }, // Builds a place from a `string` path. buildPlace: function(path) { var params; if (!this.route) { throw new Error("place can only be built when the original place is a string"); } params = this.route.extractParameters(path); return new this.Place(params); } }); activities.Match = Match; // activities.DisplayRegion // ------------------------ var DisplayRegion = function(el) { this.setElement(el); } _.extend(DisplayRegion.prototype, { display: function(view) { this.close(); // Test if it's a Backbone view. if (view instanceof Backbone.View) { // first render the Backbone view. view.render(); // Insert the rendered view into de DOM. this.$el.html(view.el); } else if (view instanceof $ || typeof view === "string") { this.$el.html(view); } else { throw new TypeError("DisplayRegion#show: invalid type for `view`."); } }, close: function() { this.$el.empty(); }, show: function() { this.$el.show(); }, hide: function() { this.$el.hide(); }, setElement: function(element) { this.$el = $(element); this.el = this.$el[0]; return this; } }); DisplayRegion.extend = activities.helpers.extend; activities.DisplayRegion = DisplayRegion; // activities.ActivityManager // -------------------------- function ActivityManager(displayRegion) { this._displayRegion = displayRegion; this._matchs = []; } _.extend(ActivityManager.prototype, { eventBus: activities.getEventBus(), // Registers activities to this `ActivityManager`. It can receive a single // `Activity` or an array of them. register: function(ActivityClass) { var i=0, len; // If we received an array of activities, register each one. if (isArray(ActivityClass)) { len = ActivityClass.length; for (; i