From: dsc Date: Thu, 12 Apr 2012 00:34:41 +0000 (-0700) Subject: Adds CascadingModel base model, for looking up model attributes on a chain of diction... X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=c3c0bf81f799a7fc9214056f02b6d37b81816704;p=kraken-ui.git Adds CascadingModel base model, for looking up model attributes on a chain of dictionaries. --- diff --git a/lib/base.co b/lib/base.co deleted file mode 100644 index 1a818a3..0000000 --- a/lib/base.co +++ /dev/null @@ -1,354 +0,0 @@ -{ _, op, -} = require 'kraken/util' - -Backbone = require 'backbone' - - - -BaseBackboneMixin = exports.BaseBackboneMixin = - - initialize: -> - @__apply_bind__() - - - ### Auto-Bound methods - - /** - * 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__: -> - names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value() - _.bindAll this, ...names if names.length - - - - ### Synchronization - - /** - * Count of outstanding tasks. - * @type Number - */ - waitingOn : 0 - - - /** - * Increment the waiting task counter. - * @returns {this} - */ - wait: -> - count = @waitingOn - @waitingOn += 1 - console.log "#this.wait! #count --> #{@waitingOn}" - @trigger('start-waiting', this) if count is 0 and @waitingOn > 0 - this - - /** - * Decrement the waiting task counter. - * @returns {this} - */ - unwait: -> - count = @waitingOn - @waitingOn -= 1 - console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0 - console.log "#this.unwait! #count --> #{@waitingOn}" - @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0 - 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: (fn) -> - self = this - -> - console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )" - self.unwait() - fn ... - - - -mixinBase = exports.mixinBase = (body) -> - _.clone(BaseBackboneMixin) import body - - -/** - * @class Base model, extending Backbone.Model, used by scaffold and others. - * @extends Backbone.Model - */ -BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ - - constructor : function BaseModel - @__class__ = @constructor - @__superclass__ = @..__super__.constructor - @waitingOn = 0 - Backbone.Model ... - @trigger 'create', this - - - - - ### Accessors - - has: (key) -> - @get(key)? - - get: (key) -> - _.getNested @attributes, key - - # set: (key, value, opts) -> - # if _.isObject(key) and key? - # [values, opts] = [key, value] - # else - # values = { "#key": value } - # - # # TODO: Validation - # @_changed or= {} - # - # for key, value in values - # if _.str.contains key, '.' - # _.setNested @attributes, key, value, opts - # else - # Backbone.Model::set.call this, key, value, opts - # - # this - # - # unset : (key, opts) -> - # - - - - - - ### Serialization - - serialize: (v) -> - # if v!? - # v = '' - if _.isBoolean v - v = Number v - else if _.isObject v - v = JSON.stringify v - 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. - * @returns {Object} - */ - toKVPairs: -> - kvo = _.collapseObject @toJSON() - for k, v in kvo - kvo[k] = @serialize v - kvo - - /** - * Serialize the model into a `www-form-encoded` string suitable for use as - * a query string or a POST body. - * @returns {String} - */ - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - /** - * @returns {String} URL identifying this model. - */ - toURL: -> - "?#{@toKV ...}" - - toString: -> "#{@..name or @..displayName}(cid=#{@cid}, id=#{@id})" - - -# Class Methods -BaseModel import do - /** - * 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: (o, item_delim='&', kv_delim='=') -> - o = _.fromKV o, item_delim, kv_delim if typeof o is 'string' - Cls = if typeof this is 'function' then this else this.constructor - 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 do # {{{ - - - constructor : function BaseList - @__class__ = @constructor - @__superclass__ = @..__super__.constructor - @waitingOn = 0 - Backbone.Collection ... - @trigger 'create', this - - - ### Serialization - - toKVPairs: -> - _.collapseObject @toJSON() - - toKV: (item_delim='&', kv_delim='=') -> - _.toKV @toKVPairs(), item_delim, kv_delim - - toURL: (item_delim='&', kv_delim='=') -> - "?#{@toKV ...}" - - toString: -> "#{@..name or @..displayName}(length=#{@length})" -# }}} - - -/** - * @class Base view, extending Backbone.View, used by scaffold and others. - * @extends Backbone.View - */ -BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ - tagName : 'section' - - /** - * Array of [view, selector]-pairs. - * @type Array<[BaseView, String]> - */ - subviews : [] - - - - constructor : function BaseView - @__class__ = @constructor - @__superclass__ = @..__super__.constructor - @waitingOn = 0 - @subviews = [] - Backbone.View ... - @trigger 'create', this - - initialize: -> - @__apply_bind__() - - @setModel @model - @build() - - setModel: (model) -> - if @model - @model.off 'change', @render, this - @model.off 'destroy', @remove, this - delete @model.view - data = @$el.data() - delete data.model - delete data.view - if @model = model - @model.view = this - @$el.data { @model, view:this } - @model.on 'change', @render, this - @model.on 'destroy', @remove, this - @model - - - - ### Subviews - - addSubview: (selector, view) -> - [view, selector] = [selector, null] unless view - @subviews.push [view, selector] - view - - removeSubview: (view) -> - for [v, sel], idx of @subviews - if v is view - @subviews.splice(idx, 1) - return [v, sel] - null - - hasSubview: (view) -> - _.any @subviews, ([v]) -> v is view - - attachSubviews: -> - for [view, selector] of @subviews - return unless view - view.undelegateEvents() - return unless el = view.render()?.el - if selector - @$el.find selector .append el - else - @$el.append el - view.delegateEvents() - this - - - ### Rendering Chain - - toTemplateLocals: -> - json = {value:v} = @model.toJSON() - if _.isArray(v) or _.isObject(v) - json.value = JSON.stringify v - json - - $template: (locals={}) -> - $ @template do - { $, _, op, @model, view:this } import @toTemplateLocals() import locals - - build: -> - return this unless @template - outer = @$template() - @$el.html outer.html() - .attr do - id : outer.attr 'id' - class : outer.attr('class') - @attachSubviews() - this - - render: -> - @build() - @trigger 'render', this - this - - renderSubviews: -> - _.invoke _.pluck(@subviews, 0), 'render' - this - - - - - ### UI Utilities - - hide : -> @$el.hide(); this - show : -> @$el.show(); this - remove : -> @$el.remove(); this - clear : -> @model.destroy(); @remove() - - - # remove : -> - # if (p = @$el.parent()).length - # @$parent or= p - # # @parent_index = p.children().indexOf @$el - # @$el.remove() - # this - # - # reparent : (parent=@$parent) -> - # parent = $ parent - # @$el.appendTo parent if parent?.length - # this - - toString : -> "#{@..name or @..displayName}(model=#{@model})" - - -# Proxy model methods -<[ get set unset toJSON toKV toURL ]> - .forEach (methodname) -> - BaseView::[methodname] = -> @model[methodname].apply @model, arguments - -# }}} - diff --git a/lib/base/base-mixin.co b/lib/base/base-mixin.co new file mode 100644 index 0000000..00d25ce --- /dev/null +++ b/lib/base/base-mixin.co @@ -0,0 +1,82 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' + + + +BaseBackboneMixin = exports.BaseBackboneMixin = + + initialize: -> + @__apply_bind__() + + + ### Auto-Bound methods + + /** + * 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__: -> + names = _ @pluckSuperAndSelf '__bind__' .chain().flatten().compact().unique().value() + _.bindAll this, ...names if names.length + + + + ### Synchronization + + /** + * Count of outstanding tasks. + * @type Number + */ + waitingOn : 0 + + + /** + * Increment the waiting task counter. + * @returns {this} + */ + wait: -> + count = @waitingOn + @waitingOn += 1 + # console.log "#this.wait! #count --> #{@waitingOn}" + # console.trace() + @trigger('start-waiting', this) if count is 0 and @waitingOn > 0 + this + + /** + * Decrement the waiting task counter. + * @returns {this} + */ + unwait: -> + count = @waitingOn + @waitingOn -= 1 + # console.warn "#this.unwait! #{@waitingOn} < 0" if @waitingOn < 0 + # console.log "#this.unwait! #count --> #{@waitingOn}" + # console.trace() + @trigger('stop-waiting', this) if @waitingOn is 0 and count > 0 + 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: (fn) -> + self = this + -> + # console.log "#self.unwaitAnd( function #{fn.name or fn.displayName}() )" + # console.trace() + self.unwait(); fn ... + + + +mixinBase = exports.mixinBase = (body) -> + _.clone(BaseBackboneMixin) import body + + diff --git a/lib/base/base-model.co b/lib/base/base-model.co new file mode 100644 index 0000000..5eb7c18 --- /dev/null +++ b/lib/base/base-model.co @@ -0,0 +1,144 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' +{ BaseBackboneMixin, mixinBase, +} = require 'kraken/base/base-mixin' + + + +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ +BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ + + constructor : function BaseModel + @__class__ = @constructor + @__superclass__ = @..__super__.constructor + @waitingOn = 0 + Backbone.Model ... + @trigger 'create', this + + + + + ### Accessors + + has: (key) -> + @get(key)? + + get: (key) -> + _.getNested @attributes, key + + # set: (key, value, opts) -> + # if _.isObject(key) and key? + # [values, opts] = [key, value] + # else + # values = { "#key": value } + # + # # TODO: Validation + # @_changed or= {} + # + # for key, value in values + # if _.str.contains key, '.' + # _.setNested @attributes, key, value, opts + # else + # Backbone.Model::set.call this, key, value, opts + # + # this + # + # unset : (key, opts) -> + # + + + + + + ### Serialization + + serialize: (v) -> + # if v!? + # v = '' + if _.isBoolean v + v = Number v + else if _.isObject v + v = JSON.stringify v + 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. + * @returns {Object} + */ + toKVPairs: -> + kvo = _.collapseObject @toJSON() + for k, v in kvo + kvo[k] = @serialize v + kvo + + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */ + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + /** + * @returns {String} URL identifying this model. + */ + toURL: -> + "?#{@toKV ...}" + + toString: -> "#{@..name or @..displayName}(cid=#{@cid}, id=#{@id})" + + +# Class Methods +BaseModel import do + /** + * 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: (o, item_delim='&', kv_delim='=') -> + o = _.fromKV o, item_delim, kv_delim if typeof o is 'string' + Cls = if typeof this is 'function' then this else this.constructor + 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 do # {{{ + + + constructor : function BaseList + @__class__ = @constructor + @__superclass__ = @..__super__.constructor + @waitingOn = 0 + Backbone.Collection ... + @trigger 'create', this + + + ### Serialization + + toKVPairs: -> + _.collapseObject @toJSON() + + toKV: (item_delim='&', kv_delim='=') -> + _.toKV @toKVPairs(), item_delim, kv_delim + + toURL: (item_delim='&', kv_delim='=') -> + "?#{@toKV ...}" + + toString: -> "#{@..name or @..displayName}(length=#{@length})" +# }}} + + diff --git a/lib/base/base-view.co b/lib/base/base-view.co new file mode 100644 index 0000000..9c4e5ec --- /dev/null +++ b/lib/base/base-view.co @@ -0,0 +1,149 @@ +Backbone = require 'backbone' + +{ _, op, +} = require 'kraken/util' +{ BaseBackboneMixin, mixinBase, +} = require 'kraken/base/base-mixin' + + + +/** + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View + */ +BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{ + tagName : 'section' + + /** + * Array of [view, selector]-pairs. + * @type Array<[BaseView, String]> + */ + subviews : [] + + + + constructor : function BaseView + @__class__ = @constructor + @__superclass__ = @..__super__.constructor + @waitingOn = 0 + @subviews = [] + Backbone.View ... + @trigger 'create', this + + initialize: -> + @__apply_bind__() + + @setModel @model + @build() + + setModel: (model) -> + if @model + @model.off 'change', @render, this + @model.off 'destroy', @remove, this + delete @model.view + data = @$el.data() + delete data.model + delete data.view + if @model = model + @model.view = this + @$el.data { @model, view:this } + @model.on 'change', @render, this + @model.on 'destroy', @remove, this + @model + + + + ### Subviews + + addSubview: (selector, view) -> + [view, selector] = [selector, null] unless view + @subviews.push [view, selector] + view + + removeSubview: (view) -> + for [v, sel], idx of @subviews + if v is view + @subviews.splice(idx, 1) + return [v, sel] + null + + hasSubview: (view) -> + _.any @subviews, ([v]) -> v is view + + attachSubviews: -> + for [view, selector] of @subviews + return unless view + view.undelegateEvents() + return unless el = view.render()?.el + if selector + @$el.find selector .append el + else + @$el.append el + view.delegateEvents() + this + + + ### Rendering Chain + + toTemplateLocals: -> + json = {value:v} = @model.toJSON() + if _.isArray(v) or _.isObject(v) + json.value = JSON.stringify v + json + + $template: (locals={}) -> + $ @template do + { $, _, op, @model, view:this } import @toTemplateLocals() import locals + + build: -> + return this unless @template + outer = @$template() + @$el.html outer.html() + .attr do + id : outer.attr 'id' + class : outer.attr('class') + @attachSubviews() + this + + render: -> + @build() + @trigger 'render', this + this + + renderSubviews: -> + _.invoke _.pluck(@subviews, 0), 'render' + this + + + + + ### UI Utilities + + hide : -> @$el.hide(); this + show : -> @$el.show(); this + remove : -> @$el.remove(); this + clear : -> @model.destroy(); @remove() + + + # remove : -> + # if (p = @$el.parent()).length + # @$parent or= p + # # @parent_index = p.children().indexOf @$el + # @$el.remove() + # this + # + # reparent : (parent=@$parent) -> + # parent = $ parent + # @$el.appendTo parent if parent?.length + # this + + toString : -> "#{@..name or @..displayName}(model=#{@model})" + + +# Proxy model methods +<[ get set unset toJSON toKV toURL ]> + .forEach (methodname) -> + BaseView::[methodname] = -> @model[methodname].apply @model, arguments + +# }}} + diff --git a/lib/base/cascading-model.co b/lib/base/cascading-model.co new file mode 100644 index 0000000..e16f4d3 --- /dev/null +++ b/lib/base/cascading-model.co @@ -0,0 +1,50 @@ +{ _, op, +} = require 'kraken/util' +{ BaseModel, BaseList, +} = require 'kraken/base/base-model' + +Cascade = require 'kraken/util/cascade' + + + +/** + * @class A model that implements cascading lookups for its attributes. + */ +CascadingModel = exports.CascadingModel = BaseModel.extend do # {{{ + /** + * The lookup cascade. + * @type Cascade + */ + cascade : null + + + constructor: function CascadingModel (attributes={}, opts) + @cascade = new Cascade attributes + BaseModel.call this, attributes, opts + + initialize: -> + BaseModel::initialize ... + + get: (key) -> + @cascade.get key + + toJSON: (opts={}) -> + opts = {-collapseCascade, ...opts} + if opts.collapseCascade + @cascade.toJSON() + else + BaseModel::toJSON() + + + +# Proxy Cascade methods +<[ + addLookup removeLookup popLookup shiftLookup unshiftLookup + isOwnProperty isOwnValue isInheritedValue isModifiedValue +]>.forEach (methodname) -> + CascadingModel::[methodname] = -> @cascade[methodname].apply @cascade, arguments + +# }}} + + + diff --git a/lib/base/index.co b/lib/base/index.co new file mode 100644 index 0000000..eb20dbb --- /dev/null +++ b/lib/base/index.co @@ -0,0 +1,5 @@ +mixins = require 'kraken/base/base-mixin' +models = require 'kraken/base/base-model' +views = require 'kraken/base/base-view' +cascading = require 'kraken/base/cascading-model' +exports import mixins import models import views import cascading diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index ee33f02..21d9d5f 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -241,7 +241,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ * @returns {Boolean} */ isChangedOption: (k) -> - @optionCascade.isChangedValue k + @optionCascade.isModifiedValue k and not @isDefaultOption k diff --git a/lib/util/cascade.co b/lib/util/cascade.co index fb1cdb7..2f62caf 100644 --- a/lib/util/cascade.co +++ b/lib/util/cascade.co @@ -53,6 +53,13 @@ class Cascade @_data = @_lookups[0] = data this + + /** + * @returns {Number} Number of lookup dictionaries. + */ + size: -> + @_lookups.length + /** * Adds a new lookup dictionary to the chain. * @returns {this} @@ -64,18 +71,46 @@ class Cascade this /** - * Removes a lookup dictionary from the chain. + * Removes a lookup dictionary from the chain (but will not remove the data object). * @returns {this} */ removeLookup: (dict) -> - _.remove @_lookups, dict if dict + _.remove @_lookups, dict if dict and dict is not @_data this /** + * Pops the last dictionary off the lookup chain and returns it. + * @returns {*} The last dictionary, or `undefined` if there are no additional lookups. + */ + popLookup: -> + return if @size() <= 1 + @_lookups.pop() + + /** + * Shifts the first additional lookup dictionary off the chain and returns it. + * @returns {*} The first dictionary, or `undefined` if there are no additional lookups. + */ + shiftLookup: -> + return if @size() <= 1 + @_lookups.splice(1, 1)[0] + + /** + * Adds a lookup dictionary to the front of the chain, just after the Cascade's own data + * object. + * @returns {this} + */ + unshiftLookup: (dict) -> + return this unless dict? + throw new Error "Lookup dictionary must be an object! dict=#dict" unless _.isObject dict + @_lookups.splice 1, 0, dict + this + + + /** * @returns {Boolean} Whether `key` belongs to this object (not inherited * from the cascade). */ - hasOwnProperty: (key) -> + isOwnProperty: (key) -> meta = _.getNestedMeta(@_data, key) meta?.obj and hasOwn.call meta.obj, key @@ -84,32 +119,26 @@ class Cascade * from the cascade) and is defined. */ isOwnValue: (key) -> - @hasOwnProperty(key) and _.getNested(@_data, key, MISSING) is not MISSING + @isOwnProperty(key) and _.getNested(@_data, key, MISSING) is not MISSING /** * @returns {Boolean} Whether the value at `key` is different from that * inherited by from the cascade. */ - isChangedValue: (key, strict=false) -> - val = @get key - cVal = @_getInCascade key, MISSING, 1 - if strict - val is not cVal - else - not _.isEqual val, cVal + isModifiedValue: (key, strict=false) -> + not @isInheritedValue key, strict /** * @returns {Boolean} Whether the value at `key` is the same as that * inherited by from the cascade. */ isInheritedValue: (key, strict=false) -> - not @isChangedValue key, strict - - /** - * @returns {Number} Number of lookup dictionaries. - */ - size: -> - @_lookups.length + val = @get key + cVal = @_getInCascade key, MISSING, 1 + if strict + val is cVal + else + _.isEqual val, cVal diff --git a/www/modules.yaml b/www/modules.yaml index 93bb9d9..1e965c5 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -53,7 +53,12 @@ dev: - parser - cascade - index - - base + - base: + - base-mixin + - base-model + - base-view + - cascading-model + - index - scaffold: - scaffold-model - scaffold-view