b136923e9aff5dce1e9cec16f2ce811e18f9e829
[lhc/web/wiklou.git] / resources / lib / oojs-router / oojs-router.js
1 /*!
2 * OOjs Router v0.1.0
3 * https://www.mediawiki.org/wiki/OOjs
4 *
5 * Copyright 2011-2016 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs-router.mit-license.org
8 *
9 * Date: 2016-05-05T19:27:58Z
10 */
11 ( function ( $ ) {
12
13 'use strict';
14
15 /**
16 * Does hash match entry.path? If it does apply the
17 * callback for the Entry object.
18 *
19 * @method
20 * @private
21 * @ignore
22 * @param {string} hash string to match
23 * @param {Object} entry Entry object
24 * @return {boolean} Whether hash matches entry.path
25 */
26 function matchRoute( hash, entry ) {
27 var match = hash.match( entry.path );
28 if ( match ) {
29 entry.callback.apply( this, match.slice( 1 ) );
30 return true;
31 }
32 return false;
33 }
34
35 /**
36 * Provides navigation routing and location information
37 *
38 * @class Router
39 * @mixins OO.EventEmitter
40 */
41 function Router() {
42 var self = this;
43 OO.EventEmitter.call( this );
44 // use an object instead of an array for routes so that we don't
45 // duplicate entries that already exist
46 this.routes = {};
47 this.enabled = true;
48 this.oldHash = this.getPath();
49
50 $( window ).on( 'popstate', function () {
51 self.emit( 'popstate' );
52 } );
53
54 $( window ).on( 'hashchange', function () {
55 self.emit( 'hashchange' );
56 } );
57
58 this.on( 'hashchange', function () {
59 // ev.originalEvent.newURL is undefined on Android 2.x
60 var routeEv;
61
62 if ( self.enabled ) {
63 routeEv = $.Event( 'route', {
64 path: self.getPath()
65 } );
66 self.emit( 'route', routeEv );
67
68 if ( !routeEv.isDefaultPrevented() ) {
69 self.checkRoute();
70 } else {
71 // if route was prevented, ignore the next hash change and revert the
72 // hash to its old value
73 self.enabled = false;
74 self.navigate( self.oldHash );
75 }
76 } else {
77 self.enabled = true;
78 }
79
80 self.oldHash = self.getPath();
81 } );
82 }
83 OO.mixinClass( Router, OO.EventEmitter );
84
85 /**
86 * Check the current route and run appropriate callback if it matches.
87 *
88 * @method
89 */
90 Router.prototype.checkRoute = function () {
91 var hash = this.getPath();
92
93 $.each( this.routes, function ( id, entry ) {
94 return !matchRoute( hash, entry );
95 } );
96 };
97
98 /**
99 * Bind a specific callback to a hash-based route, e.g.
100 *
101 * @example
102 * route( 'alert', function () { alert( 'something' ); } );
103 * route( /hi-(.*)/, function ( name ) { alert( 'Hi ' + name ) } );
104 * Note that after defining all available routes it is up to the caller
105 * to check the existing route via the checkRoute method.
106 *
107 * @method
108 * @param {Object} path string or RegExp to match.
109 * @param {Function} callback Callback to be run when hash changes to one
110 * that matches.
111 */
112 Router.prototype.route = function ( path, callback ) {
113 var entry = {
114 path: typeof path === 'string' ?
115 new RegExp( '^' + path.replace( /[\\^$*+?.()|[\]{}]/g, '\\$&' ) + '$' )
116 : path,
117 callback: callback
118 };
119 this.routes[ entry.path ] = entry;
120 };
121
122 /**
123 * Navigate to a specific route.
124 *
125 * @method
126 * @param {string} path string with a route (hash without #).
127 */
128 Router.prototype.navigate = function ( path ) {
129 var history = window.history;
130 // Take advantage of `pushState` when available, to clear the hash and
131 // not leave `#` in the history. An entry with `#` in the history has
132 // the side-effect of resetting the scroll position when navigating the
133 // history.
134 if ( path === '' && history && history.pushState ) {
135 // To clear the hash we need to cut the hash from the URL.
136 path = window.location.href.replace( /#.*$/, '' );
137 history.pushState( null, document.title, path );
138 this.checkRoute();
139 } else {
140 window.location.hash = path;
141 }
142 };
143
144 /**
145 * Triggers back on the window
146 */
147 Router.prototype.goBack = function () {
148 window.history.back();
149 };
150
151 /**
152 * Navigate to the previous route. This is a wrapper for window.history.back
153 *
154 * @method
155 * @return {jQuery.Deferred}
156 */
157 Router.prototype.back = function () {
158 var deferredRequest = $.Deferred(),
159 self = this,
160 timeoutID;
161
162 this.once( 'popstate', function () {
163 clearTimeout( timeoutID );
164 deferredRequest.resolve();
165 } );
166
167 this.goBack();
168
169 // If for some reason (old browser, bug in IE/windows 8.1, etc) popstate doesn't fire,
170 // resolve manually. Since we don't know for sure which browsers besides IE10/11 have
171 // this problem, it's better to fall back this way rather than singling out browsers
172 // and resolving the deferred request for them individually.
173 // See https://connect.microsoft.com/IE/feedback/details/793618/history-back-popstate-not-working-as-expected-in-webview-control
174 // Give browser a few ms to update its history.
175 timeoutID = setTimeout( function () {
176 self.off( 'popstate' );
177 deferredRequest.resolve();
178 }, 50 );
179
180 return deferredRequest;
181 };
182
183 /**
184 * Get current path (hash).
185 *
186 * @method
187 * @return {string} Current path.
188 */
189 Router.prototype.getPath = function () {
190 return window.location.hash.slice( 1 );
191 };
192
193 /**
194 * Determine if current browser supports onhashchange event
195 *
196 * @method
197 * @return {boolean}
198 */
199 Router.prototype.isSupported = function () {
200 return 'onhashchange' in window;
201 };
202
203 module.exports = Router;
204
205 }( jQuery ) );