feat(editor): Add wysiwyg editor
This commit is contained in:
@@ -60,6 +60,15 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
@state()
|
||||
private slashMenuSelectedIndex: number = 0;
|
||||
|
||||
@state()
|
||||
private draggedBlockId: string | null = null;
|
||||
|
||||
@state()
|
||||
private dragOverBlockId: string | null = null;
|
||||
|
||||
@state()
|
||||
private dragOverPosition: 'before' | 'after' | null = null;
|
||||
|
||||
private editorContentRef: HTMLDivElement;
|
||||
private isComposing: boolean = false;
|
||||
private saveTimeout: any = null;
|
||||
@@ -73,26 +82,35 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
async firstUpdated() {
|
||||
this.updateValue();
|
||||
this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
// Set initial content for blocks
|
||||
this.setBlockContents();
|
||||
// Set initial content for blocks after a brief delay to ensure DOM is ready
|
||||
await this.updateComplete;
|
||||
setTimeout(() => {
|
||||
this.setBlockContents();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, any>) {
|
||||
// When blocks change (e.g., from setValue), update DOM content
|
||||
if (changedProperties.has('blocks')) {
|
||||
this.setBlockContents();
|
||||
// Wait for render to complete
|
||||
setTimeout(() => {
|
||||
this.setBlockContents();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
private setBlockContents() {
|
||||
// Only set content for blocks that aren't being edited
|
||||
this.blocks.forEach(block => {
|
||||
const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`) as HTMLDivElement;
|
||||
if (blockElement && document.activeElement !== blockElement && block.type !== 'divider') {
|
||||
if (block.type === 'list') {
|
||||
blockElement.innerHTML = WysiwygBlocks.renderListContent(block.content, block.metadata);
|
||||
} else {
|
||||
blockElement.textContent = block.content;
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`);
|
||||
if (wrapperElement) {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement && document.activeElement !== blockElement && block.type !== 'divider') {
|
||||
if (block.type === 'list') {
|
||||
blockElement.innerHTML = WysiwygBlocks.renderListContent(block.content, block.metadata);
|
||||
} else {
|
||||
blockElement.textContent = block.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -107,7 +125,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
></dees-label>
|
||||
<div class="wysiwyg-container">
|
||||
<div
|
||||
class="editor-content"
|
||||
class="editor-content ${this.draggedBlockId ? 'dragging' : ''}"
|
||||
@click="${this.handleEditorClick}"
|
||||
>
|
||||
${this.blocks.map(block => this.renderBlock(block))}
|
||||
@@ -119,15 +137,35 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
|
||||
private renderBlock(block: IBlock): TemplateResult {
|
||||
const isSelected = this.selectedBlockId === block.id;
|
||||
const isDragging = this.draggedBlockId === block.id;
|
||||
const isDragOver = this.dragOverBlockId === block.id;
|
||||
|
||||
return WysiwygBlocks.renderBlock(block, isSelected, {
|
||||
onInput: (e: InputEvent) => this.handleBlockInput(e, block),
|
||||
onKeyDown: (e: KeyboardEvent) => this.handleBlockKeyDown(e, block),
|
||||
onFocus: () => this.handleBlockFocus(block),
|
||||
onBlur: () => this.handleBlockBlur(block),
|
||||
onCompositionStart: () => this.isComposing = true,
|
||||
onCompositionEnd: () => this.isComposing = false,
|
||||
});
|
||||
return html`
|
||||
<div
|
||||
class="block-wrapper ${isDragging ? 'dragging' : ''} ${isDragOver && this.dragOverPosition === 'before' ? 'drag-over-before' : ''} ${isDragOver && this.dragOverPosition === 'after' ? 'drag-over-after' : ''}"
|
||||
data-block-id="${block.id}"
|
||||
@dragover="${(e: DragEvent) => this.handleDragOver(e, block)}"
|
||||
@drop="${(e: DragEvent) => this.handleDrop(e, block)}"
|
||||
@dragleave="${() => this.handleDragLeave(block)}"
|
||||
>
|
||||
${block.type !== 'divider' ? html`
|
||||
<div
|
||||
class="drag-handle"
|
||||
draggable="true"
|
||||
@dragstart="${(e: DragEvent) => this.handleDragStart(e, block)}"
|
||||
@dragend="${() => this.handleDragEnd()}"
|
||||
></div>
|
||||
` : ''}
|
||||
${WysiwygBlocks.renderBlock(block, isSelected, {
|
||||
onInput: (e: InputEvent) => this.handleBlockInput(e, block),
|
||||
onKeyDown: (e: KeyboardEvent) => this.handleBlockKeyDown(e, block),
|
||||
onFocus: () => this.handleBlockFocus(block),
|
||||
onBlur: () => this.handleBlockBlur(block),
|
||||
onCompositionStart: () => this.isComposing = true,
|
||||
onCompositionEnd: () => this.isComposing = false,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getFilteredMenuItems(): ISlashMenuItem[] {
|
||||
@@ -611,4 +649,84 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
}
|
||||
this.importBlocks(state.blocks);
|
||||
}
|
||||
|
||||
// Drag and Drop Handlers
|
||||
private handleDragStart(e: DragEvent, block: IBlock): void {
|
||||
if (!e.dataTransfer) return;
|
||||
|
||||
this.draggedBlockId = block.id;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', block.id);
|
||||
|
||||
// Add a slight delay to show the dragging state
|
||||
setTimeout(() => {
|
||||
this.requestUpdate();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private handleDragEnd(): void {
|
||||
this.draggedBlockId = null;
|
||||
this.dragOverBlockId = null;
|
||||
this.dragOverPosition = null;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleDragOver(e: DragEvent, block: IBlock): void {
|
||||
e.preventDefault();
|
||||
if (!e.dataTransfer || !this.draggedBlockId || this.draggedBlockId === block.id) return;
|
||||
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const midpoint = rect.top + rect.height / 2;
|
||||
|
||||
this.dragOverBlockId = block.id;
|
||||
this.dragOverPosition = e.clientY < midpoint ? 'before' : 'after';
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleDragLeave(block: IBlock): void {
|
||||
if (this.dragOverBlockId === block.id) {
|
||||
this.dragOverBlockId = null;
|
||||
this.dragOverPosition = null;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private handleDrop(e: DragEvent, targetBlock: IBlock): void {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.draggedBlockId || this.draggedBlockId === targetBlock.id) return;
|
||||
|
||||
const draggedIndex = this.blocks.findIndex(b => b.id === this.draggedBlockId);
|
||||
const targetIndex = this.blocks.findIndex(b => b.id === targetBlock.id);
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) return;
|
||||
|
||||
// Remove the dragged block
|
||||
const [draggedBlock] = this.blocks.splice(draggedIndex, 1);
|
||||
|
||||
// Calculate the new index
|
||||
let newIndex = targetIndex;
|
||||
if (this.dragOverPosition === 'after') {
|
||||
newIndex = draggedIndex < targetIndex ? targetIndex : targetIndex + 1;
|
||||
} else {
|
||||
newIndex = draggedIndex < targetIndex ? targetIndex - 1 : targetIndex;
|
||||
}
|
||||
|
||||
// Insert at new position
|
||||
this.blocks.splice(newIndex, 0, draggedBlock);
|
||||
|
||||
// Update state
|
||||
this.updateValue();
|
||||
this.handleDragEnd();
|
||||
|
||||
// Focus the moved block
|
||||
setTimeout(() => {
|
||||
const movedBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${draggedBlock.id}"] .block`) as HTMLDivElement;
|
||||
if (movedBlockElement && draggedBlock.type !== 'divider') {
|
||||
movedBlockElement.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user