Skip to content

Commit 95c9113

Browse files
committed
refactor: Overlays (Modal,OffCanvas, ImgPopup) stacks
- Move img popup to component - OffCanvas toggler with action button
1 parent e415eb1 commit 95c9113

File tree

9 files changed

+200
-62
lines changed

9 files changed

+200
-62
lines changed

dist/assets/app.js

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export default () => ({
2+
open: false,
3+
popupId: '',
4+
src: '',
5+
styles: '',
6+
auto: true,
7+
wide: false,
8+
9+
init() {
10+
this.popupId = this.$id('img-popup')
11+
},
12+
13+
registerInStack() {
14+
Alpine.store('overlays').register(this.popupId, () => this.close())
15+
},
16+
17+
unregisterFromStack() {
18+
Alpine.store('overlays').unregister(this.popupId)
19+
},
20+
21+
show(detail) {
22+
this.src = detail.src
23+
this.auto = detail.auto ?? true
24+
this.wide = detail.wide ?? false
25+
this.styles = detail.styles ?? ''
26+
27+
// Move to end of body to ensure it's above all other modals
28+
const el = document.body.querySelector('[data-img-popup]')
29+
if (el) {
30+
document.body.appendChild(el)
31+
}
32+
33+
this.open = true
34+
this.registerInStack()
35+
},
36+
37+
close() {
38+
if (this.open) {
39+
this.open = false
40+
this.unregisterFromStack()
41+
}
42+
},
43+
})

resources/js/Components/Modal.js

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,32 @@ import load from '../Support/AsyncLoadContent.js'
44
export default (open = false, asyncUrl = '', autoClose = true) => ({
55
open: open,
66
id: '',
7+
modalId: '',
78
asyncUrl: asyncUrl,
89
inModal: true,
910
asyncLoaded: false,
1011
autoClose: autoClose,
1112

1213
init() {
1314
this.id = this.$id('modal-content')
15+
this.modalId = this.$id('modal')
1416

15-
if (this.open && this.asyncUrl) {
16-
load(asyncUrl, this.id)
17+
// Register if initially open
18+
if (this.open) {
19+
this.registerInStack()
20+
21+
if (this.asyncUrl) {
22+
load(asyncUrl, this.id)
23+
}
1724
}
25+
},
1826

19-
Alpine.bind('dismissModal', () => ({
20-
'@keydown.escape.window'() {
21-
if (this.open) {
22-
this.open = false
27+
registerInStack() {
28+
Alpine.store('overlays').register(this.modalId, () => this.closeModal())
29+
},
2330

24-
this.dispatchEvents()
25-
}
26-
},
27-
}))
31+
unregisterFromStack() {
32+
Alpine.store('overlays').unregister(this.modalId)
2833
},
2934

3035
dispatchEvents() {
@@ -37,13 +42,26 @@ export default (open = false, asyncUrl = '', autoClose = true) => ({
3742
}
3843
},
3944

45+
closeModal() {
46+
if (this.open) {
47+
this.open = false
48+
this.unregisterFromStack()
49+
this.dispatchEvents()
50+
}
51+
},
52+
4053
async toggleModal() {
4154
this.open = !this.open
4255

43-
if (this.open && this.asyncUrl && !this.asyncLoaded) {
44-
await load(asyncUrl, this.id)
56+
if (this.open) {
57+
this.registerInStack()
4558

46-
this.asyncLoaded = !this.$root.dataset.alwaysLoad
59+
if (this.asyncUrl && !this.asyncLoaded) {
60+
await load(asyncUrl, this.id)
61+
this.asyncLoaded = !this.$root.dataset.alwaysLoad
62+
}
63+
} else {
64+
this.unregisterFromStack()
4765
}
4866

4967
this.dispatchEvents()

resources/js/Components/OffCanvas.js

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,32 @@ import load from '../Support/AsyncLoadContent.js'
44
export default (open = false, asyncUrl = '', autoClose = true) => ({
55
open: open,
66
id: '',
7+
canvasId: '',
78
asyncUrl: asyncUrl,
89
inOffCanvas: true,
910
asyncLoaded: false,
1011
autoClose: autoClose,
1112

1213
init() {
1314
this.id = this.$id('offcanvas-content')
15+
this.canvasId = this.$id('offcanvas')
1416

15-
if (this.open && this.asyncUrl) {
16-
load(asyncUrl, this.id)
17+
// Register if initially open
18+
if (this.open) {
19+
this.registerInStack()
20+
21+
if (this.asyncUrl) {
22+
load(asyncUrl, this.id)
23+
}
1724
}
25+
},
26+
27+
registerInStack() {
28+
Alpine.store('overlays').register(this.canvasId, () => this.closeCanvas())
29+
},
1830

19-
Alpine.bind('dismissCanvas', () => ({
20-
'@click.outside'() {
21-
if (this.open) {
22-
this.open = false
23-
24-
this.dispatchEvents()
25-
}
26-
},
27-
'@keydown.escape.window'() {
28-
if (this.open) {
29-
this.open = false
30-
31-
this.dispatchEvents()
32-
}
33-
},
34-
}))
31+
unregisterFromStack() {
32+
Alpine.store('overlays').unregister(this.canvasId)
3533
},
3634

3735
dispatchEvents() {
@@ -44,13 +42,26 @@ export default (open = false, asyncUrl = '', autoClose = true) => ({
4442
}
4543
},
4644

45+
closeCanvas() {
46+
if (this.open) {
47+
this.open = false
48+
this.unregisterFromStack()
49+
this.dispatchEvents()
50+
}
51+
},
52+
4753
async toggleCanvas() {
4854
this.open = !this.open
4955

50-
if (this.open && this.asyncUrl && !this.asyncLoaded) {
51-
await load(asyncUrl, this.id)
56+
if (this.open) {
57+
this.registerInStack()
5258

53-
this.asyncLoaded = !this.$root.dataset.alwaysLoad
59+
if (this.asyncUrl && !this.asyncLoaded) {
60+
await load(asyncUrl, this.id)
61+
this.asyncLoaded = !this.$root.dataset.alwaysLoad
62+
}
63+
} else {
64+
this.unregisterFromStack()
5465
}
5566

5667
this.dispatchEvents()

resources/js/app.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import fragment from './Components/Fragment'
3030
import tabs from './Components/Tabs.js'
3131
import collapse from './Components/Collapse.js'
3232
import bottomBarMenu from './Components/BottomBar.js'
33+
import imgPopup from './Components/ImgPopup.js'
3334
import {validationInHiddenBlocks} from './Support/Forms.js'
3435

3536
window.MoonShine = new MoonShine()
@@ -62,12 +63,60 @@ Alpine.data('fragment', fragment)
6263
Alpine.data('tabs', tabs)
6364
Alpine.data('collapse', collapse)
6465
Alpine.data('bottomBarMenu', bottomBarMenu)
66+
Alpine.data('imgPopup', imgPopup)
6567

6668
window.Alpine = Alpine
6769

6870
document.addEventListener('alpine:init', () => {
6971
validationInHiddenBlocks()
7072

73+
/* Overlay stack (modals, offcanvas, popups) for proper Escape key handling */
74+
Alpine.store('overlays', {
75+
stack: [],
76+
77+
register(id, closeCallback) {
78+
this.stack = this.stack.filter(m => m.id !== id)
79+
this.stack.push({id, close: closeCallback})
80+
},
81+
82+
unregister(id) {
83+
this.stack = this.stack.filter(m => m.id !== id)
84+
},
85+
86+
closeTop() {
87+
const top = this.stack.at(-1)
88+
if (top) {
89+
top.close()
90+
}
91+
},
92+
93+
isTop(id) {
94+
return this.stack.at(-1)?.id === id
95+
},
96+
})
97+
98+
/* Global Escape handler for overlays */
99+
window.addEventListener('keydown', (e) => {
100+
if (e.key === 'Escape') {
101+
const store = Alpine.store('overlays')
102+
if (store.stack.length > 0) {
103+
e.stopImmediatePropagation()
104+
store.closeTop()
105+
}
106+
}
107+
})
108+
109+
/* Global bind for OffCanvas click outside */
110+
Alpine.bind('dismissCanvas', () => ({
111+
'@click.outside'() {
112+
// Only close if this OffCanvas is the top-most overlay
113+
// Prevents closing when clicking on a Modal that's above this OffCanvas
114+
if (this.open && Alpine.store('overlays').isTop(this.canvasId)) {
115+
this.closeCanvas()
116+
}
117+
},
118+
}))
119+
71120
/* Dark mode */
72121
Alpine.store('darkMode', {
73122
init() {

resources/views/components/modal.blade.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ class="modal-template"
3939
<div
4040
class="modal-dialog
4141
@if($wide) modal-dialog-xl @elseif($full) w-full max-w-none @elseif($auto) modal-dialog-auto @endif"
42-
x-bind="dismissModal"
4342
>
4443
<div class="modal-content">
4544
<div class="modal-header">

resources/views/components/off-canvas.blade.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'async' => false,
1010
'asyncUrl' => '',
1111
'toggler' => null,
12+
'togglerButton' => false,
1213
])
1314
<div x-data="offCanvas(
1415
`{{ $open }}`,
@@ -18,13 +19,19 @@
1819
{{ $attributes }}
1920
>
2021
@if($toggler?->isNotEmpty())
21-
<x-moonshine::link-button
22-
:attributes="$toggler->attributes?->merge([
23-
'@click.prevent' => 'toggleCanvas',
24-
])"
25-
>
26-
{{ $toggler ?? '' }}
27-
</x-moonshine::link-button>
22+
@if(!$togglerButton)
23+
<x-moonshine::link-button
24+
:attributes="$toggler->attributes?->merge([
25+
'@click.prevent' => 'toggleCanvas',
26+
])"
27+
>
28+
{{ $toggler ?? '' }}
29+
</x-moonshine::link-button>
30+
@else
31+
<div {{ $toggler->attributes }}>
32+
{{ $toggler ?? '' }}
33+
</div>
34+
@endif
2835
@endif
2936

3037
<template x-teleport="body">

resources/views/shared/img-popup.blade.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
<div x-data="{ open : false, src : '', auto: true, wide: false, styles: ''}">
2-
<template
3-
@img-popup.window="open = true; src = $event.detail.src; auto = $event.detail.auto; wide = $event.detail.wide; styles = $event.detail.styles"
4-
x-teleport="body"
5-
>
6-
<div class="modal-template">
1+
<div x-data="imgPopup()" @img-popup.window="show($event.detail)">
2+
<template x-teleport="body">
3+
<div class="modal-template" data-img-popup>
74
<div
85
x-show="open"
96
x-transition:enter="transition ease-out duration-250"
@@ -15,7 +12,7 @@
1512
class="modal"
1613
aria-modal="true"
1714
role="dialog"
18-
@click.self="open=false"
15+
@click.self="close"
1916
>
2017
<div
2118
class="modal-dialog"
@@ -26,15 +23,15 @@ class="modal-dialog"
2623
<button
2724
type="button"
2825
class="modal-close btn-fit"
29-
@click.stop="open=false"
26+
@click.stop="close"
3027
aria-label="Close"
3128
>
3229
<x-moonshine::icon icon="x-mark" />
3330
</button>
3431
</div>
3532
<div class="modal-body">
3633
<img
37-
@click.outside="open = false"
34+
@click.outside="close"
3835
src=""
3936
:src="src"
4037
:style="styles"

0 commit comments

Comments
 (0)