@inertiaui/modal-react 0.1.3 → 0.5.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/src/ModalRoot.jsx CHANGED
@@ -1,22 +1,80 @@
1
- import { useState } from 'react'
1
+ import { createElement, useEffect, useState } from 'react'
2
2
  import { default as Axios } from 'axios'
3
- import { except, only, resolveInteriaPageFromRouter } from './helpers'
3
+ import { except, only } from './helpers'
4
4
  import { router } from '@inertiajs/react'
5
5
  import { mergeDataIntoQueryString } from '@inertiajs/core'
6
6
  import { createContext, useContext } from 'react'
7
7
  import ModalRenderer from './ModalRenderer'
8
+ import { waitFor } from './helpers'
8
9
 
9
10
  const ModalStackContext = createContext(null)
10
11
  ModalStackContext.displayName = 'ModalStackContext'
11
12
 
13
+ let pageVersion = null
14
+ let resolveComponent = null
15
+ let baseUrl = null
16
+ let newModalOnBase = null
17
+ let localStackCopy = []
18
+
12
19
  export const ModalStackProvider = ({ children }) => {
13
20
  const [stack, setStack] = useState([])
14
21
  const [localModals, setLocalModals] = useState({})
15
22
 
23
+ const updateStack = (withStack) => {
24
+ setStack((prevStack) => {
25
+ const newStack = withStack([...prevStack])
26
+
27
+ const isOnTopOfStack = (modalId) => {
28
+ if (newStack.length < 2) {
29
+ return true
30
+ }
31
+
32
+ return (
33
+ newStack
34
+ .map((modal) => ({ id: modal.id, shouldRender: modal.shouldRender }))
35
+ .reverse()
36
+ .find((modal) => modal.shouldRender)?.id === modalId
37
+ )
38
+ }
39
+
40
+ newStack.forEach((modal, index) => {
41
+ newStack[index].onTopOfStack = isOnTopOfStack(modal.id)
42
+ newStack[index].getParentModal = () => {
43
+ if (index < 1) {
44
+ // This is the first modal in the stack
45
+ return null
46
+ }
47
+
48
+ // Find the first open modal before this one
49
+ return newStack
50
+ .slice(0, index)
51
+ .reverse()
52
+ .find((modal) => modal.isOpen)
53
+ }
54
+ newStack[index].getChildModal = () => {
55
+ if (index === newStack.length - 1) {
56
+ // This is the last modal in the stack
57
+ return null
58
+ }
59
+
60
+ // Find the first open modal after this one
61
+ return newStack.slice(index + 1).find((modal) => modal.isOpen)
62
+ }
63
+ })
64
+
65
+ return newStack
66
+ })
67
+ }
68
+
69
+ useEffect(() => {
70
+ localStackCopy = stack
71
+ }, [stack])
72
+
16
73
  class Modal {
17
74
  constructor(component, response, modalProps, onClose, afterLeave) {
18
75
  this.id = Modal.generateId()
19
- this.open = true
76
+ this.isOpen = false
77
+ this.shouldRender = false
20
78
  this.listeners = {}
21
79
 
22
80
  this.component = component
@@ -40,36 +98,45 @@ export const ModalStackProvider = ({ children }) => {
40
98
  return `inertiaui_modal_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`
41
99
  }
42
100
 
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
- })
101
+ update = (modalProps, onClose, afterLeave) => {
102
+ updateStack((prevStack) =>
103
+ prevStack.map((modal) => {
104
+ if (modal.id === this.id) {
105
+ modal.modalProps = modalProps
106
+ modal.onCloseCallback = onClose
107
+ modal.afterLeaveCallback = afterLeave
108
+ }
109
+ return modal
110
+ }),
111
+ )
112
+ }
51
113
 
52
- return newStack
53
- })
114
+ show = () => {
115
+ updateStack((prevStack) =>
116
+ prevStack.map((modal) => {
117
+ if (modal.id === this.id && !modal.isOpen) {
118
+ modal.isOpen = true
119
+ modal.shouldRender = true
120
+ }
121
+ return modal
122
+ }),
123
+ )
54
124
  }
55
125
 
56
126
  setOpen = (open) => {
57
- if (open) {
58
- this.open = true
59
- } else {
60
- this.close()
61
- }
127
+ open ? this.show() : this.close()
62
128
  }
63
129
 
64
130
  close = () => {
65
- this.updateStack((prevStack) =>
131
+ console.log('Closing', this.id)
132
+ updateStack((prevStack) =>
66
133
  prevStack.map((modal) => {
67
- if (modal.id === this.id) {
134
+ if (modal.id === this.id && modal.isOpen) {
68
135
  Object.keys(modal.listeners).forEach((event) => {
69
136
  modal.off(event)
70
137
  })
71
138
 
72
- modal.open = false
139
+ modal.isOpen = false
73
140
  modal.onCloseCallback?.()
74
141
  }
75
142
  return modal
@@ -78,20 +145,23 @@ export const ModalStackProvider = ({ children }) => {
78
145
  }
79
146
 
80
147
  afterLeave = () => {
81
- if (this.open) {
148
+ console.log('After leave', this.id)
149
+ if (this.isOpen) {
82
150
  return
83
151
  }
84
152
 
85
- this.updateStack((prevStack) =>
86
- prevStack.filter((modal) => {
87
- if (modal.id !== this.id) {
88
- return true
153
+ updateStack((prevStack) => {
154
+ const updatedStack = prevStack.map((modal) => {
155
+ if (modal.id === this.id && !modal.isOpen) {
156
+ modal.shouldRender = false
157
+ modal.afterLeaveCallback?.()
158
+ modal.afterLeaveCallback = null
89
159
  }
160
+ return modal
161
+ })
90
162
 
91
- modal.afterLeaveCallback?.()
92
- return false
93
- }),
94
- )
163
+ return this.index === 0 ? [] : updatedStack
164
+ })
95
165
  }
96
166
 
97
167
  on = (event, callback) => {
@@ -108,9 +178,7 @@ export const ModalStackProvider = ({ children }) => {
108
178
  }
109
179
 
110
180
  emit = (event, ...args) => {
111
- console.log('Emitting', event, 'with args', args)
112
181
  this.listeners[event]?.forEach((callback) => callback(...args))
113
- return 'OK'
114
182
  }
115
183
 
116
184
  registerEventListenersFromProps = (props) => {
@@ -144,6 +212,10 @@ export const ModalStackProvider = ({ children }) => {
144
212
  keys = except(keys, options.except)
145
213
  }
146
214
 
215
+ if (!this.response?.url) {
216
+ return
217
+ }
218
+
147
219
  Axios.get(this.response.url, {
148
220
  headers: {
149
221
  Accept: 'text/html, application/xhtml+xml',
@@ -151,34 +223,27 @@ export const ModalStackProvider = ({ children }) => {
151
223
  'X-Inertia-Partial-Component': this.response.component,
152
224
  'X-Inertia-Version': this.response.version,
153
225
  'X-Inertia-Partial-Data': keys.join(','),
226
+ 'X-InertiaUI-Modal': true,
227
+ 'X-InertiaUI-Modal-Use-Router': 0,
154
228
  },
155
229
  }).then((response) => {
156
230
  Object.assign(this.componentProps, response.data.props)
157
- setStack((prevStack) => [...prevStack]) // Trigger re-render
231
+ updateStack((prevStack) => prevStack) // Trigger re-render
158
232
  })
159
233
  }
160
234
  }
161
235
 
236
+ const pushFromResponseData = (responseData, modalProps = {}, onClose = null, onAfterLeave = null) => {
237
+ return resolveComponent(responseData.component).then((component) => push(component, responseData, modalProps, onClose, onAfterLeave))
238
+ }
239
+
162
240
  const push = (component, response, modalProps, onClose, afterLeave) => {
163
241
  const newModal = new Modal(component, response, modalProps, onClose, afterLeave)
242
+ newModal.index = stack.length
164
243
 
165
- setStack((prevStack) => {
166
- const updatedStack = [...prevStack, newModal]
244
+ updateStack((prevStack) => [...prevStack, newModal])
167
245
 
168
- // Set index and update onTopOfStack for all modals
169
- updatedStack.forEach((modal, index) => {
170
- modal.index = index
171
- modal.onTopOfStack = index === updatedStack.length - 1
172
- })
173
-
174
- // Set getParentModal and getChildModal
175
- updatedStack.forEach((modal, index) => {
176
- modal.getParentModal = () => (index > 0 ? updatedStack[index - 1] : null)
177
- modal.getChildModal = () => (index < updatedStack.length - 1 ? updatedStack[index + 1] : null)
178
- })
179
-
180
- return updatedStack
181
- })
246
+ newModal.show()
182
247
 
183
248
  return newModal
184
249
  }
@@ -194,8 +259,8 @@ export const ModalStackProvider = ({ children }) => {
194
259
  return modal
195
260
  }
196
261
 
197
- const visitModal = (url, options = {}) => {
198
- return visit(
262
+ const visitModal = (url, options = {}) =>
263
+ visit(
199
264
  url,
200
265
  options.method ?? 'get',
201
266
  options.data ?? {},
@@ -205,41 +270,87 @@ export const ModalStackProvider = ({ children }) => {
205
270
  options.onAfterLeave,
206
271
  options.queryStringArrayFormat ?? 'brackets',
207
272
  )
208
- }
209
273
 
210
- const visit = (href, method, payload = {}, headers = {}, modalProps = {}, onClose = null, onAfterLeave = null, queryStringArrayFormat = 'brackets') => {
274
+ const visit = (
275
+ href,
276
+ method,
277
+ payload = {},
278
+ headers = {},
279
+ modalProps = {},
280
+ onClose = null,
281
+ onAfterLeave = null,
282
+ queryStringArrayFormat = 'brackets',
283
+ useBrowserHistory = false,
284
+ ) => {
211
285
  return new Promise((resolve, reject) => {
212
286
  if (href.startsWith('#')) {
213
- const localModal = pushLocalModal(href.substring(1), modalProps, onClose, onAfterLeave)
214
- resolve(localModal)
287
+ resolve(pushLocalModal(href.substring(1), modalProps, onClose, onAfterLeave))
215
288
  return
216
289
  }
217
290
 
218
291
  const [url, data] = mergeDataIntoQueryString(method, href || '', payload, queryStringArrayFormat)
219
292
 
220
- resolveInteriaPageFromRouter().then((inertiaPage) => {
221
- Axios({
222
- url,
293
+ let useInertiaRouter = useBrowserHistory && stack.length === 0
294
+
295
+ if (stack.length === 0) {
296
+ baseUrl = typeof window !== 'undefined' ? window.location.href : ''
297
+ }
298
+
299
+ headers = {
300
+ ...headers,
301
+ Accept: 'text/html, application/xhtml+xml',
302
+ 'X-Requested-With': 'XMLHttpRequest',
303
+ 'X-Inertia': true,
304
+ 'X-Inertia-Version': pageVersion,
305
+ 'X-InertiaUI-Modal': true,
306
+ 'X-InertiaUI-Modal-Use-Router': useInertiaRouter ? 1 : 0,
307
+ }
308
+
309
+ if (useInertiaRouter) {
310
+ // Pushing the modal to the stack will be handled by the ModalRoot...
311
+ return router.visit(url, {
223
312
  method,
224
313
  data,
225
- headers: {
226
- ...headers,
227
- Accept: 'text/html, application/xhtml+xml',
228
- 'X-Requested-With': 'XMLHttpRequest',
229
- 'X-Inertia': true,
230
- 'X-Inertia-Version': inertiaPage.version,
231
- 'X-InertiaUI-Modal': true,
314
+ headers,
315
+ preserveScroll: true,
316
+ preserveState: true,
317
+ onError: reject,
318
+ onFinish: () => {
319
+ waitFor(() => newModalOnBase).then((modal) => {
320
+ const originalOnClose = modal.onCloseCallback
321
+ const originalAfterLeave = modal.afterLeaveCallback
322
+
323
+ modal.update(
324
+ modalProps,
325
+ () => {
326
+ onClose?.()
327
+ originalOnClose?.()
328
+ },
329
+ () => {
330
+ onAfterLeave?.()
331
+ originalAfterLeave?.()
332
+ },
333
+ )
334
+
335
+ resolve(modal)
336
+ newModalOnBase = null
337
+ })
232
338
  },
233
339
  })
234
- .then((response) => {
235
- router.resolveComponent(response.data.component).then((component) => {
236
- resolve(push(component, response.data, modalProps, onClose, onAfterLeave))
237
- })
238
- })
239
- .catch((error) => {
240
- reject(error)
241
- })
340
+ }
341
+
342
+ //
343
+
344
+ Axios({
345
+ url,
346
+ method,
347
+ data,
348
+ headers,
242
349
  })
350
+ .then((response) => resolve(pushFromResponseData(response.data, modalProps, onClose, onAfterLeave)))
351
+ .catch((error) => {
352
+ reject(error)
353
+ })
243
354
  })
244
355
  }
245
356
 
@@ -262,7 +373,12 @@ export const ModalStackProvider = ({ children }) => {
262
373
  stack,
263
374
  localModals,
264
375
  push,
265
- reset: () => setStack([]),
376
+ pushFromResponseData,
377
+ closeAll: () => {
378
+ console.log('Closing all modals', { stack, localStackCopy })
379
+ localStackCopy.reverse().forEach((modal) => modal.close())
380
+ },
381
+ reset: () => updateStack(() => []),
266
382
  visit,
267
383
  visitModal,
268
384
  registerLocalModal,
@@ -282,13 +398,107 @@ export const useModalStack = () => {
282
398
 
283
399
  export const modalPropNames = ['closeButton', 'closeExplicitly', 'maxWidth', 'paddingClasses', 'panelClasses', 'position', 'slideover']
284
400
 
401
+ export const renderApp = (App, pageProps) => {
402
+ if (pageProps.initialPage) {
403
+ pageVersion = pageProps.initialPage.version
404
+ }
405
+
406
+ if (pageProps.resolveComponent) {
407
+ resolveComponent = pageProps.resolveComponent
408
+ }
409
+
410
+ const renderInertiaApp = ({ Component, props, key }) => {
411
+ const renderComponent = () => {
412
+ const child = createElement(Component, { key, ...props })
413
+
414
+ if (typeof Component.layout === 'function') {
415
+ return Component.layout(child)
416
+ }
417
+
418
+ if (Array.isArray(Component.layout)) {
419
+ const layouts = Component.layout
420
+ .concat(child)
421
+ .reverse()
422
+ .reduce((children, Layout) => createElement(Layout, props, children))
423
+
424
+ return layouts
425
+ }
426
+
427
+ return child
428
+ }
429
+
430
+ return (
431
+ <>
432
+ {renderComponent()}
433
+ <ModalRoot />
434
+ </>
435
+ )
436
+ }
437
+
438
+ return (
439
+ <ModalStackProvider>
440
+ <App {...pageProps}>{renderInertiaApp}</App>
441
+ </ModalStackProvider>
442
+ )
443
+ }
444
+
285
445
  export const ModalRoot = ({ children }) => {
286
- const stack = useContext(ModalStackContext).stack
446
+ const context = useContext(ModalStackContext)
447
+
448
+ let isNavigating = false
449
+ let previousModalOnBase = false
450
+
451
+ useEffect(() => router.on('start', () => (isNavigating = true)), [])
452
+ useEffect(() => router.on('finish', () => (isNavigating = false)), [])
453
+ useEffect(
454
+ () =>
455
+ router.on('navigate', function ($event) {
456
+ const modalOnBase = $event.detail.page.props._inertiaui_modal
457
+
458
+ if (!modalOnBase) {
459
+ previousModalOnBase && context.closeAll()
460
+ return
461
+ }
462
+
463
+ previousModalOnBase = modalOnBase
464
+ baseUrl = modalOnBase.baseUrl
465
+
466
+ context
467
+ .pushFromResponseData(modalOnBase, {}, () => {
468
+ if (!modalOnBase.baseUrl) {
469
+ console.error('No base url in modal response data so cannot navigate back')
470
+ return
471
+ }
472
+ if (!isNavigating && window.location.href !== modalOnBase.baseUrl) {
473
+ router.visit(modalOnBase.baseUrl, {
474
+ preserveScroll: true,
475
+ preserveState: true,
476
+ })
477
+ }
478
+ })
479
+ .then((newModal) => {
480
+ newModalOnBase = newModal
481
+ })
482
+ }),
483
+ [],
484
+ )
485
+
486
+ const axiosRequestInterceptor = (config) => {
487
+ // A Modal is opened on top of a base route, so we need to pass this base route
488
+ // so it can redirect back with the back() helper method...
489
+ config.headers['X-InertiaUI-Modal-Base-Url'] = baseUrl
490
+ return config
491
+ }
492
+
493
+ useEffect(() => {
494
+ Axios.interceptors.request.use(axiosRequestInterceptor)
495
+ return () => Axios.interceptors.request.eject(axiosRequestInterceptor)
496
+ }, [])
287
497
 
288
498
  return (
289
499
  <>
290
500
  {children}
291
- {stack.length > 0 && <ModalRenderer index={0} />}
501
+ {context.stack.length > 0 && <ModalRenderer index={0} />}
292
502
  </>
293
503
  )
294
504
  }
package/src/helpers.js CHANGED
@@ -1,36 +1,2 @@
1
- import { router } from '@inertiajs/react'
2
- import { except, only, rejectNullValues } from './../../vue/src/helpers.js'
3
-
4
- /**
5
- * Resolves router.page from the Inertia router or waits for it to be available
6
- */
7
- function resolveInteriaPageFromRouter(waitForSeconds = 3, checkIntervalMilliseconds = 100) {
8
- const resolvePage = () => router.page || null
9
-
10
- return new Promise((resolve, reject) => {
11
- let page = resolvePage()
12
-
13
- if (page) {
14
- resolve(page)
15
- return
16
- }
17
-
18
- let maxAttempts = (waitForSeconds * 1000) / checkIntervalMilliseconds
19
-
20
- const interval = setInterval(() => {
21
- page = resolvePage()
22
-
23
- if (page) {
24
- clearInterval(interval)
25
- resolve(page)
26
- }
27
-
28
- if (--maxAttempts <= 0) {
29
- clearInterval(interval)
30
- reject(new Error('Inertia page not available'))
31
- }
32
- }, checkIntervalMilliseconds)
33
- })
34
- }
35
-
36
- export { except, only, rejectNullValues, resolveInteriaPageFromRouter }
1
+ import { except, only, rejectNullValues, waitFor } from './../../vue/src/helpers.js'
2
+ export { except, only, rejectNullValues, waitFor }
@@ -1,8 +1,8 @@
1
1
  import { getConfig, putConfig, resetConfig } from './config.js'
2
2
  import { useModalIndex } from './ModalRenderer.jsx'
3
- import { useModalStack, ModalRoot, ModalStackProvider } from './ModalRoot.jsx'
3
+ import { useModalStack, ModalRoot, ModalStackProvider, renderApp } from './ModalRoot.jsx'
4
4
  import HeadlessModal from './HeadlessModal.jsx'
5
5
  import Modal from './Modal.jsx'
6
6
  import ModalLink from './ModalLink.jsx'
7
7
 
8
- export { getConfig, putConfig, resetConfig, useModalStack, useModalIndex, HeadlessModal, Modal, ModalLink, ModalRoot, ModalStackProvider }
8
+ export { getConfig, putConfig, resetConfig, useModalStack, useModalIndex, HeadlessModal, Modal, ModalLink, ModalRoot, ModalStackProvider, renderApp }