Jump to content

MediaWiki:Gadget-TabOverride.js

From mediawiki.org

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * @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();
		}
	} );
} );