helios 0.0.5 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +38 -44
  4. data/bin/helios +2 -2
  5. data/example/Gemfile +2 -2
  6. data/example/Gemfile.lock +60 -49
  7. data/example/Procfile +1 -1
  8. data/example/Sample.xcdatamodeld/Sample.xcdatamodel/contents +11 -17
  9. data/example/config.ru +12 -1
  10. data/helios-0.1.0.gem +0 -0
  11. data/helios.gemspec +5 -3
  12. data/lib/helios.rb +9 -5
  13. data/lib/helios/backend.rb +27 -17
  14. data/lib/helios/backend/accounts.rb +38 -0
  15. data/lib/helios/backend/data.rb +7 -6
  16. data/lib/helios/backend/in-app-purchase.rb +10 -2
  17. data/lib/helios/backend/passbook.rb +12 -2
  18. data/lib/helios/backend/push-notification.rb +8 -1
  19. data/lib/helios/commands.rb +3 -1
  20. data/lib/helios/commands/link.rb +18 -0
  21. data/lib/helios/commands/new.rb +43 -6
  22. data/lib/helios/commands/server.rb +16 -0
  23. data/lib/helios/frontend.rb +11 -5
  24. data/lib/helios/frontend/fonts/icons.eot +0 -0
  25. data/lib/helios/frontend/fonts/icons.ttf +0 -0
  26. data/lib/helios/frontend/fonts/icons.woff +0 -0
  27. data/lib/helios/frontend/images/bg.jpg +0 -0
  28. data/lib/helios/frontend/javascripts/helios.coffee +5 -5
  29. data/lib/helios/frontend/javascripts/helios/collections.coffee +67 -6
  30. data/lib/helios/frontend/javascripts/helios/models.coffee +1 -4
  31. data/lib/helios/frontend/javascripts/helios/router.coffee +0 -14
  32. data/lib/helios/frontend/javascripts/helios/views.coffee +164 -23
  33. data/lib/helios/frontend/javascripts/vendor/backbone.paginator.js +1046 -0
  34. data/lib/helios/frontend/javascripts/vendor/codemirror.javascript.js +411 -0
  35. data/lib/helios/frontend/javascripts/vendor/codemirror.js +3047 -0
  36. data/lib/helios/frontend/javascripts/vendor/date.js +104 -0
  37. data/lib/helios/frontend/javascripts/{foundation → vendor}/foundation.js +0 -0
  38. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.alerts.js +0 -0
  39. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.clearing.js +0 -0
  40. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.cookie.js +0 -0
  41. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.dropdown.js +0 -0
  42. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.forms.js +0 -0
  43. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.joyride.js +0 -0
  44. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.magellan.js +0 -0
  45. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.orbit.js +0 -0
  46. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.placeholder.js +0 -0
  47. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.reveal.js +0 -0
  48. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.section.js +0 -0
  49. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.tooltips.js +0 -0
  50. data/lib/helios/frontend/javascripts/{foundation → vendor/foundation}/foundation.topbar.js +0 -0
  51. data/lib/helios/frontend/stylesheets/_codemirror.sass +219 -0
  52. data/lib/helios/frontend/stylesheets/_fonts.sass +80 -0
  53. data/lib/helios/frontend/stylesheets/_iphone.sass +141 -0
  54. data/lib/helios/frontend/stylesheets/_settings.scss +12 -11
  55. data/lib/helios/frontend/stylesheets/screen.sass +119 -34
  56. data/lib/helios/frontend/views/compose.jst.tpl +70 -0
  57. data/lib/helios/frontend/views/devices.jst.tpl +15 -2
  58. data/lib/helios/frontend/views/entities.jst.tpl +6 -4
  59. data/lib/helios/frontend/views/index.haml +24 -8
  60. data/lib/helios/frontend/views/passes.jst.tpl +10 -2
  61. data/lib/helios/frontend/views/receipts.jst.tpl +10 -2
  62. data/lib/helios/templates/Gemfile.erb +5 -2
  63. data/lib/helios/templates/Procfile.erb +1 -1
  64. data/lib/helios/templates/README.md.erb +1 -1
  65. data/lib/helios/templates/config.ru.erb +8 -1
  66. data/lib/helios/version.rb +1 -1
  67. metadata +174 -102
  68. data/helios-0.0.2.gem +0 -0
  69. data/helios-0.0.3.gem +0 -0
  70. data/helios-0.0.4.gem +0 -0
  71. data/lib/helios/frontend/images/data.png +0 -0
  72. data/lib/helios/frontend/images/data.svg +0 -1
  73. data/lib/helios/frontend/images/in-app-purchase.png +0 -0
  74. data/lib/helios/frontend/images/in-app-purchase.svg +0 -1
  75. data/lib/helios/frontend/images/passbook.png +0 -0
  76. data/lib/helios/frontend/images/passbook.svg +0 -1
  77. data/lib/helios/frontend/images/push-notification.png +0 -0
  78. data/lib/helios/frontend/images/push-notification.svg +0 -1
  79. data/lib/helios/frontend/templates/data/entities.hbs +0 -7
  80. data/lib/helios/frontend/templates/data/entity.hbs +0 -17
  81. data/lib/helios/frontend/templates/in-app-purchase/receipt.hbs +0 -53
  82. data/lib/helios/frontend/templates/in-app-purchase/receipts.hbs +0 -39
  83. data/lib/helios/frontend/templates/passbook/passes.hbs +0 -25
  84. data/lib/helios/frontend/templates/push-notification/device.hbs +0 -35
  85. data/lib/helios/frontend/templates/push-notification/devices.hbs +0 -37
@@ -1,4 +1,17 @@
1
- class Helios.Collection extends Backbone.Collection
1
+ class Helios.Collection extends Backbone.Paginator.requestPager
2
+ paginator_ui:
3
+ firstPage: 1,
4
+ currentPage: 1,
5
+ perPage: 20
6
+
7
+ server_api:
8
+ 'q': ->
9
+ @query || ""
10
+ 'limit': ->
11
+ @perPage
12
+ 'offset': ->
13
+ (@currentPage - 1) * @perPage
14
+
2
15
  parse: (response, options) ->
3
16
  if _.isArray(response)
4
17
  response
@@ -6,25 +19,73 @@ class Helios.Collection extends Backbone.Collection
6
19
  if response.page?
7
20
  @total = response.total
8
21
  @page = response.page
9
-
22
+ @totalPages = Math.ceil(@total / @perPage)
23
+
10
24
  _.detect response, (value, key) ->
11
25
  _.isArray(value)
12
26
 
13
- class Helios.Collections.Entities extends Helios.Collection
27
+ class Helios.Collections.Entities extends Backbone.Collection
14
28
  model: Helios.Models.Entity
15
29
  url: '/'
16
30
 
17
- class Helios.Collections.Resources extends Helios.Collection
31
+ parse: (response, options) ->
32
+ if _.isArray(response)
33
+ response
34
+ else
35
+ if response.page?
36
+ @total = response.total
37
+ @page = response.page
38
+ @totalPages = Math.ceil(@total / @perPage)
39
+
40
+ _.detect response, (value, key) ->
41
+ _.isArray(value)
42
+
43
+ class Helios.Collections.Resources extends Backbone.Collection
18
44
  model: Helios.Models.Resource
19
45
 
46
+ parse: (response, options) ->
47
+ if _.isArray(response)
48
+ response
49
+ else
50
+ if response.page?
51
+ @total = response.total
52
+ @page = response.page
53
+ @totalPages = Math.ceil(@total / @perPage)
54
+
55
+ _.detect response, (value, key) ->
56
+ _.isArray(value)
57
+
20
58
  class Helios.Collections.Devices extends Helios.Collection
21
59
  model: Helios.Models.Device
22
- url: '/devices'
60
+ fields: ['token', 'alias', 'badge', 'locale', 'language', 'timezone', 'ip_address', 'lat', 'lng']
61
+
62
+ paginator_core:
63
+ type: 'GET'
64
+ dataType: 'json'
65
+ url: '/devices?'
66
+
67
+ comparator: (device) ->
68
+ device.get('token')
69
+
23
70
 
24
71
  class Helios.Collections.Receipts extends Helios.Collection
25
72
  model: Helios.Models.Receipt
26
73
  url: '/receipts'
74
+ fields: ['transaction_id', 'product_id', 'purchase_date', 'original_transaction_id', 'original_purchase_date', 'app_item_id', 'version_external_identifier', 'bid', 'bvrs', 'ip_address']
75
+
76
+ paginator_core:
77
+ type: 'GET'
78
+ dataType: 'json'
79
+ url: '/receipts?'
80
+
27
81
 
28
82
  class Helios.Collections.Passes extends Helios.Collection
29
83
  model: Helios.Models.Pass
30
- url: '/passes'
84
+ url: '/passes'
85
+ fields: ['pass_type_identifier', 'serial_number', 'authentication_token']
86
+
87
+ paginator_core:
88
+ type: 'GET'
89
+ dataType: 'json'
90
+ url: '/passes?'
91
+
@@ -13,11 +13,8 @@ class Helios.Models.Resource extends Backbone.Model
13
13
 
14
14
  class Helios.Models.Device extends Backbone.Model
15
15
  idAttribute: "token"
16
- fields: ['token', 'alias', 'badge', 'locale', 'language', 'timezone', 'ip_address', 'lat', 'lng']
17
-
16
+
18
17
  class Helios.Models.Receipt extends Backbone.Model
19
18
  idAttribute: "transaction_id"
20
- fields: ['transaction_id', 'product_id', 'purchase_date', 'original_transaction_id', 'original_purchase_date', 'app_item_id', 'version_external_identifier', 'bid', 'bvrs', 'ip_address']
21
19
 
22
20
  class Helios.Models.Pass extends Backbone.Model
23
- fields: ['pass_type_identifier', 'serial_number', 'authentication_token']
@@ -14,37 +14,23 @@ class Helios.Routers.Root extends Backbone.Router
14
14
  'passbook': 'passbook'
15
15
 
16
16
  index: ->
17
- console.log("index")
18
- # @_activateNavbarLink("devices")
19
-
20
17
  Helios.entities.fetch(type: 'OPTIONS')
21
- # @views.devices ?= new RPN.Views.Devices(collection: RPN.devices)
22
- # @views.devices.render()
23
-
24
- # _activateNavbarLink: (className) ->
25
- # $li = $("header nav li")
26
- # $li.removeClass("active")
27
- # $li.filter("." + className).addClass("active")
28
18
 
29
19
  data: ->
30
- console.log("data")
31
20
  Helios.entities.fetch(type: 'OPTIONS')
32
21
  @views.entities.render()
33
22
 
34
23
  push_notification: ->
35
- console.log("push")
36
24
  @devices ?= new Helios.Collections.Devices
37
25
  @views.devices ?= new Helios.Views.Devices(collection: @devices)
38
26
  @views.devices.render()
39
27
 
40
28
  in_app_purchase: ->
41
- console.log("IAP")
42
29
  @receipts ?= new Helios.Collections.Receipts
43
30
  @views.receipts ?= new Helios.Views.Receipts(collection: @receipts)
44
31
  @views.receipts.render()
45
32
 
46
33
  passbook: ->
47
- console.log("passbook")
48
34
  @passes ?= new Helios.Collections.Passes
49
35
  @views.passes ?= new Helios.Views.Passes(collection: @passes)
50
36
  @views.passes.render()
@@ -25,7 +25,7 @@ class Helios.Views.Entity extends Backbone.View
25
25
 
26
26
  render: =>
27
27
  if @collection
28
- @datagrid = new Backbone.Datagrid({
28
+ @datagrid ?= new Backbone.Datagrid({
29
29
  collection: @collection,
30
30
  columns: @collection.first().attributes.keys,
31
31
  paginated: true,
@@ -39,54 +39,195 @@ class Helios.Views.Devices extends Backbone.View
39
39
  template: JST['devices']
40
40
  el: "[role='main']"
41
41
 
42
+ events:
43
+ 'keyup form.filter input': 'filter'
44
+
45
+ initialize: ->
46
+ @datagrid = new Backbone.Datagrid({
47
+ collection: @collection,
48
+ columns: @collection.fields,
49
+ paginated: true,
50
+ perPage: 20
51
+ })
52
+
42
53
  render: =>
43
54
  @$el.html(@template())
44
55
 
45
- if @collection
46
- @datagrid = new Backbone.Datagrid({
47
- collection: @collection,
48
- columns: Helios.Models.Device.fields,
49
- paginated: true,
50
- perPage: 20
51
- })
52
- @$el.find("#datagrid").html(@datagrid.el)
56
+ @composeView ?= new Helios.Views.Compose()
57
+ @composeView.render()
58
+ @$el.find("#datagrid").html(@datagrid.el)
59
+
60
+ @
61
+
62
+ filter: (e) ->
63
+ e.preventDefault()
64
+ @collection.query = $(e.target).val()
65
+ @collection.fetch()
66
+
67
+ class Helios.Views.Compose extends Backbone.View
68
+ template: JST['compose']
69
+ el: "#compose-modal"
70
+
71
+ events:
72
+ 'submit form': 'submit'
73
+ 'click button#send': 'submit'
74
+ 'keyup textarea': 'updatePreview'
75
+ 'focus textarea': ->
76
+ @$el.find("input[type=radio][value=selected]").prop('checked',true)
77
+
78
+ initialize: ->
79
+ window.setInterval(@updateTime, 10000)
80
+
81
+ render: ->
82
+ @$el.html(@template())
83
+
84
+ @editor = CodeMirror.fromTextArea(document.getElementById("payload"), {
85
+ mode: "application/json",
86
+ theme: "solarized-dark",
87
+ tabMode: "indent",
88
+ lineNumbers : true,
89
+ matchBrackets: true
90
+ })
91
+
92
+ @updatePreview()
93
+ @updateTime()
53
94
 
95
+ # $.ajax("/message"
96
+ # type: "HEAD"
97
+
98
+ # error: (data, status) =>
99
+ # @disable()
100
+ # )
54
101
  @
55
102
 
103
+ submit: ->
104
+ $form = @$el.find("form#compose")
105
+ payload = @editor.getValue()
106
+
107
+ tokens = undefined
108
+ if $("input[name='recipients']:checked").val() == "specified"
109
+ tokens = [$form.find("#tokens").val()]
110
+
111
+ $.ajax("/message"
112
+ type: "POST"
113
+ dataType: "json"
114
+ data: {
115
+ tokens: tokens,
116
+ payload: payload
117
+ }
118
+ )
119
+
120
+ beforeSend: =>
121
+ @$el.find(".alert-error, .alert-success").remove()
122
+
123
+ success: (data, status) =>
124
+ alert = """
125
+ <div class="alert alert-block alert-success">
126
+ <button type="button" class="close" data-dismiss="alert">×</button>
127
+ <h4>Push Notification Succeeded</h4>
128
+ </div>
129
+ """
130
+ @$el.prepend(alert)
131
+
132
+ error: (data, status) =>
133
+ alert = """
134
+ <div class="alert alert-block alert-error">
135
+ <button type="button" class="close" data-dismiss="alert">×</button>
136
+ <h4>Push Notification Failed</h4>
137
+ <p>#{$.parseJSON(data.responseText).error}</p>
138
+ </div>
139
+ """
140
+ @$el.prepend(alert)
141
+
142
+
143
+ disable: ->
144
+ alert = """
145
+ <div class="alert alert-block">
146
+ <button type="button" class="close" data-dismiss="alert">×</button>
147
+ <h4>Push Notification Sending Unavailable</h4>
148
+ <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>
149
+ </div>
150
+ """
151
+
152
+ @$el.prepend(alert)
153
+
154
+ $(".iphone").css(opacity: 0.5)
155
+
156
+ $form = @$el.find("form#compose")
157
+ $form.css(opacity: 0.5)
158
+ $form.find("input").disable()
159
+
160
+ updatePreview: ->
161
+ try
162
+ json = $.parseJSON(@editor.getValue())
163
+ if alert = json.aps.alert
164
+ $(".preview p").text(alert)
165
+
166
+ catch error
167
+ $(".alert strong").text(error.name)
168
+ $(".alert span").text(error.message)
169
+ finally
170
+ if alert? and alert.length > 0
171
+ $(".notification").show()
172
+ $(".alert").hide()
173
+ else
174
+ $(".notification").hide()
175
+ $(".alert").show()
176
+
177
+ updateTime: ->
178
+ $time = $("time")
179
+ $time.attr("datetime", Date.now().toISOString())
180
+ $time.find(".time").text(Date.now().toString("HH:mm"))
181
+ $time.find(".date").text(Date.now().toString("dddd, MMMM d"))
182
+
56
183
  class Helios.Views.Receipts extends Backbone.View
57
184
  template: JST['receipts']
58
185
  el: "[role='main']"
59
186
 
60
- render: =>
61
- @$el.html(@template())
187
+ events:
188
+ 'keyup form.filter input': 'filter'
62
189
 
63
- if @collection
64
- console.log("receipts")
65
- @datagrid = new Backbone.Datagrid({
190
+ initialize: ->
191
+ @datagrid = new Backbone.Datagrid({
66
192
  collection: @collection,
67
- columns: Helios.Models.Receipt.fields,
193
+ columns: @collection.fields,
68
194
  paginated: true,
69
195
  perPage: 20
70
196
  })
71
- @$el.find("#datagrid").html(@datagrid.el)
197
+
198
+ render: =>
199
+ @$el.html(@template())
200
+ @$el.find("#datagrid").html(@datagrid.el)
72
201
 
73
202
  @
74
203
 
204
+ filter: (e) ->
205
+ e.preventDefault()
206
+ @collection.query = $(e.target).val()
207
+ @collection.fetch()
208
+
75
209
  class Helios.Views.Passes extends Backbone.View
76
210
  template: JST['passes']
77
211
  el: "[role='main']"
78
212
 
79
- render: =>
80
- @$el.html(@template())
213
+ events:
214
+ 'keyup form.filter input': 'filter'
81
215
 
82
- if @collection
83
- console.log("passes")
84
- @datagrid = new Backbone.Datagrid({
216
+ initialize: ->
217
+ @datagrid = new Backbone.Datagrid({
85
218
  collection: @collection,
86
- columns: Helios.Models.Pass.fields,
219
+ columns: @collection.fields,
87
220
  paginated: true,
88
221
  perPage: 20
89
222
  })
90
- @$el.find("#datagrid").html(@datagrid.el)
223
+
224
+ render: =>
225
+ @$el.html(@template())
226
+ @$el.find("#datagrid").html(@datagrid.el)
91
227
 
92
228
  @
229
+
230
+ filter: (e) ->
231
+ e.preventDefault()
232
+ @collection.query = $(e.target).val()
233
+ @collection.fetch()
@@ -0,0 +1,1046 @@
1
+ /*! backbone.paginator - v0.5.1-dev - 3/19/2013
2
+ * http://github.com/addyosmani/backbone.paginator
3
+ * Copyright (c) 2013 Addy Osmani; Licensed MIT */
4
+
5
+ Backbone.Paginator = (function ( Backbone, _, $ ) {
6
+ "use strict";
7
+
8
+ var Paginator = {};
9
+ Paginator.version = "0.6.0";
10
+
11
+ // @name: clientPager
12
+ //
13
+ // @tagline: Paginator for client-side data
14
+ //
15
+ // @description:
16
+ // This paginator is responsible for providing pagination
17
+ // and sort capabilities for a single payload of data
18
+ // we wish to paginate by the UI for easier browsering.
19
+ //
20
+ Paginator.clientPager = Backbone.Collection.extend({
21
+
22
+ // DEFAULTS FOR SORTING & FILTERING
23
+ useDiacriticsPlugin: true, // use diacritics plugin if available
24
+ useLevenshteinPlugin: true, // use levenshtein plugin if available
25
+ sortColumn: "",
26
+ sortDirection: "desc",
27
+ lastSortColumn: "",
28
+ fieldFilterRules: [],
29
+ lastFieldFilterRules: [],
30
+ filterFields: "",
31
+ filterExpression: "",
32
+ lastFilterExpression: "",
33
+
34
+ //DEFAULT PAGINATOR UI VALUES
35
+ defaults_ui: {
36
+ firstPage: 0,
37
+ currentPage: 1,
38
+ perPage: 5,
39
+ totalPages: 10,
40
+ pagesInRange: 4
41
+ },
42
+
43
+ // Default values used when sorting and/or filtering.
44
+ initialize: function(){
45
+ //LISTEN FOR ADD & REMOVE EVENTS THEN REMOVE MODELS FROM ORGINAL MODELS
46
+ this.on('add', this.addModel, this);
47
+ this.on('remove', this.removeModel, this);
48
+
49
+ // SET DEFAULT VALUES (ALLOWS YOU TO POPULATE PAGINATOR MAUNALLY)
50
+ this.setDefaults();
51
+ },
52
+
53
+
54
+ setDefaults: function() {
55
+ // SET DEFAULT UI SETTINGS
56
+ var options = _.defaults(this.paginator_ui, this.defaults_ui);
57
+
58
+ //UPDATE GLOBAL UI SETTINGS
59
+ _.defaults(this, options);
60
+ },
61
+
62
+ addModel: function(model) {
63
+ this.origModels.push(model);
64
+ },
65
+
66
+ removeModel: function(model) {
67
+ var index = _.indexOf(this.origModels, model);
68
+
69
+ this.origModels.splice(index, 1);
70
+ },
71
+
72
+ sync: function ( method, model, options ) {
73
+ var self = this;
74
+
75
+ // SET DEFAULT VALUES
76
+ this.setDefaults();
77
+
78
+ // Some values could be functions, let's make sure
79
+ // to change their scope too and run them
80
+ var queryAttributes = {};
81
+ _.each(_.result(self, "server_api"), function(value, key){
82
+ if( _.isFunction(value) ) {
83
+ value = _.bind(value, self);
84
+ value = value();
85
+ }
86
+ queryAttributes[key] = value;
87
+ });
88
+
89
+ var queryOptions = _.clone(self.paginator_core);
90
+ _.each(queryOptions, function(value, key){
91
+ if( _.isFunction(value) ) {
92
+ value = _.bind(value, self);
93
+ value = value();
94
+ }
95
+ queryOptions[key] = value;
96
+ });
97
+
98
+ // Create default values if no others are specified
99
+ queryOptions = _.defaults(queryOptions, {
100
+ timeout: 25000,
101
+ cache: false,
102
+ type: 'GET',
103
+ dataType: 'jsonp'
104
+ });
105
+
106
+ queryOptions = _.extend(queryOptions, {
107
+ data: decodeURIComponent($.param(queryAttributes)),
108
+ processData: false,
109
+ url: _.result(queryOptions, 'url')
110
+ }, options);
111
+
112
+ var bbVer = Backbone.VERSION.split('.');
113
+ var oldSuccessFormat = (parseInt(bbVer[0], 10) === 0 &&
114
+ parseInt(bbVer[1], 10) === 9 &&
115
+ parseInt(bbVer[2], 10) <= 9);
116
+
117
+ var success = queryOptions.success;
118
+ queryOptions.success = function ( resp, status, xhr ) {
119
+ if ( success ) {
120
+ // This is to keep compatibility with Backbone older than 0.9.10
121
+ if (oldSuccessFormat) {
122
+ success( resp, status, xhr );
123
+ } else {
124
+ success( model, resp, queryOptions );
125
+ }
126
+ }
127
+ if ( model && model.trigger ) {
128
+ model.trigger( 'sync', model, resp, queryOptions );
129
+ }
130
+ };
131
+
132
+ var error = queryOptions.error;
133
+ queryOptions.error = function ( xhr ) {
134
+ if ( error ) {
135
+ error( model, xhr, queryOptions );
136
+ }
137
+ if ( model && model.trigger ) {
138
+ model.trigger( 'error', model, xhr, queryOptions );
139
+ }
140
+ };
141
+
142
+ var xhr = queryOptions.xhr = $.ajax( queryOptions );
143
+ if ( model && model.trigger ) {
144
+ model.trigger('request', model, xhr, queryOptions);
145
+ }
146
+ return xhr;
147
+ },
148
+
149
+ nextPage: function (options) {
150
+ if(this.currentPage < this.information.totalPages) {
151
+ this.currentPage = ++this.currentPage;
152
+ this.pager(options);
153
+ }
154
+ },
155
+
156
+ previousPage: function (options) {
157
+ if(this.currentPage > 1) {
158
+ this.currentPage = --this.currentPage;
159
+ this.pager(options);
160
+ }
161
+ },
162
+
163
+ goTo: function ( page, options ) {
164
+ if(page !== undefined){
165
+ this.currentPage = parseInt(page, 10);
166
+ this.pager(options);
167
+ }
168
+ },
169
+
170
+ howManyPer: function ( perPage ) {
171
+ if(perPage !== undefined){
172
+ var lastPerPage = this.perPage;
173
+ this.perPage = parseInt(perPage, 10);
174
+ this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage);
175
+ this.pager();
176
+ }
177
+ },
178
+
179
+
180
+ // setSort is used to sort the current model. After
181
+ // passing 'column', which is the model's field you want
182
+ // to filter and 'direction', which is the direction
183
+ // desired for the ordering ('asc' or 'desc'), pager()
184
+ // and info() will be called automatically.
185
+ setSort: function ( column, direction ) {
186
+ if(column !== undefined && direction !== undefined){
187
+ this.lastSortColumn = this.sortColumn;
188
+ this.sortColumn = column;
189
+ this.sortDirection = direction;
190
+ this.pager();
191
+ this.info();
192
+ }
193
+ },
194
+
195
+ // setFieldFilter is used to filter each value of each model
196
+ // according to `rules` that you pass as argument.
197
+ // Example: You have a collection of books with 'release year' and 'author'.
198
+ // You can filter only the books that were released between 1999 and 2003
199
+ // And then you can add another `rule` that will filter those books only to
200
+ // authors who's name start with 'A'.
201
+ setFieldFilter: function ( fieldFilterRules ) {
202
+ if( !_.isEmpty( fieldFilterRules ) ) {
203
+ this.lastFieldFilterRules = this.fieldFilterRules;
204
+ this.fieldFilterRules = fieldFilterRules;
205
+ this.pager();
206
+ this.info();
207
+ // if all the filters are removed, we should save the last filter
208
+ // and then let the list reset to it's original state.
209
+ } else {
210
+ this.lastFieldFilterRules = this.fieldFilterRules;
211
+ this.fieldFilterRules = '';
212
+ this.pager();
213
+ this.info();
214
+ }
215
+ },
216
+
217
+ // doFakeFieldFilter can be used to get the number of models that will remain
218
+ // after calling setFieldFilter with a filter rule(s)
219
+ doFakeFieldFilter: function ( rules ) {
220
+ if( !_.isEmpty( rules ) ) {
221
+ var testModels = this.origModels;
222
+ if (testModels === undefined) {
223
+ testModels = this.models;
224
+ }
225
+
226
+ testModels = this._fieldFilter(testModels, rules);
227
+
228
+ // To comply with current behavior, also filter by any previously defined setFilter rules.
229
+ if ( this.filterExpression !== "" ) {
230
+ testModels = this._filter(testModels, this.filterFields, this.filterExpression);
231
+ }
232
+
233
+ // Return size
234
+ return testModels.length;
235
+ }
236
+
237
+ },
238
+
239
+ // setFilter is used to filter the current model. After
240
+ // passing 'fields', which can be a string referring to
241
+ // the model's field, an array of strings representing
242
+ // each of the model's fields or an object with the name
243
+ // of the model's field(s) and comparing options (see docs)
244
+ // you wish to filter by and
245
+ // 'filter', which is the word or words you wish to
246
+ // filter by, pager() and info() will be called automatically.
247
+ setFilter: function ( fields, filter ) {
248
+ if( fields !== undefined && filter !== undefined ){
249
+ this.filterFields = fields;
250
+ this.lastFilterExpression = this.filterExpression;
251
+ this.filterExpression = filter;
252
+ this.pager();
253
+ this.info();
254
+ }
255
+ },
256
+
257
+ // doFakeFilter can be used to get the number of models that will
258
+ // remain after calling setFilter with a `fields` and `filter` args.
259
+ doFakeFilter: function ( fields, filter ) {
260
+ if( fields !== undefined && filter !== undefined ){
261
+ var testModels = this.origModels;
262
+ if (testModels === undefined) {
263
+ testModels = this.models;
264
+ }
265
+
266
+ // To comply with current behavior, first filter by any previously defined setFieldFilter rules.
267
+ if ( !_.isEmpty( this.fieldFilterRules ) ) {
268
+ testModels = this._fieldFilter(testModels, this.fieldFilterRules);
269
+ }
270
+
271
+ testModels = this._filter(testModels, fields, filter);
272
+
273
+ // Return size
274
+ return testModels.length;
275
+ }
276
+ },
277
+
278
+
279
+ // pager is used to sort, filter and show the data
280
+ // you expect the library to display.
281
+ pager: function (options) {
282
+ var self = this,
283
+ disp = this.perPage,
284
+ start = (self.currentPage - 1) * disp,
285
+ stop = start + disp;
286
+ // Saving the original models collection is important
287
+ // as we could need to sort or filter, and we don't want
288
+ // to loose the data we fetched from the server.
289
+ if (self.origModels === undefined) {
290
+ self.origModels = self.models;
291
+ }
292
+
293
+ self.models = self.origModels.slice();
294
+
295
+ // Check if sorting was set using setSort.
296
+ if ( this.sortColumn !== "" ) {
297
+ self.models = self._sort(self.models, this.sortColumn, this.sortDirection);
298
+ }
299
+
300
+ // Check if field-filtering was set using setFieldFilter
301
+ if ( !_.isEmpty( this.fieldFilterRules ) ) {
302
+ self.models = self._fieldFilter(self.models, this.fieldFilterRules);
303
+ }
304
+
305
+ // Check if filtering was set using setFilter.
306
+ if ( this.filterExpression !== "" ) {
307
+ self.models = self._filter(self.models, this.filterFields, this.filterExpression);
308
+ }
309
+
310
+ // If the sorting or the filtering was changed go to the first page
311
+ if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRules) ) {
312
+ start = 0;
313
+ stop = start + disp;
314
+ self.currentPage = 1;
315
+
316
+ this.lastSortColumn = this.sortColumn;
317
+ this.lastFieldFilterRules = this.fieldFilterRules;
318
+ this.lastFilterExpression = this.filterExpression;
319
+ }
320
+
321
+ // We need to save the sorted and filtered models collection
322
+ // because we'll use that sorted and filtered collection in info().
323
+ self.sortedAndFilteredModels = self.models.slice();
324
+ self.info();
325
+ self.reset(self.models.slice(start, stop));
326
+
327
+ // This is somewhat of a hack to get all the nextPage, prevPage, and goTo methods
328
+ // to work with a success callback (as in the requestPager). Realistically there is no failure case here,
329
+ // but maybe we could catch exception and trigger a failure callback?
330
+ _.result(options, 'success');
331
+ },
332
+
333
+ // The actual place where the collection is sorted.
334
+ // Check setSort for arguments explicacion.
335
+ _sort: function ( models, sort, direction ) {
336
+ models = models.sort(function (a, b) {
337
+ var ac = a.get(sort),
338
+ bc = b.get(sort);
339
+
340
+ if ( _.isUndefined(ac) || _.isUndefined(bc) || ac === null || bc === null ) {
341
+ return 0;
342
+ } else {
343
+ /* Make sure that both ac and bc are lowercase strings.
344
+ * .toString() first so we don't have to worry if ac or bc
345
+ * have other String-only methods.
346
+ */
347
+ ac = ac.toString().toLowerCase();
348
+ bc = bc.toString().toLowerCase();
349
+ }
350
+
351
+ if (direction === 'desc') {
352
+
353
+ // We need to know if there aren't any non-number characters
354
+ // and that there are numbers-only characters and maybe a dot
355
+ // if we have a float.
356
+ // Oh, also a '-' for negative numbers!
357
+ if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
358
+ (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))
359
+ ){
360
+
361
+ if( (ac - 0) < (bc - 0) ) {
362
+ return 1;
363
+ }
364
+ if( (ac - 0) > (bc - 0) ) {
365
+ return -1;
366
+ }
367
+ } else {
368
+ if (ac < bc) {
369
+ return 1;
370
+ }
371
+ if (ac > bc) {
372
+ return -1;
373
+ }
374
+ }
375
+
376
+ } else {
377
+
378
+ //Same as the regexp check in the 'if' part.
379
+ if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
380
+ (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))
381
+ ){
382
+ if( (ac - 0) < (bc - 0) ) {
383
+ return -1;
384
+ }
385
+ if( (ac - 0) > (bc - 0) ) {
386
+ return 1;
387
+ }
388
+ } else {
389
+ if (ac < bc) {
390
+ return -1;
391
+ }
392
+ if (ac > bc) {
393
+ return 1;
394
+ }
395
+ }
396
+
397
+ }
398
+
399
+ if (a.cid && b.cid){
400
+ var aId = a.cid,
401
+ bId = b.cid;
402
+
403
+ if (aId < bId) {
404
+ return -1;
405
+ }
406
+ if (aId > bId) {
407
+ return 1;
408
+ }
409
+ }
410
+
411
+ return 0;
412
+ });
413
+
414
+ return models;
415
+ },
416
+
417
+ // The actual place where the collection is field-filtered.
418
+ // Check setFieldFilter for arguments explicacion.
419
+ _fieldFilter: function( models, rules ) {
420
+
421
+ // Check if there are any rules
422
+ if ( _.isEmpty(rules) ) {
423
+ return models;
424
+ }
425
+
426
+ var filteredModels = [];
427
+
428
+ // Iterate over each rule
429
+ _.each(models, function(model){
430
+
431
+ var should_push = true;
432
+
433
+ // Apply each rule to each model in the collection
434
+ _.each(rules, function(rule){
435
+
436
+ // Don't go inside the switch if we're already sure that the model won't be included in the results
437
+ if( !should_push ){
438
+ return false;
439
+ }
440
+
441
+ should_push = false;
442
+
443
+ // The field's value will be passed to a custom function, which should
444
+ // return true (if model should be included) or false (model should be ignored)
445
+ if(rule.type === "function"){
446
+ var f = _.wrap(rule.value, function(func){
447
+ return func( model.get(rule.field) );
448
+ });
449
+ if( f() ){
450
+ should_push = true;
451
+ }
452
+
453
+ // The field's value is required to be non-empty
454
+ }else if(rule.type === "required"){
455
+ if( !_.isEmpty( model.get(rule.field).toString() ) ) {
456
+ should_push = true;
457
+ }
458
+
459
+ // The field's value is required to be greater tan N (numbers only)
460
+ }else if(rule.type === "min"){
461
+ if( !_.isNaN( Number( model.get(rule.field) ) ) &&
462
+ !_.isNaN( Number( rule.value ) ) &&
463
+ Number( model.get(rule.field) ) >= Number( rule.value ) ) {
464
+ should_push = true;
465
+ }
466
+
467
+ // The field's value is required to be smaller tan N (numbers only)
468
+ }else if(rule.type === "max"){
469
+ if( !_.isNaN( Number( model.get(rule.field) ) ) &&
470
+ !_.isNaN( Number( rule.value ) ) &&
471
+ Number( model.get(rule.field) ) <= Number( rule.value ) ) {
472
+ should_push = true;
473
+ }
474
+
475
+ // The field's value is required to be between N and M (numbers only)
476
+ }else if(rule.type === "range"){
477
+ if( !_.isNaN( Number( model.get(rule.field) ) ) &&
478
+ _.isObject( rule.value ) &&
479
+ !_.isNaN( Number( rule.value.min ) ) &&
480
+ !_.isNaN( Number( rule.value.max ) ) &&
481
+ Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
482
+ Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
483
+ should_push = true;
484
+ }
485
+
486
+ // The field's value is required to be more than N chars long
487
+ }else if(rule.type === "minLength"){
488
+ if( model.get(rule.field).toString().length >= rule.value ) {
489
+ should_push = true;
490
+ }
491
+
492
+ // The field's value is required to be no more than N chars long
493
+ }else if(rule.type === "maxLength"){
494
+ if( model.get(rule.field).toString().length <= rule.value ) {
495
+ should_push = true;
496
+ }
497
+
498
+ // The field's value is required to be more than N chars long and no more than M chars long
499
+ }else if(rule.type === "rangeLength"){
500
+ if( _.isObject( rule.value ) &&
501
+ !_.isNaN( Number( rule.value.min ) ) &&
502
+ !_.isNaN( Number( rule.value.max ) ) &&
503
+ model.get(rule.field).toString().length >= rule.value.min &&
504
+ model.get(rule.field).toString().length <= rule.value.max ) {
505
+ should_push = true;
506
+ }
507
+
508
+ // The field's value is required to be equal to one of the values in rules.value
509
+ }else if(rule.type === "oneOf"){
510
+ if( _.isArray( rule.value ) &&
511
+ _.include( rule.value, model.get(rule.field) ) ) {
512
+ should_push = true;
513
+ }
514
+
515
+ // The field's value is required to be equal to the value in rules.value
516
+ }else if(rule.type === "equalTo"){
517
+ if( rule.value === model.get(rule.field) ) {
518
+ should_push = true;
519
+ }
520
+
521
+ }else if(rule.type === "containsAllOf"){
522
+ if( _.isArray( rule.value ) &&
523
+ _.isArray(model.get(rule.field)) &&
524
+ _.intersection( rule.value, model.get(rule.field)).length === rule.value.length
525
+ ) {
526
+ should_push = true;
527
+ }
528
+
529
+ // The field's value is required to match the regular expression
530
+ }else if(rule.type === "pattern"){
531
+ if( model.get(rule.field).toString().match(rule.value) ) {
532
+ should_push = true;
533
+ }
534
+
535
+ //Unknown type
536
+ }else{
537
+ should_push = false;
538
+ }
539
+
540
+ });
541
+
542
+ if( should_push ){
543
+ filteredModels.push(model);
544
+ }
545
+
546
+ });
547
+
548
+ return filteredModels;
549
+ },
550
+
551
+ // The actual place where the collection is filtered.
552
+ // Check setFilter for arguments explicacion.
553
+ _filter: function ( models, fields, filter ) {
554
+
555
+ // For example, if you had a data model containing cars like { color: '', description: '', hp: '' },
556
+ // your fields was set to ['color', 'description', 'hp'] and your filter was set
557
+ // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then
558
+ // "Mustang" in the description and then the HP in the 'hp' field.
559
+ // NOTE: "Black Musta 300" will return the same as "Black Mustang 300"
560
+
561
+ // We accept fields to be a string, an array or an object
562
+ // but if string or array is passed we need to convert it
563
+ // to an object.
564
+
565
+ var self = this;
566
+
567
+ var obj_fields = {};
568
+
569
+ if( _.isString( fields ) ) {
570
+ obj_fields[fields] = {cmp_method: 'regexp'};
571
+ }else if( _.isArray( fields ) ) {
572
+ _.each(fields, function(field){
573
+ obj_fields[field] = {cmp_method: 'regexp'};
574
+ });
575
+ }else{
576
+ _.each(fields, function( cmp_opts, field ) {
577
+ obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' });
578
+ });
579
+ }
580
+
581
+ fields = obj_fields;
582
+
583
+ //Remove diacritic characters if diacritic plugin is loaded
584
+ if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
585
+ filter = Backbone.Paginator.removeDiacritics(filter);
586
+ }
587
+
588
+ // 'filter' can be only a string.
589
+ // If 'filter' is string we need to convert it to
590
+ // a regular expression.
591
+ // For example, if 'filter' is 'black dog' we need
592
+ // to find every single word, remove duplicated ones (if any)
593
+ // and transform the result to '(black|dog)'
594
+ if( filter === '' || !_.isString(filter) ) {
595
+ return models;
596
+ } else {
597
+ var words = _.map(filter.match(/\w+/ig), function(element) { return element.toLowerCase(); });
598
+ var pattern = "(" + _.uniq(words).join("|") + ")";
599
+ var regexp = new RegExp(pattern, "igm");
600
+ }
601
+
602
+ var filteredModels = [];
603
+
604
+ // We need to iterate over each model
605
+ _.each( models, function( model ) {
606
+
607
+ var matchesPerModel = [];
608
+
609
+ // and over each field of each model
610
+ _.each( fields, function( cmp_opts, field ) {
611
+
612
+ var value = model.get( field );
613
+
614
+ if( value ) {
615
+
616
+ // The regular expression we created earlier let's us detect if a
617
+ // given string contains each and all of the words in the regular expression
618
+ // or not, but in both cases match() will return an array containing all
619
+ // the words it matched.
620
+ var matchesPerField = [];
621
+
622
+ if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
623
+ value = Backbone.Paginator.removeDiacritics(value.toString());
624
+ }else{
625
+ value = value.toString();
626
+ }
627
+
628
+ // Levenshtein cmp
629
+ if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) {
630
+ var distance = Backbone.Paginator.levenshtein(value, filter);
631
+
632
+ _.defaults(cmp_opts, { max_distance: 0 });
633
+
634
+ if( distance <= cmp_opts.max_distance ) {
635
+ matchesPerField = _.uniq(words);
636
+ }
637
+
638
+ // Default (RegExp) cmp
639
+ }else{
640
+ matchesPerField = value.match( regexp );
641
+ }
642
+
643
+ matchesPerField = _.map(matchesPerField, function(match) {
644
+ return match.toString().toLowerCase();
645
+ });
646
+
647
+ _.each(matchesPerField, function(match){
648
+ matchesPerModel.push(match);
649
+ });
650
+
651
+ }
652
+
653
+ });
654
+
655
+ // We just need to check if the returned array contains all the words in our
656
+ // regex, and if it does, it means that we have a match, so we should save it.
657
+ matchesPerModel = _.uniq( _.without(matchesPerModel, "") );
658
+
659
+ if( _.isEmpty( _.difference(words, matchesPerModel) ) ) {
660
+ filteredModels.push(model);
661
+ }
662
+
663
+ });
664
+
665
+ return filteredModels;
666
+ },
667
+
668
+ // You shouldn't need to call info() as this method is used to
669
+ // calculate internal data as first/prev/next/last page...
670
+ info: function () {
671
+ var self = this,
672
+ info = {},
673
+ totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length,
674
+ totalPages = Math.ceil(totalRecords / self.perPage);
675
+
676
+ info = {
677
+ totalUnfilteredRecords: self.origModels.length,
678
+ totalRecords: totalRecords,
679
+ currentPage: self.currentPage,
680
+ perPage: this.perPage,
681
+ totalPages: totalPages,
682
+ lastPage: totalPages,
683
+ previous: false,
684
+ next: false,
685
+ startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1,
686
+ endRecord: Math.min(totalRecords, self.currentPage * this.perPage)
687
+ };
688
+
689
+ if (self.currentPage > 1) {
690
+ info.previous = self.currentPage - 1;
691
+ }
692
+
693
+ if (self.currentPage < info.totalPages) {
694
+ info.next = self.currentPage + 1;
695
+ }
696
+
697
+ info.pageSet = self.setPagination(info);
698
+
699
+ self.information = info;
700
+ return info;
701
+ },
702
+
703
+
704
+ // setPagination also is an internal function that shouldn't be called directly.
705
+ // It will create an array containing the pages right before and right after the
706
+ // actual page.
707
+ setPagination: function ( info ) {
708
+
709
+ var pages = [], i = 0, l = 0;
710
+
711
+ // How many adjacent pages should be shown on each side?
712
+ var ADJACENTx2 = this.pagesInRange * 2,
713
+ LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
714
+
715
+ if (LASTPAGE > 1) {
716
+
717
+ // not enough pages to bother breaking it up
718
+ if (LASTPAGE <= (1 + ADJACENTx2)) {
719
+ for (i = 1, l = LASTPAGE; i <= l; i++) {
720
+ pages.push(i);
721
+ }
722
+ }
723
+
724
+ // enough pages to hide some
725
+ else {
726
+
727
+ //close to beginning; only hide later pages
728
+ if (info.currentPage <= (this.pagesInRange + 1)) {
729
+ for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
730
+ pages.push(i);
731
+ }
732
+ }
733
+
734
+ // in middle; hide some front and some back
735
+ else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
736
+ for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
737
+ pages.push(i);
738
+ }
739
+ }
740
+
741
+ // close to end; only hide early pages
742
+ else {
743
+ for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
744
+ pages.push(i);
745
+ }
746
+ }
747
+ }
748
+
749
+ }
750
+
751
+ return pages;
752
+
753
+ },
754
+
755
+ bootstrap: function(options) {
756
+ _.extend(this, options);
757
+ this.goTo(1);
758
+ this.info();
759
+ return this;
760
+ }
761
+
762
+ });
763
+
764
+ // function aliasing
765
+ Paginator.clientPager.prototype.prevPage = Paginator.clientPager.prototype.previousPage;
766
+
767
+ // @name: requestPager
768
+ //
769
+ // Paginator for server-side data being requested from a backend/API
770
+ //
771
+ // @description:
772
+ // This paginator is responsible for providing pagination
773
+ // and sort capabilities for requests to a server-side
774
+ // data service (e.g an API)
775
+ //
776
+ Paginator.requestPager = Backbone.Collection.extend({
777
+
778
+ sync: function ( method, model, options ) {
779
+
780
+ var self = this;
781
+
782
+ self.setDefaults();
783
+
784
+ // Some values could be functions, let's make sure
785
+ // to change their scope too and run them
786
+ var queryAttributes = {};
787
+ _.each(_.result(self, "server_api"), function(value, key){
788
+ if( _.isFunction(value) ) {
789
+ value = _.bind(value, self);
790
+ value = value();
791
+ }
792
+ queryAttributes[key] = value;
793
+ });
794
+
795
+ var queryOptions = _.clone(self.paginator_core);
796
+ _.each(queryOptions, function(value, key){
797
+ if( _.isFunction(value) ) {
798
+ value = _.bind(value, self);
799
+ value = value();
800
+ }
801
+ queryOptions[key] = value;
802
+ });
803
+
804
+ // Create default values if no others are specified
805
+ queryOptions = _.defaults(queryOptions, {
806
+ timeout: 25000,
807
+ cache: false,
808
+ type: 'GET',
809
+ dataType: 'jsonp'
810
+ });
811
+
812
+ // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults
813
+ if( options.data ){
814
+ options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data)));
815
+ }else{
816
+ options.data = decodeURIComponent($.param(queryAttributes));
817
+ }
818
+
819
+ queryOptions = _.extend(queryOptions, {
820
+ data: decodeURIComponent($.param(queryAttributes)),
821
+ processData: false,
822
+ url: _.result(queryOptions, 'url')
823
+ }, options);
824
+
825
+ var bbVer = Backbone.VERSION.split('.');
826
+ var oldSuccessFormat = (parseInt(bbVer[0], 10) === 0 &&
827
+ parseInt(bbVer[1], 10) === 9 &&
828
+ parseInt(bbVer[2], 10) <= 9);
829
+
830
+ var success = queryOptions.success;
831
+ queryOptions.success = function ( resp, status, xhr ) {
832
+
833
+ if ( success ) {
834
+ // This is to keep compatibility with Backbone older than 0.9.10
835
+ if (oldSuccessFormat) {
836
+ success( resp, status, xhr );
837
+ } else {
838
+ success( model, resp, queryOptions );
839
+ }
840
+ }
841
+ if ( model && model.trigger ) {
842
+ model.trigger( 'sync', model, resp, queryOptions );
843
+ }
844
+ };
845
+
846
+ var error = queryOptions.error;
847
+ queryOptions.error = function ( xhr ) {
848
+ if ( error ) {
849
+ error( model, xhr, queryOptions );
850
+ }
851
+ if ( model && model.trigger ) {
852
+ model.trigger( 'error', model, xhr, queryOptions );
853
+ }
854
+ };
855
+
856
+ var xhr = queryOptions.xhr = $.ajax( queryOptions );
857
+ if ( model && model.trigger ) {
858
+ model.trigger('request', model, xhr, queryOptions);
859
+ }
860
+ return xhr;
861
+ },
862
+
863
+ setDefaults: function() {
864
+ var self = this;
865
+
866
+ // Create default values if no others are specified
867
+ _.defaults(self.paginator_ui, {
868
+ firstPage: 0,
869
+ currentPage: 1,
870
+ perPage: 5,
871
+ totalPages: 10,
872
+ pagesInRange: 4
873
+ });
874
+
875
+ // Change scope of 'paginator_ui' object values
876
+ _.each(self.paginator_ui, function(value, key) {
877
+ if (_.isUndefined(self[key])) {
878
+ self[key] = self.paginator_ui[key];
879
+ }
880
+ });
881
+ },
882
+
883
+ requestNextPage: function ( options ) {
884
+ if ( this.currentPage !== undefined ) {
885
+ this.currentPage += 1;
886
+ return this.pager( options );
887
+ } else {
888
+ var response = new $.Deferred();
889
+ response.reject();
890
+ return response.promise();
891
+ }
892
+ },
893
+
894
+ requestPreviousPage: function ( options ) {
895
+ if ( this.currentPage !== undefined ) {
896
+ this.currentPage -= 1;
897
+ return this.pager( options );
898
+ } else {
899
+ var response = new $.Deferred();
900
+ response.reject();
901
+ return response.promise();
902
+ }
903
+ },
904
+
905
+ updateOrder: function ( column ) {
906
+ if (column !== undefined) {
907
+ this.sortField = column;
908
+ this.pager();
909
+ }
910
+
911
+ },
912
+
913
+ goTo: function ( page, options ) {
914
+ if ( page !== undefined ) {
915
+ this.currentPage = parseInt(page, 10);
916
+ return this.pager( options );
917
+ } else {
918
+ var response = new $.Deferred();
919
+ response.reject();
920
+ return response.promise();
921
+ }
922
+ },
923
+
924
+ howManyPer: function ( count ) {
925
+ if( count !== undefined ){
926
+ this.currentPage = this.firstPage;
927
+ this.perPage = count;
928
+ this.pager();
929
+ }
930
+ },
931
+
932
+ info: function () {
933
+
934
+ var info = {
935
+ // If parse() method is implemented and totalRecords is set to the length
936
+ // of the records returned, make it available. Else, default it to 0
937
+ totalRecords: this.totalRecords || 0,
938
+
939
+ currentPage: this.currentPage,
940
+ firstPage: this.firstPage,
941
+ totalPages: Math.ceil(this.totalRecords / this.perPage),
942
+ lastPage: this.totalPages, // should use totalPages in template
943
+ perPage: this.perPage,
944
+ previous:false,
945
+ next:false
946
+ };
947
+
948
+ if (this.currentPage > 1) {
949
+ info.previous = this.currentPage - 1;
950
+ }
951
+
952
+ if (this.currentPage < info.totalPages) {
953
+ info.next = this.currentPage + 1;
954
+ }
955
+
956
+ // left around for backwards compatibility
957
+ info.hasNext = info.next;
958
+ info.hasPrevious = info.next;
959
+
960
+ info.pageSet = this.setPagination(info);
961
+
962
+ this.information = info;
963
+ return info;
964
+ },
965
+
966
+ setPagination: function ( info ) {
967
+
968
+ var pages = [], i = 0, l = 0;
969
+
970
+ // How many adjacent pages should be shown on each side?
971
+ var ADJACENTx2 = this.pagesInRange * 2,
972
+ LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
973
+
974
+ if (LASTPAGE > 1) {
975
+
976
+ // not enough pages to bother breaking it up
977
+ if (LASTPAGE <= (1 + ADJACENTx2)) {
978
+ for (i = 1, l = LASTPAGE; i <= l; i++) {
979
+ pages.push(i);
980
+ }
981
+ }
982
+
983
+ // enough pages to hide some
984
+ else {
985
+
986
+ //close to beginning; only hide later pages
987
+ if (info.currentPage <= (this.pagesInRange + 1)) {
988
+ for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
989
+ pages.push(i);
990
+ }
991
+ }
992
+
993
+ // in middle; hide some front and some back
994
+ else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
995
+ for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
996
+ pages.push(i);
997
+ }
998
+ }
999
+
1000
+ // close to end; only hide early pages
1001
+ else {
1002
+ for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
1003
+ pages.push(i);
1004
+ }
1005
+ }
1006
+ }
1007
+
1008
+ }
1009
+
1010
+ return pages;
1011
+
1012
+ },
1013
+
1014
+ // fetches the latest results from the server
1015
+ pager: function ( options ) {
1016
+ if ( !_.isObject(options) ) {
1017
+ options = {};
1018
+ }
1019
+ return this.fetch( options );
1020
+ },
1021
+
1022
+ url: function(){
1023
+ // Expose url parameter enclosed in this.paginator_core.url to properly
1024
+ // extend Collection and allow Collection CRUD
1025
+ if(this.paginator_core !== undefined && this.paginator_core.url !== undefined){
1026
+ return this.paginator_core.url;
1027
+ } else {
1028
+ return null;
1029
+ }
1030
+ },
1031
+
1032
+ bootstrap: function(options) {
1033
+ _.extend(this, options);
1034
+ this.setDefaults();
1035
+ this.info();
1036
+ return this;
1037
+ }
1038
+ });
1039
+
1040
+ // function aliasing
1041
+ Paginator.requestPager.prototype.nextPage = Paginator.requestPager.prototype.requestNextPage;
1042
+ Paginator.requestPager.prototype.prevPage = Paginator.requestPager.prototype.requestPreviousPage;
1043
+
1044
+ return Paginator;
1045
+
1046
+ }( Backbone, _, jQuery ));