From: David Schoonover Date: Tue, 10 Jul 2012 13:19:39 +0000 (-0700) Subject: Adds compiled JS source files to be published with the npm distribution. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=6d66c7a2b381fa68a27ab5e9ae5de373cb7ea08a;p=limn.git Adds compiled JS source files to be published with the npm distribution. --- diff --git a/lib/app.js b/lib/app.js new file mode 100644 index 0000000..b4937f3 --- /dev/null +++ b/lib/app.js @@ -0,0 +1,58 @@ +var Backbone, op, AppView, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend({ + appSelector: '#content .inner' + /** + * @constructor + */, + constructor: (function(){ + function AppView(options){ + var that, _this = this; + options == null && (options = {}); + if (typeof options === 'function') { + this.initialize = options; + options = {}; + } else { + if (that = options.initialize) { + this.initialize = that; + } + } + if (that = options.appSelector) { + this.appSelector = that; + } + options.el || (options.el = jQuery(this.appSelector)[0]); + Backbone.View.call(this, options); + jQuery(function(){ + return _this.render(); + }); + return this; + } + return AppView; + }()) + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */, + initialize: function(){} + /** + * Append subviews. + */, + render: function(){ + var _ref; + if (this.view && !((_ref = this.view.$el.parent()) != null && _ref.length)) { + return this.$el.append(this.view.el); + } + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); \ No newline at end of file diff --git a/lib/app.mod.js b/lib/app.mod.js new file mode 100644 index 0000000..e816fc5 --- /dev/null +++ b/lib/app.mod.js @@ -0,0 +1,62 @@ +require.define('/node_modules/kraken/app.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, AppView, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Application view, automatically attaching to an existing element + * found at `appSelector`. + * @extends Backbone.View + */ +AppView = exports.AppView = Backbone.View.extend({ + appSelector: '#content .inner' + /** + * @constructor + */, + constructor: (function(){ + function AppView(options){ + var that, _this = this; + options == null && (options = {}); + if (typeof options === 'function') { + this.initialize = options; + options = {}; + } else { + if (that = options.initialize) { + this.initialize = that; + } + } + if (that = options.appSelector) { + this.appSelector = that; + } + options.el || (options.el = jQuery(this.appSelector)[0]); + Backbone.View.call(this, options); + jQuery(function(){ + return _this.render(); + }); + return this; + } + return AppView; + }()) + /** + * Override to set up your app. This method may be passed + * as an option to the constructor. + */, + initialize: function(){} + /** + * Append subviews. + */, + render: function(){ + var _ref; + if (this.view && !((_ref = this.view.$el.parent()) != null && _ref.length)) { + return this.$el.append(this.view.el); + } + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}); + +}); diff --git a/lib/base/base-mixin.js b/lib/base/base-mixin.js new file mode 100644 index 0000000..eba7b88 --- /dev/null +++ b/lib/base/base-mixin.js @@ -0,0 +1,216 @@ +var Backbone, op, BaseBackboneMixin, Mixin, mixinBase, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseBackboneMixin = exports.BaseBackboneMixin = { + initialize: function(){ + return this.__apply_bind__(); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */, + __bind__: [] + /** + * Applies the contents of `__bind__`. + */, + __apply_bind__: function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + } + /** + * Whether we're ready. + * @type Boolean + */, + ready: false + /** + * Triggers the 'ready' event if it has not yet been triggered. + * Subsequent listeners added on this event will be auto-triggered. + * @returns {this} + */, + triggerReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (this[lock]) { + return this; + } + this[lock] = true; + this.trigger(event, this); + return this; + } + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */, + resetReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (!this[lock]) { + return this; + } + this[lock] = false; + this.trigger(event + "-reset", this); + return this; + } + /** + * Wrap {@link Backbone.Event#on} registration to handle registrations + * on 'ready' after we've broadcast the event. Handler will always still + * be registered, however, in case the emitter is reset. + * + * @param {String} events Space-separated events for which to register. + * @param {Function} callback + * @param {Object} [context] + * @returns {this} + */, + on: function(events, callback, context){ + context == null && (context = this); + if (!callback) { + return this; + } + Backbone.Events.on.apply(this, arguments); + if (this.ready && _.contains(events.split(/\s+/), 'ready')) { + callback.call(context, this); + } + return this; + }, + makeHandlersForCallback: function(cb){ + var _this = this; + return { + success: function(){ + return cb.call(_this, [null].concat(arguments)); + }, + error: function(it){ + return cb.call(_this, it); + } + }; + } + /** + * Count of outstanding tasks. + * @type Number + */, + waitingOn: 0 + /** + * Increment the waiting task counter. + * @returns {this} + */, + wait: function(){ + var count; + count = this.waitingOn; + this.waitingOn += 1; + if (count === 0 && this.waitingOn > 0) { + this.trigger('start-waiting', this); + } + return this; + } + /** + * Decrement the waiting task counter. + * @returns {this} + */, + unwait: function(){ + var count; + count = this.waitingOn; + this.waitingOn -= 1; + if (this.waitingOn === 0 && count > 0) { + this.trigger('stop-waiting', this); + } + return this; + } + /** + * @param {Function} fn Function to wrap. + * @returns {Function} A function wrapping the passed function with a call + * to `unwait()`, then delegating with current context and arguments. + */, + unwaitAnd: function(fn){ + var self; + self = this; + return function(){ + self.unwait(); + return fn.apply(this, arguments); + }; + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}; +/** + * @class Base mixin class. Extend this to create a new mixin, attaching the + * donor methods as you would instance methods. + * + * To mingle your mixin with another class or object: + * + * class MyMixin extends Mixin + * foo: -> "foo!" + * + * # Mix into an object... + * o = MyMixin.mix { bar:1 } + * + * # Mix into a Coco class... + * class Bar + * MyMixin.mix this + * bar : 1 + * + */ +exports.Mixin = Mixin = (function(){ + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + Mixin.displayName = 'Mixin'; + var prototype = Mixin.prototype, constructor = Mixin; + Mixin.mix = function(target){ + var MixinClass; + if (!target) { + return that; + } + MixinClass = Mixin; + if (this instanceof Mixin) { + MixinClass = this.constructor; + } + if (this instanceof Function) { + MixinClass = this; + } + if (typeof target === 'function') { + __import(target.prototype, MixinClass.prototype); + } else { + target = __import(_.clone(MixinClass.prototype), target); + } + (target.__mixins__ || (target.__mixins__ = [])).push(MixinClass); + return target; + }; + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + Mixin.extended = function(SubClass){ + var SuperClass, k, v, _own = {}.hasOwnProperty; + SuperClass = this; + for (k in SuperClass) if (_own.call(SuperClass, k)) { + v = SuperClass[k]; + if (!SubClass[k]) { + SubClass[k] = v; + } + } + return SubClass; + }; + function Mixin(){} + return Mixin; +}()); +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = function(){ + var bodies; + bodies = __slice.call(arguments); + return _.extend.apply(_, [_.clone(BaseBackboneMixin)].concat(__slice.call(bodies))); +}; +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base-mixin.mod.js b/lib/base/base-mixin.mod.js new file mode 100644 index 0000000..e2c68e2 --- /dev/null +++ b/lib/base/base-mixin.mod.js @@ -0,0 +1,220 @@ +require.define('/node_modules/kraken/base/base-mixin.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, Mixin, mixinBase, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +BaseBackboneMixin = exports.BaseBackboneMixin = { + initialize: function(){ + return this.__apply_bind__(); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */, + __bind__: [] + /** + * Applies the contents of `__bind__`. + */, + __apply_bind__: function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + } + /** + * Whether we're ready. + * @type Boolean + */, + ready: false + /** + * Triggers the 'ready' event if it has not yet been triggered. + * Subsequent listeners added on this event will be auto-triggered. + * @returns {this} + */, + triggerReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (this[lock]) { + return this; + } + this[lock] = true; + this.trigger(event, this); + return this; + } + /** + * Resets the 'ready' event to its non-triggered state, firing a + * 'ready-reset' event. + * @returns {this} + */, + resetReady: function(lock, event){ + lock == null && (lock = 'ready'); + event == null && (event = 'ready'); + if (!this[lock]) { + return this; + } + this[lock] = false; + this.trigger(event + "-reset", this); + return this; + } + /** + * Wrap {@link Backbone.Event#on} registration to handle registrations + * on 'ready' after we've broadcast the event. Handler will always still + * be registered, however, in case the emitter is reset. + * + * @param {String} events Space-separated events for which to register. + * @param {Function} callback + * @param {Object} [context] + * @returns {this} + */, + on: function(events, callback, context){ + context == null && (context = this); + if (!callback) { + return this; + } + Backbone.Events.on.apply(this, arguments); + if (this.ready && _.contains(events.split(/\s+/), 'ready')) { + callback.call(context, this); + } + return this; + }, + makeHandlersForCallback: function(cb){ + var _this = this; + return { + success: function(){ + return cb.call(_this, [null].concat(arguments)); + }, + error: function(it){ + return cb.call(_this, it); + } + }; + } + /** + * Count of outstanding tasks. + * @type Number + */, + waitingOn: 0 + /** + * Increment the waiting task counter. + * @returns {this} + */, + wait: function(){ + var count; + count = this.waitingOn; + this.waitingOn += 1; + if (count === 0 && this.waitingOn > 0) { + this.trigger('start-waiting', this); + } + return this; + } + /** + * Decrement the waiting task counter. + * @returns {this} + */, + unwait: function(){ + var count; + count = this.waitingOn; + this.waitingOn -= 1; + if (this.waitingOn === 0 && count > 0) { + this.trigger('stop-waiting', this); + } + return this; + } + /** + * @param {Function} fn Function to wrap. + * @returns {Function} A function wrapping the passed function with a call + * to `unwait()`, then delegating with current context and arguments. + */, + unwaitAnd: function(fn){ + var self; + self = this; + return function(){ + self.unwait(); + return fn.apply(this, arguments); + }; + }, + getClassName: function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }, + toString: function(){ + return this.getClassName() + "()"; + } +}; +/** + * @class Base mixin class. Extend this to create a new mixin, attaching the + * donor methods as you would instance methods. + * + * To mingle your mixin with another class or object: + * + * class MyMixin extends Mixin + * foo: -> "foo!" + * + * # Mix into an object... + * o = MyMixin.mix { bar:1 } + * + * # Mix into a Coco class... + * class Bar + * MyMixin.mix this + * bar : 1 + * + */ +exports.Mixin = Mixin = (function(){ + /** + * Mixes this mixin into the target. If `target` is not a class, a new + * object will be returned which inherits from the mixin. + */ + Mixin.displayName = 'Mixin'; + var prototype = Mixin.prototype, constructor = Mixin; + Mixin.mix = function(target){ + var MixinClass; + if (!target) { + return that; + } + MixinClass = Mixin; + if (this instanceof Mixin) { + MixinClass = this.constructor; + } + if (this instanceof Function) { + MixinClass = this; + } + if (typeof target === 'function') { + __import(target.prototype, MixinClass.prototype); + } else { + target = __import(_.clone(MixinClass.prototype), target); + } + (target.__mixins__ || (target.__mixins__ = [])).push(MixinClass); + return target; + }; + /** + * Coco metaprogramming hook to propagate class properties and methods. + */ + Mixin.extended = function(SubClass){ + var SuperClass, k, v, _own = {}.hasOwnProperty; + SuperClass = this; + for (k in SuperClass) if (_own.call(SuperClass, k)) { + v = SuperClass[k]; + if (!SubClass[k]) { + SubClass[k] = v; + } + } + return SubClass; + }; + function Mixin(){} + return Mixin; +}()); +/** + * Mixes BaseBackboneMixin into another object or prototype. + * @returns {Object} The merged prototype object. + */ +mixinBase = exports.mixinBase = function(){ + var bodies; + bodies = __slice.call(arguments); + return _.extend.apply(_, [_.clone(BaseBackboneMixin)].concat(__slice.call(bodies))); +}; +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/base-model.js b/lib/base/base-model.js new file mode 100644 index 0000000..7a4a6d6 --- /dev/null +++ b/lib/base/base-model.js @@ -0,0 +1,254 @@ +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ + constructor: (function(){ + function BaseModel(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Model.apply(this, arguments); + } + return BaseModel; + }()), + url: function(){ + return this.urlRoot + "/" + this.get('id') + ".json"; + }, + has: function(key){ + return this.get(key) != null; + }, + get: function(key){ + return _.getNested(this.attributes, key); + } + /** + * Override to customize what data or assets the model requires, + * and how they should be loaded. + * + * By default, `load()` simply calls `loadModel()` via `loader()`. + * + * @see BaseModel#loader + * @see BaseModel#loadModel + * @returns {this} + */, + load: function(){ + console.log(this + ".load()"); + this.loader({ + start: this.loadModel, + completeEvent: 'fetch-success' + }); + return this; + } + /** + * Wraps the loading workflow boilerplate: + * - Squelches multiple loads from running at once + * - Squelches loads post-ready, unless forced + * - Triggers a start event + * - Triggers "ready" when complete + * - Wraps workflow with wait/unwait + * - Cleans up "loading" state + * + * @protected + * @param {Object} [opts={}] Options: + * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context. + * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load. + * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed successfully. + * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. + * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. + * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. + * @returns {this} + */, + loader: function(opts){ + var _this = this; + opts == null && (opts = {}); + opts = (__import({ + force: false, + readyIfError: false, + startEvent: 'load', + completeEvent: 'load-success', + errorEvent: 'load-error' + }, opts)); + if (opts.force) { + this.resetReady(); + } + if (!opts.start) { + throw new Error('You must specify a `start` function to start loading!'); + } + if (this.loading || this.ready) { + return this; + } + this.wait(); + this.loading = true; + this.trigger(opts.startEvent, this); + this.once(opts.completeEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.completeEvent !== 'load-success') { + _this.trigger('load-success', _this); + } + return _this.triggerReady(); + }); + this.once(opts.errorEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.errorEvent !== 'load-error') { + _this.trigger('load-error', _this); + } + if (opts.readyIfError) { + return _this.triggerReady(); + } + }); + opts.start.call(this); + return this; + } + /** + * Runs `.fetch()`, triggering a `fetch` event at start, and + * `fetch-success` / `fetch-error` on completion. + * + * @protected + * @returns {this} + */, + loadModel: function(){ + var _this = this; + this.wait(); + this.trigger('fetch', this); + this.fetch({ + success: function(){ + _this.unwait(); + return _this.trigger('fetch-success', _this); + }, + error: function(){ + _this.unwait(); + return _this.trigger.apply(_this, ['fetch-error', _this].concat(__slice.call(arguments))); + } + }); + return this; + }, + serialize: function(v){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + } + /** + * Like `.toJSON()` in that it should return a plain object with no functions, + * but for the purpose of `.toKV()`, allowing you to customize the values + * included and keys used. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepFunctions=false] If false, functions will be omitted from the result. + * @returns {Object} + */, + toKVPairs: function(opts){ + var kvo, k, v; + opts == null && (opts = {}); + opts = (__import({ + keepFunctions: false + }, opts)); + kvo = _.collapseObject(this.toJSON()); + for (k in kvo) { + v = kvo[k]; + if (opts.keepFunctions || typeof v !== 'function') { + kvo[k] = this.serialize(v); + } + } + return kvo; + } + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + } + /** + * @returns {String} URL identifying this model. + */, + toURL: function(){ + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "(cid=" + this.cid + ", id=" + this.id + ")"; + } +})); +__import(BaseModel, { + /** + * Factory method which constructs an instance of this model from a string of KV-pairs. + * This is a class method inherited by models which extend {BaseModel}. + * @static + * @param {String|Object} o Serialized KV-pairs (or a plain object). + * @returns {BaseModel} An instance of this model. + */ + fromKV: function(o, item_delim, kv_delim){ + var Cls; + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + if (typeof o === 'string') { + o = _.fromKV(o, item_delim, kv_delim); + } + Cls = typeof this === 'function' + ? this + : this.constructor; + return new Cls(_.uncollapseObject(o)); + } +}); +/** + * @class Base collection, extending Backbone.Collection, used by scaffold and others. + * @extends Backbone.Collection + */ +BaseList = exports.BaseList = Backbone.Collection.extend(mixinBase({ + constructor: (function(){ + function BaseList(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Collection.apply(this, arguments); + } + return BaseList; + }()), + getIds: function(){ + return this.models.map(function(it){ + return it.id || it.get('id') || it.cid; + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.toJSON()); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "[" + this.length + "]"; + }, + toStringWithIds: function(){ + var modelIds; + modelIds = this.models.map(function(it){ + var _ref; + return "\"" + ((_ref = it.id) != null + ? _ref + : it.cid) + "\""; + }).join(', '); + return this.getClassName() + "[" + this.length + "](" + modelIds + ")"; + } +})); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base-model.mod.js b/lib/base/base-model.mod.js new file mode 100644 index 0000000..1fdd53b --- /dev/null +++ b/lib/base/base-model.mod.js @@ -0,0 +1,258 @@ +require.define('/node_modules/kraken/base/base-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, BaseList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend(mixinBase({ + constructor: (function(){ + function BaseModel(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Model.apply(this, arguments); + } + return BaseModel; + }()), + url: function(){ + return this.urlRoot + "/" + this.get('id') + ".json"; + }, + has: function(key){ + return this.get(key) != null; + }, + get: function(key){ + return _.getNested(this.attributes, key); + } + /** + * Override to customize what data or assets the model requires, + * and how they should be loaded. + * + * By default, `load()` simply calls `loadModel()` via `loader()`. + * + * @see BaseModel#loader + * @see BaseModel#loadModel + * @returns {this} + */, + load: function(){ + console.log(this + ".load()"); + this.loader({ + start: this.loadModel, + completeEvent: 'fetch-success' + }); + return this; + } + /** + * Wraps the loading workflow boilerplate: + * - Squelches multiple loads from running at once + * - Squelches loads post-ready, unless forced + * - Triggers a start event + * - Triggers "ready" when complete + * - Wraps workflow with wait/unwait + * - Cleans up "loading" state + * + * @protected + * @param {Object} [opts={}] Options: + * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context. + * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load. + * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed successfully. + * @param {String} [opts.errorEvent='load-error'] Event which signals loading has completed but failed. + * @param {Boolean} [opts.force=false] If true, reset ready state if we're ready before proceeding. + * @param {Boolean} [opts.readyIfError=false] If true, move fire the ready event when loading completes, even if it failed. + * @returns {this} + */, + loader: function(opts){ + var _this = this; + opts == null && (opts = {}); + opts = (__import({ + force: false, + readyIfError: false, + startEvent: 'load', + completeEvent: 'load-success', + errorEvent: 'load-error' + }, opts)); + if (opts.force) { + this.resetReady(); + } + if (!opts.start) { + throw new Error('You must specify a `start` function to start loading!'); + } + if (this.loading || this.ready) { + return this; + } + this.wait(); + this.loading = true; + this.trigger(opts.startEvent, this); + this.once(opts.completeEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.completeEvent !== 'load-success') { + _this.trigger('load-success', _this); + } + return _this.triggerReady(); + }); + this.once(opts.errorEvent, function(){ + _this.loading = false; + _this.unwait(); + if (opts.errorEvent !== 'load-error') { + _this.trigger('load-error', _this); + } + if (opts.readyIfError) { + return _this.triggerReady(); + } + }); + opts.start.call(this); + return this; + } + /** + * Runs `.fetch()`, triggering a `fetch` event at start, and + * `fetch-success` / `fetch-error` on completion. + * + * @protected + * @returns {this} + */, + loadModel: function(){ + var _this = this; + this.wait(); + this.trigger('fetch', this); + this.fetch({ + success: function(){ + _this.unwait(); + return _this.trigger('fetch-success', _this); + }, + error: function(){ + _this.unwait(); + return _this.trigger.apply(_this, ['fetch-error', _this].concat(__slice.call(arguments))); + } + }); + return this; + }, + serialize: function(v){ + if (_.isBoolean(v)) { + v = Number(v); + } else if (_.isObject(v)) { + v = JSON.stringify(v); + } + return String(v); + } + /** + * Like `.toJSON()` in that it should return a plain object with no functions, + * but for the purpose of `.toKV()`, allowing you to customize the values + * included and keys used. + * + * @param {Object} [opts={}] Options: + * @param {Boolean} [opts.keepFunctions=false] If false, functions will be omitted from the result. + * @returns {Object} + */, + toKVPairs: function(opts){ + var kvo, k, v; + opts == null && (opts = {}); + opts = (__import({ + keepFunctions: false + }, opts)); + kvo = _.collapseObject(this.toJSON()); + for (k in kvo) { + v = kvo[k]; + if (opts.keepFunctions || typeof v !== 'function') { + kvo[k] = this.serialize(v); + } + } + return kvo; + } + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + } + /** + * @returns {String} URL identifying this model. + */, + toURL: function(){ + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "(cid=" + this.cid + ", id=" + this.id + ")"; + } +})); +__import(BaseModel, { + /** + * Factory method which constructs an instance of this model from a string of KV-pairs. + * This is a class method inherited by models which extend {BaseModel}. + * @static + * @param {String|Object} o Serialized KV-pairs (or a plain object). + * @returns {BaseModel} An instance of this model. + */ + fromKV: function(o, item_delim, kv_delim){ + var Cls; + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + if (typeof o === 'string') { + o = _.fromKV(o, item_delim, kv_delim); + } + Cls = typeof this === 'function' + ? this + : this.constructor; + return new Cls(_.uncollapseObject(o)); + } +}); +/** + * @class Base collection, extending Backbone.Collection, used by scaffold and others. + * @extends Backbone.Collection + */ +BaseList = exports.BaseList = Backbone.Collection.extend(mixinBase({ + constructor: (function(){ + function BaseList(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + return Backbone.Collection.apply(this, arguments); + } + return BaseList; + }()), + getIds: function(){ + return this.models.map(function(it){ + return it.id || it.get('id') || it.cid; + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.toJSON()); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + }, + toString: function(){ + return this.getClassName() + "[" + this.length + "]"; + }, + toStringWithIds: function(){ + var modelIds; + modelIds = this.models.map(function(it){ + var _ref; + return "\"" + ((_ref = it.id) != null + ? _ref + : it.cid) + "\""; + }).join(', '); + return this.getClassName() + "[" + this.length + "](" + modelIds + ")"; + } +})); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/base-view.js b/lib/base/base-view.js new file mode 100644 index 0000000..9dec625 --- /dev/null +++ b/lib/base/base-view.js @@ -0,0 +1,330 @@ +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, DataBinding, BaseView, ViewList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +BaseModel = require('kraken/base/base-mixin').BaseModel; +DataBinding = require('kraken/base/data-binding').DataBinding; +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend(mixinBase({ + tagName: 'section', + model: BaseModel + /** + * Method-name called by `onReturnKeypress` when used as an event-handler. + * @type String + */, + callOnReturnKeypress: null + /** + * Parent view of this view. + * @type BaseView + */, + parent: null + /** + * Array of [view, selector]-pairs. + * @type Array<[BaseView, String]> + */, + subviews: [] + /** + * Whether this view has been added to the DOM. + * @type Boolean + */, + isAttached: false, + constructor: (function(){ + function BaseView(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + this.subviews = new ViewList; + this.onReturnKeypress = _.debounce(this.onReturnKeypress.bind(this), 50); + Backbone.View.apply(this, arguments); + return this.trigger('create', this); + } + return BaseView; + }()), + initialize: function(){ + this.__apply_bind__(); + this.setModel(this.model); + this.build(); + return this.$el.on('form submit', function(it){ + return it.preventDefault(); + }); + }, + setModel: function(model){ + var data; + if (this.model) { + this.model.off('change', this.render, this); + this.model.off('destroy', this.remove, this); + delete this.model.view; + data = this.$el.data(); + delete data.model; + delete data.view; + } + if (this.model = model) { + this.model.view = this; + this.$el.data({ + model: this.model, + view: this + }); + this.model.on('change', this.render, this); + this.model.on('destroy', this.remove, this); + this.trigger('change:model', this, model); + } + return model; + }, + setParent: function(parent){ + var old_parent, _ref; + _ref = [this.parent, parent], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('parent', this, parent, old_parent); + return this; + }, + unsetParent: function(){ + var old_parent, _ref; + _ref = [this.parent, null], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('unparent', this, old_parent); + return this; + }, + addSubview: function(view){ + this.removeSubview(view); + this.subviews.push(view); + view.setParent(this); + return view; + }, + removeSubview: function(view){ + if (this.hasSubview(view)) { + view.remove(); + this.subviews.remove(view); + view.unsetParent(); + } + return view; + }, + hasSubview: function(view){ + return this.subviews.contains(view); + }, + invokeSubviews: function(){ + var _ref; + return (_ref = this.subviews).invoke.apply(_ref, arguments); + }, + removeAllSubviews: function(){ + this.subviews.forEach(this.removeSubview, this); + return this; + }, + attach: function(el){ + var _this = this; + this.$el.appendTo(el); + if (this.isAttached) { + return this; + } + this.isAttached = true; + _.delay(function(){ + _this.delegateEvents(); + return _this.trigger('attach', _this); + }, 50); + return this; + }, + remove: function(){ + this.$el.remove(); + if (!this.isAttached) { + return this; + } + this.isAttached = false; + this.trigger('unattach', this); + return this; + }, + clear: function(){ + this.remove(); + this.model.destroy(); + this.trigger('clear', this); + return this; + }, + hide: function(){ + this.$el.hide(); + this.trigger('hide', this); + return this; + }, + show: function(){ + this.$el.show(); + this.trigger('show', this); + return this; + } + /** + * Attach each subview to its bind-point. + * @returns {this} + */, + attachSubviews: function(){ + var bps, view, bp, _i, _ref, _len; + bps = this.getOwnSubviewBindPoints(); + if (this.subviews.length && !bps.length) { + console.warn(this + ".attachSubviews(): no subview bind-points found!"); + return this; + } + for (_i = 0, _len = (_ref = this.subviews).length; _i < _len; ++_i) { + view = _ref[_i]; + if (bp = this.findSubviewBindPoint(view, bps)) { + view.attach(bp); + } else { + console.warn(this + ".attachSubviews(): Unable to find bind-point for " + view + "!"); + } + } + return this; + } + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */, + getOwnSubviewBindPoints: function(){ + return this.$('[data-subview]').not(this.$('[data-subview] [data-subview]')); + } + /** + * Find the matching subview bind-point for the given view. + */, + findSubviewBindPoint: function(view, bind_points){ + var bp; + bind_points || (bind_points = this.getOwnSubviewBindPoints()); + if (view.id) { + bp = bind_points.filter("[data-subview$=':" + view.id + "']"); + if (bp.length) { + return bp.eq(0); + } + } + bp = bind_points.filter("[data-subview='" + view.getClassName() + "']"); + if (bp.length) { + return bp.eq(0); + } + }, + toTemplateLocals: function(){ + return this.model.toJSON(); + }, + $template: function(){ + return $(this.template((__import({ + _: _, + op: op, + model: this.model, + view: this + }, this.toTemplateLocals())))); + }, + build: function(){ + var outer; + if (!this.template) { + return this; + } + outer = this.$template(); + this.$el.html(outer.html()).attr({ + id: outer.attr('id'), + 'class': outer.attr('class') + }); + this.attachSubviews(); + this.isBuilt = true; + return this; + }, + render: function(){ + this.wait(); + if (this.isBuilt) { + this.update(); + } else { + this.build(); + } + this.renderSubviews(); + this.trigger('render', this); + this.unwait(); + return this; + }, + renderSubviews: function(){ + this.attachSubviews(); + this.subviews.invoke('render'); + return this; + }, + update: function(){ + var locals; + new DataBinding(this).update(locals = this.toTemplateLocals()); + this.trigger('update', this, locals); + return this; + } + /* * * * Events * * * */, + bubbleEventDown: function(evt){ + this.invokeSubviews.apply(this, ['trigger'].concat(__slice.call(arguments))); + return this; + }, + redispatch: function(evt){ + var args; + args = __slice.call(arguments, 1); + this.trigger.apply(this, [evt, this].concat(__slice.call(args))); + return this; + }, + onlyOnReturn: function(fn){ + var args, _this = this; + args = __slice.call(arguments, 1); + fn = _.debounce(fn.bind(this), 50); + return function(evt){ + if (evt.keyCode === 13) { + return fn.apply(_this, args); + } + }; + } + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */, + onReturnKeypress: function(evt){ + var fn; + if (this.callOnReturnKeypress) { + fn = this[this.callOnReturnKeypress]; + } + if (fn && evt.keyCode === 13) { + return fn.call(this); + } + }, + toString: function(){ + return this.getClassName() + "(model=" + this.model + ")"; + } +})); +['get', 'set', 'unset', 'toJSON', 'toKV', 'toURL'].forEach(function(methodname){ + return BaseView.prototype[methodname] = function(){ + return this.model[methodname].apply(this.model, arguments); + }; +}); +exports.ViewList = ViewList = (function(superclass){ + ViewList.displayName = 'ViewList'; + var prototype = __extend(ViewList, superclass).prototype, constructor = ViewList; + function ViewList(views){ + views == null && (views = []); + superclass.apply(this, arguments); + } + prototype.extend = function(views){ + var _this = this; + _.each(views, function(it){ + return _this.push(it); + }); + return this; + }; + prototype.findByModel = function(model){ + return this.find(function(it){ + return it.model === model; + }); + }; + prototype.toString = function(){ + var contents; + contents = this.length ? "\"" + this.join('","') + "\"" : ''; + return "ViewList[" + this.length + "](" + contents + ")"; + }; + return ViewList; +}(Array)); +['each', 'contains', 'invoke', 'pluck', 'find', 'remove', 'compact', 'flatten', 'without', 'union', 'intersection', 'difference', 'unique', 'uniq'].forEach(function(methodname){ + return ViewList.prototype[methodname] = function(){ + var _ref; + return (_ref = _[methodname]).call.apply(_ref, [_, this].concat(__slice.call(arguments))); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} \ No newline at end of file diff --git a/lib/base/base-view.mod.js b/lib/base/base-view.mod.js new file mode 100644 index 0000000..2105eea --- /dev/null +++ b/lib/base/base-view.mod.js @@ -0,0 +1,334 @@ +require.define('/node_modules/kraken/base/base-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, BaseBackboneMixin, mixinBase, BaseModel, DataBinding, BaseView, ViewList, _ref, _, __slice = [].slice; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-mixin'), BaseBackboneMixin = _ref.BaseBackboneMixin, mixinBase = _ref.mixinBase; +BaseModel = require('kraken/base/base-mixin').BaseModel; +DataBinding = require('kraken/base/data-binding').DataBinding; +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend(mixinBase({ + tagName: 'section', + model: BaseModel + /** + * Method-name called by `onReturnKeypress` when used as an event-handler. + * @type String + */, + callOnReturnKeypress: null + /** + * Parent view of this view. + * @type BaseView + */, + parent: null + /** + * Array of [view, selector]-pairs. + * @type Array<[BaseView, String]> + */, + subviews: [] + /** + * Whether this view has been added to the DOM. + * @type Boolean + */, + isAttached: false, + constructor: (function(){ + function BaseView(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.__super__.constructor; + this.waitingOn = 0; + this.subviews = new ViewList; + this.onReturnKeypress = _.debounce(this.onReturnKeypress.bind(this), 50); + Backbone.View.apply(this, arguments); + return this.trigger('create', this); + } + return BaseView; + }()), + initialize: function(){ + this.__apply_bind__(); + this.setModel(this.model); + this.build(); + return this.$el.on('form submit', function(it){ + return it.preventDefault(); + }); + }, + setModel: function(model){ + var data; + if (this.model) { + this.model.off('change', this.render, this); + this.model.off('destroy', this.remove, this); + delete this.model.view; + data = this.$el.data(); + delete data.model; + delete data.view; + } + if (this.model = model) { + this.model.view = this; + this.$el.data({ + model: this.model, + view: this + }); + this.model.on('change', this.render, this); + this.model.on('destroy', this.remove, this); + this.trigger('change:model', this, model); + } + return model; + }, + setParent: function(parent){ + var old_parent, _ref; + _ref = [this.parent, parent], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('parent', this, parent, old_parent); + return this; + }, + unsetParent: function(){ + var old_parent, _ref; + _ref = [this.parent, null], old_parent = _ref[0], this.parent = _ref[1]; + this.trigger('unparent', this, old_parent); + return this; + }, + addSubview: function(view){ + this.removeSubview(view); + this.subviews.push(view); + view.setParent(this); + return view; + }, + removeSubview: function(view){ + if (this.hasSubview(view)) { + view.remove(); + this.subviews.remove(view); + view.unsetParent(); + } + return view; + }, + hasSubview: function(view){ + return this.subviews.contains(view); + }, + invokeSubviews: function(){ + var _ref; + return (_ref = this.subviews).invoke.apply(_ref, arguments); + }, + removeAllSubviews: function(){ + this.subviews.forEach(this.removeSubview, this); + return this; + }, + attach: function(el){ + var _this = this; + this.$el.appendTo(el); + if (this.isAttached) { + return this; + } + this.isAttached = true; + _.delay(function(){ + _this.delegateEvents(); + return _this.trigger('attach', _this); + }, 50); + return this; + }, + remove: function(){ + this.$el.remove(); + if (!this.isAttached) { + return this; + } + this.isAttached = false; + this.trigger('unattach', this); + return this; + }, + clear: function(){ + this.remove(); + this.model.destroy(); + this.trigger('clear', this); + return this; + }, + hide: function(){ + this.$el.hide(); + this.trigger('hide', this); + return this; + }, + show: function(){ + this.$el.show(); + this.trigger('show', this); + return this; + } + /** + * Attach each subview to its bind-point. + * @returns {this} + */, + attachSubviews: function(){ + var bps, view, bp, _i, _ref, _len; + bps = this.getOwnSubviewBindPoints(); + if (this.subviews.length && !bps.length) { + console.warn(this + ".attachSubviews(): no subview bind-points found!"); + return this; + } + for (_i = 0, _len = (_ref = this.subviews).length; _i < _len; ++_i) { + view = _ref[_i]; + if (bp = this.findSubviewBindPoint(view, bps)) { + view.attach(bp); + } else { + console.warn(this + ".attachSubviews(): Unable to find bind-point for " + view + "!"); + } + } + return this; + } + /** + * Finds all subview bind-points under this view's element, but not under + * the view element of any subview. + * @returns {jQuery|undefined} + */, + getOwnSubviewBindPoints: function(){ + return this.$('[data-subview]').not(this.$('[data-subview] [data-subview]')); + } + /** + * Find the matching subview bind-point for the given view. + */, + findSubviewBindPoint: function(view, bind_points){ + var bp; + bind_points || (bind_points = this.getOwnSubviewBindPoints()); + if (view.id) { + bp = bind_points.filter("[data-subview$=':" + view.id + "']"); + if (bp.length) { + return bp.eq(0); + } + } + bp = bind_points.filter("[data-subview='" + view.getClassName() + "']"); + if (bp.length) { + return bp.eq(0); + } + }, + toTemplateLocals: function(){ + return this.model.toJSON(); + }, + $template: function(){ + return $(this.template((__import({ + _: _, + op: op, + model: this.model, + view: this + }, this.toTemplateLocals())))); + }, + build: function(){ + var outer; + if (!this.template) { + return this; + } + outer = this.$template(); + this.$el.html(outer.html()).attr({ + id: outer.attr('id'), + 'class': outer.attr('class') + }); + this.attachSubviews(); + this.isBuilt = true; + return this; + }, + render: function(){ + this.wait(); + if (this.isBuilt) { + this.update(); + } else { + this.build(); + } + this.renderSubviews(); + this.trigger('render', this); + this.unwait(); + return this; + }, + renderSubviews: function(){ + this.attachSubviews(); + this.subviews.invoke('render'); + return this; + }, + update: function(){ + var locals; + new DataBinding(this).update(locals = this.toTemplateLocals()); + this.trigger('update', this, locals); + return this; + } + /* * * * Events * * * */, + bubbleEventDown: function(evt){ + this.invokeSubviews.apply(this, ['trigger'].concat(__slice.call(arguments))); + return this; + }, + redispatch: function(evt){ + var args; + args = __slice.call(arguments, 1); + this.trigger.apply(this, [evt, this].concat(__slice.call(args))); + return this; + }, + onlyOnReturn: function(fn){ + var args, _this = this; + args = __slice.call(arguments, 1); + fn = _.debounce(fn.bind(this), 50); + return function(evt){ + if (evt.keyCode === 13) { + return fn.apply(_this, args); + } + }; + } + /** + * Call a delegate on keypress == the return key. + * @returns {Function} Keypress event handler. + */, + onReturnKeypress: function(evt){ + var fn; + if (this.callOnReturnKeypress) { + fn = this[this.callOnReturnKeypress]; + } + if (fn && evt.keyCode === 13) { + return fn.call(this); + } + }, + toString: function(){ + return this.getClassName() + "(model=" + this.model + ")"; + } +})); +['get', 'set', 'unset', 'toJSON', 'toKV', 'toURL'].forEach(function(methodname){ + return BaseView.prototype[methodname] = function(){ + return this.model[methodname].apply(this.model, arguments); + }; +}); +exports.ViewList = ViewList = (function(superclass){ + ViewList.displayName = 'ViewList'; + var prototype = __extend(ViewList, superclass).prototype, constructor = ViewList; + function ViewList(views){ + views == null && (views = []); + superclass.apply(this, arguments); + } + prototype.extend = function(views){ + var _this = this; + _.each(views, function(it){ + return _this.push(it); + }); + return this; + }; + prototype.findByModel = function(model){ + return this.find(function(it){ + return it.model === model; + }); + }; + prototype.toString = function(){ + var contents; + contents = this.length ? "\"" + this.join('","') + "\"" : ''; + return "ViewList[" + this.length + "](" + contents + ")"; + }; + return ViewList; +}(Array)); +['each', 'contains', 'invoke', 'pluck', 'find', 'remove', 'compact', 'flatten', 'without', 'union', 'intersection', 'difference', 'unique', 'uniq'].forEach(function(methodname){ + return ViewList.prototype[methodname] = function(){ + var _ref; + return (_ref = _[methodname]).call.apply(_ref, [_, this].concat(__slice.call(arguments))); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} + +}); diff --git a/lib/base/base.js b/lib/base/base.js new file mode 100644 index 0000000..e3f4bce --- /dev/null +++ b/lib/base/base.js @@ -0,0 +1,77 @@ +var EventEmitter, op, Base, k, _ref, _, _i, _len, __slice = [].slice; +EventEmitter = require('events').EventEmitter; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.trigger = EventEmitter.prototype.emit; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Eventful base class. + * @extends EventEmitter + */ +Base = (function(superclass){ + /** + * After the super chain has exhausted (but not necessarily at the end + * of init -- it depends on when you super()), Base will publish a 'new' + * event on the instance's class, allowing anyone to subscribe to + * notifications about new objects. + * @constructor + */ + Base.displayName = 'Base'; + var prototype = __extend(Base, superclass).prototype, constructor = Base; + function Base(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.superclass; + this.__apply_bind__(); + superclass.call(this); + this.__class__.emit('new', this); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + prototype.__bind__ = []; + /** + * Applies the contents of `__bind__`. + */ + prototype.__apply_bind__ = function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + }; + prototype.getClassName = function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }; + prototype.toString = function(){ + return this.getClassName() + "()"; + }; + Base.extended = function(Subclass){ + var k, v, _own = {}.hasOwnProperty; + for (k in this) if (_own.call(this, k)) { + v = this[k]; + if (typeof v === 'function') { + Subclass[k] = v; + } + } + Subclass.__super__ = this.prototype; + return Subclass; + }; + return Base; +}(EventEmitter)); +for (_i = 0, _len = (_ref = ['getSuperClasses', 'pluckSuper', 'pluckSuperAndSelf']).length; _i < _len; ++_i) { + k = _ref[_i]; + Base[k] = Base.prototype[k] = _.methodize(_[k]); +} +__import(Base, EventEmitter.prototype); +module.exports = Base; +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/base.mod.js b/lib/base/base.mod.js new file mode 100644 index 0000000..68ac0c5 --- /dev/null +++ b/lib/base/base.mod.js @@ -0,0 +1,81 @@ +require.define('/node_modules/kraken/base/base.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var EventEmitter, op, Base, k, _ref, _, _i, _len, __slice = [].slice; +EventEmitter = require('events').EventEmitter; +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.trigger = EventEmitter.prototype.emit; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +/** + * @class Eventful base class. + * @extends EventEmitter + */ +Base = (function(superclass){ + /** + * After the super chain has exhausted (but not necessarily at the end + * of init -- it depends on when you super()), Base will publish a 'new' + * event on the instance's class, allowing anyone to subscribe to + * notifications about new objects. + * @constructor + */ + Base.displayName = 'Base'; + var prototype = __extend(Base, superclass).prototype, constructor = Base; + function Base(){ + this.__class__ = this.constructor; + this.__superclass__ = this.constructor.superclass; + this.__apply_bind__(); + superclass.call(this); + this.__class__.emit('new', this); + } + /** + * A list of method-names to bind on `initialize`; set this on a subclass to override. + * @type Array + */ + prototype.__bind__ = []; + /** + * Applies the contents of `__bind__`. + */ + prototype.__apply_bind__ = function(){ + var names; + names = _(this.pluckSuperAndSelf('__bind__')).chain().flatten().compact().unique().value(); + if (names.length) { + return _.bindAll.apply(_, [this].concat(__slice.call(names))); + } + }; + prototype.getClassName = function(){ + return (this.constructor.name || this.constructor.displayName) + ""; + }; + prototype.toString = function(){ + return this.getClassName() + "()"; + }; + Base.extended = function(Subclass){ + var k, v, _own = {}.hasOwnProperty; + for (k in this) if (_own.call(this, k)) { + v = this[k]; + if (typeof v === 'function') { + Subclass[k] = v; + } + } + Subclass.__super__ = this.prototype; + return Subclass; + }; + return Base; +}(EventEmitter)); +for (_i = 0, _len = (_ref = ['getSuperClasses', 'pluckSuper', 'pluckSuperAndSelf']).length; _i < _len; ++_i) { + k = _ref[_i]; + Base[k] = Base.prototype[k] = _.methodize(_[k]); +} +__import(Base, EventEmitter.prototype); +module.exports = Base; +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/cascading-model.js b/lib/base/cascading-model.js new file mode 100644 index 0000000..8a1f89c --- /dev/null +++ b/lib/base/cascading-model.js @@ -0,0 +1,54 @@ +var op, BaseModel, BaseList, Cascade, CascadingModel, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-model'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Cascade = require('kraken/util/cascade'); +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend({ + /** + * The lookup cascade. + * @type Cascade + */ + cascade: null, + constructor: (function(){ + function CascadingModel(attributes, opts){ + attributes == null && (attributes = {}); + this.cascade = new Cascade(attributes); + return BaseModel.call(this, attributes, opts); + } + return CascadingModel; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + } + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */, + get: function(key){ + return this.cascade.get(key); + }, + toJSON: function(opts){ + opts == null && (opts = {}); + opts = (__import({ + collapseCascade: false + }, opts)); + if (opts.collapseCascade) { + return this.cascade.collapse(); + } else { + return BaseModel.prototype.toJSON.apply(this, arguments); + } + } +}); +['addLookup', 'removeLookup', 'popLookup', 'shiftLookup', 'unshiftLookup', 'isOwnProperty', 'isOwnValue', 'isInheritedValue', 'isModifiedValue'].forEach(function(methodname){ + return CascadingModel.prototype[methodname] = function(){ + return this.cascade[methodname].apply(this.cascade, arguments); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/cascading-model.mod.js b/lib/base/cascading-model.mod.js new file mode 100644 index 0000000..5fc62d5 --- /dev/null +++ b/lib/base/cascading-model.mod.js @@ -0,0 +1,58 @@ +require.define('/node_modules/kraken/base/cascading-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, BaseList, Cascade, CascadingModel, _ref, _; +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +_ref = require('kraken/base/base-model'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Cascade = require('kraken/util/cascade'); +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend({ + /** + * The lookup cascade. + * @type Cascade + */ + cascade: null, + constructor: (function(){ + function CascadingModel(attributes, opts){ + attributes == null && (attributes = {}); + this.cascade = new Cascade(attributes); + return BaseModel.call(this, attributes, opts); + } + return CascadingModel; + }()), + initialize: function(){ + return BaseModel.prototype.initialize.apply(this, arguments); + } + /** + * Recursively look up a (potenitally nested) attribute in the lookup chain. + * @param {String} key Attribute key (potenitally nested using dot-delimited subkeys). + * @returns {*} + */, + get: function(key){ + return this.cascade.get(key); + }, + toJSON: function(opts){ + opts == null && (opts = {}); + opts = (__import({ + collapseCascade: false + }, opts)); + if (opts.collapseCascade) { + return this.cascade.collapse(); + } else { + return BaseModel.prototype.toJSON.apply(this, arguments); + } + } +}); +['addLookup', 'removeLookup', 'popLookup', 'shiftLookup', 'unshiftLookup', 'isOwnProperty', 'isOwnValue', 'isInheritedValue', 'isModifiedValue'].forEach(function(methodname){ + return CascadingModel.prototype[methodname] = function(){ + return this.cascade[methodname].apply(this.cascade, arguments); + }; +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/data-binding.js b/lib/base/data-binding.js new file mode 100644 index 0000000..9f1835c --- /dev/null +++ b/lib/base/data-binding.js @@ -0,0 +1,65 @@ +var Backbone, op, DataBinding, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +exports.DataBinding = DataBinding = (function(){ + DataBinding.displayName = 'DataBinding'; + var prototype = DataBinding.prototype, constructor = DataBinding; + prototype.data = null; + prototype.context = null; + prototype.el = null; + prototype.$el = null; + prototype.bindPoints = null; + function DataBinding(el, context){ + this.context = context != null ? context : el; + if (el instanceof Backbone.View) { + el = el.$el; + } + this.$el = $(el); + this.el = this.$el.get(0); + this.bindPoints = this.$('[data-bind], [name]').not(this.$('[data-subview]').find('[data-bind], [name]')); + } + prototype.$ = function(sel){ + return this.$el.find(sel); + }; + prototype.serialize = function(it){ + return it; + }; + prototype.update = function(data){ + var key, val, _ref; + this.data = data; + for (key in _ref = _.collapseObject(this.data)) { + val = _ref[key]; + this.updateBinding(key, val); + } + return this; + }; + prototype.updateBinding = function(key, val){ + var bp; + if (bp = this.findDataBindPoint(key)) { + if (_.isFunction(val)) { + val.call(this.context, val, key, bp, this.data); + } else if (bp.is('input:checkbox')) { + bp.attr('checked', !!val); + } else if (bp.is('input, textarea')) { + bp.val(this.serialize(val)); + } else { + if (op.toBool(bp.data('data-bind-escape'))) { + bp.text(this.serialize(val)); + } else { + bp.html(this.serialize(val)); + } + } + } else { + false && console.warn(this + ".updateBinding(): Unable to find data bind-point for " + key + "=" + val + "!"); + } + return this; + }; + prototype.findDataBindPoint = function(key){ + var bp; + bp = this.bindPoints.filter("[name='" + key + "'], [data-bind='" + key + "']"); + if (bp.length) { + return bp.eq(0); + } + }; + return DataBinding; +}()); \ No newline at end of file diff --git a/lib/base/data-binding.mod.js b/lib/base/data-binding.mod.js new file mode 100644 index 0000000..957bc2c --- /dev/null +++ b/lib/base/data-binding.mod.js @@ -0,0 +1,69 @@ +require.define('/node_modules/kraken/base/data-binding.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Backbone, op, DataBinding, _ref, _; +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +exports.DataBinding = DataBinding = (function(){ + DataBinding.displayName = 'DataBinding'; + var prototype = DataBinding.prototype, constructor = DataBinding; + prototype.data = null; + prototype.context = null; + prototype.el = null; + prototype.$el = null; + prototype.bindPoints = null; + function DataBinding(el, context){ + this.context = context != null ? context : el; + if (el instanceof Backbone.View) { + el = el.$el; + } + this.$el = $(el); + this.el = this.$el.get(0); + this.bindPoints = this.$('[data-bind], [name]').not(this.$('[data-subview]').find('[data-bind], [name]')); + } + prototype.$ = function(sel){ + return this.$el.find(sel); + }; + prototype.serialize = function(it){ + return it; + }; + prototype.update = function(data){ + var key, val, _ref; + this.data = data; + for (key in _ref = _.collapseObject(this.data)) { + val = _ref[key]; + this.updateBinding(key, val); + } + return this; + }; + prototype.updateBinding = function(key, val){ + var bp; + if (bp = this.findDataBindPoint(key)) { + if (_.isFunction(val)) { + val.call(this.context, val, key, bp, this.data); + } else if (bp.is('input:checkbox')) { + bp.attr('checked', !!val); + } else if (bp.is('input, textarea')) { + bp.val(this.serialize(val)); + } else { + if (op.toBool(bp.data('data-bind-escape'))) { + bp.text(this.serialize(val)); + } else { + bp.html(this.serialize(val)); + } + } + } else { + false && console.warn(this + ".updateBinding(): Unable to find data bind-point for " + key + "=" + val + "!"); + } + return this; + }; + prototype.findDataBindPoint = function(key){ + var bp; + bp = this.bindPoints.filter("[name='" + key + "'], [data-bind='" + key + "']"); + if (bp.length) { + return bp.eq(0); + } + }; + return DataBinding; +}()); + +}); diff --git a/lib/base/index.js b/lib/base/index.js new file mode 100644 index 0000000..1c9458c --- /dev/null +++ b/lib/base/index.js @@ -0,0 +1,14 @@ +var mixins, models, views, cache, cascading, data_binding; +exports.Base = require('kraken/base/base'); +mixins = require('kraken/base/base-mixin'); +models = require('kraken/base/base-model'); +views = require('kraken/base/base-view'); +cache = require('kraken/base/model-cache'); +cascading = require('kraken/base/cascading-model'); +data_binding = require('kraken/base/data-binding'); +__import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/index.mod.js b/lib/base/index.mod.js new file mode 100644 index 0000000..a599753 --- /dev/null +++ b/lib/base/index.mod.js @@ -0,0 +1,18 @@ +require.define('/node_modules/kraken/base/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var mixins, models, views, cache, cascading, data_binding; +exports.Base = require('kraken/base/base'); +mixins = require('kraken/base/base-mixin'); +models = require('kraken/base/base-model'); +views = require('kraken/base/base-view'); +cache = require('kraken/base/model-cache'); +cascading = require('kraken/base/cascading-model'); +data_binding = require('kraken/base/data-binding'); +__import(__import(__import(__import(__import(__import(exports, mixins), models), views), cache), cascading), data_binding); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/model-cache.js b/lib/base/model-cache.js new file mode 100644 index 0000000..f07c776 --- /dev/null +++ b/lib/base/model-cache.js @@ -0,0 +1,218 @@ +var Seq, ReadyEmitter, ModelCache, _; +_ = require('underscore'); +Seq = require('seq'); +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +/** + * @class Caches models and provides static lookups by ID. + */ +exports.ModelCache = ModelCache = (function(superclass){ + ModelCache.displayName = 'ModelCache'; + var prototype = __extend(ModelCache, superclass).prototype, constructor = ModelCache; + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + prototype.readyEventName = 'cache-ready'; + /** + * Default options. + * @private + * @constant + * @type Object + */ + prototype.DEFAULT_OPTIONS = { + ready: true, + cache: null, + create: null, + ModelType: null + }; + /** + * @private + * @type Object + */ + prototype.options = null; + /** + * Type we're caching (presumably extending `Backbone.Model`), used to create new + * instances unless a `create` function was provided in options. + * @private + * @type Class + */ + prototype.ModelType = null; + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + prototype.cache = null; + /** + * @constructor + * @param {Class} [ModelType] Type of cached object (presumably extending + * `Backbone.Model`), used to create new instances unless `options.create` + * is provided. + * @param {Object} [options] Options: + * @param {Boolean} [options.ready=true] Starting `ready` state. If false, + * the cache will queue lookup calls until `triggerReady()` is called. + * @param {Class} [options.cache=new Backbone.Collection] + * The backing data-structure for the cache. If omitted, we'll use a new + * `Backbone.Collection`, but really, anything with a `get(id)` method for + * model lookup will work here. + * @param {Function} [options.create] A function called when a new Model + * object is needed, being passed the new model ID. + * @param {Class} [options.ModelType] Type of cached object + * (presumably extending `Backbone.Model`), used to create new instances + * unless `options.create` is provided. + */; + function ModelCache(ModelType, options){ + var that, _ref; + if (!_.isFunction(ModelType)) { + _ref = [ModelType || {}, null], options = _ref[0], ModelType = _ref[1]; + } + this.options = (_ref = {}, __import(_ref, this.DEFAULT_OPTIONS), __import(_ref, options)); + this.cache = this.options.cache || new Backbone.Collection; + this.ModelType = ModelType || this.options.ModelType; + if (that = this.options.create) { + this.createModel = that; + } + this.ready = !!this.options.ready; + if (this.ModelType) { + this.decorate(this.ModelType); + } + } + /** + * Called when a new Model object is needed, being passed the new model ID. + * Uses the supplied `ModelType`; overriden by `options.create` if provided. + * + * @param {String} id The model ID to create. + * @returns {Model} Created model. + */ + prototype.createModel = function(id){ + return new this.ModelType({ + id: id + }); + }; + /** + * Registers a model with the cache. If a model by this ID already exists + * in the cache, it will be removed and this one will take its place. + * + * Fires an `add` event. + * + * @param {Model} model The model. + * @returns {Model} The model. + */ + prototype.register = function(model){ + if (this.cache.contains(model)) { + this.cache.remove(model, { + silent: true + }); + } + this.cache.add(model); + this.trigger('add', this, model); + return model; + }; + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + prototype.get = function(id){ + return this.cache.get(id); + }; + /** + * Asynchronously look up any number of models, requesting them from the + * server if not already known to the cache. + * + * @param {String|Array} ids List of model IDs to lookup. + * @param {Function} cb Callback of the form `(err, models)`, + * where `err` will be null on success and `models` will be an Array + * of model objects. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookupAll = function(ids, cb, cxt){ + var _this = this; + cxt == null && (cxt = this); + if (!_.isArray(ids)) { + ids = [ids]; + } + if (!this.ready) { + this.on('cache-ready', function(){ + _this.off('cache-ready', arguments.callee); + return _this.lookupAll(ids, cb, cxt); + }); + return this; + } + Seq(ids).parMap_(function(next, id){ + var that; + if (that = _this.cache.get(id)) { + return next.ok(that); + } + return _this.register(_this.createModel(id)).on('ready', function(it){ + return next.ok(it); + }).load(); + }).unflatten().seq(function(models){ + return cb.call(cxt, null, models); + })['catch'](function(err){ + return cb.call(cxt, err); + }); + return this; + }; + /** + * Looks up a model, requesting it from the server if it is not already + * known to the cache. + * + * @param {String|Array} id Model ID to lookup. + * @param {Function} cb Callback of the form `(err, model)`, + * where `err` will be null on success and `model` will be the + * model object. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookup = function(id, cb, cxt){ + cxt == null && (cxt = this); + return this.lookupAll([id], function(err, models){ + if (err) { + return cb.call(cxt, err); + } else { + return cb.call(cxt, null, models[0]); + } + }); + }; + /** + * Decorate an object with the cache methods: + * - register + * - get + * - lookup + * - lookupAll + * + * This is automatically called on `ModelType` if supplied. + * + * @param {Object} obj Object to decorate. + * @returns {obj} The supplied object. + */ + prototype.decorate = function(obj){ + var m, _i, _ref, _len; + obj.__cache__ = this; + for (_i = 0, _len = (_ref = ['register', 'get', 'lookup', 'lookupAll']).length; _i < _len; ++_i) { + m = _ref[_i]; + obj[m] = this[m].bind(this); + } + return obj; + }; + prototype.toString = function(){ + return (this.constructor.displayName || this.constructor.name) + "(cache=" + this.cache + ")"; + }; + return ModelCache; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/model-cache.mod.js b/lib/base/model-cache.mod.js new file mode 100644 index 0000000..4e529f9 --- /dev/null +++ b/lib/base/model-cache.mod.js @@ -0,0 +1,222 @@ +require.define('/node_modules/kraken/base/model-cache.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var Seq, ReadyEmitter, ModelCache, _; +_ = require('underscore'); +Seq = require('seq'); +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +/** + * @class Caches models and provides static lookups by ID. + */ +exports.ModelCache = ModelCache = (function(superclass){ + ModelCache.displayName = 'ModelCache'; + var prototype = __extend(ModelCache, superclass).prototype, constructor = ModelCache; + /** + * @see ReadyEmitter#readyEventName + * @private + * @constant + * @type String + */ + prototype.readyEventName = 'cache-ready'; + /** + * Default options. + * @private + * @constant + * @type Object + */ + prototype.DEFAULT_OPTIONS = { + ready: true, + cache: null, + create: null, + ModelType: null + }; + /** + * @private + * @type Object + */ + prototype.options = null; + /** + * Type we're caching (presumably extending `Backbone.Model`), used to create new + * instances unless a `create` function was provided in options. + * @private + * @type Class + */ + prototype.ModelType = null; + /** + * Collection holding the cached Models. + * @private + * @type Backbone.Collection + */ + prototype.cache = null; + /** + * @constructor + * @param {Class} [ModelType] Type of cached object (presumably extending + * `Backbone.Model`), used to create new instances unless `options.create` + * is provided. + * @param {Object} [options] Options: + * @param {Boolean} [options.ready=true] Starting `ready` state. If false, + * the cache will queue lookup calls until `triggerReady()` is called. + * @param {Class} [options.cache=new Backbone.Collection] + * The backing data-structure for the cache. If omitted, we'll use a new + * `Backbone.Collection`, but really, anything with a `get(id)` method for + * model lookup will work here. + * @param {Function} [options.create] A function called when a new Model + * object is needed, being passed the new model ID. + * @param {Class} [options.ModelType] Type of cached object + * (presumably extending `Backbone.Model`), used to create new instances + * unless `options.create` is provided. + */; + function ModelCache(ModelType, options){ + var that, _ref; + if (!_.isFunction(ModelType)) { + _ref = [ModelType || {}, null], options = _ref[0], ModelType = _ref[1]; + } + this.options = (_ref = {}, __import(_ref, this.DEFAULT_OPTIONS), __import(_ref, options)); + this.cache = this.options.cache || new Backbone.Collection; + this.ModelType = ModelType || this.options.ModelType; + if (that = this.options.create) { + this.createModel = that; + } + this.ready = !!this.options.ready; + if (this.ModelType) { + this.decorate(this.ModelType); + } + } + /** + * Called when a new Model object is needed, being passed the new model ID. + * Uses the supplied `ModelType`; overriden by `options.create` if provided. + * + * @param {String} id The model ID to create. + * @returns {Model} Created model. + */ + prototype.createModel = function(id){ + return new this.ModelType({ + id: id + }); + }; + /** + * Registers a model with the cache. If a model by this ID already exists + * in the cache, it will be removed and this one will take its place. + * + * Fires an `add` event. + * + * @param {Model} model The model. + * @returns {Model} The model. + */ + prototype.register = function(model){ + if (this.cache.contains(model)) { + this.cache.remove(model, { + silent: true + }); + } + this.cache.add(model); + this.trigger('add', this, model); + return model; + }; + /** + * Synchronously check if a model is in the cache, returning it if so. + * + * @param {String} id The model ID to get. + * @returns {Model} + */ + prototype.get = function(id){ + return this.cache.get(id); + }; + /** + * Asynchronously look up any number of models, requesting them from the + * server if not already known to the cache. + * + * @param {String|Array} ids List of model IDs to lookup. + * @param {Function} cb Callback of the form `(err, models)`, + * where `err` will be null on success and `models` will be an Array + * of model objects. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookupAll = function(ids, cb, cxt){ + var _this = this; + cxt == null && (cxt = this); + if (!_.isArray(ids)) { + ids = [ids]; + } + if (!this.ready) { + this.on('cache-ready', function(){ + _this.off('cache-ready', arguments.callee); + return _this.lookupAll(ids, cb, cxt); + }); + return this; + } + Seq(ids).parMap_(function(next, id){ + var that; + if (that = _this.cache.get(id)) { + return next.ok(that); + } + return _this.register(_this.createModel(id)).on('ready', function(it){ + return next.ok(it); + }).load(); + }).unflatten().seq(function(models){ + return cb.call(cxt, null, models); + })['catch'](function(err){ + return cb.call(cxt, err); + }); + return this; + }; + /** + * Looks up a model, requesting it from the server if it is not already + * known to the cache. + * + * @param {String|Array} id Model ID to lookup. + * @param {Function} cb Callback of the form `(err, model)`, + * where `err` will be null on success and `model` will be the + * model object. + * @param {Object} [cxt=this] Callback context. + * @returns {this} + */ + prototype.lookup = function(id, cb, cxt){ + cxt == null && (cxt = this); + return this.lookupAll([id], function(err, models){ + if (err) { + return cb.call(cxt, err); + } else { + return cb.call(cxt, null, models[0]); + } + }); + }; + /** + * Decorate an object with the cache methods: + * - register + * - get + * - lookup + * - lookupAll + * + * This is automatically called on `ModelType` if supplied. + * + * @param {Object} obj Object to decorate. + * @returns {obj} The supplied object. + */ + prototype.decorate = function(obj){ + var m, _i, _ref, _len; + obj.__cache__ = this; + for (_i = 0, _len = (_ref = ['register', 'get', 'lookup', 'lookupAll']).length; _i < _len; ++_i) { + m = _ref[_i]; + obj[m] = this[m].bind(this); + } + return obj; + }; + prototype.toString = function(){ + return (this.constructor.displayName || this.constructor.name) + "(cache=" + this.cache + ")"; + }; + return ModelCache; +}(ReadyEmitter)); +function __extend(sub, sup){ + function fun(){} fun.prototype = (sub.superclass = sup).prototype; + (sub.prototype = new fun).constructor = sub; + if (typeof sup.extended == 'function') sup.extended(sub); + return sub; +} +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/index.js b/lib/base/scaffold/index.js new file mode 100644 index 0000000..c0ee89d --- /dev/null +++ b/lib/base/scaffold/index.js @@ -0,0 +1,9 @@ +var models, views; +models = require('kraken/base/scaffold/scaffold-model'); +views = require('kraken/base/scaffold/scaffold-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/scaffold/index.mod.js b/lib/base/scaffold/index.mod.js new file mode 100644 index 0000000..2278e76 --- /dev/null +++ b/lib/base/scaffold/index.mod.js @@ -0,0 +1,13 @@ +require.define('/node_modules/kraken/base/scaffold/index.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var models, views; +models = require('kraken/base/scaffold/scaffold-model'); +views = require('kraken/base/scaffold/scaffold-view'); +__import(__import(exports, models), views); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/scaffold-model.js b/lib/base/scaffold/scaffold-model.js new file mode 100644 index 0000000..948a703 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.js @@ -0,0 +1,131 @@ +var op, BaseModel, BaseList, Field, FieldList, _, _ref, __slice = [].slice; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Field = exports.Field = BaseModel.extend({ + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function Field(){ + return BaseModel.apply(this, arguments); + } + return Field; + }()), + initialize: function(){ + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + return Field.__super__.initialize.apply(this, arguments); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serializers * * */, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +FieldList = exports.FieldList = BaseList.extend({ + model: Field, + constructor: (function(){ + function FieldList(){ + return BaseList.apply(this, arguments); + } + return FieldList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: true, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} \ No newline at end of file diff --git a/lib/base/scaffold/scaffold-model.mod.js b/lib/base/scaffold/scaffold-model.mod.js new file mode 100644 index 0000000..ee06f50 --- /dev/null +++ b/lib/base/scaffold/scaffold-model.mod.js @@ -0,0 +1,135 @@ +require.define('/node_modules/kraken/base/scaffold/scaffold-model.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseModel, BaseList, Field, FieldList, _, _ref, __slice = [].slice; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +_ref = require('kraken/base'), BaseModel = _ref.BaseModel, BaseList = _ref.BaseList; +Field = exports.Field = BaseModel.extend({ + valueAttribute: 'value', + defaults: function(){ + return { + name: '', + type: 'String', + 'default': null, + desc: '', + include: 'diff', + tags: [], + examples: [] + }; + }, + constructor: (function(){ + function Field(){ + return BaseModel.apply(this, arguments); + } + return Field; + }()), + initialize: function(){ + _.bindAll.apply(_, [this].concat(__slice.call(_.functions(this).filter(function(it){ + return _.startsWith(it, 'parse'); + })))); + this.set('id', this.id = _.camelize(this.get('name'))); + if (!this.has('value')) { + this.set('value', this.get('default'), { + silent: true + }); + } + return Field.__super__.initialize.apply(this, arguments); + } + /* * * Value Accessors * * */, + getValue: function(def){ + return this.getParser()(this.get(this.valueAttribute, def)); + }, + setValue: function(v, options){ + var def, val; + def = this.get('default'); + if (!v && def == null) { + val = null; + } else { + val = this.getParser()(v); + } + return this.set(this.valueAttribute, val, options); + }, + clearValue: function(){ + return this.set(this.valueAttribute, this.get('default')); + }, + isDefault: function(){ + return this.get(this.valueAttribute) === this.get('default'); + } + /* * * Serializers * * */, + serializeValue: function(){ + return this.serialize(this.getValue()); + }, + toJSON: function(){ + var _ref; + return __import({ + id: this.id + }, (_ref = _.clone(this.attributes), _ref.value = this.getValue(), _ref.def = this.get('default'), _ref)); + }, + toKVPairs: function(){ + var _ref; + return _ref = {}, _ref[this.id + ""] = this.serializeValue(), _ref; + }, + toString: function(){ + return "(" + this.id + ": " + this.serializeValue() + ")"; + } +}); +FieldList = exports.FieldList = BaseList.extend({ + model: Field, + constructor: (function(){ + function FieldList(){ + return BaseList.apply(this, arguments); + } + return FieldList; + }()) + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */, + values: function(opts){ + opts == null && (opts = {}); + opts = __import({ + keepDefaults: true, + serialize: false + }, opts); + return _.synthesize(opts.keepDefaults + ? this.models + : this.models.filter(function(it){ + return !it.isDefault(); + }), function(it){ + return [ + it.get('name'), opts.serialize + ? it.serializeValue() + : it.getValue() + ]; + }); + }, + toJSON: function(){ + return this.values({ + keepDefaults: true, + serialize: false + }); + }, + toKVPairs: function(){ + return _.collapseObject(this.values({ + keepDefaults: true, + serialize: true + })); + }, + toKV: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return _.toKV(this.toKVPairs(), item_delim, kv_delim); + }, + toURL: function(item_delim, kv_delim){ + item_delim == null && (item_delim = '&'); + kv_delim == null && (kv_delim = '='); + return "?" + this.toKV.apply(this, arguments); + } +}); +function __import(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; +} + +}); diff --git a/lib/base/scaffold/scaffold-view.js b/lib/base/scaffold/scaffold-view.js new file mode 100644 index 0000000..2bc3791 --- /dev/null +++ b/lib/base/scaffold/scaffold-view.js @@ -0,0 +1,116 @@ +var op, BaseView, Field, FieldList, FieldView, Scaffold, _, _ref; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/base/scaffold/scaffold-model'), Field = _ref.Field, FieldList = _ref.FieldList; +FieldView = exports.FieldView = BaseView.extend({ + tagName: 'div', + className: 'field', + type: 'string', + events: { + 'blur .value': 'onChange', + 'submit .value': 'onChange' + }, + constructor: (function(){ + function FieldView(){ + return BaseView.apply(this, arguments); + } + return FieldView; + }()), + initialize: function(){ + BaseView.prototype.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + }, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this); + }, + toTemplateLocals: function(){ + var json, v; + json = FieldView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + if (v = json.value && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * A ghetto default template, typically overridden by superclass. + */, + template: function(locals){ + return $("\n"); + }, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + return FieldView.__super__.render.apply(this, arguments); + } +}); +Scaffold = exports.Scaffold = BaseView.extend({ + __bind__: ['addField', 'resetFields'], + tagName: 'form', + className: 'scaffold', + collectionType: FieldList, + subviewType: FieldView, + constructor: (function(){ + function Scaffold(){ + return BaseView.apply(this, arguments); + } + return Scaffold; + }()), + initialize: function(){ + var CollectionType; + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + BaseView.prototype.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + return this.collection.on('reset', this.resetFields, this); + }, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + view = this.addSubview(new SubviewType({ + model: field + })); + view.on('change', this.onChange.bind(this, field)); + this.render(); + return view; + }, + resetFields: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return this; + }, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return Scaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); \ No newline at end of file diff --git a/lib/base/scaffold/scaffold-view.mod.js b/lib/base/scaffold/scaffold-view.mod.js new file mode 100644 index 0000000..f71a8da --- /dev/null +++ b/lib/base/scaffold/scaffold-view.mod.js @@ -0,0 +1,120 @@ +require.define('/node_modules/kraken/base/scaffold/scaffold-view.js.js', function(require, module, exports, __dirname, __filename, undefined){ + +var op, BaseView, Field, FieldList, FieldView, Scaffold, _, _ref; +_ = require('kraken/util/underscore'); +op = require('kraken/util/op'); +BaseView = require('kraken/base').BaseView; +_ref = require('kraken/base/scaffold/scaffold-model'), Field = _ref.Field, FieldList = _ref.FieldList; +FieldView = exports.FieldView = BaseView.extend({ + tagName: 'div', + className: 'field', + type: 'string', + events: { + 'blur .value': 'onChange', + 'submit .value': 'onChange' + }, + constructor: (function(){ + function FieldView(){ + return BaseView.apply(this, arguments); + } + return FieldView; + }()), + initialize: function(){ + BaseView.prototype.initialize.apply(this, arguments); + return this.type = this.model.get('type').toLowerCase() || 'string'; + }, + onChange: function(){ + var val, current; + if (this.type === 'boolean') { + val = !!this.$('.value').attr('checked'); + } else { + val = this.model.getParser()(this.$('.value').val()); + } + current = this.model.getValue(); + if (_.isEqual(val, current)) { + return; + } + this.model.setValue(val, { + silent: true + }); + return this.trigger('change', this); + }, + toTemplateLocals: function(){ + var json, v; + json = FieldView.__super__.toTemplateLocals.apply(this, arguments); + json.id || (json.id = _.camelize(json.name)); + json.value == null && (json.value = ''); + if (v = json.value && (_.isArray(v) || _.isPlainObject(v))) { + json.value = JSON.stringify(v); + } + return json; + } + /** + * A ghetto default template, typically overridden by superclass. + */, + template: function(locals){ + return $("\n"); + }, + render: function(){ + if (this.model.get('ignore')) { + return this.remove(); + } + return FieldView.__super__.render.apply(this, arguments); + } +}); +Scaffold = exports.Scaffold = BaseView.extend({ + __bind__: ['addField', 'resetFields'], + tagName: 'form', + className: 'scaffold', + collectionType: FieldList, + subviewType: FieldView, + constructor: (function(){ + function Scaffold(){ + return BaseView.apply(this, arguments); + } + return Scaffold; + }()), + initialize: function(){ + var CollectionType; + CollectionType = this.collectionType; + this.model = this.collection || (this.collection = new CollectionType); + BaseView.prototype.initialize.apply(this, arguments); + this.collection.on('add', this.addField, this); + return this.collection.on('reset', this.resetFields, this); + }, + addField: function(field){ + var SubviewType, view; + if (field.view) { + this.removeSubview(field.view); + } + field.off('change:value', this.onChange, this); + field.on('change:value', this.onChange, this); + SubviewType = this.subviewType; + view = this.addSubview(new SubviewType({ + model: field + })); + view.on('change', this.onChange.bind(this, field)); + this.render(); + return view; + }, + resetFields: function(){ + this.removeAllSubviews(); + this.collection.each(this.addField); + return this; + }, + onChange: function(field){ + var key, value; + key = field.get('name'); + value = field.getValue(); + this.trigger("change:" + key, this, value, key, field); + this.trigger("change", this, value, key, field); + return this; + } +}); +['get', 'at', 'pluck', 'invoke', 'values', 'toJSON', 'toKVPairs', 'toKV', 'toURL'].forEach(function(methodname){ + return Scaffold.prototype[methodname] = function(){ + return this.collection[methodname].apply(this.collection, arguments); + }; +}); + +}); diff --git a/lib/chart/chart-type.js b/lib/chart/chart-type.js new file mode 100644 index 0000000..037682a --- /dev/null +++ b/lib/chart/chart-type.js @@ -0,0 +1,435 @@ +var moment, Backbone, op, ReadyEmitter, Parsers, ParserMixin, KNOWN_CHART_TYPES, ChartType, _ref, _, __slice = [].slice; +moment = require('moment'); +Backbone = require('backbone'); +_ref = require('kraken/util'), _ = _ref._, op = _ref.op; +ReadyEmitter = require('kraken/util/event').ReadyEmitter; +_ref = require('kraken/util/parser'), Parsers = _ref.Parsers, ParserMixin = _ref.ParserMixin; +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_TYPES = exports.KNOWN_CHART_TYPES = {}; +/** + * @class Abstraction of a chart-type or charting library, encapsulating its + * logic and options. In addition, a `ChartType` also mediates the + * transformation of the domain-specific data types (the model and its view) + * with its specific needs. + * + * `ChartType`s mix in `ParserMixin`: when implementing a `ChartType`, you can + * add or supplement parsers merely by subclassing and overriding the + * corresponding `parseXXX` method (such as `parseArray` or `parseDate`). + * + * @extends EventEmitter + * @borrows ParserMixin + */ +exports.ChartType = ChartType = (function(superclass){ + /** + * Register a new chart type. + */ + ChartType.displayName = 'ChartType'; + var prototype = __extend(ChartType, superclass).prototype, constructor = ChartType; + ChartType.register = function(Subclass){ + return KNOWN_CHART_TYPES[Subclass.prototype.typeName] = Subclass; + }; + /** + * Look up a `ChartType` by `typeName`. + */ + ChartType.lookup = function(name){ + if (name instanceof Backbone.Model) { + name = name.get('chartType'); + } + return KNOWN_CHART_TYPES[name]; + }; + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {ChartType} + */ + ChartType.create = function(model, view){ + var Type; + if (!(Type = this.lookup(model))) { + return null; + } + return new Type(model, view); + }; + /* + * These are "class properties": each is set on the prototype at the class-level, + * and the reference is therefore shared by all instances. It is expected you + * will not modify this on the instance-level. + */ + /** + * URL for the Chart Spec JSON. Loaded once, the first time an instance of + * that class is created. + * @type String + * @readonly + */ + prototype.SPEC_URL = null; + /** + * Chart-type name. + * @type String + * @readonly + */ + prototype.typeName = null; + /** + * Map of option name to ChartOption objects. + * @type { name:ChartOption, ... } + * @readonly + */ + prototype.options = null; + /** + * Ordered ChartOption objects. + * + * This is a "class-property": it is set on the prototype at the class-level, + * and the reference is shared by all instances. It is expected you will not + * modify that instance. + * + * @type ChartOption[] + * @readonly + */ + prototype.options_ordered = null; + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + prototype.roles = { + viewport: '.viewport' + }; + /** + * Whether the ChartType has loaded all its data and is ready. + * @type Boolean + */ + prototype.ready = false; + /** + * Model to be rendered as a chart. + * @type Backbone.Model + */ + prototype.model = null; + /** + * View to render the chart into. + * @type Backbone.View + */ + prototype.view = null; + /** + * Last chart rendered by this ChartType. + * @private + */ + prototype.chart = null; + /** + * @constructor + */; + function ChartType(model, view){ + this.model = model; + this.view = view; + this.roles || (this.roles = {}); + _.bindAll.apply(_, [this].concat(__slice.call(this.__bind__))); + if (!this.ready) { + this.loadSpec(); + } + } + prototype.withModel = function(model){ + this.model = model; + return this; + }; + prototype.withView = function(view){ + this.view = view; + return this; + }; + /** + * Load