Blog Entry - 24th July 2007 - Programming - JavaScript

Keeping Tabs


One of the things which I like to have is a text area in which I can input TAB characters using the TAB key.

It breaks the web page of course, because the TAB key is used for navigating from one form control to the next; but there are circumstances when I want to use a tab. Usually when I am inputting code, as I prefer the TAB character for my indenting.

Opera does not play so well, as preventDefault() does not seem to work for the tab key.

So lets get stuck in.

The TEXTAREA

Firstly we need to tell the TEXTAREA to call-up a suitable function in response to a keyboard event. The appropriate keyboard event is onkeydown.

<textarea onkeydown="HTMLTextArea_InsertTab(event)" style="width:500px; height:200px;"></textarea>

The Event Object

Then in the function, we need to get the event object.

In Internet Explorer the event is accessed by window.event. In other browsers the event object is either supplied as an argument to the function (if you use addEventListener) or must be supplied as an argument (for inline events as above).

Thus:

<textarea onkeydown="HTMLTextArea_InsertTab(event)" style="width:500px; height:200px;"></textarea>

function HTMLTextArea_InsertTab(
	event /*: Event*/
) /*: void*/
{
	event = event || window.event;

...

The Key Code

Next we need to get the key code for the pressed key, so that we respond appropriately if the TAB key was pressed.

The ASCII value for a TAB is 9.

if (event.keyCode == 9)
{
	... do our stuff
}

Inserting the Tab

In order to insert a TAB character, we need to identify where the cursor is in the TEXTAREA, and then proceed to insert the TAB at the appropriate point. Easy, but unfortunately IE and the other browsers vary dramatically in how this can be achieved.

Internet Explorer

Internet Explorer has a non-standard TextRange object which gives you programmatic control over cursors and text editing. This is accessed through the selection object on the document.

To insert the TAB you need the following:-

if (document.selection)
{
	var range /*: HostObject(TextRange)*/ = document.selection.createRange();
	range.collapse();
	range.text = "\t";
	range.collapse();
	range.select();
}

The document.selection references a TextRange which covers a region of text on your web page. This is either an empty region (just the cursor) or a region of text which you have highlighted (i.e. selected).

The range.collapse() collapses the range to the end of whatever region of text is highlighted. If the range is empty, it does nothing.

The range.select() method returns focus to the point after the point at which the tab was inserted.

Simple.

Mozilla/Firefox and Opera

The Firefox and Opera textarea has a selection range, which is represented by the selectionStart and selectionEnd properties. These will be the same values if it is just an empty cursor.

It offers the setSelectionRange(start : int, end : int) method to set the range, and the focus() method to bring the cursor back to life.

if (typeof textarea.selectionStart !== "undefined")
{
	/* Collapse Insertion Point */
	textarea.setSelectionRange(textarea.selectionEnd, textarea.selectionEnd);
	textarea.focus();

	/* Insert Tab */
	var selStart /*: int*/ = textarea.selectionStart;
	var selEnd /*: int*/ = textarea.selectionEnd;
	var currentScroll /*: int*/ = textarea.scrollTop;
	var contents /*: String*/ = textarea.value;
	
	textarea.value = contents.substring(0, selStart) + "\t" + contents.substring(selEnd, contents.length);

	textarea.scrollTop = currentScroll;
	textarea.setSelectionRange(selEnd + 1, selEnd + 1);

	textarea.focus();
	
	//window.setTimeout(function(){
	//	textarea.focus();
	//}, 0);
}

The scrollTop test is needed, because the scroll may reset when you write values back into the textarea.

The timeout may need to be used in order to give the cursor a nudge.

Sometimes this code does not work - no error is thrown, it just randomly fails. Have not worked out why, and failure cannot be reliably reproduced.

Blocking the Default Behaviour

Once we have inserted the TAB, we need to stop the browser from jumping to the next form control, which is the normal consequence of pressing the TAB character.

Internet Explorer

In IE the event object has a returnValue property, so we say the following at the end of our function:-

if (window.event)
{
	window.event.returnValue = false;
}

Firefox

In Firefox the event object has a preventDefault method, so we say the following at the end of our function:-

if (typeof event.preventDefault != "undefined")

{

event.preventDefault();

}

In Firefox the event object is supplied as an argument to the function automatically, whereas in IE, the event object is a property of the window object.

Opera

Unfortunately, with Opera, if there is another control on the page, the default behaviour (tabbing to another control) fires first, before the keydown event, so you cannot stop it. This seems like a bug with opera. The focus() method brings focus back, but not without a jump in the page, if the other control is above or below the current scroll window.

The final code

So here is the final code.

<html>
<head>
<script>
function HTMLTextArea_InsertTab(
	event /*: Event*/
) /*: void*/
{
	event = event || window.event;

	var textarea /*: HTMLTextAreaElement*/ = event.srcElement || event.target;

	if (event.keyCode == 9)
	{
		if (typeof textarea.selectionStart !== "undefined")
		{
			/* Collapse Insertion Point */
			textarea.setSelectionRange(textarea.selectionEnd, textarea.selectionEnd);
			textarea.focus();

			/* Insert Tab */
			var selStart /*: int*/ = textarea.selectionStart;
			var selEnd /*: int*/ = textarea.selectionEnd;
			var currentScroll /*: int*/ = textarea.scrollTop;
			var contents /*: String*/ = textarea.value;
	
			textarea.value = contents.substring(0, selStart) + "\t" + contents.substring(selEnd, contents.length);

			textarea.scrollTop = currentScroll;
			textarea.setSelectionRange(selEnd + 1, selEnd + 1);

			textarea.focus();
	
			//window.setTimeout(function(){
			//	textarea.focus();
			//}, 0);

			event.preventDefault();
		}
		else if (document.selection)
		{
			var range /*: HostObject(TextRange)*/ = document.selection.createRange();
			range.collapse();
			range.text = String.fromCharCode(9);
			range.collapse();
			range.select();

			event.returnValue = false;
		}
	}
}
</script>
</head>
<body>
<textarea onkeydown="HTMLTextArea_InsertTab(event)" style="width:500px; height:200px;"></textarea>
<input value="An input to test whether focus shifts">
</body>
</html>

editpop-upiframe

Note finally that the IE test comes second. This is because for some odd reason Opera returns true for a test on document.selection.


Comment(s)


Sorry, comments have been suspended. Too much offensive comment spam is causing the site to be blocked by firewalls (which ironically therefore defeats the point of posting spam in the first place!). I don't get that many comments anyway, so I am going to look at a better way of managing the comment spam before reinstating the comments.


Leave a comment ...


Maximum number of comments reached.