From 53762645e0be9b840bd881c61ec7629fcb2f5c14 Mon Sep 17 00:00:00 2001 From: Joakim Date: Sun, 7 Sep 2025 19:43:26 +0200 Subject: [PATCH] enhance: implement dynamic modal repositioning with ResizeObserver - Add ResizeObserver to automatically detect element height changes during live preview - Implement repositionModal() to keep modal positioned below expanded content - Combine modal repositioning with scroll-to-fit for optimal visibility - Solve UX issue where modal would overlap newly added content --- lib/src/ui/form-renderer.js | 93 ++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/lib/src/ui/form-renderer.js b/lib/src/ui/form-renderer.js index 20d04ae..08a3b61 100644 --- a/lib/src/ui/form-renderer.js +++ b/lib/src/ui/form-renderer.js @@ -7,6 +7,8 @@ class LivePreviewManager { this.activeElement = null; this.originalContent = null; this.originalStyles = null; + this.resizeObserver = null; + this.onHeightChangeCallback = null; } schedulePreview(element, newValue, elementType) { @@ -33,6 +35,8 @@ class LivePreviewManager { // Apply preview styling and content this.applyPreviewContent(element, newValue, elementType); + + // ResizeObserver will automatically detect height changes } extractOriginalContent(element, elementType) { @@ -101,6 +105,9 @@ class LivePreviewManager { this.previewTimeouts.delete(elementId); } + // Stop ResizeObserver + this.stopResizeObserver(); + // Restore original content if (this.originalContent && element === this.activeElement) { this.restoreOriginalContent(element); @@ -138,6 +145,36 @@ class LivePreviewManager { setActiveElement(element) { this.activeElement = element; this.originalContent = null; + this.startResizeObserver(element); + } + + setHeightChangeCallback(callback) { + this.onHeightChangeCallback = callback; + } + + startResizeObserver(element) { + // Clean up existing observer + this.stopResizeObserver(); + + // Create new ResizeObserver for this element + this.resizeObserver = new ResizeObserver(entries => { + // Use requestAnimationFrame to ensure smooth updates + requestAnimationFrame(() => { + if (this.onHeightChangeCallback && element === this.activeElement) { + this.onHeightChangeCallback(element); + } + }); + }); + + // Start observing the element + this.resizeObserver.observe(element); + } + + stopResizeObserver() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } } } @@ -168,6 +205,11 @@ export class InsertrFormRenderer { // Initialize preview manager for this element this.previewManager.setActiveElement(element); + + // Set up height change callback to reposition modal based on new element size + this.previewManager.setHeightChangeCallback((changedElement) => { + this.repositionModal(changedElement, overlay); + }); // Create form const form = this.createEditForm(contentId, config, currentContent); @@ -357,7 +399,7 @@ export class InsertrFormRenderer { } /** - * Position form relative to element + * Position form relative to element and ensure visibility with scroll-to-fit */ positionForm(element, overlay) { const rect = element.getBoundingClientRect(); @@ -398,6 +440,55 @@ export class InsertrFormRenderer { overlay.style.top = `${top}px`; overlay.style.left = `${left}px`; overlay.style.zIndex = '10000'; + + // Ensure modal is fully visible after positioning + this.ensureModalVisible(element, overlay); + } + + /** + * Reposition modal based on current element size and ensure visibility + */ + repositionModal(element, overlay) { + // Wait for next frame to ensure DOM is updated + requestAnimationFrame(() => { + const rect = element.getBoundingClientRect(); + const form = overlay.querySelector('.insertr-edit-form'); + + // Calculate new position below the current element boundaries + const newTop = rect.bottom + window.scrollY + 10; + + // Update modal position + overlay.style.top = `${newTop}px`; + + // After repositioning, ensure modal is still visible + this.ensureModalVisible(element, overlay); + }); + } + + /** + * Ensure modal is fully visible by scrolling viewport if necessary + */ + ensureModalVisible(element, overlay) { + // Wait for next frame to ensure DOM is updated + requestAnimationFrame(() => { + const modal = overlay.querySelector('.insertr-edit-form'); + const modalRect = modal.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + + // Calculate if modal extends below viewport + const modalBottom = modalRect.bottom; + const viewportBottom = viewportHeight; + + if (modalBottom > viewportBottom) { + // Calculate scroll amount needed with some padding + const scrollAmount = modalBottom - viewportBottom + 20; + + window.scrollBy({ + top: scrollAmount, + behavior: 'smooth' + }); + } + }); } /**