Skip to content

Commit

Permalink
Initial 0.0.1 release
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Dec 7, 2024
0 parents commit f89f3bc
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 0 deletions.
264 changes: 264 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
const Prompts = (function () {
let modalOpen = false;
let previouslyFocusedElement = null;

// Common styles
const overlayStyle = {
position: "fixed",
top: "0",
left: "0",
right: "0",
bottom: "0",
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: "999999",
};

const modalStyle = {
backgroundColor: "#fff",
borderRadius: "6px",
padding: "20px",
minWidth: "300px",
maxWidth: "80%",
boxSizing: "border-box",
fontFamily: "sans-serif",
boxShadow: "0 2px 10px rgba(0,0,0,0.2)",
};

const messageStyle = {
marginBottom: "20px",
fontSize: "16px",
color: "#333",
wordWrap: "break-word",
whiteSpace: "pre-wrap",
};

const buttonRowStyle = {
textAlign: "right",
marginTop: "20px",
};

const buttonStyle = {
backgroundColor: "#007bff",
color: "#fff",
border: "none",
borderRadius: "4px",
padding: "8px 12px",
fontSize: "14px",
cursor: "pointer",
marginLeft: "8px",
};

const inputStyle = {
width: "100%",
boxSizing: "border-box",
padding: "8px",
fontSize: "14px",
marginBottom: "20px",
borderRadius: "4px",
border: "1px solid #ccc",
};

function applyStyles(element, styles) {
for (const prop in styles) {
element.style[prop] = styles[prop];
}
}

function createModal(message) {
const overlay = document.createElement("div");
applyStyles(overlay, overlayStyle);

const modal = document.createElement("div");
applyStyles(modal, modalStyle);

const form = document.createElement("form");
form.setAttribute("novalidate", "true");
modal.appendChild(form);

const msg = document.createElement("div");
applyStyles(msg, messageStyle);
msg.textContent = message;

form.appendChild(msg);
overlay.appendChild(modal);

return { overlay, modal, form };
}

function createButton(label, onClick, type = "button") {
const btn = document.createElement("button");
applyStyles(btn, buttonStyle);
btn.type = type;
btn.textContent = label;
if (onClick) {
btn.addEventListener("click", onClick);
}
return btn;
}

function showModal(overlay, onClose) {
if (modalOpen) return; // Prevent multiple modals
modalOpen = true;

previouslyFocusedElement = document.activeElement;
document.body.appendChild(overlay);

// Focus trap and ESC handler
const focusableElements = overlay.querySelectorAll(
'button, input, [href], select, textarea, [tabindex]:not([tabindex="-1"])',
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];

function keyHandler(e) {
if (e.key === "Escape") {
e.preventDefault();
cleanup();
onClose("escape");
} else if (e.key === "Tab") {
// Focus trapping
if (focusableElements.length === 1) {
e.preventDefault(); // Only one focusable element, cycle back to it.
} else {
if (e.shiftKey && document.activeElement === firstFocusable) {
// If Shift+Tab on first element, go to last
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
// If Tab on last element, go to first
e.preventDefault();
firstFocusable.focus();
}
}
}
}

document.addEventListener("keydown", keyHandler);

function cleanup() {
document.removeEventListener("keydown", keyHandler);
if (overlay.parentNode) {
document.body.removeChild(overlay);
}
modalOpen = false;
if (previouslyFocusedElement && previouslyFocusedElement.focus) {
previouslyFocusedElement.focus();
}
}

return { cleanup, firstFocusable, focusableElements };
}

async function alert(message) {
return new Promise((resolve) => {
const { overlay, form } = createModal(message);

const buttonRow = document.createElement("div");
applyStyles(buttonRow, buttonRowStyle);

// OK button submits the form
const okBtn = createButton("OK", null, "submit");
buttonRow.appendChild(okBtn);
form.appendChild(buttonRow);

form.onsubmit = (e) => {
e.preventDefault();
cleanup();
resolve();
};

const { cleanup, firstFocusable } = showModal(overlay, (reason) => {
// Escape pressed
resolve();
});

// Move focus to OK button
firstFocusable.focus();
});
}

async function confirm(message) {
return new Promise((resolve) => {
const { overlay, form } = createModal(message);

const buttonRow = document.createElement("div");
applyStyles(buttonRow, buttonRowStyle);

const cancelBtn = createButton("Cancel", (e) => {
e.preventDefault();
cleanup();
resolve(false);
});
cancelBtn.style.backgroundColor = "#6c757d";

const okBtn = createButton("OK", null, "submit");

buttonRow.appendChild(cancelBtn);
buttonRow.appendChild(okBtn);
form.appendChild(buttonRow);

form.onsubmit = (e) => {
e.preventDefault();
// Enter key (submit) is treated as clicking OK
cleanup();
resolve(true);
};

const { cleanup, focusableElements } = showModal(overlay, (reason) => {
// If escaped, treat as cancel
resolve(false);
});

// Move focus to OK button (second button)
focusableElements[1].focus();
});
}

async function prompt(message) {
return new Promise((resolve) => {
const { overlay, form } = createModal(message);

const input = document.createElement("input");
applyStyles(input, inputStyle);
input.type = "text";
input.name = "promptInput";
form.appendChild(input);

const buttonRow = document.createElement("div");
applyStyles(buttonRow, buttonRowStyle);

const cancelBtn = createButton("Cancel", (e) => {
e.preventDefault();
cleanup();
resolve(null);
});
cancelBtn.style.backgroundColor = "#6c757d";

const okBtn = createButton("OK", null, "submit");

buttonRow.appendChild(cancelBtn);
buttonRow.appendChild(okBtn);
form.appendChild(buttonRow);

form.onsubmit = (e) => {
e.preventDefault();
const val = input.value;
cleanup();
resolve(val);
};

const { cleanup, firstFocusable } = showModal(overlay, (reason) => {
// If escaped, treat as cancel
resolve(null);
});

// Focus on the input for convenience
input.focus();
});
}

return { alert, confirm, prompt };
})();
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "prompts-js",
"version": "0.0.1",
"description": "async alternatives to browser alert() and prompt() and confirm()",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Simon Willison",
"license": "ISC"
}

0 comments on commit f89f3bc

Please # to comment.