Skip initial property list render when map will reposition
On fresh page load without URL state or filters, the map repositions to fit all properties. Previously, the server-rendered property list would briefly show before being replaced by viewport-filtered results. Now we immediately show a spinner when we know the map will reposition, preventing the flash of unfiltered content and unnecessary rendering. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -282,11 +282,12 @@
|
||||
currentMode: null, // Track current visualization mode
|
||||
initialCenter: [45.0, -93.5], // Initial map center (Minnesota)
|
||||
initialZoom: 7, // Initial zoom level
|
||||
needsInitialFit: false, // Flag: true when map should fit to all properties on first load
|
||||
|
||||
/**
|
||||
* Initialize the map
|
||||
*/
|
||||
init: function(filters) {
|
||||
init: function(filters, skipInitialFit) {
|
||||
var $mapContainer = $('#property-map');
|
||||
if (!$mapContainer.length || typeof L === 'undefined') {
|
||||
return;
|
||||
@@ -295,7 +296,11 @@
|
||||
// Store initial filters
|
||||
this.currentFilters = filters || {};
|
||||
|
||||
// Initialize map centered on Minnesota
|
||||
// Check if we need to fit to all properties on first load
|
||||
// (fresh visit with no URL state and no filters)
|
||||
this.needsInitialFit = !skipInitialFit;
|
||||
|
||||
// Initialize map centered on Minnesota (temporary until we fit to bounds)
|
||||
this.map = L.map('property-map').setView([45.0, -93.5], 7);
|
||||
|
||||
// Add OpenStreetMap tiles
|
||||
@@ -325,8 +330,75 @@
|
||||
// Bind card hover events
|
||||
this.bindCardHoverEvents();
|
||||
|
||||
// Load initial clusters
|
||||
this.loadClusters();
|
||||
// Set up sticky boundary (stop sticky before footer, accounting for sticky filters)
|
||||
this.initStickyBoundary();
|
||||
|
||||
// If we need initial fit, fetch bounds first then load clusters
|
||||
if (this.needsInitialFit) {
|
||||
this.fitToAllProperties();
|
||||
} else {
|
||||
// Load initial clusters
|
||||
this.loadClusters();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fit map to show all properties with 15% margin
|
||||
* Called on fresh page load with no URL state
|
||||
*/
|
||||
fitToAllProperties: function() {
|
||||
var self = this;
|
||||
|
||||
$.ajax({
|
||||
url: homeprozAjax.ajaxUrl,
|
||||
type: 'GET',
|
||||
data: {
|
||||
action: 'homeproz_get_filter_bounds',
|
||||
// No filters - get bounds for all properties
|
||||
property_type: this.currentFilters.property_type || '',
|
||||
city: this.currentFilters.city || '',
|
||||
min_price: this.currentFilters.min_price || '',
|
||||
max_price: this.currentFilters.max_price || '',
|
||||
min_beds: this.currentFilters.min_beds || ''
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.data) {
|
||||
var bounds = response.data;
|
||||
|
||||
// Add 15% padding to bounds
|
||||
var latPadding = (bounds.ne_lat - bounds.sw_lat) * 0.15;
|
||||
var lngPadding = (bounds.ne_lng - bounds.sw_lng) * 0.15;
|
||||
|
||||
var paddedBounds = L.latLngBounds(
|
||||
[bounds.sw_lat - latPadding, bounds.sw_lng - lngPadding],
|
||||
[bounds.ne_lat + latPadding, bounds.ne_lng + lngPadding]
|
||||
);
|
||||
|
||||
// Fit map to bounds (this triggers moveend which calls loadClusters)
|
||||
self.map.fitBounds(paddedBounds);
|
||||
} else {
|
||||
// No properties found, use default view and load clusters
|
||||
self.loadClusters();
|
||||
}
|
||||
self.needsInitialFit = false;
|
||||
},
|
||||
error: function() {
|
||||
// On error, just load clusters with default view
|
||||
self.loadClusters();
|
||||
self.needsInitialFit = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sticky boundary - CSS sticky handles this automatically now
|
||||
* The .property-sidebar-content element is sticky within .property-sidebar
|
||||
* which acts as the containing block. The sticky element naturally stops
|
||||
* at the bottom of its container.
|
||||
*/
|
||||
initStickyBoundary: function() {
|
||||
// CSS position:sticky on .property-sidebar-content handles this automatically
|
||||
// The sticky element is constrained within its containing block (.property-sidebar)
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -537,16 +609,19 @@
|
||||
|
||||
clusters.forEach(function(cluster) {
|
||||
var size = 'small';
|
||||
if (cluster.count >= 100) {
|
||||
var iconSize = 30;
|
||||
if (cluster.count > 200) {
|
||||
size = 'large';
|
||||
} else if (cluster.count >= 10) {
|
||||
iconSize = 40;
|
||||
} else if (cluster.count >= 100) {
|
||||
size = 'medium';
|
||||
iconSize = 35;
|
||||
}
|
||||
|
||||
var icon = L.divIcon({
|
||||
html: '<div><span>' + cluster.count + '</span></div>',
|
||||
className: 'marker-cluster marker-cluster-' + size + ' server-cluster',
|
||||
iconSize: L.point(40, 40)
|
||||
iconSize: L.point(iconSize, iconSize)
|
||||
});
|
||||
|
||||
var marker = L.marker([cluster.lat, cluster.lng], { icon: icon });
|
||||
@@ -651,15 +726,6 @@
|
||||
this.loadClusters();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset map to initial position (Minnesota overview)
|
||||
*/
|
||||
resetToInitialPosition: function() {
|
||||
if (this.map) {
|
||||
this.map.setView(this.initialCenter, this.initialZoom);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format number with commas
|
||||
*/
|
||||
@@ -1069,7 +1135,6 @@
|
||||
mapBounds: null, // Current map viewport bounds
|
||||
mapCenter: null, // Current map center for distance sorting
|
||||
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
||||
isResetTriggered: false, // Flag: true when reset button was clicked
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
@@ -1133,10 +1198,39 @@
|
||||
self.onFilterChange();
|
||||
});
|
||||
|
||||
// Reset button
|
||||
$('.filters-reset').on('click', function(e) {
|
||||
// Reset buttons - preserve scroll position across page reload
|
||||
$(document).on('click', 'a.filters-reset', function(e) {
|
||||
e.preventDefault();
|
||||
self.resetFilters();
|
||||
|
||||
// Clear sessionStorage cache to ensure fresh state on reload
|
||||
try {
|
||||
var keysToRemove = [];
|
||||
for (var i = 0; i < sessionStorage.length; i++) {
|
||||
var key = sessionStorage.key(i);
|
||||
if (key && key.indexOf('HOMEPROZ_AJAX_') === 0) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(function(key) {
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// Get current scroll position
|
||||
var scrollY = $(window).scrollTop();
|
||||
|
||||
// Cap scroll at the position just below the filter bar (same as after map viewport change)
|
||||
var $filters = $('.property-filters').first();
|
||||
var $masthead = $('#masthead');
|
||||
if ($filters.length) {
|
||||
var mastheadHeight = $masthead.length ? $masthead.outerHeight() : 0;
|
||||
var maxScroll = $filters.offset().top + $filters.outerHeight() - mastheadHeight;
|
||||
scrollY = Math.min(scrollY, Math.max(0, maxScroll));
|
||||
}
|
||||
|
||||
// Navigate with scroll position in hash, then force reload
|
||||
window.location.href = '/properties/#scroll=' + Math.round(scrollY);
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// Pagination clicks (delegated)
|
||||
@@ -1181,7 +1275,7 @@
|
||||
});
|
||||
|
||||
// Store restoration state for map init to use
|
||||
if (state.lat !== null || state.page > 1) {
|
||||
if (state.lat !== null || state.page > 1 || state.scroll > 0) {
|
||||
this.pendingRestoreState = state;
|
||||
}
|
||||
},
|
||||
@@ -1239,6 +1333,9 @@
|
||||
}, 400);
|
||||
});
|
||||
}, 100);
|
||||
} else if (PropertyMap.map) {
|
||||
// No saved map position - reset map to fit all properties
|
||||
PropertyMap.fitToAllProperties();
|
||||
}
|
||||
|
||||
// If we have a page > 1, bulk load all pages
|
||||
@@ -1531,10 +1628,6 @@
|
||||
var wasMapUpdate = this.isMapUpdate;
|
||||
this.isMapUpdate = false;
|
||||
|
||||
// Capture and clear reset flag
|
||||
var wasResetTriggered = this.isResetTriggered;
|
||||
this.isResetTriggered = false;
|
||||
|
||||
// Queue the property list request with debounce and cancellation
|
||||
RequestQueue.queue(
|
||||
'properties',
|
||||
@@ -1550,11 +1643,6 @@
|
||||
self.$results.html(response.data.html);
|
||||
self.isFirstLoad = false;
|
||||
|
||||
// If reset was triggered and no results, reset map to initial position
|
||||
if (wasResetTriggered && response.data.found_posts === 0 && PropertyMap.map) {
|
||||
PropertyMap.resetToInitialPosition();
|
||||
}
|
||||
|
||||
// Update map with new filter params (but not if this was triggered by map move)
|
||||
if (response.data.filters && !wasMapUpdate) {
|
||||
PropertyMap.updateFilters(response.data.filters);
|
||||
@@ -1739,29 +1827,6 @@
|
||||
return queryMatch ? parseInt(queryMatch[1]) : 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset filters
|
||||
* Clears all form fields and triggers reload.
|
||||
* If no results after reset, map resets to initial position.
|
||||
*/
|
||||
resetFilters: function() {
|
||||
// Clear all selects
|
||||
this.$form.find('select').val('');
|
||||
// Clear zip input
|
||||
this.$form.find('input[name="zip"]').val('');
|
||||
|
||||
// Also sync to sticky form
|
||||
if (StickyFilters && StickyFilters.$stickyForm) {
|
||||
StickyFilters.$stickyForm.find('select').val('');
|
||||
StickyFilters.$stickyForm.find('input[name="zip"]').val('');
|
||||
}
|
||||
|
||||
// Set flag for reset-triggered request
|
||||
this.isResetTriggered = true;
|
||||
|
||||
this.onFilterChange();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle filter change - fetch bounds and reposition map if needed
|
||||
*/
|
||||
@@ -1795,6 +1860,7 @@
|
||||
action: 'homeproz_get_filter_bounds',
|
||||
property_type: formData.property_type,
|
||||
city: formData.city,
|
||||
zip: formData.zip,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
min_beds: formData.beds
|
||||
@@ -1861,13 +1927,27 @@
|
||||
max_price: formData.max_price || '',
|
||||
min_beds: formData.beds || ''
|
||||
};
|
||||
PropertyMap.init(mapFilters);
|
||||
|
||||
// Determine if we should skip initial fit:
|
||||
// - Skip if we have URL state to restore (saved map position)
|
||||
// - Skip if we have filters (onFilterChange will fit to filtered bounds)
|
||||
var hasUrlState = PropertyFilters.pendingRestoreState && PropertyFilters.pendingRestoreState.lat !== null;
|
||||
var hasFilters = formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds;
|
||||
var skipInitialFit = hasUrlState || hasFilters;
|
||||
|
||||
// If we're going to reposition the map (initial fit), show spinner immediately
|
||||
// This prevents showing server-rendered results that aren't viewport-filtered
|
||||
if (!skipInitialFit) {
|
||||
$('#property-results').html('<div class="property-results-loading"><div class="spinner"></div></div>');
|
||||
}
|
||||
|
||||
PropertyMap.init(mapFilters, skipInitialFit);
|
||||
this.mapInitialized = true;
|
||||
|
||||
// Restore state if we have pending state to restore
|
||||
if (PropertyFilters.pendingRestoreState) {
|
||||
PropertyFilters.restoreState();
|
||||
} else if (formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds) {
|
||||
} else if (hasFilters) {
|
||||
// Filters present from URL but no saved map state - fit to filter bounds
|
||||
PropertyFilters.onFilterChange();
|
||||
}
|
||||
@@ -1914,7 +1994,18 @@
|
||||
max_price: formData.max_price || '',
|
||||
min_beds: formData.beds || ''
|
||||
};
|
||||
PropertyMap.init(mapFilters);
|
||||
|
||||
// Determine if we should skip initial fit
|
||||
var hasUrlState = PropertyFilters.pendingRestoreState && PropertyFilters.pendingRestoreState.lat !== null;
|
||||
var hasFilters = formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds;
|
||||
var skipInitialFit = hasUrlState || hasFilters;
|
||||
|
||||
// If we're going to reposition the map (initial fit), show spinner immediately
|
||||
if (!skipInitialFit) {
|
||||
$('#property-results').html('<div class="property-results-loading"><div class="spinner"></div></div>');
|
||||
}
|
||||
|
||||
PropertyMap.init(mapFilters, skipInitialFit);
|
||||
this.mapInitialized = true;
|
||||
|
||||
// Restore state if we have pending state to restore
|
||||
|
||||
Reference in New Issue
Block a user