@jager-ai/holy-editor
Version:
Rich text editor with Bible verse slash commands and PWA keyboard tracking, extracted from Holy Habit project
307 lines (269 loc) β’ 7.8 kB
text/typescript
/**
* Toast Manager
*
* Toast notification system for user feedback
* Extracted from Holy Habit holy-editor-pro.js
*/
import { ToastOptions } from '../types/Editor';
export class ToastManager {
private static instance: ToastManager;
private toastContainer: HTMLElement | null = null;
private constructor() {
this.createToastContainer();
}
/**
* Get singleton instance
*/
public static getInstance(): ToastManager {
if (!ToastManager.instance) {
ToastManager.instance = new ToastManager();
}
return ToastManager.instance;
}
/**
* Show toast message
*/
public show(message: string, duration: number = 3000, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void {
// Remove existing toast
const existingToast = document.querySelector('.holy-toast');
if (existingToast) {
existingToast.remove();
}
// Create new toast
const toast = document.createElement('div');
toast.className = `holy-toast holy-toast-${type}`;
toast.textContent = message;
// Apply styles
this.applyToastStyles(toast, type);
// Add to document
document.body.appendChild(toast);
// Fade in animation
requestAnimationFrame(() => {
toast.style.opacity = '1';
});
// Auto remove
setTimeout(() => {
this.removeToast(toast);
}, duration);
console.log(`π’ Toast (${type}):`, message);
}
/**
* Show success toast
*/
public success(message: string, duration?: number): void {
this.show(message, duration, 'success');
}
/**
* Show error toast
*/
public error(message: string, duration?: number): void {
this.show(message, duration || 4000, 'error');
}
/**
* Show warning toast
*/
public warning(message: string, duration?: number): void {
this.show(message, duration, 'warning');
}
/**
* Show info toast
*/
public info(message: string, duration?: number): void {
this.show(message, duration, 'info');
}
/**
* Remove all toasts
*/
public clear(): void {
const toasts = document.querySelectorAll('.holy-toast');
toasts.forEach(toast => toast.remove());
}
/**
* Create toast container if needed
*/
private createToastContainer(): void {
if (!this.toastContainer) {
this.toastContainer = document.createElement('div');
this.toastContainer.className = 'holy-toast-container';
this.toastContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
`;
document.body.appendChild(this.toastContainer);
}
}
/**
* Apply styles to toast element
*/
private applyToastStyles(toast: HTMLElement, type: string): void {
const baseStyles = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 14px;
white-space: nowrap;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 500;
pointer-events: auto;
`;
const typeStyles = this.getTypeStyles(type);
toast.style.cssText = baseStyles + typeStyles;
}
/**
* Get type-specific styles
*/
private getTypeStyles(type: string): string {
const styles = {
info: `
background-color:
color: white;
border-left: 4px solid
`,
success: `
background-color:
color: white;
border-left: 4px solid
`,
warning: `
background-color:
color: white;
border-left: 4px solid
`,
error: `
background-color:
color: white;
border-left: 4px solid
`
};
return styles[type as keyof typeof styles] || styles.info;
}
/**
* Remove toast with animation
*/
private removeToast(toast: HTMLElement): void {
toast.style.opacity = '0';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
/**
* Handle API error and show appropriate toast
*/
public handleApiError(error: Error, ref?: string): void {
console.error('API Error:', error);
let message = '';
if (error.message.includes('404')) {
message = ref ? `"${ref}" ꡬμ μ μ°Ύμ μ μμ΅λλ€. μ±
μ΄λ¦:μ₯:μ νμμ νμΈν΄μ£ΌμΈμ.` : 'μμ²ν λ΄μ©μ μ°Ύμ μ μμ΅λλ€.';
} else if (error.message.includes('λ€νΈμν¬') || error.message.includes('Network')) {
message = 'λ€νΈμν¬ μ°κ²°μ νμΈν΄μ£ΌμΈμ.';
} else if (error.message.includes('500')) {
message = 'μλ² μ€λ₯κ° λ°μνμ΅λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ.';
} else {
message = 'μμ² μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€.';
}
this.error(message, 4000);
}
/**
* Show loading toast (returns cleanup function)
*/
public showLoading(message: string = 'μ²λ¦¬ μ€...'): () => void {
const toast = document.createElement('div');
toast.className = 'holy-toast holy-toast-loading';
toast.innerHTML = `
<div class="loading-spinner"></div>
<span>${message}</span>
`;
// Apply loading styles
toast.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color:
color: white;
padding: 12px 24px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
// Add spinner styles
const spinner = toast.querySelector('.loading-spinner') as HTMLElement;
if (spinner) {
spinner.style.cssText = `
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
`;
}
// Add spinner animation CSS if not exists
this.addSpinnerStyles();
document.body.appendChild(toast);
// Return cleanup function
return () => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
};
}
/**
* Add spinner animation CSS
*/
private addSpinnerStyles(): void {
if (!document.querySelector('#holy-toast-spinner-styles')) {
const style = document.createElement('style');
style.id = 'holy-toast-spinner-styles';
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
}
/**
* Show toast with custom HTML content
*/
public showCustom(htmlContent: string, duration: number = 3000, className: string = ''): void {
const existingToast = document.querySelector('.holy-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `holy-toast ${className}`;
toast.innerHTML = htmlContent;
this.applyToastStyles(toast, 'info');
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = '1';
});
setTimeout(() => {
this.removeToast(toast);
}, duration);
}
}