--- /dev/null
+// Backbone.js 0.5.3\r
+// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.\r
+// Backbone may be freely distributed under the MIT license.\r
+// For all details and documentation:\r
+// http://backbonejs.org\r
+\r
+(function(){\r
+\r
+ // Initial Setup\r
+ // -------------\r
+\r
+ // Save a reference to the global object (`window` in the browser, `global`\r
+ // on the server).\r
+ var root = this;\r
+\r
+ // Save the previous value of the `Backbone` variable, so that it can be\r
+ // restored later on, if `noConflict` is used.\r
+ var previousBackbone = root.Backbone;\r
+\r
+ // Create a local reference to slice/splice.\r
+ var slice = Array.prototype.slice;\r
+ var splice = Array.prototype.splice;\r
+\r
+ // The top-level namespace. All public Backbone classes and modules will\r
+ // be attached to this. Exported for both CommonJS and the browser.\r
+ var Backbone;\r
+ if (typeof exports !== 'undefined') {\r
+ Backbone = exports;\r
+ } else {\r
+ Backbone = root.Backbone = {};\r
+ }\r
+\r
+ // Current version of the library. Keep in sync with `package.json`.\r
+ Backbone.VERSION = '0.5.3';\r
+\r
+ // Require Underscore, if we're on the server, and it's not already present.\r
+ var _ = root._;\r
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore');\r
+\r
+ // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.\r
+ var $ = root.jQuery || root.Zepto || root.ender;\r
+\r
+ // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable\r
+ // to its previous owner. Returns a reference to this Backbone object.\r
+ Backbone.noConflict = function() {\r
+ root.Backbone = previousBackbone;\r
+ return this;\r
+ };\r
+\r
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option\r
+ // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and\r
+ // set a `X-Http-Method-Override` header.\r
+ Backbone.emulateHTTP = false;\r
+\r
+ // Turn on `emulateJSON` to support legacy servers that can't deal with direct\r
+ // `application/json` requests ... will encode the body as\r
+ // `application/x-www-form-urlencoded` instead and will send the model in a\r
+ // form param named `model`.\r
+ Backbone.emulateJSON = false;\r
+\r
+ // Backbone.Events\r
+ // -----------------\r
+\r
+ // A module that can be mixed in to *any object* in order to provide it with\r
+ // custom events. You may bind with `on` or remove with `off` callback functions\r
+ // to an event; trigger`-ing an event fires all callbacks in succession.\r
+ //\r
+ // var object = {};\r
+ // _.extend(object, Backbone.Events);\r
+ // object.on('expand', function(){ alert('expanded'); });\r
+ // object.trigger('expand');\r
+ //\r
+ Backbone.Events = {\r
+\r
+ // Bind an event, specified by a string name, `ev`, to a `callback`\r
+ // function. Passing `"all"` will bind the callback to all events fired.\r
+ on: function(events, callback, context) {\r
+ var ev;\r
+ events = events.split(/\s+/);\r
+ var calls = this._callbacks || (this._callbacks = {});\r
+ while (ev = events.shift()) {\r
+ // Create an immutable callback list, allowing traversal during\r
+ // modification. The tail is an empty object that will always be used\r
+ // as the next node.\r
+ var list = calls[ev] || (calls[ev] = {});\r
+ var tail = list.tail || (list.tail = list.next = {});\r
+ tail.callback = callback;\r
+ tail.context = context;\r
+ list.tail = tail.next = {};\r
+ }\r
+ return this;\r
+ },\r
+\r
+ // Remove one or many callbacks. If `context` is null, removes all callbacks\r
+ // with that function. If `callback` is null, removes all callbacks for the\r
+ // event. If `ev` is null, removes all bound callbacks for all events.\r
+ off: function(events, callback, context) {\r
+ var ev, calls, node;\r
+ if (!events) {\r
+ delete this._callbacks;\r
+ } else if (calls = this._callbacks) {\r
+ events = events.split(/\s+/);\r
+ while (ev = events.shift()) {\r
+ node = calls[ev];\r
+ delete calls[ev];\r
+ if (!callback || !node) continue;\r
+ // Create a new list, omitting the indicated event/context pairs.\r
+ while ((node = node.next) && node.next) {\r
+ if (node.callback === callback &&\r
+ (!context || node.context === context)) continue;\r
+ this.on(ev, node.callback, node.context);\r
+ }\r
+ }\r
+ }\r
+ return this;\r
+ },\r
+\r
+ // Trigger an event, firing all bound callbacks. Callbacks are passed the\r
+ // same arguments as `trigger` is, apart from the event name.\r
+ // Listening for `"all"` passes the true event name as the first argument.\r
+ trigger: function(events) {\r
+ var event, node, calls, tail, args, all, rest;\r
+ if (!(calls = this._callbacks)) return this;\r
+ all = calls['all'];\r
+ (events = events.split(/\s+/)).push(null);\r
+ // Save references to the current heads & tails.\r
+ while (event = events.shift()) {\r
+ if (all) events.push({next: all.next, tail: all.tail, event: event});\r
+ if (!(node = calls[event])) continue;\r
+ events.push({next: node.next, tail: node.tail});\r
+ }\r
+ // Traverse each list, stopping when the saved tail is reached.\r
+ rest = slice.call(arguments, 1);\r
+ while (node = events.pop()) {\r
+ tail = node.tail;\r
+ args = node.event ? [node.event].concat(rest) : rest;\r
+ while ((node = node.next) !== tail) {\r
+ node.callback.apply(node.context || this, args);\r
+ }\r
+ }\r
+ return this;\r
+ }\r
+\r
+ };\r
+\r
+ // Aliases for backwards compatibility.\r
+ Backbone.Events.bind = Backbone.Events.on;\r
+ Backbone.Events.unbind = Backbone.Events.off;\r
+\r
+ // Backbone.Model\r
+ // --------------\r
+\r
+ // Create a new model, with defined attributes. A client id (`cid`)\r
+ // is automatically generated and assigned for you.\r
+ Backbone.Model = function(attributes, options) {\r
+ var defaults;\r
+ attributes || (attributes = {});\r
+ if (options && options.parse) attributes = this.parse(attributes);\r
+ if (defaults = getValue(this, 'defaults')) {\r
+ attributes = _.extend({}, defaults, attributes);\r
+ }\r
+ if (options && options.collection) this.collection = options.collection;\r
+ this.attributes = {};\r
+ this._escapedAttributes = {};\r
+ this.cid = _.uniqueId('c');\r
+ if (!this.set(attributes, {silent: true})) {\r
+ throw new Error("Can't create an invalid model");\r
+ }\r
+ this._changed = false;\r
+ this._previousAttributes = _.clone(this.attributes);\r
+ this.initialize.apply(this, arguments);\r
+ };\r
+\r
+ // Attach all inheritable methods to the Model prototype.\r
+ _.extend(Backbone.Model.prototype, Backbone.Events, {\r
+\r
+ // Has the item been changed since the last `"change"` event?\r
+ _changed: false,\r
+\r
+ // The default name for the JSON `id` attribute is `"id"`. MongoDB and\r
+ // CouchDB users may want to set this to `"_id"`.\r
+ idAttribute: 'id',\r
+\r
+ // Initialize is an empty function by default. Override it with your own\r
+ // initialization logic.\r
+ initialize: function(){},\r
+\r
+ // Return a copy of the model's `attributes` object.\r
+ toJSON: function() {\r
+ return _.clone(this.attributes);\r
+ },\r
+\r
+ // Get the value of an attribute.\r
+ get: function(attr) {\r
+ return this.attributes[attr];\r
+ },\r
+\r
+ // Get the HTML-escaped value of an attribute.\r
+ escape: function(attr) {\r
+ var html;\r
+ if (html = this._escapedAttributes[attr]) return html;\r
+ var val = this.attributes[attr];\r
+ return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);\r
+ },\r
+\r
+ // Returns `true` if the attribute contains a value that is not null\r
+ // or undefined.\r
+ has: function(attr) {\r
+ return this.attributes[attr] != null;\r
+ },\r
+\r
+ // Set a hash of model attributes on the object, firing `"change"` unless\r
+ // you choose to silence it.\r
+ set: function(key, value, options) {\r
+ var attrs, attr, val;\r
+ if (_.isObject(key) || key == null) {\r
+ attrs = key;\r
+ options = value;\r
+ } else {\r
+ attrs = {};\r
+ attrs[key] = value;\r
+ }\r
+\r
+ // Extract attributes and options.\r
+ options || (options = {});\r
+ if (!attrs) return this;\r
+ if (attrs instanceof Backbone.Model) attrs = attrs.attributes;\r
+ if (options.unset) for (var attr in attrs) attrs[attr] = void 0;\r
+ var now = this.attributes, escaped = this._escapedAttributes;\r
+\r
+ // Run validation.\r
+ if (this.validate && !this._performValidation(attrs, options)) return false;\r
+\r
+ // Check for changes of `id`.\r
+ if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];\r
+\r
+ // We're about to start triggering change events.\r
+ var alreadyChanging = this._changing;\r
+ this._changing = true;\r
+\r
+ // Update attributes.\r
+ var changes = {};\r
+ for (attr in attrs) {\r
+ val = attrs[attr];\r
+ if (!_.isEqual(now[attr], val) || (options.unset && (attr in now))) {\r
+ delete escaped[attr];\r
+ this._changed = true;\r
+ changes[attr] = val;\r
+ }\r
+ options.unset ? delete now[attr] : now[attr] = val;\r
+ }\r
+\r
+ // Fire `change:attribute` events.\r
+ for (var attr in changes) {\r
+ if (!options.silent) this.trigger('change:' + attr, this, changes[attr], options);\r
+ }\r
+\r
+ // Fire the `"change"` event, if the model has been changed.\r
+ if (!alreadyChanging) {\r
+ if (!options.silent && this._changed) this.change(options);\r
+ this._changing = false;\r
+ }\r
+ return this;\r
+ },\r
+\r
+ // Remove an attribute from the model, firing `"change"` unless you choose\r
+ // to silence it. `unset` is a noop if the attribute doesn't exist.\r
+ unset: function(attr, options) {\r
+ (options || (options = {})).unset = true;\r
+ return this.set(attr, null, options);\r
+ },\r
+\r
+ // Clear all attributes on the model, firing `"change"` unless you choose\r
+ // to silence it.\r
+ clear: function(options) {\r
+ (options || (options = {})).unset = true;\r
+ return this.set(_.clone(this.attributes), options);\r
+ },\r
+\r
+ // Fetch the model from the server. If the server's representation of the\r
+ // model differs from its current attributes, they will be overriden,\r
+ // triggering a `"change"` event.\r
+ fetch: function(options) {\r
+ options = options ? _.clone(options) : {};\r
+ var model = this;\r
+ var success = options.success;\r
+ options.success = function(resp, status, xhr) {\r
+ if (!model.set(model.parse(resp, xhr), options)) return false;\r
+ if (success) success(model, resp);\r
+ };\r
+ options.error = Backbone.wrapError(options.error, model, options);\r
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);\r
+ },\r
+\r
+ // Set a hash of model attributes, and sync the model to the server.\r
+ // If the server returns an attributes hash that differs, the model's\r
+ // state will be `set` again.\r
+ save: function(key, value, options) {\r
+ var attrs;\r
+ if (_.isObject(key) || key == null) {\r
+ attrs = key;\r
+ options = value;\r
+ } else {\r
+ attrs = {};\r
+ attrs[key] = value;\r
+ }\r
+\r
+ options = options ? _.clone(options) : {};\r
+ if (attrs && !this[options.wait ? '_performValidation' : 'set'](attrs, options)) return false;\r
+ var model = this;\r
+ var success = options.success;\r
+ options.success = function(resp, status, xhr) {\r
+ var serverAttrs = model.parse(resp, xhr);\r
+ if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);\r
+ if (!model.set(serverAttrs, options)) return false;\r
+ if (success) {\r
+ success(model, resp);\r
+ } else {\r
+ model.trigger('sync', model, resp, options);\r
+ }\r
+ };\r
+ options.error = Backbone.wrapError(options.error, model, options);\r
+ var method = this.isNew() ? 'create' : 'update';\r
+ return (this.sync || Backbone.sync).call(this, method, this, options);\r
+ },\r
+\r
+ // Destroy this model on the server if it was already persisted.\r
+ // Optimistically removes the model from its collection, if it has one.\r
+ // If `wait: true` is passed, waits for the server to respond before removal.\r
+ destroy: function(options) {\r
+ options = options ? _.clone(options) : {};\r
+ var model = this;\r
+ var success = options.success;\r
+\r
+ var triggerDestroy = function() {\r
+ model.trigger('destroy', model, model.collection, options);\r
+ };\r
+\r
+ if (this.isNew()) return triggerDestroy();\r
+ options.success = function(resp) {\r
+ if (options.wait) triggerDestroy();\r
+ if (success) {\r
+ success(model, resp);\r
+ } else {\r
+ model.trigger('sync', model, resp, options);\r
+ }\r
+ };\r
+ options.error = Backbone.wrapError(options.error, model, options);\r
+ var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);\r
+ if (!options.wait) triggerDestroy();\r
+ return xhr;\r
+ },\r
+\r
+ // Default URL for the model's representation on the server -- if you're\r
+ // using Backbone's restful methods, override this to change the endpoint\r
+ // that will be called.\r
+ url: function() {\r
+ var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();\r
+ if (this.isNew()) return base;\r
+ return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);\r
+ },\r
+\r
+ // **parse** converts a response into the hash of attributes to be `set` on\r
+ // the model. The default implementation is just to pass the response along.\r
+ parse: function(resp, xhr) {\r
+ return resp;\r
+ },\r
+\r
+ // Create a new model with identical attributes to this one.\r
+ clone: function() {\r
+ return new this.constructor(this.attributes);\r
+ },\r
+\r
+ // A model is new if it has never been saved to the server, and lacks an id.\r
+ isNew: function() {\r
+ return this.id == null;\r
+ },\r
+\r
+ // Call this method to manually fire a `change` event for this model.\r
+ // Calling this will cause all objects observing the model to update.\r
+ change: function(options) {\r
+ this.trigger('change', this, options);\r
+ this._previousAttributes = _.clone(this.attributes);\r
+ this._changed = false;\r
+ },\r
+\r
+ // Determine if the model has changed since the last `"change"` event.\r
+ // If you specify an attribute name, determine if that attribute has changed.\r
+ hasChanged: function(attr) {\r
+ if (attr) return !_.isEqual(this._previousAttributes[attr], this.attributes[attr]);\r
+ return this._changed;\r
+ },\r
+\r
+ // Return an object containing all the attributes that have changed, or\r
+ // false if there are no changed attributes. Useful for determining what\r
+ // parts of a view need to be updated and/or what attributes need to be\r
+ // persisted to the server. Unset attributes will be set to undefined.\r
+ changedAttributes: function(now) {\r
+ if (!this._changed) return false;\r
+ now || (now = this.attributes);\r
+ var changed = false, old = this._previousAttributes;\r
+ for (var attr in now) {\r
+ if (_.isEqual(old[attr], now[attr])) continue;\r
+ (changed || (changed = {}))[attr] = now[attr];\r
+ }\r
+ for (var attr in old) {\r
+ if (!(attr in now)) (changed || (changed = {}))[attr] = void 0;\r
+ }\r
+ return changed;\r
+ },\r
+\r
+ // Get the previous value of an attribute, recorded at the time the last\r
+ // `"change"` event was fired.\r
+ previous: function(attr) {\r
+ if (!attr || !this._previousAttributes) return null;\r
+ return this._previousAttributes[attr];\r
+ },\r
+\r
+ // Get all of the attributes of the model at the time of the previous\r
+ // `"change"` event.\r
+ previousAttributes: function() {\r
+ return _.clone(this._previousAttributes);\r
+ },\r
+\r
+ // Run validation against a set of incoming attributes, returning `true`\r
+ // if all is well. If a specific `error` callback has been passed,\r
+ // call that instead of firing the general `"error"` event.\r
+ _performValidation: function(attrs, options) {\r
+ var newAttrs = _.extend({}, this.attributes, attrs);\r
+ var error = this.validate(newAttrs, options);\r
+ if (error) {\r
+ if (options.error) {\r
+ options.error(this, error, options);\r
+ } else {\r
+ this.trigger('error', this, error, options);\r
+ }\r
+ return false;\r
+ }\r
+ return true;\r
+ }\r
+\r
+ });\r
+\r
+ // Backbone.Collection\r
+ // -------------------\r
+\r
+ // Provides a standard collection class for our sets of models, ordered\r
+ // or unordered. If a `comparator` is specified, the Collection will maintain\r
+ // its models in sort order, as they're added and removed.\r
+ Backbone.Collection = function(models, options) {\r
+ options || (options = {});\r
+ if (options.comparator) this.comparator = options.comparator;\r
+ this._reset();\r
+ this.initialize.apply(this, arguments);\r
+ if (models) this.reset(models, {silent: true, parse: options.parse});\r
+ };\r
+\r
+ // Define the Collection's inheritable methods.\r
+ _.extend(Backbone.Collection.prototype, Backbone.Events, {\r
+\r
+ // The default model for a collection is just a **Backbone.Model**.\r
+ // This should be overridden in most cases.\r
+ model: Backbone.Model,\r
+\r
+ // Initialize is an empty function by default. Override it with your own\r
+ // initialization logic.\r
+ initialize: function(){},\r
+\r
+ // The JSON representation of a Collection is an array of the\r
+ // models' attributes.\r
+ toJSON: function() {\r
+ return this.map(function(model){ return model.toJSON(); });\r
+ },\r
+\r
+ // Add a model, or list of models to the set. Pass **silent** to avoid\r
+ // firing the `add` event for every new model.\r
+ add: function(models, options) {\r
+ var i, index, length, model, cids = {};\r
+ options || (options = {});\r
+ models = _.isArray(models) ? models.slice() : [models];\r
+\r
+ // Begin by turning bare objects into model references, and preventing\r
+ // invalid models or duplicate models from being added.\r
+ for (i = 0, length = models.length; i < length; i++) {\r
+ if (!(model = models[i] = this._prepareModel(models[i], options))) {\r
+ throw new Error("Can't add an invalid model to a collection");\r
+ }\r
+ var hasId = model.id != null;\r
+ if (this._byCid[model.cid] || (hasId && this._byId[model.id])) {\r
+ throw new Error("Can't add the same model to a collection twice");\r
+ }\r
+ }\r
+\r
+ // Listen to added models' events, and index models for lookup by\r
+ // `id` and by `cid`.\r
+ for (i = 0; i < length; i++) {\r
+ (model = models[i]).on('all', this._onModelEvent, this);\r
+ this._byCid[model.cid] = model;\r
+ if (model.id != null) this._byId[model.id] = model;\r
+ cids[model.cid] = true;\r
+ }\r
+\r
+ // Insert models into the collection, re-sorting if needed, and triggering\r
+ // `add` events unless silenced.\r
+ this.length += length;\r
+ index = options.at != null ? options.at : this.models.length;\r
+ splice.apply(this.models, [index, 0].concat(models));\r
+ if (this.comparator) this.sort({silent: true});\r
+ if (options.silent) return this;\r
+ for (i = 0, length = this.models.length; i < length; i++) {\r
+ if (!cids[(model = this.models[i]).cid]) continue;\r
+ options.index = i;\r
+ model.trigger('add', model, this, options);\r
+ }\r
+ return this;\r
+ },\r
+\r
+ // Remove a model, or a list of models from the set. Pass silent to avoid\r
+ // firing the `remove` event for every model removed.\r
+ remove: function(models, options) {\r
+ var i, l, index, model;\r
+ options || (options = {});\r
+ models = _.isArray(models) ? models.slice() : [models];\r
+ for (i = 0, l = models.length; i < l; i++) {\r
+ model = this.getByCid(models[i]) || this.get(models[i]);\r
+ if (!model) continue;\r
+ delete this._byId[model.id];\r
+ delete this._byCid[model.cid];\r
+ index = this.indexOf(model);\r
+ this.models.splice(index, 1);\r
+ this.length--;\r
+ if (!options.silent) {\r
+ options.index = index;\r
+ model.trigger('remove', model, this, options);\r
+ }\r
+ this._removeReference(model);\r
+ }\r
+ return this;\r
+ },\r
+\r
+ // Get a model from the set by id.\r
+ get: function(id) {\r
+ if (id == null) return null;\r
+ return this._byId[id.id != null ? id.id : id];\r
+ },\r
+\r
+ // Get a model from the set by client id.\r
+ getByCid: function(cid) {\r
+ return cid && this._byCid[cid.cid || cid];\r
+ },\r
+\r
+ // Get the model at the given index.\r
+ at: function(index) {\r
+ return this.models[index];\r
+ },\r
+\r
+ // Force the collection to re-sort itself. You don't need to call this under\r
+ // normal circumstances, as the set will maintain sort order as each item\r
+ // is added.\r
+ sort: function(options) {\r
+ options || (options = {});\r
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');\r
+ var boundComparator = _.bind(this.comparator, this);\r
+ if (this.comparator.length == 1) {\r
+ this.models = this.sortBy(boundComparator);\r
+ } else {\r
+ this.models.sort(boundComparator);\r
+ }\r
+ if (!options.silent) this.trigger('reset', this, options);\r
+ return this;\r
+ },\r
+\r
+ // Pluck an attribute from each model in the collection.\r
+ pluck: function(attr) {\r
+ return _.map(this.models, function(model){ return model.get(attr); });\r
+ },\r
+\r
+ // When you have more items than you want to add or remove individually,\r
+ // you can reset the entire set with a new list of models, without firing\r
+ // any `add` or `remove` events. Fires `reset` when finished.\r
+ reset: function(models, options) {\r
+ models || (models = []);\r
+ options || (options = {});\r
+ for (var i = 0, l = this.models.length; i < l; i++) {\r
+ this._removeReference(this.models[i]);\r
+ }\r
+ this._reset();\r
+ this.add(models, {silent: true, parse: options.parse});\r
+ if (!options.silent) this.trigger('reset', this, options);\r
+ return this;\r
+ },\r
+\r
+ // Fetch the default set of models for this collection, resetting the\r
+ // collection when they arrive. If `add: true` is passed, appends the\r
+ // models to the collection instead of resetting.\r
+ fetch: function(options) {\r
+ options = options ? _.clone(options) : {};\r
+ if (options.parse === undefined) options.parse = true;\r
+ var collection = this;\r
+ var success = options.success;\r
+ options.success = function(resp, status, xhr) {\r
+ collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);\r
+ if (success) success(collection, resp);\r
+ };\r
+ options.error = Backbone.wrapError(options.error, collection, options);\r
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);\r
+ },\r
+\r
+ // Create a new instance of a model in this collection. Add the model to the\r
+ // collection immediately, unless `wait: true` is passed, in which case we\r
+ // wait for the server to agree.\r
+ create: function(model, options) {\r
+ var coll = this;\r
+ options = options ? _.clone(options) : {};\r
+ model = this._prepareModel(model, options);\r
+ if (!model) return false;\r
+ if (!options.wait) coll.add(model, options);\r
+ var success = options.success;\r
+ options.success = function(nextModel, resp, xhr) {\r
+ if (options.wait) coll.add(nextModel, options);\r
+ if (success) {\r
+ success(nextModel, resp);\r
+ } else {\r
+ nextModel.trigger('sync', model, resp, options);\r
+ }\r
+ };\r
+ model.save(null, options);\r
+ return model;\r
+ },\r
+\r
+ // **parse** converts a response into a list of models to be added to the\r
+ // collection. The default implementation is just to pass it through.\r
+ parse: function(resp, xhr) {\r
+ return resp;\r
+ },\r
+\r
+ // Proxy to _'s chain. Can't be proxied the same way the rest of the\r
+ // underscore methods are proxied because it relies on the underscore\r
+ // constructor.\r
+ chain: function () {\r
+ return _(this.models).chain();\r
+ },\r
+\r
+ // Reset all internal state. Called when the collection is reset.\r
+ _reset: function(options) {\r
+ this.length = 0;\r
+ this.models = [];\r
+ this._byId = {};\r
+ this._byCid = {};\r
+ },\r
+\r
+ // Prepare a model or hash of attributes to be added to this collection.\r
+ _prepareModel: function(model, options) {\r
+ if (!(model instanceof Backbone.Model)) {\r
+ var attrs = model;\r
+ options.collection = this;\r
+ model = new this.model(attrs, options);\r
+ if (model.validate && !model._performValidation(model.attributes, options)) model = false;\r
+ } else if (!model.collection) {\r
+ model.collection = this;\r
+ }\r
+ return model;\r
+ },\r
+\r
+ // Internal method to remove a model's ties to a collection.\r
+ _removeReference: function(model) {\r
+ if (this == model.collection) {\r
+ delete model.collection;\r
+ }\r
+ model.off('all', this._onModelEvent, this);\r
+ },\r
+\r
+ // Internal method called every time a model in the set fires an event.\r
+ // Sets need to update their indexes when models change ids. All other\r
+ // events simply proxy through. "add" and "remove" events that originate\r
+ // in other collections are ignored.\r
+ _onModelEvent: function(ev, model, collection, options) {\r
+ if ((ev == 'add' || ev == 'remove') && collection != this) return;\r
+ if (ev == 'destroy') {\r
+ this.remove(model, options);\r
+ }\r
+ if (model && ev === 'change:' + model.idAttribute) {\r
+ delete this._byId[model.previous(model.idAttribute)];\r
+ this._byId[model.id] = model;\r
+ }\r
+ this.trigger.apply(this, arguments);\r
+ }\r
+\r
+ });\r
+\r
+ // Underscore methods that we want to implement on the Collection.\r
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',\r
+ 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',\r
+ 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',\r
+ 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',\r
+ 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];\r
+\r
+ // Mix in each Underscore method as a proxy to `Collection#models`.\r
+ _.each(methods, function(method) {\r
+ Backbone.Collection.prototype[method] = function() {\r
+ return _[method].apply(_, [this.models].concat(_.toArray(arguments)));\r
+ };\r
+ });\r
+\r
+ // Backbone.Router\r
+ // -------------------\r
+\r
+ // Routers map faux-URLs to actions, and fire events when routes are\r
+ // matched. Creating a new one sets its `routes` hash, if not set statically.\r
+ Backbone.Router = function(options) {\r
+ options || (options = {});\r
+ if (options.routes) this.routes = options.routes;\r
+ this._bindRoutes();\r
+ this.initialize.apply(this, arguments);\r
+ };\r
+\r
+ // Cached regular expressions for matching named param parts and splatted\r
+ // parts of route strings.\r
+ var namedParam = /:\w+/g;\r
+ var splatParam = /\*\w+/g;\r
+ var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;\r
+\r
+ // Set up all inheritable **Backbone.Router** properties and methods.\r
+ _.extend(Backbone.Router.prototype, Backbone.Events, {\r
+\r
+ // Initialize is an empty function by default. Override it with your own\r
+ // initialization logic.\r
+ initialize: function(){},\r
+\r
+ // Manually bind a single named route to a callback. For example:\r
+ //\r
+ // this.route('search/:query/p:num', 'search', function(query, num) {\r
+ // ...\r
+ // });\r
+ //\r
+ route: function(route, name, callback) {\r
+ Backbone.history || (Backbone.history = new Backbone.History);\r
+ if (!_.isRegExp(route)) route = this._routeToRegExp(route);\r
+ if (!callback) callback = this[name];\r
+ Backbone.history.route(route, _.bind(function(fragment) {\r
+ var args = this._extractParameters(route, fragment);\r
+ callback && callback.apply(this, args);\r
+ this.trigger.apply(this, ['route:' + name].concat(args));\r
+ Backbone.history.trigger('route', this, name, args);\r
+ }, this));\r
+ },\r
+\r
+ // Simple proxy to `Backbone.history` to save a fragment into the history.\r
+ navigate: function(fragment, options) {\r
+ Backbone.history.navigate(fragment, options);\r
+ },\r
+\r
+ // Bind all defined routes to `Backbone.history`. We have to reverse the\r
+ // order of the routes here to support behavior where the most general\r
+ // routes can be defined at the bottom of the route map.\r
+ _bindRoutes: function() {\r
+ if (!this.routes) return;\r
+ var routes = [];\r
+ for (var route in this.routes) {\r
+ routes.unshift([route, this.routes[route]]);\r
+ }\r
+ for (var i = 0, l = routes.length; i < l; i++) {\r
+ this.route(routes[i][0], routes[i][1], this[routes[i][1]]);\r
+ }\r
+ },\r
+\r
+ // Convert a route string into a regular expression, suitable for matching\r
+ // against the current location hash.\r
+ _routeToRegExp: function(route) {\r
+ route = route.replace(escapeRegExp, '\\$&')\r
+ .replace(namedParam, '([^\/]+)')\r
+ .replace(splatParam, '(.*?)');\r
+ return new RegExp('^' + route + '$');\r
+ },\r
+\r
+ // Given a route, and a URL fragment that it matches, return the array of\r
+ // extracted parameters.\r
+ _extractParameters: function(route, fragment) {\r
+ return route.exec(fragment).slice(1);\r
+ }\r
+\r
+ });\r
+\r
+ // Backbone.History\r
+ // ----------------\r
+\r
+ // Handles cross-browser history management, based on URL fragments. If the\r
+ // browser does not support `onhashchange`, falls back to polling.\r
+ Backbone.History = function() {\r
+ this.handlers = [];\r
+ _.bindAll(this, 'checkUrl');\r
+ };\r
+\r
+ // Cached regex for cleaning leading hashes and slashes .\r
+ var routeStripper = /^[#\/]/;\r
+\r
+ // Cached regex for detecting MSIE.\r
+ var isExplorer = /msie [\w.]+/;\r
+\r
+ // Has the history handling already been started?\r
+ var historyStarted = false;\r
+\r
+ // Set up all inheritable **Backbone.History** properties and methods.\r
+ _.extend(Backbone.History.prototype, Backbone.Events, {\r
+\r
+ // The default interval to poll for hash changes, if necessary, is\r
+ // twenty times a second.\r
+ interval: 50,\r
+\r
+ // Get the cross-browser normalized URL fragment, either from the URL,\r
+ // the hash, or the override.\r
+ getFragment: function(fragment, forcePushState) {\r
+ if (fragment == null) {\r
+ if (this._hasPushState || forcePushState) {\r
+ fragment = window.location.pathname;\r
+ var search = window.location.search;\r
+ if (search) fragment += search;\r
+ } else {\r
+ fragment = window.location.hash;\r
+ }\r
+ }\r
+ fragment = decodeURIComponent(fragment.replace(routeStripper, ''));\r
+ if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);\r
+ return fragment;\r
+ },\r
+\r
+ // Start the hash change handling, returning `true` if the current URL matches\r
+ // an existing route, and `false` otherwise.\r
+ start: function(options) {\r
+\r
+ // Figure out the initial configuration. Do we need an iframe?\r
+ // Is pushState desired ... is it available?\r
+ if (historyStarted) throw new Error("Backbone.history has already been started");\r
+ this.options = _.extend({}, {root: '/'}, this.options, options);\r
+ this._wantsHashChange = this.options.hashChange !== false;\r
+ this._wantsPushState = !!this.options.pushState;\r
+ this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);\r
+ var fragment = this.getFragment();\r
+ var docMode = document.documentMode;\r
+ var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));\r
+ if (oldIE) {\r
+ this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;\r
+ this.navigate(fragment);\r
+ }\r
+\r
+ // Depending on whether we're using pushState or hashes, and whether\r
+ // 'onhashchange' is supported, determine how we check the URL state.\r
+ if (this._hasPushState) {\r
+ $(window).bind('popstate', this.checkUrl);\r
+ } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {\r
+ $(window).bind('hashchange', this.checkUrl);\r
+ } else if (this._wantsHashChange) {\r
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);\r
+ }\r
+\r
+ // Determine if we need to change the base url, for a pushState link\r
+ // opened by a non-pushState browser.\r
+ this.fragment = fragment;\r
+ historyStarted = true;\r
+ var loc = window.location;\r
+ var atRoot = loc.pathname == this.options.root;\r
+\r
+ // If we've started off with a route from a `pushState`-enabled browser,\r
+ // but we're currently in a browser that doesn't support it...\r
+ if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {\r
+ this.fragment = this.getFragment(null, true);\r
+ window.location.replace(this.options.root + '#' + this.fragment);\r
+ // Return immediately as browser will do redirect to new url\r
+ return true;\r
+\r
+ // Or if we've started out with a hash-based route, but we're currently\r
+ // in a browser where it could be `pushState`-based instead...\r
+ } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {\r
+ this.fragment = loc.hash.replace(routeStripper, '');\r
+ window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);\r
+ }\r
+\r
+ if (!this.options.silent) {\r
+ return this.loadUrl();\r
+ }\r
+ },\r
+\r
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,\r
+ // but possibly useful for unit testing Routers.\r
+ stop: function() {\r
+ $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);\r
+ clearInterval(this._checkUrlInterval);\r
+ historyStarted = false;\r
+ },\r
+\r
+ // Add a route to be tested when the fragment changes. Routes added later\r
+ // may override previous routes.\r
+ route: function(route, callback) {\r
+ this.handlers.unshift({route: route, callback: callback});\r
+ },\r
+\r
+ // Checks the current URL to see if it has changed, and if it has,\r
+ // calls `loadUrl`, normalizing across the hidden iframe.\r
+ checkUrl: function(e) {\r
+ var current = this.getFragment();\r
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);\r
+ if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;\r
+ if (this.iframe) this.navigate(current);\r
+ this.loadUrl() || this.loadUrl(window.location.hash);\r
+ },\r
+\r
+ // Attempt to load the current URL fragment. If a route succeeds with a\r
+ // match, returns `true`. If no defined routes matches the fragment,\r
+ // returns `false`.\r
+ loadUrl: function(fragmentOverride) {\r
+ var fragment = this.fragment = this.getFragment(fragmentOverride);\r
+ var matched = _.any(this.handlers, function(handler) {\r
+ if (handler.route.test(fragment)) {\r
+ handler.callback(fragment);\r
+ return true;\r
+ }\r
+ });\r
+ return matched;\r
+ },\r
+\r
+ // Save a fragment into the hash history, or replace the URL state if the\r
+ // 'replace' option is passed. You are responsible for properly URL-encoding\r
+ // the fragment in advance.\r
+ //\r
+ // The options object can contain `trigger: true` if you wish to have the\r
+ // route callback be fired (not usually desirable), or `replace: true`, if\r
+ // you which to modify the current URL without adding an entry to the history.\r
+ navigate: function(fragment, options) {\r
+ if (!historyStarted) return false;\r
+ if (!options || options === true) options = {trigger: options};\r
+ var frag = (fragment || '').replace(routeStripper, '');\r
+ if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;\r
+\r
+ // If pushState is available, we use it to set the fragment as a real URL.\r
+ if (this._hasPushState) {\r
+ if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;\r
+ this.fragment = frag;\r
+ window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);\r
+\r
+ // If hash changes haven't been explicitly disabled, update the hash\r
+ // fragment to store history.\r
+ } else if (this._wantsHashChange) {\r
+ this.fragment = frag;\r
+ this._updateHash(window.location, frag, options.replace);\r
+ if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {\r
+ // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.\r
+ // When replace is true, we don't want this.\r
+ if(!options.replace) this.iframe.document.open().close();\r
+ this._updateHash(this.iframe.location, frag, options.replace);\r
+ }\r
+\r
+ // If you've told us that you explicitly don't want fallback hashchange-\r
+ // based history, then `navigate` becomes a page refresh.\r
+ } else {\r
+ window.location.assign(this.options.root + fragment);\r
+ }\r
+ if (options.trigger) this.loadUrl(fragment);\r
+ },\r
+\r
+ // Update the hash location, either replacing the current entry, or adding\r
+ // a new one to the browser history.\r
+ _updateHash: function(location, fragment, replace) {\r
+ if (replace) {\r
+ location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);\r
+ } else {\r
+ location.hash = fragment;\r
+ }\r
+ }\r
+ });\r
+\r
+ // Backbone.View\r
+ // -------------\r
+\r
+ // Creating a Backbone.View creates its initial element outside of the DOM,\r
+ // if an existing element is not provided...\r
+ Backbone.View = function(options) {\r
+ this.cid = _.uniqueId('view');\r
+ this._configure(options || {});\r
+ this._ensureElement();\r
+ this.initialize.apply(this, arguments);\r
+ this.delegateEvents();\r
+ };\r
+\r
+ // Cached regex to split keys for `delegate`.\r
+ var eventSplitter = /^(\S+)\s*(.*)$/;\r
+\r
+ // List of view options to be merged as properties.\r
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];\r
+\r
+ // Set up all inheritable **Backbone.View** properties and methods.\r
+ _.extend(Backbone.View.prototype, Backbone.Events, {\r
+\r
+ // The default `tagName` of a View's element is `"div"`.\r
+ tagName: 'div',\r
+\r
+ // jQuery delegate for element lookup, scoped to DOM elements within the\r
+ // current view. This should be prefered to global lookups where possible.\r
+ $: function(selector) {\r
+ return $(selector, this.el);\r
+ },\r
+\r
+ // Initialize is an empty function by default. Override it with your own\r
+ // initialization logic.\r
+ initialize: function(){},\r
+\r
+ // **render** is the core function that your view should override, in order\r
+ // to populate its element (`this.el`), with the appropriate HTML. The\r
+ // convention is for **render** to always return `this`.\r
+ render: function() {\r
+ return this;\r
+ },\r
+\r
+ // Remove this view from the DOM. Note that the view isn't present in the\r
+ // DOM by default, so calling this method may be a no-op.\r
+ remove: function() {\r
+ this.$el.remove();\r
+ return this;\r
+ },\r
+\r
+ // For small amounts of DOM Elements, where a full-blown template isn't\r
+ // needed, use **make** to manufacture elements, one at a time.\r
+ //\r
+ // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));\r
+ //\r
+ make: function(tagName, attributes, content) {\r
+ var el = document.createElement(tagName);\r
+ if (attributes) $(el).attr(attributes);\r
+ if (content) $(el).html(content);\r
+ return el;\r
+ },\r
+\r
+ // Change the view's element (`this.el` property), including event\r
+ // re-delegation.\r
+ setElement: function(element, delegate) {\r
+ this.$el = $(element);\r
+ this.el = this.$el[0];\r
+ if (delegate !== false) this.delegateEvents();\r
+ },\r
+\r
+ // Set callbacks, where `this.events` is a hash of\r
+ //\r
+ // *{"event selector": "callback"}*\r
+ //\r
+ // {\r
+ // 'mousedown .title': 'edit',\r
+ // 'click .button': 'save'\r
+ // 'click .open': function(e) { ... }\r
+ // }\r
+ //\r
+ // pairs. Callbacks will be bound to the view, with `this` set properly.\r
+ // Uses event delegation for efficiency.\r
+ // Omitting the selector binds the event to `this.el`.\r
+ // This only works for delegate-able events: not `focus`, `blur`, and\r
+ // not `change`, `submit`, and `reset` in Internet Explorer.\r
+ delegateEvents: function(events) {\r
+ if (!(events || (events = getValue(this, 'events')))) return;\r
+ this.undelegateEvents();\r
+ for (var key in events) {\r
+ var method = events[key];\r
+ if (!_.isFunction(method)) method = this[events[key]];\r
+ if (!method) throw new Error('Event "' + events[key] + '" does not exist');\r
+ var match = key.match(eventSplitter);\r
+ var eventName = match[1], selector = match[2];\r
+ method = _.bind(method, this);\r
+ eventName += '.delegateEvents' + this.cid;\r
+ if (selector === '') {\r
+ this.$el.bind(eventName, method);\r
+ } else {\r
+ this.$el.delegate(selector, eventName, method);\r
+ }\r
+ }\r
+ },\r
+\r
+ // Clears all callbacks previously bound to the view with `delegateEvents`.\r
+ // You usually don't need to use this, but may wish to if you have multiple\r
+ // Backbone views attached to the same DOM element.\r
+ undelegateEvents: function() {\r
+ this.$el.unbind('.delegateEvents' + this.cid);\r
+ },\r
+\r
+ // Performs the initial configuration of a View with a set of options.\r
+ // Keys with special meaning *(model, collection, id, className)*, are\r
+ // attached directly to the view.\r
+ _configure: function(options) {\r
+ if (this.options) options = _.extend({}, this.options, options);\r
+ for (var i = 0, l = viewOptions.length; i < l; i++) {\r
+ var attr = viewOptions[i];\r
+ if (options[attr]) this[attr] = options[attr];\r
+ }\r
+ this.options = options;\r
+ },\r
+\r
+ // Ensure that the View has a DOM element to render into.\r
+ // If `this.el` is a string, pass it through `$()`, take the first\r
+ // matching element, and re-assign it to `el`. Otherwise, create\r
+ // an element from the `id`, `className` and `tagName` properties.\r
+ _ensureElement: function() {\r
+ if (!this.el) {\r
+ var attrs = getValue(this, 'attributes') || {};\r
+ if (this.id) attrs.id = this.id;\r
+ if (this.className) attrs['class'] = this.className;\r
+ this.setElement(this.make(this.tagName, attrs), false);\r
+ } else {\r
+ this.setElement(this.el, false);\r
+ }\r
+ }\r
+\r
+ });\r
+\r
+ // The self-propagating extend function that Backbone classes use.\r
+ var extend = function (protoProps, classProps) {\r
+ var child = inherits(this, protoProps, classProps);\r
+ child.extend = this.extend;\r
+ return child;\r
+ };\r
+\r
+ // Set up inheritance for the model, collection, and view.\r
+ Backbone.Model.extend = Backbone.Collection.extend =\r
+ Backbone.Router.extend = Backbone.View.extend = extend;\r
+\r
+ // Backbone.sync\r
+ // -------------\r
+\r
+ // Map from CRUD to HTTP for our default `Backbone.sync` implementation.\r
+ var methodMap = {\r
+ 'create': 'POST',\r
+ 'update': 'PUT',\r
+ 'delete': 'DELETE',\r
+ 'read': 'GET'\r
+ };\r
+\r
+ // Override this function to change the manner in which Backbone persists\r
+ // models to the server. You will be passed the type of request, and the\r
+ // model in question. By default, makes a RESTful Ajax request\r
+ // to the model's `url()`. Some possible customizations could be:\r
+ //\r
+ // * Use `setTimeout` to batch rapid-fire updates into a single request.\r
+ // * Send up the models as XML instead of JSON.\r
+ // * Persist models via WebSockets instead of Ajax.\r
+ //\r
+ // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests\r
+ // as `POST`, with a `_method` parameter containing the true HTTP method,\r
+ // as well as all requests with the body as `application/x-www-form-urlencoded`\r
+ // instead of `application/json` with the model in a param named `model`.\r
+ // Useful when interfacing with server-side languages like **PHP** that make\r
+ // it difficult to read the body of `PUT` requests.\r
+ Backbone.sync = function(method, model, options) {\r
+ var type = methodMap[method];\r
+\r
+ // Default JSON-request options.\r
+ var params = {type: type, dataType: 'json'};\r
+\r
+ // Ensure that we have a URL.\r
+ if (!options.url) {\r
+ params.url = getValue(model, 'url') || urlError();\r
+ }\r
+\r
+ // Ensure that we have the appropriate request data.\r
+ if (!options.data && model && (method == 'create' || method == 'update')) {\r
+ params.contentType = 'application/json';\r
+ params.data = JSON.stringify(model.toJSON());\r
+ }\r
+\r
+ // For older servers, emulate JSON by encoding the request into an HTML-form.\r
+ if (Backbone.emulateJSON) {\r
+ params.contentType = 'application/x-www-form-urlencoded';\r
+ params.data = params.data ? {model: params.data} : {};\r
+ }\r
+\r
+ // For older servers, emulate HTTP by mimicking the HTTP method with `_method`\r
+ // And an `X-HTTP-Method-Override` header.\r
+ if (Backbone.emulateHTTP) {\r
+ if (type === 'PUT' || type === 'DELETE') {\r
+ if (Backbone.emulateJSON) params.data._method = type;\r
+ params.type = 'POST';\r
+ params.beforeSend = function(xhr) {\r
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);\r
+ };\r
+ }\r
+ }\r
+\r
+ // Don't process data on a non-GET request.\r
+ if (params.type !== 'GET' && !Backbone.emulateJSON) {\r
+ params.processData = false;\r
+ }\r
+\r
+ // Make the request, allowing the user to override any Ajax options.\r
+ return $.ajax(_.extend(params, options));\r
+ };\r
+\r
+ // Wrap an optional error callback with a fallback error event.\r
+ Backbone.wrapError = function(onError, originalModel, options) {\r
+ return function(model, resp) {\r
+ var resp = model === originalModel ? resp : model;\r
+ if (onError) {\r
+ onError(model, resp, options);\r
+ } else {\r
+ originalModel.trigger('error', model, resp, options);\r
+ }\r
+ };\r
+ };\r
+\r
+ // Helpers\r
+ // -------\r
+\r
+ // Shared empty constructor function to aid in prototype-chain creation.\r
+ var ctor = function(){};\r
+\r
+ // Helper function to correctly set up the prototype chain, for subclasses.\r
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and\r
+ // class properties to be extended.\r
+ var inherits = function(parent, protoProps, staticProps) {\r
+ var child;\r
+\r
+ // The constructor function for the new subclass is either defined by you\r
+ // (the "constructor" property in your `extend` definition), or defaulted\r
+ // by us to simply call the parent's constructor.\r
+ if (protoProps && protoProps.hasOwnProperty('constructor')) {\r
+ child = protoProps.constructor;\r
+ } else {\r
+ child = function(){ parent.apply(this, arguments); };\r
+ }\r
+\r
+ // Inherit class (static) properties from parent.\r
+ _.extend(child, parent);\r
+\r
+ // Set the prototype chain to inherit from `parent`, without calling\r
+ // `parent`'s constructor function.\r
+ ctor.prototype = parent.prototype;\r
+ child.prototype = new ctor();\r
+\r
+ // Add prototype properties (instance properties) to the subclass,\r
+ // if supplied.\r
+ if (protoProps) _.extend(child.prototype, protoProps);\r
+\r
+ // Add static properties to the constructor function, if supplied.\r
+ if (staticProps) _.extend(child, staticProps);\r
+\r
+ // Correctly set child's `prototype.constructor`.\r
+ child.prototype.constructor = child;\r
+\r
+ // Set a convenience property in case the parent's prototype is needed later.\r
+ child.__super__ = parent.prototype;\r
+\r
+ return child;\r
+ };\r
+\r
+ // Helper function to get a value from a Backbone object as a property\r
+ // or as a function.\r
+ var getValue = function(object, prop) {\r
+ if (!(object && object[prop])) return null;\r
+ return _.isFunction(object[prop]) ? object[prop]() : object[prop];\r
+ };\r
+\r
+ // Throw an error when a URL is needed, and none is supplied.\r
+ var urlError = function() {\r
+ throw new Error('A "url" property or function must be specified');\r
+ };\r
+\r
+}).call(this);\r
if (!plugin_event) {\r
return;\r
}\r
- tab = Tabview.getTab(plugin_event.destination);\r
- if (!tab) {\r
- tab = new Tabview(plugin_event.destination);\r
- }\r
- tab.addMsg(null, plugin_event.nick, plugin_event.msg);\r
\r
var chan = kiwi.bbchans.detect(function (c) {\r
return c.get("name") === plugin_event.destination;\r
});\r
- chan.addMsg(null, plugin_event.nick, plugin_event.msg);\r
+ if (chan) {\r
+ chan.addMsg(null, plugin_event.nick, plugin_event.msg);\r
+ }\r
},\r
\r
/**\r
destination = data.channel;\r
}\r
\r
- tab = Tabview.getTab(destination);\r
- if (!tab) {\r
- tab = new Tabview(destination);\r
+ var chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === plugin_event.destination;\r
+ });\r
+ if (chan) {\r
+ chan.addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');\r
}\r
- tab.addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');\r
},\r
\r
/**\r
* @param {Object} data The event data\r
*/\r
onTopic: function (e, data) {\r
- var tab = Tabview.getTab(data.channel);\r
- if (tab) {\r
- tab.changeTopic(data.topic);\r
+ var chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ if (chan) {\r
+ chan.set({"topic": data.topic});\r
}\r
},\r
\r
* @param {Object} data The event data\r
*/\r
onTopicSetBy: function (e, data) {\r
- var when, tab = Tabview.getTab(data.channel);\r
- if (tab) {\r
- when = new Date(data.when * 1000).toLocaleString();\r
- tab.addMsg(null, '', 'Topic set by ' + data.nick + ' at ' + when, 'topic');\r
- }\r
+ var when,\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ when = new Date(data.when * 1000).toLocaleString();\r
+ chan.addMsg(null, '', 'Topic set by ' + data.nick + ' at ' + when, 'topic');\r
},\r
\r
/**\r
*/\r
onNotice: function (e, data) {\r
var nick = (data.nick === undefined) ? '' : data.nick,\r
- enick = '[' + nick + ']';\r
+ enick = '[' + nick + ']',\r
+ chan;\r
\r
- if (Tabview.tabExists(data.target)) {\r
- Tabview.getTab(data.target).addMsg(null, enick, data.msg, 'notice');\r
- } else if (Tabview.tabExists(nick)) {\r
- Tabview.getTab(nick).addMsg(null, enick, data.msg, 'notice');\r
- } else {\r
- Tabview.getServerTab().addMsg(null, enick, data.msg, 'notice');\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ if (chan) {\r
+ chan.addMsg(null, enick, data.msg, 'notice');\r
}\r
},\r
\r
err_box.parent().removeClass('disconnect');\r
\r
// Rejoin channels\r
- channels = '';\r
- _.each(Tabview.getAllTabs(), function (tabview) {\r
- if (tabview.name === 'server') {\r
- return;\r
- }\r
- channels += tabview.name + ',';\r
- });\r
- console.log('Rejoining: ' + channels);\r
- kiwi.gateway.join(channels);\r
+ //channels = '';\r
+ //_.each(Tabview.getAllTabs(), function (tabview) {\r
+ // if (tabview.name === 'server') {\r
+ // return;\r
+ // }\r
+ // channels += tabview.name + ',';\r
+ //});\r
+ //console.log('Rejoining: ' + channels);\r
+ //kiwi.gateway.join(channels);\r
return;\r
}\r
\r
kiwi.front.ui.doLayout();\r
}\r
\r
- Tabview.getServerTab().addMsg(null, ' ', '=== Connected OK :)', 'status');\r
- if (typeof init_data.channel === "string") {\r
- kiwi.front.joinChannel(init_data.channel);\r
- }\r
+ //Tabview.getServerTab().addMsg(null, ' ', '=== Connected OK :)', 'status');\r
+ //if (typeof init_data.channel === "string") {\r
+ // kiwi.front.joinChannel(init_data.channel);\r
+ //}\r
kiwi.plugs.run('connect', {success: true});\r
} else {\r
- Tabview.getServerTab().addMsg(null, ' ', '=== Failed to connect :(', 'status');\r
+ //Tabview.getServerTab().addMsg(null, ' ', '=== Failed to connect :(', 'status');\r
kiwi.plugs.run('connect', {success: false});\r
}\r
\r
*/\r
onConnectFail: function (e, data) {\r
var reason = (typeof data.reason === 'string') ? data.reason : '';\r
- Tabview.getServerTab().addMsg(null, '', 'There\'s a problem connecting! (' + reason + ')', 'error');\r
+ //Tabview.getServerTab().addMsg(null, '', 'There\'s a problem connecting! (' + reason + ')', 'error');\r
kiwi.plugs.run('connect', {success: false});\r
},\r
/**\r
*/\r
onDisconnect: function (e, data) {\r
var tab, tabs;\r
- tabs = Tabview.getAllTabs();\r
- for (tab in tabs) {\r
- tabs[tab].addMsg(null, '', 'Disconnected from server!', 'error disconnect');\r
- }\r
+ //tabs = Tabview.getAllTabs();\r
+ //for (tab in tabs) {\r
+ // tabs[tab].addMsg(null, '', 'Disconnected from server!', 'error disconnect');\r
+ //}\r
kiwi.plugs.run('disconnect', {success: false});\r
},\r
/**\r
*/\r
onOptions: function (e, data) {\r
if (typeof kiwi.gateway.network_name === "string" && kiwi.gateway.network_name !== "") {\r
- Tabview.getServerTab().setTabText(kiwi.gateway.network_name);\r
+ //Tabview.getServerTab().setTabText(kiwi.gateway.network_name);\r
}\r
},\r
/**\r
* @param {Object} data The event data\r
*/\r
onMOTD: function (e, data) {\r
- Tabview.getServerTab().addMsg(null, data.server, data.msg, 'motd');\r
+ //Tabview.getServerTab().addMsg(null, data.server, data.msg, 'motd');\r
},\r
/**\r
* Handles the whois event\r
idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");\r
}\r
\r
- tab = Tabview.getCurrentTab();\r
- if (data.msg) {\r
- tab.addMsg(null, data.nick, data.msg, 'whois');\r
- } else if (data.logon) {\r
- d = new Date();\r
- d.setTime(data.logon * 1000);\r
- d = d.toLocaleString();\r
+ //tab = Tabview.getCurrentTab();\r
+ //if (data.msg) {\r
+ // tab.addMsg(null, data.nick, data.msg, 'whois');\r
+ //} else if (data.logon) {\r
+ // d = new Date();\r
+ // d.setTime(data.logon * 1000);\r
+ // d = d.toLocaleString();\r
\r
- tab.addMsg(null, data.nick, 'idle for ' + idle_time + ', signed on ' + d, 'whois');\r
- } else {\r
- tab.addMsg(null, data.nick, 'idle for ' + idle_time, 'whois');\r
- }\r
+ // tab.addMsg(null, data.nick, 'idle for ' + idle_time + ', signed on ' + d, 'whois');\r
+ //} else {\r
+ // tab.addMsg(null, data.nick, 'idle for ' + idle_time, 'whois');\r
+ //}\r
},\r
/**\r
* Handles the mode event\r
* @param {Object} data The event data\r
*/\r
onMode: function (e, data) {\r
- var tab;\r
+ var tab, mem;\r
if ((typeof data.channel === 'string') && (typeof data.effected_nick === 'string')) {\r
- tab = Tabview.getTab(data.channel);\r
- tab.addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');\r
- if (tab.userlist.hasUser(data.effected_nick)) {\r
- tab.userlist.changeUserMode(data.effected_nick, data.mode.substr(1), (data.mode[0] === '+'));\r
+ // tab = Tabview.getTab(data.channel);\r
+ // tab.addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');\r
+ // if (tab.userlist.hasUser(data.effected_nick)) {\r
+ // tab.userlist.changeUserMode(data.effected_nick, data.mode.substr(1), (data.mode[0] === '+'));\r
+ // }\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ if (chan) {\r
+ chan.addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');\r
+ mem = _.detect(chan.get("members"), function (m) {\r
+ return data.effected_nick === m.get("nick");\r
+ });\r
+ if (mem) {\r
+ if (data.mode[0] === '+') {\r
+ mem.addMode(data.mode);\r
+ } else {\r
+ mem.removeMode(data.mode);\r
+ }\r
+ }\r
}\r
}\r
-\r
// TODO: Other mode changes that aren't +/- qaohv. - JA\r
},\r
/**\r
* @param {Object} data The event data\r
*/\r
onUserList: function (e, data) {\r
- var tab;\r
+ var tab, chan;\r
\r
- tab = Tabview.getTab(data.channel);\r
- if (!tab) {\r
- return;\r
- }\r
+ //tab = Tabview.getTab(data.channel);\r
+ //if (!tab) {\r
+ // return;\r
+ //}\r
\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
if ((!kiwi.front.cache.userlist) || (!kiwi.front.cache.userlist.updating)) {\r
if (!kiwi.front.cache.userlist) {\r
kiwi.front.cache.userlist = {updating: true};\r
} else {\r
kiwi.front.cache.userlist.updating = true;\r
}\r
- tab.userlist.empty();\r
+ chan.get("members").reset();\r
}\r
\r
- tab.userlist.addUser(data.users);\r
-\r
+ //tab.userlist.addUser(data.users);\r
+ if (chan) {\r
+ chan.get("members").add(data.users, {"silent": true});\r
+ }\r
},\r
/**\r
* Handles the userListEnd event\r
kiwi.front.cache.userlist = {};\r
}\r
kiwi.front.cache.userlist.updating = false;\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ if (chan) {\r
+ chan.get("members").trigger("change");\r
+ }\r
},\r
\r
/**\r
* @param {Object} data The event data\r
*/\r
onJoin: function (e, data) {\r
- var tab = Tabview.getTab(data.channel);\r
- if (!tab) {\r
- tab = new Tabview(data.channel.toLowerCase());\r
- }\r
+ //var tab = Tabview.getTab(data.channel);\r
+ //if (!tab) {\r
+ // tab = new Tabview(data.channel.toLowerCase());\r
+ //}\r
\r
- tab.addMsg(null, ' ', '--> ' + data.nick + ' [' + data.ident + '@' + data.hostname + '] has joined', 'action join', 'color:#009900;');\r
+ //tab.addMsg(null, ' ', '--> ' + data.nick + ' [' + data.ident + '@' + data.hostname + '] has joined', 'action join', 'color:#009900;');\r
\r
- var c = new kiwi.model.Channel({"name": data.channel.toLowerCase()});\r
- c.get("members").add(new kiwi.model.Member({"nick": data.nick, "modes": []}));\r
- kiwi.bbchans.add(c);\r
- if (data.nick === kiwi.gateway.nick) {\r
- return; // Not needed as it's already in nicklist\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ if (!chan) {\r
+ chan = new kiwi.model.Channel({"name": data.channel.toLowerCase()});\r
+ chan.get("members").add(new kiwi.model.Member({"nick": data.nick, "modes": []}));\r
+ kiwi.bbchans.add(chan);\r
+ } else {\r
+ chan.get("members").add(data.users, {"silent": true});\r
}\r
+ chan.view.show();\r
+\r
+ //if (data.nick === kiwi.gateway.nick) {\r
+ // return; // Not needed as it's already in nicklist\r
+ //}\r
\r
- tab.userlist.addUser({nick: data.nick, modes: []});\r
+ //tab.userlist.addUser({nick: data.nick, modes: []});\r
},\r
/**\r
* Handles the part event\r
* @param {Object} data The event data\r
*/\r
onPart: function (e, data) {\r
- var tab = Tabview.getTab(data.channel);\r
- if (tab) {\r
+ var chan, members;\r
+ //var tab = Tabview.getTab(data.channel);\r
+ //if (tab) {\r
// If this is us, close the tabview\r
+ // if (data.nick === kiwi.gateway.nick) {\r
+ // tab.close();\r
+ // Tabview.getServerTab().show();\r
+ // return;\r
+ // }\r
+\r
+ // tab.addMsg(null, ' ', '<-- ' + data.nick + ' has left (' + data.message + ')', 'action part', 'color:#990000;');\r
+ // tab.userlist.removeUser(data.nick);\r
+ //}\r
+ chan = kiwi.bbchans.detect(function (c) {\r
+ return c.get("name") === data.channel;\r
+ });\r
+ if (chan) {\r
if (data.nick === kiwi.gateway.nick) {\r
- tab.close();\r
- Tabview.getServerTab().show();\r
- return;\r
+ chan.trigger("close");\r
+ } else {\r
+ chan.addMsg(null, ' ', '<-- ' + data.nick + ' has left (' + data.message + ')', 'action part', 'color:#990000;');\r
+ members = chan.get("members");\r
+ members.remove(_.detect(members, function (m) {\r
+ return data.nick === m.get("nick");\r
+ }));\r
}\r
-\r
- tab.addMsg(null, ' ', '<-- ' + data.nick + ' has left (' + data.message + ')', 'action part', 'color:#990000;');\r
- tab.userlist.removeUser(data.nick);\r
}\r
},\r
/**\r
* @param {Object} data The event data\r
*/\r
onKick: function (e, data) {\r
- var tab = Tabview.getTab(data.channel);\r
- if (tab) {\r
- // If this is us, close the tabview\r
- if (data.kicked === kiwi.gateway.nick) {\r
- //tab.close();\r
- tab.addMsg(null, ' ', '=== You have been kicked from ' + data.channel + '. ' + data.message, 'status kick');\r
- tab.safe_to_close = true;\r
- tab.userlist.remove();\r
- return;\r
- }\r
-\r
- tab.addMsg(null, ' ', '<-- ' + data.kicked + ' kicked by ' + data.nick + '(' + data.message + ')', 'action kick', 'color:#990000;');\r
- tab.userlist.removeUser(data.nick);\r
- }\r
+ //var tab = Tabview.getTab(data.channel);\r
+ //if (tab) {\r
+ // // If this is us, close the tabview\r
+ // if (data.kicked === kiwi.gateway.nick) {\r
+ // //tab.close();\r
+ // tab.addMsg(null, ' ', '=== You have been kicked from ' + data.channel + '. ' + data.message, 'status kick');\r
+ // tab.safe_to_close = true;\r
+ // tab.userlist.remove();\r
+ // return;\r
+ // }\r
+\r
+ // tab.addMsg(null, ' ', '<-- ' + data.kicked + ' kicked by ' + data.nick + '(' + data.message + ')', 'action kick', 'color:#990000;');\r
+ // tab.userlist.removeUser(data.nick);\r
+ //}\r
},\r
/**\r
* Handles the nick event\r
* @param {Object} data The event data\r
*/\r
onNick: function (e, data) {\r
- if (data.nick === kiwi.gateway.nick) {\r
- kiwi.gateway.nick = data.newnick;\r
- kiwi.front.ui.doLayout();\r
- }\r
+ //if (data.nick === kiwi.gateway.nick) {\r
+ // kiwi.gateway.nick = data.newnick;\r
+ // kiwi.front.ui.doLayout();\r
+ //}\r
\r
- _.each(Tabview.getAllTabs(), function (tab) {\r
- if (tab.userlist.hasUser(data.nick)) {\r
- tab.userlist.renameUser(data.nick, data.newnick);\r
- tab.addMsg(null, ' ', '=== ' + data.nick + ' is now known as ' + data.newnick, 'action changenick');\r
- }\r
- });\r
+ //_.each(Tabview.getAllTabs(), function (tab) {\r
+ // if (tab.userlist.hasUser(data.nick)) {\r
+ // tab.userlist.renameUser(data.nick, data.newnick);\r
+ // tab.addMsg(null, ' ', '=== ' + data.nick + ' is now known as ' + data.newnick, 'action changenick');\r
+ // }\r
+ //});\r
},\r
/**\r
* Handles the quit event\r
* @param {Object} data The event data\r
*/\r
onQuit: function (e, data) {\r
- _.each(Tabview.getAllTabs(), function (tab) {\r
- if (tab.userlist.hasUser(data.nick)) {\r
- tab.userlist.removeUser(data.nick);\r
- tab.addMsg(null, ' ', '<-- ' + data.nick + ' has quit (' + data.message + ')', 'action quit', 'color:#990000;');\r
- }\r
- });\r
+ //_.each(Tabview.getAllTabs(), function (tab) {\r
+ // if (tab.userlist.hasUser(data.nick)) {\r
+ // tab.userlist.removeUser(data.nick);\r
+ // tab.addMsg(null, ' ', '<-- ' + data.nick + ' has quit (' + data.message + ')', 'action quit', 'color:#990000;');\r
+ // }\r
+ //});\r
},\r
/**\r
* Handles the channelRedirect event\r
* @param {Object} data The event data\r
*/\r
onChannelRedirect: function (e, data) {\r
- var tab = Tabview.getTab(data.from);\r
- tab.close();\r
- tab = new Tabview(data.to);\r
- tab.addMsg(null, ' ', '=== Redirected from ' + data.from, 'action');\r
+ //var tab = Tabview.getTab(data.from);\r
+ //tab.close();\r
+ //tab = new Tabview(data.to);\r
+ //tab.addMsg(null, ' ', '=== Redirected from ' + data.from, 'action');\r
},\r
\r
/**\r
* @param {Object} data The event data\r
*/\r
onIRCError: function (e, data) {\r
- var t_view,\r
+ /*var t_view,\r
tab = Tabview.getTab(data.channel);\r
if (data.channel !== undefined && tab) {\r
t_view = data.channel;\r
default:\r
// We don't know what data contains, so don't do anything with it.\r
console.log(e, data);\r
- }\r
+ }*/\r
},\r
\r
\r
kiwi.bbtabs = new kiwi.view.Tabs({"el": $('#kiwi .windowlist ul')[0], "model": kiwi.bbchans});
- server_tabview = new Tabview('server');
- server_tabview.userlist.setWidth(0); // Disable the userlist
- server_tabview.setIcon('/img/app_menu.png');
- $('.icon', server_tabview.tab).tipTip({
- delay: 0,
- keepAlive: true,
- content: $('#tmpl_network_menu').tmpl({}).html(),
- activation: 'click'
- });
+ //server_tabview = new Tabview('server');
+ //server_tabview.userlist.setWidth(0); // Disable the userlist
+ //server_tabview.setIcon('/img/app_menu.png');
+ //$('.icon', server_tabview.tab).tipTip({
+ // delay: 0,
+ // keepAlive: true,
+ // content: $('#tmpl_network_menu').tmpl({}).html(),
+ // activation: 'click'
+ //});
// Any pre-defined nick?
if (typeof window.init_data.nick === "string") {
var chan, text;
text = $(this).text();
if (text !== kiwi.front.cache.original_topic) {
- chan = Tabview.getCurrentTab().name;
- kiwi.gateway.topic(chan, text);
+ //chan = Tabview.getCurrentTab().name;
+ //kiwi.gateway.topic(chan, text);
}
});
tab;
for (i in chans) {
chan = chans[i];
- tab = Tabview.getTab(chan);
- if ((!tab) || (tab.safe_to_close === true)) {
+ //tab = Tabview.getTab(chan);
+ //if ((!tab) || (tab.safe_to_close === true)) {
kiwi.gateway.join(chan);
- tab = new Tabview(chan);
- } else {
- tab.show();
- }
+ //tab = new Tabview(chan);
+ //} else {
+ // tab.show();
+ //}
}
},
parts[3] = true;
}
- Tabview.getCurrentTab().addMsg(null, ' ', '=== Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...', 'status');
+ //Tabview.getCurrentTab().addMsg(null, ' ', '=== Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...', 'status');
+ console.log('Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...');
kiwi.gateway.connect(parts[1], parts[2], parts[3], parts[4]);
break;
case '/part':
if (typeof parts[1] === "undefined") {
- if (Tabview.getCurrentTab().safe_to_close) {
- Tabview.getCurrentTab().close();
- } else {
- kiwi.gateway.part(Tabview.getCurrentTab().name);
- }
+ //if (Tabview.getCurrentTab().safe_to_close) {
+ // Tabview.getCurrentTab().close();
+ //} else {
+ // kiwi.gateway.part(Tabview.getCurrentTab().name);
+ //}
} else {
kiwi.gateway.part(msg.substring(6));
}
case '/q':
case '/query':
if (typeof parts[1] !== "undefined") {
- tab = new Tabview(parts[1]);
+ //tab = new Tabview(parts[1]);
}
break;
msg_sliced = msg.split(' ').slice(2).join(' ');
kiwi.gateway.privmsg(parts[1], msg_sliced);
- tab = Tabview.getTab(parts[1]);
- if (!tab) {
- tab = new Tabview(parts[1]);
- }
- tab.addMsg(null, kiwi.gateway.nick, msg_sliced);
+ //tab = Tabview.getTab(parts[1]);
+ //if (!tab) {
+ // tab = new Tabview(parts[1]);
+ //}
+ //tab.addMsg(null, kiwi.gateway.nick, msg_sliced);
}
break;
}
t = msg.split(' ', 3);
nick = t[1];
- kiwi.gateway.kick(Tabview.getCurrentTab().name, nick, t[2]);
+ //kiwi.gateway.kick(Tabview.getCurrentTab().name, nick, t[2]);
break;
case '/quote':
break;
case '/me':
- tab = Tabview.getCurrentTab();
- kiwi.gateway.ctcp(true, 'ACTION', tab.name, msg.substring(4));
- tab.addMsg(null, ' ', '* ' + kiwi.gateway.nick + ' ' + msg.substring(4), 'action', 'color:#555;');
+ //tab = Tabview.getCurrentTab();
+ //kiwi.gateway.ctcp(true, 'ACTION', tab.name, msg.substring(4));
+ //tab.addMsg(null, ' ', '* ' + kiwi.gateway.nick + ' ' + msg.substring(4), 'action', 'color:#555;');
break;
case '/notice':
t.setSelectionRange(pos, pos);
}
} else {
- kiwi.gateway.topic(Tabview.getCurrentTab().name, msg.split(' ', 2)[1]);
+ //kiwi.gateway.topic(Tabview.getCurrentTab().name, msg.split(' ', 2)[1]);
}
break;
case '/kiwi':
- kiwi.gateway.ctcp(true, 'KIWI', Tabview.getCurrentTab().name, msg.substring(6));
+ //kiwi.gateway.ctcp(true, 'KIWI', Tabview.getCurrentTab().name, msg.substring(6));
break;
case '/ctcp':
console.log(parts);
kiwi.gateway.ctcp(true, t, dest, msg);
- Tabview.getServerTab().addMsg(null, 'CTCP Request', '[to ' + dest + '] ' + t + ' ' + msg, 'ctcp');
+ //Tabview.getServerTab().addMsg(null, 'CTCP Request', '[to ' + dest + '] ' + t + ' ' + msg, 'ctcp');
break;
default:
//Tabview.getCurrentTab().addMsg(null, ' ', '--> Invalid command: '+parts[0].substring(1));
if (msg.trim() === '') {
return;
}
- if (Tabview.getCurrentTab().name !== 'server') {
- kiwi.gateway.privmsg(Tabview.getCurrentTab().name, msg);
- Tabview.getCurrentTab().addMsg(null, kiwi.gateway.nick, msg);
- }
+ //if (Tabview.getCurrentTab().name !== 'server') {
+ // kiwi.gateway.privmsg(Tabview.getCurrentTab().name, msg);
+ // Tabview.getCurrentTab().addMsg(null, kiwi.gateway.nick, msg);
+ //}
}
},
var win_list = $('#kiwi .windowlist ul'),
listitems = win_list.children('li').get();
- listitems.sort(function (a, b) {
+ /*listitems.sort(function (a, b) {
if (a === Tabview.getServerTab().tab[0]) {
return -1;
}
$.each(listitems, function(idx, itm) {
win_list.append(itm);
- });
+ });*/
},
};
};
-/**
-* @constructor
-* @param {String} name The name of the UserList
-*/
-var UserList = function (name) {
- /*globals User */
- var userlist, list_html, sortUsers;
-
- userlist = [];
-
- $('#kiwi .userlist').append($('<ul id="kiwi_userlist_' + name + '"></ul>'));
- list_html = $('#kiwi_userlist_' + name);
- $('a.nick', list_html[0]).live('click', this.clickHandler);
-
- /**
- * @inner
- */
- sortUsers = function () {
- var parent;
- parent = list_html.parent();
- list_html = list_html.detach();
-
- // Not sure this is needed.
- // It's O(n^2) as well, so need to test to see if it works without.
- // Alternative to test: list_html.children('li').detach();
- list_html.children().each(function (child) {
- var i, nick;
- child = $(child);
- nick = child.data('nick');
- for (i = 0; i < userlist.length; i++) {
- if (userlist[i].nick === nick) {
- userlist[i].html = child.detach();
- break;
- }
- }
- });
-
- userlist.sort(User.compare);
-
- _.each(userlist, function (user) {
- user.html = user.html.appendTo(list_html);
- });
-
- list_html = list_html.appendTo(parent);
- };
-
- /**
- * Adds a user or users to the UserList.
- * Chainable method.
- * @param {Object} users The user or Array of users to add
- * @returns {UserList} This UserList
- */
- this.addUser = function (users) {
- if (!_.isArray(users)) {
- users = [users];
- }
- _.each(users, function (user) {
- user = new User(user.nick, user.modes);
- user.html = $('<li><a class="nick">' + user.prefix + user.nick + '</a></li>');
- user.html.data('user', user);
- userlist.push(user);
- });
- sortUsers();
-
- return this;
- };
-
- /**
- * Removes a user or users from the UserList.
- * Chainable method.
- * @param {String} nicks The nick or Array of nicks to remove
- * @returns {UserList} This UserList
- */
- this.removeUser = function (nicks) {
- var toRemove;
- if (!_.isArray(nicks)) {
- nicks = [nicks];
- }
- toRemove = _.select(userlist, function (user) {
- return _.any(nicks, function (n) {
- return n === user.nick;
- });
- });
-
- _.each(toRemove, function (user) {
- user.html.remove();
- });
-
- userlist = _.difference(userlist, toRemove);
-
- return this;
- };
-
- /**
- * Renames a user in the UserList.
- * Chainable method.
- * @param {String} oldNick The old nick
- * @param {String} newNick The new nick
- * @returns {UserList} This UserList
- */
- this.renameUser = function (oldNick, newNick) {
- var user = _.detect(userlist, function (u) {
- return u.nick === oldNick;
- });
- if (user) {
- user.nick = newNick;
- user.html.text(User.getPrefix(user.modes) + newNick);
- }
-
- sortUsers();
-
- return this;
- };
-
- /**
- * Lists the users in this UserList.
- * @param {Boolean} modesort True to enable sorting by mode, false for lexicographical sort
- * @param {Array} mode If specified, only return those users who have the specified modes
- * @returns {Array} The users in the UserList that match the criteria
- */
- this.listUsers = function (modesort, modes) {
- var users = userlist;
- if (modes) {
- users = _.select(users, function (user) {
- return _.any(modes, function (m) {
- return _.any(user.modes, function (um) {
- return m === um;
- });
- });
- });
- }
- if ((modesort === true) || (typeof modesort === undefined)) {
- return users;
- } else {
- return _.sortBy(users, function (user) {
- return user.nick;
- });
- }
- };
-
- /**
- * Remove this UserList from the DOM.
- */
- this.remove = function () {
- list_html.remove();
- list_html = null;
- userlist = null;
- };
-
- /**
- * Empty the UserList.
- * Chainable method.
- * @returns {UserList} This UserList
- */
- this.empty = function () {
- list_html.children().remove();
- userlist = [];
-
- return this;
- };
-
- /**
- * Checks whether a given nick is in the UserList.
- * @param {String} nick The nick to search for
- * @returns {Boolean} True if the nick is in the userlist, false otherwise
- */
- this.hasUser = function (nick) {
- return _.any(userlist, function (user) {
- return user.nick === nick;
- });
- };
-
- /**
- * Returns the object representing the user with the given nick, if it is in the UserList.
- * @param {String} nick The nick to retrieve
- * @returns {Object} An object representing the user, if it exists, null otherwise
- */
- this.getUser = function (nick) {
- if (this.hasUser(nick)) {
- return _.detect(userlist, function (user) {
- return user.nick === nick;
- });
- } else {
- return null;
- }
- };
-
- /**
- * Sets the UserList's activity.
- * Chainable method.
- * @param {Boolean} active If true, sets the UserList to active. If False, sets it to inactive
- * @returns {UserList} This UserList
- */
- this.active = function (active) {
- if ((arguments.length === 0) || (active)) {
- list_html.addClass('active');
- list_html.show();
- } else {
- list_html.removeClass('active');
- list_html.hide();
- }
-
- return this;
- };
-
- /**
- * Updates a user's mode.
- * Chainable method.
- * @param {String} nick The nick of the user to modify
- * @param {String} mode The mode to add or remove
- * @param {Boolean} add Adds the mode if true, removes it otherwise
- * @returns {UserList} This UserList
- */
- this.changeUserMode = function (nick, mode, add) {
- var user, prefix;
- if (this.hasUser(nick)) {
- user = _.detect(userlist, function (u) {
- return u.nick === nick;
- });
-
- prefix = user.prefix;
- if ((arguments.length < 3) || (add)) {
- user.addMode(mode);
- } else {
- user.removeMode(mode);
- }
- if (prefix !== user.prefix) {
- user.html.children('a:first').text(user.prefix + user.nick);
- }
- sortUsers();
- }
-
- return this;
- };
-};
-/**
-* @memberOf UserList
-*/
-UserList.prototype.width = 100; // 0 to disable
-/**
-* Sets the width of the UserList.
-* Chainable method.
-* @param {Number} newWidth The new width of the UserList
-* @returns {UserList} This UserList
-*/
-UserList.prototype.setWidth = function (newWidth) {
- var w, u;
- if (typeof newWidth === 'number') {
- this.width = newWidth;
- }
-
- w = $('#windows');
- u = $('#kiwi .userlist');
-
- u.width(this.width);
-
- return this;
-};
-/**
-* The click handler for this UserList
-*/
-UserList.prototype.clickHandler = function () {
- var li = $(this).parent(),
- user = li.data('user'),
- userbox;
-
- // Remove any existing userboxes
- $('#kiwi .userbox').remove();
-
- if (li.data('userbox') === true) {
- // This li already has the userbox, show remove it instead
- li.removeData('userbox');
-
- } else {
- // We don't have a userbox so create one
- userbox = $('#tmpl_user_box').tmpl({nick: user.nick}).appendTo(li);
-
- $('.userbox_query', userbox).click(function (ev) {
- var nick = $('#kiwi .userbox_nick').val();
- kiwi.front.run('/query ' + nick);
- });
-
- $('.userbox_whois', userbox).click(function (ev) {
- var nick = $('#kiwi .userbox_nick').val();
- kiwi.front.run('/whois ' + nick);
- });
- li.data('userbox', true);
- }
-};
-
-
-
-/**
-* @constructor
-* The User class. Represents a user on a channel.
-* @param {String} nick The user's nickname
-* @param {Array} modes An array of channel user modes
-*/
-var User = function (nick, modes) {
- var sortModes;
- /**
- * @inner
- */
- sortModes = function (modes) {
- return modes.sort(function (a, b) {
- var a_idx, b_idx, i;
- for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
- if (kiwi.gateway.user_prefixes[i].mode === a) {
- a_idx = i;
- }
- }
- for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
- if (kiwi.gateway.user_prefixes[i].mode === b) {
- b_idx = i;
- }
- }
- if (a_idx < b_idx) {
- return -1;
- } else if (a_idx > b_idx) {
- return 1;
- } else {
- return 0;
- }
- });
- };
-
- this.nick = User.stripPrefix(nick);
- this.modes = modes || [];
- this.modes = sortModes(this.modes);
- this.prefix = User.getPrefix(this.modes);
-
- /**
- * @inner
- */
- this.addMode = function (mode) {
- this.modes.push(mode);
- this.modes = sortModes(this.modes);
- this.prefix = User.getPrefix(this.modes);
- return this;
- };
-};
-
-/**
-* Removes a channel mode from the user
-* @param {String} mode The mode(s) to remove
-* @returns {User} Returns the User object to allow chaining
-*/
-User.prototype.removeMode = function (mode) {
- this.modes = _.reject(this.modes, function (m) {
- return m === mode;
- });
- this.prefix = User.getPrefix(this.modes);
- return this;
-};
-
-/**
-* Checks to see if the user is an op on the channel
-* @returns {Boolean} True if the user is an op, false otherwise
-*/
-User.prototype.isOp = function () {
- // return true if this.mode[0] > o
- return false;
-};
-
-/**
-* Returns the highest user prefix (e.g.~, @, or +) that matches the modes given
-* @param {Array} modes An array of mode letters
-* @returns {String} The user's prefix
-*/
-User.getPrefix = function (modes) {
- var prefix = '';
- if (typeof modes[0] !== 'undefined') {
- prefix = _.detect(kiwi.gateway.user_prefixes, function (prefix) {
- return prefix.mode === modes[0];
- });
- prefix = (prefix) ? prefix.symbol : '';
- }
- return prefix;
-};
-
-/**
-* Returns the user's nick without the mode prefix
-* @param {String} nick The nick to strip the prefix from
-* @returns {String} The nick without the prefix
-*/
-User.stripPrefix = function (nick) {
- var tmp = nick, i, j, k;
- i = 0;
- for (j = 0; j < nick.length; j++) {
- for (k = 0; k < kiwi.gateway.user_prefixes.length; k++) {
- if (nick.charAt(j) === kiwi.gateway.user_prefixes[k].symbol) {
- i++;
- break;
- }
- }
- }
-
- return tmp.substr(i);
-};
-
-/**
-* Comparison function to order nicks based on their modes and/or nicks
-* @param {User} a The first User to evaluate
-* @param {User} b The second User to evaluate
-* @returns {Number} -1 if a should be sorted before b, 1 if b should be sorted before a, and 0 if the two Users are the same.
-*/
-User.compare = function (a, b) {
- var i, a_idx, b_idx, a_nick, b_nick;
- // Try to sort by modes first
- if (a.modes.length > 0) {
- // a has modes, but b doesn't so a should appear first
- if (b.modes.length === 0) {
- return -1;
- }
- a_idx = b_idx = -1;
- // Compare the first (highest) mode
- for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
- if (kiwi.gateway.user_prefixes[i].mode === a.modes[0]) {
- a_idx = i;
- }
- }
- for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
- if (kiwi.gateway.user_prefixes[i].mode === b.modes[0]) {
- b_idx = i;
- }
- }
- if (a_idx < b_idx) {
- return -1;
- } else if (a_idx > b_idx) {
- return 1;
- }
- // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting
-
- } else if (b.modes.length > 0) {
- // b has modes but a doesn't so b should appear first
- return 1;
- }
- a_nick = a.nick.toLocaleUpperCase();
- b_nick = b.nick.toLocaleUpperCase();
- // Lexicographical sorting
- if (a_nick < b_nick) {
- return -1;
- } else if (a_nick > b_nick) {
- return 1;
- } else {
- // This should never happen; both users have the same nick.
- console.log('Something\'s gone wrong somewhere - two users have the same nick!');
- return 0;
- }
-};
-
-
-
/*
* MISC VIEW
*/
$('#kiwi .toolbars .tab_part').remove();
};
-
-
-
-
-/*
- *
- * TABVIEWS
- *
- */
-
-/**
-* @constructor
-* A tab to show a channel or query window
-* @param {String} v_name The window's target's name (i.e. channel name or nickname)
-*/
-var Tabview = function (v_name) {
- /*global Tabview, UserList */
- var re, htmlsafe_name, tmp_divname, tmp_userlistname, tmp_tabname, tmp_tab, userlist_enabled = true;
-
- if (v_name.charAt(0) === kiwi.gateway.channel_prefix) {
- htmlsafe_name = 'chan_' + randomString(15);
- } else {
- htmlsafe_name = 'query_' + randomString(15);
- userlist_enabled = false;
- }
-
- tmp_divname = 'kiwi_window_' + htmlsafe_name;
- tmp_userlistname = 'kiwi_userlist_' + htmlsafe_name;
- tmp_tabname = 'kiwi_tab_' + htmlsafe_name;
-
- if (!Tabview.tabExists(v_name)) {
- // Create the window
- $('#kiwi .windows .scroller').append('<div id="' + tmp_divname + '" class="messages"></div>');
-
- // Create the window tab
-
- tmp_tab = $('<li id="' + tmp_tabname + '"><span></span></li>');
- $('span', tmp_tab).text(v_name);
- $('#kiwi .windowlist ul').append(tmp_tab);
- tmp_tab.click(function (e) {
- var tab = Tabview.getTab(v_name);
- if (tab) {
- tab.show();
- }
- });
-
- kiwi.front.sortWindowList();
- }
-
- kiwi.front.tabviews[v_name.toLowerCase()] = this;
- this.name = v_name;
- this.div = $('#' + tmp_divname);
- this.userlist = new UserList(htmlsafe_name);
- this.tab = $('#' + tmp_tabname);
- this.panel = $('#panel1');
-
- if (!userlist_enabled) {
- this.userlist.setWidth(0);
- }
- this.show();
-
- if (typeof registerTouches === "function") {
- //alert("Registering touch interface");
- //registerTouches($('#'+tmp_divname));
- registerTouches(document.getElementById(tmp_divname));
- }
-
- kiwi.front.ui.doLayoutSize();
-};
-Tabview.prototype.name = null;
-Tabview.prototype.div = null;
-Tabview.prototype.userlist = null;
-Tabview.prototype.tab = null;
-Tabview.prototype.topic = "";
-Tabview.prototype.safe_to_close = false; // If we have been kicked/banned/etc from this channel, don't wait for a part message
-Tabview.prototype.panel = null;
-Tabview.prototype.msg_count = 0;
-/**
-* Brings this view to the foreground
-*/
-Tabview.prototype.show = function () {
- var w, u;
-
- $('.messages', this.panel).removeClass("active");
- $('#kiwi .userlist ul').removeClass("active");
- $('#kiwi .toolbars ul li').removeClass("active");
-
- w = $('#windows');
- u = $('#kiwi .userlist');
-
- this.panel.css('overflow-y', 'scroll');
-
- // Set the window size accordingly
- if (this.userlist.width > 0) {
- this.userlist.setWidth();
- w.css('right', u.outerWidth(true));
- this.userlist.active(true);
- // Enable the userlist resizer
- $('#nicklist_resize').css('display', 'block');
- } else {
- w.css('right', 0);
- // Disable the userlist resizer
- $('#nicklist_resize').css('display', 'none');
- }
-
- this.div.addClass('active');
- this.tab.addClass('active');
-
- // Add the part image to the tab
- this.addPartImage();
-
- this.clearHighlight();
- kiwi.front.ui.setTopicText(this.topic);
- kiwi.front.cur_channel = this;
-
- // If we're using fancy scrolling, refresh it
- if (touch_scroll) {
- touch_scroll.refresh();
- }
-
- this.scrollBottom();
- if (!touchscreen) {
- $('#kiwi_msginput').focus();
- }
-};
-/**
-* Removes the panel from the UI and destroys its contents
-*/
-Tabview.prototype.close = function () {
- this.div.remove();
- this.userlist.remove();
- this.userlist = null;
- this.tab.remove();
-
- if (kiwi.front.cur_channel === this) {
- kiwi.front.tabviews.server.show();
- }
- delete kiwi.front.tabviews[this.name.toLowerCase()];
-};
-/**
-* Adds the close image to the tab
-*/
-Tabview.prototype.addPartImage = function () {
- this.clearPartImage();
-
- // We can't close this tab, so don't have the close image
- if (this.name === 'server') {
- return;
- }
-
- var del_html = '<img src="/img/redcross.png" class="tab_part" />';
- this.tab.append(del_html);
-
- $('.tab_part', this.tab).click(function () {
- if (kiwi.front.isChannel($(this).parent().text())) {
- kiwi.front.run("/part");
- } else {
- // Make sure we don't close the server tab
- if (kiwi.front.cur_channel.name !== 'server') {
- kiwi.front.cur_channel.close();
- }
- }
- });
-};
-/**
-* Removes the close image from the tab
-*/
-Tabview.prototype.clearPartImage = function () {
- $('#kiwi .toolbars .tab_part').remove();
-};
-/**
-* Sets the tab's icon
-* @param {String} url The URL of the icon to display
-*/
-Tabview.prototype.setIcon = function (url) {
- this.tab.prepend('<img src="' + url + '" class="icon" />');
- this.tab.css('padding-left', '33px');
-};
-/**
-* Sets the tab's label
-*/
-Tabview.prototype.setTabText = function (text) {
- $('span', this.tab).text(text);
-};
-/**
-* Adds a message to the window.
-* This method will automatically format the message (bold, underline, colours etc.)
-* @param {Date} time The timestamp of the message. May be null.
-* @param {String} nick The origin of the message
-* @param {String} msg The message to display
-* @param {String} type The CSS class to assign to the whole message line
-* @param {String} style Extra CSS commands to apply just to the msg
-*/
-Tabview.prototype.addMsg = function (time, nick, msg, type, style) {
- var self, tmp, d, re, line_msg;
-
- self = this;
-
- tmp = {msg: msg, time: time, nick: nick, tabview: this.name};
- tmp = kiwi.plugs.run('addmsg', tmp);
- if (!tmp) {
- return;
- }
-
-
- msg = tmp.msg;
- time = tmp.time;
- nick = tmp.nick;
-
- if (time === null) {
- d = new Date();
- time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0");
- }
-
- // The CSS class (action, topic, notice, etc)
- if (typeof type !== "string") {
- type = '';
- }
-
- // Make sure we don't have NaN or something
- if (typeof msg !== "string") {
- msg = '';
- }
-
- // Make the channels clickable
- re = new RegExp('\\B(' + kiwi.gateway.channel_prefix + '[^ ,.\\007]+)', 'g');
- msg = msg.replace(re, function (match) {
- return '<a class="chan">' + match + '</a>';
- });
-
- msg = kiwi.front.formatIRCMsg(msg);
-
- // Build up and add the line
- line_msg = $('<div class="msg ' + type + '"><div class="time">' + time + '</div><div class="nick">' + nick + '</div><div class="text" style="' + style + '">' + msg + ' </div></div>');
- this.div.append(line_msg);
-
- this.msg_count++;
- if (this.msg_count > 250) {
- $('.msg:first', this.div).remove();
- this.msg_count--;
- }
-
- if (!touchscreen) {
- this.scrollBottom();
- } else {
- touch_scroll.refresh();
- //console.log(this.div.attr("scrollHeight") +" - "+ $('#windows').height());
- this.scrollBottom();
- //if(this.div.attr("scrollHeight") > $('#windows').height()){
- // touch_scroll.scrollTo(0, this.div.height());
- //}
- }
-};
-/**
-* Scroll to the bottom of the window
-*/
-Tabview.prototype.scrollBottom = function () {
- var panel = this.panel;
- panel[0].scrollTop = panel[0].scrollHeight;
-};
-/**
-* Change a user's nick on the channel
-* @param {String} newNick The new nick
-* @param {String} oldNick The old nick
-*/
-Tabview.prototype.changeNick = function (newNick, oldNick) {
- var inChan = this.userlist.hasUser(oldNick);
- if (inChan) {
- this.userlist.renameUser(oldNick, newNick);
- this.addMsg(null, ' ', '=== ' + oldNick + ' is now known as ' + newNick, 'action changenick');
- }
-};
-/**
-* Highlight the tab
-*/
-Tabview.prototype.highlight = function () {
- this.tab.addClass('highlight');
-};
-/**
-* Indicate activity on the tab
-*/
-Tabview.prototype.activity = function () {
- this.tab.addClass('activity');
-};
-/**
-* Clear the tab's highlight
-*/
-Tabview.prototype.clearHighlight = function () {
- this.tab.removeClass('highlight');
- this.tab.removeClass('activity');
-};
-/**
-* Change the channel's topic
-* @param {String} new_topic The new channel topic
-*/
-Tabview.prototype.changeTopic = function (new_topic) {
- this.topic = new_topic;
- this.addMsg(null, ' ', '=== Topic for ' + this.name + ' is: ' + new_topic, 'topic');
- if (kiwi.front.cur_channel.name === this.name) {
- kiwi.front.ui.setTopicText(new_topic);
- }
-};
-// Static functions
-/**
-* Checks to see if a tab by the given name exists
-* @param {String} name The name to check
-* @returns {Boolean} True if the tab exists, false otherwise
-*/
-Tabview.tabExists = function (name) {
- return (Tabview.getTab(name) !== null);
-};
-/**
-* Returns the tab which has the given name
-* @param {String} name The name of the tab to return
-* @returns {Tabview} The Tabview with the given name, or null if it does not exist
-*/
-Tabview.getTab = function (name) {
- var tab;
-
- // Make sure we actually have a name
- if (typeof name !== 'string') {
- return null;
- }
-
- // Go through each tabview and pick out the matching one
- $.each(kiwi.front.tabviews, function (i, item) {
- if (item.name.toLowerCase() === name.toLowerCase()) {
- tab = item;
- return false;
- }
- });
-
- // If we we didn't find one, return null instead
- tab = tab || null;
-
- return tab;
-};
-/**
-* Returns the tab that corresponds to the server
-* @retruns {Tabview} The server Tabview
-*/
-Tabview.getServerTab = function () {
- return Tabview.getTab('server');
-};
-/**
-* Returns all tabs
-* @returns {Array} All of the tabs
-*/
-Tabview.getAllTabs = function () {
- return kiwi.front.tabviews;
-};
-/**
-* Returns the tab that's currently showing
-* @returns {Tabview} The tab that's currently showing
-*/
-Tabview.getCurrentTab = function () {
- return kiwi.front.cur_channel;
-};
-
-
-
-
-
-
/**
* @constructor
* Floating message box