I often find myself wanting a quick modal to accept some input, but I don't want to include a big dependency, or manually code up something. And I'm obviously not alone here: I see window.prompt
and window.confirm
used quite often on the web - even in large applications.
I'm a huge fan of having "simple things be simple, and complex things be possible", as the saying goes, and in the case where I just want to quickly grab some user input, I think it should be as simple as prompt(...)
.
An async version of this is obviously pretty easy to "polyfill". Here's a very rough sketch:
async function prompt2(message, opts) {
let ctn = document.createElement("div");
let type = opts.type;
let input;
if(type == "select") {
input = `<select style="width:100%;height:100%;background:white;color:inherit;font:inherit;box-sizing:border-box;">${opts.options.map(o => `<option value="${o.value}">${o.content}</option>`).join("")}</select>`;
} else if(type == "buttons") {
input = opts.buttons.map(o => `<button style="height:100%;margin-right:0.5rem;font:inherit;box-sizing:border-box;" data-value="${o.value}">${o.content}</button>`).join("");
} else {
input = `<input style="width:100%;height:100%;background:white;color:inherit;font:inherit;box-sizing:border-box;" type="${opts.type}">`;
}
ctn.innerHTML = `
<div style="background:rgba(0,0,0,0.2); position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999999; display:flex; justify-content:center; color:black; font-family: sans-serif;">
<div style="width:400px; background:white; height: min-content; padding:1rem; border:1px solid #eaeaea; border-radius:3px; box-shadow: 0px 1px 10px 3px rgba(0,0,0,0.24); margin-top:0.5rem;">
<div style="opacity:0.6;">${window.location.hostname} says:</div>
<div style="margin:1rem 0;">${message}</div>
<div style="display:flex; height:2.3rem;">
<div style="flex-grow:1; padding-right:0.5rem;">${input}</div>
${type !== "buttons" ? `<button style="font:inherit;box-sizing:border-box;">Submit</button>` : ""}
</div>
</div>
</div>
`;
document.body.appendChild(ctn);
let value = await new Promise((resolve) => {
if(type !== "buttons") {
ctn.querySelector("button").onclick = () => {
if(type == "file") {
resolve(ctn.querySelector("input").files);
} else {
resolve(ctn.querySelector("input,select").value);
}
}
} else {
ctn.querySelectorAll("button").forEach(b => {
b.onclick = () => resolve(b.dataset.value);
});
}
});
ctn.remove();
return value;
}
Usage:
let name = await prompt2("Please type your name:", {type:"text"});
let pw = await prompt2("Please type your password:", {type:"password"});
let date = await prompt2("Please choose a date:", {type:"date"});
let files = await prompt2("Please choose a file:", {type:"file"});
let choice = await prompt2("Please choose an option:", {type:"select", options:[{content:"Thing 1", value:"1"}, {content:"Thing 2", value:"2"}]});
let choice = await prompt2("Please choose an option:", {type:"buttons", buttons:[{content:"Thing 1", value:"1"}, {content:"Thing 2", value:"2"}]});
Example of type:"date"
:
Obviously window.prompt
can't be made async, and the second param of window.prompt
is already used for the default value, so perhaps it would be better to start over with a new function. Although it would be great to have a sync version of this API too - so that we don't have to sprinkle async
throughout a codebase that was completely sync up until that point.
I wonder if it would be safe enough to use the second param of window.prompt
? I doubt anyone is actually passing an object to the second parameter on purpose, so that they can have [object Object]
as the default value. And then a window.promptAsync
could be added which has the same UI, but returns a promise. But I'm really just thinking out loud here.
Does anyone have any thoughts here? Have there been attempts to do bring something like this through TC39 before? (TC39 is the appropriate body to standardize something like this, right? Or is TC39 aimed more at lower-level language details?)
Thanks!