diff --git a/changelog.md b/changelog.md index b2c896d..b688211 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,9 @@ # Changelog -## 2025-06-28 - 1.10.10 - improve(dees-dashboardgrid) +## 2025-06-29 - 1.10.10 - improve(dees-dashboardgrid, dees-input-wysiwyg) Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js +Dashboard Grid improvements: - Improved margin system supporting uniform or individual margins (top, right, bottom, left) - Added collision detection to prevent widget overlap during drag operations - Implemented auto-positioning for new widgets to find first available space @@ -18,6 +19,17 @@ Enhanced dashboard grid component with advanced spacing and layout features insp - Added findAvailablePosition() for intelligent widget placement - Improved drag and resize calculations for pixel-perfect positioning +WYSIWYG editor drag and drop fixes: +- Fixed drop indicator positioning to properly account for block margins +- Added defensive checks in drag event handlers to prevent potential crashes +- Improved updateBlockPositions with null checks and error handling +- Updated drop indicator calculation to use simplified margin approach +- Fixed drop indicator height to match the exact space occupied by dragged blocks +- Improved drop indicator positioning algorithm to accurately show where blocks will land +- Simplified visual block position calculations accounting for CSS transforms +- Enhanced margin calculation to use correct values based on block type (16px for paragraphs, 24px for headings, 20px for code/quotes) +- Fixed index calculation issue when dragging blocks downward by adjusting target index for excluded dragged block + ## 2025-06-28 - 1.10.9 - feat(dees-dashboardgrid) Add new dashboard grid component with drag-and-drop and resize capabilities diff --git a/test/test.wysiwyg-blockmovement.browser.ts b/test/test.wysiwyg-blockmovement.browser.ts new file mode 100644 index 0000000..889c674 --- /dev/null +++ b/test/test.wysiwyg-blockmovement.browser.ts @@ -0,0 +1,85 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg block movement during drag', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Block 1' }, + { id: 'block2', type: 'paragraph', content: 'Block 2' }, + { id: 'block3', type: 'paragraph', content: 'Block 3' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; + + // Start dragging block 1 + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: () => {}, + setDragImage: () => {} + }, + clientY: 50, + preventDefault: () => {}, + } as any; + + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + + // Wait for dragging class + await new Promise(resolve => setTimeout(resolve, 20)); + + // Verify drag state + expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); + + // Check that drag height was calculated + console.log('Checking drag height...'); + const dragHandler = element.dragDropHandler as any; + console.log('draggedBlockHeight:', dragHandler.draggedBlockHeight); + console.log('draggedBlockContentHeight:', dragHandler.draggedBlockContentHeight); + + // Manually call updateBlockPositions to simulate drag movement + console.log('Simulating drag movement...'); + const updateBlockPositions = dragHandler.updateBlockPositions.bind(dragHandler); + + // Simulate dragging down past block 2 + const block2 = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement; + const block2Rect = block2.getBoundingClientRect(); + const dragToY = block2Rect.bottom + 10; + + console.log('Dragging to Y position:', dragToY); + updateBlockPositions(dragToY); + + // Check if blocks have moved + await new Promise(resolve => setTimeout(resolve, 50)); + + const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper')); + console.log('Block states after drag:'); + blocks.forEach((block, i) => { + const classes = block.className; + const offset = (block as HTMLElement).style.getPropertyValue('--drag-offset'); + console.log(`Block ${i}: classes="${classes}", offset="${offset}"`); + }); + + // Check that at least one block has move class + const movedUpBlocks = editorContent.querySelectorAll('.block-wrapper.move-up'); + const movedDownBlocks = editorContent.querySelectorAll('.block-wrapper.move-down'); + console.log('Moved up blocks:', movedUpBlocks.length); + console.log('Moved down blocks:', movedDownBlocks.length); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragdrop-simple.browser.ts b/test/test.wysiwyg-dragdrop-simple.browser.ts new file mode 100644 index 0000000..d0a8d64 --- /dev/null +++ b/test/test.wysiwyg-dragdrop-simple.browser.ts @@ -0,0 +1,95 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg drag handler initialization', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + // Wait for element to be ready + await element.updateComplete; + + // Check that drag handler is initialized + expect(element.dragDropHandler).toBeTruthy(); + + // Set initial content with multiple blocks + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'First paragraph' }, + { id: 'block2', type: 'paragraph', content: 'Second paragraph' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + // Check that editor content ref exists + console.log('editorContentRef:', element.editorContentRef); + expect(element.editorContentRef).toBeTruthy(); + + // Check that blocks are rendered + const blockWrappers = element.shadowRoot!.querySelectorAll('.block-wrapper'); + console.log('Number of block wrappers:', blockWrappers.length); + expect(blockWrappers.length).toEqual(2); + + // Check drag handles + const dragHandles = element.shadowRoot!.querySelectorAll('.drag-handle'); + console.log('Number of drag handles:', dragHandles.length); + expect(dragHandles.length).toEqual(2); + + // Clean up + document.body.removeChild(element); +}); + +tap.test('wysiwyg drag start behavior', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Test block' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const dragHandle = element.shadowRoot!.querySelector('.drag-handle') as HTMLElement; + expect(dragHandle).toBeTruthy(); + + // Check that drag handle has draggable attribute + console.log('Drag handle draggable:', dragHandle.draggable); + expect(dragHandle.draggable).toBeTrue(); + + // Test drag handler state before drag + console.log('Initial drag state:', element.dragDropHandler.dragState); + expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull(); + + // Try to manually call handleDragStart + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: (type: string, data: string) => { + console.log('setData called with:', type, data); + }, + setDragImage: (img: any, x: number, y: number) => { + console.log('setDragImage called'); + } + }, + clientY: 100, + preventDefault: () => {}, + } as any; + + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + + // Check drag state after drag start + console.log('Drag state after start:', element.dragDropHandler.dragState); + expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragdrop-visual.browser.ts b/test/test.wysiwyg-dragdrop-visual.browser.ts new file mode 100644 index 0000000..7417e29 --- /dev/null +++ b/test/test.wysiwyg-dragdrop-visual.browser.ts @@ -0,0 +1,133 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg drag visual feedback - block movement', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Block 1' }, + { id: 'block2', type: 'paragraph', content: 'Block 2' }, + { id: 'block3', type: 'paragraph', content: 'Block 3' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; + + // Manually start drag + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: (type: string, data: string) => {}, + setDragImage: (img: any, x: number, y: number) => {} + }, + clientY: 50, + preventDefault: () => {}, + } as any; + + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + + // Wait for dragging class + await new Promise(resolve => setTimeout(resolve, 20)); + + // Check dragging state + console.log('Block 1 classes:', block1.className); + console.log('Editor content classes:', editorContent.className); + expect(block1.classList.contains('dragging')).toBeTrue(); + expect(editorContent.classList.contains('dragging')).toBeTrue(); + + // Check drop indicator exists + const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement; + console.log('Drop indicator:', dropIndicator); + expect(dropIndicator).toBeTruthy(); + + // Test block movement calculation + console.log('Testing updateBlockPositions...'); + + // Access private method for testing + const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler); + + // Simulate dragging to different position + updateBlockPositions(150); // Move down + + // Check if blocks have move classes + const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper')); + console.log('Block classes after move:'); + blocks.forEach((block, i) => { + console.log(`Block ${i}:`, block.className, 'transform:', (block as HTMLElement).style.getPropertyValue('--drag-offset')); + }); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); +}); + +tap.test('wysiwyg drop indicator positioning', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Paragraph 1' }, + { id: 'block2', type: 'heading-2', content: 'Heading 2' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + + // Start dragging first block + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: (type: string, data: string) => {}, + setDragImage: (img: any, x: number, y: number) => {} + }, + clientY: 50, + preventDefault: () => {}, + } as any; + + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + + // Wait for initialization + await new Promise(resolve => setTimeout(resolve, 20)); + + // Get drop indicator + const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement; + expect(dropIndicator).toBeTruthy(); + + // Check initial display state + console.log('Drop indicator initial display:', dropIndicator.style.display); + + // Trigger updateBlockPositions to see drop indicator + const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler); + updateBlockPositions(100); + + // Check drop indicator position + console.log('Drop indicator after update:'); + console.log('- display:', dropIndicator.style.display); + console.log('- top:', dropIndicator.style.top); + console.log('- height:', dropIndicator.style.height); + + expect(dropIndicator.style.display).toEqual('block'); + expect(dropIndicator.style.top).toBeTruthy(); + expect(dropIndicator.style.height).toBeTruthy(); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragdrop.browser.ts b/test/test.wysiwyg-dragdrop.browser.ts new file mode 100644 index 0000000..388deeb --- /dev/null +++ b/test/test.wysiwyg-dragdrop.browser.ts @@ -0,0 +1,172 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg drag and drop should work correctly', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + // Wait for element to be ready + await element.updateComplete; + + // Set initial content with multiple blocks + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'First paragraph' }, + { id: 'block2', type: 'heading-2', content: 'Test Heading' }, + { id: 'block3', type: 'paragraph', content: 'Second paragraph' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + // Check that blocks are rendered + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + expect(editorContent).toBeTruthy(); + + const blockWrappers = editorContent.querySelectorAll('.block-wrapper'); + expect(blockWrappers.length).toEqual(3); + + // Test drag handles exist for non-divider blocks + const dragHandles = editorContent.querySelectorAll('.drag-handle'); + expect(dragHandles.length).toEqual(3); + + // Get references to specific blocks + const firstBlock = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; + const secondBlock = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement; + const firstDragHandle = firstBlock.querySelector('.drag-handle') as HTMLElement; + + expect(firstBlock).toBeTruthy(); + expect(secondBlock).toBeTruthy(); + expect(firstDragHandle).toBeTruthy(); + + // Test drag initialization + console.log('Testing drag initialization...'); + + // Create drag event + const dragStartEvent = new DragEvent('dragstart', { + dataTransfer: new DataTransfer(), + clientY: 100, + bubbles: true + }); + + // Simulate drag start + firstDragHandle.dispatchEvent(dragStartEvent); + + // Check that drag state is initialized + expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); + + // Check that dragging class is applied + await new Promise(resolve => setTimeout(resolve, 20)); // Wait for setTimeout in drag start + expect(firstBlock.classList.contains('dragging')).toBeTrue(); + expect(editorContent.classList.contains('dragging')).toBeTrue(); + + // Test drop indicator creation + const dropIndicator = editorContent.querySelector('.drop-indicator'); + expect(dropIndicator).toBeTruthy(); + + // Simulate drag over + const dragOverEvent = new DragEvent('dragover', { + dataTransfer: new DataTransfer(), + clientY: 200, + bubbles: true, + cancelable: true + }); + + document.dispatchEvent(dragOverEvent); + + // Check that blocks move out of the way + console.log('Checking block movements...'); + const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper')); + const hasMovedBlocks = blocks.some(block => + block.classList.contains('move-up') || block.classList.contains('move-down') + ); + + console.log('Blocks with move classes:', blocks.filter(block => + block.classList.contains('move-up') || block.classList.contains('move-down') + ).length); + + // Test drag end + const dragEndEvent = new DragEvent('dragend', { + bubbles: true + }); + + document.dispatchEvent(dragEndEvent); + + // Wait for cleanup + await new Promise(resolve => setTimeout(resolve, 150)); + + // Check that drag state is cleaned up + expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull(); + expect(firstBlock.classList.contains('dragging')).toBeFalse(); + expect(editorContent.classList.contains('dragging')).toBeFalse(); + + // Check that drop indicator is removed + const dropIndicatorAfter = editorContent.querySelector('.drop-indicator'); + expect(dropIndicatorAfter).toBeFalsy(); + + // Clean up + document.body.removeChild(element); +}); + +tap.test('wysiwyg drag and drop visual feedback', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Block 1' }, + { id: 'block2', type: 'paragraph', content: 'Block 2' }, + { id: 'block3', type: 'paragraph', content: 'Block 3' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; + const dragHandle1 = block1.querySelector('.drag-handle') as HTMLElement; + + // Start dragging block 1 + const dragStartEvent = new DragEvent('dragstart', { + dataTransfer: new DataTransfer(), + clientY: 50, + bubbles: true + }); + + dragHandle1.dispatchEvent(dragStartEvent); + + // Wait for dragging class + await new Promise(resolve => setTimeout(resolve, 20)); + + // Simulate dragging down + const dragOverEvent = new DragEvent('dragover', { + dataTransfer: new DataTransfer(), + clientY: 150, // Move down past block 2 + bubbles: true, + cancelable: true + }); + + // Trigger the global drag over handler + element.dragDropHandler['handleGlobalDragOver'](dragOverEvent); + + // Check that transform is applied to dragged block + const transform = block1.style.transform; + console.log('Dragged block transform:', transform); + expect(transform).toContain('translateY'); + + // Check drop indicator position + const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement; + if (dropIndicator) { + const indicatorStyle = dropIndicator.style; + console.log('Drop indicator position:', indicatorStyle.top, 'display:', indicatorStyle.display); + } + + // Clean up + document.body.removeChild(element); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-dragissue.browser.ts b/test/test.wysiwyg-dragissue.browser.ts new file mode 100644 index 0000000..1dfcf7e --- /dev/null +++ b/test/test.wysiwyg-dragissue.browser.ts @@ -0,0 +1,124 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg drag full flow without await', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Test block' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + // Mock drag event + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: (type: string, data: string) => { + console.log('setData:', type, data); + }, + setDragImage: (img: any, x: number, y: number) => { + console.log('setDragImage'); + } + }, + clientY: 100, + preventDefault: () => {}, + } as any; + + console.log('Starting drag...'); + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + console.log('Drag started'); + + // Check immediate state + expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); + + // Instead of await with setTimeout, use a done callback + return new Promise((resolve) => { + console.log('Setting up delayed check...'); + + // Use regular setTimeout + setTimeout(() => { + console.log('In setTimeout callback'); + + try { + const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement; + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + + console.log('Block has dragging class:', block1?.classList.contains('dragging')); + console.log('Editor has dragging class:', editorContent?.classList.contains('dragging')); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); + + resolve(); + } catch (error) { + console.error('Error in setTimeout:', error); + throw error; + } + }, 50); + }); +}); + +tap.test('identify the crash point', async () => { + console.log('Test started'); + + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + console.log('Element created'); + await element.updateComplete; + + console.log('Setting blocks'); + element.blocks = [{ id: 'block1', type: 'paragraph', content: 'Test' }]; + element.renderBlocksProgrammatically(); + + console.log('Waiting for update'); + await element.updateComplete; + + console.log('Creating mock event'); + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: () => {}, + setDragImage: () => {} + }, + clientY: 100, + preventDefault: () => {}, + } as any; + + console.log('Calling handleDragStart'); + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + + console.log('handleDragStart completed'); + + // Try different wait methods + console.log('About to wait...'); + + // Method 1: Direct promise + await Promise.resolve(); + console.log('Promise.resolve completed'); + + // Method 2: setTimeout 0 + await new Promise(resolve => setTimeout(resolve, 0)); + console.log('setTimeout 0 completed'); + + // Method 3: requestAnimationFrame + await new Promise(resolve => requestAnimationFrame(() => resolve(undefined))); + console.log('requestAnimationFrame completed'); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); + console.log('Cleanup completed'); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-dropindicator.browser.ts b/test/test.wysiwyg-dropindicator.browser.ts new file mode 100644 index 0000000..acc1e49 --- /dev/null +++ b/test/test.wysiwyg-dropindicator.browser.ts @@ -0,0 +1,108 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg drop indicator creation', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Test block' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + // Check editorContentRef + console.log('editorContentRef exists:', !!element.editorContentRef); + console.log('editorContentRef tagName:', element.editorContentRef?.tagName); + expect(element.editorContentRef).toBeTruthy(); + + // Check initial state - no drop indicator + let dropIndicator = element.shadowRoot!.querySelector('.drop-indicator'); + console.log('Drop indicator before drag:', dropIndicator); + expect(dropIndicator).toBeFalsy(); + + // Manually call createDropIndicator + try { + console.log('Calling createDropIndicator...'); + element.dragDropHandler['createDropIndicator'](); + console.log('createDropIndicator succeeded'); + } catch (error) { + console.error('Error creating drop indicator:', error); + throw error; + } + + // Check drop indicator was created + dropIndicator = element.shadowRoot!.querySelector('.drop-indicator'); + console.log('Drop indicator after creation:', dropIndicator); + console.log('Drop indicator parent:', dropIndicator?.parentElement?.className); + expect(dropIndicator).toBeTruthy(); + expect(dropIndicator!.style.display).toEqual('none'); + + // Clean up + document.body.removeChild(element); +}); + +tap.test('wysiwyg drag initialization with drop indicator', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Test block' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + // Mock drag event + const mockDragEvent = { + dataTransfer: { + effectAllowed: '', + setData: (type: string, data: string) => { + console.log('setData:', type, data); + }, + setDragImage: (img: any, x: number, y: number) => { + console.log('setDragImage'); + } + }, + clientY: 100, + preventDefault: () => {}, + } as any; + + console.log('Starting drag...'); + + try { + element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); + console.log('Drag start succeeded'); + } catch (error) { + console.error('Error during drag start:', error); + throw error; + } + + // Wait for async operations + await new Promise(resolve => setTimeout(resolve, 20)); + + // Check drop indicator exists + const dropIndicator = element.shadowRoot!.querySelector('.drop-indicator'); + console.log('Drop indicator after drag start:', dropIndicator); + expect(dropIndicator).toBeTruthy(); + + // Check drag state + console.log('Drag state:', element.dragDropHandler.dragState); + expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); + + // Clean up + element.dragDropHandler.handleDragEnd(); + document.body.removeChild(element); +}); + +tap.start(); \ No newline at end of file diff --git a/test/test.wysiwyg-eventlisteners.browser.ts b/test/test.wysiwyg-eventlisteners.browser.ts new file mode 100644 index 0000000..c0f8d0d --- /dev/null +++ b/test/test.wysiwyg-eventlisteners.browser.ts @@ -0,0 +1,114 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; + +// Initialize the element +DeesInputWysiwyg; + +tap.test('wysiwyg global event listeners', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Test block' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement; + console.log('Block 1 found:', !!block1); + + // Set up drag state manually without using handleDragStart + element.dragDropHandler['draggedBlockId'] = 'block1'; + element.dragDropHandler['draggedBlockElement'] = block1; + element.dragDropHandler['initialMouseY'] = 100; + + // Create drop indicator manually + element.dragDropHandler['createDropIndicator'](); + + // Test adding global event listeners + console.log('Adding event listeners...'); + const handleGlobalDragOver = element.dragDropHandler['handleGlobalDragOver']; + const handleGlobalDragEnd = element.dragDropHandler['handleGlobalDragEnd']; + + try { + document.addEventListener('dragover', handleGlobalDragOver); + console.log('dragover listener added'); + + document.addEventListener('dragend', handleGlobalDragEnd); + console.log('dragend listener added'); + } catch (error) { + console.error('Error adding event listeners:', error); + throw error; + } + + // Test firing a dragover event + console.log('Creating dragover event...'); + const dragOverEvent = new Event('dragover', { + bubbles: true, + cancelable: true + }); + Object.defineProperty(dragOverEvent, 'clientY', { value: 150 }); + + console.log('Dispatching dragover event...'); + document.dispatchEvent(dragOverEvent); + console.log('dragover event dispatched'); + + // Clean up + document.removeEventListener('dragover', handleGlobalDragOver); + document.removeEventListener('dragend', handleGlobalDragEnd); + + document.body.removeChild(element); +}); + +tap.test('wysiwyg setTimeout in drag start', async () => { + const element = document.createElement('dees-input-wysiwyg'); + document.body.appendChild(element); + + await element.updateComplete; + + // Set initial content + element.blocks = [ + { id: 'block1', type: 'paragraph', content: 'Test block' }, + ]; + element.renderBlocksProgrammatically(); + + await element.updateComplete; + + const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement; + const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; + + // Set drag state + element.dragDropHandler['draggedBlockId'] = 'block1'; + element.dragDropHandler['draggedBlockElement'] = block1; + + console.log('Testing setTimeout callback...'); + + // Test the setTimeout callback directly + try { + if (block1) { + console.log('Adding dragging class to block...'); + block1.classList.add('dragging'); + console.log('Block classes:', block1.className); + } + if (editorContent) { + console.log('Adding dragging class to editor...'); + editorContent.classList.add('dragging'); + console.log('Editor classes:', editorContent.className); + } + } catch (error) { + console.error('Error in setTimeout callback:', error); + throw error; + } + + expect(block1.classList.contains('dragging')).toBeTrue(); + expect(editorContent.classList.contains('dragging')).toBeTrue(); + + // Clean up + document.body.removeChild(element); +}); + +tap.start(); \ 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 a80f13a..56a4a0d 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.dragdrophandler.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.dragdrophandler.ts @@ -11,6 +11,8 @@ export class WysiwygDragDropHandler { private initialBlockY: number = 0; private draggedBlockElement: HTMLElement | null = null; private draggedBlockHeight: number = 0; + private draggedBlockContentHeight: number = 0; + private draggedBlockMarginTop: number = 0; private lastUpdateTime: number = 0; private updateThrottle: number = 80; // milliseconds @@ -48,11 +50,33 @@ export class WysiwygDragDropHandler { this.initialMouseY = e.clientY; this.draggedBlockElement = this.component.editorContentRef.querySelector(`[data-block-id="${block.id}"]`); + if (this.draggedBlockElement) { + // Get the wrapper rect for measurements const rect = this.draggedBlockElement.getBoundingClientRect(); - this.draggedBlockHeight = rect.height; this.initialBlockY = rect.top; + // Get the inner block element for proper measurements + const innerBlock = this.draggedBlockElement.querySelector('.block'); + if (innerBlock) { + const innerRect = innerBlock.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(innerBlock); + this.draggedBlockMarginTop = parseInt(computedStyle.marginTop) || 0; + this.draggedBlockContentHeight = innerRect.height; + } + + // The drop indicator should match the wrapper height exactly + // The wrapper already includes all the space the block occupies + this.draggedBlockHeight = rect.height; + + console.log('Drag measurements:', { + wrapperHeight: rect.height, + marginTop: this.draggedBlockMarginTop, + dropIndicatorHeight: this.draggedBlockHeight, + contentHeight: this.draggedBlockContentHeight, + blockId: block.id + }); + // Create drop indicator this.createDropIndicator(); @@ -98,6 +122,8 @@ export class WysiwygDragDropHandler { this.dragOverPosition = null; this.draggedBlockElement = null; this.draggedBlockHeight = 0; + this.draggedBlockContentHeight = 0; + this.draggedBlockMarginTop = 0; this.initialBlockY = 0; // Update component state @@ -284,34 +310,93 @@ export class WysiwygDragDropHandler { 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; + // Build array of visual block positions (excluding dragged block) + const visualBlocks: { index: number, top: number, bottom: number }[] = []; + + for (let i = 0; i < blocks.length; i++) { + if (i === draggedIndex) continue; // Skip the dragged block - // 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; + const block = blocks[i]; + const rect = block.getBoundingClientRect(); + let top = rect.top - containerRect.top; + let bottom = rect.bottom - containerRect.top; + + // Account for any transforms + const transform = window.getComputedStyle(block).transform; + if (transform && transform !== 'none') { + const matrix = new DOMMatrix(transform); + const yOffset = matrix.m42; + top += yOffset; + bottom += yOffset; + } + + visualBlocks.push({ index: i, top, bottom }); + } + + // Sort by visual position + visualBlocks.sort((a, b) => a.top - b.top); + + // Adjust targetIndex to account for excluded dragged block + let adjustedTargetIndex = targetIndex; + if (targetIndex > draggedIndex) { + adjustedTargetIndex--; // Reduce by 1 since dragged block is not in visualBlocks + } + + // Calculate drop position + // Get the margin that will be applied based on the dragged block type + let blockMargin = 16; // default margin + if (this.draggedBlockElement) { + const draggedBlock = this.component.blocks.find(b => b.id === this.draggedBlockId); + if (draggedBlock) { + const blockType = draggedBlock.type; + if (blockType === 'heading-1' || blockType === 'heading-2' || blockType === 'heading-3') { + blockMargin = 24; + } else if (blockType === 'code' || blockType === 'quote') { + blockMargin = 20; } - blockCount++; } } - this.dropIndicator.style.top = `${topPosition}px`; + if (adjustedTargetIndex === 0) { + // Insert at the very top - no margin needed for first block + topPosition = 0; + } else if (adjustedTargetIndex >= visualBlocks.length) { + // Insert at the end + const lastBlock = visualBlocks[visualBlocks.length - 1]; + if (lastBlock) { + topPosition = lastBlock.bottom; + // Add margin that will be applied to the dropped block + topPosition += blockMargin; + } + } else { + // Insert between blocks + const blockBefore = visualBlocks[adjustedTargetIndex - 1]; + if (blockBefore) { + topPosition = blockBefore.bottom; + // Add margin that will be applied to the dropped block + topPosition += blockMargin; + } + } + + // Set the indicator height to match the dragged block + this.dropIndicator.style.height = `${this.draggedBlockHeight}px`; + + // Set position + this.dropIndicator.style.top = `${Math.max(0, topPosition)}px`; + + console.log('Drop indicator update:', { + targetIndex, + adjustedTargetIndex, + draggedIndex, + topPosition, + height: this.draggedBlockHeight, + blockMargin, + visualBlocks: visualBlocks.map(b => ({ index: b.index, top: b.top, bottom: b.bottom })) + }); } /**