op = require 'kraken/util/op'
+/**
+ * @class Base model, extending Backbone.Model, used by scaffold and others.
+ * @extends Backbone.Model
+ */
BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
- ctorName : 'BaseModel'
- # List of methods to bind on initialize; set on subclass
+ # A list of method-names to bind on initialize; set this on a subclass to override.
__bind__ : []
-
- idAttribute : 'id'
- valueAttribute : 'value'
-
+ ctorName : 'BaseModel'
initialize: ->
_.bindAll this, ...@__bind__ if @__bind__.length
@__super__ = @constructor.__super__
-
- toKVPairs: (kv_delimiter='=') ->
- idAttr = @idAttribute or 'id'
- key = @[idAttr] or @get idAttr
- valAttr = @valueAttribute or 'value'
- value = @get 'value'
- if key and value?
- "#{encodeURIComponent key}#kv_delimiter#{encodeURIComponent value}"
- else
- ''
-
- toString: -> "#{@ctorName}(id=#{@id}, value=#{@get 'value'})"
+ /**
+ * Like `.toJSON()` in that it should return a plain object with no functions,
+ * but for the purpose of `.toKVPairs()`, allowing you to customize the values
+ * included and keys used.
+ * @returns {Object}
+ */
+ toKVObject: ->
+ _.collapseObject @toJSON()
+
+ /**
+ * Serialize the model into a `www-form-encoded` string suitable for use as
+ * a query string or a POST body.
+ * @returns {String}
+ */
+ toKVPairs: (item_delim='&', kv_delim='=') ->
+ _.toKVPairs @toKVObject(), item_delim, kv_delim
+
+ toString: -> "#{@ctorName}(id=#{@id})"
# Class Methods
BaseModel import do
- fromKVPairs: (o) ->
- o = _.fromKVPairs o if typeof o is 'string'
+ /**
+ * 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.
+ */
+ fromKVPairs: (o, item_delim='&', kv_delim='=') ->
+ o = _.fromKVPairs o, item_delim, kv_delim if typeof o is 'string'
Cls = if typeof this is 'function' then this else this.constructor
- new Cls o
+ new Cls _.uncollapseObject o
+
+# }}}
+/**
+ * @class Base collection, extending Backbone.Collection, used by scaffold and others.
+ * @extends Backbone.Collection
+ */
+BaseList = exports.BaseList = Backbone.Collection.extend do # {{{
+ ctorName : 'BaseList'
+
+ toKVObject: ->
+ _.collapseObject @toJSON()
+
+ toKVPairs: (item_delim='&', kv_delim='=') ->
+ _.toKVPairs @toKVObject(), item_delim, kv_delim
+
+ toString: -> "#{@ctorName}(length=#{@length})"
# }}}
/**
- * @class Base View, used by scaffold and others.
+ * @class Base view, extending Backbone.View, used by scaffold and others.
+ * @extends Backbone.View
*/
BaseView = exports.BaseView = Backbone.View.extend do # {{{
ctorName : 'BaseView'
for k, v in o
o[k] = '' if v!?
o
-
- toKVPairs: ->
- key = @get 'name'
- value = @get 'value'
- if value?
- "#{encodeURIComponent key}=#{encodeURIComponent value}"
- else
- ''
-
-
# }}}
GraphOptionList = exports.GraphOptionList = FieldList.extend do # {{{
ctorName : 'GraphOptionList'
model : GraphOption
-
- /**
- * Transforms this list into form-encoded KV-pairs, excluding null values and
- * @returns {String}
- */
- toKVPairs: (keepDefaults=false) ->
- @models
- .filter -> it.get('name') and it.getValue()? and (keepDefaults or not it.isDefault())
- .map -> "#{encodeURIComponent it.get 'name'}=#{encodeURIComponent it.serializeValue()}"
- .join '&'
-
# }}}
if name and not (@id or @has 'id')
@id = @attributes.id = _.underscored name
- defaults : ->
+ defaults: ->
{
+ id : 'graph'
name : 'Kraken Graph'
dataset : '/data/pageviews_by.timestamp.language.csv'
+ desc : ''
+ width : 'auto'
+ height : 'auto'
options : {}
}
hasOption: (key) ->
options = @get 'options', {}
- key in options
+ options[key]?
getOption: (key, def) ->
- options = @get 'options', {}
- if key in options then options[key] else def
+ @get('options', {})[key] ? def
setOption: (key, value, opts={}) ->
options = @get 'options', {}
@trigger "change:options:#key", this, value, key, opts unless opts.silent
- toKVPairs: ->
- ...
-
toString: -> "#{@ctorName}(id=#{@id}, name=#{@get 'name'}, dataset=#{@get 'dataset'})"
+
# }}}
'click .collapsed' : 'onClick'
- update: ->
- val = @$el.find('.value').val()
- current = @model.get 'value'
- return if val is current
- console.log "#this.update( #current -> #val )"
- @model.setValue val, {+silent}
-
render: ->
@__super__.render ...
@$el.addClass 'collapsed' if @isCollapsed
# itemPositionDataEnabled : true
# animationEngine : 'jquery'
+ /**
+ * Add a GraphOption to this scaffold, rerendering the isotope
+ * layout after collapse events.
+ */
addOne: (field) ->
- # console.log "#this.addOne!", field
-
- field.on 'change:value', ~>
- key = field.get 'name'
- value = field.getValue()
- @trigger "change:#key", this, value, key, field
- @trigger "change", this, value, key, field
-
view = @__super__.addOne ...
view.on 'change:collapse render', @render
view
toKVPairs: ->
- @collection.toKVPairs()
+ @collection.toKVPairs ...
# }}}
initialize : (o={}) ->
- @render = _.debounce @render, DEBOUNCE_RENDER
- @renderAll = _.debounce @renderAll, DEBOUNCE_RENDER
-
@model or= new GraphModel
BaseView::initialize ...
# console.log "#this.initialize!"
+ @render = _.debounce @render, DEBOUNCE_RENDER
+ @renderAll = _.debounce @renderAll, DEBOUNCE_RENDER
+ @model.on 'destroy', @remove, this
+ @model.on 'change', @render, this
@model.on 'change:options', ~>
changes = @model.changedAttributes()
console.log 'Model.changed(options) ->', changes
@chartOptions changes, {+silent}
- @model.on 'change', @render, this
- @model.on 'destroy', @remove, this
@build()
@viewport = @$el.find '.viewport'
@scaffold.collection.reset that if o.graph_spec
@scaffold.on 'change', (scaffold, value, key, field) ~>
- console.log "scaffold.change", value, key, field
+ console.log "scaffold.change!", value, key, field
@model.setOption key, value, {+silent}
options = @model.get 'options', {}
@chartOptions options, {+silent}
- _.delay @render, DEBOUNCE_RENDER
+ _.delay @renderAll, DEBOUNCE_RENDER
chartOptions: (values, opts) ->
@render()
false
-
- toKVPairs: (keepDefaults=false) ->
- @scaffold.toKVPairs keepDefaults
+ toKVPairs: ->
+ @model.toKVPairs ...
toString: -> "#{@ctorName}(#{@model})"
# }}}
_ = require 'kraken/underscore'
op = require 'kraken/util/op'
-{BaseModel} = require 'kraken/base'
+{BaseModel, BaseList} = require 'kraken/base'
null
- /* * * Serializers * * */
- serializeValue: ->
- v = @getValue()
- if v!?
- v = ''
- else if _.isArray(v) or _.isObject(v)
- v = JSON.stringify v
- String v
-
-
/* * * Value Accessors * * */
getValue: (def) ->
- @getParser() @get 'value', def
+ @getParser() @get @valueAttribute, def
setValue: (v, options) ->
def = @get 'default'
val = null
else
val = @getParser()(v)
- @set 'value', val, options
+ @set @valueAttribute, val, options
clearValue: ->
- @set 'value', @get('default')
+ @set @valueAttribute, @get('default')
isDefault: ->
- @get('value') is @get('default')
+ @get(@valueAttribute) is @get('default')
+ /* * * Serializers * * */
+ serializeValue: ->
+ v = @getValue()
+ if v!?
+ v = ''
+ else if _.isBoolean v
+ v = Number v
+ else if _.isArray(v) or _.isObject(v)
+ v = JSON.stringify v
+ String v
+
toJSON: ->
{id:@id} import do
_.clone(@attributes) import { value:@getValue(), def:@get('default') }
+ toKVObject: ->
+ { "#{@id}": @serializeValue() }
- toString: -> "(#{@id}: #{@get 'value'})"
+ toString: -> "(#{@id}: #{@serializeValue()})"
# }}}
-FieldList = exports.FieldList = Backbone.Collection.extend do # {{{
+FieldList = exports.FieldList = BaseList.extend do # {{{
ctorName : 'FieldList'
- model : Field
+ model : Field
/**
* Collects a map of fields to their values, excluding those set to `null` or their default.
* @returns {Object}
*/
- values: ->
+ values: (keepDefaults=false) ->
_.synthesize do
- @models.filter -> not it.isDefault()
+ if keepDefaults then @models else @models.filter -> not it.isDefault()
-> [ it.get('name'), it.getValue() ]
toJSON: ->
@values()
- toKVPairs: ->
- @models
- .filter -> it.get('name') and it.getValue()?
- .map -> "#{encodeURIComponent it.id}=#{encodeURIComponent it.get 'value'}"
- .join '&'
+ toKVObject: ->
+ _.collapseObject @toJSON()
+
+ toKVPairs: (item_delim='&', kv_delim='=') ->
+ _.toKVPairs @toKVObject(), item_delim, kv_delim
toString: -> "#{@ctorName}(length=#{@length})"
# }}}
className : 'field'
events :
- 'blur .value' : 'onUIChange'
- 'submit .value' : 'onUIChange'
+ 'blur .value' : 'update'
+ 'submit .value' : 'update'
# initialize: ->
# # console.log "#this.initialize!"
# BaseView::initialize ...
- onUIChange: ->
- val = @$el.find('.value').val()
- current = @model.get 'value'
- return if val is current
- console.log "#this.onUIChange( #current -> #val )"
+ update: ->
+ val = @model.getParser() @$el.find('.value').val()
+ current = @model.getValue()
+ return if _.isEqual val, current
+ console.log "#this.update( #current -> #val )"
@model.setValue val, {+silent}
+ @trigger 'update', this
render: ->
return @remove() if @model.get 'hidden', false
# console.log "[S] #this.addOne!", @__super__
_.remove @subviews, field.view if field.view
+ # avoid duplicating event propagation
+ field.off 'change:value', @change, this
+
+ # propagate value-change events as key-value change events
+ field.on 'change:value', @change, this
+
SubviewType = @subviewType
view = new SubviewType model:field
@subviews.push view
@$el.append view.render().el unless field.get 'hidden'
+ view.on 'update', @change.bind(this, field)
@render()
view
@collection.each @addOne
this
- get: (id) ->
- @collection.get id
-
- at: (idx) ->
- @collection.at idx
-
- pluck: (prop) ->
- @collection.pluck prop
+ change: (field) ->
+ key = field.get 'name'
+ value = field.getValue()
+ @trigger "change:#key", this, value, key, field
+ @trigger "change", this, value, key, field
+ this
- values: ->
- @collection.values()
toString: -> "#{@ctorName}(collection=#{@collection})"
+
+
+
+# Proxy collection methods
+<[ get at pluck invoke values toJSON toKVObject toKVPairs ]>
+ .forEach (methodname) ->
+ Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments
+
# }}}
_ = require 'underscore'
+OBJ_PROTO = Object.prototype
+getProto = Object.getPrototypeOf
+
_obj = do
+
+ isPlainObject : (o) ->
+ !! o and _.isObject(o) and OBJ_PROTO is getProto(o)
+
+ /**
+ * Converts the collection to a list of its items:
+ * - Objects become a list of `[key, value]` pairs.
+ * - Strings become a list of characters.
+ * - Arguments objects become an array.
+ * - Arrays are copied.
+ */
+ items: (obj) ->
+ if _.isObject(obj) and not _.isArguments(obj)
+ _.map obj, (v, k) -> [k, v]
+ else
+ [].slice.call obj
+
/**
* In-place removal of a value from an Array or Object.
*/
- toKVPairs: (o, item_delimiter='&', kv_delimiter='=') ->
+ toKVPairs: (o, item_delim='&', kv_delim='=') ->
_.reduce do
o
(acc, v, k) ->
- acc.push encodeURIComponent(k)+kv_delimiter+encodeURIComponent(v) if k
+ acc.push encodeURIComponent(k)+kv_delim+encodeURIComponent(v) if k
acc
[]
- .join item_delimiter
-
+ .join item_delim
- fromKVPairs: (qs, item_delimiter='&', kv_delimiter='=') ->
+ fromKVPairs: (qs, item_delim='&', kv_delim='=') ->
_.reduce do
- qs.split item_delimiter
+ qs.split item_delim
(acc, pair) ->
- [k, v] = pair.split kv_delimiter
+ idx = pair.indexOf kv_delim
+ if idx is not -1
+ [k, v] = [pair.slice(0, idx), pair.slice(idx+1)]
+ else
+ [k, v] = [pair, '']
acc[ decodeURIComponent k ] = decodeURIComponent v if k
acc
{}
-
+
+ /**
+ * @returns {Object} Copies and flattens any sub-objects into namespaced keys on the parent object,
+ * such that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`.
+ */
+ collapseObject: (obj, parent={}, prefix='') ->
+ prefix += '.' if prefix
+ _.each obj, (v, k) ->
+ if _.isPlainObject v
+ _.collapseObject v, parent, prefix+k
+ else
+ parent[prefix+k] = v
+ parent
+
+ uncollapseObject: (obj) ->
+ _.reduce do
+ obj
+ (acc, v, k) ->
+ _.setNested acc, k, v, true
+ acc
+ {}
/**
* Searches a heirarchical object for a given subkey specified in dotted-property syntax.
* @param {Object} base The object to serve as the root of the property-chain.
* @param {Array|String} chain The property-chain to lookup.
+ * @param {Boolean} [ensure=false] If true, intermediate keys that are `null` or
+ * `undefined` will be filled in with a new empty object `{}`, ensuring the get will
+ * return valid metadata.
* @retruns {null|Object} If found, the object is of the form `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key] }`. Otherwise `null`.
*/
- getNestedMeta : (obj, chain) ->
+ getNestedMeta : (obj, chain, ensure=false) ->
chain = chain.split('.') if typeof chain is 'string'
- return _.reduce do
+ _.reduce do
chain
(current, key, idx, chain) ->
return null unless current?
key : key
val : current[key]
- if key in current
- current[key]
+ val = current[key]
+ if val?
+ val
+ else if ensure
+ current[key] = {}
else
null
obj
* @param {Object} obj The object to serve as the root of the property-chain.
* @param {Array|String} chain The property-chain to lookup.
* @param {Any} value The value to set.
- * @retruns {null|Object} If found, returns the old value, and otherwise `null`.
+ * @param {Boolean} [ensure=false] If true, intermediate keys that are `null` or
+ * `undefined` will be filled in with a new empty object `{}`, ensuring the set
+ * will succeed.
+ * @retruns {null|Any} If found, returns the old value, and otherwise `null`.
*/
- setNested : (obj, chain, value) ->
- meta = _obj.getNestedMeta obj, chain
+ setNested : (obj, chain, value, ensure=false) ->
+ meta = _obj.getNestedMeta obj, chain, ensure
if meta
meta.obj[meta.key] = value
meta.val