Initial commit
This commit is contained in:
244
src/lib/components/Modal.svelte
Normal file
244
src/lib/components/Modal.svelte
Normal file
@@ -0,0 +1,244 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user