3 const colors = require('ansi-colors');
4 const ArrayPrompt = require('../types/array');
5 const utils = require('../utils');
7 class LikertScale extends ArrayPrompt {
8 constructor(options = {}) {
10 this.widths = [].concat(options.messageWidth || 50);
11 this.align = [].concat(options.align || 'left');
12 this.linebreak = options.linebreak || false;
13 this.edgeLength = options.edgeLength || 3;
14 this.newline = options.newline || '\n ';
15 let start = options.startNumber || 1;
16 if (typeof this.scale === 'number') {
17 this.scaleKey = false;
18 this.scale = Array(this.scale).fill(0).map((v, i) => ({ name: i + start }));
23 this.tableized = false;
29 if (this.tableized === true) return;
30 this.tableized = true;
33 for (let ch of this.choices) {
34 longest = Math.max(longest, ch.message.length);
35 ch.scaleIndex = ch.initial || 2;
38 for (let i = 0; i < this.scale.length; i++) {
39 ch.scale.push({ index: i });
42 this.widths[0] = Math.min(this.widths[0], longest + 3);
45 async dispatch(s, key) {
47 return this[key.name] ? await this[key.name](s, key) : await super.dispatch(s, key);
52 heading(msg, item, i) {
53 return this.styles.strong(msg);
57 return this.styles.muted(this.symbols.ellipsis);
61 let choice = this.focused;
62 if (choice.scaleIndex >= this.scale.length - 1) return this.alert();
68 let choice = this.focused;
69 if (choice.scaleIndex <= 0) return this.alert();
79 if (this.state.submitted) {
80 let values = this.choices.map(ch => this.styles.info(ch.index));
81 return values.join(', ');
91 * Render the scale "Key". Something like:
96 if (this.scaleKey === false) return '';
97 if (this.state.submitted) return '';
98 let scale = this.scale.map(item => ` ${item.name} - ${item.message}`);
99 let key = ['', ...scale].map(item => this.styles.muted(item));
100 return key.join('\n');
104 * Render the heading row for the scale.
108 renderScaleHeading(max) {
109 let keys = this.scale.map(ele => ele.name);
110 if (typeof this.options.renderScaleHeading === 'function') {
111 keys = this.options.renderScaleHeading.call(this, max);
113 let diff = this.scaleLength - keys.join('').length;
114 let spacing = Math.round(diff / (keys.length - 1));
115 let names = keys.map(key => this.styles.strong(key));
116 let headings = names.join(' '.repeat(spacing));
117 let padding = ' '.repeat(this.widths[0]);
118 return this.margin[3] + padding + this.margin[1] + headings;
122 * Render a scale indicator => ◯ or ◉ by default
125 scaleIndicator(choice, item, i) {
126 if (typeof this.options.scaleIndicator === 'function') {
127 return this.options.scaleIndicator.call(this, choice, item, i);
129 let enabled = choice.scaleIndex === item.index;
130 if (item.disabled) return this.styles.hint(this.symbols.radio.disabled);
131 if (enabled) return this.styles.success(this.symbols.radio.on);
132 return this.symbols.radio.off;
136 * Render the actual scale => ◯────◯────◉────◯────◯
139 renderScale(choice, i) {
140 let scale = choice.scale.map(item => this.scaleIndicator(choice, item, i));
141 let padding = this.term === 'Hyper' ? '' : ' ';
142 return scale.join(padding + this.symbols.line.repeat(this.edgeLength));
146 * Render a choice, including scale =>
147 * "The website is easy to navigate. ◯───◯───◉───◯───◯"
150 async renderChoice(choice, i) {
151 await this.onChoice(choice, i);
153 let focused = this.index === i;
154 let pointer = await this.pointer(choice, i);
155 let hint = await choice.hint;
157 if (hint && !utils.hasColor(hint)) {
158 hint = this.styles.muted(hint);
161 let pad = str => this.margin[3] + str.replace(/\s+$/, '').padEnd(this.widths[0], ' ');
162 let newline = this.newline;
163 let ind = this.indent(choice);
164 let message = await this.resolve(choice.message, this.state, choice, i);
165 let scale = await this.renderScale(choice, i);
166 let margin = this.margin[1] + this.margin[3];
167 this.scaleLength = colors.unstyle(scale).length;
168 this.widths[0] = Math.min(this.widths[0], this.width - this.scaleLength - margin.length);
169 let msg = utils.wordWrap(message, { width: this.widths[0], newline });
170 let lines = msg.split('\n').map(line => pad(line) + this.margin[1]);
173 scale = this.styles.info(scale);
174 lines = lines.map(line => this.styles.info(line));
179 if (this.linebreak) lines.push('');
180 return [ind + pointer, lines.join('\n')].filter(Boolean);
183 async renderChoices() {
184 if (this.state.submitted) return '';
186 let choices = this.visible.map(async(ch, i) => await this.renderChoice(ch, i));
187 let visible = await Promise.all(choices);
188 let heading = await this.renderScaleHeading();
189 return this.margin[0] + [heading, ...visible.map(v => v.join(' '))].join('\n');
193 let { submitted, size } = this.state;
195 let prefix = await this.prefix();
196 let separator = await this.separator();
197 let message = await this.message();
200 if (this.options.promptLine !== false) {
201 prompt = [prefix, message, separator, ''].join(' ');
202 this.state.prompt = prompt;
205 let header = await this.header();
206 let output = await this.format();
207 let key = await this.renderScaleKey();
208 let help = await this.error() || await this.hint();
209 let body = await this.renderChoices();
210 let footer = await this.footer();
211 let err = this.emptyError;
213 if (output) prompt += output;
214 if (help && !prompt.includes(help)) prompt += ' ' + help;
216 if (submitted && !output && !body.trim() && this.multiple && err != null) {
217 prompt += this.styles.danger(err);
221 this.write([header, prompt, key, body, footer].filter(Boolean).join('\n'));
222 if (!this.state.submitted) {
223 this.write(this.margin[2]);
230 for (let choice of this.choices) {
231 this.value[choice.name] = choice.scaleIndex;
233 return this.base.submit.call(this);
237 module.exports = LikertScale;