From 86a850ccb0c4bf2685fb202604c137eeaba4e657 Mon Sep 17 00:00:00 2001 From: Jacob Lamont Date: Tue, 17 Oct 2023 16:32:46 +0000 Subject: [PATCH] add esm support --- index.mjs | 548 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 9 +- 2 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 index.mjs diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..2af850c --- /dev/null +++ b/index.mjs @@ -0,0 +1,548 @@ +/** + * Checkbox Plus + * + * @author Mohammad Fares + */ + +'use strict'; + +import _ from 'lodash'; +import chalk from 'chalk'; +import { map, takeUntil } from 'rxjs/operators/index.js'; +import cliCursor from 'cli-cursor'; +import figures from 'figures'; +import Base from 'inquirer/lib/prompts/base.js'; +import Choices from 'inquirer/lib/objects/choices.js'; +import observe from 'inquirer/lib/utils/events.js'; +import Paginator from 'inquirer/lib/utils/paginator.js'; + +/** + * CheckboxPlusPrompt + */ +class CheckboxPlusPrompt extends Base { + + /** + * Initialize the prompt + * + * @param {Object} questions + * @param {Object} rl + * @param {Object} answers + */ + constructor(questions, rl, answers) { + + super(questions, rl, answers); + + // Default value for the highlight option + if (typeof this.opt.highlight == 'undefined') { + this.opt.highlight = false; + } + + // Default value for the searchable option + if (typeof this.opt.searchable == 'undefined') { + this.opt.searchable = false; + } + + // Default value for the default option + if (typeof this.opt.default == 'undefined') { + this.opt.default = null; + } + + // Doesn't have source option + if (!this.opt.source) { + this.throwParamError('source'); + } + + // Init + this.pointer = 0; + this.firstSourceLoading = true; + this.choices = new Choices([], answers); + this.checkedChoices = []; + this.value = []; + this.lastQuery = null; + this.searching = false; + this.lastSourcePromise = null; + this.default = this.opt.default; + this.opt.default = null; + + this.paginator = new Paginator(this.screen); + + } + + /** + * Start the Inquiry session + * + * @param {Function} callback callback when prompt is done + * @return {this} + */ + _run(callback) { + + var self = this; + + this.done = callback; + + this.executeSource().then(function(result) { + + var events = observe(self.rl); + + var validation = self.handleSubmitEvents( + events.line.pipe(map(self.getCurrentValue.bind(self))) + ); + validation.success.forEach(self.onEnd.bind(self)); + validation.error.forEach(self.onError.bind(self)); + + events.normalizedUpKey + .pipe(takeUntil(validation.success)) + .forEach(self.onUpKey.bind(self)); + events.normalizedDownKey + .pipe(takeUntil(validation.success)) + .forEach(self.onDownKey.bind(self)); + events.keypress + .pipe(takeUntil(validation.success)) + .forEach(self.onKeypress.bind(self)); + events.spaceKey + .pipe(takeUntil(validation.success)) + .forEach(self.onSpaceKey.bind(self)); + + // If the search is enabled + if (!self.opt.searchable) { + + events.numberKey + .pipe(takeUntil(validation.success)) + .forEach(self.onNumberKey.bind(self)); + events.aKey + .pipe(takeUntil(validation.success)) + .forEach(self.onAllKey.bind(self)); + events.iKey + .pipe(takeUntil(validation.success)) + .forEach(self.onInverseKey.bind(self)); + + } else { + events.keypress + .pipe(takeUntil(validation.success)) + .forEach(self.onKeypress.bind(self)); + } + + if (self.rl.line) { + self.onKeypress(); + } + + // Init the prompt + cliCursor.hide(); + self.render(); + + }); + + return this; + + } + + /** + * Execute the source function to get the choices and render them + */ + executeSource() { + + var self = this; + var sourcePromise = null; + + // Remove spaces + this.rl.line = _.trim(this.rl.line); + + // Same last search query that already loaded + if (this.rl.line === this.lastQuery) { + return; + } + + // If the search is enabled + if (this.opt.searchable) { + sourcePromise = this.opt.source(this.answers, this.rl.line); + } else { + sourcePromise = this.opt.source(this.answers, null); + } + + this.lastQuery = this.rl.line; + this.lastSourcePromise = sourcePromise; + this.searching = true; + + sourcePromise.then(function(choices) { + + // Is not the last issued promise + if (self.lastSourcePromise !== sourcePromise) { + return; + } + + // Reset the searching status + self.searching = false; + + // Save the new choices + self.choices = new Choices(choices, self.answers); + + // Foreach choice + self.choices.forEach(function(choice) { + + // Is the current choice included in the current checked choices + if (_.findIndex(self.value, _.isEqual.bind(null, choice.value)) != -1) { + self.toggleChoice(choice, true); + } else { + self.toggleChoice(choice, false); + } + + // The default is not applied yet + if (self.default) { + + // Is the current choice included in the default values + if (_.findIndex(self.default, _.isEqual.bind(null, choice.value)) != -1) { + self.toggleChoice(choice, true); + } + + } + + }); + + // Reset the pointer to select the first choice + self.pointer = 0; + self.render(); + self.default = null; + self.firstSourceLoading = false; + + + }); + + return sourcePromise; + + } + + /** + * Render the prompt + * + * @param {Object} error + */ + render(error) { + + // Render question + var message = this.getQuestion(); + var bottomContent = ''; + + // Answered + if (this.status === 'answered') { + + message += chalk.cyan(this.selection.join(', ')); + return this.screen.render(message, bottomContent); + + } + + // No search query is entered before + if (this.firstSourceLoading) { + + // If the search is enabled + if (this.opt.searchable) { + + message += + '(Press ' + + chalk.cyan.bold('') + + ' to select, ' + + 'or type anything to filter the list)'; + + } else { + + message += + '(Press ' + + chalk.cyan.bold('') + + ' to select, ' + + chalk.cyan.bold('') + + ' to toggle all, ' + + chalk.cyan.bold('') + + ' to invert selection)'; + + } + + } + + // If the search is enabled + if (this.opt.searchable) { + + // Print the current search query + message += this.rl.line; + + } + + // Searching mode + if (this.searching) { + + message += '\n ' + chalk.cyan('Searching...'); + + // No choices + } else if (!this.choices.length) { + + message += '\n ' + chalk.yellow('No results...'); + + // Has choices + } else { + + var choicesStr = this.renderChoices(this.choices, this.pointer); + + var indexPosition = this.choices.indexOf( + this.choices.getChoice(this.pointer) + ); + + message += '\n' + this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize); + + } + + if (error) { + bottomContent = chalk.red('>> ') + error; + } + + this.screen.render(message, bottomContent); + + } + + /** + * A callback function for the event: + * When the user press `Enter` key + * + * @param {Object} state + */ + onEnd(state) { + + this.status = 'answered'; + + // Rerender prompt (and clean subline error) + this.render(); + + this.screen.done(); + cliCursor.show(); + this.done(state.value); + + } + + /** + * A callback function for the event: + * When something wrong happen + * + * @param {Object} state + */ + onError(state) { + this.render(state.isValid); + } + + /** + * Get the current values of the selected choices + * + * @return {Array} + */ + getCurrentValue() { + + this.selection = _.map(this.checkedChoices, 'short'); + return _.map(this.checkedChoices, 'value'); + + } + + /** + * A callback function for the event: + * When the user press `Up` key + */ + onUpKey() { + + var len = this.choices.realLength; + this.pointer = this.pointer > 0 ? this.pointer - 1 : len - 1; + this.render(); + + } + + /** + * A callback function for the event: + * When the user press `Down` key + */ + onDownKey() { + + var len = this.choices.realLength; + this.pointer = this.pointer < len - 1 ? this.pointer + 1 : 0; + this.render(); + + } + + /** + * A callback function for the event: + * When the user press a number key + */ + onNumberKey(input) { + + if (input <= this.choices.realLength) { + this.pointer = input - 1; + this.toggleChoice(this.choices.getChoice(this.pointer)); + } + + this.render(); + + } + + /** + * A callback function for the event: + * When the user press `Space` key + */ + onSpaceKey() { + + // When called no results + if (!this.choices.getChoice(this.pointer)) { + return; + } + + this.toggleChoice(this.choices.getChoice(this.pointer)); + this.render(); + + } + + /** + * A callback function for the event: + * When the user press 'a' key + */ + onAllKey() { + + var shouldBeChecked = Boolean( + this.choices.find(function(choice) { + return choice.type !== 'separator' && !choice.checked; + }) + ); + + this.choices.forEach(function(choice) { + if (choice.type !== 'separator') { + choice.checked = shouldBeChecked; + } + }); + + this.render(); + + } + + /** + * A callback function for the event: + * When the user press `i` key + */ + onInverseKey() { + + this.choices.forEach(function(choice) { + if (choice.type !== 'separator') { + choice.checked = !choice.checked; + } + }); + + this.render(); + + } + + /** + * A callback function for the event: + * When the user press any key + */ + onKeypress() { + + this.executeSource(); + this.render(); + + } + + /** + * Toggle (check/uncheck) a specific choice + * + * @param {Boolean} checked if not specified the status will be toggled + * @param {Object} choice + */ + toggleChoice(choice, checked) { + + // Default value for checked + if (typeof checked === 'undefined') { + checked = !choice.checked; + } + + // Remove the choice's value from the checked values + _.remove(this.value, _.isEqual.bind(null, choice.value)); + + // Remove the checkedChoices with the value of the current choice + _.remove(this.checkedChoices, function(checkedChoice) { + return _.isEqual(choice.value, checkedChoice.value); + }); + + choice.checked = false; + + // Is the choice checked + if (checked) { + this.value.push(choice.value); + this.checkedChoices.push(choice); + choice.checked = true; + } + + } + + /** + * Get the checkbox figure (sign) + * + * @param {Boolean} checked + * @return {String} + */ + getCheckboxFigure(checked) { + + return checked ? chalk.green(figures.radioOn) : figures.radioOff; + + } + + /** + * Render the checkbox choices + * + * @param {Array} choices + * @param {Number} pointer the position of the pointer + * @return {String} rendered content + */ + renderChoices(choices, pointer) { + + var self = this; + var output = ''; + var separatorOffset = 0; + + // Foreach choice + choices.forEach(function(choice, index) { + + // Is a separator + if (choice.type === 'separator') { + + separatorOffset++; + output += ' ' + choice + '\n'; + return; + + } + + // Is the choice disabled + if (choice.disabled) { + + separatorOffset++; + output += ' - ' + choice.name; + output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')'; + output += '\n'; + return; + + } + + // Is the current choice is the selected choice + if (index - separatorOffset === pointer) { + + output += chalk.cyan(figures.pointer); + output += self.getCheckboxFigure(choice.checked) + ' '; + output += self.opt.highlight ? chalk.gray(choice.name) : choice.name; + + } else { + + output += ' ' + self.getCheckboxFigure(choice.checked) + ' ' + choice.name; + + } + + output += '\n'; + + + }); + + return output.replace(/\n$/, ''); + + } + +} + +export default CheckboxPlusPrompt; diff --git a/package.json b/package.json index 00cb1be..410b62e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,13 @@ { "name": "inquirer-checkbox-plus-prompt", - "version": "1.4.2", + "exports": { + ".": { + "require": "./index.js", + "import": "./index.mjs", + "default": "./index.js" + } + }, + "version": "1.5.0", "description": "Checkbox with autocomplete and other additions for Inquirer", "main": "index.js", "repository": {