Textarea Plus

An userscript to improve plain textarea for code editing.

当前为 2014-12-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Textarea Plus
  3. // @description An userscript to improve plain textarea for code editing.
  4. // @namespace eight04.blogspot.com
  5. // @include http*
  6. // @version 1.1.1
  7. // @grant GM_addStyle
  8. // ==/UserScript==
  9.  
  10. "use strict";
  11.  
  12. var ignoreClassList = ["CodeMirror", "ace_editor"];
  13.  
  14. var textareaPlus = function(){
  15.  
  16. var editor = {
  17. indent: indent,
  18. unindent: unindent,
  19. home: home,
  20. selectHome: selectHome,
  21. enter: enter,
  22. brace0: brace0,
  23. brace1: brace1
  24. };
  25.  
  26. function selectionRange(data){
  27. return data.pos[1] - data.pos[0];
  28. }
  29.  
  30. function takeRange(data) {
  31. if (data.pos[0] > data.pos[1]) {
  32. return [data.pos[1], data.pos[0]];
  33. }
  34. return data.pos;
  35. }
  36.  
  37. function insert(data, text){
  38. var pos = takeRange(data);
  39. data.text = data.text.substr(0, pos[0]) + text + data.text.substr(pos[1]);
  40. data.pos[0] = pos[0] + text.length;
  41. data.pos[1] = data.pos[0];
  42. }
  43.  
  44. function del(data){
  45. if (!selectionRange(data)) {
  46. data.pos[1]++;
  47. }
  48. insert(data, "");
  49. }
  50.  
  51. function getLineStart(data, pos){
  52. if (pos == undefined) {
  53. pos = data.pos[1];
  54. }
  55.  
  56. if (data.text[pos] == "\n") {
  57. pos--;
  58. }
  59.  
  60. var s = data.text.lastIndexOf("\n", pos);
  61. if (s < 0) {
  62. return 0;
  63. }
  64. return s + 1;
  65. }
  66.  
  67. function getLineEnd(data, pos){
  68. if (pos == undefined) {
  69. pos = data.pos[1];
  70. }
  71. var s = data.text.indexOf("\n", pos);
  72. if (s < 0) {
  73. return data.text.length;
  74. }
  75. return s;
  76. }
  77.  
  78. function multiIndent(data){
  79. var pos = takeRange(data);
  80. var lineStart = getLineStart(data, pos[0]), lineEnd = getLineEnd(data, pos[1]);
  81. var lines = data.text.substr(lineStart, lineEnd - lineStart);
  82. var i;
  83.  
  84. lines = lines.split("\n");
  85. for (i = 0; i < lines.length; i++) {
  86. lines[i] = "\t" + lines[i];
  87. }
  88. lines = lines.join("\n");
  89. data.text = data.text.substr(0, lineStart) + lines + data.text.substr(lineEnd);
  90. if (data.pos[0] > data.pos[1]) {
  91. data.pos[1] = lineStart;
  92. data.pos[0] = lineEnd + i;
  93. } else {
  94. data.pos[0] = lineStart;
  95. data.pos[1] = lineEnd + i;
  96. }
  97. }
  98.  
  99. function inMultiLine(data) {
  100. var pos = takeRange(data);
  101. var s = data.text.indexOf("\n", pos[0]);
  102. if (s < 0) {
  103. return false;
  104. }
  105. return s < pos[1];
  106. }
  107.  
  108. function indent(data){
  109. if (!selectionRange(data)) {
  110. insert(data, "\t");
  111. if (data.pos[0] < getTextStart(data)) {
  112. home(data);
  113. }
  114. } else {
  115. if (inMultiLine(data)) {
  116. multiIndent(data);
  117. } else {
  118. insert(data, "\t");
  119. }
  120. }
  121. }
  122.  
  123. function multiUnindent(data){
  124. var pos = takeRange(data);
  125. var lineStart = getLineStart(data, pos[0]), lineEnd = getLineEnd(data, pos[1]);
  126. var lines = data.text.substr(lineStart, lineEnd - lineStart);
  127. var i, m;
  128.  
  129. lines = lines.split("\n");
  130. var len = 0;
  131. for (i = 0; i < lines.length; i++) {
  132. m = lines[i].match(/^( {4}| {0,3}\t?)(.*)$/);
  133. // console.log(m);
  134. len += m[1].length;
  135. lines[i] = m[2];
  136. }
  137. lines = lines.join("\n");
  138. data.text = data.text.substr(0, lineStart) + lines + data.text.substr(lineEnd);
  139. if (data.pos[0] > data.pos[1]) {
  140. data.pos[1] = lineStart;
  141. data.pos[0] = lineEnd - len;
  142. } else {
  143. data.pos[0] = lineStart;
  144. data.pos[1] = lineEnd - len;
  145. }
  146. }
  147.  
  148. function backspace(data) {
  149.  
  150. if (selectionRange(data)) {
  151. del(data);
  152. } else if (data.pos[0] > 0) {
  153. data.pos[0]--;
  154. del(data);
  155. }
  156. }
  157.  
  158. function unindent(data) {
  159. if (inMultiLine(data)) {
  160. multiUnindent(data);
  161. } else if (!selectionRange(data) && data.text[data.pos[0] - 1] == "\t") {
  162. backspace(data);
  163. } else {
  164. multiUnindent(data);
  165. home(data);
  166. }
  167. }
  168.  
  169. function searchFrom(text, re, pos) {
  170. pos = pos || 0;
  171. var t = text.substr(pos);
  172. var s = t.search(re);
  173. if (s < 0) {
  174. return -1;
  175. }
  176. return s + pos;
  177. }
  178.  
  179. function getTextStart(data, pos) {
  180. var lineStart = getLineStart(data, pos);
  181. pos = searchFrom(data.text, /[\S\n]/, lineStart);
  182. if (pos < 0 || data.text[pos] == "\n") {
  183. return getLineEnd(data);
  184. }
  185. return pos;
  186. }
  187.  
  188. function isTextStart(data) {
  189. return getTextStart(data) == data.pos[1];
  190. }
  191.  
  192. function home(data) {
  193. if (isTextStart(data)) {
  194. data.pos[0] = getLineStart(data);
  195. } else {
  196. data.pos[0] = getTextStart(data);
  197. }
  198. data.pos[1] = data.pos[0];
  199. }
  200.  
  201. function selectHome(data) {
  202. var pos = data.pos[0];
  203. home(data);
  204. data.pos[0] = pos;
  205. }
  206.  
  207. function getIndents(data) {
  208. var pos = takeRange(data);
  209. var lineStart = getLineStart(data, pos[0]);
  210. var len;
  211.  
  212. var textStart = getTextStart(data, pos[0]);
  213. // console.log(textStart);
  214. if (textStart >= pos[0]) {
  215. len = pos[0] - lineStart;
  216. } else {
  217. len = textStart - lineStart;
  218. }
  219. return data.text.substr(lineStart, len);
  220. }
  221.  
  222. function enter(data) {
  223. var indents = getIndents(data);
  224. var range = takeRange(data);
  225. var p = data.text[range[0] - 1];
  226. var q = data.text[range[1]];
  227. insert(data, "\n" + indents);
  228. if (p == "[" && q == "]" || p == "{" && q == "}") {
  229. insert(data, "\t\n" + indents);
  230. data.pos[0] -= indents.length + 1;
  231. data.pos[1] -= indents.length + 1;
  232. }
  233. }
  234.  
  235. function brace0(data){
  236. insert(data, "[]");
  237. data.pos[0]--;
  238. data.pos[1]--;
  239. }
  240.  
  241. function brace1(data){
  242. insert(data, "{}");
  243. data.pos[0]--;
  244. data.pos[1]--;
  245. }
  246.  
  247. function init(node, command) {
  248. var data = {
  249. text: node.value,
  250. pos: [node.selectionStart, node.selectionEnd]
  251. }, t;
  252.  
  253. if (node.selectionDirection == "backward") {
  254. t = data.pos[0];
  255. data.pos[0] = data.pos[1];
  256. data.pos[1] = t;
  257. }
  258.  
  259. editor[command](data);
  260.  
  261. node.value = data.text;
  262. if (data.pos[0] > data.pos[1]) {
  263. node.setSelectionRange(data.pos[1], data.pos[0], "backward");
  264. } else {
  265. node.setSelectionRange(data.pos[0], data.pos[1], "forward");
  266. }
  267. }
  268.  
  269. return init;
  270. }();
  271.  
  272. function validArea(area) {
  273. if (area.nodeName != "TEXTAREA") {
  274. return false;
  275. }
  276.  
  277. if (area.dataset.textareaPlus === "false") {
  278. return false;
  279. }
  280.  
  281. if (area.dataset.textareaPlus === "true") {
  282. return true;
  283. }
  284.  
  285. // if (area.onkeydown) {
  286. // area.dataset.textareaPlus = "false";
  287. // return false;
  288. // }
  289.  
  290. var node = area, i;
  291. while ((node = node.parentNode) != document.body) {
  292. for (i = 0; i < ignoreClassList.length; i++) {
  293. if (node.classList.contains(ignoreClassList[i])) {
  294. area.dataset.textareaPlus = "false";
  295. return false;
  296. }
  297. }
  298. }
  299.  
  300. area.dataset.textareaPlus = "true";
  301. return true;
  302. }
  303.  
  304. window.addEventListener("keydown", function(e){
  305. if (!validArea(e.target) || e.ctrlKey || e.altKey) {
  306. return;
  307. }
  308.  
  309. var command;
  310.  
  311. if (e.keyCode == 9) {
  312. // tab
  313. if (e.shiftKey) {
  314. command = "unindent";
  315. } else {
  316. command = "indent";
  317. }
  318. } else if (e.keyCode == 13) {
  319. // enter
  320. command = "enter";
  321. } else if (e.keyCode == 36) {
  322. // home
  323. if (!e.shiftKey) {
  324. command = "home";
  325. } else {
  326. command = "selectHome";
  327. }
  328. } else if (e.keyCode == 219) {
  329. // braces
  330. if (!e.shiftKey) {
  331. command = "brace0";
  332. } else {
  333. command = "brace1";
  334. }
  335. } else {
  336. return;
  337. }
  338.  
  339. e.preventDefault();
  340. e.stopPropagation();
  341.  
  342. textareaPlus(e.target, command);
  343. }, true);
  344.  
  345. GM_addStyle("textarea {tab-size: 4; -moz-tab-size: 4; -o-tab-size: 4;}");