Textarea Plus

An userscript to improve plain textarea for code editing.

As of 11.12.2014. See ბოლო ვერსია.

// ==UserScript==
// @name        Textarea Plus
// @description	An userscript to improve plain textarea for code editing.
// @namespace   eight04.blogspot.com
// @include     http*
// @version     1.1.1
// @grant       GM_addStyle
// ==/UserScript==

"use strict";

var ignoreClassList = ["CodeMirror", "ace_editor"];

var textareaPlus = function(){

	var editor = {
		indent: indent,
		unindent: unindent,
		home: home,
		selectHome: selectHome,
		enter: enter,
		brace0: brace0,
		brace1: brace1
	};

	function selectionRange(data){
		return data.pos[1] - data.pos[0];
	}

	function takeRange(data) {
		if (data.pos[0] > data.pos[1]) {
			return [data.pos[1], data.pos[0]];
		}
		return data.pos;
	}

	function insert(data, text){
		var pos = takeRange(data);
		data.text = data.text.substr(0, pos[0]) + text + data.text.substr(pos[1]);
		data.pos[0] = pos[0] + text.length;
		data.pos[1] = data.pos[0];
	}

	function del(data){
		if (!selectionRange(data)) {
			data.pos[1]++;
		}
		insert(data, "");
	}

	function getLineStart(data, pos){
		if (pos == undefined) {
			pos = data.pos[1];
		}

		if (data.text[pos] == "\n") {
			pos--;
		}

		var s = data.text.lastIndexOf("\n", pos);
		if (s < 0) {
			return 0;
		}
		return s + 1;
	}

	function getLineEnd(data, pos){
		if (pos == undefined) {
			pos = data.pos[1];
		}
		var s = data.text.indexOf("\n", pos);
		if (s < 0) {
			return data.text.length;
		}
		return s;
	}

	function multiIndent(data){
		var pos = takeRange(data);
		var lineStart = getLineStart(data, pos[0]), lineEnd = getLineEnd(data, pos[1]);
		var lines = data.text.substr(lineStart, lineEnd - lineStart);
		var i;

		lines = lines.split("\n");
		for (i = 0; i < lines.length; i++) {
			lines[i] = "\t" + lines[i];
		}
		lines = lines.join("\n");
		data.text = data.text.substr(0, lineStart) + lines + data.text.substr(lineEnd);
		if (data.pos[0] > data.pos[1]) {
			data.pos[1] = lineStart;
			data.pos[0] = lineEnd + i;
		} else {
			data.pos[0] = lineStart;
			data.pos[1] = lineEnd + i;
		}
	}

	function inMultiLine(data) {
		var pos = takeRange(data);
		var s = data.text.indexOf("\n", pos[0]);
		if (s < 0) {
			return false;
		}
		return  s < pos[1];
	}

	function indent(data){
		if (!selectionRange(data)) {
			insert(data, "\t");
			if (data.pos[0] < getTextStart(data)) {
				home(data);
			}
		} else {
			if (inMultiLine(data)) {
				multiIndent(data);
			} else {
				insert(data, "\t");
			}
		}
	}

	function multiUnindent(data){
		var pos = takeRange(data);
		var lineStart = getLineStart(data, pos[0]), lineEnd = getLineEnd(data, pos[1]);
		var lines = data.text.substr(lineStart, lineEnd - lineStart);
		var i, m;

		lines = lines.split("\n");
		var len = 0;
		for (i = 0; i < lines.length; i++) {
			m = lines[i].match(/^( {4}| {0,3}\t?)(.*)$/);
			// console.log(m);
			len += m[1].length;
			lines[i] = m[2];
		}
		lines = lines.join("\n");
		data.text = data.text.substr(0, lineStart) + lines + data.text.substr(lineEnd);
		if (data.pos[0] > data.pos[1]) {
			data.pos[1] = lineStart;
			data.pos[0] = lineEnd - len;
		} else {
			data.pos[0] = lineStart;
			data.pos[1] = lineEnd - len;
		}
	}

	function backspace(data) {

		if (selectionRange(data)) {
			del(data);
		} else if (data.pos[0] > 0) {
			data.pos[0]--;
			del(data);
		}
	}

	function unindent(data) {
		if (inMultiLine(data)) {
			multiUnindent(data);
		} else if (!selectionRange(data) && data.text[data.pos[0] - 1] == "\t") {
			backspace(data);
		} else {
			multiUnindent(data);
			home(data);
		}
	}

	function searchFrom(text, re, pos) {
		pos = pos || 0;
		var t = text.substr(pos);
		var s = t.search(re);
		if (s < 0) {
			return -1;
		}
		return s + pos;
	}

	function getTextStart(data, pos) {
		var lineStart = getLineStart(data, pos);
		pos = searchFrom(data.text, /[\S\n]/, lineStart);
		if (pos < 0 || data.text[pos] == "\n") {
			return getLineEnd(data);
		}
		return pos;
	}

	function isTextStart(data) {
		return getTextStart(data) == data.pos[1];
	}

	function home(data) {
		if (isTextStart(data)) {
			data.pos[0] = getLineStart(data);
		} else {
			data.pos[0] = getTextStart(data);
		}
		data.pos[1] = data.pos[0];
	}

	function selectHome(data) {
		var pos = data.pos[0];
		home(data);
		data.pos[0] = pos;
	}

	function getIndents(data) {
		var pos = takeRange(data);
		var lineStart = getLineStart(data, pos[0]);
		var len;

		var textStart = getTextStart(data, pos[0]);
		// console.log(textStart);
		if (textStart >= pos[0]) {
			len = pos[0] - lineStart;
		} else {
			len = textStart - lineStart;
		}
		return data.text.substr(lineStart, len);
	}

	function enter(data) {
		var indents = getIndents(data);
		var range = takeRange(data);
		var p = data.text[range[0] - 1];
		var q = data.text[range[1]];
		insert(data, "\n" + indents);
		if (p == "[" && q == "]" || p == "{" && q == "}") {
			insert(data, "\t\n" + indents);
			data.pos[0] -= indents.length + 1;
			data.pos[1] -= indents.length + 1;
		}
	}

	function brace0(data){
		insert(data, "[]");
		data.pos[0]--;
		data.pos[1]--;
	}

	function brace1(data){
		insert(data, "{}");
		data.pos[0]--;
		data.pos[1]--;
	}

	function init(node, command) {
		var data = {
			text: node.value,
			pos: [node.selectionStart, node.selectionEnd]
		}, t;

		if (node.selectionDirection == "backward") {
			t = data.pos[0];
			data.pos[0] = data.pos[1];
			data.pos[1] = t;
		}

		editor[command](data);

		node.value = data.text;
		if (data.pos[0] > data.pos[1]) {
			node.setSelectionRange(data.pos[1], data.pos[0], "backward");
		} else {
			node.setSelectionRange(data.pos[0], data.pos[1], "forward");
		}
	}

	return init;
}();

function validArea(area) {
	if (area.nodeName != "TEXTAREA") {
		return false;
	}

	if (area.dataset.textareaPlus === "false") {
		return false;
	}

	if (area.dataset.textareaPlus === "true") {
		return true;
	}

	// if (area.onkeydown) {
		// area.dataset.textareaPlus = "false";
		// return false;
	// }

	var node = area, i;
	while ((node = node.parentNode) != document.body) {
		for (i = 0; i < ignoreClassList.length; i++) {
			if (node.classList.contains(ignoreClassList[i])) {
				area.dataset.textareaPlus = "false";
				return false;
			}
		}
	}

	area.dataset.textareaPlus = "true";
	return true;
}

window.addEventListener("keydown", function(e){
	if (!validArea(e.target) || e.ctrlKey || e.altKey) {
		return;
	}

	var command;

	if (e.keyCode == 9) {
		// tab
		if (e.shiftKey) {
			command = "unindent";
		} else {
			command = "indent";
		}
	} else if (e.keyCode == 13) {
		// enter
		command = "enter";
	} else if (e.keyCode == 36) {
		// home
		if (!e.shiftKey) {
			command = "home";
		} else {
			command = "selectHome";
		}
	} else if (e.keyCode == 219) {
		// braces
		if (!e.shiftKey) {
			command = "brace0";
		} else {
			command = "brace1";
		}
	} else {
		return;
	}

	e.preventDefault();
	e.stopPropagation();

	textareaPlus(e.target, command);
}, true);

GM_addStyle("textarea {tab-size: 4; -moz-tab-size: 4; -o-tab-size: 4;}");