Mon 21 Jul 22:43:21 CEST 2025
This commit is contained in:
parent
8aebfeef1f
commit
b01fc5386b
540
js/term/widgets/textarea.js
Normal file
540
js/term/widgets/textarea.js
Normal file
|
@ -0,0 +1,540 @@
|
|||
/**
|
||||
** ==============================
|
||||
** O O O OOOO
|
||||
** O O O O O O
|
||||
** O O O O O O
|
||||
** OOOO OOOO O OOO OOOO
|
||||
** O O O O O O O
|
||||
** O O O O O O O
|
||||
** OOOO OOOO O O OOOO
|
||||
** ==============================
|
||||
** Dr. Stefan Bosse http://www.bsslab.de
|
||||
**
|
||||
** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
|
||||
** BY THE AUTHOR(S).
|
||||
** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
|
||||
** MODIFIED, OR OTHERWISE USED IN A CONTEXT
|
||||
** OUTSIDE OF THE SOFTWARE SYSTEM.
|
||||
**
|
||||
** $AUTHORS: Christopher Jeffrey, Stefan Bosse
|
||||
** $INITIAL: (C) 2013-2018, Christopher Jeffrey and contributors
|
||||
** $MODIFIED: by sbosse (2017-2021)
|
||||
** $REVESIO: 1.5.2
|
||||
**
|
||||
** $INFO:
|
||||
**
|
||||
** textarea.js - textarea element for blessed
|
||||
**
|
||||
** new: cursor control
|
||||
**
|
||||
** special options: {cursorControl}
|
||||
**
|
||||
** $ENDOFINFO
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modules
|
||||
*/
|
||||
var Comp = Require('com/compat');
|
||||
|
||||
var unicode = Require('term/unicode');
|
||||
|
||||
var nextTick = global.setImmediate || process.nextTick.bind(process);
|
||||
|
||||
var Node = Require('term/widgets/node');
|
||||
var Input = Require('term/widgets/input');
|
||||
|
||||
/**
|
||||
* Textarea
|
||||
*/
|
||||
|
||||
function Textarea(options) {
|
||||
var self = this;
|
||||
|
||||
if (!instanceOf(this,Node)) {
|
||||
return new Textarea(options);
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
options.scrollable = options.scrollable !== false;
|
||||
|
||||
Input.call(this, options);
|
||||
|
||||
this.screen._listenKeys(this);
|
||||
|
||||
this.value = options.value || '';
|
||||
// cursor position
|
||||
this.cpos = {x:-1,y:-1};
|
||||
this.cursorControl=true;
|
||||
this.multiline=options.multiline;
|
||||
|
||||
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;
|
||||
inheritPrototype(Textarea,Input);
|
||||
|
||||
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
|
||||
, offsetY = this.childBase||0
|
||||
, 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);
|
||||
if (this.cpos.x==-1 || !this.cursorControl) this.cpos.x = this.strWidth(last);
|
||||
if (this.cpos.y==-1 || !this.cursorControl) this.cpos.y = line;
|
||||
this.cpos.y = Math.min(this.cpos.y,line);
|
||||
this.cpos.x = Math.min(this.cpos.x,this.strWidth(this._clines[offsetY+this.cpos.y]));
|
||||
|
||||
cx = lpos.xi + this.ileft + this.cpos.x;
|
||||
cy = lpos.yi + this.itop + this.cpos.y;
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
self._reading = false;
|
||||
|
||||
// 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() {
|
||||
if (self.__listener) {
|
||||
// double fired?
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
// Cursor position must be synced with scrollablebox and vice versa (if scrollable)! A real mess.
|
||||
var done = this._done
|
||||
, self = this
|
||||
, value = this.value
|
||||
, clinesLength=this._clines.length
|
||||
, offsetY = this.childBase||0 // scrollable line offset if any
|
||||
, newline = false
|
||||
, backspace = false
|
||||
, lastline = (this.cpos.y+offsetY+1) == clinesLength;
|
||||
|
||||
if (key.name == 'linefeed' ||
|
||||
(!this.multiline && key.name=='enter')) return this.emit('enter',this.value);
|
||||
|
||||
if (key.name === 'return') return;
|
||||
if (key.name === 'enter') {
|
||||
ch = '\n';
|
||||
// this.cpos.x=1;
|
||||
// this.cpos.y++;
|
||||
newline=true;
|
||||
}
|
||||
|
||||
// Handle cursor positiong by keys.
|
||||
if (this.cursorControl) switch (key.name) {
|
||||
case 'left':
|
||||
if (this.cpos.x>0) this.cpos.x--;
|
||||
else {
|
||||
if (this.cpos.y>0) {
|
||||
this.cpos.y--;
|
||||
this.cpos.x=this._clines[offsetY+this.cpos.y].length;
|
||||
} else if (offsetY>0) {
|
||||
if (this.scrollable) this.scroll(-1);
|
||||
self.screen.render();
|
||||
this.cpos.x=this._clines[offsetY+this.cpos.y-1].length;
|
||||
}
|
||||
}
|
||||
this._updateCursor(true);
|
||||
break;
|
||||
case 'right':
|
||||
var next=++this.cpos.x;
|
||||
this._updateCursor(true);
|
||||
if (this.cpos.x!=next && (offsetY+this.cpos.y+1)<this._clines.length) {
|
||||
next=++this.cpos.y;
|
||||
this.cpos.x=0;
|
||||
this._updateCursor(true);
|
||||
if (this.scrollable && this.cpos.y!=next) this.scroll(1);
|
||||
}
|
||||
break;
|
||||
case 'up':
|
||||
if (this.cpos.y>0) {
|
||||
this.cpos.y--;
|
||||
//this.cpos.x=this.strWidth(this._clines[this.cpos.y]);
|
||||
}
|
||||
this._updateCursor(true);
|
||||
break;
|
||||
case 'down':
|
||||
this.cpos.y++;
|
||||
this._updateCursor(true);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (this.options.keys && key.ctrl && key.name === 'e') {
|
||||
return this.readEditor();
|
||||
}
|
||||
// Challenge: sync with line wrapping and adjust cursor and scrolling (done in element._wrapContent)
|
||||
|
||||
// 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') {
|
||||
backspace=true;
|
||||
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 {
|
||||
if (!this.cursorControl ||
|
||||
this.cpos.x==-1 ||
|
||||
(this.cpos.x==this._clines[offsetY+this.cpos.y].length &&
|
||||
this.cpos.y==this._clines.length-1-offsetY)) {
|
||||
// Delete last char of last line
|
||||
this.value = this.value.slice(0, -1);
|
||||
} else {
|
||||
// Delete char at current cursor position
|
||||
vpos=this.getLinearPos(this.value,offsetY+this.cpos.y, this.cpos.x);
|
||||
// vpos+= this.cpos.x;
|
||||
this.value = this.value.substr(0,vpos-1)+
|
||||
this.value.substr(vpos,1000000);
|
||||
}
|
||||
}
|
||||
if (this.cpos.x>0) this.cpos.x--;
|
||||
else {this.cpos.x=-1; if (offsetY==0 && this.cpos.y>0 && lastline) this.cpos.y--; };
|
||||
}
|
||||
} else if (ch) {
|
||||
if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
|
||||
if (!this.cursorControl ||
|
||||
this.cpos.x==-1 ||
|
||||
(this.cpos.x==this._clines[offsetY+this.cpos.y].length &&
|
||||
this.cpos.y==this._clines.length-1-offsetY))
|
||||
// Append new char at end of (last) line
|
||||
this.value += ch;
|
||||
else {
|
||||
// Insert new char into line at current cursor position
|
||||
vpos=this.getLinearPos(this.value,offsetY+this.cpos.y, this.cpos.x);
|
||||
// vpos+= this.cpos.x;
|
||||
this.value = this.value.substr(0,vpos)+ch+
|
||||
this.value.substr(vpos,1000000);
|
||||
}
|
||||
if (newline) {
|
||||
this.cpos.x=0; // first left position is zero!
|
||||
this.cpos.y++;
|
||||
} else
|
||||
this.cpos.x++;
|
||||
}
|
||||
}
|
||||
|
||||
var rmline=this.cpos.x==-1;
|
||||
// if (this.childOffset!=undefined) this.childOffset=this.cpos.y;
|
||||
|
||||
// TODO: clean up this mess; use rtof and ftor attributes of _clines
|
||||
// to determine where we are (react correctly on line wrap extension and reduction)
|
||||
|
||||
if (this.value !== value) {
|
||||
var cn0=clinesLength,
|
||||
cn1=this._clines.length,
|
||||
y0=this.cpos.y,
|
||||
linelength=this._clines[offsetY+this.cpos.y] && this._clines[offsetY+this.cpos.y].length,
|
||||
endofline=this.cpos.x==linelength+1;
|
||||
// Log(this.cpos,this.childBase);
|
||||
this.screen.render();
|
||||
var cn2=this._clines.length;
|
||||
// Log(this.cpos,lastline,endofline,rmline,newline,backspace,cn0,cn2,this.childBase);
|
||||
if (!newline && endofline && cn2>cn0) {
|
||||
// wrap expansion
|
||||
if (this.scrollable && lastline) this.scrollBottom();
|
||||
this.cpos.y++;
|
||||
this._updateCursor(true);
|
||||
if (this._clines[offsetY+this.cpos.y]) this.cpos.x=this._clines[offsetY+this.cpos.y].length;
|
||||
this._updateCursor(true);
|
||||
} else if (cn2<cn0 && !rmline) {
|
||||
// wrap reduction
|
||||
if (this.cpos.y>0 && !lastline && endofline) this.cpos.y--;
|
||||
this._updateCursor(true);
|
||||
offsetY=this.childBase||0;
|
||||
if (this._clines[offsetY+this.cpos.y]) this.cpos.x=this._clines[offsetY+this.cpos.y].length;
|
||||
this._updateCursor(true);
|
||||
} else if (cn2<cn0 && !rmline && this.cpos.x==0) {
|
||||
// wrap reduction
|
||||
if (this._clines[offsetY+this.cpos.y]) this.cpos.x=this._clines[offsetY+this.cpos.y].length;
|
||||
this._updateCursor(true);
|
||||
};
|
||||
if (offsetY>0 && backspace) {
|
||||
// @fix line deleted; refresh again due to miscalculation of height in scrollablebox!
|
||||
if (this.scrollable) this.scroll(0);
|
||||
this.screen.render();
|
||||
if (rmline && cn0!=cn2) {
|
||||
if (this._clines[offsetY+this.cpos.y]) this.cpos.x=this._clines[offsetY+this.cpos.y].length;
|
||||
else if (this._clines[offsetY+this.cpos.y-1]) this.cpos.x=this._clines[offsetY+this.cpos.y-1].length;
|
||||
this._updateCursor(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Return start position of nth (c)line in linear value string
|
||||
Textarea.prototype.getLinearPos = function(v,clineIndex,cposx) {
|
||||
// clineIndex is the index in the _clines array, cposx the cursor position in this line!
|
||||
var vpos=0,len=v.length,cline,clinePos=0,clineNum=0;
|
||||
cline=this._clines[clineNum];
|
||||
// To support auto line wrapping the clines have to be parsed, too!
|
||||
while (vpos < len && clineIndex) {
|
||||
if (v.charAt(vpos)=='\n') {
|
||||
clinePos=-1;
|
||||
clineIndex--;
|
||||
clineNum++;
|
||||
cline=this._clines[clineNum];
|
||||
} else {
|
||||
if (v.charAt(vpos) != cline.charAt(clinePos)) {
|
||||
//
|
||||
clinePos=0;
|
||||
clineIndex--;
|
||||
clineNum++;
|
||||
cline=this._clines[clineNum];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
vpos++; clinePos++;
|
||||
}
|
||||
if (clineIndex==0) return vpos+cposx;
|
||||
else 0
|
||||
}
|
||||
|
||||
Textarea.prototype._typeScroll = function() {
|
||||
// XXX Workaround
|
||||
var height = this.height - this.iheight;
|
||||
// Scroll down?
|
||||
// if (typeof Log != 'undefined') Log(this.childBase,this.childOffset,this.cpos.y,height);
|
||||
//if (this._clines.length - this.childBase > height) {
|
||||
if (this.cpos.y == height) {
|
||||
if (this.scrollable) 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;
|
Loading…
Reference in New Issue
Block a user