helios_aim 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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
+ })();