helios_aim 0.2.2

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 (79) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +173 -0
  4. data/LICENSE +19 -0
  5. data/README.md +363 -0
  6. data/Rakefile +10 -0
  7. data/bin/helios +20 -0
  8. data/helios_aim.gemspec +47 -0
  9. data/lib/helios.rb +27 -0
  10. data/lib/helios/backend.rb +64 -0
  11. data/lib/helios/backend/data.rb +40 -0
  12. data/lib/helios/backend/in-app-purchase.rb +43 -0
  13. data/lib/helios/backend/newsstand.rb +97 -0
  14. data/lib/helios/backend/passbook.rb +41 -0
  15. data/lib/helios/backend/push-notification.rb +110 -0
  16. data/lib/helios/commands.rb +4 -0
  17. data/lib/helios/commands/console.rb +21 -0
  18. data/lib/helios/commands/link.rb +19 -0
  19. data/lib/helios/commands/new.rb +76 -0
  20. data/lib/helios/commands/server.rb +52 -0
  21. data/lib/helios/frontend.rb +66 -0
  22. data/lib/helios/frontend/fonts/icons.eot +0 -0
  23. data/lib/helios/frontend/fonts/icons.ttf +0 -0
  24. data/lib/helios/frontend/fonts/icons.woff +0 -0
  25. data/lib/helios/frontend/images/bg.jpg +0 -0
  26. data/lib/helios/frontend/images/helios.svg +33 -0
  27. data/lib/helios/frontend/javascripts/helios.coffee +72 -0
  28. data/lib/helios/frontend/javascripts/helios/collections.coffee +99 -0
  29. data/lib/helios/frontend/javascripts/helios/models.coffee +23 -0
  30. data/lib/helios/frontend/javascripts/helios/router.coffee +50 -0
  31. data/lib/helios/frontend/javascripts/helios/views.coffee +307 -0
  32. data/lib/helios/frontend/javascripts/vendor/backbone.datagrid.js +662 -0
  33. data/lib/helios/frontend/javascripts/vendor/backbone.js +1487 -0
  34. data/lib/helios/frontend/javascripts/vendor/backbone.paginator.js +1046 -0
  35. data/lib/helios/frontend/javascripts/vendor/codemirror.javascript.js +411 -0
  36. data/lib/helios/frontend/javascripts/vendor/codemirror.js +3047 -0
  37. data/lib/helios/frontend/javascripts/vendor/date.js +104 -0
  38. data/lib/helios/frontend/javascripts/vendor/foundation.js +331 -0
  39. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.alerts.js +50 -0
  40. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.clearing.js +478 -0
  41. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.cookie.js +74 -0
  42. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.dropdown.js +122 -0
  43. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.forms.js +403 -0
  44. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.joyride.js +613 -0
  45. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.magellan.js +130 -0
  46. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.orbit.js +355 -0
  47. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.placeholder.js +159 -0
  48. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.reveal.js +272 -0
  49. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.section.js +183 -0
  50. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.tooltips.js +195 -0
  51. data/lib/helios/frontend/javascripts/vendor/foundation/foundation.topbar.js +208 -0
  52. data/lib/helios/frontend/javascripts/vendor/jquery.js +9597 -0
  53. data/lib/helios/frontend/javascripts/vendor/jquery/jquery.fileupload-ui.js +807 -0
  54. data/lib/helios/frontend/javascripts/vendor/jquery/jquery.fileupload.js +1201 -0
  55. data/lib/helios/frontend/javascripts/vendor/jquery/jquery.ui.widget.js +530 -0
  56. data/lib/helios/frontend/javascripts/vendor/linkheaders.js +117 -0
  57. data/lib/helios/frontend/javascripts/vendor/underscore.js +1227 -0
  58. data/lib/helios/frontend/stylesheets/_codemirror.sass +219 -0
  59. data/lib/helios/frontend/stylesheets/_fonts.sass +80 -0
  60. data/lib/helios/frontend/stylesheets/_iphone.sass +141 -0
  61. data/lib/helios/frontend/stylesheets/_settings.scss +989 -0
  62. data/lib/helios/frontend/stylesheets/screen.sass +187 -0
  63. data/lib/helios/frontend/templates/data/entities.jst.tpl +11 -0
  64. data/lib/helios/frontend/templates/in-app-purchase/receipts.jst.tpl +11 -0
  65. data/lib/helios/frontend/templates/navigation.jst.tpl +31 -0
  66. data/lib/helios/frontend/templates/newsstand/issues.jst.tpl +16 -0
  67. data/lib/helios/frontend/templates/newsstand/new.jst.tpl +28 -0
  68. data/lib/helios/frontend/templates/passbook/passes.jst.tpl +11 -0
  69. data/lib/helios/frontend/templates/push-notification/compose.jst.tpl +70 -0
  70. data/lib/helios/frontend/templates/push-notification/devices.jst.tpl +17 -0
  71. data/lib/helios/frontend/views/index.haml +22 -0
  72. data/lib/helios/templates/.env.erb +1 -0
  73. data/lib/helios/templates/.gitignore +3 -0
  74. data/lib/helios/templates/Gemfile.erb +10 -0
  75. data/lib/helios/templates/Procfile.erb +1 -0
  76. data/lib/helios/templates/README.md.erb +4 -0
  77. data/lib/helios/templates/config.ru.erb +11 -0
  78. data/lib/helios/version.rb +3 -0
  79. metadata +475 -0
@@ -0,0 +1,23 @@
1
+ class Helios.Models.Entity extends Backbone.Model
2
+ idAttribute: "name"
3
+ url: ->
4
+ @get('resources').url.replace(/^\//, 'data/')
5
+
6
+ parse: (response) ->
7
+ response.resources = new Helios.Collections.Resources()
8
+ response.resources.url = response.url
9
+ response
10
+
11
+ class Helios.Models.Resource extends Backbone.Model
12
+ idAttribute: "url"
13
+
14
+ class Helios.Models.Device extends Backbone.Model
15
+ idAttribute: "token"
16
+
17
+ class Helios.Models.Receipt extends Backbone.Model
18
+ idAttribute: "transaction_id"
19
+
20
+ class Helios.Models.Pass extends Backbone.Model
21
+
22
+ class Helios.Models.Issue extends Backbone.Model
23
+ idAttribute: "name"
@@ -0,0 +1,50 @@
1
+ class Helios.Routers.Root extends Backbone.Router
2
+ el:
3
+ "div[role='main']"
4
+
5
+ initialize: (options) ->
6
+ @views = {}
7
+
8
+ @views.navigation = new Helios.Views.Navigation
9
+ @views.navigation.render()
10
+
11
+ super
12
+
13
+ routes:
14
+ '': 'index'
15
+ 'data': 'data'
16
+ 'push-notification': 'push_notification'
17
+ 'in-app-purchase': 'in_app_purchase'
18
+ 'passbook': 'passbook'
19
+ 'newsstand': 'newsstand'
20
+
21
+ index: ->
22
+ Helios.entities.fetch(type: 'OPTIONS')
23
+
24
+ data: ->
25
+ Helios.entities.fetch(type: 'OPTIONS')
26
+ @views.entities.render()
27
+
28
+ push_notification: ->
29
+ @devices ?= new Helios.Collections.Devices
30
+ @devices.paginator_core.url = Helios.services['push-notification'] + '/devices'
31
+ @views.devices ?= new Helios.Views.Devices(collection: @devices)
32
+ @views.devices.render()
33
+
34
+ in_app_purchase: ->
35
+ @receipts ?= new Helios.Collections.Receipts
36
+ @receipts.paginator_core.url = Helios.services['in-app-purchase'] + '/receipts'
37
+ @views.receipts ?= new Helios.Views.Receipts(collection: @receipts)
38
+ @views.receipts.render()
39
+
40
+ passbook: ->
41
+ @passes ?= new Helios.Collections.Passes
42
+ @passes.paginator_core.url = Helios.services['passbook'] + '/passes'
43
+ @views.passes ?= new Helios.Views.Passes(collection: @passes)
44
+ @views.passes.render()
45
+
46
+ newsstand: ->
47
+ @issues ?= new Helios.Collections.Issues
48
+ @issues.paginator_core.url = Helios.services['newsstand'] + '/issues'
49
+ @views.issues ?= new Helios.Views.Issues(collection: @issues)
50
+ @views.issues.render()
@@ -0,0 +1,307 @@
1
+ class Helios.Views.Navigation extends Backbone.View
2
+ template: JST['navigation']
3
+ el: "[role='navigation']"
4
+
5
+ render: =>
6
+ @$el.html(@template())
7
+
8
+ class Helios.Views.Entities extends Backbone.View
9
+ template: JST['data/entities']
10
+ el: "[role='main']"
11
+
12
+ events:
13
+ 'change #entities': ->
14
+ window.app.navigate(@$el.find("#entities").val(), {trigger: true})
15
+
16
+ initialize: ->
17
+ @collection.on 'reset', @render
18
+
19
+ render: =>
20
+ @$el.html(@template(entities: @collection))
21
+
22
+ @
23
+
24
+ class Helios.Views.Entity extends Backbone.View
25
+ el: "[role='main']"
26
+
27
+ initialize: ->
28
+ @model.on 'reset', @render
29
+
30
+ @collection = @model.get('resources')
31
+ @collection.fetch({success: @render})
32
+
33
+ render: =>
34
+ if @collection
35
+ @datagrid ?= new Backbone.Datagrid({
36
+ collection: @collection,
37
+ columns: @collection.first().attributes.keys,
38
+ paginated: true,
39
+ perPage: 20
40
+ })
41
+ @$el.find("#datagrid").html(@datagrid.el)
42
+
43
+ @
44
+
45
+ class Helios.Views.Devices extends Backbone.View
46
+ template: JST['push-notification/devices']
47
+ el: "[role='main']"
48
+
49
+ events:
50
+ 'keyup form.filter input': 'filter'
51
+
52
+ initialize: ->
53
+ @datagrid = new Backbone.Datagrid({
54
+ collection: @collection,
55
+ columns: @collection.fields,
56
+ paginated: true,
57
+ perPage: 20
58
+ })
59
+
60
+ render: =>
61
+ @$el.html(@template())
62
+
63
+ # @composeView ?= new Helios.Views.Compose()
64
+ # @composeView.render()
65
+ @$el.find("#datagrid").html(@datagrid.el)
66
+
67
+ @
68
+
69
+ filter: (e) ->
70
+ e.preventDefault()
71
+ @collection.query = $(e.target).val()
72
+ @collection.fetch()
73
+
74
+ class Helios.Views.Compose extends Backbone.View
75
+ template: JST['push-notification/compose']
76
+ el: "#compose-modal"
77
+
78
+ events:
79
+ 'submit form': 'submit'
80
+ 'click button#send': 'submit'
81
+ 'keyup textarea': 'updatePreview'
82
+ 'focus textarea': ->
83
+ @$el.find("input[type=radio][value=selected]").prop('checked',true)
84
+
85
+ initialize: ->
86
+ window.setInterval(@updateTime, 10000)
87
+
88
+ render: ->
89
+ @$el.html(@template())
90
+
91
+ @editor = CodeMirror.fromTextArea(document.getElementById("payload"), {
92
+ mode: "application/json",
93
+ theme: "solarized-dark",
94
+ tabMode: "indent",
95
+ lineNumbers : true,
96
+ matchBrackets: true
97
+ })
98
+
99
+ @updatePreview()
100
+ @updateTime()
101
+
102
+ # $.ajax("/message"
103
+ # type: "HEAD"
104
+
105
+ # error: (data, status) =>
106
+ # @disable()
107
+ # )
108
+ @
109
+
110
+ submit: ->
111
+ $form = @$el.find("form#compose")
112
+ payload = @editor.getValue()
113
+
114
+ tokens = undefined
115
+ if $("input[name='recipients']:checked").val() == "specified"
116
+ tokens = [$form.find("#tokens").val()]
117
+
118
+ $.ajax(
119
+ url:"/message"
120
+ type: "POST"
121
+ dataType: "json"
122
+ data: {
123
+ tokens: tokens,
124
+ payload: payload
125
+ }
126
+ )
127
+
128
+ beforeSend: =>
129
+ @$el.find(".alert-error, .alert-success").remove()
130
+
131
+ success: (data, status) =>
132
+ alert = """
133
+ <div class="alert alert-block alert-success">
134
+ <button type="button" class="close" data-dismiss="alert">×</button>
135
+ <h4>Push Notification Succeeded</h4>
136
+ </div>
137
+ """
138
+ @$el.prepend(alert)
139
+
140
+ error: (data, status) =>
141
+ alert = """
142
+ <div class="alert alert-block alert-error">
143
+ <button type="button" class="close" data-dismiss="alert">×</button>
144
+ <h4>Push Notification Failed</h4>
145
+ <p>#{$.parseJSON(data.responseText).error}</p>
146
+ </div>
147
+ """
148
+ @$el.prepend(alert)
149
+
150
+
151
+ disable: ->
152
+ alert = """
153
+ <div class="alert alert-block">
154
+ <button type="button" class="close" data-dismiss="alert">×</button>
155
+ <h4>Push Notification Sending Unavailable</h4>
156
+ <p>Check that Rack::PushNotification initializes with a <tt>:certificate</tt> parameter, and that the certificate exists and is readable in the location specified.</p>
157
+ </div>
158
+ """
159
+
160
+ @$el.prepend(alert)
161
+
162
+ $(".iphone").css(opacity: 0.5)
163
+
164
+ $form = @$el.find("form#compose")
165
+ $form.css(opacity: 0.5)
166
+ $form.find("input").disable()
167
+
168
+ updatePreview: ->
169
+ try
170
+ json = $.parseJSON(@editor.getValue())
171
+ if alert = json.aps.alert
172
+ $(".preview p").text(alert)
173
+
174
+ catch error
175
+ $(".alert strong").text(error.name)
176
+ $(".alert span").text(error.message)
177
+ finally
178
+ if alert? and alert.length > 0
179
+ $(".notification").show()
180
+ $(".alert").hide()
181
+ else
182
+ $(".notification").hide()
183
+ $(".alert").show()
184
+
185
+ updateTime: ->
186
+ $time = $("time")
187
+ $time.attr("datetime", Date.now().toISOString())
188
+ $time.find(".time").text(Date.now().toString("HH:mm"))
189
+ $time.find(".date").text(Date.now().toString("dddd, MMMM d"))
190
+
191
+ class Helios.Views.Receipts extends Backbone.View
192
+ template: JST['in-app-purchase/receipts']
193
+ el: "[role='main']"
194
+
195
+ events:
196
+ 'keyup form.filter input': 'filter'
197
+
198
+ initialize: ->
199
+ @datagrid = new Backbone.Datagrid({
200
+ collection: @collection,
201
+ columns: @collection.fields,
202
+ paginated: true,
203
+ perPage: 20
204
+ })
205
+
206
+ render: =>
207
+ @$el.html(@template())
208
+ @$el.find("#datagrid").html(@datagrid.el)
209
+
210
+ @
211
+
212
+ filter: (e) ->
213
+ e.preventDefault()
214
+ @collection.query = $(e.target).val()
215
+ @collection.fetch()
216
+
217
+ class Helios.Views.Passes extends Backbone.View
218
+ template: JST['passbook/passes']
219
+ el: "[role='main']"
220
+
221
+ events:
222
+ 'keyup form.filter input': 'filter'
223
+
224
+ initialize: ->
225
+ @datagrid = new Backbone.Datagrid({
226
+ collection: @collection,
227
+ columns: @collection.fields,
228
+ paginated: true,
229
+ perPage: 20
230
+ })
231
+
232
+ render: =>
233
+ @$el.html(@template())
234
+ @$el.find("#datagrid").html(@datagrid.el)
235
+
236
+ @
237
+
238
+ filter: (e) ->
239
+ e.preventDefault()
240
+ @collection.query = $(e.target).val()
241
+ @collection.fetch()
242
+
243
+ class Helios.Views.Issues extends Backbone.View
244
+ template: JST['newsstand/issues']
245
+ el: "[role='main']"
246
+
247
+ events:
248
+ 'keyup form.filter input': 'filter'
249
+
250
+ initialize: ->
251
+ @datagrid = new Backbone.Datagrid({
252
+ collection: @collection,
253
+ columns: @collection.fields,
254
+ paginated: true,
255
+ perPage: 20
256
+ })
257
+
258
+ $.ajax(
259
+ url: Helios.services['newsstand'] + "/issues/new",
260
+ type: "HEAD",
261
+ error: ->
262
+ $(".auxiliary button").prop('disabled', true)
263
+ )
264
+
265
+ render: =>
266
+ @$el.html(@template())
267
+ @$el.find("#datagrid").html(@datagrid.el)
268
+
269
+ @newView ?= new Helios.Views.NewIssue()
270
+ @newView.render()
271
+
272
+ @
273
+
274
+ filter: (e) ->
275
+ e.preventDefault()
276
+ @collection.query = $(e.target).val()
277
+ @collection.fetch()
278
+
279
+ class Helios.Views.NewIssue extends Backbone.View
280
+ template: JST['newsstand/new']
281
+ el: "#new-issue-modal"
282
+
283
+ events:
284
+ 'submit form': 'submit'
285
+ 'click button#create': 'submit'
286
+
287
+ render: ->
288
+ @$el.html(@template())
289
+
290
+ @
291
+
292
+ submit: ->
293
+ $form = @$el.find("form#new")
294
+ console.log($form.find("input[type='file']"))
295
+ $.ajax(
296
+ url: $form.attr("action")
297
+ type: "POST"
298
+ dataType: "json"
299
+ data: $form.serializeMultipart()
300
+ cache: false
301
+ contentType: false
302
+ processData: false
303
+ success: (data) ->
304
+ console.log("Success", data)
305
+ error: (data) ->
306
+ console.log("Failure", data)
307
+ )
@@ -0,0 +1,662 @@
1
+ // backbone.datagrid v0.3.2
2
+ //
3
+ // Copyright (c) 2012 Loïc Frering <loic.frering@gmail.com>
4
+ // Distributed under the MIT license
5
+
6
+ (function() {
7
+
8
+ var Datagrid = Backbone.View.extend({
9
+ initialize: function() {
10
+ this.columns = this.options.columns;
11
+ this.options = _.defaults(this.options, {
12
+ paginated: false,
13
+ page: 1,
14
+ perPage: 10,
15
+ tableClassName: 'table',
16
+ emptyMessage: '<p>No results found.</p>'
17
+ });
18
+
19
+ this.collection.on('reset', this.render, this);
20
+ this._prepare();
21
+ },
22
+
23
+ render: function() {
24
+ this.$el.empty();
25
+ this.renderTable();
26
+ if (this.options.paginated) {
27
+ this.renderPagination();
28
+ }
29
+
30
+ return this;
31
+ },
32
+
33
+ renderTable: function() {
34
+ var $table = $('<table></table>', {'class': this.options.tableClassName});
35
+ this.$el.append($table);
36
+
37
+ var header = new Header({columns: this.columns, sorter: this.sorter});
38
+ $table.append(header.render().el);
39
+
40
+ $table.append('<tbody></tbody>');
41
+
42
+ if (this.collection.isEmpty()) {
43
+ this.$el.append(this.options.emptyMessage);
44
+ } else {
45
+ this.collection.forEach(this.renderRow, this);
46
+ }
47
+ },
48
+
49
+ renderPagination: function() {
50
+ var pagination = new Pagination({pager: this.pager});
51
+ this.$el.append(pagination.render().el);
52
+ },
53
+
54
+ renderRow: function(model) {
55
+ var options = {
56
+ model: model,
57
+ columns: this.columns
58
+ };
59
+ var rowClassName = this.options.rowClassName;
60
+ if (_.isFunction(rowClassName)) {
61
+ rowClassName = rowClassName(model);
62
+ }
63
+ options.className = rowClassName;
64
+
65
+ var row = new Row(options);
66
+ this.$('tbody').append(row.render(this.columns).el);
67
+ },
68
+
69
+ refresh: function(options) {
70
+ if (this.options.paginated) {
71
+ this._page(options);
72
+ } else {
73
+ if (this.options.inMemory) {
74
+ this.collection.trigger('reset', this.collection);
75
+ if (options && options.success) {
76
+ options.success();
77
+ }
78
+ } else {
79
+ this._request(options);
80
+ }
81
+ }
82
+ },
83
+
84
+ sort: function(column, order) {
85
+ this.sorter.sort(column, order);
86
+ },
87
+
88
+ page: function(page) {
89
+ this.pager.page(page);
90
+ },
91
+
92
+ perPage: function(perPage) {
93
+ this.pager.set('perPage', perPage);
94
+ },
95
+
96
+ _sort: function() {
97
+ if (this.options.inMemory) {
98
+ this._sortInMemory();
99
+ } else {
100
+ this._sortRequest();
101
+ }
102
+ },
103
+
104
+ _sortInMemory: function() {
105
+ if (this.options.paginated) {
106
+ this._originalCollection.comparator = _.bind(this._comparator, this);
107
+ this._originalCollection.sort();
108
+ this.page(1);
109
+ } else {
110
+ this.collection.comparator = _.bind(this._comparator, this);
111
+ this.collection.sort();
112
+ }
113
+ },
114
+
115
+ _comparator: function(model1, model2) {
116
+ var columnComparator = this._comparatorForColumn(this.sorter.get('column'));
117
+ var order = columnComparator(model1, model2);
118
+ return this.sorter.sortedASC() ? order : -order;
119
+ },
120
+
121
+ _comparatorForColumn: function(column) {
122
+ var c = _.find(this.columns, function(c) {
123
+ return c.property === column || c.index === column;
124
+ });
125
+ return c ? c.comparator : undefined;
126
+ },
127
+
128
+ _sortRequest: function() {
129
+ this._request();
130
+ },
131
+
132
+ _page: function(options) {
133
+ if (this.options.inMemory) {
134
+ this._pageInMemory(options);
135
+ } else {
136
+ this._pageRequest(options);
137
+ }
138
+ },
139
+
140
+ _pageRequest: function(options) {
141
+ this._request(options);
142
+ },
143
+
144
+ _request: function(options) {
145
+ options = options || {};
146
+ var success = options.success;
147
+ var silent = options.silent;
148
+
149
+ options.data = this._getRequestData();
150
+ options.success = _.bind(function(collection) {
151
+ if (!this.columns || _.isEmpty(this.columns)) {
152
+ this._prepareColumns();
153
+ }
154
+ if (success) {
155
+ success();
156
+ }
157
+ if (this.options.paginated) {
158
+ this.pager.update(collection);
159
+ }
160
+ if (!silent) {
161
+ collection.trigger('reset', collection);
162
+ }
163
+ }, this);
164
+ options.silent = true;
165
+
166
+ this.collection.fetch(options);
167
+ },
168
+
169
+ _getRequestData: function() {
170
+ if (this.collection.data && _.isFunction(this.collection.data)) {
171
+ return this.collection.data(this.pager, this.sorter);
172
+ } else if (this.collection.data && typeof this.collection.data === 'object') {
173
+ var data = {};
174
+ _.each(this.collection.data, function(value, param) {
175
+ if (_.isFunction(value)) {
176
+ value = value(this.pager, this.sorter);
177
+ }
178
+ data[param] = value;
179
+ }, this);
180
+ return data;
181
+ } else if (this.options.paginated) {
182
+ return {
183
+ page: this.pager.get('currentPage'),
184
+ per_page: this.pager.get('perPage')
185
+ };
186
+ }
187
+
188
+ return {};
189
+ },
190
+
191
+ _pageInMemory: function(options) {
192
+ if (!this._originalCollection) {
193
+ this._originalCollection = this.collection.clone();
194
+ }
195
+
196
+ var page = this.pager.get('currentPage');
197
+ var perPage = this.pager.get('perPage');
198
+
199
+ var begin = (page - 1) * perPage;
200
+ var end = begin + perPage;
201
+
202
+ if (options && options.success) {
203
+ options.success();
204
+ }
205
+ this.pager.set('total', this._originalCollection.size());
206
+
207
+ this.collection.reset(this._originalCollection.slice(begin, end), options);
208
+ },
209
+
210
+ _prepare: function() {
211
+ this._prepareSorter();
212
+ this._preparePager();
213
+ this._prepareColumns();
214
+ this.refresh();
215
+ },
216
+
217
+ _prepareSorter: function() {
218
+ this.sorter = new Sorter();
219
+ this.sorter.on('change', function() {
220
+ this._sort(this.sorter.get('column'), this.sorter.get('order'));
221
+ }, this);
222
+ },
223
+
224
+ _preparePager: function() {
225
+ this.pager = new Pager({
226
+ currentPage: this.options.page,
227
+ perPage: this.options.perPage
228
+ });
229
+
230
+ this.pager.on('change:currentPage', function () {
231
+ this._page();
232
+ }, this);
233
+ this.pager.on('change:perPage', function() {
234
+ this.page(1);
235
+ }, this);
236
+ },
237
+
238
+ _prepareColumns: function() {
239
+ if (!this.columns || _.isEmpty(this.columns)) {
240
+ this._defaultColumns();
241
+ } else {
242
+ _.each(this.columns, function(column, i) {
243
+ this.columns[i] = this._prepareColumn(column, i);
244
+ }, this);
245
+ }
246
+ },
247
+
248
+ _prepareColumn: function(column, index) {
249
+ if (_.isString(column)) {
250
+ column = { property: column };
251
+ }
252
+ if (_.isObject(column)) {
253
+ column.index = index;
254
+ if (column.property) {
255
+ column.title = column.title || this._formatTitle(column.property);
256
+ } else if (!column.property && !column.view) {
257
+ throw new Error('Column \'' + column.title + '\' has no property and must accordingly define a custom cell view.');
258
+ }
259
+ if (this.options.inMemory && column.sortable) {
260
+ if (!column.comparator && !column.property && !column.sortedProperty) {
261
+ throw new Error('Invalid column definition: a sortable column must have a comparator, property or sortedProperty defined.');
262
+ }
263
+ column.comparator = column.comparator || this._defaultComparator(column.sortedProperty || column.property);
264
+ }
265
+ }
266
+ return column;
267
+ },
268
+
269
+ _formatTitle: function(title) {
270
+ return _.map(title.split(/\s|_/), function(word) {
271
+ return word.charAt(0).toUpperCase() + word.substr(1);
272
+ }).join(' ');
273
+ },
274
+
275
+ _defaultColumns: function() {
276
+ this.columns = [];
277
+ var model = this.collection.first(), i = 0;
278
+ if (model) {
279
+ for (var p in model.toJSON()) {
280
+ this.columns.push(this._prepareColumn(p, i++));
281
+ }
282
+ }
283
+ },
284
+
285
+ _defaultComparator: function(column) {
286
+ return function(model1, model2) {
287
+ var val1 = model1.has(column) ? model1.get(column) : '';
288
+ var val2 = model2.has(column) ? model2.get(column) : '';
289
+ return val1.localeCompare(val2);
290
+ };
291
+ }
292
+ });
293
+
294
+ var Header = Datagrid.Header = Backbone.View.extend({
295
+ tagName: 'thead',
296
+
297
+ initialize: function() {
298
+ this.columns = this.options.columns;
299
+ this.sorter = this.options.sorter;
300
+ },
301
+
302
+ render: function() {
303
+ var model = new Backbone.Model();
304
+ var headerColumn, columns = [];
305
+ _.each(this.columns, function(column, i) {
306
+ headerColumn = _.clone(column);
307
+ headerColumn.property = column.property || column.index;
308
+ headerColumn.view = column.headerView || {
309
+ type: HeaderCell,
310
+ sorter: this.sorter
311
+ };
312
+
313
+ model.set(headerColumn.property, column.title);
314
+ columns.push(headerColumn);
315
+ }, this);
316
+
317
+ var row = new Row({model: model, columns: columns, header: true});
318
+ this.$el.html(row.render().el);
319
+
320
+ return this;
321
+ }
322
+ });
323
+
324
+ var Row = Datagrid.Row = Backbone.View.extend({
325
+ tagName: 'tr',
326
+
327
+ initialize: function() {
328
+ this.columns = this.options.columns;
329
+ this.model.on('change', this.render, this);
330
+ },
331
+
332
+ render: function() {
333
+ this.$el.empty();
334
+ _.each(this.columns, this.renderCell, this);
335
+ return this;
336
+ },
337
+
338
+ renderCell: function(column) {
339
+ var cellView = this._resolveCellView(column);
340
+ this.$el.append(cellView.render().el);
341
+ },
342
+
343
+ _resolveCellView: function(column) {
344
+ var options = {
345
+ model: this.model,
346
+ column: column
347
+ };
348
+ if (this.options.header || column.header) {
349
+ options.tagName = 'th';
350
+ }
351
+ var cellClassName = column.cellClassName;
352
+ if (_.isFunction(cellClassName)) {
353
+ cellClassName = cellClassName(this.model);
354
+ }
355
+ options.className = cellClassName;
356
+
357
+
358
+ var view = column.view || Cell;
359
+
360
+ // Resolve view from string or function
361
+ if (typeof view !== 'object' && !(view.prototype && view.prototype.render)) {
362
+ if (_.isString(view)) {
363
+ options.callback = _.template(view);
364
+ view = CallbackCell;
365
+ } else if (_.isFunction(view) && !view.prototype.render) {
366
+ options.callback = view;
367
+ view = CallbackCell;
368
+ } else {
369
+ throw new TypeError('Invalid view passed to column "' + column.title + '".');
370
+ }
371
+ }
372
+
373
+ // Resolve view from options
374
+ else if (typeof view === 'object') {
375
+ _.extend(options, view);
376
+ view = view.type;
377
+ if (!view || !view.prototype || !view.prototype.render) {
378
+ throw new TypeError('Invalid view passed to column "' + column.title + '".');
379
+ }
380
+ }
381
+
382
+ return new view(options);
383
+ }
384
+ });
385
+
386
+ var Pagination = Datagrid.Pagination = Backbone.View.extend({
387
+ className: 'pagination pagination-centered',
388
+
389
+ events: {
390
+ 'click li:not(.disabled) a': 'page',
391
+ 'click li.disabled a': function(e) { e.preventDefault(); }
392
+ },
393
+
394
+ initialize: function() {
395
+ this.pager = this.options.pager;
396
+ },
397
+
398
+ render: function() {
399
+ var $ul = $('<ul></ul>'), $li;
400
+
401
+ $li = $('<li class="prev"><a href="#">«</a></li>');
402
+ if (!this.pager.hasPrev()) {
403
+ $li.addClass('disabled');
404
+ }
405
+ $ul.append($li);
406
+
407
+ if (this.pager.hasTotal()) {
408
+ for (var i = 1; i <= this.pager.get('totalPages'); i++) {
409
+ $li = $('<li></li>');
410
+ if (i === this.pager.get('currentPage')) {
411
+ $li.addClass('active');
412
+ }
413
+ $li.append('<a href="#">' + i + '</a>');
414
+ $ul.append($li);
415
+ }
416
+ }
417
+
418
+ $li = $('<li class="next"><a href="#">»</a></li>');
419
+ if (!this.pager.hasNext()) {
420
+ $li.addClass('disabled');
421
+ }
422
+ $ul.append($li);
423
+
424
+ this.$el.append($ul);
425
+ return this;
426
+ },
427
+
428
+ page: function(event) {
429
+ var $target = $(event.target), page;
430
+ if ($target.parent().hasClass('prev')) {
431
+ this.pager.prev();
432
+ } else if ($target.parent().hasClass('next')) {
433
+ this.pager.next();
434
+ }
435
+ else {
436
+ this.pager.page(parseInt($(event.target).html(), 10));
437
+ }
438
+ return false;
439
+ }
440
+ });
441
+
442
+ var Cell = Datagrid.Cell = Backbone.View.extend({
443
+ tagName: 'td',
444
+
445
+ initialize: function() {
446
+ this.column = this.options.column;
447
+ },
448
+
449
+ render: function() {
450
+ this._prepareValue();
451
+ this.$el.html(this.value);
452
+ return this;
453
+ },
454
+
455
+ _prepareValue: function() {
456
+ this.value = this.model.get(this.column.property);
457
+
458
+ if (this.value && this.value.length > 32) {
459
+ this.value = "<div>" + this.value + "</div>";
460
+ }
461
+ }
462
+ });
463
+
464
+ var CallbackCell = Datagrid.CallbackCell = Cell.extend({
465
+ initialize: function() {
466
+ CallbackCell.__super__.initialize.call(this);
467
+ this.callback = this.options.callback;
468
+ },
469
+
470
+ _prepareValue: function() {
471
+ this.value = this.callback(this.model.toJSON());
472
+ }
473
+ });
474
+
475
+ var ActionCell = Datagrid.ActionCell = Cell.extend({
476
+ initialize: function() {
477
+ ActionCell.__super__.initialize.call(this);
478
+ },
479
+
480
+ action: function() {
481
+ return this.options.action(this.model);
482
+ },
483
+
484
+ _prepareValue: function() {
485
+ var a = $('<a></a>');
486
+
487
+ a.html(this.options.label);
488
+ a.attr('href', this.options.href || '#');
489
+ if (this.options.actionClassName) {
490
+ a.addClass(this.options.actionClassName);
491
+ }
492
+ if (this.options.action) {
493
+ this.delegateEvents({
494
+ 'click a': this.action
495
+ });
496
+ }
497
+
498
+ this.value = a;
499
+ }
500
+ });
501
+
502
+ var HeaderCell = Datagrid.HeaderCell = Cell.extend({
503
+ initialize: function() {
504
+ HeaderCell.__super__.initialize.call(this);
505
+
506
+ this.sorter = this.options.sorter;
507
+
508
+ if (this.column.sortable) {
509
+ this.delegateEvents({click: 'sort'});
510
+ }
511
+ },
512
+
513
+ render: function() {
514
+ this._prepareValue();
515
+ var html = this.value, icon;
516
+
517
+ if (this.column.sortable) {
518
+ this.$el.addClass('sortable');
519
+ if (this.sorter.sortedBy(this.column.sortedProperty || this.column.property) || this.sorter.sortedBy(this.column.index)) {
520
+ if (this.sorter.sortedASC()) {
521
+ icon = 'icon-chevron-up';
522
+ } else {
523
+ icon = 'icon-chevron-down';
524
+ }
525
+ } else {
526
+ icon = 'icon-minus';
527
+ }
528
+
529
+ html += ' <i class="' + icon + ' pull-right"></i>';
530
+ }
531
+
532
+ this.$el.html(html);
533
+ return this;
534
+ },
535
+
536
+ sort: function() {
537
+ this.sorter.sort(this.column.sortedProperty || this.column.property);
538
+ }
539
+ });
540
+
541
+ var Pager = Datagrid.Pager = Backbone.Model.extend({
542
+ initialize: function() {
543
+ this.on('change:perPage change:total', function() {
544
+ this.totalPages(this.get('total'));
545
+ }, this);
546
+ if (this.has('total')) {
547
+ this.totalPages(this.get('total'));
548
+ }
549
+ },
550
+
551
+ update: function(options) {
552
+ _.each(['hasNext', 'hasPrev', 'total', 'totalPages', 'lastPage'], function(p) {
553
+ if (!_.isUndefined(options[p])) {
554
+ this.set(p, options[p]);
555
+ }
556
+ }, this);
557
+ },
558
+
559
+ totalPages: function(total) {
560
+ if (_.isNumber(total)) {
561
+ this.set('totalPages', Math.ceil(total/this.get('perPage')));
562
+ } else {
563
+ this.set('totalPages', undefined);
564
+ }
565
+ },
566
+
567
+ page: function(page) {
568
+ if (this.inBounds(page)) {
569
+ if (page === this.get('currentPage')) {
570
+ this.trigger('change:currentPage');
571
+ } else {
572
+ this.set('currentPage', page);
573
+ }
574
+ }
575
+ },
576
+
577
+ next: function() {
578
+ this.page(this.get('currentPage') + 1);
579
+ },
580
+
581
+ prev: function() {
582
+ this.page(this.get('currentPage') - 1);
583
+ },
584
+
585
+ hasTotal: function() {
586
+ return this.has('totalPages');
587
+ },
588
+
589
+ hasNext: function() {
590
+ if (this.hasTotal()) {
591
+ return this.get('currentPage') < this.get('totalPages');
592
+ } else {
593
+ return this.get('hasNext');
594
+ }
595
+ },
596
+
597
+ hasPrev: function() {
598
+ if (this.has('hasPrev')) {
599
+ return this.get('hasPrev');
600
+ } else {
601
+ return this.get('currentPage') > 1;
602
+ }
603
+ },
604
+
605
+ inBounds: function(page) {
606
+ return !this.hasTotal() || page > 0 && page <= this.get('totalPages');
607
+ },
608
+
609
+ set: function() {
610
+ var args = [];
611
+ for (var i = 0; i < arguments.length; i++) {
612
+ args.push(arguments[i]);
613
+ }
614
+ args[2] = _.extend({}, args[2], {validate: true});
615
+ Backbone.Model.prototype.set.apply(this, args);
616
+ },
617
+
618
+ validate: function(attrs) {
619
+ if (attrs.perPage < 1) {
620
+ throw new Error('perPage must be greater than zero.');
621
+ }
622
+ }
623
+ });
624
+
625
+ var Sorter = Datagrid.Sorter = Backbone.Model.extend({
626
+ sort: function(column, order) {
627
+ if (!order && this.get('column') === column) {
628
+ this.toggleOrder();
629
+ } else {
630
+ this.set({
631
+ column: column,
632
+ order: order || Sorter.ASC
633
+ });
634
+ }
635
+ },
636
+
637
+ sortedBy: function(column) {
638
+ return this.get('column') === column;
639
+ },
640
+
641
+ sortedASC: function() {
642
+ return this.get('order') === Sorter.ASC;
643
+ },
644
+
645
+ sortedDESC: function() {
646
+ return this.get('order') === Sorter.DESC;
647
+ },
648
+
649
+ toggleOrder: function() {
650
+ if (this.get('order') === Sorter.ASC) {
651
+ this.set('order', Sorter.DESC);
652
+ } else {
653
+ this.set('order', Sorter.ASC);
654
+ }
655
+ }
656
+ });
657
+
658
+ Sorter.ASC = 'asc';
659
+ Sorter.DESC = 'desc';
660
+
661
+ Backbone.Datagrid = Datagrid;
662
+ })();