4 * Provide navigation routing and location information.
6 * A router responds to hashchange and popstate events.
8 * OOjs Router Copyright 2011-2024 OOjs Team and other contributors.
9 * Released under the MIT license
10 * http://oojs.mit-license.org
12 * @author Ed Sanders <esanders@wikimedia.org>
13 * @author James D. Forrester <jforrester@wikimedia.org>
14 * @author Jon Robson <jdlrobson@gmail.com>
15 * @author Kunal Mehta <legoktm@member.fsf.org>
16 * @author MarcoAurelio <maurelio@tools.wmflabs.org>
17 * @author Prateek Saxena <prtksxna@gmail.com>
18 * @author Timo Tijhof <krinkle@fastmail.com>
20 * @exports mediawiki.router
22 class Router extends OO.Registry {
25 * Create an instance of a router that responds to hashchange and popstate events.
32 this.oldHash = this.getPath();
35 window.addEventListener( 'popstate', () => {
36 this.emit( 'popstate' );
39 window.addEventListener( 'hashchange', () => {
40 this.emit( 'hashchange' );
43 this.connect( this, { hashchange: 'onRouterHashChange' } );
49 * @event module:mediawiki.router#popstate
53 * @event module:mediawiki.router#hashchange
57 * Event fired whenever the hash changes.
59 * @event module:mediawiki.router#route
60 * @param {jQuery.Event} routeEvent
66 * Handle hashchange events emitted by ourselves
68 * @param {HashChangeEvent} [event] Hash change event, if triggered by native event
70 onRouterHashChange() {
72 // event.originalEvent.newURL is undefined on Android 2.x
73 const routeEvent = $.Event( 'route', {
76 this.emit( 'route', routeEvent );
78 if ( !routeEvent.isDefaultPrevented() ) {
81 // if route was prevented, ignore the next hash change and revert the
82 // hash to its old value
84 this.navigate( this.oldHash, true );
90 this.oldHash = this.getPath();
94 * Check the current route and run appropriate callback if it matches.
97 const hash = this.getPath();
99 for ( const id in this.registry ) {
100 const entry = this.registry[ id ];
101 const match = hash.match( entry.path );
103 entry.callback.apply( this, match.slice( 1 ) );
110 * Bind a specific callback to a hash-based route.
113 * addRoute( 'alert', function () { alert( 'something' ); } );
114 * addRoute( /hi-(.*)/, function ( name ) { alert( 'Hi ' + name ) } );
117 * Note that after defining all available routes it is up to the caller
118 * to check the existing route via the checkRoute method.
120 * @param {string|RegExp} path Path to match, string or regular expression
121 * @param {Function} callback Callback to be run when hash changes to one
124 addRoute( path, callback ) {
126 path: typeof path === 'string' ?
128 new RegExp( '^' + path.replace( /[\\^$*+?.()|[\]{}]/g, '\\$&' ) + '$' ) :
132 this.register( entry.path.toString(), entry );
136 * @deprecated Use {@link module:mediawiki.router#addRoute} instead.
139 this.addRoute.apply( this, arguments );
143 * Navigate to a specific route.
145 * @param {string} title Title of new page
146 * @param {Object} options
147 * @param {string} options.path e.g. '/path/' or '/path/#foo'
148 * @param {boolean} options.useReplaceState Set replaceStateState to use pushState when you want to
149 * avoid long history queues.
151 navigateTo( title, options ) {
152 const oldHash = this.getPath();
153 if ( options.useReplaceState ) {
154 history.replaceState( null, title, options.path );
156 history.pushState( null, title, options.path );
158 if ( this.getPath() !== oldHash ) {
159 // history.replaceState/pushState doesn't trigger a hashchange event
160 this.onRouterHashChange();
165 * Navigate to a specific 'hash fragment' route.
167 * @deprecated Use {@link module:mediawiki.router#navigateTo} instead
168 * @param {string} path String with a route (hash without #).
169 * @param {boolean} [fromHashchange] (Internal) The navigate call originated
170 * form a hashchange event, so don't emit another one.
172 navigate( path, fromHashchange ) {
173 // Take advantage of `pushState` when available, to clear the hash and
174 // not leave `#` in the history. An entry with `#` in the history has
175 // the side-effect of resetting the scroll position when navigating the
178 // To clear the hash we need to cut the hash from the URL.
179 path = window.location.href.replace( /#.*$/, '' );
180 history.pushState( null, document.title, path );
181 if ( !fromHashchange ) {
182 // history.pushState doesn't trigger a hashchange event
183 this.onRouterHashChange();
188 window.location.hash = path;
193 * Navigate to the previous route. This is a wrapper for window.history.back.
195 * @return {jQuery.Promise} Promise which resolves when the back navigation is complete
198 // eslint-disable-next-line prefer-const
200 const deferred = $.Deferred();
202 this.once( 'popstate', () => {
203 clearTimeout( timeoutID );
207 window.history.back();
209 // If for some reason (old browser, bug in IE/windows 8.1, etc) popstate doesn't fire,
210 // resolve manually. Since we don't know for sure which browsers besides IE10/11 have
211 // this problem, it's better to fall back this way rather than singling out browsers
212 // and resolving the deferred request for them individually.
213 // See https://connect.microsoft.com/IE/feedback/details/793618/history-back-popstate-not-working-as-expected-in-webview-control
214 // Give browser a few ms to update its history.
215 timeoutID = setTimeout( () => {
216 this.off( 'popstate' );
220 return deferred.promise();
224 * Get current path (hash).
226 * @return {string} Current path.
229 return window.location.hash.slice( 1 );
233 * Whether the current browser supports 'hashchange' events.
235 * @deprecated No longer needed
236 * @return {boolean} Always true
244 module.exports = new Router();