mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
251 lines
6.8 KiB
TypeScript
251 lines
6.8 KiB
TypeScript
// We should consider moving to https://primevue.org/dynamicdialog/ once everything is in Vue.
|
|
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
|
|
import { merge } from 'es-toolkit/compat'
|
|
import { defineStore } from 'pinia'
|
|
import type { DialogPassThroughOptions } from 'primevue/dialog'
|
|
import { type Component, markRaw, ref } from 'vue'
|
|
|
|
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
|
|
|
type DialogPosition =
|
|
| 'center'
|
|
| 'top'
|
|
| 'bottom'
|
|
| 'left'
|
|
| 'right'
|
|
| 'topleft'
|
|
| 'topright'
|
|
| 'bottomleft'
|
|
| 'bottomright'
|
|
|
|
interface CustomDialogComponentProps {
|
|
maximizable?: boolean
|
|
maximized?: boolean
|
|
onClose?: () => void
|
|
closable?: boolean
|
|
modal?: boolean
|
|
position?: DialogPosition
|
|
pt?: DialogPassThroughOptions
|
|
closeOnEscape?: boolean
|
|
dismissableMask?: boolean
|
|
unstyled?: boolean
|
|
headless?: boolean
|
|
}
|
|
|
|
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
|
CustomDialogComponentProps
|
|
|
|
interface DialogInstance {
|
|
key: string
|
|
visible: boolean
|
|
title?: string
|
|
headerComponent?: Component
|
|
component: Component
|
|
contentProps: Record<string, any>
|
|
footerComponent?: Component
|
|
dialogComponentProps: DialogComponentProps
|
|
priority: number
|
|
}
|
|
|
|
export interface ShowDialogOptions {
|
|
key?: string
|
|
title?: string
|
|
headerComponent?: Component
|
|
footerComponent?: Component
|
|
component: Component
|
|
props?: Record<string, any>
|
|
dialogComponentProps?: DialogComponentProps
|
|
/**
|
|
* Optional priority for dialog stacking.
|
|
* A dialog will never be shown above a dialog with a higher priority.
|
|
* @default 1
|
|
*/
|
|
priority?: number
|
|
}
|
|
|
|
export const useDialogStore = defineStore('dialog', () => {
|
|
const dialogStack = ref<DialogInstance[]>([])
|
|
|
|
/**
|
|
* The key of the currently active (top-most) dialog.
|
|
* Only the active dialog can be closed with the ESC key.
|
|
*/
|
|
const activeKey = ref<string | null>(null)
|
|
|
|
const genDialogKey = () => `dialog-${Math.random().toString(36).slice(2, 9)}`
|
|
|
|
/**
|
|
* Inserts a dialog into the stack at the correct position based on priority.
|
|
* Higher priority dialogs are placed before lower priority ones.
|
|
*/
|
|
function insertDialogByPriority(dialog: DialogInstance) {
|
|
const insertIndex = dialogStack.value.findIndex(
|
|
(d) => d.priority <= dialog.priority
|
|
)
|
|
|
|
dialogStack.value.splice(
|
|
insertIndex === -1 ? dialogStack.value.length : insertIndex,
|
|
0,
|
|
dialog
|
|
)
|
|
}
|
|
|
|
function riseDialog(options: { key: string }) {
|
|
const dialogKey = options.key
|
|
|
|
const index = dialogStack.value.findIndex((d) => d.key === dialogKey)
|
|
if (index !== -1) {
|
|
const [dialog] = dialogStack.value.splice(index, 1)
|
|
insertDialogByPriority(dialog)
|
|
activeKey.value = dialogKey
|
|
updateCloseOnEscapeStates()
|
|
}
|
|
}
|
|
|
|
function closeDialog(options?: { key: string }) {
|
|
const targetDialog = options
|
|
? dialogStack.value.find((d) => d.key === options.key)
|
|
: dialogStack.value.find((d) => d.key === activeKey.value)
|
|
if (!targetDialog) return
|
|
|
|
targetDialog.dialogComponentProps?.onClose?.()
|
|
const index = dialogStack.value.indexOf(targetDialog)
|
|
dialogStack.value.splice(index, 1)
|
|
|
|
activeKey.value =
|
|
dialogStack.value.length > 0
|
|
? dialogStack.value[dialogStack.value.length - 1].key
|
|
: null
|
|
|
|
updateCloseOnEscapeStates()
|
|
}
|
|
|
|
function createDialog(options: {
|
|
key: string
|
|
title?: string
|
|
headerComponent?: Component
|
|
footerComponent?: Component
|
|
component: Component
|
|
props?: Record<string, any>
|
|
dialogComponentProps?: DialogComponentProps
|
|
priority?: number
|
|
}) {
|
|
if (dialogStack.value.length >= 10) {
|
|
dialogStack.value.shift()
|
|
}
|
|
|
|
const dialog = {
|
|
key: options.key,
|
|
visible: true,
|
|
title: options.title,
|
|
headerComponent: options.headerComponent
|
|
? markRaw(options.headerComponent)
|
|
: undefined,
|
|
footerComponent: options.footerComponent
|
|
? markRaw(options.footerComponent)
|
|
: undefined,
|
|
component: markRaw(options.component),
|
|
contentProps: { ...options.props },
|
|
priority: options.priority ?? 1,
|
|
dialogComponentProps: {
|
|
maximizable: false,
|
|
modal: true,
|
|
closable: true,
|
|
closeOnEscape: true,
|
|
dismissableMask: true,
|
|
...options.dialogComponentProps,
|
|
maximized: false,
|
|
onMaximize: () => {
|
|
dialog.dialogComponentProps.maximized = true
|
|
},
|
|
onUnmaximize: () => {
|
|
dialog.dialogComponentProps.maximized = false
|
|
},
|
|
onAfterHide: () => {
|
|
closeDialog(dialog)
|
|
},
|
|
pt: merge(options.dialogComponentProps?.pt || {}, {
|
|
root: {
|
|
onMousedown: () => {
|
|
riseDialog(dialog)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
insertDialogByPriority(dialog)
|
|
activeKey.value = options.key
|
|
updateCloseOnEscapeStates()
|
|
|
|
return dialog
|
|
}
|
|
|
|
/**
|
|
* Ensures only the top-most dialog in the stack can be closed with the Escape key.
|
|
* This is necessary because PrimeVue Dialogs do not handle `closeOnEscape` prop
|
|
* correctly when multiple dialogs are open.
|
|
*/
|
|
function updateCloseOnEscapeStates() {
|
|
const topDialog = dialogStack.value.find((d) => d.key === activeKey.value)
|
|
const topClosable = topDialog?.dialogComponentProps.closable
|
|
|
|
dialogStack.value.forEach((dialog) => {
|
|
dialog.dialogComponentProps = {
|
|
...dialog.dialogComponentProps,
|
|
closeOnEscape: dialog === topDialog && !!topClosable
|
|
}
|
|
})
|
|
}
|
|
|
|
function showDialog(options: ShowDialogOptions) {
|
|
const dialogKey = options.key || genDialogKey()
|
|
|
|
let dialog = dialogStack.value.find((d) => d.key === dialogKey)
|
|
|
|
if (dialog) {
|
|
dialog.visible = true
|
|
riseDialog(dialog)
|
|
} else {
|
|
dialog = createDialog({ ...options, key: dialogKey })
|
|
}
|
|
return dialog
|
|
}
|
|
|
|
/**
|
|
* Shows a dialog from a third party extension.
|
|
* Explicitly keys extension dialogs with `extension-` prefix,
|
|
* to avoid conflicts & prevent use of internal dialogs (available via `dialogService`).
|
|
*/
|
|
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
|
|
const { key } = options
|
|
if (!key) {
|
|
console.error('Extension dialog key is required')
|
|
return
|
|
}
|
|
|
|
const extKey = key.startsWith('extension-') ? key : `extension-${key}`
|
|
|
|
const dialog = dialogStack.value.find((d) => d.key === extKey)
|
|
if (!dialog) return createDialog({ ...options, key: extKey })
|
|
|
|
dialog.visible = true
|
|
riseDialog(dialog)
|
|
return dialog
|
|
}
|
|
|
|
function isDialogOpen(key: string) {
|
|
return dialogStack.value.some((d) => d.key === key)
|
|
}
|
|
|
|
return {
|
|
dialogStack,
|
|
riseDialog,
|
|
showDialog,
|
|
closeDialog,
|
|
showExtensionDialog,
|
|
isDialogOpen,
|
|
activeKey
|
|
}
|
|
})
|