mediawiki-extensions-Visual.../modules/ve-mw/ui/widgets/ve.ui.MWCategoryWidget.js
Roan Kattouw 267212c531 Put category popups in the inner overlay for now
We really shouldn't need the inner overlay for this,
we should be able to deal with popups being in
oo-ui-window-overlay. But for now, we're not, and
this fixes the current problems.

Depends on If16d16d2b in oojs-ui.

Bug: 72052
Change-Id: Ie06056b96db19ac4caf1f9c0e3a1c49cfddc6682
2014-10-14 17:02:51 -07:00

349 lines
9.2 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
* @cfg {jQuery} [$popupOverlay] Overlay to render popups 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.$, $overlay: config.$popupOverlay
} );
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.$( '<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 } );
}
}
};