form.js 4.97 KB
'use strict';

const colors = require('ansi-colors');
const SelectPrompt = require('./select');
const placeholder = require('../placeholder');

class FormPrompt extends SelectPrompt {
  constructor(options) {
    super({ ...options, multiple: true });
    this.type = 'form';
    this.initial = this.options.initial;
    this.align = [this.options.align, 'right'].find(v => v != null);
    this.emptyError = '';
    this.values = {};
  }

  async reset(first) {
    await super.reset();
    if (first === true) this._index = this.index;
    this.index = this._index;
    this.values = {};
    this.choices.forEach(choice => choice.reset && choice.reset());
    return this.render();
  }

  dispatch(char) {
    return !!char && this.append(char);
  }

  append(char) {
    let choice = this.focused;
    if (!choice) return this.alert();
    let { cursor, input } = choice;
    choice.value = choice.input = input.slice(0, cursor) + char + input.slice(cursor);
    choice.cursor++;
    return this.render();
  }

  delete() {
    let choice = this.focused;
    if (!choice || choice.cursor <= 0) return this.alert();
    let { cursor, input } = choice;
    choice.value = choice.input = input.slice(0, cursor - 1) + input.slice(cursor);
    choice.cursor--;
    return this.render();
  }

  deleteForward() {
    let choice = this.focused;
    if (!choice) return this.alert();
    let { cursor, input } = choice;
    if (input[cursor] === void 0) return this.alert();
    let str = `${input}`.slice(0, cursor) + `${input}`.slice(cursor + 1);
    choice.value = choice.input = str;
    return this.render();
  }

  right() {
    let choice = this.focused;
    if (!choice) return this.alert();
    if (choice.cursor >= choice.input.length) return this.alert();
    choice.cursor++;
    return this.render();
  }

  left() {
    let choice = this.focused;
    if (!choice) return this.alert();
    if (choice.cursor <= 0) return this.alert();
    choice.cursor--;
    return this.render();
  }

  space(ch, key) {
    return this.dispatch(ch, key);
  }

  number(ch, key) {
    return this.dispatch(ch, key);
  }

  next() {
    let ch = this.focused;
    if (!ch) return this.alert();
    let { initial, input } = ch;
    if (initial && initial.startsWith(input) && input !== initial) {
      ch.value = ch.input = initial;
      ch.cursor = ch.value.length;
      return this.render();
    }
    return super.next();
  }

  prev() {
    let ch = this.focused;
    if (!ch) return this.alert();
    if (ch.cursor === 0) return super.prev();
    ch.value = ch.input = '';
    ch.cursor = 0;
    return this.render();
  }

  separator() {
    return '';
  }

  format(value) {
    return !this.state.submitted ? super.format(value) : '';
  }

  pointer() {
    return '';
  }

  indicator(choice) {
    return choice.input ? '⦿' : '⊙';
  }

  async choiceSeparator(choice, i) {
    let sep = await this.resolve(choice.separator, this.state, choice, i) || ':';
    return sep ? ' ' + this.styles.disabled(sep) : '';
  }

  async renderChoice(choice, i) {
    await this.onChoice(choice, i);

    let { state, styles } = this;
    let { cursor, initial = '', name, hint, input = '' } = choice;
    let { muted, submitted, primary, danger } = styles;

    let help = hint;
    let focused = this.index === i;
    let validate = choice.validate || (() => true);
    let sep = await this.choiceSeparator(choice, i);
    let msg = choice.message;

    if (this.align === 'right') msg = msg.padStart(this.longest + 1, ' ');
    if (this.align === 'left') msg = msg.padEnd(this.longest + 1, ' ');

    // re-populate the form values (answers) object
    let value = this.values[name] = (input || initial);
    let color = input ? 'success' : 'dark';

    if ((await validate.call(choice, value, this.state)) !== true) {
      color = 'danger';
    }

    let style = styles[color];
    let indicator = style(await this.indicator(choice, i)) + (choice.pad || '');

    let indent = this.indent(choice);
    let line = () => [indent, indicator, msg + sep, input, help].filter(Boolean).join(' ');

    if (state.submitted) {
      msg = colors.unstyle(msg);
      input = submitted(input);
      help = '';
      return line();
    }

    if (choice.format) {
      input = await choice.format.call(this, input, choice, i);
    } else {
      let color = this.styles.muted;
      let options = { input, initial, pos: cursor, showCursor: focused, color };
      input = placeholder(this, options);
    }

    if (!this.isValue(input)) {
      input = this.styles.muted(this.symbols.ellipsis);
    }

    if (choice.result) {
      this.values[name] = await choice.result.call(this, value, choice, i);
    }

    if (focused) {
      msg = primary(msg);
    }

    if (choice.error) {
      input += (input ? ' ' : '') + danger(choice.error.trim());
    } else if (choice.hint) {
      input += (input ? ' ' : '') + muted(choice.hint.trim());
    }

    return line();
  }

  async submit() {
    this.value = this.values;
    return super.base.submit.call(this);
  }
}

module.exports = FormPrompt;