mediawiki-extensions-Visual.../modules/ve-mw/ui/widgets/ve.ui.MWCategoryWidget.js
Roan Kattouw 7157488e20 Use relative positioning for category popups, move out of overlay
Instead of putting these popups in an overlay, put them
in the category widget. This makes scrolling work more
nicely, and makes things easier to deal with in general.

This requires that the popup position itself using
getRelativePosition(), because it's no longer in an
overlay. This also means these popups should now position
themselves correctly no matter where they are.

Change-Id: I09a1e5891a897d634c41d386a2307fe3df2a9157
2014-10-15 22:58:05 -07:00

349 lines
9.1 KiB
JavaScript

/*!
* VisualEditor UserInterface MWCategoryWidget class.
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.ui.MWCategoryWidget object.
*
* @class
* @abstract
* @extends OO.ui.Widget
* @mixins OO.ui.GroupElement
*
* @constructor
* @param {Object} [config] Configuration options
* @cfg {jQuery} [$overlay] Overlay to render dropdowns in
*/
ve.ui.MWCategoryWidget = function VeUiMWCategoryWidget( config ) {
// Config intialization
config = config || {};
// Parent constructor
OO.ui.Widget.call( this, config );
// Mixin constructors
OO.ui.GroupElement.call( this, config );
// Properties
this.categories = {};
this.categoryRedirects = {}; // Source -> target
this.popupState = false;
this.savedPopupState = false;
this.popup = new ve.ui.MWCategoryPopupWidget( {
$: this.$
} );
this.input = new ve.ui.MWCategoryInputWidget( this, {
$: this.$, $overlay: config.$overlay
} );
// Events
this.input.on( 'enter', this.onLookupEnter.bind( this ) );
this.input.lookupMenu.connect( this, { choose: 'onLookupMenuItemChoose' } );
this.popup.connect( this, {
removeCategory: 'onRemoveCategory',
updateSortkey: 'onUpdateSortkey',
hide: 'onPopupHide'
} );
// Initialization
this.$element.addClass( 've-ui-mwCategoryWidget' )
.append(
this.$group.addClass( 've-ui-mwCategoryWidget-items' ),
this.input.$element,
this.popup.$element,
this.$( '<div>' ).css( 'clear', 'both' )
);
};
/* Inheritance */
OO.inheritClass( ve.ui.MWCategoryWidget, OO.ui.Widget );
OO.mixinClass( ve.ui.MWCategoryWidget, OO.ui.GroupElement );
/* Events */
/**
* @event newCategory
* @param {Object} item Category item
* @param {string} item.name Fully prefixed category name
* @param {string} item.value Category value (name without prefix)
* @param {Object} item.metaItem Category meta item
*/
/**
* @event updateSortkey
* @param {Object} item Category item
* @param {string} item.name Fully prefixed category name
* @param {string} item.value Category value (name without prefix)
* @param {Object} item.metaItem Category meta item
*/
/* Methods */
/**
* Handle enter event in input field.
*/
ve.ui.MWCategoryWidget.prototype.onLookupEnter = function () {
if ( this.input.getValue() !== '' ) {
var item = this.input.getCategoryItemFromValue( this.input.getValue() ),
categoryWidget = this;
this.queryCategoryHiddenStatus( [item.name] ).done( function () {
categoryWidget.emit( 'newCategory', item );
} );
this.input.setValue( '' );
}
};
/**
* Handle menu item select event.
*
* @method
* @param {OO.ui.MenuItemWidget} item Selected item
*/
ve.ui.MWCategoryWidget.prototype.onLookupMenuItemChoose = function ( item ) {
var categoryItem,
value = item && item.getData(),
categoryWidget = this;
if ( value && value !== '' ) {
// Remove existing items by value
if ( value in this.categories ) {
this.categories[value].metaItem.remove();
}
// Add new item
categoryItem = this.input.getCategoryItemFromValue( value );
this.queryCategoryHiddenStatus( [categoryItem.name] ).done( function () {
categoryWidget.emit( 'newCategory', categoryItem );
} );
// Reset input
this.input.setValue( '' );
}
};
/**
* Removes category from model.
*
* @method
* @param {string} name category name
*/
ve.ui.MWCategoryWidget.prototype.onRemoveCategory = function ( name ) {
this.categories[name].metaItem.remove();
};
/**
* Update sortkey value, emit updateSortkey event
*
* @method
* @param {string} name
* @param {string} value
*/
ve.ui.MWCategoryWidget.prototype.onUpdateSortkey = function ( name, value ) {
this.categories[name].sortKey = value;
this.emit( 'updateSortkey', this.categories[name] );
};
/**
* Sets popup state when popup is hidden
*/
ve.ui.MWCategoryWidget.prototype.onPopupHide = function () {
this.popupState = false;
};
/**
* Saves current popup state
*/
ve.ui.MWCategoryWidget.prototype.onSavePopupState = function () {
this.savedPopupState = this.popupState;
};
/**
* Toggles popup menu per category item
* @param {Object} item
*/
ve.ui.MWCategoryWidget.prototype.onTogglePopupMenu = function ( item ) {
// Close open popup.
if ( this.savedPopupState === false || item.value !== this.popup.category ) {
this.popup.openPopup( item );
} else {
// Handle toggle
this.popup.closePopup();
}
};
/** */
ve.ui.MWCategoryWidget.prototype.setDefaultSortKey = function ( value ) {
this.popup.setDefaultSortKey( value );
};
/**
* Get list of category names.
*
* @method
* @param {string[]} List of category names
*/
ve.ui.MWCategoryWidget.prototype.getCategories = function () {
return ve.getObjectKeys( this.categories );
};
/**
* Starts a request to update the link cache's hidden status for the given titles.
* The returned promise will be resolved with an API result if an API call was made,
* or no arguments if it was unnecessary.
*
* @param {string[]} categoryNames
* @return {jQuery.Promise}
*/
ve.ui.MWCategoryWidget.prototype.queryCategoryHiddenStatus = function ( categoryNames ) {
var categoryWidget = this, categoryNamesToQuery = [];
// Get rid of any we already know the hidden status of.
categoryNamesToQuery = $.grep( categoryNames, function ( categoryTitle ) {
var cacheEntry = ve.init.platform.linkCache.getCached( categoryTitle );
return !( cacheEntry && cacheEntry.hidden );
} );
if ( !categoryNamesToQuery.length ) {
return $.Deferred().resolve().promise();
}
return new mw.Api().get( {
action: 'query',
prop: 'pageprops',
titles: categoryNamesToQuery.join( '|' ),
ppprop: 'hiddencat',
redirects: ''
} ).then( function ( result ) {
var linkCacheUpdate = {};
if ( result && result.query && result.query.pages ) {
$.each( result.query.pages, function ( index, pageInfo ) {
linkCacheUpdate[pageInfo.title] = {
missing: false,
hidden: pageInfo.pageprops &&
Object.prototype.hasOwnProperty.call( pageInfo.pageprops, 'hiddencat' )
};
} );
}
if ( result && result.query && result.query.redirects ) {
$.each( result.query.redirects, function ( index, redirectInfo ) {
categoryWidget.categoryRedirects[redirectInfo.from] = redirectInfo.to;
} );
}
ve.init.platform.linkCache.set( linkCacheUpdate );
} );
};
/**
* Adds category items.
*
* @method
* @param {Object[]} items Items to add
* @param {number} [index] Index to insert items after
* @return {jQuery.Promise}
*/
ve.ui.MWCategoryWidget.prototype.addItems = function ( items, index ) {
var i, len, item, categoryItem,
categoryItems = [],
existingCategoryItem = null,
categoryNames = $.map( items, function ( item ) {
return item.name;
} ),
categoryWidget = this;
return this.queryCategoryHiddenStatus( categoryNames ).then( function () {
var itemTitle, config;
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
itemTitle = new mw.Title( item.name, mw.config.get( 'wgNamespaceIds' ).category ).getPrefixedText();
// Create a widget using the item data
config = {
$: categoryWidget.$,
item: item
};
if ( Object.prototype.hasOwnProperty.call( categoryWidget.categoryRedirects, itemTitle ) ) {
config.redirectTo = new mw.Title(
categoryWidget.categoryRedirects[itemTitle],
mw.config.get( 'wgNamespaceIds' ).category
).getMainText();
config.hidden = ve.init.platform.linkCache.getCached( categoryWidget.categoryRedirects[itemTitle] ).hidden;
} else {
config.hidden = ve.init.platform.linkCache.getCached( item.name ).hidden;
}
categoryItem = new ve.ui.MWCategoryItemWidget( config );
categoryItem.connect( categoryWidget, {
savePopupState: 'onSavePopupState',
togglePopupMenu: 'onTogglePopupMenu'
} );
// Index item by value
categoryWidget.categories[item.value] = categoryItem;
// Copy sortKey from old item when "moving"
if ( existingCategoryItem ) {
categoryItem.sortKey = existingCategoryItem.sortKey;
}
categoryItems.push( categoryItem );
}
OO.ui.GroupElement.prototype.addItems.call( categoryWidget, categoryItems, index );
categoryWidget.fitInput();
} );
};
/**
* Remove category items.
*
* @method
* @param {string[]} names Names of categories to remove
*/
ve.ui.MWCategoryWidget.prototype.removeItems = function ( names ) {
var i, len, categoryItem,
items = [];
for ( i = 0, len = names.length; i < len; i++ ) {
categoryItem = this.categories[names[i]];
if ( categoryItem ) {
categoryItem.disconnect( this );
items.push( categoryItem );
delete this.categories[names[i]];
}
}
OO.ui.GroupElement.prototype.removeItems.call( this, items );
this.fitInput();
};
/**
* Auto-fit the input.
*
* @method
*/
ve.ui.MWCategoryWidget.prototype.fitInput = function () {
var gap, min, $lastItem,
$input = this.input.$element;
if ( !$input.is( ':visible') ) {
return;
}
$input.css( { width: 'inherit' } );
min = $input.outerWidth();
$input.css( { width: '100%' } );
$lastItem = this.$element.find( '.ve-ui-mwCategoryItemWidget:last' );
if ( $lastItem.length ) {
// Try to fit to the right of the last item
gap = ( $input.offset().left + $input.outerWidth() ) -
( $lastItem.offset().left + $lastItem.outerWidth() );
if ( gap >= min ) {
$input.css( { width: gap } );
}
}
};