'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',
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();
--- /dev/null
+.mw-rcfilters-ui-capsuleItemWidget {
+ &-popup {
+ padding: 1em;
+ }
+
+ .oo-ui-popupWidget {
+ // Fix the positioning of the popup itself
+ margin-top: 1em;
+ }
+}
--- /dev/null
+.mw-rcfilters-ui-overlay {
+ font-size: 0.875em;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1;
+}
--- /dev/null
+( 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 ) );
/**
* Filter-specific CapsuleMultiselectWidget
*
+ * @class
* @extends OO.ui.CapsuleMultiselectWidget
*
* @constructor
* @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
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 );
};
/**
* @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 || {};
this.controller = controller;
this.model = model;
+ this.$overlay = config.$overlay || this.$element;
+
this.filtersInCapsule = [];
this.filterPopup = new mw.rcfilters.ui.FiltersListWidget(
} );
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 ) );