RCFilters UI: Ajaxify everything
authorMoriel Schottlender <moriel@gmail.com>
Tue, 28 Feb 2017 01:56:40 +0000 (17:56 -0800)
committerMoriel Schottlender <moriel@gmail.com>
Thu, 2 Mar 2017 20:28:21 +0000 (12:28 -0800)
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
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js

index 73a209b..1505308 100644 (file)
@@ -697,7 +697,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        $title = new HtmlArmor( '<strong>' . htmlspecialchars( $title ) . '</strong>' );
                }
 
-               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 );
        }
 
        /**
index edb6744..d6ce734 100644 (file)
         * 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 ) );
index 1df31a2..c0f453c 100644 (file)
@@ -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
 
        /**
         * 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() );
        };
 
         * 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(),
                        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
+                       );
        };
 
        /**
index 6cfeb1a..af42f34 100644 (file)
@@ -23,7 +23,7 @@
 
                        // eslint-disable-next-line no-new
                        new mw.rcfilters.ui.FormWrapperWidget(
-                               changesListModel, $( '.rcoptions' ) );
+                               changesListModel, controller, $( 'fieldset.rcoptions' ) );
 
                        controller.initialize( {
                                registration: {
                        } );
 
                        window.addEventListener( 'popstate', function () {
-                               controller.updateFromURL();
                                controller.updateChangesList();
                        } );
                }
index e2775f9..897a9e8 100644 (file)
@@ -29,3 +29,7 @@
                }
        }
 }
+
+.mw-rcfilters-staticfilters-selected {
+       font-weight: bold;
+}
index d089086..3c81ff1 100644 (file)
@@ -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' );
        /* 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 <strong> 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 ) );