refactor
This commit is contained in:
411
ts_web/elements/wysiwyg/blocks/text/code.block.ts
Normal file
411
ts_web/elements/wysiwyg/blocks/text/code.block.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
||||
import type { IBlock } from '../../wysiwyg.types.js';
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
|
||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||
|
||||
export class CodeBlockHandler extends BaseBlockHandler {
|
||||
type = 'code';
|
||||
|
||||
// Track cursor position
|
||||
private lastKnownCursorPosition: number = 0;
|
||||
|
||||
render(block: IBlock, isSelected: boolean): string {
|
||||
const language = block.metadata?.language || 'plain text';
|
||||
const selectedClass = isSelected ? ' selected' : '';
|
||||
|
||||
console.log('CodeBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, language });
|
||||
|
||||
return `
|
||||
<div class="code-block-container">
|
||||
<div class="code-language">${language}</div>
|
||||
<div
|
||||
class="block code${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-block-id="${block.id}"
|
||||
data-block-type="${block.type}"
|
||||
spellcheck="false"
|
||||
>${block.content || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) {
|
||||
console.error('CodeBlockHandler.setup: No code block element found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('CodeBlockHandler.setup: Setting up code block', { blockId: block.id });
|
||||
|
||||
// Set initial content if needed - use textContent for code blocks
|
||||
if (block.content && !codeBlock.textContent) {
|
||||
codeBlock.textContent = block.content;
|
||||
}
|
||||
|
||||
// Input handler
|
||||
codeBlock.addEventListener('input', (e) => {
|
||||
console.log('CodeBlockHandler: Input event', { blockId: block.id });
|
||||
handlers.onInput(e as InputEvent);
|
||||
|
||||
// Track cursor position after input
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
});
|
||||
|
||||
// Keydown handler
|
||||
codeBlock.addEventListener('keydown', (e) => {
|
||||
// Track cursor position before keydown
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
|
||||
// Special handling for Tab key in code blocks
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
// Insert two spaces for tab
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const textNode = document.createTextNode(' ');
|
||||
range.insertNode(textNode);
|
||||
range.setStartAfter(textNode);
|
||||
range.setEndAfter(textNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
// Trigger input event
|
||||
handlers.onInput(new InputEvent('input'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handlers.onKeyDown(e);
|
||||
});
|
||||
|
||||
// Focus handler
|
||||
codeBlock.addEventListener('focus', () => {
|
||||
console.log('CodeBlockHandler: Focus event', { blockId: block.id });
|
||||
handlers.onFocus();
|
||||
});
|
||||
|
||||
// Blur handler
|
||||
codeBlock.addEventListener('blur', () => {
|
||||
console.log('CodeBlockHandler: Blur event', { blockId: block.id });
|
||||
handlers.onBlur();
|
||||
});
|
||||
|
||||
// Composition handlers for IME support
|
||||
codeBlock.addEventListener('compositionstart', () => {
|
||||
console.log('CodeBlockHandler: Composition start', { blockId: block.id });
|
||||
handlers.onCompositionStart();
|
||||
});
|
||||
|
||||
codeBlock.addEventListener('compositionend', () => {
|
||||
console.log('CodeBlockHandler: Composition end', { blockId: block.id });
|
||||
handlers.onCompositionEnd();
|
||||
});
|
||||
|
||||
// Mouse up handler
|
||||
codeBlock.addEventListener('mouseup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
handlers.onMouseUp?.(e);
|
||||
});
|
||||
|
||||
// Click handler with delayed cursor tracking
|
||||
codeBlock.addEventListener('click', (e: MouseEvent) => {
|
||||
setTimeout(() => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Keyup handler for cursor tracking
|
||||
codeBlock.addEventListener('keyup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
});
|
||||
|
||||
// Paste handler - handle as plain text
|
||||
codeBlock.addEventListener('paste', (e) => {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData?.getData('text/plain');
|
||||
if (text) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const textNode = document.createTextNode(text);
|
||||
range.insertNode(textNode);
|
||||
range.setStartAfter(textNode);
|
||||
range.setEndAfter(textNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
// Trigger input event
|
||||
handlers.onInput(new InputEvent('input'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStyles(): string {
|
||||
return `
|
||||
/* Code block specific styles */
|
||||
.code-block-container {
|
||||
position: relative;
|
||||
margin: 20px 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: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getPlaceholder(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Helper methods for code functionality
|
||||
|
||||
getCursorPosition(element: HTMLElement, context?: any): number | null {
|
||||
// Get the actual code element
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) {
|
||||
console.log('CodeBlockHandler.getCursorPosition: No code element found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
|
||||
if (!selectionInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a range from start of element to cursor position
|
||||
const preCaretRange = document.createRange();
|
||||
preCaretRange.selectNodeContents(codeBlock);
|
||||
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// Get the text content length up to cursor
|
||||
const position = preCaretRange.toString().length;
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
getContent(element: HTMLElement, context?: any): string {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) return '';
|
||||
|
||||
// For code blocks, get textContent to avoid HTML formatting
|
||||
const content = codeBlock.textContent || '';
|
||||
console.log('CodeBlockHandler.getContent:', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
setContent(element: HTMLElement, content: string, context?: any): void {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) return;
|
||||
|
||||
// Store if we have focus
|
||||
const hadFocus = document.activeElement === codeBlock ||
|
||||
element.shadowRoot?.activeElement === codeBlock;
|
||||
|
||||
// Use textContent for code blocks
|
||||
codeBlock.textContent = content;
|
||||
|
||||
// Restore focus if we had it
|
||||
if (hadFocus) {
|
||||
codeBlock.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToStart(element: HTMLElement, context?: any): void {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (codeBlock) {
|
||||
WysiwygBlocks.setCursorToStart(codeBlock);
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToEnd(element: HTMLElement, context?: any): void {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (codeBlock) {
|
||||
WysiwygBlocks.setCursorToEnd(codeBlock);
|
||||
}
|
||||
}
|
||||
|
||||
focus(element: HTMLElement, context?: any): void {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) return;
|
||||
|
||||
// Ensure the element is focusable
|
||||
if (!codeBlock.hasAttribute('contenteditable')) {
|
||||
codeBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
codeBlock.focus();
|
||||
|
||||
// If focus failed, try again after a microtask
|
||||
if (document.activeElement !== codeBlock && element.shadowRoot?.activeElement !== codeBlock) {
|
||||
Promise.resolve().then(() => {
|
||||
codeBlock.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) return;
|
||||
|
||||
// Ensure element is focusable first
|
||||
if (!codeBlock.hasAttribute('contenteditable')) {
|
||||
codeBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
// Focus the element
|
||||
codeBlock.focus();
|
||||
|
||||
// Set cursor position after focus is established
|
||||
const setCursor = () => {
|
||||
if (position === 'start') {
|
||||
this.setCursorToStart(element, context);
|
||||
} else if (position === 'end') {
|
||||
this.setCursorToEnd(element, context);
|
||||
} else if (typeof position === 'number') {
|
||||
// Use the selection utility to set cursor position
|
||||
WysiwygSelection.setCursorPosition(codeBlock, position);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure cursor is set after focus
|
||||
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
|
||||
setCursor();
|
||||
} else {
|
||||
// Wait for focus to be established
|
||||
Promise.resolve().then(() => {
|
||||
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
|
||||
setCursor();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
|
||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
||||
if (!codeBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
|
||||
if (!selectionInfo) {
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = codeBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
return {
|
||||
before: fullText.substring(0, pos),
|
||||
after: fullText.substring(pos)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make sure the selection is within this block
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = codeBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
return {
|
||||
before: fullText.substring(0, pos),
|
||||
after: fullText.substring(pos)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get cursor position
|
||||
const cursorPos = this.getCursorPosition(element, context);
|
||||
|
||||
if (cursorPos === null || cursorPos === 0) {
|
||||
// If cursor is at start or can't determine position, move all content
|
||||
return {
|
||||
before: '',
|
||||
after: codeBlock.textContent || ''
|
||||
};
|
||||
}
|
||||
|
||||
// For code blocks, split based on text content only
|
||||
const fullText = codeBlock.textContent || '';
|
||||
|
||||
return {
|
||||
before: fullText.substring(0, cursorPos),
|
||||
after: fullText.substring(cursorPos)
|
||||
};
|
||||
}
|
||||
}
|
||||
566
ts_web/elements/wysiwyg/blocks/text/heading.block.ts
Normal file
566
ts_web/elements/wysiwyg/blocks/text/heading.block.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
||||
import type { IBlock } from '../../wysiwyg.types.js';
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
|
||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||
|
||||
export class HeadingBlockHandler extends BaseBlockHandler {
|
||||
type: string;
|
||||
private level: 1 | 2 | 3;
|
||||
|
||||
// Track cursor position
|
||||
private lastKnownCursorPosition: number = 0;
|
||||
private lastSelectedText: string = '';
|
||||
private selectionHandler: (() => void) | null = null;
|
||||
|
||||
constructor(type: 'heading-1' | 'heading-2' | 'heading-3') {
|
||||
super();
|
||||
this.type = type;
|
||||
this.level = parseInt(type.split('-')[1]) as 1 | 2 | 3;
|
||||
}
|
||||
|
||||
render(block: IBlock, isSelected: boolean): string {
|
||||
const selectedClass = isSelected ? ' selected' : '';
|
||||
const placeholder = this.getPlaceholder();
|
||||
|
||||
console.log('HeadingBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, level: this.level });
|
||||
|
||||
return `
|
||||
<div
|
||||
class="block heading-${this.level}${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-placeholder="${placeholder}"
|
||||
data-block-id="${block.id}"
|
||||
data-block-type="${block.type}"
|
||||
>${block.content || ''}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) {
|
||||
console.error('HeadingBlockHandler.setup: No heading block element found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('HeadingBlockHandler.setup: Setting up heading block', { blockId: block.id, level: this.level });
|
||||
|
||||
// Set initial content if needed
|
||||
if (block.content && !headingBlock.innerHTML) {
|
||||
headingBlock.innerHTML = block.content;
|
||||
}
|
||||
|
||||
// Input handler with cursor tracking
|
||||
headingBlock.addEventListener('input', (e) => {
|
||||
console.log('HeadingBlockHandler: Input event', { blockId: block.id });
|
||||
handlers.onInput(e as InputEvent);
|
||||
|
||||
// Track cursor position after input
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('HeadingBlockHandler: Updated cursor position after input', { pos });
|
||||
}
|
||||
});
|
||||
|
||||
// Keydown handler with cursor tracking
|
||||
headingBlock.addEventListener('keydown', (e) => {
|
||||
// Track cursor position before keydown
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('HeadingBlockHandler: Cursor position before keydown', { pos, key: e.key });
|
||||
}
|
||||
|
||||
handlers.onKeyDown(e);
|
||||
});
|
||||
|
||||
// Focus handler
|
||||
headingBlock.addEventListener('focus', () => {
|
||||
console.log('HeadingBlockHandler: Focus event', { blockId: block.id });
|
||||
handlers.onFocus();
|
||||
});
|
||||
|
||||
// Blur handler
|
||||
headingBlock.addEventListener('blur', () => {
|
||||
console.log('HeadingBlockHandler: Blur event', { blockId: block.id });
|
||||
handlers.onBlur();
|
||||
});
|
||||
|
||||
// Composition handlers for IME support
|
||||
headingBlock.addEventListener('compositionstart', () => {
|
||||
console.log('HeadingBlockHandler: Composition start', { blockId: block.id });
|
||||
handlers.onCompositionStart();
|
||||
});
|
||||
|
||||
headingBlock.addEventListener('compositionend', () => {
|
||||
console.log('HeadingBlockHandler: Composition end', { blockId: block.id });
|
||||
handlers.onCompositionEnd();
|
||||
});
|
||||
|
||||
// Mouse up handler
|
||||
headingBlock.addEventListener('mouseup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('HeadingBlockHandler: Cursor position after mouseup', { pos });
|
||||
}
|
||||
|
||||
// Selection will be handled by selectionchange event
|
||||
handlers.onMouseUp?.(e);
|
||||
});
|
||||
|
||||
// Click handler with delayed cursor tracking
|
||||
headingBlock.addEventListener('click', (e: MouseEvent) => {
|
||||
// Small delay to let browser set cursor position
|
||||
setTimeout(() => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('HeadingBlockHandler: Cursor position after click', { pos });
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Keyup handler for additional cursor tracking
|
||||
headingBlock.addEventListener('keyup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('HeadingBlockHandler: Cursor position after keyup', { pos, key: e.key });
|
||||
}
|
||||
});
|
||||
|
||||
// Set up selection change handler
|
||||
this.setupSelectionHandler(element, headingBlock, block);
|
||||
}
|
||||
|
||||
private setupSelectionHandler(element: HTMLElement, headingBlock: HTMLDivElement, block: IBlock): void {
|
||||
// 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.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parent wysiwyg component's shadow root - in setup, we need to traverse
|
||||
const wysiwygBlock = (headingBlock.getRootNode() as ShadowRoot).host as any;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = wysiwygBlock?.shadowRoot;
|
||||
|
||||
// Use getComposedRanges with shadow roots as per MDN docs
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
// 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(headingBlock, selectionInfo.startContainer);
|
||||
const endInBlock = WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.endContainer);
|
||||
|
||||
if (startInBlock || endInBlock) {
|
||||
if (selectedText !== this.lastSelectedText) {
|
||||
this.lastSelectedText = selectedText;
|
||||
|
||||
console.log('HeadingBlockHandler: Text selected', {
|
||||
text: selectedText,
|
||||
blockId: block.id
|
||||
});
|
||||
|
||||
// Create range and get rect
|
||||
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Dispatch event
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: selectedText.trim(),
|
||||
blockId: block.id,
|
||||
range: range,
|
||||
rect: rect,
|
||||
hasSelection: true
|
||||
});
|
||||
}
|
||||
} else if (this.lastSelectedText) {
|
||||
// Clear selection if no longer in this block
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for selection changes
|
||||
document.addEventListener('selectionchange', checkSelection);
|
||||
|
||||
// Store the handler for cleanup
|
||||
this.selectionHandler = checkSelection;
|
||||
|
||||
// Clean up on disconnect (will be called by dees-wysiwyg-block)
|
||||
const wysiwygBlock = (headingBlock.getRootNode() as ShadowRoot).host as any;
|
||||
if (wysiwygBlock) {
|
||||
const originalDisconnectedCallback = (wysiwygBlock as any).disconnectedCallback;
|
||||
(wysiwygBlock as any).disconnectedCallback = async function() {
|
||||
if (this.selectionHandler) {
|
||||
document.removeEventListener('selectionchange', this.selectionHandler);
|
||||
this.selectionHandler = null;
|
||||
}
|
||||
if (originalDisconnectedCallback) {
|
||||
await originalDisconnectedCallback.call(wysiwygBlock);
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchSelectionEvent(element: HTMLElement, detail: any): void {
|
||||
const event = new CustomEvent('block-text-selected', {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
getStyles(): string {
|
||||
// Return styles for all heading levels
|
||||
return `
|
||||
.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')};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getPlaceholder(): string {
|
||||
switch(this.level) {
|
||||
case 1:
|
||||
return 'Heading 1';
|
||||
case 2:
|
||||
return 'Heading 2';
|
||||
case 3:
|
||||
return 'Heading 3';
|
||||
default:
|
||||
return 'Heading';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for heading functionality (mostly the same as paragraph)
|
||||
|
||||
getCursorPosition(element: HTMLElement, context?: any): number | null {
|
||||
// Get the actual heading element
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) {
|
||||
console.log('HeadingBlockHandler.getCursorPosition: No heading element found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
console.log('HeadingBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
|
||||
selectionInfo,
|
||||
shadowRootsCount: shadowRoots.length
|
||||
});
|
||||
|
||||
if (!selectionInfo) {
|
||||
console.log('HeadingBlockHandler.getCursorPosition: No selection found');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('HeadingBlockHandler.getCursorPosition: Range info:', {
|
||||
startContainer: selectionInfo.startContainer,
|
||||
startOffset: selectionInfo.startOffset,
|
||||
collapsed: selectionInfo.collapsed,
|
||||
startContainerText: selectionInfo.startContainer.textContent
|
||||
});
|
||||
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) {
|
||||
console.log('HeadingBlockHandler.getCursorPosition: Range not in element');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a range from start of element to cursor position
|
||||
const preCaretRange = document.createRange();
|
||||
preCaretRange.selectNodeContents(headingBlock);
|
||||
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// Get the text content length up to cursor
|
||||
const position = preCaretRange.toString().length;
|
||||
console.log('HeadingBlockHandler.getCursorPosition: Calculated position:', {
|
||||
position,
|
||||
preCaretText: preCaretRange.toString(),
|
||||
elementText: headingBlock.textContent,
|
||||
elementTextLength: headingBlock.textContent?.length
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
getContent(element: HTMLElement, context?: any): string {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) return '';
|
||||
|
||||
// For headings, get the innerHTML which includes formatting tags
|
||||
const content = headingBlock.innerHTML || '';
|
||||
console.log('HeadingBlockHandler.getContent:', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
setContent(element: HTMLElement, content: string, context?: any): void {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) return;
|
||||
|
||||
// Store if we have focus
|
||||
const hadFocus = document.activeElement === headingBlock ||
|
||||
element.shadowRoot?.activeElement === headingBlock;
|
||||
|
||||
headingBlock.innerHTML = content;
|
||||
|
||||
// Restore focus if we had it
|
||||
if (hadFocus) {
|
||||
headingBlock.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToStart(element: HTMLElement, context?: any): void {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (headingBlock) {
|
||||
WysiwygBlocks.setCursorToStart(headingBlock);
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToEnd(element: HTMLElement, context?: any): void {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (headingBlock) {
|
||||
WysiwygBlocks.setCursorToEnd(headingBlock);
|
||||
}
|
||||
}
|
||||
|
||||
focus(element: HTMLElement, context?: any): void {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) return;
|
||||
|
||||
// Ensure the element is focusable
|
||||
if (!headingBlock.hasAttribute('contenteditable')) {
|
||||
headingBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
headingBlock.focus();
|
||||
|
||||
// If focus failed, try again after a microtask
|
||||
if (document.activeElement !== headingBlock && element.shadowRoot?.activeElement !== headingBlock) {
|
||||
Promise.resolve().then(() => {
|
||||
headingBlock.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) return;
|
||||
|
||||
// Ensure element is focusable first
|
||||
if (!headingBlock.hasAttribute('contenteditable')) {
|
||||
headingBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
// Focus the element
|
||||
headingBlock.focus();
|
||||
|
||||
// Set cursor position after focus is established
|
||||
const setCursor = () => {
|
||||
if (position === 'start') {
|
||||
this.setCursorToStart(element, context);
|
||||
} else if (position === 'end') {
|
||||
this.setCursorToEnd(element, context);
|
||||
} else if (typeof position === 'number') {
|
||||
// Use the selection utility to set cursor position
|
||||
WysiwygSelection.setCursorPosition(headingBlock, position);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure cursor is set after focus
|
||||
if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) {
|
||||
setCursor();
|
||||
} else {
|
||||
// Wait for focus to be established
|
||||
Promise.resolve().then(() => {
|
||||
if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) {
|
||||
setCursor();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
|
||||
console.log('HeadingBlockHandler.getSplitContent: Starting...');
|
||||
|
||||
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
|
||||
if (!headingBlock) {
|
||||
console.log('HeadingBlockHandler.getSplitContent: No heading element found');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('HeadingBlockHandler.getSplitContent: Element info:', {
|
||||
innerHTML: headingBlock.innerHTML,
|
||||
textContent: headingBlock.textContent,
|
||||
textLength: headingBlock.textContent?.length
|
||||
});
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
console.log('HeadingBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
|
||||
selectionInfo,
|
||||
shadowRootsCount: shadowRoots.length
|
||||
});
|
||||
|
||||
if (!selectionInfo) {
|
||||
console.log('HeadingBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = headingBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
console.log('HeadingBlockHandler.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('HeadingBlockHandler.getSplitContent: Selection range:', {
|
||||
startContainer: selectionInfo.startContainer,
|
||||
startOffset: selectionInfo.startOffset,
|
||||
startContainerInElement: headingBlock.contains(selectionInfo.startContainer)
|
||||
});
|
||||
|
||||
// Make sure the selection is within this block
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) {
|
||||
console.log('HeadingBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = headingBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
return {
|
||||
before: fullText.substring(0, pos),
|
||||
after: fullText.substring(pos)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get cursor position first
|
||||
const cursorPos = this.getCursorPosition(element, context);
|
||||
console.log('HeadingBlockHandler.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('HeadingBlockHandler.getSplitContent: Cursor at start or null, moving all content');
|
||||
return {
|
||||
before: '',
|
||||
after: headingBlock.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(headingBlock, 0);
|
||||
beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// After range: from cursor to end of element
|
||||
afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
afterRange.setEnd(headingBlock, headingBlock.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('HeadingBlockHandler.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
|
||||
};
|
||||
}
|
||||
}
|
||||
458
ts_web/elements/wysiwyg/blocks/text/list.block.ts
Normal file
458
ts_web/elements/wysiwyg/blocks/text/list.block.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
||||
import type { IBlock } from '../../wysiwyg.types.js';
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
|
||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||
|
||||
export class ListBlockHandler extends BaseBlockHandler {
|
||||
type = 'list';
|
||||
|
||||
// Track cursor position and list state
|
||||
private lastKnownCursorPosition: number = 0;
|
||||
private lastSelectedText: string = '';
|
||||
private selectionHandler: (() => void) | null = null;
|
||||
|
||||
render(block: IBlock, isSelected: boolean): string {
|
||||
const selectedClass = isSelected ? ' selected' : '';
|
||||
const listType = block.metadata?.listType || 'unordered';
|
||||
const listTag = listType === 'ordered' ? 'ol' : 'ul';
|
||||
|
||||
console.log('ListBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, listType });
|
||||
|
||||
// Render list content
|
||||
const listContent = this.renderListContent(block.content, block.metadata);
|
||||
|
||||
return `
|
||||
<div
|
||||
class="block list${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-block-id="${block.id}"
|
||||
data-block-type="${block.type}"
|
||||
>${listContent}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderListContent(content: string | undefined, metadata: any): string {
|
||||
if (!content) return '<ul><li></li></ul>';
|
||||
|
||||
const listType = metadata?.listType || 'unordered';
|
||||
const listTag = listType === 'ordered' ? 'ol' : 'ul';
|
||||
|
||||
// Split content by newlines to create list items
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
if (lines.length === 0) {
|
||||
return `<${listTag}><li></li></${listTag}>`;
|
||||
}
|
||||
|
||||
const listItems = lines.map(line => `<li>${line}</li>`).join('');
|
||||
return `<${listTag}>${listItems}</${listTag}>`;
|
||||
}
|
||||
|
||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) {
|
||||
console.error('ListBlockHandler.setup: No list block element found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('ListBlockHandler.setup: Setting up list block', { blockId: block.id });
|
||||
|
||||
// Set initial content if needed
|
||||
if (block.content && !listBlock.innerHTML) {
|
||||
listBlock.innerHTML = this.renderListContent(block.content, block.metadata);
|
||||
}
|
||||
|
||||
// Input handler
|
||||
listBlock.addEventListener('input', (e) => {
|
||||
console.log('ListBlockHandler: Input event', { blockId: block.id });
|
||||
handlers.onInput(e as InputEvent);
|
||||
|
||||
// Track cursor position after input
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
});
|
||||
|
||||
// Keydown handler
|
||||
listBlock.addEventListener('keydown', (e) => {
|
||||
// Track cursor position before keydown
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
|
||||
// Special handling for Enter key in lists
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const currentLi = range.startContainer.parentElement?.closest('li');
|
||||
|
||||
if (currentLi && currentLi.textContent === '') {
|
||||
// Empty list item - exit list mode
|
||||
e.preventDefault();
|
||||
handlers.onKeyDown(e);
|
||||
return;
|
||||
}
|
||||
// Otherwise, let browser create new list item naturally
|
||||
}
|
||||
}
|
||||
|
||||
handlers.onKeyDown(e);
|
||||
});
|
||||
|
||||
// Focus handler
|
||||
listBlock.addEventListener('focus', () => {
|
||||
console.log('ListBlockHandler: Focus event', { blockId: block.id });
|
||||
handlers.onFocus();
|
||||
});
|
||||
|
||||
// Blur handler
|
||||
listBlock.addEventListener('blur', () => {
|
||||
console.log('ListBlockHandler: Blur event', { blockId: block.id });
|
||||
handlers.onBlur();
|
||||
});
|
||||
|
||||
// Composition handlers for IME support
|
||||
listBlock.addEventListener('compositionstart', () => {
|
||||
console.log('ListBlockHandler: Composition start', { blockId: block.id });
|
||||
handlers.onCompositionStart();
|
||||
});
|
||||
|
||||
listBlock.addEventListener('compositionend', () => {
|
||||
console.log('ListBlockHandler: Composition end', { blockId: block.id });
|
||||
handlers.onCompositionEnd();
|
||||
});
|
||||
|
||||
// Mouse up handler
|
||||
listBlock.addEventListener('mouseup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
handlers.onMouseUp?.(e);
|
||||
});
|
||||
|
||||
// Click handler
|
||||
listBlock.addEventListener('click', (e: MouseEvent) => {
|
||||
setTimeout(() => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Keyup handler
|
||||
listBlock.addEventListener('keyup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up selection handler
|
||||
this.setupSelectionHandler(element, listBlock, block);
|
||||
}
|
||||
|
||||
private setupSelectionHandler(element: HTMLElement, listBlock: HTMLDivElement, block: IBlock): void {
|
||||
const checkSelection = () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const selectedText = selection.toString();
|
||||
if (selectedText.length === 0) {
|
||||
if (this.lastSelectedText) {
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parent wysiwyg component's shadow root
|
||||
const wysiwygBlock = (listBlock.getRootNode() as ShadowRoot).host as any;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = wysiwygBlock?.shadowRoot;
|
||||
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
if (!selectionInfo) return;
|
||||
|
||||
const startInBlock = WysiwygSelection.containsAcrossShadowDOM(listBlock, selectionInfo.startContainer);
|
||||
const endInBlock = WysiwygSelection.containsAcrossShadowDOM(listBlock, selectionInfo.endContainer);
|
||||
|
||||
if (startInBlock || endInBlock) {
|
||||
if (selectedText !== this.lastSelectedText) {
|
||||
this.lastSelectedText = selectedText;
|
||||
|
||||
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: selectedText.trim(),
|
||||
blockId: block.id,
|
||||
range: range,
|
||||
rect: rect,
|
||||
hasSelection: true
|
||||
});
|
||||
}
|
||||
} else if (this.lastSelectedText) {
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('selectionchange', checkSelection);
|
||||
this.selectionHandler = checkSelection;
|
||||
|
||||
// Cleanup on disconnect
|
||||
const wysiwygBlock = (listBlock.getRootNode() as ShadowRoot).host as any;
|
||||
if (wysiwygBlock) {
|
||||
const originalDisconnectedCallback = (wysiwygBlock as any).disconnectedCallback;
|
||||
(wysiwygBlock as any).disconnectedCallback = async function() {
|
||||
if (this.selectionHandler) {
|
||||
document.removeEventListener('selectionchange', this.selectionHandler);
|
||||
this.selectionHandler = null;
|
||||
}
|
||||
if (originalDisconnectedCallback) {
|
||||
await originalDisconnectedCallback.call(wysiwygBlock);
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchSelectionEvent(element: HTMLElement, detail: any): void {
|
||||
const event = new CustomEvent('block-text-selected', {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
getStyles(): string {
|
||||
return `
|
||||
/* List specific styles */
|
||||
.block.list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.block.list ul,
|
||||
.block.list ol {
|
||||
margin: 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.block.list li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.block.list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getPlaceholder(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Helper methods for list functionality
|
||||
|
||||
getCursorPosition(element: HTMLElement, context?: any): number | null {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return null;
|
||||
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
if (!selectionInfo) return null;
|
||||
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(listBlock, selectionInfo.startContainer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For lists, calculate position based on text content
|
||||
const preCaretRange = document.createRange();
|
||||
preCaretRange.selectNodeContents(listBlock);
|
||||
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
return preCaretRange.toString().length;
|
||||
}
|
||||
|
||||
getContent(element: HTMLElement, context?: any): string {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return '';
|
||||
|
||||
// Extract text content from list items
|
||||
const listItems = listBlock.querySelectorAll('li');
|
||||
const content = Array.from(listItems)
|
||||
.map(li => li.textContent || '')
|
||||
.join('\n');
|
||||
|
||||
console.log('ListBlockHandler.getContent:', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
setContent(element: HTMLElement, content: string, context?: any): void {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return;
|
||||
|
||||
const hadFocus = document.activeElement === listBlock ||
|
||||
element.shadowRoot?.activeElement === listBlock;
|
||||
|
||||
// Get current metadata to preserve list type
|
||||
const listElement = listBlock.querySelector('ul, ol');
|
||||
const isOrdered = listElement?.tagName === 'OL';
|
||||
|
||||
// Update content
|
||||
listBlock.innerHTML = this.renderListContent(content, { listType: isOrdered ? 'ordered' : 'unordered' });
|
||||
|
||||
if (hadFocus) {
|
||||
listBlock.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToStart(element: HTMLElement, context?: any): void {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return;
|
||||
|
||||
const firstLi = listBlock.querySelector('li');
|
||||
if (firstLi) {
|
||||
const textNode = this.getFirstTextNode(firstLi);
|
||||
if (textNode) {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
range.setStart(textNode, 0);
|
||||
range.setEnd(textNode, 0);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToEnd(element: HTMLElement, context?: any): void {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return;
|
||||
|
||||
const lastLi = listBlock.querySelector('li:last-child');
|
||||
if (lastLi) {
|
||||
const textNode = this.getLastTextNode(lastLi);
|
||||
if (textNode) {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const textLength = textNode.textContent?.length || 0;
|
||||
range.setStart(textNode, textLength);
|
||||
range.setEnd(textNode, textLength);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getFirstTextNode(element: Node): Text | null {
|
||||
if (element.nodeType === Node.TEXT_NODE) {
|
||||
return element as Text;
|
||||
}
|
||||
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const firstText = this.getFirstTextNode(element.childNodes[i]);
|
||||
if (firstText) return firstText;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getLastTextNode(element: Node): Text | null {
|
||||
if (element.nodeType === Node.TEXT_NODE) {
|
||||
return element as Text;
|
||||
}
|
||||
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const lastText = this.getLastTextNode(element.childNodes[i]);
|
||||
if (lastText) return lastText;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
focus(element: HTMLElement, context?: any): void {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return;
|
||||
|
||||
if (!listBlock.hasAttribute('contenteditable')) {
|
||||
listBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
listBlock.focus();
|
||||
|
||||
if (document.activeElement !== listBlock && element.shadowRoot?.activeElement !== listBlock) {
|
||||
Promise.resolve().then(() => {
|
||||
listBlock.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return;
|
||||
|
||||
if (!listBlock.hasAttribute('contenteditable')) {
|
||||
listBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
listBlock.focus();
|
||||
|
||||
const setCursor = () => {
|
||||
if (position === 'start') {
|
||||
this.setCursorToStart(element, context);
|
||||
} else if (position === 'end') {
|
||||
this.setCursorToEnd(element, context);
|
||||
} else if (typeof position === 'number') {
|
||||
// For numeric positions in lists, we need custom logic
|
||||
// This is complex due to list structure, so default to end
|
||||
this.setCursorToEnd(element, context);
|
||||
}
|
||||
};
|
||||
|
||||
if (document.activeElement === listBlock || element.shadowRoot?.activeElement === listBlock) {
|
||||
setCursor();
|
||||
} else {
|
||||
Promise.resolve().then(() => {
|
||||
if (document.activeElement === listBlock || element.shadowRoot?.activeElement === listBlock) {
|
||||
setCursor();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
|
||||
const listBlock = element.querySelector('.block.list') as HTMLDivElement;
|
||||
if (!listBlock) return null;
|
||||
|
||||
// For lists, we don't split content - instead let the keyboard handler
|
||||
// create a new paragraph block when Enter is pressed on empty list item
|
||||
return null;
|
||||
}
|
||||
}
|
||||
538
ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts
Normal file
538
ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts
Normal file
@@ -0,0 +1,538 @@
|
||||
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
||||
import type { IBlock } from '../../wysiwyg.types.js';
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
|
||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||
|
||||
export class ParagraphBlockHandler extends BaseBlockHandler {
|
||||
type = 'paragraph';
|
||||
|
||||
// Track cursor position
|
||||
private lastKnownCursorPosition: number = 0;
|
||||
private lastSelectedText: string = '';
|
||||
private selectionHandler: (() => void) | null = null;
|
||||
|
||||
render(block: IBlock, isSelected: boolean): string {
|
||||
const selectedClass = isSelected ? ' selected' : '';
|
||||
const placeholder = this.getPlaceholder();
|
||||
|
||||
console.log('ParagraphBlockHandler.render:', { blockId: block.id, isSelected, content: block.content });
|
||||
|
||||
return `
|
||||
<div
|
||||
class="block paragraph${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-placeholder="${placeholder}"
|
||||
data-block-id="${block.id}"
|
||||
data-block-type="${block.type}"
|
||||
>${block.content || ''}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) {
|
||||
console.error('ParagraphBlockHandler.setup: No paragraph block element found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('ParagraphBlockHandler.setup: Setting up paragraph block', { blockId: block.id });
|
||||
|
||||
// Set initial content if needed
|
||||
if (block.content && !paragraphBlock.innerHTML) {
|
||||
paragraphBlock.innerHTML = block.content;
|
||||
}
|
||||
|
||||
// Input handler with cursor tracking
|
||||
paragraphBlock.addEventListener('input', (e) => {
|
||||
console.log('ParagraphBlockHandler: Input event', { blockId: block.id });
|
||||
handlers.onInput(e as InputEvent);
|
||||
|
||||
// Track cursor position after input
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('ParagraphBlockHandler: Updated cursor position after input', { pos });
|
||||
}
|
||||
});
|
||||
|
||||
// Keydown handler with cursor tracking
|
||||
paragraphBlock.addEventListener('keydown', (e) => {
|
||||
// Track cursor position before keydown
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('ParagraphBlockHandler: Cursor position before keydown', { pos, key: e.key });
|
||||
}
|
||||
|
||||
handlers.onKeyDown(e);
|
||||
});
|
||||
|
||||
// Focus handler
|
||||
paragraphBlock.addEventListener('focus', () => {
|
||||
console.log('ParagraphBlockHandler: Focus event', { blockId: block.id });
|
||||
handlers.onFocus();
|
||||
});
|
||||
|
||||
// Blur handler
|
||||
paragraphBlock.addEventListener('blur', () => {
|
||||
console.log('ParagraphBlockHandler: Blur event', { blockId: block.id });
|
||||
handlers.onBlur();
|
||||
});
|
||||
|
||||
// Composition handlers for IME support
|
||||
paragraphBlock.addEventListener('compositionstart', () => {
|
||||
console.log('ParagraphBlockHandler: Composition start', { blockId: block.id });
|
||||
handlers.onCompositionStart();
|
||||
});
|
||||
|
||||
paragraphBlock.addEventListener('compositionend', () => {
|
||||
console.log('ParagraphBlockHandler: Composition end', { blockId: block.id });
|
||||
handlers.onCompositionEnd();
|
||||
});
|
||||
|
||||
// Mouse up handler
|
||||
paragraphBlock.addEventListener('mouseup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('ParagraphBlockHandler: Cursor position after mouseup', { pos });
|
||||
}
|
||||
|
||||
// Selection will be handled by selectionchange event
|
||||
handlers.onMouseUp?.(e);
|
||||
});
|
||||
|
||||
// Click handler with delayed cursor tracking
|
||||
paragraphBlock.addEventListener('click', (e: MouseEvent) => {
|
||||
// Small delay to let browser set cursor position
|
||||
setTimeout(() => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('ParagraphBlockHandler: Cursor position after click', { pos });
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Keyup handler for additional cursor tracking
|
||||
paragraphBlock.addEventListener('keyup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('ParagraphBlockHandler: Cursor position after keyup', { pos, key: e.key });
|
||||
}
|
||||
});
|
||||
|
||||
// Set up selection change handler
|
||||
this.setupSelectionHandler(element, paragraphBlock, block);
|
||||
}
|
||||
|
||||
private setupSelectionHandler(element: HTMLElement, paragraphBlock: HTMLDivElement, block: IBlock): void {
|
||||
// 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.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parent wysiwyg component's shadow root - traverse from shadow root
|
||||
const wysiwygBlock = (paragraphBlock.getRootNode() as ShadowRoot).host as any;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = wysiwygBlock?.shadowRoot;
|
||||
|
||||
// Use getComposedRanges with shadow roots as per MDN docs
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
// 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(paragraphBlock, selectionInfo.startContainer);
|
||||
const endInBlock = WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.endContainer);
|
||||
|
||||
if (startInBlock || endInBlock) {
|
||||
if (selectedText !== this.lastSelectedText) {
|
||||
this.lastSelectedText = selectedText;
|
||||
|
||||
console.log('ParagraphBlockHandler: Text selected', {
|
||||
text: selectedText,
|
||||
blockId: block.id
|
||||
});
|
||||
|
||||
// Create range and get rect
|
||||
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Dispatch event
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: selectedText.trim(),
|
||||
blockId: block.id,
|
||||
range: range,
|
||||
rect: rect,
|
||||
hasSelection: true
|
||||
});
|
||||
}
|
||||
} else if (this.lastSelectedText) {
|
||||
// Clear selection if no longer in this block
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for selection changes
|
||||
document.addEventListener('selectionchange', checkSelection);
|
||||
|
||||
// Store the handler for cleanup
|
||||
this.selectionHandler = checkSelection;
|
||||
|
||||
// Clean up on disconnect (will be called by dees-wysiwyg-block)
|
||||
const wysiwygBlock = element.closest('dees-wysiwyg-block');
|
||||
if (wysiwygBlock) {
|
||||
const originalDisconnectedCallback = (wysiwygBlock as any).disconnectedCallback;
|
||||
(wysiwygBlock as any).disconnectedCallback = async function() {
|
||||
if (this.selectionHandler) {
|
||||
document.removeEventListener('selectionchange', this.selectionHandler);
|
||||
this.selectionHandler = null;
|
||||
}
|
||||
if (originalDisconnectedCallback) {
|
||||
await originalDisconnectedCallback.call(wysiwygBlock);
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchSelectionEvent(element: HTMLElement, detail: any): void {
|
||||
const event = new CustomEvent('block-text-selected', {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
getStyles(): string {
|
||||
return `
|
||||
/* Paragraph specific styles */
|
||||
.block.paragraph {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getPlaceholder(): string {
|
||||
return "Type '/' for commands...";
|
||||
}
|
||||
|
||||
// Helper methods for paragraph functionality
|
||||
|
||||
getCursorPosition(element: HTMLElement, context?: any): number | null {
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: Called with element:', element, 'context:', context);
|
||||
|
||||
// Get the actual paragraph element
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) {
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: No paragraph element found');
|
||||
console.log('Element innerHTML:', element.innerHTML);
|
||||
console.log('Element tagName:', element.tagName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
|
||||
selectionInfo,
|
||||
shadowRootsCount: shadowRoots.length,
|
||||
element: element,
|
||||
paragraphBlock: paragraphBlock
|
||||
});
|
||||
|
||||
if (!selectionInfo) {
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: No selection found');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: Range info:', {
|
||||
startContainer: selectionInfo.startContainer,
|
||||
startOffset: selectionInfo.startOffset,
|
||||
collapsed: selectionInfo.collapsed,
|
||||
startContainerText: selectionInfo.startContainer.textContent
|
||||
});
|
||||
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) {
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: Range not in element');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a range from start of element to cursor position
|
||||
const preCaretRange = document.createRange();
|
||||
preCaretRange.selectNodeContents(paragraphBlock);
|
||||
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// Get the text content length up to cursor
|
||||
const position = preCaretRange.toString().length;
|
||||
console.log('ParagraphBlockHandler.getCursorPosition: Calculated position:', {
|
||||
position,
|
||||
preCaretText: preCaretRange.toString(),
|
||||
elementText: paragraphBlock.textContent,
|
||||
elementTextLength: paragraphBlock.textContent?.length
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
getContent(element: HTMLElement, context?: any): string {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) return '';
|
||||
|
||||
// For paragraphs, get the innerHTML which includes formatting tags
|
||||
const content = paragraphBlock.innerHTML || '';
|
||||
console.log('ParagraphBlockHandler.getContent:', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
setContent(element: HTMLElement, content: string, context?: any): void {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) return;
|
||||
|
||||
// Store if we have focus
|
||||
const hadFocus = document.activeElement === paragraphBlock ||
|
||||
element.shadowRoot?.activeElement === paragraphBlock;
|
||||
|
||||
paragraphBlock.innerHTML = content;
|
||||
|
||||
// Restore focus if we had it
|
||||
if (hadFocus) {
|
||||
paragraphBlock.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToStart(element: HTMLElement, context?: any): void {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (paragraphBlock) {
|
||||
WysiwygBlocks.setCursorToStart(paragraphBlock);
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToEnd(element: HTMLElement, context?: any): void {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (paragraphBlock) {
|
||||
WysiwygBlocks.setCursorToEnd(paragraphBlock);
|
||||
}
|
||||
}
|
||||
|
||||
focus(element: HTMLElement, context?: any): void {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) return;
|
||||
|
||||
// Ensure the element is focusable
|
||||
if (!paragraphBlock.hasAttribute('contenteditable')) {
|
||||
paragraphBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
paragraphBlock.focus();
|
||||
|
||||
// If focus failed, try again after a microtask
|
||||
if (document.activeElement !== paragraphBlock && element.shadowRoot?.activeElement !== paragraphBlock) {
|
||||
Promise.resolve().then(() => {
|
||||
paragraphBlock.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) return;
|
||||
|
||||
// Ensure element is focusable first
|
||||
if (!paragraphBlock.hasAttribute('contenteditable')) {
|
||||
paragraphBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
// Focus the element
|
||||
paragraphBlock.focus();
|
||||
|
||||
// Set cursor position after focus is established
|
||||
const setCursor = () => {
|
||||
if (position === 'start') {
|
||||
this.setCursorToStart(element, context);
|
||||
} else if (position === 'end') {
|
||||
this.setCursorToEnd(element, context);
|
||||
} else if (typeof position === 'number') {
|
||||
// Use the selection utility to set cursor position
|
||||
WysiwygSelection.setCursorPosition(paragraphBlock, position);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure cursor is set after focus
|
||||
if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) {
|
||||
setCursor();
|
||||
} else {
|
||||
// Wait for focus to be established
|
||||
Promise.resolve().then(() => {
|
||||
if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) {
|
||||
setCursor();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
|
||||
console.log('ParagraphBlockHandler.getSplitContent: Starting...');
|
||||
|
||||
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
|
||||
if (!paragraphBlock) {
|
||||
console.log('ParagraphBlockHandler.getSplitContent: No paragraph element found');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('ParagraphBlockHandler.getSplitContent: Element info:', {
|
||||
innerHTML: paragraphBlock.innerHTML,
|
||||
textContent: paragraphBlock.textContent,
|
||||
textLength: paragraphBlock.textContent?.length
|
||||
});
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
console.log('ParagraphBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
|
||||
selectionInfo,
|
||||
shadowRootsCount: shadowRoots.length
|
||||
});
|
||||
|
||||
if (!selectionInfo) {
|
||||
console.log('ParagraphBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = paragraphBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
console.log('ParagraphBlockHandler.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('ParagraphBlockHandler.getSplitContent: Selection range:', {
|
||||
startContainer: selectionInfo.startContainer,
|
||||
startOffset: selectionInfo.startOffset,
|
||||
startContainerInElement: paragraphBlock.contains(selectionInfo.startContainer)
|
||||
});
|
||||
|
||||
// Make sure the selection is within this block
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) {
|
||||
console.log('ParagraphBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = paragraphBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
return {
|
||||
before: fullText.substring(0, pos),
|
||||
after: fullText.substring(pos)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get cursor position first
|
||||
const cursorPos = this.getCursorPosition(element, context);
|
||||
console.log('ParagraphBlockHandler.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('ParagraphBlockHandler.getSplitContent: Cursor at start or null, moving all content');
|
||||
return {
|
||||
before: '',
|
||||
after: paragraphBlock.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(paragraphBlock, 0);
|
||||
beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// After range: from cursor to end of element
|
||||
afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
afterRange.setEnd(paragraphBlock, paragraphBlock.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('ParagraphBlockHandler.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
|
||||
};
|
||||
}
|
||||
}
|
||||
541
ts_web/elements/wysiwyg/blocks/text/quote.block.ts
Normal file
541
ts_web/elements/wysiwyg/blocks/text/quote.block.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
||||
import type { IBlock } from '../../wysiwyg.types.js';
|
||||
import { cssManager } from '@design.estate/dees-element';
|
||||
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
|
||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
||||
|
||||
export class QuoteBlockHandler extends BaseBlockHandler {
|
||||
type = 'quote';
|
||||
|
||||
// Track cursor position
|
||||
private lastKnownCursorPosition: number = 0;
|
||||
private lastSelectedText: string = '';
|
||||
private selectionHandler: (() => void) | null = null;
|
||||
|
||||
render(block: IBlock, isSelected: boolean): string {
|
||||
const selectedClass = isSelected ? ' selected' : '';
|
||||
const placeholder = this.getPlaceholder();
|
||||
|
||||
console.log('QuoteBlockHandler.render:', { blockId: block.id, isSelected, content: block.content });
|
||||
|
||||
return `
|
||||
<div
|
||||
class="block quote${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-placeholder="${placeholder}"
|
||||
data-block-id="${block.id}"
|
||||
data-block-type="${block.type}"
|
||||
>${block.content || ''}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) {
|
||||
console.error('QuoteBlockHandler.setup: No quote block element found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('QuoteBlockHandler.setup: Setting up quote block', { blockId: block.id });
|
||||
|
||||
// Set initial content if needed
|
||||
if (block.content && !quoteBlock.innerHTML) {
|
||||
quoteBlock.innerHTML = block.content;
|
||||
}
|
||||
|
||||
// Input handler with cursor tracking
|
||||
quoteBlock.addEventListener('input', (e) => {
|
||||
console.log('QuoteBlockHandler: Input event', { blockId: block.id });
|
||||
handlers.onInput(e as InputEvent);
|
||||
|
||||
// Track cursor position after input
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('QuoteBlockHandler: Updated cursor position after input', { pos });
|
||||
}
|
||||
});
|
||||
|
||||
// Keydown handler with cursor tracking
|
||||
quoteBlock.addEventListener('keydown', (e) => {
|
||||
// Track cursor position before keydown
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('QuoteBlockHandler: Cursor position before keydown', { pos, key: e.key });
|
||||
}
|
||||
|
||||
handlers.onKeyDown(e);
|
||||
});
|
||||
|
||||
// Focus handler
|
||||
quoteBlock.addEventListener('focus', () => {
|
||||
console.log('QuoteBlockHandler: Focus event', { blockId: block.id });
|
||||
handlers.onFocus();
|
||||
});
|
||||
|
||||
// Blur handler
|
||||
quoteBlock.addEventListener('blur', () => {
|
||||
console.log('QuoteBlockHandler: Blur event', { blockId: block.id });
|
||||
handlers.onBlur();
|
||||
});
|
||||
|
||||
// Composition handlers for IME support
|
||||
quoteBlock.addEventListener('compositionstart', () => {
|
||||
console.log('QuoteBlockHandler: Composition start', { blockId: block.id });
|
||||
handlers.onCompositionStart();
|
||||
});
|
||||
|
||||
quoteBlock.addEventListener('compositionend', () => {
|
||||
console.log('QuoteBlockHandler: Composition end', { blockId: block.id });
|
||||
handlers.onCompositionEnd();
|
||||
});
|
||||
|
||||
// Mouse up handler
|
||||
quoteBlock.addEventListener('mouseup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('QuoteBlockHandler: Cursor position after mouseup', { pos });
|
||||
}
|
||||
|
||||
// Selection will be handled by selectionchange event
|
||||
handlers.onMouseUp?.(e);
|
||||
});
|
||||
|
||||
// Click handler with delayed cursor tracking
|
||||
quoteBlock.addEventListener('click', (e: MouseEvent) => {
|
||||
// Small delay to let browser set cursor position
|
||||
setTimeout(() => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('QuoteBlockHandler: Cursor position after click', { pos });
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Keyup handler for additional cursor tracking
|
||||
quoteBlock.addEventListener('keyup', (e) => {
|
||||
const pos = this.getCursorPosition(element);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('QuoteBlockHandler: Cursor position after keyup', { pos, key: e.key });
|
||||
}
|
||||
});
|
||||
|
||||
// Set up selection change handler
|
||||
this.setupSelectionHandler(element, quoteBlock, block);
|
||||
}
|
||||
|
||||
private setupSelectionHandler(element: HTMLElement, quoteBlock: HTMLDivElement, block: IBlock): void {
|
||||
// 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.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parent wysiwyg component's shadow root - traverse from shadow root
|
||||
const wysiwygBlock = (quoteBlock.getRootNode() as ShadowRoot).host as any;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = wysiwygBlock?.shadowRoot;
|
||||
|
||||
// Use getComposedRanges with shadow roots as per MDN docs
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
// 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(quoteBlock, selectionInfo.startContainer);
|
||||
const endInBlock = WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.endContainer);
|
||||
|
||||
if (startInBlock || endInBlock) {
|
||||
if (selectedText !== this.lastSelectedText) {
|
||||
this.lastSelectedText = selectedText;
|
||||
|
||||
console.log('QuoteBlockHandler: Text selected', {
|
||||
text: selectedText,
|
||||
blockId: block.id
|
||||
});
|
||||
|
||||
// Create range and get rect
|
||||
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Dispatch event
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: selectedText.trim(),
|
||||
blockId: block.id,
|
||||
range: range,
|
||||
rect: rect,
|
||||
hasSelection: true
|
||||
});
|
||||
}
|
||||
} else if (this.lastSelectedText) {
|
||||
// Clear selection if no longer in this block
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchSelectionEvent(element, {
|
||||
text: '',
|
||||
blockId: block.id,
|
||||
hasSelection: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for selection changes
|
||||
document.addEventListener('selectionchange', checkSelection);
|
||||
|
||||
// Store the handler for cleanup
|
||||
this.selectionHandler = checkSelection;
|
||||
|
||||
// Clean up on disconnect (will be called by dees-wysiwyg-block)
|
||||
const wysiwygBlock = (quoteBlock.getRootNode() as ShadowRoot).host as any;
|
||||
if (wysiwygBlock) {
|
||||
const originalDisconnectedCallback = (wysiwygBlock as any).disconnectedCallback;
|
||||
(wysiwygBlock as any).disconnectedCallback = async function() {
|
||||
if (this.selectionHandler) {
|
||||
document.removeEventListener('selectionchange', this.selectionHandler);
|
||||
this.selectionHandler = null;
|
||||
}
|
||||
if (originalDisconnectedCallback) {
|
||||
await originalDisconnectedCallback.call(wysiwygBlock);
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchSelectionEvent(element: HTMLElement, detail: any): void {
|
||||
const event = new CustomEvent('block-text-selected', {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
getStyles(): string {
|
||||
return `
|
||||
/* Quote specific styles */
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getPlaceholder(): string {
|
||||
return 'Add a quote...';
|
||||
}
|
||||
|
||||
// Helper methods for quote functionality
|
||||
|
||||
getCursorPosition(element: HTMLElement, context?: any): number | null {
|
||||
console.log('QuoteBlockHandler.getCursorPosition: Called with element:', element, 'context:', context);
|
||||
|
||||
// Get the actual quote element
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) {
|
||||
console.log('QuoteBlockHandler.getCursorPosition: No quote element found');
|
||||
console.log('Element innerHTML:', element.innerHTML);
|
||||
console.log('Element tagName:', element.tagName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
console.log('QuoteBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
|
||||
selectionInfo,
|
||||
shadowRootsCount: shadowRoots.length,
|
||||
element: element,
|
||||
quoteBlock: quoteBlock
|
||||
});
|
||||
|
||||
if (!selectionInfo) {
|
||||
console.log('QuoteBlockHandler.getCursorPosition: No selection found');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('QuoteBlockHandler.getCursorPosition: Range info:', {
|
||||
startContainer: selectionInfo.startContainer,
|
||||
startOffset: selectionInfo.startOffset,
|
||||
collapsed: selectionInfo.collapsed,
|
||||
startContainerText: selectionInfo.startContainer.textContent
|
||||
});
|
||||
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) {
|
||||
console.log('QuoteBlockHandler.getCursorPosition: Range not in element');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a range from start of element to cursor position
|
||||
const preCaretRange = document.createRange();
|
||||
preCaretRange.selectNodeContents(quoteBlock);
|
||||
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// Get the text content length up to cursor
|
||||
const position = preCaretRange.toString().length;
|
||||
console.log('QuoteBlockHandler.getCursorPosition: Calculated position:', {
|
||||
position,
|
||||
preCaretText: preCaretRange.toString(),
|
||||
elementText: quoteBlock.textContent,
|
||||
elementTextLength: quoteBlock.textContent?.length
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
getContent(element: HTMLElement, context?: any): string {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) return '';
|
||||
|
||||
// For quotes, get the innerHTML which includes formatting tags
|
||||
const content = quoteBlock.innerHTML || '';
|
||||
console.log('QuoteBlockHandler.getContent:', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
setContent(element: HTMLElement, content: string, context?: any): void {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) return;
|
||||
|
||||
// Store if we have focus
|
||||
const hadFocus = document.activeElement === quoteBlock ||
|
||||
element.shadowRoot?.activeElement === quoteBlock;
|
||||
|
||||
quoteBlock.innerHTML = content;
|
||||
|
||||
// Restore focus if we had it
|
||||
if (hadFocus) {
|
||||
quoteBlock.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToStart(element: HTMLElement, context?: any): void {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (quoteBlock) {
|
||||
WysiwygBlocks.setCursorToStart(quoteBlock);
|
||||
}
|
||||
}
|
||||
|
||||
setCursorToEnd(element: HTMLElement, context?: any): void {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (quoteBlock) {
|
||||
WysiwygBlocks.setCursorToEnd(quoteBlock);
|
||||
}
|
||||
}
|
||||
|
||||
focus(element: HTMLElement, context?: any): void {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) return;
|
||||
|
||||
// Ensure the element is focusable
|
||||
if (!quoteBlock.hasAttribute('contenteditable')) {
|
||||
quoteBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
quoteBlock.focus();
|
||||
|
||||
// If focus failed, try again after a microtask
|
||||
if (document.activeElement !== quoteBlock && element.shadowRoot?.activeElement !== quoteBlock) {
|
||||
Promise.resolve().then(() => {
|
||||
quoteBlock.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) return;
|
||||
|
||||
// Ensure element is focusable first
|
||||
if (!quoteBlock.hasAttribute('contenteditable')) {
|
||||
quoteBlock.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
|
||||
// Focus the element
|
||||
quoteBlock.focus();
|
||||
|
||||
// Set cursor position after focus is established
|
||||
const setCursor = () => {
|
||||
if (position === 'start') {
|
||||
this.setCursorToStart(element, context);
|
||||
} else if (position === 'end') {
|
||||
this.setCursorToEnd(element, context);
|
||||
} else if (typeof position === 'number') {
|
||||
// Use the selection utility to set cursor position
|
||||
WysiwygSelection.setCursorPosition(quoteBlock, position);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure cursor is set after focus
|
||||
if (document.activeElement === quoteBlock || element.shadowRoot?.activeElement === quoteBlock) {
|
||||
setCursor();
|
||||
} else {
|
||||
// Wait for focus to be established
|
||||
Promise.resolve().then(() => {
|
||||
if (document.activeElement === quoteBlock || element.shadowRoot?.activeElement === quoteBlock) {
|
||||
setCursor();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
|
||||
console.log('QuoteBlockHandler.getSplitContent: Starting...');
|
||||
|
||||
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
|
||||
if (!quoteBlock) {
|
||||
console.log('QuoteBlockHandler.getSplitContent: No quote element found');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('QuoteBlockHandler.getSplitContent: Element info:', {
|
||||
innerHTML: quoteBlock.innerHTML,
|
||||
textContent: quoteBlock.textContent,
|
||||
textLength: quoteBlock.textContent?.length
|
||||
});
|
||||
|
||||
// Get shadow roots from context
|
||||
const wysiwygBlock = context?.component;
|
||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
const blockShadowRoot = context?.shadowRoot;
|
||||
|
||||
// Get selection info with both shadow roots for proper traversal
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
||||
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
console.log('QuoteBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
|
||||
selectionInfo,
|
||||
shadowRootsCount: shadowRoots.length
|
||||
});
|
||||
|
||||
if (!selectionInfo) {
|
||||
console.log('QuoteBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = quoteBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
console.log('QuoteBlockHandler.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('QuoteBlockHandler.getSplitContent: Selection range:', {
|
||||
startContainer: selectionInfo.startContainer,
|
||||
startOffset: selectionInfo.startOffset,
|
||||
startContainerInElement: quoteBlock.contains(selectionInfo.startContainer)
|
||||
});
|
||||
|
||||
// Make sure the selection is within this block
|
||||
if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) {
|
||||
console.log('QuoteBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
|
||||
// Try using last known cursor position
|
||||
if (this.lastKnownCursorPosition !== null) {
|
||||
const fullText = quoteBlock.textContent || '';
|
||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
||||
return {
|
||||
before: fullText.substring(0, pos),
|
||||
after: fullText.substring(pos)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get cursor position first
|
||||
const cursorPos = this.getCursorPosition(element, context);
|
||||
console.log('QuoteBlockHandler.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('QuoteBlockHandler.getSplitContent: Cursor at start or null, moving all content');
|
||||
return {
|
||||
before: '',
|
||||
after: quoteBlock.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(quoteBlock, 0);
|
||||
beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
|
||||
// After range: from cursor to end of element
|
||||
afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||
afterRange.setEnd(quoteBlock, quoteBlock.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('QuoteBlockHandler.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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user