spinebox 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/README.textile +26 -0
- data/Rakefile +2 -0
- data/bin/spinebox +32 -0
- data/lib/spinebox/base.rb +10 -0
- data/lib/spinebox/command.rb +53 -0
- data/lib/spinebox/compiler.rb +10 -0
- data/lib/spinebox/config.rb +22 -0
- data/lib/spinebox/generator.rb +24 -0
- data/lib/spinebox/routes.rb +41 -0
- data/lib/spinebox/templates/Gemfile +3 -0
- data/lib/spinebox/templates/Rakefile +0 -0
- data/lib/spinebox/templates/app/assets/images/.gitignore +0 -0
- data/lib/spinebox/templates/app/assets/javascripts/app/controllers/.gitignore +0 -0
- data/lib/spinebox/templates/app/assets/javascripts/app/index.js.coffee +6 -0
- data/lib/spinebox/templates/app/assets/javascripts/app/models/.gitignore +0 -0
- data/lib/spinebox/templates/app/assets/javascripts/app/views/.gitignore +0 -0
- data/lib/spinebox/templates/app/assets/javascripts/application.js +14 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/jquery.js +4 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/json2.js +485 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/ajax.coffee +208 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/list.coffee +43 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/local.coffee +16 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/manager.coffee +83 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/relation.coffee +144 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/route.coffee +145 -0
- data/lib/spinebox/templates/app/assets/javascripts/lib/spine/spine.coffee +537 -0
- data/lib/spinebox/templates/app/assets/stylesheets/application.css.scss +13 -0
- data/lib/spinebox/templates/config/config.rb +7 -0
- data/lib/spinebox/templates/config/routes.rb +13 -0
- data/lib/spinebox/templates/config.ru +3 -0
- data/lib/spinebox/templates/public/index.html +14 -0
- data/lib/spinebox/version.rb +3 -0
- data/lib/spinebox.rb +19 -0
- data/spec/base_spec.rb +9 -0
- data/spec/command_spec.rb +23 -0
- data/spec/config_spec.rb +29 -0
- data/spec/generator_spec.rb +25 -0
- data/spec/helpers.rb +9 -0
- data/spec/routes_spec.rb +14 -0
- data/spinebox.gemspec +17 -0
- metadata +97 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
Spine = @Spine or require('spine')
|
2
|
+
$ = Spine.$
|
3
|
+
|
4
|
+
class Spine.List extends Spine.Controller
|
5
|
+
events:
|
6
|
+
'click .item': 'click'
|
7
|
+
|
8
|
+
selectFirst: false
|
9
|
+
|
10
|
+
constructor: ->
|
11
|
+
super
|
12
|
+
@bind 'change', @change
|
13
|
+
|
14
|
+
template: ->
|
15
|
+
throw 'Override template'
|
16
|
+
|
17
|
+
change: (item) =>
|
18
|
+
@current = item
|
19
|
+
|
20
|
+
unless @current
|
21
|
+
@children().removeClass('active')
|
22
|
+
return
|
23
|
+
|
24
|
+
@children().removeClass('active')
|
25
|
+
$(@children().get(@items.indexOf(@current))).addClass('active')
|
26
|
+
|
27
|
+
render: (items) ->
|
28
|
+
@items = items if items
|
29
|
+
@html @template(@items)
|
30
|
+
@change @current
|
31
|
+
if @selectFirst
|
32
|
+
unless @children('.active').length
|
33
|
+
@children(':first').click()
|
34
|
+
|
35
|
+
children: (sel) ->
|
36
|
+
@el.children(sel)
|
37
|
+
|
38
|
+
click: (e) ->
|
39
|
+
item = @items[$(e.currentTarget).index()]
|
40
|
+
@trigger('change', item)
|
41
|
+
true
|
42
|
+
|
43
|
+
module?.exports = Spine.List
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Spine = @Spine or require('spine')
|
2
|
+
|
3
|
+
Spine.Model.Local =
|
4
|
+
extended: ->
|
5
|
+
@change @saveLocal
|
6
|
+
@fetch @loadLocal
|
7
|
+
|
8
|
+
saveLocal: ->
|
9
|
+
result = JSON.stringify(@)
|
10
|
+
localStorage[@className] = result
|
11
|
+
|
12
|
+
loadLocal: ->
|
13
|
+
result = localStorage[@className]
|
14
|
+
@refresh(result or [], clear: true)
|
15
|
+
|
16
|
+
module?.exports = Spine.Model.Local
|
@@ -0,0 +1,83 @@
|
|
1
|
+
Spine = @Spine or require('spine')
|
2
|
+
$ = Spine.$
|
3
|
+
|
4
|
+
class Spine.Manager extends Spine.Module
|
5
|
+
@include Spine.Events
|
6
|
+
|
7
|
+
constructor: ->
|
8
|
+
@controllers = []
|
9
|
+
@bind 'change', @change
|
10
|
+
@add(arguments...)
|
11
|
+
|
12
|
+
add: (controllers...) ->
|
13
|
+
@addOne(cont) for cont in controllers
|
14
|
+
|
15
|
+
addOne: (controller) ->
|
16
|
+
controller.bind 'active', (args...) =>
|
17
|
+
@trigger('change', controller, args...)
|
18
|
+
controller.bind 'release', =>
|
19
|
+
@controllers.splice(@controllers.indexOf(controller), 1)
|
20
|
+
|
21
|
+
@controllers.push(controller)
|
22
|
+
|
23
|
+
deactivate: ->
|
24
|
+
@trigger('change', false, arguments...)
|
25
|
+
|
26
|
+
# Private
|
27
|
+
|
28
|
+
change: (current, args...) ->
|
29
|
+
for cont in @controllers
|
30
|
+
if cont is current
|
31
|
+
cont.activate(args...)
|
32
|
+
else
|
33
|
+
cont.deactivate(args...)
|
34
|
+
|
35
|
+
Spine.Controller.include
|
36
|
+
active: (args...) ->
|
37
|
+
if typeof args[0] is 'function'
|
38
|
+
@bind('active', args[0])
|
39
|
+
else
|
40
|
+
args.unshift('active')
|
41
|
+
@trigger(args...)
|
42
|
+
@
|
43
|
+
|
44
|
+
isActive: ->
|
45
|
+
@el.hasClass('active')
|
46
|
+
|
47
|
+
activate: ->
|
48
|
+
@el.addClass('active')
|
49
|
+
@
|
50
|
+
|
51
|
+
deactivate: ->
|
52
|
+
@el.removeClass('active')
|
53
|
+
@
|
54
|
+
|
55
|
+
class Spine.Stack extends Spine.Controller
|
56
|
+
controllers: {}
|
57
|
+
routes: {}
|
58
|
+
|
59
|
+
className: 'spine stack'
|
60
|
+
|
61
|
+
constructor: ->
|
62
|
+
super
|
63
|
+
|
64
|
+
@manager = new Spine.Manager
|
65
|
+
|
66
|
+
for key, value of @controllers
|
67
|
+
@[key] = new value(stack: @)
|
68
|
+
@add(@[key])
|
69
|
+
|
70
|
+
for key, value of @routes
|
71
|
+
do (key, value) =>
|
72
|
+
callback = value if typeof value is 'function'
|
73
|
+
callback or= => @[value].active(arguments...)
|
74
|
+
@route(key, callback)
|
75
|
+
|
76
|
+
@[@default].active() if @default
|
77
|
+
|
78
|
+
add: (controller) ->
|
79
|
+
@manager.add(controller)
|
80
|
+
@append(controller)
|
81
|
+
|
82
|
+
module?.exports = Spine.Manager
|
83
|
+
module?.exports.Stack = Spine.Stack
|
@@ -0,0 +1,144 @@
|
|
1
|
+
Spine = @Spine or require('spine')
|
2
|
+
isArray = Spine.isArray
|
3
|
+
require = @require or ((value) -> eval(value))
|
4
|
+
|
5
|
+
class Collection extends Spine.Module
|
6
|
+
constructor: (options = {}) ->
|
7
|
+
for key, value of options
|
8
|
+
@[key] = value
|
9
|
+
|
10
|
+
all: ->
|
11
|
+
@model.select (rec) => @associated(rec)
|
12
|
+
|
13
|
+
first: ->
|
14
|
+
@all()[0]
|
15
|
+
|
16
|
+
last: ->
|
17
|
+
values = @all()
|
18
|
+
values[values.length - 1]
|
19
|
+
|
20
|
+
find: (id) ->
|
21
|
+
records = @select (rec) =>
|
22
|
+
rec.id + '' is id + ''
|
23
|
+
throw('Unknown record') unless records[0]
|
24
|
+
records[0]
|
25
|
+
|
26
|
+
findAllByAttribute: (name, value) ->
|
27
|
+
@model.select (rec) =>
|
28
|
+
@associated(rec) and rec[name] is value
|
29
|
+
|
30
|
+
findByAttribute: (name, value) ->
|
31
|
+
@findAllByAttribute(name, value)[0]
|
32
|
+
|
33
|
+
select: (cb) ->
|
34
|
+
@model.select (rec) =>
|
35
|
+
@associated(rec) and cb(rec)
|
36
|
+
|
37
|
+
refresh: (values) ->
|
38
|
+
delete @model.records[record.id] for record in @all()
|
39
|
+
records = @model.fromJSON(values)
|
40
|
+
|
41
|
+
records = [records] unless isArray(records)
|
42
|
+
|
43
|
+
for record in records
|
44
|
+
record.newRecord = false
|
45
|
+
record[@fkey] = @record.id
|
46
|
+
@model.records[record.id] = record
|
47
|
+
|
48
|
+
@model.trigger('refresh', @model.cloneArray(records))
|
49
|
+
|
50
|
+
create: (record) ->
|
51
|
+
record[@fkey] = @record.id
|
52
|
+
@model.create(record)
|
53
|
+
|
54
|
+
# Private
|
55
|
+
|
56
|
+
associated: (record) ->
|
57
|
+
record[@fkey] is @record.id
|
58
|
+
|
59
|
+
class Instance extends Spine.Module
|
60
|
+
constructor: (options = {}) ->
|
61
|
+
for key, value of options
|
62
|
+
@[key] = value
|
63
|
+
|
64
|
+
exists: ->
|
65
|
+
@record[@fkey] and @model.exists(@record[@fkey])
|
66
|
+
|
67
|
+
update: (value) ->
|
68
|
+
unless value instanceof @model
|
69
|
+
value = new @model(value)
|
70
|
+
value.save() if value.isNew()
|
71
|
+
@record[@fkey] = value and value.id
|
72
|
+
|
73
|
+
class Singleton extends Spine.Module
|
74
|
+
constructor: (options = {}) ->
|
75
|
+
for key, value of options
|
76
|
+
@[key] = value
|
77
|
+
|
78
|
+
find: ->
|
79
|
+
@record.id and @model.findByAttribute(@fkey, @record.id)
|
80
|
+
|
81
|
+
update: (value) ->
|
82
|
+
unless value instanceof @model
|
83
|
+
value = @model.fromJSON(value)
|
84
|
+
|
85
|
+
value[@fkey] = @record.id
|
86
|
+
value.save()
|
87
|
+
|
88
|
+
singularize = (str) ->
|
89
|
+
str.replace(/s$/, '')
|
90
|
+
|
91
|
+
underscore = (str) ->
|
92
|
+
str.replace(/::/g, '/')
|
93
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
94
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
95
|
+
.replace(/-/g, '_')
|
96
|
+
.toLowerCase()
|
97
|
+
|
98
|
+
Spine.Model.extend
|
99
|
+
hasMany: (name, model, fkey) ->
|
100
|
+
fkey ?= "#{underscore(this.className)}_id"
|
101
|
+
|
102
|
+
association = (record) ->
|
103
|
+
model = require(model) if typeof model is 'string'
|
104
|
+
|
105
|
+
new Collection(
|
106
|
+
name: name, model: model,
|
107
|
+
record: record, fkey: fkey
|
108
|
+
)
|
109
|
+
|
110
|
+
@::[name] = (value) ->
|
111
|
+
association(@).refresh(value) if value?
|
112
|
+
association(@)
|
113
|
+
|
114
|
+
belongsTo: (name, model, fkey) ->
|
115
|
+
fkey ?= "#{singularize(name)}_id"
|
116
|
+
|
117
|
+
association = (record) ->
|
118
|
+
model = require(model) if typeof model is 'string'
|
119
|
+
|
120
|
+
new Instance(
|
121
|
+
name: name, model: model,
|
122
|
+
record: record, fkey: fkey
|
123
|
+
)
|
124
|
+
|
125
|
+
@::[name] = (value) ->
|
126
|
+
association(@).update(value) if value?
|
127
|
+
association(@).exists()
|
128
|
+
|
129
|
+
@attributes.push(fkey)
|
130
|
+
|
131
|
+
hasOne: (name, model, fkey) ->
|
132
|
+
fkey ?= "#{underscore(@className)}_id"
|
133
|
+
|
134
|
+
association = (record) ->
|
135
|
+
model = require(model) if typeof model is 'string'
|
136
|
+
|
137
|
+
new Singleton(
|
138
|
+
name: name, model: model,
|
139
|
+
record: record, fkey: fkey
|
140
|
+
)
|
141
|
+
|
142
|
+
@::[name] = (value) ->
|
143
|
+
association(@).update(value) if value?
|
144
|
+
association(@).find()
|
@@ -0,0 +1,145 @@
|
|
1
|
+
Spine = @Spine or require('spine')
|
2
|
+
$ = Spine.$
|
3
|
+
|
4
|
+
hashStrip = /^#*/
|
5
|
+
namedParam = /:([\w\d]+)/g
|
6
|
+
splatParam = /\*([\w\d]+)/g
|
7
|
+
escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g
|
8
|
+
|
9
|
+
class Spine.Route extends Spine.Module
|
10
|
+
@extend Spine.Events
|
11
|
+
|
12
|
+
@historySupport: window.history?.pushState?
|
13
|
+
|
14
|
+
@routes: []
|
15
|
+
|
16
|
+
@options:
|
17
|
+
trigger: true
|
18
|
+
history: false
|
19
|
+
shim: false
|
20
|
+
|
21
|
+
@add: (path, callback) ->
|
22
|
+
if (typeof path is 'object' and path not instanceof RegExp)
|
23
|
+
@add(key, value) for key, value of path
|
24
|
+
else
|
25
|
+
@routes.push(new @(path, callback))
|
26
|
+
|
27
|
+
@setup: (options = {}) ->
|
28
|
+
@options = $.extend({}, @options, options)
|
29
|
+
|
30
|
+
if (@options.history)
|
31
|
+
@history = @historySupport && @options.history
|
32
|
+
|
33
|
+
return if @options.shim
|
34
|
+
|
35
|
+
if @history
|
36
|
+
$(window).bind('popstate', @change)
|
37
|
+
else
|
38
|
+
$(window).bind('hashchange', @change)
|
39
|
+
@change()
|
40
|
+
|
41
|
+
@unbind: ->
|
42
|
+
if @history
|
43
|
+
$(window).unbind('popstate', @change)
|
44
|
+
else
|
45
|
+
$(window).unbind('hashchange', @change)
|
46
|
+
|
47
|
+
@navigate: (args...) ->
|
48
|
+
options = {}
|
49
|
+
|
50
|
+
lastArg = args[args.length - 1]
|
51
|
+
if typeof lastArg is 'object'
|
52
|
+
options = args.pop()
|
53
|
+
else if typeof lastArg is 'boolean'
|
54
|
+
options.trigger = args.pop()
|
55
|
+
|
56
|
+
options = $.extend({}, @options, options)
|
57
|
+
|
58
|
+
path = args.join('/')
|
59
|
+
return if @path is path
|
60
|
+
@path = path
|
61
|
+
|
62
|
+
@trigger('navigate', @path)
|
63
|
+
|
64
|
+
@matchRoute(@path, options) if options.trigger
|
65
|
+
|
66
|
+
return if options.shim
|
67
|
+
|
68
|
+
if @history
|
69
|
+
history.pushState(
|
70
|
+
{},
|
71
|
+
document.title,
|
72
|
+
@path
|
73
|
+
)
|
74
|
+
else
|
75
|
+
window.location.hash = @path
|
76
|
+
|
77
|
+
# Private
|
78
|
+
|
79
|
+
@getPath: ->
|
80
|
+
path = window.location.pathname
|
81
|
+
if path.substr(0,1) isnt '/'
|
82
|
+
path = '/' + path
|
83
|
+
path
|
84
|
+
|
85
|
+
@getHash: -> window.location.hash
|
86
|
+
|
87
|
+
@getFragment: -> @getHash().replace(hashStrip, '')
|
88
|
+
|
89
|
+
@getHost: ->
|
90
|
+
(document.location + '').replace(@getPath() + @getHash(), '')
|
91
|
+
|
92
|
+
@change: ->
|
93
|
+
path = if @getFragment() isnt '' then @getFragment() else @getPath()
|
94
|
+
return if path is @path
|
95
|
+
@path = path
|
96
|
+
@matchRoute(@path)
|
97
|
+
|
98
|
+
@matchRoute: (path, options) ->
|
99
|
+
for route in @routes
|
100
|
+
if route.match(path, options)
|
101
|
+
@trigger('change', route, path)
|
102
|
+
return route
|
103
|
+
|
104
|
+
constructor: (@path, @callback) ->
|
105
|
+
@names = []
|
106
|
+
|
107
|
+
if typeof path is 'string'
|
108
|
+
namedParam.lastIndex = 0
|
109
|
+
while (match = namedParam.exec(path)) != null
|
110
|
+
@names.push(match[1])
|
111
|
+
|
112
|
+
path = path.replace(escapeRegExp, '\\$&')
|
113
|
+
.replace(namedParam, '([^\/]*)')
|
114
|
+
.replace(splatParam, '(.*?)')
|
115
|
+
|
116
|
+
@route = new RegExp('^' + path + '$')
|
117
|
+
else
|
118
|
+
@route = path
|
119
|
+
|
120
|
+
match: (path, options = {}) ->
|
121
|
+
match = @route.exec(path)
|
122
|
+
return false unless match
|
123
|
+
options.match = match
|
124
|
+
params = match.slice(1)
|
125
|
+
|
126
|
+
if @names.length
|
127
|
+
for param, i in params
|
128
|
+
options[@names[i]] = param
|
129
|
+
|
130
|
+
@callback.call(null, options) isnt false
|
131
|
+
|
132
|
+
# Coffee-script bug
|
133
|
+
Spine.Route.change = Spine.Route.proxy(Spine.Route.change)
|
134
|
+
|
135
|
+
Spine.Controller.include
|
136
|
+
route: (path, callback) ->
|
137
|
+
Spine.Route.add(path, @proxy(callback))
|
138
|
+
|
139
|
+
routes: (routes) ->
|
140
|
+
@route(key, value) for key, value of routes
|
141
|
+
|
142
|
+
navigate: ->
|
143
|
+
Spine.Route.navigate.apply(Spine.Route, arguments)
|
144
|
+
|
145
|
+
module?.exports = Spine.Route
|