Merge "RC/Watchlist: Filter out parameters that cannot be displayed"
[lhc/web/wiklou.git] / resources / src / 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 // Number of open notification boxes at any time
8 openNotificationCount = 0,
9 isPageReady = false,
10 preReadyNotifQueue = [],
11 rAF = window.requestAnimationFrame || setTimeout;
12
13 /**
14 * A Notification object for 1 message.
15 *
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.
18 *
19 * The constructor is not publicly accessible; use mw.notification#notify instead.
20 * This does not insert anything into the document (see #start).
21 *
22 * @class mw.Notification_
23 * @alternateClassName mw.Notification
24 * @constructor
25 * @private
26 * @param {mw.Message|jQuery|HTMLElement|string} message
27 * @param {Object} options
28 */
29 function Notification( message, options ) {
30 var $notification, $notificationContent;
31
32 $notification = $( '<div class="mw-notification"></div>' )
33 .data( 'mw.notification', this )
34 .addClass( options.autoHide ? 'mw-notification-autohide' : 'mw-notification-noautohide' );
35
36 if ( options.tag ) {
37 // Sanitize options.tag before it is used by any code. (Including Notification class methods)
38 options.tag = options.tag.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
39 if ( options.tag ) {
40 $notification.addClass( 'mw-notification-tag-' + options.tag );
41 } else {
42 delete options.tag;
43 }
44 }
45
46 if ( options.type ) {
47 // Sanitize options.type
48 options.type = options.type.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
49 $notification.addClass( 'mw-notification-type-' + options.type );
50 }
51
52 if ( options.title ) {
53 $( '<div class="mw-notification-title"></div>' )
54 .text( options.title )
55 .appendTo( $notification );
56 }
57
58 $notificationContent = $( '<div class="mw-notification-content"></div>' );
59
60 if ( typeof message === 'object' ) {
61 // Handle mw.Message objects separately from DOM nodes and jQuery objects
62 if ( message instanceof mw.Message ) {
63 $notificationContent.html( message.parse() );
64 } else {
65 $notificationContent.append( message );
66 }
67 } else {
68 $notificationContent.text( message );
69 }
70
71 $notificationContent.appendTo( $notification );
72
73 // Private state parameters, meant for internal use only
74 // isOpen: Set to true after .start() is called to avoid double calls.
75 // Set back to false after .close() to avoid duplicating the close animation.
76 // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts.
77 // Set to true initially so .start() can call .resume().
78 // message: The message passed to the notification. Unused now but may be used in the future
79 // to stop replacement of a tagged notification with another notification using the same message.
80 // options: The options passed to the notification with a little sanitization. Used by various methods.
81 // $notification: jQuery object containing the notification DOM node.
82 this.isOpen = false;
83 this.isPaused = true;
84 this.message = message;
85 this.options = options;
86 this.$notification = $notification;
87 }
88
89 /**
90 * Start the notification. Called automatically by mw.notification#notify
91 * (possibly asynchronously on document-ready).
92 *
93 * This inserts the notification into the page, closes any matching tagged notifications,
94 * handles the fadeIn animations and replacement transitions, and starts autoHide timers.
95 *
96 * @private
97 */
98 Notification.prototype.start = function () {
99 var options, $notification, $tagMatches, autohideCount;
100
101 $area.show();
102
103 if ( this.isOpen ) {
104 return;
105 }
106
107 this.isOpen = true;
108 openNotificationCount++;
109
110 options = this.options;
111 $notification = this.$notification;
112
113 if ( options.tag ) {
114 // Find notifications with the same tag
115 $tagMatches = $area.find( '.mw-notification-tag-' + options.tag );
116 }
117
118 // If we found existing notification with the same tag, replace them
119 if ( options.tag && $tagMatches.length ) {
120
121 // While there can be only one "open" notif with a given tag, there can be several
122 // matches here because they remain in the DOM until the animation is finished.
123 $tagMatches.each( function () {
124 var notif = $( this ).data( 'mw.notification' );
125 if ( notif && notif.isOpen ) {
126 // Detach from render flow with position absolute so that the new tag can
127 // occupy its space instead.
128 notif.$notification
129 .css( {
130 position: 'absolute',
131 width: notif.$notification.width()
132 } )
133 .css( notif.$notification.position() )
134 .addClass( 'mw-notification-replaced' );
135 notif.close();
136 }
137 } );
138
139 $notification
140 .insertBefore( $tagMatches.first() )
141 .addClass( 'mw-notification-visible' );
142 } else {
143 $area.append( $notification );
144 rAF( function () {
145 // This frame renders the element in the area (invisible)
146 rAF( function () {
147 $notification.addClass( 'mw-notification-visible' );
148 } );
149 } );
150 }
151
152 // By default a notification is paused.
153 // If this notification is within the first {autoHideLimit} notifications then
154 // start the auto-hide timer as soon as it's created.
155 autohideCount = $area.find( '.mw-notification-autohide' ).length;
156 if ( autohideCount <= notification.autoHideLimit ) {
157 this.resume();
158 }
159 };
160
161 /**
162 * Pause any running auto-hide timer for this notification
163 */
164 Notification.prototype.pause = function () {
165 if ( this.isPaused ) {
166 return;
167 }
168 this.isPaused = true;
169
170 if ( this.timeout ) {
171 clearTimeout( this.timeout );
172 delete this.timeout;
173 }
174 };
175
176 /**
177 * Start autoHide timer if not already started.
178 * Does nothing if autoHide is disabled.
179 * Either to resume from pause or to make the first start.
180 */
181 Notification.prototype.resume = function () {
182 var notif = this;
183 if ( !notif.isPaused ) {
184 return;
185 }
186 // Start any autoHide timeouts
187 if ( notif.options.autoHide ) {
188 notif.isPaused = false;
189 notif.timeout = setTimeout( function () {
190 // Already finished, so don't try to re-clear it
191 delete notif.timeout;
192 notif.close();
193 }, notification.autoHideSeconds * 1000 );
194 }
195 };
196
197 /**
198 * Close the notification.
199 */
200 Notification.prototype.close = function () {
201 var notif = this;
202
203 if ( !this.isOpen ) {
204 return;
205 }
206
207 this.isOpen = false;
208 openNotificationCount--;
209
210 // Clear any remaining timeout on close
211 this.pause();
212
213 // Remove the mw-notification-autohide class from the notification to avoid
214 // having a half-closed notification counted as a notification to resume
215 // when handling {autoHideLimit}.
216 this.$notification.removeClass( 'mw-notification-autohide' );
217
218 // Now that a notification is being closed. Start auto-hide timers for any
219 // notification that has now become one of the first {autoHideLimit} notifications.
220 notification.resume();
221
222 rAF( function () {
223 notif.$notification.removeClass( 'mw-notification-visible' );
224
225 setTimeout( function () {
226 if ( openNotificationCount === 0 ) {
227 // Hide the area after the last notification closes. Otherwise, the padding on
228 // the area can be obscure content, despite the area being empty/invisible (T54659). // FIXME
229 $area.hide();
230 notif.$notification.remove();
231 } else {
232 notif.$notification.slideUp( 'fast', function () {
233 $( this ).remove();
234 } );
235 }
236 }, 500 );
237 } );
238 };
239
240 /**
241 * Helper function, take a list of notification divs and call
242 * a function on the Notification instance attached to them.
243 *
244 * @private
245 * @static
246 * @param {jQuery} $notifications A jQuery object containing notification divs
247 * @param {string} fn The name of the function to call on the Notification instance
248 */
249 function callEachNotification( $notifications, fn ) {
250 $notifications.each( function () {
251 var notif = $( this ).data( 'mw.notification' );
252 if ( notif ) {
253 notif[ fn ]();
254 }
255 } );
256 }
257
258 /**
259 * Initialisation.
260 * Must only be called once, and not before the document is ready.
261 *
262 * @ignore
263 */
264 function init() {
265 var offset,
266 isFloating = false;
267
268 $area = $( '<div id="mw-notification-area" class="mw-notification-area mw-notification-area-layout"></div>' )
269 // Pause auto-hide timers when the mouse is in the notification area.
270 .on( {
271 mouseenter: notification.pause,
272 mouseleave: notification.resume
273 } )
274 // When clicking on a notification close it.
275 .on( 'click', '.mw-notification', function () {
276 var notif = $( this ).data( 'mw.notification' );
277 if ( notif ) {
278 notif.close();
279 }
280 } )
281 // Stop click events from <a> tags from propogating to prevent clicking.
282 // on links from hiding a notification.
283 .on( 'click', 'a', function ( e ) {
284 e.stopPropagation();
285 } );
286
287 // Prepend the notification area to the content area and save it's object.
288 mw.util.$content.prepend( $area );
289 offset = $area.offset();
290 $area.hide();
291
292 function updateAreaMode() {
293 var shouldFloat = window.pageYOffset > offset.top;
294 if ( isFloating === shouldFloat ) {
295 return;
296 }
297 isFloating = shouldFloat;
298 $area
299 .toggleClass( 'mw-notification-area-floating', isFloating )
300 .toggleClass( 'mw-notification-area-layout', !isFloating );
301 }
302
303 $( window ).on( 'scroll', updateAreaMode );
304
305 // Initial mode
306 updateAreaMode();
307 }
308
309 /**
310 * @class mw.notification
311 * @singleton
312 */
313 notification = {
314 /**
315 * Pause auto-hide timers for all notifications.
316 * Notifications will not auto-hide until resume is called.
317 *
318 * @see mw.Notification#pause
319 */
320 pause: function () {
321 callEachNotification(
322 $area.children( '.mw-notification' ),
323 'pause'
324 );
325 },
326
327 /**
328 * Resume any paused auto-hide timers from the beginning.
329 * Only the first #autoHideLimit timers will be resumed.
330 */
331 resume: function () {
332 callEachNotification(
333 // Only call resume on the first #autoHideLimit notifications.
334 // Exclude noautohide notifications to avoid bugs where #autoHideLimit
335 // `{ autoHide: false }` notifications are at the start preventing any
336 // auto-hide notifications from being autohidden.
337 $area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ),
338 'resume'
339 );
340 },
341
342 /**
343 * Display a notification message to the user.
344 *
345 * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
346 * @param {Object} options The options to use for the notification.
347 * See #defaults for details.
348 * @return {mw.Notification} Notification object
349 */
350 notify: function ( message, options ) {
351 var notif;
352 options = $.extend( {}, notification.defaults, options );
353
354 notif = new Notification( message, options );
355
356 if ( isPageReady ) {
357 notif.start();
358 } else {
359 preReadyNotifQueue.push( notif );
360 }
361
362 return notif;
363 },
364
365 /**
366 * @property {Object}
367 * The defaults for #notify options parameter.
368 *
369 * - autoHide:
370 * A boolean indicating whether the notifification should automatically
371 * be hidden after shown. Or if it should persist.
372 *
373 * - tag:
374 * An optional string. When a notification is tagged only one message
375 * with that tag will be displayed. Trying to display a new notification
376 * with the same tag as one already being displayed will cause the other
377 * notification to be closed and this new notification to open up inside
378 * the same place as the previous notification.
379 *
380 * - title:
381 * An optional title for the notification. Will be displayed above the
382 * content. Usually in bold.
383 *
384 * - type:
385 * An optional string for the type of the message used for styling:
386 * Examples: 'info', 'warn', 'error'.
387 */
388 defaults: {
389 autoHide: true,
390 tag: false,
391 title: undefined,
392 type: false
393 },
394
395 /**
396 * @property {number}
397 * Number of seconds to wait before auto-hiding notifications.
398 */
399 autoHideSeconds: 5,
400
401 /**
402 * @property {number}
403 * Maximum number of notifications to count down auto-hide timers for.
404 * Only the first #autoHideLimit notifications being displayed will
405 * auto-hide. Any notifications further down in the list will only start
406 * counting down to auto-hide after the first few messages have closed.
407 *
408 * This basically represents the number of notifications the user should
409 * be able to process in #autoHideSeconds time.
410 */
411 autoHideLimit: 3
412 };
413
414 $( function () {
415 var notif;
416
417 init();
418
419 // Handle pre-ready queue.
420 isPageReady = true;
421 while ( preReadyNotifQueue.length ) {
422 notif = preReadyNotifQueue.shift();
423 notif.start();
424 }
425 } );
426
427 mw.notification = notification;
428
429 }( mediaWiki, jQuery ) );