Jump to content

User:Theopolisme/Scripts/autocompleter.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * autocompleter.js
 * Bringing tab-based autocompletion to the mediawiki edit interface!
 *
 * @see [[User:Theopolisme/Scripts/autocompleter]]
 * @author Theopolisme
 */
/* global jQuery, mediaWiki */
( function ( $, mw ) {
	'use strict';

	var DELIMTER = /[\[\:\s|]/,
		MATCHERS = [
			// Usernames: [[User (talk):$$$]], {{ping|$$$}}, {{u|$$$}}
			/(?:\[\[user(?:[_ ]talk)?:(.*?)[\|\]#]|\{\{(?:ping|u)\|(.*?)\}\})/gi,
			// Wikipages: [[Xyz]], [[Wikipedia:Xyz]], [[Talk:Foo|Bar]]
			/\[\[(.*?)(?:\||\]\])/g
		];

	function log () {
		var args = Array.prototype.slice.call( arguments );
		if ( console && console.log ) {
			args.unshift( '[autocompleter]' );
			console.log.apply( console, args );
		}
	}

	function Autocompleter ( $textarea ) {
		this.$textarea = $textarea;
		this.isListening = false;
		this.cache = [];
		this.updateCache();
	}

	Autocompleter.prototype.updateCache = function () {
		var i, j, matcher, match, value,
			cache = this.cache,
			content = this.$textarea.val();

		for ( i = 0; i < MATCHERS.length; i++ ) {
			matcher = MATCHERS[i];
			match =	matcher.exec( content );

			while ( match !== null ) {
				j = match.length - 1;
				do {
					value = match[j];
					j--;
				} while ( value === undefined );

				if ( cache.indexOf( value ) === -1 ) {
					cache.push( value );
				}

				match =	matcher.exec( content );
			}
		}
		
		this.cache = cache;
		log( 'cache updated', this.cache );
	};

	Autocompleter.prototype.autocomplete = function () {
		var ac = this,
			pattern, completions, currentCompletion,
			content = this.$textarea.val(),
			caretPosition = this.$textarea.textSelection( 'getCaretPosition' );

		function findPattern( content, caretPosition ) {
			var piece = content.substring( 0, caretPosition ),
				i = piece.length;

			while ( i >= 0 ) {
				if ( DELIMTER.test( piece[i] ) ) {
					return piece.substring( i + 1 ).toLowerCase();
				}
				i--;
			}

			log( 'could not find a delimeter' );
			return false;
		}

		function complete( pattern ) {
			var i, cache = ac.cache,
				completions = [];

			for (  i = 0; i < cache.length; i++ ) {
				if ( cache[i].toLowerCase().indexOf( pattern ) === 0 ) {
					completions.push( cache[i] );
				}
			}

			return completions;
		}

		function updateTextarea ( content, caretPosition, pattern, completion ) {
			var start = caretPosition - pattern.length,
				end = start + completion.length,
				newContent = content.substring( 0, start ) + completion + content.substring( caretPosition );

			ac.$textarea.val( newContent );

			ac.$textarea.textSelection( 'setSelection', {
				start: start,
				end: end
			} );
		}

		pattern = findPattern( content, caretPosition );
		completions = complete( pattern );
		currentCompletion = 0;

		log( 'pattern', pattern );
		log( 'completions', completions );

		if ( !completions.length ) {
			log( 'could not find a match' );
			return;
		}

		updateTextarea( content, caretPosition, pattern, completions[ currentCompletion ] );

		// Allow the user to "scroll" through matches
		function keydownHandler ( e ) {
			switch ( e.which ) {
				case 38: // up arrow
					currentCompletion += 1;
					if ( currentCompletion > completions.length - 1 ) {
						currentCompletion = 0;
					}
					break;
				case 40: // down arrow
					currentCompletion -= 1;
					if ( currentCompletion < 0 ) {
						currentCompletion = completions.length - 1;
					}
					break;
				default:
					ac.$textarea.off( 'keydown', keydownHandler );
					return;
			}

			updateTextarea( content, caretPosition, pattern, completions[ currentCompletion ] );

			e.preventDefault();
			return false;
		}

		this.$textarea.on( 'keydown', keydownHandler );
	};

	Autocompleter.prototype.listen = function () {
		var ac = this;

		if ( this.isListening ) {
			return;
		}

		this.$textarea.on( 'keydown', function ( e ) {
			if ( e.which === 9 ) { // tab
				e.preventDefault();
				ac.autocomplete();
				return false;
			}
		} );

		this.isListening = true;
	};

	$( document ).ready( function () {
		mw.loader.using( 'mediawiki.util', function () {
			if ( [ 'edit', 'submit' ].indexOf( mw.util.getParamValue( 'action' ) ) !== -1 ) {
				mw.loader.using( 'jquery.textSelection', function () {
					var autocompleter = new Autocompleter( $( '#wpTextbox1' ) );
					autocompleter.listen();
				} );
			}
		});
	} );

}( jQuery, mediaWiki ) );