| 1 | /* |
| 2 | * Tabby jQuery plugin version 0.12 |
| 3 | * |
| 4 | * Ted Devito - http://teddevito.com/demos/textarea.html |
| 5 | * |
| 6 | * You should have received a copy of the GNU General Public License |
| 7 | * along with Easy Widgets. If not, see <http://www.gnu.org/licenses/> |
| 8 | * |
| 9 | * Plugin development pattern based on: http://www.learningjquery.com/2007/10/a-plugin-development-pattern |
| 10 | * |
| 11 | */ |
| 12 | |
| 13 | // create closure |
| 14 | |
| 15 | (function($) { |
| 16 | |
| 17 | // plugin definition |
| 18 | |
| 19 | $.fn.tabby = function(options) { |
| 20 | //debug(this); |
| 21 | // build main options before element iteration |
| 22 | var opts = $.extend({}, $.fn.tabby.defaults, options); |
| 23 | var pressed = $.fn.tabby.pressed; |
| 24 | |
| 25 | // iterate and reformat each matched element |
| 26 | return this.each(function() { |
| 27 | $this = $(this); |
| 28 | |
| 29 | // build element specific options |
| 30 | var options = $.meta ? $.extend({}, opts, $this.data()) : opts; |
| 31 | |
| 32 | $this.bind('keydown',function (e) { |
| 33 | var kc = $.fn.tabby.catch_kc(e); |
| 34 | if (16 == kc) pressed.shft = true; |
| 35 | /* |
| 36 | because both CTRL+TAB and ALT+TAB default to an event (changing tab/window) that |
| 37 | will prevent js from capturing the keyup event, we'll set a timer on releasing them. |
| 38 | */ |
| 39 | if (17 == kc) {pressed.ctrl = true; setTimeout("$.fn.tabby.pressed.ctrl = false;",1000);} |
| 40 | if (18 == kc) {pressed.alt = true; setTimeout("$.fn.tabby.pressed.alt = false;",1000);} |
| 41 | |
| 42 | if (9 == kc && !pressed.ctrl && !pressed.alt) { |
| 43 | e.preventDefault; // does not work in O9.63 ?? |
| 44 | pressed.last = kc; setTimeout("$.fn.tabby.pressed.last = null;",0); |
| 45 | process_keypress ($(e.target).get(0), pressed.shft, options); |
| 46 | return false; |
| 47 | } |
| 48 | |
| 49 | }).bind('keyup',function (e) { |
| 50 | if (16 == $.fn.tabby.catch_kc(e)) pressed.shft = false; |
| 51 | }).bind('blur',function (e) { // workaround for Opera -- http://www.webdeveloper.com/forum/showthread.php?p=806588 |
| 52 | if (9 == pressed.last) $(e.target).one('focus',function (e) {pressed.last = null;}).get(0).focus(); |
| 53 | }); |
| 54 | |
| 55 | }); |
| 56 | }; |
| 57 | |
| 58 | // define and expose any extra methods |
| 59 | $.fn.tabby.catch_kc = function(e) { return e.keyCode ? e.keyCode : e.charCode ? e.charCode : e.which; }; |
| 60 | $.fn.tabby.pressed = {shft : false, ctrl : false, alt : false, last: null}; |
| 61 | |
| 62 | // private function for debugging |
| 63 | function debug($obj) { |
| 64 | if (window.console && window.console.log) |
| 65 | window.console.log('textarea count: ' + $obj.size()); |
| 66 | }; |
| 67 | |
| 68 | function process_keypress (o,shft,options) { |
| 69 | var scrollTo = o.scrollTop; |
| 70 | //var tabString = String.fromCharCode(9); |
| 71 | |
| 72 | // gecko; o.setSelectionRange is only available when the text box has focus |
| 73 | if (o.setSelectionRange) gecko_tab (o, shft, options); |
| 74 | |
| 75 | // ie; document.selection is always available |
| 76 | else if (document.selection) ie_tab (o, shft, options); |
| 77 | |
| 78 | o.scrollTop = scrollTo; |
| 79 | } |
| 80 | |
| 81 | // plugin defaults |
| 82 | $.fn.tabby.defaults = {tabString : String.fromCharCode(9)}; |
| 83 | |
| 84 | function gecko_tab (o, shft, options) { |
| 85 | var ss = o.selectionStart; |
| 86 | var es = o.selectionEnd; |
| 87 | |
| 88 | // 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 |
| 89 | if(ss == es) { |
| 90 | // SHIFT+TAB |
| 91 | if (shft) { |
| 92 | // check to the left of the caret first |
| 93 | if ("\t" == o.value.substring(ss-options.tabString.length, ss)) { |
| 94 | o.value = o.value.substring(0, ss-options.tabString.length) + o.value.substring(ss); // put it back together omitting one character to the left |
| 95 | o.focus(); |
| 96 | o.setSelectionRange(ss - options.tabString.length, ss - options.tabString.length); |
| 97 | } |
| 98 | // then check to the right of the caret |
| 99 | else if ("\t" == o.value.substring(ss, ss + options.tabString.length)) { |
| 100 | o.value = o.value.substring(0, ss) + o.value.substring(ss + options.tabString.length); // put it back together omitting one character to the right |
| 101 | o.focus(); |
| 102 | o.setSelectionRange(ss,ss); |
| 103 | } |
| 104 | } |
| 105 | // TAB |
| 106 | else { |
| 107 | o.value = o.value.substring(0, ss) + options.tabString + o.value.substring(ss); |
| 108 | o.focus(); |
| 109 | o.setSelectionRange(ss + options.tabString.length, ss + options.tabString.length); |
| 110 | } |
| 111 | } |
| 112 | // selections will always add/remove tabs from the start of the line |
| 113 | else { |
| 114 | // split the textarea up into lines and figure out which lines are included in the selection |
| 115 | var lines = o.value.split("\n"); |
| 116 | var indices = new Array(); |
| 117 | var sl = 0; // start of the line |
| 118 | var el = 0; // end of the line |
| 119 | var sel = false; |
| 120 | for (var i in lines) { |
| 121 | el = sl + lines[i].length; |
| 122 | indices.push({start: sl, end: el, selected: (sl <= ss && el > ss) || (el >= es && sl < es) || (sl > ss && el < es)}); |
| 123 | sl = el + 1;// for "\n" |
| 124 | } |
| 125 | |
| 126 | // walk through the array of lines (indices) and add tabs where appropriate |
| 127 | var modifier = 0; |
| 128 | for (var i in indices) { |
| 129 | if (indices[i].selected) { |
| 130 | var pos = indices[i].start + modifier; // adjust for tabs already inserted/removed |
| 131 | // SHIFT+TAB |
| 132 | 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 |
| 133 | o.value = o.value.substring(0,pos) + o.value.substring(pos + options.tabString.length); // omit the tabstring to the right |
| 134 | modifier -= options.tabString.length; |
| 135 | } |
| 136 | // TAB |
| 137 | else if (!shft) { |
| 138 | o.value = o.value.substring(0,pos) + options.tabString + o.value.substring(pos); // insert the tabstring |
| 139 | modifier += options.tabString.length; |
| 140 | } |
| 141 | } |
| 142 | } |
| 143 | o.focus(); |
| 144 | var ns = ss + ((modifier > 0) ? options.tabString.length : (modifier < 0) ? -options.tabString.length : 0); |
| 145 | var ne = es + modifier; |
| 146 | o.setSelectionRange(ns,ne); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | function ie_tab (o, shft, options) { |
| 151 | var range = document.selection.createRange(); |
| 152 | |
| 153 | if (o == range.parentElement()) { |
| 154 | // 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 |
| 155 | if ('' == range.text) { |
| 156 | // SHIFT+TAB |
| 157 | if (shft) { |
| 158 | var bookmark = range.getBookmark(); |
| 159 | //first try to the left by moving opening up our empty range to the left |
| 160 | range.moveStart('character', -options.tabString.length); |
| 161 | if (options.tabString == range.text) { |
| 162 | range.text = ''; |
| 163 | } else { |
| 164 | // if that didn't work then reset the range and try opening it to the right |
| 165 | range.moveToBookmark(bookmark); |
| 166 | range.moveEnd('character', options.tabString.length); |
| 167 | if (options.tabString == range.text) |
| 168 | range.text = ''; |
| 169 | } |
| 170 | // move the pointer to the start of them empty range and select it |
| 171 | range.collapse(true); |
| 172 | range.select(); |
| 173 | } |
| 174 | |
| 175 | else { |
| 176 | // very simple here. just insert the tab into the range and put the pointer at the end |
| 177 | range.text = options.tabString; |
| 178 | range.collapse(false); |
| 179 | range.select(); |
| 180 | } |
| 181 | } |
| 182 | // selections will always add/remove tabs from the start of the line |
| 183 | else { |
| 184 | |
| 185 | var selection_text = range.text; |
| 186 | var selection_len = selection_text.length; |
| 187 | var selection_arr = selection_text.split("\r\n"); |
| 188 | |
| 189 | var before_range = document.body.createTextRange(); |
| 190 | before_range.moveToElementText(o); |
| 191 | before_range.setEndPoint("EndToStart", range); |
| 192 | var before_text = before_range.text; |
| 193 | var before_arr = before_text.split("\r\n"); |
| 194 | var before_len = before_text.length; // - before_arr.length + 1; |
| 195 | |
| 196 | var after_range = document.body.createTextRange(); |
| 197 | after_range.moveToElementText(o); |
| 198 | after_range.setEndPoint("StartToEnd", range); |
| 199 | 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 |
| 200 | |
| 201 | var end_range = document.body.createTextRange(); |
| 202 | end_range.moveToElementText(o); |
| 203 | end_range.setEndPoint("StartToEnd", before_range); |
| 204 | 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 |
| 205 | |
| 206 | var check_html = $(o).html(); |
| 207 | $("#r3").text(before_len + " + " + selection_len + " + " + after_text.length + " = " + check_html.length); |
| 208 | if((before_len + end_text.length) < check_html.length) { |
| 209 | before_arr.push(""); |
| 210 | before_len += 2; // for the \r\n that was trimmed |
| 211 | if (shft && options.tabString == selection_arr[0].substring(0,options.tabString.length)) |
| 212 | selection_arr[0] = selection_arr[0].substring(options.tabString.length); |
| 213 | else if (!shft) selection_arr[0] = options.tabString + selection_arr[0]; |
| 214 | } else { |
| 215 | if (shft && options.tabString == before_arr[before_arr.length-1].substring(0,options.tabString.length)) |
| 216 | before_arr[before_arr.length-1] = before_arr[before_arr.length-1].substring(options.tabString.length); |
| 217 | else if (!shft) before_arr[before_arr.length-1] = options.tabString + before_arr[before_arr.length-1]; |
| 218 | } |
| 219 | |
| 220 | for (var i = 1; i < selection_arr.length; i++) { |
| 221 | if (shft && options.tabString == selection_arr[i].substring(0,options.tabString.length)) |
| 222 | selection_arr[i] = selection_arr[i].substring(options.tabString.length); |
| 223 | else if (!shft) selection_arr[i] = options.tabString + selection_arr[i]; |
| 224 | } |
| 225 | |
| 226 | if (1 == before_arr.length && 0 == before_len) { |
| 227 | if (shft && options.tabString == selection_arr[0].substring(0,options.tabString.length)) |
| 228 | selection_arr[0] = selection_arr[0].substring(options.tabString.length); |
| 229 | else if (!shft) selection_arr[0] = options.tabString + selection_arr[0]; |
| 230 | } |
| 231 | |
| 232 | if ((before_len + selection_len + after_text.length) < check_html.length) { |
| 233 | selection_arr.push(""); |
| 234 | selection_len += 2; // for the \r\n that was trimmed |
| 235 | } |
| 236 | |
| 237 | before_range.text = before_arr.join("\r\n"); |
| 238 | range.text = selection_arr.join("\r\n"); |
| 239 | |
| 240 | var new_range = document.body.createTextRange(); |
| 241 | new_range.moveToElementText(o); |
| 242 | |
| 243 | if (0 < before_len) new_range.setEndPoint("StartToEnd", before_range); |
| 244 | else new_range.setEndPoint("StartToStart", before_range); |
| 245 | new_range.setEndPoint("EndToEnd", range); |
| 246 | |
| 247 | new_range.select(); |
| 248 | |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | // end of closure |
| 254 | })(jQuery); |
| 255 | No newline at end of file |