(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__slice = [].slice;
(function(root, factory) {
var $, Backbone, _;
if (typeof define !== "undefined" && define !== null ? define.amd : void 0) {
return define('backbone.giraffe', ['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
return root.Giraffe = factory(root, $, _, Backbone);
});
} else if (typeof module !== "undefined" && module !== null ? module.exports : void 0) {
$ = require('jquery');
_ = require('underscore');
Backbone = require('backbone');
return module.exports = factory(root, $, _, Backbone);
} else {
return root.Giraffe = factory(root, root.$, root._, root.Backbone);
}
})(this, function(root, $, _, Backbone) {
var $document, $window, Giraffe, previousGiraffe, _afterInitialize, _setEventBindings, _setEventMapBindings;
if (!$) {
throw new Error('Giraffe cannot find jQuery');
}
if (!_) {
throw new Error('Giraffe cannot find Underscore');
}
if (!Backbone) {
throw new Error('Giraffe cannot find Backbone');
}
Giraffe = Backbone.Giraffe = {
version: '0.2.8',
app: null,
apps: {},
views: {}
};
previousGiraffe = root.Giraffe;
Giraffe.noConflict = function() {
root.Giraffe = previousGiraffe;
return this;
};
$window = $(window);
$document = $(document);
/*
* __Giraffe.View__ is optimized for simplicity and flexibility. Views can move
* around the DOM safely and freely with the `attachTo` method, which accepts any
* selector, DOM element, or view, as well as an optional __jQuery__ insertion
* method like `'prepend'`, `'after'`, or `'html'`. The default is `'append'`.
*
* var parentView = new Giraffe.View();
* parentView.attachTo('body', {method: 'prepend'});
* $('body').find(parentView.$el).length; // => 1
*
* The `attachTo` method automatically sets up parent-child relationships between
* views via the references `children` and `parent` to allow nesting with no
* extra work.
*
* var childView = new Giraffe.View();
* childView.attachTo(parentView); // or `parentView.attach(childView);`
* childView.parent === parentView; // => true
* parentView.children[0] === childView; // => true
*
* Views automatically manage the lifecycle of all `children`, and any object
* with a `dispose` method can be added to `children` via `addChild`.
* When a view is disposed, it disposes of all of its `children`, allowing the
* disposal of an entire application with a single method call.
*
* parentView.dispose(); // disposes both `parentView` and `childView`
*
* When a view is attached, `render` is called if it has not yet been rendered.
* When a view renders, it first calls `detach` on all of its `children`, and
* when a view is detached, the default behavior is to call `dispose` on it.
* To overried this behavior and cache a view even when its `parent` renders, you
* can set the cached view's `disposeOnDetach` property to `false`.
*
* var parentView = new Giraffe.View();
* parentView.attach(new Giraffe.View());
* parentView.attach(new Giraffe.View({disposeOnDetach: false}));
* parentView.attachTo('body'); // render() is called, disposes of the first view
* parentView.children.length; // => 1
*
* Views are not automatically reattached after `render`, so you retain control,
* but their parent-child relationships stay intact unless they're disposed.
* See [`Giraffe.View#afterRender`](#View-afterRender) for more.
*
* __Giraffe.View__ gets much of its smarts by way of the `data-view-cid`
* attribute attached to `view.$el`. This attribute allows us to find a view's
* parent when attached to a DOM element and safely detach views when they would
* otherwise be clobbered.
*
* Currently, __Giraffe__ has only one class that extends __Giraffe.View__,
* __Giraffe.App__, which encapsulates app-wide messaging and routing.
*
* Like all __Giraffe__ objects, __Giraffe.View__ extends each instance with
* every property in `options`.
*
* @param {Object} [options]
*/
Giraffe.View = (function(_super) {
__extends(View, _super);
View.defaultOptions = {
disposeOnDetach: true
};
function View(options) {
this.render = __bind(this.render, this);
Giraffe.configure(this, options);
/*
* When one view is attached to another, the child view is added to the
* parent's `children` array. When `dispose` is called on a view, it disposes
* of all `children`, enabling the teardown of a single view or an entire app
* with one method call. Any object with a `dispose` method can be added
* to a view's `children` via `addChild` to take advantage of lifecycle
* management.
*/
this.children = [];
/*
* Child views attached via `attachTo` have a reference to their parent view.
*/
this.parent = null;
this._renderedOnce = false;
this._isAttached = false;
this._createEventsFromUIElements();
if (typeof this.templateStrategy === 'string') {
Giraffe.View.setTemplateStrategy(this.templateStrategy, this);
}
View.__super__.constructor.apply(this, arguments);
}
View.prototype._beforeInitialize = function() {
this._cache();
this.$el.attr('data-view-cid', this.cid);
this.setParent(Giraffe.View.getClosestView(this.$el));
return this._cacheUiElements();
};
View.prototype._attachMethods = ['append', 'prepend', 'html', 'after', 'before', 'insertAfter', 'insertBefore'];
View.prototype._siblingAttachMethods = ['after', 'before', 'insertAfter', 'insertBefore'];
/*
* Attaches this view to `el`, which can be a selector, DOM element, or view.
* If `el` is inside another view, a parent-child relationship is set up.
* `options.method` is the __jQuery__ method used to attach the view. It
* defaults to `'append'` and also accepts `'prepend'`, `'after'`, `'before'`,
* and `'html'`. If the view has not yet been rendered when attached, `render`
* is called. This `render` behavior can be overridden via
* `options.forceRender` and `options.suppressRender`. See the
* [_View Basics_ example](viewBasics.html) for more.
* Triggers `attaching` and `attached` events.
*
* @param {String/Element/$/Giraffe.View} el A view, selector, or DOM element to attach `view.$el` to.
* @param {Object} [options]
* {String} method The jQuery method used to put this view in `el`. Accepts `'append'`, `'prepend'`, `'html'`, `'after'`, and `'before'`. Defaults to `'append'`.
* {Boolean} forceRender Calls `render` when attached, even if the view has already been rendered.
* {Boolean} suppressRender Prevents `render` when attached, even if the view hasn't yet been rendered.
*/
View.prototype.attachTo = function(el, options) {
var $container, $el, forceRender, method, shouldRender, suppressRender;
method = (options != null ? options.method : void 0) || 'append';
forceRender = (options != null ? options.forceRender : void 0) || false;
suppressRender = (options != null ? options.suppressRender : void 0) || false;
if (!this.$el) {
throw new Error('Trying to attach a disposed view. Make a new one or create the view with the option `disposeOnDetach` set to false.');
}
if (!_.contains(this._attachMethods, method)) {
throw new Error("The attach method '" + method + "' isn't supported - must be one of [" + this._attachMethods + "].");
}
$el = Giraffe.View.to$El(el);
if ($el.length !== 1) {
throw new Error("Expected to attach to a single element but found " + $el.length + " elements");
}
this.trigger('attaching', this, $el, options);
$container = _.contains(this._siblingAttachMethods, method) ? $el.parent() : $el;
if (method === 'insertAfter') {
method = 'after';
}
if (method === 'insertBefore') {
method = 'before';
}
this.detach(true);
this.setParent(Giraffe.View.getClosestView($container));
if (method === 'html') {
Giraffe.View.detachByEl($el);
$el.empty();
}
$el[method](this.$el);
this._isAttached = true;
shouldRender = !suppressRender && (!this._renderedOnce || forceRender || this.alwaysRender);
if (shouldRender) {
this.render(options);
}
if (this.saveScrollPosition) {
this._loadScrollPosition();
}
if (this.documentTitle != null) {
document.title = this.documentTitle;
}
this.trigger('attached', this, $el, options);
return this;
};
/*
* `attach` is an inverted way to call `attachTo`. Unlike `attachTo`, calling
* this function requires a parent view. It's here only for aesthetics. Takes
* the same `options` as `attachTo` in addition to the optional `options.el`,
* which is the first argument passed to `attachTo`, defaulting to the parent
* view.
*
* @param {View} view
* @param {Object} [options]
* @caption parentView.attach(childView, [options])
*/
View.prototype.attach = function(view, options) {
var childEl, target;
target = null;
if (options != null ? options.el : void 0) {
childEl = Giraffe.View.to$El(options.el, this.$el, true);
if (childEl.length) {
target = childEl;
} else {
throw new Error('Attempting to attach to an element that doesn\'t exist inside this view!');
}
} else {
target = this.$el;
}
view.attachTo(target, options);
return this;
};
/*
* __Giraffe__ implements `render` so it can do some helpful things, but you can
* still call it like you normally would. By default, `render` uses a view's
* `template`, which is the DOM selector of an __Underscore__ template, but
* this is easily configured. See [`Giraffe.View#template`](#View-template),
* [`Giraffe.View.setTemplateStrategy`](#View-setTemplateStrategy), and
* [`Giraffe.View#templateStrategy`](#View-templateStrategy) for more.
*
* @caption Do not override unless you know what you're doing!
*/
View.prototype.render = function(options) {
var html;
this.trigger('rendering', this, options);
this.beforeRender.apply(this, arguments);
this._renderedOnce = true;
this.detachChildren(options != null ? options.preserve : void 0);
html = this.templateStrategy.apply(this, arguments) || '';
this.$el.empty()[0].innerHTML = html;
this._cacheUiElements();
this.afterRender.apply(this, arguments);
this.trigger('rendered', this, options);
return this;
};
/*
* This is an empty function hook for you to implement. Less commonly used than
* `afterRender`, but helpful in circumstances where the DOM has state that
* needs to be preserved across renders. For example, if a view with a dropdown
* menu is rendering, you may want to save its open state in `beforeRender`
* and reapply it in `afterRender`.
*
* @caption Implement this function as needed in your views.
*/
View.prototype.beforeRender = function() {};
/*
* This is an empty function hook for you to implement. After a view renders,
* `afterRender` is called. Child views are normally created and attached to the DOM here.
* Views that are cached by setting `disposeOnDetach` to `false` will be
* in `view.children` in `afterRender`, but will not be attached to the
* parent's `$el`.
*
* @caption Implement this function as needed in your views.
*/
View.prototype.afterRender = function() {};
/*
* __Giraffe__ implements its own `render` function which calls `templateStrategy`
* to get the HTML string to put inside `view.$el`. Your views can either
* define a `template`, which uses __Underscore__ templates by default and is
* customizable via [`Giraffe.View#setTemplateStrategy`](#View-setTemplateStrategy),
* or override `templateStrategy` with a function returning a string of HTML
* from your favorite templating engine. See the
* [_Template Strategies_ example](templateStrategies.html) for more.
*/
View.prototype.templateStrategy = function() {
return '';
};
/*
* Consumed by the `templateStrategy` function created by
* [`Giraffe.View#setTemplateStrategy`](#View-setTemplateStrategy). By default,
* `template` is the DOM selector of an __Underscore__ template. See the
* [_Template Strategies_ example](templateStrategies.html) for more.
*
* // the default `templateStrategy` is 'underscore-template-selector'
* view.template = '#my-template-selector';
* // or
* Giraffe.View.setTemplateStrategy('underscore-template');
* view.template = '
hello <%= name %>
';
* // or
* Giraffe.View.setTemplateStrategy('jst');
* view.template = function(data) { return 'hello' + data.name + '
'};
*/
View.prototype.template = null;
/*
* Gets the data passed to the `template`. Returns the view by default.
*
* @caption Override this function to pass custom data to a view's `template`.
*/
View.prototype.serialize = function() {
return this;
};
/*
* Detaches the view from the DOM. If `view.disposeOnDetach` is `true`,
* which is the default, `dispose` will be called on the view and its
* `children` unless the argument `preserve` is `true`.
* When a view renders, it first calls `detach(false)` on the views inside its `$el`.
*
* @param {Boolean} [preserve] If `true`, doesn't dispose of the view, even if `disposeOnDetach` is `true`. Defaults to `false`.
*/
View.prototype.detach = function(preserve) {
if (preserve == null) {
preserve = false;
}
if (!this._isAttached) {
return this;
}
this._isAttached = false;
if (this.saveScrollPosition) {
this._saveScrollPosition();
}
this.trigger('detaching', this, preserve);
this.$el.detach();
this.trigger('detached', this, preserve);
if (this.disposeOnDetach && !preserve) {
this.dispose();
}
return this;
};
/*
* Calls `detach` on each object in `children`, passing `preserve` through.
*
* @param {Boolean} [preserve]
*/
View.prototype.detachChildren = function(preserve) {
var child, _i, _len, _ref;
if (preserve == null) {
preserve = false;
}
_ref = this.children.slice();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
child = _ref[_i];
if (typeof child.detach === "function") {
child.detach(preserve);
}
}
return this;
};
View.prototype._saveScrollPosition = function() {
this._scrollPosition = this._getScrollPositionEl().scrollTop();
return this;
};
View.prototype._loadScrollPosition = function() {
if (this._scrollPosition != null) {
this._getScrollPositionEl().scrollTop(this._scrollPosition);
}
return this;
};
View.prototype._getScrollPositionEl = function() {
var $el;
if (typeof this.saveScrollPosition === 'boolean' || this.$el.is(this.saveScrollPosition)) {
return this.$el;
} else {
$el = Giraffe.View.to$El(this.saveScrollPosition, this.$el).first();
if ($el.length) {
return $el;
} else {
$el = Giraffe.View.to$El(this.saveScrollPosition).first();
if ($el.length) {
return $el;
} else {
return this.$el;
}
}
}
};
/*
* Adds `child` to this view's `children` and assigns this view as
* `child.parent`. If `child` implements `dispose`, it will be called when the
* view is disposed. If `child` implements `detach`, it will be called before
* the view renders.
*
* @param {Object} child
*/
View.prototype.addChild = function(child) {
var _ref;
if (!_.contains(this.children, child)) {
if ((_ref = child.parent) != null) {
_ref.removeChild(child, true);
}
child.parent = this;
this.children.push(child);
}
return this;
};
/*
* Calls `addChild` on the given array of objects.
*
* @param {Array} children Array of objects
*/
View.prototype.addChildren = function(children) {
var child, _i, _len;
for (_i = 0, _len = children.length; _i < _len; _i++) {
child = children[_i];
this.addChild(child);
}
return this;
};
/*
* Removes an object from this view's `children`. If `preserve` is `false`, the
* default, __Giraffe__ will attempt to call `dispose` on the child. If
* `preserve` is true, __Giraffe__ will attempt to call `detach(true)` on the
* child.
*
* @param {Object} child
* @param {Boolean} [preserve] If `true`, Giraffe attempts to call `detach` on the child, otherwise it attempts to call `dispose` on the child. Is `false` by default.
*/
View.prototype.removeChild = function(child, preserve) {
var index;
if (preserve == null) {
preserve = false;
}
index = _.indexOf(this.children, child);
if (index !== -1) {
this.children.splice(index, 1);
child.parent = null;
if (preserve) {
if (typeof child.detach === "function") {
child.detach(true);
}
} else {
if (typeof child.dispose === "function") {
child.dispose();
}
}
}
return this;
};
/*
* Calls `removeChild` on all `children`, passing `preserve` through.
*
* @param {Boolean} [preserve] If `true`, detaches rather than removes the children.
*/
View.prototype.removeChildren = function(preserve) {
var child, _i, _len, _ref;
if (preserve == null) {
preserve = false;
}
_ref = this.children.slice();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
child = _ref[_i];
this.removeChild(child, preserve);
}
return this;
};
/*
* Sets a new parent for a view, first removing any current parent-child
* relationship. `parent` can be falsy to remove the current parent.
*
* @param {Giraffe.View} [parent]
*/
View.prototype.setParent = function(parent) {
if (parent && parent !== this) {
parent.addChild(this);
} else if (this.parent) {
this.parent.removeChild(this, true);
this.parent = null;
}
return this;
};
/*
* If `el` is `null` or `undefined`, tests if the view is somewhere on the DOM
* by calling `$document.find(view.$el)`. If `el` is a view, tests if `el` contains
* this view. Otherwise, tests if `el` is the immediate parent of `view.$el`.
*
* @param {String} [el] Optional selector, DOM element, or view to test against the view's immediate parent.
* @returns {Boolean}
*/
View.prototype.isAttached = function(el) {
if (el != null) {
if (el.$el) {
return !!el.$el.find(this.$el).length;
} else {
return this.$el.parent().is(el);
}
} else {
return !!$document.find(this.$el).length;
}
};
/*
* `ui` is an optional view property that helps DRY up DOM references in views.
* It provides a convenient way to cache __jQuery__ objects after every `render`,
* and the names given to these objects can be used in `Backbone.View#events`.
* Declaring `this.ui = {$button: '#button'}` in a view makes `this.$button`
* always available once `render` has been called. Typically the `ui` value is
* a string which is then searched for inside `this.$el`, but if it's a
* function, its return value will be assigned. If it's neither a string nor a
* function, the value itself is assigned.
*
* Giraffe.View.extend({
* ui: {
* $someButton: '#some-button-selector'
* },
* afterRender: {
* this.$someButton; // just got cached
* },
* events: {
* '#click $someButton': 'onClickSomeButton' // ui names work here
* }
* });
*/
View.prototype.ui = null;
View.prototype._cacheUiElements = function() {
var name, selector, _ref;
if (this.ui) {
_ref = this.ui;
for (name in _ref) {
selector = _ref[name];
this[name] = (function() {
switch (typeof selector) {
case 'string':
return this.$(selector);
case 'function':
return selector.call(this);
default:
return selector;
}
}).call(this);
}
}
return this;
};
View.prototype._uncacheUiElements = function() {
var name;
if (this.ui) {
for (name in this.ui) {
delete this[name];
}
}
return this;
};
View.prototype._createEventsFromUIElements = function() {
var eventKey, method, newEventKey, _ref;
if (!(this.events && this.ui)) {
return this;
}
if (typeof this.ui === 'function') {
this.ui = this.ui.call(this);
}
if (typeof this.events === 'function') {
this.events = this.events.call(this);
}
_ref = this.events;
for (eventKey in _ref) {
method = _ref[eventKey];
newEventKey = this._getEventKeyFromUIElements(eventKey);
if (newEventKey !== eventKey) {
delete this.events[eventKey];
this.events[newEventKey] = method;
}
}
return this;
};
View.prototype._getEventKeyFromUIElements = function(eventKey) {
var lastPart, length, parts, uiTarget;
parts = eventKey.split(' ');
length = parts.length;
if (length < 2) {
return eventKey;
}
lastPart = parts[length - 1];
uiTarget = this.ui[lastPart];
if (uiTarget) {
parts[length - 1] = uiTarget;
return parts.join(' ');
} else {
return eventKey;
}
};
/*
* Inspired by `Backbone.View#events`, `dataEvents` binds a space-separated
* list of events ending with the target object to methods on a view.
* It is a shorthand way of calling `view.listenTo(targetObj, event, cb)`.
* In this example `collection` is used, but any object on the view that
* implements __Backbone.Events__ is a valid target object. To have a view
* listen to itself, the keywords `'this'` and `'@'` can be used.
*
* Giraffe.View.extend({
* dataEvents: {
* 'add remove change collection': 'render',
* 'event anotherEvent targetObj': function() {},
* 'eventOnThisView @': 'methodName'
* }
* });
*
* As a result of using `listenTo`, `dataEvents` accepts multiple events per
* definition, handlers are called in the context of the view, and
* bindings are cleaned up in `dispose` via `stopListening`.
*
* There are some unfortunate restrictions to `dataEvents`. Objects created
* after `initialize` will not be bound to, and events fired during the
* `constructor` and `initialize` will not be heard. We advocate using
* `Backbone.Events#listenTo` directly in these circumstances.
*
* See the [__Data Events__ example](dataEvents.html) for more.
*/
View.prototype.dataEvents = null;
View.prototype._uncache = function() {
delete Giraffe.views[this.cid];
return this;
};
View.prototype._cache = function() {
Giraffe.views[this.cid] = this;
return this;
};
/*
* Calls `methodName` on the view, or if not found, up the view hierarchy until
* it either finds the method or fails on a view without a `parent`. Used by
* __Giraffe__ to call the methods defined for the events bound in
* `Giraffe.View.setDocumentEvents`.
*
* @param {String} methodName
* @param {Any} [args...]
*/
View.prototype.invoke = function() {
var args, methodName, view;
methodName = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
view = this;
while (view && !view[methodName]) {
view = view.parent;
}
if (typeof (view != null ? view[methodName] : void 0) === 'function') {
return view[methodName].apply(view, args);
} else {
throw new Error("No such method name in view hierarchy '" + methodName + "'");
}
};
/*
* See [`Giraffe.App#appEvents`](#App-appEvents).
*/
View.prototype.appEvents = null;
/*
* Destroys a view, unbinding its events and freeing its resources. Calls
* `Backbone.View#remove` and calls `dispose` on all `children`.
*/
View.prototype._dispose = function() {
this.setParent(null);
this.removeChildren();
this._uncacheUiElements();
this._uncache();
this._isAttached = false;
if (this.$el) {
this.remove();
this.$el = null;
} else {
throw new Error('Disposed of a view that has already been disposed');
}
return this;
};
/*
* This is an empty function hook for you to implement.
*
* @caption Implement this function as needed in your Giraffe and `Giraffe.configure`d objects.
*/
View.prototype.beforeDispose = function() {};
/*
* This is an empty function hook for you to implement.
*
* @caption Implement this function as needed in your Giraffe and `Giraffe.configure`d objects.
*/
View.prototype.afterDispose = function() {};
/*
* This is an empty function hook for you to implement.
*
* @caption Implement this function as needed in your Giraffe and `Giraffe.configure`d objects.
*/
View.prototype.beforeInitialize = function() {};
/*
* This is an empty function hook for you to implement.
*
* @caption Implement this function as needed in your Giraffe and `Giraffe.configure`d objects.
*/
View.prototype.afterInitialize = function() {};
/*
* Detaches the top-level views inside `el`, which can be a selector, element,
* or __Giraffe.View__. Used internally by __Giraffe__ to remove views that
* would otherwise be clobbered when the option `method: 'html'` is used
* in `attachTo`. Uses the `data-view-cid` attribute to match DOM nodes to view
* instances.
*
* @param {String/Element/$/Giraffe.View} el
* @param {Boolean} [preserve]
*/
View.detachByEl = function(el, preserve) {
var $child, $el, cid, view;
if (preserve == null) {
preserve = false;
}
$el = Giraffe.View.to$El(el);
while (($child = $el.find('[data-view-cid]:first')).length) {
cid = $child.attr('data-view-cid');
view = Giraffe.View.getByCid(cid);
view.detach(preserve);
}
return this;
};
/*
* Gets the closest parent view of `el`, which can be a selector, element, or
* __Giraffe.View__. Uses the `data-view-cid` attribute to match DOM nodes to
* view instances.
*
* @param {String/Element/$/Giraffe.View} el
*/
View.getClosestView = function(el) {
var $el, cid;
$el = Giraffe.View.to$El(el);
cid = $el.closest('[data-view-cid]').attr('data-view-cid');
return Giraffe.View.getByCid(cid);
};
/*
* Looks up a view from the cache by `cid`, returning undefined if not found.
*
* @param {String} cid
*/
View.getByCid = function(cid) {
return Giraffe.views[cid];
};
/*
* Gets a __jQuery__ object from `el`, which can be a selector, element,
* __jQuery__ object, or __Giraffe.View__, scoped by an optional `parent`,
* which has the same available types as `el`. If the third parameter is
* truthy, `el` can be the same element as `parent`.
*
* @param {String/Element/$/Giraffe.View} el
* @param {String/Element/$/Giraffe.View} [parent] Opitional. Scopes `el` if provided.
* @param {Boolean} [allowParentMatch] Optional. If truthy, `el` can be `parent`.
*/
View.to$El = function(el, parent, allowParentMatch) {
var $parent;
if (allowParentMatch == null) {
allowParentMatch = false;
}
if (parent) {
$parent = Giraffe.View.to$El(parent);
if (el != null ? el.$el : void 0) {
el = el.$el;
}
if (allowParentMatch && $parent.is(el)) {
return $parent;
} else {
return $parent.find(el);
}
} else if (el != null ? el.$el : void 0) {
return el.$el;
} else if (el instanceof $) {
return el;
} else {
return $(el);
}
};
/*
* __Giraffe__ provides a convenient high-performance way to declare view
* method calls in your HTML markup. Using the form
* `data-gf-eventName='methodName'`, when a bound DOM event is triggered,
* __Giraffe__ looks for the defined method on the element's view. For example,
* putting `data-gf-click='onSubmitForm'` on a button calls the method
* `onSubmitForm` on its view on `'click'`. If the view does not define the
* method, __Giraffe__ searches up the view hierarchy until it finds it or runs
* out of views. By default, only the `click` and `change` events are bound by
* __Giraffe__, but `setDocumentEvents` allows you to set a custom list of
* events, first unbinding the existing ones and then setting the ones you give
* it, if any.
*
* Giraffe.View.setDocumentEvents(['click', 'change']); // default
* // or
* Giraffe.View.setDocumentEvents(['click', 'change', 'keydown']);
* // or
* Giraffe.View.setDocumentEvents('click change keydown keyup');
*
* @param {Array/String} events An array or space-separated string of DOM events to bind to the document.
*/
View.setDocumentEvents = function(events, prefix) {
var attr, event, selector, _fn, _i, _len;
if (prefix == null) {
prefix = Giraffe.View._documentEventPrefix;
}
prefix = prefix || '';
if (typeof events === 'string') {
events = events.split(' ');
}
if (!_.isArray(events)) {
events = [events];
}
events = _.compact(events);
Giraffe.View.removeDocumentEvents();
Giraffe.View._currentDocumentEvents = events;
Giraffe.View._documentEventPrefix = prefix;
_fn = function(event, attr, selector) {
return $document.on(event, selector, function(e) {
var $target, method, view;
$target = $(e.target).closest(selector);
method = $target.attr(attr);
view = Giraffe.View.getClosestView($target);
return view.invoke(method, e);
});
};
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
attr = prefix + event;
selector = '[' + attr + ']';
_fn(event, attr, selector);
}
return events;
};
/*
* Equivalent to `Giraffe.View.setDocumentEvents(null)`.
*/
View.removeDocumentEvents = function(prefix) {
var currentEvents, event, selector, _i, _len;
if (prefix == null) {
prefix = Giraffe.View._documentEventPrefix;
}
prefix = prefix || '';
currentEvents = Giraffe.View._currentDocumentEvents;
if (!(currentEvents != null ? currentEvents.length : void 0)) {
return;
}
for (_i = 0, _len = currentEvents.length; _i < _len; _i++) {
event = currentEvents[_i];
selector = '[' + prefix + event + ']';
$document.off(event, selector);
}
return Giraffe.View._currentDocumentEvents = null;
};
/*
* Sets the prefix for document events. Defaults to `data-gf-`,
* so to bind to `'click'` events, one would put the `data-gf-click`
* attribute on DOM elements with the name of a view method as the value.
*
* @param {String} prefix If `null` or `undefined`, defaults to the empty string.
*/
View.setDocumentEventPrefix = function(prefix) {
if (prefix == null) {
prefix = '';
}
return Giraffe.View.setDocumentEvents(Giraffe.View._currentDocumentEvents, prefix);
};
/*
* Giraffe provides common strategies for templating.
*
* The `strategy` argument can be a function returning an HTML string or one of the following:
*
* - `'underscore-template-selector'`
*
* - `view.template` is a string or function returning DOM selector
*
* - `'underscore-template'`
*
* - `view.template` is a string or function returning underscore template
*
* - `'jst'`
*
* - `view.template` is an html string or a JST function
*
* See the [_Template Strategies_ example](templateStrategies.html) for more.
*
* @param {String} strategy Choose 'underscore-template-selector', 'underscore-template', 'jst'
*
*/
View.setTemplateStrategy = function(strategy, instance) {
var strategyType, templateStrategy;
strategyType = typeof strategy;
if (strategyType === 'function') {
templateStrategy = strategy;
} else if (strategyType !== 'string') {
throw new Error("Unrecognized template strategy '" + strategy + "'");
} else {
switch (strategy.toLowerCase()) {
case 'underscore-template-selector':
templateStrategy = function() {
var selector,
_this = this;
if (!this.template) {
return '';
}
if (!this._templateFn) {
switch (typeof this.template) {
case 'string':
selector = this.template;
this._templateFn = _.template($(selector).html() || '');
break;
case 'function':
this._templateFn = function(locals) {
selector = _this.template();
return _.template($(selector).html() || '', locals);
};
break;
default:
throw new Error('this.template must be string or function');
}
}
return this._templateFn(this.serialize.apply(this, arguments));
};
break;
case 'underscore-template':
templateStrategy = function() {
var _this = this;
if (!this.template) {
return '';
}
if (!this._templateFn) {
switch (typeof this.template) {
case 'string':
this._templateFn = _.template(this.template);
break;
case 'function':
this._templateFn = function(locals) {
return _.template(_this.template(), locals);
};
break;
default:
throw new Error('this.template must be string or function');
}
}
return this._templateFn(this.serialize.apply(this, arguments));
};
break;
case 'jst':
templateStrategy = function() {
var html;
if (!this.template) {
return '';
}
if (!this._templateFn) {
switch (typeof this.template) {
case 'string':
html = this.template;
this._templateFn = function() {
return html;
};
break;
case 'function':
this._templateFn = this.template;
break;
default:
throw new Error('this.template must be string or function');
}
}
return this._templateFn(this.serialize.apply(this, arguments));
};
break;
default:
throw new Error('Unrecognized template strategy: ' + strategy);
}
}
if (instance) {
return instance.templateStrategy = templateStrategy;
} else {
return Giraffe.View.prototype.templateStrategy = templateStrategy;
}
};
return View;
})(Backbone.View);
Giraffe.View.setTemplateStrategy('underscore-template-selector');
Giraffe.View.setDocumentEvents(['click', 'change'], 'data-gf-');
/*
* __Giraffe.App__ is a special __Giraffe.View__ that provides encapsulation for
* an entire application. Like all __Giraffe__ views, the app has lifecycle
* management for all `children`, so calling `dispose` on an app will call
* `dispose` on all `children` that have the method. The first __Giraffe.App__
* created on a page is available globally at `Giraffe.app`, and by default all
* __Giraffe__ objects reference this app as `this.app` unless they're passed a
* different app in `options.app`. This app reference is used to bind
* `appEvents`, a hash that all __Giraffe__ objects can implement which uses the
* app as an event aggregator for communication and routing.
*
* var myApp = new Giraffe.App();
* window.Giraffe.app; // => `myApp`
* myApp.attach(new Giraffe.View({
* appEvents: {
* 'say:hello': function() { console.log('hello world'); }
* },
* // app: someOtherApp // if you don't want to use `window.Giraffe.app`
* }));
* myApp.trigger('say:hello'); // => 'hello world'
*
* `appEvents` are also used by the __Giraffe.Router__. See
* [`Giraffe.App#routes`](#App-routes) for more.
*
* The app also provides synchronous and asynchronous initializers with `addInitializer` and `start`.
*
* Like all __Giraffe__ objects, __Giraffe.App__ extends each instance with
* every property in `options`.
*
* @param {Object} [options]
*/
Giraffe.App = (function(_super) {
__extends(App, _super);
function App(options) {
this._onUnload = __bind(this._onUnload, this);
this.app = this;
this._initializers = [];
this.started = false;
App.__super__.constructor.apply(this, arguments);
}
App.prototype._cache = function() {
if (Giraffe.app == null) {
Giraffe.app = this;
}
Giraffe.apps[this.cid] = this;
if (this.routes) {
if (this.router == null) {
this.router = new Giraffe.Router({
app: this,
triggers: this.routes
});
}
}
$window.on('unload', this._onUnload);
return App.__super__._cache.apply(this, arguments);
};
App.prototype._uncache = function() {
if (Giraffe.app === this) {
Giraffe.app = null;
}
delete Giraffe.apps[this.cid];
if (this.router) {
this.router = null;
}
$window.off('unload', this._onUnload);
return App.__super__._uncache.apply(this, arguments);
};
App.prototype._onUnload = function() {
return this.dispose();
};
/*
* Similar to the `events` hash of __Backbone.View__, `appEvents` maps events
* on `this.app` to methods on a __Giraffe__ object. App events can be
* triggered from routes or by any object in your application. If a
* __Giraffe.App__ has been created, every __Giraffe__ object has a reference
* to the global __Giraffe.app__ instance at `this.app`, and a specific app
* instance can be set by passing `options.app` to the object's constructor.
* This instance of `this.app` is used to bind `appEvents`, and these bindings
* are automatically cleaned up when an object is disposed.
*
* // in a Giraffe object
* this.appEvents = {'some:appEvent': 'someMethod'};
* this.app.trigger('some:appEvent', params) // => this.someMethod(params)
*/
App.prototype.appEvents = null;
/*
* If `routes` is defined on a __Giraffe.App__ or passed to its constructor
* as an option, the app will create an instance of __Giraffe.Router__ as
* `this.router` and set the router's `triggers` to the app's `routes`. Any
* number of routers can be instantiated manually, but they do require that an
* instance of __Giraffe.App__ is first created, because they use `appEvents`
* for route handling. See [`Giraffe.Router#triggers`](#Router-triggers)
* for more.
*
* var app = new Giraffe.App({routes: {'route': 'appEvent'}});
* app.router; // => instance of Giraffe.Router
* // or
* var MyApp = Giraffe.App.extend({routes: {'route': 'appEvent'}});
* var myApp = new MyApp();
* myApp.router; // => instance of Giraffe.Router
*/
App.prototype.routes = null;
/*
* Queues up the provided function to be run on `start`. The functions you
* provide are called with the same `options` object passed to `start`. If the
* provided function has two arguments, the options and a callback, the app's
* initialization will wait until you call the callback. If the callback is
* called with a truthy first argument, an error will be logged and
* initialization will halt. If the app has already started when you call
* `addInitializer`, the function is called immediately.
*
* app.addInitializer(function(options) {
* doSyncStuff();
* });
* app.addInitializer(function(options, cb) {
* doAsyncStuff(cb);
* });
* app.start();
*
* @param {Function} fn `function(options)` or `function(options, cb)`
* {Object} options - options passed from `start`
* {Function} cb - optional async callback `function(err)`
*/
App.prototype.addInitializer = function(fn) {
if (this.started) {
fn.call(this, this._startOptions);
_.extend(this, this._startOptions);
} else {
this._initializers.push(fn);
}
return this;
};
/*
* Starts the app by executing each initializer in the order it was added,
* passing `options` through the initializer queue. Triggers the `appEvents`
* `'app:initializing'` and `'app:initialized'`.
*
* @param {Object} [options]
*/
App.prototype.start = function(options) {
var next,
_this = this;
if (options == null) {
options = {};
}
this._startOptions = options;
this.trigger('app:initializing', options);
next = function(err) {
var fn;
if (err) {
throw new Error(err);
}
fn = _this._initializers.shift();
if (fn) {
if (fn.length === 2) {
return fn.call(_this, options, next);
} else {
fn.call(_this, options);
return next();
}
} else {
_.extend(_this, options);
_this.started = true;
return _this.trigger('app:initialized', options);
}
};
next();
return this;
};
return App;
})(Giraffe.View);
/*
* The __Giraffe.Router__ integrates with a __Giraffe.App__ to decouple your
* router and route handlers and to provide programmatic encapsulation for your
* routes. Routes trigger `appEvents` on the router's instance of
* __Giraffe.App__. All __Giraffe__ objects implement the `appEvents` hash as a
* shortcut. `Giraffe.Router#cause` triggers an app event and navigates to its
* route if one exists in `Giraffe.Router#triggers`, and you can ask the router
* if a given app event is currently caused via `Giraffe.Router#isCaused`.
* Additionally, rather than building anchor links and window locations manually,
* you can build routes from app events and optional parameters with
* `Giraffe.Router#getRoute`.
*
* var myApp = new Giraffe.App;
* var myRouter = Giraffe.Router.extend({
* triggers: {
* 'post/:id': 'route:post'
* }
* });
* myRouter.app === myApp; // => true
* myRouter.cause('route:post', 42); // goes to `#post/42` and triggers 'route:post' on `myApp`
* myRouter.isCaused('route:post', 42); // => true
* myRouter.getRoute('route:post', 42); // => '#post/42'
*
* The __Giraffe.Router__ requires that a __Giraffe.App__ has been created on the
* page so it can trigger events for your objects to listen to. For convenience,
* if a __Giraffe.App__ is created with a `routes` hash, it will automatically
* instantiate a router and set its `triggers` equal to the app's `routes`.
*
* var myApp = Giraffe.App.extend({
* routes: {'my/route': 'app:event'}
* });
* myApp.router.triggers; // => {'my/route': 'app:event'}
*
* Like all __Giraffe__ objects, __Giraffe.Router__ extends each instance with
* every property in `options`.
*
* @param {Object} [options]
*/
Giraffe.Router = (function(_super) {
__extends(Router, _super);
function Router(options) {
Giraffe.configure(this, options);
if (!this.app) {
throw new Error('Giraffe routers require an app! Please create an instance of Giraffe.App before creating a router.');
}
this.app.addChild(this);
if (typeof this.triggers === 'function') {
this.triggers = this.triggers.call(this);
}
if (!this.triggers) {
throw new Error('Giraffe routers require a `triggers` map of routes to app events.');
}
this._routes = {};
this._bindTriggers();
Router.__super__.constructor.apply(this, arguments);
}
Router.prototype.namespace = '';
Router.prototype._fullNamespace = function() {
if (this.parentRouter) {
return this.parentRouter._fullNamespace() + '/' + this.namespace;
} else {
return this.namespace;
}
};
/*
* The __Giraffe.Router__ `triggers` hash is similar `Backbone.Router#routes`,
* but instead of `route: method` the __Giraffe.Router__ expects
* `route: appEvent`. `Backbone.Router#routes` is used internally, which is why
* `Giraffe.Router#triggers` is renamed. The router also has a redirect
* feature as demonstrated below.
*
* triggers: {
* 'some/route/:andItsParams': 'some:appEvent', // triggers 'some:appEvent' on this.app
* 'some/other/route': '-> some/redirect/route' // redirect
* }
*/
Router.prototype.triggers = null;
Router.prototype._bindTriggers = function() {
var appEvent, fullNs, route, _fn, _ref,
_this = this;
if (!this.triggers) {
throw new Error('Expected router to implement `triggers` hash in the form {route: appEvent}');
}
fullNs = this._fullNamespace();
if (fullNs.length > 0) {
fullNs += '/';
}
_ref = this.triggers;
_fn = function(route, appEvent, fullNs) {
var callback;
if (appEvent.indexOf('-> ') === 0) {
callback = function() {
var redirect;
redirect = appEvent.slice(3);
return _this.navigate(redirect, {
trigger: true
});
};
} else if (appEvent.indexOf('=> ') === 0) {
callback = function() {
var redirect;
redirect = appEvent.slice(3);
return _this.navigate(fullNs + redirect, {
trigger: true
});
};
} else {
route = fullNs + route;
callback = function() {
var args, _ref1;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return (_ref1 = _this.app).trigger.apply(_ref1, [appEvent].concat(__slice.call(args), [route]));
};
_this._registerRoute(appEvent, route);
}
return _this.route(route, appEvent, callback);
};
for (route in _ref) {
appEvent = _ref[route];
_fn(route, appEvent, fullNs);
}
return this;
};
Router.prototype._unbindTriggers = function() {
var triggers;
triggers = this._getTriggerRegExpStrings();
return Backbone.history.handlers = _.reject(Backbone.history.handlers, function(handler) {
return _.contains(triggers, handler.route.toString());
});
};
Router.prototype._getTriggerRegExpStrings = function() {
return _.map(_.keys(this.triggers), function(route) {
return Backbone.Router.prototype._routeToRegExp(route).toString();
});
};
/*
* If `this.triggers` has a route that maps to `appEvent`, the router navigates
* to the route, triggering the `appEvent`. If no such matching route exists,
* `cause` acts as an alias for `this.app.trigger`.
*
* @param {String} appEvent App event name.
* @param {Object} [any] Optional parameters.
*/
Router.prototype.cause = function() {
var any, appEvent, last, route, _ref;
appEvent = arguments[0], any = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
route = this.getRoute.apply(this, [appEvent].concat(__slice.call(any)));
if (route != null) {
last = any[any.length - 1];
return Backbone.history.navigate(route, _.extend({
trigger: true
}, (_.isObject(last) ? last : {})));
} else {
return (_ref = this.app).trigger.apply(_ref, [appEvent].concat(__slice.call(any)));
}
};
/*
* Returns true if the current `window.location` matches the route that the
* given app event and optional arguments map to in this router's `triggers`.
*
* @param {String} appEvent App event name.
* @param {Object} [any] Optional parameters.
*/
Router.prototype.isCaused = function() {
var any, appEvent, route;
appEvent = arguments[0], any = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
route = this.getRoute.apply(this, [appEvent].concat(__slice.call(any)));
if (route != null) {
return this._getLocation() === route;
} else {
return false;
}
};
Router.prototype._getLocation = function() {
if (Backbone.history._hasPushState) {
return window.location.pathname.slice(1);
} else {
return window.location.hash;
}
};
/*
* Converts an app event and optional arguments into a url mapped in
* `this.triggers`. Useful to build links to the routes in your app without
* manually manipulating route strings.
*
* @param {String} appEvent App event name.
* @param {Object} [any] Optional parameter.
*/
Router.prototype.getRoute = function() {
var any, appEvent, route;
appEvent = arguments[0], any = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
route = this._routes[appEvent];
if (route != null) {
route = this._reverseHash.apply(this, [route].concat(__slice.call(any)));
if (route) {
if (Backbone.history._hasPushState) {
return route;
} else {
return '#' + route;
}
} else if (route === '') {
return '';
} else {
return null;
}
} else {
return null;
}
};
Router.prototype._registerRoute = function(appEvent, route) {
return this._routes[appEvent] = route;
};
Router.prototype._reverseHash = function() {
var any, first, result, route, wildcards;
route = arguments[0], any = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
first = any[0];
if (first == null) {
return route.replace(/\(.+?\)/g, '');
}
wildcards = /[:|\*]\w+/g;
if (_.isObject(first)) {
result = route.replace(wildcards, function(token) {
var key, _ref;
key = token.slice(1);
return (_ref = first[key]) != null ? _ref : '';
});
} else {
result = route.replace(wildcards, function(token) {
var value;
value = any.shift();
return value != null ? value : '';
});
}
result = result.replace(/\(.+\)/g, function(token) {
if (token.match(/\w+/g)) {
return token.replace(/\(|\)/g, '');
} else {
return '';
}
});
return result;
};
/*
* Performs a page refresh. If `url` is defined, the router first silently
* navigates to it before refeshing.
*
* @param {String} [url]
*/
Router.prototype.reload = function(url) {
if (url) {
Backbone.history.stop();
window.location = url;
}
return window.location.reload();
};
/*
* See [`Giraffe.App#appEvents`](#App-appEvents).
*/
Router.prototype.appEvents = null;
/*
* Removes registered callbacks.
*
*/
Router.prototype._dispose = function() {
return this._unbindTriggers();
};
return Router;
})(Backbone.Router);
/*
* __Giraffe.Model__ and __Giraffe.Collection__ are thin wrappers that add
* lifecycle management and `appEvents` support. To add lifecycle management to
* an arbitrary object, simply give it a `dispose` method and add it to a view
* via `addChild`. To use this functionality in your own objects, see
* [`Giraffe.dispose`](#dispose) and [`Giraffe.bindEventMap`](#bindEventMap).
*
* Like all __Giraffe__ objects, __Giraffe.Model__ and __Giraffe.Collection__
* extend each instance with every property in `options` except `parse` which
* is problematic per issue 7.
*
* @param {Object} [attributes]
* @param {Object} [options]
*/
Giraffe.Model = (function(_super) {
__extends(Model, _super);
Model.defaultOptions = {
omittedOptions: 'parse'
};
function Model(attributes, options) {
Giraffe.configure(this, options);
Model.__super__.constructor.apply(this, arguments);
}
/*
* See [`Giraffe.App#appEvents`](#App-appEvents).
*/
Model.prototype.appEvents = null;
/*
* Removes event listeners and removes this model from its collection.
*/
Model.prototype._dispose = function() {
var _ref;
return (_ref = this.collection) != null ? _ref.remove(this) : void 0;
};
return Model;
})(Backbone.Model);
/*
* See [`Giraffe.Model`](#Model).
*
* @param {Array} [models]
* @param {Object} [options]
*/
Giraffe.Collection = (function(_super) {
__extends(Collection, _super);
Collection.defaultOptions = {
omittedOptions: 'parse'
};
Collection.prototype.model = Giraffe.Model;
function Collection(models, options) {
Giraffe.configure(this, options);
Collection.__super__.constructor.apply(this, arguments);
}
/*
* See [`Giraffe.App#appEvents`](#App-appEvents).
*/
Collection.prototype.appEvents = null;
/*
* Removes event listeners and disposes of all models, which removes them from
* the collection.
*/
Collection.prototype._dispose = function() {
var model, _i, _len, _ref;
_ref = this.models.slice();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
model = _ref[_i];
model.dispose();
}
return this;
};
Collection.prototype._removeReference = function(model) {
Collection.__super__._removeReference.apply(this, arguments);
if (!model._disposed) {
return typeof model.dispose === "function" ? model.dispose() : void 0;
}
};
return Collection;
})(Backbone.Collection);
/*
* Initializes an object with several generic features.
* All __Giraffe__ objects call this function in their constructors to gain much
* of their functionality.
* Uses duck typing to initialize features when dependencies are met.
*
* Features:
*
* - -pulls option defaults from the global `Giraffe.defaultOptions`, the static `obj.constructor.defaultOptions`, and the instance/prototype `obj.defaultOptions`
* - -extends the object with all options minus `omittedOptions` (omits all if `true`)
* - -defaults `obj.dispose` to `Giraffe.dispose`
* - -defaults `obj.app` to `Giraffe.app`
* - -binds `appEvents` if `appEvents` and `app` are defined and `obj` extends `Backbone.Events`
* - -binds `dataEvents` if `dataEvents` is defined and `obj` extends `Backbone.Events`
* - -wraps `initialize` with `beforeInitialize` and `afterInitialize` if it exists
*
* @param {Object} obj Any object.
* @param {Obj} [opts] Extended along with `defaultOptions` onto `obj` minus `options.omittedOptions`. If `options.omittedOptions` is true, all are omitted.
*/
Giraffe.configure = function(obj, opts) {
var omittedOptions, options, _ref, _ref1;
if (!_.isObject(obj)) {
throw new Error("Giraffe.configure requires an object as the first parameter");
}
options = _.extend({}, Giraffe.defaultOptions, (_ref = obj.constructor) != null ? _ref.defaultOptions : void 0, obj.defaultOptions, opts);
omittedOptions = (_ref1 = options.omittedOptions) != null ? _ref1 : obj.omittedOptions;
if (omittedOptions !== true) {
_.extend(obj, _.omit(options, omittedOptions));
}
if (obj.dispose == null) {
obj.dispose = Giraffe.dispose;
}
if (obj.app == null) {
obj.app = Giraffe.app;
}
if (obj.appEvents) {
Giraffe.bindAppEvents(obj);
}
if (obj.initialize) {
Giraffe.wrapFn(obj, 'initialize', obj._beforeInitialize, _afterInitialize);
} else {
_afterInitialize.call(obj);
}
return obj;
};
/*
* The global defaults extended to every object passed to `Giraffe.configure`.
* Empty by default.
* Setting `omittedOptions` here globally prevents those properties from being
* copied over, and if its value is `true` extension is completely disabled.
*
* function Foo() {
* Giraffe.configure(this);
* };
* Giraffe.defaultOptions = {bar: 'global'};
* var foo = new Foo();
* foo.bar; // => 'global'
*
* You can also define `defaultOptions` on a function constructor.
* These override the global defaults.
*
* Foo.defaultOptions = {bar: 'constructor'};
* foo = new Foo();
* foo.bar; // => 'constructor'
*
* The instance/prototype defaults take even higher precedence:
*
* Foo.prototype.defaultOptions = {bar: 'instance/prototype'};
* foo = new Foo();
* foo.bar; // => 'instance/prototype'
*
* Options passed as arguments always override `defaultOptions`.
*
* foo = new Foo({bar: 'option'});
* foo.bar; // => 'option'
*
* Be aware that the values of all `defaultOptions` are not cloned when copied over.
*
* @caption Giraffe.defaultOptions
*/
Giraffe.defaultOptions = {};
_afterInitialize = function() {
if (this.dataEvents) {
return Giraffe.bindDataEvents(this);
}
};
/*
* Disposes of an object, removing event listeners and freeing resources.
* An instance method of `dispose` is added for
* all objects passed through `Giraffe.configure`, and so you will normally
* call `dispose` directly on your objects.
*
* Calls `Backbone.Events#stopListening` and sets
* `obj.app` to null. Also triggers the `'disposing'` and `'disposed'` events
* and calls the `beforeDispose` and `afterDispose` hooks on `obj` before and
* after the disposal. Takes optional `args` that are passed through to the
* events and the function calls.
*
* @param {Any} [args...] Any arguments to be passed to the `fn` and disposal events.
*/
Giraffe.dispose = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
if (typeof this.trigger === "function") {
this.trigger.apply(this, ['disposing', this].concat(__slice.call(args)));
}
if (typeof this.beforeDispose === "function") {
this.beforeDispose.apply(this, args);
}
this._disposed = true;
if (typeof this._dispose === "function") {
this._dispose.apply(this, args);
}
this.app = null;
if (typeof this.stopListening === "function") {
this.stopListening();
}
if (typeof this.trigger === "function") {
this.trigger.apply(this, ['disposed', this].concat(__slice.call(args)));
}
if (typeof this.afterDispose === "function") {
this.afterDispose.apply(this, args);
}
return this;
};
/*
* Attempts to bind `appEvents` for an object. Called by `Giraffe.configure`.
*/
Giraffe.bindAppEvents = function(obj) {
return Giraffe.bindEventMap(obj, obj.app, obj.appEvents);
};
/*
* Binds the `dataEvents` hash that allows any instance property of `obj` to
* be bound to easily. Expects the form {'event1 event2 targetObj': 'handler'}.
* Called by `Giraffe.configure`.
*/
Giraffe.bindDataEvents = function(obj) {
var cb, dataEvents, eventKey, eventName, pieces, targetObj;
dataEvents = obj.dataEvents;
if (!dataEvents) {
return;
}
if (typeof dataEvents === 'function') {
dataEvents = obj.dataEvents();
}
for (eventKey in dataEvents) {
cb = dataEvents[eventKey];
pieces = eventKey.split(' ');
if (pieces.length < 2) {
throw new Error('Data event must specify target object, ex: {\'change collection\': \'handler\'}');
}
targetObj = pieces.pop();
targetObj = targetObj === 'this' || targetObj === '@' ? obj : obj[targetObj];
if (!targetObj) {
throw new Error("Target object not found for data event '" + eventKey + "'");
}
eventName = pieces.join(' ');
Giraffe.bindEvent(obj, targetObj, eventName, cb);
}
return obj;
};
/*
* Uses `Backbone.Events.listenTo` to make `contextObj` listen for `eventName` on
* `targetObj` with the callback `cb`, which can be a function or the string name
* of a method on `contextObj`.
*
* @param {Backbone.Events} contextObj The object doing the listening.
* @param {Backbone.Events} targetObj The object to listen to.
* @param {String/Function} eventName The name of the event to listen to.
* @param {Function} cb The event's callback.
*/
Giraffe.bindEvent = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
args.push('listenTo');
return _setEventBindings.apply(null, args);
};
/*
* The `stopListening` equivalent of `bindEvent`.
*
* @param {Backbone.Events} contextObj The object doing the listening.
* @param {Backbone.Events} targetObj The object to listen to.
* @param {String/Function} eventName The name of the event to listen to.
* @param {Function} cb The event's callback.
*/
Giraffe.unbindEvent = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
args.push('stopListening');
return _setEventBindings.apply(null, args);
};
/*
* Makes `contextObj` listen to `targetObj` for the events of `eventMap` in the
* form `eventName: method`, where `method` is a function or the name of a
* function on `contextObj`.
*
* Giraffe.bindEventMap(this, this.app, this.appEvents);
*
* @param {Backbone.Events} contextObj The object doing the listening.
* @param {Backbone.Events} targetObj The object to listen to.
* @param {Object} eventMap A map of events to callbacks in the form {eventName: methodName/methodFn} to listen to.
*/
Giraffe.bindEventMap = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
args.push('listenTo');
return _setEventMapBindings.apply(null, args);
};
/*
* The `stopListening` equivalent of `bindEventMap`.
*
* @param {Backbone.Events} contextObj The object doing the listening.
* @param {Backbone.Events} targetObj The object to listen to.
* @param {Object} eventMap A map of events to callbacks in the form {eventName: methodName/methodFn} to listen to.
*/
Giraffe.unbindEventMap = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
args.push('stopListening');
return _setEventMapBindings.apply(null, args);
};
_setEventBindings = function(contextObj, targetObj, eventName, cb, bindOrUnbindFnName) {
if (typeof cb === 'string') {
cb = contextObj[cb];
}
if (typeof cb !== 'function') {
throw new Error("callback for `'" + eventName + "'` not found");
}
return contextObj[bindOrUnbindFnName](targetObj, eventName, cb);
};
_setEventMapBindings = function(contextObj, targetObj, eventMap, bindOrUnbindFnName) {
var cb, eventName;
if (typeof eventMap === 'function') {
eventMap = eventMap.call(contextObj);
}
if (!eventMap) {
return;
}
for (eventName in eventMap) {
cb = eventMap[eventName];
_setEventBindings(contextObj, targetObj, eventName, cb, bindOrUnbindFnName);
}
return contextObj;
};
/*
* Wraps `obj[fnName]` with `beforeFnName` and `afterFnName` invocations. Also
* calls the optional arguments `beforeFn` and `afterFn`.
*
* @param {Object} obj
* @param {String} fnName
* @param {Function} [beforeFn]
* @param {Function} [afterFn]
*/
Giraffe.wrapFn = function(obj, fnName, beforeFn, afterFn) {
var capFnName, fn;
fn = obj[fnName];
if (typeof fn !== 'function') {
return;
}
capFnName = fnName[0].toUpperCase() + fnName.slice(1);
return obj[fnName] = function() {
var args, result, _name, _name1;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
if (beforeFn != null) {
beforeFn.apply(obj, args);
}
if (typeof obj[_name = 'before' + capFnName] === "function") {
obj[_name].apply(obj, args);
}
result = fn.apply(obj, args);
if (typeof obj[_name1 = 'after' + capFnName] === "function") {
obj[_name1].apply(obj, args);
}
if (afterFn != null) {
afterFn.apply(obj, args);
}
return result;
};
};
return Giraffe;
});
}).call(this);