prompts icon indicating copy to clipboard operation
prompts copied to clipboard

Allow input in autocomplete if not in list

Open LarsVonQualen opened this issue 5 years ago • 17 comments

Hi,

I was surprised to see that when using the autocomplete, i'm only able to actually input something from the choices (i know it's what the documentation says, but i still tried 😅).

So i was wondering if it would be a good idea to add a options flag allowing the user input, if it wasn't found in the list. This wouldn't change existing behavior at all, but would allow the use case where you want to use the autocomplete as a suggestive input instead. It could also be made into another prompt type, if that would be easier, extending the autocomplete instead.

Any thoughts?

LarsVonQualen avatar Feb 08 '19 17:02 LarsVonQualen

Hej @LarsVonQualen! I think this could be an option in the current one or maybe a completely new prompt type

terkelg avatar Feb 09 '19 15:02 terkelg

@terkelg I thought maybe tab could be used to append the the selected value from the list to the typed value? Like the autocomplete in chrome DevTools :wink:

This behaviour would be activated by an option.

elie-g avatar Feb 18 '19 07:02 elie-g

Yeah, that could totally work!

terkelg avatar Feb 18 '19 15:02 terkelg

@terkelg I can't use the tab for this purpose cause it would break its current behaviour.

I thought maybe when user input is activated enter would append the selected value and ctrl + enter would submit otherwise if user input isn't activated enter would behave like it currently behave?

On Mac it would be something like cmd + enter instead of ctrl + enter

elie-g avatar Mar 09 '19 04:03 elie-g

I see – good point. Sounds good to me!

terkelg avatar Mar 09 '19 15:03 terkelg

I just realised I can't do this cause for some reason node does not capture the ctrl, shift or alt key with enter.

I don't really know what we can do now... any idea?

elie-g avatar Mar 10 '19 05:03 elie-g

Maybe let's put this on hold for now?

terkelg avatar Mar 10 '19 21:03 terkelg

I created a PR in the NodeJS repo that could be used to solve this issue but it still needs to be accepted. With the PR, we will be able to capture Alt + Enter key combo so it will now be possible to do what I had suggested last year.

However, even if the PR is accepted, the new functionalities added by the PR would probably not be supported by the previous NodeJS versions. But anyway since now I know how it works behind the scene, I know how to implement this directly in prompts so we will still support older versions of NodeJS and that also means it doesn't really matter anymore if the PR is accepted or not.

Moreover, while making this PR I dug so deeply into ANSI related source codes, documentations and standards like RXVT, xTerm, ECMA-48 and ISO-6429 that I might possibly be able to implement mouse events and other interesting things I found. Though, I haven't tried the mouse events yet but theoretically I know how, I just need to make it and see if it really works.

@terkelg Let me now what you think.

Refs: https://github.com/nodejs/node/pull/35268

elie-g avatar Sep 19 '20 10:09 elie-g

Awesome! Congratulation on the NodeJS contribution! Let's have a hangout soon and plan the next rewrite, I'm super keen to learn more about your findings. High five mate!

terkelg avatar Sep 22 '20 19:09 terkelg

Any news on this? If i were to take a stab at this, which solution would be preferable. Option on existing autocomplete type or a new type all together, and what would that be named?

LarsVonQualen avatar Nov 16 '20 22:11 LarsVonQualen

On one hand a simple option should suffice. On the other, it might not be obvious that you're free to type your own name.

jakobrosenberg avatar Mar 28 '21 18:03 jakobrosenberg

I'm not sure if this has been implemented yet, so I went ahead and hacked together my own version. I am posting it so that others can see how I got it working. If someone has a better way to get this functionality, I would love to hear about it. Here is my current solution:

#!/usr/bin/env node

const prompts = require("prompts");

async function main() {
    // Print a new line for separation
    console.log("");

    // List of all autocomplete entries
    const entries = ["asdf", "bob", "alice", "AAAA", "javascript", "Hello", "&*$)#FdsaJdsa", "2021"];

    // User selects an entry
    const { selection } = await prompts(
        {
            type: "autocomplete",
            name: "selection",
            message: "Pick an option or press ^c to exit",

            // Maps the entries to an array of objects with title, description, and value properties
            choices: entries.map((entry) => ({ title: entry, description: `Selects ${entry}`, value: entry })),

            // When the user updates the prompt, i.e by pressing a key, we set the
            // fallback to an object with the current user input. The fallback object
            // is what gets rendered in the gray text, and it is not enough to simply
            // set the fallback object. While prompt will return the fallback object
            // if no choice is selected, for some reason, it cuts off the last char
            // from the title and value, thus we have to set the this.value property
            // as well. Also, the onState hook can not be an es6 arrow frunction,
            // becuase that will break the binding of the 'this' property, so it must
            // be declared like this.
            onState: function () {
                this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input };

                // Check to make sure there are no suggestions so we do not override a suggestion
                if (this.suggestions.length === 0) {
                    this.value = this.input;
                }
            },
        },
        {
            onCancel: () => {
                console.log("Canceled by user, exiting...");
                process.exit(1);
            },
        }
    );

    console.log(`Selection: ${selection}`);
}

main();

Here it is in action: gif3

Here shows the canceling: gif2

And this is what happens when you only set the fallback object in the onState hook and not update the value: gif1

leonitousconforti avatar Aug 02 '21 17:08 leonitousconforti

onState: function () {
                this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input };

                // Check to make sure there are no suggestions so we do not override a suggestion
                if (this.suggestions.length === 0) {
                    this.value = this.input;
                }
            },

In the latest version ,there is no this.suggestions anymore, it has been renamed to filteredOptions.

So use this.filteredOptions.length

But your code also throws TypeError: Cannot read properties of undefined (reading 'filter') error

What I'm trying to do is make it work with multiselectAutocomplete and by far I already made it work with custom options but it does not saves as selected:

onRender: function () {
  this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input }
  if (this.filteredOptions.length === 0) {
    this.filteredOptions = [
      {
        title: this.inputValue,
        description: undefined,
        value: this.inputValue,
        selected: this.value.some(({ title }) => title)?.selected,
        disabled: undefined
      }
    ]
  }
}

VityaSchel avatar Mar 01 '22 21:03 VityaSchel

Ok I wrote a solution for multiselectAutocomplete propmpt type but it's far from ideal and has a lot of cosmetical bugs, which I will try to fix soon.

  1. It duplicates last custom result if Return key was pressed after selecting it
  2. It does not remove un-selected custom result if it was unselected in empty search
  3. It continues to show selected custom result if you erase last char because it is added to values list
onRender: function () {
  // this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input }

  const options = this.filteredOptions
  if (options.length === 0) {
    this.filteredOptions = [
      {
        title: this.inputValue,
        description: undefined,
        value: this.inputValue,
        selected: this.value.some(({ title }) => title)?.selected || undefined,
        disabled: undefined,
        custom: true
      }
    ]
  } else if (options.length === 1 && options[0].custom) {
    const customOption = options[0]
    if (customOption.selected === true) {
      this.value = this.value.concat({ ...customOption, custom: undefined, customAdded: true })
    } else if (customOption.selected === false) {
      const customOptionIndex = this.value.findIndex(({ title }) => title === customOption.title)
      this.value.splice(customOptionIndex, 1)
    }
  }

  // removes custom un-selected options after re-search
  this.value = this.value.filter(({ customAdded, selected }) => customAdded ? selected : true)
},

VityaSchel avatar Mar 01 '22 22:03 VityaSchel

My previous solution still works for me with promps v2.4.2 (which I believe is the latest). ~~The exact code I am using is the same as above.~~ The exact code I am using is:

// User selects the command
const { command } = await prompts(
    {
        type: "autocomplete",
        name: "command",
        message: "Enter a command or press ^c to exit",
        choices: availableCommands.map((command) => ({ title: command, description: `Executes ${command}`, value: command })),
        onState: function () {
            // @ts-ignore
            this.fallback = { title: this.input, description: `Executes ${this.input}`, value: this.input };

            // @ts-ignore
            if (this.suggestions.length === 0) {
                this.value = this.fallback.value;
            }
        },
    },
    {
        onCancel: () => Promise.all([console.log("Canceled by user, exiting..."), process.exit()]),
    }
);

The this.suggestions isn't in the types, thats why I have added the @ts-ignore comment. This is a solution for the autocomplete input type and I have not tested it with the multiselectAutocomplete like you want

leonitousconforti avatar Mar 02 '22 01:03 leonitousconforti

@leonitousconforti, using your solution, works perfect with latest version! updated it a bit to get rid of @ts-ignore

        onState(this: any) {
          this.fallback = { title: this.input, value: this.input }
          if (this.suggestions.length === 0) {
            this.value = this.fallback.value
          }
        }

sleewoo avatar Jan 30 '23 19:01 sleewoo

@leonitousconforti, using your solution, works perfect with latest version! updated it a bit to get rid of @ts-ignore

        onState(this: any) {
          this.fallback = { title: this.input, value: this.input }
          if (this.suggestions.length === 0) {
            this.value = this.fallback.value
          }
        }

Works perfectly

elkhayder avatar Apr 25 '23 12:04 elkhayder