Skip to content

Commit

Permalink
Prompts.js
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw authored Dec 7, 2024
1 parent 561dc27 commit e7d7371
Showing 1 changed file with 337 additions and 0 deletions.
337 changes: 337 additions & 0 deletions prompt-js.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
<!DOCTYPE html>
<html>
<head>
<title>Prompts.js</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
margin: 2rem;
padding: 2rem;
max-width: 800px;
margin: 2rem auto;
line-height: 1.6;
}
input, textarea {
font-size: 16px;
}
.button-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.button-container button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.result {
margin-top: 10px;
padding: 10px;
background-color: #f4f4f4;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Prompts.js</h1>
<p>A lightweight JavaScript library for creating modal dialogs with alert, confirm, and prompt functionalities.</p>
<pre style="font-size: 0.8em; white-space: pre-wrap">
await Prompts.alert("This is an alert message!");
const resultBoolean = await Prompts.confirm("Do you want to proceed?");
const name = await Prompts.prompt("What is your name?");
</pre>

<div class="button-container">
<button id="alertBtn">Show Alert</button>
<button id="confirmBtn">Show Confirm</button>
<button id="promptBtn">Show Prompt</button>
</div>

<div id="resultArea" class="result"></div>

<script type="module">
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 };
})();


const resultArea = document.getElementById('resultArea');

document.getElementById('alertBtn').addEventListener('click', async () => {
await Prompts.alert("This is an alert message!");
resultArea.textContent = "Alert was shown and dismissed.";
});

document.getElementById('confirmBtn').addEventListener('click', async () => {
const result = await Prompts.confirm("Do you want to proceed?");
resultArea.textContent = `Confirm result: ${result}`;
});

document.getElementById('promptBtn').addEventListener('click', async () => {
const name = await Prompts.prompt("What is your name?");
resultArea.textContent = name ? `Hello, ${name}!` : "Prompt was cancelled.";
});
</script>
</body>
</html>

0 comments on commit e7d7371

Please sign in to comment.