123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /**
- * textarea.js - textarea element for blessed
- * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
- * https://github.com/chjj/blessed
- */
- /**
- * Modules
- */
- var unicode = require('../unicode');
- var nextTick = global.setImmediate || process.nextTick.bind(process);
- var Node = require('./node');
- var Input = require('./input');
- /**
- * Textarea
- */
- function Textarea(options) {
- var self = this;
- if (!(this instanceof Node)) {
- return new Textarea(options);
- }
- options = options || {};
- options.scrollable = options.scrollable !== false;
- Input.call(this, options);
- this.screen._listenKeys(this);
- this.value = options.value || '';
- this.__updateCursor = this._updateCursor.bind(this);
- this.on('resize', this.__updateCursor);
- this.on('move', this.__updateCursor);
- if (options.inputOnFocus) {
- this.on('focus', this.readInput.bind(this, null));
- }
- if (!options.inputOnFocus && options.keys) {
- this.on('keypress', function(ch, key) {
- if (self._reading) return;
- if (key.name === 'enter' || (options.vi && key.name === 'i')) {
- return self.readInput();
- }
- if (key.name === 'e') {
- return self.readEditor();
- }
- });
- }
- if (options.mouse) {
- this.on('click', function(data) {
- if (self._reading) return;
- if (data.button !== 'right') return;
- self.readEditor();
- });
- }
- }
- Textarea.prototype.__proto__ = Input.prototype;
- Textarea.prototype.type = 'textarea';
- Textarea.prototype._updateCursor = function(get) {
- if (this.screen.focused !== this) {
- return;
- }
- var lpos = get ? this.lpos : this._getCoords();
- if (!lpos) return;
- var last = this._clines[this._clines.length - 1]
- , program = this.screen.program
- , line
- , cx
- , cy;
- // Stop a situation where the textarea begins scrolling
- // and the last cline appears to always be empty from the
- // _typeScroll `+ '\n'` thing.
- // Maybe not necessary anymore?
- if (last === '' && this.value[this.value.length - 1] !== '\n') {
- last = this._clines[this._clines.length - 2] || '';
- }
- line = Math.min(
- this._clines.length - 1 - (this.childBase || 0),
- (lpos.yl - lpos.yi) - this.iheight - 1);
- // When calling clearValue() on a full textarea with a border, the first
- // argument in the above Math.min call ends up being -2. Make sure we stay
- // positive.
- line = Math.max(0, line);
- cy = lpos.yi + this.itop + line;
- cx = lpos.xi + this.ileft + this.strWidth(last);
- // XXX Not sure, but this may still sometimes
- // cause problems when leaving editor.
- if (cy === program.y && cx === program.x) {
- return;
- }
- if (cy === program.y) {
- if (cx > program.x) {
- program.cuf(cx - program.x);
- } else if (cx < program.x) {
- program.cub(program.x - cx);
- }
- } else if (cx === program.x) {
- if (cy > program.y) {
- program.cud(cy - program.y);
- } else if (cy < program.y) {
- program.cuu(program.y - cy);
- }
- } else {
- program.cup(cy, cx);
- }
- };
- Textarea.prototype.input =
- Textarea.prototype.setInput =
- Textarea.prototype.readInput = function(callback) {
- var self = this
- , focused = this.screen.focused === this;
- if (this._reading) return;
- this._reading = true;
- this._callback = callback;
- if (!focused) {
- this.screen.saveFocus();
- this.focus();
- }
- this.screen.grabKeys = true;
- this._updateCursor();
- this.screen.program.showCursor();
- //this.screen.program.sgr('normal');
- this._done = function fn(err, value) {
- if (!self._reading) return;
- if (fn.done) return;
- fn.done = true;
- self._reading = false;
- delete self._callback;
- delete self._done;
- self.removeListener('keypress', self.__listener);
- delete self.__listener;
- self.removeListener('blur', self.__done);
- delete self.__done;
- self.screen.program.hideCursor();
- self.screen.grabKeys = false;
- if (!focused) {
- self.screen.restoreFocus();
- }
- if (self.options.inputOnFocus) {
- self.screen.rewindFocus();
- }
- // Ugly
- if (err === 'stop') return;
- if (err) {
- self.emit('error', err);
- } else if (value != null) {
- self.emit('submit', value);
- } else {
- self.emit('cancel', value);
- }
- self.emit('action', value);
- if (!callback) return;
- return err
- ? callback(err)
- : callback(null, value);
- };
- // Put this in a nextTick so the current
- // key event doesn't trigger any keys input.
- nextTick(function() {
- self.__listener = self._listener.bind(self);
- self.on('keypress', self.__listener);
- });
- this.__done = this._done.bind(this, null, null);
- this.on('blur', this.__done);
- };
- Textarea.prototype._listener = function(ch, key) {
- var done = this._done
- , value = this.value;
- if (key.name === 'return') return;
- if (key.name === 'enter') {
- ch = '\n';
- }
- // TODO: Handle directional keys.
- if (key.name === 'left' || key.name === 'right'
- || key.name === 'up' || key.name === 'down') {
- ;
- }
- if (this.options.keys && key.ctrl && key.name === 'e') {
- return this.readEditor();
- }
- // TODO: Optimize typing by writing directly
- // to the screen and screen buffer here.
- if (key.name === 'escape') {
- done(null, null);
- } else if (key.name === 'backspace') {
- if (this.value.length) {
- if (this.screen.fullUnicode) {
- if (unicode.isSurrogate(this.value, this.value.length - 2)) {
- // || unicode.isCombining(this.value, this.value.length - 1)) {
- this.value = this.value.slice(0, -2);
- } else {
- this.value = this.value.slice(0, -1);
- }
- } else {
- this.value = this.value.slice(0, -1);
- }
- }
- } else if (ch) {
- if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
- this.value += ch;
- }
- }
- if (this.value !== value) {
- this.screen.render();
- }
- };
- Textarea.prototype._typeScroll = function() {
- // XXX Workaround
- var height = this.height - this.iheight;
- if (this._clines.length - this.childBase > height) {
- this.scroll(this._clines.length);
- }
- };
- Textarea.prototype.getValue = function() {
- return this.value;
- };
- Textarea.prototype.setValue = function(value) {
- if (value == null) {
- value = this.value;
- }
- if (this._value !== value) {
- this.value = value;
- this._value = value;
- this.setContent(this.value);
- this._typeScroll();
- this._updateCursor();
- }
- };
- Textarea.prototype.clearInput =
- Textarea.prototype.clearValue = function() {
- return this.setValue('');
- };
- Textarea.prototype.submit = function() {
- if (!this.__listener) return;
- return this.__listener('\x1b', { name: 'escape' });
- };
- Textarea.prototype.cancel = function() {
- if (!this.__listener) return;
- return this.__listener('\x1b', { name: 'escape' });
- };
- Textarea.prototype.render = function() {
- this.setValue();
- return this._render();
- };
- Textarea.prototype.editor =
- Textarea.prototype.setEditor =
- Textarea.prototype.readEditor = function(callback) {
- var self = this;
- if (this._reading) {
- var _cb = this._callback
- , cb = callback;
- this._done('stop');
- callback = function(err, value) {
- if (_cb) _cb(err, value);
- if (cb) cb(err, value);
- };
- }
- if (!callback) {
- callback = function() {};
- }
- return this.screen.readEditor({ value: this.value }, function(err, value) {
- if (err) {
- if (err.message === 'Unsuccessful.') {
- self.screen.render();
- return self.readInput(callback);
- }
- self.screen.render();
- self.readInput(callback);
- return callback(err);
- }
- self.setValue(value);
- self.screen.render();
- return self.readInput(callback);
- });
- };
- /**
- * Expose
- */
- module.exports = Textarea;
|