Add sticky filter form below map on desktop
- Create property-filters-sticky.php with compact filter layout - Add StickyFilters JS module with IntersectionObserver - Show sticky filter with 200ms fade when main filter scrolls out of view - Hide sticky filter instantly when main filter scrolls back into view - Bidirectional sync between main and sticky filter forms - Changes in either form trigger the same filterProperties() call - Desktop map view only (>= 1024px)
This commit is contained in:
@@ -77,6 +77,8 @@ $view_class = $show_map ? 'is-map-view' : 'is-grid-view';
|
||||
<div id="property-map" class="property-map">
|
||||
<!-- Leaflet map will be initialized here -->
|
||||
</div>
|
||||
<!-- Sticky Filters (below map, visible when main filters scroll out of view) -->
|
||||
<?php get_template_part('template-parts/property/property-filters-sticky'); ?>
|
||||
</div>
|
||||
<div class="property-list-container">
|
||||
<div id="property-results" class="property-results-map">
|
||||
|
||||
+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
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Property Filters - Sticky Version (below map)
|
||||
*
|
||||
* Compact filter form that appears when the main filter scrolls out of view.
|
||||
* Values are synced with the main filter via JavaScript.
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get current filter values from URL (same as main filter)
|
||||
$current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
|
||||
$current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : '';
|
||||
$current_zip = isset($_GET['zip']) ? sanitize_text_field($_GET['zip']) : '';
|
||||
$current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
|
||||
$current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
|
||||
$current_beds = isset($_GET['beds']) ? intval($_GET['beds']) : '';
|
||||
|
||||
// Get MLS property types and cities
|
||||
$property_types = homeproz_get_mls_property_types();
|
||||
$mls_cities = homeproz_get_mls_cities(50);
|
||||
?>
|
||||
|
||||
<div class="property-filters-sticky" id="property-filters-sticky">
|
||||
<form class="filters-form-sticky" method="get" action="<?php echo esc_url(get_post_type_archive_link('property')); ?>">
|
||||
<div class="filters-sticky-grid">
|
||||
<div class="filter-item-sticky">
|
||||
<select name="property_type" class="filter-select" aria-label="Property Type">
|
||||
<option value="">All Types</option>
|
||||
<?php foreach ($property_types as $type) : ?>
|
||||
<option value="<?php echo esc_attr($type->property_type); ?>" <?php selected($current_type, $type->property_type); ?>>
|
||||
<?php echo esc_html($type->property_type); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item-sticky">
|
||||
<select name="property_location" class="filter-select" aria-label="City">
|
||||
<option value="">All Cities</option>
|
||||
<?php foreach ($mls_cities as $city) : ?>
|
||||
<option value="<?php echo esc_attr($city); ?>" <?php selected($current_location, $city); ?>>
|
||||
<?php echo esc_html($city); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item-sticky">
|
||||
<select name="beds" class="filter-select" aria-label="Bedrooms">
|
||||
<option value="">Beds</option>
|
||||
<option value="1" <?php selected($current_beds, 1); ?>>1+</option>
|
||||
<option value="2" <?php selected($current_beds, 2); ?>>2+</option>
|
||||
<option value="3" <?php selected($current_beds, 3); ?>>3+</option>
|
||||
<option value="4" <?php selected($current_beds, 4); ?>>4+</option>
|
||||
<option value="5" <?php selected($current_beds, 5); ?>>5+</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item-sticky">
|
||||
<select name="min_price" class="filter-select" aria-label="Minimum Price">
|
||||
<option value="">Min $</option>
|
||||
<option value="50000" <?php selected($current_min_price, 50000); ?>>$50k</option>
|
||||
<option value="100000" <?php selected($current_min_price, 100000); ?>>$100k</option>
|
||||
<option value="150000" <?php selected($current_min_price, 150000); ?>>$150k</option>
|
||||
<option value="200000" <?php selected($current_min_price, 200000); ?>>$200k</option>
|
||||
<option value="250000" <?php selected($current_min_price, 250000); ?>>$250k</option>
|
||||
<option value="300000" <?php selected($current_min_price, 300000); ?>>$300k</option>
|
||||
<option value="400000" <?php selected($current_min_price, 400000); ?>>$400k</option>
|
||||
<option value="500000" <?php selected($current_min_price, 500000); ?>>$500k</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item-sticky">
|
||||
<select name="max_price" class="filter-select" aria-label="Maximum Price">
|
||||
<option value="">Max $</option>
|
||||
<option value="100000" <?php selected($current_max_price, 100000); ?>>$100k</option>
|
||||
<option value="150000" <?php selected($current_max_price, 150000); ?>>$150k</option>
|
||||
<option value="200000" <?php selected($current_max_price, 200000); ?>>$200k</option>
|
||||
<option value="250000" <?php selected($current_max_price, 250000); ?>>$250k</option>
|
||||
<option value="300000" <?php selected($current_max_price, 300000); ?>>$300k</option>
|
||||
<option value="400000" <?php selected($current_max_price, 400000); ?>>$400k</option>
|
||||
<option value="500000" <?php selected($current_max_price, 500000); ?>>$500k</option>
|
||||
<option value="750000" <?php selected($current_max_price, 750000); ?>>$750k</option>
|
||||
<option value="1000000" <?php selected($current_max_price, 1000000); ?>>$1M+</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item-sticky filter-item-sticky-zip">
|
||||
<input type="text" name="zip" class="filter-input" placeholder="Zip" value="<?php echo esc_attr($current_zip); ?>" maxlength="10" pattern="[0-9\-]*" aria-label="Zip Code">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -625,6 +625,157 @@
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sticky Filters Module
|
||||
* Shows a compact filter form below the map when the main filter scrolls out of view.
|
||||
* Desktop only (>= 1024px in map view).
|
||||
*/
|
||||
var StickyFilters = {
|
||||
$mainFilter: null,
|
||||
$stickyFilter: null,
|
||||
$mainForm: null,
|
||||
$stickyForm: null,
|
||||
observer: null,
|
||||
isVisible: false,
|
||||
|
||||
/**
|
||||
* Initialize sticky filters
|
||||
*/
|
||||
init: function() {
|
||||
// Only on desktop map view
|
||||
if (window.innerWidth < 1024 || !$('.is-map-view').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$mainFilter = $('#property-filters');
|
||||
this.$stickyFilter = $('#property-filters-sticky');
|
||||
this.$mainForm = this.$mainFilter.find('.filters-form');
|
||||
this.$stickyForm = this.$stickyFilter.find('.filters-form-sticky');
|
||||
|
||||
if (!this.$mainFilter.length || !this.$stickyFilter.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupObserver();
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup IntersectionObserver on main filter
|
||||
*/
|
||||
setupObserver: function() {
|
||||
var self = this;
|
||||
|
||||
this.observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.isIntersecting) {
|
||||
// Main filter is visible - hide sticky (instant)
|
||||
self.hideStickyFilter();
|
||||
} else {
|
||||
// Main filter scrolled out - show sticky (animated)
|
||||
self.showStickyFilter();
|
||||
}
|
||||
});
|
||||
}, {
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
this.observer.observe(this.$mainFilter[0]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind change events for syncing
|
||||
*/
|
||||
bindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
// Sync main form -> sticky form
|
||||
this.$mainForm.find('select, input').on('change input', function() {
|
||||
self.syncToSticky(this.name, $(this).val());
|
||||
});
|
||||
|
||||
// Sync sticky form -> main form and trigger filter
|
||||
this.$stickyForm.find('select').on('change', function() {
|
||||
self.syncToMain(this.name, $(this).val());
|
||||
PropertyFilters.filterProperties(1);
|
||||
});
|
||||
|
||||
// Sticky form input (zip) - sync and filter on enter or blur
|
||||
this.$stickyForm.find('input').on('change', function() {
|
||||
self.syncToMain(this.name, $(this).val());
|
||||
PropertyFilters.filterProperties(1);
|
||||
});
|
||||
|
||||
// Prevent form submission (we use AJAX)
|
||||
this.$stickyForm.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
PropertyFilters.filterProperties(1);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync a value from main form to sticky form
|
||||
*/
|
||||
syncToSticky: function(name, value) {
|
||||
var $field = this.$stickyForm.find('[name="' + name + '"]');
|
||||
if ($field.length && $field.val() !== value) {
|
||||
$field.val(value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync a value from sticky form to main form
|
||||
*/
|
||||
syncToMain: function(name, value) {
|
||||
var $field = this.$mainForm.find('[name="' + name + '"]');
|
||||
if ($field.length && $field.val() !== value) {
|
||||
$field.val(value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show sticky filter with fade-in animation (200ms)
|
||||
*/
|
||||
showStickyFilter: function() {
|
||||
if (this.isVisible) return;
|
||||
this.isVisible = true;
|
||||
|
||||
this.$stickyFilter
|
||||
.removeClass('is-hiding')
|
||||
.addClass('is-visible');
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide sticky filter instantly (no animation)
|
||||
*/
|
||||
hideStickyFilter: function() {
|
||||
if (!this.isVisible) return;
|
||||
this.isVisible = false;
|
||||
|
||||
this.$stickyFilter
|
||||
.removeClass('is-visible')
|
||||
.addClass('is-hiding');
|
||||
|
||||
// Remove hiding class after a tick
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.$stickyFilter.removeClass('is-hiding');
|
||||
}, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync all values from main to sticky (for initial state)
|
||||
*/
|
||||
syncAllToSticky: function() {
|
||||
var self = this;
|
||||
this.$mainForm.find('select, input').each(function() {
|
||||
self.syncToSticky(this.name, $(this).val());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var PropertyFilters = {
|
||||
// Elements
|
||||
$form: null,
|
||||
@@ -1837,6 +1988,7 @@
|
||||
ResponsiveView.init();
|
||||
LayoutCalculator.init();
|
||||
CardImageLoader.init();
|
||||
StickyFilters.init();
|
||||
|
||||
// Initialize infinite scroll (all views - desktop map and mobile grid)
|
||||
setTimeout(function() {
|
||||
|
||||
@@ -521,6 +521,79 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// Sticky Filters (below map on desktop)
|
||||
.property-filters-sticky {
|
||||
display: none; // Hidden by default, shown via JS when main filter scrolls out
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin-top: 1rem;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
&.is-visible {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.is-hiding {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: none; // Instant hide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-sticky-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item-sticky {
|
||||
.filter-select,
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
background-color: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--color-text);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding-right: 1.75rem;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23B0B0B0' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item-sticky-zip {
|
||||
.filter-input {
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Results loading spinner (only for first load)
|
||||
.property-results-loading {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user