MediaWiki:Gadget-navboxFeaturedArticles.js

З Вікіпедыі, свабоднай энцыклапедыі

Заўвага: Пасля апублікавання, вам можа спатрэбіцца ачыстка кэшу браўзера, каб убачыць унесеныя змены.

  • Firefox / Safari: націсніце Reload, утрымліваючы Shift, або націсніце Ctrl-F5 ці Ctrl-R (⌘-R на Макінтошах)
  • Google Chrome: Націсніце Ctrl-Shift-R (⌘-Shift-R на Mac)
  • Internet Explorer / Edge: націсніце Refresh, утрымліваючы Ctrl, або націсніце Ctrl-F5
  • Opera: Увайдзіце Menu → Settings (Opera → Preferences на Mac), далей Privacy & security → Clear browsing data → Cached images and files.
/**
 * Navbox Featured Articles
 * transferred from ruwiki (https://ru.wikipedia.org/wiki/MediaWiki:Gadget-navboxFeaturedArticles.js)
 * 
 * Author: Serhio Magpie
 * Licenses: MIT, CC BY-SA
 */

// <nowiki>

( function () {
	var _config = {
			name: 'gadget-navboxFeaturedArticles',
			optionsName: 'userjs-navboxFeaturedArticles-options',
			pagename: 'MediaWiki:Gadget-navboxFeaturedArticles.js',
			wikilink: '[[MediaWiki:Gadget-navboxFeaturedArticles.js]]',
			wdAPI: 'https://www.wikidata.org/w/api.php',
			wdLimit: 50,
			wgDBname: mw.config.get( 'wgDBname' ),
			wgPageName: mw.config.get( 'wgPageName' ),
			commonsThumb: 'https://upload.wikimedia.org/wikipedia/commons/',
			navboxSelector: '.navbox:not([data-name="External links"])',
			gearSelector: '%navbox% .navbox-gear',
			linkQuoteRegExp: /('|‘|"|«|“|„)$/,
			linkSelector: '%navbox% a',
			linkExcludeSelector: '%navbox% [data-nfa-badge="0"] a',
			linkExcludeClasses: [ 'new', 'extiw', 'external', 'userlink' ],
			badgeSelector: '%navbox% a[title="%title%"], %navbox% sup[title="%title%"]',
			badgeSpaceCharacter: '\u2060',
			badgeSizeMin: 8,
			badgeSizeMax: 16,
			badges: {
				Q17437796: {
					id: 'Q17437796',
					name: 'featured',
					title: 'Выдатны артыкул',
					link: 'Вікіпедыя:Выдатныя артыкулы',
					image: {
						default: '8/8b/Small_Skew_Star_SVG.svg',
						modern: '0/08/Yellow_star_unboxed.svg'
					},
					alt: '✯'
				},
				Q17437798: {
					id: 'Q17437798',
					name: 'good',
					title: 'Добры артыкул',
					link: 'Вікіпедыя:Добрыя артыкулы',
					image: {
						default: '2/20/Blue_star_unboxed.svg',
						modern: '2/20/Blue_star_unboxed.svg'
					},
					alt: '✯'
				},
				Q17506997: {
					id: 'Q17506997',
					name: 'list',
					title: 'Выдатны спіс',
					link: 'Вікіпедыя:Выдатныя спіс',
					image: {
						default: '2/22/Feat_lists.svg',
						modern: '2/22/Feat_lists.svg'
					},
					alt: '≛'
				},
				AOFY: {
					id: 'AOFY',
					name: 'aofy',
					title: 'Артыкул года',
					link: 'Вікіпедыя:Артыкулы года',
					image: {
						default: '/2/28/Статьи_года_Статья_года.svg',
						modern: '/2/28/Статьи_года_Статья_года.svg'
					},
					alt: '✯'
				}
			}
		},
		_strings = {
			'nfa-error-link-title': '$1: імя спасылкі некарэктнае: $2',
			'nfa-error-badge-link-title': '$1: імя статуснай спаылки некарэктнае: $2',
			'nfa-error-user-settings-read': '$1: не атрымалася загрузіць настройкі ўдзельніка',
			'nfa-template-view': 'Перайсці да шаблона',
			'nfa-template-settings': 'Наладзіць адлюстраванне статусаў',
			'nfa-settings-title': 'Іконкі статусных артыкулаў',
			'nfa-settings-save': 'Захаваць',
			'nfa-settings-cancel': 'Адмена',
			'nfa-settings-enable': 'Уключыць',
			'nfa-settings-disable': 'Адключыць',
			'nfa-settings-done': 'Гатова',
			'nfa-settings-good': 'Паказваць добрыя артыкулы',
			'nfa-settings-featured': 'Паказваць выдатныя артыкулы',
			'nfa-settings-list': 'Паказваць выдатныя спісы',
			'nfa-settings-style' : 'Стыль іконак',
			'nfa-settings-style-default' : 'Класічны',
			'nfa-settings-style-modern' : 'Сучасны',
			'nfa-settings-size': 'Памер іконкі',
			'nfa-settings-error': 'Не атрымалася захаваць настройкі!'
		},
		_options = {
			enabled: true,
			good: true,
			featured: true,
			list: true,
			aofy: false,
			style: 'modern',
			size: 12
		},
		_userOptions = {},
		_gears = [],
		_pages = {},
		_alias = {},
		_links = [],
		_badgeNodes = [],
		_$context,
		_isNavbox,
		_settingsDialog;
  
	/******* COMMON *******/

	function inDOM( $node ) {
		var node = $node.get( 0 );
		return $.contains( document.documentElement, node );
	}

	/******* PAGE *******/

	function Page ( title, isRedirect ) {
		this.title = title;
		this.isRedirect = isRedirect;
		this.isProcessed = false;
		this.hasBadge = false;
		this.links = [];

		_pages[this.title] = this;
	}

	Page.prototype.setProcessed = function ( status ) {
		this.isProcessed = status;
	};

	Page.prototype.addLink = function ( link ) {
		this.links.push( link );
	};

	Page.prototype.resolveRedirect = function ( title ) {
		this.origTitle = this.title;
		this.title = title;
		this.isRedirect = false;

		var page = _pages[this.title];
		if ( page ) {
			_alias[this.origTitle] = page;
			$( this.links ).each( function () {
				this.setParent( page );
			} );
		} else {
			_alias[this.origTitle] = this;
			_pages[this.title] = this;
		}

		delete _pages[this.origTitle];
	};

	Page.prototype.setBadge = function ( badge ) {
		this.badge = badge;
		this.hasBadge = true;
	};

	Page.prototype.renderLinkBadges = function () {
		var that = this;

		if ( !that.hasBadge ) {
			return;
		}

		$( that.links ).each( function () {
			var link = this;
			if ( !link.hasBadge ) {
				link.setBadge( that.badge );
			} else {
				link.toggleBadge();
			}
		} );
	};

	/******* LINK *******/

	function Link( $node ) {
		this.$node = $node;
		this.isRedirect = $node.hasClass( 'mw-redirect' );
		this.isSelf = $node.hasClass( 'selflink' );
		this.hasBadge = false;
		this.title = this.isSelf ? _config.wgPageName : $node.attr( 'title' );

		// Skip new and external links from processing
		var hasExcludeClass = _config.linkExcludeClasses.some( function ( className ) {
			return $node.hasClass( className );
		} );
		if ( hasExcludeClass ) {
			return;
		}

		// Skip images
		var $children = $node.children( 'img' );
		if ( $children.length !== 0 ) {
			return;
		}

		// Skip links with empty titles
		if ( typeof this.title === 'undefined' || this.title.length === 0 ) {
			return;
		}

		// Normalize title
		try {
			var title = new mw.Title( this.title );
			this.title = title.getPrefixedText();
			this.namespace = title.getNamespaceId();
		} catch ( e ) {
			console.log( mw.msg( 'nfa-error-link-title', _config.wikilink, this.title ) );
		}

		// Process only main namespace
		if ( typeof this.namespace === 'undefined' || this.namespace !== 0 ) {
			return;
		}

		// Find parent page object or construct it
		var page = _pages[this.title];
		if ( !page ) {
			page = _alias[this.title];
		}
		if ( !page ) {
			page = new Page( this.title, this.isRedirect );
		}
		this.setParent( page );

		_links.push( this );
	}

	Link.prototype.setParent = function ( page ) {
		this.parent = page;
		this.parent.addLink( this );
	};

	Link.prototype.setBadge = function ( badge ) {
		this.badge = $.extend( {}, badge );
		this.hasBadge = true;
		this.toggleBadge();
	};

	Link.prototype.toggleBadge = function () {
		if ( this.isBadgeShown ) {
			this.detachBadge();
		}
		if ( _userOptions[this.badge.name] ) {
			this.renderBadge();
		}
	};

	Link.prototype.renderBadge = function () {
		var imageStyle = this.badge.image[_userOptions.style] ? _userOptions.style : 'default';

		this.badge.$image = $( '<img>' )
			.attr( 'height', _userOptions.size )
			.attr( 'alt', this.badge.alt )
			.attr( 'src', this.badge.image[imageStyle] );

		this.badge.$link = $( '<a>' )
			.addClass( 'nfa__badge' )
			.addClass( [ 'nfa__badge', this.badge.name ].join( '--' ) )
			.attr( 'title', this.badge.title )
			.attr( 'href', this.badge.url )
			.append( this.badge.$image );

		this.appendBadge();

		this.badge.link = this.badge.$link.get( 0 );
		_badgeNodes.push( this.badge.link );
	};

	Link.prototype.appendBadge = function () {
		var beforeSibling, beforeContent,
			$node = this.$node,
			sibling = this.getSibling( $node ),
			spacer;

		this.isBadgeShown = true;

		// If closest previous sibling is not exists, check parent node
		if ( !sibling ) {
			$node = $node.parent();
			sibling = this.getSibling( $node  );
		}

		// Insert bedge before closest to link quote character
		if ( sibling && sibling.isTextNode && sibling.quote ) {
			$node = $( sibling.node );
			// Split previous sibling text line if contains not only one quote character
			if ( sibling.content.length > 1 ) {
				sibling.node.textContent = sibling.quote[0];
				beforeContent = sibling.content.replace( _config.linkQuoteRegExp, '' );
				beforeSibling = document.createTextNode( beforeContent );
				$node.before( beforeSibling );
			}
		} else {
			$node = this.$node;
		}
		$node.before( this.badge.$link );

		// Add non-breaking space character between badge and link node
		this.badge.spacer = document.createTextNode( _config.badgeSpaceCharacter );
		$node.before( this.badge.spacer );
	};

	Link.prototype.getSibling = function ( $node ) {
		var node = $node.get( 0 ).previousSibling,
			sibling = {};

		if ( !node ) {
			return;
		}

		sibling.node = node,
		sibling.content = sibling.node && sibling.node.textContent || '',
		sibling.isTextNode = sibling.node && sibling.node.nodeType === Node.TEXT_NODE,
		sibling.quote = sibling.content.match( _config.linkQuoteRegExp );

		return sibling;
	};

	Link.prototype.detachBadge = function () {
		var index;

		if ( this.isBadgeShown ) {
			this.isBadgeShown = false;

			this.badge.$link.remove();
			$( this.badge.spacer ).remove();

			index = _badgeNodes.indexOf( this.badge.link );
			if ( index > -1 ) {
				_badgeNodes.splice( index, 1 );
			}
		}
	};

	/******* NAVBOX GEAR *******/

	function Gear( $node ) {
		this.$node = $node;
		this.$link = this.$node.find( 'a' );
		this.href = this.$link.attr( 'href' );

		this.renderMenu();

		_gears.push( this );
	}

	Gear.prototype.renderMenu = function () {
		var that = this,
			collapsible; 

		this.$node
			.addClass( 'nfa__menu' );
		this.$link
			.addClass( 'nfa__toggle mw-collapsible-toggle' );
		this.$menu = $( '<span>' )
			.addClass( 'nfa__dropdown mw-collapsible-content' )
			.insertAfter( this.$link );
		this.$menuList = $( '<ul>' )
			.appendTo( this.$menu );

		this.renderMenuItem( mw.msg( 'nfa-template-view' ), this.href )
			.appendTo( this.$menuList );
		this.renderMenuItem( mw.msg( 'nfa-template-settings' ), null, this.openSettingsDialog.bind( this ) )
			.appendTo( this.$menuList );

		this.$node.makeCollapsible( {
			collapsed: true
		} );

		// Unbind collapsible events and handle them manually
		this.$link
			.off( 'click' )
			.on( 'click', function ( event ) {
				event.preventDefault();
				collapsible = that.getCollapsible();
				if ( collapsible ) {
					collapsible.toggle();
				}
			} )
	};

	Gear.prototype.renderMenuItem = function ( label, href, callback ) {
		var li, link, span;

		li = $( '<li>' );
		link = $( '<a>' )
			.attr( 'href', href )
			.appendTo( li );
		span = $( '<span>' )
			.addClass( 'mw-ui-button mw-ui-quiet' )
			.text( label )
			.appendTo( link );

		if ( callback ) {
			link.on( 'click', function ( event ) {
				event.preventDefault();
				callback( event );
			} );
		}

		return li;
	}
	
	Gear.prototype.getCollapsible = function() {
		return this.$node.data( 'mw-collapsible' );
	};

	Gear.prototype.showMenu = function () {
		var collapsible = this.getCollapsible();
		if ( collapsible ) {
			collapsible.expand();
		}
	};

	Gear.prototype.hideMenu = function () {
		var collapsible = this.getCollapsible();
		if ( collapsible ) {
			collapsible.collapse();
		}
	};

	Gear.prototype.openSettingsDialog = function () {
		if ( !_settingsDialog ) {
			_settingsDialog = new SettingsDialogHelper( this );
		} else {
			_settingsDialog.open();
		}
	};
  
	Gear.prototype.isInDOM = function () {
		return inDOM( this.$node );
	};

	/******* REDIRECT RESOLVER *******/

	function RedirectResolver( pages ) {
		this.pages = pages;
		this.titles = Object.keys( this.pages );
	}

	RedirectResolver.prototype.get = function () {
		var api = new mw.Api(),
			params = {
				action: 'query',
				titles: this.titles,
				redirects: true,
				formatversion: 2
			};

		this.promise = api
			.post( params )
			.done( this.onDone.bind( this ) );

		return this.promise;
	};

	RedirectResolver.prototype.onDone = function ( data ) {
		var that = this;

		if ( !data.query || !data.query.redirects ) {
			return;
		}

		$( data.query.redirects ).each( function () {
			var item = this,
				page = that.pages[item.from];

			if ( page ) {
				page.resolveRedirect( item.to );
			}
		} );
	};

	/******* BADGE REQUEST *******/

	function BadgeRequest( titles ) {
		this.titles = titles;
	}

	BadgeRequest.prototype.get = function () {
		var api = new mw.ForeignApi( _config.wdAPI ),
			params = {
				action: 'wbgetentities',
				sites: _config.wgDBname,
				sitefilter: _config.wgDBname,
				titles: this.titles,
				props: 'sitelinks',
				redirects: 'yes',
				formatversion: 2
			};

		this.promise = api
			.post( params )
			.done( this.onDone.bind(this) );

		return this.promise;
	};

	BadgeRequest.prototype.onDone = function ( data ) {
		if ( !data.entities ) {
			return;
		}

		$.each( data.entities, function ( qid, entity ) {
			if ( entity.sitelinks && entity.sitelinks[_config.wgDBname] ) {
				var item = entity.sitelinks[_config.wgDBname],
					page = _pages[item.title];
				if ( page ) {
					$( item.badges ).each( function () {
						var badge = _config.badges[this];
						if ( badge ) {
							page.setBadge( badge );
							return false;
						}
					} );
				}
			}
		} );
	};

	/******* SETTINGS DIALOG ******* */

	function SettingsDialog() {
		SettingsDialog.parent.call( this );
	}

	function SettingsDialogHelper( gear ) {
		this.gear = gear;
		this.isDestroyed = false;
		this.isLoaded = false;
		this.isOpen = false
		this.isOpening = false;

		mw.loader.using( [ 'oojs', 'oojs-ui' ], this.load.bind( this ) );
	}

	SettingsDialogHelper.prototype.load = function () {
		var helper = this;
		helper.isLoaded = true;

		// Configure Setting Dialog
		OO.inheritClass( SettingsDialog, OO.ui.ProcessDialog );
		SettingsDialog.static.name = 'settingsDialog';
		SettingsDialog.static.title = mw.msg( 'nfa-settings-title' );
		SettingsDialog.static.actions = [
			{
				action: 'save',
				label: mw.msg( 'nfa-settings-save' ),
				flags: [ 'primary', 'progressive' ]
			},
			{
				label: mw.msg( 'nfa-settings-cancel' ),
				flags: 'safe'
			}
		];

		SettingsDialog.prototype.initialize = function () {
			SettingsDialog.super.prototype.initialize.apply( this, arguments );

			// Panel Settings
			this.enableOption = new OO.ui.RadioOptionWidget( {
				label: mw.msg( 'nfa-settings-enable' )
			} );
			this.disableOption = new OO.ui.RadioOptionWidget( {
				label: mw.msg( 'nfa-settings-disable' )
			} );
			this.enableSelect = new OO.ui.RadioSelectWidget( {
				items: [ this.enableOption, this.disableOption ],
				classes: [ 'nfa__enable-select' ]
			} );
			this.enableSelect.on( 'choose', this.onEnableSelectChoose.bind( this ) );
			this.enableSelect.selectItem( _userOptions.enabled ? this.enableOption : this.disableOption );

			this.goodSelect = new OO.ui.CheckboxInputWidget( {
				selected: _userOptions.good,
				disabled: !_userOptions.enabled
			} );
			this.goodField = new OO.ui.FieldLayout( this.goodSelect, {
				label: mw.msg( 'nfa-settings-good' ),
				align: 'inline'
			} );

			this.featuredSelect = new OO.ui.CheckboxInputWidget( {
				selected: _userOptions.featured,
				disabled: !_userOptions.enabled
			} );
			this.featuredField = new OO.ui.FieldLayout( this.featuredSelect, {
				label: mw.msg( 'nfa-settings-featured' ),
				align: 'inline'
			} );

			this.listSelect = new OO.ui.CheckboxInputWidget( {
				selected: _userOptions.list,
				disabled: !_userOptions.enabled
			} );
			this.listField = new OO.ui.FieldLayout( this.listSelect, {
				label: mw.msg( 'nfa-settings-list' ),
				align: 'inline'
			} );

			this.styleInput = new OO.ui.DropdownInputWidget( {
				options: [
					{
						data: 'default',
						label: mw.msg( 'nfa-settings-style-default' )
					},
					{
						data: 'modern',
						label: mw.msg( 'nfa-settings-style-modern' )
					}
				],
				value: _userOptions.style,
				disabled: !_userOptions.enabled,
				classes: [ 'nfa__style-input' ]
			} );
			this.styleField = new OO.ui.FieldLayout( this.styleInput, {
				label: mw.msg( 'nfa-settings-style' ),
				align: 'inline'
			} );

			this.sizeInput = new OO.ui.NumberInputWidget( {
				disabled: !_userOptions.enabled,
				input: { value: _userOptions.size },
				step: 1,
				min: _config.badgeSizeMin,
				max: _config.badgeSizeMax,
				classes: [ 'nfa__size-input' ]
			} );
			this.sizeField = new OO.ui.FieldLayout( this.sizeInput, {
				label: mw.msg( 'nfa-settings-size' ),
				align: 'top'
			} );

			this.fieldsetLayout = new OO.ui.FieldsetLayout();
			this.fieldsetLayout.addItems( [
			    this.featuredField,
			    this.goodField,
			    this.listField,
			    this.styleField,
			    this.sizeField
			] );

			this.panelLayout = new OO.ui.PanelLayout( {
				padded: true,
				expanded: false
			} );
			this.panelLayout.$element.append(
				this.enableSelect.$element,
				$( '<hr>' ).addClass( 'nfa__options-form-separator' ),
				this.fieldsetLayout.$element
			);

			this.$body.append( this.panelLayout.$element );
		};

		SettingsDialog.prototype.onEnableSelectChoose = function ( item ) {
			var isDisabled = item === this.disableOption;

			this.goodSelect.setDisabled( isDisabled );
			this.featuredSelect.setDisabled( isDisabled );
			this.listSelect.setDisabled( isDisabled );
			this.styleInput.setDisabled( isDisabled );
			this.sizeInput.setDisabled( isDisabled );
		};

		SettingsDialog.prototype.getActionProcess = function ( action ) {
			var dialog = this;

			if ( action === 'save' ) {
				return new OO.ui.Process( function () {
					return dialog.saveAction();
				} );
			}

			return SettingsDialog.parent.prototype.getActionProcess.call( this, action );
		};

		SettingsDialog.prototype.saveAction = function () {
			var options,
				dialog = this;

			dialog.pushPending();

			options = {
				enabled: dialog.enableOption.isSelected(),
				good: dialog.goodSelect.isSelected(),
				featured: dialog.featuredSelect.isSelected(),
				list: dialog.listSelect.isSelected(),
				style: dialog.styleInput.getValue(),
				size: dialog.sizeInput.getValue()
			};

			return helper.save( options )
				.always( function () {
					dialog.popPending();
				} )
				.fail( function () {
					dialog.showErrors(
						new OO.ui.Error( mw.msg( 'nfa-settings-error' ) )
					);
				} )
				.done( function () {
					helper.apply( options );
					dialog.close();
				} );
		};

		SettingsDialog.prototype.getBodyHeight = function () {
			return this.panelLayout.$element.outerHeight( true );
		};

		// Configure Windows Manager
		if ( !helper.windowManager ) {
			helper.windowManager = new OO.ui.WindowManager();
			$( document.body ).append( helper.windowManager.$element );
		}

		helper.open();
	};

	SettingsDialogHelper.prototype.open = function () {
		var that = this;

		if( that.isLoaded && !that.isOpen && !that.isOpening ) {
			that.isOpening = true;

			that.dialog = new SettingsDialog();
			that.windowManager.addWindows( [ that.dialog ] );
			that.settingsWindow = that.windowManager.openWindow( that.dialog );
			that.settingsWindow.opened.then( function () {
				that.isOpen = true;
				that.isOpening = false;
			} );
			that.settingsWindow.closed.then( function () {
				that.isOpen = false;
				that.windowManager.clearWindows();
			} );
		}
	};

	SettingsDialogHelper.prototype.close = function () {
		this.dialog.close();
	};

	SettingsDialogHelper.prototype.save = function ( options ) {
		options = $.extend( _userOptions, options );

		if ( options.size < _config.badgeSizeMin || options.size > _config.badgeSizeMax ) {
			options.size = _userOptions.size;
		}

		var api = new mw.Api(),
			params = {
				action: 'options',
				optionname: _config.optionsName,
				optionvalue: JSON.stringify( options )
			};

		return api.postWithEditToken( api.assertCurrentUser( params ) );
	};

	SettingsDialogHelper.prototype.apply = function ( options ) {
		_userOptions = $.extend( _userOptions, options );

		mw.user.options.set( _config.optionsName, JSON.stringify( _userOptions ) );

		if ( _userOptions.enabled ) {
			processBadges();
		} else {
			detachBadges();
		}
	};

	/******* MAIN *******/
  
	function garbage() {
		// Clear dead gears
		_gears = _gears.filter( function( gear ) {
			return gear.isInDOM(); 
		} );
	}

	function prepare() {
		var options, title;

		// Set interface strings
		mw.messages.set( _strings );

		// Get user options
		try {
			options = JSON.parse( mw.user.options.get( _config.optionsName ) );
		} catch ( e ) {
			console.log( mw.msg( 'nfa-error-user-settings-read', _config.wikilink ) );
		}
		_userOptions = $.extend( _options, options );

		// Normalize badge config
		$.each( _config.badges, function ( id, item ) {
			// Normalize link url
			try {
				title = new mw.Title( item.link );
				item.url = title.getUrl();
			} catch ( e ) {
				console.log( mw.msg( 'nfa-error-badge-link-title', _config.wikilink, item.title ) );
			}
			// Normalize image url
			$.each( item.image, function ( name, value ) {
				item.image[name] = _config.commonsThumb + value;
			} );
		} );

		// Add body click event to carry dropdown menus
		dropdownMenuHelper();
	}

	function init( $content ) {
		var $nodes, $excludeNodes, selector, excludeSelector, $node;

		if ( !$content || $content.length === 0 ) {
			return;
		}

		_$context = $content;
		_isNavbox = _$context.hasClass( 'navbox' );

		// Find gear icons in navboxes
		selector = _config.gearSelector.replace( '%navbox%', ( _isNavbox ? '' : _config.navboxSelector ) ).trim();
		$nodes = _$context.find( selector );
		$nodes.each( function () {
			$node = $( this );
			new Gear( $node );
		} );

		// Find links in navboxes
		selector = _config.linkSelector.replace( '%navbox%', ( _isNavbox ? '' : _config.navboxSelector ) ).trim();
		$nodes = _$context.find( selector );
		excludeSelector = _config.linkExcludeSelector.replace( '%navbox%', ( _isNavbox ? '' : _config.navboxSelector ) ).trim();
		$excludeNodes = _$context.find( excludeSelector );
		$nodes.each( function () {
			if ( $.inArray( this, $excludeNodes ) === -1 ) {
				$node = $( this );
				new Link( $node );
			}
		} );

		if ( _userOptions.enabled ) {
			processBadges();
		}else {
			removeBadges();
		}
	}

	function processBadges() {
		var redirects = {},
			matchRedirects = false;

		// Resolve redirects if exists
		$.each( _pages, function ( title, page ) {
			if ( page.isRedirect ) {
				matchRedirects = true;
				redirects[title] = page;
			}
		} );

		if ( matchRedirects ) {
			$.when( resolveRedirects( redirects ) ).always( requestBadges );
		} else {
			requestBadges();
		}
	}

	function resolveRedirects( redirects ) {
		var resolver = new RedirectResolver( redirects ),
			promise = resolver.get();
		return promise;
	}

	function requestBadges() {
		var filtered = {},
			matchFiltered = false;

		// Request badges if not processed yet
		$.each( _pages, function ( title, page ) {
			if ( !page.isProcessed && !page.isRedirect ) {
				matchFiltered = true
				filtered[title] = page;
				page.setProcessed( true );
			}
		} );

		if ( matchFiltered ) {
			$.when( requestBadgesChains( filtered ) ).always( renderBadges );
		} else {
			renderBadges();
		}
	}

	function requestBadgesChains( pages ) {
		var titles = Object.keys( pages ),
			length = titles.length,
			count = Math.ceil( length / _config.wdLimit ),
			promises = [],
			promise;

		if ( length > _config.wdLimit ) {
			for ( var i = 0; i < count; i++ ) {
				var from = i * _config.wdLimit,
					to = Math.min( from + _config.wdLimit, length ),
					data = titles.slice( from, to );
				promises.push( requestBadgesPromise( data ) );
			}
			promise = $.when.apply($, promises);
		} else {
			promise = requestBadgesPromise( titles )
		}

		return promise;
	}

	function requestBadgesPromise( titles ) {
		var request = new BadgeRequest( titles ),
			promise = request.get();
		return promise;
	}

	function renderBadges() {
		// Execute Order 66 and replace badges with clones
		removeBadges();

		// Render only pages with badge
		$.each( _pages, function ( title, page ) {
			if ( page.isProcessed && page.hasBadge ) {
				page.renderLinkBadges();
			}
		} );
	}

	function removeBadges() {
		$.each( _config.badges, function ( id, badge ) {
			removeBadgesByType( badge.title );
			if ( typeof badge.titleAlias !== 'undefined' ) {
				$( badge.titleAlias ).each( function () {
					removeBadgesByType( this );
				} );
			}
		} );
	}

	function removeBadgesByType( title ) {
		var $node,
			rawRegExp = new RegExp( '%navbox%', 'g'),
			regExp = new RegExp( '%title%', 'g'),
			rawSelector = _config.badgeSelector.replace( rawRegExp, ( _isNavbox ? '' : _config.navboxSelector ) ).trim(),
			selector = rawSelector.replace( regExp, title ),
			$nodes = _$context.find( selector );

		$nodes.each( function () {
			$node = $( this );
			if ( _badgeNodes.indexOf( this ) === -1 ) {
				$node.remove();
			}
		} );
	}

	function detachBadges() {
		$( _links ).each( function () {
			this.detachBadge();
		} );
	}

	function dropdownMenuHelper() {
		var $target, $parents;

		document.body.addEventListener( 'click', function ( event ) {
			$target = $( event.target );
		    $parents = $target
		    	.parents( '.nfa__toggle' )
		    	.add( $target );

		    $( _gears ).each( function () {
		        if ( !$parents.is( this.$link ) ) {
		            this.hideMenu();
		        }
		    } );
		}, true );
	}

	/******* INIT *******/

	mw.loader.using( [ 'mediawiki.util', 'mediawiki.ForeignApi', 'jquery.makeCollapsible' ] ).done( function () {
		// Prepare config
		prepare();
		// Then
		mw.hook( 'wikipage.editform' ).add( garbage );
		mw.hook( 'wikipage.content' ).add( init );
	} );
} )();

// </nowiki>