5 // The .mw-notification-area div that all notifications are contained inside.
7 // Number of open notification boxes at any time
8 openNotificationCount
= 0,
10 preReadyNotifQueue
= [],
11 rAF
= window
.requestAnimationFrame
|| setTimeout
;
14 * A Notification object for 1 message.
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.
19 * The constructor is not publicly accessible; use mw.notification#notify instead.
20 * This does not insert anything into the document (see #start).
22 * @class mw.Notification_
23 * @alternateClassName mw.Notification
26 * @param {mw.Message|jQuery|HTMLElement|string} message
27 * @param {Object} options
29 function Notification( message
, options
) {
30 var $notification
, $notificationContent
;
32 $notification
= $( '<div>' )
33 .data( 'mw-notification', this )
36 options
.autoHide
? 'mw-notification-autohide' : 'mw-notification-noautohide'
40 // Sanitize options.tag before it is used by any code. (Including Notification class methods)
41 options
.tag
= options
.tag
.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' );
43 $notification
.addClass( 'mw-notification-tag-' + options
.tag
);
50 // Sanitize options.type
51 options
.type
= options
.type
.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' );
52 $notification
.addClass( 'mw-notification-type-' + options
.type
);
55 if ( options
.title
) {
57 .addClass( 'mw-notification-title' )
58 .text( options
.title
)
59 .appendTo( $notification
);
62 $notificationContent
= $( '<div>' ).addClass( 'mw-notification-content' );
64 if ( typeof message
=== 'object' ) {
65 // Handle mw.Message objects separately from DOM nodes and jQuery objects
66 if ( message
instanceof mw
.Message
) {
67 $notificationContent
.html( message
.parse() );
69 $notificationContent
.append( message
);
72 $notificationContent
.text( message
);
75 $notificationContent
.appendTo( $notification
);
77 // Private state parameters, meant for internal use only
78 // autoHideSeconds: String alias for number of seconds for timeout of auto-hiding notifications.
79 // isOpen: Set to true after .start() is called to avoid double calls.
80 // Set back to false after .close() to avoid duplicating the close animation.
81 // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts.
82 // Set to true initially so .start() can call .resume().
83 // message: The message passed to the notification. Unused now but may be used in the future
84 // to stop replacement of a tagged notification with another notification using the same message.
85 // options: The options passed to the notification with a little sanitization. Used by various methods.
86 // $notification: jQuery object containing the notification DOM node.
87 // timeout: Holds appropriate methods to set/clear timeouts
88 this.autoHideSeconds
= options
.autoHideSeconds
&&
89 notification
.autoHideSeconds
[ options
.autoHideSeconds
] ||
90 notification
.autoHideSeconds
.short;
93 this.message
= message
;
94 this.options
= options
;
95 this.$notification
= $notification
;
96 if ( options
.visibleTimeout
) {
97 this.timeout
= require( 'mediawiki.visibleTimeout' );
107 * Start the notification. Called automatically by mw.notification#notify
108 * (possibly asynchronously on document-ready).
110 * This inserts the notification into the page, closes any matching tagged notifications,
111 * handles the fadeIn animations and replacement transitions, and starts autoHide timers.
115 Notification
.prototype.start = function () {
116 var options
, $notification
, $tagMatches
, autohideCount
;
118 $area
.css( 'display', '' );
125 openNotificationCount
++;
127 options
= this.options
;
128 $notification
= this.$notification
;
131 // Find notifications with the same tag
132 $tagMatches
= $area
.find( '.mw-notification-tag-' + options
.tag
);
135 // If we found existing notification with the same tag, replace them
136 if ( options
.tag
&& $tagMatches
.length
) {
138 // While there can be only one "open" notif with a given tag, there can be several
139 // matches here because they remain in the DOM until the animation is finished.
140 $tagMatches
.each( function () {
141 var notif
= $( this ).data( 'mw-notification' );
142 if ( notif
&& notif
.isOpen
) {
143 // Detach from render flow with position absolute so that the new tag can
144 // occupy its space instead.
147 position
: 'absolute',
148 width
: notif
.$notification
.width()
150 .css( notif
.$notification
.position() )
151 .addClass( 'mw-notification-replaced' );
157 .insertBefore( $tagMatches
.first() )
158 .addClass( 'mw-notification-visible' );
160 $area
.append( $notification
);
162 // This frame renders the element in the area (invisible)
164 $notification
.addClass( 'mw-notification-visible' );
169 // By default a notification is paused.
170 // If this notification is within the first {autoHideLimit} notifications then
171 // start the auto-hide timer as soon as it's created.
172 autohideCount
= $area
.find( '.mw-notification-autohide' ).length
;
173 if ( autohideCount
<= notification
.autoHideLimit
) {
179 * Pause any running auto-hide timer for this notification
181 Notification
.prototype.pause = function () {
182 if ( this.isPaused
) {
185 this.isPaused
= true;
187 if ( this.timeoutId
) {
188 this.timeout
.clear( this.timeoutId
);
189 delete this.timeoutId
;
194 * Start autoHide timer if not already started.
195 * Does nothing if autoHide is disabled.
196 * Either to resume from pause or to make the first start.
198 Notification
.prototype.resume = function () {
201 if ( !notif
.isPaused
) {
204 // Start any autoHide timeouts
205 if ( notif
.options
.autoHide
) {
206 notif
.isPaused
= false;
207 notif
.timeoutId
= notif
.timeout
.set( function () {
208 // Already finished, so don't try to re-clear it
209 delete notif
.timeoutId
;
211 }, this.autoHideSeconds
* 1000 );
216 * Close the notification.
218 Notification
.prototype.close = function () {
221 if ( !this.isOpen
) {
226 openNotificationCount
--;
228 // Clear any remaining timeout on close
231 // Remove the mw-notification-autohide class from the notification to avoid
232 // having a half-closed notification counted as a notification to resume
233 // when handling {autoHideLimit}.
234 this.$notification
.removeClass( 'mw-notification-autohide' );
236 // Now that a notification is being closed. Start auto-hide timers for any
237 // notification that has now become one of the first {autoHideLimit} notifications.
238 notification
.resume();
241 notif
.$notification
.removeClass( 'mw-notification-visible' );
243 setTimeout( function () {
244 if ( openNotificationCount
=== 0 ) {
245 // Hide the area after the last notification closes. Otherwise, the padding on
246 // the area can be obscure content, despite the area being empty/invisible (T54659). // FIXME
247 $area
.css( 'display', 'none' );
248 notif
.$notification
.remove();
250 // FIXME: Use CSS transition
251 // eslint-disable-next-line no-jquery/no-slide
252 notif
.$notification
.slideUp( 'fast', function () {
261 * Helper function, take a list of notification divs and call
262 * a function on the Notification instance attached to them.
266 * @param {jQuery} $notifications A jQuery object containing notification divs
267 * @param {string} fn The name of the function to call on the Notification instance
269 function callEachNotification( $notifications
, fn
) {
270 $notifications
.each( function () {
271 var notif
= $( this ).data( 'mw-notification' );
280 * Must only be called once, and not before the document is ready.
288 function updateAreaMode() {
289 var shouldFloat
= window
.pageYOffset
> offset
.top
;
290 if ( isFloating
=== shouldFloat
) {
293 isFloating
= shouldFloat
;
295 .toggleClass( 'mw-notification-area-floating', isFloating
)
296 .toggleClass( 'mw-notification-area-layout', !isFloating
);
299 // Look for a preset notification area in the skin.
300 // 'data-mw*' attributes are banned from user content in Sanitizer.
301 $area
= $( '.mw-notification-area[data-mw="interface"]' ).first();
302 if ( !$area
.length
) {
303 $area
= $( '<div>' ).addClass( 'mw-notification-area' );
304 // Prepend the notification area to the content area
305 mw
.util
.$content
.prepend( $area
);
308 .addClass( 'mw-notification-area-layout' )
309 // The ID attribute here is deprecated.
310 .attr( 'id', 'mw-notification-area' )
311 // Pause auto-hide timers when the mouse is in the notification area.
313 mouseenter
: notification
.pause
,
314 mouseleave
: notification
.resume
316 // When clicking on a notification close it.
317 .on( 'click', '.mw-notification', function () {
318 var notif
= $( this ).data( 'mw-notification' );
323 // Stop click events from <a> tags from propogating to prevent clicking.
324 // on links from hiding a notification.
325 .on( 'click', 'a', function ( e
) {
329 // Read from the DOM:
330 // Must be in the next frame to avoid synchronous layout
331 // computation from offset()/getBoundingClientRect().
333 offset
= $area
.offset();
335 // Initial mode (reads, and then maybe writes)
338 // Once we have the offset for where it would normally render, set the
339 // initial state of the (currently empty) notification area to be hidden.
340 $area
.css( 'display', 'none' );
342 $( window
).on( 'scroll', updateAreaMode
);
344 // Handle pre-ready queue.
346 while ( preReadyNotifQueue
.length
) {
347 notif
= preReadyNotifQueue
.shift();
354 * @class mw.notification
359 * Pause auto-hide timers for all notifications.
360 * Notifications will not auto-hide until resume is called.
362 * @see mw.Notification#pause
365 callEachNotification(
366 $area
.children( '.mw-notification' ),
372 * Resume any paused auto-hide timers from the beginning.
373 * Only the first #autoHideLimit timers will be resumed.
375 resume: function () {
376 callEachNotification(
377 // Only call resume on the first #autoHideLimit notifications.
378 // Exclude noautohide notifications to avoid bugs where #autoHideLimit
379 // `{ autoHide: false }` notifications are at the start preventing any
380 // auto-hide notifications from being autohidden.
381 $area
.children( '.mw-notification-autohide' ).slice( 0, notification
.autoHideLimit
),
387 * Display a notification message to the user.
389 * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
390 * @param {Object} options The options to use for the notification.
391 * See #defaults for details.
392 * @return {mw.Notification} Notification object
394 notify: function ( message
, options
) {
396 options
= $.extend( {}, notification
.defaults
, options
);
398 notif
= new Notification( message
, options
);
403 preReadyNotifQueue
.push( notif
);
411 * The defaults for #notify options parameter.
414 * A boolean indicating whether the notifification should automatically
415 * be hidden after shown. Or if it should persist.
418 * Key to #autoHideSeconds for number of seconds for timeout of auto-hide
422 * An optional string. When a notification is tagged only one message
423 * with that tag will be displayed. Trying to display a new notification
424 * with the same tag as one already being displayed will cause the other
425 * notification to be closed and this new notification to open up inside
426 * the same place as the previous notification.
429 * An optional title for the notification. Will be displayed above the
430 * content. Usually in bold.
433 * An optional string for the type of the message used for styling:
434 * Examples: 'info', 'warn', 'error'.
437 * A boolean indicating if the autoHide timeout should be based on
438 * time the page was visible to user. Or if it should use wall clock time.
442 autoHideSeconds
: 'short',
460 * Maximum number of simultaneous notifications to start auto-hide timers for.
461 * Only this number of notifications being displayed will be auto-hidden at one time.
462 * Any additional notifications in the list will only start counting their timeout for
463 * auto-hiding after the previous messages have been closed.
465 * This basically represents the minimal number of notifications the user should
466 * be able to process during the {@link #defaults default} #autoHideSeconds time.
473 mw
.notification
= notification
;