4 let notification
= null,
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 const preReadyNotifQueue
= [];
13 * @classdesc Describes a notification. See [mw.notification module]{@link mw.notification}. A Notification object for 1 message.
15 * The constructor is not publicly accessible; use [mw.notification.notify]{@link mw.notification} instead.
16 * This does not insert anything into the document. To add to document use
17 * [mw.notification.notify]{@link mw.notification#notify}.
22 * @param {mw.Message|jQuery|HTMLElement|string} message
23 * @param {mw.notification.NotificationOptions} options
25 function Notification( message
, options
) {
27 const $notification
= $( '<div>' )
28 .data( 'mw-notification', this )
29 .attr( 'role', 'status' )
32 options
.autoHide
? 'mw-notification-autohide' : 'mw-notification-noautohide'
36 // Sanitize options.tag before it is used by any code. (Including Notification class methods)
37 options
.tag
= options
.tag
.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' );
39 // eslint-disable-next-line mediawiki/class-doc
40 $notification
.addClass( 'mw-notification-tag-' + options
.tag
);
47 // Sanitize options.type
48 options
.type
= options
.type
.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' );
49 // The following classes are used here:
50 // * mw-notification-type-error
51 // * mw-notification-type-warn
52 $notification
.addClass( 'mw-notification-type-' + options
.type
);
55 if ( options
.title
) {
57 .addClass( 'mw-notification-title' )
58 .text( options
.title
)
59 .appendTo( $notification
);
63 $notification
.attr( 'id', options
.id
);
66 if ( options
.classes
) {
67 // eslint-disable-next-line mediawiki/class-doc
68 $notification
.addClass( options
.classes
);
71 const $notificationContent
= $( '<div>' ).addClass( 'mw-notification-content' );
73 if ( typeof message
=== 'object' ) {
74 // Handle mw.Message objects separately from DOM nodes and jQuery objects
75 if ( message
instanceof mw
.Message
) {
76 $notificationContent
.html( message
.parse() );
78 $notificationContent
.append( message
);
81 $notificationContent
.text( message
);
84 $notificationContent
.appendTo( $notification
);
86 // Private state parameters, meant for internal use only
87 // autoHideSeconds: String alias for number of seconds for timeout of auto-hiding notifications.
88 // isOpen: Set to true after .start() is called to avoid double calls.
89 // Set back to false after .close() to avoid duplicating the close animation.
90 // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts.
91 // Set to true initially so .start() can call .resume().
92 // message: The message passed to the notification. Unused now but may be used in the future
93 // to stop replacement of a tagged notification with another notification using the same message.
94 // options: The options passed to the notification with a little sanitization. Used by various methods.
95 // $notification: jQuery object containing the notification DOM node.
96 // timeout: Holds appropriate methods to set/clear timeouts
97 this.autoHideSeconds
= options
.autoHideSeconds
&&
98 notification
.autoHideSeconds
[ options
.autoHideSeconds
] ||
99 notification
.autoHideSeconds
.short;
101 this.isPaused
= true;
102 this.message
= message
;
103 this.options
= options
;
104 this.$notification
= $notification
;
105 if ( options
.visibleTimeout
) {
106 this.timeout
= require( 'mediawiki.visibleTimeout' );
116 * Start the notification. Called automatically by mw.notification#notify
117 * (possibly asynchronously on document-ready).
119 * This inserts the notification into the page, closes any matching tagged notifications,
120 * handles the fadeIn animations and replacement transitions, and starts autoHide timers.
124 Notification
.prototype.start = function () {
125 $area
.css( 'display', '' );
132 openNotificationCount
++;
134 const options
= this.options
;
135 const $notification
= this.$notification
;
139 // Find notifications with the same tag
140 $tagMatches
= $area
.find( '.mw-notification-tag-' + options
.tag
);
143 // If we found existing notification with the same tag, replace them
144 if ( options
.tag
&& $tagMatches
.length
) {
146 // While there can be only one "open" notif with a given tag, there can be several
147 // matches here because they remain in the DOM until the animation is finished.
148 $tagMatches
.each( function () {
149 const notif
= $( this ).data( 'mw-notification' );
150 if ( notif
&& notif
.isOpen
) {
151 // Detach from render flow with position absolute so that the new tag can
152 // occupy its space instead.
155 position
: 'absolute',
156 width
: notif
.$notification
.width()
158 .css( notif
.$notification
.position() )
159 .addClass( 'mw-notification-replaced' );
165 .insertBefore( $tagMatches
.first() )
166 .addClass( 'mw-notification-visible' );
168 $area
.append( $notification
);
169 requestAnimationFrame( () => {
170 // This frame renders the element in the area (invisible)
171 requestAnimationFrame( () => {
172 $notification
.addClass( 'mw-notification-visible' );
177 // By default a notification is paused.
178 // If this notification is within the first {autoHideLimit} notifications then
179 // start the auto-hide timer as soon as it's created.
180 const autohideCount
= $area
.find( '.mw-notification-autohide' ).length
;
181 if ( autohideCount
<= notification
.autoHideLimit
) {
187 * Pause any running auto-hide timer for this notification.
189 * @memberof Notification
191 Notification
.prototype.pause = function () {
192 if ( this.isPaused
) {
195 this.isPaused
= true;
197 if ( this.timeoutId
) {
198 this.timeout
.clear( this.timeoutId
);
199 delete this.timeoutId
;
204 * Start autoHide timer if not already started.
205 * Does nothing if autoHide is disabled.
206 * Either to resume from pause or to make the first start.
208 * @memberof Notification
210 Notification
.prototype.resume = function () {
211 if ( !this.isPaused
) {
214 // Start any autoHide timeouts
215 if ( this.options
.autoHide
) {
216 this.isPaused
= false;
217 this.timeoutId
= this.timeout
.set( () => {
218 // Already finished, so don't try to re-clear it
219 delete this.timeoutId
;
221 }, this.autoHideSeconds
* 1000 );
226 * Close the notification.
228 * @memberof Notification
230 Notification
.prototype.close = function () {
231 if ( !this.isOpen
) {
236 openNotificationCount
--;
238 // Clear any remaining timeout on close
241 // Remove the mw-notification-autohide class from the notification to avoid
242 // having a half-closed notification counted as a notification to resume
243 // when handling {autoHideLimit}.
244 this.$notification
.removeClass( 'mw-notification-autohide' );
246 // Now that a notification is being closed. Start auto-hide timers for any
247 // notification that has now become one of the first {autoHideLimit} notifications.
248 notification
.resume();
250 requestAnimationFrame( () => {
251 this.$notification
.removeClass( 'mw-notification-visible' );
254 if ( openNotificationCount
=== 0 ) {
255 // Hide the area after the last notification closes. Otherwise, the padding on
256 // the area can be obscure content, despite the area being empty/invisible (T54659). // FIXME
257 $area
.css( 'display', 'none' );
258 this.$notification
.remove();
260 // FIXME: Use CSS transition
261 // eslint-disable-next-line no-jquery/no-slide
262 this.$notification
.slideUp( 'fast', function () {
271 * Helper function, take a list of notification divs and call
272 * a function on the Notification instance attached to them.
276 * @param {jQuery} $notifications A jQuery object containing notification divs
277 * @param {string} fn The name of the function to call on the Notification instance
279 function callEachNotification( $notifications
, fn
) {
280 $notifications
.each( function () {
281 const notif
= $( this ).data( 'mw-notification' );
290 * Must only be called once, and not before the document is ready.
295 let offset
, $overlay
,
298 function updateAreaMode() {
299 const shouldFloat
= window
.pageYOffset
> offset
.top
;
300 if ( isFloating
=== shouldFloat
) {
303 isFloating
= shouldFloat
;
305 .toggleClass( 'mw-notification-area-floating', isFloating
)
306 .toggleClass( 'mw-notification-area-layout', !isFloating
);
309 // Look for a preset notification area in the skin.
310 // 'data-mw*' attributes are banned from user content in Sanitizer.
311 $area
= $( '.mw-notification-area[data-mw="interface"]' ).first();
312 if ( !$area
.length
) {
313 $area
= $( '<div>' ).addClass( 'mw-notification-area' );
314 // Create overlay div for the notification area
315 $overlay
= $( '<div>' ).addClass( 'mw-notification-area-overlay' );
316 // Append the notification area to the overlay wrapper area
317 $overlay
.append( $area
);
318 $( document
.body
).append( $overlay
);
321 .addClass( 'mw-notification-area-layout' )
322 // The ID attribute here is deprecated.
323 .attr( 'id', 'mw-notification-area' )
324 // Pause auto-hide timers when the mouse is in the notification area.
326 mouseenter
: notification
.pause
,
327 mouseleave
: notification
.resume
329 // When clicking on a notification close it.
330 .on( 'click', '.mw-notification', function () {
331 const notif
= $( this ).data( 'mw-notification' );
336 // Stop click events from <a> and <select> tags from propagating to prevent clicks
337 // from hiding a notification. stopPropagation() bubbles up, not down,
338 // hence this should not conflict with OOUI's own click handlers.
339 .on( 'click', 'a, select, .oo-ui-dropdownInputWidget', ( e
) => {
343 // Read from the DOM:
344 // Must be in the next frame to avoid synchronous layout
345 // computation from offset()/getBoundingClientRect().
346 requestAnimationFrame( () => {
349 offset
= $area
.offset();
351 // Initial mode (reads, and then maybe writes)
354 // Once we have the offset for where it would normally render, set the
355 // initial state of the (currently empty) notification area to be hidden.
356 $area
.css( 'display', 'none' );
358 $( window
).on( 'scroll', updateAreaMode
);
360 // Handle pre-ready queue.
362 while ( preReadyNotifQueue
.length
) {
363 notif
= preReadyNotifQueue
.shift();
370 * Library for sending notifications to end users.
372 * @namespace mw.notification
378 * Pause auto-hide timers for all notifications.
379 * Notifications will not auto-hide until resume is called.
381 * @see Notification#pause
382 * @memberof mw.notification
385 callEachNotification(
386 $area
.children( '.mw-notification' ),
392 * Resume any paused auto-hide timers from the beginning.
393 * Only the first {@link mw.notification.autoHideLimit} timers will be resumed.
395 * @memberof mw.notification
397 resume: function () {
398 callEachNotification(
399 // Only call resume on the first #autoHideLimit notifications.
400 // Exclude noautohide notifications to avoid bugs where #autoHideLimit
401 // `{ autoHide: false }` notifications are at the start preventing any
402 // auto-hide notifications from being autohidden.
403 $area
.children( '.mw-notification-autohide' ).slice( 0, notification
.autoHideLimit
),
409 * Display a notification message to the user.
411 * @memberof mw.notification
412 * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
413 * @param {mw.notification.NotificationOptions} [options] The options to use
414 * for the notification. Options not specified default to the values in
415 * [#defaults]{@link mw.notification.defaults}.
416 * @return {Notification} Notification object
418 notify: function ( message
, options
) {
419 options
= Object
.assign( {}, notification
.defaults
, options
);
421 const notif
= new Notification( message
, options
);
426 preReadyNotifQueue
.push( notif
);
433 * @memberof mw.notification
434 * @typedef {Object} NotificationOptions
435 * @property {boolean} autoHide Whether the notification should automatically
436 * be hidden after shown. Or if it should persist.
437 * @property {string} autoHideSeconds Key to
438 * [#autoHideSeconds]{@link mw.notification.autoHideSeconds} for number of
439 * seconds for timeout of auto-hide notifications.
440 * @property {string|null} tag When a notification is tagged only one message
441 * with that tag will be displayed. Trying to display a new notification
442 * with the same tag as one already being displayed will cause the other
443 * notification to be closed and this new notification to open up inside
444 * the same place as the previous notification.
445 * @property {string|null} title Title for the notification. Will be displayed
446 * above the content. Usually in bold.
447 * @property {string|null} type The type of the message used for styling.
448 * Examples: `info`, `warn`, `error`, `success`.
449 * @property {boolean} visibleTimeout Whether the autoHide timeout should be
450 * based on time the page was visible to user. Or if it should use wall
452 * @property {string|false} id HTML ID to set on the notification element.
453 * @property {string|string[]|false} classes CSS class names to be set on the
454 * notification element.
458 * The defaults for [#notify]{@link mw.notification.notify} options parameter.
460 * @memberof mw.notification
461 * @type {mw.notification.NotificationOptions}
465 autoHideSeconds
: 'short',
469 visibleTimeout
: true,
475 * Map of predefined auto-hide timeout keys to second values. `short` is
476 * used by default, and other values can be added for use in [#notify]{@link mw.notification.notify}.
478 * @memberof mw.notification
479 * @type {Object.<string, number>}
480 * @property {number} short 5 seconds (default)
481 * @property {number} long 30 seconds
489 * Maximum number of simultaneous notifications to start auto-hide timers for.
490 * Only this number of notifications being displayed will be auto-hidden at one time.
491 * Any additional notifications in the list will only start counting their timeout for
492 * auto-hiding after the previous messages have been closed.
494 * This basically represents the minimal number of notifications the user should
495 * be able to process during the {@link mw.notification.defaults default} `autoHideSeconds` time.
497 * @memberof mw.notification
503 if ( window
.QUnit
) {
504 $area
= $( document
.body
);
506 // Don't run UI logic while under test.
507 // Let the test control this instead.
511 mw
.notification
= notification
;