diff options
Diffstat (limited to 'www/wiki/resources/src/mediawiki/mediawiki.notification.js')
-rw-r--r-- | www/wiki/resources/src/mediawiki/mediawiki.notification.js | 462 |
1 files changed, 462 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki/mediawiki.notification.js b/www/wiki/resources/src/mediawiki/mediawiki.notification.js new file mode 100644 index 00000000..aa86a4b5 --- /dev/null +++ b/www/wiki/resources/src/mediawiki/mediawiki.notification.js @@ -0,0 +1,462 @@ +( function ( mw, $ ) { + 'use strict'; + + var notification, + // The #mw-notification-area div that all notifications are contained inside. + $area, + // Number of open notification boxes at any time + openNotificationCount = 0, + isPageReady = false, + preReadyNotifQueue = [], + rAF = window.requestAnimationFrame || setTimeout; + + /** + * A Notification object for 1 message. + * + * The underscore in the name is to avoid a bug <https://github.com/senchalabs/jsduck/issues/304>. + * It is not part of the actual class name. + * + * The constructor is not publicly accessible; use mw.notification#notify instead. + * This does not insert anything into the document (see #start). + * + * @class mw.Notification_ + * @alternateClassName mw.Notification + * @constructor + * @private + * @param {mw.Message|jQuery|HTMLElement|string} message + * @param {Object} options + */ + function Notification( message, options ) { + var $notification, $notificationContent; + + $notification = $( '<div class="mw-notification"></div>' ) + .data( 'mw.notification', this ) + .addClass( options.autoHide ? 'mw-notification-autohide' : 'mw-notification-noautohide' ); + + if ( options.tag ) { + // Sanitize options.tag before it is used by any code. (Including Notification class methods) + options.tag = options.tag.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' ); + if ( options.tag ) { + $notification.addClass( 'mw-notification-tag-' + options.tag ); + } else { + delete options.tag; + } + } + + if ( options.type ) { + // Sanitize options.type + options.type = options.type.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' ); + $notification.addClass( 'mw-notification-type-' + options.type ); + } + + if ( options.title ) { + $( '<div class="mw-notification-title"></div>' ) + .text( options.title ) + .appendTo( $notification ); + } + + $notificationContent = $( '<div class="mw-notification-content"></div>' ); + + if ( typeof message === 'object' ) { + // Handle mw.Message objects separately from DOM nodes and jQuery objects + if ( message instanceof mw.Message ) { + $notificationContent.html( message.parse() ); + } else { + $notificationContent.append( message ); + } + } else { + $notificationContent.text( message ); + } + + $notificationContent.appendTo( $notification ); + + // Private state parameters, meant for internal use only + // autoHideSeconds: String alias for number of seconds for timeout of auto-hiding notifications. + // isOpen: Set to true after .start() is called to avoid double calls. + // Set back to false after .close() to avoid duplicating the close animation. + // isPaused: false after .resume(), true after .pause(). Avoids duplicating or breaking the hide timeouts. + // Set to true initially so .start() can call .resume(). + // message: The message passed to the notification. Unused now but may be used in the future + // to stop replacement of a tagged notification with another notification using the same message. + // options: The options passed to the notification with a little sanitization. Used by various methods. + // $notification: jQuery object containing the notification DOM node. + // timeout: Holds appropriate methods to set/clear timeouts + this.autoHideSeconds = options.autoHideSeconds && + notification.autoHideSeconds[ options.autoHideSeconds ] || + notification.autoHideSeconds.short; + this.isOpen = false; + this.isPaused = true; + this.message = message; + this.options = options; + this.$notification = $notification; + if ( options.visibleTimeout ) { + this.timeout = require( 'mediawiki.visibleTimeout' ); + } else { + this.timeout = { + set: setTimeout, + clear: clearTimeout + }; + } + } + + /** + * Start the notification. Called automatically by mw.notification#notify + * (possibly asynchronously on document-ready). + * + * This inserts the notification into the page, closes any matching tagged notifications, + * handles the fadeIn animations and replacement transitions, and starts autoHide timers. + * + * @private + */ + Notification.prototype.start = function () { + var options, $notification, $tagMatches, autohideCount; + + $area.css( 'display', '' ); + + if ( this.isOpen ) { + return; + } + + this.isOpen = true; + openNotificationCount++; + + options = this.options; + $notification = this.$notification; + + if ( options.tag ) { + // Find notifications with the same tag + $tagMatches = $area.find( '.mw-notification-tag-' + options.tag ); + } + + // If we found existing notification with the same tag, replace them + if ( options.tag && $tagMatches.length ) { + + // While there can be only one "open" notif with a given tag, there can be several + // matches here because they remain in the DOM until the animation is finished. + $tagMatches.each( function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif && notif.isOpen ) { + // Detach from render flow with position absolute so that the new tag can + // occupy its space instead. + notif.$notification + .css( { + position: 'absolute', + width: notif.$notification.width() + } ) + .css( notif.$notification.position() ) + .addClass( 'mw-notification-replaced' ); + notif.close(); + } + } ); + + $notification + .insertBefore( $tagMatches.first() ) + .addClass( 'mw-notification-visible' ); + } else { + $area.append( $notification ); + rAF( function () { + // This frame renders the element in the area (invisible) + rAF( function () { + $notification.addClass( 'mw-notification-visible' ); + } ); + } ); + } + + // By default a notification is paused. + // If this notification is within the first {autoHideLimit} notifications then + // start the auto-hide timer as soon as it's created. + autohideCount = $area.find( '.mw-notification-autohide' ).length; + if ( autohideCount <= notification.autoHideLimit ) { + this.resume(); + } + }; + + /** + * Pause any running auto-hide timer for this notification + */ + Notification.prototype.pause = function () { + if ( this.isPaused ) { + return; + } + this.isPaused = true; + + if ( this.timeoutId ) { + this.timeout.clear( this.timeoutId ); + delete this.timeoutId; + } + }; + + /** + * Start autoHide timer if not already started. + * Does nothing if autoHide is disabled. + * Either to resume from pause or to make the first start. + */ + Notification.prototype.resume = function () { + var notif = this; + + if ( !notif.isPaused ) { + return; + } + // Start any autoHide timeouts + if ( notif.options.autoHide ) { + notif.isPaused = false; + notif.timeoutId = notif.timeout.set( function () { + // Already finished, so don't try to re-clear it + delete notif.timeoutId; + notif.close(); + }, this.autoHideSeconds * 1000 ); + } + }; + + /** + * Close the notification. + */ + Notification.prototype.close = function () { + var notif = this; + + if ( !this.isOpen ) { + return; + } + + this.isOpen = false; + openNotificationCount--; + + // Clear any remaining timeout on close + this.pause(); + + // Remove the mw-notification-autohide class from the notification to avoid + // having a half-closed notification counted as a notification to resume + // when handling {autoHideLimit}. + this.$notification.removeClass( 'mw-notification-autohide' ); + + // Now that a notification is being closed. Start auto-hide timers for any + // notification that has now become one of the first {autoHideLimit} notifications. + notification.resume(); + + rAF( function () { + notif.$notification.removeClass( 'mw-notification-visible' ); + + setTimeout( function () { + if ( openNotificationCount === 0 ) { + // Hide the area after the last notification closes. Otherwise, the padding on + // the area can be obscure content, despite the area being empty/invisible (T54659). // FIXME + $area.css( 'display', 'none' ); + notif.$notification.remove(); + } else { + notif.$notification.slideUp( 'fast', function () { + $( this ).remove(); + } ); + } + }, 500 ); + } ); + }; + + /** + * Helper function, take a list of notification divs and call + * a function on the Notification instance attached to them. + * + * @private + * @static + * @param {jQuery} $notifications A jQuery object containing notification divs + * @param {string} fn The name of the function to call on the Notification instance + */ + function callEachNotification( $notifications, fn ) { + $notifications.each( function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif ) { + notif[ fn ](); + } + } ); + } + + /** + * Initialisation. + * Must only be called once, and not before the document is ready. + * + * @ignore + */ + function init() { + var offset, notif, + isFloating = false; + + function updateAreaMode() { + var shouldFloat = window.pageYOffset > offset.top; + if ( isFloating === shouldFloat ) { + return; + } + isFloating = shouldFloat; + $area + .toggleClass( 'mw-notification-area-floating', isFloating ) + .toggleClass( 'mw-notification-area-layout', !isFloating ); + } + + // Write to the DOM: + // Prepend the notification area to the content area and save its object. + $area = $( '<div id="mw-notification-area" class="mw-notification-area mw-notification-area-layout"></div>' ) + // Pause auto-hide timers when the mouse is in the notification area. + .on( { + mouseenter: notification.pause, + mouseleave: notification.resume + } ) + // When clicking on a notification close it. + .on( 'click', '.mw-notification', function () { + var notif = $( this ).data( 'mw.notification' ); + if ( notif ) { + notif.close(); + } + } ) + // Stop click events from <a> tags from propogating to prevent clicking. + // on links from hiding a notification. + .on( 'click', 'a', function ( e ) { + e.stopPropagation(); + } ); + + mw.util.$content.prepend( $area ); + + // Read from the DOM: + // Must be in the next frame to avoid synchronous layout + // computation from offset()/getBoundingClientRect(). + rAF( function () { + offset = $area.offset(); + + // Initial mode (reads, and then maybe writes) + updateAreaMode(); + + // Once we have the offset for where it would normally render, set the + // initial state of the (currently empty) notification area to be hidden. + $area.css( 'display', 'none' ); + + $( window ).on( 'scroll', updateAreaMode ); + + // Handle pre-ready queue. + isPageReady = true; + while ( preReadyNotifQueue.length ) { + notif = preReadyNotifQueue.shift(); + notif.start(); + } + } ); + } + + /** + * @class mw.notification + * @singleton + */ + notification = { + /** + * Pause auto-hide timers for all notifications. + * Notifications will not auto-hide until resume is called. + * + * @see mw.Notification#pause + */ + pause: function () { + callEachNotification( + $area.children( '.mw-notification' ), + 'pause' + ); + }, + + /** + * Resume any paused auto-hide timers from the beginning. + * Only the first #autoHideLimit timers will be resumed. + */ + resume: function () { + callEachNotification( + // Only call resume on the first #autoHideLimit notifications. + // Exclude noautohide notifications to avoid bugs where #autoHideLimit + // `{ autoHide: false }` notifications are at the start preventing any + // auto-hide notifications from being autohidden. + $area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ), + 'resume' + ); + }, + + /** + * Display a notification message to the user. + * + * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message + * @param {Object} options The options to use for the notification. + * See #defaults for details. + * @return {mw.Notification} Notification object + */ + notify: function ( message, options ) { + var notif; + options = $.extend( {}, notification.defaults, options ); + + notif = new Notification( message, options ); + + if ( isPageReady ) { + notif.start(); + } else { + preReadyNotifQueue.push( notif ); + } + + return notif; + }, + + /** + * @property {Object} + * The defaults for #notify options parameter. + * + * - autoHide: + * A boolean indicating whether the notifification should automatically + * be hidden after shown. Or if it should persist. + * + * - autoHideSeconds: + * Key to #autoHideSeconds for number of seconds for timeout of auto-hide + * notifications. + * + * - tag: + * An optional string. When a notification is tagged only one message + * with that tag will be displayed. Trying to display a new notification + * with the same tag as one already being displayed will cause the other + * notification to be closed and this new notification to open up inside + * the same place as the previous notification. + * + * - title: + * An optional title for the notification. Will be displayed above the + * content. Usually in bold. + * + * - type: + * An optional string for the type of the message used for styling: + * Examples: 'info', 'warn', 'error'. + * + * - visibleTimeout: + * A boolean indicating if the autoHide timeout should be based on + * time the page was visible to user. Or if it should use wall clock time. + */ + defaults: { + autoHide: true, + autoHideSeconds: 'short', + tag: null, + title: null, + type: null, + visibleTimeout: true + }, + + /** + * @private + * @property {Object} + */ + autoHideSeconds: { + 'short': 5, + 'long': 30 + }, + + /** + * @property {number} + * Maximum number of simultaneous notifications to start auto-hide timers for. + * Only this number of notifications being displayed will be auto-hidden at one time. + * Any additional notifications in the list will only start counting their timeout for + * auto-hiding after the previous messages have been closed. + * + * This basically represents the minimal number of notifications the user should + * be able to process during the {@link #defaults default} #autoHideSeconds time. + */ + autoHideLimit: 3 + }; + + $( init ); + + mw.notification = notification; + +}( mediaWiki, jQuery ) ); |