From 5b7a7ed499b4c5ff01406e98043e28b2e5e5cff6 Mon Sep 17 00:00:00 2001 From: dsc Date: Tue, 28 Feb 2012 15:48:03 -0800 Subject: [PATCH] Renames model & view files to avoid having a ton named 'model.co' etc --- lib/graph/graph-model.co | 152 +++++++++++++++++++++++++ lib/graph/graph-view.co | 240 ++++++++++++++++++++++++++++++++++++++++ lib/graph/index.co | 4 +- lib/graph/model.co | 152 ------------------------- lib/graph/view.co | 240 ---------------------------------------- lib/scaffold/index.co | 4 +- lib/scaffold/model.co | 136 ----------------------- lib/scaffold/scaffold-model.co | 136 +++++++++++++++++++++++ lib/scaffold/scaffold-view.co | 117 +++++++++++++++++++ lib/scaffold/view.co | 117 ------------------- www/modules.yaml | 8 +- 11 files changed, 653 insertions(+), 653 deletions(-) create mode 100644 lib/graph/graph-model.co create mode 100644 lib/graph/graph-view.co delete mode 100644 lib/graph/model.co delete mode 100644 lib/graph/view.co delete mode 100644 lib/scaffold/model.co create mode 100644 lib/scaffold/scaffold-model.co create mode 100644 lib/scaffold/scaffold-view.co delete mode 100644 lib/scaffold/view.co diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co new file mode 100644 index 0000000..53fa6a9 --- /dev/null +++ b/lib/graph/graph-model.co @@ -0,0 +1,152 @@ +_ = require 'kraken/underscore' +{ BaseModel, BaseView, +} = require 'kraken/base' +{ Field, FieldList, FieldView, Scaffold +} = require 'kraken/scaffold' + + + +class exports.TagSet extends Array + tags : {} + + (values=[]) -> + @tags = {} + @add values if values?.length + + has: (tag) -> + @tags[tag]? + + get: (tag) -> + return -1 unless tag + unless @tags[tag]? + @tags[tag] = @length + @push tag + @tags[tag] + + update: (tags) -> + is_single = typeof tags is 'string' + tags = [tags] if is_single + indices = ( for tag of tags then @get tag ) + if is_single then indices[0] else indices + + toString: -> "TagSet(length=#{@length}, values=[\"#{@join '", "'}\"])" + + +KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() + + +/** + * Field with graph-option-specific handling for validation, parsing, tags, etc. + */ +GraphOption = exports.GraphOption = Field.extend do # {{{ + ctorName : 'GraphOption' + + initialize : -> + # console.log "#this.initialize!" + Field::initialize ... + + # Notify Tag indexer of category when created, to ensure all category-tags + # get indices with colors :P + KNOWN_TAGS.update @getCategory() + + + # Wrapper to ensure @set('tags') is called, as tags.push() + # will not trigger the 'changed:tags' event. + addTag: (tag) -> + return this unless tag + tags = @get('tags', []) + tags.push tag + @set 'tags', tags + this + + # Wrapper to ensure @set('tags') is called, as tags.push() + # will not trigger the 'changed:tags' event. + removeTag: (tag) -> + return this unless tag + tags = @get('tags', []) + _.remove tags, tag + @set 'tags', tags + this + + # Keep tag list up to date + onTagUpdate: -> + KNOWN_TAGS.update @get 'tags' + this + + getTagIndex: (tag) -> + KNOWN_TAGS.get tag + + # A field's category is its first tag. + getCategory: -> + @get('tags', [])[0] + + getCategoryIndex: -> + @getTagIndex @getCategory() + + + toJSON: -> + o = Field::toJSON ... + for k, v in o + o[k] = '' if v!? + o +# }}} + + +GraphOptionList = exports.GraphOptionList = FieldList.extend do # {{{ + ctorName : 'GraphOptionList' + model : GraphOption +# }}} + + +/** + * Represents a Graph, including its charting options, dataset, annotations, and all + * other settings for both its content and presentation. + */ +GraphModel = exports.GraphModel = BaseModel.extend do # {{{ + ctorName : 'GraphModel' + urlRoot : '/graphs' + idAttribute : 'slug' + + + initialize : -> + name = @get 'name' + if name and not (@id or @get('slug')) + @set 'slug', _.underscored name + + defaults: -> + { + slug : '' + name : '' + dataset : '/data/pageviews_by.timestamp.language.csv' + desc : '' + width : 'auto' + height : 320 + options : {} + } + + + hasOption: (key) -> + options = @get 'options', {} + options[key]? + + getOption: (key, def) -> + @get('options', {})[key] ? def + + setOption: (key, value, opts={}) -> + options = @get 'options', {} + options[key] = value + @set 'options', options, opts + @trigger "change:options:#key", this, value, key, opts unless opts.silent + + unsetOption: (key, opts={}) -> + options = @get 'options', {} + delete options[key] + @set 'options', options, opts + @trigger "change:options:#key", this, value, key, opts unless opts.silent + + + toString: -> "#{@ctorName}(id=#{@id}, name=#{@get 'name'}, dataset=#{@get 'dataset'})" + +# }}} + + diff --git a/lib/graph/graph-view.co b/lib/graph/graph-view.co new file mode 100644 index 0000000..d507f8e --- /dev/null +++ b/lib/graph/graph-view.co @@ -0,0 +1,240 @@ +_ = require 'kraken/underscore' +{BaseView} = require 'kraken/base' +{ Field, FieldList, FieldView, Scaffold +} = require 'kraken/scaffold' +{ GraphModel, GraphOption, GraphOptionList, TagSet, +} = require 'kraken/graph/graph-model' + +root = do -> this +DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms + + +/** + * The view for a single configurable option. + */ +GraphOptionView = exports.GraphOptionView = FieldView.extend do # {{{ + # __bind__ : <[ onClick ]> + ctorName : 'GraphOptionView' + tagName : 'div' + className : 'field option' + template : require 'kraken/template/graph-option' + + isCollapsed : true + + events : + 'blur .value' : 'update' + 'click input[type="checkbox"].value' : 'update' + 'submit .value' : 'update' + 'click .close' : 'toggleCollapsed' + 'click h3' : 'toggleCollapsed' + 'click .collapsed' : 'onClick' + + + render: -> + @__super__.render ... + @$el.addClass 'collapsed' if @isCollapsed + 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 + +# }}} + + + +GraphOptionsScaffold = exports.GraphOptionsScaffold = Scaffold.extend do # {{{ + ctorName : 'GraphOptionsScaffold' + tagName : 'form' + className : 'options scaffold' + collectionType : GraphOptionList + subviewType : GraphOptionView + + # GraphView will set this + ready : false + + + + initialize : -> + @render = _.debounce @render.bind(this), DEBOUNCE_RENDER + Scaffold::initialize ... + + render: -> + # console.log "#this.render() -> .isotope()" + @__super__.render ... + return this unless @ready + @$el.isotope do + itemSelector : '.field.option' + layoutMode : 'masonry' + masonry : columnWidth : 10 + # itemPositionDataEnabled : true + + /** + * Add a GraphOption to this scaffold, rerendering the isotope + * layout after collapse events. + */ + addOne: (field) -> + view = @__super__.addOne ... + view.on 'change:collapse render', @render + view + + toKVPairs: -> + @collection.toKVPairs ... + +# }}} + + + + +GraphView = exports.GraphView = BaseView.extend do # {{{ + __bind__ : <[ resizeViewport render renderAll onReady ]> + ctorName : 'GraphView' + tagName : 'section' + className : 'graph' + template : require 'kraken/template/graph' + + events: + 'keypress form.options .value' : 'onKeypress' + 'submit form.options' : 'onSubmit' + + ready: false + + + + initialize : (o={}) -> + @model or= new GraphModel + BaseView::initialize ... + # console.log "#this.initialize!" + + for name of <[ resizeViewport render renderAll ]> + @[name] = _.debounce @[name], DEBOUNCE_RENDER + + # Resize graph on window resize + $ root .on 'resize', @resizeViewport + + @model.on 'destroy', @remove, this + @model.on 'change', @render, this + @model.on 'change:options', ~> + changes = @model.changedAttributes() + # console.log 'Model.changed(options) ->', changes + @chartOptions changes, {+silent} + + @build() + @viewport = @$el.find '.viewport' + + @scaffold = new GraphOptionsScaffold + @$el.find 'fieldset' .append @scaffold.el + @scaffold.collection.reset that if o.graph_spec + + @scaffold.on 'change', (scaffold, value, key, field) ~> + # console.log "scaffold.change!", value, key, field + @model.setOption key, value, {+silent} unless field.isDefault() + + options = @model.get 'options', {} + @chartOptions options, {+silent} + + _.delay @onReady, DEBOUNCE_RENDER + + + onReady: -> + @ready = @scaffold.ready = true + @renderAll() + + + chartOptions: (values, opts) -> + # Handle @chartOptions(k, v, opts) + if arguments.length > 1 and typeof values is 'string' + [k, v, opts] = arguments + values = { "#k": v } + + options = @scaffold.collection + if values + for k, v in values + options.get(k)?.setValue v, opts + this + else + options.values() + + + /** + * Resizes chart according to the model's width and height. + * @return { width, height } + */ + resizeViewport: -> + return this unless @ready and @chart + + # Remove old style, as it confuses dygraph after options update + @viewport.attr 'style', '' + + if (width = @model.get 'width') is 'auto' + width = @viewport.width() + if (height = @model.get 'height') is 'auto' + height = @viewport.height() + size = { width, height } + @viewport.css size + console.log 'resizeViewport!', JSON.stringify(size), @viewport + # @chart.resize size if forceRedraw + size + + render: -> + return this unless @ready + + # console.log "#this" + # console.log do + # " .viewport.{ width=%s, height=%s, style=%s }" + # @viewport.css('width') + # @viewport.css('height') + # @viewport.attr 'style' + # console.log ' .options:', JSON.stringify options + + size = @resizeViewport() + options = @chartOptions() + # @chart?.destroy() + unless @chart + @chart = new Dygraph do + @viewport.0 + @model.get 'dataset' + options + else + @chart.updateOptions options + @chart.resize size + + path = root.location?.path or '/' + url = "#path?#{@toKVPairs()}" + # console.log 'History.pushState', url + History.pushState url, @model.get('name', root.document?.title or ''), url + + this + + renderAll: -> + return this unless @ready + _.invoke @scaffold.subviews, 'render' + @scaffold.render() + @render() + this + + onKeypress: (evt) -> + $(evt.target).submit() if evt.keyCode is 13 + + onSubmit: -> + # console.log "#this.onSubmit!" + @render() + false + + toKVPairs: -> + @model.toKVPairs.apply @model, arguments + + toString: -> "#{@ctorName}(#{@model})" +# }}} + + + diff --git a/lib/graph/index.co b/lib/graph/index.co index 881b673..cd7b8be 100644 --- a/lib/graph/index.co +++ b/lib/graph/index.co @@ -1,3 +1,3 @@ -models = require 'kraken/graph/model' -views = require 'kraken/graph/view' +models = require 'kraken/graph/graph-model' +views = require 'kraken/graph/graph-view' exports import models import views diff --git a/lib/graph/model.co b/lib/graph/model.co deleted file mode 100644 index 53fa6a9..0000000 --- a/lib/graph/model.co +++ /dev/null @@ -1,152 +0,0 @@ -_ = require 'kraken/underscore' -{ BaseModel, BaseView, -} = require 'kraken/base' -{ Field, FieldList, FieldView, Scaffold -} = require 'kraken/scaffold' - - - -class exports.TagSet extends Array - tags : {} - - (values=[]) -> - @tags = {} - @add values if values?.length - - has: (tag) -> - @tags[tag]? - - get: (tag) -> - return -1 unless tag - unless @tags[tag]? - @tags[tag] = @length - @push tag - @tags[tag] - - update: (tags) -> - is_single = typeof tags is 'string' - tags = [tags] if is_single - indices = ( for tag of tags then @get tag ) - if is_single then indices[0] else indices - - toString: -> "TagSet(length=#{@length}, values=[\"#{@join '", "'}\"])" - - -KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet() - - -/** - * Field with graph-option-specific handling for validation, parsing, tags, etc. - */ -GraphOption = exports.GraphOption = Field.extend do # {{{ - ctorName : 'GraphOption' - - initialize : -> - # console.log "#this.initialize!" - Field::initialize ... - - # Notify Tag indexer of category when created, to ensure all category-tags - # get indices with colors :P - KNOWN_TAGS.update @getCategory() - - - # Wrapper to ensure @set('tags') is called, as tags.push() - # will not trigger the 'changed:tags' event. - addTag: (tag) -> - return this unless tag - tags = @get('tags', []) - tags.push tag - @set 'tags', tags - this - - # Wrapper to ensure @set('tags') is called, as tags.push() - # will not trigger the 'changed:tags' event. - removeTag: (tag) -> - return this unless tag - tags = @get('tags', []) - _.remove tags, tag - @set 'tags', tags - this - - # Keep tag list up to date - onTagUpdate: -> - KNOWN_TAGS.update @get 'tags' - this - - getTagIndex: (tag) -> - KNOWN_TAGS.get tag - - # A field's category is its first tag. - getCategory: -> - @get('tags', [])[0] - - getCategoryIndex: -> - @getTagIndex @getCategory() - - - toJSON: -> - o = Field::toJSON ... - for k, v in o - o[k] = '' if v!? - o -# }}} - - -GraphOptionList = exports.GraphOptionList = FieldList.extend do # {{{ - ctorName : 'GraphOptionList' - model : GraphOption -# }}} - - -/** - * Represents a Graph, including its charting options, dataset, annotations, and all - * other settings for both its content and presentation. - */ -GraphModel = exports.GraphModel = BaseModel.extend do # {{{ - ctorName : 'GraphModel' - urlRoot : '/graphs' - idAttribute : 'slug' - - - initialize : -> - name = @get 'name' - if name and not (@id or @get('slug')) - @set 'slug', _.underscored name - - defaults: -> - { - slug : '' - name : '' - dataset : '/data/pageviews_by.timestamp.language.csv' - desc : '' - width : 'auto' - height : 320 - options : {} - } - - - hasOption: (key) -> - options = @get 'options', {} - options[key]? - - getOption: (key, def) -> - @get('options', {})[key] ? def - - setOption: (key, value, opts={}) -> - options = @get 'options', {} - options[key] = value - @set 'options', options, opts - @trigger "change:options:#key", this, value, key, opts unless opts.silent - - unsetOption: (key, opts={}) -> - options = @get 'options', {} - delete options[key] - @set 'options', options, opts - @trigger "change:options:#key", this, value, key, opts unless opts.silent - - - toString: -> "#{@ctorName}(id=#{@id}, name=#{@get 'name'}, dataset=#{@get 'dataset'})" - -# }}} - - diff --git a/lib/graph/view.co b/lib/graph/view.co deleted file mode 100644 index 0e32474..0000000 --- a/lib/graph/view.co +++ /dev/null @@ -1,240 +0,0 @@ -_ = require 'kraken/underscore' -{BaseView} = require 'kraken/base' -{ Field, FieldList, FieldView, Scaffold -} = require 'kraken/scaffold' -{ GraphModel, GraphOption, GraphOptionList, TagSet, -} = require 'kraken/graph/model' - -root = do -> this -DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms - - -/** - * The view for a single configurable option. - */ -GraphOptionView = exports.GraphOptionView = FieldView.extend do # {{{ - # __bind__ : <[ onClick ]> - ctorName : 'GraphOptionView' - tagName : 'div' - className : 'field option' - template : require 'kraken/template/graph-option' - - isCollapsed : true - - events : - 'blur .value' : 'update' - 'click input[type="checkbox"].value' : 'update' - 'submit .value' : 'update' - 'click .close' : 'toggleCollapsed' - 'click h3' : 'toggleCollapsed' - 'click .collapsed' : 'onClick' - - - render: -> - @__super__.render ... - @$el.addClass 'collapsed' if @isCollapsed - 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 - -# }}} - - - -GraphOptionsScaffold = exports.GraphOptionsScaffold = Scaffold.extend do # {{{ - ctorName : 'GraphOptionsScaffold' - tagName : 'form' - className : 'options scaffold' - collectionType : GraphOptionList - subviewType : GraphOptionView - - # GraphView will set this - ready : false - - - - initialize : -> - @render = _.debounce @render.bind(this), DEBOUNCE_RENDER - Scaffold::initialize ... - - render: -> - # console.log "#this.render() -> .isotope()" - @__super__.render ... - return this unless @ready - @$el.isotope do - itemSelector : '.field.option' - layoutMode : 'masonry' - masonry : columnWidth : 10 - # itemPositionDataEnabled : true - - /** - * Add a GraphOption to this scaffold, rerendering the isotope - * layout after collapse events. - */ - addOne: (field) -> - view = @__super__.addOne ... - view.on 'change:collapse render', @render - view - - toKVPairs: -> - @collection.toKVPairs ... - -# }}} - - - - -GraphView = exports.GraphView = BaseView.extend do # {{{ - __bind__ : <[ resizeViewport render renderAll onReady ]> - ctorName : 'GraphView' - tagName : 'section' - className : 'graph' - template : require 'kraken/template/graph' - - events: - 'keypress form.options .value' : 'onKeypress' - 'submit form.options' : 'onSubmit' - - ready: false - - - - initialize : (o={}) -> - @model or= new GraphModel - BaseView::initialize ... - # console.log "#this.initialize!" - - for name of <[ resizeViewport render renderAll ]> - @[name] = _.debounce @[name], DEBOUNCE_RENDER - - # Resize graph on window resize - $ root .on 'resize', @resizeViewport - - @model.on 'destroy', @remove, this - @model.on 'change', @render, this - @model.on 'change:options', ~> - changes = @model.changedAttributes() - # console.log 'Model.changed(options) ->', changes - @chartOptions changes, {+silent} - - @build() - @viewport = @$el.find '.viewport' - - @scaffold = new GraphOptionsScaffold - @$el.find 'fieldset' .append @scaffold.el - @scaffold.collection.reset that if o.graph_spec - - @scaffold.on 'change', (scaffold, value, key, field) ~> - # console.log "scaffold.change!", value, key, field - @model.setOption key, value, {+silent} unless field.isDefault() - - options = @model.get 'options', {} - @chartOptions options, {+silent} - - _.delay @onReady, DEBOUNCE_RENDER - - - onReady: -> - @ready = @scaffold.ready = true - @renderAll() - - - chartOptions: (values, opts) -> - # Handle @chartOptions(k, v, opts) - if arguments.length > 1 and typeof values is 'string' - [k, v, opts] = arguments - values = { "#k": v } - - options = @scaffold.collection - if values - for k, v in values - options.get(k)?.setValue v, opts - this - else - options.values() - - - /** - * Resizes chart according to the model's width and height. - * @return { width, height } - */ - resizeViewport: -> - return this unless @ready and @chart - - # Remove old style, as it confuses dygraph after options update - @viewport.attr 'style', '' - - if (width = @model.get 'width') is 'auto' - width = @viewport.width() - if (height = @model.get 'height') is 'auto' - height = @viewport.height() - size = { width, height } - @viewport.css size - console.log 'resizeViewport!', JSON.stringify size - # @chart.resize size if forceRedraw - size - - render: -> - return this unless @ready - - # console.log "#this" - # console.log do - # " .viewport.{ width=%s, height=%s, style=%s }" - # @viewport.css('width') - # @viewport.css('height') - # @viewport.attr 'style' - # console.log ' .options:', JSON.stringify options - - size = @resizeViewport() - options = @chartOptions() - # @chart?.destroy() - unless @chart - @chart = new Dygraph do - @viewport.0 - @model.get 'dataset' - options - else - @chart.updateOptions options - @chart.resize size - - path = root.location?.path or '/' - url = "#path?#{@toKVPairs()}" - # console.log 'History.pushState', url - History.pushState url, @model.get('name', root.document?.title or ''), url - - this - - renderAll: -> - return this unless @ready - _.invoke @scaffold.subviews, 'render' - @scaffold.render() - @render() - this - - onKeypress: (evt) -> - $(evt.target).submit() if evt.keyCode is 13 - - onSubmit: -> - # console.log "#this.onSubmit!" - @render() - false - - toKVPairs: -> - @model.toKVPairs.apply @model, arguments - - toString: -> "#{@ctorName}(#{@model})" -# }}} - - - diff --git a/lib/scaffold/index.co b/lib/scaffold/index.co index 03f7aa7..d183e5f 100644 --- a/lib/scaffold/index.co +++ b/lib/scaffold/index.co @@ -1,3 +1,3 @@ -models = require 'kraken/scaffold/model' -views = require 'kraken/scaffold/view' +models = require 'kraken/scaffold/scaffold-model' +views = require 'kraken/scaffold/scaffold-view' exports import models import views diff --git a/lib/scaffold/model.co b/lib/scaffold/model.co deleted file mode 100644 index 5049c87..0000000 --- a/lib/scaffold/model.co +++ /dev/null @@ -1,136 +0,0 @@ -_ = require 'kraken/underscore' -op = require 'kraken/util/op' -{BaseModel, BaseList} = require 'kraken/base' - - - -### Scaffold Models - -Field = exports.Field = BaseModel.extend do # {{{ - ctorName : 'Field' - idAttribute : 'name' - valueAttribute : 'value' - - - initialize: -> - @set 'value', @get('default'), {+silent} if not @has 'value' - # console.log "#this.initialize!" - # @on 'all', (evt) ~> console.log "#this.trigger(#evt)" - - # 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() - # console.log "parse#t ->", @["parse#t"] unless @["parse#t"] - 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 - else - null - - - /* * * 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') - - - /* * * Serializers * * */ - serializeValue: -> - v = @getValue() - if v!? - v = '' - else if _.isBoolean v - v = Number v - else if _.isArray(v) or _.isObject(v) - v = JSON.stringify v - String v - - toJSON: -> - {id:@id} import do - _.clone(@attributes) import { value:@getValue(), def:@get('default') } - - toKVObject: -> - { "#{@id}": @serializeValue() } - - toString: -> "(#{@id}: #{@serializeValue()})" -# }}} - - -FieldList = exports.FieldList = BaseList.extend do # {{{ - ctorName : 'FieldList' - model : Field - - /** - * Collects a map of fields to their values, excluding those set to `null` or their default. - * @returns {Object} - */ - values: (keepDefaults=false) -> - _.synthesize do - if keepDefaults then @models else @models.filter -> not it.isDefault() - -> [ it.get('name'), it.getValue() ] - - toJSON: -> - @values() - - toKVObject: -> - _.collapseObject @toJSON() - - toKVPairs: (item_delim='&', kv_delim='=') -> - _.toKVPairs @toKVObject(), item_delim, kv_delim - - toString: -> "#{@ctorName}(length=#{@length})" -# }}} - diff --git a/lib/scaffold/scaffold-model.co b/lib/scaffold/scaffold-model.co new file mode 100644 index 0000000..5049c87 --- /dev/null +++ b/lib/scaffold/scaffold-model.co @@ -0,0 +1,136 @@ +_ = require 'kraken/underscore' +op = require 'kraken/util/op' +{BaseModel, BaseList} = require 'kraken/base' + + + +### Scaffold Models + +Field = exports.Field = BaseModel.extend do # {{{ + ctorName : 'Field' + idAttribute : 'name' + valueAttribute : 'value' + + + initialize: -> + @set 'value', @get('default'), {+silent} if not @has 'value' + # console.log "#this.initialize!" + # @on 'all', (evt) ~> console.log "#this.trigger(#evt)" + + # 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() + # console.log "parse#t ->", @["parse#t"] unless @["parse#t"] + 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 + else + null + + + /* * * 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') + + + /* * * Serializers * * */ + serializeValue: -> + v = @getValue() + if v!? + v = '' + else if _.isBoolean v + v = Number v + else if _.isArray(v) or _.isObject(v) + v = JSON.stringify v + String v + + toJSON: -> + {id:@id} import do + _.clone(@attributes) import { value:@getValue(), def:@get('default') } + + toKVObject: -> + { "#{@id}": @serializeValue() } + + toString: -> "(#{@id}: #{@serializeValue()})" +# }}} + + +FieldList = exports.FieldList = BaseList.extend do # {{{ + ctorName : 'FieldList' + model : Field + + /** + * Collects a map of fields to their values, excluding those set to `null` or their default. + * @returns {Object} + */ + values: (keepDefaults=false) -> + _.synthesize do + if keepDefaults then @models else @models.filter -> not it.isDefault() + -> [ it.get('name'), it.getValue() ] + + toJSON: -> + @values() + + toKVObject: -> + _.collapseObject @toJSON() + + toKVPairs: (item_delim='&', kv_delim='=') -> + _.toKVPairs @toKVObject(), item_delim, kv_delim + + toString: -> "#{@ctorName}(length=#{@length})" +# }}} + diff --git a/lib/scaffold/scaffold-view.co b/lib/scaffold/scaffold-view.co new file mode 100644 index 0000000..12a46dd --- /dev/null +++ b/lib/scaffold/scaffold-view.co @@ -0,0 +1,117 @@ +_ = require 'kraken/underscore' +op = require 'kraken/util/op' +{BaseView} = require 'kraken/base' +{ Field, FieldList, +} = require 'kraken/scaffold/scaffold-model' + + +FieldView = exports.FieldView = BaseView.extend do # {{{ + tagName : 'div' + className : 'field' + + events : + 'blur .value' : 'update' + 'submit .value' : 'update' + + + # initialize: -> + # # console.log "#this.initialize!" + # BaseView::initialize ... + + update: -> + val = @model.getParser() @$el.find('.value').val() + current = @model.getValue() + return if _.isEqual val, current + console.log "#this.update( #current -> #val )" + @model.setValue val, {+silent} + @trigger 'update', this + + render: -> + return @remove() if @model.get 'hidden', false + return BaseView::render ... if @template + + name = @model.get 'name' + id = _.camelize name + label = name + value = @model.get 'value' + value = '' if value!? + + @$el.html """ + + + """ + + this + +# }}} + + +# There are several special options that, if passed, will be attached directly to the view: +# model, collection, el, id, className, tagName, attributes + +Scaffold = exports.Scaffold = BaseView.extend do # {{{ + __bind__ : <[ addOne addAll ]> + tagName : 'form' + className : 'scaffold' + + collectionType : FieldList + subviewType : FieldView + + + initialize: -> + @subviews = [] + CollectionType = @collectionType + @model = @collection or= new CollectionType + + BaseView::initialize ... + + @collection.on 'add', @addOne + @collection.on 'reset', @addAll + + @$el.data { model:@collection, view:this } .addClass @className + + + addOne: (field) -> + # console.log "[S] #this.addOne!", @__super__ + _.remove @subviews, field.view if field.view + + # avoid duplicating event propagation + field.off 'change:value', @change, this + + # propagate value-change events as key-value change events + field.on 'change:value', @change, this + + SubviewType = @subviewType + view = new SubviewType model:field + @subviews.push view + @$el.append view.render().el unless field.get 'hidden' + view.on 'update', @change.bind(this, field) + + @render() + view + + addAll: -> + # _.invoke @subviews, 'remove' + # @subviews = [] + @collection.each @addOne + this + + change: (field) -> + key = field.get 'name' + value = field.getValue() + @trigger "change:#key", this, value, key, field + @trigger "change", this, value, key, field + this + + + toString: -> "#{@ctorName}(collection=#{@collection})" + + + +# Proxy collection methods +<[ get at pluck invoke values toJSON toKVObject toKVPairs ]> + .forEach (methodname) -> + Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments + +# }}} + diff --git a/lib/scaffold/view.co b/lib/scaffold/view.co deleted file mode 100644 index b16cceb..0000000 --- a/lib/scaffold/view.co +++ /dev/null @@ -1,117 +0,0 @@ -_ = require 'kraken/underscore' -op = require 'kraken/util/op' -{BaseView} = require 'kraken/base' -{ Field, FieldList, -} = require 'kraken/scaffold/model' - - -FieldView = exports.FieldView = BaseView.extend do # {{{ - tagName : 'div' - className : 'field' - - events : - 'blur .value' : 'update' - 'submit .value' : 'update' - - - # initialize: -> - # # console.log "#this.initialize!" - # BaseView::initialize ... - - update: -> - val = @model.getParser() @$el.find('.value').val() - current = @model.getValue() - return if _.isEqual val, current - console.log "#this.update( #current -> #val )" - @model.setValue val, {+silent} - @trigger 'update', this - - render: -> - return @remove() if @model.get 'hidden', false - return BaseView::render ... if @template - - name = @model.get 'name' - id = _.camelize name - label = name - value = @model.get 'value' - value = '' if value!? - - @$el.html """ - - - """ - - this - -# }}} - - -# There are several special options that, if passed, will be attached directly to the view: -# model, collection, el, id, className, tagName, attributes - -Scaffold = exports.Scaffold = BaseView.extend do # {{{ - __bind__ : <[ addOne addAll ]> - tagName : 'form' - className : 'scaffold' - - collectionType : FieldList - subviewType : FieldView - - - initialize: -> - @subviews = [] - CollectionType = @collectionType - @model = @collection or= new CollectionType - - BaseView::initialize ... - - @collection.on 'add', @addOne - @collection.on 'reset', @addAll - - @$el.data { model:@collection, view:this } .addClass @className - - - addOne: (field) -> - # console.log "[S] #this.addOne!", @__super__ - _.remove @subviews, field.view if field.view - - # avoid duplicating event propagation - field.off 'change:value', @change, this - - # propagate value-change events as key-value change events - field.on 'change:value', @change, this - - SubviewType = @subviewType - view = new SubviewType model:field - @subviews.push view - @$el.append view.render().el unless field.get 'hidden' - view.on 'update', @change.bind(this, field) - - @render() - view - - addAll: -> - # _.invoke @subviews, 'remove' - # @subviews = [] - @collection.each @addOne - this - - change: (field) -> - key = field.get 'name' - value = field.getValue() - @trigger "change:#key", this, value, key, field - @trigger "change", this, value, key, field - this - - - toString: -> "#{@ctorName}(collection=#{@collection})" - - - -# Proxy collection methods -<[ get at pluck invoke values toJSON toKVObject toKVPairs ]> - .forEach (methodname) -> - Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments - -# }}} - diff --git a/www/modules.yaml b/www/modules.yaml index 4a56d7d..a35ef90 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -40,12 +40,12 @@ all: - graph.jade - graph-option.jade - scaffold: - - model - - view + - scaffold-model + - scaffold-view - index - graph: - - model - - view + - graph-model + - graph-view - index -- 1.7.0.4