Import: Handle uploads with sha1 starting with 0 properly
[mediawiki.git] / resources / src / mediawiki / mediawiki.notification.js
blob36b45f177f320a51da7750af2e6f44ee3d73ca24
1 ( function ( mw, $ ) {
2         'use strict';
4         var notification,
5                 // The #mw-notification-area div that all notifications are contained inside.
6                 $area,
7                 // Number of open notification boxes at any time
8                 openNotificationCount = 0,
9                 isPageReady = false,
10                 preReadyNotifQueue = [],
11                 rAF = window.requestAnimationFrame || setTimeout;
13         /**
14          * A Notification object for 1 message.
15          *
16          * The underscore in the name is to avoid a bug <https://github.com/senchalabs/jsduck/issues/304>.
17          * It is not part of the actual class name.
18          *
19          * The constructor is not publicly accessible; use mw.notification#notify instead.
20          * This does not insert anything into the document (see #start).
21          *
22          * @class mw.Notification_
23          * @alternateClassName mw.Notification
24          * @constructor
25          * @private
26          */
27         function Notification( message, options ) {
28                 var $notification, $notificationTitle, $notificationContent;
30                 $notification = $( '<div class="mw-notification"></div>' )
31                         .data( 'mw.notification', this )
32                         .addClass( options.autoHide ? 'mw-notification-autohide' : 'mw-notification-noautohide' );
34                 if ( options.tag ) {
35                         // Sanitize options.tag before it is used by any code. (Including Notification class methods)
36                         options.tag = options.tag.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
37                         if ( options.tag ) {
38                                 $notification.addClass( 'mw-notification-tag-' + options.tag );
39                         } else {
40                                 delete options.tag;
41                         }
42                 }
44                 if ( options.type ) {
45                         // Sanitize options.type
46                         options.type = options.type.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
47                         $notification.addClass( 'mw-notification-type-' + options.type );
48                 }
50                 if ( options.title ) {
51                         $notificationTitle = $( '<div class="mw-notification-title"></div>' )
52                                 .text( options.title )
53                                 .appendTo( $notification );
54                 }
56                 $notificationContent = $( '<div class="mw-notification-content"></div>' );
58                 if ( typeof message === 'object' ) {
59                         // Handle mw.Message objects separately from DOM nodes and jQuery objects
60                         if ( message instanceof mw.Message ) {
61                                 $notificationContent.html( message.parse() );
62                         } else {
63                                 $notificationContent.append( message );
64                         }
65                 } else {
66                         $notificationContent.text( message );
67                 }
69                 $notificationContent.appendTo( $notification );
71                 // Private state parameters, meant for internal use only
72                 // isOpen: Set to true after .start() is called to avoid double calls.
73                 //         Set back to false after .close() to avoid duplicating the close animation.
74                 // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts.
75                 //           Set to true initially so .start() can call .resume().
76                 // message: The message passed to the notification. Unused now but may be used in the future
77                 //          to stop replacement of a tagged notification with another notification using the same message.
78                 // options: The options passed to the notification with a little sanitization. Used by various methods.
79                 // $notification: jQuery object containing the notification DOM node.
80                 this.isOpen = false;
81                 this.isPaused = true;
82                 this.message = message;
83                 this.options = options;
84                 this.$notification = $notification;
85         }
87         /**
88          * Start the notification. Called automatically by mw.notification#notify
89          * (possibly asynchronously on document-ready).
90          *
91          * This inserts the notification into the page, closes any matching tagged notifications,
92          * handles the fadeIn animations and replacement transitions, and starts autoHide timers.
93          *
94          * @private
95          */
96         Notification.prototype.start = function () {
97                 var options, $notification, $tagMatches, autohideCount;
99                 $area.show();
101                 if ( this.isOpen ) {
102                         return;
103                 }
105                 this.isOpen = true;
106                 openNotificationCount++;
108                 options = this.options;
109                 $notification = this.$notification;
111                 if ( options.tag ) {
112                         // Find notifications with the same tag
113                         $tagMatches = $area.find( '.mw-notification-tag-' + options.tag );
114                 }
116                 // If we found existing notification with the same tag, replace them
117                 if ( options.tag && $tagMatches.length ) {
119                         // While there can be only one "open" notif with a given tag, there can be several
120                         // matches here because they remain in the DOM until the animation is finished.
121                         $tagMatches.each( function () {
122                                 var notif = $( this ).data( 'mw.notification' );
123                                 if ( notif && notif.isOpen ) {
124                                         // Detach from render flow with position absolute so that the new tag can
125                                         // occupy its space instead.
126                                         notif.$notification
127                                                 .css( {
128                                                         position: 'absolute',
129                                                         width: notif.$notification.width()
130                                                 } )
131                                                 .css( notif.$notification.position() )
132                                                 .addClass( 'mw-notification-replaced' );
133                                         notif.close();
134                                 }
135                         } );
137                         $notification
138                                 .insertBefore( $tagMatches.first() )
139                                 .addClass( 'mw-notification-visible' );
140                 } else {
141                         $area.append( $notification );
142                         rAF( function () {
143                                 // This frame renders the element in the area (invisible)
144                                 rAF( function () {
145                                         $notification.addClass( 'mw-notification-visible' );
146                                 } );
147                         } );
148                 }
150                 // By default a notification is paused.
151                 // If this notification is within the first {autoHideLimit} notifications then
152                 // start the auto-hide timer as soon as it's created.
153                 autohideCount = $area.find( '.mw-notification-autohide' ).length;
154                 if ( autohideCount <= notification.autoHideLimit ) {
155                         this.resume();
156                 }
157         };
159         /**
160          * Pause any running auto-hide timer for this notification
161          */
162         Notification.prototype.pause = function () {
163                 if ( this.isPaused ) {
164                         return;
165                 }
166                 this.isPaused = true;
168                 if ( this.timeout ) {
169                         clearTimeout( this.timeout );
170                         delete this.timeout;
171                 }
172         };
174         /**
175          * Start autoHide timer if not already started.
176          * Does nothing if autoHide is disabled.
177          * Either to resume from pause or to make the first start.
178          */
179         Notification.prototype.resume = function () {
180                 var notif = this;
181                 if ( !notif.isPaused ) {
182                         return;
183                 }
184                 // Start any autoHide timeouts
185                 if ( notif.options.autoHide ) {
186                         notif.isPaused = false;
187                         notif.timeout = setTimeout( function () {
188                                 // Already finished, so don't try to re-clear it
189                                 delete notif.timeout;
190                                 notif.close();
191                         }, notification.autoHideSeconds * 1000 );
192                 }
193         };
195         /**
196          * Close the notification.
197          */
198         Notification.prototype.close = function () {
199                 var notif = this;
201                 if ( !this.isOpen ) {
202                         return;
203                 }
205                 this.isOpen = false;
206                 openNotificationCount--;
208                 // Clear any remaining timeout on close
209                 this.pause();
211                 // Remove the mw-notification-autohide class from the notification to avoid
212                 // having a half-closed notification counted as a notification to resume
213                 // when handling {autoHideLimit}.
214                 this.$notification.removeClass( 'mw-notification-autohide' );
216                 // Now that a notification is being closed. Start auto-hide timers for any
217                 // notification that has now become one of the first {autoHideLimit} notifications.
218                 notification.resume();
220                 rAF( function () {
221                         notif.$notification.removeClass( 'mw-notification-visible' );
223                         setTimeout( function () {
224                                 if ( openNotificationCount === 0 ) {
225                                         // Hide the area after the last notification closes. Otherwise, the padding on
226                                         // the area can be obscure content, despite the area being empty/invisible (T54659). // FIXME
227                                         $area.hide();
228                                         notif.$notification.remove();
229                                 } else {
230                                         notif.$notification.slideUp( 'fast',  function () {
231                                                 $( this ).remove();
232                                         } );
233                                 }
234                         }, 500 );
235                 } );
236         };
238         /**
239          * Helper function, take a list of notification divs and call
240          * a function on the Notification instance attached to them.
241          *
242          * @private
243          * @static
244          * @param {jQuery} $notifications A jQuery object containing notification divs
245          * @param {string} fn The name of the function to call on the Notification instance
246          */
247         function callEachNotification( $notifications, fn ) {
248                 $notifications.each( function () {
249                         var notif = $( this ).data( 'mw.notification' );
250                         if ( notif ) {
251                                 notif[ fn ]();
252                         }
253                 } );
254         }
256         /**
257          * Initialisation.
258          * Must only be called once, and not before the document is ready.
259          *
260          * @ignore
261          */
262         function init() {
263                 var offset, $window = $( window );
265                 $area = $( '<div id="mw-notification-area" class="mw-notification-area mw-notification-area-layout"></div>' )
266                         // Pause auto-hide timers when the mouse is in the notification area.
267                         .on( {
268                                 mouseenter: notification.pause,
269                                 mouseleave: notification.resume
270                         } )
271                         // When clicking on a notification close it.
272                         .on( 'click', '.mw-notification', function () {
273                                 var notif = $( this ).data( 'mw.notification' );
274                                 if ( notif ) {
275                                         notif.close();
276                                 }
277                         } )
278                         // Stop click events from <a> tags from propogating to prevent clicking.
279                         // on links from hiding a notification.
280                         .on( 'click', 'a', function ( e ) {
281                                 e.stopPropagation();
282                         } );
284                 // Prepend the notification area to the content area and save it's object.
285                 mw.util.$content.prepend( $area );
286                 offset = $area.offset();
287                 $area.hide();
289                 function updateAreaMode() {
290                         var isFloating = $window.scrollTop() > offset.top;
291                         $area
292                                 .toggleClass( 'mw-notification-area-floating', isFloating )
293                                 .toggleClass( 'mw-notification-area-layout', !isFloating );
294                 }
296                 $window.on( 'scroll', updateAreaMode );
298                 // Initial mode
299                 updateAreaMode();
300         }
302         /**
303          * @class mw.notification
304          * @singleton
305          */
306         notification = {
307                 /**
308                  * Pause auto-hide timers for all notifications.
309                  * Notifications will not auto-hide until resume is called.
310                  *
311                  * @see mw.Notification#pause
312                  */
313                 pause: function () {
314                         callEachNotification(
315                                 $area.children( '.mw-notification' ),
316                                 'pause'
317                         );
318                 },
320                 /**
321                  * Resume any paused auto-hide timers from the beginning.
322                  * Only the first #autoHideLimit timers will be resumed.
323                  */
324                 resume: function () {
325                         callEachNotification(
326                                 // Only call resume on the first #autoHideLimit notifications.
327                                 // Exclude noautohide notifications to avoid bugs where #autoHideLimit
328                                 // `{ autoHide: false }` notifications are at the start preventing any
329                                 // auto-hide notifications from being autohidden.
330                                 $area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ),
331                                 'resume'
332                         );
333                 },
335                 /**
336                  * Display a notification message to the user.
337                  *
338                  * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
339                  * @param {Object} options The options to use for the notification.
340                  *  See #defaults for details.
341                  * @return {mw.Notification} Notification object
342                  */
343                 notify: function ( message, options ) {
344                         var notif;
345                         options = $.extend( {}, notification.defaults, options );
347                         notif = new Notification( message, options );
349                         if ( isPageReady ) {
350                                 notif.start();
351                         } else {
352                                 preReadyNotifQueue.push( notif );
353                         }
355                         return notif;
356                 },
358                 /**
359                  * @property {Object}
360                  * The defaults for #notify options parameter.
361                  *
362                  * - autoHide:
363                  *   A boolean indicating whether the notifification should automatically
364                  *   be hidden after shown. Or if it should persist.
365                  *
366                  * - tag:
367                  *   An optional string. When a notification is tagged only one message
368                  *   with that tag will be displayed. Trying to display a new notification
369                  *   with the same tag as one already being displayed will cause the other
370                  *   notification to be closed and this new notification to open up inside
371                  *   the same place as the previous notification.
372                  *
373                  * - title:
374                  *   An optional title for the notification. Will be displayed above the
375                  *   content. Usually in bold.
376                  *
377                  * - type:
378                  *   An optional string for the type of the message used for styling:
379                  *   Examples: 'info', 'warn', 'error'.
380                  */
381                 defaults: {
382                         autoHide: true,
383                         tag: false,
384                         title: undefined,
385                         type: false
386                 },
388                 /**
389                  * @property {number}
390                  * Number of seconds to wait before auto-hiding notifications.
391                  */
392                 autoHideSeconds: 5,
394                 /**
395                  * @property {number}
396                  * Maximum number of notifications to count down auto-hide timers for.
397                  * Only the first #autoHideLimit notifications being displayed will
398                  * auto-hide. Any notifications further down in the list will only start
399                  * counting down to auto-hide after the first few messages have closed.
400                  *
401                  * This basically represents the number of notifications the user should
402                  * be able to process in #autoHideSeconds time.
403                  */
404                 autoHideLimit: 3
405         };
407         $( function () {
408                 var notif;
410                 init();
412                 // Handle pre-ready queue.
413                 isPageReady = true;
414                 while ( preReadyNotifQueue.length ) {
415                         notif = preReadyNotifQueue.shift();
416                         notif.start();
417                 }
418         } );
420         mw.notification = notification;
422 }( mediaWiki, jQuery ) );