/* * Behave.js * * Copyright 2013, Jacob Kelley - http://jakiestfu.com/ * Released under the MIT Licence * http://opensource.org/licenses/MIT * * Github: http://github.com/jakiestfu/Behave.js/ * Version: 1.5 */ (function (undefined) { 'use strict'; var BehaveHooks = BehaveHooks || (function () { var hooks = {}; return { add: function (hookName, fn) { if (typeof hookName == "object") { var i; for (i = 0; i < hookName.length; i++) { var theHook = hookName[i]; if (!hooks[theHook]) { hooks[theHook] = []; } hooks[theHook].push(fn); } } else { if (!hooks[hookName]) { hooks[hookName] = []; } hooks[hookName].push(fn); } }, get: function (hookName) { if (hooks[hookName]) { return hooks[hookName]; } } }; })(), Behave = Behave || function (userOpts) { if (typeof String.prototype.repeat !== 'function') { String.prototype.repeat = function (times) { if (times < 1) { return ''; } if (times % 2) { return this.repeat(times - 1) + this; } var half = this.repeat(times / 2); return half + half; }; } if (typeof Array.prototype.filter !== 'function') { Array.prototype.filter = function (func /*, thisp */ ) { if (this === null) { throw new TypeError(); } var t = Object(this), len = t.length >>> 0; if (typeof func != "function") { throw new TypeError(); } var res = [], thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in t) { var val = t[i]; if (func.call(thisp, val, i, t)) { res.push(val); } } } return res; }; } var defaults = { textarea: null, replaceTab: true, softTabs: true, tabSize: 4, autoOpen: true, overwrite: true, autoStrip: true, autoIndent: true, fence: false }, tab, newLine, charSettings = { keyMap: [ { open: "\"", close: "\"", canBreak: false }, { open: "'", close: "'", canBreak: false }, { open: "(", close: ")", canBreak: false }, { open: "[", close: "]", canBreak: true }, { open: "{", close: "}", canBreak: true } ] }, utils = { _callHook: function (hookName, passData) { var hooks = BehaveHooks.get(hookName); passData = typeof passData == "boolean" && passData === false ? false : true; if (hooks) { if (passData) { var theEditor = defaults.textarea, textVal = theEditor.value, caretPos = utils.cursor.get(), i; for (i = 0; i < hooks.length; i++) { hooks[i].call(undefined, { editor: { element: theEditor, text: textVal, levelsDeep: utils.levelsDeep() }, caret: { pos: caretPos }, lines: { current: utils.cursor.getLine(textVal, caretPos), total: utils.editor.getLines(textVal) } }); } } else { for (i = 0; i < hooks.length; i++) { hooks[i].call(undefined); } } } }, defineNewLine: function () { var ta = document.createElement('textarea'); ta.value = "\n"; if (ta.value.length == 2) { newLine = "\r\n"; } else { newLine = "\n"; } }, defineTabSize: function (tabSize) { if (typeof defaults.textarea.style.OTabSize != "undefined") { defaults.textarea.style.OTabSize = tabSize; return; } if (typeof defaults.textarea.style.MozTabSize != "undefined") { defaults.textarea.style.MozTabSize = tabSize; return; } if (typeof defaults.textarea.style.tabSize != "undefined") { defaults.textarea.style.tabSize = tabSize; return; } }, cursor: { getLine: function (textVal, pos) { return ((textVal.substring(0, pos)).split("\n")).length; }, get: function () { if (typeof document.createElement('textarea').selectionStart === "number") { return defaults.textarea.selectionStart; } else if (document.selection) { var caretPos = 0, range = defaults.textarea.createTextRange(), rangeDupe = document.selection.createRange().duplicate(), rangeDupeBookmark = rangeDupe.getBookmark(); range.moveToBookmark(rangeDupeBookmark); while (range.moveStart('character', -1) !== 0) { caretPos++; } return caretPos; } }, set: function (start, end) { if (!end) { end = start; } if (defaults.textarea.setSelectionRange) { defaults.textarea.focus(); defaults.textarea.setSelectionRange(start, end); } else if (defaults.textarea.createTextRange) { var range = defaults.textarea.createTextRange(); range.collapse(true); range.moveEnd('character', end); range.moveStart('character', start); range.select(); } }, selection: function () { var textAreaElement = defaults.textarea, start = 0, end = 0, normalizedValue, range, textInputRange, len, endRange; if (typeof textAreaElement.selectionStart == "number" && typeof textAreaElement.selectionEnd == "number") { start = textAreaElement.selectionStart; end = textAreaElement.selectionEnd; } else { range = document.selection.createRange(); if (range && range.parentElement() == textAreaElement) { normalizedValue = utils.editor.get(); len = normalizedValue.length; textInputRange = textAreaElement.createTextRange(); textInputRange.moveToBookmark(range.getBookmark()); endRange = textAreaElement.createTextRange(); endRange.collapse(false); if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { start = end = len; } else { start = -textInputRange.moveStart("character", -len); start += normalizedValue.slice(0, start).split(newLine).length - 1; if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { end = len; } else { end = -textInputRange.moveEnd("character", -len); end += normalizedValue.slice(0, end).split(newLine).length - 1; } } } } return start == end ? false : { start: start, end: end }; } }, editor: { getLines: function (textVal) { return (textVal).split("\n").length; }, get: function () { return defaults.textarea.value.replace(/\r/g, ''); }, set: function (data) { defaults.textarea.value = data; } }, fenceRange: function () { if (typeof defaults.fence == "string") { var data = utils.editor.get(), pos = utils.cursor.get(), hacked = 0, matchedFence = data.indexOf(defaults.fence), matchCase = 0; while (matchedFence >= 0) { matchCase++; if (pos < (matchedFence + hacked)) { break; } hacked += matchedFence + defaults.fence.length; data = data.substring(matchedFence + defaults.fence.length); matchedFence = data.indexOf(defaults.fence); } if ((hacked) < pos && ((matchedFence + hacked) > pos) && matchCase % 2 === 0) { return true; } return false; } else { return true; } }, isEven: function (_this, i) { return i % 2; }, levelsDeep: function () { var pos = utils.cursor.get(), val = utils.editor.get(); var left = val.substring(0, pos), levels = 0, i, j; for (i = 0; i < left.length; i++) { for (j = 0; j < charSettings.keyMap.length; j++) { if (charSettings.keyMap[j].canBreak) { if (charSettings.keyMap[j].open == left.charAt(i)) { levels++; } if (charSettings.keyMap[j].close == left.charAt(i)) { levels--; } } } } var toDecrement = 0, quoteMap = ["'", "\""]; for (i = 0; i < charSettings.keyMap.length; i++) { if (charSettings.keyMap[i].canBreak) { for (j in quoteMap) { toDecrement += left.split(quoteMap[j]).filter(utils.isEven).join('').split(charSettings.keyMap[i].open).length - 1; } } } var finalLevels = levels - toDecrement; return finalLevels >= 0 ? finalLevels : 0; }, deepExtend: function (destination, source) { for (var property in source) { if (source[property] && source[property].constructor && source[property].constructor === Object) { destination[property] = destination[property] || {}; utils.deepExtend(destination[property], source[property]); } else { destination[property] = source[property]; } } return destination; }, addEvent: function addEvent(element, eventName, func) { if (element.addEventListener) { element.addEventListener(eventName, func, false); } else if (element.attachEvent) { element.attachEvent("on" + eventName, func); } }, removeEvent: function addEvent(element, eventName, func) { if (element.addEventListener) { element.removeEventListener(eventName, func, false); } else if (element.attachEvent) { element.detachEvent("on" + eventName, func); } }, preventDefaultEvent: function (e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } } }, intercept = { tabKey: function (e) { if (!utils.fenceRange()) { return; } if (e.keyCode == 9) { utils.preventDefaultEvent(e); var toReturn = true; utils._callHook('tab:before'); var selection = utils.cursor.selection(), pos = utils.cursor.get(), val = utils.editor.get(); if (selection) { var tempStart = selection.start; while (tempStart--) { if (val.charAt(tempStart) == "\n") { selection.start = tempStart + 1; break; } } var toIndent = val.substring(selection.start, selection.end), lines = toIndent.split("\n"), i; if (e.shiftKey) { for (i = 0; i < lines.length; i++) { if (lines[i].substring(0, tab.length) == tab) { lines[i] = lines[i].substring(tab.length); } } toIndent = lines.join("\n"); utils.editor.set(val.substring(0, selection.start) + toIndent + val.substring(selection.end)); utils.cursor.set(selection.start, selection.start + toIndent.length); } else { for (i in lines) { lines[i] = tab + lines[i]; } toIndent = lines.join("\n"); utils.editor.set(val.substring(0, selection.start) + toIndent + val.substring(selection.end)); utils.cursor.set(selection.start, selection.start + toIndent.length); } } else { var left = val.substring(0, pos), right = val.substring(pos), edited = left + tab + right; if (e.shiftKey) { if (val.substring(pos - tab.length, pos) == tab) { edited = val.substring(0, pos - tab.length) + right; utils.editor.set(edited); utils.cursor.set(pos - tab.length); } } else { utils.editor.set(edited); utils.cursor.set(pos + tab.length); toReturn = false; } } utils._callHook('tab:after'); } return toReturn; }, enterKey: function (e) { if (!utils.fenceRange()) { return; } if (e.keyCode == 13) { utils.preventDefaultEvent(e); utils._callHook('enter:before'); var pos = utils.cursor.get(), val = utils.editor.get(), left = val.substring(0, pos), right = val.substring(pos), leftChar = left.charAt(left.length - 1), rightChar = right.charAt(0), numTabs = utils.levelsDeep(), ourIndent = "", closingBreak = "", finalCursorPos, i; if (!numTabs) { finalCursorPos = 1; } else { while (numTabs--) { ourIndent += tab; } ourIndent = ourIndent; finalCursorPos = ourIndent.length + 1; for (i = 0; i < charSettings.keyMap.length; i++) { if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) { closingBreak = newLine; } } } var edited = left + newLine + ourIndent + closingBreak + (ourIndent.substring(0, ourIndent.length - tab.length)) + right; utils.editor.set(edited); utils.cursor.set(pos + finalCursorPos); utils._callHook('enter:after'); } }, deleteKey: function (e) { if (!utils.fenceRange()) { return; } if (e.keyCode == 8) { utils.preventDefaultEvent(e); utils._callHook('delete:before'); var pos = utils.cursor.get(), val = utils.editor.get(), left = val.substring(0, pos), right = val.substring(pos), leftChar = left.charAt(left.length - 1), rightChar = right.charAt(0), i; if (utils.cursor.selection() === false) { for (i = 0; i < charSettings.keyMap.length; i++) { if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) { var edited = val.substring(0, pos - 1) + val.substring(pos + 1); utils.editor.set(edited); utils.cursor.set(pos - 1); return; } } var edited = val.substring(0, pos - 1) + val.substring(pos); utils.editor.set(edited); utils.cursor.set(pos - 1); } else { var sel = utils.cursor.selection(), edited = val.substring(0, sel.start) + val.substring(sel.end); utils.editor.set(edited); utils.cursor.set(pos); } utils._callHook('delete:after'); } } }, charFuncs = { openedChar: function (_char, e) { utils.preventDefaultEvent(e); utils._callHook('openChar:before'); var pos = utils.cursor.get(), val = utils.editor.get(), left = val.substring(0, pos), right = val.substring(pos), edited = left + _char.open + _char.close + right; defaults.textarea.value = edited; utils.cursor.set(pos + 1); utils._callHook('openChar:after'); }, closedChar: function (_char, e) { var pos = utils.cursor.get(), val = utils.editor.get(), toOverwrite = val.substring(pos, pos + 1); if (toOverwrite == _char.close) { utils.preventDefaultEvent(e); utils._callHook('closeChar:before'); utils.cursor.set(utils.cursor.get() + 1); utils._callHook('closeChar:after'); return true; } return false; } }, action = { filter: function (e) { if (!utils.fenceRange()) { return; } var theCode = e.which || e.keyCode; if (theCode == 39 || theCode == 40 && e.which === 0) { return; } var _char = String.fromCharCode(theCode), i; for (i = 0; i < charSettings.keyMap.length; i++) { if (charSettings.keyMap[i].close == _char) { var didClose = defaults.overwrite && charFuncs.closedChar(charSettings.keyMap[i], e); if (!didClose && charSettings.keyMap[i].open == _char && defaults.autoOpen) { charFuncs.openedChar(charSettings.keyMap[i], e); } } else if (charSettings.keyMap[i].open == _char && defaults.autoOpen) { charFuncs.openedChar(charSettings.keyMap[i], e); } } }, listen: function () { if (defaults.replaceTab) { utils.addEvent(defaults.textarea, 'keydown', intercept.tabKey); } if (defaults.autoIndent) { utils.addEvent(defaults.textarea, 'keydown', intercept.enterKey); } if (defaults.autoStrip) { utils.addEvent(defaults.textarea, 'keydown', intercept.deleteKey); } utils.addEvent(defaults.textarea, 'keypress', action.filter); utils.addEvent(defaults.textarea, 'keydown', function () { utils._callHook('keydown'); }); utils.addEvent(defaults.textarea, 'keyup', function () { utils._callHook('keyup'); }); } }, init = function (opts) { if (opts.textarea) { utils._callHook('init:before', false); utils.deepExtend(defaults, opts); utils.defineNewLine(); if (defaults.softTabs) { tab = " ".repeat(defaults.tabSize); } else { tab = "\t"; utils.defineTabSize(defaults.tabSize); } action.listen(); utils._callHook('init:after', false); } }; this.destroy = function () { utils.removeEvent(defaults.textarea, 'keydown', intercept.tabKey); utils.removeEvent(defaults.textarea, 'keydown', intercept.enterKey); utils.removeEvent(defaults.textarea, 'keydown', intercept.deleteKey); utils.removeEvent(defaults.textarea, 'keypress', action.filter); }; init(userOpts); }; if (typeof module !== 'undefined' && module.exports) { module.exports = Behave; } if (typeof ender === 'undefined') { this.Behave = Behave; this.BehaveHooks = BehaveHooks; } if (typeof define === "function" && define.amd) { define("behave", [], function () { return Behave; }); } }).call(this);