310 lines
8.2 KiB
JavaScript
310 lines
8.2 KiB
JavaScript
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<string|boolean>}
|
|
*/
|
|
const title = ref('')
|
|
|
|
/**
|
|
* Modal body content
|
|
* Can be a string, VNode, or Vue component
|
|
* @type {import('vue').Ref<any>}
|
|
*/
|
|
const body = ref(null)
|
|
|
|
/**
|
|
* Modal footer content
|
|
* Usually a Vue component, render function, or null
|
|
* @type {import('vue').Ref<any>}
|
|
*/
|
|
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<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<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,
|
|
}
|
|
}
|