From f1f34d096bc9104ec9008ed0af15dc61996dbcd9 Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Tue, 24 Jan 2017 10:23:27 -0800 Subject: [PATCH] RCFilters UI: Add popup for capsule items Change-Id: Icdb5ef84929e5f7bf504e99f6e6987ef4e73ae60 --- resources/Resources.php | 3 + .../mediawiki.rcfilters/mw.rcfilters.init.js | 5 +- .../mw.rcfilters.ui.CapsuleItemWidget.less | 10 +++ .../styles/mw.rcfilters.ui.Overlay.less | 8 ++ .../ui/mw.rcfilters.ui.CapsuleItemWidget.js | 89 +++++++++++++++++++ ...lters.ui.FilterCapsuleMultiselectWidget.js | 66 +++++++++++++- .../ui/mw.rcfilters.ui.FilterWrapperWidget.js | 70 ++------------- 7 files changed, 187 insertions(+), 64 deletions(-) create mode 100644 resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less create mode 100644 resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less create mode 100644 resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js diff --git a/resources/Resources.php b/resources/Resources.php index 8d33057b7c..53cb917237 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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', diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js index 7b29d4b40f..94fc959338 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js @@ -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 = $( '
' ) + .addClass( 'mw-rcfilters-ui-overlay' ), + widget = new mw.rcfilters.ui.FilterWrapperWidget( controller, model, { $overlay: $overlay } ); model.initializeFilters( { registration: { @@ -148,6 +150,7 @@ } ); $( '.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 index 0000000000..4ea88b5799 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less @@ -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 index 0000000000..06840da79b --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less @@ -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 index 0000000000..547db1bfa0 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js @@ -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 = $( '
' ) + .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 ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js index c498ce9d19..bf80cd6439 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js @@ -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( @@ -91,7 +95,18 @@ /* 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(); }; @@ -129,6 +144,46 @@ 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 */ @@ -154,10 +209,17 @@ * @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 ); }; /** diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js index 788ab3c2a2..28e638b7c3 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js @@ -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' ] @@ -46,16 +50,11 @@ // 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 ); @@ -76,81 +75,30 @@ 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 ) ); -- 2.20.1