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