Merge "Special:Upload should not crash on failing previews"
[mediawiki.git] / resources / lib / oojs-router / oojs-router.js
blobb136923e9aff5dce1e9cec16f2ce811e18f9e829
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 ( $ ) {
13 'use strict';
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;
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();
50         $( window ).on( 'popstate', function () {
51                 self.emit( 'popstate' );
52         } );
54         $( window ).on( 'hashchange', function () {
55                 self.emit( 'hashchange' );
56         } );
58         this.on( 'hashchange', function () {
59                 // ev.originalEvent.newURL is undefined on Android 2.x
60                 var routeEv;
62                 if ( self.enabled ) {
63                         routeEv = $.Event( 'route', {
64                                 path: self.getPath()
65                         } );
66                         self.emit( 'route', routeEv );
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                 }
80                 self.oldHash = self.getPath();
81         } );
83 OO.mixinClass( Router, OO.EventEmitter );
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();
93         $.each( this.routes, function ( id, entry ) {
94                 return !matchRoute( hash, entry );
95         } );
98 /**
99  * Bind a specific callback to a hash-based route, e.g.
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.
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;
123  * Navigate to a specific route.
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         }
145  * Triggers back on the window
146  */
147 Router.prototype.goBack = function () {
148         window.history.back();
152  * Navigate to the previous route. This is a wrapper for window.history.back
154  * @method
155  * @return {jQuery.Deferred}
156  */
157 Router.prototype.back = function () {
158         var deferredRequest = $.Deferred(),
159                 self = this,
160                 timeoutID;
162         this.once( 'popstate', function () {
163                 clearTimeout( timeoutID );
164                 deferredRequest.resolve();
165         } );
167         this.goBack();
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 );
180         return deferredRequest;
184  * Get current path (hash).
186  * @method
187  * @return {string} Current path.
188  */
189 Router.prototype.getPath = function () {
190         return window.location.hash.slice( 1 );
194  * Determine if current browser supports onhashchange event
196  * @method
197  * @return {boolean}
198  */
199 Router.prototype.isSupported = function () {
200         return 'onhashchange' in window;
203 module.exports = Router;
205 }( jQuery ) );