Change infinite scroll to use window scroll instead of container

- Remove overflow-y:auto and max-height constraints from property list
- Use viewport-based IntersectionObserver (root: null) instead of container
- Track and maintain max grid height to prevent layout shift on scroll up
- Clear max height only on filter/map change (not on normal scroll)
- Update scroll anchor methods to use window.scrollY/scrollBy
- Mobile continues to use pagination (desktop only infinite scroll)
This commit is contained in:
Hanson.xyz Dev
2025-12-16 14:21:29 -06:00
parent 761384ee1b
commit 53d3c41917
4 changed files with 67 additions and 46 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1143,6 +1143,7 @@
totalPosts: 0, totalPosts: 0,
pendingRequests: {}, // Object: pageNum -> jqXHR pendingRequests: {}, // Object: pageNum -> jqXHR
isEnabled: false, isEnabled: false,
maxGridHeight: 0, // Track max height to prevent layout shift
config: { config: {
maxPagesInDOM: 5, maxPagesInDOM: 5,
bufferPages: 2, bufferPages: 2,
@@ -1154,6 +1155,7 @@
/** /**
* Infinite Scroll Module * Infinite Scroll Module
* Bidirectional infinite scroll for property list (desktop map view only) * Bidirectional infinite scroll for property list (desktop map view only)
* Uses window scroll - page extends naturally as content loads
*/ */
var InfiniteScroll = { var InfiniteScroll = {
$container: null, $container: null,
@@ -1164,7 +1166,6 @@
$bottomLoader: null, $bottomLoader: null,
topObserver: null, topObserver: null,
bottomObserver: null, bottomObserver: null,
scrollContainer: null,
/** /**
* Initialize infinite scroll * Initialize infinite scroll
@@ -1208,6 +1209,9 @@
// Mark as enabled // Mark as enabled
InfiniteScrollState.isEnabled = true; InfiniteScrollState.isEnabled = true;
this.$container.addClass('infinite-scroll-enabled'); this.$container.addClass('infinite-scroll-enabled');
// Track initial height
this.updateMaxHeight();
}, },
/** /**
@@ -1217,6 +1221,7 @@
InfiniteScrollState.pages = {}; InfiniteScrollState.pages = {};
InfiniteScrollState.visibleRange = { first: 1, last: 1 }; InfiniteScrollState.visibleRange = { first: 1, last: 1 };
InfiniteScrollState.pendingRequests = {}; InfiniteScrollState.pendingRequests = {};
// Note: maxGridHeight is NOT reset here - only on full reset/filter change
}, },
/** /**
@@ -1237,9 +1242,6 @@
// Insert elements // Insert elements
this.$grid.before(this.$topSentinel).before(this.$topLoader); this.$grid.before(this.$topSentinel).before(this.$topLoader);
this.$grid.after(this.$bottomLoader).after(this.$bottomSentinel); this.$grid.after(this.$bottomLoader).after(this.$bottomSentinel);
// Set scroll container reference
this.scrollContainer = this.$container[0];
}, },
/** /**
@@ -1265,13 +1267,15 @@
/** /**
* Setup IntersectionObservers for top and bottom sentinels * Setup IntersectionObservers for top and bottom sentinels
* Uses viewport (root: null) since we're using window scroll
*/ */
setupObservers: function() { setupObservers: function() {
var self = this; var self = this;
// Use viewport as root (null) with margin for pre-loading
var observerOptions = { var observerOptions = {
root: this.scrollContainer, root: null, // Use viewport, not container
rootMargin: '400px 0px', rootMargin: '600px 0px', // Load when within 600px of viewport
threshold: 0 threshold: 0
}; };
@@ -1298,6 +1302,29 @@
this.topObserver.observe(this.$topSentinel[0]); this.topObserver.observe(this.$topSentinel[0]);
}, },
/**
* Track and maintain maximum grid height to prevent layout shift
*/
updateMaxHeight: function() {
if (!this.$grid) return;
var currentHeight = this.$grid.outerHeight();
if (currentHeight > InfiniteScrollState.maxGridHeight) {
InfiniteScrollState.maxGridHeight = currentHeight;
this.$grid.css('min-height', currentHeight + 'px');
}
},
/**
* Clear max height tracking (called on filter change)
*/
clearMaxHeight: function() {
InfiniteScrollState.maxGridHeight = 0;
if (this.$grid) {
this.$grid.css('min-height', '');
}
},
/** /**
* Load the next page (append) * Load the next page (append)
*/ */
@@ -1442,6 +1469,9 @@
// Restore scroll anchor // Restore scroll anchor
this.restoreScrollAnchor(anchor); this.restoreScrollAnchor(anchor);
// Update max height tracking
this.updateMaxHeight();
// Trigger image lazy loading // Trigger image lazy loading
if (typeof CardImageLoader !== 'undefined') { if (typeof CardImageLoader !== 'undefined') {
CardImageLoader.loadVisibleImages(); CardImageLoader.loadVisibleImages();
@@ -1555,22 +1585,27 @@
}, },
/** /**
* Save scroll anchor (first visible card) * Save scroll anchor (first visible card in viewport)
* Uses window scroll position
*/ */
saveScrollAnchor: function() { saveScrollAnchor: function() {
if (!this.scrollContainer) return null; if (!this.$grid) return null;
var containerRect = this.scrollContainer.getBoundingClientRect(); var viewportTop = window.scrollY || window.pageYOffset;
var viewportBottom = viewportTop + window.innerHeight;
var $cards = this.$grid.find('.property-card'); var $cards = this.$grid.find('.property-card');
var anchor = null; var anchor = null;
$cards.each(function() { $cards.each(function() {
var rect = this.getBoundingClientRect(); var rect = this.getBoundingClientRect();
// Find first card that is at least partially visible var elementTop = rect.top + viewportTop;
if (rect.bottom > containerRect.top && rect.top < containerRect.bottom) { var elementBottom = rect.bottom + viewportTop;
// Find first card that is at least partially visible in viewport
if (elementBottom > viewportTop && elementTop < viewportBottom) {
anchor = { anchor = {
element: this, element: this,
offsetFromTop: rect.top - containerRect.top offsetFromViewport: rect.top // Distance from top of viewport
}; };
return false; // break return false; // break
} }
@@ -1580,23 +1615,24 @@
}, },
/** /**
* Restore scroll position to maintain anchor * Restore scroll position to maintain anchor element's viewport position
* Uses window scroll
*/ */
restoreScrollAnchor: function(anchor) { restoreScrollAnchor: function(anchor) {
if (!anchor || !anchor.element || !this.scrollContainer) return; if (!anchor || !anchor.element) return;
var containerRect = this.scrollContainer.getBoundingClientRect();
var elementRect = anchor.element.getBoundingClientRect(); var elementRect = anchor.element.getBoundingClientRect();
var currentOffset = elementRect.top - containerRect.top; var currentOffset = elementRect.top;
var diff = currentOffset - anchor.offsetFromTop; var diff = currentOffset - anchor.offsetFromViewport;
if (Math.abs(diff) > 1) { if (Math.abs(diff) > 1) {
this.scrollContainer.scrollTop += diff; window.scrollBy(0, diff);
} }
}, },
/** /**
* Reset infinite scroll (called on filter/map change) * Reset infinite scroll (called on filter/map change)
* Clears max height since filters changed
*/ */
reset: function() { reset: function() {
// Cancel all pending infinite scroll requests // Cancel all pending infinite scroll requests
@@ -1609,13 +1645,14 @@
// Also cancel pending property and cluster requests via RequestQueue // Also cancel pending property and cluster requests via RequestQueue
RequestQueue.cancel(); RequestQueue.cancel();
// Clear max height since filters are changing
this.clearMaxHeight();
// Clear state // Clear state
this.resetState(); this.resetState();
// Scroll to top // Scroll to top of page
if (this.scrollContainer) { window.scrollTo(0, 0);
this.scrollContainer.scrollTop = 0;
}
}, },
/** /**
@@ -1653,6 +1690,9 @@
this.$grid.find('.infinite-scroll-placeholder').remove(); this.$grid.find('.infinite-scroll-placeholder').remove();
} }
// Clear max height
this.clearMaxHeight();
// Reset state // Reset state
this.resetState(); this.resetState();
InfiniteScrollState.isEnabled = false; InfiniteScrollState.isEnabled = false;
@@ -572,30 +572,11 @@
} }
// Infinite Scroll (desktop map view only) // Infinite Scroll (desktop map view only)
// Uses window scroll, not container scroll - page extends naturally
@media (min-width: 1024px) { @media (min-width: 1024px) {
.property-list-container.infinite-scroll-enabled { .property-list-container.infinite-scroll-enabled {
max-height: calc(100vh - 180px); // No overflow constraints - content grows the page naturally
overflow-y: auto; // Min-height maintained by JS to prevent layout shift when pages unloaded
scroll-behavior: smooth;
// Custom scrollbar
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--color-bg-dark);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
&:hover {
background: var(--color-text-muted);
}
}
} }
} }