-/**
- * @class Base model, extending Backbone.Model, used by scaffold and others.
- * @extends Backbone.Model
- */
-BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{
- ctorName : 'BaseModel'
+BaseBackboneMixin = exports.BaseBackboneMixin =
- # A list of method-names to bind on initialize; set this on a subclass to override.
+ initialize: ->
+ @__apply_bind__()
+
+
+ ### Auto-Bound methods
+
+ /**
+ * A list of method-names to bind on `initialize`; set this on a subclass to override.
+ * @type Array<String>
+ */
__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
- initialize: ->
- _.bindAll this, ...@__bind__ if @__bind__.length
+
### Accessors
#
+
+
+
### Serialization
serialize: (v) ->
toURL: ->
"?#{@toKV ...}"
- toString: -> "#{@ctorName}(id=#{@id})"
+ 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}.
* @class Base collection, extending Backbone.Collection, used by scaffold and others.
* @extends Backbone.Collection
*/
-BaseList = exports.BaseList = Backbone.Collection.extend do # {{{
- ctorName : 'BaseList'
-
- # A list of method-names to bind on initialize; set this on a subclass to override.
- __bind__ : []
-
+BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{
constructor : function BaseList
@__class__ = @constructor
@__superclass__ = @..__super__.constructor
+ @waitingOn = 0
Backbone.Collection ...
@trigger 'create', this
- initialize : ->
- _.bindAll this, ...@__bind__ if @__bind__.length
+ ### Serialization
toKVPairs: ->
_.collapseObject @toJSON()
toURL: (item_delim='&', kv_delim='=') ->
"?#{@toKV ...}"
- toString: -> "#{@ctorName}(length=#{@length})"
+ 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 do # {{{
- ctorName : 'BaseView'
-
- /**
- * A list of method-names to bind on initialize; set this on a subclass to override.
- * @type Array<String>
- */
- __bind__ : []
+BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
+ tagName : 'section'
/**
- * @type Array<BaseView>
+ * 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: ->
- _.bindAll this, ...@__bind__ if @__bind__.length
+ @__apply_bind__()
@setModel @model
@build()
@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
- { $, _, op, @model, view:this } import json
+ json
$template: (locals={}) ->
- $ @template @toTemplateLocals() import 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: ->
@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
# @$el.appendTo parent if parent?.length
# this
- toString : -> "#{@ctorName}(model=#{@model})"
+ toString : -> "#{@..name or @..displayName}(model=#{@model})"
# Proxy model methods
* @class Field with chart-option-specific handling for validation, parsing, tags, etc.
*/
ChartOption = exports.ChartOption = Field.extend do # {{{
- ctorName : 'ChartOption'
IGNORED_TAGS : <[ callback deprecated debugging ]>
* @class List of ChartOption fields.
*/
ChartOptionList = exports.ChartOptionList = FieldList.extend do # {{{
- ctorName : 'ChartOptionList'
model : ChartOption
*/
ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
# __bind__ : <[ onClick ]>
- ctorName : 'ChartOptionView'
tagName : 'div'
className : 'field option'
template : require 'kraken/template/chart-option'
@$el.addClass 'collapsed' if @isCollapsed
this
+
+ /**
+ * Sets the state of `isCollapsed` and updates the UI. If the state changed,
+ * a `'change:collapse`` event will be fired.`
+ *
+ * @param {Boolean} [makeCollapsed=true] If true, set state to collapsed.
+ * @returns {Boolean} Whether the state changed.
+ */
+ collapse: (state=true) ->
+ state = !! state
+ @isCollapsed = @$el.hasClass 'collapsed'
+
+ return this if state is @isCollapsed
+ if state
+ @$el.addClass 'collapsed'
+ else
+ @$el.removeClass 'collapsed'
+ @isCollapsed = state
+ @trigger 'change:collapse', this, @isCollapsed
+ true
+
+ /**
+ * Toggles the collapsed state, updating the UI and firing a `'change:collapse'` event.
+ * @returns {this}
+ */
+ toggleCollapsed: ->
+ @collapse not @$el.hasClass 'collapsed'
+ this
+
onClick: (evt) ->
target = $ evt.target
- # console.log "#this.onClick()", target
@toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close')
- toggleCollapsed: ->
- starting = @$el.hasClass 'collapsed' #@isCollapsed
- @$el.toggleClass 'collapsed'
- @isCollapsed = not starting
- # console.log "#this.toggleCollapsed!", starting, '->', @isCollapsed
- @trigger 'change:collapse', this, @isCollapsed
- this
# }}}
* @class View for configuring a chart type.
*/
ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
- ctorName : 'ChartOptionScaffold'
+ __bind__ : <[ collapseAll expandAll ]>
tagName : 'form'
className : '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
+ ready : false
+
constructor: function ChartOptionScaffold
@render = _.debounce @render.bind(this), DEBOUNCE_RENDER
Scaffold::initialize ...
+
render: ->
- # console.log "#this.render() -> .isotope()"
+ console.log "#this.render(ready=#{@ready}) -> .isotope()"
# Scaffold::render ...
return this unless @ready
container = if @fields then @$el.find @fields else @$el
.find '.field.option' .addClass 'isotope-item'
container.isotope do
# itemPositionDataEnabled : true
- itemSelector : '.field.option'
- layoutMode : 'masonry'
- masonry : columnWidth : 10
- getSortData :
+ itemSelector : '.field.option'
+ layoutMode : 'masonry'
+ masonry : { columnWidth:10 }
+ filter : @getOptionsFilter()
+ sortBy : 'category'
+ getSortData :
category: ($el) ->
$el.data 'model' .getCategory()
- sortBy: 'category'
+ this
+
+ getOptionsFilter: ->
+ data = @$el.find '.options-filter-button.active' .toArray().map -> $ it .data()
+ sel = data.reduce do
+ (sel, d) ->
+ sel += that if d.filter
+ sel
+ ''
+ sel
+
+ collapseAll: ->
+ _.invoke @_subviews, 'collapse', true
+ # @renderSubviews()
+ false
+
+ expandAll: ->
+ _.invoke @_subviews, 'collapse', false
+ # @renderSubviews()
+ false
+
/**
* Add a ChartOption to this scaffold, rerendering the isotope
*/
addOne: (field) ->
view = Scaffold::addOne ...
- view.on 'change:collapse render', @render
+ view.on 'change:collapse render', @render, this
view
toKV: ->
@collection.toKV ...
+
+ onFilterOptions: (evt) ->
+ evt.preventDefault()
+ _.defer @render
+
# }}}
* @class
*/
DataView = exports.DataView = BaseView.extend do # {{{
- ctorName : 'DataView'
+ __bind__ : <[ onReady ]>
tagName : 'section'
className : 'data-ui'
template : require 'kraken/template/data'
- data : {}
+ datasources : {}
initialize: ->
@graph_id = @options.graph_id
BaseView::initialize ...
-
+ @on 'ready', @onReady
@load()
-
- # @subviews.push @dataset_view = new DataSetView {@model, @graph_id}
- # @$el.append @dataset_view.render().el
- # @dataset_view.on 'edit-metric', @editMetric, this
- #
- # @subviews.push @metric_edit_view = new MetricEditView {dataset:@model, @graph_id}
- # @$el.append @metric_edit_view.render().hide().el
-
-
- toTemplateLocals: ->
- attrs = _.clone @model.attributes
- { $, _, op, @model, view:this, @data, @graph_id } import attrs
load: ->
- $.getJSON '/datasources/all', (@data) ~>
+ $.getJSON '/datasources/all', (@datasources) ~>
+ @canonicalizeDataSources @datasources
@ready = true
@render()
@trigger 'ready', this
+ /**
+ * Transform the `columns` field to ensure an Array of {label, type} objects.
+ */
+ canonicalizeDataSources: (datasources) ->
+ _.each datasources, (ds) ->
+ ds.shortName or= ds.name
+ ds.title or= ds.name
+ ds.subtitle or= ''
+
+ cols = ds.columns
+ if _.isArray cols
+ ds.metrics = _.map cols, (col, idx) ->
+ if _.isArray col
+ [label, type] = col
+ {idx, label, type or 'int'}
+ else
+ col
+ else
+ ds.metrics = _.map cols.labels, (label, idx) ->
+ {idx, label, type:cols.types[idx] or 'int'}
+ datasources
+
+
+ onReady: ->
+ @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset:@model, @datasources}
+ @metric_edit_view
+ .on 'update', @onUpdateMetric, this
+
+ @dataset_view = @addSubview new DataSetView {@model, @graph_id, @dataset, @datasources}
+ @dataset_view
+ .on 'add-metric', @onMetricsChanged, this
+ .on 'remove-metric', @onMetricsChanged, this
+ .on 'edit-metric', @editMetric, this
+
+ @attachSubviews()
+ this
+
+
+ toTemplateLocals: ->
+ attrs = _.clone @model.attributes
+ { $, _, op, @model, view:this, @graph_id, @datasources, } import attrs
+
+ # attachSubviews: ->
+ # @$el.empty()
+ # BaseView::attachSubviews ...
+ # @$el.append '<div class="clearer"/>'
+ # this
+
# Don't rebuild HTML, simply notify subviews
render: ->
- BaseView::render ...
-
- &n