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
|
currentMode: null, // Track current visualization mode
|
||||||
initialCenter: [45.0, -93.5], // Initial map center (Minnesota)
|
initialCenter: [45.0, -93.5], // Initial map center (Minnesota)
|
||||||
initialZoom: 7, // Initial zoom level
|
initialZoom: 7, // Initial zoom level
|
||||||
|
needsInitialFit: false, // Flag: true when map should fit to all properties on first load
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the map
|
* Initialize the map
|
||||||
*/
|
*/
|
||||||
init: function(filters) {
|
init: function(filters, skipInitialFit) {
|
||||||
var $mapContainer = $('#property-map');
|
var $mapContainer = $('#property-map');
|
||||||
if (!$mapContainer.length || typeof L === 'undefined') {
|
if (!$mapContainer.length || typeof L === 'undefined') {
|
||||||
return;
|
return;
|
||||||
@@ -295,7 +296,11 @@
|
|||||||
// Store initial filters
|
// Store initial filters
|
||||||
this.currentFilters = 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);
|
this.map = L.map('property-map').setView([45.0, -93.5], 7);
|
||||||
|
|
||||||
// Add OpenStreetMap tiles
|
// Add OpenStreetMap tiles
|
||||||
@@ -325,8 +330,75 @@
|
|||||||
// Bind card hover events
|
// Bind card hover events
|
||||||
this.bindCardHoverEvents();
|
this.bindCardHoverEvents();
|
||||||
|
|
||||||
|
// 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
|
// Load initial clusters
|
||||||
this.loadClusters();
|
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) {
|
clusters.forEach(function(cluster) {
|
||||||
var size = 'small';
|
var size = 'small';
|
||||||
if (cluster.count >= 100) {
|
var iconSize = 30;
|
||||||
|
if (cluster.count > 200) {
|
||||||
size = 'large';
|
size = 'large';
|
||||||
} else if (cluster.count >= 10) {
|
iconSize = 40;
|
||||||
|
} else if (cluster.count >= 100) {
|
||||||
size = 'medium';
|
size = 'medium';
|
||||||
|
iconSize = 35;
|
||||||
}
|
}
|
||||||
|
|
||||||
var icon = L.divIcon({
|
var icon = L.divIcon({
|
||||||
html: '<div><span>' + cluster.count + '</span></div>',
|
html: '<div><span>' + cluster.count + '</span></div>',
|
||||||
className: 'marker-cluster marker-cluster-' + size + ' server-cluster',
|
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 });
|
var marker = L.marker([cluster.lat, cluster.lng], { icon: icon });
|
||||||
@@ -651,15 +726,6 @@
|
|||||||
this.loadClusters();
|
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
|
* Format number with commas
|
||||||
*/
|
*/
|
||||||
@@ -1069,7 +1135,6 @@
|
|||||||
mapBounds: null, // Current map viewport bounds
|
mapBounds: null, // Current map viewport bounds
|
||||||
mapCenter: null, // Current map center for distance sorting
|
mapCenter: null, // Current map center for distance sorting
|
||||||
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
||||||
isResetTriggered: false, // Flag: true when reset button was clicked
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize
|
* Initialize
|
||||||
@@ -1133,10 +1198,39 @@
|
|||||||
self.onFilterChange();
|
self.onFilterChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset button
|
// Reset buttons - preserve scroll position across page reload
|
||||||
$('.filters-reset').on('click', function(e) {
|
$(document).on('click', 'a.filters-reset', function(e) {
|
||||||
e.preventDefault();
|
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)
|
// Pagination clicks (delegated)
|
||||||
@@ -1181,7 +1275,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store restoration state for map init to use
|
// 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;
|
this.pendingRestoreState = state;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1239,6 +1333,9 @@
|
|||||||
}, 400);
|
}, 400);
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 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
|
// If we have a page > 1, bulk load all pages
|
||||||
@@ -1531,10 +1628,6 @@
|
|||||||
var wasMapUpdate = this.isMapUpdate;
|
var wasMapUpdate = this.isMapUpdate;
|
||||||
this.isMapUpdate = false;
|
this.isMapUpdate = false;
|
||||||
|
|
||||||
// Capture and clear reset flag
|
|
||||||
var wasResetTriggered = this.isResetTriggered;
|
|
||||||
this.isResetTriggered = false;
|
|
||||||
|
|
||||||
// Queue the property list request with debounce and cancellation
|
// Queue the property list request with debounce and cancellation
|
||||||
RequestQueue.queue(
|
RequestQueue.queue(
|
||||||
'properties',
|
'properties',
|
||||||
@@ -1550,11 +1643,6 @@
|
|||||||
self.$results.html(response.data.html);
|
self.$results.html(response.data.html);
|
||||||
self.isFirstLoad = false;
|
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)
|
// Update map with new filter params (but not if this was triggered by map move)
|
||||||
if (response.data.filters && !wasMapUpdate) {
|
if (response.data.filters && !wasMapUpdate) {
|
||||||
PropertyMap.updateFilters(response.data.filters);
|
PropertyMap.updateFilters(response.data.filters);
|
||||||
@@ -1739,29 +1827,6 @@
|
|||||||
return queryMatch ? parseInt(queryMatch[1]) : 1;
|
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
|
* Handle filter change - fetch bounds and reposition map if needed
|
||||||
*/
|
*/
|
||||||
@@ -1795,6 +1860,7 @@
|
|||||||
action: 'homeproz_get_filter_bounds',
|
action: 'homeproz_get_filter_bounds',
|
||||||
property_type: formData.property_type,
|
property_type: formData.property_type,
|
||||||
city: formData.city,
|
city: formData.city,
|
||||||
|
zip: formData.zip,
|
||||||
min_price: formData.min_price,
|
min_price: formData.min_price,
|
||||||
max_price: formData.max_price,
|
max_price: formData.max_price,
|
||||||
min_beds: formData.beds
|
min_beds: formData.beds
|
||||||
@@ -1861,13 +1927,27 @@
|
|||||||
max_price: formData.max_price || '',
|
max_price: formData.max_price || '',
|
||||||
min_beds: formData.beds || ''
|
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;
|
this.mapInitialized = true;
|
||||||
|
|
||||||
// Restore state if we have pending state to restore
|
// Restore state if we have pending state to restore
|
||||||
if (PropertyFilters.pendingRestoreState) {
|
if (PropertyFilters.pendingRestoreState) {
|
||||||
PropertyFilters.restoreState();
|
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
|
// Filters present from URL but no saved map state - fit to filter bounds
|
||||||
PropertyFilters.onFilterChange();
|
PropertyFilters.onFilterChange();
|
||||||
}
|
}
|
||||||
@@ -1914,7 +1994,18 @@
|
|||||||
max_price: formData.max_price || '',
|
max_price: formData.max_price || '',
|
||||||
min_beds: formData.beds || ''
|
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;
|
this.mapInitialized = true;
|
||||||
|
|
||||||
// Restore state if we have pending state to restore
|
// Restore state if we have pending state to restore
|
||||||
|
|||||||
Reference in New Issue
Block a user