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,
pendingRequests: {}, // Object: pageNum -> jqXHR
isEnabled: false,
maxGridHeight: 0, // Track max height to prevent layout shift
config: {
maxPagesInDOM: 5,
bufferPages: 2,
@@ -1154,6 +1155,7 @@
/**
* Infinite Scroll Module
* Bidirectional infinite scroll for property list (desktop map view only)
* Uses window scroll - page extends naturally as content loads
*/
var InfiniteScroll = {
$container: null,
@@ -1164,7 +1166,6 @@
$bottomLoader: null,
topObserver: null,
bottomObserver: null,
scrollContainer: null,
/**
* Initialize infinite scroll
@@ -1208,6 +1209,9 @@
// Mark as enabled
InfiniteScrollState.isEnabled = true;
this.$container.addClass('infinite-scroll-enabled');
// Track initial height
this.updateMaxHeight();
},
/**
@@ -1217,6 +1221,7 @@
InfiniteScrollState.pages = {};
InfiniteScrollState.visibleRange = { first: 1, last: 1 };
InfiniteScrollState.pendingRequests = {};
// Note: maxGridHeight is NOT reset here - only on full reset/filter change
},
/**
@@ -1237,9 +1242,6 @@
// Insert elements
this.$grid.before(this.$topSentinel).before(this.$topLoader);
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
* Uses viewport (root: null) since we're using window scroll
*/
setupObservers: function() {
var self = this;
// Use viewport as root (null) with margin for pre-loading
var observerOptions = {
root: this.scrollContainer,
rootMargin: '400px 0px',
root: null, // Use viewport, not container
rootMargin: '600px 0px', // Load when within 600px of viewport
threshold: 0
};
@@ -1298,6 +1302,29 @@
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)
*/
@@ -1442,6 +1469,9 @@
// Restore scroll anchor
this.restoreScrollAnchor(anchor);
// Update max height tracking
this.updateMaxHeight();
// Trigger image lazy loading
if (typeof CardImageLoader !== 'undefined') {
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() {
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 anchor = null;
$cards.each(function() {
var rect = this.getBoundingClientRect();
// Find first card that is at least partially visible
if (rect.bottom > containerRect.top && rect.top < containerRect.bottom) {
var elementTop = rect.top + viewportTop;
var elementBottom = rect.bottom + viewportTop;
// Find first card that is at least partially visible in viewport
if (elementBottom > viewportTop && elementTop < viewportBottom) {
anchor = {
element: this,
offsetFromTop: rect.top - containerRect.top
offsetFromViewport: rect.top // Distance from top of viewport
};
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) {
if (!anchor || !anchor.element || !this.scrollContainer) return;
if (!anchor || !anchor.element) return;
var containerRect = this.scrollContainer.getBoundingClientRect();
var elementRect = anchor.element.getBoundingClientRect();
var currentOffset = elementRect.top - containerRect.top;
var diff = currentOffset - anchor.offsetFromTop;
var currentOffset = elementRect.top;
var diff = currentOffset - anchor.offsetFromViewport;
if (Math.abs(diff) > 1) {
this.scrollContainer.scrollTop += diff;
window.scrollBy(0, diff);
}
},
/**
* Reset infinite scroll (called on filter/map change)
* Clears max height since filters changed
*/
reset: function() {
// Cancel all pending infinite scroll requests
@@ -1609,13 +1645,14 @@
// Also cancel pending property and cluster requests via RequestQueue
RequestQueue.cancel();
// Clear max height since filters are changing
this.clearMaxHeight();
// Clear state
this.resetState();
// Scroll to top
if (this.scrollContainer) {
this.scrollContainer.scrollTop = 0;
}
// Scroll to top of page
window.scrollTo(0, 0);
},
/**
@@ -1653,6 +1690,9 @@
this.$grid.find('.infinite-scroll-placeholder').remove();
}
// Clear max height
this.clearMaxHeight();
// Reset state
this.resetState();
InfiniteScrollState.isEnabled = false;
@@ -572,30 +572,11 @@
}
// Infinite Scroll (desktop map view only)
// Uses window scroll, not container scroll - page extends naturally
@media (min-width: 1024px) {
.property-list-container.infinite-scroll-enabled {
max-height: calc(100vh - 180px);
overflow-y: auto;
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);
}
}
// No overflow constraints - content grows the page naturally
// Min-height maintained by JS to prevent layout shift when pages unloaded
}
}