import { customElement, property, static as html, DeesElement, type TemplateResult, cssManager, css, } from '@design.estate/dees-element'; import { type IBlock } from './wysiwyg.types.js'; import { WysiwygBlocks } from './wysiwyg.blocks.js'; import { WysiwygSelection } from './wysiwyg.selection.js'; import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js'; import './wysiwyg.blockregistration.js'; declare global { interface HTMLElementTagNameMap { 'dees-wysiwyg-block': DeesWysiwygBlock; } } @customElement('dees-wysiwyg-block') export class DeesWysiwygBlock extends DeesElement { async disconnectedCallback() { await super.disconnectedCallback(); // Clean up selection handler if ((this as any)._selectionHandler) { document.removeEventListener('selectionchange', (this as any)._selectionHandler); } } @property({ type: Object }) public block: IBlock; @property({ type: Boolean }) public isSelected: boolean = false; @property({ type: Object }) public handlers: IBlockEventHandlers; // Reference to the editable block element private blockElement: HTMLDivElement | null = null; // Track if we've initialized the content private contentInitialized: boolean = false; // Track cursor position private lastKnownCursorPosition: number = 0; private lastSelectedText: string = ''; private static handlerStylesInjected = false; private injectHandlerStyles(): void { // Only inject once per component class if (DeesWysiwygBlock.handlerStylesInjected) return; DeesWysiwygBlock.handlerStylesInjected = true; // Get styles from all registered block handlers let styles = ''; const blockTypes = BlockRegistry.getAllTypes(); for (const type of blockTypes) { const handler = BlockRegistry.getHandler(type); if (handler) { styles += handler.getStyles(); } } if (styles) { // Create and inject style element const styleElement = document.createElement('style'); styleElement.textContent = styles; this.shadowRoot?.appendChild(styleElement); } } public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .block { padding: 4px 0; min-height: 1.6em; outline: none; width: 100%; word-wrap: break-word; position: relative; transition: all 0.15s ease; color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; } .block:empty:not(:focus)::before { content: attr(data-placeholder); color: ${cssManager.bdTheme('#999', '#666')}; position: absolute; pointer-events: none; } .block.heading-1 { font-size: 32px; font-weight: 700; line-height: 1.2; margin: 24px 0 8px 0; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block.heading-2 { font-size: 24px; font-weight: 600; line-height: 1.3; margin: 20px 0 6px 0; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block.heading-3 { font-size: 20px; font-weight: 600; line-height: 1.4; margin: 16px 0 4px 0; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block.quote { border-left: 3px solid ${cssManager.bdTheme('#0066cc', '#4d94ff')}; padding-left: 20px; color: ${cssManager.bdTheme('#555', '#b0b0b0')}; font-style: italic; line-height: 1.6; margin: 16px 0; } .block.code { font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-size: 14px; background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; padding: 16px 20px; padding-top: 32px; border-radius: 6px; white-space: pre-wrap; color: ${cssManager.bdTheme('#24292e', '#e1e4e8')}; line-height: 1.5; overflow-x: auto; margin: 20px 0; } .block.list { padding: 0; } .block.list ul, .block.list ol { margin: 0; padding-left: 24px; } .block.list li { margin: 4px 0; } /* Formatting styles */ .block :is(b, strong) { font-weight: 600; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block :is(i, em) { font-style: italic; } .block u { text-decoration: underline; } .block s { text-decoration: line-through; } .block code { font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-size: 0.9em; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')}; padding: 2px 6px; border-radius: 3px; color: ${cssManager.bdTheme('#d14', '#ff6b6b')}; } .block a { color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.15s ease; cursor: pointer; } .block a:hover { border-bottom-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } .code-language { position: absolute; top: 0; right: 0; background: ${cssManager.bdTheme('#e1e4e8', '#333333')}; color: ${cssManager.bdTheme('#586069', '#8b949e')}; padding: 4px 12px; font-size: 12px; border-radius: 0 6px 0 6px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; text-transform: lowercase; z-index: 1; } .code-block-container { position: relative; margin: 20px 0; } /* Selection styles */ .block ::selection { background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; color: inherit; } /* Paragraph specific styles */ .block.paragraph { font-size: 16px; line-height: 1.6; font-weight: 400; } /* Strike through */ .block :is(s, strike) { text-decoration: line-through; opacity: 0.7; } /* List specific margin adjustments */ .block.list li { margin-bottom: 8px; line-height: 1.6; } .block.list li:last-child { margin-bottom: 0; } /* Block margin adjustments based on type */ :host-context(.block-wrapper:first-child) .block { margin-top: 0 !important; } :host-context(.block-wrapper:last-child) .block { margin-bottom: 0; } /* Selected state */ .block.selected { background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.05)', 'rgba(77, 148, 255, 0.08)')}; box-shadow: inset 0 0 0 2px ${cssManager.bdTheme('rgba(0, 102, 204, 0.2)', 'rgba(77, 148, 255, 0.2)')}; border-radius: 4px; margin-left: -8px; margin-right: -8px; padding-left: 8px; padding-right: 8px; } /* Image block styles */ .block.image { min-height: 200px; padding: 0; margin: 16px 0; border-radius: 8px; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s ease; } .block.image:focus { outline: none; } .block.image.selected { box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; } .image-upload-placeholder { width: 100%; height: 200px; background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')}; border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')}; border-radius: 8px; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } .image-upload-placeholder:hover { background: ${cssManager.bdTheme('#f0f0f0', '#222222')}; border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } .image-upload-placeholder:active { transform: scale(0.98); } .image-upload-placeholder.drag-over { background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; border-color: ${cssManager.bdTheme('#2196F3', '#64b5f6')}; } .upload-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.7; } .upload-text { font-size: 16px; color: ${cssManager.bdTheme('#666', '#999')}; margin-bottom: 8px; } .upload-hint { font-size: 13px; color: ${cssManager.bdTheme('#999', '#666')}; } .image-container { width: 100%; position: relative; } .image-container img { width: 100%; height: auto; display: block; border-radius: 8px; } .image-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 16px 24px; background: rgba(0, 0, 0, 0.8); color: white; border-radius: 8px; font-size: 14px; } input[type="file"] { display: none; } /* YouTube block styles */ .block.youtube { padding: 0; margin: 16px 0; border-radius: 8px; overflow: hidden; position: relative; cursor: pointer; transition: all 0.15s ease; } .block.youtube:focus { outline: none; } .block.youtube.selected { box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; } .youtube-container { position: relative; padding-bottom: 56.25%; /* 16:9 aspect ratio */ height: 0; overflow: hidden; } .youtube-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; } .youtube-placeholder { background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')}; border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')}; border-radius: 8px; padding: 40px; text-align: center; } .placeholder-icon { font-size: 48px; margin-bottom: 16px; } .placeholder-text { font-size: 16px; color: ${cssManager.bdTheme('#666', '#999')}; margin-bottom: 16px; } .youtube-url-input { width: 100%; max-width: 400px; padding: 12px; border: 1px solid ${cssManager.bdTheme('#ddd', '#444')}; border-radius: 6px; font-size: 14px; margin-bottom: 16px; background: ${cssManager.bdTheme('#fff', '#222')}; color: ${cssManager.bdTheme('#000', '#fff')}; } .youtube-embed-btn { padding: 10px 24px; background: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s ease; } .youtube-embed-btn:hover { background: ${cssManager.bdTheme('#0052a3', '#3d7dd9')}; } /* Markdown block styles */ .block.markdown { padding: 0; margin: 16px 0; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.15s ease; } .block.markdown:focus { outline: none; } .block.markdown.selected { box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; } .markdown-toolbar, .html-toolbar { background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')}; padding: 8px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; } .markdown-label, .html-label { font-size: 12px; text-transform: uppercase; color: ${cssManager.bdTheme('#666', '#999')}; font-weight: 500; } .toggle-preview { padding: 6px 12px; background: ${cssManager.bdTheme('#fff', '#333')}; border: 1px solid ${cssManager.bdTheme('#ddd', '#555')}; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; } .toggle-preview:hover { background: ${cssManager.bdTheme('#f0f0f0', '#444')}; } .markdown-content, .html-content { min-height: 200px; } .markdown-editor, .html-editor { width: 100%; min-height: 200px; padding: 16px; border: none; resize: vertical; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-size: 14px; background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; color: ${cssManager.bdTheme('#000', '#fff')}; } .markdown-preview, .html-preview { padding: 16px; min-height: 200px; background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; } .markdown-preview h1, .markdown-preview h2, .markdown-preview h3 { margin-top: 16px; margin-bottom: 8px; } .markdown-preview h1:first-child, .markdown-preview h2:first-child, .markdown-preview h3:first-child { margin-top: 0; } /* HTML block styles */ .block.html { padding: 0; margin: 16px 0; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.15s ease; } .block.html:focus { outline: none; } .block.html.selected { box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; } /* Attachment block styles */ .block.attachment { padding: 0; margin: 16px 0; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.15s ease; } .block.attachment:focus { outline: none; } .block.attachment.selected { box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; } .block.attachment.drag-over { background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; } .attachment-header { background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')}; padding: 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; } .attachment-icon { font-size: 24px; } .attachment-title { font-size: 16px; font-weight: 500; } .attachment-list { padding: 16px; min-height: 100px; } .attachment-placeholder { text-align: center; padding: 40px; border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')}; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; } .attachment-placeholder:hover { background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')}; border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } .placeholder-hint { font-size: 13px; color: ${cssManager.bdTheme('#999', '#666')}; margin-top: 8px; } .attachment-item { display: flex; align-items: center; gap: 12px; padding: 12px; background: ${cssManager.bdTheme('#f8f8f8', '#222')}; border-radius: 6px; margin-bottom: 8px; transition: background 0.2s ease; } .attachment-item:hover { background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')}; } .file-icon { font-size: 24px; flex-shrink: 0; } .file-info { flex: 1; min-width: 0; } .file-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-size { font-size: 12px; color: ${cssManager.bdTheme('#666', '#999')}; } .remove-file { width: 24px; height: 24px; border: none; background: transparent; color: ${cssManager.bdTheme('#999', '#666')}; font-size: 20px; line-height: 1; cursor: pointer; opacity: 0; transition: all 0.2s ease; } .attachment-item:hover .remove-file { opacity: 1; } .remove-file:hover { color: ${cssManager.bdTheme('#d32f2f', '#f44336')}; } .add-more-files { width: 100%; padding: 10px; background: transparent; border: 1px dashed ${cssManager.bdTheme('#ddd', '#444')}; border-radius: 6px; color: ${cssManager.bdTheme('#666', '#999')}; font-size: 14px; cursor: pointer; transition: all 0.2s ease; } .add-more-files:hover { background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')}; border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } `, ]; protected shouldUpdate(changedProperties: Map): boolean { // If selection state changed, update the selected class without re-rendering if (changedProperties.has('isSelected') && this.block) { // Find the block element based on block type let element: HTMLElement | null = null; // Build the specific selector based on block type const blockType = this.block.type; const selector = `.block.${blockType}`; element = this.shadowRoot?.querySelector(selector) as HTMLElement; if (element) { if (this.isSelected) { element.classList.add('selected'); } else { element.classList.remove('selected'); } } return false; // Don't re-render, just update the class } // Never update if only the block content changed if (changedProperties.has('block') && this.block) { const oldBlock = changedProperties.get('block'); if (oldBlock && oldBlock.id && oldBlock.type && oldBlock.id === this.block.id && oldBlock.type === this.block.type) { // Only content or metadata changed, don't re-render return false; } } // Only update if the block type or id changes return !this.blockElement || this.block?.type !== this.blockElement.dataset.blockType; } public firstUpdated(): void { // Mark that content has been initialized this.contentInitialized = true; // Inject handler styles if not already done this.injectHandlerStyles(); // First, populate the container with the rendered content const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container && this.block) { container.innerHTML = this.renderBlockContent(); } // Check if we have a registered handler for this block type if (this.block) { const handler = BlockRegistry.getHandler(this.block.type); if (handler) { const blockElement = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; if (blockElement) { handler.setup(blockElement, this.block, this.handlers); } return; // Block handler takes care of all setup } } // Handle special block types if (this.block.type === 'image') { this.setupImageBlock(); return; // Image blocks don't need the standard editable setup } else if (this.block.type === 'youtube') { this.setupYouTubeBlock(); return; } else if (this.block.type === 'markdown') { this.setupMarkdownBlock(); return; } else if (this.block.type === 'html') { this.setupHtmlBlock(); return; } else if (this.block.type === 'attachment') { this.setupAttachmentBlock(); return; } // Now find the actual editable block element const editableBlock = this.block.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; // Ensure the block element maintains its content if (editableBlock) { editableBlock.setAttribute('data-block-id', this.block.id); editableBlock.setAttribute('data-block-type', this.block.type); // Set up all event handlers manually to avoid Lit re-renders editableBlock.addEventListener('input', (e) => { this.handlers?.onInput?.(e as InputEvent); // Track cursor position after input const pos = this.getCursorPosition(editableBlock); if (pos !== null) { this.lastKnownCursorPosition = pos; } }); editableBlock.addEventListener('keydown', (e) => { // Track cursor position before keydown const pos = this.getCursorPosition(editableBlock); if (pos !== null) { this.lastKnownCursorPosition = pos; } this.handlers?.onKeyDown?.(e); }); editableBlock.addEventListener('focus', () => { this.handlers?.onFocus?.(); }); editableBlock.addEventListener('blur', () => { this.handlers?.onBlur?.(); }); editableBlock.addEventListener('compositionstart', () => { this.handlers?.onCompositionStart?.(); }); editableBlock.addEventListener('compositionend', () => { this.handlers?.onCompositionEnd?.(); }); editableBlock.addEventListener('mouseup', (e) => { const pos = this.getCursorPosition(editableBlock); if (pos !== null) { this.lastKnownCursorPosition = pos; } // Selection will be handled by selectionchange event this.handlers?.onMouseUp?.(e); }); editableBlock.addEventListener('click', (e: MouseEvent) => { // Small delay to let browser set cursor position setTimeout(() => { const pos = this.getCursorPosition(editableBlock); if (pos !== null) { this.lastKnownCursorPosition = pos; } }, 0); }); // Add selection change handler const checkSelection = () => { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const selectedText = selection.toString(); if (selectedText.length === 0) { // Clear selection if no text if (this.lastSelectedText) { this.lastSelectedText = ''; this.dispatchEvent(new CustomEvent('block-text-selected', { detail: { text: '', blockId: this.block.id, hasSelection: false }, bubbles: true, composed: true })); } return; } // Get fresh reference to the editable block const currentEditableBlock = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (!currentEditableBlock) return; // Get parent wysiwyg component's shadow root const parentComponent = this.closest('dees-input-wysiwyg'); const parentShadowRoot = parentComponent?.shadowRoot; // Use getComposedRanges with shadow roots as per MDN docs const shadowRoots: ShadowRoot[] = []; if (parentShadowRoot) shadowRoots.push(parentShadowRoot); if (this.shadowRoot) shadowRoots.push(this.shadowRoot); // Get selection info using our Shadow DOM-aware utility const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); if (!selectionInfo) return; // Check if selection is within this block const startInBlock = WysiwygSelection.containsAcrossShadowDOM(currentEditableBlock, selectionInfo.startContainer); const endInBlock = WysiwygSelection.containsAcrossShadowDOM(currentEditableBlock, selectionInfo.endContainer); if (startInBlock || endInBlock) { if (selectedText !== this.lastSelectedText) { this.lastSelectedText = selectedText; // Create range and get rect const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const rect = range.getBoundingClientRect(); // Dispatch event this.dispatchEvent(new CustomEvent('block-text-selected', { detail: { text: selectedText.trim(), blockId: this.block.id, range: range, rect: rect, hasSelection: true }, bubbles: true, composed: true })); } } else if (this.lastSelectedText) { // Clear selection if no longer in this block this.lastSelectedText = ''; this.dispatchEvent(new CustomEvent('block-text-selected', { detail: { text: '', blockId: this.block.id, hasSelection: false }, bubbles: true, composed: true })); } }; // Listen for selection changes document.addEventListener('selectionchange', checkSelection); // Store the handler for cleanup (this as any)._selectionHandler = checkSelection; // Add keyup handler for cursor position tracking editableBlock.addEventListener('keyup', (e) => { // Track cursor position const pos = this.getCursorPosition(editableBlock); if (pos !== null) { this.lastKnownCursorPosition = pos; } }); // Set initial content if needed if (this.block.content) { if (this.block.type === 'code') { editableBlock.textContent = this.block.content; } else if (this.block.type === 'list') { editableBlock.innerHTML = WysiwygBlocks.renderListContent(this.block.content, this.block.metadata); } else { editableBlock.innerHTML = this.block.content; } } } // Store reference to the block element for quick access this.blockElement = editableBlock; } render(): TemplateResult { if (!this.block) return html``; // Since we need dynamic content, we'll render an empty container // and set the innerHTML in firstUpdated return html`
`; } private renderBlockContent(): string { if (!this.block) return ''; // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler) { return handler.render(this.block, this.isSelected); } if (this.block.type === 'code') { const language = this.block.metadata?.language || 'plain text'; const selectedClass = this.isSelected ? ' selected' : ''; return `
${language}
`; } if (this.block.type === 'image') { const selectedClass = this.isSelected ? ' selected' : ''; const imageUrl = this.block.metadata?.url || ''; const isLoading = this.block.metadata?.loading || false; return `
${isLoading ? `
Uploading image...
` : ''} ${imageUrl ? `
${this.block.content || 'Uploaded image'}
` : `
🖼️
Click to upload an image
or drag and drop
`}
`; } if (this.block.type === 'youtube') { const selectedClass = this.isSelected ? ' selected' : ''; const videoId = this.block.metadata?.videoId || ''; const url = this.block.metadata?.url || ''; return `
${videoId ? `
` : `
▶️
Enter YouTube URL
`}
`; } if (this.block.type === 'markdown') { const selectedClass = this.isSelected ? ' selected' : ''; const showPreview = this.block.metadata?.showPreview !== false; return `
Markdown
${showPreview ? `
` : ` `}
`; } if (this.block.type === 'html') { const selectedClass = this.isSelected ? ' selected' : ''; const showPreview = this.block.metadata?.showPreview !== false; return `
HTML
${showPreview ? `
` : ` `}
`; } if (this.block.type === 'attachment') { const selectedClass = this.isSelected ? ' selected' : ''; const files = this.block.metadata?.files || []; return `
📎
File Attachments
${files.length > 0 ? files.map((file: any) => `
${this.getFileIcon(file.type)}
${file.name}
${this.formatFileSize(file.size)}
`).join('') : `
Click to add files
or drag and drop
`}
${files.length > 0 ? '' : ''}
`; } const placeholder = this.getPlaceholder(); const selectedClass = this.isSelected ? ' selected' : ''; return `
`; } private getPlaceholder(): string { switch (this.block.type) { case 'paragraph': return "Type '/' for commands..."; case 'heading-1': return 'Heading 1'; case 'heading-2': return 'Heading 2'; case 'heading-3': return 'Heading 3'; case 'quote': return 'Quote'; case 'image': return 'Click to upload an image'; default: return ''; } } public focus(): void { // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.focus) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.focus(container, context); } // Handle non-editable blocks const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment']; if (this.block && nonEditableTypes.includes(this.block.type)) { const blockElement = this.shadowRoot?.querySelector(`.block.${this.block.type}`) as HTMLDivElement; if (blockElement) { blockElement.focus(); } return; } // Get the actual editable element (might be nested for code blocks) const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (!editableElement) return; // Ensure the element is focusable if (!editableElement.hasAttribute('contenteditable')) { editableElement.setAttribute('contenteditable', 'true'); } editableElement.focus(); // If focus failed, try again after a microtask if (document.activeElement !== editableElement && this.shadowRoot?.activeElement !== editableElement) { Promise.resolve().then(() => { editableElement.focus(); }); } } public focusWithCursor(position: 'start' | 'end' | number = 'end'): void { console.log('focusWithCursor called', { blockId: this.block?.id, blockType: this.block?.type, position }); // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.focusWithCursor) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.focusWithCursor(container, position, context); } // Non-editable blocks don't support cursor positioning const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment']; if (this.block && nonEditableTypes.includes(this.block.type)) { this.focus(); return; } // Get the actual editable element (might be nested for code blocks) const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (!editableElement) return; // Ensure element is focusable first if (!editableElement.hasAttribute('contenteditable')) { editableElement.setAttribute('contenteditable', 'true'); } // Focus the element editableElement.focus(); // Set cursor position after focus is established const setCursor = () => { if (position === 'start') { this.setCursorToStart(); } else if (position === 'end') { this.setCursorToEnd(); } else if (typeof position === 'number') { // Use the new selection utility to set cursor position WysiwygSelection.setCursorPosition(editableElement, position); } }; // Ensure cursor is set after focus if (document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement) { setCursor(); } else { // Wait for focus to be established Promise.resolve().then(() => { if (document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement) { setCursor(); } }); } } /** * Get cursor position in the editable element */ public getCursorPosition(element: HTMLElement): number | null { // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.getCursorPosition) { const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.getCursorPosition(element, context); } // Get parent wysiwyg component's shadow root const parentComponent = this.closest('dees-input-wysiwyg'); const parentShadowRoot = parentComponent?.shadowRoot; // Get selection info with both shadow roots for proper traversal const shadowRoots: ShadowRoot[] = []; if (parentShadowRoot) shadowRoots.push(parentShadowRoot); if (this.shadowRoot) shadowRoots.push(this.shadowRoot); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); console.log('getCursorPosition: Selection info from shadow DOMs:', { selectionInfo, shadowRootsCount: shadowRoots.length }); if (!selectionInfo) { console.log('getCursorPosition: No selection found'); return null; } console.log('getCursorPosition: Range info:', { startContainer: selectionInfo.startContainer, startOffset: selectionInfo.startOffset, collapsed: selectionInfo.collapsed, startContainerText: selectionInfo.startContainer.textContent }); if (!element.contains(selectionInfo.startContainer)) { console.log('getCursorPosition: Range not in element'); return null; } // Create a range from start of element to cursor position const preCaretRange = document.createRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); // Get the text content length up to cursor const position = preCaretRange.toString().length; console.log('getCursorPosition: Calculated position:', { position, preCaretText: preCaretRange.toString(), elementText: element.textContent, elementTextLength: element.textContent?.length }); return position; } public getContent(): string { // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.getContent) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.getContent(container, context); } // Handle image blocks specially if (this.block?.type === 'image') { return this.block.content || ''; // Image blocks store alt text in content } // Get the actual editable element (might be nested for code blocks) const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (!editableElement) return ''; if (this.block.type === 'list') { const listItems = editableElement.querySelectorAll('li'); return Array.from(listItems).map(li => li.innerHTML || '').join('\n'); } else if (this.block.type === 'code') { return editableElement.textContent || ''; } else { // For regular blocks, get the innerHTML which includes formatting tags const content = editableElement.innerHTML || ''; console.log('Getting content from block:', content); return content; } } public setContent(content: string): void { // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.setContent) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.setContent(container, content, context); } // Get the actual editable element (might be nested for code blocks) const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (!editableElement) return; // Store if we have focus const hadFocus = document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement; if (this.block.type === 'list') { editableElement.innerHTML = WysiwygBlocks.renderListContent(content, this.block.metadata); } else if (this.block.type === 'code') { editableElement.textContent = content; } else { editableElement.innerHTML = content; } // Restore focus if we had it if (hadFocus) { editableElement.focus(); } } public setCursorToStart(): void { // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.setCursorToStart) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.setCursorToStart(container, context); } // Always find the element fresh, don't rely on cached blockElement const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (editableElement) { WysiwygBlocks.setCursorToStart(editableElement); } } public setCursorToEnd(): void { // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.setCursorToEnd) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.setCursorToEnd(container, context); } // Always find the element fresh, don't rely on cached blockElement const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (editableElement) { WysiwygBlocks.setCursorToEnd(editableElement); } } public focusListItem(): void { if (this.block.type === 'list') { const editableElement = this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (editableElement) { WysiwygBlocks.focusListItem(editableElement); } } } /** * Setup YouTube block functionality */ private setupYouTubeBlock(): void { const youtubeBlock = this.shadowRoot?.querySelector('.block.youtube') as HTMLDivElement; if (!youtubeBlock) return; // Handle click to select youtubeBlock.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (!target.classList.contains('youtube-url-input') && !target.classList.contains('youtube-embed-btn')) { e.stopPropagation(); youtubeBlock.focus(); this.handlers?.onFocus?.(); } }); // Handle URL input and embed button const urlInput = youtubeBlock.querySelector('.youtube-url-input') as HTMLInputElement; const embedBtn = youtubeBlock.querySelector('.youtube-embed-btn') as HTMLButtonElement; if (urlInput && embedBtn) { const embedVideo = () => { const url = urlInput.value.trim(); if (url) { // Extract video ID from YouTube URL const videoId = this.extractYouTubeVideoId(url); if (videoId) { this.block.metadata = { ...this.block.metadata, videoId, url }; this.block.content = url; // Store URL as content // Re-render the block const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container) { container.innerHTML = this.renderBlockContent(); this.setupYouTubeBlock(); // Re-setup event handlers } // Notify parent of change this.handlers?.onInput?.(new InputEvent('input')); } else { alert('Invalid YouTube URL'); } } }; embedBtn.addEventListener('click', embedVideo); urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); embedVideo(); } }); } // Handle focus/blur youtubeBlock.addEventListener('focus', () => this.handlers?.onFocus?.()); youtubeBlock.addEventListener('blur', () => this.handlers?.onBlur?.()); // Handle keyboard events youtubeBlock.addEventListener('keydown', (e) => { if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); this.handlers?.onKeyDown?.(e); } else { this.handlers?.onKeyDown?.(e); } }); } /** * Setup Markdown block functionality */ private setupMarkdownBlock(): void { const markdownBlock = this.shadowRoot?.querySelector('.block.markdown') as HTMLDivElement; if (!markdownBlock) return; // Handle click to select markdownBlock.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (!target.classList.contains('markdown-editor') && !target.classList.contains('toggle-preview')) { e.stopPropagation(); markdownBlock.focus(); this.handlers?.onFocus?.(); } }); // Handle preview toggle const toggleBtn = markdownBlock.querySelector('.toggle-preview') as HTMLButtonElement; if (toggleBtn) { toggleBtn.addEventListener('click', () => { const showPreview = toggleBtn.dataset.active !== 'true'; this.block.metadata = { ...this.block.metadata, showPreview }; // Re-render const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container) { container.innerHTML = this.renderBlockContent(); this.setupMarkdownBlock(); // If switching to preview, render markdown if (showPreview) { this.renderMarkdownPreview(); } } }); } // Handle editor input const editor = markdownBlock.querySelector('.markdown-editor') as HTMLTextAreaElement; if (editor) { editor.addEventListener('input', () => { this.block.content = editor.value; this.handlers?.onInput?.(new InputEvent('input')); }); // Auto-resize textarea const autoResize = () => { editor.style.height = 'auto'; editor.style.height = editor.scrollHeight + 'px'; }; editor.addEventListener('input', autoResize); autoResize(); } // Render preview if needed if (this.block.metadata?.showPreview) { this.renderMarkdownPreview(); } // Handle focus/blur markdownBlock.addEventListener('focus', () => this.handlers?.onFocus?.()); markdownBlock.addEventListener('blur', () => this.handlers?.onBlur?.()); // Handle keyboard events markdownBlock.addEventListener('keydown', (e) => { if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); this.handlers?.onKeyDown?.(e); } else { this.handlers?.onKeyDown?.(e); } }); } /** * Setup HTML block functionality */ private setupHtmlBlock(): void { const htmlBlock = this.shadowRoot?.querySelector('.block.html') as HTMLDivElement; if (!htmlBlock) return; // Handle click to select htmlBlock.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (!target.classList.contains('html-editor') && !target.classList.contains('toggle-preview')) { e.stopPropagation(); htmlBlock.focus(); this.handlers?.onFocus?.(); } }); // Handle preview toggle const toggleBtn = htmlBlock.querySelector('.toggle-preview') as HTMLButtonElement; if (toggleBtn) { toggleBtn.addEventListener('click', () => { const showPreview = toggleBtn.dataset.active !== 'true'; this.block.metadata = { ...this.block.metadata, showPreview }; // Re-render const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container) { container.innerHTML = this.renderBlockContent(); this.setupHtmlBlock(); // If switching to preview, render HTML if (showPreview) { this.renderHtmlPreview(); } } }); } // Handle editor input const editor = htmlBlock.querySelector('.html-editor') as HTMLTextAreaElement; if (editor) { editor.addEventListener('input', () => { this.block.content = editor.value; this.handlers?.onInput?.(new InputEvent('input')); }); // Auto-resize textarea const autoResize = () => { editor.style.height = 'auto'; editor.style.height = editor.scrollHeight + 'px'; }; editor.addEventListener('input', autoResize); autoResize(); } // Render preview if needed if (this.block.metadata?.showPreview) { this.renderHtmlPreview(); } // Handle focus/blur htmlBlock.addEventListener('focus', () => this.handlers?.onFocus?.()); htmlBlock.addEventListener('blur', () => this.handlers?.onBlur?.()); // Handle keyboard events htmlBlock.addEventListener('keydown', (e) => { if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); this.handlers?.onKeyDown?.(e); } else { this.handlers?.onKeyDown?.(e); } }); } /** * Setup Attachment block functionality */ private setupAttachmentBlock(): void { const attachmentBlock = this.shadowRoot?.querySelector('.block.attachment') as HTMLDivElement; if (!attachmentBlock) return; // Handle click to select attachmentBlock.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (!target.classList.contains('remove-file')) { e.stopPropagation(); attachmentBlock.focus(); this.handlers?.onFocus?.(); } }); // Handle file input const fileInput = attachmentBlock.querySelector('input[type="file"]') as HTMLInputElement; const placeholder = attachmentBlock.querySelector('.attachment-placeholder'); const addMoreBtn = attachmentBlock.querySelector('.add-more-files') as HTMLButtonElement; const triggerFileInput = () => { if (fileInput) fileInput.click(); }; if (placeholder) { placeholder.addEventListener('click', triggerFileInput); } if (addMoreBtn) { addMoreBtn.addEventListener('click', triggerFileInput); } if (fileInput) { fileInput.addEventListener('change', async (e) => { const files = Array.from((e.target as HTMLInputElement).files || []); if (files.length > 0) { await this.handleFileAttachments(files); } }); } // Handle file removal attachmentBlock.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (target.classList.contains('remove-file')) { const fileId = target.dataset.fileId; if (fileId) { const files = this.block.metadata?.files || []; this.block.metadata = { ...this.block.metadata, files: files.filter((f: any) => f.id !== fileId) }; // Re-render const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container) { container.innerHTML = this.renderBlockContent(); this.setupAttachmentBlock(); } this.handlers?.onInput?.(new InputEvent('input')); } } }); // Handle drag and drop attachmentBlock.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); attachmentBlock.classList.add('drag-over'); }); attachmentBlock.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); attachmentBlock.classList.remove('drag-over'); }); attachmentBlock.addEventListener('drop', async (e) => { e.preventDefault(); e.stopPropagation(); attachmentBlock.classList.remove('drag-over'); const files = Array.from(e.dataTransfer?.files || []); if (files.length > 0) { await this.handleFileAttachments(files); } }); // Handle focus/blur attachmentBlock.addEventListener('focus', () => this.handlers?.onFocus?.()); attachmentBlock.addEventListener('blur', () => this.handlers?.onBlur?.()); // Handle keyboard events attachmentBlock.addEventListener('keydown', (e) => { if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); this.handlers?.onKeyDown?.(e); } else { this.handlers?.onKeyDown?.(e); } }); } /** * Extract YouTube video ID from URL */ private extractYouTubeVideoId(url: string): string | null { const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/; const match = url.match(regex); return match ? match[1] : null; } /** * Render Markdown preview */ private async renderMarkdownPreview(): Promise { const preview = this.shadowRoot?.querySelector('.markdown-preview') as HTMLDivElement; if (!preview || !this.block.content) return; // Simple markdown to HTML conversion (you might want to use a proper markdown parser) let html = this.block.content .replace(/^### (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^# (.*$)/gim, '

$1

') .replace(/\*\*(.*)\*\*/g, '$1') .replace(/\*(.*)\*/g, '$1') .replace(/\[([^\]]*)\]\(([^\)]*)\)/g, '$1') .replace(/\n/g, '
'); preview.innerHTML = html; } /** * Render HTML preview */ private renderHtmlPreview(): void { const preview = this.shadowRoot?.querySelector('.html-preview') as HTMLDivElement; if (!preview || !this.block.content) return; // Render HTML in a sandboxed way preview.innerHTML = this.block.content; } /** * Handle file attachments */ private async handleFileAttachments(files: File[]): Promise { const existingFiles = this.block.metadata?.files || []; const newFiles: any[] = []; for (const file of files) { // Convert to base64 for storage (in production, upload to server) const reader = new FileReader(); const base64 = await new Promise((resolve) => { reader.onload = (e) => resolve(e.target?.result as string); reader.readAsDataURL(file); }); newFiles.push({ id: `file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, name: file.name, size: file.size, type: file.type, data: base64 }); } this.block.metadata = { ...this.block.metadata, files: [...existingFiles, ...newFiles] }; // Re-render const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container) { container.innerHTML = this.renderBlockContent(); this.setupAttachmentBlock(); } this.handlers?.onInput?.(new InputEvent('input')); } /** * Get file icon based on mime type */ private getFileIcon(mimeType: string): string { if (mimeType.startsWith('image/')) return '🖼️'; if (mimeType.startsWith('video/')) return '🎥'; if (mimeType.startsWith('audio/')) return '🎵'; if (mimeType.includes('pdf')) return '📄'; if (mimeType.includes('zip') || mimeType.includes('compressed')) return '🗄️'; if (mimeType.includes('sheet') || mimeType.includes('excel')) return '📊'; if (mimeType.includes('document') || mimeType.includes('word')) return '📝'; if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return '📋'; if (mimeType.includes('text')) return '📃'; return '📁'; } /** * Format file size to human readable */ private formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Setup image block functionality */ private setupImageBlock(): void { const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; if (!imageBlock) return; // Note: tabindex is already set in the HTML // Handle click to select the block imageBlock.addEventListener('click', (e) => { // Don't stop propagation for file input clicks if ((e.target as HTMLElement).tagName !== 'INPUT') { e.stopPropagation(); // Focus will trigger the selection imageBlock.focus(); // Ensure focus handler is called immediately this.handlers?.onFocus?.(); } }); // Handle click on upload placeholder const uploadPlaceholder = imageBlock.querySelector('.image-upload-placeholder'); const fileInput = imageBlock.querySelector('input[type="file"]') as HTMLInputElement; if (uploadPlaceholder && fileInput) { uploadPlaceholder.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { this.handleImageUpload(file); } }); // Handle drag and drop imageBlock.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); uploadPlaceholder.classList.add('drag-over'); }); imageBlock.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); uploadPlaceholder.classList.remove('drag-over'); }); imageBlock.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); uploadPlaceholder.classList.remove('drag-over'); const files = e.dataTransfer?.files; if (files && files.length > 0) { const file = files[0]; if (file.type.startsWith('image/')) { this.handleImageUpload(file); } } }); } // Handle focus/blur for the image block imageBlock.addEventListener('focus', () => { this.handlers?.onFocus?.(); }); imageBlock.addEventListener('blur', () => { this.handlers?.onBlur?.(); }); // Handle keyboard events imageBlock.addEventListener('keydown', (e) => { if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); // Let the keyboard handler in the parent component handle the deletion this.handlers?.onKeyDown?.(e); } else { // Handle navigation keys this.handlers?.onKeyDown?.(e); } }); } /** * Handle image file upload */ private async handleImageUpload(file: File): Promise { // Check file size (max 10MB) if (file.size > 10 * 1024 * 1024) { alert('Image size must be less than 10MB'); return; } // Update block to show loading state this.block.metadata = { ...this.block.metadata, loading: true }; const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement; if (container) { container.innerHTML = this.renderBlockContent(); this.setupImageBlock(); // Re-setup event handlers } try { // Convert to base64 for now (in production, you'd upload to a server) const reader = new FileReader(); reader.onload = (e) => { const base64 = e.target?.result as string; // Update block with image URL this.block.metadata = { ...this.block.metadata, url: base64, loading: false, fileName: file.name, fileSize: file.size, mimeType: file.type }; // Set alt text as content this.block.content = file.name.replace(/\.[^/.]+$/, ''); // Remove extension // Re-render if (container) { container.innerHTML = this.renderBlockContent(); } // Notify parent component of the change this.handlers?.onInput?.(new InputEvent('input')); }; reader.onerror = () => { alert('Failed to read image file'); this.block.metadata = { ...this.block.metadata, loading: false }; if (container) { container.innerHTML = this.renderBlockContent(); this.setupImageBlock(); } }; reader.readAsDataURL(file); } catch (error) { console.error('Error uploading image:', error); alert('Failed to upload image'); this.block.metadata = { ...this.block.metadata, loading: false }; if (container) { container.innerHTML = this.renderBlockContent(); this.setupImageBlock(); } } } /** * Gets content split at cursor position */ public getSplitContent(): { before: string; after: string } | null { console.log('getSplitContent: Starting...'); // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); console.log('getSplitContent: Checking for handler', { blockType: this.block.type, hasHandler: !!handler, hasSplitMethod: !!(handler && handler.getSplitContent) }); if (handler && handler.getSplitContent) { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement; console.log('getSplitContent: Found container', { container: !!container, containerHTML: container?.innerHTML?.substring(0, 100) }); const context = { shadowRoot: this.shadowRoot!, component: this }; return handler.getSplitContent(container, context); } // Image blocks can't be split if (this.block?.type === 'image') { return null; } // Get the actual editable element first const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (!editableElement) { console.log('getSplitContent: No editable element found'); return null; } console.log('getSplitContent: Element info:', { blockType: this.block.type, innerHTML: editableElement.innerHTML, textContent: editableElement.textContent, textLength: editableElement.textContent?.length }); // Get parent wysiwyg component's shadow root const parentComponent = this.closest('dees-input-wysiwyg'); const parentShadowRoot = parentComponent?.shadowRoot; // Get selection info with both shadow roots for proper traversal const shadowRoots: ShadowRoot[] = []; if (parentShadowRoot) shadowRoots.push(parentShadowRoot); if (this.shadowRoot) shadowRoots.push(this.shadowRoot); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); console.log('getSplitContent: Selection info from shadow DOMs:', { selectionInfo, shadowRootsCount: shadowRoots.length }); if (!selectionInfo) { console.log('getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition); // Try using last known cursor position if (this.lastKnownCursorPosition !== null) { const fullText = editableElement.textContent || ''; const pos = Math.min(this.lastKnownCursorPosition, fullText.length); console.log('getSplitContent: Splitting with last known position:', { pos, fullTextLength: fullText.length, before: fullText.substring(0, pos), after: fullText.substring(pos) }); return { before: fullText.substring(0, pos), after: fullText.substring(pos) }; } return null; } console.log('getSplitContent: Selection range:', { startContainer: selectionInfo.startContainer, startOffset: selectionInfo.startOffset, startContainerInElement: editableElement.contains(selectionInfo.startContainer) }); // Make sure the selection is within this block if (!WysiwygSelection.containsAcrossShadowDOM(editableElement, selectionInfo.startContainer)) { console.log('getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition); // Try using last known cursor position if (this.lastKnownCursorPosition !== null) { const fullText = editableElement.textContent || ''; const pos = Math.min(this.lastKnownCursorPosition, fullText.length); return { before: fullText.substring(0, pos), after: fullText.substring(pos) }; } return null; } // For code blocks, use simple text splitting if (this.block.type === 'code') { const cursorPos = this.getCursorPosition(editableElement) || 0; const fullText = editableElement.textContent || ''; console.log('getSplitContent: Code block split:', { cursorPos, fullTextLength: fullText.length, before: fullText.substring(0, cursorPos), after: fullText.substring(cursorPos) }); return { before: fullText.substring(0, cursorPos), after: fullText.substring(cursorPos) }; } // For HTML content, get cursor position first const cursorPos = this.getCursorPosition(editableElement); console.log('getSplitContent: Cursor position for HTML split:', cursorPos); if (cursorPos === null || cursorPos === 0) { // If cursor is at start or can't determine position, move all content console.log('getSplitContent: Cursor at start or null, moving all content'); return { before: '', after: editableElement.innerHTML }; } // For HTML content, split using ranges to preserve formatting const beforeRange = document.createRange(); const afterRange = document.createRange(); // Before range: from start of element to cursor beforeRange.setStart(editableElement, 0); beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); // After range: from cursor to end of element afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset); afterRange.setEnd(editableElement, editableElement.childNodes.length); // Extract HTML content const beforeFragment = beforeRange.cloneContents(); const afterFragment = afterRange.cloneContents(); // Convert to HTML strings const tempDiv = document.createElement('div'); tempDiv.appendChild(beforeFragment); const beforeHtml = tempDiv.innerHTML; tempDiv.innerHTML = ''; tempDiv.appendChild(afterFragment); const afterHtml = tempDiv.innerHTML; console.log('getSplitContent: Final split result:', { cursorPos, beforeHtml, beforeLength: beforeHtml.length, beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''), afterHtml, afterLength: afterHtml.length, afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '') }); return { before: beforeHtml, after: afterHtml }; } }