Commits

Brent Tubbs committed 93ee2bb

allow typing tabs within textareas

  • Participants
  • Parent commits 532e68b

Comments (0)

Files changed (2)

File static/jquery.textarea.js

+/*
+ *	Tabby jQuery plugin version 0.12
+ *
+ *	Ted Devito - http://teddevito.com/demos/textarea.html
+ *
+ *	Copyright (c) 2009 Ted Devito
+ *	 
+ *	Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following 
+ *	conditions are met:
+ *	
+ *		1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *		2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer  
+ *			in the documentation and/or other materials provided with the distribution.
+ *		3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written 
+ *			permission. 
+ *	 
+ *	THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
+ *	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE 
+ *	LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
+ *	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
+ *	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
+ *	OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+ 
+// create closure
+
+(function($) {
+ 
+	// plugin definition
+
+	$.fn.tabby = function(options) {
+		//debug(this);
+		// build main options before element iteration
+		var opts = $.extend({}, $.fn.tabby.defaults, options);
+		var pressed = $.fn.tabby.pressed; 
+		
+		// iterate and reformat each matched element
+		return this.each(function() {
+			$this = $(this);
+			
+			// build element specific options
+			var options = $.meta ? $.extend({}, opts, $this.data()) : opts;
+			
+			$this.bind('keydown',function (e) {
+				var kc = $.fn.tabby.catch_kc(e);
+				if (16 == kc) pressed.shft = true;
+				/*
+				because both CTRL+TAB and ALT+TAB default to an event (changing tab/window) that 
+				will prevent js from capturing the keyup event, we'll set a timer on releasing them.
+				*/
+				if (17 == kc) {pressed.ctrl = true;	setTimeout("$.fn.tabby.pressed.ctrl = false;",1000);}
+				if (18 == kc) {pressed.alt = true; 	setTimeout("$.fn.tabby.pressed.alt = false;",1000);}
+					
+				if (9 == kc && !pressed.ctrl && !pressed.alt) {
+					e.preventDefault; // does not work in O9.63 ??
+					pressed.last = kc;	setTimeout("$.fn.tabby.pressed.last = null;",0);
+					process_keypress ($(e.target).get(0), pressed.shft, options);
+					return false;
+				}
+				
+			}).bind('keyup',function (e) {
+				if (16 == $.fn.tabby.catch_kc(e)) pressed.shft = false;
+			}).bind('blur',function (e) { // workaround for Opera -- http://www.webdeveloper.com/forum/showthread.php?p=806588
+				if (9 == pressed.last) $(e.target).one('focus',function (e) {pressed.last = null;}).get(0).focus();
+			});
+		
+		});
+	};
+	
+	// define and expose any extra methods
+	$.fn.tabby.catch_kc = function(e) { return e.keyCode ? e.keyCode : e.charCode ? e.charCode : e.which; };
+	$.fn.tabby.pressed = {shft : false, ctrl : false, alt : false, last: null};
+	
+	// private function for debugging
+	function debug($obj) {
+		if (window.console && window.console.log)
+		window.console.log('textarea count: ' + $obj.size());
+	};
+
+	function process_keypress (o,shft,options) {
+		var scrollTo = o.scrollTop;
+		//var tabString = String.fromCharCode(9);
+		
+		// gecko; o.setSelectionRange is only available when the text box has focus
+		if (o.setSelectionRange) gecko_tab (o, shft, options);
+		
+		// ie; document.selection is always available
+		else if (document.selection) ie_tab (o, shft, options);
+		
+		o.scrollTop = scrollTo;
+	}
+	
+	// plugin defaults
+	$.fn.tabby.defaults = {tabString : String.fromCharCode(9)};
+	
+	function gecko_tab (o, shft, options) {
+		var ss = o.selectionStart;
+		var es = o.selectionEnd;	
+				
+		// when there's no selection and we're just working with the caret, we'll add/remove the tabs at the caret, providing more control
+		if(ss == es) {
+			// SHIFT+TAB
+			if (shft) {
+				// check to the left of the caret first
+				if ("\t" == o.value.substring(ss-options.tabString.length, ss)) {
+					o.value = o.value.substring(0, ss-options.tabString.length) + o.value.substring(ss); // put it back together omitting one character to the left
+					o.focus();
+					o.setSelectionRange(ss - options.tabString.length, ss - options.tabString.length);
+				} 
+				// then check to the right of the caret
+				else if ("\t" == o.value.substring(ss, ss + options.tabString.length)) {
+					o.value = o.value.substring(0, ss) + o.value.substring(ss + options.tabString.length); // put it back together omitting one character to the right
+					o.focus();
+					o.setSelectionRange(ss,ss);
+				}
+			}
+			// TAB
+			else {			
+				o.value = o.value.substring(0, ss) + options.tabString + o.value.substring(ss);
+				o.focus();
+	    		o.setSelectionRange(ss + options.tabString.length, ss + options.tabString.length);
+			}
+		} 
+		// selections will always add/remove tabs from the start of the line
+		else {
+			// split the textarea up into lines and figure out which lines are included in the selection
+			var lines = o.value.split("\n");
+			var indices = new Array();
+			var sl = 0; // start of the line
+			var el = 0; // end of the line
+			var sel = false;
+			for (var i in lines) {
+				el = sl + lines[i].length;
+				indices.push({start: sl, end: el, selected: (sl <= ss && el > ss) || (el >= es && sl < es) || (sl > ss && el < es)});
+				sl = el + 1;// for "\n"
+			}
+			
+			// walk through the array of lines (indices) and add tabs where appropriate						
+			var modifier = 0;
+			for (var i in indices) {
+				if (indices[i].selected) {
+					var pos = indices[i].start + modifier; // adjust for tabs already inserted/removed
+					// SHIFT+TAB
+					if (shft && options.tabString == o.value.substring(pos,pos+options.tabString.length)) { // only SHIFT+TAB if there's a tab at the start of the line
+						o.value = o.value.substring(0,pos) + o.value.substring(pos + options.tabString.length); // omit the tabstring to the right
+						modifier -= options.tabString.length;
+					}
+					// TAB
+					else if (!shft) {
+						o.value = o.value.substring(0,pos) + options.tabString + o.value.substring(pos); // insert the tabstring
+						modifier += options.tabString.length;
+					}
+				}
+			}
+			o.focus();
+			var ns = ss + ((modifier > 0) ? options.tabString.length : (modifier < 0) ? -options.tabString.length : 0);
+			var ne = es + modifier;
+			o.setSelectionRange(ns,ne);
+		}
+	}
+	
+	function ie_tab (o, shft, options) {
+		var range = document.selection.createRange();
+		
+		if (o == range.parentElement()) {
+			// when there's no selection and we're just working with the caret, we'll add/remove the tabs at the caret, providing more control
+			if ('' == range.text) {
+				// SHIFT+TAB
+				if (shft) {
+					var bookmark = range.getBookmark();
+					//first try to the left by moving opening up our empty range to the left
+				    range.moveStart('character', -options.tabString.length);
+				    if (options.tabString == range.text) {
+				    	range.text = '';
+				    } else {
+				    	// if that didn't work then reset the range and try opening it to the right
+				    	range.moveToBookmark(bookmark);
+				    	range.moveEnd('character', options.tabString.length);
+				    	if (options.tabString == range.text) 
+				    		range.text = '';
+				    }
+				    // move the pointer to the start of them empty range and select it
+				    range.collapse(true);
+					range.select();
+				}
+				
+				else {
+					// very simple here. just insert the tab into the range and put the pointer at the end
+					range.text = options.tabString; 
+					range.collapse(false);
+					range.select();
+				}
+			}
+			// selections will always add/remove tabs from the start of the line
+			else {
+			
+				var selection_text = range.text;
+				var selection_len = selection_text.length;
+				var selection_arr = selection_text.split("\r\n");
+				
+				var before_range = document.body.createTextRange();
+				before_range.moveToElementText(o);
+				before_range.setEndPoint("EndToStart", range);
+				var before_text = before_range.text;
+				var before_arr = before_text.split("\r\n");
+				var before_len = before_text.length; // - before_arr.length + 1;
+				
+				var after_range = document.body.createTextRange();
+				after_range.moveToElementText(o);
+				after_range.setEndPoint("StartToEnd", range);
+				var after_text = after_range.text; // we can accurately calculate distance to the end because we're not worried about MSIE trimming a \r\n
+				
+				var end_range = document.body.createTextRange();
+				end_range.moveToElementText(o);
+				end_range.setEndPoint("StartToEnd", before_range);
+				var end_text = end_range.text; // we can accurately calculate distance to the end because we're not worried about MSIE trimming a \r\n
+								
+				var check_html = $(o).html();
+				$("#r3").text(before_len + " + " + selection_len + " + " + after_text.length + " = " + check_html.length);				
+				if((before_len + end_text.length) < check_html.length) {
+					before_arr.push("");
+					before_len += 2; // for the \r\n that was trimmed	
+					if (shft && options.tabString == selection_arr[0].substring(0,options.tabString.length))
+						selection_arr[0] = selection_arr[0].substring(options.tabString.length);
+					else if (!shft) selection_arr[0] = options.tabString + selection_arr[0];	
+				} else {
+					if (shft && options.tabString == before_arr[before_arr.length-1].substring(0,options.tabString.length)) 
+						before_arr[before_arr.length-1] = before_arr[before_arr.length-1].substring(options.tabString.length);
+					else if (!shft) before_arr[before_arr.length-1] = options.tabString + before_arr[before_arr.length-1];
+				}
+				
+				for (var i = 1; i < selection_arr.length; i++) {
+					if (shft && options.tabString == selection_arr[i].substring(0,options.tabString.length))
+						selection_arr[i] = selection_arr[i].substring(options.tabString.length);
+					else if (!shft) selection_arr[i] = options.tabString + selection_arr[i];
+				}
+				
+				if (1 == before_arr.length && 0 == before_len) {
+					if (shft && options.tabString == selection_arr[0].substring(0,options.tabString.length))
+						selection_arr[0] = selection_arr[0].substring(options.tabString.length);
+					else if (!shft) selection_arr[0] = options.tabString + selection_arr[0];
+				}
+
+				if ((before_len + selection_len + after_text.length) < check_html.length) {
+					selection_arr.push("");
+					selection_len += 2; // for the \r\n that was trimmed
+				}
+				
+				before_range.text = before_arr.join("\r\n");
+				range.text = selection_arr.join("\r\n");
+				
+				var new_range = document.body.createTextRange();
+				new_range.moveToElementText(o);
+				
+				if (0 < before_len)	new_range.setEndPoint("StartToEnd", before_range);
+				else new_range.setEndPoint("StartToStart", before_range);
+				new_range.setEndPoint("EndToEnd", range);
+				
+				new_range.select();
+				
+			} 
+		}
+	}
+
+// end of closure
+})(jQuery);

File templates/index.html

 <html>
 <head>
-    <link href="http://fonts.googleapis.com/css?family=Droid+Sans:regular,bold&amp;subset=latin" rel="stylesheet" type="text/css"> 
+    <link href="http://fonts.googleapis.com/css?family=Droid+Sans:regular,bold&amp;subset=latin" rel="stylesheet" type="text/css">
     <link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
     <link rel="stylesheet" href="/static/reset.css" type="text/css" media="screen" charset="utf-8">
     <link rel="stylesheet" href="/static/tmplatur.css" type="text/css" media="screen" charset="utf-8">
+    <script type="text/javascript" charset="utf-8" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
+    <script type="text/javascript" charset="utf-8" src="/static/jquery.textarea.js"></script>
     <title>Tmplatur</title>
-    <style>
-    </style>
+    <script type="text/javascript" charset="utf-8">
+    $(document).ready(function () {
+        $("textarea").tabby({tabString: '    '});
+    });
+    </script>
 </head>
 <body>
-  
+
   <div id="header">
     <h1>Tmplatur</h1>
     <div id="username">{{ account.user.nickname }} <a href="{{ logout_url }}">Logout</a></div>
   </div>
 
   <div id="main">
-      
+
     <form method="post" >
-    
+
     <div class="half_block">
         <h3>Data</h3>
         <textarea name="data" class="half_inner">{{ account.data }}</textarea>
     </div>
-    
+
     <div class="half_block">
       <h3>Template</h3>
       <textarea name="template" class="half_inner">{{ account.template }}</textarea>
     </div>
-    
+
     <div class="full_block">
         <input type="submit" value="Render" id="btn_render" />
     </div>
 
     </form>
-    
+
     <div class="full_block">
         <h3>Result</h3>
         <iframe id="rendered" src="/rendered"></iframe>