From b3ee23c220c728199ad899d257954f1342f61cb5 Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Mon, 27 Feb 2017 17:56:40 -0800 Subject: [PATCH] RCFilters UI: Ajaxify everything Make sure all links and 'show' button form information uses the ajax method rather than reloading the page. Bug: T157594 Change-Id: I97a452082e2d06f78cbec2235e2ed07a2eb6bca0 --- includes/specials/SpecialRecentchanges.php | 5 +- .../mw.rcfilters.dm.ChangesListViewModel.js | 5 +- .../mw.rcfilters.Controller.js | 85 +++++++++++---- .../mediawiki.rcfilters/mw.rcfilters.init.js | 3 +- .../styles/mw.rcfilters.less | 4 + .../ui/mw.rcfilters.ui.FormWrapperWidget.js | 102 +++++++++++++++++- 6 files changed, 177 insertions(+), 27 deletions(-) diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 73a209bb01..150530808b 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -697,7 +697,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $title = new HtmlArmor( '' . htmlspecialchars( $title ) . '' ); } - return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [], $params ); + return $this->getLinkRenderer()->makeKnownLink( $this->getPageTitle(), $title, [ + 'data-params' => json_encode( $override ), + 'data-keys' => implode( ',', array_keys( $override ) ), + ], $params ); } /** diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js index edb6744bac..d6ce734caf 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js @@ -50,10 +50,11 @@ * Update the model with an updated list of changes * * @param {jQuery|string} changesListContent + * @param {jQuery} $fieldset */ - mw.rcfilters.dm.ChangesListViewModel.prototype.update = function ( changesListContent ) { + mw.rcfilters.dm.ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset ) { this.valid = true; - this.emit( 'update', changesListContent ); + this.emit( 'update', changesListContent, $fieldset ); }; }( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 1df31a279a..c0f453ca59 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -62,7 +62,6 @@ // Check all filter interactions this.filtersModel.reassessFilterInteractions(); - this.updateURL(); this.updateChangesList(); }; @@ -75,7 +74,6 @@ // Check all filter interactions this.filtersModel.reassessFilterInteractions(); - this.updateURL(); this.updateChangesList(); }; @@ -93,7 +91,6 @@ obj[ filterName ] = isSelected; this.filtersModel.updateFilters( obj ); - this.updateURL(); this.updateChangesList(); // Check filter interactions @@ -103,9 +100,23 @@ /** * Update the URL of the page to reflect current filters + * + * This should not be called directly from outside the controller. + * If an action requires changing the URL, it should either use the + * highlighting actions below, or call #updateChangesList which does + * the uri corrections already. + * + * @private + * @param {Object} [params] Extra parameters to add to the API call */ - mw.rcfilters.Controller.prototype.updateURL = function () { - var uri = this.getUpdatedUri(); + mw.rcfilters.Controller.prototype.updateURL = function ( params ) { + var uri; + + params = params || {}; + + uri = this.getUpdatedUri(); + uri.extend( params ); + window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() ); }; @@ -140,6 +151,7 @@ * Fetch the list of changes from the server for the current filters * * @return {jQuery.Promise} Promise object that will resolve with the changes list + * or with a string denoting no results. */ mw.rcfilters.Controller.prototype.fetchChangesList = function () { var uri = this.getUpdatedUri(), @@ -147,28 +159,63 @@ 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; - } ); + .then( + // Success + function ( html ) { + var $parsed; + if ( !latestRequest() ) { + return $.Deferred().reject(); + } + + $parsed = $( $.parseHTML( html ) ); + + return { + // Changes list + changes: $parsed.find( '.mw-changeslist' ).first().contents(), + // Fieldset + fieldset: $parsed.find( 'fieldset.rcoptions' ).first() + }; + }, + // Failure + function ( responseObj ) { + var $parsed; + + if ( !latestRequest() ) { + return $.Deferred().reject(); + } + + $parsed = $( $.parseHTML( responseObj.responseText ) ); + + // Force a resolve state to this promise + return $.Deferred().resolve( { + changes: 'NO_RESULTS', + fieldset: $parsed.find( 'fieldset.rcoptions' ).first() + } ).promise(); + } + ); }; /** * Update the list of changes and notify the model + * + * @param {Object} [params] Extra parameters to add to the API call */ - mw.rcfilters.Controller.prototype.updateChangesList = function () { + mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) { + this.updateURL( params ); this.changesListModel.invalidate(); this.fetchChangesList() - .always( function ( changesListContent ) { - if ( changesListContent ) { - this.changesListModel.update( changesListContent ); - } - }.bind( this ) ); + .then( + // Success + function ( pieces ) { + var $changesListContent = pieces.changes, + $fieldset = pieces.fieldset; + + this.changesListModel.update( $changesListContent, $fieldset ); + }.bind( this ) + // Do nothing for failure + ); }; /** diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js index 6cfeb1a72a..af42f34e5b 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js @@ -23,7 +23,7 @@ // eslint-disable-next-line no-new new mw.rcfilters.ui.FormWrapperWidget( - changesListModel, $( '.rcoptions' ) ); + changesListModel, controller, $( 'fieldset.rcoptions' ) ); controller.initialize( { registration: { @@ -226,7 +226,6 @@ } ); window.addEventListener( 'popstate', function () { - controller.updateFromURL(); controller.updateChangesList(); } ); } diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less index e2775f92a1..897a9e8191 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less @@ -29,3 +29,7 @@ } } } + +.mw-rcfilters-staticfilters-selected { + font-weight: bold; +} diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js index d089086dda..3c81ff1e80 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js @@ -6,26 +6,38 @@ * * @constructor * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model + * @param {mw.rcfilters.Controller} controller RCfilters controller * @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 ) { + mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( model, controller, $formRoot, config ) { config = config || {}; // Parent mw.rcfilters.ui.FormWrapperWidget.parent.call( this, $.extend( {}, config, { $element: $formRoot } ) ); + // Mixin constructors + OO.ui.mixin.PendingElement.call( this, config ); this.model = model; + this.controller = controller; this.$submitButton = this.$element.find( 'form input[type=submit]' ); + this.$element + .on( 'click', 'a[data-params]', this.onLinkClick.bind( this ) ); + + this.$element + .on( 'submit', 'form', this.onFormSubmit.bind( this ) ); + // Events this.model.connect( this, { invalidate: 'onModelInvalidate', update: 'onModelUpdate' } ); + // Initialize + this.cleanupForm(); this.$element .addClass( 'mw-rcfilters-ui-FormWrapperWidget' ) .addClass( 'mw-rcfilters-ui-ready' ); @@ -34,18 +46,102 @@ /* Initialization */ OO.inheritClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.Widget ); + OO.mixinClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.mixin.PendingElement ); + + /** + * Clean up the base form we're getting from the back-end. + * Remove tags and replace those with classes, so + * we can toggle those on click. + */ + mw.rcfilters.ui.FormWrapperWidget.prototype.cleanupForm = function () { + this.$element.find( '[data-keys] strong' ).each( function () { + $( this ) + .parent().addClass( 'mw-rcfilters-staticfilters-selected' ); + + $( this ) + .replaceWith( $( this ).contents() ); + } ); + }; + + /** + * Respond to link click + * + * @param {jQuery.Event} e Event + * @return {boolean} false + */ + mw.rcfilters.ui.FormWrapperWidget.prototype.onLinkClick = function ( e ) { + var $element = $( e.target ), + data = $element.data( 'params' ), + keys = $element.data( 'keys' ), + $similarElements = $element.parent().find( '[data-keys="' + keys + '"]' ); + + // Only highlight choice if this link isn't a show/hide link + if ( !$element.parents( '.rcshowhideoption' ).length ) { + // Remove the class from similar elements + $similarElements.removeClass( 'mw-rcfilters-staticfilters-selected' ); + // Add the class to this element + $element.addClass( 'mw-rcfilters-staticfilters-selected' ); + } + + e.stopPropagation(); + + this.controller.updateChangesList( data ); + return false; + }; + + /** + * Respond to form submit event + * + * @param {jQuery.Event} e Event + * @return {boolean} false + */ + mw.rcfilters.ui.FormWrapperWidget.prototype.onFormSubmit = function ( e ) { + var data = {}; + + // Collect all data from form + $( e.target ).find( 'input:not([type="hidden"],[type="submit"]), select' ).each( function () { + if ( !$( this ).is( ':checkbox' ) || $( this ).is( ':checked' ) ) { + data[ $( this ).prop( 'name' ) ] = $( this ).val(); + } + } ); + + this.controller.updateChangesList( data ); + return false; + }; /** * Respond to model invalidate */ mw.rcfilters.ui.FormWrapperWidget.prototype.onModelInvalidate = function () { + this.pushPending(); this.$submitButton.prop( 'disabled', true ); }; /** - * Respond to model update + * Respond to model update, replace the show/hide links with the ones from the + * server so they feature the correct state. + * + * @param {jQuery|string} $changesList Updated changes list + * @param {jQuery} $fieldset Updated fieldset */ - mw.rcfilters.ui.FormWrapperWidget.prototype.onModelUpdate = function () { + mw.rcfilters.ui.FormWrapperWidget.prototype.onModelUpdate = function ( $changesList, $fieldset ) { this.$submitButton.prop( 'disabled', false ); + + // Replace the links we have in the content + // We don't want to replace the entire thing, because there is a big difference between + // the links in the backend and the links we have initialized, since we are removing + // the ones that are implemented in the new system + this.$element.find( '.rcshowhide' ).children().each( function () { + // Go over existing links and replace only them + var classes = $( this ).attr( 'class' ).split( ' ' ), + // Look for that item in the fieldset from the server + $remoteItem = $fieldset.find( '.' + classes.join( '.' ) ); + + if ( $remoteItem ) { + $( this ).replaceWith( $remoteItem ); + } + } ); + + this.popPending(); }; }( mediaWiki ) ); -- 2.20.1