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

4 years ago
  1. 'use strict';
  2. const colors = require('ansi-colors');
  3. const ArrayPrompt = require('../types/array');
  4. const utils = require('../utils');
  5. class LikertScale extends ArrayPrompt {
  6. constructor(options = {}) {
  7. super(options);
  8. this.widths = [].concat(options.messageWidth || 50);
  9. this.align = [].concat(options.align || 'left');
  10. this.linebreak = options.linebreak || false;
  11. this.edgeLength = options.edgeLength || 3;
  12. this.newline = options.newline || '\n ';
  13. let start = options.startNumber || 1;
  14. if (typeof this.scale === 'number') {
  15. this.scaleKey = false;
  16. this.scale = Array(this.scale).fill(0).map((v, i) => ({ name: i + start }));
  17. }
  18. }
  19. async reset() {
  20. this.tableized = false;
  21. await super.reset();
  22. return this.render();
  23. }
  24. tableize() {
  25. if (this.tableized === true) return;
  26. this.tableized = true;
  27. let longest = 0;
  28. for (let ch of this.choices) {
  29. longest = Math.max(longest, ch.message.length);
  30. ch.scaleIndex = ch.initial || 2;
  31. ch.scale = [];
  32. for (let i = 0; i < this.scale.length; i++) {
  33. ch.scale.push({ index: i });
  34. }
  35. }
  36. this.widths[0] = Math.min(this.widths[0], longest + 3);
  37. }
  38. async dispatch(s, key) {
  39. if (this.multiple) {
  40. return this[key.name] ? await this[key.name](s, key) : await super.dispatch(s, key);
  41. }
  42. this.alert();
  43. }
  44. heading(msg, item, i) {
  45. return this.styles.strong(msg);
  46. }
  47. separator() {
  48. return this.styles.muted(this.symbols.ellipsis);
  49. }
  50. right() {
  51. let choice = this.focused;
  52. if (choice.scaleIndex >= this.scale.length - 1) return this.alert();
  53. choice.scaleIndex++;
  54. return this.render();
  55. }
  56. left() {
  57. let choice = this.focused;
  58. if (choice.scaleIndex <= 0) return this.alert();
  59. choice.scaleIndex--;
  60. return this.render();
  61. }
  62. indent() {
  63. return '';
  64. }
  65. format() {
  66. if (this.state.submitted) {
  67. let values = this.choices.map(ch => this.styles.info(ch.index));
  68. return values.join(', ');
  69. }
  70. return '';
  71. }
  72. pointer() {
  73. return '';
  74. }
  75. /**
  76. * Render the scale "Key". Something like:
  77. * @return {String}
  78. */
  79. renderScaleKey() {
  80. if (this.scaleKey === false) return '';
  81. if (this.state.submitted) return '';
  82. let scale = this.scale.map(item => ` ${item.name} - ${item.message}`);
  83. let key = ['', ...scale].map(item => this.styles.muted(item));
  84. return key.join('\n');
  85. }
  86. /**
  87. * Render the heading row for the scale.
  88. * @return {String}
  89. */
  90. renderScaleHeading(max) {
  91. let keys = this.scale.map(ele => ele.name);
  92. if (typeof this.options.renderScaleHeading === 'function') {
  93. keys = this.options.renderScaleHeading.call(this, max);
  94. }
  95. let diff = this.scaleLength - keys.join('').length;
  96. let spacing = Math.round(diff / (keys.length - 1));
  97. let names = keys.map(key => this.styles.strong(key));
  98. let headings = names.join(' '.repeat(spacing));
  99. let padding = ' '.repeat(this.widths[0]);
  100. return this.margin[3] + padding + this.margin[1] + headings;
  101. }
  102. /**
  103. * Render a scale indicator => or by default
  104. */
  105. scaleIndicator(choice, item, i) {
  106. if (typeof this.options.scaleIndicator === 'function') {
  107. return this.options.scaleIndicator.call(this, choice, item, i);
  108. }
  109. let enabled = choice.scaleIndex === item.index;
  110. if (item.disabled) return this.styles.hint(this.symbols.radio.disabled);
  111. if (enabled) return this.styles.success(this.symbols.radio.on);
  112. return this.symbols.radio.off;
  113. }
  114. /**
  115. * Render the actual scale =>
  116. */
  117. renderScale(choice, i) {
  118. let scale = choice.scale.map(item => this.scaleIndicator(choice, item, i));
  119. let padding = this.term === 'Hyper' ? '' : ' ';
  120. return scale.join(padding + this.symbols.line.repeat(this.edgeLength));
  121. }
  122. /**
  123. * Render a choice, including scale =>
  124. * "The website is easy to navigate. ◯───◯───◉───◯───◯"
  125. */
  126. async renderChoice(choice, i) {
  127. await this.onChoice(choice, i);
  128. let focused = this.index === i;
  129. let pointer = await this.pointer(choice, i);
  130. let hint = await choice.hint;
  131. if (hint && !utils.hasColor(hint)) {
  132. hint = this.styles.muted(hint);
  133. }
  134. let pad = str => this.margin[3] + str.replace(/\s+$/, '').padEnd(this.widths[0], ' ');
  135. let newline = this.newline;
  136. let ind = this.indent(choice);
  137. let message = await this.resolve(choice.message, this.state, choice, i);
  138. let scale = await this.renderScale(choice, i);
  139. let margin = this.margin[1] + this.margin[3];
  140. this.scaleLength = colors.unstyle(scale).length;
  141. this.widths[0] = Math.min(this.widths[0], this.width - this.scaleLength - margin.length);
  142. let msg = utils.wordWrap(message, { width: this.widths[0], newline });
  143. let lines = msg.split('\n').map(line => pad(line) + this.margin[1]);
  144. if (focused) {
  145. scale = this.styles.info(scale);
  146. lines = lines.map(line => this.styles.info(line));
  147. }
  148. lines[0] += scale;
  149. if (this.linebreak) lines.push('');
  150. return [ind + pointer, lines.join('\n')].filter(Boolean);
  151. }
  152. async renderChoices() {
  153. if (this.state.submitted) return '';
  154. this.tableize();
  155. let choices = this.visible.map(async(ch, i) => await this.renderChoice(ch, i));
  156. let visible = await Promise.all(choices);
  157. let heading = await this.renderScaleHeading();
  158. return this.margin[0] + [heading, ...visible.map(v => v.join(' '))].join('\n');
  159. }
  160. async render() {
  161. let { submitted, size } = this.state;
  162. let prefix = await this.prefix();
  163. let separator = await this.separator();
  164. let message = await this.message();
  165. let prompt = '';
  166. if (this.options.promptLine !== false) {
  167. prompt = [prefix, message, separator, ''].join(' ');
  168. this.state.prompt = prompt;
  169. }
  170. let header = await this.header();
  171. let output = await this.format();
  172. let key = await this.renderScaleKey();
  173. let help = await this.error() || await this.hint();
  174. let body = await this.renderChoices();
  175. let footer = await this.footer();
  176. let err = this.emptyError;
  177. if (output) prompt += output;
  178. if (help && !prompt.includes(help)) prompt += ' ' + help;
  179. if (submitted && !output && !body.trim() && this.multiple && err != null) {
  180. prompt += this.styles.danger(err);
  181. }
  182. this.clear(size);
  183. this.write([header, prompt, key, body, footer].filter(Boolean).join('\n'));
  184. if (!this.state.submitted) {
  185. this.write(this.margin[2]);
  186. }
  187. this.restore();
  188. }
  189. submit() {
  190. this.value = {};
  191. for (let choice of this.choices) {
  192. this.value[choice.name] = choice.scaleIndex;
  193. }
  194. return this.base.submit.call(this);
  195. }
  196. }
  197. module.exports = LikertScale;