203 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			203 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | const color = require('kleur'); | ||
|  | const Prompt = require('./prompt'); | ||
|  | const { cursor, erase } = require('sisteransi'); | ||
|  | const { style, clear, figures, strip } = require('../util'); | ||
|  | 
 | ||
|  | const isNumber = /[0-9]/; | ||
|  | const isDef = any => any !== undefined; | ||
|  | const round = (number, precision) => { | ||
|  |   let factor = Math.pow(10, precision); | ||
|  |   return Math.round(number * factor) / factor; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * NumberPrompt Base Element | ||
|  |  * @param {Object} opts Options | ||
|  |  * @param {String} opts.message Message | ||
|  |  * @param {String} [opts.style='default'] Render style | ||
|  |  * @param {Number} [opts.initial] Default value | ||
|  |  * @param {Number} [opts.max=+Infinity] Max value | ||
|  |  * @param {Number} [opts.min=-Infinity] Min value | ||
|  |  * @param {Boolean} [opts.float=false] Parse input as floats | ||
|  |  * @param {Number} [opts.round=2] Round floats to x decimals | ||
|  |  * @param {Number} [opts.increment=1] Number to increment by when using arrow-keys | ||
|  |  * @param {Function} [opts.validate] Validate function | ||
|  |  * @param {Stream} [opts.stdin] The Readable stream to listen to | ||
|  |  * @param {Stream} [opts.stdout] The Writable stream to write readline data to | ||
|  |  * @param {String} [opts.error] The invalid error label | ||
|  |  */ | ||
|  | class NumberPrompt extends Prompt { | ||
|  |   constructor(opts={}) { | ||
|  |     super(opts); | ||
|  |     this.transform = style.render(opts.style); | ||
|  |     this.msg = opts.message; | ||
|  |     this.initial = isDef(opts.initial) ? opts.initial : ''; | ||
|  |     this.float = !!opts.float; | ||
|  |     this.round = opts.round || 2; | ||
|  |     this.inc = opts.increment || 1; | ||
|  |     this.min = isDef(opts.min) ? opts.min : -Infinity; | ||
|  |     this.max = isDef(opts.max) ? opts.max : Infinity; | ||
|  |     this.errorMsg = opts.error || `Please Enter A Valid Value`; | ||
|  |     this.validator = opts.validate || (() => true); | ||
|  |     this.color = `cyan`; | ||
|  |     this.value = ``; | ||
|  |     this.typed = ``; | ||
|  |     this.lastHit = 0; | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   set value(v) { | ||
|  |     if (!v && v !== 0) { | ||
|  |       this.placeholder = true; | ||
|  |       this.rendered = color.gray(this.transform.render(`${this.initial}`)); | ||
|  |       this._value = ``; | ||
|  |     } else { | ||
|  |       this.placeholder = false; | ||
|  |       this.rendered = this.transform.render(`${round(v, this.round)}`); | ||
|  |       this._value = round(v, this.round); | ||
|  |     } | ||
|  |     this.fire(); | ||
|  |   } | ||
|  | 
 | ||
|  |   get value() { | ||
|  |     return this._value; | ||
|  |   } | ||
|  | 
 | ||
|  |   parse(x) { | ||
|  |     return this.float ? parseFloat(x) : parseInt(x); | ||
|  |   } | ||
|  | 
 | ||
|  |   valid(c) { | ||
|  |     return c === `-` || c === `.` && this.float || isNumber.test(c) | ||
|  |   } | ||
|  | 
 | ||
|  |   reset() { | ||
|  |     this.typed = ``; | ||
|  |     this.value = ``; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   abort() { | ||
|  |     let x = this.value; | ||
|  |     this.value = x !== `` ? x : this.initial; | ||
|  |     this.done = this.aborted = true; | ||
|  |     this.error = false; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |     this.out.write(`\n`); | ||
|  |     this.close(); | ||
|  |   } | ||
|  | 
 | ||
|  |   async validate() { | ||
|  |     let valid = await this.validator(this.value); | ||
|  |     if (typeof valid === `string`) { | ||
|  |       this.errorMsg = valid; | ||
|  |       valid = false; | ||
|  |     } | ||
|  |     this.error = !valid; | ||
|  |   } | ||
|  | 
 | ||
|  |   async submit() { | ||
|  |     await this.validate(); | ||
|  |     if (this.error) { | ||
|  |       this.color = `red`; | ||
|  |       this.fire(); | ||
|  |       this.render(); | ||
|  |       return; | ||
|  |     } | ||
|  |     let x = this.value; | ||
|  |     this.value = x !== `` ? x : this.initial; | ||
|  |     this.done = true; | ||
|  |     this.aborted = false; | ||
|  |     this.error = false; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |     this.out.write(`\n`); | ||
|  |     this.close(); | ||
|  |   } | ||
|  | 
 | ||
|  |   up() { | ||
|  |     this.typed = ``; | ||
|  |     if (this.value >= this.max) return this.bell(); | ||
|  |     this.value += this.inc; | ||
|  |     this.color = `cyan`; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   down() { | ||
|  |     this.typed = ``; | ||
|  |     if (this.value <= this.min) return this.bell(); | ||
|  |     this.value -= this.inc; | ||
|  |     this.color = `cyan`; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   delete() { | ||
|  |     let val = this.value.toString(); | ||
|  |     if (val.length === 0) return this.bell(); | ||
|  |     this.value = this.parse((val = val.slice(0, -1))) || ``; | ||
|  |     this.color = `cyan`; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   next() { | ||
|  |     this.value = this.initial; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   _(c, key) { | ||
|  |     if (!this.valid(c)) return this.bell(); | ||
|  | 
 | ||
|  |     const now = Date.now(); | ||
|  |     if (now - this.lastHit > 1000) this.typed = ``; // 1s elapsed
 | ||
|  |     this.typed += c; | ||
|  |     this.lastHit = now; | ||
|  |     this.color = `cyan`; | ||
|  | 
 | ||
|  |     if (c === `.`) return this.fire(); | ||
|  | 
 | ||
|  |     this.value = Math.min(this.parse(this.typed), this.max); | ||
|  |     if (this.value > this.max) this.value = this.max; | ||
|  |     if (this.value < this.min) this.value = this.min; | ||
|  |     this.fire(); | ||
|  |     this.render(); | ||
|  |   } | ||
|  | 
 | ||
|  |   render() { | ||
|  |     if (this.closed) return; | ||
|  |     super.render(); | ||
|  |     let clear = erase.line + (this.lines ? erase.down(this.lines) : ``) + cursor.to(0); | ||
|  |     this.lines = 0; | ||
|  | 
 | ||
|  |     let error = ``; | ||
|  |     if (this.error) { | ||
|  |       let lines = this.errorMsg.split(`\n`); | ||
|  |       error += lines.reduce((a, l, i) => a + `\n${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``); | ||
|  |       this.lines = lines.length; | ||
|  |     } | ||
|  | 
 | ||
|  |     let underline = !this.done || (!this.done && !this.placeholder); | ||
|  |     let prompt = [ | ||
|  |       style.symbol(this.done, this.aborted), | ||
|  |       color.bold(this.msg), | ||
|  |       style.delimiter(this.done), | ||
|  |       underline ? color[this.color]().underline(this.rendered) : this.rendered | ||
|  |     ].join(` `); | ||
|  | 
 | ||
|  |     let position = ``; | ||
|  |     if (this.lines) { | ||
|  |       position += cursor.up(this.lines); | ||
|  |       position += cursor.left+cursor.to(strip(prompt).length); | ||
|  |     } | ||
|  | 
 | ||
|  |     this.out.write(clear+prompt+error+position); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = NumberPrompt; |