| | 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 |