expand.js 5.87 KB
/**
 * `rawlist` type prompt
 */

var _ = require('lodash');
var util = require('util');
var chalk = require('chalk');
var Base = require('./base');
var Separator = require('../objects/separator');
var observe = require('../utils/events');
var Paginator = require('../utils/paginator');

/**
 * Module exports
 */

module.exports = Prompt;

/**
 * Constructor
 */

function Prompt() {
  Base.apply(this, arguments);

  if (!this.opt.choices) {
    this.throwParamError('choices');
  }

  this.validateChoices(this.opt.choices);

  // Add the default `help` (/expand) option
  this.opt.choices.push({
    key: 'h',
    name: 'Help, list all options',
    value: 'help'
  });

  this.opt.validate = function (choice) {
    if (choice == null) {
      return 'Please enter a valid command';
    }

    return choice !== 'help';
  };

  // Setup the default string (capitalize the default key)
  this.opt.default = this.generateChoicesString(this.opt.choices, this.opt.default);

  this.paginator = new Paginator();
}
util.inherits(Prompt, Base);

/**
 * Start the Inquiry session
 * @param  {Function} cb      Callback when prompt is done
 * @return {this}
 */

Prompt.prototype._run = function (cb) {
  this.done = cb;

  // Save user answer and update prompt to show selected option.
  var events = observe(this.rl);
  var validation = this.handleSubmitEvents(
    events.line.map(this.getCurrentValue.bind(this))
  );
  validation.success.forEach(this.onSubmit.bind(this));
  validation.error.forEach(this.onError.bind(this));
  this.keypressObs = events.keypress.takeUntil(validation.success)
    .forEach(this.onKeypress.bind(this));

  // Init the prompt
  this.render();

  return this;
};

/**
 * Render the prompt to screen
 * @return {Prompt} self
 */

Prompt.prototype.render = function (error, hint) {
  var message = this.getQuestion();
  var bottomContent = '';

  if (this.status === 'answered') {
    message += chalk.cyan(this.answer);
  } else if (this.status === 'expanded') {
    var choicesStr = renderChoices(this.opt.choices, this.selectedKey);
    message += this.paginator.paginate(choicesStr, this.selectedKey, this.opt.pageSize);
    message += '\n  Answer: ';
  }

  message += this.rl.line;

  if (error) {
    bottomContent = chalk.red('>> ') + error;
  }

  if (hint) {
    bottomContent = chalk.cyan('>> ') + hint;
  }

  this.screen.render(message, bottomContent);
};

Prompt.prototype.getCurrentValue = function (input) {
  if (!input) {
    input = this.rawDefault;
  }
  var selected = this.opt.choices.where({key: input.toLowerCase().trim()})[0];
  if (!selected) {
    return null;
  }

  return selected.value;
};

/**
 * Generate the prompt choices string
 * @return {String}  Choices string
 */

Prompt.prototype.getChoices = function () {
  var output = '';

  this.opt.choices.forEach(function (choice) {
    output += '\n  ';

    if (choice.type === 'separator') {
      output += ' ' + choice;
      return;
    }

    var choiceStr = choice.key + ') ' + choice.name;
    if (this.selectedKey === choice.key) {
      choiceStr = chalk.cyan(choiceStr);
    }
    output += choiceStr;
  }.bind(this));

  return output;
};

Prompt.prototype.onError = function (state) {
  if (state.value === 'help') {
    this.selectedKey = '';
    this.status = 'expanded';
    this.render();
    return;
  }
  this.render(state.isValid);
};

/**
 * When user press `enter` key
 */

Prompt.prototype.onSubmit = function (state) {
  this.status = 'answered';
  var choice = this.opt.choices.where({value: state.value})[0];
  this.answer = choice.short || choice.name;

  // Re-render prompt
  this.render();
  this.screen.done();
  this.done(state.value);
};

/**
 * When user press a key
 */

Prompt.prototype.onKeypress = function () {
  this.selectedKey = this.rl.line.toLowerCase();
  var selected = this.opt.choices.where({key: this.selectedKey})[0];
  if (this.status === 'expanded') {
    this.render();
  } else {
    this.render(null, selected ? selected.name : null);
  }
};

/**
 * Validate the choices
 * @param {Array} choices
 */

Prompt.prototype.validateChoices = function (choices) {
  var formatError;
  var errors = [];
  var keymap = {};
  choices.filter(Separator.exclude).forEach(function (choice) {
    if (!choice.key || choice.key.length !== 1) {
      formatError = true;
    }
    if (keymap[choice.key]) {
      errors.push(choice.key);
    }
    keymap[choice.key] = true;
    choice.key = String(choice.key).toLowerCase();
  });

  if (formatError) {
    throw new Error('Format error: `key` param must be a single letter and is required.');
  }
  if (keymap.h) {
    throw new Error('Reserved key error: `key` param cannot be `h` - this value is reserved.');
  }
  if (errors.length) {
    throw new Error('Duplicate key error: `key` param must be unique. Duplicates: ' +
        _.uniq(errors).join(', '));
  }
};

/**
 * Generate a string out of the choices keys
 * @param  {Array}  choices
 * @param  {Number} defaultIndex - the choice index to capitalize
 * @return {String} The rendered choices key string
 */
Prompt.prototype.generateChoicesString = function (choices, defaultIndex) {
  var defIndex = choices.realLength - 1;
  if (_.isNumber(defaultIndex) && this.opt.choices.getChoice(defaultIndex)) {
    defIndex = defaultIndex;
  }
  var defStr = this.opt.choices.pluck('key');
  this.rawDefault = defStr[defIndex];
  defStr[defIndex] = String(defStr[defIndex]).toUpperCase();
  return defStr.join('');
};

/**
 * Function for rendering checkbox choices
 * @param  {String} pointer Selected key
 * @return {String}         Rendered content
 */

function renderChoices(choices, pointer) {
  var output = '';

  choices.forEach(function (choice) {
    output += '\n  ';

    if (choice.type === 'separator') {
      output += ' ' + choice;
      return;
    }

    var choiceStr = choice.key + ') ' + choice.name;
    if (pointer === choice.key) {
      choiceStr = chalk.cyan(choiceStr);
    }
    output += choiceStr;
  });

  return output;
}