@inertiaui/modal-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/inertiaui-modal.js +5486 -0
- package/dist/inertiaui-modal.umd.cjs +43 -0
- package/package.json +51 -0
- package/src/CloseButton.jsx +26 -0
- package/src/Modal.jsx +137 -0
- package/src/ModalContent.jsx +48 -0
- package/src/ModalLink.jsx +132 -0
- package/src/ModalRenderer.jsx +34 -0
- package/src/ModalRoot.jsx +282 -0
- package/src/SlideoverContent.jsx +47 -0
- package/src/config.js +3 -0
- package/src/helpers.js +36 -0
- package/src/inertiauiModal.js +6 -0
package/src/Modal.jsx
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Dialog, Transition, TransitionChild } from '@headlessui/react'
|
|
2
|
+
import { useMemo, useState, forwardRef, useImperativeHandle, useEffect } from 'react'
|
|
3
|
+
import { getConfig, getConfigByType } from './config'
|
|
4
|
+
import { useModalIndex } from './ModalRenderer.jsx'
|
|
5
|
+
import { useModalStack } from './ModalRoot.jsx'
|
|
6
|
+
import ModalContent from './ModalContent'
|
|
7
|
+
import ModalRenderer from './ModalRenderer'
|
|
8
|
+
import SlideoverContent from './SlideoverContent'
|
|
9
|
+
|
|
10
|
+
const Modal = forwardRef(({ name, children, ...props }, ref) => {
|
|
11
|
+
const modalIndex = useModalIndex()
|
|
12
|
+
const { stack, registerLocalModal, removeLocalModal } = useModalStack()
|
|
13
|
+
|
|
14
|
+
const [localModalContext, setLocalModalContext] = useState(null)
|
|
15
|
+
const modalContext = useMemo(() => (name ? localModalContext : stack[modalIndex]))
|
|
16
|
+
|
|
17
|
+
const modalPropsSlideover = useMemo(() => modalContext?.modalProps.slideover ?? props.slideover ?? getConfig('type') === 'slideover', [props.slideover])
|
|
18
|
+
|
|
19
|
+
const modalProps = useMemo(
|
|
20
|
+
() => ({
|
|
21
|
+
slideover: modalPropsSlideover,
|
|
22
|
+
closeButton: props.closeButton ?? getConfigByType(modalPropsSlideover, 'closeButton'),
|
|
23
|
+
closeExplicitly: props.closeExplicitly ?? getConfigByType(modalPropsSlideover, 'closeExplicitly'),
|
|
24
|
+
maxWidth: props.maxWidth ?? getConfigByType(modalPropsSlideover, 'maxWidth'),
|
|
25
|
+
paddingClasses: props.paddingClasses ?? getConfigByType(modalPropsSlideover, 'paddingClasses'),
|
|
26
|
+
panelClasses: props.panelClasses ?? getConfigByType(modalPropsSlideover, 'panelClasses'),
|
|
27
|
+
position: props.position ?? getConfigByType(modalPropsSlideover, 'position'),
|
|
28
|
+
...modalContext?.modalProps,
|
|
29
|
+
}),
|
|
30
|
+
[props, modalContext?.modalProps],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const Content = useMemo(() => (modalProps.slideover ? SlideoverContent : ModalContent), [modalProps.slideover])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (name) {
|
|
37
|
+
let removeListeners = null
|
|
38
|
+
|
|
39
|
+
registerLocalModal(name, (localContext) => {
|
|
40
|
+
removeListeners = localContext.registerEventListenersFromProps(props)
|
|
41
|
+
setLocalModalContext(localContext)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
removeListeners?.()
|
|
46
|
+
removeListeners = null
|
|
47
|
+
removeLocalModal(name)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return modalContext.registerEventListenersFromProps(props)
|
|
52
|
+
}, [name])
|
|
53
|
+
|
|
54
|
+
useImperativeHandle(
|
|
55
|
+
ref,
|
|
56
|
+
() => ({
|
|
57
|
+
close: () => modalContext.close(),
|
|
58
|
+
emit: (...args) => modalContext.emit(...args),
|
|
59
|
+
getChildModal: () => modalContext.getChildModal(),
|
|
60
|
+
getParentModal: () => modalContext.getParentModal(),
|
|
61
|
+
modalContext: modalContext,
|
|
62
|
+
reload: () => modalContext.reload(),
|
|
63
|
+
}),
|
|
64
|
+
[modalContext],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const closeDialog = () => {
|
|
68
|
+
if (!modalProps.closeExplicitly) {
|
|
69
|
+
modalContext.close()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
modalContext && (
|
|
75
|
+
<Transition
|
|
76
|
+
appear={true}
|
|
77
|
+
show={modalContext.open ?? false}
|
|
78
|
+
>
|
|
79
|
+
<Dialog
|
|
80
|
+
as="div"
|
|
81
|
+
className="im-dialog relative z-20"
|
|
82
|
+
onClose={closeDialog}
|
|
83
|
+
data-inertiaui-modal-id={modalContext.id}
|
|
84
|
+
data-inertiaui-modal-index={modalContext.index}
|
|
85
|
+
>
|
|
86
|
+
{/* Only transition the backdrop for the first modal in the stack */}
|
|
87
|
+
{modalContext.index === 0 ? (
|
|
88
|
+
<TransitionChild
|
|
89
|
+
enter="transition transform ease-in-out duration-300"
|
|
90
|
+
enterFrom="opacity-0"
|
|
91
|
+
enterTo="opacity-100"
|
|
92
|
+
leave="transition transform ease-in-out duration-300"
|
|
93
|
+
leaveFrom="opacity-100"
|
|
94
|
+
leaveTo="opacity-0"
|
|
95
|
+
>
|
|
96
|
+
{modalContext.onTopOfStack ? (
|
|
97
|
+
<div
|
|
98
|
+
className="im-backdrop fixed inset-0 z-30 bg-black/75"
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
/>
|
|
101
|
+
) : (
|
|
102
|
+
<div />
|
|
103
|
+
)}
|
|
104
|
+
</TransitionChild>
|
|
105
|
+
) : null}
|
|
106
|
+
|
|
107
|
+
{/* On multiple modals, only show a backdrop for the modal that is on top of the stack */}
|
|
108
|
+
{modalContext.index > 0 && modalContext.onTopOfStack ? <div className="im-backdrop fixed inset-0 z-30 bg-black/75" /> : null}
|
|
109
|
+
|
|
110
|
+
{/* The modal/slideover content itself */}
|
|
111
|
+
<Content
|
|
112
|
+
modalContext={modalContext}
|
|
113
|
+
modalProps={modalProps}
|
|
114
|
+
>
|
|
115
|
+
{typeof children === 'function'
|
|
116
|
+
? children({
|
|
117
|
+
close: modalContext.close,
|
|
118
|
+
emit: modalContext.emit,
|
|
119
|
+
getChildModal: modalContext.getChildModal,
|
|
120
|
+
getParentModal: modalContext.getParentModal,
|
|
121
|
+
modalContext,
|
|
122
|
+
modalProps,
|
|
123
|
+
reload: modalContext.reload,
|
|
124
|
+
})
|
|
125
|
+
: children}
|
|
126
|
+
</Content>
|
|
127
|
+
|
|
128
|
+
{/* Next modal in the stack */}
|
|
129
|
+
{stack[modalContext.index + 1] && <ModalRenderer index={modalContext.index + 1} />}
|
|
130
|
+
</Dialog>
|
|
131
|
+
</Transition>
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
Modal.displayName = 'Modal'
|
|
137
|
+
export default Modal
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TransitionChild, DialogPanel } from '@headlessui/react'
|
|
2
|
+
import CloseButton from './CloseButton'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
|
|
5
|
+
const ModalContent = ({ modalContext, modalProps, children }) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="im-modal-container fixed inset-0 z-40 overflow-y-auto p-4">
|
|
8
|
+
<div
|
|
9
|
+
className={clsx('im-modal-positioner flex min-h-full justify-center', {
|
|
10
|
+
'items-start': modalProps.position === 'top',
|
|
11
|
+
'items-center': modalProps.position === 'center',
|
|
12
|
+
'items-end': modalProps.position === 'bottom',
|
|
13
|
+
})}
|
|
14
|
+
>
|
|
15
|
+
<TransitionChild
|
|
16
|
+
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
17
|
+
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
18
|
+
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
19
|
+
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
20
|
+
afterLeave={modalContext.afterLeave}
|
|
21
|
+
className={clsx('im-modal-wrapper w-full transition duration-300 ease-in-out', modalContext.onTopOfStack ? '' : 'blur-sm', {
|
|
22
|
+
'sm:max-w-sm': modalProps.maxWidth === 'sm',
|
|
23
|
+
'sm:max-w-md': modalProps.maxWidth === 'md',
|
|
24
|
+
'sm:max-w-md md:max-w-lg': modalProps.maxWidth === 'lg',
|
|
25
|
+
'sm:max-w-md md:max-w-xl': modalProps.maxWidth === 'xl',
|
|
26
|
+
'sm:max-w-md md:max-w-xl lg:max-w-2xl': modalProps.maxWidth === '2xl',
|
|
27
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl': modalProps.maxWidth === '3xl',
|
|
28
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-4xl': modalProps.maxWidth === '4xl',
|
|
29
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl': modalProps.maxWidth === '5xl',
|
|
30
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-6xl': modalProps.maxWidth === '6xl',
|
|
31
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-7xl': modalProps.maxWidth === '7xl',
|
|
32
|
+
})}
|
|
33
|
+
>
|
|
34
|
+
<DialogPanel className={`im-modal-content relative ${modalProps.paddingClasses} ${modalProps.panelClasses}`}>
|
|
35
|
+
{modalProps.closeButton && (
|
|
36
|
+
<div className="absolute right-0 top-0 pr-3 pt-3">
|
|
37
|
+
<CloseButton onClick={modalContext.close} />
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
{typeof children === 'function' ? children({ modalContext, modalProps }) : children}
|
|
41
|
+
</DialogPanel>
|
|
42
|
+
</TransitionChild>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default ModalContent
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useCallback, useState, useEffect } from 'react'
|
|
2
|
+
import { useModalStack, modalPropNames } from './ModalRoot'
|
|
3
|
+
import { only, rejectNullValues } from './helpers'
|
|
4
|
+
|
|
5
|
+
const ModalLink = ({
|
|
6
|
+
href,
|
|
7
|
+
method = 'get',
|
|
8
|
+
data = {},
|
|
9
|
+
as: Component = 'a',
|
|
10
|
+
fragment = null,
|
|
11
|
+
headers = {},
|
|
12
|
+
queryStringArrayFormat = 'brackets',
|
|
13
|
+
onAfterLeave = null,
|
|
14
|
+
onBlur = null,
|
|
15
|
+
onClose = null,
|
|
16
|
+
onError = null,
|
|
17
|
+
onFocus = null,
|
|
18
|
+
onStart = null,
|
|
19
|
+
onSuccess = null,
|
|
20
|
+
children,
|
|
21
|
+
...props
|
|
22
|
+
}) => {
|
|
23
|
+
const [loading, setLoading] = useState(false)
|
|
24
|
+
const [modalContext, setModalContext] = useState(null)
|
|
25
|
+
const { stack, visit } = useModalStack()
|
|
26
|
+
|
|
27
|
+
// Separate standard props from custom event handlers
|
|
28
|
+
const standardProps = {}
|
|
29
|
+
const customEvents = {}
|
|
30
|
+
|
|
31
|
+
Object.keys(props).forEach((key) => {
|
|
32
|
+
if (modalPropNames.includes(key)) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (key.startsWith('on') && typeof props[key] === 'function') {
|
|
37
|
+
if (key.toLowerCase() in window) {
|
|
38
|
+
standardProps[key] = props[key]
|
|
39
|
+
} else {
|
|
40
|
+
customEvents[key] = props[key]
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
standardProps[key] = props[key]
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (fragment && window.location.hash === `#${fragment}`) {
|
|
49
|
+
handle()
|
|
50
|
+
}
|
|
51
|
+
}, [fragment])
|
|
52
|
+
|
|
53
|
+
const [isBlurred, setIsBlurred] = useState(false)
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!modalContext) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (modalContext.onTopOfStack && isBlurred) {
|
|
61
|
+
onFocus?.()
|
|
62
|
+
} else if (!modalContext.onTopOfStack && !isBlurred) {
|
|
63
|
+
onBlur?.()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setIsBlurred(!modalContext.onTopOfStack)
|
|
67
|
+
}, [stack])
|
|
68
|
+
|
|
69
|
+
const onCloseCallback = useCallback(
|
|
70
|
+
(index) => {
|
|
71
|
+
if (fragment && index === 0) {
|
|
72
|
+
window.location.hash = ''
|
|
73
|
+
}
|
|
74
|
+
onClose?.()
|
|
75
|
+
},
|
|
76
|
+
[onClose, fragment],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const onAfterLeaveCallback = useCallback(() => {
|
|
80
|
+
setModalContext(null)
|
|
81
|
+
onAfterLeave?.()
|
|
82
|
+
}, [onAfterLeave])
|
|
83
|
+
|
|
84
|
+
const handle = useCallback(
|
|
85
|
+
(e) => {
|
|
86
|
+
e?.preventDefault()
|
|
87
|
+
if (loading) return
|
|
88
|
+
|
|
89
|
+
if (!href.startsWith('#')) {
|
|
90
|
+
setLoading(true)
|
|
91
|
+
onStart?.()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
visit(
|
|
95
|
+
href,
|
|
96
|
+
method,
|
|
97
|
+
data,
|
|
98
|
+
headers,
|
|
99
|
+
rejectNullValues(only(props, modalPropNames)),
|
|
100
|
+
() => onCloseCallback(stack.length),
|
|
101
|
+
onAfterLeaveCallback,
|
|
102
|
+
queryStringArrayFormat,
|
|
103
|
+
)
|
|
104
|
+
.then((newModalContext) => {
|
|
105
|
+
setModalContext(newModalContext)
|
|
106
|
+
if (fragment && newModalContext.index === 0) {
|
|
107
|
+
window.location.hash = fragment
|
|
108
|
+
}
|
|
109
|
+
newModalContext.registerEventListenersFromProps(customEvents)
|
|
110
|
+
onSuccess?.()
|
|
111
|
+
})
|
|
112
|
+
.catch((error) => {
|
|
113
|
+
console.error(error)
|
|
114
|
+
onError?.(error)
|
|
115
|
+
})
|
|
116
|
+
.finally(() => setLoading(false))
|
|
117
|
+
},
|
|
118
|
+
[href, method, data, headers, queryStringArrayFormat, props, onCloseCallback, onAfterLeaveCallback],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Component
|
|
123
|
+
{...standardProps}
|
|
124
|
+
href={href}
|
|
125
|
+
onClick={handle}
|
|
126
|
+
>
|
|
127
|
+
{typeof children === 'function' ? children({ loading }) : children}
|
|
128
|
+
</Component>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default ModalLink
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useModalStack } from './ModalRoot'
|
|
3
|
+
|
|
4
|
+
const ModalIndexContext = React.createContext(null)
|
|
5
|
+
ModalIndexContext.displayName = 'ModalIndexContext'
|
|
6
|
+
|
|
7
|
+
export const useModalIndex = () => {
|
|
8
|
+
const context = React.useContext(ModalIndexContext)
|
|
9
|
+
if (context === undefined) {
|
|
10
|
+
throw new Error('useModalIndex must be used within a ModalIndexProvider')
|
|
11
|
+
}
|
|
12
|
+
return context
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ModalRenderer = ({ index }) => {
|
|
16
|
+
const { stack } = useModalStack()
|
|
17
|
+
|
|
18
|
+
const modalContext = stack[index]
|
|
19
|
+
|
|
20
|
+
if (!modalContext || !modalContext.component) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ModalIndexContext.Provider value={index}>
|
|
26
|
+
<modalContext.component
|
|
27
|
+
{...modalContext.componentProps}
|
|
28
|
+
onModalEvent={(...args) => modalContext.emit(...args)}
|
|
29
|
+
/>
|
|
30
|
+
</ModalIndexContext.Provider>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default ModalRenderer
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { default as Axios } from 'axios'
|
|
3
|
+
import { except, only, resolveInteriaPageFromRouter } from './helpers'
|
|
4
|
+
import { router } from '@inertiajs/react'
|
|
5
|
+
import { mergeDataIntoQueryString } from '@inertiajs/core'
|
|
6
|
+
import { createContext, useContext } from 'react'
|
|
7
|
+
import ModalRenderer from './ModalRenderer'
|
|
8
|
+
|
|
9
|
+
const ModalStackContext = createContext(null)
|
|
10
|
+
ModalStackContext.displayName = 'ModalStackContext'
|
|
11
|
+
|
|
12
|
+
export const ModalStackProvider = ({ children }) => {
|
|
13
|
+
const [stack, setStack] = useState([])
|
|
14
|
+
const [localModals, setLocalModals] = useState({})
|
|
15
|
+
|
|
16
|
+
class Modal {
|
|
17
|
+
constructor(component, response, modalProps, onClose, afterLeave) {
|
|
18
|
+
this.id = Modal.generateId()
|
|
19
|
+
this.open = true
|
|
20
|
+
this.listeners = {}
|
|
21
|
+
|
|
22
|
+
this.component = component
|
|
23
|
+
this.componentProps = response.props
|
|
24
|
+
this.response = response
|
|
25
|
+
this.modalProps = modalProps
|
|
26
|
+
this.onCloseCallback = onClose
|
|
27
|
+
this.afterLeaveCallback = afterLeave
|
|
28
|
+
|
|
29
|
+
this.index = -1 // Will be set when added to the stack
|
|
30
|
+
this.getParentModal = () => null // Will be set in push()
|
|
31
|
+
this.getChildModal = () => null // Will be set in push()
|
|
32
|
+
this.onTopOfStack = true // Will be updated in push()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static generateId() {
|
|
36
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
37
|
+
return `inertiaui_modal_${crypto.randomUUID()}`
|
|
38
|
+
}
|
|
39
|
+
// Fallback for environments where crypto.randomUUID is not available
|
|
40
|
+
return `inertiaui_modal_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
updateStack = (withStack) => {
|
|
44
|
+
setStack((prevStack) => {
|
|
45
|
+
const newStack = withStack([...prevStack])
|
|
46
|
+
|
|
47
|
+
newStack.forEach((modal, index) => {
|
|
48
|
+
newStack[index].index = index
|
|
49
|
+
newStack[index].onTopOfStack = index === newStack.length - 1
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return newStack
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
close = () => {
|
|
57
|
+
this.updateStack((prevStack) =>
|
|
58
|
+
prevStack.map((modal) => {
|
|
59
|
+
if (modal.id === this.id) {
|
|
60
|
+
Object.keys(modal.listeners).forEach((event) => {
|
|
61
|
+
modal.off(event)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
modal.open = false
|
|
65
|
+
modal.onCloseCallback?.()
|
|
66
|
+
}
|
|
67
|
+
return modal
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
afterLeave = () => {
|
|
73
|
+
this.updateStack((prevStack) =>
|
|
74
|
+
prevStack.filter((modal) => {
|
|
75
|
+
if (modal.id !== this.id) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
modal.afterLeaveCallback?.()
|
|
80
|
+
return false
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
on = (event, callback) => {
|
|
86
|
+
this.listeners[event] = this.listeners[event] ?? []
|
|
87
|
+
this.listeners[event].push(callback)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
off = (event, callback) => {
|
|
91
|
+
if (callback) {
|
|
92
|
+
this.listeners[event] = this.listeners[event]?.filter((cb) => cb !== callback) ?? []
|
|
93
|
+
} else {
|
|
94
|
+
delete this.listeners[event]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
emit = (event, ...args) => {
|
|
99
|
+
console.log('Emitting', event, 'with args', args)
|
|
100
|
+
this.listeners[event]?.forEach((callback) => callback(...args))
|
|
101
|
+
return 'OK'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
registerEventListenersFromProps = (props) => {
|
|
105
|
+
const unsubscribers = []
|
|
106
|
+
|
|
107
|
+
Object.keys(props)
|
|
108
|
+
.filter((key) => key.startsWith('on'))
|
|
109
|
+
.forEach((key) => {
|
|
110
|
+
// e.g. onRefreshKey -> refresh-key
|
|
111
|
+
const snakeCaseKey = key
|
|
112
|
+
.replace(/^on/, '')
|
|
113
|
+
.replace(/^./, (firstLetter) => firstLetter.toLowerCase())
|
|
114
|
+
.replace(/([A-Z])/g, '-$1')
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
|
|
117
|
+
this.on(snakeCaseKey, props[key])
|
|
118
|
+
unsubscribers.push(() => this.off(snakeCaseKey, props[key]))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return () => unsubscribers.forEach((unsub) => unsub())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
reload = (options = {}) => {
|
|
125
|
+
let keys = Object.keys(this.response.props)
|
|
126
|
+
|
|
127
|
+
if (options.only) {
|
|
128
|
+
keys = only(keys, options.only)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (options.except) {
|
|
132
|
+
keys = except(keys, options.except)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Axios.get(this.response.url, {
|
|
136
|
+
headers: {
|
|
137
|
+
Accept: 'text/html, application/xhtml+xml',
|
|
138
|
+
'X-Inertia': true,
|
|
139
|
+
'X-Inertia-Partial-Component': this.response.component,
|
|
140
|
+
'X-Inertia-Version': this.response.version,
|
|
141
|
+
'X-Inertia-Partial-Data': keys.join(','),
|
|
142
|
+
},
|
|
143
|
+
}).then((response) => {
|
|
144
|
+
Object.assign(this.componentProps, response.data.props)
|
|
145
|
+
setStack((prevStack) => [...prevStack]) // Trigger re-render
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const push = (component, response, modalProps, onClose, afterLeave) => {
|
|
151
|
+
const newModal = new Modal(component, response, modalProps, onClose, afterLeave)
|
|
152
|
+
|
|
153
|
+
setStack((prevStack) => {
|
|
154
|
+
const updatedStack = [...prevStack, newModal]
|
|
155
|
+
|
|
156
|
+
// Set index and update onTopOfStack for all modals
|
|
157
|
+
updatedStack.forEach((modal, index) => {
|
|
158
|
+
modal.index = index
|
|
159
|
+
modal.onTopOfStack = index === updatedStack.length - 1
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Set getParentModal and getChildModal
|
|
163
|
+
updatedStack.forEach((modal, index) => {
|
|
164
|
+
modal.getParentModal = () => (index > 0 ? updatedStack[index - 1] : null)
|
|
165
|
+
modal.getChildModal = () => (index < updatedStack.length - 1 ? updatedStack[index + 1] : null)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return updatedStack
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return newModal
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function pushLocalModal(name, modalProps, onClose, afterLeave) {
|
|
175
|
+
if (!localModals[name]) {
|
|
176
|
+
throw new Error(`The local modal "${name}" has not been registered.`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const modal = push(null, {}, modalProps, onClose, afterLeave)
|
|
180
|
+
modal.name = name
|
|
181
|
+
localModals[name].callback(modal)
|
|
182
|
+
return modal
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const visitModal = (url, options = {}) => {
|
|
186
|
+
return visit(
|
|
187
|
+
url,
|
|
188
|
+
options.method ?? 'get',
|
|
189
|
+
options.data ?? {},
|
|
190
|
+
options.headers ?? {},
|
|
191
|
+
options.config ?? {},
|
|
192
|
+
options.onClose,
|
|
193
|
+
options.onAfterLeave,
|
|
194
|
+
options.queryStringArrayFormat ?? 'brackets',
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const visit = (href, method, payload = {}, headers = {}, modalProps = {}, onClose = null, onAfterLeave = null, queryStringArrayFormat = 'brackets') => {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
if (href.startsWith('#')) {
|
|
201
|
+
const localModal = pushLocalModal(href.substring(1), modalProps, onClose, onAfterLeave)
|
|
202
|
+
resolve(localModal)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [url, data] = mergeDataIntoQueryString(method, href || '', payload, queryStringArrayFormat)
|
|
207
|
+
|
|
208
|
+
resolveInteriaPageFromRouter().then((inertiaPage) => {
|
|
209
|
+
Axios({
|
|
210
|
+
url,
|
|
211
|
+
method,
|
|
212
|
+
data,
|
|
213
|
+
headers: {
|
|
214
|
+
...headers,
|
|
215
|
+
Accept: 'text/html, application/xhtml+xml',
|
|
216
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
217
|
+
'X-Inertia': true,
|
|
218
|
+
'X-Inertia-Version': inertiaPage.version,
|
|
219
|
+
'X-InertiaUI-Modal': true,
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
.then((response) => {
|
|
223
|
+
router.resolveComponent(response.data.component).then((component) => {
|
|
224
|
+
resolve(push(component, response.data, modalProps, onClose, onAfterLeave))
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
.catch((error) => {
|
|
228
|
+
reject(error)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const registerLocalModal = (name, callback) => {
|
|
235
|
+
setLocalModals((prevLocalModals) => ({
|
|
236
|
+
...prevLocalModals,
|
|
237
|
+
[name]: { name, callback },
|
|
238
|
+
}))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const removeLocalModal = (name) => {
|
|
242
|
+
setLocalModals((prevLocalModals) => {
|
|
243
|
+
const newLocalModals = { ...prevLocalModals }
|
|
244
|
+
delete newLocalModals[name]
|
|
245
|
+
return newLocalModals
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const value = {
|
|
250
|
+
stack,
|
|
251
|
+
localModals,
|
|
252
|
+
push,
|
|
253
|
+
reset: () => setStack([]),
|
|
254
|
+
visit,
|
|
255
|
+
visitModal,
|
|
256
|
+
registerLocalModal,
|
|
257
|
+
removeLocalModal,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return <ModalStackContext.Provider value={value}>{children}</ModalStackContext.Provider>
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const useModalStack = () => {
|
|
264
|
+
const context = useContext(ModalStackContext)
|
|
265
|
+
if (context === null) {
|
|
266
|
+
throw new Error('useModalStack must be used within a ModalStackProvider')
|
|
267
|
+
}
|
|
268
|
+
return context
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const modalPropNames = ['closeButton', 'closeExplicitly', 'maxWidth', 'paddingClasses', 'panelClasses', 'position', 'slideover']
|
|
272
|
+
|
|
273
|
+
export const ModalRoot = ({ children }) => {
|
|
274
|
+
const stack = useContext(ModalStackContext).stack
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<>
|
|
278
|
+
{children}
|
|
279
|
+
{stack.length > 0 && <ModalRenderer index={0} />}
|
|
280
|
+
</>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TransitionChild, DialogPanel } from '@headlessui/react'
|
|
2
|
+
import CloseButton from './CloseButton'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
|
|
5
|
+
const SlideoverContent = ({ modalContext, modalProps, children }) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="im-slideover-container fixed inset-0 z-40 overflow-y-auto overflow-x-hidden">
|
|
8
|
+
<div
|
|
9
|
+
className={clsx('im-slideover-positioner flex min-h-full items-center', {
|
|
10
|
+
'justify-start': modalProps.position === 'left',
|
|
11
|
+
'justify-end': modalProps.position === 'right',
|
|
12
|
+
})}
|
|
13
|
+
>
|
|
14
|
+
<TransitionChild
|
|
15
|
+
enterFrom={`opacity-0 ${modalProps.position === 'left' ? '-translate-x-full' : 'translate-x-full'}`}
|
|
16
|
+
enterTo="opacity-100 translate-x-0"
|
|
17
|
+
leaveFrom="opacity-100 translate-x-0"
|
|
18
|
+
leaveTo={`opacity-0 ${modalProps.position === 'left' ? '-translate-x-full' : 'translate-x-full'}`}
|
|
19
|
+
afterLeave={modalContext.afterLeave}
|
|
20
|
+
className={clsx('im-slideover-wrapper w-full transition duration-300 ease-in-out', modalContext.onTopOfStack ? '' : 'blur-sm', {
|
|
21
|
+
'sm:max-w-sm': modalProps.maxWidth === 'sm',
|
|
22
|
+
'sm:max-w-md': modalProps.maxWidth === 'md',
|
|
23
|
+
'sm:max-w-md md:max-w-lg': modalProps.maxWidth === 'lg',
|
|
24
|
+
'sm:max-w-md md:max-w-xl': modalProps.maxWidth === 'xl',
|
|
25
|
+
'sm:max-w-md md:max-w-xl lg:max-w-2xl': modalProps.maxWidth === '2xl',
|
|
26
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl': modalProps.maxWidth === '3xl',
|
|
27
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-4xl': modalProps.maxWidth === '4xl',
|
|
28
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl': modalProps.maxWidth === '5xl',
|
|
29
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-6xl': modalProps.maxWidth === '6xl',
|
|
30
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-7xl': modalProps.maxWidth === '7xl',
|
|
31
|
+
})}
|
|
32
|
+
>
|
|
33
|
+
<DialogPanel className={`im-slideover-content relative ${modalProps.paddingClasses} ${modalProps.panelClasses}`}>
|
|
34
|
+
{modalProps.closeButton && (
|
|
35
|
+
<div className="absolute right-0 top-0 pr-3 pt-3">
|
|
36
|
+
<CloseButton onClick={modalContext.close} />
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
{typeof children === 'function' ? children({ modalContext, modalProps }) : children}
|
|
40
|
+
</DialogPanel>
|
|
41
|
+
</TransitionChild>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default SlideoverContent
|
package/src/config.js
ADDED