src/backbone_modules/history.js   D
last analyzed

Complexity

Total Complexity 65
Complexity/F 3.61

Size

Lines of Code 353
Function Count 18

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 0
c 2
b 0
f 0
nc 150994944
dl 0
loc 353
rs 4.5
wmc 65
mnd 4
bc 51
fnc 18
bpm 2.8333
cpm 3.6111
noi 10

15 Functions

Rating   Name   Duplication   Size   Complexity  
A _.extend.atRoot 0 4 1
F _.extend.start 0 85 16
A _.extend.getSearch 0 5 2
A _.extend.getHash 0 4 2
B _.extend.checkUrl 0 17 5
A _.extend.getPath 0 6 2
C _.extend.navigate 0 64 14
A _.extend.decodeFragment 0 3 1
A _.extend.route 0 6 1
A _.extend._updateHash 0 9 2
A history.js ➔ History 0 10 2
A _.extend.matchRoot 0 5 1
A _.extend.loadUrl 0 11 2
A _.extend.getFragment 0 10 4
B _.extend.stop 0 27 6

How to fix   Complexity   

Complexity

Complex classes like src/backbone_modules/history.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import $ from 'jquery';
0 ignored issues
show
introduced by
Definition for rule 'keyword-spacing' was not found
Loading history...
2
import _ from 'underscore';
3
import {
4
  Backbone
5
} from './core.js';
6
import {
7
  Events
8
} from './events.js';
9
10
// Backbone.History
11
// ----------------
12
13
// Handles cross-browser history management, based on either
14
// [pushState](http://diveintohtml5.info/history.html) and real URLs, or
15
// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
16
// and URL fragments. If the browser supports neither (old IE, natch),
17
// falls back to polling.
18
var History = function () {
19
  this.handlers = [];
20
  this.checkUrl = _.bind(this.checkUrl, this);
21
22
  // Ensure that `History` can be used outside of the browser.
23
  if (typeof window !== 'undefined') {
24
    this.location = window.location;
25
    this.history = window.history;
26
  }
27
};
28
29
// Cached regex for stripping a leading hash/slash and trailing space.
30
var routeStripper = /^[#\/]|\s+$/g;
31
32
// Cached regex for stripping leading and trailing slashes.
33
var rootStripper = /^\/+|\/+$/g;
34
35
// Cached regex for stripping urls of hash.
36
var pathStripper = /#.*$/;
37
38
// Has the history handling already been started?
39
History.started = false;
40
41
// Set up all inheritable **Backbone.History** properties and methods.
42
_.extend(History.prototype, Events, {
43
44
  // The default interval to poll for hash changes, if necessary, is
45
  // twenty times a second.
46
  interval: 50,
47
48
  // Are we at the app root?
49
  atRoot: function () {
50
    var path = this.location.pathname.replace(/[^\/]$/, '$&/');
51
    return path === this.root && !this.getSearch();
52
  },
53
54
  // Does the pathname match the root?
55
  matchRoot: function () {
56
    var path = this.decodeFragment(this.location.pathname);
57
    var rootPath = path.slice(0, this.root.length - 1) + '/';
58
    return rootPath === this.root;
59
  },
60
61
  // Unicode characters in `location.pathname` are percent encoded so they're
62
  // decoded for comparison. `%25` should not be decoded since it may be part
63
  // of an encoded parameter.
64
  decodeFragment: function (fragment) {
65
    return decodeURI(fragment.replace(/%25/g, '%2525'));
66
  },
67
68
  // In IE6, the hash fragment and search params are incorrect if the
69
  // fragment contains `?`.
70
  getSearch: function () {
71
    var match = this.location.href.replace(/#.*/, '').match(
72
      /\?.+/);
73
    return match ? match[0] : '';
74
  },
75
76
  // Gets the true hash value. Cannot use location.hash directly due to bug
77
  // in Firefox where location.hash will always be decoded.
78
  getHash: function (window) {
79
    var match = (window || this).location.href.match(/#(.*)$/);
80
    return match ? match[1] : '';
81
  },
82
83
  // Get the pathname and search params, without the root.
84
  getPath: function () {
85
    var path = this.decodeFragment(
86
      this.location.pathname + this.getSearch()
87
    ).slice(this.root.length - 1);
88
    return path.charAt(0) === '/' ? path.slice(1) : path;
89
  },
90
91
  // Get the cross-browser normalized URL fragment from the path or hash.
92
  getFragment: function (fragment) {
93
    if (fragment == null) {
94
      if (this._usePushState || !this._wantsHashChange) {
95
        fragment = this.getPath();
96
      } else {
97
        fragment = this.getHash();
98
      }
99
    }
100
    return fragment.replace(routeStripper, '');
101
  },
102
103
  // Start the hash change handling, returning `true` if the current URL matches
104
  // an existing route, and `false` otherwise.
105
  start: function (options) {
106
    if (History.started) {
107
      throw new Error('Backbone.history has already been started');
108
    }
109
    History.started = true;
110
111
    // Figure out the initial configuration. Do we need an iframe?
112
    // Is pushState desired ... is it available?
113
    this.options = _.extend({
114
      root: '/'
115
    }, this.options, options);
116
    this.root = this.options.root;
117
    this._wantsHashChange = this.options.hashChange !== false;
118
    this._hasHashChange = 'onhashchange' in window && (document.documentMode ===
119
      void 0 || document.documentMode > 7);
120
    this._useHashChange = this._wantsHashChange && this._hasHashChange;
121
    this._wantsPushState = !!this.options.pushState;
122
    this._hasPushState = !!(this.history && this.history.pushState);
123
    this._usePushState = this._wantsPushState && this._hasPushState;
124
    this.fragment = this.getFragment();
125
126
    // Normalize root to always include a leading and trailing slash.
127
    this.root = ('/' + this.root + '/').replace(rootStripper,
128
      '/');
129
130
    // Transition from hashChange to pushState or vice versa if both are
131
    // requested.
132
    if (this._wantsHashChange && this._wantsPushState) {
133
134
      // If we've started off with a route from a `pushState`-enabled
135
      // browser, but we're currently in a browser that doesn't support it...
136
      if (!this._hasPushState && !this.atRoot()) {
137
        var rootPath = this.root.slice(0, -1) || '/';
138
        this.location.replace(rootPath + '#' + this.getPath());
139
        // Return immediately as browser will do redirect to new url
140
        return true;
141
142
        // Or if we've started out with a hash-based route, but we're currently
143
        // in a browser where it could be `pushState`-based instead...
144
      } else if (this._hasPushState && this.atRoot()) {
145
        this.navigate(this.getHash(), {
146
          replace: true
147
        });
148
      }
149
150
    }
151
152
    // Proxy an iframe to handle location events if the browser doesn't
153
    // support the `hashchange` event, HTML5 history, or the user wants
154
    // `hashChange` but not `pushState`.
155
    if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
156
      this.iframe = document.createElement('iframe');
157
      this.iframe.src = 'javascript:0';
0 ignored issues
show
introduced by
Script URL is a form of eval.
Loading history...
158
      this.iframe.style.display = 'none';
159
      this.iframe.tabIndex = -1;
160
      var body = document.body;
161
      // Using `appendChild` will throw on IE < 9 if the document is not ready.
162
      var iWindow = body.insertBefore(this.iframe, body.firstChild)
163
        .contentWindow;
164
      iWindow.document.open();
165
      iWindow.document.close();
166
      iWindow.location.hash = '#' + this.fragment;
167
    }
168
169
    // Add a cross-platform `addEventListener` shim for older browsers.
170
    var addEventListener = window.addEventListener || function (
171
      eventName,
172
      listener) {
173
      return attachEvent('on' + eventName, listener);
174
    };
175
176
    // Depending on whether we're using pushState or hashes, and whether
177
    // 'onhashchange' is supported, determine how we check the URL state.
178
    if (this._usePushState) {
179
      addEventListener('popstate', this.checkUrl, false);
180
    } else if (this._useHashChange && !this.iframe) {
181
      addEventListener('hashchange', this.checkUrl, false);
182
    } else if (this._wantsHashChange) {
183
      this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
184
    }
185
186
    if (!this.options.silent) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if !this.options.silent is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
187
      return this.loadUrl();
188
    }
189
  },
190
191
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
192
  // but possibly useful for unit testing Routers.
193
  stop: function () {
194
    // Add a cross-platform `removeEventListener` shim for older browsers.
195
    var removeEventListener = window.removeEventListener ||
196
      function (
197
        eventName, listener) {
198
        return detachEvent('on' + eventName, listener);
199
      };
200
201
    // Remove window listeners.
202
    if (this._usePushState) {
203
      removeEventListener('popstate', this.checkUrl, false);
204
    } else if (this._useHashChange && !this.iframe) {
205
      removeEventListener('hashchange', this.checkUrl, false);
206
    }
207
208
    // Clean up the iframe if necessary.
209
    if (this.iframe) {
210
      document.body.removeChild(this.iframe);
211
      this.iframe = null;
212
    }
213
214
    // Some environments will throw when clearing an undefined interval.
215
    if (this._checkUrlInterval) {
216
      clearInterval(this._checkUrlInterval);
217
    }
218
    History.started = false;
219
  },
220
221
  // Add a route to be tested when the fragment changes. Routes added later
222
  // may override previous routes.
223
  route: function (route, callback) {
224
    this.handlers.unshift({
225
      route: route,
226
      callback: callback
227
    });
228
  },
229
230
  // Checks the current URL to see if it has changed, and if it has,
231
  // calls `loadUrl`, normalizing across the hidden iframe.
232
  checkUrl: function (e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
233
    var current = this.getFragment();
234
235
    // If the user pressed the back button, the iframe's hash will have
236
    // changed and we should use that for comparison.
237
    if (current === this.fragment && this.iframe) {
238
      current = this.getHash(this.iframe.contentWindow);
239
    }
240
241
    if (current === this.fragment) {
242
      return false;
243
    }
244
    if (this.iframe) {
245
      this.navigate(current);
246
    }
247
    this.loadUrl();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
248
  },
249
250
  // Attempt to load the current URL fragment. If a route succeeds with a
251
  // match, returns `true`. If no defined routes matches the fragment,
252
  // returns `false`.
253
  loadUrl: function (fragment) {
254
    // If the root doesn't match, no routes can match either.
255
    if (!this.matchRoot()) return false;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
introduced by
Expected { after 'if' condition.
Loading history...
256
    fragment = this.fragment = this.getFragment(fragment);
257
    return _.some(this.handlers, function (handler) {
258
      if (handler.route.test(fragment)) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if handler.route.test(fragment) is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
259
        handler.callback(fragment);
260
        return true;
261
      }
262
    });
263
  },
264
265
  // Save a fragment into the hash history, or replace the URL state if the
266
  // 'replace' option is passed. You are responsible for properly URL-encoding
267
  // the fragment in advance.
268
  //
269
  // The options object can contain `trigger: true` if you wish to have the
270
  // route callback be fired (not usually desirable), or `replace: true`, if
271
  // you wish to modify the current URL without adding an entry to the history.
272
  navigate: function (fragment, options) {
273
    if (!History.started) {
274
      return false;
275
    }
276
    if (!options || options === true) {
277
      options = {
278
        trigger: !!options
279
      };
280
    }
281
282
    // Normalize the fragment.
283
    fragment = this.getFragment(fragment || '');
284
285
    // Don't include a trailing slash on the root.
286
    var rootPath = this.root;
287
    if (fragment === '' || fragment.charAt(0) === '?') {
288
      rootPath = rootPath.slice(0, -1) || '/';
289
    }
290
    var url = rootPath + fragment;
291
292
    // Strip the fragment of the query and hash for matching.
293
    fragment = fragment.replace(pathStripper, '');
294
295
    // Decode for matching.
296
    var decodedFragment = this.decodeFragment(fragment);
297
298
    if (this.fragment === decodedFragment) {
299
      return;
300
    }
301
    this.fragment = decodedFragment;
302
303
    // If pushState is available, we use it to set the fragment as a real URL.
304
    if (this._usePushState) {
305
      this.history[options.replace ? 'replaceState' : 'pushState']
0 ignored issues
show
introduced by
Unexpected space between function name and paren.
Loading history...
306
        ({},
307
          document.title, url);
308
309
      // If hash changes haven't been explicitly disabled, update the hash
310
      // fragment to store history.
311
    } else if (this._wantsHashChange) {
312
      this._updateHash(this.location, fragment, options.replace);
313
      if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
314
        var iWindow = this.iframe.contentWindow;
315
316
        // Opening and closing the iframe tricks IE7 and earlier to push a
317
        // history entry on hash-tag change.  When replace is true, we don't
318
        // want this.
319
        if (!options.replace) {
320
          iWindow.document.open();
321
          iWindow.document.close();
322
        }
323
324
        this._updateHash(iWindow.location, fragment, options.replace);
325
      }
326
327
      // If you've told us that you explicitly don't want fallback hashchange-
328
      // based history, then `navigate` becomes a page refresh.
329
    } else {
330
      return this.location.assign(url);
331
    }
332
    if (options.trigger) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if options.trigger is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
333
      return this.loadUrl(fragment);
334
    }
335
  },
336
337
  // Update the hash location, either replacing the current entry, or adding
338
  // a new one to the browser history.
339
  _updateHash: function (location, fragment, replace) {
340
    if (replace) {
341
      var href = location.href.replace(/(javascript:|#).*$/, '');
342
      location.replace(href + '#' + fragment);
343
    } else {
344
      // Some browsers require that `hash` contains a leading #.
345
      location.hash = '#' + fragment;
346
    }
347
  }
348
349
});
350
351
export {
352
  History
353
};
354