laces 0.1.0

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.
Files changed (106) hide show
  1. data/CONTRIBUTING.md +38 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +122 -0
  4. data/LICENSE +21 -0
  5. data/README.md +5 -0
  6. data/Rakefile +8 -0
  7. data/bin/laces +16 -0
  8. data/features/creating_a_heroku_app.feature +9 -0
  9. data/features/rake_clean.feature +21 -0
  10. data/features/skipping_clearance.feature +13 -0
  11. data/features/step_definitions/gem_steps.rb +5 -0
  12. data/features/step_definitions/heroku_steps.rb +3 -0
  13. data/features/step_definitions/shell_steps.rb +55 -0
  14. data/features/support/bin/heroku +5 -0
  15. data/features/support/env.rb +15 -0
  16. data/features/support/fake_heroku.rb +21 -0
  17. data/laces-0.0.1.gem +0 -0
  18. data/laces.gemspec +35 -0
  19. data/lib/laces/actions.rb +35 -0
  20. data/lib/laces/app_builder.rb +237 -0
  21. data/lib/laces/generators/app_generator.rb +111 -0
  22. data/lib/laces/version.rb +3 -0
  23. data/templates/.DS_Store +0 -0
  24. data/templates/Gemfile_template +76 -0
  25. data/templates/HEROKU_README.md +66 -0
  26. data/templates/Procfile +1 -0
  27. data/templates/README.md +81 -0
  28. data/templates/app/assets/imgs/glyphicons-halflings-white.png +0 -0
  29. data/templates/app/assets/imgs/glyphicons-halflings.png +0 -0
  30. data/templates/app/assets/javascripts/admin.coffee +20 -0
  31. data/templates/app/assets/javascripts/application.coffee +21 -0
  32. data/templates/app/assets/javascripts/lib/actinology.coffee +47 -0
  33. data/templates/app/assets/javascripts/lib/analytics.js +11 -0
  34. data/templates/app/assets/javascripts/lib/auth_token_sync.js +17 -0
  35. data/templates/app/assets/javascripts/lib/backbone-ui.js +2455 -0
  36. data/templates/app/assets/javascripts/lib/backbone.coffee +27 -0
  37. data/templates/app/assets/javascripts/lib/backbone/collection.js +249 -0
  38. data/templates/app/assets/javascripts/lib/backbone/events.js +64 -0
  39. data/templates/app/assets/javascripts/lib/backbone/helpers.js +68 -0
  40. data/templates/app/assets/javascripts/lib/backbone/history.js +144 -0
  41. data/templates/app/assets/javascripts/lib/backbone/model.js +291 -0
  42. data/templates/app/assets/javascripts/lib/backbone/router.coffee +45 -0
  43. data/templates/app/assets/javascripts/lib/backbone/sync.coffee +38 -0
  44. data/templates/app/assets/javascripts/lib/backbone/view.js +150 -0
  45. data/templates/app/assets/javascripts/lib/backbone_extended.coffee +276 -0
  46. data/templates/app/assets/javascripts/lib/bootstrap.js +1836 -0
  47. data/templates/app/assets/javascripts/lib/inflection.js +658 -0
  48. data/templates/app/assets/javascripts/lib/jquery-ui.js +343 -0
  49. data/templates/app/assets/javascripts/lib/jquery.hotkeys.js +102 -0
  50. data/templates/app/assets/javascripts/lib/jquery.js +4 -0
  51. data/templates/app/assets/javascripts/lib/milk.js.coffee +265 -0
  52. data/templates/app/assets/javascripts/lib/raw.js +143 -0
  53. data/templates/app/assets/javascripts/lib/strftime.js +732 -0
  54. data/templates/app/assets/javascripts/lib/throttle-debounce.js +251 -0
  55. data/templates/app/assets/javascripts/lib/timeago.js +148 -0
  56. data/templates/app/assets/javascripts/lib/underscore.js +28 -0
  57. data/templates/app/assets/styles/application.sass +21 -0
  58. data/templates/app/assets/styles/layouts/default.sass +15 -0
  59. data/templates/app/assets/styles/layouts/footer.sass +0 -0
  60. data/templates/app/assets/styles/layouts/forms.sass +34 -0
  61. data/templates/app/assets/styles/layouts/header.sass +0 -0
  62. data/templates/app/assets/styles/layouts/navigation.sass +0 -0
  63. data/templates/app/assets/styles/lib/backbone-ui.css +580 -0
  64. data/templates/app/assets/styles/lib/bootstrap.sass +4248 -0
  65. data/templates/app/assets/styles/pages/home.sass +0 -0
  66. data/templates/app/assets/styles/sessions/new.sass +0 -0
  67. data/templates/app/assets/styles/users/activate.sass +0 -0
  68. data/templates/app/assets/styles/users/new.sass +0 -0
  69. data/templates/app/assets/styles/users/suspended.sass +0 -0
  70. data/templates/app/controllers/app_controller.rb +14 -0
  71. data/templates/app/controllers/pages_controller.rb +2 -0
  72. data/templates/app/controllers/sessions_controller.rb +2 -0
  73. data/templates/app/controllers/templating_controller.rb +31 -0
  74. data/templates/app/controllers/users_controller.rb +2 -0
  75. data/templates/app/helpers/app_helper.rb +94 -0
  76. data/templates/app/helpers/users_helper.rb +53 -0
  77. data/templates/app/models/user.rb +9 -0
  78. data/templates/app/views/devise/confirmations/new.haml +9 -0
  79. data/templates/app/views/devise/passwords/edit.haml +11 -0
  80. data/templates/app/views/devise/passwords/new.haml +9 -0
  81. data/templates/app/views/devise/registrations/edit.haml +13 -0
  82. data/templates/app/views/devise/registrations/new.haml +8 -0
  83. data/templates/app/views/devise/sessions/_form.haml +7 -0
  84. data/templates/app/views/devise/sessions/new.haml +5 -0
  85. data/templates/app/views/devise/unlocks/new.haml +7 -0
  86. data/templates/app/views/layouts/_ascii.haml +0 -0
  87. data/templates/app/views/layouts/_column.haml +0 -0
  88. data/templates/app/views/layouts/_content.haml +1 -0
  89. data/templates/app/views/layouts/_extra.haml +0 -0
  90. data/templates/app/views/layouts/_footer.haml +9 -0
  91. data/templates/app/views/layouts/_head.haml +13 -0
  92. data/templates/app/views/layouts/_header.haml +6 -0
  93. data/templates/app/views/layouts/application.html.haml +16 -0
  94. data/templates/app/views/pages/home.haml +1 -0
  95. data/templates/config/app.yml +29 -0
  96. data/templates/config/application.erb +28 -0
  97. data/templates/config/database.yml +32 -0
  98. data/templates/config/initializers/devise.rb +232 -0
  99. data/templates/config/initializers/rabl_init.rb +4 -0
  100. data/templates/config/initializers/setup_mail.rb +13 -0
  101. data/templates/config/initializers/wrap_parameters.rb +12 -0
  102. data/templates/db/migrate/user_migration.rb +36 -0
  103. data/templates/laces_gitignore +9 -0
  104. data/templates/lib/development_mail_interceptor.rb +7 -0
  105. data/templates/lib/templating.rb +40 -0
  106. metadata +225 -0
@@ -0,0 +1,27 @@
1
+ #= require_self
2
+ #= require lib/backbone/events
3
+ #= require lib/backbone/model
4
+ #= require lib/backbone/collection
5
+ #= require lib/backbone/router
6
+ #= require lib/backbone/history
7
+ #= require lib/backbone/view
8
+ #= require lib/backbone/sync
9
+ #= require lib/backbone/helpers
10
+
11
+ root = this
12
+ previousBackbone = root.Backbone
13
+ root.Backbone = {}
14
+ slice = Array::slice
15
+ Backbone = if typeof exports isnt "undefined" then exports else root.Backbone
16
+ Backbone.VERSION = "0.5.3"
17
+
18
+ _ = root._
19
+ _ = require("underscore")._ if not _ and (typeof require isnt "undefined")
20
+ $ = root.jQuery or root.Zepto or root.ender
21
+
22
+ Backbone.noConflict = ->
23
+ root.Backbone = previousBackbone
24
+ this
25
+
26
+ Backbone.emulateHTTP = false
27
+ Backbone.emulateJSON = false
@@ -0,0 +1,249 @@
1
+ // Backbone.Collection
2
+ // -------------------
3
+
4
+ // Provides a standard collection class for our sets of models, ordered
5
+ // or unordered. If a `comparator` is specified, the Collection will maintain
6
+ // its models in sort order, as they're added and removed.
7
+ Backbone.Collection = function(models, options) {
8
+ options || (options = {});
9
+ if (options.comparator) this.comparator = options.comparator;
10
+ _.bindAll(this, '_onModelEvent', '_removeReference');
11
+ this._reset();
12
+ if (models) this.reset(models, {silent: true});
13
+ this.initialize.apply(this, arguments);
14
+ };
15
+
16
+ // Define the Collection's inheritable methods.
17
+ _.extend(Backbone.Collection.prototype, Backbone.Events, {
18
+
19
+ // The default model for a collection is just a **Backbone.Model**.
20
+ // This should be overridden in most cases.
21
+ model : Backbone.Model,
22
+
23
+ // Initialize is an empty function by default. Override it with your own
24
+ // initialization logic.
25
+ initialize : function(){},
26
+
27
+ // The JSON representation of a Collection is an array of the
28
+ // models' attributes.
29
+ toJSON : function() {
30
+ return this.map(function(model){ return model.toJSON(); });
31
+ },
32
+
33
+ // Add a model, or list of models to the set. Pass **silent** to avoid
34
+ // firing the `added` event for every new model.
35
+ add : function(models, options) {
36
+ if (_.isArray(models)) {
37
+ for (var i = 0, l = models.length; i < l; i++) {
38
+ this._add(models[i], options);
39
+ }
40
+ } else {
41
+ this._add(models, options);
42
+ }
43
+ return this;
44
+ },
45
+
46
+ // Remove a model, or a list of models from the set. Pass silent to avoid
47
+ // firing the `removed` event for every model removed.
48
+ remove : function(models, options) {
49
+ if (_.isArray(models)) {
50
+ for (var i = 0, l = models.length; i < l; i++) {
51
+ this._remove(models[i], options);
52
+ }
53
+ } else {
54
+ this._remove(models, options);
55
+ }
56
+ return this;
57
+ },
58
+
59
+ // Get a model from the set by id.
60
+ get : function(id) {
61
+ if (id == null) return null;
62
+ return this._byId[id.id != null ? id.id : id];
63
+ },
64
+
65
+ // Get a model from the set by client id.
66
+ getByCid : function(cid) {
67
+ return cid && this._byCid[cid.cid || cid];
68
+ },
69
+
70
+ // Get the model at the given index.
71
+ at: function(index) {
72
+ return this.models[index];
73
+ },
74
+
75
+ // Force the collection to re-sort itself. You don't need to call this under normal
76
+ // circumstances, as the set will maintain sort order as each item is added.
77
+ sort : function(options) {
78
+ options || (options = {});
79
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
80
+ this.models = this.sortBy(this.comparator);
81
+ if (!options.silent) this.trigger('reset', this, options);
82
+ return this;
83
+ },
84
+
85
+ // Pluck an attribute from each model in the collection.
86
+ pluck : function(attr) {
87
+ return _.map(this.models, function(model){ return model.get(attr); });
88
+ },
89
+
90
+ // When you have more items than you want to add or remove individually,
91
+ // you can reset the entire set with a new list of models, without firing
92
+ // any `added` or `removed` events. Fires `reset` when finished.
93
+ reset : function(models, options) {
94
+ models || (models = []);
95
+ options || (options = {});
96
+ this.each(this._removeReference);
97
+ this._reset();
98
+ this.add(models, {silent: true});
99
+ if (!options.silent) this.trigger('reset', this, options);
100
+ return this;
101
+ },
102
+
103
+ // Fetch the default set of models for this collection, resetting the
104
+ // collection when they arrive. If `add: true` is passed, appends the
105
+ // models to the collection instead of resetting.
106
+ fetch : function(options) {
107
+ options || (options = {});
108
+ var collection = this;
109
+ var success = options.success;
110
+ var parse = options.parse;
111
+ options.success = function(resp, status, xhr) {
112
+ if (parse)
113
+ parse_results = parse.call(collection, resp, xhr);
114
+ else
115
+ parse_results = collection.parse(resp, xhr);
116
+ collection[options.add ? 'add' : 'reset'](parse_results, options);
117
+ if (success) success(collection, resp);
118
+ };
119
+ options.error = wrapError(options.error, collection, options);
120
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);
121
+ },
122
+
123
+ // Create a new instance of a model in this collection. After the model
124
+ // has been created on the server, it will be added to the collection.
125
+ // Returns the model, or 'false' if validation on a new model fails.
126
+ create : function(model, options) {
127
+ var coll = this;
128
+ options || (options = {});
129
+ model = this._prepareModel(model, options);
130
+ if (!model) return false;
131
+ var success = options.success;
132
+ options.success = function(nextModel, resp, xhr) {
133
+ coll.add(nextModel, options);
134
+ if (success) success(nextModel, resp, xhr);
135
+ };
136
+ model.save(null, options);
137
+ return model;
138
+ },
139
+
140
+ // **parse** converts a response into a list of models to be added to the
141
+ // collection. The default implementation is just to pass it through.
142
+ parse : function(resp, xhr) {
143
+ return resp;
144
+ },
145
+
146
+ // Proxy to _'s chain. Can't be proxied the same way the rest of the
147
+ // underscore methods are proxied because it relies on the underscore
148
+ // constructor.
149
+ chain: function () {
150
+ return _(this.models).chain();
151
+ },
152
+
153
+ // Reset all internal state. Called when the collection is reset.
154
+ _reset : function(options) {
155
+ this.length = 0;
156
+ this.models = [];
157
+ this._byId = {};
158
+ this._byCid = {};
159
+ },
160
+
161
+ // Prepare a model to be added to this collection
162
+ _prepareModel: function(model, options) {
163
+ if (!(model instanceof Backbone.Model)) {
164
+ var attrs = model;
165
+ model = new this.model(attrs, {collection: this});
166
+ if (model.validate && !model._performValidation(model.attributes, options)) model = false;
167
+ } else if (!model.collection) {
168
+ model.collection = this;
169
+ }
170
+ return model;
171
+ },
172
+
173
+ // Internal implementation of adding a single model to the set, updating
174
+ // hash indexes for `id` and `cid` lookups.
175
+ // Returns the model, or 'false' if validation on a new model fails.
176
+ _add : function(model, options) {
177
+ options || (options = {});
178
+ model = this._prepareModel(model, options);
179
+ if (!model) return false;
180
+ var already = this.getByCid(model);
181
+ if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
182
+ this._byId[model.id] = model;
183
+ this._byCid[model.cid] = model;
184
+ var index = options.at != null ? options.at :
185
+ this.comparator ? this.sortedIndex(model, this.comparator) :
186
+ this.length;
187
+ this.models.splice(index, 0, model);
188
+ model.bind('all', this._onModelEvent);
189
+ this.length++;
190
+ options.index = index;
191
+ if (!options.silent) model.trigger('add', model, this, options);
192
+ return model;
193
+ },
194
+
195
+ // Internal implementation of removing a single model from the set, updating
196
+ // hash indexes for `id` and `cid` lookups.
197
+ _remove : function(model, options) {
198
+ options || (options = {});
199
+ model = this.getByCid(model) || this.get(model);
200
+ if (!model) return null;
201
+ delete this._byId[model.id];
202
+ delete this._byCid[model.cid];
203
+ var index = this.indexOf(model);
204
+ this.models.splice(index, 1);
205
+ this.length--;
206
+ options.index = index;
207
+ if (!options.silent) model.trigger('remove', model, this, options);
208
+ this._removeReference(model);
209
+ return model;
210
+ },
211
+
212
+ // Internal method to remove a model's ties to a collection.
213
+ _removeReference : function(model) {
214
+ if (this == model.collection) {
215
+ delete model.collection;
216
+ }
217
+ model.unbind('all', this._onModelEvent);
218
+ },
219
+
220
+ // Internal method called every time a model in the set fires an event.
221
+ // Sets need to update their indexes when models change ids. All other
222
+ // events simply proxy through. "add" and "remove" events that originate
223
+ // in other collections are ignored.
224
+ _onModelEvent : function(ev, model, collection, options) {
225
+ if ((ev == 'add' || ev == 'remove') && collection != this) return;
226
+ if (ev == 'destroy') {
227
+ this._remove(model, options);
228
+ }
229
+ if (model && ev === 'change:' + model.idAttribute) {
230
+ delete this._byId[model.previous(model.idAttribute)];
231
+ this._byId[model.id] = model;
232
+ }
233
+ this.trigger.apply(this, arguments);
234
+ }
235
+
236
+ });
237
+
238
+ // Underscore methods that we want to implement on the Collection.
239
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
240
+ 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
241
+ 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
242
+ 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
243
+
244
+ // Mix in each Underscore method as a proxy to `Collection#models`.
245
+ _.each(methods, function(method) {
246
+ Backbone.Collection.prototype[method] = function() {
247
+ return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
248
+ };
249
+ });
@@ -0,0 +1,64 @@
1
+ // Backbone.Events
2
+ // -----------------
3
+
4
+ // A module that can be mixed in to *any object* in order to provide it with
5
+ // custom events. You may `bind` or `unbind` a callback function to an event;
6
+ // `trigger`-ing an event fires all callbacks in succession.
7
+ //
8
+ // var object = {};
9
+ // _.extend(object, Backbone.Events);
10
+ // object.bind('expand', function(){ alert('expanded'); });
11
+ // object.trigger('expand');
12
+ //
13
+ Backbone.Events = {
14
+
15
+ // Bind an event, specified by a string name, `ev`, to a `callback` function.
16
+ // Passing `"all"` will bind the callback to all events fired.
17
+ bind : function(ev, callback, context) {
18
+ var calls = this._callbacks || (this._callbacks = {});
19
+ var list = calls[ev] || (calls[ev] = {});
20
+ var tail = list.tail || (list.tail = list.next = {});
21
+ tail.callback = callback;
22
+ tail.context = context;
23
+ list.tail = tail.next = {};
24
+ return this;
25
+ },
26
+
27
+ // Remove one or many callbacks. If `callback` is null, removes all
28
+ // callbacks for the event. If `ev` is null, removes all bound callbacks
29
+ // for all events.
30
+ unbind : function(ev, callback) {
31
+ var calls, node, prev;
32
+ if (!ev) {
33
+ this._callbacks = null;
34
+ } else if (calls = this._callbacks) {
35
+ if (!callback) {
36
+ calls[ev] = {};
37
+ } else if (node = calls[ev]) {
38
+ while ((prev = node) && (node = node.next)) {
39
+ if (node.callback !== callback) continue;
40
+ prev.next = node.next;
41
+ node.context = node.callback = null;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ return this;
47
+ },
48
+
49
+ // Trigger an event, firing all bound callbacks. Callbacks are passed the
50
+ // same arguments as `trigger` is, apart from the event name.
51
+ // Listening for `"all"` passes the true event name as the first argument.
52
+ trigger : function(eventName) {
53
+ var node, calls, callback, args, ev, events = ['all', eventName];
54
+ if (!(calls = this._callbacks)) return this;
55
+ while (ev = events.pop()) {
56
+ if (!(node = calls[ev])) continue;
57
+ args = ev == 'all' ? arguments : Array.prototype.slice.call(arguments, 1);
58
+ while (node = node.next) if (callback = node.callback) callback.apply(node.context || this, args);
59
+ }
60
+ return this;
61
+ }
62
+
63
+ };
64
+
@@ -0,0 +1,68 @@
1
+ // Helpers
2
+ // -------
3
+
4
+ // Shared empty constructor function to aid in prototype-chain creation.
5
+ var ctor = function(){};
6
+
7
+ // Helper function to correctly set up the prototype chain, for subclasses.
8
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
9
+ // class properties to be extended.
10
+ var inherits = function(parent, protoProps, staticProps) {
11
+ var child;
12
+
13
+ // The constructor function for the new subclass is either defined by you
14
+ // (the "constructor" property in your `extend` definition), or defaulted
15
+ // by us to simply call `super()`.
16
+ if (protoProps && protoProps.hasOwnProperty('constructor')) {
17
+ child = protoProps.constructor;
18
+ } else {
19
+ child = function(){ return parent.apply(this, arguments); };
20
+ }
21
+
22
+ // Inherit class (static) properties from parent.
23
+ _.extend(child, parent);
24
+
25
+ // Set the prototype chain to inherit from `parent`, without calling
26
+ // `parent`'s constructor function.
27
+ ctor.prototype = parent.prototype;
28
+ child.prototype = new ctor();
29
+
30
+ // Add prototype properties (instance properties) to the subclass,
31
+ // if supplied.
32
+ if (protoProps) _.extend(child.prototype, protoProps);
33
+
34
+ // Add static properties to the constructor function, if supplied.
35
+ if (staticProps) _.extend(child, staticProps);
36
+
37
+ // Correctly set child's `prototype.constructor`.
38
+ child.prototype.constructor = child;
39
+
40
+ // Set a convenience property in case the parent's prototype is needed later.
41
+ child.__super__ = parent.prototype;
42
+
43
+ return child;
44
+ };
45
+
46
+ // Helper function to get a URL from a Model or Collection as a property
47
+ // or as a function.
48
+ var getUrl = function(object) {
49
+ if (!(object && object.url)) return null;
50
+ return _.isFunction(object.url) ? object.url() : object.url;
51
+ };
52
+
53
+ // Throw an error when a URL is needed, and none is supplied.
54
+ var urlError = function() {
55
+ throw new Error('A "url" property or function must be specified');
56
+ };
57
+
58
+ // Wrap an optional error callback with a fallback error event.
59
+ var wrapError = function(onError, originalModel, options) {
60
+ return function(model, resp) {
61
+ var resp = model === originalModel ? resp : model;
62
+ if (onError) {
63
+ onError(model, resp, options);
64
+ } else {
65
+ model.trigger('error', model, resp, options);
66
+ }
67
+ };
68
+ };
@@ -0,0 +1,144 @@
1
+ // Backbone.History
2
+ // ----------------
3
+
4
+ // Handles cross-browser history management, based on URL fragments. If the
5
+ // browser does not support `onhashchange`, falls back to polling.
6
+ Backbone.History = function() {
7
+ this.handlers = [];
8
+ _.bindAll(this, 'checkUrl');
9
+ };
10
+
11
+ // Cached regex for cleaning hashes.
12
+ var hashStrip = /^#*/;
13
+
14
+ // Cached regex for detecting MSIE.
15
+ var isExplorer = /msie [\w.]+/;
16
+
17
+ // Has the history handling already been started?
18
+ var historyStarted = false;
19
+
20
+ // Set up all inheritable **Backbone.History** properties and methods.
21
+ _.extend(Backbone.History.prototype, {
22
+
23
+ // The default interval to poll for hash changes, if necessary, is
24
+ // twenty times a second.
25
+ interval: 50,
26
+
27
+ // Get the cross-browser normalized URL fragment, either from the URL,
28
+ // the hash, or the override.
29
+ getFragment : function(fragment, forcePushState) {
30
+ if (fragment == null) {
31
+ if (this._hasPushState || forcePushState) {
32
+ fragment = window.location.pathname;
33
+ var search = window.location.search;
34
+ if (search) fragment += search;
35
+ } else {
36
+ fragment = window.location.hash;
37
+ }
38
+ }
39
+ fragment = decodeURIComponent(fragment.replace(hashStrip, ''));
40
+ if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
41
+ return fragment;
42
+ },
43
+
44
+ // Start the hash change handling, returning `true` if the current URL matches
45
+ // an existing route, and `false` otherwise.
46
+ start : function(options) {
47
+
48
+ // Figure out the initial configuration. Do we need an iframe?
49
+ // Is pushState desired ... is it available?
50
+ if (historyStarted) throw new Error("Backbone.history has already been started");
51
+ this.options = _.extend({}, {root: '/'}, this.options, options);
52
+ this._wantsPushState = !!this.options.pushState;
53
+ this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
54
+ var fragment = this.getFragment();
55
+ var docMode = document.documentMode;
56
+ var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
57
+ if (oldIE) {
58
+ this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
59
+ this.navigate(fragment);
60
+ }
61
+
62
+ // Depending on whether we're using pushState or hashes, and whether
63
+ // 'onhashchange' is supported, determine how we check the URL state.
64
+ if (this._hasPushState) {
65
+ $(window).bind('popstate', this.checkUrl);
66
+ } else if ('onhashchange' in window && !oldIE) {
67
+ $(window).bind('hashchange', this.checkUrl);
68
+ } else {
69
+ setInterval(this.checkUrl, this.interval);
70
+ }
71
+
72
+ // Determine if we need to change the base url, for a pushState link
73
+ // opened by a non-pushState browser.
74
+ this.fragment = fragment;
75
+ historyStarted = true;
76
+ var loc = window.location;
77
+ var atRoot = loc.pathname == this.options.root;
78
+ if (this._wantsPushState && !this._hasPushState && !atRoot) {
79
+ this.fragment = this.getFragment(null, true);
80
+ window.location.replace(this.options.root + '#' + this.fragment);
81
+ // Return immediately as browser will do redirect to new url
82
+ return true;
83
+ } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
84
+ this.fragment = loc.hash.replace(hashStrip, '');
85
+ window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
86
+ }
87
+
88
+ if (!this.options.silent) {
89
+ return this.loadUrl();
90
+ }
91
+ },
92
+
93
+ // Add a route to be tested when the fragment changes. Routes added later may
94
+ // override previous routes.
95
+ route : function(route, callback) {
96
+ this.handlers.unshift({route : route, callback : callback});
97
+ },
98
+
99
+ // Checks the current URL to see if it has changed, and if it has,
100
+ // calls `loadUrl`, normalizing across the hidden iframe.
101
+ checkUrl : function(e) {
102
+ var current = this.getFragment();
103
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
104
+ if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
105
+ if (this.iframe) this.navigate(current);
106
+ this.loadUrl() || this.loadUrl(window.location.hash);
107
+ },
108
+
109
+ // Attempt to load the current URL fragment. If a route succeeds with a
110
+ // match, returns `true`. If no defined routes matches the fragment,
111
+ // returns `false`.
112
+ loadUrl : function(fragmentOverride) {
113
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
114
+ var matched = _.any(this.handlers, function(handler) {
115
+ if (handler.route.test(fragment)) {
116
+ handler.callback(fragment);
117
+ return true;
118
+ }
119
+ });
120
+ return matched;
121
+ },
122
+
123
+ // Save a fragment into the hash history. You are responsible for properly
124
+ // URL-encoding the fragment in advance. This does not trigger
125
+ // a `hashchange` event.
126
+ navigate : function(fragment, triggerRoute) {
127
+ var frag = (fragment || '').replace(hashStrip, '');
128
+ if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
129
+ if (this._hasPushState) {
130
+ var loc = window.location;
131
+ if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
132
+ this.fragment = frag;
133
+ window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
134
+ } else {
135
+ window.location.hash = this.fragment = frag;
136
+ if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
137
+ this.iframe.document.open().close();
138
+ this.iframe.location.hash = frag;
139
+ }
140
+ }
141
+ if (triggerRoute) this.loadUrl(fragment);
142
+ }
143
+
144
+ });