Merge "[FileBackend] Clear swift connection on invalid HTTP responses."
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.notification.js
1 /**
2 * Implements mediaWiki.notification library
3 */
4 ( function ( mw, $ ) {
5 'use strict';
6
7 var isPageReady = false,
8 isInitialized = false,
9 preReadyNotifQueue = [],
10 /**
11 * @var {jQuery}
12 * The #mw-notification-area div that all notifications are contained inside.
13 */
14 $area = null;
15
16 /**
17 * Creates a Notification object for 1 message.
18 * Does not insert anything into the document (see .start()).
19 *
20 * @constructor
21 * @see mw.notification.notify
22 */
23 function Notification( message, options ) {
24 var $notification, $notificationTitle, $notificationContent;
25
26 $notification = $( '<div class="mw-notification"></div>' )
27 .data( 'mw.notification', this )
28 .addClass( options.autoHide ? 'mw-notification-autohide' : 'mw-notification-noautohide' );
29
30 if ( options.tag ) {
31 // Sanitize options.tag before it is used by any code. (Including Notification class methods)
32 options.tag = options.tag.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
33 if ( options.tag ) {
34 $notification.addClass( 'mw-notification-tag-' + options.tag );
35 } else {
36 delete options.tag;
37 }
38 }
39
40 if ( options.title ) {
41 $notificationTitle = $( '<div class="mw-notification-title"></div>' )
42 .text( options.title )
43 .appendTo( $notification );
44 }
45
46 $notificationContent = $( '<div class="mw-notification-content"></div>' );
47
48 if ( typeof message === 'object' ) {
49 // Handle mw.Message objects separately from DOM nodes and jQuery objects
50 if ( message instanceof mw.Message ) {
51 $notificationContent.html( message.parse() );
52 } else {
53 $notificationContent.append( message );
54 }
55 } else {
56 $notificationContent.text( message );
57 }
58
59 $notificationContent.appendTo( $notification );
60
61 // Private state parameters, meant for internal use only
62 // isOpen: Set to true after .start() is called to avoid double calls.
63 // Set back to false after .close() to avoid duplicating the close animation.
64 // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts.
65 // Set to true initially so .start() can call .resume().
66 // message: The message passed to the notification. Unused now but may be used in the future
67 // to stop replacement of a tagged notification with another notification using the same message.
68 // options: The options passed to the notification with a little sanitization. Used by various methods.
69 // $notification: jQuery object containing the notification DOM node.
70 this.isOpen = false;
71 this.isPaused = true;
72 this.message = message;
73 this.options = options;
74 this.$notification = $notification;
75 }
76
77 /**
78 * Start the notification.
79 * This inserts it into the page, closes any matching tagged notifications,
80 * handles the fadeIn animations and repacement transitions, and starts autoHide timers.
81 */
82 Notification.prototype.start = function () {
83 var
84 // Local references
85 $notification, options,
86 // Original opacity so that we can animate back to it later
87 opacity,
88 // Other notification elements matching the same tag
89 $tagMatches,
90 outerHeight,
91 placeholderHeight;
92
93 if ( this.isOpen ) {
94 return;
95 }
96
97 this.isOpen = true;
98
99 options = this.options;
100 $notification = this.$notification;
101
102 opacity = this.$notification.css( 'opacity' );
103
104 // Set the opacity to 0 so we can fade in later.
105 $notification.css( 'opacity', 0 );
106
107 if ( options.tag ) {
108 // Check to see if there are any tagged notifications with the same tag as the new one
109 $tagMatches = $area.find( '.mw-notification-tag-' + options.tag );
110 }
111
112 // If we found a tagged notification use the replacement pattern instead of the new
113 // notification fade-in pattern.
114 if ( options.tag && $tagMatches.length ) {
115
116 // Iterate over the tag matches to find the outerHeight we should use
117 // for the placeholder.
118 outerHeight = 0;
119 $tagMatches.each( function () {
120 var notif = $( this ).data( 'mw.notification' );
121 if ( notif ) {
122 // Use the notification's height + padding + border + margins
123 // as the placeholder height.
124 outerHeight = notif.$notification.outerHeight( true );
125 if ( notif.$replacementPlaceholder ) {
126 // Grab the height of a placeholder that has not finished animating.
127 placeholderHeight = notif.$replacementPlaceholder.height();
128 // Remove any placeholders added by a previous tagged
129 // notification that was in the middle of replacing another.
130 // This also makes sure that we only grab the placeholderHeight
131 // for the most recent notification.
132 notif.$replacementPlaceholder.remove();
133 delete notif.$replacementPlaceholder;
134 }
135 // Close the previous tagged notification
136 // Since we're replacing it do this with a fast speed and don't output a placeholder
137 // since we're taking care of that transition ourselves.
138 notif.close( { speed: 'fast', placeholder: false } );
139 }
140 } );
141 if ( placeholderHeight !== undefined ) {
142 // If the other tagged notification was in the middle of replacing another
143 // tagged notification, continue from the placeholder's height instead of
144 // using the outerHeight of the notification.
145 outerHeight = placeholderHeight;
146 }
147
148 $notification
149 // Insert the new notification before the tagged notification(s)
150 .insertBefore( $tagMatches.first() )
151 .css( {
152 // Use an absolute position so that we can use a placeholder to gracefully push other notifications
153 // into the right spot.
154 position: 'absolute',
155 width: $notification.width()
156 } )
157 // Fade-in the notification
158 .animate( { opacity: opacity },
159 {
160 duration: 'slow',
161 complete: function () {
162 // After we've faded in clear the opacity and let css take over
163 $( this ).css( { opacity: '' } );
164 }
165 } );
166
167 // Create a clear placeholder we can use to make the notifications around the notification that is being
168 // replaced expand or contract gracefully to fit the height of the new notification.
169 var self = this;
170 self.$replacementPlaceholder = $( '<div>' )
171 // Set the height to the space the previous notification or placeholder took
172 .css( 'height', outerHeight )
173 // Make sure that this placeholder is at the very end of this tagged notification group
174 .insertAfter( $tagMatches.eq( -1 ) )
175 // Animate the placeholder height to the space that this new notification will take up
176 .animate( { height: $notification.outerHeight( true ) },
177 {
178 // Do space animations fast
179 speed: 'fast',
180 complete: function () {
181 // Reset the notification position after we've finished the space animation
182 // However do not do it if the placeholder was removed because another tagged
183 // notification went and closed this one.
184 if ( self.$replacementPlaceholder ) {
185 $notification.css( 'position', '' );
186 }
187 // Finally, remove the placeholder from the DOM
188 $( this ).remove();
189 }
190 } );
191 } else {
192 // Append to the notification area and fade in to the original opacity.
193 $notification
194 .appendTo( $area )
195 .animate( { opacity: opacity },
196 {
197 duration: 'fast',
198 complete: function () {
199 // After we've faded in clear the opacity and let css take over
200 $( this ).css( 'opacity', '' );
201 }
202 }
203 );
204 }
205
206 // By default a notification is paused.
207 // If this notification is within the first {autoHideLimit} notifications then
208 // start the auto-hide timer as soon as it's created.
209 var autohideCount = $area.find( '.mw-notification-autohide' ).length;
210 if ( autohideCount <= notification.autoHideLimit ) {
211 this.resume();
212 }
213 };
214
215 /**
216 * Pause any running auto-hide timer for this notification
217 */
218 Notification.prototype.pause = function () {
219 if ( this.isPaused ) {
220 return;
221 }
222 this.isPaused = true;
223
224 if ( this.timeout ) {
225 clearTimeout( this.timeout );
226 delete this.timeout;
227 }
228 };
229
230 /**
231 * Start autoHide timer if not already started.
232 * Does nothing if autoHide is disabled.
233 * Either to resume from pause or to make the first start.
234 */
235 Notification.prototype.resume = function () {
236 var notif = this;
237 if ( !notif.isPaused ) {
238 return;
239 }
240 // Start any autoHide timeouts
241 if ( notif.options.autoHide ) {
242 notif.isPaused = false;
243 notif.timeout = setTimeout( function () {
244 // Already finished, so don't try to re-clear it
245 delete notif.timeout;
246 notif.close();
247 }, notification.autoHideSeconds * 1000 );
248 }
249 };
250
251 /**
252 * Close/hide the notification.
253 *
254 * @param {Object} options An object containing options for the closing of the notification.
255 * These are typically only used internally.
256 * - speed: Use a close speed different than the default 'slow'.
257 * - placeholder: Set to false to disable the placeholder transition.
258 */
259 Notification.prototype.close = function ( options ) {
260 if ( !this.isOpen ) {
261 return;
262 }
263 this.isOpen = false;
264 // Clear any remaining timeout on close
265 this.pause();
266
267 options = $.extend( {
268 speed: 'slow',
269 placeholder: true
270 }, options );
271
272 // Remove the mw-notification-autohide class from the notification to avoid
273 // having a half-closed notification counted as a notification to resume
274 // when handling {autoHideLimit}.
275 this.$notification.removeClass( 'mw-notification-autohide' );
276
277 // Now that a notification is being closed. Start auto-hide timers for any
278 // notification that has now become one of the first {autoHideLimit} notifications.
279 notification.resume();
280
281 this.$notification
282 .css( {
283 // Don't trigger any mouse events while fading out, just in case the cursor
284 // happens to be right above us when we transition upwards.
285 pointerEvents: 'none',
286 // Set an absolute position so we can move upwards in the animation.
287 // Notification replacement doesn't look right unless we use an animation like this.
288 position: 'absolute',
289 // We must fix the width to avoid it shrinking horizontally.
290 width: this.$notification.width()
291 } )
292 // Fix the top/left position to the current computed position from which we
293 // can animate upwards.
294 .css( this.$notification.position() )
295 // Animate opacity and top to create fade upwards animation for notification closing
296 .animate( {
297 opacity: 0,
298 top: '-=35'
299 }, {
300 duration: options.speed,
301 complete: function () {
302 // Remove the notification
303 $( this ).remove();
304 if ( options.placeholder ) {
305 // Use a fast slide up animation after closing to make it look like the notifications
306 // below slide up into place when the notification disappears
307 $placeholder.slideUp( 'fast', function () {
308 // Remove the placeholder
309 $( this ).remove();
310 } );
311 }
312 }
313 } );
314
315 if ( options.placeholder ) {
316 // Insert a placeholder with a height equal to the height of the
317 // notification plus it's vertical margins in place of the notification
318 var $placeholder = $( '<div>' )
319 .css( 'height', this.$notification.outerHeight( true ) )
320 .insertBefore( this.$notification );
321 }
322 };
323
324 /**
325 * Helper function, take a list of notification divs and call
326 * a function on the Notification instance attached to them
327 *
328 * @param {jQuery} $notifications A jQuery object containing notification divs
329 * @param {string} fn The name of the function to call on the Notification instance
330 */
331 function callEachNotification( $notifications, fn ) {
332 $notifications.each( function () {
333 var notif = $( this ).data( 'mw.notification' );
334 if ( notif ) {
335 notif[fn]();
336 }
337 } );
338 }
339
340 /**
341 * Initialisation
342 * (don't call before document ready)
343 */
344 function init() {
345 if ( !isInitialized ) {
346 isInitialized = true;
347 $area = $( '<div id="mw-notification-area"></div>' )
348 // Pause auto-hide timers when the mouse is in the notification area.
349 .on( {
350 mouseenter: notification.pause,
351 mouseleave: notification.resume
352 } )
353 // When clicking on a notification close it.
354 .on( 'click', '.mw-notification', function () {
355 var notif = $( this ).data( 'mw.notification' );
356 if ( notif ) {
357 notif.close();
358 }
359 } )
360 // Stop click events from <a> tags from propogating to prevent clicking.
361 // on links from hiding a notification.
362 .on( 'click', 'a', function ( e ) {
363 e.stopPropagation();
364 } );
365
366 // Prepend the notification area to the content area and save it's object.
367 mw.util.$content.prepend( $area );
368 }
369 }
370
371 var notification = {
372 /**
373 * Pause auto-hide timers for all notifications.
374 * Notifications will not auto-hide until resume is called.
375 */
376 pause: function () {
377 callEachNotification(
378 $area.children( '.mw-notification' ),
379 'pause'
380 );
381 },
382
383 /**
384 * Resume any paused auto-hide timers from the beginning.
385 * Only the first {autoHideLimit} timers will be resumed.
386 */
387 resume: function () {
388 callEachNotification(
389 // Only call resume on the first {autoHideLimit} notifications.
390 // Exclude noautohide notifications to avoid bugs where {autoHideLimit}
391 // { autoHide: false } notifications are at the start preventing any
392 // auto-hide notifications from being autohidden.
393 $area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ),
394 'resume'
395 );
396 },
397
398 /**
399 * Display a notification message to the user.
400 *
401 * @param {mixed} message The DOM-element, jQuery object, mw.Message instance,
402 * or plaintext string to be used as the message.
403 * @param {Object} options The options to use for the notification.
404 * See mw.notification.defaults for details.
405 */
406 notify: function ( message, options ) {
407 var notif;
408 options = $.extend( {}, notification.defaults, options );
409
410 notif = new Notification( message, options );
411
412 if ( isPageReady ) {
413 notif.start();
414 } else {
415 preReadyNotifQueue.push( notif );
416 }
417 },
418
419 /**
420 * @var {Object}
421 * The defaults for mw.notification.notify's options parameter
422 * autoHide:
423 * A boolean indicating whether the notifification should automatically
424 * be hidden after shown. Or if it should persist.
425 *
426 * tag:
427 * An optional string. When a notification is tagged only one message
428 * with that tag will be displayed. Trying to display a new notification
429 * with the same tag as one already being displayed will cause the other
430 * notification to be closed and this new notification to open up inside
431 * the same place as the previous notification.
432 *
433 * title:
434 * An optional title for the notification. Will be displayed above the
435 * content. Usually in bold.
436 */
437 defaults: {
438 autoHide: true,
439 tag: false,
440 title: undefined
441 },
442
443 /**
444 * @var {number}
445 * Number of seconds to wait before auto-hiding notifications.
446 */
447 autoHideSeconds: 5,
448
449 /**
450 * @var {number}
451 * Maximum number of notifications to count down auto-hide timers for.
452 * Only the first {autoHideLimit} notifications being displayed will
453 * auto-hide. Any notifications further down in the list will only start
454 * counting down to auto-hide after the first few messages have closed.
455 *
456 * This basically represents the number of notifications the user should
457 * be able to process in {autoHideSeconds} time.
458 */
459 autoHideLimit: 3
460 };
461
462 $( function () {
463 var notif;
464
465 init();
466
467 // Handle pre-ready queue.
468 isPageReady = true;
469 while ( preReadyNotifQueue.length ) {
470 notif = preReadyNotifQueue.shift();
471 notif.start();
472 }
473 } );
474
475 mw.notification = notification;
476
477 }( mediaWiki, jQuery ) );