} = 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!", metric
- @$el.find '.metrics .dataset-metric' .removeClass 'metric-active'
+ @$ '.metrics .dataset-metric' .removeClass 'metric-active'
view.$el.addClass 'metric-active'
view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height()
-{ _, op, CSVData,
+{ _, op,
} = require 'kraken/util'
+{ TimeSeriesData, CSVData,
+} = require 'kraken/timeseries'
{ BaseModel, BaseList, BaseView,
} = require 'kraken/base'
{ Metric, MetricList,
@constructor.register this
@metrics = new MetricList @attributes.metrics
@on 'change:metrics', @onMetricChange, this
- @load()
+ # @load()
canonicalize: (ds) ->
$.ajax do
url : url
dataType : 'json'
- success : @onLoadSuccess
+ success : (data) ~> @onLoadSuccess new TimeSeriesData data
error : @onLoadError
this
Cls = this
@register new Cls {id}
.on 'ready', -> cb.call cxt, null, it
+ .load()
_.bindAll DataSource, 'register', 'get', 'lookup'
template : require 'kraken/template/datasource-ui'
events :
- 'click .datasource-summary': 'onHeaderClick'
+ 'click .datasource-summary' : 'onHeaderClick'
+ 'click .datasource-source-metric' : 'onSelectMetric'
graph_id : null
dataset : null
onHeaderClick: ->
@$el.toggleClass 'in'
+ onSelectMetric: (evt) ->
+ tr = evt.currentTarget
+ idx = @$ '.source-metrics .datasource-source-metric' .toArray().indexOf tr
+ return unless idx is not -1
+
+
# }}}
className : 'metric-edit-ui'
template : require 'kraken/template/metric-edit'
+ callOnReturnKeypress : 'onChanged'
+ events:
+ 'keydown .metric-label' : 'onReturnKeypress'
+
graph_id : null
dataset : null
datasources : null
this import @options.{graph_id, dataset, datasources}
@model or= new Metric
BaseView::initialize ...
- @datasource_ui_view = @addSubview '.metric-datasource', new DataSourceUIView {@model, @graph_id, @dataset, @datasources}
- @$el.find '.metric-datasource' .append @datasource_ui_view.render().el
+ @datasource_ui_view = new DataSourceUIView {@model, @graph_id, @dataset, @datasources}
+ @addSubview @datasource_ui_view
+ .on 'update', ~> @trigger 'update', this
+ @$ '.metric-datasource' .append @datasource_ui_view.render().el
toTemplateLocals: ->
build: ->
BaseView::build ...
if @datasource_ui_view
- @$el.find '.metric-datasource' .append @datasource_ui_view.render().el
+ @$ '.metric-datasource' .append @datasource_ui_view.render().el
+ this
+
+ update: ->
+ color = @model.get 'color'
+ @$ '.color-swatch' .css 'background-color', color
+ @$ '.metric-color' .val color
+ @$ '.metric-label' .val @model.get 'label'
+
this
+ onChanged: ->
+ attrs = @$ 'form.metric-edit-form' .formData()
+ @model.set attrs, {+silent}
+ @trigger 'update', this
+
editMetric: (metric) ->
console.log "#this.editMetric!", metric
- @datasource_ui_view.model = @model = metric
+ @datasource_ui_view.setModel @setModel metric
@render()
@show()
this
@lookupSource()
- getDates: ->
- @source.getDates()
+ getDateColumn: ->
+ @source.getDateColumn()
getData: ->
@source.getColumn @get 'source_col'
return { width, height } unless @ready
# Remove old style, as it confuses dygraph after options update
- viewport = @$el.find '.viewport'
+ viewport = @$ '.viewport'
viewport.attr 'style', ''
- label = @$el.find '.graph-legend'
+ label = @$ '.graph-legend'
if width is 'auto'
vpWidth = viewport.innerWidth()
renderChart: ->
data = @model.get 'dataset' #.getData()
size = @resizeViewport()
- viewport = @$el.find '.viewport'
+ viewport = @$ '.viewport'
# XXX: use @model.changedAttributes() to calculate what to update
options = @chartOptions() #import size
options import do
- labelsDiv : @$el.find '.graph-legend' .0
+ labelsDiv : @$ '.graph-legend' .0
valueFormatter : @numberFormatterHTML
axes:
x:
update: ->
locals = @toTemplateLocals()
- @$el.find '.graph-name a' .text(locals.name or '')
- @$el.find '.graph-desc' .html jade.filters.markdown locals.desc or ''
+ @$ '.graph-name a' .text(locals.name or '')
+ @$ '.graph-desc' .html jade.filters.markdown locals.desc or ''
this
render: ->
# last action that happens. If we don't
# defer, the focusing click will
# unselect the text.
- _.defer( ~> @$el.find '.graph-permalink input' .select() )
+ _.defer( ~> @$ '.graph-permalink input' .select() )
# Needed because (sigh) _.debounce returns undefined
stopAndRender: ->
valueFormatter xValueFormatter yValueFormatter
]>
__bind__ : <[
- render renderAll stopAndRender stopAndRenderAll resizeViewport wait unwait checkWaiting
+ render stopAndRender resizeViewport wait unwait checkWaiting
numberFormatter numberFormatterHTML
onReady onSync onModelChange onScaffoldChange
onFirstClickRenderOptionsTab onFirstClickRenderDataTab
]>
- __debounce__: <[ render renderAll ]>
+ __debounce__: <[ render ]>
tagName : 'section'
className : 'graph-edit graph'
template : require 'kraken/template/graph-edit'
BaseView ...
initialize : (o={}) ->
- # @data = {}
@model or= new Graph
@id = @graph_id = _.domize 'graph', (@model.id or @model.get('slug') or @model.cid)
BaseView::initialize ...
.on 'ready', @onReady, this
### Chart Options Tab, Scaffold
- @scaffold = @addSubview '.graph-options-pane', new ChartOptionScaffold
- @$el.find '.graph-options-pane' .append @scaffold.el
+ @scaffold = @addSubview new ChartOptionScaffold
+ # @$ '.graph-options-pane' .append @scaffold.el
@scaffold.collection.reset that if o.graph_spec
@scaffold.on 'change', @onScaffoldChange
@chartOptions @model.getOptions(), {+silent}
- @$el.on 'click', '.graph-data-tab', @onFirstClickRenderDataTab
- # Rerender the options boxes once the tab is visible
+ # Rerender once the tab is visible
# Can't use @events because we need to bind before registering
- @$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab
+ @$el.on 'click', '.graph-data-tab', @onFirstClickRenderDataTab
+ @$el.on 'click', '.graph-options-tab', @onFirstClickRenderOptionsTab
### Graph Data UI
- @data = @addSubview '.graph-data-pane', new DataView { model:@model.get('data'), graph_id:@id }
- @$el.find '.graph-data-pane' .append @data.render().el
- @data
+ @data_view = @addSubview new DataView { model:@model.get('data'), graph_id:@id }
+ # @$ '.graph-data-pane' .append @data_view.render().el
+ @data_view
.on 'change', @onDataChange, this
.on 'start-waiting', @wait, this
.on 'stop-waiting', @unwait, this