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
This commit is contained in:
2025-09-07 19:43:26 +02:00
parent 2346eea874
commit 53762645e0

View File

@@ -7,6 +7,8 @@ class LivePreviewManager {
this.activeElement = null; this.activeElement = null;
this.originalContent = null; this.originalContent = null;
this.originalStyles = null; this.originalStyles = null;
this.resizeObserver = null;
this.onHeightChangeCallback = null;
} }
schedulePreview(element, newValue, elementType) { schedulePreview(element, newValue, elementType) {
@@ -33,6 +35,8 @@ class LivePreviewManager {
// Apply preview styling and content // Apply preview styling and content
this.applyPreviewContent(element, newValue, elementType); this.applyPreviewContent(element, newValue, elementType);
// ResizeObserver will automatically detect height changes
} }
extractOriginalContent(element, elementType) { extractOriginalContent(element, elementType) {
@@ -101,6 +105,9 @@ class LivePreviewManager {
this.previewTimeouts.delete(elementId); this.previewTimeouts.delete(elementId);
} }
// Stop ResizeObserver
this.stopResizeObserver();
// Restore original content // Restore original content
if (this.originalContent && element === this.activeElement) { if (this.originalContent && element === this.activeElement) {
this.restoreOriginalContent(element); this.restoreOriginalContent(element);
@@ -138,6 +145,36 @@ class LivePreviewManager {
setActiveElement(element) { setActiveElement(element) {
this.activeElement = element; this.activeElement = element;
this.originalContent = null; 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 // Initialize preview manager for this element
this.previewManager.setActiveElement(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 // Create form
const form = this.createEditForm(contentId, config, currentContent); 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) { positionForm(element, overlay) {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
@@ -398,6 +440,55 @@ export class InsertrFormRenderer {
overlay.style.top = `${top}px`; overlay.style.top = `${top}px`;
overlay.style.left = `${left}px`; overlay.style.left = `${left}px`;
overlay.style.zIndex = '10000'; 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'
});
}
});
} }
/** /**