BaseBackboneMixin = exports.BaseBackboneMixin =
-
initialize: ->
@__apply_bind__()
"#{@getClassName()}()"
-mixinBase = exports.mixinBase = (body) ->
- _.clone(BaseBackboneMixin) import body
+
+
+/**
+ * @class Base mixin class. Extend this to create a new mixin, attaching the
+ * donor methods as you would instance methods. Your constructor must delegate
+ * to its superclass, passing itself as the context.
+ *
+ * Then, to mingle your mixin with another class or object, invoke the mixin
+ * *without* the `new` operator:
+ *
+ * class MyMixin extends Mixin
+ * -> return Mixin.call MyMixin, it
+ * foo: -> "foo!"
+ *
+ * # Mix into an object...
+ * o = MyMixin { bar:1 }
+ *
+ * # Mix into a Coco class...
+ * class Bar
+ * MyMixin this
+ * bar : 1
+ *
+ */
+class exports.Mixin
+
+ /**
+ * Mixes this mixin into If the target is not a class, a new object will be
+ * returned which inherits from the mixin.
+ * @constructor
+ */
+ (target) ->
+ return unless target
+
+ MixinClass = Mixin
+ MixinClass = @constructor if this instanceof Mixin
+ MixinClass = this if this instanceof Function
+
+ if typeof target is 'function'
+ target:: import MixinClass::
+ else
+ target = _.clone(MixinClass::) import target
+
+ return target
+
+
+
+
+# /**
+# * @returns {Function} Function which takes a target object or class,
+# * mixes the MixinClass into it, and then returns it. If the target is
+# * not a class, a new object will be returned which inherits from the mixin.
+# */
+# makeMixer = exports.makeMixer = (MixinClass) ->
+# mixinBody = if typeof MixinClass is 'function' then MixinClass:: else MixinClass
+# mixinMixer = (target) ->
+# if typeof target is 'function'
+# target:: import mixinBody
+# else
+# target = _.clone(mixinBody) import target
+# target
+#
+# mixinBase = exports.mixinBase = makeMixer BaseBackboneMixin
+
+
+/**
+ * Mixes BaseBackboneMixin into another object or prototype.
+ * @returns {Object} The merged prototype object.
+ */
+mixinBase = exports.mixinBase = (...bodies) ->
+ _.extend _.clone(BaseBackboneMixin), ...bodies
+
*/
BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
-
constructor : function BaseModel
@__class__ = @constructor
@__superclass__ = @..__super__.constructor
get: (key) ->
_.getNested @attributes, key
+ # TODO: nested sets, handling events
+
# set: (key, value, opts) ->
# if _.isObject(key) and key?
# [values, opts] = [key, value]
# }}}
+
/**
* @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
+ # @trigger 'create', this
+
getIds: ->
@models.map -> it.id or it.get('id') or it.cid
-_ = require 'kraken/util/underscore'
-{ Field, FieldList, FieldView, Scaffold,
-} = require 'kraken/scaffold'
+{ _, op,
+} = require 'kraken/util'
+{ Parsers, ParserMixin, ParsingModel, ParsingView,
+} = require 'kraken/util/parser'
+{ BaseModel, BaseList,
+} = require 'kraken/base'
/**
/**
* @class Field with chart-option-specific handling for validation, parsing, tags, etc.
*/
-ChartOption = exports.ChartOption = Field.extend do # {{{
+ChartOption = exports.ChartOption = ParsingModel.extend do # {{{
IGNORED_TAGS : <[ callback deprecated debugging ]>
+ valueAttribute : 'value'
+
+ defaults: ->
+ name : ''
+ type : 'String'
+ default : null
+ desc : ''
+ include : 'diff'
+ tags : []
+ examples : []
+
constructor: function ChartOption
- Field ...
+ ParsingModel ...
initialize : ->
# console.log "#this.initialize!"
- Field::initialize ...
+
+ # Bind all the `parseXXX()` methods so they can be passed about independent from the class
+ _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse'))
+
+ ChartOption.__super__.initialize ...
+ @set 'id', @id = _.camelize @get 'name'
+ @set 'value', @get('default'), {+silent} if not @has 'value'
+
# Notify Tag indexer of category when created, to ensure all category-tags
# get indices with colors :P
@set 'ignore', true
+
+ ### Tag Handling
+
# Wrapper to ensure @set('tags') is called, as tags.push()
# will not trigger the 'changed:tags' event.
addTag: (tag) ->
@getTagIndex @getCategory()
+
+ /* * * Value Accessors * * */
+
+ getValue: (def) ->
+ @getParser() @get @valueAttribute, def
+
+ setValue: (v, options) ->
+ def = @get 'default'
+ if not v and def == null
+ val = null
+ else
+ val = @getParser()(v)
+ @set @valueAttribute, val, options
+
+ clearValue: ->
+ @set @valueAttribute, @get 'default'
+
+ isDefault: ->
+ @get(@valueAttribute) is @get 'default'
+
+
+
+ /* * * Serialization * * */
+
+ /**
+ * Override to default `type` to the model attribute of the same name.
+ * @returns {Function} Parser for the given type.
+ */
+ getParser: (type) ->
+ type or= @get('type') or 'String'
+ ChartOption.__super__.getParser.call this, type
+
+ serializeValue: ->
+ @serialize @getValue()
+
toJSON: ->
- o = Field::toJSON ...
- for k, v in o
- o[k] = '' if v!?
- o
+ {id:@id} import do
+ _.clone(@attributes) import { value:@getValue(), def:@get 'default' }
+
+ toKVPairs: ->
+ { "#{@id}":@serializeValue() }
+
+ toString: -> "(#{@id}: #{@serializeValue()})"
+
# }}}
/**
* @class List of ChartOption fields.
*/
-ChartOptionList = exports.ChartOptionList = FieldList.extend do # {{{
+ChartOptionList = exports.ChartOptionList = BaseList.extend do # {{{
model : ChartOption
constructor: function ChartOptionList
- FieldList ...
+ BaseList ...
+
+
+ /**
+ * Collects a map of fields to their values, excluding those set to `null` or their default.
+ *
+ * @param {Object} [opts={}] Options:
+ * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that
+ * haven't changed from their default value.
+ * @param {Boolean} [opts.serialize=false] If true, replace each value
+ * with its String version by calling `value.serializeValue()`.
+ * @returns {Object} Map of fields to their values.
+ */
+ values: (opts={}) ->
+ opts = {+keepDefaults, -serialize} import opts
+ _.synthesize do
+ if opts.keepDefaults then @models else @models.filter -> not it.isDefault()
+ -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ]
+
+ toJSON: ->
+ @values {+keepDefaults, -serialize}
/**
* Override to omit defaults from URL.
*/
toKVPairs: ->
- _.collapseObject @values {-keepDefaults, +serialize}
+ _.collapseObject @values {-keepDefaults, +serialize}
+
+ toKV: (item_delim='&', kv_delim='=') ->
+ _.toKV @toKVPairs(), item_delim, kv_delim
+ toURL: (item_delim='&', kv_delim='=') ->
+ "?#{@toKV ...}"
+
+
# }}}
-_ = require 'kraken/util/underscore'
-{ Field, FieldList, FieldView, Scaffold,
-} = require 'kraken/scaffold'
+{ _, op,
+} = require 'kraken/util'
+{ BaseView,
+} = require 'kraken/base'
{ ChartOption, ChartOptionList,
} = require 'kraken/chart/chart-option-model'
/**
* @class View for a single configurable option in a chart type.
*/
-ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
- # __bind__ : <[ onClick ]>
+ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{
tagName : 'section'
className : 'chart-option field'
template : require 'kraken/template/chart-option'
+ type : 'string'
isCollapsed : true
events :
- 'blur .value' : 'change'
- 'click input[type="checkbox"].value' : 'change'
- 'submit .value' : 'change'
+ 'blur .value' : 'onChange'
+ 'click input[type="checkbox"].value' : 'onChange'
+ 'submit .value' : 'onChange'
'click .close' : 'toggleCollapsed'
'click h3' : 'toggleCollapsed'
'click .collapsed' : 'onClick'
+
constructor: function ChartOptionView
- FieldView ...
+ BaseView ...
+
+ initialize: ->
+ ChartOptionView.__super__.initialize ...
+ @type = @model.get 'type' .toLowerCase() or 'string'
+
+
+ /* * * * Rendering * * * */
+ toTemplateLocals: ->
+ json = ChartOptionView.__super__.toTemplateLocals ...
+ json.id or= _.camelize json.name
+ json.value ?= ''
+ json.value = JSON.stringify v if v = json.value and (_.isArray(v) or _.isPlainObject(v))
+ json
+
+ /**
+ * Override to annotate with collapsed state and to kill off ignored options
+ * so they do not contribute their values when looking at form updates.
+ */
render: ->
- FieldView::render ...
- @$el.addClass 'ignore' if @get 'ignore'
+ return @remove() if @model.get 'ignore'
+ ChartOptionView.__super__.render ...
@$el.addClass 'collapsed' if @isCollapsed
this
+
+ /* * * * Option Collapsing * * * */
+
/**
* Sets the state of `isCollapsed` and updates the UI. If the state changed,
* a `'change:collapse`` event will be fired.`
@collapse not @$el.hasClass 'collapsed'
this
+
+
+ /* * * * Events * * * */
+
+ /**
+ * To prevent `toggleCollapsed()` from being called multiple times due to
+ * overlapping listeners, we're only looking for clicks on the collapsed header.
+ */
onClick: (evt) ->
target = $ evt.target
@toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close')
+ /**
+ * Propagate user input changes to the model, and upward to the parent view.
+ */
+ onChange: ->
+ if @type is 'boolean'
+ val = !! @$('.value').attr('checked')
+ else
+ val = @model.getParser() @$('.value').val()
+
+ current = @model.getValue()
+ return if _.isEqual val, current
+ console.log "#this.onChange( #current -> #val )"
+ @model.setValue val, {+silent}
+ @trigger 'change', @model, this
+ # false
+
# }}}
/**
* @class View for configuring a chart type.
*/
-ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
- __bind__ : <[ collapseAll expandAll ]>
+ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{
+ __bind__ : <[ addField ]>
tagName : 'form'
className : 'chart-options scaffold'
template : require 'kraken/template/chart-scaffold'
collectionType : ChartOptionList
subviewType : ChartOptionView
- fields : '.fields'
events:
'click .options-filter-button' : 'onFilterOptions'
'click .collapse-all-options-button' : 'collapseAll'
'click .expand-all-options-button' : 'expandAll'
- # GraphView will set this
- ready : false
constructor: function ChartOptionScaffold
- Scaffold ...
+ BaseView ...
initialize : ->
@render = _.debounce @render.bind(this), DEBOUNCE_RENDER
- Scaffold::initialize ...
+ CollectionType = @collectionType
+ @model = (@collection or= new CollectionType)
+ ChartOptionScaffold.__super__.initialize ...
+
+ @collection.on 'add', @addField, this
+ @collection.on 'reset', @onReset, this
+ @on 'render', @onRender, this
- render: ->
- console.log "#this.render(ready=#{@ready}) -> .isotope()"
- Scaffold::render ...
- return this unless @ready
+ /**
+ * Bookkeeping for new ChartOptions, creating it a new subview and subscribing
+ * to its activity, and then rendering it.
+ * @returns {ChartOptionView} The Option's new view.
+ */
+ addField: (field) ->
+ @removeSubview field.view if field.view
+
+ # avoid duplicating event propagation
+ field.off 'change:value', @onChange, this
+
+ # propagate value-change events as key-value change events
+ field.on 'change:value', @onChange, this
+
+ SubviewType = @subviewType
+ @addSubview view = new SubviewType model:field
+ .on 'change', @onChange.bind(this, field)
+ .on 'change:collapse', @render, this
+
+ @render() # WTF: hmm.
+ view
+
+
+ /* * * * UI * * * */
+
+ /**
+ * Collapse all expanded subviews.
+ * @returns {false} Returns false so event-dispatchers don't propagate
+ * the triggering event (usually a click or submit).
+ */
+ collapseAll: ->
+ _.invoke @subviews, 'collapse', true
+ false
+
+ /**
+ * Expand all collapsed subviews.
+ * @returns {false} Returns false so event-dispatchers don't propagate
+ * the triggering event (usually a click or submit).
+ */
+ expandAll: ->
+ _.invoke @subviews, 'collapse', false
+ false
+
+ /**
+ * Reflow Isotope post-`render()`.
+ */
+ onRender: ->
+ # console.log "#this.onRender(ready=#{@ready}) -> .isotope()"
- container = if @fields then @$ @fields else @$el
- container
- .addClass 'isotope'
- .find '.chart-option.field' .addClass 'isotope-item'
- container.isotope do
+ # The DOM doesn't calculate dimensions of elements that are not visible,
+ # which makes it impossible for Isotope to do its job.
+ return unless @$el.is ':visible'
+
+ # Invoke Isotope to re-layout the option elements
+ @$ '.isotope' .isotope do
# itemPositionDataEnabled : true
itemSelector : '.chart-option.field'
layoutMode : 'masonry'
getSortData :
category: ($el) ->
$el.data 'model' .getCategory()
- this
+ /**
+ * @returns {String} Selector representing the selected set of Option filters.
+ */
getOptionsFilter: ->
data = @$ '.options-filter-button.active' .toArray().map -> $ it .data()
sel = data.reduce do
- (sel, d) ->
- sel += that if d.filter
- sel
+ (sel, d) -> sel += if d.filter then that else ''
':not(.ignore)'
sel
- collapseAll: ->
- _.invoke @subviews, 'collapse', true
- # @renderSubviews()
- false
- expandAll: ->
- _.invoke @subviews, 'collapse', false
- # @renderSubviews()
- false
+ /* * * * Events * * * */
/**
- * Add a ChartOption to this scaffold, rerendering the isotope
- * layout after collapse events.
+ * Propagate change events from fields as if they were attribute changes.
+ * Note: `field` is bound to the handler
*/
- addField: (field) ->
- view = Scaffold::addField ...
- # view.on 'change:collapse render', @render, this
- view.on 'change:collapse', @render, this
- view
-
- toKV: ->
- @collection.toKV ...
+ onChange: (field) ->
+ key = field.get 'name'
+ value = field.getValue()
+ @trigger "change:#key", this, value, key, field
+ @trigger "change", this, value, key, field
+ this
+ onReset: ->
+ # The collection has been reset, assume all subviews are
+ # invalid and rebuild them.
+ @removeAllSubviews()
+ @collection.each @addField
+ _.defer @render
onFilterOptions: (evt) ->
evt.preventDefault()
+ # Defer re-rendering until after we yield for the DOM to do its thang
_.defer @render
+
+# Proxy collection methods
+<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]>
+ .forEach (methodname) ->
+ ChartOptionScaffold::[methodname] = -> @collection[methodname].apply @collection, arguments
+
+
# }}}
-_ = require 'kraken/util/underscore'
-{EventEmitter} = require 'events'
-{Parsers, ParserMixin} = require 'kraken/util/parser'
+{ EventEmitter,
+} = require 'events'
+
+{ _, op,
+} = require 'kraken/util'
+{ Parsers, ParserMixin,
+} = require 'kraken/util/parser'
+
/**
v = @spec[k]
@[k] = v if v?
@tags or= []
- @parse = @chartType.getParser @type
+ @parseValue = @chartType.getParser @type
- parse : Parsers.parseString
+ parseValue : Parsers.parseString
isDefault: (v) ->
# @default is v
* When implementing a ChartType, you can add or override parsers
* merely by subclassing.
*/
- this:: import ParserMixin::
+ ParserMixin this
getParserFor: (name) ->
@getParser @get(name).type
### Scaffold Models
Field = exports.Field = BaseModel.extend do # {{{
- idAttribute : 'name'
valueAttribute : 'value'
+ defaults: ->
+ name : ''
+ type : 'String'
+ default : null
+ desc : ''
+ include : 'diff'
+ tags : []
+ examples : []
+
+
constructor: function Field
BaseModel ...
initialize: ->
_.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse'))
+ @set 'id', @id = _.camelize @get 'name'
@set 'value', @get('default'), {+silent} if not @has 'value'
- # console.log "#this.initialize!"
- # @on 'all', (evt) ~> console.log "#this.trigger(#evt)"
+ Field.__super__.initialize ...
+
+
+
- # Model defaults
- defaults: ->
- {
- name : ''
- type : 'String'
- default : null
- desc : ''
- include : 'diff'
- tags : []
- examples : []
- }
-
-
- /* * * Parsers * * */
-
- getParser: (type) ->
- # XXX: handle 'or' by returning an array of parsers?
- type = _ (type or @get 'type').toLowerCase()
- for t of <[ Integer Float Boolean Object Array Function ]>
- if type.startsWith t.toLowerCase()
- return @["parse#t"]
- @parseString
-
- parseString: (v) ->
- if v? then op.toStr v else null
-
- parseInteger: (v) ->
- r = op.toInt v
- unless isNaN r then r else null
-
- parseFloat: (v) ->
- r = op.toFloat v
- unless isNaN r then r else null
-
- parseBoolean: (v) ->
- op.toBool v
-
- parseArray: (v) ->
- if v then op.toObject v else null
-
- parseObject: (v) ->
- if v then op.toObject v else null
-
- parseFunction: (fn) ->
- if fn and _.startswith String(fn), 'function'
- try eval "(#fn)" catch err then null
- else
- null
/* * * Value Accessors * * */
+
getValue: (def) ->
@getParser() @get @valueAttribute, def
@set @valueAttribute, val, options
clearValue: ->