import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js'; import type { IBlock } from '../../wysiwyg.types.js'; import { cssManager } from '@design.estate/dees-element'; /** * YouTubeBlockHandler - Handles YouTube video embedding * * Features: * - YouTube URL parsing and validation * - Video ID extraction from various YouTube URL formats * - Embedded iframe player * - Clean minimalist design */ export class YouTubeBlockHandler extends BaseBlockHandler { type = 'youtube'; render(block: IBlock, isSelected: boolean): string { const videoId = block.metadata?.videoId; const url = block.metadata?.url || ''; return `
${videoId ? this.renderVideo(videoId) : this.renderPlaceholder(url)}
`; } private renderPlaceholder(url: string): string { return `
Enter YouTube URL
`; } private renderVideo(videoId: string): string { return `
`; } setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { const container = element.querySelector('.youtube-block-container') as HTMLElement; if (!container) return; // If video is already embedded, just handle focus/blur if (block.metadata?.videoId) { container.setAttribute('tabindex', '0'); container.addEventListener('focus', () => handlers.onFocus()); container.addEventListener('blur', () => handlers.onBlur()); // Handle deletion container.addEventListener('keydown', (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); handlers.onKeyDown(e); } else { handlers.onKeyDown(e); } }); return; } // Setup placeholder interactions const urlInput = element.querySelector('.youtube-url-input') as HTMLInputElement; const embedBtn = element.querySelector('.youtube-embed-btn') as HTMLButtonElement; if (!urlInput || !embedBtn) return; // Focus management urlInput.addEventListener('focus', () => handlers.onFocus()); urlInput.addEventListener('blur', () => handlers.onBlur()); // Handle embed button click embedBtn.addEventListener('click', () => { this.embedVideo(urlInput.value, block, handlers); }); // Handle Enter key in input urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.embedVideo(urlInput.value, block, handlers); } else if (e.key === 'Escape') { e.preventDefault(); urlInput.blur(); } }); // Handle paste event urlInput.addEventListener('paste', (e) => { // Allow paste to complete first setTimeout(() => { const pastedUrl = urlInput.value; if (this.extractYouTubeVideoId(pastedUrl)) { // Auto-embed if valid YouTube URL was pasted this.embedVideo(pastedUrl, block, handlers); } }, 0); }); // Update URL in metadata as user types urlInput.addEventListener('input', () => { if (!block.metadata) block.metadata = {}; block.metadata.url = urlInput.value; }); } private embedVideo(url: string, block: IBlock, handlers: IBlockEventHandlers): void { const videoId = this.extractYouTubeVideoId(url); if (!videoId) { // Could show an error message here console.error('Invalid YouTube URL'); return; } // Update block metadata if (!block.metadata) block.metadata = {}; block.metadata.videoId = videoId; block.metadata.url = url; // Set content as video title (could be fetched from API in the future) block.content = `YouTube Video: ${videoId}`; // Request immediate UI update to show embedded video handlers.onRequestUpdate?.(); } private extractYouTubeVideoId(url: string): string | null { // Handle various YouTube URL formats const patterns = [ /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/, /youtube\.com\/embed\/([^"&?\/ ]{11})/, /youtube\.com\/watch\?v=([^"&?\/ ]{11})/, /youtu\.be\/([^"&?\/ ]{11})/ ]; for (const pattern of patterns) { const match = url.match(pattern); if (match) { return match[1]; } } return null; } private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } getContent(element: HTMLElement): string { // Content is the video description/title const block = this.getBlockFromElement(element); return block?.content || ''; } setContent(element: HTMLElement, content: string): void { // Content is the video description/title const block = this.getBlockFromElement(element); if (block) { block.content = content; } } private getBlockFromElement(element: HTMLElement): IBlock | null { const container = element.querySelector('.youtube-block-container'); const blockId = container?.getAttribute('data-block-id'); if (!blockId) return null; // Simplified version - in real implementation would need access to block data return { id: blockId, type: 'youtube', content: '', metadata: {} }; } getCursorPosition(element: HTMLElement): number | null { return null; // YouTube blocks don't have cursor position } setCursorToStart(element: HTMLElement): void { this.focus(element); } setCursorToEnd(element: HTMLElement): void { this.focus(element); } focus(element: HTMLElement): void { const container = element.querySelector('.youtube-block-container') as HTMLElement; const urlInput = element.querySelector('.youtube-url-input') as HTMLInputElement; if (urlInput) { urlInput.focus(); } else if (container) { container.focus(); } } focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void { this.focus(element); } getSplitContent(element: HTMLElement): { before: string; after: string } | null { return null; // YouTube blocks can't be split } getStyles(): string { return ` /* YouTube Block Container */ .youtube-block-container { position: relative; margin: 12px 0; border-radius: 6px; overflow: hidden; transition: all 0.15s ease; outline: none; } .youtube-block-container.selected { box-shadow: 0 0 0 2px ${cssManager.bdTheme('#6366f1', '#818cf8')}; } /* YouTube Placeholder */ .youtube-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 24px; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')}; border-radius: 6px; background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; gap: 12px; } .placeholder-icon { color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; opacity: 0.8; } .placeholder-text { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#374151', '#e5e7eb')}; } .youtube-url-input { width: 100%; max-width: 400px; padding: 8px 12px; border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')}; border-radius: 4px; background: ${cssManager.bdTheme('#ffffff', '#111827')}; color: ${cssManager.bdTheme('#111827', '#f9fafb')}; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; transition: all 0.15s ease; outline: none; } .youtube-url-input:focus { border-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; background: ${cssManager.bdTheme('#ffffff', '#1f2937')}; } .youtube-url-input::placeholder { color: ${cssManager.bdTheme('#9ca3af', '#4b5563')}; } .youtube-embed-btn { padding: 6px 16px; background: ${cssManager.bdTheme('#111827', '#f9fafb')}; color: ${cssManager.bdTheme('#f9fafb', '#111827')}; border: 1px solid transparent; border-radius: 4px; font-size: 13px; font-weight: 500; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; cursor: pointer; transition: all 0.15s ease; outline: none; } .youtube-embed-btn:hover { background: ${cssManager.bdTheme('#374151', '#e5e7eb')}; } .youtube-embed-btn:active { transform: scale(0.98); } /* YouTube Container */ .youtube-container { position: relative; width: 100%; padding-bottom: 56.25%; /* 16:9 aspect ratio */ background: ${cssManager.bdTheme('#000000', '#000000')}; } .youtube-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; border-radius: 6px; } `; } }