} = require 'kraken/util'
{ BaseBackboneMixin, mixinBase,
} = require 'kraken/base/base-mixin'
+{ DataBinding,
+} = require 'kraken/base/data-binding'
*/
BaseView = exports.BaseView = Backbone.View.extend mixinBase do # {{{
tagName : 'section'
+ model : BaseModel
/**
- * The identifier for this view class.
- * By default, the class name is converted to underscore-case, and a
- * trailing '_view' suffix is dropped.
- * Example: "CamelCaseView" becomes "camel_case"
+ * Method-name called by `onReturnKeypress` when used as an event-handler.
* @type String
*/
- __view_type_id__: null
+ callOnReturnKeypress: null
+
+
+ /**
+ * Parent view of this view.
+ * @type BaseView
+ */
+ parent : null
/**
* Array of [view, selector]-pairs.
* Whether this view has been added to the DOM.
* @type Boolean
*/
- _parented: false
+ isAttached: false
@__superclass__ = @..__super__.constructor
@__view_type_id__ or= _.str.underscored @getClassName() .replace /_view$/, ''
@waitingOn = 0
- @subviews = []
+ @subviews = new ViewList
+ @onReturnKeypress = _.debounce @onReturnKeypress.bind(this), 50
Backbone.View ...
@trigger 'create', this
@$el.data { @model, view:this }
@model.on 'change', @render, this
@model.on 'destroy', @remove, this
- @model
+ @trigger 'change:model', this, model
+ model
### Subviews
- addSubview: (selector, view) ->
- [view, selector] = [selector, null] unless view
- @subviews.push [view, selector]
+ setParent: (parent) ->
+ [old_parent, @parent] = [@parent, parent]
+ @trigger 'parent', this, parent, old_parent
+ this
+
+ unsetParent: ->
+ [old_parent, @parent] = [@parent, null]
+ @trigger 'unparent', this, old_parent
+ this
+
+
+ addSubview: (view) ->
+ @removeSubview view
+ @subviews.push view
+ view.setParent this
view
removeSubview: (view) ->
- for [v, sel], idx of @subviews
- if v is view
- @subviews.splice(idx, 1)
- return [v, sel]
- null
+ if @hasSubview view
+ view.remove()
+ @subviews.remove view
+ view.unsetParent()
+ view
hasSubview: (view) ->
- _.any @subviews, ([v]) -> v is view
+ @subviews.contains view
invokeSubviews: ->
- _ _.pluck(@subviews, 0) .invoke ...arguments
+ @subviews.invoke ...arguments
- 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()
+ removeAllSubviews: ->
+ @subviews.forEach @removeSubview, this
+ @subviews = new ViewList
this
- removeAllSubviews: ->
- @invokeSubviews 'remove'
- _.pluck @subviews, 0 .forEach @removeSubview, this
+
+
+ ### UI Utilities
+
+ attach: (el) ->
+ # @undelegateEvents()
+ @$el.appendTo el
+ # only trigger the event the first time
+ return this if @isAttached
+ @isAttached = true
+ _.delay do
+ ~> # have to let DOM settle to ensure elements can be found
+ @delegateEvents()
+ @trigger 'attach', this
+ 50
this
- bubbleEvent: (evt) ->
- @invokeSubviews 'trigger', ...arguments
+ remove : ->
+ # @undelegateEvents()
+ @$el.remove()
+ return this unless @isAttached
+ @isAttached = false
+ @trigger 'unattach', this
+ this
+
+ clear : ->
+ @remove()
+ @model.destroy()
+ @trigger 'clear', this
+ this
+
+ hide : -> @$el.hide(); @trigger('hide', this); this
+ show : -> @$el.show(); @trigger('show', this); this
+
+ /**
+ * Attach each subview to its bind-point.
+ * @returns {this}
+ */
+ attachSubviews: ->
+ bps = @getOwnSubviewBindPoints()
+ if @subviews.length and not bps.length
+ console.warn "#this.attachSubviews(): no subview bind-points found!"
+ return this
+ for view of @subviews
+ if bp = @findSubviewBindPoint view, bps
+ view.attach bp
+ else
+ console.warn "#this.attachSubviews(): Unable to find bind-point for #view!"
this
+ /**
+ * Finds all subview bind-points under this view's element, but not under
+ * the view element of any subview.
+ * @returns {jQuery|undefined}
+ */
+ getOwnSubviewBindPoints: ->
+ @$ '[data-subview]' .not @$ '[data-subview] [data-subview]'
+
+ /**
+ * Find the matching subview bind-point for the given view.
+ */
+ findSubviewBindPoint: (view, bind_points) ->
+ bind_points or= @getOwnSubviewBindPoints()
+
+ # check if any bindpoint specifies this subview by id
+ if view.id
+ bp = bind_points.filter "[data-subview$=':#{view.id}']"
+ return bp.eq 0 if bp.length
+
+ # Find all elements that specify this type as the subview type
+ bp = bind_points.filter "[data-subview='#{view.getClassName()}']"
+ return bp.eq 0 if bp.length
+
+
### Rendering Chain
toTemplateLocals: ->
- json = {value:v} = @model.toJSON()
- if _.isArray(v) or _.isObject(v)
- json.value = JSON.stringify v
- json
+ @model.toJSON()
- $template: (locals={}) ->
- $ @template do
- { $, _, op, @model, view:this } import @toTemplateLocals() import locals
+ $template: ->
+ $ @template { _, op, @model, view:this, ...@toTemplateLocals() }
build: ->
return this unless @template
@$el.html outer.html()
.attr do
id : outer.attr 'id'
- class : outer.attr('class')
+ class : outer.attr 'class'
@attachSubviews()
+ @isBuilt = true
this
render: ->
- @build()
+ if @isBuilt
+ @update()
+ else
+ @build()
+ @renderSubviews()
@trigger 'render', this
this
renderSubviews: ->
- _.invoke _.pluck(@subviews, 0), 'render'
+ @attachSubviews()
+ @subviews.invoke 'render'
this
-
- attach: (el) ->
- @$el.appendTo el
- # only trigger the event the first time
- return this if @_parented
- @_parented = true
- _.delay do
- ~> # have to let DOM settle to ensure elements can be found
- @delegateEvents()
- @trigger 'parent', this
- 50
+ update: ->
+ new DataBinding this .update @toTemplateLocals()
+ @trigger 'update', this
this
- remove : ->
- @undelegateEvents()
- @$el.remove()
- return this unless @_parented
- @_parented = false
- @trigger 'unparent', this
- this
+ /* * * * Events * * * */
- ### UI Utilities
+ bubbleEvent: (evt) ->
+ @invokeSubviews 'trigger', ...arguments
+ this
- hide : -> @$el.hide(); @trigger('hide', this); this
- show : -> @$el.show(); @trigger('show', this); this
- clear : -> @model.destroy(); @trigger('clear', this); @remove()
+ redispatch: (evt) ->
+ @trigger ...arguments
+ this
+ onlyOnReturn: (fn, ...args) ->
+ fn = _.debounce fn.bind(this), 50
+ (evt) ~> fn.apply this, args if evt.keyCode is 13
- # 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
+ /**
+ * Call a delegate on keypress == the return key.
+ * @returns {Function} Keypress event handler.
+ */
+ onReturnKeypress: (evt) ->
+ fn = this[@callOnReturnKeypress] if @callOnReturnKeypress
+ fn.call this if fn and evt.keyCode is 13
toString : ->
"#{@getClassName()}(model=#{@model})"
# }}}
+
+
+class exports.ViewList extends Array
+
+ (views=[]) ->
+ super ...
+
+ extend: (views) ->
+ _.each views, ~> @push it
+ this
+
+ findByModel: (model) ->
+ @find -> it.model is model
+
+ toString: ->
+ contents = if @length then "\"#{@join '","'}\"" else ''
+ "ViewList[#{@length}](#contents)"
+
+
+<[ each contains invoke pluck find remove compact flatten without union intersection difference unique uniq ]>
+ .forEach (methodname) ->
+ ViewList::[methodname] = -> _[methodname].call _, this, ...arguments
+
+
-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'
-exports import mixins import models import views import cache import cascading
+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'
+exports import mixins import models import views import cache import cascading import data_binding
KNOWN_TAGS.update @getCategory()
# Ignore functions/callbacks and, ahem, hidden tags.
- type = @get 'type', '' .toLowerCase()
- tags = @get 'tags', []
+ type = @get 'type' .toLowerCase() or ''
+ tags = @get('tags') or []
if _.str.include(type, 'function') or _.intersection(tags, @IGNORED_TAGS).length
@set 'ignore', true
# will not trigger the 'changed:tags' event.
addTag: (tag) ->
return this unless tag
- tags = @get('tags', [])
+ tags = @get('tags') or []
tags.push tag
@set 'tags', tags
this
# will not trigger the 'changed:tags' event.
removeTag: (tag) ->
return this unless tag
- tags = @get('tags', [])
+ tags = @get('tags') or []
_.remove tags, tag
@set 'tags', tags
this
# A field's category is its first tag.
getCategory: ->
- @get('tags', [])[0]
+ tags = (@get('tags') or [])[0]
getCategoryIndex: ->
@getTagIndex @getCategory()
* @class List of ChartOption fields.
*/
ChartOptionList = exports.ChartOptionList = FieldList.extend do # {{{
- model : ChartOption
+ model : ChartOption
constructor: function ChartOptionList
isCollapsed : true
events :
- 'blur .value' : 'update'
- 'click input[type="checkbox"].value' : 'update'
- 'submit .value' : 'update'
+ 'blur .value' : 'change'
+ 'click input[type="checkbox"].value' : 'change'
+ 'submit .value' : 'change'
'click .close' : 'toggleCollapsed'
'click h3' : 'toggleCollapsed'
'click .collapsed' : 'onClick'
render: ->
FieldView::render ...
+ @$el.addClass 'ignore' if @get 'ignore'
@$el.addClass 'collapsed' if @isCollapsed
this
render: ->
console.log "#this.render(ready=#{@ready}) -> .isotope()"
- # Scaffold::render ...
+ Scaffold::render ...
return this unless @ready
- container = if @fields then @$el.find @fields else @$el
+
+ container = if @fields then @$ @fields else @$el
container
.addClass 'isotope'
.find '.chart-option.field' .addClass 'isotope-item'
this
getOptionsFilter: ->
- data = @$el.find '.options-filter-button.active' .toArray().map -> $ it .data()
+ data = @$ '.options-filter-button.active' .toArray().map -> $ it .data()
sel = data.reduce do
(sel, d) ->
sel += that if d.filter
sel
- ''
+ ':not(.ignore)'
sel
collapseAll: ->
- _.invoke @_subviews, 'collapse', true
+ _.invoke @subviews, 'collapse', true
# @renderSubviews()
false
expandAll: ->
- _.invoke @_subviews, 'collapse', false
+ _.invoke @subviews, 'collapse', false
# @renderSubviews()
false
* Add a ChartOption to this scaffold, rerendering the isotope
* layout after collapse events.
*/
- addOne: (field) ->
- view = Scaffold::addOne ...
- view.on 'change:collapse render', @render, this
+ addField: (field) ->
+ view = Scaffold::addField ...
+ # view.on 'change:collapse render', @render, this
+ view.on 'change:collapse', @render, this
view
toKV: ->
@trigger 'ready', this
attachGraphs: ->
- graphs_el = @$el.find '#graphs'
+ graphs_el = @$ '#graphs'
for id of @graph_ids
break unless graph = @graphs.get id
continue if graph.view.isAttached
Seq = require 'seq'
{ _, op,
} = require 'kraken/util'
-{ BaseView,
+{ BaseView, ViewList,
} = require 'kraken/base'
{ DataSetView,
} = require 'kraken/dataset/dataset-view'
className : 'data-ui'
template : require 'kraken/template/data'
- data : {}
datasources : null
initialize: ->
@graph_id = @options.graph_id
BaseView::initialize ...
+ @metric_views = new ViewList
@datasources = @model.sources
+ @model.metrics
+ .on 'add', @addMetric, this
+ .on 'remove', @removeMetric, this
@on 'ready', @onReady, this
@load()
onReady: ->
dataset = @model
- @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset, @datasources}
- @metric_edit_view
- .on 'update', @onUpdateMetric, this
+ @model.metrics.each @addMetric, this
+ # @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset, @datasources}
+ # @metric_edit_view
+ # .on 'update', @onUpdateMetric, this
- @dataset_view = @addSubview new DataSetView {@model, @graph_id, dataset, @datasources}
- @dataset_view
+ @dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources}
+ @addSubview @dataset_view
.on 'add-metric', @onMetricsChanged, this
.on 'remove-metric', @onMetricsChanged, this
.on 'edit-metric', @editMetric, this
- @attachSubviews()
+ @render()
this
load: ->
@wait()
- $.getJSON '/datasources/all', (@data) ~>
- _.each @data, @canonicalizeDataSource, this
- @model.sources.reset _.map @data, -> it
- @ready = true
- @unwait()
- @render()
- @trigger 'ready', this
+ # $.getJSON '/datasources/all', (@data) ~>
+ # _.each @data, @canonicalizeDataSource, this
+ # @model.sources.reset _.map @data, -> it
+ @ready = true
+ @unwait()
+ # @render()
+ @trigger 'ready', this
/**
* Transform the `columns` field to ensure an Array of {label, type} objects.
toTemplateLocals: ->
attrs = _.clone @model.attributes
- { $, _, op, @model, view:this, @graph_id, @datasources } import attrs
+ { @graph_id, @datasources } import attrs
- # attachSubviews: ->
- # @$el.empty()
- # BaseView::attachSubviews ...
- # @$el.append '<div class="clearer"/>'
+ # Don't rebuild HTML, simply notify subviews
+ # render: ->
+ # @renderSubviews()
+ # @trigger 'render', this
# this
- # Don't rebuild HTML, simply notify subviews
- render: ->
- @renderSubviews()
- @trigger 'render', this
- this
+ addMetric: (metric) ->
+ console.log "#this.addMetric!", metric
+ return metric if @metric_views.findByModel metric
+ view = new MetricEditView {model:metric, @graph_id, dataset, @datasources}
+ @metric_views.push @addSubview view
+ metric
+
+ removeMetric: (metric) ->
+ console.log "#this.removeMetric!", metric
+ return unless view = @metric_views.findByModel metric
+ @metric_views.remove view
+ @removeSubview view
+ metric
editMetric: (metric) ->
- @metric_edit_view.editMetric metric
+ console.log "#this.editMetric!", metric
+ @metric_views.invoke 'hide'
+ @metric_edit_view = @metric_views.findByModel metric
+ @metric_edit_view?.show()
@onMetricsChanged()
onMetricsChanged: ->
newMinHeight = Math.max do
oldMinHeight
@dataset_view.$el.height()
- @metric_edit_view.$el.height()
+ @metric_edit_view?.$el.height()
# console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight
@$el.css 'min-height', newMinHeight
view = @addSubview new DataSetMetricView {model:metric, @graph_id}
@views_by_cid[metric.cid] = view
- @$el.find '.metrics' .append view.render().el
+ @$ '.metrics' .append view.render().el
# @render()
@trigger 'add-metric', metric, view, this
this
editMetric: (metric) ->
- console.log "#this.editMetric!", metric
+ # console.log "#this.editMetric!", metric
if metric instanceof [jQuery.Event, Event]
metric = $ metric.currentTarget .data 'model'
view = @active_view = @views_by_cid[metric.cid]
- console.log ' --> metric:', metric, 'view:', view
+ console.log "#this.editMetric!",