MediaWiki:Gadget-TabOverride.js

/** * @fileOverview The JavaScript component of the Tab Override MediaWiki extension * @author Bill Bryant * @version 2.0.1-MediaWiki * @note Rewritten by Jack Phoenix to work without legacy JS globals * @url http://tinsology.net/plugins/tab-override/ Original source code etc. */

// register the keydown event listener on the content textarea element // after the window is loaded to make sure that the element is accessible // using the document's getElementById method jQuery( document ).ready( function {	// only on edit pages (action=edit in the URL)	// action=submit is preview mode (the "Show preview" button has been pressed)	if ( mw.config.get( 'wgAction' ) !== 'edit' && mw.config.get( 'wgAction' ) !== 'submit' )	{		return;	}

var content = jQuery( '#wpTextbox1' ); // the MediaWiki page edit textarea

// if there is no content textarea element on this page, do nothing if ( !content.length ) { return; }

content.keydown( function( e ) {		var text, // initial text in the textarea			range, // the IE TextRange object			tempRange, // used to calculate selection start and end positions in IE			preNewlines, // the number of newline (\r\n) characters before the selection start (for IE)			selNewlines, // the number of newline (\r\n) characters within the selection (for IE)			initScrollTop, // initial scrollTop value to fix scrolling in Firefox			selStart, // the selection start position			selEnd, // the selection end position			sel, // the selected text			startLine, // for multi-line selections, the first character position of the first line			endLine, // for multi-line selections, the last character position of the last line			numTabs, // the number of tabs inserted / removed in the selection			startTab, // if a tab was removed from the start of the first line			preTab; // if a tab was removed before the start of the selection

// tab key - insert / remove tab if ( e.keyCode === 9 ) { // initialize variables text = this.value; initScrollTop = this.scrollTop; // scrollTop is supported by all modern browsers numTabs = 0; startTab = 0; preTab = 0;

if ( typeof this.selectionStart !== 'undefined' ) { selStart = this.selectionStart; selEnd = this.selectionEnd; sel = text.slice( selStart, selEnd ); } else if ( document.selection ) { // IE				range = document.selection.createRange; sel = range.text; tempRange = range.duplicate; tempRange.moveToElementText( this ); tempRange.setEndPoint( 'EndToEnd', range ); selEnd = tempRange.text.length; selStart = selEnd - sel.length; // whenever the value of the textarea is changed, the range needs to be reset // IE (and Opera) use both \r and \n for newlines - this adds an extra character // that needs to be accounted for when doing position calculations // these values are used to offset the selection start and end positions preNewlines = text.slice( 0, selStart ).split( '\r\n' ).length - 1; selNewlines = sel.split( '\r\n' ).length - 1; } else { // cannot access textarea selection - do nothing return; }

// special case of multi-line selection if ( selStart !== selEnd && sel.indexOf( '\n' ) !== -1 ) { // for multiple lines, only insert / remove tabs from the beginning of each line

// find the start of the first selected line if ( selStart === 0 || text.charAt( selStart - 1 ) === '\n' ) { // the selection starts at the beginning of a line startLine = selStart; } else { // the selection starts after the beginning of a line // set startLine to the beginning of the first partially selected line // subtract 1 from selStart in case the cursor is at the newline character, // for instance, if the very end of the previous line was selected // add 1 to get the next character after the newline // if there is none before the selection, lastIndexOf returns -1 // when 1 is added to that it becomes 0 and the first character is used startLine = text.lastIndexOf( '\n', selStart - 1 ) + 1; }

// find the end of the last selected line if ( selEnd === text.length || text.charAt( selEnd ) === '\n' ) { // the selection ends at the end of a line endLine = selEnd; } else { // the selection ends before the end of a line // set endLine to the end of the last partially selected line endLine = text.indexOf( '\n', selEnd ); if ( endLine === -1 ) { endLine = text.length; }				}

// if the shift key was pressed, remove tabs instead of inserting them if ( e.shiftKey ) { if ( text.charAt( startLine ) === '\t' ) { // is this tab part of the selection? if ( startLine === selStart ) { // it is, remove it							sel = sel.slice( 1 ); } else { // the tab comes before the selection preTab = 1; }						startTab = 1; }

this.value = text.slice( 0, startLine ) + text.slice( startLine + preTab, selStart ) + sel.replace( /\n\t/g, function {							numTabs += 1;							return '\n';						}) + text.slice( selEnd );

// set start and end points if ( range ) { // IE						// setting end first makes calculations easier range.collapse; range.moveEnd( 'character', selEnd - startTab - numTabs - selNewlines - preNewlines ); range.moveStart( 'character', selStart - preTab - preNewlines ); range.select; } else { // set start first for Opera this.selectionStart = selStart - preTab; // preTab is 0 or 1 // move the selection end over by the total number of tabs removed this.selectionEnd = selEnd - startTab - numTabs; }				} else { // no shift key // insert tabs at the beginning of each line of the selection this.value = text.slice( 0, startLine ) + '\t' + text.slice( startLine, selStart ) + sel.replace( /\n/g, function {							numTabs += 1;							return '\n\t';						}) + text.slice( selEnd );

// set start and end points if ( range ) { // IE						range.collapse; range.moveEnd( 'character', selEnd + 1 - preNewlines ); // numTabs cancels out selNewlines range.moveStart( 'character', selStart + 1 - preNewlines ); range.select; } else { // the selection start is always moved by 1 character this.selectionStart = selStart + 1; // move the selection end over by the total number of tabs inserted this.selectionEnd = selEnd + 1 + numTabs; }				}			} else { // "normal" case (no selection or selection on one line only)

// if the shift key was pressed, remove a tab instead of inserting one if ( e.shiftKey ) { // if the character before the selection is a tab, remove it					if ( text.charAt( selStart - 1 ) === '\t' ) { this.value = text.slice( 0, selStart - 1 ) + text.slice( selStart );

// set start and end points if ( range ) { // IE							// collapses range and moves it by -1 character range.move( 'character', selStart - 1 - preNewlines ); range.select; } else { this.selectionEnd = this.selectionStart = selStart - 1; }					}				} else { // no shift key - insert a tab if ( range ) { // IE						// if no text is selected and the cursor is at the beginning of a line // (except the first line), IE places the cursor at the carriage return character // the tab must be placed after the \r\n pair if ( text.charAt( selStart ) === '\r' ) { this.value = text.slice( 0, selStart + 2 ) + '\t' + text.slice( selEnd + 2 ); // collapse the range and move it to the appropriate location range.move( 'character', selStart + 2 - preNewlines ); } else { this.value = text.slice( 0, selStart ) + '\t' + text.slice( selEnd ); // collapse the range and move it to the appropriate location range.move( 'character', selStart + 1 - preNewlines ); }						range.select; } else { this.value = text.slice( 0, selStart ) + '\t' + text.slice( selEnd ); this.selectionEnd = this.selectionStart = selStart + 1; }				}			}

// this is really just for Firefox, but will be executed by all browsers // whenever the textarea value property is reset, Firefox scrolls back to the top // this will reset it to the original scroll value this.scrollTop = initScrollTop;

// prevent the default action if ( e.preventDefault ) { e.preventDefault; }			e.returnValue = false; }	} ).keypress( function( e ) { // Opera (and Firefox) also fire a keypress event when the tab key is pressed // Opera requires that the default action be prevented on this event, or the // textarea will lose focus (preventDefault is enough, IE never fires this		// for the tab key) if ( e.keyCode === 9 && e.preventDefault ) { e.preventDefault; }	} ); } );