├── Rakefile ├── lib ├── assets │ └── javascripts │ │ ├── list │ │ ├── namespace.js │ │ ├── views │ │ │ ├── collection_view.js │ │ │ ├── search.js │ │ │ └── simple_collection_view.js │ │ └── routers │ │ │ └── base.js │ │ ├── loader │ │ ├── namespace.js │ │ ├── utils │ │ │ └── counting_latch.js │ │ ├── sync.js │ │ └── views │ │ │ └── base.js │ │ ├── pagination │ │ ├── namespace.js │ │ ├── templates │ │ │ └── pagination.jst.ejs │ │ ├── collections │ │ │ └── paginated_collection.js │ │ ├── models │ │ │ └── pagination.js │ │ └── views │ │ │ └── pagination.js │ │ └── jquery │ │ └── serialize-object.js ├── backbone_rails_extensions │ └── version.rb └── backbone_rails_extensions.rb ├── Gemfile ├── .gitignore ├── LICENSE.txt ├── backbone_rails_extensions.gemspec ├── README.md └── vendor └── assets └── javascripts ├── jquery.spin.js └── spin.js /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/list/namespace.js: -------------------------------------------------------------------------------- 1 | var List = { 2 | Router: {}, 3 | View: {} 4 | } -------------------------------------------------------------------------------- /lib/assets/javascripts/loader/namespace.js: -------------------------------------------------------------------------------- 1 | var Loader = { 2 | View: {}, 3 | Util: {} 4 | }; -------------------------------------------------------------------------------- /lib/backbone_rails_extensions/version.rb: -------------------------------------------------------------------------------- 1 | module BackboneRailsExtensions 2 | VERSION = '1.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in backbone-rails-extensions.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/assets/javascripts/pagination/namespace.js: -------------------------------------------------------------------------------- 1 | var Pagination = { 2 | View: {}, 3 | Model: {}, 4 | Collection: {} 5 | }; -------------------------------------------------------------------------------- /lib/backbone_rails_extensions.rb: -------------------------------------------------------------------------------- 1 | require "backbone_rails_extensions/version" 2 | 3 | 4 | module BackboneRailsExtensions 5 | class Engine < ::Rails::Engine; end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /lib/assets/javascripts/pagination/templates/pagination.jst.ejs: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /lib/assets/javascripts/loader/utils/counting_latch.js: -------------------------------------------------------------------------------- 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 | }); -------------------------------------------------------------------------------- /lib/assets/javascripts/pagination/collections/paginated_collection.js: -------------------------------------------------------------------------------- 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 | }); -------------------------------------------------------------------------------- /lib/assets/javascripts/list/views/collection_view.js: -------------------------------------------------------------------------------- 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 | }); 28 | -------------------------------------------------------------------------------- /lib/assets/javascripts/list/views/search.js: -------------------------------------------------------------------------------- 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 | }); 31 | -------------------------------------------------------------------------------- /lib/assets/javascripts/loader/sync.js: -------------------------------------------------------------------------------- 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 | if (success) { success.apply(this, arguments) }; 11 | Loader.View.Base.hide(); 12 | }; 13 | 14 | options.error = function() { 15 | if (error) { 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 | }) 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /lib/assets/javascripts/jquery/serialize-object.js: -------------------------------------------------------------------------------- 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); -------------------------------------------------------------------------------- /backbone_rails_extensions.gemspec: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /lib/assets/javascripts/loader/views/base.js: -------------------------------------------------------------------------------- 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 | }; -------------------------------------------------------------------------------- /lib/assets/javascripts/list/routers/base.js: -------------------------------------------------------------------------------- 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 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 42 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.spin.js: -------------------------------------------------------------------------------- 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); -------------------------------------------------------------------------------- /lib/assets/javascripts/pagination/models/pagination.js: -------------------------------------------------------------------------------- 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 | }); -------------------------------------------------------------------------------- /lib/assets/javascripts/list/views/simple_collection_view.js: -------------------------------------------------------------------------------- 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 | }); -------------------------------------------------------------------------------- /lib/assets/javascripts/pagination/views/pagination.js: -------------------------------------------------------------------------------- 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 $('', 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 | }); -------------------------------------------------------------------------------- /vendor/assets/javascripts/spin.js: -------------------------------------------------------------------------------- 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> 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); --------------------------------------------------------------------------------