flipper-ui 0.2.0.beta1

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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +17 -0
  5. data/Guardfile +26 -0
  6. data/LICENSE +22 -0
  7. data/README.md +101 -0
  8. data/Rakefile +7 -0
  9. data/examples/basic.ru +44 -0
  10. data/examples/flipper.html +14 -0
  11. data/examples/flipper.png +0 -0
  12. data/flipper-ui.gemspec +21 -0
  13. data/lib/flipper-ui.rb +1 -0
  14. data/lib/flipper/ui.rb +23 -0
  15. data/lib/flipper/ui/action.rb +172 -0
  16. data/lib/flipper/ui/action_collection.rb +20 -0
  17. data/lib/flipper/ui/actions/features.rb +21 -0
  18. data/lib/flipper/ui/actions/file.rb +17 -0
  19. data/lib/flipper/ui/actions/gate.rb +143 -0
  20. data/lib/flipper/ui/actions/index.rb +17 -0
  21. data/lib/flipper/ui/assets/javascripts/application.coffee +305 -0
  22. data/lib/flipper/ui/assets/javascripts/spine/ajax.coffee +223 -0
  23. data/lib/flipper/ui/assets/javascripts/spine/list.coffee +43 -0
  24. data/lib/flipper/ui/assets/javascripts/spine/local.coffee +16 -0
  25. data/lib/flipper/ui/assets/javascripts/spine/manager.coffee +83 -0
  26. data/lib/flipper/ui/assets/javascripts/spine/relation.coffee +148 -0
  27. data/lib/flipper/ui/assets/javascripts/spine/route.coffee +146 -0
  28. data/lib/flipper/ui/assets/javascripts/spine/spine.coffee +542 -0
  29. data/lib/flipper/ui/assets/javascripts/spine/version +1 -0
  30. data/lib/flipper/ui/assets/stylesheets/application.scss +237 -0
  31. data/lib/flipper/ui/decorators/feature.rb +37 -0
  32. data/lib/flipper/ui/decorators/gate.rb +36 -0
  33. data/lib/flipper/ui/error.rb +10 -0
  34. data/lib/flipper/ui/eruby.rb +11 -0
  35. data/lib/flipper/ui/middleware.rb +66 -0
  36. data/lib/flipper/ui/public/css/application.css +183 -0
  37. data/lib/flipper/ui/public/css/images/animated-overlay.gif +0 -0
  38. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  39. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  40. data/lib/flipper/ui/public/css/images/ui-bg_flat_10_000000_40x100.png +0 -0
  41. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  42. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  43. data/lib/flipper/ui/public/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  44. data/lib/flipper/ui/public/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  45. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  46. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  47. data/lib/flipper/ui/public/css/images/ui-icons_222222_256x240.png +0 -0
  48. data/lib/flipper/ui/public/css/images/ui-icons_228ef1_256x240.png +0 -0
  49. data/lib/flipper/ui/public/css/images/ui-icons_ef8c08_256x240.png +0 -0
  50. data/lib/flipper/ui/public/css/images/ui-icons_ffd27a_256x240.png +0 -0
  51. data/lib/flipper/ui/public/css/images/ui-icons_ffffff_256x240.png +0 -0
  52. data/lib/flipper/ui/public/css/jquery-ui-1.10.3.slider.min.css +5 -0
  53. data/lib/flipper/ui/public/images/logo.png +0 -0
  54. data/lib/flipper/ui/public/images/remove.png +0 -0
  55. data/lib/flipper/ui/public/js/application.js +544 -0
  56. data/lib/flipper/ui/public/js/handlebars.js +1992 -0
  57. data/lib/flipper/ui/public/js/jquery-ui-1.10.3.slider.min.js +6 -0
  58. data/lib/flipper/ui/public/js/jquery.js +9555 -0
  59. data/lib/flipper/ui/public/js/jquery.min.js +4 -0
  60. data/lib/flipper/ui/public/js/jquery.min.map +1 -0
  61. data/lib/flipper/ui/public/js/spine/ajax.js +320 -0
  62. data/lib/flipper/ui/public/js/spine/list.js +72 -0
  63. data/lib/flipper/ui/public/js/spine/local.js +29 -0
  64. data/lib/flipper/ui/public/js/spine/manager.js +157 -0
  65. data/lib/flipper/ui/public/js/spine/relation.js +260 -0
  66. data/lib/flipper/ui/public/js/spine/route.js +223 -0
  67. data/lib/flipper/ui/public/js/spine/spine.js +927 -0
  68. data/lib/flipper/ui/util.rb +12 -0
  69. data/lib/flipper/ui/version.rb +5 -0
  70. data/lib/flipper/ui/views/index.erb +9 -0
  71. data/lib/flipper/ui/views/layout.erb +161 -0
  72. data/script/bootstrap +21 -0
  73. data/script/server +19 -0
  74. data/script/test +30 -0
  75. data/spec/flipper/ui/decorators/feature_spec.rb +59 -0
  76. data/spec/flipper/ui/decorators/gate_spec.rb +47 -0
  77. data/spec/flipper/ui/util_spec.rb +18 -0
  78. data/spec/flipper/ui_spec.rb +470 -0
  79. data/spec/helper.rb +35 -0
  80. metadata +168 -0
@@ -0,0 +1,146 @@
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 and @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
+ return if @options.shim
43
+
44
+ if @history
45
+ $(window).unbind('popstate', @change)
46
+ else
47
+ $(window).unbind('hashchange', @change)
48
+
49
+ @navigate: (args...) ->
50
+ options = {}
51
+
52
+ lastArg = args[args.length - 1]
53
+ if typeof lastArg is 'object'
54
+ options = args.pop()
55
+ else if typeof lastArg is 'boolean'
56
+ options.trigger = args.pop()
57
+
58
+ options = $.extend({}, @options, options)
59
+
60
+ path = args.join('/')
61
+ return if @path is path
62
+ @path = path
63
+
64
+ @trigger('navigate', @path)
65
+
66
+ @matchRoute(@path, options) if options.trigger
67
+
68
+ return if options.shim
69
+
70
+ if @history
71
+ history.pushState({}, document.title, @path)
72
+ else
73
+ window.location.hash = @path
74
+
75
+ # Private
76
+
77
+ @getPath: ->
78
+ path = window.location.pathname
79
+ if path.substr(0,1) isnt '/'
80
+ path = '/' + path
81
+ path
82
+
83
+ @getHash: -> window.location.hash
84
+
85
+ @getFragment: -> @getHash().replace(hashStrip, '')
86
+
87
+ @getHost: ->
88
+ (document.location + '').replace(@getPath() + @getHash(), '')
89
+
90
+ @change: ->
91
+ path = if @history then @getPath() else @getFragment()
92
+ return if path is @path
93
+ @path = path
94
+ @matchRoute(@path)
95
+
96
+ @matchRoute: (path, options) ->
97
+ for route in @routes when route.match(path, options)
98
+ @trigger('change', route, path)
99
+ return route
100
+
101
+ constructor: (@path, @callback) ->
102
+ @names = []
103
+
104
+ if typeof path is 'string'
105
+ namedParam.lastIndex = 0
106
+ while (match = namedParam.exec(path)) != null
107
+ @names.push(match[1])
108
+
109
+ splatParam.lastIndex = 0
110
+ while (match = splatParam.exec(path)) != null
111
+ @names.push(match[1])
112
+
113
+ path = path.replace(escapeRegExp, '\\$&')
114
+ .replace(namedParam, '([^\/]*)')
115
+ .replace(splatParam, '(.*?)')
116
+
117
+ @route = new RegExp("^#{path}$")
118
+ else
119
+ @route = path
120
+
121
+ match: (path, options = {}) ->
122
+ match = @route.exec(path)
123
+ return false unless match
124
+ options.match = match
125
+ params = match.slice(1)
126
+
127
+ if @names.length
128
+ for param, i in params
129
+ options[@names[i]] = param
130
+
131
+ @callback.call(null, options) isnt false
132
+
133
+ # Coffee-script bug
134
+ Spine.Route.change = Spine.Route.proxy(Spine.Route.change)
135
+
136
+ Spine.Controller.include
137
+ route: (path, callback) ->
138
+ Spine.Route.add(path, @proxy(callback))
139
+
140
+ routes: (routes) ->
141
+ @route(key, value) for key, value of routes
142
+
143
+ navigate: ->
144
+ Spine.Route.navigate.apply(Spine.Route, arguments)
145
+
146
+ module?.exports = Spine.Route
@@ -0,0 +1,542 @@
1
+ Events =
2
+ bind: (ev, callback) ->
3
+ evs = ev.split(' ')
4
+ calls = @hasOwnProperty('_callbacks') and @_callbacks or= {}
5
+
6
+ for name in evs
7
+ calls[name] or= []
8
+ calls[name].push(callback)
9
+ this
10
+
11
+ one: (ev, callback) ->
12
+ @bind ev, ->
13
+ @unbind(ev, arguments.callee)
14
+ callback.apply(this, arguments)
15
+
16
+ trigger: (args...) ->
17
+ ev = args.shift()
18
+
19
+ list = @hasOwnProperty('_callbacks') and @_callbacks?[ev]
20
+ return unless list
21
+
22
+ for callback in list
23
+ if callback.apply(this, args) is false
24
+ break
25
+ true
26
+
27
+ unbind: (ev, callback) ->
28
+ unless ev
29
+ @_callbacks = {}
30
+ return this
31
+
32
+ list = @_callbacks?[ev]
33
+ return this unless list
34
+
35
+ unless callback
36
+ delete @_callbacks[ev]
37
+ return this
38
+
39
+ for cb, i in list when cb is callback
40
+ list = list.slice()
41
+ list.splice(i, 1)
42
+ @_callbacks[ev] = list
43
+ break
44
+ this
45
+
46
+ Log =
47
+ trace: true
48
+
49
+ logPrefix: '(App)'
50
+
51
+ log: (args...) ->
52
+ return unless @trace
53
+ if @logPrefix then args.unshift(@logPrefix)
54
+ console?.log?(args...)
55
+ this
56
+
57
+ moduleKeywords = ['included', 'extended']
58
+
59
+ class Module
60
+ @include: (obj) ->
61
+ throw new Error('include(obj) requires obj') unless obj
62
+ for key, value of obj when key not in moduleKeywords
63
+ @::[key] = value
64
+ obj.included?.apply(this)
65
+ this
66
+
67
+ @extend: (obj) ->
68
+ throw new Error('extend(obj) requires obj') unless obj
69
+ for key, value of obj when key not in moduleKeywords
70
+ @[key] = value
71
+ obj.extended?.apply(this)
72
+ this
73
+
74
+ @proxy: (func) ->
75
+ => func.apply(this, arguments)
76
+
77
+ proxy: (func) ->
78
+ => func.apply(this, arguments)
79
+
80
+ constructor: ->
81
+ @init?(arguments...)
82
+
83
+ class Model extends Module
84
+ @extend Events
85
+
86
+ @records: {}
87
+ @crecords: {}
88
+ @attributes: []
89
+
90
+ @configure: (name, attributes...) ->
91
+ @className = name
92
+ @records = {}
93
+ @crecords = {}
94
+ @attributes = attributes if attributes.length
95
+ @attributes and= makeArray(@attributes)
96
+ @attributes or= []
97
+ @unbind()
98
+ this
99
+
100
+ @toString: -> "#{@className}(#{@attributes.join(", ")})"
101
+
102
+ @find: (id) ->
103
+ record = @records[id]
104
+ if !record and ("#{id}").match(/c-\d+/)
105
+ return @findCID(id)
106
+ throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record
107
+ record.clone()
108
+
109
+ @findCID: (cid) ->
110
+ record = @crecords[cid]
111
+ throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record
112
+ record.clone()
113
+
114
+ @exists: (id) ->
115
+ try
116
+ return @find(id)
117
+ catch e
118
+ return false
119
+
120
+ @refresh: (values, options = {}) ->
121
+ if options.clear
122
+ @records = {}
123
+ @crecords = {}
124
+
125
+ records = @fromJSON(values)
126
+ records = [records] unless isArray(records)
127
+
128
+ for record in records
129
+ record.id or= record.cid
130
+ @records[record.id] = record
131
+ @crecords[record.cid] = record
132
+
133
+ @trigger('refresh', @cloneArray(records))
134
+ this
135
+
136
+ @select: (callback) ->
137
+ result = (record for id, record of @records when callback(record))
138
+ @cloneArray(result)
139
+
140
+ @findByAttribute: (name, value) ->
141
+ for id, record of @records
142
+ if record[name] is value
143
+ return record.clone()
144
+ null
145
+
146
+ @findAllByAttribute: (name, value) ->
147
+ @select (item) ->
148
+ item[name] is value
149
+
150
+ @each: (callback) ->
151
+ for key, value of @records
152
+ callback(value.clone())
153
+
154
+ @all: ->
155
+ @cloneArray(@recordsValues())
156
+
157
+ @first: ->
158
+ record = @recordsValues()[0]
159
+ record?.clone()
160
+
161
+ @last: ->
162
+ values = @recordsValues()
163
+ record = values[values.length - 1]
164
+ record?.clone()
165
+
166
+ @count: ->
167
+ @recordsValues().length
168
+
169
+ @deleteAll: ->
170
+ for key, value of @records
171
+ delete @records[key]
172
+
173
+ @destroyAll: ->
174
+ for key, value of @records
175
+ @records[key].destroy()
176
+
177
+ @update: (id, atts, options) ->
178
+ @find(id).updateAttributes(atts, options)
179
+
180
+ @create: (atts, options) ->
181
+ record = new @(atts)
182
+ record.save(options)
183
+
184
+ @destroy: (id, options) ->
185
+ @find(id).destroy(options)
186
+
187
+ @change: (callbackOrParams) ->
188
+ if typeof callbackOrParams is 'function'
189
+ @bind('change', callbackOrParams)
190
+ else
191
+ @trigger('change', callbackOrParams)
192
+
193
+ @fetch: (callbackOrParams) ->
194
+ if typeof callbackOrParams is 'function'
195
+ @bind('fetch', callbackOrParams)
196
+ else
197
+ @trigger('fetch', callbackOrParams)
198
+
199
+ @toJSON: ->
200
+ @recordsValues()
201
+
202
+ @fromJSON: (objects) ->
203
+ return unless objects
204
+ if typeof objects is 'string'
205
+ objects = JSON.parse(objects)
206
+ if isArray(objects)
207
+ (new @(value) for value in objects)
208
+ else
209
+ new @(objects)
210
+
211
+ @fromForm: ->
212
+ (new this).fromForm(arguments...)
213
+
214
+ # Private
215
+
216
+ @recordsValues: ->
217
+ result = []
218
+ for key, value of @records
219
+ result.push(value)
220
+ result
221
+
222
+ @cloneArray: (array) ->
223
+ (value.clone() for value in array)
224
+
225
+ @idCounter: 0
226
+
227
+ @uid: (prefix = '') ->
228
+ uid = prefix + @idCounter++
229
+ uid = @uid(prefix) if @exists(uid)
230
+ uid
231
+
232
+ # Instance
233
+
234
+ constructor: (atts) ->
235
+ super
236
+ @load atts if atts
237
+ @cid = @constructor.uid('c-')
238
+
239
+ isNew: ->
240
+ not @exists()
241
+
242
+ isValid: ->
243
+ not @validate()
244
+
245
+ validate: ->
246
+
247
+ load: (atts) ->
248
+ for key, value of atts
249
+ if typeof @[key] is 'function'
250
+ @[key](value)
251
+ else
252
+ @[key] = value
253
+ this
254
+
255
+ attributes: ->
256
+ result = {}
257
+ for key in @constructor.attributes when key of this
258
+ if typeof @[key] is 'function'
259
+ result[key] = @[key]()
260
+ else
261
+ result[key] = @[key]
262
+ result.id = @id if @id
263
+ result
264
+
265
+ eql: (rec) ->
266
+ !!(rec and rec.constructor is @constructor and
267
+ (rec.cid is @cid) or (rec.id and rec.id is @id))
268
+
269
+ save: (options = {}) ->
270
+ unless options.validate is false
271
+ error = @validate()
272
+ if error
273
+ @trigger('error', error)
274
+ return false
275
+
276
+ @trigger('beforeSave', options)
277
+ record = if @isNew() then @create(options) else @update(options)
278
+ @stripCloneAttrs()
279
+ @trigger('save', options)
280
+ record
281
+
282
+ stripCloneAttrs: ->
283
+ return if @hasOwnProperty 'cid' # Make sure it's not the raw object
284
+ for own key, value of @
285
+ delete @[key] if @constructor.attributes.indexOf(key) > -1
286
+ this
287
+
288
+ updateAttribute: (name, value, options) ->
289
+ @[name] = value
290
+ @save(options)
291
+
292
+ updateAttributes: (atts, options) ->
293
+ @load(atts)
294
+ @save(options)
295
+
296
+ changeID: (id) ->
297
+ records = @constructor.records
298
+ records[id] = records[@id]
299
+ delete records[@id]
300
+ @id = id
301
+ @save()
302
+
303
+ destroy: (options = {}) ->
304
+ @trigger('beforeDestroy', options)
305
+ delete @constructor.records[@id]
306
+ delete @constructor.crecords[@cid]
307
+ @destroyed = true
308
+ @trigger('destroy', options)
309
+ @trigger('change', 'destroy', options)
310
+ @unbind()
311
+ this
312
+
313
+ dup: (newRecord) ->
314
+ result = new @constructor(@attributes())
315
+ if newRecord is false
316
+ result.cid = @cid
317
+ else
318
+ delete result.id
319
+ result
320
+
321
+ clone: ->
322
+ createObject(this)
323
+
324
+ reload: ->
325
+ return this if @isNew()
326
+ original = @constructor.find(@id)
327
+ @load(original.attributes())
328
+ original
329
+
330
+ toJSON: ->
331
+ @attributes()
332
+
333
+ toString: ->
334
+ "<#{@constructor.className} (#{JSON.stringify(this)})>"
335
+
336
+ fromForm: (form) ->
337
+ result = {}
338
+ for key in $(form).serializeArray()
339
+ result[key.name] = key.value
340
+ @load(result)
341
+
342
+ exists: ->
343
+ @id && @id of @constructor.records
344
+
345
+ # Private
346
+
347
+ update: (options) ->
348
+ @trigger('beforeUpdate', options)
349
+ records = @constructor.records
350
+ records[@id].load @attributes()
351
+ clone = records[@id].clone()
352
+ clone.trigger('update', options)
353
+ clone.trigger('change', 'update', options)
354
+ clone
355
+
356
+ create: (options) ->
357
+ @trigger('beforeCreate', options)
358
+ @id = @cid unless @id
359
+
360
+ record = @dup(false)
361
+ @constructor.records[@id] = record
362
+ @constructor.crecords[@cid] = record
363
+
364
+ clone = record.clone()
365
+ clone.trigger('create', options)
366
+ clone.trigger('change', 'create', options)
367
+ clone
368
+
369
+ bind: (events, callback) ->
370
+ @constructor.bind events, binder = (record) =>
371
+ if record && @eql(record)
372
+ callback.apply(this, arguments)
373
+ @constructor.bind 'unbind', unbinder = (record) =>
374
+ if record && @eql(record)
375
+ @constructor.unbind(events, binder)
376
+ @constructor.unbind('unbind', unbinder)
377
+ binder
378
+
379
+ one: (events, callback) ->
380
+ binder = @bind events, =>
381
+ @constructor.unbind(events, binder)
382
+ callback.apply(this, arguments)
383
+
384
+ trigger: (args...) ->
385
+ args.splice(1, 0, this)
386
+ @constructor.trigger(args...)
387
+
388
+ unbind: ->
389
+ @trigger('unbind')
390
+
391
+ class Controller extends Module
392
+ @include Events
393
+ @include Log
394
+
395
+ eventSplitter: /^(\S+)\s*(.*)$/
396
+ tag: 'div'
397
+
398
+ constructor: (options) ->
399
+ @options = options
400
+
401
+ for key, value of @options
402
+ @[key] = value
403
+
404
+ @el = document.createElement(@tag) unless @el
405
+ @el = $(@el)
406
+ @$el = @el
407
+
408
+ @el.addClass(@className) if @className
409
+ @el.attr(@attributes) if @attributes
410
+
411
+ @events = @constructor.events unless @events
412
+ @elements = @constructor.elements unless @elements
413
+
414
+ @delegateEvents(@events) if @events
415
+ @refreshElements() if @elements
416
+
417
+ super
418
+
419
+ release: =>
420
+ @trigger 'release'
421
+ @el.remove()
422
+ @unbind()
423
+
424
+ $: (selector) -> $(selector, @el)
425
+
426
+ delegateEvents: (events) ->
427
+ for key, method of events
428
+
429
+ if typeof(method) is 'function'
430
+ # Always return true from event handlers
431
+ method = do (method) => =>
432
+ method.apply(this, arguments)
433
+ true
434
+ else
435
+ unless @[method]
436
+ throw new Error("#{method} doesn't exist")
437
+
438
+ method = do (method) => =>
439
+ @[method].apply(this, arguments)
440
+ true
441
+
442
+ match = key.match(@eventSplitter)
443
+ eventName = match[1]
444
+ selector = match[2]
445
+
446
+ if selector is ''
447
+ @el.bind(eventName, method)
448
+ else
449
+ @el.delegate(selector, eventName, method)
450
+
451
+ refreshElements: ->
452
+ for key, value of @elements
453
+ @[value] = @$(key)
454
+
455
+ delay: (func, timeout) ->
456
+ setTimeout(@proxy(func), timeout || 0)
457
+
458
+ html: (element) ->
459
+ @el.html(element.el or element)
460
+ @refreshElements()
461
+ @el
462
+
463
+ append: (elements...) ->
464
+ elements = (e.el or e for e in elements)
465
+ @el.append(elements...)
466
+ @refreshElements()
467
+ @el
468
+
469
+ appendTo: (element) ->
470
+ @el.appendTo(element.el or element)
471
+ @refreshElements()
472
+ @el
473
+
474
+ prepend: (elements...) ->
475
+ elements = (e.el or e for e in elements)
476
+ @el.prepend(elements...)
477
+ @refreshElements()
478
+ @el
479
+
480
+ replace: (element) ->
481
+ [previous, @el] = [@el, $(element.el or element)]
482
+ previous.replaceWith(@el)
483
+ @delegateEvents(@events)
484
+ @refreshElements()
485
+ @el
486
+
487
+ # Utilities & Shims
488
+
489
+ $ = window?.jQuery or window?.Zepto or (element) -> element
490
+
491
+ createObject = Object.create or (o) ->
492
+ Func = ->
493
+ Func.prototype = o
494
+ new Func()
495
+
496
+ isArray = (value) ->
497
+ Object::toString.call(value) is '[object Array]'
498
+
499
+ isBlank = (value) ->
500
+ return true unless value
501
+ return false for key of value
502
+ true
503
+
504
+ makeArray = (args) ->
505
+ Array::slice.call(args, 0)
506
+
507
+ # Globals
508
+
509
+ Spine = @Spine = {}
510
+ module?.exports = Spine
511
+
512
+ Spine.version = '1.0.8'
513
+ Spine.isArray = isArray
514
+ Spine.isBlank = isBlank
515
+ Spine.$ = $
516
+ Spine.Events = Events
517
+ Spine.Log = Log
518
+ Spine.Module = Module
519
+ Spine.Controller = Controller
520
+ Spine.Model = Model
521
+
522
+ # Global events
523
+
524
+ Module.extend.call(Spine, Events)
525
+
526
+ # JavaScript compatability
527
+
528
+ Module.create = Module.sub =
529
+ Controller.create = Controller.sub =
530
+ Model.sub = (instances, statics) ->
531
+ class Result extends this
532
+ Result.include(instances) if instances
533
+ Result.extend(statics) if statics
534
+ Result.unbind?()
535
+ Result
536
+
537
+ Model.setup = (name, attributes = []) ->
538
+ class Instance extends this
539
+ Instance.configure(name, attributes...)
540
+ Instance
541
+
542
+ Spine.Class = Module