summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js')
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js633
1 files changed, 633 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js b/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js
new file mode 100644
index 00000000..eda0a43b
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/js/ext.translate.groupselector.js
@@ -0,0 +1,633 @@
+( function () {
+ 'use strict';
+
+ var groupsLoader, delay;
+
+ /**
+ * options
+ * - position: accepts same values as jquery.ui.position
+ * - onSelect:
+ * - language:
+ * - preventSelector: boolean to load but not show the group selector.
+ * - recent: list of recent group ids
+ * groups: list of message group ids
+ *
+ * @param {Element} element
+ * @param {Object} options
+ * @param {Object} [options.position] Accepts same values as jquery.ui.position.
+ * @param {Function} [options.onSelect] Callback with message group id when selected.
+ * @param {string} options.language Language code for statistics.
+ * @param {boolean} [options.preventSelector] Whether not to show the group selector.
+ * @param {string[]} [options.recent] List of recent message group ids.
+ * @param {string[]} [groups] List of message group ids to show.
+ */
+ function TranslateMessageGroupSelector( element, options, groups ) {
+ this.$trigger = $( element );
+ this.$menu = null;
+ this.$search = null;
+ this.$list = null;
+ this.$loader = null;
+
+ this.parentGroupId = null;
+ this.options = $.extend( true, {}, $.fn.msggroupselector.defaults, options );
+ // Store the explicitly given options, which can be passed to subgroup
+ // selectors.
+ this.customOptions = options;
+ this.flatGroupList = null;
+ this.groups = groups;
+ this.firstShow = true;
+
+ this.init();
+ }
+
+ TranslateMessageGroupSelector.prototype = {
+ constructor: TranslateMessageGroupSelector,
+
+ /**
+ * Initialize the plugin
+ */
+ init: function () {
+ this.parentGroupId = this.$trigger.data( 'msggroupid' );
+ this.prepareSelectorMenu();
+ this.listen();
+ },
+
+ /**
+ * Prepare the selector menu rendering
+ */
+ prepareSelectorMenu: function () {
+ var $listFilters,
+ $listFiltersGroup,
+ $search,
+ $searchIcon,
+ $searchGroup;
+
+ this.$menu = $( '<div>' )
+ .addClass( 'tux-groupselector' )
+ .addClass( 'grid' );
+
+ $searchIcon = $( '<div>' )
+ .addClass( 'two columns tux-groupselector__filter__search__icon' );
+
+ this.$search = $( '<input>' )
+ .prop( 'type', 'text' )
+ .addClass( 'tux-groupselector__filter__search__input' );
+
+ if ( mw.translate.isPlaceholderSupported( this.$search ) ) {
+ this.$search.prop( 'placeholder', mw.msg( 'translate-msggroupselector-search-placeholder' ) );
+ }
+
+ $search = $( '<div>' )
+ .addClass( 'ten columns' )
+ .append( this.$search );
+
+ $listFilters = $( '<div>' )
+ .addClass( 'tux-groupselector__filter__tabs' )
+ .addClass( 'six columns' )
+ .append(
+ $( '<div>' )
+ .addClass( 'tux-grouptab tux-grouptab--all tux-grouptab--selected' )
+ .text( mw.msg( 'translate-msggroupselector-search-all' ) )
+ );
+
+ if ( this.options.recent && this.options.recent.length ) {
+ $listFilters.append(
+ $( '<div>' )
+ .addClass( 'tux-grouptab tux-grouptab--recent' )
+ .text( mw.msg( 'translate-msggroupselector-search-recent' ) )
+ );
+ }
+
+ $searchGroup = $( '<div>' )
+ .addClass( 'tux-groupselector__filter__search' )
+ .addClass( 'six columns' )
+ .append( $searchIcon, $search );
+
+ $listFiltersGroup = $( '<div>' )
+ .addClass( 'tux-groupselector__filter' )
+ .addClass( 'row' )
+ .append( $listFilters, $searchGroup );
+
+ this.$list = $( '<div>' )
+ .addClass( 'tux-grouplist' )
+ .addClass( 'row' );
+
+ this.$loader = $( '<div>' )
+ .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 = $( '<span>' )
+ .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 (<div>).
+ */
+ prepareMessageGroupRow: function ( messagegroup ) {
+ var $row,
+ $icon,
+ $label,
+ $statsbar,
+ $subGroupsLabel,
+ style = '';
+
+ $row = $( '<div>' )
+ .addClass( 'row tux-grouplist__item' )
+ .attr( 'data-msggroupid', messagegroup.id )
+ .data( 'msggroup', messagegroup );
+
+ $icon = $( '<div>' )
+ .addClass( 'tux-grouplist__item__icon' )
+ .addClass( 'one column' );
+
+ $statsbar = $( '<div>' ).languagestatsbar( {
+ language: this.options.language,
+ group: messagegroup.id
+ } );
+
+ $label = $( '<div>' )
+ .addClass( 'tux-grouplist__item__label' )
+ .addClass( 'seven columns' )
+ .append(
+ $( '<span>' )
+ // 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 = $( '<div>' )
+ .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 );
+ };
+ }() );
+}() );