textarea.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /**
  2. * textarea.js - textarea element for blessed
  3. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
  4. * https://github.com/chjj/blessed
  5. */
  6. /**
  7. * Modules
  8. */
  9. var unicode = require('../unicode');
  10. var nextTick = global.setImmediate || process.nextTick.bind(process);
  11. var Node = require('./node');
  12. var Input = require('./input');
  13. /**
  14. * Textarea
  15. */
  16. function Textarea(options) {
  17. var self = this;
  18. if (!(this instanceof Node)) {
  19. return new Textarea(options);
  20. }
  21. options = options || {};
  22. options.scrollable = options.scrollable !== false;
  23. Input.call(this, options);
  24. this.screen._listenKeys(this);
  25. this.value = options.value || '';
  26. this.__updateCursor = this._updateCursor.bind(this);
  27. this.on('resize', this.__updateCursor);
  28. this.on('move', this.__updateCursor);
  29. if (options.inputOnFocus) {
  30. this.on('focus', this.readInput.bind(this, null));
  31. }
  32. if (!options.inputOnFocus && options.keys) {
  33. this.on('keypress', function(ch, key) {
  34. if (self._reading) return;
  35. if (key.name === 'enter' || (options.vi && key.name === 'i')) {
  36. return self.readInput();
  37. }
  38. if (key.name === 'e') {
  39. return self.readEditor();
  40. }
  41. });
  42. }
  43. if (options.mouse) {
  44. this.on('click', function(data) {
  45. if (self._reading) return;
  46. if (data.button !== 'right') return;
  47. self.readEditor();
  48. });
  49. }
  50. }
  51. Textarea.prototype.__proto__ = Input.prototype;
  52. Textarea.prototype.type = 'textarea';
  53. Textarea.prototype._updateCursor = function(get) {
  54. if (this.screen.focused !== this) {
  55. return;
  56. }
  57. var lpos = get ? this.lpos : this._getCoords();
  58. if (!lpos) return;
  59. var last = this._clines[this._clines.length - 1]
  60. , program = this.screen.program
  61. , line
  62. , cx
  63. , cy;
  64. // Stop a situation where the textarea begins scrolling
  65. // and the last cline appears to always be empty from the
  66. // _typeScroll `+ '\n'` thing.
  67. // Maybe not necessary anymore?
  68. if (last === '' && this.value[this.value.length - 1] !== '\n') {
  69. last = this._clines[this._clines.length - 2] || '';
  70. }
  71. line = Math.min(
  72. this._clines.length - 1 - (this.childBase || 0),
  73. (lpos.yl - lpos.yi) - this.iheight - 1);
  74. // When calling clearValue() on a full textarea with a border, the first
  75. // argument in the above Math.min call ends up being -2. Make sure we stay
  76. // positive.
  77. line = Math.max(0, line);
  78. cy = lpos.yi + this.itop + line;
  79. cx = lpos.xi + this.ileft + this.strWidth(last);
  80. // XXX Not sure, but this may still sometimes
  81. // cause problems when leaving editor.
  82. if (cy === program.y && cx === program.x) {
  83. return;
  84. }
  85. if (cy === program.y) {
  86. if (cx > program.x) {
  87. program.cuf(cx - program.x);
  88. } else if (cx < program.x) {
  89. program.cub(program.x - cx);
  90. }
  91. } else if (cx === program.x) {
  92. if (cy > program.y) {
  93. program.cud(cy - program.y);
  94. } else if (cy < program.y) {
  95. program.cuu(program.y - cy);
  96. }
  97. } else {
  98. program.cup(cy, cx);
  99. }
  100. };
  101. Textarea.prototype.input =
  102. Textarea.prototype.setInput =
  103. Textarea.prototype.readInput = function(callback) {
  104. var self = this
  105. , focused = this.screen.focused === this;
  106. if (this._reading) return;
  107. this._reading = true;
  108. this._callback = callback;
  109. if (!focused) {
  110. this.screen.saveFocus();
  111. this.focus();
  112. }
  113. this.screen.grabKeys = true;
  114. this._updateCursor();
  115. this.screen.program.showCursor();
  116. //this.screen.program.sgr('normal');
  117. this._done = function fn(err, value) {
  118. if (!self._reading) return;
  119. if (fn.done) return;
  120. fn.done = true;
  121. self._reading = false;
  122. delete self._callback;
  123. delete self._done;
  124. self.removeListener('keypress', self.__listener);
  125. delete self.__listener;
  126. self.removeListener('blur', self.__done);
  127. delete self.__done;
  128. self.screen.program.hideCursor();
  129. self.screen.grabKeys = false;
  130. if (!focused) {
  131. self.screen.restoreFocus();
  132. }
  133. if (self.options.inputOnFocus) {
  134. self.screen.rewindFocus();
  135. }
  136. // Ugly
  137. if (err === 'stop') return;
  138. if (err) {
  139. self.emit('error', err);
  140. } else if (value != null) {
  141. self.emit('submit', value);
  142. } else {
  143. self.emit('cancel', value);
  144. }
  145. self.emit('action', value);
  146. if (!callback) return;
  147. return err
  148. ? callback(err)
  149. : callback(null, value);
  150. };
  151. // Put this in a nextTick so the current
  152. // key event doesn't trigger any keys input.
  153. nextTick(function() {
  154. self.__listener = self._listener.bind(self);
  155. self.on('keypress', self.__listener);
  156. });
  157. this.__done = this._done.bind(this, null, null);
  158. this.on('blur', this.__done);
  159. };
  160. Textarea.prototype._listener = function(ch, key) {
  161. var done = this._done
  162. , value = this.value;
  163. if (key.name === 'return') return;
  164. if (key.name === 'enter') {
  165. ch = '\n';
  166. }
  167. // TODO: Handle directional keys.
  168. if (key.name === 'left' || key.name === 'right'
  169. || key.name === 'up' || key.name === 'down') {
  170. ;
  171. }
  172. if (this.options.keys && key.ctrl && key.name === 'e') {
  173. return this.readEditor();
  174. }
  175. // TODO: Optimize typing by writing directly
  176. // to the screen and screen buffer here.
  177. if (key.name === 'escape') {
  178. done(null, null);
  179. } else if (key.name === 'backspace') {
  180. if (this.value.length) {
  181. if (this.screen.fullUnicode) {
  182. if (unicode.isSurrogate(this.value, this.value.length - 2)) {
  183. // || unicode.isCombining(this.value, this.value.length - 1)) {
  184. this.value = this.value.slice(0, -2);
  185. } else {
  186. this.value = this.value.slice(0, -1);
  187. }
  188. } else {
  189. this.value = this.value.slice(0, -1);
  190. }
  191. }
  192. } else if (ch) {
  193. if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
  194. this.value += ch;
  195. }
  196. }
  197. if (this.value !== value) {
  198. this.screen.render();
  199. }
  200. };
  201. Textarea.prototype._typeScroll = function() {
  202. // XXX Workaround
  203. var height = this.height - this.iheight;
  204. if (this._clines.length - this.childBase > height) {
  205. this.scroll(this._clines.length);
  206. }
  207. };
  208. Textarea.prototype.getValue = function() {
  209. return this.value;
  210. };
  211. Textarea.prototype.setValue = function(value) {
  212. if (value == null) {
  213. value = this.value;
  214. }
  215. if (this._value !== value) {
  216. this.value = value;
  217. this._value = value;
  218. this.setContent(this.value);
  219. this._typeScroll();
  220. this._updateCursor();
  221. }
  222. };
  223. Textarea.prototype.clearInput =
  224. Textarea.prototype.clearValue = function() {
  225. return this.setValue('');
  226. };
  227. Textarea.prototype.submit = function() {
  228. if (!this.__listener) return;
  229. return this.__listener('\x1b', { name: 'escape' });
  230. };
  231. Textarea.prototype.cancel = function() {
  232. if (!this.__listener) return;
  233. return this.__listener('\x1b', { name: 'escape' });
  234. };
  235. Textarea.prototype.render = function() {
  236. this.setValue();
  237. return this._render();
  238. };
  239. Textarea.prototype.editor =
  240. Textarea.prototype.setEditor =
  241. Textarea.prototype.readEditor = function(callback) {
  242. var self = this;
  243. if (this._reading) {
  244. var _cb = this._callback
  245. , cb = callback;
  246. this._done('stop');
  247. callback = function(err, value) {
  248. if (_cb) _cb(err, value);
  249. if (cb) cb(err, value);
  250. };
  251. }
  252. if (!callback) {
  253. callback = function() {};
  254. }
  255. return this.screen.readEditor({ value: this.value }, function(err, value) {
  256. if (err) {
  257. if (err.message === 'Unsuccessful.') {
  258. self.screen.render();
  259. return self.readInput(callback);
  260. }
  261. self.screen.render();
  262. self.readInput(callback);
  263. return callback(err);
  264. }
  265. self.setValue(value);
  266. self.screen.render();
  267. return self.readInput(callback);
  268. });
  269. };
  270. /**
  271. * Expose
  272. */
  273. module.exports = Textarea;