' )
.addClass( 'tux-grouplist' )
.addClass( 'row' );
this.$loader = $( '
' )
.addClass( 'tux-loading-indicator tux-loading-indicator--centered' );
this.$menu.append( $listFiltersGroup, this.$loader, this.$list );
$( 'body' ).append( this.$menu );
},
/**
* Show the selector
*/
show: function () {
this.$menu.addClass( 'open' ).show();
this.position();
// Place the focus in the message group search box.
this.$search.focus();
// Start loading the groups, but assess the situation again after
// they are loaded, in case user has made further interactions.
if ( this.firstShow ) {
this.loadGroups().done( this.showList.bind( this ) );
this.firstShow = false;
}
},
/**
* Hide the selector
*
* @param {jQuery.Event} e
*/
hide: function ( e ) {
// Do not hide if the trigger is clicked
if ( e && ( this.$trigger.is( e.target ) || this.$trigger.has( e.target ).length ) ) {
return;
}
this.$menu.hide().removeClass( 'open' );
},
/**
* Toggle the menu open/close state
*/
toggle: function () {
if ( this.$menu.hasClass( 'open' ) ) {
this.hide();
} else {
this.show();
}
},
/**
* Attach event listeners
*/
listen: function () {
var $tabs,
groupSelector = this;
// Hide the selector panel when clicking outside of it
$( 'html' ).on( 'click', this.hide.bind( this ) );
groupSelector.$trigger.on( 'click', function () {
groupSelector.toggle();
} );
groupSelector.$menu.on( 'click', function ( e ) {
e.preventDefault();
e.stopPropagation();
} );
// Handle click on row item. This selects the group, and in case it has
// subgroups, also opens a new menu to show them.
groupSelector.$menu.on( 'click', '.tux-grouplist__item', function () {
var $newLink,
messageGroup = $( this ).data( 'msggroup' );
groupSelector.hide();
groupSelector.$trigger.nextAll().remove();
if ( !groupSelector.options.preventSelector ) {
$newLink = $( '
' )
.addClass( 'grouptitle grouplink' )
.text( messageGroup.label )
.data( 'msggroupid', messageGroup.id );
groupSelector.$trigger.after( $newLink );
if ( messageGroup.groups && messageGroup.groups.length > 0 ) {
// Show the new menu immediately.
// Pass options for callbacks, language etc. but ignore the position
// option unless explicitly given to allow automatic recalculation
// of the position compared to the new trigger.
$newLink
.addClass( 'tux-breadcrumb__item--aggregate' )
.msggroupselector( groupSelector.customOptions )
.data( 'msggroupselector' ).show();
}
}
if ( groupSelector.options.onSelect ) {
groupSelector.options.onSelect( messageGroup );
}
} );
// Handle the tabs All | Recent
$tabs = groupSelector.$menu.find( '.tux-grouptab' );
$tabs.on( 'click', function () {
var $this = $( this );
/* Do nothing if user clicks the active tab.
* Fixes two things:
* - The blue bottom border highlight doesn't jump around
* - No flash when clicking recent tab again
*/
if ( $this.hasClass( 'tux-grouptab--selected' ) ) {
return;
}
// This is okay as long as we only have two classes
$tabs.toggleClass( 'tux-grouptab--selected' );
groupSelector.$search.val( '' );
groupSelector.showList();
} );
this.$search.on( 'click', this.show.bind( this ) )
.on( 'keypress', this.keyup.bind( this ) )
.on( 'keyup', this.keyup.bind( this ) );
if ( this.eventSupported( 'keydown' ) ) {
this.$search.on( 'keydown', this.keyup.bind( this ) );
}
},
/**
* Handle the keypress/keyup events in the message group search box.
*/
keyup: function () {
delay( this.showList.bind( this ), 300 );
},
/**
* Position the menu
*/
position: function () {
if ( this.options.position.of === undefined ) {
this.options.position.of = this.$trigger;
}
this.$menu.position( this.options.position );
},
/**
* Shows suitable list for current view, taking possible filter into account
*/
showList: function () {
var query = this.$search.val().trim().toLowerCase();
if ( query ) {
this.filter( query );
} else {
this.showUnfilteredList();
}
},
/**
* Shows an unfiltered list of groups depending on the selected tab.
*/
showUnfilteredList: function () {
var $selected = this.$menu.find( '.tux-grouptab--selected' );
if ( $selected.hasClass( 'tux-grouptab--all' ) ) {
if ( this.groups ) {
this.showSelectedGroups( this.groups );
} else {
this.showDefaultGroups();
}
} else if ( $selected.hasClass( 'tux-grouptab--recent' ) ) {
this.showRecentGroups();
}
},
/**
* Shows the list of message groups excluding subgroups.
*
* In case a parent message group has been given, only subgroups of that
* message group are shown, otherwise all top-level message groups are shown.
*/
showDefaultGroups: function () {
var groupSelector = this;
this.$loader.show();
this.loadGroups().done( function ( groups ) {
var groupsToShow = mw.translate.findGroup( groupSelector.parentGroupId, groups );
// We do not want to display the group itself, only its subgroups
if ( groupSelector.parentGroupId ) {
groupsToShow = groupsToShow.groups;
}
groupSelector.$loader.hide();
groupSelector.$list.empty();
groupSelector.addGroupRows( groupsToShow );
} );
},
/**
* Show recent message groups.
*/
showRecentGroups: function () {
var recent = this.options.recent || [];
this.showSelectedGroups( recent );
},
/**
* Load message groups.
*
* @param {Array} groups List of the message group ids to show.
*/
showSelectedGroups: function ( groups ) {
var groupSelector = this;
this.$loader.show();
this.loadGroups()
.then( function ( allGroups ) {
var rows = [];
$.each( groups, function ( index, id ) {
var group = mw.translate.findGroup( id, allGroups );
if ( group ) {
rows.push( groupSelector.prepareMessageGroupRow( group ) );
}
} );
return rows;
} )
.always( function () {
groupSelector.$loader.hide();
groupSelector.$list.empty();
} )
.done( function ( rows ) {
groupSelector.$list.append( rows );
} );
},
/**
* Flattens a message group tree.
*
* @param {Array} messageGroups An array or data object.
* @param {Object} foundIDs The array in which the keys are IDs of message groups that were found already.
*/
flattenGroupList: function ( messageGroups, foundIDs ) {
var i;
if ( messageGroups.groups ) {
messageGroups = messageGroups.groups;
}
for ( i = 0; i < messageGroups.length; i++ ) {
// Avoid duplicate groups, and add the parent before subgroups
if ( !foundIDs[ messageGroups[ i ].id ] ) {
this.flatGroupList.push( messageGroups[ i ] );
foundIDs[ messageGroups[ i ].id ] = true;
}
// In case there are subgroups, add them recursively
if ( messageGroups[ i ].groups ) {
this.flattenGroupList( messageGroups[ i ].groups, foundIDs );
}
}
},
/**
* Search the message groups based on label or id.
* Label match is prefix match, while id match is exact match.
*
* @param {string} query
*/
filter: function ( query ) {
var self = this;
this.loadGroups().done( function ( groups ) {
var currentGroup, index, matcher, foundGroups = [];
if ( !self.flatGroupList ) {
self.flatGroupList = [];
currentGroup = mw.translate.findGroup( self.parentGroupId, groups );
if ( self.parentGroupId ) {
currentGroup = currentGroup.groups;
}
self.flattenGroupList( currentGroup, {} );
}
// Optimization, assuming that people search the beginning
// of the group name.
matcher = new RegExp( '\\b' + escapeRegex( query ), 'i' );
for ( index = 0; index < self.flatGroupList.length; index++ ) {
if ( matcher.test( self.flatGroupList[ index ].label ) ||
query === self.flatGroupList[ index ].id ) {
foundGroups.push( self.flatGroupList[ index ] );
}
}
self.$loader.hide();
self.$list.empty();
self.addGroupRows( foundGroups );
} );
},
/**
* Load message groups and relevant properties using the API.
*
* @return {jQuery.Promise}
*/
loadGroups: function () {
var params;
if ( groupsLoader !== undefined ) {
return groupsLoader;
}
params = {
action: 'query',
meta: 'messagegroups',
mgformat: 'tree',
mgprop: 'id|label|icon|priority|prioritylangs|priorityforce',
mgiconsize: '32'
};
groupsLoader = new mw.Api()
.get( params )
.then( function ( result ) {
return result.query.messagegroups;
} )
.promise();
return groupsLoader;
},
/**
* Add rows with message groups to the selector.
*
* @param {Array} groups Array of message group objects to add.
*/
addGroupRows: function ( groups ) {
var groupSelector = this,
$msgGroupRows = [],
$parent,
targetLanguage = this.options.language;
if ( !groups ) {
return;
}
$.each( groups, function ( index, group ) {
/* Hide from the selector:
* - discouraged groups (the only priority value currently supported).
* - groups that are recommended for other languages.
*/
if ( group.priority === 'discouraged' ||
( group.priorityforce &&
group.prioritylangs &&
group.prioritylangs.indexOf( targetLanguage ) === -1 )
) {
return;
}
$msgGroupRows.push( groupSelector.prepareMessageGroupRow( group ) );
} );
if ( this.parentGroupId ) {
$parent = this.$list.find( '.tux-grouplist__item[data-msggroupid="' +
this.parentGroupId + '"]' );
if ( $parent.length ) {
$parent.after( $msgGroupRows );
return;
}
}
this.$list.append( $msgGroupRows );
},
/**
* Prepare a message group row in the selector.
*
* @param {Object} messagegroup object.
* @return {Object} a jQuery object with the groups selector row ().
*/
prepareMessageGroupRow: function ( messagegroup ) {
var $row,
$icon,
$label,
$statsbar,
$subGroupsLabel,
style = '';
$row = $( '
' )
.addClass( 'row tux-grouplist__item' )
.attr( 'data-msggroupid', messagegroup.id )
.data( 'msggroup', messagegroup );
$icon = $( '
' )
.addClass( 'tux-grouplist__item__icon' )
.addClass( 'one column' );
$statsbar = $( '
' ).languagestatsbar( {
language: this.options.language,
group: messagegroup.id
} );
$label = $( '
' )
.addClass( 'tux-grouplist__item__label' )
.addClass( 'seven columns' )
.append(
$( '
' )
// T130390: must be attr for IE/Edge.
.attr( { dir: 'auto' } )
.text( messagegroup.label ),
$statsbar
);
if ( messagegroup.icon && messagegroup.icon.raster ) {
style += 'background-image: url(--);';
style = style.replace( /--/g, messagegroup.icon.raster );
}
if ( messagegroup.icon && messagegroup.icon.vector ) {
style += 'background-image: linear-gradient(transparent, transparent), url(--);';
style = style.replace( /--/g, messagegroup.icon.vector );
}
if ( style !== '' ) {
$icon.attr( 'style', style );
}
$subGroupsLabel = $( [] );
if ( messagegroup.groups && messagegroup.groups.length > 0 ) {
$subGroupsLabel = $( '' )
.addClass( 'tux-grouplist__item__subgroups' )
.addClass( 'four columns' )
.text( mw.msg( 'translate-msggroupselector-view-subprojects',
messagegroup.groups.length ) );
}
return $row.append( $icon, $label, $subGroupsLabel );
},
/**
* Check that a DOM event is supported by the $menu jQuery object.
*
* @param {string} eventName
* @return {boolean}
*/
eventSupported: function ( eventName ) {
var $search = this.$menu.find( '.tux-groupselector__filter__search__input' ),
isSupported = eventName in $search;
if ( !isSupported ) {
this.$element.setAttribute( eventName, 'return;' );
isSupported = typeof this.$element[ eventName ] === 'function';
}
return isSupported;
}
};
/*
* msggroupselector PLUGIN DEFINITION
*/
$.fn.msggroupselector = function ( options, groups ) {
return this.each( function () {
var $this = $( this ),
data = $this.data( 'msggroupselector' );
if ( !data ) {
$this.data( 'msggroupselector',
( data = new TranslateMessageGroupSelector( this, options, groups ) )
);
}
if ( typeof options === 'string' ) {
data[ options ].call( $this );
}
} );
};
$.fn.msggroupselector.Constructor = TranslateMessageGroupSelector;
$.fn.msggroupselector.defaults = {
language: 'en',
position: {
my: 'left top',
at: 'left-90 bottom+5'
}
};
/*
* Private functions
*/
/**
* Escape the search query for regex match
*
* @param {string} value A search string to be escaped.
* @return {string} Escaped string that is safe to use for a search.
*/
function escapeRegex( value ) {
return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' );
}
delay = ( function () {
var timer = 0;
return function ( callback, milliseconds ) {
clearTimeout( timer );
timer = setTimeout( callback, milliseconds );
};
}() );
}() );