Участник:Serhio Magpie/topCategories.js

/**
 * Top Categories
 * 
 * Author: Serhio Magpie
 * Licenses: MIT, CC BY-SA
 */

( function () {
	var _config = {
			linkContainer: $( '#contentSub' ),
			infoContainer: $( '.mw-parser-output' ),
			action: mw.config.get( 'wgAction' ),
			nameSpace: mw.config.get( 'wgNamespaceNumber' ),
			pageTitle: mw.config.get( 'wgPageName' ).replaceAll( '_', ' ' ),
			articlePath : mw.config.get( 'wgArticlePath' ),
			separator: ' | ',
			outputCount: 50
		},
		_strings = {
			'top-categories-label': 'Отобразить самые включаемые категории',
			'top-categories-link-self': 'по собственному числу',
			'top-categories-link-total': 'по общему числу',
			'top-categories-link-preliminary': 'предварительные результаты',
			'top-categories-status-count': 'Обработано категорий: $1 / $2',
			'top-categories-status-error': 'Ошибка при запросе, данные могут быть неполными! Ошибок: $1',
			'top-categories-header-self': 'Топ $1 самых включаемых категорий',
			'top-categories-header-total': 'Топ $1 самых включаемых категорий по общему числу',
			'top-categories-action-self': 'Отобразить по собственному числу',
			'top-categories-action-total': 'Отобразить по общему числу',
			'top-categories-members': '($1)',
			'top-categories-subcats': '$1 кат.',
			'top-categories-pages': '$1 с.'
		},
		_api,
		_nodes = {},
		_mode = 'self',
		_statusCount,
		_errorsCount,
		_category,
		_categories = {},
		_categoriesList = [];
		
	function mergeArrays( a, b ) {
		var c;
		if ( 'Set' in window ) {
			c = Array.from(
				new Set( [ ...a, ...b ] )
			);
		} else {
			c = a.slice();
			b.forEach( function( item ) {
				if( !c.includes( item ) ) {
					c.push( item );
				}
			} );
		}
		return c;
	}
  
	function Category( title, info ) {
		this.title = title;
		this.info = $.extend( {
            pages: 0,
            subcats: 0
		}, info );
		
		this.pages = [];
		this.pagesList = [];
		this.subcats = [];
		this.subcatsList = [];
		
		this.mode = 'subcat';
		this.hasInfo = !!info;
		this.hasPagesData = false;
		this.hasSubcatsData = false;
		
		this.isRequesting = false;
		
		if ( _categories[ this.title ] ) {
			return _categories[ this.title ];
		}
		
		_categories[ this.title ] = this;
		_categoriesList.push( this );
	}
  
	Category.prototype.request = function( mode ) {
		if ( this.isRequesting || this.checkData( mode ) ) {
			return true;
		}
		
		this.isRequesting = true;
		this.mode = [ 'subcat', 'all' ].includes( mode ) ? mode : 'subcat';
		
		return $.when( this.getInfo() )
			.then( this.getMembers.bind( this ) )
			.then( this.onSuccess.bind( this ) )
			.fail( this.onError.bind( this ) );
	};
	
	Category.prototype.checkData = function( mode ) {
		return ( mode === 'subcat' && this.hasSubcatsData )
			|| ( mode === 'all' && this.hasSubcatsData && this.hasPagesData );
	};
	
	Category.prototype.onSuccess = function() {
		this.isRequesting = false;
		renderStatus();
	};
	
	Category.prototype.onError = function() {
		this.isRequesting = false;
		renderError();
	};

	Category.prototype.getInfo = function() {
		if ( this.hasInfo ) {
			return true;
		}
		
		var params = {
			action: 'query',
			prop: 'categoryinfo',
			titles: this.title,
			redirects: true,
			format: 'json',
			formatversion: 2
		};
		return _api
			.get( params )
			.then( this.onGetInfoDone.bind( this ) )
			.fail( renderError );
	};
  
	Category.prototype.onGetInfoDone = function( data ) {
		if ( !data ) {
			return;
		}
		
		this.hasInfo = true;
		this.info = $.extend( this.info, data.query.pages[0].categoryinfo );
		
		if ( this.info.pages === 0 ) {
			this.hasPagesData = true;
		}
		if ( this.info.subcats === 0 ) {
			this.hasSubcatsData = true;
		}
		return true;
	};
  
	Category.prototype.getMembers = function() {
		if ( this.checkData( this.mode ) ) {
			return true;
		}
		
		var params = {
			action: 'query',
			prop: 'categoryinfo',
			generator: 'categorymembers',
			gcmtitle: this.title,
			gcmtype: this.mode === 'subcat' ? 'subcat' : undefined,
			gcmlimit: 500,
			gcmcontinue: this.continue,
			redirects: true,
			format: 'json',
			formatversion: 2
		};
		return _api
			.get( params )
			.then( this.onGetMembersDone.bind( this ) )
			.fail( renderError );
	};
  
	Category.prototype.onGetMembersDone = function( data ) {
		if ( !data ) {
			return;
		}
		
		var that = this;
		var promises = [];
		var members = data.query ? data.query.pages : [];
		members.forEach( function( item ) {
			if ( item.ns === 14 ) {
				var category = _categories[ item.title ];
				if( !category ) {
					category = new Category( item.title, item.categoryinfo );
				}
				promises.push( category.request( that.mode ) );
				if( !that.subcats.includes( category ) ) {
					that.subcats.push( category );
				}
				if( !that.subcatsList.includes( item.title ) ) {
					that.subcatsList.push( item.title );
				}
			} else {
				if( !that.pagesList.includes( item.title ) ) {
					that.pagesList.push( item.title );
				}
			}
		} );
		
		this.continue = data.continue ? data.continue.gcmcontinue : undefined;
		if ( this.continue ) {
			if ( promises.length > 0 ) {
				return $.when
					.apply( $, promises )
					.always( this.getMembers.bind( this ) );
			}
			return this.getMembers();
		} else {
			this.hasSubcatsData = true;
			if ( this.mode === 'all' ) {
				this.hasPagesData = true;
			}
		}
		
		if ( promises.length > 0 ) {
			return $.when.apply( $, promises );
		}
		
		return true;
	};
  
	function init() {
		if ( _config.nameSpace !== 14 || _config.action !== 'view' ) {
			return;
		}
		
		// Config
		_api = new mw.Api();
		
		// Set interface strings
		mw.messages.set( _strings );
		
		// Render structure
		_nodes.$container = $( '<div>' )
			.addClass( 'hlist' )
			.css( 'margin-top', '0.5em' )
			.appendTo( _config.linkContainer );
		
		_nodes.$ul = $( '<dl>' )
			.appendTo( _nodes.$container );
		
		_nodes.$li = $( '<li>' )
			.text( mw.msg( 'top-categories-label' ) )
			.appendTo( _nodes.$ul );
		
		_nodes.$ulLinks = $( '<ul>' )
			.appendTo( _nodes.$li );
		
		_nodes.$liSelf = $( '<li>' )
			.appendTo( _nodes.$ulLinks );
		
		_nodes.$linkSelf = $( '<a>' )
			.text( mw.msg( 'top-categories-link-self' ) )
			.attr( 'role', 'button' )
			.attr( 'tabindex', '0' )
			.on( 'click', function() {
				_mode = 'self';
				request();
			} )
			.appendTo( _nodes.$liSelf );
		
		_nodes.$liTotal = $( '<li>' )
			.appendTo( _nodes.$ulLinks );
		
		_nodes.$linkTotal = $( '<a>' )
			.text( mw.msg( 'top-categories-link-total' ) )
			.attr( 'role', 'button' )
			.attr( 'tabindex', '0' )
			.on( 'click', function() {
				_mode = 'total';
				request();
			} )
			.appendTo( _nodes.$liTotal );
		
		_nodes.$content = $( '<div>' )
			.css( 'clear', 'both' )
			.appendTo( _config.infoContainer );
	}
  
	function request() {
		_statusCount = -1;
		_errorsCount = 0;
		
		if ( _nodes.$statusError ) {
			_nodes.$statusError.remove();
			_nodes.$statusError = null;
		}
		
		if ( !_nodes.$status ) {
			_nodes.$status = $( '<li>' )
				.css( 'font-style', 'italic' )
				.appendTo( _nodes.$ul );
		}
		renderStatus();
		
		if( !_category ){
			_category = new Category( _config.pageTitle );
		}
		
		var requestMode = _mode === 'total' ? 'all' : 'subcat';
		$.when( _category.request( requestMode ) )
			.always( renderInfo )
			.fail( renderError );
	}
	
	function renderInfo() {
		if ( _mode === 'total' ) {
			renderInfoTotal();
		} else {
			renderInfoSelf();
		}
	}
  
	function renderInfoSelf() {
		var outputCount = Math.min( _categoriesList.length, _config.outputCount );
		
        var $title = $( '<h2>' )
			.text( mw.msg( 'top-categories-header-self', _config.outputCount ) );
        var $columns = $ ( '<div>' )
			.addClass( 'columns' )
			.css( 'column-count', '3' );
		var $list = $( '<ol>' )
			.appendTo( $columns );
			
		_categoriesList.sort( function( a, b ) {
			return b.info.pages - a.info.pages;
		} );
		
		for( var i = 0; i < outputCount; i++ ) {
			$list.append(
				renderLineSelf( _categoriesList[i] )
			);
		}
		
		_nodes.$content
			.empty()
			.append( $title )
			.append( $columns );
	}
  
	function renderLineSelf( item ) {
		var label = item.title.split( ':' ).pop();
		var href = _config.articlePath.replace( '$1', item.title );
		var pagesMessage = mw.msg( 'top-categories-pages', item.info.pages );
		var subcatsMessage = item.info.subcats > 0 ? mw.msg( 'top-categories-subcats', item.info.subcats ) : null;
		var message = pagesMessage + ( subcatsMessage ? ', ' + subcatsMessage : '' );
			
		var $li = $( '<li>' );
		var $link = $( '<a>' )
			.attr( 'href', href )
			.text( label )
			.appendTo( $li );
		var $sep = $( document.createTextNode(' ') )
			.appendTo( $li );
		var $count  = $( '<span>' )
			.text(	mw.msg( 'top-categories-members', message ) )
			.appendTo( $li );
				
		return $li;
	}
  
	function renderInfoTotal() {
		var outputCount = Math.min( _categoriesList.length, _config.outputCount );
		var $title = $( '<h2>' )
			.text( mw.msg( 'top-categories-header-total', _config.outputCount ) );
		var $titleBeta = $( '<sup title="Бета">(β)</sup>' )
			.appendTo( $title );
		var $columns = $ ( '<div>' )
			.addClass( 'columns' )
			.css( 'column-count', '3' );
		var $list = $( '<ol>' )
			.appendTo( $columns );
		
		calculateTotalMemebrs( [ _category ] );
		_categoriesList.sort( function( a, b ) {
			return b.info.pagesTotal - a.info.pagesTotal;
		} );
		
		for( var i = 0; i < outputCount; i++ ) {
			$list.append(
				renderLineTotal( _categoriesList[i] )
			);
		}
		
		_nodes.$content
			.empty()
			.append( $title )
			.append( $columns );
	}
  
	function renderLineTotal( item ) {
		var label = item.title.split( ':' ).pop();
		var href = _config.articlePath.replace( '$1', item.title );
		var pagesMessage = mw.msg( 'top-categories-pages', item.info.pagesTotal );
		var subcatsMessage = item.info.subcatsTotal > 0 ? mw.msg( 'top-categories-subcats', item.info.subcatsTotal ) : null;
		var message = pagesMessage + ( subcatsMessage ? ', ' + subcatsMessage : '' );
			
		var $li = $( '<li>' );
		var $link = $( '<a>' )
			.attr( 'href', href )
			.text( label )
			.appendTo( $li );
		var $sep = $( document.createTextNode(' ') )
			.appendTo( $li );
		var $count  = $( '<span>' )
			.text(	mw.msg( 'top-categories-members', message ) )
			.appendTo( $li );
		
		return $li;
	}
	
	function calculateTotalMemebrs( list ) {
		var total = {
			'pagesList' : [],
			'subcatsList' : []
		};
		
		list.forEach( function( item ) {
			if ( !item.pagesTotalList ) {
				item.pagesTotalList = [];
				
				if ( item.pagesList.length > 0 ) {
					item.pagesTotalList = item.pagesList.slice();
				}
				
				if ( !item.subcatsTotalList ) {
					item.subcatsTotalList = [];
					
					if ( item.subcatsList.length > 0 ) {
						item.subcatsTotalList = item.subcatsList.slice();
						var itemTotal = calculateTotalMemebrs( item.subcats );
						
						item.pagesTotalList = mergeArrays( item.pagesTotalList, itemTotal.pagesList );
						item.subcatsTotalList = mergeArrays( item.subcatsTotalList, itemTotal.subcatsList );
					}
				}
				
				item.info.pagesTotal = item.pagesTotalList.length;
				item.info.subcatsTotal = item.subcatsTotalList.length;
			}
			
			total.pagesList = mergeArrays( total.pagesList, item.pagesTotalList );
			total.subcatsList = mergeArrays( total.subcatsList, item.subcatsTotalList );
		} );
		
		return total;
	}
	
	function renderStatus() {
		_statusCount++;
		
		_nodes.$status
			.empty()
			.text( mw.msg( 'top-categories-status-count', _statusCount, _categoriesList.length ) );
			
		return _nodes.$status;
	}
	
	function renderError() {
		_errorsCount++;
		
		if ( !_nodes.$statusError ) {
			_nodes.$statusError = $( '<li>' )
				.text( mw.msg( 'top-categories-status-error', _errorsCount ) )
				.addClass( 'error' )
				.css( 'font-size', 'inherit' )
				.appendTo( _nodes.$ul );
		}
		
		return _nodes.$statusError;
	}
  
	mw.hook( 'wikipage.content' ).add( init );
} )();