实现一个 inquirer 里面的 checkbox - CNode技术社区

实现一个 inquirer 里面的 checkbox
发布于 8 年前 作者 yviscool 5209 次浏览 来自 分享

先看下目标 深度录屏_选择区域_20180701181546.gif

主要用 node 自带的 realine 以及第三方库 figures 来实现。

var param = {
 message: 'select toppings',
 choices: [
 {name: 'Pepperoni'},
 {name: 'Ham'},
 {name: 'Ground Meat'},
 {name: 'Bacon'},
 {name: '张三'},
 {name: '李四'},
 {name: '狗五'},
 {name: 'hahah', },
 ],
}

先来实现最简单的展示。

var figures = require('figures');
var readline = require('readline');
class Checkbox {
 constructor(quesiton) {
 this.rl = readline.createInterface({
 terminal: true,
 input: process.stdin,
 output: process.stdout,
 })
 this.opt = Object.assign({}, quesiton);
 this.opt.choices = this.opt.choices.map((choice) => ({name: choice.name}))
 }
 run() {
 this.render();
 }
 render() {
 var message = this.opt.message;
 var choiceStr = this.renderChoices()
 message += 
 '\n' + 
 choiceStr +
 '\n' + 
 '(Move up and down to reveal more choices)';
 this.realRender(message)
 }
 renderChoices() {
 var choices = this.opt.choices;
 var output = '';
 choices.forEach((choice, i) => {
 output +=
 ( figures.radioOff ) +
 ' ' + 
 choice.name;
 output += '\n';
 });
 return output.replace(/\n$/, ''); // 替换最后一个换行
 }
 realRender(message) {
 this.rl.output.write(message);
 }
}
new Checkbox(param).run()

很简单把每个 choices 转化成一打对象。遍历输出即可。 圈圈圆圆圈圈 figures.radioOff 来表示。

箭头实现

箭头表示当前行,可以用 figures.pointer 符号来展示。

constructor(quesiton) {
 // add
 this.pointer = 0;
}

在构造函数加上一个 pointer 属性, 表示当前箭头所在的行。 因为我们现在移动不了,所以当前箭头只能在第一行。然后 renderChoice 遍历的时候加上即可。

renderChoices() {
 var choices = this.opt.choices;
 var output = '';
 
 // add
 var pointer = this.pointer;
 // add end
 
 choices.forEach((choice, i) => {
 // add
 var isSelected = i === pointer;
 output += isSelected ? figures.pointer : ' ';
 // add end
 output += 
 ( figures.radioOff ) +
 ' ' + 
 choice.name;
 output += '\n';
 });
 return output.replace(/\n$/, ''); 
}

选中效果

单选 spapce,全选 a , 反选 i

选中就是让 figures.radioOff -> figures.radioOn , 然后重新 render 一次。

怎么知道自己被选中了?

给 choice 加个 checked 属性。renderChoice 加上即可。

在这之前有个问题,我怎么知道你按下的是哪颗键?

process.stdin 有一个 keypress 事件,回调能获取到对应属性。

class Checkbox{
 constructor(){
 ...
 this.opt.choices = this.opt.choices.map((choice) => ({
 name: choice.name,
 checked: choice.checked, // add
 }))
 ...
 }
 run() {
 this.listen() // add
 this.render();
 }
 renderChoices() {
 var choices = this.opt.choices;
 var output = '';
 var pointer = this.pointer;
 choices.forEach((choice, i) => {
 var isSelected = i === pointer;
 output += isSelected ? figures.pointer : ' ';
 output +=
 (
 choice.checked ? // add
 figures.radioOn : // add
 figures.radioOff
 ) +
 ' ' +
 choice.name;
 output += '\n';
 });
 return output.replace(/\n$/, '');
 }
 // add
 listen(){
 var self = this;
 process.stdin.on('keypress', (name, key) => {
 if (key.name === 'space') {
 var item = self.opt.choices[self.pointer];
 item && (self.opt.choices[self.pointer].checked = !item.checked);
 self.render();
 }
 if (key.name === 'a') {
 var shouldBeChecked = !!self.opt.choices.find(choice => {
 return !choice.checked;
 });
 self.opt.choices.forEach(choice => {
 choice.checked = shouldBeChecked;
 });
 self.render();
 }
 if (key.name === 'i') {
 self.opt.choices.forEach(choice => {
 choice.checked = !choice.checked;
 });
 self.render();
 }
 })
 }
}

满心欢喜的运行后,你会发现,竟然会多出好几行一模一样的。 因为我们光标是在最下面,所以渲染的时候会从那一行开始。造成了这个情况。 解决办法: 清除,然后重新输出。 那么问题来了,怎么清除? inquire 使用 ansi 指令来清除。node 有对应的库 ansi-escapes readline 自身也提供了几个方法供我们使用,moveCursor clearLine clearScreenDown

目前为止的代码

class Checkbox {
 constructor(quesiton) {
 this.rl = readline.createInterface({
 terminal: true,
 input: process.stdin,
 output: process.stdout,
 })
 this.opt = Object.assign({}, quesiton);
 this.opt.choices = this.opt.choices.map((choice) => ({
 name: choice.name,
 checked: choice.checked,
 }))
 
 this.firstRender = true; // add
 this.pointer = 0;
 }
 run() {
 this.listen();
 this.render();
 
 this.firstRender = false;// add
 }
 render() {
 var message = this.opt.message;
 var choiceStr = this.renderChoices()
 message +=
 '\n' +
 choiceStr +
 '\n' +
 '(Move up and down to reveal more choices)';
 this.realRender(message)
 }
 renderChoices() {
 var choices = this.opt.choices;
 var output = '';
 var pointer = this.pointer;
 choices.forEach((choice, i) => {
 var isSelected = i === pointer;
 output += isSelected ? figures.pointer : ' ';
 output +=
 (
 choice.checked 
 ? figures.radioOn
 : figures.radioOff
 ) +
 ' ' +
 choice.name;
 output += '\n';
 });
 return output.replace(/\n$/, '');
 }
 realRender(message) {
 var line = message.split('\n');
 // 获取最后一条提示,以便获取他的长度。好让光标移动到相对位置
 var lastLine = line[line.length - 1]; 
 readline.moveCursor(process.stdout, - lastLine.length - 1, !this.firstRender ? -line.length + 1 : 0)
 readline.clearLine(process.stdout, 0); // 清除整行 
 readline.clearScreenDown(process.stdout); 
 this.rl.output.write(message);
 }
 listen() {
 var self = this;
 process.stdin.on('keypress', (name, key) => {
 if (key.name === 'space') {
 var item = self.opt.choices[self.pointer];
 item && (self.opt.choices[self.pointer].checked = !item.checked);
 self.render();
 }
 if (key.name === 'a') {
 var shouldBeChecked = !!self.opt.choices.find(choice => {
 return !choice.checked;
 });
 self.opt.choices.forEach(choice => {
 choice.checked = shouldBeChecked;
 });
 self.render();
 }
 if (key.name === 'i') {
 self.opt.choices.forEach(choice => {
 choice.checked = !choice.checked;
 });
 self.render();
 }
 })
 }
}

箭头上下移动

前面我们讲过了, pointer 代表当前行,箭头移动就是让 pointer + 1 -1 置零。。

process.stdin.on('keypress', (name, key) => {
 // add
 var len = self.opt.choices.length;
 if (key.name === 'up' || key.name === 'k' || (key.name === 'p' && key.ctrl)) {
 self.pointer = self.pointer > 0 ? self.pointer - 1 : len - 1;
 self.render();
 }
 if (key.name === 'down' || key.name === 'j' || (key.name === 'n' && key.ctrl)) {
 self.pointer = self.pointer < len - 1 ? self.pointer + 1 : 0;
 self.render();
 }
})

分页

我们的低配版 checkbox 已经完成的差不多了, 能上下移动,能选中。恩 挺不错了。

如果你仔细观看会发现,inquirer 的 checkbox 移动的时候有个特点。

向上移动,数据肯定动。开始向下移动的时候数据不动,

只有向下移动到或者上下移动到列表中间位置时,箭头就固定了。

道理我都懂,怎么做到的?

这个分页比较难做,inquirer 把列表复制成三份,截取 pagesize 。

为什么复制成三份? 向上移动,和向下移动时要显示循环部分,用一个数组截取追加是十分麻烦的。

对三份 splice ,这样就很方便,只需要考虑截取的位置即可。

class Checkbox {
 constructor(quesiton) {
		...
 this.p = 0; // add 
 this.lastIndex = 0; //add 
 }
 render() {
		...
 message +=
 '\n' +
 this.paginate(choiceStr) + //add 
 '\n' +
 '(Move up and down to reveal more choices)';
 this.realRender(message)
 }
 	// add
 paginate(message, pageSize = 7) {
 var pointer = this.pointer;
 var middleOflist = Math.floor(pageSize / 2);
 var lines = message.split('\n');
 var infinite = [...lines, ...lines, ...lines];
		if (lines.length <= pageSize) {
 		return message;
	 }
 if (
 (
 this.lastIndex < pointer && !this.up ||
 this.lastIndex > pointer && !this.up
 ) &&
 this.p < middleOflist
 ) {
 this.p += 1;
 }
 this.lastIndex = pointer;
 var index = lines.length + pointer - this.p;
 return infinite.splice(index, pageSize).join('\n')
 }
 listen() {
		...
 process.stdin.on('keypress', (name, key) => {	
 	...
 if (key.name === 'up' || key.name === 'k' || (key.name === 'p' && key.ctrl)) {
 self.pointer = self.pointer > 0 ? self.pointer - 1 : len - 1;
 self.up = true; // add
 self.render();
 self.up = false; // add 
 }
			...
 })
 }
}

这个位置截取比较复杂,是不在 inquirer checkbox 里面的,也就是说我们扩展 checkbox 的时候不用考虑这个实现。

扩展

中配版的 checkbox 完成了。但还有很多可以完善的地方。

firstRender 的时候添加提示, 给 message 上色. 默认 default 选中。光标隐藏。

分割线类型的 choice, disabled 的 choice ,这样当前箭头就不能在这一行,而且移动的时候就必须跳过(用偏移量和真实choices实现)。

还有应答部分,就是回车的时候把选中取出来(监听 pipe 事件)。进行 filter,validate.

inquirer 最底下有一个插件 checkbox-plus 比较有意思。可以看看效果然后想想怎么实现。

回到顶部

AltStyle によって変換されたページ (->オリジナル) /