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