RCFilters UI: Add popup for capsule items
authorMoriel Schottlender <moriel@gmail.com>
Tue, 24 Jan 2017 18:23:27 +0000 (10:23 -0800)
committerRoan Kattouw <roan.kattouw@gmail.com>
Tue, 7 Feb 2017 23:01:11 +0000 (23:01 +0000)
Change-Id: Icdb5ef84929e5f7bf504e99f6e6987ef4e73ae60

resources/Resources.php
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js

index 8d33057..53cb917 100644 (file)
@@ -1742,13 +1742,16 @@ return [
                        '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/mw.rcfilters.Controller.js',
                        'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
                ],
                'styles' => [
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
index 7b29d4b..94fc959 100644 (file)
@@ -11,7 +11,9 @@
                init: function () {
                        var model = new mw.rcfilters.dm.FiltersViewModel(),
                                controller = new mw.rcfilters.Controller( model ),
-                               widget = new mw.rcfilters.ui.FilterWrapperWidget( controller, model );
+                               $overlay = $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-overlay' ),
+                               widget = new mw.rcfilters.ui.FilterWrapperWidget( controller, model, { $overlay: $overlay } );
 
                        model.initializeFilters( {
                                registration: {
                        } );
 
                        $( '.rcoptions' ).before( widget.$element );
+                       $( 'body' ).append( $overlay );
 
                        // Initialize values
                        controller.initialize();
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less
new file mode 100644 (file)
index 0000000..4ea88b5
--- /dev/null
@@ -0,0 +1,10 @@
+.mw-rcfilters-ui-capsuleItemWidget {
+       &-popup {
+               padding: 1em;
+       }
+
+       .oo-ui-popupWidget {
+               // Fix the positioning of the popup itself
+               margin-top: 1em;
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less
new file mode 100644 (file)
index 0000000..06840da
--- /dev/null
@@ -0,0 +1,8 @@
+.mw-rcfilters-ui-overlay {
+       font-size: 0.875em;
+       position: absolute;
+       top: 0;
+       right: 0;
+       left: 0;
+       z-index: 1;
+}
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js
new file mode 100644 (file)
index 0000000..547db1b
--- /dev/null
@@ -0,0 +1,89 @@
+( function ( mw, $ ) {
+       /**
+        * Extend OOUI's CapsuleItemWidget to also display a popup on hover.
+        *
+        * @class
+        * @extends OO.ui.CapsuleItemWidget
+        * @mixins OO.ui.mixin.PopupElement
+        *
+        * @constructor
+        * @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 ) {
+               var $popupContent = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup' ),
+                       descLabelWidget = new OO.ui.LabelWidget();
+
+               // Configuration initialization
+               config = config || {};
+
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+               this.positioned = false;
+
+               // Parent constructor
+               mw.rcfilters.ui.CapsuleItemWidget.parent.call( this, $.extend( {
+                       data: this.model.getName(),
+                       label: this.model.getLabel()
+               }, config ) );
+
+               // Mixin constructors
+               OO.ui.mixin.PopupElement.call( this, $.extend( {
+                       popup: {
+                               padded: true,
+                               align: 'center',
+                               $content: $popupContent
+                                       .append( descLabelWidget.$element ),
+                               $floatableContainer: this.$element
+                       }
+               }, config ) );
+
+               // Set initial text for the popup - the description
+               descLabelWidget.setLabel( this.model.getDescription() );
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+
+               // Initialization
+               this.$overlay.append( this.popup.$element );
+               this.$element
+                       .attr( 'aria-haspopup', 'true' )
+                       .addClass( 'mw-rcfilters-ui-capsuleItemWidget' )
+                       .on( 'mouseover', this.onHover.bind( this, true ) )
+                       .on( 'mouseout', this.onHover.bind( this, false ) );
+       };
+
+       OO.inheritClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.CapsuleItemWidget );
+       OO.mixinClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.mixin.PopupElement );
+
+       /**
+        * Respond to model update event
+        */
+       mw.rcfilters.ui.CapsuleItemWidget.prototype.onModelUpdate = function () {
+               // Deal with active/inactive capsule filter items
+               this.$element
+                       .toggleClass(
+                               'mw-rcfilters-ui-filterCapsuleMultiselectWidget-item-inactive',
+                               !this.model.isActive()
+                       );
+       };
+
+       /**
+        * Respond to hover event on the capsule item.
+        *
+        * @param {boolean} isHovering Mouse is hovering on the item
+        */
+       mw.rcfilters.ui.CapsuleItemWidget.prototype.onHover = function ( isHovering ) {
+               if ( this.model.getDescription() ) {
+                       this.popup.toggle( isHovering );
+
+                       if ( isHovering && !this.positioned ) {
+                               // Recalculate position to be center of the capsule item
+                               this.popup.$element.css( 'margin-left', ( this.$element.width() / 2 ) );
+                               this.positioned = true;
+                       }
+               }
+       };
+}( mediaWiki, jQuery ) );
index c498ce9..bf80cd6 100644 (file)
@@ -2,6 +2,7 @@
        /**
         * Filter-specific CapsuleMultiselectWidget
         *
+        * @class
         * @extends OO.ui.CapsuleMultiselectWidget
         *
         * @constructor
@@ -9,6 +10,7 @@
         * @param {mw.rcfilters.dm.FiltersViewModel} model RCFilters view model
         * @param {OO.ui.InputWidget} filterInput A filter input that focuses the capsule widget
         * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
         */
        mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
                // Parent
@@ -18,6 +20,8 @@
 
                this.controller = controller;
                this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
                this.filterInput = filterInput;
 
                this.$content.prepend(
 
        /* Methods */
 
-       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function () {
+       /**
+        * Respond to model itemUpdate event
+        *
+        * @param {mw.rcfilters.dm.FilterItem} item Filter item model
+        */
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
+               if ( item.isSelected() ) {
+                       this.addItemByName( item.getName() );
+               } else {
+                       this.removeItemByName( item.getName() );
+               }
+
                // Re-evaluate reset state
                this.reevaluateResetRestoreState();
        };
                this.emptyFilterMessage.toggle( currFiltersAreEmpty );
        };
 
+       /**
+        * @inheritdoc
+        */
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.createItemWidget = function ( data ) {
+               var item = this.model.getItemByName( data );
+
+               if ( !item ) {
+                       return;
+               }
+
+               return new mw.rcfilters.ui.CapsuleItemWidget( item, { $overlay: this.$overlay } );
+       };
+
+       /**
+        * Add items by their filter name
+        *
+        * @param {string} name Filter name
+        */
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.addItemByName = function ( name ) {
+               var item = this.model.getItemByName( name );
+
+               if ( !item ) {
+                       return;
+               }
+
+               // Check that the item isn't already added
+               if ( !this.getItemFromData( name ) ) {
+                       this.addItems( [ this.createItemWidget( name ) ] );
+               }
+       };
+
+       /**
+        * Remove items by their filter name
+        *
+        * @param {string} name Filter name
+        */
+       mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItemByName = function ( name ) {
+               this.removeItemsFromData( [ name ] );
+       };
+
        /**
         * @inheritdoc
         */
         * @inheritdoc
         */
        mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItems = function ( items ) {
+               var filterData = {};
+
                // Parent
                mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.prototype.removeItems.call( this, items );
 
-               this.emit( 'remove', items.map( function ( item ) { return item.getData(); } ) );
+               items.forEach( function ( itemWidget ) {
+                       filterData[ itemWidget.getData() ] = false;
+               } );
+
+               // Update the model
+               this.model.updateFilters( filterData );
        };
 
        /**
index 788ab3c..28e638b 100644 (file)
@@ -10,6 +10,7 @@
         * @param {mw.rcfilters.dm.FiltersViewModel} model View model
         * @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
         */
        mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, config ) {
                config = config || {};
@@ -21,6 +22,8 @@
 
                this.controller = controller;
                this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
                this.filtersInCapsule = [];
 
                this.filterPopup = new mw.rcfilters.ui.FiltersListWidget(
@@ -38,6 +41,7 @@
                } );
 
                this.capsule = new mw.rcfilters.ui.FilterCapsuleMultiselectWidget( controller, this.model, this.textInput, {
+                       $overlay: this.$overlay,
                        popup: {
                                $content: this.filterPopup.$element,
                                classes: [ 'mw-rcfilters-ui-filterWrapperWidget-popup' ]
 
                // Events
                this.model.connect( this, {
-                       initialize: 'onModelInitialize',
-                       itemUpdate: 'onModelItemUpdate'
+                       initialize: 'onModelInitialize'
                } );
                this.textInput.connect( this, {
                        change: 'onTextInputChange'
                } );
-               this.capsule.connect( this, {
-                       remove: 'onCapsuleRemoveItem'
-               } );
-
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
                        .append( this.capsule.$element, this.textInput.$element );
                this.filterPopup.filter( this.model.findMatches( newValue ) );
        };
 
-       /**
-        * Respond to an event where an item is removed from the capsule.
-        * This is the case where a user actively removes a filter box from the capsule widget.
-        *
-        * @param {string[]} filterNames An array of filter names that were removed
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onCapsuleRemoveItem = function ( filterNames ) {
-               var filterItem,
-                       widget = this;
-
-               filterNames.forEach( function ( filterName ) {
-                       // Go over filters
-                       filterItem = widget.model.getItemByName( filterName );
-                       filterItem.toggleSelected( false );
-               } );
-       };
-
        /**
         * Respond to model update event and set up the available filters to choose
         * from.
         */
        mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelInitialize = function () {
-               var items,
-                       wrapper = this,
-                       filters = this.model.getItems();
-
-               // Reset
-               this.capsule.getMenu().clearItems();
-
-               // Insert hidden options for the capsule to get its item data from
-               items = filters.map( function ( filterItem ) {
-                       return new OO.ui.MenuOptionWidget( {
-                               data: filterItem.getName(),
-                               label: filterItem.getLabel()
-                       } );
-               } );
-
-               this.capsule.getMenu().addItems( items );
+               var wrapper = this;
 
                // Add defaults to capsule. We have to do this
                // after we added to the capsule menu, since that's
                // how the capsule multiselect widget knows which
                // object to add
-               filters.forEach( function ( filterItem ) {
+               this.model.getItems().forEach( function ( filterItem ) {
                        if ( filterItem.isSelected() ) {
-                               wrapper.addCapsuleItemFromName( filterItem.getName() );
+                               wrapper.capsule.addItemByName( filterItem.getName() );
                        }
                } );
        };
 
-       /**
-        * Respond to model item update
-        *
-        * @param {mw.rcfilters.dm.FilterItem} item Filter item that was updated
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onModelItemUpdate = function ( item ) {
-               if ( item.isSelected() ) {
-                       this.addCapsuleItemFromName( item.getName() );
-               } else {
-                       this.capsule.removeItemsFromData( [ item.getName() ] );
-               }
-       };
-
        /**
         * Add a capsule item by its filter name
         *
         * @param {string} itemName Filter name
         */
        mw.rcfilters.ui.FilterWrapperWidget.prototype.addCapsuleItemFromName = function ( itemName ) {
-               var item = this.model.getItemByName( itemName );
-
-               this.capsule.addItemsFromData( [ itemName ] );
-
-               // Deal with active/inactive capsule filter items
-               this.capsule.getItemFromData( itemName ).$element
-                       .toggleClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-item-inactive', !item.isActive() );
+               this.capsule.addItemByName( [ itemName ] );
        };
 }( mediaWiki ) );