PluginProbe
The Innovative Form Builder – IvyForms / 0.3
The Innovative Form Builder – IvyForms v0.3
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / modal / IvyModal.vue

IvyModal.vue in The Innovative Form Builder – IvyForms 0.3, at frontend/src/views/_components/modal/IvyModal.vue

225 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <Teleport v-if="teleport" to="body">
3 <Transition name="ivyforms-modal">
4 <div
5 v-if="isVisible"
6 class="ivyforms-modal-overlay"
7 :class="{ 'ivyforms-modal-local': !teleport }"
8 @click.self="handleOverlayClick"
9 >
10 <div
11 class="ivyforms-modal-container"
12 :style="modalStyles"
13 role="dialog"
14 aria-modal="true"
15 :aria-labelledby="title ? 'ivyforms-modal-title' : undefined"
16 >
17 <slot />
18 </div>
19 </div>
20 </Transition>
21 </Teleport>
22
23 <Transition v-else name="ivyforms-modal">
24 <div
25 v-if="isVisible"
26 class="ivyforms-modal-overlay ivyforms-modal-local"
27 @click.self="handleOverlayClick"
28 >
29 <div
30 class="ivyforms-modal-container"
31 :style="modalStyles"
32 role="dialog"
33 aria-modal="true"
34 :aria-labelledby="title ? 'ivyforms-modal-title' : undefined"
35 >
36 <slot />
37 </div>
38 </div>
39 </Transition>
40 </template>
41
42 <script setup lang="ts">
43 import { computed, onMounted, onUnmounted, watch } from 'vue'
44
45 interface Props {
46 visible: boolean
47 title?: string
48 width?: number | string
49 maxWidth?: number | string
50 showCloseButton?: boolean
51 showHeader?: boolean
52 closeOnOverlayClick?: boolean
53 closeOnEscape?: boolean
54 teleport?: boolean
55 }
56
57 const props = withDefaults(defineProps<Props>(), {
58 title: '',
59 width: '',
60 maxWidth: '90vw',
61 showCloseButton: true,
62 showHeader: true,
63 closeOnOverlayClick: true,
64 closeOnEscape: true,
65 teleport: true,
66 })
67
68 const emit = defineEmits<{
69 'update:visible': [value: boolean]
70 close: []
71 open: []
72 }>()
73
74 // Computed properties
75 const isVisible = computed({
76 get: () => props.visible,
77 set: (value) => emit('update:visible', value),
78 })
79
80 const modalStyles = computed(() => ({
81 width: typeof props.width === 'number' ? `${props.width}px` : props.width,
82 maxWidth: typeof props.maxWidth === 'number' ? `${props.maxWidth}px` : props.maxWidth,
83 }))
84
85 // Methods
86 const handleClose = () => {
87 isVisible.value = false
88 emit('close')
89 }
90
91 const handleOverlayClick = () => {
92 if (props.closeOnOverlayClick) {
93 handleClose()
94 }
95 }
96
97 const handleEscapeKey = (event: KeyboardEvent) => {
98 if (event.key === 'Escape' && props.closeOnEscape && isVisible.value) {
99 handleClose()
100 }
101 }
102
103 const preventBodyScroll = () => {
104 document.body.style.overflow = 'hidden'
105 }
106
107 const restoreBodyScroll = () => {
108 document.body.style.overflow = ''
109 }
110
111 // Lifecycle hooks
112 onMounted(() => {
113 document.addEventListener('keydown', handleEscapeKey)
114 })
115
116 onUnmounted(() => {
117 document.removeEventListener('keydown', handleEscapeKey)
118 restoreBodyScroll()
119 })
120
121 // Watch for visibility changes
122 watch(isVisible, (newValue) => {
123 if (newValue) {
124 preventBodyScroll()
125 emit('open')
126 } else {
127 restoreBodyScroll()
128 }
129 })
130 </script>
131
132 <style scoped lang="scss">
133 .ivyforms-modal-overlay {
134 position: fixed;
135 inset: 0;
136 z-index: 9999;
137 background: var(--map-overlay);
138 display: flex;
139 justify-content: center;
140 align-items: flex-start;
141
142 &.ivyforms-modal-local {
143 @apply absolute;
144 position: absolute !important;
145 top: 0 !important;
146 left: 0 !important;
147 right: 0 !important;
148 bottom: 0 !important;
149 z-index: 100;
150 width: 100% !important;
151 height: 100% !important;
152 }
153 .ivyforms-modal-container {
154 max-width: 1300px;
155 min-width: 800px;
156 height: calc(100vh - 80px);
157 max-height: none;
158 min-height: 600px;
159 padding: 0;
160 overflow-y: auto;
161 margin: 45px 20px 20px 180px;
162 border-radius: var(--Radius-radius-xl, 16px);
163 background: var(--map-ground-level-2-foreground);
164 box-shadow:
165 0 8px 12px 6px rgba(18, 26, 38, 0.15),
166 0 4px 4px 0 rgba(18, 26, 38, 0.3);
167 box-sizing: border-box;
168 position: relative;
169 z-index: 10000;
170
171 // Responsive adjustments
172 @media (max-width: 1200px) {
173 width: calc(100vw - 200px);
174 margin-left: 180px;
175 min-width: 700px;
176 height: calc(100vh - 60px);
177 min-height: 500px;
178 }
179
180 @media (max-width: 960px) {
181 width: calc(100vw - 2rem) !important;
182 max-width: calc(100vw - 2rem) !important;
183 min-width: auto !important;
184 height: calc(100vh - 40px) !important;
185 min-height: auto !important;
186 margin: 20px auto auto auto;
187 }
188
189 @media (max-width: 782px) {
190 width: calc(100vw - 76px);
191 margin-left: 56px;
192 min-width: auto;
193 height: calc(100vh - 60px);
194 min-height: 400px;
195 }
196
197 @media (max-width: 600px) {
198 width: calc(100vw - 1rem) !important;
199 margin: 10px auto auto auto;
200 min-width: auto !important;
201 height: calc(100vh - 20px) !important;
202 min-height: auto !important;
203 }
204 }
205 }
206 // Transitions
207 .ivyforms-modal-enter-active,
208 .ivyforms-modal-leave-active {
209 transition: opacity 0.3s ease;
210
211 .ivyforms-modal-container {
212 transition: transform 0.3s ease;
213 }
214 }
215
216 .ivyforms-modal-enter-from,
217 .ivyforms-modal-leave-to {
218 opacity: 0;
219
220 .ivyforms-modal-container {
221 transform: scale(0.9) translateY(-20px);
222 }
223 }
224 </style>
225