User:Theopolisme/Scripts/autocompleter.js

/**

* 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, 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 ) );