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