244 lines
5.8 KiB
Svelte
244 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
|
import { fade, scale } from 'svelte/transition';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
// Props
|
|
export let title = '';
|
|
export let closable = true;
|
|
export let component = null;
|
|
export let componentProps = {};
|
|
|
|
// State
|
|
let isOpen = false;
|
|
let modalContent;
|
|
let componentInstance = null;
|
|
|
|
// Event callbacks
|
|
let onSubmitCallback = null;
|
|
let onCancelCallback = null;
|
|
|
|
// Handle component dispatch events
|
|
function handleComponentEvent(event) {
|
|
if (event.type === 'save') {
|
|
if (onSubmitCallback) {
|
|
onSubmitCallback(event.detail);
|
|
}
|
|
close();
|
|
} else if (event.type === 'cancel') {
|
|
if (onCancelCallback) {
|
|
onCancelCallback();
|
|
}
|
|
close();
|
|
}
|
|
}
|
|
|
|
// Open the modal
|
|
export function open() {
|
|
console.log('Opening modal');
|
|
isOpen = true;
|
|
}
|
|
|
|
// Close the modal
|
|
export function close() {
|
|
console.log('Closing modal');
|
|
isOpen = false;
|
|
|
|
// Schedule component cleanup
|
|
setTimeout(() => {
|
|
if (!isOpen) {
|
|
cleanupComponent();
|
|
}
|
|
}, 300); // Wait for transitions to complete
|
|
}
|
|
|
|
// Set properties and callbacks
|
|
export function setProps(props) {
|
|
if (props.title !== undefined) title = props.title;
|
|
if (props.closable !== undefined) closable = props.closable;
|
|
if (props.component !== undefined) component = props.component;
|
|
if (props.componentProps !== undefined) componentProps = props.componentProps;
|
|
if (props.onSubmit) onSubmitCallback = props.onSubmit;
|
|
if (props.onCancel) onCancelCallback = props.onCancel;
|
|
}
|
|
|
|
// Clean up any previous component instance
|
|
function cleanupComponent() {
|
|
if (componentInstance) {
|
|
try {
|
|
componentInstance.$destroy();
|
|
} catch (error) {
|
|
console.error('Error destroying component instance:', error);
|
|
}
|
|
componentInstance = null;
|
|
}
|
|
}
|
|
|
|
// Only create component when modal is open and modalContent is available
|
|
$: if (isOpen && component && modalContent) {
|
|
console.log('Creating component in modal:', component.name || 'Unknown component');
|
|
|
|
// Clean up previous component first to prevent conflicts
|
|
cleanupComponent();
|
|
|
|
try {
|
|
// Create the component instance
|
|
componentInstance = new component({
|
|
target: modalContent,
|
|
props: componentProps
|
|
});
|
|
|
|
console.log('Component created successfully');
|
|
|
|
// Listen for events from the component
|
|
for (const event of ['save', 'cancel']) {
|
|
componentInstance.$on(event, (e) => handleComponentEvent({ type: event, detail: e.detail }));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating component in modal:', error);
|
|
}
|
|
}
|
|
|
|
// Close on ESC key
|
|
function handleKeydown(event) {
|
|
if (event.key === 'Escape' && closable && isOpen) {
|
|
close();
|
|
if (onCancelCallback) onCancelCallback();
|
|
}
|
|
}
|
|
|
|
// Cleanup on destroy
|
|
onDestroy(() => {
|
|
cleanupComponent();
|
|
window.removeEventListener('keydown', handleKeydown);
|
|
});
|
|
|
|
// Listen for ESC key
|
|
onMount(() => {
|
|
window.addEventListener('keydown', handleKeydown);
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeydown);
|
|
};
|
|
});
|
|
|
|
// Prevent clicks inside the modal from bubbling up
|
|
function handleModalClick(event) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
// Handle backdrop click
|
|
function handleBackdropClick() {
|
|
if (closable) {
|
|
close();
|
|
if (onCancelCallback) onCancelCallback();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window on:keydown={handleKeydown} />
|
|
|
|
{#if isOpen}
|
|
<div class="modal-backdrop" on:click={handleBackdropClick} transition:fade={{ duration: 150 }}>
|
|
<div class="modal-content" on:click={handleModalClick} transition:scale={{ start: 0.95, duration: 200 }}>
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">{title}</h2>
|
|
{#if closable}
|
|
<button type="button" class="modal-close-button" on:click={close} aria-label="Close modal">
|
|
×
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="modal-body" bind:this={modalContent}></div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.modal-backdrop {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background-color: #fff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.modal-title {
|
|
margin: 0;
|
|
font-size: 1.25rem;
|
|
color: #333;
|
|
}
|
|
|
|
.modal-close-button {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: #999;
|
|
padding: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.modal-close-button:hover {
|
|
color: #333;
|
|
background-color: #f0f0f0;
|
|
}
|
|
|
|
.modal-body {
|
|
max-height: calc(90vh - 70px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Dark mode support */
|
|
:global(body.dark) .modal-content {
|
|
background-color: #2d3748;
|
|
color: #f8f9fa;
|
|
}
|
|
|
|
:global(body.dark) .modal-header {
|
|
border-bottom-color: #4a5568;
|
|
}
|
|
|
|
:global(body.dark) .modal-title {
|
|
color: #f8f9fa;
|
|
}
|
|
|
|
:global(body.dark) .modal-close-button {
|
|
color: #cbd5e0;
|
|
}
|
|
|
|
:global(body.dark) .modal-close-button:hover {
|
|
color: #f8f9fa;
|
|
background-color: #4a5568;
|
|
}
|
|
</style> |