livelist-rails 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);