UNPKG

fez-lisp

Version:

Lisp interpreted & compiled to JavaScript

201 lines (169 loc) 5.11 kB
export const Prompt = (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 } } async function prompt(message, defaultInput) { return new Promise((resolve) => { const { overlay, form } = createModal(message) const input = document.createElement('textarea') applyStyles(input, inputStyle) // input.type = 'text' input.name = 'promptInput' if (defaultInput) input.value = defaultInput 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 prompt })()