( 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, Object.assign( { 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 () {
return this.model.getItems().some( ( item ) => !item.isRead() );
};
/**
* 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();
};
}() );