( function () { /** * Sub group list widget. * This widget contains a list of notifications from a single source * in a cross-wiki notifications group. * * @param {mw.echo.Controller} controller Notifications controller * @param {mw.echo.dm.SortedList} listModel Notifications list model for this source * @param {Object} config Configuration object * @param {boolean} [config.showTitle=false] Show the title of this group * @param {boolean} [config.showMarkAllRead=false] Show a mark all read button for this group * @param {boolean} [config.animateSorting=false] Animate the sorting of items * @param {jQuery} [config.$overlay] A jQuery element functioning as an overlay * for popups. */ mw.echo.ui.SubGroupListWidget = function MwEchoUiSubGroupListWidget( controller, listModel, config ) { this.$header = $( '
' ) .addClass( 'mw-echo-ui-subGroupListWidget-header' ); config = config || {}; this.controller = controller; this.model = listModel; // Parent constructor mw.echo.ui.SubGroupListWidget.super.call( this, $.extend( { data: this.getSource() }, config ) ); this.showTitle = !!config.showTitle; this.showMarkAllRead = !!config.showMarkAllRead; this.$overlay = config.$overlay || this.$element; this.listWidget = new mw.echo.ui.SortedListWidget( // Sorting callback config.sortingCallback || ( ( a, b ) => { // Reverse sorting if ( b.getTimestamp() < a.getTimestamp() ) { return -1; } else if ( b.getTimestamp() > a.getTimestamp() ) { return 1; } // Fallback on IDs return b.getId() - a.getId(); } ), // Config { $overlay: this.$overlay, animated: config.animateSorting } ); const sourceURL = this.model.getSourceURL() ? this.model.getSourceURL().replace( '$1', 'Special:Notifications' ) : null; if ( sourceURL ) { this.title = new OO.ui.ButtonWidget( { framed: false, classes: [ 'mw-echo-ui-subGroupListWidget-header-row-title', 'mw-echo-ui-subGroupListWidget-header-row-cell' ], href: sourceURL } ); } else { this.title = new OO.ui.LabelWidget( { classes: [ 'mw-echo-ui-subGroupListWidget-header-row-title', 'mw-echo-ui-subGroupListWidget-header-row-cell' ] } ); } if ( this.model.getTitle() ) { this.title.setLabel( this.model.getTitle() ); } this.title.toggle( this.showTitle ); // Mark all as read button this.markAllReadButton = new OO.ui.ButtonWidget( { framed: true, icon: 'checkAll', label: mw.msg( 'echo-specialpage-section-markread' ), classes: [ 'mw-echo-ui-subGroupListWidget-header-row-markAllReadButton', 'mw-echo-ui-subGroupListWidget-header-row-cell' ] } ); // Events this.model.connect( this, { // Cross-wiki items can be discarded when marked as read. // We need to differentiate this explicit action from the // action of 'remove' because 'remove' is also used when // an item is resorted by OO.SortedEmitterWidget before // it is re-added again discard: 'onModelDiscardItems', // Update all items update: 'resetItemsFromModel' } ); this.model.connect( this, { itemUpdate: 'toggleMarkAllReadButton' } ); this.markAllReadButton.connect( this, { click: 'onMarkAllReadButtonClick' } ); // Initialize this.toggleMarkAllReadButton(); this.$element .addClass( 'mw-echo-ui-subGroupListWidget' ) .append( this.$header.append( $( '
' ) .addClass( 'mw-echo-ui-subGroupListWidget-header-row' ) .append( this.title.$element, this.markAllReadButton.$element ) ), this.listWidget.$element ); // eslint-disable-next-line no-jquery/no-global-selector this.$pageContentText = $( '#mw-content-text' ); $( window ).on( 'resize', this.resizeHeader.bind( this ) ); // Resize the header after the stack finishes loading // so the widget is attached setTimeout( this.resizeHeader.bind( this ), 0 ); }; /* Initialization */ OO.inheritClass( mw.echo.ui.SubGroupListWidget, OO.ui.Widget ); /* Methods */ /** * Respond to window resize event */ mw.echo.ui.SubGroupListWidget.prototype.resizeHeader = function () { const contentWidth = this.$pageContentText.width(), screenTooNarrow = this.$header.width() > contentWidth; // Screen too narrow, put the button under the date this.title.$element.toggleClass( 'mw-echo-ui-subGroupListWidget-header-row-cell', !screenTooNarrow ); this.markAllReadButton.$element.toggleClass( 'mw-echo-ui-subGroupListWidget-header-row-cell', !screenTooNarrow ); }; /** * Destroy the widget and disconnect events */ mw.echo.ui.SubGroupListWidget.prototype.destroy = function () { $( window ).off( 'resize' ); }; /** * Toggle the visibility of the mark all read button for this group * based on whether there are unread notifications */ mw.echo.ui.SubGroupListWidget.prototype.toggleMarkAllReadButton = function () { this.markAllReadButton.toggle( this.showMarkAllRead && this.hasUnread() ); }; /** * Respond to 'mark all as read' button click */ mw.echo.ui.SubGroupListWidget.prototype.onMarkAllReadButtonClick = function () { this.controller.markEntireListModelRead( this.model.getName() ); }; /** * Check whether this sub group list has any unread notifications * * @return {boolean} Sub group has unread notifications */ mw.echo.ui.SubGroupListWidget.prototype.hasUnread = function () { const isUnread = function ( item ) { return !item.isRead(); }, items = this.model.getItems(); return items.some( isUnread ); }; /** * Reset the items and rebuild them according to the model. * * @param {mw.echo.dm.NotificationItem[]} [items] Item models that are added. * If this is empty, the widget will request all the items from the model. */ mw.echo.ui.SubGroupListWidget.prototype.resetItemsFromModel = function ( items ) { const itemWidgets = []; let $elements = $(); items = items || this.model.getItems(); for ( let i = 0; i < items.length; i++ ) { const widget = new mw.echo.ui.SingleNotificationItemWidget( this.controller, items[ i ], { $overlay: this.$overlay, bundle: items[ i ].isBundled() } ); itemWidgets.push( widget ); $elements = $elements.add( widget.$element ); } // Clear the current items if any exist this.getListWidget().clearItems(); // fire render hook mw.hook( 'ext.echo.notifications.beforeRender' ).fire( this.$element, $elements ); // Add the new items this.getListWidget().addItems( itemWidgets ); }; /** * Respond to model remove event. This may happen when an item * is marked as read. * * @param {mw.echo.dm.NotificationItem[]} items Notification item models */ mw.echo.ui.SubGroupListWidget.prototype.onModelDiscardItems = function ( items ) { const itemWidgets = []; for ( let i = 0; i < items.length; i++ ) { itemWidgets.push( this.listWidget.getItemFromId( items[ i ].getId() ) ); } this.listWidget.removeItems( itemWidgets ); }; /** * Get the associated list widget. This is useful to specifically * add and/or remove items from the list. * * @return {mw.echo.ui.SortedListWidget} List widget */ mw.echo.ui.SubGroupListWidget.prototype.getListWidget = function () { return this.listWidget; }; /** * Get the timestamp for the list * * @return {number} Timestamp */ mw.echo.ui.SubGroupListWidget.prototype.getTimestamp = function () { return this.model.getTimestamp(); }; /** * Toggle the visibility of the title * * @param {boolean} show Show the title */ mw.echo.ui.SubGroupListWidget.prototype.toggleTitle = function ( show ) { show = show !== undefined ? show : !this.showTitle; if ( this.showTitle !== show ) { this.showTitle = show; this.title.toggle( this.showTitle ); } }; /** * Get a the source of this list. * * @return {string} Group source */ mw.echo.ui.SubGroupListWidget.prototype.getSource = function () { return this.model.getSource(); }; /** * Get an array of IDs of all of the items in this group * * @return {number[]} Array of item IDs */ mw.echo.ui.SubGroupListWidget.prototype.getAllItemIDs = function () { return this.model.getAllItemIds(); }; /** * Get an array of IDs of all of the items in this group that * correspond to a specific type * * @param {string} type Item type * @return {number[]} Array of item IDs */ mw.echo.ui.SubGroupListWidget.prototype.getAllItemIDsByType = function ( type ) { return this.model.getAllItemIdsByType( type ); }; /** * Check whether this group is foreign * * @return {boolean} This group is foreign */ mw.echo.ui.SubGroupListWidget.prototype.isForeign = function () { return this.model.isForeign(); }; /** * Get the group id, which is represented by its model symbolic name. * This is meant for sorting callbacks that fallback on * sorting by IDs. * * @return {string} Group source */ mw.echo.ui.SubGroupListWidget.prototype.getId = function () { return this.model.getName(); }; }() );