Add a mark-all-read button and a settings menu to Special:Notifications

Add a global-wiki 'mark all as read' to the Special:Notifications page.
The 'mark all as read' will makr all notifications in the given
wiki. The context of the wiki changes when filters are chosen,
and so the message of the button changes as well.

Bug: T115528
Change-Id: Ibd9dcdf7072d6cbc1a268c18e558e6d0df28f929
This commit is contained in:
Moriel Schottlender 2016-06-29 16:36:03 -07:00
parent c1365be90d
commit 545d4e67ae
15 changed files with 429 additions and 50 deletions

View file

@ -129,6 +129,7 @@ $wgResourceModules += array(
'echo-badge-count',
'echo-overlay-link',
'echo-mark-all-as-read',
'echo-mark-wiki-as-read',
'echo-more-info',
'echo-feedback',
'echo-notification-alert',
@ -298,6 +299,7 @@ $wgResourceModules += array(
'ui/mw.echo.ui.PageFilterWidget.js',
'ui/mw.echo.ui.CrossWikiUnreadFilterWidget.js',
'ui/mw.echo.ui.NotificationsInboxWidget.js',
'ui/mw.echo.ui.SpecialHelpMenuWidget.js',
'special/ext.echo.special.js',
),
'styles' => array(
@ -308,6 +310,7 @@ $wgResourceModules += array(
'styles/mw.echo.ui.PageNotificationsOptionWidget.less',
'styles/mw.echo.ui.PageFilterWidget.less',
'styles/mw.echo.ui.CrossWikiUnreadFilterWidget.less',
'styles/mw.echo.ui.SpecialHelpMenuWidget.less',
),
'dependencies' => array(
'ext.echo.ui',
@ -323,7 +326,10 @@ $wgResourceModules += array(
'echo-specialpage-pagination-range',
'echo-specialpage-pagefilters-title',
'echo-specialpage-pagefilters-subtitle',
'echo-mark-all-as-read',
'echo-more-info',
'echo-learn-more',
'mypreferences',
'echo-feedback',
'echo-specialpage-section-markread',
),

View file

@ -219,6 +219,7 @@
"echo-overlay-title": "<b>Notifications</b>",
"echo-overlay-title-overflow": "<b>{{PLURAL:$1|Notification|Notifications}}</b> (showing $1 of $2 unread)",
"echo-mark-all-as-read": "Mark all as read",
"echo-mark-wiki-as-read": "Mark all as read in selected wiki: $1",
"echo-date-today": "Today",
"echo-date-yesterday": "Yesterday",
"echo-load-more-error": "An error occurred while fetching more results.",

View file

@ -210,6 +210,7 @@
"echo-overlay-title": "Title at the top of the notifications overlay. Should include bold tags.\n{{Identical|Notification}}",
"echo-overlay-title-overflow": "Title at the top of the notifications overlay when there are additional unread notifications that are not being shown.\n\nParameters:\n* $1 - the number of unread notifications being shown\n* $2 - the total number of unread notifications that exist",
"echo-mark-all-as-read": "Text for button that marks all unread notifications as read. Keep this short as possible.\n{{Identical|Mark all as read}}",
"echo-mark-wiki-as-read": "Text for button that marks all unread notifications as read in a specific wiki. Keep this short as possible.\n{{Identical|Mark all as read}}\n\nParameters:\n* $1 - The human readable name of the selected wiki",
"echo-date-today": "The header text for today's notification section.\n{{Identical|Today}}",
"echo-date-yesterday": "The header text for yesterday's notification section.\n{{Identical|Yesterday}}",
"echo-load-more-error": "Error message for errors in loading more notifications",

View file

@ -249,12 +249,6 @@
* for that type in the given source
*/
mw.echo.api.EchoApi.prototype.markAllRead = function ( source, type ) {
// FIXME: This specific method sends an operation
// to the API that marks all notifications of the given type as read regardless
// of whether they were actually seen by the user.
// We should consider removing the use of this method and, instead,
// using strictly the 'markItemsRead' by giving the API only the
// notifications that are available to the user.
return this.network.getApiHandler( source ).markAllRead( type );
};
@ -264,11 +258,13 @@
*
* @param {string} source Notifications source
* @param {string} type Notification type
* @param {boolean} [localOnly] Fetches only the count of local notifications,
* and ignores cross-wiki notifications.
* @return {jQuery.Promise} A promise that is resolved with the number of
* unread notifications for the given type and source.
*/
mw.echo.api.EchoApi.prototype.fetchUnreadCount = function ( source, type ) {
return this.network.getApiHandler( source ).fetchUnreadCount( type );
mw.echo.api.EchoApi.prototype.fetchUnreadCount = function ( source, type, localOnly ) {
return this.network.getApiHandler( source ).fetchUnreadCount( type, localOnly );
};
/**

View file

@ -55,12 +55,12 @@
* @inheritdoc
*/
mw.echo.api.LocalAPIHandler.prototype.markAllRead = function ( type ) {
var data = {
action: 'echomarkread',
sections: this.normalizedType[ type ]
};
type = Array.isArray( type ) ? type : [ type ];
return this.api.postWithToken( 'csrf', data )
return this.api.postWithToken( 'csrf', {
action: 'echomarkread',
sections: type.join( '|' )
} )
.then( function ( result ) {
return OO.getProp( result.query, 'echomarkread', type, 'rawcount' ) || 0;
} );
@ -84,9 +84,13 @@
};
/**
* @inheritdoc
* Fetch the number of unread notifications.
*
* @param {string} type Notification type, 'alert', 'message' or 'all'
* @param {boolean} [ignoreCrossWiki] Ignore cross-wiki notifications when fetching the count.
* If set to false (by default) it counts notifications across all wikis.
*/
mw.echo.api.LocalAPIHandler.prototype.fetchUnreadCount = function ( type ) {
mw.echo.api.LocalAPIHandler.prototype.fetchUnreadCount = function ( type, ignoreCrossWiki ) {
var normalizedType = this.normalizedType[ type ],
apiData = {
action: 'query',
@ -96,10 +100,13 @@
notmessageunreadfirst: 1,
notlimit: this.limit,
notprop: 'count',
notcrosswikisummary: 1,
uselang: this.userLang
};
if ( !ignoreCrossWiki ) {
apiData.notcrosswikisummary = 1;
}
return this.api.get( apiData )
.then( function ( result ) {
if ( type === 'message' || type === 'alert' ) {

View file

@ -33,6 +33,7 @@
filtersModel.setReadState( values[ 0 ] );
} else if ( filter === 'sourcePage' ) {
filtersModel.setCurrentSourcePage( values[ 0 ], values[ 1 ] );
this.manager.getLocalCounter().setSource( filtersModel.getSourcePagesModel().getCurrentSource() );
}
// Reset pagination
@ -191,6 +192,9 @@
// Update the pagination
pagination.setNextPageContinue( data.continue );
// Update the local counter
controller.manager.getLocalCounter().update();
return dateItemIds;
} );
};
@ -374,6 +378,49 @@
return this.markItemsRead( itemIds, model.getName(), isRead );
};
/**
* Mark all notifications of a certain source as read, even those that
* are not currently displayed.
*
* @param {string} [source] Notification source. If not given, the currently
* selected source is used.
* @return {jQuery.Promise} A promise that is resolved after
* all notifications for the given source were marked as read
*/
mw.echo.Controller.prototype.markAllRead = function ( source ) {
var model,
controller = this,
itemIds = [],
readState = this.manager.getFiltersModel().getReadState(),
localCounter = this.manager.getLocalCounter();
source = source || this.manager.getFiltersModel().getSourcePagesModel().getCurrentSource();
this.manager.getNotificationsBySource( source ).forEach( function ( notification ) {
if ( !notification.isRead() ) {
itemIds = itemIds.concat( notification.getAllIds() );
notification.toggleRead( true );
if ( readState === 'unread' ) {
// Remove the items if we are in 'unread' filter state
model = controller.manager.getNotificationModel( notification.getModelName() );
model.discardItems( notification );
}
}
} );
// Update pagination count
this.manager.updateCurrentPageItemCount();
localCounter.estimateChange( -itemIds.length );
return this.api.markAllRead(
source,
this.getTypes()
)
.then( this.refreshUnreadCount.bind( this ) )
.then( localCounter.update.bind( localCounter, true ) );
};
/**
* Mark all local notifications as read
*
@ -381,16 +428,35 @@
* local notifications have been marked as read.
*/
mw.echo.Controller.prototype.markLocalNotificationsRead = function () {
var itemIds = [];
var modelName, model,
itemIds = [],
readState = this.manager.getFiltersModel().getReadState(),
modelItems = {};
this.manager.getLocalNotifications().forEach( function ( notification ) {
if ( !notification.isRead() ) {
itemIds = itemIds.concat( notification.getAllIds() );
notification.toggleRead( true );
modelName = notification.getModelName();
modelItems[ modelName ] = modelItems[ modelName ] || [];
modelItems[ modelName ].push( notification );
}
} );
// Remove the items if we are in 'unread' filter state
if ( readState === 'unread' ) {
for ( modelName in modelItems ) {
model = this.manager.getNotificationModel( modelName );
model.discardItems( modelItems[ modelName ] );
}
}
// Update pagination count
this.manager.updateCurrentPageItemCount();
this.manager.getUnreadCounter().estimateChange( -itemIds.length );
this.manager.getLocalCounter().estimateChange( -itemIds.length );
return this.api.markItemsRead( itemIds, 'local', true ).then( this.refreshUnreadCount.bind( this ) );
};
@ -498,6 +564,11 @@
this.manager.updateCurrentPageItemCount();
this.manager.getUnreadCounter().estimateChange( isRead ? -allIds.length : allIds.length );
if ( modelName !== 'xwiki' ) {
// For the local counter, we should only estimate the change if the items
// are not cross-wiki
this.manager.getLocalCounter().estimateChange( isRead ? -allIds.length : allIds.length );
}
return this.api.markItemsRead( allIds, model.getSource(), isRead ).then( this.refreshUnreadCount.bind( this ) );
};
@ -526,6 +597,8 @@
sourceModel = xwikiModel.getList().getGroupByName( source );
notifs = sourceModel.findByIds( itemIds );
sourceModel.discardItems( notifs );
// Update pagination count
this.manager.updateCurrentPageItemCount();
return this.api.markItemsRead( itemIds, source, true )
.then( this.refreshUnreadCount.bind( this ) );

View file

@ -117,6 +117,20 @@
return false;
};
/**
* Get all items in the cross wiki notification bundle
*
* @return {mw.echo.dm.NotificationItem[]} All items across all sources
*/
mw.echo.dm.CrossWikiNotificationItem.prototype.getItems = function () {
var notifications = [];
this.list.getItems().forEach( function ( sourceList ) {
notifications = notifications.concat( sourceList.getItems() );
} );
return notifications;
};
/**
* This item is a group.
* This method is required for all models that are managed by the

View file

@ -50,6 +50,9 @@
// Events
this.seenTimeModel.connect( this, { update: 'onSeenTimeUpdate' } );
this.localCounter = config.localCounter || new mw.echo.dm.UnreadNotificationCounter();
this.localCounter.connect( this, { countChange: [ 'emit', 'localCountChange' ] } );
};
OO.initClass( mw.echo.dm.ModelManager );
@ -93,6 +96,12 @@
* A specific item inside a notifications model has been updated
*/
/**
* @event localCountChange
*
* There was a change in the count of local unread notifications
*/
/* Methods */
/**
@ -161,14 +170,17 @@
this.emit( 'update', this.getAllNotificationModels() );
};
mw.echo.dm.ModelManager.prototype.onModelItemUpdate = function ( modelId, item ) {
var model = this.getNotificationModel( modelId );
/**
* Respond to model update event
*
* @param {string} modelName Model name
* @param {mw.echo.dm.notificationItem} item Notification item
* @fires modelUpdate
*/
mw.echo.dm.ModelManager.prototype.onModelItemUpdate = function ( modelName, item ) {
this.checkLocalUnreadTalk();
if ( model.getSource() === 'local' ) {
this.checkLocalUnreadTalk();
}
this.emit( 'modelItemUpdate', modelId, item );
this.emit( 'modelItemUpdate', modelName, item );
};
/**
@ -195,6 +207,7 @@
return count;
};
/**
* Get a notification model.
*
@ -381,6 +394,15 @@
);
};
/**
* Get the local counter
*
* @return {mw.echo.dm.UnreadNotificationCounter} Local counter
*/
mw.echo.dm.ModelManager.prototype.getLocalCounter = function () {
return this.localCounter;
};
/**
* Get all local notifications
*
@ -438,6 +460,7 @@
*/
mw.echo.dm.ModelManager.prototype.getSeenTimeModel = function () {
return this.seenTimeModel;
};
} )( mediaWiki, jQuery );

View file

@ -1,4 +1,4 @@
( function ( mw ) {
( function ( mw, $ ) {
/**
* Echo notification UnreadNotificationCounter model
*
@ -9,17 +9,26 @@
* @param {Object} api An instance of EchoAPI.
* @param {string} type The notification type 'message', 'alert', or 'all'.
* @param {number} max Maximum number supported. Above this number there is no precision, we only know it is 'more than max'.
* @param {Object} config Configuration object
* @cfg {boolean} [localOnly=false] The update only takes into account
* local notifications and ignores the number of cross-wiki notifications.
* @cfg {string} [source='local'] The source for this counter. Specifically important if the counter
* is set to be counting only local notifications
*/
mw.echo.dm.UnreadNotificationCounter = function mwEchoDmUnreadNotificationCounter( api, type, max ) {
mw.echo.dm.UnreadNotificationCounter = function mwEchoDmUnreadNotificationCounter( api, type, max, config ) {
config = config || {};
// Mixin constructor
OO.EventEmitter.call( this );
this.api = api;
this.type = type;
this.max = max;
this.prioritizer = new mw.echo.api.PromisePrioritizer();
this.count = 0;
this.source = 'local';
this.localOnly = config.localOnly === undefined ? false : !!config.localOnly;
this.source = config.source || 'local';
};
/* Inheritance */
@ -81,12 +90,35 @@
/**
* Request that this counter update itself from the API
*
* @return {jQuery.Promise} Promise that is resolved when the actual unread
* count is fetched, with the actual unread notification count.
*/
mw.echo.dm.UnreadNotificationCounter.prototype.update = function () {
var model = this;
this.api.fetchUnreadCount( this.source, this.type ).then( function ( actualCount ) {
if ( !this.api ) {
return $.Deferred().reject();
}
return this.prioritizer.prioritize( this.api.fetchUnreadCount(
this.source,
this.type,
this.localOnly
) ).then( function ( actualCount ) {
model.setCount( actualCount, false );
return actualCount;
} );
};
}( mediaWiki ) );
/**
* Set the source for this counter
*
* @param {string} source Source name
*/
mw.echo.dm.UnreadNotificationCounter.prototype.setSource = function ( source ) {
this.source = source;
};
}( mediaWiki, jQuery ) );

View file

@ -11,6 +11,10 @@
}
}
.client-js #mw-indicator-mw-helplink {
display: none;
}
/* Custom header styling for Vector and Monobook skins */
.mw-special-Notifications.skin-vector #firstHeading,
.mw-special-Notifications.skin-monobook #firstHeading {

View file

@ -4,30 +4,47 @@
* Echo Special:Notifications page initialization
*/
$( document ).ready( function () {
var limitNotifications = 50,
var prefLink, specialPageContainer,
limitNotifications = 50,
$content = $( '#mw-content-text' ),
echoApi = new mw.echo.api.EchoApi( { limit: limitNotifications, bundle: false } ),
unreadCounter = new mw.echo.dm.UnreadNotificationCounter( echoApi, [ 'message', 'alert' ], limitNotifications ),
modelManager = new mw.echo.dm.ModelManager( unreadCounter, {
type: [ 'message', 'alert' ],
itemsPerPage: limitNotifications
itemsPerPage: limitNotifications,
localCounter: new mw.echo.dm.UnreadNotificationCounter(
echoApi,
[ 'message', 'alert' ],
limitNotifications,
{
localOnly: true,
source: 'local'
}
)
} ),
controller = new mw.echo.Controller(
echoApi,
modelManager
),
specialPageContainer = new mw.echo.ui.NotificationsInboxWidget(
controller,
modelManager,
{
limit: limitNotifications,
$overlay: mw.echo.ui.$overlay
}
);
prefLink = new mw.Uri( $( '#pt-preferences a' ).prop( 'href' ) );
prefLink.fragment = 'mw-prefsection-echo';
specialPageContainer = new mw.echo.ui.NotificationsInboxWidget(
controller,
modelManager,
{
limit: limitNotifications,
$overlay: mw.echo.ui.$overlay,
prefLink: prefLink.toString(),
helpLink: $( '#mw-indicator-mw-helplink a' ).prop( 'href' )
}
);
// Overlay
$( 'body' ).append( mw.echo.ui.$overlay );
// Notifications
$content.empty().append( specialPageContainer.$element );
} );
} )( jQuery, mediaWiki );

View file

@ -39,6 +39,10 @@
margin-right: auto;
margin-top: 3 * @specialpage-separation-unit;
}
&-settings {
padding-left: 1em;
}
}
}

View file

@ -0,0 +1,41 @@
@import '../echo.variables';
.mw-echo-ui-specialHelpMenuWidget {
.oo-ui-popupWidget-body {
// Override clippable inline rules
overflow-y: hidden !important;
overflow-x: hidden !important;
}
&-markAllRead-label {
&-title {
display: block;
}
&-subtitle {
display: block;
color: @grey-light;
margin-top: 0.2em;
}
}
&-menu {
.oo-ui-buttonWidget {
display: block;
padding: 0.5em;
margin-right: 0;
.oo-ui-labelElement-label {
white-space: normal;
// Width of the popup (300px) minus the
// width of the icon (1.875em) minus the
// padding of the button (0.5em * 2)
width: ~'calc(300px - 1.875em - 1em)';
}
&:hover {
background-color: #DDDDDD;
}
}
}
}

View file

@ -58,6 +58,16 @@
}
);
// Settings menu
this.settingsMenu = new mw.echo.ui.SpecialHelpMenuWidget(
this.manager,
{
framed: true,
helpLink: config.helpLink,
prefLink: config.prefLink
}
);
// Filter by read state
this.readStateSelectWidget = new mw.echo.ui.ReadStateButtonSelectWidget();
@ -70,11 +80,15 @@
// Events
this.readStateSelectWidget.connect( this, { filter: 'onReadStateFilter' } );
this.xwikiUnreadWidget.connect( this, { filter: 'onSourcePageFilter' } );
this.manager.connect( this, { modelItemUpdate: 'onManagerModelItemUpdate' } );
this.manager.connect( this, {
modelItemUpdate: 'updatePaginationLabels',
localCountChange: 'updatePaginationLabels'
} );
this.manager.getFiltersModel().connect( this, { update: 'updateReadStateSelectWidget' } );
this.manager.getPaginationModel().connect( this, { update: 'onPaginationModelUpdate' } );
this.manager.getPaginationModel().connect( this, { update: 'updatePaginationLabels' } );
this.topPaginationWidget.connect( this, { change: 'populateNotifications' } );
this.bottomPaginationWidget.connect( this, { change: 'populateNotifications' } );
this.settingsMenu.connect( this, { markAllRead: 'onSettingsMarkAllRead' } );
this.topPaginationWidget.setDisabled( true );
this.bottomPaginationWidget.setDisabled( true );
@ -102,7 +116,11 @@
$( '<div>' )
.addClass( 'mw-echo-ui-notificationsInboxWidget-main-toolbar-pagination' )
.addClass( 'mw-echo-ui-notificationsInboxWidget-cell' )
.append( this.topPaginationWidget.$element )
.append( this.topPaginationWidget.$element ),
$( '<div>' )
.addClass( 'mw-echo-ui-notificationsInboxWidget-main-toolbar-settings' )
.addClass( 'mw-echo-ui-notificationsInboxWidget-cell' )
.append( this.settingsMenu.$element )
)
),
this.noticeMessageWidget.$element,
@ -157,21 +175,24 @@
};
/**
* Respond to pagination model update event
* Update pagination messages
*/
mw.echo.ui.NotificationsInboxWidget.prototype.onPaginationModelUpdate = function () {
mw.echo.ui.NotificationsInboxWidget.prototype.updatePaginationLabels = function () {
this.resetMessageLabel();
};
/**
* Respond to a change in model item
*/
mw.echo.ui.NotificationsInboxWidget.prototype.onManagerModelItemUpdate = function () {
// Update the pagination label
this.topPaginationWidget.updateLabel();
this.bottomPaginationWidget.updateLabel();
};
/**
* Respond to mark all read for selected wiki
*/
mw.echo.ui.NotificationsInboxWidget.prototype.onSettingsMarkAllRead = function () {
this.pushPending();
this.controller.markAllRead()
.always( this.popPending.bind( this ) );
};
/**
* Respond to unread page filter
*

View file

@ -0,0 +1,139 @@
( function ( $, mw ) {
/**
* Widget for the settings menu in the Special:Notifications page
*
* @param {mw.echo.dm.ModelManager} manager Model manager
* @param {Object} config Configuration object
*/
mw.echo.ui.SpecialHelpMenuWidget = function MwEchoUiSpecialHelpMenuWidget( manager, config ) {
var $menu = $( '<div>' )
.addClass( 'mw-echo-ui-specialHelpMenuWidget-menu' );
config = config || {};
// Parent constructor
mw.echo.ui.SpecialHelpMenuWidget.parent.call( this, $.extend( {
icon: 'advanced',
popup: {
$content: $menu,
width: 300
}
}, config ) );
// Mixin constructors
OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: $menu } ) );
OO.ui.mixin.PendingElement.call( this, config );
this.manager = manager;
this.markAllReadButton = new OO.ui.ButtonWidget( {
framed: false,
icon: 'doubleCheck',
label: this.getMarkAllReadButtonLabel()
} );
this.setPendingElement( this.$element );
this.markAllReadButton.toggle( false );
this.addItems( [
this.markAllReadButton,
// Preferences link
new OO.ui.ButtonWidget( {
framed: false,
icon: 'advanced',
label: mw.msg( 'mypreferences' ),
href: config.prefLink
} ),
// Help link
new OO.ui.ButtonWidget( {
framed: false,
icon: 'help',
label: mw.msg( 'echo-learn-more' ),
href: config.helpLink
} )
] );
// Events
this.markAllReadButton.connect( this, { click: 'onMarkAllreadButtonClick' } );
this.manager.connect( this, {
localCountChange: 'onLocalCountChange'
} );
this.manager.getFiltersModel().getSourcePagesModel().connect( this, { update: 'onSourcePageUpdate' } );
this.$element
.addClass( 'mw-echo-ui-specialHelpMenuWidget' );
};
/* Initialization */
OO.inheritClass( mw.echo.ui.SpecialHelpMenuWidget, OO.ui.PopupButtonWidget );
OO.mixinClass( mw.echo.ui.SpecialHelpMenuWidget, OO.ui.mixin.GroupElement );
OO.mixinClass( mw.echo.ui.SpecialHelpMenuWidget, OO.ui.mixin.PendingElement );
/* Events */
/**
* @event markAllRead
*
* Mark all notifications as read in the selected wiki
*/
/* Methods */
/**
* Respond to source page change
*/
mw.echo.ui.SpecialHelpMenuWidget.prototype.onSourcePageUpdate = function () {
this.markAllReadButton.setLabel( this.getMarkAllReadButtonLabel() );
};
/**
* Respond to local counter update event
*/
mw.echo.ui.SpecialHelpMenuWidget.prototype.onLocalCountChange = function ( count ) {
this.markAllReadButton.toggle( count > 0 );
};
/**
* Respond to mark all read button click
*/
mw.echo.ui.SpecialHelpMenuWidget.prototype.onMarkAllreadButtonClick = function () {
this.popup.toggle( false );
this.emit( 'markAllRead' );
};
/**
* Build the button label
*
* @return {string} Mark all read button label
*/
mw.echo.ui.SpecialHelpMenuWidget.prototype.getMarkAllReadButtonLabel = function () {
var pageModel = this.manager.getFiltersModel().getSourcePagesModel(),
source = pageModel.getCurrentSource(),
sourceTitle = pageModel.getSourceTitle( source );
return sourceTitle ?
mw.msg( 'echo-mark-wiki-as-read', sourceTitle ) :
mw.msg( 'echo-mark-all-as-read' );
};
/**
* Extend the pushPending method to disable the mark all read button
*/
mw.echo.ui.SpecialHelpMenuWidget.prototype.pushPending = function () {
this.markAllReadButton.setDisabled( true );
// Mixin method
OO.ui.mixin.PendingElement.prototype.pushPending.call( this );
};
/**
* Extend the popPending method to enable the mark all read button
*/
mw.echo.ui.SpecialHelpMenuWidget.prototype.popPending = function () {
this.markAllReadButton.setDisabled( false );
// Mixin method
OO.ui.mixin.PendingElement.prototype.popPending.call( this );
};
} )( jQuery, mediaWiki );