} - Resolves to true if confirmed, false if cancelled
*/
function showConfirmDialog(title, message, options = {}) {
return new Promise((resolve) => {
const {
confirmText = 'Confirm',
cancelText = 'Cancel',
danger = false
} = options;
const confirmBtnClass = danger ? 'btn btn-danger' : 'btn btn-primary';
const content = `
`;
openModal(title, content);
// Attach event handlers after modal is opened
setTimeout(() => {
const confirmBtn = document.getElementById('confirm-dialog-confirm');
const cancelBtn = document.getElementById('confirm-dialog-cancel');
if (confirmBtn) {
confirmBtn.onclick = () => {
closeModal();
resolve(true);
};
}
if (cancelBtn) {
cancelBtn.onclick = () => {
closeModal();
resolve(false);
};
}
}, 50);
});
}
// ============ Prompt Dialog ============
/**
* Show a prompt dialog with a text input. Replaces native window.prompt().
* @param {string} title - Dialog title
* @param {string} message - Prompt message (rendered above the input)
* @param {Object} options - Optional settings
* @param {string} options.defaultValue - Initial input value
* @param {string} options.placeholder - Input placeholder
* @param {string} options.confirmText - Text for confirm button (default: "OK")
* @param {string} options.cancelText - Text for cancel button (default: "Cancel")
* @param {Function} options.validate - Optional sync validator; return error string to block, null to allow
* @returns {Promise} - Resolves to the entered value (trimmed), or null if cancelled
*/
function showPromptDialog(title, message, options = {}) {
return new Promise((resolve) => {
const {
defaultValue = '',
placeholder = '',
confirmText = 'OK',
cancelText = 'Cancel',
validate = null,
} = options;
const inputId = 'prompt-dialog-input';
const errorId = 'prompt-dialog-error';
const content = `
`;
openModal(title, content);
setTimeout(() => {
const input = document.getElementById(inputId);
const errorEl = document.getElementById(errorId);
const confirmBtn = document.getElementById('prompt-dialog-confirm');
const cancelBtn = document.getElementById('prompt-dialog-cancel');
const submit = () => {
const value = (input?.value || '').trim();
if (validate) {
const err = validate(value);
if (err) {
if (errorEl) {
errorEl.textContent = err;
errorEl.classList.add('visible');
}
return;
}
}
closeModal();
resolve(value);
};
if (confirmBtn) confirmBtn.onclick = submit;
if (cancelBtn) cancelBtn.onclick = () => { closeModal(); resolve(null); };
if (input) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); submit(); }
});
input.focus();
input.select();
}
}, 50);
});
}
// ============ Confirm Delete Helper ============
/**
* Show a confirmation dialog for deleting items.
* @param {string} itemType - Type of item ('task', 'email', 'project', 'event', etc.)
* @param {number} count - Number of items to delete (default: 1)
* @returns {Promise} - Resolves to true if confirmed
*/
async function confirmDelete(itemType, count = 1) {
const plural = count > 1;
const itemName = plural ? `${count} ${itemType}s` : `this ${itemType}`;
const title = plural ? `Delete ${count} ${itemType}s` : `Delete ${itemType.charAt(0).toUpperCase() + itemType.slice(1)}`;
return showConfirmDialog(
title,
`Are you sure you want to delete ${itemName}? This cannot be undone.`,
{ confirmText: 'Delete', danger: true }
);
}
// ============ API Call Wrapper ============
/**
* Wrapper for API calls with standardized error handling, toasts, and modal closing.
* @param {Promise} promise - The API promise to execute
* @param {Object} options - Options for handling the call
* @param {string} options.successMessage - Toast message on success
* @param {string} options.errorMessage - Base error message (actual error appended)
* @param {Function} options.onSuccess - Callback on success (receives result)
* @param {boolean} options.closeModal - Whether to close modal on success (default: true)
* @param {Function|Function[]} options.reload - Function(s) to call to reload data
* @returns {Promise<*>} - The result of the API call, or null on error
*/
async function apiCall(promise, options = {}) {
const {
successMessage,
errorMessage = 'Operation failed',
onSuccess,
closeModal: shouldCloseModal = true,
reload,
button,
retry,
} = options;
// Set button loading state
if (button) setButtonLoading(button, true);
try {
const result = await promise;
if (button) setButtonLoading(button, false);
if (successMessage) {
showToast(successMessage, 'success');
}
if (shouldCloseModal) {
closeModal();
}
if (onSuccess) {
onSuccess(result);
}
if (reload) {
const reloadFns = Array.isArray(reload) ? reload : [reload];
for (const fn of reloadFns) {
if (typeof fn === 'function') {
fn();
}
}
}
return result;
} catch (err) {
if (button) setButtonLoading(button, false);
const message = GoingsOn.utils.getErrorMessage(err, errorMessage);
const toastOpts = {};
if (retry) {
toastOpts.action = { label: 'Retry', fn: retry };
toastOpts.duration = 8000;
}
showToast(message, 'error', toastOpts);
return null;
}
}
// ============ Button Loading State ============
/**
* Set button loading state
* @param {HTMLButtonElement} button - The button element
* @param {boolean} loading - Whether to show loading state
*/
function setButtonLoading(button, loading) {
if (!button) return;
if (loading) {
// Wrap existing children in a span so CSS can hide text during loading
const wrapper = document.createElement('span');
wrapper.className = 'btn-text';
while (button.firstChild) {
wrapper.appendChild(button.firstChild);
}
button.appendChild(wrapper);
button.classList.add('btn-loading');
button.disabled = true;
} else {
// Unwrap: move children out of the span wrapper
const wrapper = button.querySelector('.btn-text');
if (wrapper) {
while (wrapper.firstChild) {
button.insertBefore(wrapper.firstChild, wrapper);
}
wrapper.remove();
}
button.classList.remove('btn-loading');
button.disabled = false;
}
}
// ============ Modal Swipe-to-Dismiss (mobile) ============
function initModalSwipeDismiss() {
if (!GoingsOn.touch?.isTouchDevice) return;
const container = document.getElementById('modal-container');
if (!container) return;
GoingsOn.touch.addDragToDismiss(container, () => {
closeModal();
});
}
// Initialize on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initModalSwipeDismiss);
} else {
// Delay slightly to ensure touch.js has loaded
setTimeout(initModalSwipeDismiss, 0);
}
// ============ Populate GoingsOn.modal Namespace ============
GoingsOn.modal = {
openModal,
closeModal,
showToast,
showUndoToast,
bulkActionWithUndo,
executeUndo,
cancelUndo,
showConfirmDialog,
showPromptDialog,
confirmDelete,
apiCall,
setButtonLoading,
};
})();