From 08a4c361faf5873c541bf17fc101215c91eff8c0 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 24 Jun 2025 12:24:02 +0000 Subject: [PATCH] fix(wysiwyg):Improve Wysiwyg editor --- ts_web/elements/wysiwyg/dees-input-wysiwyg.ts | 1 - ts_web/elements/wysiwyg/instructions.md | 1 + .../wysiwyg/wysiwyg.dragdrophandler.ts | 283 +++++++++++++++++- ts_web/elements/wysiwyg/wysiwyg.styles.ts | 52 ++-- 4 files changed, 311 insertions(+), 26 deletions(-) diff --git a/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts b/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts index 3dc3e5b..72bf00c 100644 --- a/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts +++ b/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts @@ -168,7 +168,6 @@ export class DeesInputWysiwyg extends DeesInputBase { dragHandle.className = 'drag-handle'; dragHandle.draggable = true; dragHandle.addEventListener('dragstart', (e) => this.dragDropHandler.handleDragStart(e, block)); - dragHandle.addEventListener('dragend', () => this.dragDropHandler.handleDragEnd()); wrapper.appendChild(dragHandle); } diff --git a/ts_web/elements/wysiwyg/instructions.md b/ts_web/elements/wysiwyg/instructions.md index 209a26a..8fe46fd 100644 --- a/ts_web/elements/wysiwyg/instructions.md +++ b/ts_web/elements/wysiwyg/instructions.md @@ -1,3 +1,4 @@ * We don't use lit template logic, but use static`` here to handle dom operations ourselves * We try to have separated concerns in different classes * We try to have clean concise and managable code +* lets log whats happening, so if something goes wrong, we understand whats happening. \ No newline at end of file diff --git a/ts_web/elements/wysiwyg/wysiwyg.dragdrophandler.ts b/ts_web/elements/wysiwyg/wysiwyg.dragdrophandler.ts index 21287a7..1dcc535 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.dragdrophandler.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.dragdrophandler.ts @@ -5,6 +5,13 @@ export class WysiwygDragDropHandler { private draggedBlockId: string | null = null; private dragOverBlockId: string | null = null; private dragOverPosition: 'before' | 'after' | null = null; + private dropIndicator: HTMLElement | null = null; + private initialMouseY: number = 0; + private initialBlockY: number = 0; + private draggedBlockElement: HTMLElement | null = null; + private draggedBlockHeight: number = 0; + private lastUpdateTime: number = 0; + private updateThrottle: number = 80; // milliseconds constructor(component: any) { this.component = component; @@ -31,26 +38,71 @@ export class WysiwygDragDropHandler { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', block.id); - // Update component state (for parent drag handler access) + // Hide the default drag image + const emptyImg = new Image(); + emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; + e.dataTransfer.setDragImage(emptyImg, 0, 0); + + // Store initial mouse position and block element + this.initialMouseY = e.clientY; + this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`); + + if (this.draggedBlockElement) { + const rect = this.draggedBlockElement.getBoundingClientRect(); + this.draggedBlockHeight = rect.height; + this.initialBlockY = rect.top; + + // Create drop indicator + this.createDropIndicator(); + + // Set up drag event listeners + document.addEventListener('dragover', this.handleGlobalDragOver); + document.addEventListener('dragend', this.handleGlobalDragEnd); + } + + // Update component state this.component.draggedBlockId = this.draggedBlockId; - // The parent component already handles adding dragging classes programmatically + // Add dragging class after a small delay + setTimeout(() => { + if (this.draggedBlockElement) { + this.draggedBlockElement.classList.add('dragging'); + } + if (this.component.editorContentRef) { + this.component.editorContentRef.classList.add('dragging'); + } + }, 10); } /** * Handles drag end */ handleDragEnd(): void { + // Clean up visual state + const allBlocks = this.component.editorContentRef.querySelectorAll('.block-wrapper'); + allBlocks.forEach((block: HTMLElement) => { + block.classList.remove('dragging', 'move-up', 'move-down'); + block.style.removeProperty('--drag-offset'); + block.style.removeProperty('transform'); + }); + + // Remove dragging class from editor + if (this.component.editorContentRef) { + this.component.editorContentRef.classList.remove('dragging'); + } + + // Reset drag state this.draggedBlockId = null; this.dragOverBlockId = null; this.dragOverPosition = null; + this.draggedBlockElement = null; + this.draggedBlockHeight = 0; + this.initialBlockY = 0; // Update component state this.component.draggedBlockId = null; this.component.dragOverBlockId = null; this.component.dragOverPosition = null; - - // The parent component already handles removing dragging classes programmatically } /** @@ -126,4 +178,227 @@ export class WysiwygDragDropHandler { if (!this.isDragOver(blockId)) return ''; return this.dragOverPosition === 'before' ? 'drag-over-before' : 'drag-over-after'; } + + + /** + * Creates the drop indicator element + */ + private createDropIndicator(): void { + this.dropIndicator = document.createElement('div'); + this.dropIndicator.className = 'drop-indicator'; + this.dropIndicator.style.display = 'none'; + this.component.editorContentRef.appendChild(this.dropIndicator); + } + + /** + * Handles global dragover to update dragged block position and move other blocks + */ + private handleGlobalDragOver = (e: DragEvent): void => { + e.preventDefault(); + + if (!this.draggedBlockElement) return; + + // Calculate vertical offset from initial position + const deltaY = e.clientY - this.initialMouseY; + + // Apply transform to move the dragged block vertically + this.draggedBlockElement.style.transform = `translateY(${deltaY}px)`; + + // Throttle position updates to reduce stuttering + const now = Date.now(); + if (now - this.lastUpdateTime < this.updateThrottle) { + return; + } + this.lastUpdateTime = now; + + // Calculate which blocks should move + this.updateBlockPositions(e.clientY); + }; + + /** + * Updates block positions based on cursor position + */ + private updateBlockPositions(mouseY: number): void { + const blocks = Array.from(this.component.editorContentRef.querySelectorAll('.block-wrapper')) as HTMLElement[]; + const draggedIndex = blocks.findIndex(b => b.getAttribute('data-block-id') === this.draggedBlockId); + + if (draggedIndex === -1) return; + + // Reset all transforms first (except the dragged block) + blocks.forEach(block => { + if (block.getAttribute('data-block-id') !== this.draggedBlockId) { + block.classList.remove('move-up', 'move-down'); + block.style.removeProperty('--drag-offset'); + } + }); + + // Calculate where the dragged block should be inserted + let newIndex = blocks.length; // Default to end + + for (let i = 0; i < blocks.length; i++) { + if (i === draggedIndex) continue; + + const block = blocks[i]; + const rect = block.getBoundingClientRect(); + const blockTop = rect.top; + + // Check if mouse is above this block's middle + if (mouseY < blockTop + (rect.height * 0.5)) { + newIndex = i; + break; + } + } + + + // Apply transforms to move blocks out of the way + for (let i = 0; i < blocks.length; i++) { + if (i === draggedIndex) continue; + + const block = blocks[i]; + + // Determine if this block needs to move + if (draggedIndex < newIndex) { + // Dragging down: blocks between original and new position move up + if (i > draggedIndex && i < newIndex) { + block.classList.add('move-up'); + block.style.setProperty('--drag-offset', `${this.draggedBlockHeight}px`); + } + } else if (draggedIndex > newIndex) { + // Dragging up: blocks between new and original position move down + if (i >= newIndex && i < draggedIndex) { + block.classList.add('move-down'); + block.style.setProperty('--drag-offset', `${this.draggedBlockHeight}px`); + } + } + } + + // Update drop indicator position + this.updateDropIndicator(blocks, newIndex, draggedIndex); + } + + /** + * Updates the drop indicator position + */ + private updateDropIndicator(blocks: HTMLElement[], targetIndex: number, draggedIndex: number): void { + if (!this.dropIndicator || !this.draggedBlockElement) return; + + this.dropIndicator.style.display = 'block'; + this.dropIndicator.style.height = `${this.draggedBlockHeight}px`; + + const containerRect = this.component.editorContentRef.getBoundingClientRect(); + // Calculate where the block will actually land + let topPosition = 0; + + if (targetIndex === 0) { + // Before first block + topPosition = 0; + } else { + // After a specific block + const prevIndex = targetIndex - 1; + let blockCount = 0; + + // Find the visual position of the block that will be before our dropped block + for (let i = 0; i < blocks.length; i++) { + if (i === draggedIndex) continue; // Skip the dragged block + + if (blockCount === prevIndex) { + const rect = blocks[i].getBoundingClientRect(); + topPosition = rect.bottom - containerRect.top + 16; // 16px gap + break; + } + blockCount++; + } + } + + this.dropIndicator.style.top = `${topPosition}px`; + } + + /** + * Handles global drag end + */ + private handleGlobalDragEnd = (): void => { + // Clean up event listeners + document.removeEventListener('dragover', this.handleGlobalDragOver); + document.removeEventListener('dragend', this.handleGlobalDragEnd); + + // Remove drop indicator + if (this.dropIndicator) { + this.dropIndicator.remove(); + this.dropIndicator = null; + } + + // Trigger the actual drop if we have a dragged block + if (this.draggedBlockId) { + // Small delay to ensure transforms are applied + requestAnimationFrame(() => { + this.performDrop(); + // Call the regular drag end handler after drop + this.handleDragEnd(); + }); + } else { + // Call the regular drag end handler + this.handleDragEnd(); + } + }; + + /** + * Performs the actual drop operation + */ + private performDrop(): void { + if (!this.draggedBlockId) return; + + // Get the visual order of blocks based on their positions + const blockElements = Array.from(this.component.editorContentRef.querySelectorAll('.block-wrapper')) as HTMLElement[]; + const draggedElement = blockElements.find(el => el.getAttribute('data-block-id') === this.draggedBlockId); + + if (!draggedElement) return; + + + // Create an array of blocks with their visual positions + const visualOrder = blockElements.map(el => { + const id = el.getAttribute('data-block-id'); + const rect = el.getBoundingClientRect(); + const centerY = rect.top + rect.height / 2; + return { id, centerY, element: el }; + }); + + // Sort by visual Y position + visualOrder.sort((a, b) => a.centerY - b.centerY); + + // Get the new order of block IDs + const newBlockIds = visualOrder.map(item => item.id).filter(id => id !== null); + + // Find the original block data + const originalBlocks = [...this.component.blocks]; + const draggedBlock = originalBlocks.find(b => b.id === this.draggedBlockId); + + if (!draggedBlock) return; + + // Check if order actually changed + const oldOrder = originalBlocks.map(b => b.id); + const orderChanged = !newBlockIds.every((id, index) => id === oldOrder[index]); + + if (!orderChanged) { + return; + } + + // Reorder blocks based on visual positions + const newBlocks = newBlockIds.map(id => originalBlocks.find(b => b.id === id)!).filter(Boolean); + + // Update blocks + this.component.blocks = newBlocks; + + // Re-render blocks programmatically + this.component.renderBlocksProgrammatically(); + + // Update value + this.component.updateValue(); + + // Focus the moved block after a delay + setTimeout(() => { + if (draggedBlock.type !== 'divider') { + this.component.blockOperations.focusBlock(draggedBlock.id); + } + }, 100); + } } \ No newline at end of file diff --git a/ts_web/elements/wysiwyg/wysiwyg.styles.ts b/ts_web/elements/wysiwyg/wysiwyg.styles.ts index 3ed5b22..5afddfd 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.styles.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.styles.ts @@ -326,7 +326,7 @@ export const wysiwygStyles = css` /* Drag and Drop Styles */ .block-wrapper { position: relative; - transition: all 0.2s ease; + transition: transform 0.3s ease, opacity 0.2s ease; } /* Ensure proper spacing context for blocks */ @@ -385,31 +385,41 @@ export const wysiwygStyles = css` } .block-wrapper.dragging { - opacity: 0.5; + opacity: 0.8; + pointer-events: none; + position: relative; + z-index: 2001; + transition: none !important; } - .block-wrapper.drag-over-before::before { - content: ''; - position: absolute; - top: -8px; - left: -8px; - right: -8px; - height: 2px; - background: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; - border-radius: 1px; - box-shadow: 0 0 8px ${cssManager.bdTheme('rgba(0, 102, 204, 0.4)', 'rgba(77, 148, 255, 0.4)')}; + + /* Blocks that should move out of the way */ + .block-wrapper.move-down { + transform: translateY(var(--drag-offset, 0px)); } - .block-wrapper.drag-over-after::after { - content: ''; + .block-wrapper.move-up { + transform: translateY(calc(-1 * var(--drag-offset, 0px))); + } + + /* Drop indicator */ + .drop-indicator { position: absolute; - bottom: -8px; - left: -8px; - right: -8px; - height: 2px; - background: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; - border-radius: 1px; - box-shadow: 0 0 8px ${cssManager.bdTheme('rgba(0, 102, 204, 0.4)', 'rgba(77, 148, 255, 0.4)')}; + left: 0; + right: 0; + background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.08)', 'rgba(77, 148, 255, 0.08)')}; + border: 2px dashed ${cssManager.bdTheme('#0066cc', '#4d94ff')}; + border-radius: 8px; + transition: top 0.2s ease, height 0.2s ease; + pointer-events: none; + z-index: 1999; + box-sizing: border-box; + } + + /* Remove old drag-over styles */ + .block-wrapper.drag-over-before, + .block-wrapper.drag-over-after { + /* No longer needed, using drop indicator instead */ } .editor-content.dragging * {