You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
6.7 KiB
237 lines
6.7 KiB
'use strict'; |
|
|
|
const colors = require('ansi-colors'); |
|
const ArrayPrompt = require('../types/array'); |
|
const utils = require('../utils'); |
|
|
|
class LikertScale extends ArrayPrompt { |
|
constructor(options = {}) { |
|
super(options); |
|
this.widths = [].concat(options.messageWidth || 50); |
|
this.align = [].concat(options.align || 'left'); |
|
this.linebreak = options.linebreak || false; |
|
this.edgeLength = options.edgeLength || 3; |
|
this.newline = options.newline || '\n '; |
|
let start = options.startNumber || 1; |
|
if (typeof this.scale === 'number') { |
|
this.scaleKey = false; |
|
this.scale = Array(this.scale).fill(0).map((v, i) => ({ name: i + start })); |
|
} |
|
} |
|
|
|
async reset() { |
|
this.tableized = false; |
|
await super.reset(); |
|
return this.render(); |
|
} |
|
|
|
tableize() { |
|
if (this.tableized === true) return; |
|
this.tableized = true; |
|
let longest = 0; |
|
|
|
for (let ch of this.choices) { |
|
longest = Math.max(longest, ch.message.length); |
|
ch.scaleIndex = ch.initial || 2; |
|
ch.scale = []; |
|
|
|
for (let i = 0; i < this.scale.length; i++) { |
|
ch.scale.push({ index: i }); |
|
} |
|
} |
|
this.widths[0] = Math.min(this.widths[0], longest + 3); |
|
} |
|
|
|
async dispatch(s, key) { |
|
if (this.multiple) { |
|
return this[key.name] ? await this[key.name](s, key) : await super.dispatch(s, key); |
|
} |
|
this.alert(); |
|
} |
|
|
|
heading(msg, item, i) { |
|
return this.styles.strong(msg); |
|
} |
|
|
|
separator() { |
|
return this.styles.muted(this.symbols.ellipsis); |
|
} |
|
|
|
right() { |
|
let choice = this.focused; |
|
if (choice.scaleIndex >= this.scale.length - 1) return this.alert(); |
|
choice.scaleIndex++; |
|
return this.render(); |
|
} |
|
|
|
left() { |
|
let choice = this.focused; |
|
if (choice.scaleIndex <= 0) return this.alert(); |
|
choice.scaleIndex--; |
|
return this.render(); |
|
} |
|
|
|
indent() { |
|
return ''; |
|
} |
|
|
|
format() { |
|
if (this.state.submitted) { |
|
let values = this.choices.map(ch => this.styles.info(ch.index)); |
|
return values.join(', '); |
|
} |
|
return ''; |
|
} |
|
|
|
pointer() { |
|
return ''; |
|
} |
|
|
|
/** |
|
* Render the scale "Key". Something like: |
|
* @return {String} |
|
*/ |
|
|
|
renderScaleKey() { |
|
if (this.scaleKey === false) return ''; |
|
if (this.state.submitted) return ''; |
|
let scale = this.scale.map(item => ` ${item.name} - ${item.message}`); |
|
let key = ['', ...scale].map(item => this.styles.muted(item)); |
|
return key.join('\n'); |
|
} |
|
|
|
/** |
|
* Render the heading row for the scale. |
|
* @return {String} |
|
*/ |
|
|
|
renderScaleHeading(max) { |
|
let keys = this.scale.map(ele => ele.name); |
|
if (typeof this.options.renderScaleHeading === 'function') { |
|
keys = this.options.renderScaleHeading.call(this, max); |
|
} |
|
let diff = this.scaleLength - keys.join('').length; |
|
let spacing = Math.round(diff / (keys.length - 1)); |
|
let names = keys.map(key => this.styles.strong(key)); |
|
let headings = names.join(' '.repeat(spacing)); |
|
let padding = ' '.repeat(this.widths[0]); |
|
return this.margin[3] + padding + this.margin[1] + headings; |
|
} |
|
|
|
/** |
|
* Render a scale indicator => ◯ or ◉ by default |
|
*/ |
|
|
|
scaleIndicator(choice, item, i) { |
|
if (typeof this.options.scaleIndicator === 'function') { |
|
return this.options.scaleIndicator.call(this, choice, item, i); |
|
} |
|
let enabled = choice.scaleIndex === item.index; |
|
if (item.disabled) return this.styles.hint(this.symbols.radio.disabled); |
|
if (enabled) return this.styles.success(this.symbols.radio.on); |
|
return this.symbols.radio.off; |
|
} |
|
|
|
/** |
|
* Render the actual scale => ◯────◯────◉────◯────◯ |
|
*/ |
|
|
|
renderScale(choice, i) { |
|
let scale = choice.scale.map(item => this.scaleIndicator(choice, item, i)); |
|
let padding = this.term === 'Hyper' ? '' : ' '; |
|
return scale.join(padding + this.symbols.line.repeat(this.edgeLength)); |
|
} |
|
|
|
/** |
|
* Render a choice, including scale => |
|
* "The website is easy to navigate. ◯───◯───◉───◯───◯" |
|
*/ |
|
|
|
async renderChoice(choice, i) { |
|
await this.onChoice(choice, i); |
|
|
|
let focused = this.index === i; |
|
let pointer = await this.pointer(choice, i); |
|
let hint = await choice.hint; |
|
|
|
if (hint && !utils.hasColor(hint)) { |
|
hint = this.styles.muted(hint); |
|
} |
|
|
|
let pad = str => this.margin[3] + str.replace(/\s+$/, '').padEnd(this.widths[0], ' '); |
|
let newline = this.newline; |
|
let ind = this.indent(choice); |
|
let message = await this.resolve(choice.message, this.state, choice, i); |
|
let scale = await this.renderScale(choice, i); |
|
let margin = this.margin[1] + this.margin[3]; |
|
this.scaleLength = colors.unstyle(scale).length; |
|
this.widths[0] = Math.min(this.widths[0], this.width - this.scaleLength - margin.length); |
|
let msg = utils.wordWrap(message, { width: this.widths[0], newline }); |
|
let lines = msg.split('\n').map(line => pad(line) + this.margin[1]); |
|
|
|
if (focused) { |
|
scale = this.styles.info(scale); |
|
lines = lines.map(line => this.styles.info(line)); |
|
} |
|
|
|
lines[0] += scale; |
|
|
|
if (this.linebreak) lines.push(''); |
|
return [ind + pointer, lines.join('\n')].filter(Boolean); |
|
} |
|
|
|
async renderChoices() { |
|
if (this.state.submitted) return ''; |
|
this.tableize(); |
|
let choices = this.visible.map(async(ch, i) => await this.renderChoice(ch, i)); |
|
let visible = await Promise.all(choices); |
|
let heading = await this.renderScaleHeading(); |
|
return this.margin[0] + [heading, ...visible.map(v => v.join(' '))].join('\n'); |
|
} |
|
|
|
async render() { |
|
let { submitted, size } = this.state; |
|
|
|
let prefix = await this.prefix(); |
|
let separator = await this.separator(); |
|
let message = await this.message(); |
|
|
|
let prompt = ''; |
|
if (this.options.promptLine !== false) { |
|
prompt = [prefix, message, separator, ''].join(' '); |
|
this.state.prompt = prompt; |
|
} |
|
|
|
let header = await this.header(); |
|
let output = await this.format(); |
|
let key = await this.renderScaleKey(); |
|
let help = await this.error() || await this.hint(); |
|
let body = await this.renderChoices(); |
|
let footer = await this.footer(); |
|
let err = this.emptyError; |
|
|
|
if (output) prompt += output; |
|
if (help && !prompt.includes(help)) prompt += ' ' + help; |
|
|
|
if (submitted && !output && !body.trim() && this.multiple && err != null) { |
|
prompt += this.styles.danger(err); |
|
} |
|
|
|
this.clear(size); |
|
this.write([header, prompt, key, body, footer].filter(Boolean).join('\n')); |
|
if (!this.state.submitted) { |
|
this.write(this.margin[2]); |
|
} |
|
this.restore(); |
|
} |
|
|
|
submit() { |
|
this.value = {}; |
|
for (let choice of this.choices) { |
|
this.value[choice.name] = choice.scaleIndex; |
|
} |
|
return this.base.submit.call(this); |
|
} |
|
} |
|
|
|
module.exports = LikertScale;
|
|
|