backbone_rails_extensions 0.9.3
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/backbone_rails_extensions.gemspec +20 -0
- data/lib/assets/javascripts/jquery/serialize-object.js +35 -0
- data/lib/assets/javascripts/list/namespace.js +4 -0
- data/lib/assets/javascripts/list/routers/base.js +43 -0
- data/lib/assets/javascripts/list/views/collection_view.js +27 -0
- data/lib/assets/javascripts/list/views/search.js +30 -0
- data/lib/assets/javascripts/list/views/simple_collection_view.js +56 -0
- data/lib/assets/javascripts/loader/namespace.js +4 -0
- data/lib/assets/javascripts/loader/sync.js +24 -0
- data/lib/assets/javascripts/loader/utils/counting_latch.js +26 -0
- data/lib/assets/javascripts/loader/views/base.js +41 -0
- data/lib/assets/javascripts/pagination/collections/paginated_collection.js +24 -0
- data/lib/assets/javascripts/pagination/models/pagination.js +57 -0
- data/lib/assets/javascripts/pagination/namespace.js +5 -0
- data/lib/assets/javascripts/pagination/templates/pagination.jst.ejs +6 -0
- data/lib/assets/javascripts/pagination/views/pagination.js +164 -0
- data/lib/backbone_rails_extensions.rb +6 -0
- data/lib/backbone_rails_extensions/version.rb +3 -0
- data/vendor/assets/javascripts/jquery.spin.js +48 -0
- data/vendor/assets/javascripts/spin.js +295 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2e1cae0d9815728d45601caf3e5db499706ebd00
|
4
|
+
data.tar.gz: c589d820ef317fbd5b73539085c078797d165144
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3aefb0bd3fdaaefd9f0e9bd42e63cf54b35822cb0781c7a453fd70a277da10ab4bcc4f276064fce16ddebe5d289315522ca72fa21d0c332378b1410055c7420
|
7
|
+
data.tar.gz: 63c49bd6f35ba5b324e79438347df43dd90d7107502471fedbeb8ec9bdd1e1b56369413d5a781f08f21817633a49e275130a3ad9ed2a8199ebf52b5c7b039d51
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Coroutine LLC
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# BackboneRailsExtensions
|
2
|
+
|
3
|
+
This gem provides a set of common extensions to Backbone.js. These extensions include simple collection views, paginated collection views, searching, and loading indicators.
|
4
|
+
|
5
|
+
All files are included in the asset pipeline by default. To leverage the files in your project, simply reference the components in your manifest files.
|
6
|
+
|
7
|
+
## Dependencies
|
8
|
+
|
9
|
+
The project has a dependency on Spin.js. Appropriate files are included in the vendor directory.
|
10
|
+
|
11
|
+
## Versioning
|
12
|
+
|
13
|
+
Major and minor version numbers are pegged to Backbone.js. Patch versions are specific
|
14
|
+
to this project.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
gem 'backbone-rails-extensions', :git => 'git@github.com:coroutine/backbone-rails-extensions.git',
|
21
|
+
:tag => '0.9.0'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install backbone-rails-extensions
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
TODO: Write usage instructions here
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
1. Fork it
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
39
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
40
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
41
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'backbone_rails_extensions/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "backbone_rails_extensions"
|
8
|
+
gem.version = BackboneRailsExtensions::VERSION
|
9
|
+
gem.authors = ["Coroutine", "Tim Lowrimore", "John Dugan"]
|
10
|
+
gem.email = ["gems@coroutine.com"]
|
11
|
+
gem.description = %q{This gem provides a set of Backbone.js extensions commonly used in Coroutine projects. These extensions include simple collection views, paginated collection views, searching, and loading indicators.}
|
12
|
+
gem.summary = %q{This gem provides a set of Backbone.js extensions commonly used in Coroutine projects.}
|
13
|
+
gem.homepage = "https://github.com/coroutine/backbone-rails-extensions"
|
14
|
+
gem.licenses = ['MIT']
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
(function($) {
|
2
|
+
var methods = {
|
3
|
+
setValue: function(path, value, obj) {
|
4
|
+
if(path.length) {
|
5
|
+
var attr = path.shift();
|
6
|
+
if(attr) {
|
7
|
+
obj[attr] = methods.setValue(path, value, obj[attr] || {});
|
8
|
+
return obj;
|
9
|
+
} else {
|
10
|
+
if(obj.push) {
|
11
|
+
obj.push(value);
|
12
|
+
return obj;
|
13
|
+
} else {
|
14
|
+
return [value];
|
15
|
+
}
|
16
|
+
}
|
17
|
+
} else {
|
18
|
+
return value;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
};
|
22
|
+
|
23
|
+
$.fn.serializeObject = function() {
|
24
|
+
var obj = {},
|
25
|
+
params = this.serializeArray(),
|
26
|
+
path = null;
|
27
|
+
|
28
|
+
$.each(params, function() {
|
29
|
+
path = this.name.replace(/\]/g, "").split(/\[/);
|
30
|
+
methods.setValue(path, this.value, obj);
|
31
|
+
});
|
32
|
+
|
33
|
+
return obj;
|
34
|
+
};
|
35
|
+
})(jQuery);
|
@@ -0,0 +1,43 @@
|
|
1
|
+
//= require list/namespace
|
2
|
+
|
3
|
+
List.Router.Base = Backbone.Router.extend({
|
4
|
+
routes: {
|
5
|
+
"": "index",
|
6
|
+
"page/:page": "_page",
|
7
|
+
"query/*query/within/*within": "_search",
|
8
|
+
"page/:page/query/*query/within/*within": "_searchWithPage"
|
9
|
+
},
|
10
|
+
|
11
|
+
currentParams: {},
|
12
|
+
|
13
|
+
initialize: function() {
|
14
|
+
this.initializeView();
|
15
|
+
},
|
16
|
+
|
17
|
+
initializeView: function() {
|
18
|
+
this._raiseUnimplementedError();
|
19
|
+
},
|
20
|
+
|
21
|
+
index: function(parameters) {
|
22
|
+
this._raiseUnimplementedError();
|
23
|
+
},
|
24
|
+
|
25
|
+
_raiseUnimplementedError: function() {
|
26
|
+
throw "method not implemented";
|
27
|
+
},
|
28
|
+
|
29
|
+
_search: function(query, within) {
|
30
|
+
this._searchWithPage(1, query, within);
|
31
|
+
},
|
32
|
+
|
33
|
+
_searchWithPage: function(page, query, within) {
|
34
|
+
this.index({
|
35
|
+
query: query,
|
36
|
+
within: within,
|
37
|
+
page: page || 1 });
|
38
|
+
},
|
39
|
+
|
40
|
+
_page: function(page) {
|
41
|
+
this.index({ page: page || 1 });
|
42
|
+
}
|
43
|
+
});
|
@@ -0,0 +1,27 @@
|
|
1
|
+
//= require list/namespace
|
2
|
+
|
3
|
+
List.View.CollectionView = Backbone.View.extend({
|
4
|
+
setCollection: function(collection) {
|
5
|
+
if(collection !== this.collection) {
|
6
|
+
if(this.collection) {
|
7
|
+
this.collection.off("add remove reset", this.render, this);
|
8
|
+
this.collection.off("error", this.renderError, this);
|
9
|
+
}
|
10
|
+
|
11
|
+
this.collection = collection;
|
12
|
+
if(this.collection) {
|
13
|
+
this.collection.on("add remove reset", this.render, this);
|
14
|
+
this.collection.on("error", this.renderError, this);
|
15
|
+
this.render();
|
16
|
+
}
|
17
|
+
}
|
18
|
+
},
|
19
|
+
|
20
|
+
render: function() {
|
21
|
+
throw "method not implemented";
|
22
|
+
},
|
23
|
+
|
24
|
+
renderError: function(data, response) {
|
25
|
+
alert("An error occurred while fetching the records from the server.");
|
26
|
+
}
|
27
|
+
});
|
@@ -0,0 +1,30 @@
|
|
1
|
+
//= require list/namespace
|
2
|
+
|
3
|
+
List.View.Search = Backbone.View.extend({
|
4
|
+
events: {
|
5
|
+
"keydown #search_field_name": "_onNameChanged",
|
6
|
+
"change #search_field_type": "_onTypeChanged"
|
7
|
+
},
|
8
|
+
|
9
|
+
// handlers
|
10
|
+
_onNameChanged: function(evt) {
|
11
|
+
if (evt.keyCode == 13) { // suppress enter key
|
12
|
+
evt.preventDefault();
|
13
|
+
}
|
14
|
+
this._doSearch();
|
15
|
+
},
|
16
|
+
|
17
|
+
_onTypeChanged: function(evt) {
|
18
|
+
this._doSearch();
|
19
|
+
},
|
20
|
+
|
21
|
+
// actions
|
22
|
+
_doSearch: _.debounce(function() {
|
23
|
+
var query = escape(this.$("#search_field_name").val()),
|
24
|
+
within = this.$("#search_field_type").val(),
|
25
|
+
route = [ "query", query, "within", within ].join("/");
|
26
|
+
|
27
|
+
Backbone.history.navigate(route, { trigger: true });
|
28
|
+
}, 500)
|
29
|
+
|
30
|
+
});
|
@@ -0,0 +1,56 @@
|
|
1
|
+
//= require list/namespace
|
2
|
+
//= require list/views/collection_view
|
3
|
+
|
4
|
+
List.View.SimpleCollectionView = List.View.CollectionView.extend({
|
5
|
+
|
6
|
+
//-----------------------------------------------------
|
7
|
+
// initialization
|
8
|
+
//-----------------------------------------------------
|
9
|
+
|
10
|
+
initialize: function() {
|
11
|
+
this.beforeInitialize();
|
12
|
+
|
13
|
+
if(!this.getItemClass) {
|
14
|
+
throw "The method 'getItemClass' was expected, but was not found. \
|
15
|
+
Make sure you've specified this method in your subclasses of \
|
16
|
+
List.View.SimpleCollectionView"
|
17
|
+
}
|
18
|
+
this.itemViews = [];
|
19
|
+
|
20
|
+
_.bindAll(this, "render", "_appendItemToView");
|
21
|
+
|
22
|
+
this.afterInitialize();
|
23
|
+
},
|
24
|
+
|
25
|
+
render: function() {
|
26
|
+
this._clearItemList();
|
27
|
+
if(this.collection) {
|
28
|
+
_.each(this.collection.models, this._appendItemToView);
|
29
|
+
}
|
30
|
+
},
|
31
|
+
|
32
|
+
|
33
|
+
//-----------------------------------------------------
|
34
|
+
// public methods
|
35
|
+
//-----------------------------------------------------
|
36
|
+
|
37
|
+
// callbacks: override if needed
|
38
|
+
afterInitialize: function() {},
|
39
|
+
beforeInitialize: function() {},
|
40
|
+
|
41
|
+
|
42
|
+
//-----------------------------------------------------
|
43
|
+
// private methods
|
44
|
+
//-----------------------------------------------------
|
45
|
+
|
46
|
+
_appendItemToView: function(item) {
|
47
|
+
var itemClass = this.getItemClass(),
|
48
|
+
itemView = new itemClass({ model: item });
|
49
|
+
|
50
|
+
this.itemViews.push(itemView);
|
51
|
+
this.$el.append(itemView.el);
|
52
|
+
},
|
53
|
+
_clearItemList: function() {
|
54
|
+
while(this.itemViews.length) { this.itemViews.pop().remove(); }
|
55
|
+
}
|
56
|
+
});
|
@@ -0,0 +1,24 @@
|
|
1
|
+
//= require loader/namespace
|
2
|
+
|
3
|
+
Loader.customizeBackboneSync = _.once(function() {
|
4
|
+
var customSync = function(method, model, options) {
|
5
|
+
var success = options.success,
|
6
|
+
error = options.error,
|
7
|
+
showLoader = _.has(options, "showLoader") ? options.showLoader : true;
|
8
|
+
|
9
|
+
options.success = function() {
|
10
|
+
success.apply(this, arguments);
|
11
|
+
Loader.View.Base.hide();
|
12
|
+
};
|
13
|
+
|
14
|
+
options.error = function() {
|
15
|
+
error.apply(this, arguments);
|
16
|
+
Loader.View.Base.hide();
|
17
|
+
};
|
18
|
+
|
19
|
+
if(showLoader) Loader.View.Base.show();
|
20
|
+
Backbone.sync.call(this, method, model, options);
|
21
|
+
};
|
22
|
+
|
23
|
+
Backbone.Model.prototype.sync = Backbone.Collection.prototype.sync = customSync;
|
24
|
+
})
|
@@ -0,0 +1,26 @@
|
|
1
|
+
//= require loader/namespace
|
2
|
+
|
3
|
+
Loader.Util.CountingLatch = function(initialCount) {
|
4
|
+
this.count = initialCount || 0;
|
5
|
+
}
|
6
|
+
|
7
|
+
_.extend(Loader.Util.CountingLatch.prototype, Backbone.Events, {
|
8
|
+
countUp: function() {
|
9
|
+
this.count++;
|
10
|
+
this._check();
|
11
|
+
},
|
12
|
+
|
13
|
+
countDown: function() {
|
14
|
+
// ensure the count never goes below zero.
|
15
|
+
this.count = Math.max(0, this.count - 1);
|
16
|
+
this._check();
|
17
|
+
},
|
18
|
+
|
19
|
+
_check: function() {
|
20
|
+
if(this.count) {
|
21
|
+
this.trigger("latch:counting");
|
22
|
+
} else {
|
23
|
+
this.trigger("latch:complete");
|
24
|
+
}
|
25
|
+
}
|
26
|
+
});
|
@@ -0,0 +1,41 @@
|
|
1
|
+
//= require jquery.spin
|
2
|
+
//= require loader/namespace
|
3
|
+
//= require loader/utils/counting_latch
|
4
|
+
|
5
|
+
Loader.View.Base = {
|
6
|
+
CONFIG: {
|
7
|
+
lines: 12,
|
8
|
+
length: 3,
|
9
|
+
width: 4,
|
10
|
+
radius: 15,
|
11
|
+
trail: 34,
|
12
|
+
speed: 1.3,
|
13
|
+
shadow: true,
|
14
|
+
hwaccel: true,
|
15
|
+
color: "#FFF"
|
16
|
+
},
|
17
|
+
|
18
|
+
initialize: _.once(function() {
|
19
|
+
_.bindAll(this, "show", "hide");
|
20
|
+
|
21
|
+
this.latch = new Loader.Util.CountingLatch();
|
22
|
+
this.latch.on("latch:counting", this._showLoaders, this);
|
23
|
+
this.latch.on("latch:complete", this._hideLoaders, this);
|
24
|
+
}),
|
25
|
+
|
26
|
+
show: function() {
|
27
|
+
this.latch.countUp();
|
28
|
+
},
|
29
|
+
|
30
|
+
hide: function() {
|
31
|
+
this.latch.countDown();
|
32
|
+
},
|
33
|
+
|
34
|
+
_showLoaders: function() {
|
35
|
+
$("#loading-indicator").fadeIn(50).spin(this.CONFIG);
|
36
|
+
},
|
37
|
+
|
38
|
+
_hideLoaders: function() {
|
39
|
+
$("#loading-indicator").fadeOut(50).spin(false);
|
40
|
+
}
|
41
|
+
};
|
@@ -0,0 +1,24 @@
|
|
1
|
+
//= require pagination/namespace
|
2
|
+
//= require pagination/models/pagination
|
3
|
+
|
4
|
+
Pagination.Collection.PaginatedCollection = Backbone.Collection.extend({
|
5
|
+
pagination: new Pagination.Model.Pagination(),
|
6
|
+
|
7
|
+
initialize: function() {
|
8
|
+
this.on("destroy", this._decrement, this);
|
9
|
+
this.on("add", this._increment, this);
|
10
|
+
},
|
11
|
+
|
12
|
+
parse: function(response) {
|
13
|
+
this.pagination.set(response.pagination);
|
14
|
+
return response;
|
15
|
+
},
|
16
|
+
|
17
|
+
_decrement: function() {
|
18
|
+
this.pagination.decrement("totalCount");
|
19
|
+
},
|
20
|
+
|
21
|
+
_increment: function() {
|
22
|
+
this.pagination.increment("totalCount");
|
23
|
+
}
|
24
|
+
});
|
@@ -0,0 +1,57 @@
|
|
1
|
+
//= require pagination/namespace
|
2
|
+
|
3
|
+
Pagination.Model.Pagination = Backbone.Model.extend({
|
4
|
+
defaults: {
|
5
|
+
"totalCount": 0,
|
6
|
+
"pageCount": 0,
|
7
|
+
"numPages": 0,
|
8
|
+
"currentPage": 1,
|
9
|
+
"offsetValue": 0,
|
10
|
+
"maxPageLinks": 10
|
11
|
+
},
|
12
|
+
|
13
|
+
validate: function(attrs) {
|
14
|
+
var nonNumericAttrs = _(attrs).reduce(function(m, v, k) {
|
15
|
+
if(!_(v).isNumber()) {
|
16
|
+
m.push("'" + k + "' must be a real number.");
|
17
|
+
}
|
18
|
+
return m;
|
19
|
+
}, []);
|
20
|
+
|
21
|
+
if(nonNumericAttrs.length) {
|
22
|
+
return nonNumericAttrs.join("\n");
|
23
|
+
}
|
24
|
+
},
|
25
|
+
|
26
|
+
increment: function(field) {
|
27
|
+
var incremented = this.get(field) + 1,
|
28
|
+
args = {};
|
29
|
+
|
30
|
+
args[field] = incremented;
|
31
|
+
this.set(args);
|
32
|
+
},
|
33
|
+
|
34
|
+
decrement: function(field) {
|
35
|
+
var decremented = Math.max(0, this.get(field) - 1),
|
36
|
+
args = {};
|
37
|
+
|
38
|
+
args[field] = decremented;
|
39
|
+
this.set(args);
|
40
|
+
},
|
41
|
+
|
42
|
+
isFirstPage: function() {
|
43
|
+
return this.get("currentPage") == 1;
|
44
|
+
},
|
45
|
+
|
46
|
+
isLastPage: function() {
|
47
|
+
return this.get("currentPage") >= this.get("numPages");
|
48
|
+
},
|
49
|
+
|
50
|
+
nextPage: function() {
|
51
|
+
return this.get("currentPage") + 1;
|
52
|
+
},
|
53
|
+
|
54
|
+
prevPage: function() {
|
55
|
+
return Math.max(1, this.get("currentPage") - 1);
|
56
|
+
}
|
57
|
+
});
|
@@ -0,0 +1,164 @@
|
|
1
|
+
//= require pagination/namespace
|
2
|
+
//= require pagination/templates/pagination
|
3
|
+
|
4
|
+
Pagination.View.Pagination = Backbone.View.extend({
|
5
|
+
tagName: "div",
|
6
|
+
infoTemplate: _.template("<%= start %> - <%= end %> of <%= total %>"),
|
7
|
+
hashPartExpr: /(page\/\d+)/,
|
8
|
+
|
9
|
+
events: {
|
10
|
+
"click .prev": "fetchPreviousPage",
|
11
|
+
"click .next": "fetchNextPage",
|
12
|
+
"click .page_numbers a": "fetchPage"
|
13
|
+
},
|
14
|
+
|
15
|
+
initialize: function(options) {
|
16
|
+
if(!this.model) {
|
17
|
+
throw "No pagination model was specified.";
|
18
|
+
}
|
19
|
+
|
20
|
+
if(_(options.infoTemplate).isFunction()) {
|
21
|
+
this.infoTemplate = options.infoTemplate;
|
22
|
+
}
|
23
|
+
|
24
|
+
this.model.on("change", function() {
|
25
|
+
this.renderInfo();
|
26
|
+
this.renderLinks();
|
27
|
+
}, this);
|
28
|
+
|
29
|
+
this.render();
|
30
|
+
},
|
31
|
+
|
32
|
+
render: function() {
|
33
|
+
this.$el.append(JST["pagination/templates/pagination"]());
|
34
|
+
this.renderInfo();
|
35
|
+
this.renderLinks();
|
36
|
+
},
|
37
|
+
|
38
|
+
renderInfo: function() {
|
39
|
+
var model = this.model,
|
40
|
+
total = model.get("totalCount"),
|
41
|
+
pageCount = model.get("pageCount"),
|
42
|
+
numPages = model.get("numPages"),
|
43
|
+
currentPage = model.get("currentPage"),
|
44
|
+
offsetValue = model.get("offsetValue"),
|
45
|
+
info = this.infoTemplate({
|
46
|
+
start: offsetValue + 1,
|
47
|
+
end: pageCount + offsetValue,
|
48
|
+
total: total
|
49
|
+
});
|
50
|
+
|
51
|
+
this.$(".pagination_totals").html(info);
|
52
|
+
},
|
53
|
+
|
54
|
+
renderLinks: function() {
|
55
|
+
this.renderPreviousAndNextLinks();
|
56
|
+
this.renderPageLinks();
|
57
|
+
},
|
58
|
+
|
59
|
+
renderPreviousAndNextLinks: function() {
|
60
|
+
var isFirstPage = this.model.isFirstPage(),
|
61
|
+
isLastPage = this.model.isLastPage(),
|
62
|
+
linkPrev = this.$(".prev"),
|
63
|
+
linkNext = this.$(".next");
|
64
|
+
|
65
|
+
if(isFirstPage) {
|
66
|
+
linkPrev.addClass("disabled");
|
67
|
+
} else {
|
68
|
+
linkPrev.removeClass("disabled");
|
69
|
+
}
|
70
|
+
|
71
|
+
if(isLastPage) {
|
72
|
+
linkNext.addClass("disabled");
|
73
|
+
} else {
|
74
|
+
linkNext.removeClass("disabled");
|
75
|
+
}
|
76
|
+
},
|
77
|
+
|
78
|
+
renderPageLinks: function() {
|
79
|
+
var numPages = this.model.get("numPages"),
|
80
|
+
maxPageLinks = Math.min(this.model.get("maxPageLinks"), numPages),
|
81
|
+
loopLinks = Math.max(maxPageLinks - 2, 0),
|
82
|
+
currentPage = this.model.get("currentPage"),
|
83
|
+
pageNumbers = this.$(".page_numbers");
|
84
|
+
|
85
|
+
// clear the previous page numbers
|
86
|
+
pageNumbers.empty();
|
87
|
+
|
88
|
+
// Add the first page
|
89
|
+
pageNumbers.append(this._createPageLink(1, currentPage));
|
90
|
+
|
91
|
+
if(numPages > 1) {
|
92
|
+
var maxOffset = Math.max(currentPage - loopLinks, 0) + maxPageLinks,
|
93
|
+
minOffset = Math.min(maxOffset, numPages) - maxPageLinks;
|
94
|
+
|
95
|
+
// if we have an offset value greater than zero, render an ellipsis
|
96
|
+
// after the page-1 link.
|
97
|
+
if(minOffset) { pageNumbers.append("…"); }
|
98
|
+
|
99
|
+
// render page links between the first and last page.
|
100
|
+
_(loopLinks).times(_.bind(function(i) {
|
101
|
+
pageNumbers.append(
|
102
|
+
this._createPageLink(i + 2 + minOffset, currentPage));
|
103
|
+
}, this));
|
104
|
+
|
105
|
+
// If the tail of our 'in-between' links is not within
|
106
|
+
// range of the last page link, such that it creates a
|
107
|
+
// natural numeric series upto the last page link, render
|
108
|
+
// an ellipsis.
|
109
|
+
if(maxOffset < numPages) {
|
110
|
+
pageNumbers.append("…");
|
111
|
+
}
|
112
|
+
|
113
|
+
// Add the last page
|
114
|
+
pageNumbers.append(this._createPageLink(numPages, currentPage));
|
115
|
+
}
|
116
|
+
},
|
117
|
+
|
118
|
+
fetchPreviousPage: function(evt) {
|
119
|
+
this._fetchPage(evt, { direction: "prevPage" });
|
120
|
+
},
|
121
|
+
|
122
|
+
fetchNextPage: function(evt) {
|
123
|
+
this._fetchPage(evt, { direction: "nextPage" });
|
124
|
+
},
|
125
|
+
|
126
|
+
fetchPage: function(evt) {
|
127
|
+
var link = $(evt.currentTarget),
|
128
|
+
page = link.data("page");
|
129
|
+
|
130
|
+
this._fetchPage(evt, { page: page });
|
131
|
+
},
|
132
|
+
|
133
|
+
_createPageLink: function(pageNumber, currentPage) {
|
134
|
+
var attrs = { "href": "#", "data-page": pageNumber, "text": pageNumber };
|
135
|
+
|
136
|
+
if(pageNumber == currentPage) {
|
137
|
+
attrs["class"] = "disabled";
|
138
|
+
}
|
139
|
+
return $('<a/>', attrs);
|
140
|
+
},
|
141
|
+
|
142
|
+
_fetchPage: function(evt, options) {
|
143
|
+
evt.preventDefault();
|
144
|
+
var link = $(evt.currentTarget),
|
145
|
+
enabled = !link.hasClass("disabled");
|
146
|
+
|
147
|
+
if(enabled) {
|
148
|
+
var pageNum = options.page ? options.page : this.model[options.direction](),
|
149
|
+
fragment = ["page", pageNum].join("/"),
|
150
|
+
route = this._updateRoute(fragment);
|
151
|
+
|
152
|
+
Backbone.history.navigate(route, { trigger: true });
|
153
|
+
}
|
154
|
+
},
|
155
|
+
|
156
|
+
_updateRoute: function(fragment) {
|
157
|
+
var hash = location.hash;
|
158
|
+
if(hash.match(this.hashPartExpr)) {
|
159
|
+
return hash.replace(RegExp.$1, fragment);
|
160
|
+
} else {
|
161
|
+
return _([fragment, hash.replace("#", "")]).compact().join("/");
|
162
|
+
}
|
163
|
+
}
|
164
|
+
});
|
@@ -0,0 +1,48 @@
|
|
1
|
+
//= require spin
|
2
|
+
/*
|
3
|
+
|
4
|
+
You can now create a spinner using any of the variants below:
|
5
|
+
|
6
|
+
$("#el").spin(); // Produces default Spinner using the text color of #el.
|
7
|
+
$("#el").spin("small"); // Produces a 'small' Spinner using the text color of #el.
|
8
|
+
$("#el").spin("large", "white"); // Produces a 'large' Spinner in white (or any valid CSS color).
|
9
|
+
$("#el").spin({ ... }); // Produces a Spinner using your custom settings.
|
10
|
+
|
11
|
+
$("#el").spin(false); // Kills the spinner.
|
12
|
+
|
13
|
+
*/
|
14
|
+
(function($) {
|
15
|
+
$.fn.spin = function(opts, color) {
|
16
|
+
var presets = {
|
17
|
+
"tiny": { lines: 8, length: 2, width: 2, radius: 3 },
|
18
|
+
"small": { lines: 8, length: 4, width: 3, radius: 5 },
|
19
|
+
"large": { lines: 10, length: 8, width: 4, radius: 8 }
|
20
|
+
};
|
21
|
+
if (Spinner) {
|
22
|
+
return this.each(function() {
|
23
|
+
var $this = $(this),
|
24
|
+
data = $this.data();
|
25
|
+
|
26
|
+
if (data.spinner) {
|
27
|
+
data.spinner.stop();
|
28
|
+
delete data.spinner;
|
29
|
+
}
|
30
|
+
if (opts !== false) {
|
31
|
+
if (typeof opts === "string") {
|
32
|
+
if (opts in presets) {
|
33
|
+
opts = presets[opts];
|
34
|
+
} else {
|
35
|
+
opts = {};
|
36
|
+
}
|
37
|
+
if (color) {
|
38
|
+
opts.color = color;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
data.spinner = new Spinner($.extend({color: $this.css('color')}, opts)).spin(this);
|
42
|
+
}
|
43
|
+
});
|
44
|
+
} else {
|
45
|
+
throw "Spinner class not available.";
|
46
|
+
}
|
47
|
+
};
|
48
|
+
})(jQuery);
|
@@ -0,0 +1,295 @@
|
|
1
|
+
//fgnass.github.com/spin.js#v1.2.3
|
2
|
+
(function(window, document, undefined) {
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Copyright (c) 2011 Felix Gnass [fgnass at neteye dot de]
|
6
|
+
* Licensed under the MIT license
|
7
|
+
*/
|
8
|
+
|
9
|
+
var prefixes = ['webkit', 'Moz', 'ms', 'O'], /* Vendor prefixes */
|
10
|
+
animations = {}, /* Animation rules keyed by their name */
|
11
|
+
useCssAnimations;
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Utility function to create elements. If no tag name is given,
|
15
|
+
* a DIV is created. Optionally properties can be passed.
|
16
|
+
*/
|
17
|
+
function createEl(tag, prop) {
|
18
|
+
var el = document.createElement(tag || 'div'),
|
19
|
+
n;
|
20
|
+
|
21
|
+
for(n in prop) {
|
22
|
+
el[n] = prop[n];
|
23
|
+
}
|
24
|
+
return el;
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Appends children and returns the parent.
|
29
|
+
*/
|
30
|
+
function ins(parent /* child1, child2, ...*/) {
|
31
|
+
for(var i=1, n=arguments.length; i<n; i++) {
|
32
|
+
parent.appendChild(arguments[i]);
|
33
|
+
}
|
34
|
+
return parent;
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Insert a new stylesheet to hold the @keyframe or VML rules.
|
39
|
+
*/
|
40
|
+
var sheet = (function() {
|
41
|
+
var el = createEl('style');
|
42
|
+
ins(document.getElementsByTagName('head')[0], el);
|
43
|
+
return el.sheet || el.styleSheet;
|
44
|
+
})();
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Creates an opacity keyframe animation rule and returns its name.
|
48
|
+
* Since most mobile Webkits have timing issues with animation-delay,
|
49
|
+
* we create separate rules for each line/segment.
|
50
|
+
*/
|
51
|
+
function addAnimation(alpha, trail, i, lines) {
|
52
|
+
var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-'),
|
53
|
+
start = 0.01 + i/lines*100,
|
54
|
+
z = Math.max(1-(1-alpha)/trail*(100-start) , alpha),
|
55
|
+
prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase(),
|
56
|
+
pre = prefix && '-'+prefix+'-' || '';
|
57
|
+
|
58
|
+
if (!animations[name]) {
|
59
|
+
sheet.insertRule(
|
60
|
+
'@' + pre + 'keyframes ' + name + '{' +
|
61
|
+
'0%{opacity:'+z+'}' +
|
62
|
+
start + '%{opacity:'+ alpha + '}' +
|
63
|
+
(start+0.01) + '%{opacity:1}' +
|
64
|
+
(start+trail)%100 + '%{opacity:'+ alpha + '}' +
|
65
|
+
'100%{opacity:'+ z + '}' +
|
66
|
+
'}', 0);
|
67
|
+
animations[name] = 1;
|
68
|
+
}
|
69
|
+
return name;
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Tries various vendor prefixes and returns the first supported property.
|
74
|
+
**/
|
75
|
+
function vendor(el, prop) {
|
76
|
+
var s = el.style,
|
77
|
+
pp,
|
78
|
+
i;
|
79
|
+
|
80
|
+
if(s[prop] !== undefined) return prop;
|
81
|
+
prop = prop.charAt(0).toUpperCase() + prop.slice(1);
|
82
|
+
for(i=0; i<prefixes.length; i++) {
|
83
|
+
pp = prefixes[i]+prop;
|
84
|
+
if(s[pp] !== undefined) return pp;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Sets multiple style properties at once.
|
90
|
+
*/
|
91
|
+
function css(el, prop) {
|
92
|
+
for (var n in prop) {
|
93
|
+
el.style[vendor(el, n)||n] = prop[n];
|
94
|
+
}
|
95
|
+
return el;
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Fills in default values.
|
100
|
+
*/
|
101
|
+
function merge(obj) {
|
102
|
+
for (var i=1; i < arguments.length; i++) {
|
103
|
+
var def = arguments[i];
|
104
|
+
for (var n in def) {
|
105
|
+
if (obj[n] === undefined) obj[n] = def[n];
|
106
|
+
}
|
107
|
+
}
|
108
|
+
return obj;
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Returns the absolute page-offset of the given element.
|
113
|
+
*/
|
114
|
+
function pos(el) {
|
115
|
+
var o = {x:el.offsetLeft, y:el.offsetTop};
|
116
|
+
while((el = el.offsetParent)) {
|
117
|
+
o.x+=el.offsetLeft;
|
118
|
+
o.y+=el.offsetTop;
|
119
|
+
}
|
120
|
+
return o;
|
121
|
+
}
|
122
|
+
|
123
|
+
/** The constructor */
|
124
|
+
var Spinner = function Spinner(o) {
|
125
|
+
if (!this.spin) return new Spinner(o);
|
126
|
+
this.opts = merge(o || {}, Spinner.defaults, defaults);
|
127
|
+
},
|
128
|
+
defaults = Spinner.defaults = {
|
129
|
+
lines: 12, // The number of lines to draw
|
130
|
+
length: 7, // The length of each line
|
131
|
+
width: 5, // The line thickness
|
132
|
+
radius: 10, // The radius of the inner circle
|
133
|
+
color: '#000', // #rgb or #rrggbb
|
134
|
+
speed: 1, // Rounds per second
|
135
|
+
trail: 100, // Afterglow percentage
|
136
|
+
opacity: 1/4,
|
137
|
+
fps: 20
|
138
|
+
},
|
139
|
+
proto = Spinner.prototype = {
|
140
|
+
spin: function(target) {
|
141
|
+
this.stop();
|
142
|
+
var self = this,
|
143
|
+
el = self.el = css(createEl(), {position: 'relative'}),
|
144
|
+
ep, // element position
|
145
|
+
tp; // target position
|
146
|
+
|
147
|
+
if (target) {
|
148
|
+
target.insertBefore(el, target.firstChild||null);
|
149
|
+
tp = pos(target);
|
150
|
+
ep = pos(el);
|
151
|
+
css(el, {
|
152
|
+
left: (target.offsetWidth >> 1) - ep.x+tp.x + 'px',
|
153
|
+
top: (target.offsetHeight >> 1) - ep.y+tp.y + 'px'
|
154
|
+
});
|
155
|
+
}
|
156
|
+
el.setAttribute('aria-role', 'progressbar');
|
157
|
+
self.lines(el, self.opts);
|
158
|
+
if (!useCssAnimations) {
|
159
|
+
// No CSS animation support, use setTimeout() instead
|
160
|
+
var o = self.opts,
|
161
|
+
i = 0,
|
162
|
+
fps = o.fps,
|
163
|
+
f = fps/o.speed,
|
164
|
+
ostep = (1-o.opacity)/(f*o.trail / 100),
|
165
|
+
astep = f/o.lines;
|
166
|
+
|
167
|
+
(function anim() {
|
168
|
+
i++;
|
169
|
+
for (var s=o.lines; s; s--) {
|
170
|
+
var alpha = Math.max(1-(i+s*astep)%f * ostep, o.opacity);
|
171
|
+
self.opacity(el, o.lines-s, alpha, o);
|
172
|
+
}
|
173
|
+
self.timeout = self.el && setTimeout(anim, ~~(1000/fps));
|
174
|
+
})();
|
175
|
+
}
|
176
|
+
return self;
|
177
|
+
},
|
178
|
+
stop: function() {
|
179
|
+
var el = this.el;
|
180
|
+
if (el) {
|
181
|
+
clearTimeout(this.timeout);
|
182
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
183
|
+
this.el = undefined;
|
184
|
+
}
|
185
|
+
return this;
|
186
|
+
}
|
187
|
+
};
|
188
|
+
proto.lines = function(el, o) {
|
189
|
+
var i = 0,
|
190
|
+
seg;
|
191
|
+
|
192
|
+
function fill(color, shadow) {
|
193
|
+
return css(createEl(), {
|
194
|
+
position: 'absolute',
|
195
|
+
width: (o.length+o.width) + 'px',
|
196
|
+
height: o.width + 'px',
|
197
|
+
background: color,
|
198
|
+
boxShadow: shadow,
|
199
|
+
transformOrigin: 'left',
|
200
|
+
transform: 'rotate(' + ~~(360/o.lines*i) + 'deg) translate(' + o.radius+'px' +',0)',
|
201
|
+
borderRadius: (o.width>>1) + 'px'
|
202
|
+
});
|
203
|
+
}
|
204
|
+
for (; i < o.lines; i++) {
|
205
|
+
seg = css(createEl(), {
|
206
|
+
position: 'absolute',
|
207
|
+
top: 1+~(o.width/2) + 'px',
|
208
|
+
transform: o.hwaccel ? 'translate3d(0,0,0)' : '',
|
209
|
+
opacity: o.opacity,
|
210
|
+
animation: useCssAnimations && addAnimation(o.opacity, o.trail, i, o.lines) + ' ' + 1/o.speed + 's linear infinite'
|
211
|
+
});
|
212
|
+
if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'}));
|
213
|
+
ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)')));
|
214
|
+
}
|
215
|
+
return el;
|
216
|
+
};
|
217
|
+
proto.opacity = function(el, i, val) {
|
218
|
+
if (i < el.childNodes.length) el.childNodes[i].style.opacity = val;
|
219
|
+
};
|
220
|
+
|
221
|
+
/////////////////////////////////////////////////////////////////////////
|
222
|
+
// VML rendering for IE
|
223
|
+
/////////////////////////////////////////////////////////////////////////
|
224
|
+
|
225
|
+
/**
|
226
|
+
* Check and init VML support
|
227
|
+
*/
|
228
|
+
(function() {
|
229
|
+
var s = css(createEl('group'), {behavior: 'url(#default#VML)'}),
|
230
|
+
i;
|
231
|
+
|
232
|
+
if (!vendor(s, 'transform') && s.adj) {
|
233
|
+
|
234
|
+
// VML support detected. Insert CSS rules ...
|
235
|
+
for (i=4; i--;) sheet.addRule(['group', 'roundrect', 'fill', 'stroke'][i], 'behavior:url(#default#VML)');
|
236
|
+
|
237
|
+
proto.lines = function(el, o) {
|
238
|
+
var r = o.length+o.width,
|
239
|
+
s = 2*r;
|
240
|
+
|
241
|
+
function grp() {
|
242
|
+
return css(createEl('group', {coordsize: s +' '+s, coordorigin: -r +' '+-r}), {width: s, height: s});
|
243
|
+
}
|
244
|
+
|
245
|
+
var g = grp(),
|
246
|
+
margin = ~(o.length+o.radius+o.width)+'px',
|
247
|
+
i;
|
248
|
+
|
249
|
+
function seg(i, dx, filter) {
|
250
|
+
ins(g,
|
251
|
+
ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}),
|
252
|
+
ins(css(createEl('roundrect', {arcsize: 1}), {
|
253
|
+
width: r,
|
254
|
+
height: o.width,
|
255
|
+
left: o.radius,
|
256
|
+
top: -o.width>>1,
|
257
|
+
filter: filter
|
258
|
+
}),
|
259
|
+
createEl('fill', {color: o.color, opacity: o.opacity}),
|
260
|
+
createEl('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change
|
261
|
+
)
|
262
|
+
)
|
263
|
+
);
|
264
|
+
}
|
265
|
+
|
266
|
+
if (o.shadow) {
|
267
|
+
for (i = 1; i <= o.lines; i++) {
|
268
|
+
seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)');
|
269
|
+
}
|
270
|
+
}
|
271
|
+
for (i = 1; i <= o.lines; i++) {
|
272
|
+
seg(i);
|
273
|
+
}
|
274
|
+
return ins(css(el, {
|
275
|
+
margin: margin + ' 0 0 ' + margin,
|
276
|
+
zoom: 1
|
277
|
+
}), g);
|
278
|
+
};
|
279
|
+
proto.opacity = function(el, i, val, o) {
|
280
|
+
var c = el.firstChild;
|
281
|
+
o = o.shadow && o.lines || 0;
|
282
|
+
if (c && i+o < c.childNodes.length) {
|
283
|
+
c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild;
|
284
|
+
if (c) c.opacity = val;
|
285
|
+
}
|
286
|
+
};
|
287
|
+
}
|
288
|
+
else {
|
289
|
+
useCssAnimations = vendor(s, 'animation');
|
290
|
+
}
|
291
|
+
})();
|
292
|
+
|
293
|
+
window.Spinner = Spinner;
|
294
|
+
|
295
|
+
})(window, document);
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: backbone_rails_extensions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Coroutine
|
8
|
+
- Tim Lowrimore
|
9
|
+
- John Dugan
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2015-07-22 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description: This gem provides a set of Backbone.js extensions commonly used in Coroutine
|
16
|
+
projects. These extensions include simple collection views, paginated collection
|
17
|
+
views, searching, and loading indicators.
|
18
|
+
email:
|
19
|
+
- gems@coroutine.com
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- ".gitignore"
|
25
|
+
- Gemfile
|
26
|
+
- LICENSE.txt
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- backbone_rails_extensions.gemspec
|
30
|
+
- lib/assets/javascripts/jquery/serialize-object.js
|
31
|
+
- lib/assets/javascripts/list/namespace.js
|
32
|
+
- lib/assets/javascripts/list/routers/base.js
|
33
|
+
- lib/assets/javascripts/list/views/collection_view.js
|
34
|
+
- lib/assets/javascripts/list/views/search.js
|
35
|
+
- lib/assets/javascripts/list/views/simple_collection_view.js
|
36
|
+
- lib/assets/javascripts/loader/namespace.js
|
37
|
+
- lib/assets/javascripts/loader/sync.js
|
38
|
+
- lib/assets/javascripts/loader/utils/counting_latch.js
|
39
|
+
- lib/assets/javascripts/loader/views/base.js
|
40
|
+
- lib/assets/javascripts/pagination/collections/paginated_collection.js
|
41
|
+
- lib/assets/javascripts/pagination/models/pagination.js
|
42
|
+
- lib/assets/javascripts/pagination/namespace.js
|
43
|
+
- lib/assets/javascripts/pagination/templates/pagination.jst.ejs
|
44
|
+
- lib/assets/javascripts/pagination/views/pagination.js
|
45
|
+
- lib/backbone_rails_extensions.rb
|
46
|
+
- lib/backbone_rails_extensions/version.rb
|
47
|
+
- vendor/assets/javascripts/jquery.spin.js
|
48
|
+
- vendor/assets/javascripts/spin.js
|
49
|
+
homepage: https://github.com/coroutine/backbone-rails-extensions
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
metadata: {}
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 2.4.8
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: This gem provides a set of Backbone.js extensions commonly used in Coroutine
|
73
|
+
projects.
|
74
|
+
test_files: []
|