From: dsc Date: Fri, 11 May 2012 18:00:01 +0000 (-0700) Subject: Updates ParserMixin, working on simplifying Scaffolding. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=397b4a43923df26486e70938ead5b8a544c845f8;p=limn.git Updates ParserMixin, working on simplifying Scaffolding. --- diff --git a/lib/base/base-mixin.co b/lib/base/base-mixin.co index 0b081a7..29a8581 100644 --- a/lib/base/base-mixin.co +++ b/lib/base/base-mixin.co @@ -7,7 +7,6 @@ Backbone = require 'backbone' BaseBackboneMixin = exports.BaseBackboneMixin = - initialize: -> @__apply_bind__() @@ -142,7 +141,76 @@ BaseBackboneMixin = exports.BaseBackboneMixin = "#{@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 + diff --git a/lib/base/base-model.co b/lib/base/base-model.co index 9723f78..5048a1d 100644 --- a/lib/base/base-model.co +++ b/lib/base/base-model.co @@ -13,7 +13,6 @@ Backbone = require 'backbone' */ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ - constructor : function BaseModel @__class__ = @constructor @__superclass__ = @..__super__.constructor @@ -32,6 +31,8 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{ get: (key) -> _.getNested @attributes, key + # TODO: nested sets, handling events + # set: (key, value, opts) -> # if _.isObject(key) and key? # [values, opts] = [key, value] @@ -189,19 +190,20 @@ BaseModel import do # }}} + /** * @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 diff --git a/lib/chart/chart-option-model.co b/lib/chart/chart-option-model.co index cdba717..2a3c37a 100644 --- a/lib/chart/chart-option-model.co +++ b/lib/chart/chart-option-model.co @@ -1,6 +1,9 @@ -_ = 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' /** @@ -43,16 +46,34 @@ KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() /** * @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 @@ -65,6 +86,9 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ @set 'ignore', true + + ### Tag Handling + # Wrapper to ensure @set('tags') is called, as tags.push() # will not trigger the 'changed:tags' event. addTag: (tag) -> @@ -99,11 +123,50 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ @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()})" + # }}} @@ -111,18 +174,45 @@ ChartOption = exports.ChartOption = Field.extend do # {{{ /** * @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 ...}" + + # }}} diff --git a/lib/chart/chart-option-view.co b/lib/chart/chart-option-view.co index 86d86e0..5a6f84f 100644 --- a/lib/chart/chart-option-view.co +++ b/lib/chart/chart-option-view.co @@ -1,6 +1,7 @@ -_ = 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' @@ -10,33 +11,55 @@ DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms /** * @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.` @@ -65,10 +88,34 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{ @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 + # }}} @@ -77,44 +124,92 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{ /** * @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' @@ -124,46 +219,51 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{ 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 + + # }}} diff --git a/lib/chart/chart-type.co b/lib/chart/chart-type.co index 637477b..0831a29 100644 --- a/lib/chart/chart-type.co +++ b/lib/chart/chart-type.co @@ -1,6 +1,11 @@ -_ = 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' + /** @@ -24,9 +29,9 @@ class exports.ChartTypeOption 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 @@ -119,7 +124,7 @@ class exports.ChartType extends EventEmitter * When implementing a ChartType, you can add or override parsers * merely by subclassing. */ - this:: import ParserMixin:: + ParserMixin this getParserFor: (name) -> @getParser @get(name).type diff --git a/lib/scaffold/scaffold-model.co b/lib/scaffold/scaffold-model.co index ee302ab..2800782 100644 --- a/lib/scaffold/scaffold-model.co +++ b/lib/scaffold/scaffold-model.co @@ -8,70 +8,35 @@ op = require 'kraken/util/op' ### 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 @@ -84,32 +49,35 @@ Field = exports.Field = BaseModel.extend do # {{{ @set @valueAttribute, val, options clearValue: -> - @set @valueAttribute, @get('default') + @set @valueAttribute, @get 'default' isDefault: -> - @get(@valueAttribute) is @get('default') + @get(@valueAttribute) is @get 'default' /* * * Serializers * * */ + serializeValue: -> @serialize @getValue() toJSON: -> {id:@id} import do - _.clone(@attributes) import { value:@getValue(), def:@get('default') } + _.clone(@attributes) import { value:@getValue(), def:@get 'default' } toKVPairs: -> { "#{@id}":@serializeValue() } toString: -> "(#{@id}: #{@serializeValue()})" + # }}} FieldList = exports.FieldList = BaseList.extend do # {{{ - model : Field + model : Field + constructor: function FieldList + BaseList ... - constructor: function FieldList then BaseList ... /** * Collects a map of fields to their values, excluding those set to `null` or their default. diff --git a/lib/scaffold/scaffold-view.co b/lib/scaffold/scaffold-view.co index 5690062..2a44f93 100644 --- a/lib/scaffold/scaffold-view.co +++ b/lib/scaffold/scaffold-view.co @@ -13,8 +13,8 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ type : 'string' events : - 'blur .value' : 'change' - 'submit .value' : 'change' + 'blur .value' : 'onChange' + 'submit .value' : 'onChange' constructor: function FieldView @@ -23,15 +23,9 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ initialize: -> # console.log "#this.initialize!" BaseView::initialize ... - @type = @model.get('type').toLowerCase() or 'string' + @type = @model.get 'type' .toLowerCase() or 'string' - toTemplateLocals: -> - json = {value:v} = @model.toJSON() - if _.isArray(v) or _.isPlainObject(v) - json.value = JSON.stringify v - json - - change: -> + onChange: -> if @type is 'boolean' val = !! @$('.value').attr('checked') else @@ -39,26 +33,29 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ current = @model.getValue() return if _.isEqual val, current - console.log "#this.change( #current -> #val )" + console.log "#this.onChange( #current -> #val )" @model.setValue val, {+silent} @trigger 'change', this - render: -> - return @remove() if @model.get 'ignore', false - return BaseView::render ... if @template - - name = @model.get 'name' - id = _.camelize name - label = name - value = @model.get 'value' - value = '' if value!? - - @$el.html """ - - + toTemplateLocals: -> + json = FieldView.__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 + + /** + * A ghetto default template, typically overridden by superclass. + */ + template: (locals) -> + $ """ + + """ - - this + + render: -> + return @remove() if @model.get 'ignore' + FieldView.__super__.render ... # }}} @@ -75,8 +72,8 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ subviewType : FieldView + constructor: function Scaffold - @_subviews = [] BaseView ... initialize: -> @@ -86,22 +83,21 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ @collection.on 'add', @addField, this @collection.on 'reset', @resetFields, this - - @$el.data { model:@collection, view:this } .addClass @className + addField: (field) -> @removeSubview field.view if field.view # avoid duplicating event propagation - field.off 'change:value', @change, this + field.off 'change:value', @onChange, this # propagate value-change events as key-value change events - field.on 'change:value', @change, this + field.on 'change:value', @onChange, this SubviewType = @subviewType view = @addSubview new SubviewType model:field - view.on 'change', @change.bind(this, field) + view.on 'change', @onChange.bind(this, field) @render() view @@ -111,7 +107,7 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ @collection.each @addField this - change: (field) -> + onChange: (field) -> key = field.get 'name' value = field.getValue() @trigger "change:#key", this, value, key, field @@ -120,8 +116,6 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ - - # Proxy collection methods <[ get at pluck invoke values toJSON toKVPairs toKV toURL ]> .forEach (methodname) -> diff --git a/lib/template/chart-option.jade b/lib/template/chart-option.jade index 5fc5084..97353f6 100644 --- a/lib/template/chart-option.jade +++ b/lib/template/chart-option.jade @@ -8,7 +8,7 @@ include browser-helpers - var tags_cls = tags.map(_.domize('tag')).join(' ') -section.chart-option.field(id=option_id, class="#{category_cls} #{tags_cls}") +section.chart-option.field.isotope-item(id=option_id, class="#{category_cls} #{tags_cls}") a.close(title="Click to collapse") × h3.shortname(title="Click to collapse") #{name} @@ -17,11 +17,11 @@ section.chart-option.field(id=option_id, class="#{category_cls} #{tags_cls}") label.name(for=value_id) #{name} if ( /object|array|function/i.test(type) ) - textarea.value(id=value_id, name=name, class=type_cls) #{value} + textarea.value(id=value_id, name=name, class=type_cls, data-bind="value") #{value} else - var input_type = (/boolean/i.test(type) ? 'checkbox' : 'text'); - var checked = ((/boolean/i.test(type) && value) ? 'checked' : null); - input.value(type=input_type, id=value_id, name=name, class=type_cls, value=value, checked=checked) + input.value(type=input_type, id=value_id, name=name, class=type_cls, value=value, checked=checked, data-bind="value") .type(class=type_cls) #{type} .default(class=type_cls, title="Default: #{def} (#{type})") #{def} diff --git a/lib/template/chart-scaffold.jade b/lib/template/chart-scaffold.jade index 7b7c895..1f49b82 100644 --- a/lib/template/chart-scaffold.jade +++ b/lib/template/chart-scaffold.jade @@ -7,7 +7,6 @@ form.chart-options.scaffold.form-inline .std-adv-filter-buttons.btn-group.pull-right(data-toggle="buttons-radio") a.standard-filter-button.options-filter-button.btn.active(href="#", data-filter=".tag_standard") Standard a.advanced-filter-button.options-filter-button.btn(href="#", data-filter="") Advanced - - .fields(data-subview="ChartOptionView") + .fields.isotope(data-subview="ChartOptionView") diff --git a/lib/timeseries/csv.co b/lib/timeseries/csv.co index ec87318..84ef3a3 100644 --- a/lib/timeseries/csv.co +++ b/lib/timeseries/csv.co @@ -53,7 +53,7 @@ class CSVData extends TimeSeriesData * @private * @returns {this} */ - parse: (@rawData) -> + parseData: (@rawData) -> return this if typeof rawData is not 'string' o = @options diff --git a/lib/timeseries/timeseries.co b/lib/timeseries/timeseries.co index f9ff9cb..2eae2a1 100644 --- a/lib/timeseries/timeseries.co +++ b/lib/timeseries/timeseries.co @@ -31,7 +31,7 @@ class TimeSeriesData @labels = @options.labels or [] @types = @options.types or [] - @parse that if data or @options.data + @parseData that if data or @options.data @rebuildDerived() @@ -75,7 +75,7 @@ class TimeSeriesData * Subclass and override to perform preprocessing of the data. * @private */ - parse : (rawData) -> + parseData : (rawData) -> this /** diff --git a/lib/util/backbone.co b/lib/util/backbone.co index f87fda5..e8a0daf 100644 --- a/lib/util/backbone.co +++ b/lib/util/backbone.co @@ -41,6 +41,12 @@ _bb_events = */ _backbone = do + # /** + # * Mix the given object or prototype into this class + # */ + # addMixin: (mixin) -> + # ... + /** * @returns {Array} The list of all superclasses for this class or object. */ diff --git a/lib/util/op.co b/lib/util/op.co index 83477f0..2fb5172 100644 --- a/lib/util/op.co +++ b/lib/util/op.co @@ -1,3 +1,5 @@ +DASH_PATTERN = /-/g + STRIP_PAT = /(^\s*|\s*$)/g strip = (s) -> if s then s.replace STRIP_PAT, '' else s @@ -62,12 +64,20 @@ module.exports = op = toInt : (v) -> parseInt v toFloat : (v) -> parseFloat v toStr : (v) -> String v - toObject : (v) -> + toRegExp : (v) -> new RegExp v + + toObject : (v) -> if typeof v is 'string' and strip(v) JSON.parse v else v + toDate : (v) -> + return v if v!? or v instanceof Date + return new Date v if typeof v is 'number' + return new Date String(v).replace DASH_PATTERN, '/' + + ### comparison cmp : (x,y) -> if x < y then -1 else (if x > y then 1 else 0) eq : (x,y) -> x == y diff --git a/lib/util/parser.co b/lib/util/parser.co index abcb0ea..42c7c80 100644 --- a/lib/util/parser.co +++ b/lib/util/parser.co @@ -1,5 +1,7 @@ _ = require 'kraken/util/underscore' op = require 'kraken/util/op' +{ BaseModel, BaseList, BaseView, Mixin, +} = require 'kraken/base' /** @@ -21,6 +23,12 @@ Parsers = exports.Parsers = parseString: (v) -> if v? then op.toStr v else null + parseDate: (v) -> + if v then op.toDate v else null + + parseRegExp: (v) -> + if v then op.toRegExp v else null + parseArray: (v) -> if v then op.toObject v else null @@ -34,12 +42,33 @@ Parsers = exports.Parsers = null +# Aliases +Parsers.parseNumber = Parsers.parseFloat + -class exports.ParserMixin +/** + * @class Methods for a class to select parsers by type reflection. + * @mixin + */ +class exports.ParserMixin extends Mixin this:: import Parsers + (target) -> + return Mixin.call ParserMixin, target + - parse: (v, type) -> + # XXX: So I'm meh about mixing in the Parsers dictionary. + # + # - Pros: mixing in `parseXXX()` methods makes it easy to + # override in the target class. + # - Cons: `parse()` is a Backbone method, which bit me once + # already, so conflicts aren't unlikely. + # + # Other ideas: + # - Parsers live at `@__parsers__`, and each instance gets its own clone + # - Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?) + + parseValue: (v, type) -> @getParser(type)(v) getParser: (type='String') -> @@ -50,7 +79,7 @@ class exports.ParserMixin # Handle compound/optional types # XXX: handle 'or' by returning an array of parsers? type = _ String(type).toLowerCase() - for t of <[ Integer Float Boolean Object Array Function ]> + for t of <[ Integer Float Number Boolean Object Array Function ]> if type.startsWith t.toLowerCase() return @["parse#t"] @defaultParser or @parseString @@ -66,6 +95,33 @@ class exports.ParserMixin else @getParser 'Object' - + + + +/** + * @class Basic model which mixes in the ParserMixin. + * @extends BaseModel + * @borrows ParserMixin + */ +ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin do + constructor: function ParsingModel then BaseModel ... + + +/** + * @class Basic collection which mixes in the ParserMixin. + * @extends BaseList + * @borrows ParserMixin + */ +ParsingList = exports.ParsingList = BaseList.extend ParserMixin do + constructor: function ParsingList then BaseList ... + + +/** + * @class Basic view which mixes in the ParserMixin. + * @extends BaseView + * @borrows ParserMixin + */ +ParsingView = exports.ParsingView = BaseView.extend ParserMixin do + constructor: function ParsingView then BaseView ...