import { ref, h } from 'vue' /** * Global modal visibility state * Shared across the entire application */ const show = ref(false) /** * Modal title text * Can also be `false` to hide the header * @type {import('vue').Ref} */ const title = ref('') /** * Modal body content * Can be a string, VNode, or Vue component * @type {import('vue').Ref} */ const body = ref(null) /** * Modal footer content * Usually a Vue component, render function, or null * @type {import('vue').Ref} */ const footer = ref(null) /** * Global modal composable. * * Provides a single shared modal instance * that can be opened, updated, and closed from anywhere. * * Supports string content or Vue components in the body and footer. * * Usage: * ```js * import { useModal } from '@/composables/useModal' * import { h } from 'vue' * import UserDetails from '@/components/UserDetails.vue' * * const modal = useModal() * * // Open with a string body * modal.open({ title: 'Notice', body: 'Hello world' }) * * // Open with a Vue component body * modal.open({ * title: 'User Details', * body: h(UserDetails, { userId: 42 }) * }) * ``` */ export function useModal() { /** Opens the modal */ function open({ title: t = '', body: b = null, footer: f = null } = {}) { title.value = t body.value = b footer.value = f show.value = true } /** Closes the modal */ function close() { show.value = false } /** Update modal body dynamically */ function setBody(content) { body.value = content } /** Update modal footer dynamically */ function setFooter(content) { footer.value = content } /** Quickly replaces the currently open modal with new content */ function quickDismiss({ title: t = '', body: b = '', footer: f = null, condition = true, onShown = null } = {}) { if (!condition) return show.value = false setTimeout(() => { open({ title: t, body: b, footer: f }) if (typeof onShown === 'function') onShown() }, 10) } /** * Displays a modal with navigation buttons for SPA routing. * * Each button can pass props to the destination page component. * * @param {Object} options * @param {string} options.title - Modal title * @param {string|VNode|Component} options.body - Modal body * @param {Array} options.buttons - Buttons array: * [pageName, buttonText, target?, props?] * Example: ['Users.View', 'View User', 0, { userId: 42 }] * @param {boolean} [options.condition=true] - Show modal if true * @param {Function} [options.onShown] - Callback after modal opens */ /** * @param {Object} options * @param {string} options.title - Modal title * @param {string|VNode|Component} options.body - Modal body * @param {Array} options.buttons - Buttons array: * [pageName, buttonText, target?, props?] * @param {Function} options.navigate - The navigate function from useNavigate() * @param {boolean} [options.condition=true] - Show modal if true * @param {Function} [options.onShown] - Callback after modal opens */ function quickDismissGoto({ title: t = '', body: b = '', buttons = [], navigate = null, condition = true, onShown = null } = {}) { if (!condition) return const normalized = Array.isArray(buttons[0]) ? buttons : [buttons] const FooterComponent = { render() { const renderedButtons = normalized.map(btn => { const page = btn[0] const text = btn[1] || 'Go to Page' const props = btn[3] || {} return h('button', { class: 'btn btn-primary flex-fill py-2 rounded-3 shadow-sm fw-bold', onClick: () => { close() if (typeof navigate === 'function') navigate({ page, props }) } }, text) }) return h('div', { class: 'd-flex w-100 gap-3' }, renderedButtons) } } show.value = false setTimeout(() => { open({ title: t, body: b, footer: FooterComponent }) if (typeof onShown === 'function') onShown() }, 10) } /** * Displays a Yes / No modal with custom callbacks. * * @param {Object} options * @param {string} options.title - Modal title * @param {string|VNode|Component} options.body - Modal body * @param {string} options.yesText - Yes button text * @param {Function} options.onYes - Yes callback * @param {string} options.noText - No button text * @param {Function} options.onNo - No callback * @param {boolean} [options.condition=true] - Show modal if true * @param {Function} [options.onShown] - Callback after modal opens */ function yesNoModal({ title: t = '', body: b = '', yesText = 'Yes', yesClass = 'btn btn-primary w-50 py-2 rounded-3 shadow-sm fw-bold', onYes = null, noText = 'No', noClass = 'btn btn-light w-50 py-2 rounded-3 border fw-bold text-muted', onNo = null, condition = true, onShown = null } = {}) { if (!condition) return const FooterComponent = { render() { return h('div', { class: 'd-flex w-100 gap-3' }, [ h('button', { class: noClass, onClick: () => { close(); onNo?.() } }, noText), h('button', { class: yesClass, onClick: () => { close(); onYes?.() } }, yesText) ]) } } show.value = false setTimeout(() => { open({ title: t, body: b, footer: FooterComponent }) onShown?.() }, 10) } /** * Displays a Continue / Cancel modal. * * @param {Object} options * @param {string} options.title - Modal title * @param {string|VNode|Component} options.body - Modal body content * @param {Function} options.onContinue - Callback when Continue is clicked * @param {string} [options.continueText='Continue'] - Continue button text * @param {string} [options.cancelText='Cancel'] - Cancel button text * @param {string} [options.continueClass='btn btn-danger'] - Continue button CSS * @param {string} [options.cancelClass='btn btn-warning'] - Cancel button CSS * @param {boolean} [options.condition=true] - Only show if true * @param {Function|null} [options.onShown] - Callback after modal opens * * @example * modal.continueCancelModal({ * title: 'Unsaved Changes', * body: 'You have unsaved changes. Continue?', * onContinue: () => saveAndProceed() * }) */ function continueCancelModal({ title = '', body = '', onContinue = null, continueText = 'Continue', cancelText = 'Cancel', continueClass = 'btn btn-danger w-50 py-2 rounded-3 shadow-sm fw-bold', cancelClass = 'btn btn-light w-50 py-2 rounded-3 border fw-bold text-muted', condition = true, showCancel = true, onShown = null } = {}) { if (condition !== true) return const FooterComponent = { render() { const buttons = [] // Cancel button if (showCancel) { buttons.push( h( 'button', { class: cancelClass, onClick: () => { close() } }, cancelText ) ) } // Continue button buttons.push( h( 'button', { class: continueClass, onClick: () => { close() if (typeof onContinue === 'function') { onContinue() } } }, continueText ) ) return h('div', { class: 'd-flex w-100 gap-3' }, buttons) } } // Close any existing modal first show.value = false setTimeout(() => { open({ title, body, footer: FooterComponent }) if (typeof onShown === 'function') { onShown() } }, 10) } /** * Hides the currently open modal. * Vue replacement for hideallmodals() and hidemodal() */ function hideModal() { close() } /** * Alias for hideModal (migration helper) */ function hideAllModals() { close() } return { show, title, body, footer, open, close, setBody, setFooter, quickDismiss, quickDismissGoto, yesNoModal, continueCancelModal, hideModal, hideAllModals, } }