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">
|
<div id="property-map" class="property-map">
|
||||||
<!-- Leaflet map will be initialized here -->
|
<!-- Leaflet map will be initialized here -->
|
||||||
</div>
|
</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>
|
||||||
<div class="property-list-container">
|
<div class="property-list-container">
|
||||||
<div id="property-results" class="property-results-map">
|
<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 = {
|
var PropertyFilters = {
|
||||||
// Elements
|
// Elements
|
||||||
$form: null,
|
$form: null,
|
||||||
@@ -1837,6 +1988,7 @@
|
|||||||
ResponsiveView.init();
|
ResponsiveView.init();
|
||||||
LayoutCalculator.init();
|
LayoutCalculator.init();
|
||||||
CardImageLoader.init();
|
CardImageLoader.init();
|
||||||
|
StickyFilters.init();
|
||||||
|
|
||||||
// Initialize infinite scroll (all views - desktop map and mobile grid)
|
// Initialize infinite scroll (all views - desktop map and mobile grid)
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
|||||||
@@ -521,6 +521,79 @@
|
|||||||
opacity: 0.7;
|
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)
|
// Results loading spinner (only for first load)
|
||||||
.property-results-loading {
|
.property-results-loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user