spine-rails 0.1.0 → 0.1.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/README.md +12 -3
- data/build/update.sh +9 -0
- data/lib/generators/spine/model/model_generator.rb +1 -1
- data/lib/generators/spine/new/new_generator.rb +14 -14
- data/lib/generators/spine/scaffold/scaffold_generator.rb +5 -5
- data/lib/generators/spine/scaffold/templates/controller.coffee.erb +1 -1
- data/lib/spine/generators.rb +5 -5
- data/lib/spine/rails/version.rb +2 -2
- data/spine-rails.gemspec +2 -1
- data/vendor/assets/javascripts/spine.coffee +640 -0
- data/vendor/assets/javascripts/spine/ajax.coffee +237 -0
- data/vendor/assets/javascripts/spine/list.coffee +43 -0
- data/vendor/assets/javascripts/spine/local.coffee +16 -0
- data/vendor/assets/javascripts/spine/manager.coffee +84 -0
- data/vendor/assets/javascripts/spine/relation.coffee +136 -0
- data/vendor/assets/javascripts/spine/route.coffee +148 -0
- metadata +41 -17
- data/app/assets/javascripts/json2.js +0 -485
- data/app/assets/javascripts/spine.js +0 -808
- data/app/assets/javascripts/spine/ajax.js +0 -290
- data/app/assets/javascripts/spine/list.js +0 -70
- data/app/assets/javascripts/spine/local.js +0 -28
- data/app/assets/javascripts/spine/manager.js +0 -154
- data/app/assets/javascripts/spine/relation.js +0 -222
- data/app/assets/javascripts/spine/route.js +0 -197
- data/app/assets/javascripts/spine/tabs.js +0 -66
- data/app/assets/javascripts/spine/tmpl.js +0 -22
data/README.md
CHANGED
@@ -14,10 +14,19 @@ This gem does two things:
|
|
14
14
|
|
15
15
|
### Installation
|
16
16
|
|
17
|
-
In your Gemfile, add
|
17
|
+
In your Gemfile, add the following lines:
|
18
18
|
|
19
|
+
gem "json2-rails"
|
19
20
|
gem "spine-rails"
|
20
21
|
|
22
|
+
Make sure that your Gemfile also includes
|
23
|
+
|
24
|
+
gem "jquery-rails"
|
25
|
+
|
26
|
+
Or if your using Zepto
|
27
|
+
|
28
|
+
gem "zepto-rails"
|
29
|
+
|
21
30
|
Then run the following commands:
|
22
31
|
|
23
32
|
bundle install
|
@@ -71,7 +80,7 @@ The generator will create views in `hamljs`, `eco` or `ejs` format, depending on
|
|
71
80
|
|
72
81
|
## Example Usage
|
73
82
|
|
74
|
-
Created a new Rails
|
83
|
+
Created a new Rails application called `blog`.
|
75
84
|
|
76
85
|
rails new blog
|
77
86
|
|
@@ -120,4 +129,4 @@ Also if you want to have some useful helpers to bridge the gap between Spine and
|
|
120
129
|
|
121
130
|
## Attributions
|
122
131
|
|
123
|
-
This plugin was made by [Alex MacCaw](http://alexmaccaw.co.uk) with major contributions from [Dmytrii Nagirniak](https://github.com/dnagir). It's under the same license as [Spine](http://spinejs.com) (MIT).
|
132
|
+
This plugin was made by [Alex MacCaw](http://alexmaccaw.co.uk) with major contributions from [Dmytrii Nagirniak](https://github.com/dnagir) [Zohar Arad] (https:://github.com/zohararad) and [Ben Haines] (http://github.com/benhainez). It's under the same license as [Spine](http://spinejs.com) (MIT).
|
data/build/update.sh
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
TAG=v1.1.0
|
2
|
+
FILES=( ajax list local manager relation route )
|
3
|
+
curl -s -o vendor/assets/javascripts/spine.coffee https://raw.github.com/spine/spine/$TAG/src/spine.coffee
|
4
|
+
|
5
|
+
for i in "${FILES[@]}"
|
6
|
+
do
|
7
|
+
:
|
8
|
+
curl -s -o vendor/assets/javascripts/spine/$i.coffee https://raw.github.com/spine/spine/$TAG/src/$i.coffee
|
9
|
+
done
|
@@ -7,7 +7,7 @@ module Spine
|
|
7
7
|
source_root File.expand_path("../templates", __FILE__)
|
8
8
|
desc "Generate a Spine model with configured fields"
|
9
9
|
|
10
|
-
argument :fields, :
|
10
|
+
argument :fields, desc: 'List of model attributes', type: :array, banner: 'field1 field2'
|
11
11
|
|
12
12
|
def create_model
|
13
13
|
template "model.coffee.erb", "app/assets/javascripts/#{app_name}/models/#{file_name}.js.coffee"
|
@@ -4,40 +4,40 @@ module Spine
|
|
4
4
|
module Generators
|
5
5
|
class NewGenerator < ::Rails::Generators::Base
|
6
6
|
source_root File.expand_path("../templates", __FILE__)
|
7
|
-
|
7
|
+
|
8
8
|
desc "This generator installs Spine #{Spine::Rails::SPINE_VERSION} as part of assets pipeline"
|
9
|
-
|
10
|
-
class_option :app, :
|
11
|
-
|
9
|
+
|
10
|
+
class_option :app, type: :string, default: "app", desc: "app name"
|
11
|
+
|
12
12
|
def app_name
|
13
13
|
options[:app]
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def app_class
|
17
17
|
app_name.camelize
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def create_dir_layout
|
21
21
|
%W{models views controllers lib}.each do |dir|
|
22
|
-
empty_directory "app/assets/javascripts/#{app_name}/#{dir}"
|
23
|
-
create_file "app/assets/javascripts/#{app_name}/#{dir}/.
|
22
|
+
empty_directory "app/assets/javascripts/#{app_name}/#{dir}"
|
23
|
+
create_file "app/assets/javascripts/#{app_name}/#{dir}/.keep"
|
24
24
|
end
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
def create_app_file
|
28
28
|
template "index.coffee.erb", "app/assets/javascripts/#{app_name}/index.js.coffee"
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
def create_lib_file
|
32
|
-
template "view.coffee.erb", "app/assets/javascripts/#{app_name}/lib/view.js.coffee"
|
32
|
+
template "view.coffee.erb", "app/assets/javascripts/#{app_name}/lib/view.js.coffee"
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
def add_spine_app_to_application
|
36
36
|
source = "app/assets/javascripts/application.js"
|
37
37
|
content = File.read(source)
|
38
|
-
|
38
|
+
|
39
39
|
if content.include?("//= require_tree .")
|
40
|
-
inject_into_file source, :
|
40
|
+
inject_into_file source, before: "//= require_tree ." do
|
41
41
|
"//= require #{app_name}\n"
|
42
42
|
end
|
43
43
|
else
|
@@ -7,11 +7,11 @@ module Spine
|
|
7
7
|
source_root File.expand_path("../templates", __FILE__)
|
8
8
|
desc "Generate a Spine scaffold with configured fields"
|
9
9
|
|
10
|
-
argument :fields, :
|
11
|
-
|
10
|
+
argument :fields, desc: "List of model attributes", type: :array, banner: "field1 field2"
|
11
|
+
|
12
12
|
def create_scaffold
|
13
13
|
raise("The 'eco' gem is required; add it to the Gemfile") unless defined?(::Eco)
|
14
|
-
|
14
|
+
|
15
15
|
generate "spine:model #{model_name} #{fields.join(" ")} --app #{app_name}"
|
16
16
|
template "controller.coffee.erb", "app/assets/javascripts/#{app_name}/controllers/#{controller_name}.js.coffee"
|
17
17
|
template "edit.jst.erb", "app/assets/javascripts/#{app_name}/views/#{controller_name}/edit.jst.eco"
|
@@ -19,9 +19,9 @@ module Spine
|
|
19
19
|
template "new.jst.erb", "app/assets/javascripts/#{app_name}/views/#{controller_name}/new.jst.eco"
|
20
20
|
template "show.jst.erb", "app/assets/javascripts/#{app_name}/views/#{controller_name}/show.jst.eco"
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
protected
|
24
|
-
|
24
|
+
|
25
25
|
def model_name
|
26
26
|
file_name.singularize
|
27
27
|
end
|
data/lib/spine/generators.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
module Spine
|
2
2
|
module Generators
|
3
3
|
class Base < ::Rails::Generators::NamedBase
|
4
|
-
class_option :app, :
|
5
|
-
|
4
|
+
class_option :app, type: :string, default: "app", desc: "app name"
|
5
|
+
|
6
6
|
protected
|
7
|
-
|
7
|
+
|
8
8
|
def class_name
|
9
9
|
(class_path + [file_name]).map!{ |m| m.camelize }.join('')
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def app_name
|
13
13
|
options[:app]
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def app_class
|
17
17
|
app_name.camelize
|
18
18
|
end
|
data/lib/spine/rails/version.rb
CHANGED
data/spine-rails.gemspec
CHANGED
@@ -15,8 +15,9 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.rubyforge_project = "spine-rails"
|
16
16
|
|
17
17
|
s.add_dependency "rails", ">= 3.1.0"
|
18
|
+
s.add_dependency "json2-rails", ">= 0.0.2"
|
18
19
|
s.add_development_dependency "bundler"
|
19
|
-
|
20
|
+
|
20
21
|
s.files = `git ls-files`.split("\n")
|
21
22
|
s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
|
22
23
|
s.require_path = 'lib'
|
@@ -0,0 +1,640 @@
|
|
1
|
+
Events =
|
2
|
+
bind: (ev, callback) ->
|
3
|
+
evs = ev.split(' ')
|
4
|
+
calls = @hasOwnProperty('_callbacks') and @_callbacks or= {}
|
5
|
+
for name in evs
|
6
|
+
calls[name] or= []
|
7
|
+
calls[name].push(callback)
|
8
|
+
this
|
9
|
+
|
10
|
+
one: (ev, callback) ->
|
11
|
+
@bind ev, handler = ->
|
12
|
+
@unbind(ev, handler)
|
13
|
+
callback.apply(this, arguments)
|
14
|
+
|
15
|
+
trigger: (args...) ->
|
16
|
+
ev = args.shift()
|
17
|
+
list = @hasOwnProperty('_callbacks') and @_callbacks?[ev]
|
18
|
+
return unless list
|
19
|
+
for callback in list
|
20
|
+
if callback.apply(this, args) is false
|
21
|
+
break
|
22
|
+
true
|
23
|
+
|
24
|
+
listenTo: (obj, ev, callback) ->
|
25
|
+
obj.bind(ev, callback)
|
26
|
+
@listeningTo or= []
|
27
|
+
@listeningTo.push obj
|
28
|
+
this
|
29
|
+
|
30
|
+
listenToOnce: (obj, ev, callback) ->
|
31
|
+
listeningToOnce = @listeningToOnce or = []
|
32
|
+
listeningToOnce.push obj
|
33
|
+
obj.one ev, ->
|
34
|
+
idx = listeningToOnce.indexOf(obj)
|
35
|
+
listeningToOnce.splice(idx, 1) unless idx is -1
|
36
|
+
callback.apply(this, arguments)
|
37
|
+
this
|
38
|
+
|
39
|
+
stopListening: (obj, ev, callback) ->
|
40
|
+
if arguments.length is 0
|
41
|
+
retain = []
|
42
|
+
for listeningTo in [@listeningTo, @listeningToOnce]
|
43
|
+
continue unless listeningTo
|
44
|
+
for obj in listeningTo when not (obj in retain)
|
45
|
+
obj.unbind()
|
46
|
+
retain.push(obj)
|
47
|
+
@listeningTo = undefined
|
48
|
+
@listeningToOnce = undefined
|
49
|
+
|
50
|
+
else if obj
|
51
|
+
obj.unbind(ev, callback) if ev
|
52
|
+
obj.unbind() unless ev
|
53
|
+
for listeningTo in [@listeningTo, @listeningToOnce]
|
54
|
+
continue unless listeningTo
|
55
|
+
idx = listeningTo.indexOf(obj)
|
56
|
+
listeningTo.splice(idx, 1) unless idx is -1
|
57
|
+
|
58
|
+
unbind: (ev, callback) ->
|
59
|
+
if arguments.length is 0
|
60
|
+
@_callbacks = {}
|
61
|
+
return this
|
62
|
+
return this unless ev
|
63
|
+
evs = ev.split(' ')
|
64
|
+
for name in evs
|
65
|
+
list = @_callbacks?[name]
|
66
|
+
continue unless list
|
67
|
+
unless callback
|
68
|
+
delete @_callbacks[name]
|
69
|
+
continue
|
70
|
+
for cb, i in list when (cb is callback)
|
71
|
+
list = list.slice()
|
72
|
+
list.splice(i, 1)
|
73
|
+
@_callbacks[name] = list
|
74
|
+
break
|
75
|
+
this
|
76
|
+
|
77
|
+
Events.on = Events.bind
|
78
|
+
Events.off = Events.unbind
|
79
|
+
|
80
|
+
Log =
|
81
|
+
trace: true
|
82
|
+
|
83
|
+
logPrefix: '(App)'
|
84
|
+
|
85
|
+
log: (args...) ->
|
86
|
+
return unless @trace
|
87
|
+
if @logPrefix then args.unshift(@logPrefix)
|
88
|
+
console?.log?(args...)
|
89
|
+
this
|
90
|
+
|
91
|
+
moduleKeywords = ['included', 'extended']
|
92
|
+
|
93
|
+
class Module
|
94
|
+
@include: (obj) ->
|
95
|
+
throw new Error('include(obj) requires obj') unless obj
|
96
|
+
for key, value of obj when key not in moduleKeywords
|
97
|
+
@::[key] = value
|
98
|
+
obj.included?.apply(this)
|
99
|
+
this
|
100
|
+
|
101
|
+
@extend: (obj) ->
|
102
|
+
throw new Error('extend(obj) requires obj') unless obj
|
103
|
+
for key, value of obj when key not in moduleKeywords
|
104
|
+
@[key] = value
|
105
|
+
obj.extended?.apply(this)
|
106
|
+
this
|
107
|
+
|
108
|
+
@proxy: (func) ->
|
109
|
+
=> func.apply(this, arguments)
|
110
|
+
|
111
|
+
proxy: (func) ->
|
112
|
+
=> func.apply(this, arguments)
|
113
|
+
|
114
|
+
constructor: ->
|
115
|
+
@init?(arguments...)
|
116
|
+
|
117
|
+
class Model extends Module
|
118
|
+
@extend Events
|
119
|
+
|
120
|
+
@records: []
|
121
|
+
@irecords: {}
|
122
|
+
@crecords: {}
|
123
|
+
@attributes: []
|
124
|
+
|
125
|
+
@configure: (name, attributes...) ->
|
126
|
+
@className = name
|
127
|
+
@deleteAll()
|
128
|
+
@attributes = attributes if attributes.length
|
129
|
+
@attributes and= makeArray(@attributes)
|
130
|
+
@attributes or= []
|
131
|
+
@unbind()
|
132
|
+
this
|
133
|
+
|
134
|
+
@toString: -> "#{@className}(#{@attributes.join(", ")})"
|
135
|
+
|
136
|
+
@find: (id) ->
|
137
|
+
record = @exists(id)
|
138
|
+
throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record
|
139
|
+
return record
|
140
|
+
|
141
|
+
@exists: (id) ->
|
142
|
+
(@records[id] ? @irecords[id])?.clone()
|
143
|
+
|
144
|
+
@refresh: (values, options = {}) ->
|
145
|
+
if options.clear
|
146
|
+
@deleteAll()
|
147
|
+
|
148
|
+
records = @fromJSON(values)
|
149
|
+
records = [records] unless isArray(records)
|
150
|
+
|
151
|
+
for record in records
|
152
|
+
if record.id and @irecords[record.id]
|
153
|
+
@records[@records.indexOf(@irecords[record.id])] = record
|
154
|
+
else
|
155
|
+
record.id or= record.cid
|
156
|
+
@records.push(record)
|
157
|
+
@irecords[record.id] = record
|
158
|
+
@crecords[record.cid] = record
|
159
|
+
|
160
|
+
@sort()
|
161
|
+
|
162
|
+
result = @cloneArray(records)
|
163
|
+
@trigger('refresh', @cloneArray(records))
|
164
|
+
result
|
165
|
+
|
166
|
+
@select: (callback) ->
|
167
|
+
(record.clone() for record in @records when callback(record))
|
168
|
+
|
169
|
+
@findByAttribute: (name, value) ->
|
170
|
+
for record in @records
|
171
|
+
if record[name] is value
|
172
|
+
return record.clone()
|
173
|
+
null
|
174
|
+
|
175
|
+
@findAllByAttribute: (name, value) ->
|
176
|
+
@select (item) ->
|
177
|
+
item[name] is value
|
178
|
+
|
179
|
+
@each: (callback) ->
|
180
|
+
callback(record.clone()) for record in @records
|
181
|
+
|
182
|
+
@all: ->
|
183
|
+
@cloneArray(@records)
|
184
|
+
|
185
|
+
@first: ->
|
186
|
+
@records[0]?.clone()
|
187
|
+
|
188
|
+
@last: ->
|
189
|
+
@records[@records.length - 1]?.clone()
|
190
|
+
|
191
|
+
@count: ->
|
192
|
+
@records.length
|
193
|
+
|
194
|
+
@deleteAll: ->
|
195
|
+
@records = []
|
196
|
+
@irecords = {}
|
197
|
+
@crecords = {}
|
198
|
+
|
199
|
+
@destroyAll: (options) ->
|
200
|
+
record.destroy(options) for record in @records
|
201
|
+
|
202
|
+
@update: (id, atts, options) ->
|
203
|
+
@find(id).updateAttributes(atts, options)
|
204
|
+
|
205
|
+
@create: (atts, options) ->
|
206
|
+
record = new @(atts)
|
207
|
+
record.save(options)
|
208
|
+
|
209
|
+
@destroy: (id, options) ->
|
210
|
+
@find(id).destroy(options)
|
211
|
+
|
212
|
+
@change: (callbackOrParams) ->
|
213
|
+
if typeof callbackOrParams is 'function'
|
214
|
+
@bind('change', callbackOrParams)
|
215
|
+
else
|
216
|
+
@trigger('change', arguments...)
|
217
|
+
|
218
|
+
@fetch: (callbackOrParams) ->
|
219
|
+
if typeof callbackOrParams is 'function'
|
220
|
+
@bind('fetch', callbackOrParams)
|
221
|
+
else
|
222
|
+
@trigger('fetch', arguments...)
|
223
|
+
|
224
|
+
@toJSON: ->
|
225
|
+
@records
|
226
|
+
|
227
|
+
@fromJSON: (objects) ->
|
228
|
+
return unless objects
|
229
|
+
if typeof objects is 'string'
|
230
|
+
objects = JSON.parse(objects)
|
231
|
+
if isArray(objects)
|
232
|
+
(new @(value) for value in objects)
|
233
|
+
else
|
234
|
+
new @(objects)
|
235
|
+
|
236
|
+
@fromForm: ->
|
237
|
+
(new this).fromForm(arguments...)
|
238
|
+
|
239
|
+
@sort: ->
|
240
|
+
if @comparator
|
241
|
+
@records.sort @comparator
|
242
|
+
@records
|
243
|
+
|
244
|
+
# Private
|
245
|
+
|
246
|
+
@cloneArray: (array) ->
|
247
|
+
(value.clone() for value in array)
|
248
|
+
|
249
|
+
@idCounter: 0
|
250
|
+
|
251
|
+
@uid: (prefix = '') ->
|
252
|
+
uid = prefix + @idCounter++
|
253
|
+
uid = @uid(prefix) if @exists(uid)
|
254
|
+
uid
|
255
|
+
|
256
|
+
# Instance
|
257
|
+
|
258
|
+
constructor: (atts) ->
|
259
|
+
super
|
260
|
+
@load atts if atts
|
261
|
+
@cid = @constructor.uid('c-')
|
262
|
+
|
263
|
+
isNew: ->
|
264
|
+
not @exists()
|
265
|
+
|
266
|
+
isValid: ->
|
267
|
+
not @validate()
|
268
|
+
|
269
|
+
validate: ->
|
270
|
+
|
271
|
+
load: (atts) ->
|
272
|
+
if atts.id then @id = atts.id
|
273
|
+
for key, value of atts
|
274
|
+
if atts.hasOwnProperty(key) and typeof @[key] is 'function'
|
275
|
+
@[key](value)
|
276
|
+
else
|
277
|
+
@[key] = value
|
278
|
+
this
|
279
|
+
|
280
|
+
attributes: ->
|
281
|
+
result = {}
|
282
|
+
for key in @constructor.attributes when key of this
|
283
|
+
if typeof @[key] is 'function'
|
284
|
+
result[key] = @[key]()
|
285
|
+
else
|
286
|
+
result[key] = @[key]
|
287
|
+
result.id = @id if @id
|
288
|
+
result
|
289
|
+
|
290
|
+
eql: (rec) ->
|
291
|
+
!!(rec and rec.constructor is @constructor and
|
292
|
+
(rec.cid is @cid) or (rec.id and rec.id is @id))
|
293
|
+
|
294
|
+
save: (options = {}) ->
|
295
|
+
unless options.validate is false
|
296
|
+
error = @validate()
|
297
|
+
if error
|
298
|
+
@trigger('error', error)
|
299
|
+
return false
|
300
|
+
|
301
|
+
@trigger('beforeSave', options)
|
302
|
+
record = if @isNew() then @create(options) else @update(options)
|
303
|
+
@stripCloneAttrs()
|
304
|
+
@trigger('save', options)
|
305
|
+
record
|
306
|
+
|
307
|
+
stripCloneAttrs: ->
|
308
|
+
return if @hasOwnProperty 'cid' # Make sure it's not the raw object
|
309
|
+
for own key, value of @
|
310
|
+
delete @[key] if @constructor.attributes.indexOf(key) > -1
|
311
|
+
this
|
312
|
+
|
313
|
+
updateAttribute: (name, value, options) ->
|
314
|
+
atts = {}
|
315
|
+
atts[name] = value
|
316
|
+
@updateAttributes(atts, options)
|
317
|
+
|
318
|
+
updateAttributes: (atts, options) ->
|
319
|
+
@load(atts)
|
320
|
+
@save(options)
|
321
|
+
|
322
|
+
changeID: (id) ->
|
323
|
+
records = @constructor.irecords
|
324
|
+
records[id] = records[@id]
|
325
|
+
delete records[@id]
|
326
|
+
@id = id
|
327
|
+
@save()
|
328
|
+
|
329
|
+
destroy: (options = {}) ->
|
330
|
+
@trigger('beforeDestroy', options)
|
331
|
+
|
332
|
+
# Remove record from model
|
333
|
+
records = @constructor.records.slice(0)
|
334
|
+
for record, i in records when @eql(record)
|
335
|
+
records.splice(i, 1)
|
336
|
+
break
|
337
|
+
@constructor.records = records
|
338
|
+
|
339
|
+
# Remove the ID and CID
|
340
|
+
delete @constructor.irecords[@id]
|
341
|
+
delete @constructor.crecords[@cid]
|
342
|
+
|
343
|
+
@destroyed = true
|
344
|
+
@trigger('destroy', options)
|
345
|
+
@trigger('change', 'destroy', options)
|
346
|
+
if @listeningTo
|
347
|
+
@stopListening()
|
348
|
+
@unbind()
|
349
|
+
this
|
350
|
+
|
351
|
+
dup: (newRecord) ->
|
352
|
+
result = new @constructor(@attributes())
|
353
|
+
if newRecord is false
|
354
|
+
result.cid = @cid
|
355
|
+
else
|
356
|
+
delete result.id
|
357
|
+
result
|
358
|
+
|
359
|
+
clone: ->
|
360
|
+
createObject(this)
|
361
|
+
|
362
|
+
reload: ->
|
363
|
+
return this if @isNew()
|
364
|
+
original = @constructor.find(@id)
|
365
|
+
@load(original.attributes())
|
366
|
+
original
|
367
|
+
|
368
|
+
toJSON: ->
|
369
|
+
@attributes()
|
370
|
+
|
371
|
+
toString: ->
|
372
|
+
"<#{@constructor.className} (#{JSON.stringify(this)})>"
|
373
|
+
|
374
|
+
fromForm: (form) ->
|
375
|
+
result = {}
|
376
|
+
for key in $(form).serializeArray()
|
377
|
+
result[key.name] = key.value
|
378
|
+
@load(result)
|
379
|
+
|
380
|
+
exists: ->
|
381
|
+
@constructor.exists(@id)
|
382
|
+
|
383
|
+
# Private
|
384
|
+
|
385
|
+
update: (options) ->
|
386
|
+
@trigger('beforeUpdate', options)
|
387
|
+
|
388
|
+
records = @constructor.irecords
|
389
|
+
records[@id].load @attributes()
|
390
|
+
|
391
|
+
@constructor.sort()
|
392
|
+
|
393
|
+
clone = records[@id].clone()
|
394
|
+
clone.trigger('update', options)
|
395
|
+
clone.trigger('change', 'update', options)
|
396
|
+
clone
|
397
|
+
|
398
|
+
create: (options) ->
|
399
|
+
@trigger('beforeCreate', options)
|
400
|
+
@id = @cid unless @id
|
401
|
+
|
402
|
+
record = @dup(false)
|
403
|
+
@constructor.records.push(record)
|
404
|
+
@constructor.irecords[@id] = record
|
405
|
+
@constructor.crecords[@cid] = record
|
406
|
+
|
407
|
+
@constructor.sort()
|
408
|
+
|
409
|
+
clone = record.clone()
|
410
|
+
clone.trigger('create', options)
|
411
|
+
clone.trigger('change', 'create', options)
|
412
|
+
clone
|
413
|
+
|
414
|
+
bind: (events, callback) ->
|
415
|
+
@constructor.bind events, binder = (record) =>
|
416
|
+
if record && @eql(record)
|
417
|
+
callback.apply(this, arguments)
|
418
|
+
# create a wrapper function to be called with 'unbind' for each event
|
419
|
+
for singleEvent in events.split(' ')
|
420
|
+
do (singleEvent) =>
|
421
|
+
@constructor.bind "unbind", unbinder = (record, event, cb) =>
|
422
|
+
if record && @eql(record)
|
423
|
+
return if event and event isnt singleEvent
|
424
|
+
return if cb and cb isnt callback
|
425
|
+
@constructor.unbind(singleEvent, binder)
|
426
|
+
@constructor.unbind("unbind", unbinder)
|
427
|
+
this
|
428
|
+
|
429
|
+
one: (events, callback) ->
|
430
|
+
@bind events, handler = =>
|
431
|
+
@unbind(events, handler)
|
432
|
+
callback.apply(this, arguments)
|
433
|
+
|
434
|
+
trigger: (args...) ->
|
435
|
+
args.splice(1, 0, this)
|
436
|
+
@constructor.trigger(args...)
|
437
|
+
|
438
|
+
listenTo: (obj, events, callback) ->
|
439
|
+
obj.bind events, callback
|
440
|
+
@listeningTo or= []
|
441
|
+
@listeningTo.push(obj)
|
442
|
+
|
443
|
+
listenToOnce: (obj, events, callback) ->
|
444
|
+
listeningToOnce = @listeningToOnce or= []
|
445
|
+
listeningToOnce.push obj
|
446
|
+
obj.bind events, handler = =>
|
447
|
+
idx = listeningToOnce.indexOf(obj)
|
448
|
+
listeningToOnce.splice(idx, 1) unless idx is -1
|
449
|
+
obj.unbind(events, handler)
|
450
|
+
callback.apply(obj, arguments)
|
451
|
+
|
452
|
+
stopListening: (obj, events, callback) ->
|
453
|
+
if arguments.length is 0
|
454
|
+
retain = []
|
455
|
+
for listeningTo in [@listeningTo, @listeningToOnce]
|
456
|
+
continue unless listeningTo
|
457
|
+
for obj in @listeningTo when not (obj in retain)
|
458
|
+
obj.unbind()
|
459
|
+
retain.push(obj)
|
460
|
+
@listeningTo = undefined
|
461
|
+
@listeningToOnce = undefined
|
462
|
+
return
|
463
|
+
|
464
|
+
if obj
|
465
|
+
obj.unbind() unless events
|
466
|
+
obj.unbind(events, callback) if events
|
467
|
+
for listeningTo in [@listeningTo, @listeningToOnce]
|
468
|
+
continue unless listeningTo
|
469
|
+
idx = listeningTo.indexOf(obj)
|
470
|
+
listeningTo.splice(idx, 1) unless idx is -1
|
471
|
+
|
472
|
+
unbind: (events, callback) ->
|
473
|
+
if arguments.length is 0
|
474
|
+
@trigger('unbind')
|
475
|
+
else if events
|
476
|
+
for event in events.split(' ')
|
477
|
+
@trigger('unbind', event, callback)
|
478
|
+
|
479
|
+
Model::on = Model::bind
|
480
|
+
Model::off = Model::unbind
|
481
|
+
|
482
|
+
class Controller extends Module
|
483
|
+
@include Events
|
484
|
+
@include Log
|
485
|
+
|
486
|
+
eventSplitter: /^(\S+)\s*(.*)$/
|
487
|
+
tag: 'div'
|
488
|
+
|
489
|
+
constructor: (options) ->
|
490
|
+
@options = options
|
491
|
+
|
492
|
+
for key, value of @options
|
493
|
+
@[key] = value
|
494
|
+
|
495
|
+
@el = document.createElement(@tag) unless @el
|
496
|
+
@el = $(@el)
|
497
|
+
@$el = @el
|
498
|
+
|
499
|
+
@el.addClass(@className) if @className
|
500
|
+
@el.attr(@attributes) if @attributes
|
501
|
+
|
502
|
+
@events = @constructor.events unless @events
|
503
|
+
@elements = @constructor.elements unless @elements
|
504
|
+
|
505
|
+
context = @
|
506
|
+
while parent_prototype = context.constructor.__super__
|
507
|
+
@events = $.extend({}, parent_prototype.events, @events) if parent_prototype.events
|
508
|
+
@elements = $.extend({}, parent_prototype.elements, @elements) if parent_prototype.elements
|
509
|
+
context = parent_prototype
|
510
|
+
|
511
|
+
@delegateEvents(@events) if @events
|
512
|
+
@refreshElements() if @elements
|
513
|
+
|
514
|
+
super
|
515
|
+
|
516
|
+
release: =>
|
517
|
+
@trigger 'release', this
|
518
|
+
@el.remove()
|
519
|
+
@unbind()
|
520
|
+
@stopListening()
|
521
|
+
|
522
|
+
$: (selector) -> $(selector, @el)
|
523
|
+
|
524
|
+
delegateEvents: (events) ->
|
525
|
+
for key, method of events
|
526
|
+
|
527
|
+
if typeof(method) is 'function'
|
528
|
+
# Always return true from event handlers
|
529
|
+
method = do (method) => =>
|
530
|
+
method.apply(this, arguments)
|
531
|
+
true
|
532
|
+
else
|
533
|
+
unless @[method]
|
534
|
+
throw new Error("#{method} doesn't exist")
|
535
|
+
|
536
|
+
method = do (method) => =>
|
537
|
+
@[method].apply(this, arguments)
|
538
|
+
true
|
539
|
+
|
540
|
+
match = key.match(@eventSplitter)
|
541
|
+
eventName = match[1]
|
542
|
+
selector = match[2]
|
543
|
+
|
544
|
+
if selector is ''
|
545
|
+
@el.bind(eventName, method)
|
546
|
+
else
|
547
|
+
@el.delegate(selector, eventName, method)
|
548
|
+
|
549
|
+
refreshElements: ->
|
550
|
+
for key, value of @elements
|
551
|
+
@[value] = @$(key)
|
552
|
+
|
553
|
+
delay: (func, timeout) ->
|
554
|
+
setTimeout(@proxy(func), timeout || 0)
|
555
|
+
|
556
|
+
html: (element) ->
|
557
|
+
@el.html(element.el or element)
|
558
|
+
@refreshElements()
|
559
|
+
@el
|
560
|
+
|
561
|
+
append: (elements...) ->
|
562
|
+
elements = (e.el or e for e in elements)
|
563
|
+
@el.append(elements...)
|
564
|
+
@refreshElements()
|
565
|
+
@el
|
566
|
+
|
567
|
+
appendTo: (element) ->
|
568
|
+
@el.appendTo(element.el or element)
|
569
|
+
@refreshElements()
|
570
|
+
@el
|
571
|
+
|
572
|
+
prepend: (elements...) ->
|
573
|
+
elements = (e.el or e for e in elements)
|
574
|
+
@el.prepend(elements...)
|
575
|
+
@refreshElements()
|
576
|
+
@el
|
577
|
+
|
578
|
+
replace: (element) ->
|
579
|
+
[previous, @el] = [@el, $(element.el or element)]
|
580
|
+
previous.replaceWith(@el)
|
581
|
+
@delegateEvents(@events)
|
582
|
+
@refreshElements()
|
583
|
+
@el
|
584
|
+
|
585
|
+
# Utilities & Shims
|
586
|
+
|
587
|
+
$ = window?.jQuery or window?.Zepto or (element) -> element
|
588
|
+
|
589
|
+
createObject = Object.create or (o) ->
|
590
|
+
Func = ->
|
591
|
+
Func.prototype = o
|
592
|
+
new Func()
|
593
|
+
|
594
|
+
isArray = (value) ->
|
595
|
+
Object::toString.call(value) is '[object Array]'
|
596
|
+
|
597
|
+
isBlank = (value) ->
|
598
|
+
return true unless value
|
599
|
+
return false for key of value
|
600
|
+
true
|
601
|
+
|
602
|
+
makeArray = (args) ->
|
603
|
+
Array::slice.call(args, 0)
|
604
|
+
|
605
|
+
# Globals
|
606
|
+
|
607
|
+
Spine = @Spine = {}
|
608
|
+
module?.exports = Spine
|
609
|
+
|
610
|
+
Spine.version = '1.1.0'
|
611
|
+
Spine.isArray = isArray
|
612
|
+
Spine.isBlank = isBlank
|
613
|
+
Spine.$ = $
|
614
|
+
Spine.Events = Events
|
615
|
+
Spine.Log = Log
|
616
|
+
Spine.Module = Module
|
617
|
+
Spine.Controller = Controller
|
618
|
+
Spine.Model = Model
|
619
|
+
|
620
|
+
# Global events
|
621
|
+
|
622
|
+
Module.extend.call(Spine, Events)
|
623
|
+
|
624
|
+
# JavaScript compatability
|
625
|
+
|
626
|
+
Module.create = Module.sub =
|
627
|
+
Controller.create = Controller.sub =
|
628
|
+
Model.sub = (instances, statics) ->
|
629
|
+
class Result extends this
|
630
|
+
Result.include(instances) if instances
|
631
|
+
Result.extend(statics) if statics
|
632
|
+
Result.unbind?()
|
633
|
+
Result
|
634
|
+
|
635
|
+
Model.setup = (name, attributes = []) ->
|
636
|
+
class Instance extends this
|
637
|
+
Instance.configure(name, attributes...)
|
638
|
+
Instance
|
639
|
+
|
640
|
+
Spine.Class = Module
|