1/* 2 * Behave.js 3 * 4 * Copyright 2013, Jacob Kelley - http://jakiestfu.com/ 5 * Released under the MIT Licence 6 * http://opensource.org/licenses/MIT 7 * 8 * Github: http://github.com/jakiestfu/Behave.js/ 9 * Version: 1.5 10 */ 11 12 13(function(undefined){ 14 15 'use strict'; 16 17 var BehaveHooks = BehaveHooks || (function(){ 18 var hooks = {}; 19 20 return { 21 add: function(hookName, fn){ 22 if(typeof hookName == "object"){ 23 var i; 24 for(i=0; i<hookName.length; i++){ 25 var theHook = hookName[i]; 26 if(!hooks[theHook]){ 27 hooks[theHook] = []; 28 } 29 hooks[theHook].push(fn); 30 } 31 } else { 32 if(!hooks[hookName]){ 33 hooks[hookName] = []; 34 } 35 hooks[hookName].push(fn); 36 } 37 }, 38 get: function(hookName){ 39 if(hooks[hookName]){ 40 return hooks[hookName]; 41 } 42 } 43 }; 44 45 })(), 46 Behave = Behave || function (userOpts) { 47 48 if (typeof String.prototype.repeat !== 'function') { 49 String.prototype.repeat = function(times) { 50 if(times < 1){ 51 return ''; 52 } 53 if(times % 2){ 54 return this.repeat(times - 1) + this; 55 } 56 var half = this.repeat(times / 2); 57 return half + half; 58 }; 59 } 60 61 if (typeof Array.prototype.filter !== 'function') { 62 Array.prototype.filter = function(func /*, thisp */) { 63 if (this === null) { 64 throw new TypeError(); 65 } 66 67 var t = Object(this), 68 len = t.length >>> 0; 69 if (typeof func != "function"){ 70 throw new TypeError(); 71 } 72 var res = [], 73 thisp = arguments[1]; 74 for (var i = 0; i < len; i++) { 75 if (i in t) { 76 var val = t[i]; 77 if (func.call(thisp, val, i, t)) { 78 res.push(val); 79 } 80 } 81 } 82 return res; 83 }; 84 } 85 86 var defaults = { 87 textarea: null, 88 replaceTab: true, 89 softTabs: true, 90 tabSize: 4, 91 autoOpen: true, 92 overwrite: true, 93 autoStrip: true, 94 autoIndent: true, 95 fence: false 96 }, 97 tab, 98 newLine, 99 charSettings = { 100 101 keyMap: [ 102 { open: "\"", close: "\"", canBreak: false }, 103 { open: "'", close: "'", canBreak: false }, 104 { open: "(", close: ")", canBreak: false }, 105 { open: "[", close: "]", canBreak: true }, 106 { open: "{", close: "}", canBreak: true } 107 ] 108 109 }, 110 utils = { 111 112 _callHook: function(hookName, passData){ 113 var hooks = BehaveHooks.get(hookName); 114 passData = typeof passData=="boolean" && passData === false ? false : true; 115 116 if(hooks){ 117 if(passData){ 118 var theEditor = defaults.textarea, 119 textVal = theEditor.value, 120 caretPos = utils.cursor.get(), 121 i; 122 123 for(i=0; i<hooks.length; i++){ 124 hooks[i].call(undefined, { 125 editor: { 126 element: theEditor, 127 text: textVal, 128 levelsDeep: utils.levelsDeep() 129 }, 130 caret: { 131 pos: caretPos 132 }, 133 lines: { 134 current: utils.cursor.getLine(textVal, caretPos), 135 total: utils.editor.getLines(textVal) 136 } 137 }); 138 } 139 } else { 140 for(i=0; i<hooks.length; i++){ 141 hooks[i].call(undefined); 142 } 143 } 144 } 145 }, 146 147 defineNewLine: function(){ 148 var ta = document.createElement('textarea'); 149 ta.value = "\n"; 150 151 if(ta.value.length==2){ 152 newLine = "\r\n"; 153 } else { 154 newLine = "\n"; 155 } 156 }, 157 defineTabSize: function(tabSize){ 158 if(typeof defaults.textarea.style.OTabSize != "undefined"){ 159 defaults.textarea.style.OTabSize = tabSize; return; 160 } 161 if(typeof defaults.textarea.style.MozTabSize != "undefined"){ 162 defaults.textarea.style.MozTabSize = tabSize; return; 163 } 164 if(typeof defaults.textarea.style.tabSize != "undefined"){ 165 defaults.textarea.style.tabSize = tabSize; return; 166 } 167 }, 168 cursor: { 169 getLine: function(textVal, pos){ 170 return ((textVal.substring(0,pos)).split("\n")).length; 171 }, 172 get: function() { 173 174 if (typeof document.createElement('textarea').selectionStart==="number") { 175 return defaults.textarea.selectionStart; 176 } else if (document.selection) { 177 var caretPos = 0, 178 range = defaults.textarea.createTextRange(), 179 rangeDupe = document.selection.createRange().duplicate(), 180 rangeDupeBookmark = rangeDupe.getBookmark(); 181 range.moveToBookmark(rangeDupeBookmark); 182 183 while (range.moveStart('character' , -1) !== 0) { 184 caretPos++; 185 } 186 return caretPos; 187 } 188 }, 189 set: function (start, end) { 190 if(!end){ 191 end = start; 192 } 193 if (defaults.textarea.setSelectionRange) { 194 defaults.textarea.focus(); 195 defaults.textarea.setSelectionRange(start, end); 196 } else if (defaults.textarea.createTextRange) { 197 var range = defaults.textarea.createTextRange(); 198 range.collapse(true); 199 range.moveEnd('character', end); 200 range.moveStart('character', start); 201 range.select(); 202 } 203 }, 204 selection: function(){ 205 var textAreaElement = defaults.textarea, 206 start = 0, 207 end = 0, 208 normalizedValue, 209 range, 210 textInputRange, 211 len, 212 endRange; 213 214 if (typeof textAreaElement.selectionStart == "number" && typeof textAreaElement.selectionEnd == "number") { 215 start = textAreaElement.selectionStart; 216 end = textAreaElement.selectionEnd; 217 } else { 218 range = document.selection.createRange(); 219 220 if (range && range.parentElement() == textAreaElement) { 221 222 normalizedValue = utils.editor.get(); 223 len = normalizedValue.length; 224 225 textInputRange = textAreaElement.createTextRange(); 226 textInputRange.moveToBookmark(range.getBookmark()); 227 228 endRange = textAreaElement.createTextRange(); 229 endRange.collapse(false); 230 231 if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { 232 start = end = len; 233 } else { 234 start = -textInputRange.moveStart("character", -len); 235 start += normalizedValue.slice(0, start).split(newLine).length - 1; 236 237 if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { 238 end = len; 239 } else { 240 end = -textInputRange.moveEnd("character", -len); 241 end += normalizedValue.slice(0, end).split(newLine).length - 1; 242 } 243 } 244 } 245 } 246 247 return start==end ? false : { 248 start: start, 249 end: end 250 }; 251 } 252 }, 253 editor: { 254 getLines: function(textVal){ 255 return (textVal).split("\n").length; 256 }, 257 get: function(){ 258 return defaults.textarea.value.replace(/\r/g,''); 259 }, 260 set: function(data){ 261 defaults.textarea.value = data; 262 } 263 }, 264 fenceRange: function(){ 265 if(typeof defaults.fence == "string"){ 266 267 var data = utils.editor.get(), 268 pos = utils.cursor.get(), 269 hacked = 0, 270 matchedFence = data.indexOf(defaults.fence), 271 matchCase = 0; 272 273 while(matchedFence>=0){ 274 matchCase++; 275 if( pos < (matchedFence+hacked) ){ 276 break; 277 } 278 279 hacked += matchedFence+defaults.fence.length; 280 data = data.substring(matchedFence+defaults.fence.length); 281 matchedFence = data.indexOf(defaults.fence); 282 283 } 284 285 if( (hacked) < pos && ( (matchedFence+hacked) > pos ) && matchCase%2===0){ 286 return true; 287 } 288 return false; 289 } else { 290 return true; 291 } 292 }, 293 isEven: function(_this,i){ 294 return i%2; 295 }, 296 levelsDeep: function(){ 297 var pos = utils.cursor.get(), 298 val = utils.editor.get(); 299 300 var left = val.substring(0, pos), 301 levels = 0, 302 i, j; 303 304 for(i=0; i<left.length; i++){ 305 for (j=0; j<charSettings.keyMap.length; j++) { 306 if(charSettings.keyMap[j].canBreak){ 307 if(charSettings.keyMap[j].open == left.charAt(i)){ 308 levels++; 309 } 310 311 if(charSettings.keyMap[j].close == left.charAt(i)){ 312 levels--; 313 } 314 } 315 } 316 } 317 318 var toDecrement = 0, 319 quoteMap = ["'", "\""]; 320 for(i=0; i<charSettings.keyMap.length; i++) { 321 if(charSettings.keyMap[i].canBreak){ 322 for(j in quoteMap){ 323 toDecrement += left.split(quoteMap[j]).filter(utils.isEven).join('').split(charSettings.keyMap[i].open).length - 1; 324 } 325 } 326 } 327 328 var finalLevels = levels - toDecrement; 329 330 return finalLevels >=0 ? finalLevels : 0; 331 }, 332 deepExtend: function(destination, source) { 333 for (var property in source) { 334 if (source[property] && source[property].constructor && 335 source[property].constructor === Object) { 336 destination[property] = destination[property] || {}; 337 utils.deepExtend(destination[property], source[property]); 338 } else { 339 destination[property] = source[property]; 340 } 341 } 342 return destination; 343 }, 344 addEvent: function addEvent(element, eventName, func) { 345 if (element.addEventListener){ 346 element.addEventListener(eventName,func,false); 347 } else if (element.attachEvent) { 348 element.attachEvent("on"+eventName, func); 349 } 350 }, 351 removeEvent: function addEvent(element, eventName, func){ 352 if (element.addEventListener){ 353 element.removeEventListener(eventName,func,false); 354 } else if (element.attachEvent) { 355 element.detachEvent("on"+eventName, func); 356 } 357 }, 358 359 preventDefaultEvent: function(e){ 360 if(e.preventDefault){ 361 e.preventDefault(); 362 } else { 363 e.returnValue = false; 364 } 365 } 366 }, 367 intercept = { 368 tabKey: function (e) { 369 370 if(!utils.fenceRange()){ return; } 371 372 if (e.keyCode == 9) { 373 utils.preventDefaultEvent(e); 374 375 var toReturn = true; 376 utils._callHook('tab:before'); 377 378 var selection = utils.cursor.selection(), 379 pos = utils.cursor.get(), 380 val = utils.editor.get(); 381 382 if(selection){ 383 384 var tempStart = selection.start; 385 while(tempStart--){ 386 if(val.charAt(tempStart)=="\n"){ 387 selection.start = tempStart + 1; 388 break; 389 } 390 } 391 392 var toIndent = val.substring(selection.start, selection.end), 393 lines = toIndent.split("\n"), 394 i; 395 396 if(e.shiftKey){ 397 for(i = 0; i<lines.length; i++){ 398 if(lines[i].substring(0,tab.length) == tab){ 399 lines[i] = lines[i].substring(tab.length); 400 } 401 } 402 toIndent = lines.join("\n"); 403 404 utils.editor.set( val.substring(0,selection.start) + toIndent + val.substring(selection.end) ); 405 utils.cursor.set(selection.start, selection.start+toIndent.length); 406 407 } else { 408 for(i in lines){ 409 lines[i] = tab + lines[i]; 410 } 411 toIndent = lines.join("\n"); 412 413 utils.editor.set( val.substring(0,selection.start) + toIndent + val.substring(selection.end) ); 414 utils.cursor.set(selection.start, selection.start+toIndent.length); 415 } 416 } else { 417 var left = val.substring(0, pos), 418 right = val.substring(pos), 419 edited = left + tab + right; 420 421 if(e.shiftKey){ 422 if(val.substring(pos-tab.length, pos) == tab){ 423 edited = val.substring(0, pos-tab.length) + right; 424 utils.editor.set(edited); 425 utils.cursor.set(pos-tab.length); 426 } 427 } else { 428 utils.editor.set(edited); 429 utils.cursor.set(pos + tab.length); 430 toReturn = false; 431 } 432 } 433 utils._callHook('tab:after'); 434 } 435 return toReturn; 436 }, 437 enterKey: function (e) { 438 439 if(!utils.fenceRange()){ return; } 440 441 if (e.keyCode == 13) { 442 443 utils.preventDefaultEvent(e); 444 utils._callHook('enter:before'); 445 446 var pos = utils.cursor.get(), 447 val = utils.editor.get(), 448 left = val.substring(0, pos), 449 right = val.substring(pos), 450 leftChar = left.charAt(left.length - 1), 451 rightChar = right.charAt(0), 452 numTabs = utils.levelsDeep(), 453 ourIndent = "", 454 closingBreak = "", 455 finalCursorPos, 456 i; 457 if(!numTabs){ 458 finalCursorPos = 1; 459 } else { 460 while(numTabs--){ 461 ourIndent+=tab; 462 } 463 ourIndent = ourIndent; 464 finalCursorPos = ourIndent.length + 1; 465 466 for(i=0; i<charSettings.keyMap.length; i++) { 467 if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar){ 468 closingBreak = newLine; 469 } 470 } 471 472 } 473 474 var edited = left + newLine + ourIndent + closingBreak + (ourIndent.substring(0, ourIndent.length-tab.length) ) + right; 475 utils.editor.set(edited); 476 utils.cursor.set(pos + finalCursorPos); 477 utils._callHook('enter:after'); 478 } 479 }, 480 deleteKey: function (e) { 481 482 if(!utils.fenceRange()){ return; } 483 484 if(e.keyCode == 8){ 485 utils.preventDefaultEvent(e); 486 487 utils._callHook('delete:before'); 488 489 var pos = utils.cursor.get(), 490 val = utils.editor.get(), 491 left = val.substring(0, pos), 492 right = val.substring(pos), 493 leftChar = left.charAt(left.length - 1), 494 rightChar = right.charAt(0), 495 i; 496 497 if( utils.cursor.selection() === false ){ 498 for(i=0; i<charSettings.keyMap.length; i++) { 499 if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) { 500 var edited = val.substring(0,pos-1) + val.substring(pos+1); 501 utils.editor.set(edited); 502 utils.cursor.set(pos - 1); 503 return; 504 } 505 } 506 var edited = val.substring(0,pos-1) + val.substring(pos); 507 utils.editor.set(edited); 508 utils.cursor.set(pos - 1); 509 } else { 510 var sel = utils.cursor.selection(), 511 edited = val.substring(0,sel.start) + val.substring(sel.end); 512 utils.editor.set(edited); 513 utils.cursor.set(pos); 514 } 515 516 utils._callHook('delete:after'); 517 518 } 519 } 520 }, 521 charFuncs = { 522 openedChar: function (_char, e) { 523 utils.preventDefaultEvent(e); 524 utils._callHook('openChar:before'); 525 var pos = utils.cursor.get(), 526 val = utils.editor.get(), 527 left = val.substring(0, pos), 528 right = val.substring(pos), 529 edited = left + _char.open + _char.close + right; 530 531 defaults.textarea.value = edited; 532 utils.cursor.set(pos + 1); 533 utils._callHook('openChar:after'); 534 }, 535 closedChar: function (_char, e) { 536 var pos = utils.cursor.get(), 537 val = utils.editor.get(), 538 toOverwrite = val.substring(pos, pos + 1); 539 if (toOverwrite == _char.close) { 540 utils.preventDefaultEvent(e); 541 utils._callHook('closeChar:before'); 542 utils.cursor.set(utils.cursor.get() + 1); 543 utils._callHook('closeChar:after'); 544 return true; 545 } 546 return false; 547 } 548 }, 549 action = { 550 filter: function (e) { 551 552 if(!utils.fenceRange()){ return; } 553 554 var theCode = e.which || e.keyCode; 555 556 if(theCode == 39 || theCode == 40 && e.which===0){ return; } 557 558 var _char = String.fromCharCode(theCode), 559 i; 560 561 for(i=0; i<charSettings.keyMap.length; i++) { 562 563 if (charSettings.keyMap[i].close == _char) { 564 var didClose = defaults.overwrite && charFuncs.closedChar(charSettings.keyMap[i], e); 565 566 if (!didClose && charSettings.keyMap[i].open == _char && defaults.autoOpen) { 567 charFuncs.openedChar(charSettings.keyMap[i], e); 568 } 569 } else if (charSettings.keyMap[i].open == _char && defaults.autoOpen) { 570 charFuncs.openedChar(charSettings.keyMap[i], e); 571 } 572 } 573 }, 574 listen: function () { 575 576 if(defaults.replaceTab){ utils.addEvent(defaults.textarea, 'keydown', intercept.tabKey); } 577 if(defaults.autoIndent){ utils.addEvent(defaults.textarea, 'keydown', intercept.enterKey); } 578 if(defaults.autoStrip){ utils.addEvent(defaults.textarea, 'keydown', intercept.deleteKey); } 579 580 utils.addEvent(defaults.textarea, 'keypress', action.filter); 581 582 utils.addEvent(defaults.textarea, 'keydown', function(){ utils._callHook('keydown'); }); 583 utils.addEvent(defaults.textarea, 'keyup', function(){ utils._callHook('keyup'); }); 584 } 585 }, 586 init = function (opts) { 587 588 if(opts.textarea){ 589 utils._callHook('init:before', false); 590 utils.deepExtend(defaults, opts); 591 utils.defineNewLine(); 592 593 if (defaults.softTabs) { 594 tab = " ".repeat(defaults.tabSize); 595 } else { 596 tab = "\t"; 597 598 utils.defineTabSize(defaults.tabSize); 599 } 600 601 action.listen(); 602 utils._callHook('init:after', false); 603 } 604 605 }; 606 607 this.destroy = function(){ 608 utils.removeEvent(defaults.textarea, 'keydown', intercept.tabKey); 609 utils.removeEvent(defaults.textarea, 'keydown', intercept.enterKey); 610 utils.removeEvent(defaults.textarea, 'keydown', intercept.deleteKey); 611 utils.removeEvent(defaults.textarea, 'keypress', action.filter); 612 }; 613 614 init(userOpts); 615 616 }; 617 618 if (typeof module !== 'undefined' && module.exports) { 619 module.exports = Behave; 620 } 621 622 if (typeof ender === 'undefined') { 623 this.Behave = Behave; 624 this.BehaveHooks = BehaveHooks; 625 } 626 627 if (typeof define === "function" && define.amd) { 628 define("behave", [], function () { 629 return Behave; 630 }); 631 } 632 633}).call(this); 634