livelist-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .rvmrc
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in livelist-rails.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ //= require mustache
2
+ //= require underscore
3
+ //= require livelist
@@ -0,0 +1,199 @@
1
+ class window.Utilities
2
+ setOptions: (options, context=@) =>
3
+ _.each( options, (value, option) => context[option] = value )
4
+
5
+ class window.LiveList extends Utilities
6
+ constructor: (options) ->
7
+ @globalOptions.listSelector = options.list.renderTo
8
+ @globalOptions.eventName = "livelist:#{options.global.resourceName}"
9
+ @globalOptions.urlPrefix = "/#{options.global.resourceName}"
10
+
11
+ @setOptions(options.global, @globalOptions)
12
+
13
+ @search = new Search(@globalOptions, options.search)
14
+ @filters = new Filters(@globalOptions, options.filters)
15
+ @pagination = new Pagination(@globalOptions, options.pagination)
16
+ @list = new List(@search, @filters, @pagination, @globalOptions, options.list)
17
+
18
+ globalOptions:
19
+ data: null
20
+ resourceName: 'items'
21
+ resourceNameSingular: 'item'
22
+
23
+ class window.List extends Utilities
24
+ constructor: (search, filters, pagination, globalOptions, options = {}) ->
25
+ @data = globalOptions.data
26
+ @fetchRequest = null
27
+ @search = search
28
+ @filters = filters
29
+ @pagination = pagination
30
+
31
+ @setOptions(globalOptions)
32
+ @listTemplate = "{{##{@resourceName}}}{{>#{@resourceNameSingular}}}{{/#{@resourceName}}}"
33
+ @listItemTemplate = '<li>{{id}}</li>'
34
+ @fetchingIndicationClass = 'updating'
35
+ @setOptions(options)
36
+
37
+ $(@renderTo).bind(@eventName, (event, params) => @fetch(filterPresets: null, page: params?.page))
38
+ @fetch(filterPresets: @filters.presets)
39
+
40
+ displayFetchingIndication: => $(@renderTo).addClass(@fetchingIndicationClass)
41
+ removeFetchingIndication: => $(@renderTo).removeClass(@fetchingIndicationClass)
42
+
43
+ renderIndex: (data, textStatus, jqXHR) =>
44
+ @data = data
45
+ @render()
46
+ @pagination.render(@data)
47
+ @filters.filters = _.pluck( @data.filters, 'filter_slug' )
48
+ @filters.render(@data)
49
+
50
+ fetch: (options) ->
51
+ @fetchRequest.abort() if @fetchRequest
52
+ searchTerm = @search.searchTerm()
53
+ params = { filters: {} }
54
+ if options.filterPresets?.length > 0
55
+ params.filters = options.filterPresets
56
+ else
57
+ _.each( @filters.filters, (filter) => params.filters[filter] = @filters.filterSelections( filter ) )
58
+ if searchTerm then params.q = searchTerm
59
+ if options.page then params.page = options.page
60
+ @fetchRequest = $.ajax(
61
+ url: @urlPrefix
62
+ dataType: 'json'
63
+ data: params
64
+ type: @httpMethod
65
+ beforeSend: @displayFetchingIndication
66
+ success: @renderIndex
67
+ )
68
+
69
+ render: ->
70
+ partials = {}
71
+ partials[@resourceNameSingular] = @listItemTemplate
72
+ $(@renderTo).html( Mustache.to_html(@listTemplate, @data, partials) )
73
+ @removeFetchingIndication()
74
+
75
+ class window.Filters extends Utilities
76
+ constructor: (globalOptions, options = {}) ->
77
+ @setOptions(globalOptions)
78
+ @filters = if options.presets then _.keys(options.presets) else []
79
+ @setOptions(options)
80
+ $('input.filter_option', @renderTo).live( 'change', => $(@listSelector).trigger(@eventName) )
81
+ $(@advancedOptionsToggleSelector).click(@handleAdvancedOptionsClick)
82
+
83
+ filtersTemplate: '''
84
+ {{#filters}}
85
+ <div class='filter'>
86
+ <h3>
87
+ {{name}}
88
+ </h3>
89
+ <ul id='{{filter_slug}}_filter_options'>
90
+ {{#options}}
91
+ <label>
92
+ <li>
93
+ <input {{#selected}}checked='checked'{{/selected}}
94
+ class='left filter_option'
95
+ id='filter_{{slug}}'
96
+ name='filters[]'
97
+ type='checkbox'
98
+ value='{{value}}' />
99
+ <div class='left filter_name'>{{name}}</div>
100
+ <div class='right filter_count'>{{count}}</div>
101
+ <div class='clear'></div>
102
+ </li>
103
+ </label>
104
+ {{/options}}
105
+ </ul>
106
+ </div>
107
+ {{/filters}}
108
+ '''
109
+
110
+ filterValues: (filter) -> _.pluck( $(".#{filter}_filter_input"), 'value' )
111
+ filterSelections: (filter) -> _.pluck( $("##{filter}_filter_options input.filter_option:checked"), 'value' )
112
+
113
+ render: (data) -> $(@renderTo).html( Mustache.to_html(@filtersTemplate, data) )
114
+
115
+ handleAdvancedOptionsClick: (event) =>
116
+ event.preventDefault()
117
+ $(@renderTo).slideToggle()
118
+
119
+ class window.Pagination extends Utilities
120
+ constructor: (globalOptions, options = {}) ->
121
+ @pagination = null
122
+ @maxPages = 30
123
+
124
+ @setOptions(globalOptions)
125
+ @emptyListMessage = "<p>No #{@resourceName} matched your filter criteria</p>"
126
+ @setOptions(options)
127
+
128
+ $("#{@renderTo} a").live('click', @handlePaginationLinkClick)
129
+
130
+ paginationTemplate: '''
131
+ {{#isEmpty}}
132
+ {{{emptyListMessage}}}
133
+ {{/isEmpty}}
134
+ {{^isEmpty}}
135
+ {{#previousPage}}
136
+ <a href='{{urlPrefix}}?page={{previousPage}}' data-page='{{previousPage}}'>← Previous</a>
137
+ {{/previousPage}}
138
+ {{^previousPage}}
139
+ <span>← Previous</span>
140
+ {{/previousPage}}
141
+ {{#pages}}
142
+ {{#currentPage}}
143
+ <span>{{page}}</span>
144
+ {{/currentPage}}
145
+ {{^currentPage}}
146
+ <a href='{{urlPrefix}}?page={{page}}' data-page='{{page}}'>{{page}}</a>
147
+ {{/currentPage}}
148
+ {{/pages}}
149
+ {{#nextPage}}
150
+ <a href='{{urlPrefix}}?page={{nextPage}}' data-page='{{nextPage}}'>Next →</a>
151
+ {{/nextPage}}
152
+ {{^nextPage}}
153
+ <span>Next →</span>
154
+ {{/nextPage}}
155
+ {{/isEmpty}}
156
+ '''
157
+
158
+ pagesJSON: (currentPage, totalPages) ->
159
+ groupSize = @maxPages / 2
160
+ firstPage = if currentPage < groupSize then 1 else currentPage - groupSize
161
+ previousPage = firstPage + groupSize * 2 - 1
162
+ lastPage = if previousPage >= totalPages then totalPages else previousPage
163
+ _.map([firstPage..lastPage], (page) ->
164
+ page: page
165
+ currentPage: -> currentPage is page
166
+ )
167
+
168
+ paginationJSON: (pagination) ->
169
+ {
170
+ isEmpty : pagination.total_pages == 0
171
+ emptyListMessage : @emptyListMessage
172
+ currentPage : pagination.current_page
173
+ nextPage : pagination.next_page
174
+ previousPage : pagination.previous_page
175
+ urlPrefix : @urlPrefix
176
+ pages : @pagesJSON(pagination.current_page, pagination.total_pages)
177
+ }
178
+
179
+ render: (data) ->
180
+ @pagination = @paginationJSON(data.pagination)
181
+ $(@renderTo).html( Mustache.to_html(@paginationTemplate, @pagination) )
182
+
183
+ handlePaginationLinkClick: (event) =>
184
+ event.preventDefault()
185
+ $(@listSelector).trigger(@eventName, {page: $(event.target).data('page')})
186
+
187
+ class window.Search extends Utilities
188
+ constructor: (globalOptions, options = {}) ->
189
+ @setOptions(globalOptions)
190
+ @setOptions(options)
191
+ $(@formSelector).submit( (event) => @handleSearchFormSubmit(event) )
192
+
193
+ searchTerm: ->
194
+ q = $(@searchTextInputSelector).val()
195
+ if !q or (q is '') then null else q
196
+
197
+ handleSearchFormSubmit: (event) =>
198
+ event.preventDefault()
199
+ $(@listSelector).trigger(@eventName)
@@ -0,0 +1,274 @@
1
+ (function() {
2
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
3
+ __hasProp = Object.prototype.hasOwnProperty,
4
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
5
+
6
+ window.Utilities = (function() {
7
+
8
+ function Utilities() {
9
+ this.setOptions = __bind(this.setOptions, this);
10
+ }
11
+
12
+ Utilities.prototype.setOptions = function(options, context) {
13
+ var _this = this;
14
+ if (context == null) context = this;
15
+ return _.each(options, function(value, option) {
16
+ return context[option] = value;
17
+ });
18
+ };
19
+
20
+ return Utilities;
21
+
22
+ })();
23
+
24
+ window.LiveList = (function(_super) {
25
+
26
+ __extends(LiveList, _super);
27
+
28
+ function LiveList(options) {
29
+ this.globalOptions.listSelector = options.list.renderTo;
30
+ this.globalOptions.eventName = "livelist:" + options.global.resourceName;
31
+ this.globalOptions.urlPrefix = "/" + options.global.resourceName;
32
+ this.setOptions(options.global, this.globalOptions);
33
+ this.search = new Search(this.globalOptions, options.search);
34
+ this.filters = new Filters(this.globalOptions, options.filters);
35
+ this.pagination = new Pagination(this.globalOptions, options.pagination);
36
+ this.list = new List(this.search, this.filters, this.pagination, this.globalOptions, options.list);
37
+ }
38
+
39
+ LiveList.prototype.globalOptions = {
40
+ data: null,
41
+ resourceName: 'items',
42
+ resourceNameSingular: 'item'
43
+ };
44
+
45
+ return LiveList;
46
+
47
+ })(Utilities);
48
+
49
+ window.List = (function(_super) {
50
+
51
+ __extends(List, _super);
52
+
53
+ function List(search, filters, pagination, globalOptions, options) {
54
+ var _this = this;
55
+ if (options == null) options = {};
56
+ this.renderIndex = __bind(this.renderIndex, this);
57
+ this.removeFetchingIndication = __bind(this.removeFetchingIndication, this);
58
+ this.displayFetchingIndication = __bind(this.displayFetchingIndication, this);
59
+ this.data = globalOptions.data;
60
+ this.fetchRequest = null;
61
+ this.search = search;
62
+ this.filters = filters;
63
+ this.pagination = pagination;
64
+ this.setOptions(globalOptions);
65
+ this.listTemplate = "{{#" + this.resourceName + "}}{{>" + this.resourceNameSingular + "}}{{/" + this.resourceName + "}}";
66
+ this.listItemTemplate = '<li>{{id}}</li>';
67
+ this.fetchingIndicationClass = 'updating';
68
+ this.setOptions(options);
69
+ $(this.renderTo).bind(this.eventName, function(event, params) {
70
+ return _this.fetch({
71
+ filterPresets: null,
72
+ page: params != null ? params.page : void 0
73
+ });
74
+ });
75
+ this.fetch({
76
+ filterPresets: this.filters.presets
77
+ });
78
+ }
79
+
80
+ List.prototype.displayFetchingIndication = function() {
81
+ return $(this.renderTo).addClass(this.fetchingIndicationClass);
82
+ };
83
+
84
+ List.prototype.removeFetchingIndication = function() {
85
+ return $(this.renderTo).removeClass(this.fetchingIndicationClass);
86
+ };
87
+
88
+ List.prototype.renderIndex = function(data, textStatus, jqXHR) {
89
+ this.data = data;
90
+ this.render();
91
+ this.pagination.render(this.data);
92
+ this.filters.filters = _.pluck(this.data.filters, 'filter_slug');
93
+ return this.filters.render(this.data);
94
+ };
95
+
96
+ List.prototype.fetch = function(options) {
97
+ var params, searchTerm, _ref,
98
+ _this = this;
99
+ if (this.fetchRequest) this.fetchRequest.abort();
100
+ searchTerm = this.search.searchTerm();
101
+ params = {
102
+ filters: {}
103
+ };
104
+ if (((_ref = options.filterPresets) != null ? _ref.length : void 0) > 0) {
105
+ params.filters = options.filterPresets;
106
+ } else {
107
+ _.each(this.filters.filters, function(filter) {
108
+ return params.filters[filter] = _this.filters.filterSelections(filter);
109
+ });
110
+ }
111
+ if (searchTerm) params.q = searchTerm;
112
+ if (options.page) params.page = options.page;
113
+ return this.fetchRequest = $.ajax({
114
+ url: this.urlPrefix,
115
+ dataType: 'json',
116
+ data: params,
117
+ type: this.httpMethod,
118
+ beforeSend: this.displayFetchingIndication,
119
+ success: this.renderIndex
120
+ });
121
+ };
122
+
123
+ List.prototype.render = function() {
124
+ var partials;
125
+ partials = {};
126
+ partials[this.resourceNameSingular] = this.listItemTemplate;
127
+ $(this.renderTo).html(Mustache.to_html(this.listTemplate, this.data, partials));
128
+ return this.removeFetchingIndication();
129
+ };
130
+
131
+ return List;
132
+
133
+ })(Utilities);
134
+
135
+ window.Filters = (function(_super) {
136
+
137
+ __extends(Filters, _super);
138
+
139
+ function Filters(globalOptions, options) {
140
+ var _this = this;
141
+ if (options == null) options = {};
142
+ this.handleAdvancedOptionsClick = __bind(this.handleAdvancedOptionsClick, this);
143
+ this.setOptions(globalOptions);
144
+ this.filters = options.presets ? _.keys(options.presets) : [];
145
+ this.setOptions(options);
146
+ $('input.filter_option', this.renderTo).live('change', function() {
147
+ return $(_this.listSelector).trigger(_this.eventName);
148
+ });
149
+ $(this.advancedOptionsToggleSelector).click(this.handleAdvancedOptionsClick);
150
+ }
151
+
152
+ Filters.prototype.filtersTemplate = '{{#filters}}\n<div class=\'filter\'>\n <h3>\n {{name}}\n </h3>\n <ul id=\'{{filter_slug}}_filter_options\'>\n {{#options}}\n <label>\n <li>\n <input {{#selected}}checked=\'checked\'{{/selected}}\n class=\'left filter_option\'\n id=\'filter_{{slug}}\'\n name=\'filters[]\'\n type=\'checkbox\'\n value=\'{{value}}\' />\n <div class=\'left filter_name\'>{{name}}</div>\n <div class=\'right filter_count\'>{{count}}</div>\n <div class=\'clear\'></div>\n </li>\n </label>\n {{/options}}\n </ul>\n</div>\n{{/filters}}';
153
+
154
+ Filters.prototype.filterValues = function(filter) {
155
+ return _.pluck($("." + filter + "_filter_input"), 'value');
156
+ };
157
+
158
+ Filters.prototype.filterSelections = function(filter) {
159
+ return _.pluck($("#" + filter + "_filter_options input.filter_option:checked"), 'value');
160
+ };
161
+
162
+ Filters.prototype.render = function(data) {
163
+ return $(this.renderTo).html(Mustache.to_html(this.filtersTemplate, data));
164
+ };
165
+
166
+ Filters.prototype.handleAdvancedOptionsClick = function(event) {
167
+ event.preventDefault();
168
+ return $(this.renderTo).slideToggle();
169
+ };
170
+
171
+ return Filters;
172
+
173
+ })(Utilities);
174
+
175
+ window.Pagination = (function(_super) {
176
+
177
+ __extends(Pagination, _super);
178
+
179
+ function Pagination(globalOptions, options) {
180
+ if (options == null) options = {};
181
+ this.handlePaginationLinkClick = __bind(this.handlePaginationLinkClick, this);
182
+ this.pagination = null;
183
+ this.maxPages = 30;
184
+ this.setOptions(globalOptions);
185
+ this.emptyListMessage = "<p>No " + this.resourceName + " matched your filter criteria</p>";
186
+ this.setOptions(options);
187
+ $("" + this.renderTo + " a").live('click', this.handlePaginationLinkClick);
188
+ }
189
+
190
+ Pagination.prototype.paginationTemplate = '{{#isEmpty}}\n {{{emptyListMessage}}}\n{{/isEmpty}}\n{{^isEmpty}}\n{{#previousPage}}\n <a href=\'{{urlPrefix}}?page={{previousPage}}\' data-page=\'{{previousPage}}\'>← Previous</a>\n{{/previousPage}}\n{{^previousPage}}\n <span>← Previous</span>\n{{/previousPage}}\n{{#pages}}\n {{#currentPage}}\n <span>{{page}}</span>\n {{/currentPage}}\n {{^currentPage}}\n <a href=\'{{urlPrefix}}?page={{page}}\' data-page=\'{{page}}\'>{{page}}</a>\n {{/currentPage}}\n{{/pages}}\n{{#nextPage}}\n <a href=\'{{urlPrefix}}?page={{nextPage}}\' data-page=\'{{nextPage}}\'>Next →</a>\n{{/nextPage}}\n{{^nextPage}}\n <span>Next →</span>\n{{/nextPage}}\n{{/isEmpty}}';
191
+
192
+ Pagination.prototype.pagesJSON = function(currentPage, totalPages) {
193
+ var firstPage, groupSize, lastPage, previousPage, _i, _results;
194
+ groupSize = this.maxPages / 2;
195
+ firstPage = currentPage < groupSize ? 1 : currentPage - groupSize;
196
+ previousPage = firstPage + groupSize * 2 - 1;
197
+ lastPage = previousPage >= totalPages ? totalPages : previousPage;
198
+ return _.map((function() {
199
+ _results = [];
200
+ for (var _i = firstPage; firstPage <= lastPage ? _i <= lastPage : _i >= lastPage; firstPage <= lastPage ? _i++ : _i--){ _results.push(_i); }
201
+ return _results;
202
+ }).apply(this), function(page) {
203
+ return {
204
+ page: page,
205
+ currentPage: function() {
206
+ return currentPage === page;
207
+ }
208
+ };
209
+ });
210
+ };
211
+
212
+ Pagination.prototype.paginationJSON = function(pagination) {
213
+ return {
214
+ isEmpty: pagination.total_pages === 0,
215
+ emptyListMessage: this.emptyListMessage,
216
+ currentPage: pagination.current_page,
217
+ nextPage: pagination.next_page,
218
+ previousPage: pagination.previous_page,
219
+ urlPrefix: this.urlPrefix,
220
+ pages: this.pagesJSON(pagination.current_page, pagination.total_pages)
221
+ };
222
+ };
223
+
224
+ Pagination.prototype.render = function(data) {
225
+ this.pagination = this.paginationJSON(data.pagination);
226
+ return $(this.renderTo).html(Mustache.to_html(this.paginationTemplate, this.pagination));
227
+ };
228
+
229
+ Pagination.prototype.handlePaginationLinkClick = function(event) {
230
+ event.preventDefault();
231
+ return $(this.listSelector).trigger(this.eventName, {
232
+ page: $(event.target).data('page')
233
+ });
234
+ };
235
+
236
+ return Pagination;
237
+
238
+ })(Utilities);
239
+
240
+ window.Search = (function(_super) {
241
+
242
+ __extends(Search, _super);
243
+
244
+ function Search(globalOptions, options) {
245
+ var _this = this;
246
+ if (options == null) options = {};
247
+ this.handleSearchFormSubmit = __bind(this.handleSearchFormSubmit, this);
248
+ this.setOptions(globalOptions);
249
+ this.setOptions(options);
250
+ $(this.formSelector).submit(function(event) {
251
+ return _this.handleSearchFormSubmit(event);
252
+ });
253
+ }
254
+
255
+ Search.prototype.searchTerm = function() {
256
+ var q;
257
+ q = $(this.searchTextInputSelector).val();
258
+ if (!q || (q === '')) {
259
+ return null;
260
+ } else {
261
+ return q;
262
+ }
263
+ };
264
+
265
+ Search.prototype.handleSearchFormSubmit = function(event) {
266
+ event.preventDefault();
267
+ return $(this.listSelector).trigger(this.eventName);
268
+ };
269
+
270
+ return Search;
271
+
272
+ })(Utilities);
273
+
274
+ }).call(this);