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