Selecting/unselecting a filter now refreshes the results list using AJAX.
Also added pushState to update the URL, and popstate handling
to make the back button work.
Bug: T153949
Change-Id: I8c1ec557ccfe4b1d20aaaab3ef0d3182a1993f24
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js',
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js',
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js',
+ 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterGroupWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
],
'rcfilters-filter-categorization-description',
'rcfilters-filter-logactions-label',
'rcfilters-filter-logactions-description',
+ 'recentchanges-noresult',
],
'dependencies' => [
'oojs-ui',
--- /dev/null
+( function ( mw ) {
+ /**
+ * View model for the changes list
+ *
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ */
+ mw.rcfilters.dm.ChangesListViewModel = function MwRcfiltersDmChangesListViewModel() {
+ // Mixin constructor
+ OO.EventEmitter.call( this );
+
+ this.valid = true;
+ };
+
+ /* Initialization */
+ OO.initClass( mw.rcfilters.dm.ChangesListViewModel );
+ OO.mixinClass( mw.rcfilters.dm.ChangesListViewModel, OO.EventEmitter );
+
+ /* Events */
+
+ /**
+ * @event invalidate
+ *
+ * The list of changes is now invalid (out of date)
+ */
+
+ /**
+ * @event update
+ * @param {jQuery|string} changesListContent
+ *
+ * The list of change is now up to date
+ */
+
+ /* Methods */
+
+ /**
+ * Invalidate the list of changes
+ *
+ * @fires invalidate
+ */
+ mw.rcfilters.dm.ChangesListViewModel.prototype.invalidate = function () {
+ if ( this.valid ) {
+ this.valid = false;
+ this.emit( 'invalidate' );
+ }
+ };
+
+ /**
+ * Update the model with an updated list of changes
+ *
+ * @param {jQuery|string} changesListContent
+ */
+ mw.rcfilters.dm.ChangesListViewModel.prototype.update = function ( changesListContent ) {
+ this.valid = true;
+ this.emit( 'update', changesListContent );
+ };
+
+}( mediaWiki ) );
-( function ( mw ) {
+( function ( mw, $ ) {
/**
* Controller for the filters in Recent Changes
*
- * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
*/
- mw.rcfilters.Controller = function MwRcfiltersController( model ) {
- this.model = model;
- // TODO: When we are ready, update the URL when a filter is updated
- // this.model.connect( this, { itemUpdate: 'updateURL' } );
+ mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel ) {
+ this.filtersModel = filtersModel;
+ this.changesListModel = changesListModel;
+ this.requestCounter = 0;
};
/* Initialization */
* Initialize the filter and parameter states
*/
mw.rcfilters.Controller.prototype.initialize = function () {
+ this.updateFromURL();
+ };
+
+ /**
+ * Update the model state based on the URL parameters.
+ */
+ mw.rcfilters.Controller.prototype.updateFromURL = function () {
var uri = new mw.Uri();
- // Give the model a full parameter state from which to
- // update the filters
- this.model.updateFilters(
+ this.filtersModel.updateFilters(
// Translate the url params to filter select states
- this.model.getFiltersFromParameters( uri.query )
+ this.filtersModel.getFiltersFromParameters( uri.query )
);
};
* Reset to default filters
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
- this.model.setFiltersToDefaults();
+ this.filtersModel.setFiltersToDefaults();
+ this.updateURL();
+ this.updateChangesList();
};
/**
* Empty all selected filters
*/
mw.rcfilters.Controller.prototype.emptyFilters = function () {
- this.model.emptyAllFilters();
+ this.filtersModel.emptyAllFilters();
+ this.updateURL();
+ this.updateChangesList();
};
/**
var obj = {};
obj[ filterName ] = isSelected;
- this.model.updateFilters( obj );
+ this.filtersModel.updateFilters( obj );
+ this.updateURL();
+ this.updateChangesList();
};
/**
// TODO: Clean up the list of filters; perhaps 'falsy' filters
// shouldn't appear at all? Or compare to existing query string
// and see if current state of a specific filter is needed?
- uri.extend( this.model.getParametersFromFilters() );
+ uri.extend( this.filtersModel.getParametersFromFilters() );
// Update the URL itself
window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
};
-}( mediaWiki ) );
+
+ /**
+ * Fetch the list of changes from the server for the current filters
+ *
+ * @returns {jQuery.Promise} Promise object that will resolve with the changes list
+ */
+ mw.rcfilters.Controller.prototype.fetchChangesList = function () {
+ var uri = new mw.Uri(),
+ requestId = ++this.requestCounter,
+ latestRequest = function () {
+ return requestId === this.requestCounter;
+ }.bind( this );
+ uri.extend( this.filtersModel.getParametersFromFilters() );
+ return $.ajax( uri.toString(), { contentType: 'html' } )
+ .then( function ( html ) {
+ return latestRequest() ?
+ $( $.parseHTML( html ) ).find( '.mw-changeslist' ).first().contents() :
+ null;
+ } ).then( null, function () {
+ return latestRequest() ? 'NO_RESULTS' : null;
+ } );
+ };
+
+ /**
+ * Update the list of changes and notify the model
+ */
+ mw.rcfilters.Controller.prototype.updateChangesList = function () {
+ this.changesListModel.invalidate();
+ this.fetchChangesList()
+ .always( function ( changesListContent ) {
+ if ( changesListContent ) {
+ this.changesListModel.update( changesListContent );
+ }
+ }.bind( this ) );
+ };
+}( mediaWiki, jQuery ) );
var rcfilters = {
/** */
init: function () {
- var model = new mw.rcfilters.dm.FiltersViewModel(),
- controller = new mw.rcfilters.Controller( model ),
+ var filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+ changesListModel = new mw.rcfilters.dm.ChangesListViewModel(),
+ controller = new mw.rcfilters.Controller( filtersModel, changesListModel ),
$overlay = $( '<div>' )
.addClass( 'mw-rcfilters-ui-overlay' ),
- widget = new mw.rcfilters.ui.FilterWrapperWidget( controller, model, { $overlay: $overlay } );
+ filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
+ controller, filtersModel, { $overlay: $overlay } );
- model.initializeFilters( {
+ // eslint-disable-next-line no-new
+ new mw.rcfilters.ui.ChangesListWrapperWidget(
+ changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
+
+ // eslint-disable-next-line no-new
+ new mw.rcfilters.ui.FormWrapperWidget(
+ changesListModel, $( '.rcoptions form' ) );
+
+ filtersModel.initializeFilters( {
registration: {
title: mw.msg( 'rcfilters-filtergroup-registration' ),
type: 'send_unselected_if_any',
}
} );
- $( '.rcoptions' ).before( widget.$element );
+ $( '.rcoptions' ).before( filtersWidget.$element );
$( 'body' ).append( $overlay );
// Initialize values
name = 'hidemyself';
}
// This span corresponds to a filter that's in our model, so remove it
- if ( model.getItemByName( name ) ) {
+ if ( filtersModel.getItemByName( name ) ) {
// HACK: Remove the text node after the span.
// If there isn't one, we're at the end, so remove the text node before the span.
// This would be unnecessary if we added separators with CSS.
}
} );
- $( '.rcoptions form' ).submit( function () {
- var $form = $( this );
-
- // Get current filter values
- $.each( model.getParametersFromFilters(), function ( paramName, paramValue ) {
- var $existingInput = $form.find( 'input[name=' + paramName + ']' );
- // Check if the hidden input already exists
- // This happens if the parameter was already given
- // on load
- if ( $existingInput.length ) {
- // Update the value
- $existingInput.val( paramValue );
- } else {
- // Append hidden fields with filter values
- $form.append(
- $( '<input>' )
- .attr( 'type', 'hidden' )
- .attr( 'name', paramName )
- .val( paramValue )
- );
- }
- } );
-
- // Continue the submission process
- return true;
+ window.addEventListener( 'popstate', function () {
+ controller.updateFromURL();
+ controller.updateChangesList();
} );
}
};
* @mixins OO.ui.mixin.PopupElement
*
* @constructor
+ * @param {mw.rcfilters.Controller} controller
* @param {mw.rcfilters.dm.FilterItem} model Item model
* @param {Object} config Configuration object
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
- mw.rcfilters.ui.CapsuleItemWidget = function MwRcfiltersUiCapsuleItemWidget( model, config ) {
+ mw.rcfilters.ui.CapsuleItemWidget = function MwRcfiltersUiCapsuleItemWidget( controller, model, config ) {
var $popupContent = $( '<div>' )
.addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup' ),
descLabelWidget = new OO.ui.LabelWidget();
// Configuration initialization
config = config || {};
+ this.controller = controller;
this.model = model;
this.$overlay = config.$overlay || this.$element;
this.positioned = false;
// Events
this.model.connect( this, { update: 'onModelUpdate' } );
+ this.closeButton.connect( this, { click: 'onCapsuleRemovedByUser' } );
+
// Initialization
this.$overlay.append( this.popup.$element );
this.$element
}
}
};
+
+ /**
+ * Respond to the user removing the capsule with the close button
+ */
+ mw.rcfilters.ui.CapsuleItemWidget.prototype.onCapsuleRemovedByUser = function () {
+ this.controller.updateFilter( this.model.getName(), false );
+ };
}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * List of changes
+ *
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.PendingElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.ChangesListViewModel} model View model
+ * @param {jQuery} $changesListRoot Root element of the changes list to attach to
+ * @param {Object} config Configuration object
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget( model, $changesListRoot, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, $.extend( {}, config, {
+ $element: $changesListRoot
+ } ) );
+ // Mixin constructors
+ OO.ui.mixin.PendingElement.call( this, config );
+
+ this.model = model;
+
+ // Events
+ this.model.connect( this, {
+ invalidate: 'onModelInvalidate',
+ update: 'onModelUpdate'
+ } );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.Widget );
+ OO.mixinClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.mixin.PendingElement );
+
+ /**
+ * Respond to model invalidate
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
+ this.pushPending();
+ };
+
+ /**
+ * Respond to model update
+ *
+ * @param {jQuery|string} changesListContent The content of the updated changes list
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function ( changesListContent ) {
+ var isEmpty = changesListContent === 'NO_RESULTS';
+ this.$element.toggleClass( 'mw-changeslist', !isEmpty );
+ this.$element.toggleClass( 'mw-changeslist-empty', isEmpty );
+ this.$element.empty().append(
+ isEmpty ?
+ document.createTextNode( mw.message( 'recentchanges-noresult' ).text() ) :
+ changesListContent
+ );
+ this.popPending();
+ };
+}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * A widget representing a single toggle filter
+ *
+ * @extends OO.ui.CheckboxInputWidget
+ *
+ * @constructor
+ * @param {Object} config Configuration object
+ */
+ mw.rcfilters.ui.CheckboxInputWidget = function MwRcfiltersUiCheckboxInputWidget( config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.CheckboxInputWidget.parent.call( this, config );
+
+ // Event
+ this.$input.on( 'change', this.onUserChange.bind( this ) );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.CheckboxInputWidget, OO.ui.CheckboxInputWidget );
+
+ /* Events */
+
+ /**
+ * @event userChange
+ * @param {boolean} Current state of the checkbox
+ *
+ * The user has checked or unchecked this checkbox
+ */
+
+ /* Methods */
+
+ /**
+ * Respond to checkbox change by a user and emit 'userChange'.
+ */
+ mw.rcfilters.ui.CheckboxInputWidget.prototype.onUserChange = function () {
+ this.emit( 'userChange', this.$input.prop( 'checked' ) );
+ };
+}( mediaWiki ) );
return;
}
- return new mw.rcfilters.ui.CapsuleItemWidget( item, { $overlay: this.$overlay } );
+ return new mw.rcfilters.ui.CapsuleItemWidget(
+ this.controller,
+ item,
+ { $overlay: this.$overlay }
+ );
};
/**
this.focus();
};
- /**
- * @inheritdoc
- */
- mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItems = function ( items ) {
- var filterData = {};
-
- // Parent
- mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.prototype.removeItems.call( this, items );
-
- items.forEach( function ( itemWidget ) {
- filterData[ itemWidget.getData() ] = false;
- } );
-
- // Update the model
- this.model.updateFilters( filterData );
- };
-
/**
* @inheritdoc
*/
this.controller = controller;
this.model = model;
- this.checkboxWidget = new OO.ui.CheckboxInputWidget( {
+ this.checkboxWidget = new mw.rcfilters.ui.CheckboxInputWidget( {
value: this.model.getName(),
selected: this.model.isSelected()
} );
} );
// Event
- this.checkboxWidget.connect( this, { change: 'onCheckboxChange' } );
+ this.checkboxWidget.connect( this, { userChange: 'onCheckboxChange' } );
this.model.connect( this, { update: 'onModelUpdate' } );
this.$element
* @constructor
* @param {mw.rcfilters.Controller} controller Controller
* @param {mw.rcfilters.dm.FiltersViewModel} model View model
- * @param {Object} config Configuration object
+ * @param {Object} [config] Configuration object
* @cfg {Object} [filters] A definition of the filter groups in this list
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
this.model = model;
this.$overlay = config.$overlay || this.$element;
- this.filtersInCapsule = [];
-
this.filterPopup = new mw.rcfilters.ui.FiltersListWidget(
this.controller,
this.model,
}
} );
};
-
- /**
- * Add a capsule item by its filter name
- *
- * @param {string} itemName Filter name
- */
- mw.rcfilters.ui.FilterWrapperWidget.prototype.addCapsuleItemFromName = function ( itemName ) {
- this.capsule.addItemByName( [ itemName ] );
- };
}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Wrapper for the RC form with hide/show links
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
+ * @param {jQuery} $formRoot Root element of the form to attach to
+ * @param {Object} config Configuration object
+ */
+ mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( model, $formRoot, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.FormWrapperWidget.parent.call( this, $.extend( {}, config, {
+ $element: $formRoot
+ } ) );
+
+ this.model = model;
+ this.$submitButton = this.$element.find( 'input[type=submit]' );
+
+ // Events
+ this.model.connect( this, {
+ invalidate: 'onModelInvalidate',
+ update: 'onModelUpdate'
+ } );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.Widget );
+
+ /**
+ * Respond to model invalidate
+ */
+ mw.rcfilters.ui.FormWrapperWidget.prototype.onModelInvalidate = function () {
+ this.$submitButton.prop( 'disabled', true );
+ };
+
+ /**
+ * Respond to model update
+ */
+ mw.rcfilters.ui.FormWrapperWidget.prototype.onModelUpdate = function () {
+ this.$submitButton.prop( 'disabled', false );
+ };
+}( mediaWiki ) );