Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.router / router.js
blob48ee08ca3c8df3e3fee1734b1c47e4a0d4da6f01
1 'use strict';
3 /**
4  * Provide navigation routing and location information.
5  *
6  * A router responds to hashchange and popstate events.
7  *
8  * OOjs Router Copyright 2011-2024 OOjs Team and other contributors.
9  * Released under the MIT license
10  * http://oojs.mit-license.org
11  *
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>
19  *
20  * @exports mediawiki.router
21  */
22 class Router extends OO.Registry {
24         /**
25          * Create an instance of a router that responds to hashchange and popstate events.
26          */
27         constructor() {
28                 // Parent constructor
29                 super();
31                 this.enabled = true;
32                 this.oldHash = this.getPath();
34                 // Events
35                 window.addEventListener( 'popstate', () => {
36                         this.emit( 'popstate' );
37                 } );
39                 window.addEventListener( 'hashchange', () => {
40                         this.emit( 'hashchange' );
41                 } );
43                 this.connect( this, { hashchange: 'onRouterHashChange' } );
44         }
46         /* Events */
48         /**
49          * @event module:mediawiki.router#popstate
50          */
52         /**
53          * @event module:mediawiki.router#hashchange
54          */
56         /**
57          * Event fired whenever the hash changes.
58          *
59          * @event module:mediawiki.router#route
60          * @param {jQuery.Event} routeEvent
61          */
63         /* Methods */
65         /**
66          * Handle hashchange events emitted by ourselves
67          *
68          * @param {HashChangeEvent} [event] Hash change event, if triggered by native event
69          */
70         onRouterHashChange() {
71                 if ( this.enabled ) {
72                         // event.originalEvent.newURL is undefined on Android 2.x
73                         const routeEvent = $.Event( 'route', {
74                                 path: this.getPath()
75                         } );
76                         this.emit( 'route', routeEvent );
78                         if ( !routeEvent.isDefaultPrevented() ) {
79                                 this.checkRoute();
80                         } else {
81                                 // if route was prevented, ignore the next hash change and revert the
82                                 // hash to its old value
83                                 this.enabled = false;
84                                 this.navigate( this.oldHash, true );
85                         }
86                 } else {
87                         this.enabled = true;
88                 }
90                 this.oldHash = this.getPath();
91         }
93         /**
94          * Check the current route and run appropriate callback if it matches.
95          */
96         checkRoute() {
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 );
102                         if ( match ) {
103                                 entry.callback.apply( this, match.slice( 1 ) );
104                                 return;
105                         }
106                 }
107         }
109         /**
110          * Bind a specific callback to a hash-based route.
111          *
112          * ```
113          * addRoute( 'alert', function () { alert( 'something' ); } );
114          * addRoute( /hi-(.*)/, function ( name ) { alert( 'Hi ' + name ) } );
115          * ```
116          *
117          * Note that after defining all available routes it is up to the caller
118          * to check the existing route via the checkRoute method.
119          *
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
122          *  that matches.
123          */
124         addRoute( path, callback ) {
125                 const entry = {
126                         path: typeof path === 'string' ?
128                                 new RegExp( '^' + path.replace( /[\\^$*+?.()|[\]{}]/g, '\\$&' ) + '$' ) :
129                                 path,
130                         callback: callback
131                 };
132                 this.register( entry.path.toString(), entry );
133         }
135         /**
136          * @deprecated Use {@link module:mediawiki.router#addRoute} instead.
137          */
138         route() {
139                 this.addRoute.apply( this, arguments );
140         }
142         /**
143          * Navigate to a specific route.
144          *
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.
150          */
151         navigateTo( title, options ) {
152                 const oldHash = this.getPath();
153                 if ( options.useReplaceState ) {
154                         history.replaceState( null, title, options.path );
155                 } else {
156                         history.pushState( null, title, options.path );
157                 }
158                 if ( this.getPath() !== oldHash ) {
159                         // history.replaceState/pushState doesn't trigger a hashchange event
160                         this.onRouterHashChange();
161                 }
162         }
164         /**
165          * Navigate to a specific 'hash fragment' route.
166          *
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.
171          */
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
176                 // history.
177                 if ( path === '' ) {
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();
184                         } else {
185                                 this.checkRoute();
186                         }
187                 } else {
188                         window.location.hash = path;
189                 }
190         }
192         /**
193          * Navigate to the previous route. This is a wrapper for window.history.back.
194          *
195          * @return {jQuery.Promise} Promise which resolves when the back navigation is complete
196          */
197         back() {
198                 // eslint-disable-next-line prefer-const
199                 let timeoutID;
200                 const deferred = $.Deferred();
202                 this.once( 'popstate', () => {
203                         clearTimeout( timeoutID );
204                         deferred.resolve();
205                 } );
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' );
217                         deferred.resolve();
218                 }, 50 );
220                 return deferred.promise();
221         }
223         /**
224          * Get current path (hash).
225          *
226          * @return {string} Current path.
227          */
228         getPath() {
229                 return window.location.hash.slice( 1 );
230         }
232         /**
233          * Whether the current browser supports 'hashchange' events.
234          *
235          * @deprecated No longer needed
236          * @return {boolean} Always true
237          */
238         isSupported() {
239                 return true;
240         }
243 OO.Router = Router;
244 module.exports = new Router();