const Configurer = require("../Configurer.js");
const Validators = require("../Validators.js");
const Filters = require("../Filters.js");
/**
* Options for the Promptlet. Not every property is documented here.<br>
* Some properties have been overridden: filter & validate (use addFilter() and addValidator() instead)<br>
* Options are passed to inquirer.js so additional properties and option can be found [here]{@link https://github.com/SBoudrias/Inquirer.js/#questions}
* @typedef {Object} PromptletOptions
* @property {string} [optionName] The string displayed on the list of prompts from PromptSet.selectPromptlet(). Required unless running a Promptlet by itself
* @property {string} name Name for the Promptlet. Used as the key in PromptSet.reduce
* @property {string} message Text displayed when the Promptlet is run
* @property {string} [type = "input"] Type of [inquirer.js prompt]{@link https://github.com/SBoudrias/Inquirer.js/#prompt} to display
* @property {string|number|boolean|function} [default] Value to use if blank answer is given or a function that returns a value to use
* @property {string[]} [prerequisites] Array of Promptlet names. Promptlets with those names must be answered before this instance can run. (Note: Setting prerequisites with this property bypasses function that checks to make sure the Promptlets with these names exist)
* @property {function|function[]} [filter] Constructor-only shortcut property. All functions in the filter property will be passed to this.addFilter
* @property {function|function[]} [validate] Constructor-only shortcut property. All functions in the validate property will be passed to this.addValidator
* @property {boolean} [allowBlank = true] Constructor-only shortcut property for setter Promptlet.allowBlank
* @property {boolean} [autoTrim = true] Constructor-only shortcut property for setter Promptlet.autoTrim
* @property {string|boolean|number} [value = "<Incomplete>"] Constructor-only shortcut property for forcefully setting a value for Promptlet.value without running it first
* @property {boolean} [required = false] Whether this Promptlet MUST be answered before closing a PromptSet. Does nothing if Promptlet is run directly
* @property {boolean} [editable = false] Whether the prompt can be selected and answered again after being completed once
*/
class Promptlet {
/**
* Default object template for the inquirer prompt function. Options passed to the Promptlet will overwrite these.<br>
* The name property will be ignored on the default if set.<br>
* No longer a static property due to arrays
* @type {PromptletOptions}
*/
info = {
type: "input",
name: undefined,
message: undefined,
optionName: undefined,
prerequisites: [],
validate: [],
filter: [],
allowBlank: true,
autoTrim: true,
value: "<Incomplete>",
required: false,
editable: false
};
// @formatter:off IDE likes to complain so ignore the following line
optionName; editable; value; satisfied; prerequisites; filters; validators; postProcessors;
// @formatter:on
/**
* Instantiates a new Promptlet
* @class
* @classdesc Class that manages individual prompts and their responses. Wraps inquirer.prompt()
* @alias Promptlet
* @memberOf module:Prompt-Set.Classes
* @param {PromptletOptions} info Object with all the prompt configurations passed to inquirer. See the 'inquirer' documentation on npm or Github for specific details. Name property required
* @throws {TypeError} Will be thrown if info.name is not a string
*/
constructor(info) {
if(typeof info.name !== "string") throw new TypeError("Name Property Required (Type: string)");
/**
* Text displayed for this Promptlet on the PromptSet option list
* @type {string}
*/
this.optionName = info.optionName || "Select to Answer";
/**
* Object containing all the data needed to start an inquirer prompt. Requires name property
* @type {PromptletOptions|Object}
*/
this.info = Object.assign(this.info, info);
/**
* Whether the question can be answered again and edited after the initial prompt
* @type {boolean}
*/
this.editable = Boolean(this.info.editable);
/**
* The answer given to the Promptlet<br>
* Typeof value depends on the type of prompt being used and the output of the filters in that order
* @type {string|number|boolean|*}
*/
this.value = this.info.value;
/**
* Whether this Promptlet has been answered satisfactorily yet
* @type {boolean}
*/
this.satisfied = false;
/**
* Array with a list of names from Promptlets in a PromptSet that must be answered before this instance can be asked
* @type {string[]}
*/
this.prerequisites = this.info.prerequisites;
/**
* Array of filter functions to pass prompt answers through to be altered<br>
* Function output must be a string. Outputs will be used as the input to the next function in the array
* @type {function[]}
*/
this.filters = [];
/**
* Array of validator functions for prompt answers<br>
* Each function will be called in order until the end of the array or until an error or error message is returned instead of true<br>
* Return a string to display an error message, true to continue, or throw an error to crash
* @type {function[]}
*/
this.validators = [];
/**
* Array of post-processor functions for prompt answers<br>
* Each function will be called in order until the end of the array. Error handling must be handled by each individual function<br>
* Each function should return something to pass to the next function or throw an error to crash
* @type {function[]}
*/
this.postProcessors = [];
this
.autoTrim(this.info.autoTrim)
.allowBlank(this.info.allowBlank);
this
.addFilter(this.info.filter)
.addValidator(this.info.validate);
this.info.filter = async(ans, answers) => {
let filteredAns = ans;
for(const filter of this.filters) {
filteredAns = await filter(filteredAns, answers);
}
return filteredAns;
};
this.info.validate = async(ans, answers) => {
for(const validator of this.validators) {
const valid = await validator(ans, answers);
if(valid !== true) return valid;
}
return true;
};
}
/**
* Whether to automatically use the built-in trim filter. Defaults to true if called without arguments
* @param {boolean} [allow = true] Determines whether to include the Filters.autoTrim function as a filter
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
autoTrim(allow = true) {
if(allow) this.addFilter(Filters.autoTrim);
else this.removeFilter(Filters.autoTrim);
return this;
}
/**
* Whether to allow blank answers (responses). Defaults to true if called without arguments
* @param {boolean} [allow = true] Determines whether to include the Validators.disableBlank function as a validator
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
allowBlank(allow = true) {
if(allow) this.removeValidator(Validators.disableBlank);
else this.addValidator(Validators.disableBlank);
return this;
}
/**
* Getter for Promptlet.name property
* @return {string} Returns the name property of the Promptlet
*/
get name() {
return this.info.name;
}
/**
* Whether the current Promptlet instance must be run before the parent PromptSet can terminate. Defaults to true if called without arguments
* @param {boolean} [toggle = true] Whether answering the Promptlet is required. Defaults to true if this function is called.
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
required(toggle = true) {
this.info.required = Boolean(toggle);
return this;
}
/**
* Adds a prerequisite that must be completed before this Promptlet can run
* @param {string|Promptlet} newPrerequisite The name property of the prerequisite Promptlet
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
addPrerequisite(newPrerequisite) {
if(newPrerequisite instanceof Promptlet) newPrerequisite = newPrerequisite.name;
newPrerequisite = newPrerequisite.trim();
if(!this.prerequisites.includes(newPrerequisite)) {
this.prerequisites.push(newPrerequisite);
this.prerequisites.sort();
}
return this;
}
/**
* Removes a prerequisite that must be completed before this Promptlet can run
* @param {string|Promptlet} removePrerequisite The name property of the prerequisite Promptlet
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
removePrerequisite(removePrerequisite) {
if(removePrerequisite instanceof Promptlet) removePrerequisite = removePrerequisite.name;
removePrerequisite = removePrerequisite.trim();
if(this.prerequisites.includes(removePrerequisite)) {
this.prerequisites.splice(this.prerequisites.indexOf(removePrerequisite), 1);
}
return this;
}
/**
* Add a filter to the prompt. Will not add identical duplicates (Wrap or recreate a function if multiple copies are desired)
* @param {function|function[]} filter Filter function to add
* @return {Promptlet} Returns 'this' Promptlet for chaining
* @throws {TypeError} Thrown if a function is not provided
*/
addFilter(filter) {
if(Array.isArray(filter)) {
filter.forEach(fil => this.addFilter(fil));
return this;
}
if(typeof filter !== "function") throw new TypeError("Function required");
if(!this.filters.includes(filter)) {
this.filters.push(filter);
}
return this;
}
/**
* Remove a filter from the prompt. (Requires exact same function instance to remove)
* @param {function} filter Filter function to remove
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
removeFilter(filter) {
if(this.filters.includes(filter)) {
this.filters.splice(this.filters.indexOf(filter), 1);
}
return this;
}
/**
* Add a validator to the prompt. Will not add identical duplicates (Wrap or recreate a function if multiple copies are desired)
* @param {function|function[]} validator Validator function to add
* @return {Promptlet} Returns 'this' Promptlet for chaining
* @throws {TypeError} Thrown if a function is not provided
*/
addValidator(validator) {
if(Array.isArray(validator)) {
validator.forEach(val => this.addValidator(val));
return this;
}
if(typeof validator !== "function") throw new TypeError("Function required");
if(!this.validators.includes(validator)) {
this.validators.push(validator);
}
return this;
}
/**
* Remove a validator from the prompt. (Requires exact same function instance to remove)
* @param {function} validator Validator function to remove
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
removeValidator(validator) {
if(this.validators.includes(validator)) {
this.validators.splice(this.validators.indexOf(validator), 1);
}
return this;
}
/**
* Add a post processor to the prompt. Will not add identical duplicates
* @param {function|function[]} postProcessor Post processor function to add
* @return {Promptlet} Returns 'this' Promptlet for chaining
* @throws {TypeError} Thrown if a function is not provided
*/
addPostProcessor(postProcessor) {
if(Array.isArray(postProcessor)) {
postProcessor.forEach(val => this.addValidator(val));
return this;
}
if(typeof postProcessor !== "function") throw new TypeError("Function required");
if(!this.postProcessors.includes(postProcessor)) {
this.postProcessors.push(postProcessor);
}
return this;
}
/**
* Remove a post processor from the prompt. (Requires exact same function instance to remove)
* @param {function} postProcessor Post processor function to remove
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
removePostProcessor(postProcessor) {
if(this.postProcessors.includes(postProcessor)) {
this.postProcessors.splice(this.postProcessors.indexOf(postProcessor), 1);
}
return this;
}
/**
* Runs all the post processor functions with the current value
* Modifies value property
* @param {Object} [answers = {}] Object with all the previous Promptlet answers if run by a PromptSet
* @return {Promptlet} Returns 'this' Promptlet for chaining
*/
async postProcess(answers = {}) {
for(const postProcessor of this.postProcessors) {
this.value = await postProcessor(this.value, answers);
}
return this;
}
/**
* Generates the listing for this Promptlet through an Object for inquirer lists
* @param {boolean} preSatisfied Whether the prerequisites have been satisfied (Cannot be automatically done internally by a Promptlet without a PromptSet)
* @return {{name: string, value: string}} An entry for the PromptSet.selectPromptlet() function's prompt
*/
generateListing(preSatisfied) {
return {
name: `${this.satisfied ? (this.editable ? "✎" : "✔") : (preSatisfied ? "○" : "⛝")} ${this.optionName}`,
value: this.name
};
}
/**
* Runs the Promptlet and sets satisfied property to true. Updates value property
* @async
* @param {Object} [answers = {}] Object with all the previous Promptlet answers if run by a PromptSet
* @return {string} Resolves to the answer value after inquirer finishes
*/
async start(answers = {}) {
this.value = (await Configurer.inquirer(this.info, answers))[this.name];
await this.postProcess(answers);
this.satisfied = true;
return this.value;
}
/**
* Returns basic data about the Promptlet as a string
* @return {string} String detailing the current state of the Promptlet
*/
toString() {
return `Promptlet <${this.name}>: ${this.info.type} | [${this.optionName}]\nMessage: ${this.info.message}\nStatus: [${this.satisfied ? "✔" : "⛝"} ${this.editable ? "✎" : ""}]\nValue: ${this.value}\nPrerequisites: ${this.prerequisites.join(" & ")}`;
}
}
module.exports = Promptlet;