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'; 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: { onInput: (e: InputEvent) => void; onKeyDown: (e: KeyboardEvent) => void; onFocus: () => void; onBlur: () => void; onCompositionStart: () => void; onCompositionEnd: () => void; onMouseUp?: (e: MouseEvent) => void; }; // 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 = ''; 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; } .block.divider { padding: 0; margin: 16px 0; pointer-events: none; } .block.divider hr { border: none; border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; margin: 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; } .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; } `, ]; protected shouldUpdate(changedProperties: Map): boolean { // Never update if only the block content changed if (changedProperties.has('block') && this.block) { const oldBlock = changedProperties.get('block'); if (oldBlock && 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; // 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(); } // Handle image block setup if (this.block.type === 'image') { this.setupImageBlock(); return; // Image blocks don't need the standard editable setup } // 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; console.log('Cursor position after mouseup:', 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; console.log('Cursor position after click:', 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 = currentEditableBlock.contains(selectionInfo.startContainer); const endInBlock = currentEditableBlock.contains(selectionInfo.endContainer); if (startInBlock || endInBlock) { if (selectedText !== this.lastSelectedText) { this.lastSelectedText = selectedText; console.log('✅ Selection detected in block using getComposedRanges:', 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 ''; if (this.block.type === 'divider') { return `

`; } 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
`}
`; } 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 { // Image blocks don't focus in the traditional way if (this.block?.type === 'image') { const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; if (imageBlock) { imageBlock.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 { // Image blocks don't support cursor positioning if (this.block?.type === 'image') { 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 { // 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 { // 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 { // 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 { const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.blockElement; if (editableElement) { WysiwygBlocks.setCursorToStart(editableElement); } } public setCursorToEnd(): void { const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.blockElement; 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 image block functionality */ private setupImageBlock(): void { const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; if (!imageBlock) return; // Make the image block focusable imageBlock.setAttribute('tabindex', '0'); // 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) => { 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...'); // 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 (!editableElement.contains(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 }; } }