Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

added a lot of style related classes

  • Loading branch information...
commit 05c328b397309ad5fff90281d18fa3306212d84a 1 parent 9906b82
@collin authored
Showing with 2,201 additions and 595 deletions.
  1. +1 −1  .rvmrc
  2. +3 −1 Gemfile
  3. +3 −0  Gemfile.lock
  4. +1 −0  Procfile.build
  5. +5 −2 alpha_simprini.erb
  6. +64 −0 autobuild.watchr
  7. +1 −1  examples/todo/todo.coffee
  8. +3 −1 src/alpha_simprini.coffee
  9. +5 −3 src/alpha_simprini/client.coffee
  10. +117 −43 src/alpha_simprini/client/application.coffee
  11. +1 −1  src/alpha_simprini/client/binding.coffee
  12. +30 −22 src/alpha_simprini/client/binding/edit_line.coffee
  13. +27 −0 src/alpha_simprini/client/binding/if.coffee
  14. +3 −1 src/alpha_simprini/client/binding/input.coffee
  15. +10 −6 src/alpha_simprini/client/binding/many.coffee
  16. +8 −8 src/alpha_simprini/client/binding/model.coffee
  17. +5 −3 src/alpha_simprini/client/binding_group.coffee
  18. +10 −4 src/alpha_simprini/client/dom.coffee
  19. +56 −0 src/alpha_simprini/client/key_router.coffee
  20. +20 −2 src/alpha_simprini/client/view.coffee
  21. +6 −0 src/alpha_simprini/client/view_model.coffee
  22. +3 −1 src/alpha_simprini/client/views/dialog.coffee
  23. +19 −2 src/alpha_simprini/core.coffee
  24. +27 −19 src/alpha_simprini/core/collection.coffee
  25. +2 −13 src/alpha_simprini/core/filtered_collection.coffee
  26. +8 −0 src/alpha_simprini/core/logging.coffee
  27. +86 −4 src/alpha_simprini/core/model.coffee
  28. +4 −8 src/alpha_simprini/core/model/dendrite.coffee
  29. +86 −0 src/alpha_simprini/core/model/local.coffee
  30. +108 −0 src/alpha_simprini/core/model/post_message.coffee
  31. +245 −64 src/alpha_simprini/core/model/share.coffee
  32. +5 −0 src/alpha_simprini/core/model/synapse.coffee
  33. +1 −1  src/alpha_simprini/core/models/grouping.coffee
  34. +11 −2 src/alpha_simprini/core/models/multiple_selection_model.coffee
  35. +4 −3 src/alpha_simprini/core/models/radio_selection_model.coffee
  36. +9 −1 src/alpha_simprini/core/properties/belongs_to.coffee
  37. +60 −39 src/alpha_simprini/core/properties/field.coffee
  38. +47 −15 src/alpha_simprini/core/properties/has_many.coffee
  39. +10 −2 src/alpha_simprini/core/properties/has_one.coffee
  40. +10 −1 src/alpha_simprini/core/properties/virtual_property.coffee
  41. +15 −0 src/alpha_simprini/css.coffee
  42. +2 −0  src/alpha_simprini/css/models/angle.coffee
  43. +21 −0 src/alpha_simprini/css/models/color.coffee
  44. +2 −0  src/alpha_simprini/css/models/color_stop.coffee
  45. +15 −0 src/alpha_simprini/css/models/color_stops.coffee
  46. +41 −0 src/alpha_simprini/css/models/font_family.coffee
  47. +7 −0 src/alpha_simprini/css/models/font_size.coffee
  48. +18 −0 src/alpha_simprini/css/models/length.coffee
  49. +3 −0  src/alpha_simprini/css/models/margin.coffee
  50. +3 −0  src/alpha_simprini/css/models/padding.coffee
  51. +2 −0  src/alpha_simprini/css/models/percent.coffee
  52. +5 −0 src/alpha_simprini/css/models/siding.coffee
  53. +41 −0 src/alpha_simprini/css/views/angle_picker.coffee
  54. +15 −0 src/alpha_simprini/css/views/color_picker.coffee
  55. +75 −0 src/alpha_simprini/css/views/color_stop_picker.coffee
  56. +67 −0 src/alpha_simprini/css/views/dialogs/color.coffee
  57. +7 −0 src/alpha_simprini/keyboard.coffee
  58. +17 −0 src/alpha_simprini/keyboard/models/zone.coffee
  59. +41 −0 src/alpha_simprini/keyboard/models/zone_controller.coffee
  60. +50 −0 src/alpha_simprini/keyboard/models/zone_group.coffee
  61. +10 −0 src/alpha_simprini/keyboard/views/destructable.coffee
  62. +26 −0 src/alpha_simprini/keyboard/views/keyboard_navigation.coffee
  63. +11 −0 src/alpha_simprini/keyboard/views/selectable.coffee
  64. +6 −0 src/alpha_simprini/keyboard/views/zone.coffee
  65. +1 −0  test/client/application.coffee
  66. +2 −0  test/client/binding/check_box.coffee
  67. +60 −0 test/client/binding/field.coffee
  68. +1 −0  test/client/binding/input.coffee
  69. +10 −2 test/client/binding/many.coffee
  70. +2 −0  test/client/binding/model.coffee
  71. +1 −0  test/client/binding/one.coffee
  72. +2 −2 test/client/binding/select.coffee
  73. +2 −2 test/client/binding_group.coffee
  74. +3 −0  test/client/models/targets.coffee
  75. +8 −4 test/client/view.coffee
  76. +1 −1  test/client/view_events.coffee
  77. +1 −1  test/core.coffee
  78. +19 −7 test/core/collection.coffee
  79. +24 −12 test/core/filtered_collection.coffee
  80. +231 −231 test/core/model/share.coffee
  81. +5 −1 test/core/models/grouping.coffee
  82. +40 −7 test/core/properties/belongs_to.coffee
  83. +31 −15 test/core/properties/field.coffee
  84. +87 −28 test/core/properties/has_many.coffee
  85. +28 −0 test/core/properties/has_one.coffee
  86. +11 −1 test/core/properties/virtual_property.coffee
  87. +7 −7 test/helper.coffee
View
2  .rvmrc
@@ -1 +1 @@
-rvm use 1.9.2-p290
+rvm 1.9.3
View
4 Gemfile
@@ -8,4 +8,6 @@ gem "uglifier", "~> 1.0.3"
gem "coffee-script"
gem "github_uploader", "~> 0.1.0", require: false
gem "html_package", "~> 0.0.3"
-gem "tilt"
+gem "tilt"
+gem "rake"
+gem "watchr"
View
3  Gemfile.lock
@@ -56,6 +56,7 @@ GEM
uglifier (1.0.4)
execjs (>= 0.3.0)
multi_json (>= 1.0.2)
+ watchr (0.7)
PLATFORMS
ruby
@@ -65,7 +66,9 @@ DEPENDENCIES
colored
github_uploader (~> 0.1.0)
html_package (~> 0.0.3)
+ rake
rake-pipeline!
rake-pipeline-web-filters!
tilt
uglifier (~> 1.0.3)
+ watchr
View
1  Procfile.build
@@ -0,0 +1 @@
+watchr: watchr autobuild.watchr
View
7 alpha_simprini.erb
@@ -19,10 +19,10 @@
href="http://spader.herokuapp.com/spades/jwerty-0.3.0.js">
<link rel="dependency" type="text/package+html"
- href="http://cloud.github.com/downloads/collin/pathology/pathology-0.3.0.html">
+ href="http://cloud.github.com/downloads/collin/pathology/pathology-0.3.1.html">
<link rel="dependency" type="text/package+html"
- href="http://cloud.github.com/downloads/collin/knead/knead-0.3.1.html">
+ href="http://cloud.github.com/downloads/collin/knead/knead-0.3.3.html">
<link rel="dependency" type="text/package+html"
href="http://cloud.github.com/downloads/collin/taxi/taxi-0.3.1.html">
@@ -42,6 +42,9 @@
<link rel="dependency" type="text/spade+javascript"
href="http://spader.herokuapp.com/spades/rangy-core-1.2.3.js">
+ <link rel="dependency" type="text/spade+javascript"
+ herf="http://spader.herokuapp.com/spades/jPicker-1.1.6.js">
+
<!-- DEVELOPMENT DEPENDENCIES -->
<link rel="development-dependency" type="text/spade+javascript"
View
64 autobuild.watchr
@@ -0,0 +1,64 @@
+# def run_all_tests
+# print `clear`
+# puts "Tests run #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
+# test_files = `find test |grep coffee | tr "\\n" " "`
+# cleaned = test_files.split(" ").reject{|path| path["helper"]}.join(" ")
+# puts "nodeunit #{cleaned}"
+# puts `nodeunit #{cleaned}`
+# end
+
+# def run_tests(m)
+# return if m.to_s["helper"]
+# print `clear`
+# puts "Tests run @ #{m} an #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
+# puts `nodeunit #{m}`
+# end
+
+# run_all_tests
+# watch("(lib)(/.*)+.coffee") { |m|
+# hit = m.to_s
+# hit.gsub!(/^lib\/alpha_simprini/, "test")
+# run_tests(hit)
+# }
+# watch("(test)(/.*)+.coffee") { |m| run_tests(m) }
+
+require "rake"
+load File.expand_path("./Rakefile")
+
+def build
+ Rake::Task["build"].reenable
+ Rake::Task["build"].invoke
+end
+
+watch "(test)(/.*)+.coffee" do |match|
+ hit = match.to_s
+ puts hit
+ puts hit =~ /tmp/
+ return if hit["tmp"]
+ puts "coffee -co tmp/#{File.dirname hit} #{hit}"
+ system "coffee -co tmp/#{File.dirname hit} #{hit}"
+ build
+end
+
+watch "(src)(/.*)+.coffee" do |match|
+ hit = match.to_s
+ puts "coffee -co #{File.dirname hit.sub('src', 'lib')} #{hit}"
+ system "coffee -co #{File.dirname hit.sub('src', 'lib')} #{hit}"
+ build
+end
+
+@interrupted = false
+
+# Ctrl-C
+Signal.trap "INT" do
+ if @interrupted
+ abort("\n")
+ else
+ puts "Interrupt a second time to quit"
+ @interrupted = true
+ Kernel.sleep 1.5
+
+ # run_all_tests
+ @interrupted = false
+ end
+end
View
2  examples/todo/todo.coffee
@@ -64,7 +64,7 @@ class Todo.Models.Item extends AS.Model
@field "task", default: "Something to do..."
# #### Field types.
# A field may have a type. This field is a boolean field.
- @field "done", type: Boolean, default: false
+ @field "done", type: AS.Model.Boolean, default: false
# ### Todo.Views.List
class Todo.Views.List extends AS.View
View
4 src/alpha_simprini.coffee
@@ -25,6 +25,8 @@ AS.Map = Pathology.Map
AS.Module = Pathology.Module
AS.Namespace = Pathology.Namespace
AS.Property = Taxi.Property
+AS.Property.Instance.def rawValue: -> @value
+
AS.COLLECTION_DELEGATES = ["first", "rest", "last", "compact", "flatten", "without", "union", "filter", "reverse",
"intersection", "difference", "uniq", "zip", "indexOf", "find", "detect", "sortBy",
@@ -35,7 +37,7 @@ AS.require = (framework="alpha_simprini", libraries) ->
require "alpha_simprini/#{framework}"
else
for library in libraries.split(/\s+/)
- continue if library.blank()
+ continue if library.match(/^\s+$/)
require "alpha_simprini/#{framework}/#{library}"
return
View
8 src/alpha_simprini/client.coffee
@@ -7,12 +7,14 @@ Client.require """
binding
binding/container
- binding/model binding/field binding/input binding/select binding/file
- binding/check_box binding/edit_line binding/one binding/many
+ binding/model binding/field binding/if binding/input binding/select
+ binding/file binding/check_box binding/edit_line binding/one binding/many
views/panel views/region views/dialog
models/targets
- application
+ application key_router
"""
+
+AS.require("keyboard")
View
160 src/alpha_simprini/client/application.coffee
@@ -1,5 +1,5 @@
-{each} = _
domready = jQuery
+{defer} = _
require("jwerty")
AS.Application = AS.Object.extend ({def, include}) ->
@@ -7,79 +7,153 @@ AS.Application = AS.Object.extend ({def, include}) ->
def initialize: (config={}) ->
_.extend(this, config)
+ @stateObjects = {}
@params = AS.params
- @el ?= $("body")
- @godGivenKeyHandlers()
- domready =>
- @boot()
+
+ @keyRouter = AS.KeyRouter.new(this, document.body)
# @::initialize.doc =
# params: [
# []
# ]
# desc: """
- #
+ # Initializes the Application object. Setting up a ZoneController,
+ # states objects, and view objects.
# """
+ JQUERY_EVENTS = """
+ bind blur change click dblclick focus focusin focusout
+ keydown keypress keyup load mousedown mousenter mouseleave mousemove
+ mouseout mouseover mouseup ready resize scroll select submit load unload
+ """
+ def applyTo: (element) ->
+ @el = $(element)
+
+ _document = window.parent.document
+ for event in JQUERY_EVENTS.split(/\s+/)
+ continue if event.match(/^\s+$/)
+ jQuery(_document).on "#{event}.runloop", ->
+ defer -> Taxi.Governer.exit() if Taxi.Governer.currentLoop
+
+ jQuery(_document).ajaxSend ->
+ Taxi.Governer.exit() if Taxi.Governer.currentLoop
+
+ jQuery(_document).ajaxComplete ->
+ Taxi.Governer.exit() if Taxi.Governer.currentLoop
+
+ # FIXME: don't do this on applyTo
+ # would rather them happen on load, not on application
+ @connect()
+ @buildState()
+ @content()
+
+ AS.unbindGoverner = -> $(_document).unbind(".runloop")
- def boot: ->
- # @::boot.doc =
+ def connect: ->
+
+ # @::connect.doc =
+ # desc: """
+ # Override the connect method to instantiate to storage/connection adapters
+ # """
+
+ def buildState: ->
+ @state 'zoneController', AS.Models.ZoneController.new(application: this)
+ @state 'appZones', @zoneController.defaultZoneGroup.get()
+ # @::buildState.doc =
+ # desc: """
+ # Override this method to build your application state objects.
+ # """
+
+ def content: ->
+ # @::content.doc =
+ # desc: """
+ # Override this method to build your application view objects.
+ # """
+
+ def prepareModel: (id, _model) ->
+ path = _model.constructor.path()
+ constructor = AS.loadPath(path)
+ model = constructor.prepare(id: id, application: this)
+ console.log "[preparedModel] #{model.toString()}, #{model.id}"
+ # @::prepareModel.doc =
# params: [
- # []
+ # ["id", [String], true]
+ # ["model", AS.Model, true]
# ]
# desc: """
- #
+ # Prepare a model with the same id and constructor path
+ # as another model. Used when cutting over applications
+ # to an updated code base.
# """
- def godGivenKeyHandlers: ->
- handlers =
- '': 'escape'
- '⌘+↩': 'accept'
- 'backspace': 'delete'
-
- #TODO: add to test suite
- "": "open"
- "up": "up"
- "down": "down"
- "home": "first"
- "end": "last"
- "left": "left"
- "right": "right"
- "tab": "indent"
- "shift+tab": "dedent"
- "[a-z]/[0-9]/shift+[a-z]": "alphanum"
-
-
- each handlers, (trigger, key) =>
- jwerty.key key, ( (event) => @trigger(trigger, event) ), @el
-
- jwerty.key "backspace", (event) =>
- @trigger("delete", event)
- # @::godGivenKeyHandlers.doc =
+ def takeOverModel: (id, _model) ->
+ path = _model.constructor.path()
+ constructor = AS.loadPath(path)
+ model = constructor.find(id)
+ model.takeOver(_model)
+ # @::takeOverMadel.doc =
# params: [
- # []
+ # ["id", String, true, tag: "The id of the model to take over"]
+ # ["_model", AS.Model, true, tag: "The corresponding model to take over."]
# ]
# desc: """
- #
+ # Take over the properties of another model. Used to cut over applicatons
+ # to an updated code base.
+ # """
+
+ def takeOverState: (application) ->
+ for key, value of application.stateObjects
+ console.log "[takeOverState] #{key} => #{value.toString()}", value.id
+ @stateObjects[key] = @[key] = AS.Model.find(value.id)
+ # @::takeOverState.doc =
+ # params: [
+ # ["application", AS.Application, true, tag:"The application to take over."]
+ # ]
+ # desc: """
+ # Take aver the state objects of another application. Used when cutting over.
+ # """
+
+ def state: (name, constructor, options...) ->
+ return if @[name]?
+
+ stateObject = if constructor.new
+ constructor.new.apply(constructor, options)
+ else
+ constructor
+
+ @[name] = @stateObjects[name] = stateObject
+ # @::state.doc =
+ # desc: """
+ # Creates a state object in the application.
# """
def view: (constructor, options={}) ->
options.application = this
constructor.new options
# @::view.doc =
+ # desc: """
+ # Creates a view in the application.
+ # """
+
+ def zone: (name) ->
+ @appZones.add -> @object.application[name]
+ # @::zone.doc =
# params: [
- # []
+ # ["name", String, true,
+ # tag: """
+ # The name of a view in the application that will act as
+ # a navigation zone
+ # """]
# ]
# desc: """
- #
+ # Add a zone to the main application zone group. Lookup of application
+ # zones is extremely late-bound, so they must be referenced by name only.
+ # this is because zone state
# """
def append: (view) ->
@el.append view.el
# @::append.doc =
- # params: [
- # []
- # ]
# desc: """
- #
+ # Append a view's element to the application element.
# """
View
2  src/alpha_simprini/client/binding.coffee
@@ -6,7 +6,7 @@ AS.Binding = AS.Object.extend ({def}) ->
if _.isFunction(@options)
[@fn, @options] = [@options, {}]
- @container ?= @context.$ @context.currentNode
+ @container ?= $ @context.currentNode
@bindingGroup = @context.bindingGroup
@content = @makeContent()
View
52 src/alpha_simprini/client/binding/edit_line.coffee
@@ -2,8 +2,10 @@ require "rangy-core"
AS.Binding.EditLine = AS.Binding.extend ({def}) ->
def rangy: rangy
- def applyChange: (doc, oldval, newval) ->
- return if oldval == newval
+ def applyChange: (doc, oldval="", newval="") ->
+ return if oldval is newval
+ return if newval is ""
+ doc.set "" unless doc.get()
commonStart = 0
commonStart++ while oldval.charAt(commonStart) == newval.charAt(commonStart)
@@ -53,13 +55,13 @@ AS.Binding.EditLine = AS.Binding.extend ({def}) ->
@content = @makeContent()
@elem = @content[0]
@elem.innerHTML = @fieldValue()
- @previous_value = @fieldValue()
+ @previousValue = @fieldValue()
@selection = start: 0, end: 0
- @context.binds @model, "share:insert:#{_(@field).last()}", @insert, this
- @context.binds @model, "share:delete:#{_(@field).last()}", @delete, this
+ @context.binds @model, "share:insert:#{@field.options.name}", @insert, this
+ @context.binds @model, "share:delete:#{@field.options.name}", @delete, this
- @context.binds @model, "change:#{_(@field).last()}", @updateUnlessFocused, this
+ @context.binds @model, "change:#{@field.options.name}", @updateUnlessFocused, this
for event in ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste', 'click', 'focus']
@context.binds @content, event, @generateOperation, this
@@ -73,8 +75,8 @@ AS.Binding.EditLine = AS.Binding.extend ({def}) ->
def updateUnlessFocused: (event) ->
# Defer this because we want text input to feel fluid!
- _.defer ->
- return if @context.$(this.elem).closest(":focus")[0]
+ _.defer =>
+ return if $(@elem).closest(":focus")[0]
@elem.innerHTML = @fieldValue()
# @::updateUnlessFocused.doc =
# params: [
@@ -94,12 +96,12 @@ AS.Binding.EditLine = AS.Binding.extend ({def}) ->
#
# """
- def replaceText: (new_text="") ->
+ def replaceText: (newText="") ->
range = @rangy.createRange()
selection = @rangy.getSelection()
scrollTop = @elem.scrollTop
- @elem.innerHTML = new_text
+ @elem.innerHTML = newText
@elem.scrollTop = scrollTop unless @elem.scrollTop is scrollTop
return unless selection.anchorNode?.parentNode is @elem
@@ -141,19 +143,25 @@ AS.Binding.EditLine = AS.Binding.extend ({def}) ->
# """
def generateOperation: ->
- selection = @rangy.getSelection()
- if selection.rangeCount
- range = @rangy.getSelection().getRangeAt(0)
+ if @model.share
+ selection = @rangy.getSelection()
+ if selection.rangeCount
+ range = @rangy.getSelection().getRangeAt(0)
+ else
+ range = @rangy.createRange()
+ @selection.start = range.startOffset
+ @selection.end = range.endOffset
+ if @elem.innerHTML isnt @previousValue
+ @previousValue = @elem.innerHTML
+ # IE constantly replaces unix newlines with \r\n. ShareJS docs
+ # should only have unix newlines.
+ @applyChange @model.share.at(@field.options.name), @model.share.at(@field.options.name).getText(), @elem.innerHTML.replace(/\r\n/g, '\n')
+ @model[@field.options.name].set @model.share.at(@field.options.name).getText()
else
- range = @rangy.createRange()
- @selection.start = range.startOffset
- @selection.end = range.endOffset
- if @elem.innerHTML isnt @previous_value
- @previous_value = @elem.innerHTML
- # IE constantly replaces unix newlines with \r\n. ShareJS docs
- # should only have unix newlines.
- @applyChange @model.share.at(@field), @model.share.at(@field).getText(), @elem.innerHTML.replace(/\r\n/g, '\n')
- @model[@field] @model.share.at(@field).getText(), remote: true
+ if @elem.innerHTML isnt @previousValue
+ @previousValue = @elem.innerHTML
+ @field.set @elem.innerHTML
+ return
# @::generateOperation.doc =
# params: [
# []
View
27 src/alpha_simprini/client/binding/if.coffee
@@ -0,0 +1,27 @@
+AS.Binding.If = AS.Binding.Field.extend ({delegate, include, def, defs}) ->
+ def setContent: ->
+ @content.empty()
+ @bindingGroup.unbind()
+
+ value = fieldValue = @fieldValue()
+
+ value = false if fieldValue in [null, undefined, "null", "undefined", "false", false]
+
+ if value isnt false
+ contentFn = @options.then
+ else
+ contentFn = @options.else
+
+ contentFn = if value then @options.then else @options.else
+ return unless contentFn
+
+ @context.withinBindingGroup @bindingGroup, =>
+ @context.withinNode @content, =>
+ contentFn.call(@context)
+
+ @bindContent()
+ # @::setContent.doc =
+ # desc: """
+ # Sets the content based on the fieldValue and the given branches.
+ # """
+
View
4 src/alpha_simprini/client/binding/input.coffee
@@ -1,7 +1,9 @@
AS.Binding.Input = AS.Binding.Field.extend ({def}) ->
def initialize: ->
@_super.apply(this, arguments)
- if _.isArray @field
+ if @options.bindingPath
+ @context.binds @model, @options.bindingPath, @setContent, this
+ else if _.isArray(@field)
@context.binds @model, @field, @setContent, this
else
@context.binds @field, "change", @setContent, this
View
16 src/alpha_simprini/client/binding/many.coffee
@@ -1,8 +1,11 @@
+{bind} = _
AS.Binding.Many = AS.Binding.extend ({def}) ->
@willGroupBindings = true
def initialize: ->
@_super.apply(this, arguments)
+
+ @options.indexOffset ?= 0
@collection = @field
@contents = {}
@@ -22,7 +25,7 @@ AS.Binding.Many = AS.Binding.extend ({def}) ->
# """
def makeAll: ->
- @sortedModels().each _.bind @makeItemContent, this
+ @sortedModels().each _.bind @insertItem, this
# @::makeAll.doc =
# params: [
# []
@@ -62,12 +65,13 @@ AS.Binding.Many = AS.Binding.extend ({def}) ->
# """
def insertItem: (item) ->
+ return if @contents[item.cid]
return if @skipItem(item)
content = @context.danglingContent => @makeItemContent(item)
- index = @sortedModels().indexOf(item).value?()
+ index = @sortedModels().indexOf(item).value?() + @options.indexOffset
index ?= 0
- siblings = @container.children()
+ siblings = @container.contents()
unless siblings.get(0)
@container.append(content)
@@ -76,7 +80,7 @@ AS.Binding.Many = AS.Binding.extend ({def}) ->
@container.append(content)
else
- @context.$(siblings.get(index)).before(content)
+ $(siblings.get(index)).before(content)
@sorting = @sortedModels()
# @::insertItem.doc =
@@ -107,8 +111,8 @@ AS.Binding.Many = AS.Binding.extend ({def}) ->
def moveItem: (item) ->
content = @contents[item.cid]
currentIndex = content.index()
- newIndex = @sortedModels().indexOf(item).value()
- siblings = content.parent().children()
+ newIndex = @sortedModels().indexOf(item).value() + @options.indexOffset
+ siblings = content.parent().contents()
if currentIndex < newIndex
@context.$(siblings[newIndex]).after(content)
View
16 src/alpha_simprini/client/binding/model.coffee
@@ -15,14 +15,14 @@ AS.Binding.Model = AS.Object.extend ({def}) ->
do (property, options) =>
if _.isArray(options)
@styles[property] = => @model.readPath(options)
- painter = =>
+ painter = => _.defer =>
value = @styles[property]()
@content.css property, value
@context.binds @model, options, painter, this
else
@styles[property] = => options.fn(@model)
- painter = => @content.css property, @styles[property]()
+ painter = => _.defer => @content.css property, @styles[property]()
@context.binds @model, options.field, painter, this
# @::css.doc =
# params: [
@@ -38,14 +38,14 @@ AS.Binding.Model = AS.Object.extend ({def}) ->
if _.isArray(options)
@attrs[property] = =>
value = @model.readPath(options)
- if value is true
+ if value
"yes"
- else if value is false
+ else if value in [false, null, undefined]
"no"
else
value
- painter = =>
+ painter = => _.defer =>
@content.attr property, @attrs[property]()
bindingPath = options
@@ -56,14 +56,14 @@ AS.Binding.Model = AS.Object.extend ({def}) ->
options.fn(@model)
else
value = @model[options.field].get()
- if value is true
+ if value
"yes"
- else if vaule is false
+ else if value in [false, null, undefined]
"no"
else
value
- painter = =>
+ painter = => _.defer =>
@content.attr property, @attrs[property]()
@context.binds @model, options.field, painter, this
View
8 src/alpha_simprini/client/binding_group.coffee
@@ -15,6 +15,7 @@ AS.BindingGroup = AS.Object.extend ({def}) ->
# Unbind all bindings, and then unbind all children binding groups
def unbind: ->
object.unbind("."+@namespace) for object in @boundObjects
+ @boundObjects = []
@unbindChildren()
# @::unbind.doc =
# params: [
@@ -42,16 +43,17 @@ AS.BindingGroup = AS.Object.extend ({def}) ->
def binds: (object, event, handler, context) ->
@boundObjects.push object
+ reactor = _.bind(handler, context)
if object.jquery
- object.bind "#{event}.#{@namespace}", _.bind(handler, context)
+ object.bind "#{event}.#{@namespace}", reactor
else if _.isArray(event)
- object.bindPath(event, _.bind(handler, context))
+ @boundObjects.push object.bindPath(event, reactor)
else
object.bind
event: event
namespace: @namespace
- handler: handler
+ handler: reactor
context: context
# @::binds.doc =
# params: [
View
14 src/alpha_simprini/client/dom.coffee
@@ -27,11 +27,17 @@ SVG_ELEMENTS = _('
AS.DOM = AS.Object.extend ({delegate, include, def, defs}) ->
def $: $
+ def _document: document
+
def text: (textContent) ->
# createTextNode creates a text node, no DOM injection here
# TODO: DOUBLE EXPRESS VERIFY THIS ASSUMPTION AND PASTE
# LINKS TO SUPPORTING EVIDENCE IN THE CODE.
- @currentNode.appendChild document.createTextNode(textContent)
+ textNode = @_document.createTextNode(textContent)
+ if @currentNode
+ @currentNode.appendChild textNode
+ else
+ textNode
# @::text.doc =
# params: [
# []
@@ -51,7 +57,7 @@ AS.DOM = AS.Object.extend ({delegate, include, def, defs}) ->
# """
def tag: (name, attrs, content) ->
- node = document.createElement(name)
+ node = @_document.createElement(name)
return @_tag node, attrs, content
# @::tag.doc =
# params: [
@@ -62,7 +68,7 @@ AS.DOM = AS.Object.extend ({delegate, include, def, defs}) ->
# """
def svgTag: (name, attrs, content) ->
- node = document.createElementNS(SVG.ns, name)
+ node = @_document.createElementNS(SVG.ns, name)
return @_tag node, attrs, content
# @::svgTag.doc =
# params: [
@@ -73,7 +79,7 @@ AS.DOM = AS.Object.extend ({delegate, include, def, defs}) ->
# """
def _tag: (node, attrs, content) ->
- @currentNode ?= document.createDocumentFragment()
+ @currentNode ?= @_document.createDocumentFragment()
if _.isFunction(attrs)
content = attrs
attrs = undefined
View
56 src/alpha_simprini/client/key_router.coffee
@@ -0,0 +1,56 @@
+{each} = _
+
+AS.KeyRouter = AS.Object.extend ({delegate, include, def, defs}) ->
+ def initialize: (@target, source) ->
+ @reroute(source)
+ # @::initialize.doc =
+ # params: [
+ # ["@target", "AS.Application", true, tag: "Application that key events will be triggered on."]
+ # ["@source", "HTMLElement", true, tag: "Source of keyboard events."]
+ # ]
+ # desc: """
+ #
+ # """
+
+ def reroute: (newSource) ->
+ @source?.unbind(".#{@objectId()}")
+ @source = $(newSource)
+ @registerHandlers()
+ # @::reroute.doc =
+ # params: [
+ # ["@source", "HTMLElement", true, tag: "Source of keyboard events."]
+ # ]
+ # desc: """
+ # Re-route keyboard events to a new application.
+ # """
+
+ def registerHandlers: ->
+ handlers =
+ '': 'escape'
+ '⌘+↩': 'accept'
+ 'backspace': 'delete'
+ "": "open"
+ "up": "up"
+ "down": "down"
+ "home": "first"
+ "end": "last"
+ "left": "left"
+ "right": "right"
+ "tab": "indent"
+ "shift+tab": "dedent"
+ "[a-z]/[0-9]/shift+[a-z]": "alphanum"
+
+ each handlers, (trigger, key) =>
+ console.log trigger, key
+ @source.on "keydown.#{@objectId()}", jwerty.event(key, (event) =>
+ console.log "KEY", trigger, @target.toString()
+ @target.trigger(trigger, event)
+ )
+
+ @source.on "keydown.#{@objectId()}", jwerty.event("backspace", (event) =>
+ @target.trigger("delete", event)
+ )
+ # @::registerHandlers.doc =
+ # desc: """
+ # Binds the standard key handlers for an Alpha Simprini application.
+ # """
View
22 src/alpha_simprini/client/view.coffee
@@ -168,7 +168,7 @@ AS.View = AS.DOM.extend ({delegate, include, def, defs}) ->
view = constructor.new(options)
@childViews.push(view)
@bindingGroup.addChild(view)
- @currentNode?.appendChild view.el[0]
+ @currentNode?.appendChild view.el[0] unless view.el.parent().is("*")
view.el[0]
# @::view.doc =
# params: [
@@ -245,6 +245,7 @@ AS.View = AS.DOM.extend ({delegate, include, def, defs}) ->
def bindAttrs: ->
return unless @attrBindings
@modelBinding().attr @attrBindings
+ @modelBinding().paint()
# @::bindAttrs.doc =
# params: [
# []
@@ -318,6 +319,17 @@ AS.View = AS.DOM.extend ({delegate, include, def, defs}) ->
#
# """
+ mergeViewOptions = (left, right) ->
+ for key, value of right
+ if left[key]
+ switch key
+ when 'class'
+ left[key] += " " + value
+ else
+ left[key] = value
+ else
+ left[key] = value
+
def toggle: ->
@button class:"toggle expand"
@button class:"toggle collapse"
@@ -328,4 +340,10 @@ AS.View = AS.DOM.extend ({delegate, include, def, defs}) ->
# desc: """
#
# """
-
+ def icon: (name, options={}) ->
+ if options.class
+ options.class = "#{options.class} icon-#{name}"
+ else
+ options.class = "icon-#{name}"
+ @i options
+
View
6 src/alpha_simprini/client/view_model.coffee
@@ -48,6 +48,12 @@ AS.ViewModel = AS.Object.extend ({delegate, include, def, defs}) ->
#
# """
+ def if: (field, branches) ->
+ unless branches.then
+ throw new Error("#{@toString()} 'if' binding must be given at least a 'then' function")
+
+ AS.Binding.If.new(@view, @model, @model[field], branches)
+
def binding: (field, options, fn) ->
if _.isFunction(options)
[fn, options] = [options, {}]
View
4 src/alpha_simprini/client/views/dialog.coffee
@@ -1,6 +1,9 @@
require "knead"
+AS.Views.Dialogs = Pathology.Namespace.new()
AS.Views.Dialog = AS.Views.Panel.extend ({delegate, include, def, defs}) ->
+ @afterContent (view) -> knead.monitor view.head
+
def initialize: ->
@constructor::events ?= {}
_.extend @constructor::events,
@@ -26,7 +29,6 @@ AS.Views.Dialog = AS.Views.Panel.extend ({delegate, include, def, defs}) ->
@head = @$ @header @headerContent
@content = @$ @section @mainContent
@foot = @$ @footer @footerContent
- knead.monitor @head
# @::content.doc =
# params: [
# []
View
21 src/alpha_simprini/core.coffee
@@ -22,12 +22,29 @@ Core.require """
# # ## Some little utility functions.
+AS.util =
+ lpad: (str, padder, length) ->
+ str = str.toString()
+ return str unless str.length < length
+ for num in [0..(length - str.length - 1)]
+ str = "#{padder}#{str}"
+
+ str
+
+ rpad: (str, padder, length) ->
+ str = str.toString()
+ return str unless str.length < length
+ for num in [0..(length - str.length - 1)]
+ str = "#{str}#{padder}"
+
+ str
+
AS.ConstructorIdentity = (constructor) -> (object) -> object.constructor is constructor
AS.Identity = (object) -> (other) -> object is other
AS.IdentitySort = (object) -> object
AS.loadPath = (path) ->
- target = require("pathology").Namespaces
+ target = Pathology.Namespaces
for segment in path.split(".")
target = target[segment]
target
@@ -51,7 +68,7 @@ AS.deepClone = (it) ->
# `uniq` generates a probably unique identifier.
# large random numbers are base32 encoded and combined with the current time base32 encoded
AS.uniq = ->
- (Math.floor Math.random() * 100000000000000000).toString(32) + "-" + (Math.floor Math.random() * 100000000000000000).toString(32) + "-" + (new Date).getTime().toString(32)
+ (Math.floor Math.random() * 100000000000000000).toString(16) + (new Date).getTime().toString(16)
AS.humanSize = (size) ->
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
View
46 src/alpha_simprini/core/collection.coffee
@@ -35,7 +35,6 @@ AS.Collection = AS.Object.extend ({def, include, delegate}) ->
model[@inverse].set(@source) if @inverse and @source
- throw new Error("Cannot add model to collection twice.") if @models.include(model).value()
@_add(model, options)
model
@@ -71,18 +70,23 @@ AS.Collection = AS.Object.extend ({def, include, delegate}) ->
# """
def _add: (model, options={}) ->
- options.at ?= this.length
+ options.at ?= @length
+
index = options.at
@byCid[model.cid] = @byId[model.id] = model
- @models._wrapped.splice index, 0, model
- @length++
- model.bind
- event: "all"
- namespace: @objectId()
- handler: @_onModelEvent
- context: this
-
- model.trigger "add", this, options
+
+ if @models.include(model).value()
+ console.warn "Cannot add model to collection twice.", model.toString()
+ else
+ @models._wrapped.splice index, 0, model
+ @length++
+ model.bind
+ event: "all"
+ namespace: @objectId()
+ handler: @_onModelEvent
+ context: this
+
+ model.trigger "add", this, options
# @::_add.doc =
# private: true
# params: [
@@ -122,14 +126,18 @@ AS.Collection = AS.Object.extend ({def, include, delegate}) ->
def _remove: (model, options={}) ->
options.at = @models.indexOf(model).value()
- @length--
- delete @byId[model.id]
- delete @byCid[model.cid]
- @models = @models.without(model)
- model.trigger("remove", this, options)
- model.unbind
- event: "all"
- namespace: @objectId()
+ if @models.include(model).value()
+ @length--
+ delete @byId[model.id]
+ delete @byCid[model.cid]
+ @models = @models.without(model)
+ model.trigger("remove", this, options)
+ model.unbind
+ event: "all"
+ namespace: @objectId()
+ else
+ console.warn "Cannot remove model from collection twice.", model?.toString()
+
# @::_remove.doc =
# private: true
# params: [
View
15 src/alpha_simprini/core/filtered_collection.coffee
@@ -1,18 +1,7 @@
-# How would you implement this such that the @parent/@filter property could be changed?
-#
-# perhaps.. something like this?
-#
-# @property "parent"
-# @property "filter"
-
-# @virtualProperty "parent", "filter",
-# models:
-# get: ->
-# set: ->
-# add: ->
-# remove: ->
{extend, isString, isFunction, isArray} = _
+# Order in a FilteredCollection is not guaranteed to match the order
+# af the Collection being filtered.
AS.FilteredCollection = AS.Collection.extend ({delegate, include, def, defs}) ->
delegate 'add', 'remove', to: 'parent'
View
8 src/alpha_simprini/core/logging.coffee
@@ -1,6 +1,14 @@
AS.warn = ->
console.warn.apply(console, arguments)
+COUNTS = Pathology.Map.new()
+AS.count = (key) ->
+ count = COUNTS.get(key) ? 0
+ COUNTS.set(key, count + 1)
+
+AS.getCount = (key) ->
+ COUNTS.get(key) ? 0
+
AS.error = () ->
console.trace()
console.error.apply(console, arguments)
View
90 src/alpha_simprini/core/model.coffee
@@ -16,9 +16,9 @@ AS.Model = AS.Object.extend ({delegate, include, def, defs}) ->
'initialize'
]
- defs find: (id) ->
+ defs find: (id, options) ->
idRef = makeIdRef(id, this)
- AS.All.byIdRef[idRef] or @new(id:id)
+ AS.All.byIdRef[idRef] or @new(id:id, options)
# @find.doc =
# params: [
# ["id", String, true]
@@ -29,14 +29,14 @@ AS.Model = AS.Object.extend ({delegate, include, def, defs}) ->
# Otherwise a model is created with `id`.
# """
- def initialize: (attributes={}) ->
+ def initialize: (attributes={}, options={}) ->
attributes.id ?= AS.uniq()
@model = this
if id = attributes.id
delete attributes.id
@setId(id)
@set(attributes)
- @runCallbacks 'afterInitialize'
+ @runCallbacks 'afterInitialize' unless options.skipCallbacks is true
# @::initialize.doc =
# params: [
# ["attributes", Object, false, default: {}]
@@ -44,8 +44,60 @@ AS.Model = AS.Object.extend ({delegate, include, def, defs}) ->
# desc: """
#
# """
+
+ defs prepare: (attributes) ->
+ @new(attributes, {skipCallbacks: true})
+ # @::prepare.doc =
+ # params: [
+ # ["id", String, true, tag:"id for a new model"]
+ # ]
+ # desc: """
+ # Creates a new model of this class without running callbacks.
+ # """
+
+ def takeOver: (model) ->
+ for property in model.properties()
+ continue unless property.rawValue?
+ name = property.options.name
+ console.log "takeOver #{name} #{property.rawValue()}"
+ @[name].set property.rawValue()
+ @runCallbacks 'afterInitialize'
+ # @::takeOver.doc =
+ # params: [
+ # []
+ # ]
+ # desc: """
+ # Take on the properties of another model. Used when cutting
+ # over to a new code base.
+ # """
+
+ def payload: ->
+ payload = {}
+
+ for property in @properties()
+ continue unless property instanceof AS.Model.Field.Instance
+ name = property.options.name
+ switch property.constructor
+ when AS.Model.Field.Instance
+ [key, value] = [name, property.value or property.options.default]
+ when AS.Model.BelongsTo.Instance, AS.Model.HasOne.Instance
+ [key, value] = ["#{name}", property.get()?.id or null]
+ when AS.Model.HasMany.Instance
+ [key, value] = ["#{name}", property.map((model) -> model.id).value()]
+
+ payload[key] = if value? then value else null
+
+ return payload
+ # @::payload.doc =
+ # params: [
+ # []
+ # ]
+ # desc: """
+ #
+ # """
def properties: ->
+ return [] unless @constructor.properties?
@[name] for name in keys(@constructor.properties)
# @::properties.doc =
# desc: """
@@ -105,3 +157,33 @@ AS.Model = AS.Object.extend ({delegate, include, def, defs}) ->
# ]
# desc: """
# """
+
+ def readPath: (path) ->
+ if path[1]
+ this[path[0]].get().model?.readPath(path[1..])
+ else if path[0]
+ this[path[0]].get()
+ # @::readPath.doc =
+ # params: [
+ # []
+ # ]
+ # desc: """
+ #
+ # """
+
+ def writePath: (path, value) ->
+ target = this
+ for piece in path[..-2]
+ target = target[piece].get()
+ target[path[path.length - 1]].set(value)
+ # @::writePath.doc =
+ # params: [
+ # []
+ # ]
+ # desc: """
+ #
+ # """
+
+AS.Model.UniqueId = AS.Module.extend ({delegate, include, def, defs}) ->
+ defs find: (id) ->
+ AS.All.byId[id] or @new(id:id)
View
12 src/alpha_simprini/core/model/dendrite.coffee
@@ -10,12 +10,12 @@ AS.Model.Dendrite = AS.Object.extend ({delegate, include, def, defs}) ->
def equal: -> @notifier.get() is @observer.get()
def on: ->
- @notifier.binds @callback
+ @notifier.binds @callback unless @config.bindEvents is false
return if @equal()
@callback() unless _.isEmpty @notifier.get()
def off: ->
- @notifier.unbinds @callback
+ @notifier.unbinds @callback unless @config.bindEvents is false
AS.Model.CollectionDendrite = AS.Model.Dendrite.extend ({delegate, include, def, defs}) ->
def initialize: (@observer, @notifier, @config={}) ->
@@ -32,12 +32,8 @@ AS.Model.CollectionDendrite = AS.Model.Dendrite.extend ({delegate, include, def,
@observer.remove(item, options)
def on: ->
- @notifier.binds @insertCallback, @removeCallback
-
- if @config.syncNow
- @notifier.each (item, index) =>
- @insertCallback(item, index)
+ @notifier.binds @insertCallback, @removeCallback unless @config.bindEvents is false
def off: ->
- @notifier.unbinds @insertCallback, @removeCallback
+ @notifier.unbinds @insertCallback, @removeCallback unless @config.bindEvents is false
View
86 src/alpha_simprini/core/model/local.coffee
@@ -0,0 +1,86 @@
+# {bind, flatten, each} = _
+# {upperCamelize} = fleck
+
+# AS.Model.LocalStorageAdapter = AS.Object.extend ({delegate, include, def, defs}) ->
+# def localStorage: window.localStorage
+
+# delegate 'getItem', 'setItem', 'removeItem', to: 'localStorage'
+
+# def initialize: (@namespace) ->
+
+# def get: (key) ->
+# JSON.parse @getItem(@key key)
+
+# def set: (key, value) ->
+# JSON.parse @setItem(@key key)
+
+# def del: (key) ->
+# @removeItem(@key key)
+
+# def key: (key) ->
+# "#{@namespace}:#{key}"
+
+# AS.Model.LocalStore = AS.Object.extend ({delegate, include, def, defs}) ->
+# delegate 'get', 'set', 'del', to: 'adapter'
+
+# def initialize: (rootModel, adapterClass=AS.Model.LocalStorageAdapter) ->
+# @adapter = adapterClass.new(rootModel.id)
+# @registrations = Pathology.Set.new()
+# @register(rootModel)
+
+# def register: ->
+
+# def register: (model) ->
+# return if @registrations.include(model)
+# @registrations.add(model)
+
+# @binds model, "destroy", ->
+# @unbind(model)
+# @del model.id
+# @registrations.remove(model)
+
+# each model.properties(), (property) =>
+# if property.constructor is AS.Model.HasMany.Instance
+# property.each bind(@register, this)
+
+# @binds property, "add", (item, options) ->
+# @register(item)
+# @set
+# @post ["add", model.id, property.options.name, item.id, at:options.indexOf(item).value()]
+
+# @binds property, "remove", (item) ->
+# @post ["remove", model.id, property.options.name, item.id]
+
+# else
+# if current = property.get()
+# console.log "currentValue", current.toString()
+# @register(current) if current instanceof AS.Model
+
+# @binds property, "change", ->
+# value = property.get()
+# if value instanceof AS.Model
+# @register(value)
+# value = value.id
+# @post ["change", model.id, property.options.name, value]
+
+# @post ["create", model.constructor.path(), model.id, model.payload()]
+
+# return model
+
+# def binds: (model, event, handler) ->
+# model.bind
+# namespace: @objectId()
+# event: event
+# handler: handler
+# context: this
+
+# def reset: ->
+# @registrations.each (model) => @unbind(model)
+# PL.editor.postMessageAdapter.registrations.empty()
+
+# def unbind: (model) ->
+# model.unbind ".#{@objectId()}"
+# for property in model.properties()
+# property.unbind ".#{@objectId()}"
+
+
View
108 src/alpha_simprini/core/model/post_message.coffee
@@ -0,0 +1,108 @@
+{bind, flatten, each} = _
+{upperCamelize} = fleck
+
+AS.PostMessageController = Pathology.Object.extend ({delegate, include, def, defs}) ->
+ include Taxi.Mixin
+
+ def initialize: (@accept, @commands={}) ->
+ window.addEventListener "message", bind(@receiveMessage, this), false
+
+ def receiveMessage: (event) ->
+ return unless @accept in ["*", event.source, event.origin]
+ [identifier, args...] = flatten event.data
+ if command = @commands[identifier]
+ return unless command.apply
+ command.apply(null, [event].concat(args))
+ else
+ return if @commands.write is false
+ @["receiveMessage#{upperCamelize identifier}"].apply(this, [event].concat(args))
+
+ @trigger "message"
+ Taxi.Governer.exit() if Taxi.Governer.currentLoop
+
+ def receiveMessageCreate: (event, path, id, data) ->
+ AS.loadPath(path).find(id).set(data)
+
+ def receiveMessageDestroy: (event, id) ->
+ AS.All.byId[id].destroy()
+
+ def receiveMessageAdd: (event, id, field, itemId, options) ->
+ AS.All.byId[id][field].add(itemId, options)
+
+ def receiveMessageRemove: (event, id, field, itemId) ->
+ AS.All.byId[id][field].remove(AS.Model.find itemId)
+
+ def receiveMessageChange: (event, id, field, value) ->
+ AS.All.byId[id][field].set(value)
+
+AS.PostMessageSource = Pathology.Object.extend ({delegate, include, def, defs}) ->
+ def initialize: (@_window, @origin) ->
+
+ def post: (_arguments) ->
+ @_window?.postMessage _arguments, @origin
+
+AS.Model.PostMessageAdapter = Pathology.Object.extend ({delegate, include, def, defs}) ->
+ include Taxi.Mixin
+
+ delegate 'post', to: 'source'
+
+ def initialize: ->
+ @registrations = Pathology.Set.new()
+
+ def connect: (event) ->
+ @source = AS.PostMessageSource.new(event.source, event.origin)
+ @trigger("ready")
+
+ def register: (model) ->
+ return if @registrations.include(model)
+ @registrations.add(model)
+ console.log "[postmessage] register", model.toString(), model.id
+
+ @binds model, "destroy", ->
+ @unbind(model)
+ @post ["destroy", model.id]
+ @registrations.remove(model)
+
+ each model.properties(), (property) =>
+ if property.constructor is AS.Model.HasMany.Instance
+ property.each bind(@register, this)
+
+ @binds property, "add", (item, options) ->
+ @register(item)
+ @post ["add", model.id, property.options.name, item.id, at:options.indexOf(item).value()]
+
+ @binds property, "remove", (item) ->
+ @post ["remove", model.id, property.options.name, item.id]
+
+ else
+ if current = property.get()
+ console.log "currentValue", current.toString()
+ @register(current) if current instanceof AS.Model
+
+ @binds property, "change", ->
+ value = property.get()
+ if value instanceof AS.Model
+ @register(value)
+ value = value.id
+ @post ["change", model.id, property.options.name, value]
+
+ @post ["create", model.constructor.path(), model.id, model.payload()]
+
+ return model
+
+ def binds: (model, event, handler) ->
+ model.bind
+ namespace: @objectId()
+ event: event
+ handler: handler
+ context: this
+
+ def reset: ->
+ @registrations.each (model) => @unbind(model)
+ PL.editor.postMessageAdapter.registrations.empty()
+
+ def unbind: (model) ->
+ model.unbind ".#{@objectId()}"
+ for property in model.properties()
+ property.unbind ".#{@objectId()}"
+
View
309 src/alpha_simprini/core/model/share.coffee
@@ -1,106 +1,287 @@
-{keys} = _
+{keys,bind,defer,each,flatten,compact} = _
require("bcsocket")
require("sharejs")
require("sharejs.json")
-window.ShareJS = sharejs.client
+window.ShareJS = sharejs
-AS.ShareJSURL = "http://#{window?.location.host or 'localhost'}/sjs"
+AS.ShareJSURL = "http://#{window?.location.host or 'localhost'}/channel"
-AS.Model.ShareJSAdapter = AS.Object.extend ({delegate, include, def, defs}) ->
- INDEX = "index"
+connections = {}
+
+getConnection = (origin) ->
+ unless connections[origin]
+ c = new ShareJS.Connection origin
- # delegate "adapterFor", to: "store"
+ del = -> delete connections[origin]
+ c.on 'disconnecting', del
+ c.on 'connect failed', del
+ connections[origin] = c
+
+ connections[origin]
+AS.Model.ShareJSAdapter = AS.Object.extend ({delegate, include, def, defs}) ->
+ include Taxi.Mixin
+ # delegate 'open', to: 'store'
- def initialize: ({@store, @url, @model, @share}) ->
- @model.adapter = this
+ def initialize: (@url, @documentName) ->
+ @registrations = Pathology.Set.new()
@url ?= AS.ShareJSURL
+ # Create the ShareJS document/connection
+ @connection = getConnection(@url)
+ @document = new ShareJS.Doc(@connection, @documentName, type: 'json')
+ @connection.docs[@documentName] = @document
- def open: () ->
- ShareJS.open @model.id, "json", @url, (error, share) =>
- if error
- @store.trigger("share:open:error", error, this)
- else
- share.set(new Object) if share.get() is null
- @didOpen(share)
+ # fetch the doc from localStorage if it's there.
+ @fetchDoc()
+
+ # And whenever the document changes stash it in localStorage
+ @document.on "change", bind(@stashDoc, this)
+ # And whenever an op as acknowledged, update the stash
+ @document.on "acknowledge", bind(@stashDoc, this)
+
+ # Finally, open the document
+ @open()
- def sync: (data={}) ->
- for property in @model.properties()
- property.syncWith?(@model.share, data[property.options.name])
- # when all indexed data is loaded, "ready" event is triggered on @model
- def didOpen: (@share) ->
- path = @model.constructor.path()
+ def fetchDoc: ->
+ version = localStorage.getItem "#{@documentName}:share:version"
+ pendingOp = localStorage.getItem "#{@documentName}:share:pendingOp"
+ snapshot = localStorage.getItem "#{@documentName}:share:snapshot"
- constructorGroup = @constructorGroup(path)
- modelDocument = @modelDocument(constructorGroup)
+ return unless version? and pendingOp? and Snapshot?
+
+ @document.pendingOp = JSON.parse pendingOp
+ @document.snapshot = JSON.parse snapshot
+ @document.version = parseInt(version, 10)
@loadEmbeddedData()
- @model.trigger("ready")
- def didLoad: (@share) ->
- path = @model.constructor.path()
+ def stashDoc: (op, snapshot) ->
+ pendingOp = flatten compact [@document.inflightOp, @document.pendingOp]
+ localStorage.setItem "#{@documentName}:share:version", @document.version
+ localStorage.setItem "#{@documentName}:share:pendingOp", JSON.stringify(pendingOp)
+ localStorage.setItem "#{@documentName}:share:snapshot", JSON.stringify(@document.snapshot)
- constructorGroup = @constructorGroup(path)
- modelDocument = @modelDocument(constructorGroup)
+ def maybeClose: ->
+ numDocs = 0
+ for name, doc of @connection.docs
+ numDocs++ if doc.state isnt 'closed' || doc.autoOpen
- @model.share = modelDocument
- @sync()
- @model.trigger("ready")
+ @connection.disconnect() if numDocs == 0
- def modelDocument: (constructorGroup) ->
- modelDocument = constructorGroup.at(@model.id)
- modelDocument.set(new Object) unless modelDocument.get()
- modelDocument
+ def open: ->
+ @document.open (error) =>
+ if error
+ @maybeClose()
+ @trigger("share:open:error", error, this)
+ else
+ @document.set(new Object) if @document.get() is null
+ @didOpen()
- def constructorGroup: (path) ->
- constructorGroup = @share.at(path)
- constructorGroup.set(new Object) unless constructorGroup.get()
- constructorGroup
+ def didOpen: () ->
+ @loadEmbeddedData()
+ @bindRemoteOperationHandler()
+ @trigger("ready")
def eachEmbed: (fn) ->
- for key, value of @share.get()
- continue if key is INDEX
- fn.call(this, key, value)
+ fn.call(this, key, value) for key, value of @document.get()
def loadEmbeddedData: ->
- # @share.at().on "insert", -> console.log "INSERT"
- # @share.at().on "replace", -> console.log "REPLACE"
+ # @document.at().on "insert", -> console.log "INSERT"
+ # @document.at().on "replace", -> console.log "REPLACE"
# First Pass creates objects
@eachEmbed (path, data) ->
- @share.at(path).set(new Object) unless @share.at(path).get()
+ @document.at(path).set(new Object) unless @document.at(path).get()
constructor = AS.loadPath(path)
for id, datum of data
- share = @share.at(path, id)
- model = constructor.find(id)
- model.share = share
+ # skipCallbacks, we'll call afterInitialize manually after the second pass
+ constructor.find(id, skipCallbacks:true)
# Second Pass associates objects
@eachEmbed (path, data) ->
constructor = AS.loadPath(path)
for id, datum of data
model = constructor.find(id)
+ @register(model, datum)
+
+ # Third Pass initializes objects
+ @eachEmbed (path, data) ->
+ constructor = AS.loadPath(path)
+ for id, datum of data
+ constructor.find(id).runCallbacks "afterInitialize"
+
+ # Get out of the runLoop so the display will paint.
+ Taxi.Governer.exit() if Taxi.Governer.currentLoop
+
+
+ # # Third Pass trigers "ready" event
+ # @eachEmbed (path, data) ->
+ # constructor = AS.loadPath(path)
+ # for id, datum of data
+ # model = constructor.find(id)
+ # model.trigger("share:ready")
+
+ def modelDocument: (model) ->
+ @constructorGroup(model.constructor).at(model.id)
+
+ def constructorGroup: (constructor) ->
+ constructorGroup = @document.at(constructor.path())
+ constructorGroup.set(new Object) unless constructorGroup.get()
+ constructorGroup
+
+ def register: (model, data={}) ->
+ return if @registrations.include(model)
+ @registrations.add(model)
+ console.log "[share] register", model.toString(), model.id
+
+ model.share = @modelDocument(model)
+ model.share.set(new Object) unless model.share.get()
+
+ @binds model, "destroy", ->
+ @unbind(model)
+ @registrations.remove(model)
+
+ each model.properties(), (property) =>
+ return unless property.syncWith
+
+ property.syncWith(model.share, data[property.options.name])
+
+ if property.constructor is AS.Model.HasMany.Instance
+ property.each (item) => @register(item)
+
+ @binds property, "add", (item) =>
+ @register(item, item.payload())
+
+ else
+ if current = property.get()
+ console.log "currentValue", current.toString()
+ @register(current, current.payload()) if current instanceof AS.Model
+
+ @binds property, "change", ->
+ value = property.get()
+ @register(value, value.payload()) if value instanceof AS.Model
+
+ return
+
+ def bindRemoteOperationHandler: ->
+ @document.on "remoteop", bind(@remoteOperationHandler, this)
+
+ NUMBER_ADD = "na"
+ STRING_INSERT = "si"
+ STRING_DELETE = "sd"
+ LIST_INSERT = "li"
+ LIST_DELETE = "ld"
+ LIST_MOVE = "lm"
+ OBJECT_INSERT = "oi"
+ OBJECT_DELETE = "od"
+
+ def remoteOperationHandler: (operation) ->
+ for change in operation
+ console.log "remoteop change", change
+
+ path = change.p
+ if path.length is 1
+ @remoteClassOperation(change, path)
+ else if path.length is 2
+ @remoteInstanceOperation(change, path)
+ else
+ @remotePropertyOperation(change, path)
+
+ def remoteClassOperation: (change, path) ->
+ # Added a class to the document
+ constructor = AS.loadPath(path[0])
+
+ if change[OBJECT_INSERT]?
+ objects = @document.at(path[0..1]).get()
+ for id, object in objects
+ @register constructor.find(id), object
+
+ # Removed a class from the document
+ # else if change[OBJECT_DELETE]
+ # don't think we should do much here
+
+ def remoteInstanceOperation: (change, path) ->
+ constructor = AS.loadPath(path[0])
+ id = path[1]
+ # Added an instance to the document
+ if change[OBJECT_INSERT]?
+ object = @document.at(path[0..1]).get()
+ @register constructor.find(id), object
+
+ # Deleted an instance from the document
+ else if change[OBJECT_DELETE]?
+ constructor.find(id).destroy()
+
+ def remotePropertyOperation: (change, path) ->
+ constructorPath = path[0]
+ constructor = AS.loadPath(constructorPath)
+ recordId = path[1]
+ propertyName = path[2]
+
+ record = constructor.find(recordId)
+ property = record[propertyName]
+
+ property.shareSynapse.block =>
+ if change[NUMBER_ADD]?
+ @log -> ["remote number add"]
+
+ else if change[STRING_INSERT]?
+ value = @document.at(path[0..2]).get()
+ property.synapse.set value
+ @log -> ["remote string insert", property, path[0..2], value]
+
+ else if change[STRING_DELETE]?
+ value = @document.at(path[0..2]).get()
+ property.synapse.set value
+ @log -> ["remote string delete", property, path[0..2], value]
+
+ else if change[LIST_INSERT]? and change[LIST_DELETE]? # LIST REPLACE
+ @log -> ["remote list replace"]
+
+ else if change[LIST_INSERT]?
+ property.synapse.insert change.li, at: path[3]
+ @log -> ["remote list insert", property, change.li, path[3]]
+
+ else if change[LIST_DELETE]?
+ property.synapse.remove AS.All.byId[change.ld], at: path[3]
+ @log -> ["remote list delete", property, change.li, path[3]]
- if model is @model
- @sync()
- else
- @adapterFor({model, @share}).sync()
+ else if change[LIST_MOVE]?
+ @log -> ["remote list move"]
- # model.set(datum)
+ else if change[OBJECT_INSERT]? and change[OBJECT_DELETE]? # OBJECT REPLACE
+ property.synapse.set value
+ @log -> ["remote object replace", property, value, path[0..3]]
- def adapterFor: (options) ->
- @store.adapterFor(options)
+ else if change[OBJECT_INSERT]?
+ value = @document.at(path[0..3]).get()
+ property.synapse.set value
+ @log -> ["remote object insert", property, value, path[0..3]]
+ else if change[OBJECT_DELETE]?
+ property.synapse.set null
+ @log -> ["remote object delete", property, value, path[0..3]]
- # def loadIndexedData: ->
- # selfReady = => @model.trigger("ready")
+ # FIXME: don't break the runloop here.
+ # breaking runloop here ensures
+ # property.shareSynapse.block takes
+ # effect.
+ Taxi.Governer.exit() if Taxi.Governer.currentLoop
- # index = @share.at(INDEX).get() or {}
- # loadedIndex = _.after keys(index).length, selfReady
+ def log: (fn) ->
+ [event, data...] = fn()
+ console.log "[#{event}] #{data.join(',')}"
+
+# TODO: DRY this up. see PostMessageAdapter
+ def binds: (model, event, handler) ->
+ model.bind
+ namespace: @objectId()
+ event: event
+ handler: handler
+ context: this
- # for id, path of index
- # constructor = AS.loadPath(path)
- # model = constructor.find(id)
- # model.bind "ready", loadedIndex
- # @adapterFor({model}).open()
+ def unbind: (model) ->
+ model.unbind ".#{@objectId()}"
+ for property in model.properties()
+ property.unbind ".#{@objectId()}"
View
5 src/alpha_simprini/core/model/synapse.coffee
@@ -12,10 +12,12 @@ AS.Model.AbstractSynapse = AS.Object.extend ({delegate, include, def, defs}) ->
def observe: (other, config={}) ->
config.syncNow ?= true
+ config.bindEvents ?= false
@observations.push @dendriteClass.new(this, other, config)
def notify: (other, config={}) ->
config.syncNow ?= false
+ # config.bindEvents ?= false
@notifications.push @dendriteClass.new(other, this, config)
def block: (fn) ->
@@ -52,5 +54,8 @@ AS.Model.CollectionSynapse = AS.Model.AbstractSynapse.extend ({delegate, include
def insert: AS.unimplemented("insert: (item, options) ->")
def remove: AS.unimplemented("remove: (item) ->")
+ def set: (list) ->
+ @insert(item) for item in list
+
def each: AS.unimplemented("each: (fn) ->")
View
2  src/alpha_simprini/core/models/grouping.coffee
@@ -35,7 +35,7 @@ AS.Models.Grouping = AS.Model.extend ({delegate, include, def, defs}) ->
# """
def addToGroup: (item) ->
- name = item[@groupByProperty].get()
+ name = item[@groupByProperty].get() ? "default"
unless group = @groupMap.get(name)
group = AS.Models.Group.new(name: name, metaData: @metaData)
@groups.add(group)
View
13 src/alpha_simprini/core/models/multiple_selection_model.coffee
@@ -1,7 +1,9 @@
AS.Models.MultipleSelectionModel = AS.Model.extend ({def}) ->
@hasMany "items"
- def initialize: ->
+ def initialize: (options={}) ->
+ @property = options.property
+
@_super()
@items.bind "add", (item) => @trigger("add", item)
@@ -12,6 +14,8 @@ AS.Models.MultipleSelectionModel = AS.Model.extend ({def}) ->
# """
def select: (item) ->
+ item[@property]?.set(true) if @property
+
@items.add(item)
# @::select.doc =
# params: [
@@ -22,6 +26,8 @@ AS.Models.MultipleSelectionModel = AS.Model.extend ({def}) ->
# """
def deselect: (item) ->
+ item[@property]?.set(null) if @property
+
@items.remove(item)
# @::deselect.doc =
# params: [
@@ -32,7 +38,10 @@ AS.Models.MultipleSelectionModel = AS.Model.extend ({def}) ->
# """
def clear: ->
- @items.each @items.remove, @items
+ @items.each (item) =>
+ @items.remove(item)
+ item[@property]?.set(null) if @property
+
# @::clear.doc =
# desc: """
#
View
7 src/alpha_simprini/core/models/radio_selection_model.coffee
@@ -3,7 +3,8 @@ AS.Models.RadioSelectionModel = AS.Model.extend ({def}) ->
def initialize: (options={}) ->
@property = options.property
- @_super()
+ delete options.property
+ @_super.apply(this, arguments)
@select undefined
# @::initialize.doc =
# params: [
@@ -15,8 +16,8 @@ AS.Models.RadioSelectionModel = AS.Model.extend ({def}) ->
def select: (item) ->
if @property
- @selected.get()?[@property].set(null)
- item?[@property].set(true)
+ @selected.get()?.model[@property]?.set(null)
+ item?.model[@property]?.set(true)
@selected.set(item)
# @::select.doc =
View
10 src/alpha_simprini/core/properties/belongs_to.coffee
@@ -1,5 +1,13 @@
-AS.Model.BelongsTo = AS.Model.HasOne.extend()
+AS.Model.BelongsTo = AS.Model.HasOne.extend ({delegate, include, def, defs}) ->
AS.Model.BelongsTo.Instance = AS.Model.HasOne.Instance.extend ({delegate, include, def, defs}) ->
+ def bindToValue: (value) ->
+ @_super.apply(this, arguments)
+ value.bind "destroy#{@namespace}", =>
+ if @options.dependant is "destroy"
+ @object.destroy()
+ else
+ @set(null)
+
@Synapse = AS.Model.Field.Instance.Synapse.extend ({delegate, include, def, defs}) ->
def get: ->
@raw.get()
View
99 src/alpha_simprini/core/properties/field.coffee
@@ -2,7 +2,7 @@
# TODO: Field is generic. reuse it.
-AS.Enum = AS.Object.extend ({delegate, include, def, defs}) ->
+AS.Model.Enum = AS.Object.extend ({delegate, include, def, defs}) ->
defs read: (value, options) ->
# AS.Assert options.values
options.values[value]
@@ -11,49 +11,66 @@ AS.Enum = AS.Object.extend ({delegate, include, def, defs}) ->
# AS.Assert options.values
options.values.indexOf(value)
-AS.Model.Field = AS.Property.extend ({delegate, include, def, defs}) ->
- defs Casters: AS.Map.new()
+AS.Model.String = AS.Object.extend ({delegate, include, def, defs}) ->
+ defs read: (value) ->
+ String(value) if value?
- Casters = @Casters
+ defs write: (value) ->
+ String(value) if value?
+
+AS.Model.Number = AS.Object.extend ({delegate, include, def, defs}) ->
+ defs read: (value) ->
+ Number(value) if value?
+
+ defs write: (value) ->
+ String(value) if value?
+
+AS.Model.Date = AS.Object.extend ({delegate, include, def, defs}) ->
+ defs read: (value) ->
+ if isString(value) then new Date(value) else value
- Casters.set String,
- read: (value) ->
- String(value) if value?
+ defs write: (value) ->
+ if value instanceof Date then value.toJSON() else value
- write: (value) ->
- String(value) if value?
+AS.Model.Boolean = AS.Object.extend ({delegate, include, def, defs}) ->
+ defs read: (value) ->
+ return value if isBoolean(value)
+ return true if value is "true"
+ return false if value is "false"
+ return false
- Casters.set Number,
- read: (value) ->
- Number(value) if value?
+ defs write: (value) ->
+ return "true" if value is "true" or value is true
+ return "false" if value is "false" or value is false
+ return "false"
- write: (value) ->
- String(value) if value?
+AS.Model.TokenList = AS.Object.extend ({delegate, include, def, defs}) ->
+ defs read: (value) ->
+ value.split(",")
+
+ defs write: (value) ->
+ value.join(",")
+
- Casters.set Date,
- read: (value) ->
- if isString(value) then new Date(value) else value
+AS.Model.Field = AS.Property.extend ({delegate, include, def, defs}) ->
+ defs Casters: AS.Map.new()
+
+ Casters = @Casters
+
+ Casters.set AS.Model.String, AS.Model.String
- write: (value) ->
- if value instanceof Date then value.toJSON() else value
+ Casters.set AS.Model.Number, AS.Model.Number
- Casters.set Boolean,
- read: (value) ->
- return value if isBoolean(value)
- return true if value is "true"
- return false if value is "false"
- return false
+ Casters.set AS.Model.Date, AS.Model.Date