This commit is contained in:
Juergen Kunz
2025-06-24 22:45:50 +00:00
parent 68b4e9ec8e
commit e9541da8ff
28 changed files with 4733 additions and 88 deletions

View File

@ -11,6 +11,8 @@ import {
import { type IBlock } from './wysiwyg.types.js';
import { WysiwygBlocks } from './wysiwyg.blocks.js';
import { WysiwygSelection } from './wysiwyg.selection.js';
import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js';
import './wysiwyg.blockregistration.js';
declare global {
interface HTMLElementTagNameMap {
@ -34,15 +36,7 @@ export class DeesWysiwygBlock extends DeesElement {
public isSelected: boolean = false;
@property({ type: Object })
public handlers: {
onInput: (e: InputEvent) => void;
onKeyDown: (e: KeyboardEvent) => void;
onFocus: () => void;
onBlur: () => void;
onCompositionStart: () => void;
onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void;
};
public handlers: IBlockEventHandlers;
// Reference to the editable block element
private blockElement: HTMLDivElement | null = null;
@ -54,6 +48,31 @@ export class DeesWysiwygBlock extends DeesElement {
private lastKnownCursorPosition: number = 0;
private lastSelectedText: string = '';
private static handlerStylesInjected = false;
private injectHandlerStyles(): void {
// Only inject once per component class
if (DeesWysiwygBlock.handlerStylesInjected) return;
DeesWysiwygBlock.handlerStylesInjected = true;
// Get styles from all registered block handlers
let styles = '';
const blockTypes = BlockRegistry.getAllTypes();
for (const type of blockTypes) {
const handler = BlockRegistry.getHandler(type);
if (handler) {
styles += handler.getStyles();
}
}
if (styles) {
// Create and inject style element
const styleElement = document.createElement('style');
styleElement.textContent = styles;
this.shadowRoot?.appendChild(styleElement);
}
}
public static styles = [
cssManager.defaultStyles,
css`
@ -141,30 +160,6 @@ export class DeesWysiwygBlock extends DeesElement {
margin: 4px 0;
}
.block.divider {
padding: 8px 0;
margin: 16px 0;
cursor: pointer;
position: relative;
border-radius: 4px;
transition: all 0.15s ease;
}
.block.divider:focus {
outline: none;
}
.block.divider.selected {
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.05)', 'rgba(77, 148, 255, 0.08)')};
box-shadow: inset 0 0 0 2px ${cssManager.bdTheme('rgba(0, 102, 204, 0.2)', 'rgba(77, 148, 255, 0.2)')};
}
.block.divider hr {
border: none;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
margin: 0;
pointer-events: none;
}
/* Formatting styles */
.block :is(b, strong) {
@ -722,7 +717,7 @@ export class DeesWysiwygBlock extends DeesElement {
// Never update if only the block content changed
if (changedProperties.has('block') && this.block) {
const oldBlock = changedProperties.get('block');
if (oldBlock && oldBlock.id === this.block.id && oldBlock.type === this.block.type) {
if (oldBlock && oldBlock.id && oldBlock.type && oldBlock.id === this.block.id && oldBlock.type === this.block.type) {
// Only content or metadata changed, don't re-render
return false;
}
@ -736,19 +731,31 @@ export class DeesWysiwygBlock extends DeesElement {
// Mark that content has been initialized
this.contentInitialized = true;
// Inject handler styles if not already done
this.injectHandlerStyles();
// First, populate the container with the rendered content
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container && this.block) {
container.innerHTML = this.renderBlockContent();
}
// Check if we have a registered handler for this block type
if (this.block) {
const handler = BlockRegistry.getHandler(this.block.type);
if (handler) {
const blockElement = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
if (blockElement) {
handler.setup(blockElement, this.block, this.handlers);
}
return; // Block handler takes care of all setup
}
}
// Handle special block types
if (this.block.type === 'image') {
this.setupImageBlock();
return; // Image blocks don't need the standard editable setup
} else if (this.block.type === 'divider') {
this.setupDividerBlock();
return; // Divider blocks don't need the standard editable setup
} else if (this.block.type === 'youtube') {
this.setupYouTubeBlock();
return;
@ -875,8 +882,8 @@ export class DeesWysiwygBlock extends DeesElement {
if (!selectionInfo) return;
// Check if selection is within this block
const startInBlock = currentEditableBlock.contains(selectionInfo.startContainer);
const endInBlock = currentEditableBlock.contains(selectionInfo.endContainer);
const startInBlock = WysiwygSelection.containsAcrossShadowDOM(currentEditableBlock, selectionInfo.startContainer);
const endInBlock = WysiwygSelection.containsAcrossShadowDOM(currentEditableBlock, selectionInfo.endContainer);
if (startInBlock || endInBlock) {
if (selectedText !== this.lastSelectedText) {
@ -956,13 +963,10 @@ export class DeesWysiwygBlock extends DeesElement {
private renderBlockContent(): string {
if (!this.block) return '';
if (this.block.type === 'divider') {
const selectedClass = this.isSelected ? ' selected' : '';
return `
<div class="block divider${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
<hr>
</div>
`;
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler) {
return handler.render(this.block, this.isSelected);
}
if (this.block.type === 'code') {
@ -1145,6 +1149,14 @@ export class DeesWysiwygBlock extends DeesElement {
public focus(): void {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.focus) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.focus(container, context);
}
// Handle non-editable blocks
const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
if (this.block && nonEditableTypes.includes(this.block.type)) {
@ -1178,6 +1190,14 @@ export class DeesWysiwygBlock extends DeesElement {
}
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.focusWithCursor) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.focusWithCursor(container, position, context);
}
// Non-editable blocks don't support cursor positioning
const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
if (this.block && nonEditableTypes.includes(this.block.type)) {
@ -1231,6 +1251,13 @@ export class DeesWysiwygBlock extends DeesElement {
* Get cursor position in the editable element
*/
public getCursorPosition(element: HTMLElement): number | null {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.getCursorPosition) {
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.getCursorPosition(element, context);
}
// Get parent wysiwyg component's shadow root
const parentComponent = this.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
@ -1281,6 +1308,14 @@ export class DeesWysiwygBlock extends DeesElement {
}
public getContent(): string {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.getContent) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.getContent(container, context);
}
// Handle image blocks specially
if (this.block?.type === 'image') {
return this.block.content || ''; // Image blocks store alt text in content
@ -1307,6 +1342,14 @@ export class DeesWysiwygBlock extends DeesElement {
}
public setContent(content: string): void {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.setContent) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.setContent(container, content, context);
}
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
@ -1332,6 +1375,14 @@ export class DeesWysiwygBlock extends DeesElement {
}
public setCursorToStart(): void {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.setCursorToStart) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.setCursorToStart(container, context);
}
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
@ -1341,6 +1392,14 @@ export class DeesWysiwygBlock extends DeesElement {
}
public setCursorToEnd(): void {
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
if (handler && handler.setCursorToEnd) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const context = { shadowRoot: this.shadowRoot!, component: this };
return handler.setCursorToEnd(container, context);
}
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
@ -1358,43 +1417,6 @@ export class DeesWysiwygBlock extends DeesElement {
}
}
/**
* Setup divider block functionality
*/
private setupDividerBlock(): void {
const dividerBlock = this.shadowRoot?.querySelector('.block.divider') as HTMLDivElement;
if (!dividerBlock) return;
// Handle click to select
dividerBlock.addEventListener('click', (e) => {
e.stopPropagation();
// Focus will trigger the selection
dividerBlock.focus();
// Ensure focus handler is called immediately
this.handlers?.onFocus?.();
});
// Handle focus/blur
dividerBlock.addEventListener('focus', () => {
this.handlers?.onFocus?.();
});
dividerBlock.addEventListener('blur', () => {
this.handlers?.onBlur?.();
});
// Handle keyboard events
dividerBlock.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
// Let the keyboard handler in the parent component handle the deletion
this.handlers?.onKeyDown?.(e);
} else {
// Handle navigation keys
this.handlers?.onKeyDown?.(e);
}
});
}
/**
* Setup YouTube block functionality
@ -1988,6 +2010,27 @@ export class DeesWysiwygBlock extends DeesElement {
public getSplitContent(): { before: string; after: string } | null {
console.log('getSplitContent: Starting...');
// Check if we have a registered handler for this block type
const handler = BlockRegistry.getHandler(this.block.type);
console.log('getSplitContent: Checking for handler', {
blockType: this.block.type,
hasHandler: !!handler,
hasSplitMethod: !!(handler && handler.getSplitContent)
});
if (handler && handler.getSplitContent) {
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
console.log('getSplitContent: Found container', {
container: !!container,
containerHTML: container?.innerHTML?.substring(0, 100)
});
const context = {
shadowRoot: this.shadowRoot!,
component: this
};
return handler.getSplitContent(container, context);
}
// Image blocks can't be split
if (this.block?.type === 'image') {
return null;
@ -2052,7 +2095,7 @@ export class DeesWysiwygBlock extends DeesElement {
});
// Make sure the selection is within this block
if (!editableElement.contains(selectionInfo.startContainer)) {
if (!WysiwygSelection.containsAcrossShadowDOM(editableElement, selectionInfo.startContainer)) {
console.log('getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position
if (this.lastKnownCursorPosition !== null) {