<div class="property-listing" data-module="propertyListing" data-method="get" data-endpoint="/data/residential-property-data.json">
<link media="all" rel="stylesheet" href="/assets/themes/default/css/property-listing.css?cb=">
<link media="all" rel="stylesheet" href="/assets/themes/default/css/property-list-card.css?cb=">
<link media="all" rel="stylesheet" href="/assets/themes/default/css/gallery.css?cb=">
<div class="list-view js-list-view" data-page-size="5" data-paging-type="pagination">
<template class="js-property-card-template">
<a href="" class="property-list-card" data-module="propertyListCard" aria-label="View ">
<div class="property-list-card__container">
<div class="property-list-card__gallery">
<div class="property-list-card__gallery-image-tag js-image-tag"> </div>
<div class="property-list-card__gallery-container">
<div class="property-list-card__main-image" data-module="imageLazyLoad">
<div class="gallery ">
<div class="gallery__slides-container">
<div class="gallery__slides js-slides">
</div>
<span class="gallery__arrow gallery__arrow--left js-arrow-left"><span>Previous</span> </span>
<div class="gallery__selection-label">
<span class="gallery__current-slide js-current-slide">1</span>
<span class="gallery__total-slides js-total-slides"></span>
</div>
<span class="gallery__arrow gallery__arrow--right js-arrow-right"><span>Next</span> </span>
</div>
<div class="gallery__dots js-dots">
</div>
</div>
</div>
<div class="property-list-card__stacked-image js-image-1">
<div class="image ">
<picture>
<img src="" loading="lazy" alt="">
</picture>
</div>
</div>
<div class="property-list-card__stacked-image js-image-2">
<div class="image ">
<picture>
<img src="" loading="lazy" alt="">
</picture>
</div>
</div>
</div>
</div>
<div class="property-list-card__content-container">
<div class="property-list-card__headings-container">
<h2 class="property-list-card__title js-title"></h2>
</div>
<div class="property-list-card__keys-cta-container">
<div class="property-list-card__keys-container">
<div class="property-list-card__property-key-type js-type"> </div>
<div class="property-list-card__property-key-size js-size"> </div>
</div>
<div class="property-list-card__cta js-cta">
</div>
</div>
</div>
</div>
</a>
</template>
<div class="list-view__drag-handle js-drag-handle"></div>
<div class="list-view__inner js-scrollable-container">
<header class="list-view__header js-header">
<a href="#" class="list-view__filter-trigger js-filter-trigger"><span>Filters</span></a>
<div class="list-view__results-bar">
<span class="list-view__results-count-text js-property-count"></span>
<label for="sortBy">Sort By:</label>
<select name="sortBy" id="sortBy">
<option value="development">Building</option>
<option value="size DESC">Size: High to low</option>
<option value="size ASC">Size: Low to high</option>
<option value="price DESC">Price: High to low</option>
<option value="price ASC">Price: Low to high</option>
<option value="dateAdded DESC">Date Added</option>
</select>
</div>
</header>
<div class="list-view__results js-results-container">
</div>
<link rel="stylesheet" href="/assets/themes/default/css/pagination.css?cb=" />
<div class="pagination js-pagination">
<a href="#top" class="pagination__previous js-previous"><span>Previous</span></a>
<a href="#top" class="pagination__page js-page js-first selected" data-page="1">1</a>
<span class="pagination__dots js-dots-left">…</span>
<a href="#top" class="pagination__page js-page js-second-page" data-page="2">2</a>
<a href="#top" class="pagination__page js-page js-third-page" data-page="3">3</a>
<a href="#top" class="pagination__page js-page js-fourth-page" data-page="4">4</a>
<span class="pagination__dots js-dots-right">…</span>
<a href="#top" class="pagination__page js-page js-last-page" data-page="5">5</a>
<a href="#top" class="pagination__next js-next"><span>Next</span></a>
</div>
<div class="list-view__no-results js-no-results-container">
<div class="no-results">
<h2 class="no-results__title">No properties found</h2>
<div class="no-results__copy">Try changing or removing your filters</div>
<div class="no-results__buttons-container js-remove-filter-buttons"></div>
<div class="no-results__reset-all-button js-reset-all-filters">Reset all filters</div>
</div>
</div>
</div>
</div>
<div class="map-view js-map-view">
<a href="#" class="map-view__filter-trigger js-filter-trigger"><span>Filters</span></a>
<a href="#" class="js-view-toggle property-listing__toggle-button"><span class="map-view__toggle-list">List View</span><span class="map-view__toggle-map">Map View</span></a>
<link media="all" href='https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.css' rel='stylesheet' />
<link media="all" rel="stylesheet" href="/assets/themes/default/css/mapbox.css?cb=">
<div class="mapbox js-mapbox" data-longitude="-81.378112" data-latitude="19.322003" data-map-key="pk.eyJ1IjoiYmVubGVmdHdpY2hkYXJ0IiwiYSI6ImNrNmI3dmNnYjB5bWMzbGxqcWJvdHphcWUifQ.bQwqoScE2wXdFGsK3BpkRA" data-map-style="mapbox://styles/benleftwichdart/ck6b7xnox14vs1irwey7g5rdn" data-zoom="7" data-pitch="0">
<div class="mapbox__map js-map"></div>
</div>
<div class="map-view__property-list-card-container js-property-list-card-container">
<div class="map-view__property-list-card--close js-close-card"></div>
<link media="all" rel="stylesheet" href="/assets/themes/default/css/property-list-card.css?cb=">
<a href="" class="property-list-card" data-module="propertyListCard" aria-label="View ">
<div class="property-list-card__container">
<div class="property-list-card__gallery">
<div class="property-list-card__gallery-image-tag js-image-tag"> </div>
<div class="property-list-card__gallery-container">
<div class="property-list-card__main-image" data-module="imageLazyLoad">
<div class="gallery ">
<div class="gallery__slides-container">
<div class="gallery__slides js-slides">
</div>
<span class="gallery__arrow gallery__arrow--left js-arrow-left"><span>Previous</span> </span>
<div class="gallery__selection-label">
<span class="gallery__current-slide js-current-slide">1</span>
<span class="gallery__total-slides js-total-slides"></span>
</div>
<span class="gallery__arrow gallery__arrow--right js-arrow-right"><span>Next</span> </span>
</div>
<div class="gallery__dots js-dots">
</div>
</div>
</div>
<div class="property-list-card__stacked-image js-image-1">
<div class="image ">
<picture>
<img src="" loading="lazy" alt="">
</picture>
</div>
</div>
<div class="property-list-card__stacked-image js-image-2">
<div class="image ">
<picture>
<img src="" loading="lazy" alt="">
</picture>
</div>
</div>
</div>
</div>
<div class="property-list-card__content-container">
<div class="property-list-card__headings-container">
<h2 class="property-list-card__title js-title"></h2>
</div>
<div class="property-list-card__keys-cta-container">
<div class="property-list-card__keys-container">
<div class="property-list-card__property-key-type js-type"> </div>
<div class="property-list-card__property-key-size js-size"> </div>
</div>
<div class="property-list-card__cta js-cta">
</div>
</div>
</div>
</div>
</a>
</div>
<div class="map-view__multi-property-card-container js-multi-property-card-container">
<div class="map-view__multi-property-card--close js-close-card"></div>
<link media="all" rel="stylesheet" href="/assets/themes/default/css/multi-property-card.css?cb=">
<div class="multi-property-card" data-module="multiPropertyCard">
<div class="multi-property-card__outer-container">
<div class="multi-property-card__arrow multi-property-card__arrow--left disabled js-arrow-left"></div>
<div class="multi-property-card__container">
<div class="multi-property-card__container-left">
<div class="multi-property-card__location js-multi-location"></div>
<div class="multi-property-card__development js-multi-development"></div>
<div class="multi-property-card__unit-text js-multi-length"> units in this development</div>
</div>
<div class="multi-property-card__container-results js-results-container default">
</div>
</div>
<div class="multi-property-card__arrow multi-property-card__arrow--right js-arrow-right"></div>
</div>
</div>
</div>
</div>
<dialog class="js-filter-dialog property-listing-filter">
<form class="property-listing-filter__form">
<div class="property-listing-filter__header">
Filters <a href="#" class="js-close property-listing-filter__close-button"><span>Close</span></a>
</div>
<div class="property-listing-filter__form-inner">
<fieldset class="property-listing-filter__property-types">
<legend class="property-listing-filter__filter-title">Property Type</legend>
<div class="js-property-types"></div>
</fieldset>
<fieldset class="property-listing-filter__size">
<legend class="property-listing-filter__filter-title">Square Footage</legend>
<div class="js-size" data-type="squareFootageValue">
<div class="slider__range-container slider__range-container--with-bars js-slider-container">
<div class="slider__range-tracker"></div>
<div class="slider__range-tracker-between js-tracker-between"></div>
<label for="range-lower-size"></label><input data-key="size" class="slider__range-lower js-range-lower js-min-size" type="range" min="0" max="20000" value="0" name="min-size" id="range-lower-size">
<label for="range-upper-size"></label><input class="slider__range-upper js-range-upper js-max-size" type="range" min="0" max="20000" value="20000" name="max-size" id="range-upper-size">
</div>
<div class="slider__min-max-container">
<div class="slider__min-text js-min">0</div>
<div class="slider__max-text js-max"></div>
</div>
</div>
</fieldset>
<fieldset class="property-listing-filter__locations">
<legend class="property-listing-filter__filter-title">Location</legend>
<div class="property-listing-filter__checkbox-container">
<label class="property-listing-filter__checkbox-label">
<input data-key="location" type="checkbox" value="West Bay" name="location"><span class="property-listing-filter__checkbox-text">West Bay</span>
</label>
</div>
<div class="property-listing-filter__checkbox-container">
<label class="property-listing-filter__checkbox-label">
<input type="checkbox" value="Camana Bay" name="location"><span class="property-listing-filter__checkbox-text">Camana Bay</span>
</label>
</div>
<div class="property-listing-filter__checkbox-container">
<label class="property-listing-filter__checkbox-label">
<input type="checkbox" value="George Town" name="location"><span class="property-listing-filter__checkbox-text">George Town</span>
</label>
</div>
</fieldset>
<fieldset class="property-listing-filter__building-type">
<legend class="property-listing-filter__filter-title">Buildings</legend>
<div class="js-building-type"></div>
</fieldset>
<fieldset class="property-listing-filter__price">
<legend class="property-listing-filter__filter-title">Price</legend>
<div class="js-price" data-type="priceValue">
<div class="slider__range-container slider__range-container--with-bars js-slider-container">
<div class="slider__range-tracker"></div>
<div class="slider__range-tracker-between js-tracker-between"></div>
<label for="range-lower-price"></label><input data-key="price" class="slider__range-lower js-range-lower js-min-price" type="range" min="0" max="5000000" value="0" name="min-price" id="range-lower-price">
<label for="range-upper-price"></label><input class="slider__range-upper js-range-upper js-max-price" type="range" min="0" max="5000000" value="5000000" name="max-price" id="range-upper-price">
</div>
<div class="slider__min-max-container">
<div class="slider__min-text"><span>$</span><span class="js-min">0</span></div>
<div class="slider__max-text"><span>$</span><span class="js-max"></span></div>
</div>
</div>
</fieldset>
<fieldset class="property-listing-filter__bedrooms">
<legend class="property-listing-filter__filter-title">Bedrooms</legend>
<div class="js-bedrooms">
<div class="slider__range-container js-slider-container">
<div class="slider__range-tracker"></div>
<div class="slider__range-tracker-between js-tracker-between"></div>
<label for="range-lower-bedrooms"></label><input data-key="bedrooms" class="slider__range-lower js-range-lower js-min-bedrooms" type="range" min="0" max="100000000" value="0" name="min-bedrooms" id="range-lower-bedrooms">
<label for="range-upper-bedrooms"></label><input class="slider__range-upper js-range-upper js-max-bedrooms" type="range" min="0" max="100000000" value="100000000" name="max-bedrooms" id="range-upper-bedrooms">
</div>
<div class="slider__min-max-container">
<div class="slider__min-text js-min">0</div>
<div class="slider__max-text js-max"></div>
</div>
</div>
</fieldset>
<fieldset class="property-listing-filter__bathrooms">
<legend class="property-listing-filter__filter-title">Bathrooms</legend>
<div class="js-bathrooms">
<div class="slider__range-container js-slider-container">
<div class="slider__range-tracker"></div>
<div class="slider__range-tracker-between js-tracker-between"></div>
<label for="range-lower-bathrooms"></label><input data-key="bathrooms" class="slider__range-lower js-range-lower js-min-bathrooms" type="range" min="0" max="100000000" value="0" name="min-bathrooms" id="range-lower-bathrooms">
<label for="range-upper-bathrooms"></label><input class="slider__range-upper js-range-upper js-max-bathrooms" type="range" min="0" max="100000000" value="100000000" name="max-bathrooms" id="range-upper-bathrooms">
</div>
<div class="slider__min-max-container">
<div class="slider__min-text js-min">0</div>
<div class="slider__max-text js-max"></div>
</div>
</div>
</fieldset>
<fieldset class="property-listing-filter__amenities">
<legend class="property-listing-filter__filter-title">Amenities</legend>
<div class="js-amenities"></div>
<div class="js-amenities-toggle">
<span class="property-listing-filter__amenities-show">Show more</span>
<span class="property-listing-filter__amenities-hide">Show less</span>
</div>
</fieldset>
</div>
<div class="property-listing-filter__footer">
<input type="reset" />
<input type="submit" value="Show Properties" />
</div>
</form>
</dialog>
</div>
<div class="property-listing" data-module="propertyListing" data-method="{{method}}" data-endpoint="{{endpoint}}">
{{#if firstInstance}}
<link media="all" rel="stylesheet"
href="/assets/themes/{{theme}}/css/property-listing.css?cb={{cacheBuster}}">
<link media="all" rel="stylesheet"
href="/assets/themes/{{theme}}/css/property-list-card.css?cb={{cacheBuster}}">
<link media="all" rel="stylesheet"
href="/assets/themes/{{theme}}/css/gallery.css?cb={{cacheBuster}}">
{{/if}}
<div class="list-view js-list-view" data-page-size="{{pageSize}}" data-paging-type="{{pagingType}}">
<template class="js-property-card-template">
{{> @property-list-card firstInstance=false imageTag=" " location=" " squareFootage=" " propertyType=" " }}
</template>
<div class="list-view__drag-handle js-drag-handle"></div>
<div class="list-view__inner js-scrollable-container">
<header class="list-view__header js-header">
<a href="#" class="list-view__filter-trigger js-filter-trigger"><span>Filters</span></a>
<div class="list-view__results-bar">
<span class="list-view__results-count-text js-property-count"></span>
<label for="sortBy">Sort By:</label>
<select name="sortBy" id="sortBy">
<option value="development">Building</option>
<option value="size DESC">Size: High to low</option>
<option value="size ASC">Size: Low to high</option>
<option value="price DESC">Price: High to low</option>
<option value="price ASC">Price: Low to high</option>
<option value="dateAdded DESC">Date Added</option>
</select>
</div>
</header>
<div class="list-view__results js-results-container">
</div>
{{#ifEquals pagingType "pagination" }}
{{> @pagination }}
{{/ifEquals}}
{{#ifEquals pagingType "loadmore" }}
<div class="list-view__load-more-container">
<a href="#" class="list-view__load-more-button js-load-more-button">Load More</a>
</div>
{{/ifEquals}}
<div class="list-view__no-results js-no-results-container">
{{> @no-results }}
</div>
</div>
</div>
<div class="map-view js-map-view">
<a href="#" class="map-view__filter-trigger js-filter-trigger"><span>Filters</span></a>
<a href="#" class="js-view-toggle property-listing__toggle-button"><span class="map-view__toggle-list">List View</span><span class="map-view__toggle-map">Map View</span></a>
{{> @mapbox latitude=map.latitude longitude=map.longitude key=map.key style=map.style doNotInitialise=true zoomLevel=map.zoomLevel }}
<div class="map-view__property-list-card-container js-property-list-card-container">
<div class="map-view__property-list-card--close js-close-card"></div>
{{> @property-list-card imageTag=" " location=" " squareFootage=" " propertyType=" " }}
</div>
<div class="map-view__multi-property-card-container js-multi-property-card-container">
<div class="map-view__multi-property-card--close js-close-card"></div>
{{> @multi-property-card }}
</div>
</div>
{{> @property-listing-filter }}
</div>
{
"theme": "default",
"firstInstance": true,
"method": "get",
"endpoint": "/data/residential-property-data.json",
"loadMoreText": "Load more",
"pageSize": 5,
"map": {
"firstInstance": true,
"theme": "default",
"key": "pk.eyJ1IjoiYmVubGVmdHdpY2hkYXJ0IiwiYSI6ImNrNmI3dmNnYjB5bWMzbGxqcWJvdHphcWUifQ.bQwqoScE2wXdFGsK3BpkRA",
"longitude": "-81.378112",
"latitude": "19.322003",
"style": "mapbox://styles/benleftwichdart/ck6b7xnox14vs1irwey7g5rdn",
"pitch": 30,
"zoomLevel": 7
},
"pagingType": "pagination"
}
@import './_slider';
.property-listing-filter {
$this: &;
height: calc(100vh - #{rem(80)});
max-width: 100%;
padding: 0;
position: relative;
width: rem(844);
&__header {
padding: rem(20);
text-align: left;
}
&__close-button {
background-image: url('../images/property-filter-close.svg');
height: rem(24);
position: absolute;
right: rem(20);
top: rem(20);
width: rem(24);
span {
@extend %visually-hidden;
}
}
&__form {
height: 100%;
}
&__form-inner {
display: flex;
flex-direction: column;
padding: rem(20);
}
&__footer {
background-color: $color-black;
margin-top: auto;
padding: rem(16);
width: 100%;
}
fieldset {
display: contents;
}
&__filter-title {
margin: rem(40) 0 rem(20);
}
&__checkbox-text {
background-color: $color-steel-grey;
border: rem(1) solid $color-white;
border-radius: rem(8);
display: block;
padding: rem(20);
text-align: center;
width: 100%;
}
&__checkbox-label {
cursor: pointer;
display: block;
height: 100%;
width: fit-content;
&.disabled {
#{$this}__checkbox-text {
cursor: not-allowed;
opacity: 0.3;
}
}
}
&__checkbox-container {
display: inline-block;
input[type="checkbox"] {
color: $color-white;
display: none;
position: absolute;
&:checked + {
#{$this}__checkbox-text {
background-color: $color-black;
color: $color-white;
text-shadow: 0 0 6px rgba(0, 0, 0, 0.8);
}
}
}
}
&__amenities {
#{$this}__amenities-show {
cursor: pointer;
display: block;
}
#{$this}__amenities-hide {
display: none;
}
&.show {
#{$this}__amenities-show {
display: none;
}
#{$this}__amenities-hide {
cursor: pointer;
display: block;
}
#{$this}__amenity-container {
&:nth-of-type(1n) {
display: inline-block;
}
}
}
#{$this}__amenity-container {
&:nth-of-type(1n + 4) {
display: none;
}
}
}
&__amenity-container {
display: inline-block;
margin-bottom: rem(20);
min-width: 30%;
}
&__amenity-text {
display: inline-block;
margin-left: rem(5);
margin-right: rem(35);
text-transform: capitalize;
vertical-align: super;
}
}
@keyframes highlight-card {
from {
background-color: $color-white;
}
50% {
background-color: $color-black;
}
to {
background-color: $color-white;
}
}
.list-view {
flex: 1 1 65%;
height: 65%;
max-height: calc(100% - #{rem(77)}); // We don't want the drag handle covering the filters bar
min-height: rem(21); // This needs to be the height of the drag handle
position: relative;
@include breakpoint(medium) {
flex-basis: 75%;
height: 100%;
transition: flex-basis 1s ease-in-out;
}
&__inner {
height: 100%;
overflow-y: scroll;
position: relative;
scrollbar-color: transparent transparent; // Hide the scrollbar in FF
scrollbar-width: thin;
// Hide the scrollbar in Safari, Chrome, Opera and (the new Chromium) Edge
&::-webkit-scrollbar {
display: none;
}
}
&__drag-handle {
background-color: $color-white;
border-radius: rem(34) rem(34) 0 0;
cursor: row-resize;
height: rem(21);
position: absolute;
transform: translateY(-100%);
width: 100%;
z-index: 10;
@include breakpoint(medium) {
display: none;
}
&::after {
background-color: $color-grey;
border-radius: rem(6);
bottom: 0;
content: '';
height: rem(6);
left: calc(50% - rem(28));
position: absolute;
width: rem(56);
}
}
.show-full-map-view & {
flex-basis: 0;
width: 0;
}
&__header {
position: sticky;
top: 0;
z-index: 1;
}
&__filter-trigger {
@extend %property-listing-filter-trigger;
display: none;
@include breakpoint(medium) {
display: block;
}
}
&__results {
&.loading {
@extend %lazy-loader;
}
}
&__results-bar {
background-color: $color-white;
display: flex;
justify-content: space-between;
padding: rem(20) rem(30);
@include breakpoint(medium) {
margin-top: 0;
}
label {
@extend %visually-hidden;
}
select {
width: auto;
}
}
&__building-heading {
@extend %p--lead;
background-color: $color-grey;
margin-bottom: rem(30);
padding: rem(9) rem(30);
}
&__results-count-text {
@extend %p--lead;
}
&__no-results {
display: none;
}
&__property-card-container {
&.highlight {
animation: highlight-card 1s ease-in-out 1 1s forwards;
}
}
&__load-more-container {
text-align: center;
}
&__load-more-button {
@extend %primary-cta;
display: none; // Hide by default.
}
}
@import 'source/scss/01-settings/_import';
@import 'source/scss/02-tools/_import';
@keyframes animHideOnLoad {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.map-view {
background-color: #9CEFF7; // set it to the color of the sea in mapbox to avoid any white displaying when toggling map and split view
flex: 1 1 35%;
height: 35%;
min-height: rem(55); // The height of the filter block
overflow: hidden;
width: 100%;
@include breakpoint(medium) {
flex-basis: 25%;
height: 100% !important;
position: relative;
width: 25%;
}
&__toggle-list {
display: none;
}
&__toggle-map {
display: block;
}
.show-full-map-view & {
flex-basis: 100%;
width: 100%;
.map-view__toggle-list {
display: block;
}
.map-view__toggle-map {
display: none;
}
}
&__filter-trigger {
@extend %property-listing-filter-trigger;
@include breakpoint(medium) {
left: 50%;
min-width: rem(260);
position: absolute;
top: 0;
transform: translate3d(-50%, -100%, 0);
transition: transform 1s ease-in-out;
z-index: 10;
.show-full-map-view & {
transform: translate3d(-50%, 0, 0);
}
}
}
&__property-cluster, &__property-marker, &__property-multi-marker {
animation: animHideOnLoad 0.1s ease;
background-size: cover;
cursor: pointer;
height: rem(64);
width: rem(64);
&.mapboxgl-marker {
opacity: 1;
}
}
&__property-cluster {
background-color: $color-steel-grey;
color: $color-white;
display: flex;
flex-direction: column;
justify-content: center;
&::after {
border: rem(5) solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
content: '';
height: rem(64);
margin-left: - rem(5);
position: absolute;
width: rem(64);
}
}
&__property-marker {
background-image: url('data:image/svg+xml,<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19 50C24.2156 50 38 31.147 38 18.75C38 8.39466 29.4934 0 19 0C8.50659 0 0 8.39466 0 18.75C0 31.147 13.7844 50 19 50ZM19 23C21.2091 23 23 21.2091 23 19C23 16.7909 21.2091 15 19 15C16.7909 15 15 16.7909 15 19C15 21.2091 16.7909 23 19 23Z" fill="%2300ACD8"/></svg>');
background-repeat: no-repeat;
background-size: rem(38) rem(50);
&.clicked,
&:hover {
background-image: url('data:image/svg+xml,<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19 50C24.2156 50 38 31.147 38 18.75C38 8.39466 29.4934 0 19 0C8.50659 0 0 8.39466 0 18.75C0 31.147 13.7844 50 19 50ZM19 23C21.2091 23 23 21.2091 23 19C23 16.7909 21.2091 15 19 15C16.7909 15 15 16.7909 15 19C15 21.2091 16.7909 23 19 23Z" fill="%23000000"/></svg>');
}
}
&__property-multi-marker {
background-image: url('data:image/svg+xml,<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M35.5944 50.5264C33.462 52.6698 31.5114 53.9743 30.1536 53.9743C28.7483 53.9743 26.708 52.577 24.4883 50.2984C24.1863 49.9884 23.881 49.6621 23.5736 49.3208C20.5088 45.9186 17.2282 41.0248 14.8682 35.9262C14.3606 34.8297 13.8956 33.7236 13.4846 32.6209C13.4802 32.6092 13.4758 32.5975 13.4715 32.5858C12.2789 29.378 11.5434 26.1994 11.5434 23.3654C11.5434 22.9026 11.5607 22.4439 11.5948 21.9897C12.3077 12.4893 20.3444 5 30.1536 5C39.9773 5 48.0232 12.5114 48.7155 22.0317C48.7164 22.0441 48.7173 22.0566 48.7182 22.069C48.7485 22.4972 48.7638 22.9295 48.7638 23.3654C48.7638 26.3193 47.9648 29.6474 46.682 32.9929C46.389 33.7571 46.0707 34.5222 45.7309 35.2839C45.6474 35.4712 45.5625 35.6583 45.4765 35.8452C42.8195 41.6118 38.9781 47.1253 35.5944 50.5264ZM34.0715 23.6102C34.0715 25.774 32.3174 27.5282 30.1535 27.5282C27.9897 27.5282 26.2356 25.774 26.2356 23.6102C26.2356 21.4464 27.9897 19.6923 30.1535 19.6923C32.3174 19.6923 34.0715 21.4464 34.0715 23.6102ZM38.2214 54.5666C37.6152 54.2991 37.0038 53.547 36.4312 52.449C36.4805 52.4019 36.5299 52.3544 36.5793 52.3066C38.6922 50.261 40.9483 47.4524 43.0137 44.2959C44.4147 42.1548 45.7624 39.8004 46.9282 37.3626C47.0649 37.4556 47.212 37.5376 47.3687 37.6068C48.7623 38.2219 50.3908 37.5908 51.0059 36.1971C51.621 34.8035 50.9899 33.175 49.5963 32.5599C49.399 32.4728 49.1969 32.4107 48.9938 32.3722C50.0647 29.2814 50.7228 26.1965 50.7228 23.3654C50.7228 23.0701 50.7164 22.7764 50.7039 22.4843C51.1879 22.6346 51.6685 22.8141 52.1436 23.0238C58.7634 25.9456 61.7924 33.6101 58.909 40.1428C55.4571 47.9635 41.5117 56.0189 38.2214 54.5666ZM23.6701 52.2504C21.5738 50.2097 19.3401 47.4236 17.2935 44.2959C15.929 42.2105 14.6151 39.9228 13.4705 37.5528C12.762 37.9049 11.9081 37.9512 11.1278 37.6068C9.73419 36.9916 9.10307 35.3632 9.71819 33.9696C10.0418 33.2364 10.6459 32.7142 11.3487 32.4736C10.257 29.3489 9.58442 26.2275 9.58442 23.3654C9.58442 23.0389 9.59219 22.7144 9.60756 22.3918C9.01822 22.5591 8.4332 22.7692 7.85641 23.0238C1.23658 25.9456 -1.79241 33.6101 1.09098 40.1428C4.54285 47.9635 18.4883 56.0189 21.7786 54.5666C22.4206 54.2832 23.0686 53.4562 23.6701 52.2504Z" fill="%2300ACD8"/></svg>');
&.clicked,
&:hover {
background-image: url('data:image/svg+xml,<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M35.5944 50.5264C33.462 52.6698 31.5114 53.9743 30.1536 53.9743C28.7483 53.9743 26.708 52.577 24.4883 50.2984C24.1863 49.9884 23.881 49.6621 23.5736 49.3208C20.5088 45.9186 17.2282 41.0248 14.8682 35.9262C14.3606 34.8297 13.8956 33.7236 13.4846 32.6209C13.4802 32.6092 13.4758 32.5975 13.4715 32.5858C12.2789 29.378 11.5434 26.1994 11.5434 23.3654C11.5434 22.9026 11.5607 22.4439 11.5948 21.9897C12.3077 12.4893 20.3444 5 30.1536 5C39.9773 5 48.0232 12.5114 48.7155 22.0317C48.7164 22.0441 48.7173 22.0566 48.7182 22.069C48.7485 22.4972 48.7638 22.9295 48.7638 23.3654C48.7638 26.3193 47.9648 29.6474 46.682 32.9929C46.389 33.7571 46.0707 34.5222 45.7309 35.2839C45.6474 35.4712 45.5625 35.6583 45.4765 35.8452C42.8195 41.6118 38.9781 47.1253 35.5944 50.5264ZM34.0715 23.6102C34.0715 25.774 32.3174 27.5282 30.1535 27.5282C27.9897 27.5282 26.2356 25.774 26.2356 23.6102C26.2356 21.4464 27.9897 19.6923 30.1535 19.6923C32.3174 19.6923 34.0715 21.4464 34.0715 23.6102ZM38.2214 54.5666C37.6152 54.2991 37.0038 53.547 36.4312 52.449C36.4805 52.4019 36.5299 52.3544 36.5793 52.3066C38.6922 50.261 40.9483 47.4524 43.0137 44.2959C44.4147 42.1548 45.7624 39.8004 46.9282 37.3626C47.0649 37.4556 47.212 37.5376 47.3687 37.6068C48.7623 38.2219 50.3908 37.5908 51.0059 36.1971C51.621 34.8035 50.9899 33.175 49.5963 32.5599C49.399 32.4728 49.1969 32.4107 48.9938 32.3722C50.0647 29.2814 50.7228 26.1965 50.7228 23.3654C50.7228 23.0701 50.7164 22.7764 50.7039 22.4843C51.1879 22.6346 51.6685 22.8141 52.1436 23.0238C58.7634 25.9456 61.7924 33.6101 58.909 40.1428C55.4571 47.9635 41.5117 56.0189 38.2214 54.5666ZM23.6701 52.2504C21.5738 50.2097 19.3401 47.4236 17.2935 44.2959C15.929 42.2105 14.6151 39.9228 13.4705 37.5528C12.762 37.9049 11.9081 37.9512 11.1278 37.6068C9.73419 36.9916 9.10307 35.3632 9.71819 33.9696C10.0418 33.2364 10.6459 32.7142 11.3487 32.4736C10.257 29.3489 9.58442 26.2275 9.58442 23.3654C9.58442 23.0389 9.59219 22.7144 9.60756 22.3918C9.01822 22.5591 8.4332 22.7692 7.85641 23.0238C1.23658 25.9456 -1.79241 33.6101 1.09098 40.1428C4.54285 47.9635 18.4883 56.0189 21.7786 54.5666C22.4206 54.2832 23.0686 53.4562 23.6701 52.2504Z" fill="%23000000"/></svg>');
}
}
&__property-list-card-container,
&__multi-property-card-container {
background: $color-white;
bottom: rem(30);
left: 0;
margin: auto auto 0;
opacity: 0;
padding: rem(30);
position: absolute;
right: 0;
transform: translateY(calc(100% + 30px));
width: 80%;
&.show {
opacity: 1;
transform: translateY(0);
transition: opacity, transform linear 0.5s;
}
}
&__property-list-card--close,
&__multi-property-card--close {
height: rem(24);
position: absolute;
right: rem(10);
top: rem(10);
width: rem(24);
z-index: 10;
&::before {
background: transparent url(../images/close.svg) right top no-repeat;
content: '';
cursor: pointer;
display: block;
height: rem(24);
width: rem(24);
}
}
.mapbox {
aspect-ratio: unset;
height: calc(100vh - rem(100));
&__map {
aspect-ratio: unset;
bottom: 0;
height: calc(100vh - rem(60));
position: absolute;
top: 0;
width: 100%;
}
}
}
.no-results {
&__remove-filter-button,
&__reset-all-button {
background: $color-dark-grey;
cursor: pointer;
margin-bottom: rem(20);
padding: rem(20);
}
&__remove-filter-button {
display: inline-block;
}
&__reset-all-button {
display: none;
&.show {
display: block;
}
}
}
export default class PropertyListingPropertyStore {
constructor() {
this.allProperties = [];
this.properties = [];
}
setProperties(properties) {
this.properties = properties;
}
getProperties() {
return this.properties;
}
filterProperties(formData) {
const propertyTypes = formData.getAll('propertyType') || [];
const minSize = formData.get('min-size');
const maxSize = formData.get('max-size');
const locations = formData.getAll('location') || [];
const development = formData.getAll('development') || [];
const minPrice = formData.get('min-price');
const maxPrice = formData.get('max-price');
const maxBedrooms = formData.get('max-bedrooms');
const minBedrooms = formData.get('min-bedrooms');
const maxBathrooms = formData.get('max-bathrooms');
const minBathrooms = formData.get('min-bathrooms');
const amenities = formData.getAll('amenity');
return this.properties.filter((p) => (
(!propertyTypes.length || propertyTypes.includes(p.propertyType))
&& (!minSize || p.squareFootageValue >= minSize)
&& (!maxSize || p.squareFootageValue <= maxSize)
&& (!locations.length || (p.region && locations.includes(p.region.toLowerCase())))
&& (!development.length || development.includes(p.development))
&& (!minPrice || p.priceValue >= minPrice)
&& (!maxPrice || p.priceValue <= maxPrice)
&& (!minBedrooms || p.bedrooms >= minBedrooms)
&& (!maxBedrooms || p.bedrooms <= maxBedrooms)
&& (!minBathrooms || p.bathrooms >= minBathrooms)
&& (!maxBathrooms || p.bathrooms <= maxBathrooms)
&& (!amenities.length || amenities.every((a) => p.amenities.includes(a)))
));
}
}
import setSlider from './_slider';
import { getAllQueryStringParameters, setQueryParams } from '../../../../js/utils/routing';
export default class PropertyListingFilter {
constructor(component, triggers, propertyStore, onFilter) {
this.component = component;
this.onFilter = onFilter;
this.propertyStore = propertyStore;
this.form = this.component.querySelector('form');
this.formSubmitButton = this.component.querySelector('input[type="submit"]');
this.init(triggers);
}
init(triggers) {
triggers.forEach((trigger) => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
this.setSubmitText();
this.component.showModal();
});
});
const closeButton = this.component.querySelector('.js-close');
closeButton.addEventListener('click', (e) => {
e.preventDefault();
this.component.close();
});
const amenitiesToggle = this.component.querySelector('.js-amenities-toggle');
if (amenitiesToggle) {
amenitiesToggle.addEventListener('click', () => {
amenitiesToggle.parentNode.classList.toggle('show');
});
}
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.component.close();
// Set the query string parameters so users have the filters still selected when they hit the back button from the PDP
// First clear out any existing values by getting the form element names
const formElements = this.component.querySelectorAll('input, select');
formElements.forEach((formElement) => {
setQueryParams(formElement.name, '');
});
const formData = this.getFormData();
[...formData.keys()].forEach((key) => {
if (key) {
const values = formData.getAll(key);
const queryStringValue = values.join(',');
if (queryStringValue) {
setQueryParams(key, queryStringValue);
}
}
});
if (this.onFilter) {
this.onFilter();
}
});
this.form.addEventListener('reset', () => {
setTimeout(() => {
this.setSubmitText();
this.resetAllSliders();
}, 50);
});
const locationElements = this.form.querySelectorAll('[name="location"]');
locationElements.forEach((location) => {
location.addEventListener('change', () => {
const buildingElements = this.form.querySelectorAll('[name="development"]');
buildingElements.forEach((building) => {
const buildingElement = building;
buildingElement.disabled = false;
building.parentElement.classList.remove('disabled');
});
const selectedLocations = Array.from(locationElements)
.filter((dev) => dev.checked)
.map((dev) => dev.value.toLowerCase());
if (selectedLocations.length > 0) {
const properties = this.propertyStore.getProperties();
const filteredProperties = properties.filter((property) => {
const regionLowerCase = property.region ? property.region.toString().toLowerCase() : '';
return selectedLocations.includes(regionLowerCase);
});
const distinctBuildingTypes = [...new Set(filteredProperties.map((property) => property.development.toLowerCase()))];
buildingElements.forEach((building) => {
const elementName = building.getAttribute('value').toLowerCase();
if (!distinctBuildingTypes.includes(elementName)) {
const buildingElement = building;
buildingElement.disabled = true;
building.parentElement.classList.add('disabled');
}
});
}
});
});
}
handleBuildingClick(building) {
const locationElements = this.form.querySelectorAll('[name="location"]');
building.addEventListener('change', () => {
const buildingElements = this.form.querySelectorAll('[name="development"]');
locationElements.forEach((location) => {
const locationElement = location;
locationElement.disabled = false;
location.parentElement.classList.remove('disabled');
});
const selectedBuildingTypes = Array.from(buildingElements)
.filter((dev) => dev.checked)
.map((dev) => dev.value.toLowerCase());
if (selectedBuildingTypes.length > 0) {
const properties = this.propertyStore.getProperties();
const filteredProperties = properties.filter((property) => {
const regionLowerCase = property.development ? property.development.toString().toLowerCase() : '';
return selectedBuildingTypes.includes(regionLowerCase);
});
const distinctLocations = [...new Set(filteredProperties.map((property) => property.region.toLowerCase()))];
locationElements.forEach((location) => {
const elementValue = location.getAttribute('value').toLowerCase();
if (!distinctLocations.includes(elementValue)) {
const locationElement = location;
locationElement.disabled = true;
location.parentElement.classList.add('disabled');
}
});
}
});
}
getFormData() {
const formData = new FormData(this.form);
function removeValueIfMax(formElementName, component) {
const formElementValue = formData.get(formElementName);
const slider = component.querySelector(`.js-${formElementName}`);
if (slider) {
const max = slider.getAttribute('max');
if (max && (max === formElementValue)) {
formData.delete(formElementName);
}
}
}
// If the price and size values are the maximum the slider goes to then
// we want to remove the value from the form data so there is no top limit
removeValueIfMax('max-price', this.component);
removeValueIfMax('max-bedrooms', this.component);
removeValueIfMax('max-bathrooms', this.component);
removeValueIfMax('max-size', this.component);
return formData;
}
setValues() {
const properties = this.propertyStore.getProperties();
const distinctPropertyTypes = [...new Set(properties.map((x) => x.propertyType))];
const propertyTypeContainer = this.component.querySelector('.js-property-types');
if (propertyTypeContainer) {
distinctPropertyTypes.sort();
distinctPropertyTypes.forEach((propertyType) => {
const propertyElement = document.createElement('div');
propertyElement.classList.add('property-listing-filter__checkbox-container');
propertyElement.innerHTML = `
<label class="property-listing-filter__checkbox-label">
<input data-key="propertyType" type="checkbox" value="${propertyType}" name="propertyType"><span class="property-listing-filter__checkbox-text">${propertyType}</span>
</label>
`;
propertyTypeContainer.appendChild(propertyElement);
});
}
const maxSizeSlider = this.component.querySelector('.js-max-size');
if (maxSizeSlider) {
setSlider(this.component.querySelector('.js-size'), properties);
}
const buildingTypeContainer = this.component.querySelector('.js-building-type');
if (buildingTypeContainer) {
const distinctBuildingTypes = [...new Set(properties.map((x) => x.development))];
distinctBuildingTypes.forEach((buildingType) => {
if (buildingType !== '') {
const buildingElement = document.createElement('div');
buildingElement.classList.add('property-listing-filter__checkbox-container');
buildingElement.innerHTML = `
<label class="property-listing-filter__checkbox-label">
<input data-key="development" type="checkbox" value="${buildingType}" name="development"><span class="property-listing-filter__checkbox-text">${buildingType}</span>
</label>
`;
buildingTypeContainer.appendChild(buildingElement);
const checkbox = buildingElement.querySelector('[name="development"]');
checkbox.addEventListener('click', () => {
this.handleBuildingClick(checkbox);
});
}
});
}
const maxPriceSlider = this.component.querySelector('.js-max-price');
if (maxPriceSlider) {
setSlider(this.component.querySelector('.js-price'), properties);
}
if (this.component.querySelector('.js-max-bedrooms')) {
setSlider(this.component.querySelector('.js-bedrooms'));
}
if (this.component.querySelector('.js-max-bathrooms')) {
setSlider(this.component.querySelector('.js-bathrooms'));
}
if (this.component.querySelector('.js-amenities')) {
const distinctAmenitiesArray = properties.map((x) => x.amenities).flat();
const amenityContainer = this.component.querySelector('.js-amenities');
const distinctAmenities = [...new Set(distinctAmenitiesArray.map((amenity) => amenity))];
distinctAmenities.sort();
distinctAmenities.forEach((amenity) => {
if (amenity.length !== 0) {
const amenityElement = document.createElement('div');
amenityElement.classList.add('property-listing-filter__amenity-container');
amenityElement.innerHTML = `
<label class="property-listing-filter__amenity-label">
<input data-key="amenity" class="property-listing-filter__amenity-input" type="checkbox" value="${amenity}" name="amenity"><span class="property-listing-filter__amenity-text">${amenity}</span>
</label>
`;
amenityContainer.appendChild(amenityElement);
}
});
}
this.setFormChangeEvents();
// Set values from query string
const params = getAllQueryStringParameters();
if (params) {
params.forEach((param) => {
const filterElement = this.component.querySelector(`[name='${param.name}']`);
if (filterElement) {
if (filterElement.type === 'checkbox') {
// It's a checkbox so the query string value might hold multiple values that are separated by commas
// Get all these values and check the associated checkboxes
const values = param.value.split(',');
values.forEach((value) => {
const checkboxToSelect = this.component.querySelector(`[name='${param.name}'][value='${value}']`);
if (checkboxToSelect) {
checkboxToSelect.checked = true;
}
});
} else {
filterElement.value = param.value;
}
}
});
if (this.onFilter) {
this.onFilter(true);
}
}
}
setFormChangeEvents() {
this.component.querySelectorAll('input, select').forEach((formElement) => {
formElement.addEventListener('change', () => {
// A form element has changed so update the property count text
this.setSubmitText();
});
});
}
setSubmitText() {
const filteredProperties = this.propertyStore.filterProperties(this.getFormData());
if (!filteredProperties.length) {
this.formSubmitButton.value = 'no properties found';
} else {
this.formSubmitButton.value = `Show ${filteredProperties.length} Properties`;
}
}
submitClick() {
const submitButton = this.form.querySelector('[type="submit"]');
submitButton.click();
}
getSelectedFilters(formData) {
const propertyTypes = formData.getAll('propertyType') || [];
const minSize = formData.get('min-size') && Number(formData.get('min-size')) !== 0;
const maxSize = formData.get('max-size') && Number(formData.get('max-size')) !== Number(this.form.querySelector('[name="max-size"]').getAttribute('max'));
const locations = formData.getAll('location') || [];
const development = formData.getAll('development') || [];
const maxPrice = formData.get('max-price') && Number(formData.get('max-price')) !== Number(this.form.querySelector('[name="max-price"]').getAttribute('max'));
const minPrice = formData.get('min-price') && Number(formData.get('min-price')) !== 0;
const maxBedrooms = formData.get('max-bedrooms') && Number(formData.get('max-bedrooms')) !== Number(this.form.querySelector('[name="max-bedrooms"]').getAttribute('max'));
const minBedrooms = formData.get('min-bedrooms') && Number(formData.get('min-bedrooms')) !== 0;
const maxBathrooms = formData.get('max-bathrooms') && Number(formData.get('max-bathrooms')) !== Number(this.form.querySelector('[name="max-bathrooms"]').getAttribute('max'));
const minBathrooms = formData.get('min-bathrooms') && Number(formData.get('min-bathrooms')) !== 0;
const amenities = formData.getAll('amenity') || [];
const conditions = [
{
key: 'propertyType', condition: propertyTypes.length > 0,
},
{
key: 'size', condition: maxSize || minSize,
},
{
key: 'location', condition: locations.length > 0,
},
{
key: 'development', condition: development.length > 0,
},
{
key: 'price', condition: maxPrice || minPrice,
},
{
key: 'bedrooms', condition: maxBedrooms || minBedrooms,
},
{
key: 'bathrooms', condition: maxBathrooms || minBathrooms,
},
{
key: 'amenity', condition: amenities.length > 0,
},
];
function getOrderFromForm(form) {
const order = [];
const formElements = form.elements;
for (let i = 0; i < formElements.length; i++) {
const element = formElements[i];
if (element.dataset.key) {
const matchingCondition = conditions.find((condition) => condition.key === element.dataset.key);
if (matchingCondition) {
matchingCondition.title = element.closest('fieldset').querySelector('legend').textContent.toLowerCase();
if (!order.includes(element.dataset.key)) {
order.push(element.dataset.key);
}
}
}
}
return order;
}
const formOrder = getOrderFromForm(this.form);
const orderedConditions = formOrder.map((conditionKey) => conditions.find((condition) => condition.key === conditionKey));
return orderedConditions.filter(({ condition }) => condition).map(({ key, title }) => ({ key, title }));
}
handleReset(key) {
if (key === 'propertyType' || key === 'location' || key === 'development' || key === 'amenity') {
this.resetCheckbox(key);
}
if (key === 'size' || key === 'price' || key === 'bedrooms' || key === 'bathrooms') {
this.resetSlider(key);
}
this.submitClick();
}
resetCheckbox(name) {
this.form.querySelectorAll(`[name=${name}]`).forEach((checkbox) => {
const cb = checkbox;
cb.checked = false;
});
}
resetSlider(key) {
this.form.querySelector(`[name='min-${key}']`).value = 0;
this.form.querySelector(`[name='max-${key}']`).value = this.form.querySelector(`[name='max-${key}']`).getAttribute('max');
setSlider(this.form.querySelector(`.js-${key}`));
}
resetAllSliders() {
const sliderClasses = ['.js-size', '.js-price', '.js-bedrooms', '.js-bathrooms'];
sliderClasses.forEach((sliderClass) => {
const sliderElement = this.component.querySelector(sliderClass);
if (sliderElement) {
setSlider(this.form.querySelector(sliderClass));
}
});
}
resetAllFilters() {
this.resetAllSliders();
this.form.reset();
this.submitClick();
}
}
import { PropertyListCard } from '../../cards/property-list-card/property-list-card';
import { getQueryStringParameter, setQueryParams } from '../../../../js/utils/routing';
import Pagination from '../../listings/pagination/pagination';
const pageQueryStringParameterName = 'page';
const propertyQueryStringParameterName = 'vpid';
export default class PropertyListingList {
constructor(component, filter, mapView) {
this.component = component;
this.filter = filter;
this.resultsContainer = this.component.querySelector('.js-results-container');
this.scrollableContainer = this.component.querySelector('.js-scrollable-container');
this.propertyCardTemplate = this.component.querySelector('.js-property-card-template');
this.resultCountText = this.component.querySelector('.js-property-count');
this.noResultsContainer = this.component.querySelector('.js-no-results-container');
this.orderBySelect = this.component.querySelector('select[name="sortBy"]');
this.loadMoreButton = this.component.querySelector('.js-load-more-button');
if (this.component.dataset.pageSize) {
this.pageSize = parseInt(this.component.dataset.pageSize, 10);
}
if (this.component.dataset.pagingType) {
this.pagingType = this.component.dataset.pagingType;
}
this.numberOfPropertiesRendered = 0;
this.mapView = mapView;
this.init();
}
init() {
this.orderBySelect.addEventListener('change', () => {
this.orderProperties();
this.refreshListView();
});
this.noResultsContainer.querySelector('.js-reset-all-filters').addEventListener('click', () => {
const resetButton = this.noResultsContainer.querySelector('.js-reset-all-filters.show');
if (resetButton && resetButton.classList.contains('show')) {
this.noResultsContainer.querySelector('.js-remove-filters-copy').classList.remove('show');
resetButton.classList.remove('show');
}
this.filter.resetAllFilters();
});
if (this.pagingType && this.pagingType === 'loadmore') {
this.loadMoreButton.addEventListener('click', (e) => {
e.preventDefault();
this.page++;
setQueryParams(pageQueryStringParameterName, this.page);
this.renderResults();
});
}
// drag handle functionality on small breakpoints
const listComponent = this.component;
const base = this;
const dragHandleElement = this.component.querySelector('.js-drag-handle');
function mousemove(e) {
const listPanelRect = base.component.getBoundingClientRect();
const yPos = e.type === 'touchmove' ? e.touches[0].pageY : e.y;
const newHeight = listPanelRect.bottom - yPos - dragHandleElement.clientHeight;
const newMapHeight = listComponent.parentElement.getBoundingClientRect().height - newHeight;
base.mapView.setHeight(newMapHeight);
base.setHeight(newHeight);
}
function mouseup(e) {
if (e.type === 'touchend') {
window.removeEventListener('touchmove', mousemove);
window.removeEventListener('touchend', mouseup);
} else {
window.removeEventListener('mousemove', mousemove);
window.removeEventListener('mouseup', mouseup);
}
base.mapView.resize();
}
function resizer(e) {
e.preventDefault();
if (e.type === 'touchstart') {
window.addEventListener('touchmove', mousemove);
window.addEventListener('touchend', mouseup);
} else {
window.addEventListener('mousemove', mousemove);
window.addEventListener('mouseup', mouseup);
}
}
dragHandleElement.addEventListener('touchstart', resizer);
dragHandleElement.addEventListener('mousedown', resizer);
if (this.pagingType && this.pagingType === 'pagination') {
const paginationElement = this.component.querySelector('.js-pagination');
if (paginationElement) {
this.pagination = new Pagination(this.component.querySelector('.js-pagination'), () => {
setQueryParams(pageQueryStringParameterName, this.pagination.currentPage);
this.renderResults(true);
});
}
}
}
setHeight(newHeight) {
this.component.style.flexBasis = newHeight !== null ? `${newHeight}px` : '';
}
showLoader() {
this.resultsContainer.classList.add('loading');
}
hideLoader() {
this.resultsContainer.classList.remove('loading');
}
setProperties(properties, init) {
this.properties = properties;
this.orderProperties();
this.resultCountText.innerText = `${this.properties.length} properties`;
this.refreshListView(init);
if (this.pagination) {
this.pagination.setNumberOfPages(Math.ceil(this.properties.length / this.pageSize));
}
}
orderProperties() {
const orderByValue = this.orderBySelect.value;
switch (orderByValue) {
case 'development':
this.properties = this.properties.sort((a, b) => {
if (a.development < b.development) { return -1; }
if (a.development > b.development) { return 1; }
return 0;
});
break;
case 'price DESC':
this.properties = this.properties.sort((a, b) => { return a.priceValue > b.priceValue ? -1 : 1; });
break;
case 'price ASC':
this.properties = this.properties.sort((a, b) => { return a.priceValue < b.priceValue ? -1 : 1; });
break;
case 'size DESC':
this.properties = this.properties.sort((a, b) => { return a.squareFootageValue > b.squareFootageValue ? -1 : 1; });
break;
case 'size ASC':
this.properties = this.properties.sort((a, b) => { return a.squareFootageValue < b.squareFootageValue ? -1 : 1; });
break;
case 'dateAdded DESC':
this.properties = this.properties.sort((a, b) => { return a.dateAdded > b.dateAdded ? -1 : 1; });
break;
default:
console.error(`${orderByValue} not known`);
break;
}
}
refreshListView(init) {
this.resultsContainer.innerHTML = '';
this.page = 1;
this.numberOfPropertiesRendered = 0;
if (this.pagingType && this.pagingType === 'loadmore') {
this.loadMoreButton.style.display = 'none';
}
this.renderResults(init);
}
renderResults(init) {
let propertiesToShow = this.properties;
if (this.pageSize) {
// Page size is set so we need to paginate the results
const pageQueryStringValue = getQueryStringParameter(pageQueryStringParameterName);
if (init && pageQueryStringValue) {
this.page = parseInt(pageQueryStringValue, 10);
propertiesToShow = this.properties.slice(0, this.page * this.pageSize);
if (this.pagingType && this.pagingType === 'pagination') {
propertiesToShow = this.properties.slice((this.page - 1) * this.pageSize, this.page * this.pageSize);
}
} else {
propertiesToShow = this.properties.slice((this.page - 1) * this.pageSize, this.page * this.pageSize);
}
}
if (this.pagingType && this.pagingType === 'pagination') {
this.resultsContainer.innerHTML = '';
}
this.numberOfPropertiesRendered += propertiesToShow.length;
if (propertiesToShow.length > 0) {
this.noResultsContainer.style.display = '';
const groupDevelopments = this.orderBySelect.value === 'development';
let currentDevelopment = ''; // Used to track when we enter a new development
propertiesToShow.forEach((property) => {
if (groupDevelopments && currentDevelopment !== property.development) {
const developmentHeaderElement = document.createElement('div');
developmentHeaderElement.className = 'list-view__building-heading';
developmentHeaderElement.textContent = property.development;
developmentHeaderElement.dataset.developmentName = property.development;
this.resultsContainer.appendChild(developmentHeaderElement);
currentDevelopment = property.development;
}
const propertyContainerDiv = document.createElement('div');
propertyContainerDiv.dataset.propertyId = property.id;
propertyContainerDiv.classList.add('list-view__property-card-container');
const clone = this.propertyCardTemplate.content.cloneNode(true);
const propertyListCardElement = clone.querySelector('[data-module="propertyListCard"]');
propertyContainerDiv.appendChild(propertyListCardElement);
new PropertyListCard(propertyListCardElement, property);
this.resultsContainer.appendChild(propertyContainerDiv);
propertyContainerDiv.querySelector('a').addEventListener('click', () => {
setQueryParams(propertyQueryStringParameterName, property.id);
});
});
} else if (this.page === 1) {
this.noResultsContainer.style.display = 'block';
}
if (this.pageSize && this.pagingType && this.pagingType === 'loadmore') {
// 'Pagination' is enabled so show/hide the load more button
this.loadMoreButton.style.display = this.numberOfPropertiesRendered >= this.properties.length ? 'none' : 'inline-block';
}
if (init) {
const propertyIdToShow = getQueryStringParameter(propertyQueryStringParameterName);
const propertyElementToDisplay = this.resultsContainer.querySelector(`[data-property-id='${propertyIdToShow}']`);
if (propertyElementToDisplay) {
propertyElementToDisplay.scrollIntoView();
}
if (this.pagingType && this.pagingType === 'pagination') {
this.resultsContainer.scrollIntoView();
}
}
}
static highlightProperty(propertyContainerDiv) {
// Add the highlight class, the css will deal with the animation
const highlightClassName = 'highlight';
if (propertyContainerDiv.classList.contains(highlightClassName)) {
propertyContainerDiv.classList.remove(highlightClassName);
}
setTimeout(() => propertyContainerDiv.classList.add(highlightClassName), 100);
}
createRemoveFilters(formData) {
const selectedFilters = this.filter.getSelectedFilters(formData);
function createButtons() {
return selectedFilters.map(({ key, title }) => `
<div class="no-results__remove-filter">
<span data-key="${key}" class="no-results__remove-filter-button js-remove-filter">Remove ${title}</span>
</div>
`).join('').trim();
}
this.noResultsContainer.querySelector('.js-remove-filter-buttons').innerHTML = createButtons();
const removeButtons = this.component.querySelectorAll('.js-remove-filter');
removeButtons.forEach((removeButton) => {
removeButton.addEventListener('click', () => this.handleRemoveFilter(removeButton.dataset.key));
});
if (removeButtons.length && !this.noResultsContainer.querySelector('.js-reset-all-filters').classList.contains('show')) {
this.noResultsContainer.querySelector('.js-remove-filters-copy').classList.add('show');
this.noResultsContainer.querySelector('.js-reset-all-filters').classList.add('show');
}
}
handleRemoveFilter(key) {
this.filter.handleReset(key);
}
}
import mapboxgl from 'mapbox-gl';
import { MapBox } from '../../maps/mapbox/mapbox';
import { PropertyListCard } from '../../cards/property-list-card/property-list-card';
import { MultiPropertyCard } from '../../cards/multi-property-card/multi-property-card';
import { getQueryStringParameter, setQueryParams } from '../../../../js/utils/routing';
const decimalPlaces = 2;
const mapZoomQueryStringParameterName = 'map-zoom';
const mapLatQueryStringParameterName = 'lat';
const mapLngQueryStringParameterName = 'lng';
const propertyCardQueryStringParameterName = 'mpid';
const multiPropertyCardQueryStringParameterName = 'mmid';
export default class PropertyListingMap {
constructor(component, onPropertyClick, onMultiPropertyClick, onMapMove) {
this.component = component;
this.onPropertyClick = onPropertyClick;
this.onMultiPropertyClick = onMultiPropertyClick;
this.onMapMove = onMapMove;
this.mapBox = new MapBox(this.component.querySelector('.js-mapbox'));
this.mapBox.map.scrollZoom.enable();
this.propertyListCard = new PropertyListCard(this.component.querySelector('[data-module="propertyListCard"]'));
this.isMapLoaded = false;
const multiPropertyCardElement = this.component.querySelector('[data-module="multiPropertyCard"]');
if (multiPropertyCardElement) {
this.multiPropertyCard = new MultiPropertyCard(multiPropertyCardElement);
}
this.markersOnScreen = {};
this.developmentsArray = [];
// Array of (potentially) filtered properties
this.properties = [];
this.initMap();
const closeCardButtons = this.component.querySelectorAll('.js-close-card');
if (closeCardButtons.length > 0) {
closeCardButtons.forEach((closeCard) => {
closeCard.addEventListener('click', () => {
closeCard.parentElement.classList.remove('show');
closeCard.parentElement.setAttribute('aria-hidden', 'true');
setQueryParams(multiPropertyCardQueryStringParameterName, '');
setQueryParams(propertyCardQueryStringParameterName, '');
});
});
}
}
setHeight(newHeight) {
this.component.style.flexBasis = newHeight ? `${newHeight}px` : '';
}
initMap() {
const that = this;
this.mapBox.map.on('load', () => {
this.isMapLoaded = true;
// Set center and zoom values from the query string if present
const latQueryStringValue = getQueryStringParameter(mapLatQueryStringParameterName);
const lngQueryStringValue = getQueryStringParameter(mapLngQueryStringParameterName);
if (latQueryStringValue && lngQueryStringValue) {
const latValue = parseFloat(latQueryStringValue, 10);
const lngValue = parseFloat(lngQueryStringValue, 10);
this.mapBox.map.setCenter([lngValue, latValue]);
}
const zoomLevel = getQueryStringParameter(mapZoomQueryStringParameterName);
if (zoomLevel) {
this.mapBox.map.setZoom(zoomLevel);
}
// Source for grouping properties into developments
this.mapBox.map.addSource('locations', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50,
});
// Source for grouping properties into developments
this.mapBox.map.addSource('all-properties', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
});
// Layer for clusters and property pins
this.mapBox.map.addLayer({
id: 'clusters',
type: 'circle',
source: 'locations',
filter: ['==', 'cluster', true],
paint: {
'circle-radius': 20,
'circle-opacity': 0,
},
});
// Layer for all properties so we can tell the list view which ones are visible. This will be hidden from the user.
this.mapBox.map.addLayer({
'id': 'property-data',
'source': 'all-properties',
'type': 'circle',
'paint': {
'circle-color': 'transparent',
'circle-radius': 4,
'circle-stroke-width': 2,
'circle-stroke-color': 'transparent',
},
});
// array of markers. These can be clusters or individual properties
const { map } = this.mapBox;
const {
component,
} = this;
const base = this;
function updateMarkers() {
const newMarkers = {};
const features = map.querySourceFeatures('locations');
for (let i = 0; i < features.length; i++) {
const coords = features[i].geometry.coordinates;
// props is either a cluster or property data
const props = features[i].properties;
const id = props.cluster ? `cluster${props.cluster_id}` : props.id;
if (newMarkers[id]) {
// We've already added this marker (duplicate)
continue;
}
if (props.cluster && that.markersOnScreen[id]) {
// It's a cluster and is already on the screen
newMarkers[id] = that.markersOnScreen[id];
} else if (props.cluster && !that.markersOnScreen[id]) {
// This feature is clustered and is not already on the screen, create an icon for it and use props.point_count for its count
const el = document.createElement('div');
el.className = 'map-view__property-cluster';
el.style.textAlign = 'center';
el.style.borderRadius = '50%';
el.innerText = props.point_count;
const marker = new mapboxgl.Marker(el).setLngLat(coords);
marker.addTo(map);
newMarkers[id] = marker;
} else if (!props.cluster && that.markersOnScreen[id]) {
// It's a property and is already on the screen
newMarkers[id] = that.markersOnScreen[id];
} else if (!props.cluster && !that.markersOnScreen[id] && !props.hasMatch) {
// Feature marker is a property and is not already on screen
const el = document.createElement('div');
el.className = 'map-view__property-marker';
const marker = new mapboxgl.Marker(el).setLngLat(coords);
marker.addTo(map);
const propertyData = that.properties.find((item) => item.id === props.propertyId);
marker.getElement().addEventListener('click', (e) => {
if (component.querySelector('.clicked')) {
component.querySelector('.clicked').classList.remove('clicked');
}
e.target.classList.add('clicked');
base.onPropertyClick(propertyData);
});
newMarkers[id] = marker;
} else if (!props.cluster && !that.markersOnScreen[id] && props.hasMatch) {
const el = document.createElement('div');
el.className = 'map-view__property-multi-marker';
const marker = new mapboxgl.Marker(el).setLngLat(coords);
that.developmentsArray.push(features[i]);
const findDevelopments = that.developmentsArray.filter((item) => item.properties.development === features[i].properties.development
&& item.properties.longitude.toFixed(decimalPlaces) === features[i].geometry.coordinates[0].toFixed(decimalPlaces)
&& item.properties.latitude.toFixed(decimalPlaces) === features[i].geometry.coordinates[1].toFixed(decimalPlaces));
if (findDevelopments.length === 1) {
marker.addTo(map);
marker.getElement().addEventListener('click', (e) => {
if (component.querySelector('.clicked')) {
component.querySelector('.clicked').classList.remove('clicked');
}
e.target.classList.add('clicked');
// Filter matching property data
const matchingPropertyData = that.properties.filter((item) => item.latitude.toFixed(decimalPlaces) === props.latitude.toFixed(decimalPlaces) && item.longitude.toFixed(decimalPlaces) === props.longitude.toFixed(decimalPlaces) && item.development === props.development);
base.onMultiPropertyClick(matchingPropertyData);
});
}
newMarkers[id] = marker;
}
}
// Remove the markers that are no longer on the screen
const ids = Object.getOwnPropertyNames(that.markersOnScreen);
for (let i = 0; i < ids.length; i++) {
const onScreenId = ids[i];
if (!newMarkers[onScreenId]) {
that.markersOnScreen[onScreenId].remove();
}
}
that.markersOnScreen = newMarkers;
}
this.mapBox.map.on('click', 'clusters', (e) => {
this.developmentsArray = [];
const features = this.mapBox.map.queryRenderedFeatures(e.point, {
layers: ['clusters'],
});
const clusterId = features[0].properties.cluster_id;
this.mapBox.map.getSource('locations').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
this.mapBox.map.easeTo({
center: features[0].geometry.coordinates,
zoom,
});
},
);
});
this.mapBox.map.on('render', () => {
if (!this.mapBox.map.isSourceLoaded('locations')) return;
updateMarkers();
});
this.mapBox.map.on('mouseenter', 'clusters', () => {
this.mapBox.map.getCanvas().style.cursor = 'pointer';
});
this.mapBox.map.on('mouseleave', 'clusters', () => {
this.mapBox.map.getCanvas().style.cursor = '';
});
this.mapBox.map.on('moveend', (eventData) => {
// A move end event also fires when the map is resized. This causes a big problem on small screen sizes
// when the user moves the list view to cover the enitre map.
// This is a work around so this code only gets executed when the user moves the map
// https://github.com/mapbox/mapbox-gl-js/issues/6512#issuecomment-572670290
let sendAction = true;
if (eventData && eventData.originalEvent && eventData.originalEvent.type === 'resize') {
sendAction = false;
}
if (sendAction) {
const features = this.mapBox.map.queryRenderedFeatures({ layers: ['property-data'] });
const visiblePropertyIds = features.map((feature) => { return feature.properties.id; });
const visibleProperties = this.properties.filter((property) => { return visiblePropertyIds.includes(property.id); });
this.onMapMove(visibleProperties);
}
setQueryParams(mapZoomQueryStringParameterName, this.mapBox.map.getZoom());
const { lng, lat } = this.mapBox.map.getCenter();
setQueryParams(mapLatQueryStringParameterName, lat);
setQueryParams(mapLngQueryStringParameterName, lng);
});
});
}
setAllProperties(properties) {
// Set the complete set of properties so we can work out which ones are visible
const newArray = properties.map((property) => {
return { type: 'Feature', properties: property, geometry: { type: 'Point', coordinates: [property.longitude, property.latitude] } };
});
const geoJson = {
'type': 'FeatureCollection',
'features': [
...newArray,
],
};
const setLocationData = () => {
const geojsonSource = this.mapBox.map.getSource('all-properties');
geojsonSource.setData(geoJson);
};
if (this.isMapLoaded) {
setLocationData();
} else {
// Map hasn't loaded yet so wait until it has
this.mapBox.map.on('load', () => {
setLocationData();
});
}
// Show cards if they appear in the query string
const propertyId = getQueryStringParameter(propertyCardQueryStringParameterName);
if (propertyId) {
const propertyToShow = properties.find((p) => p.id === parseInt(propertyId, 10));
if (propertyToShow) {
this.showPropertyCard(propertyToShow);
}
}
const multiPropertyIdString = getQueryStringParameter(multiPropertyCardQueryStringParameterName);
if (multiPropertyIdString) {
const multiPropertyIds = multiPropertyIdString.split(',').map((id) => { return parseInt(id, 10); });
const propertiesToShow = properties.filter((p) => multiPropertyIds.includes(p.id));
if (propertiesToShow) {
this.showMultiPropertyCard(propertiesToShow);
}
}
}
// Sets properties to display on the map. This could be a filtered list.
setProperties(properties) {
this.properties = properties;
// Remove all existing markers
const ids = Object.getOwnPropertyNames(this.markersOnScreen);
for (let i = 0; i < ids.length; i++) {
const onScreenId = ids[i];
this.markersOnScreen[onScreenId].remove();
}
const newArray = properties.map((property) => {
return {
properties: { ...property },
geometry: { type: 'Point', coordinates: [property.longitude, property.latitude] },
};
});
function haveSameCoordinates(property1, property2) {
return (
(
property1.geometry.coordinates[0] !== undefined
&& property2.geometry.coordinates[0] !== undefined
&& property1.geometry.coordinates[0].toFixed(decimalPlaces) === property2.geometry.coordinates[0].toFixed(decimalPlaces)
)
&& (
property1.geometry.coordinates[1] !== undefined
&& property2.geometry.coordinates[1] !== undefined
&& property1.geometry.coordinates[1].toFixed(decimalPlaces) === property2.geometry.coordinates[1].toFixed(decimalPlaces)
) && (
property1.properties.development
&& property2.properties.development
&& property1.properties.development === property2.properties.development
)
);
}
const updatedProperties = newArray.map((property, index) => {
const hasMatchingCoordinates = newArray.some((otherProperty, otherIndex) => {
return index !== otherIndex && haveSameCoordinates(property, otherProperty);
});
return {
type: 'Feature',
properties: {
...property.properties,
propertyId: property.properties.id,
id: index,
hasMatch: hasMatchingCoordinates,
},
geometry: {
type: property.geometry.type,
coordinates: [...property.geometry.coordinates],
},
};
});
const geoJson = {
'type': 'FeatureCollection',
'features': updatedProperties,
};
const setLocationData = () => {
const geojsonSource = this.mapBox.map.getSource('locations');
// clear any existing data
geojsonSource.setData({
type: 'FeatureCollection',
features: [],
});
geojsonSource.setData(geoJson);
};
if (this.isMapLoaded) {
setLocationData();
} else {
// Map hasn't loaded yet so wait until it has
this.mapBox.map.on('load', () => {
setLocationData();
});
}
}
resize() {
this.mapBox.map.resize();
}
showPropertyCard(propertyData) {
const multiPropertyCardContainer = this.component.querySelector('.js-multi-property-card-container.show');
if (multiPropertyCardContainer) {
multiPropertyCardContainer.classList.remove('show');
multiPropertyCardContainer.setAttribute('aria-hidden', 'true');
}
this.propertyListCard.setPropertyData(propertyData);
this.component.querySelector('.js-property-list-card-container').classList.add('show');
this.component.querySelector('.js-property-list-card-container').removeAttribute('aria-hidden');
setQueryParams(multiPropertyCardQueryStringParameterName, '');
setQueryParams(propertyCardQueryStringParameterName, propertyData.id);
}
showMultiPropertyCard(properties) {
const propertyListCardContainer = this.component.querySelector('.js-property-list-card-container.show');
if (propertyListCardContainer) {
propertyListCardContainer.classList.remove('show');
propertyListCardContainer.setAttribute('aria-hidden', 'true');
}
this.multiPropertyCard.setPropertyData(properties);
this.component.querySelector('.js-multi-property-card-container').classList.add('show');
this.component.querySelector('.js-multi-property-card-container').removeAttribute('aria-hidden');
setQueryParams(propertyCardQueryStringParameterName, '');
setQueryParams(multiPropertyCardQueryStringParameterName, properties.map((p) => { return p.id; }).join(','));
}
}
%property-listing-filter-trigger {
background: $color-black;
color: $color-white;
display: block;
padding: rem(18);
text-align: center;
text-decoration: none;
span {
background: url('../images/property-filter.svg') left center no-repeat;
background-size: rem(24);
padding-left: rem(34);
}
&:hover {
color: $color-white;
}
}
export default function setSlider(component, properties = null) {
const container = component.querySelector('.js-slider-container');
const rangeLower = component.querySelector('.js-range-lower');
const rangeUpper = component.querySelector('.js-range-upper');
const trackerBetween = component.querySelector('.js-tracker-between');
const minValue = Number(rangeUpper.getAttribute('min'));
const maxValue = Number(rangeUpper.getAttribute('max'));
const minText = component.querySelector('.js-min');
const maxText = component.querySelector('.js-max');
const { type } = component.dataset;
const createBarChart = () => {
const values = properties.map((property) => Number(property[type])).filter((value) => !Number.isNaN(value) && value !== null);
if (values.length === 0) {
return;
}
const numBins = Math.min(40, values.length);
const valueRange = maxValue - minValue;
const binWidth = valueRange / numBins;
const binCounts = new Array(numBins).fill(0);
values.forEach((value) => {
let binIndex = Math.floor((value - minValue) / binWidth);
binIndex = Math.min(Math.max(binIndex, 0), numBins - 1);
binCounts[binIndex]++;
});
const barContainer = document.createElement('div');
barContainer.classList.add('slider__bar-container');
let maxBarHeight = 0;
for (let i = 0; i < numBins; i++) {
const barHeight = (binCounts[i] / values.length) * 100;
const bar = document.createElement('div');
bar.style.width = `${100 / numBins}%`;
bar.classList.add('slider__bar');
bar.classList.add('js-bar');
bar.dataset.percentage = barHeight;
barContainer.appendChild(bar);
if (barHeight > maxBarHeight) {
maxBarHeight = barHeight;
}
}
container.appendChild(barContainer);
const bars = container.querySelectorAll('.js-bar');
bars.forEach((bar) => {
const b = bar;
const percentage = parseFloat(b.dataset.percentage, 10);
b.style.transform = `translateY(${100 - ((percentage / maxBarHeight) * 100)}%)`;
});
};
if (type && properties) {
createBarChart();
}
let containerHoverOnPercent = 0;
const updateTrackerBetween = () => {
const lowerValue = Number(rangeLower.value);
const upperValue = Number(rangeUpper.value);
const range = maxValue - minValue;
const widthPercentage = ((upperValue - lowerValue) / range) * 100;
const leftPercentage = ((lowerValue - minValue) / range) * 100;
trackerBetween.style.width = `${widthPercentage.toFixed(2)}%`;
trackerBetween.style.left = `${leftPercentage.toFixed(2)}%`;
minText.innerText = Number(lowerValue).toLocaleString('en', { useGrouping: true });
if (maxValue === upperValue) {
maxText.innerHTML = `${Number(upperValue).toLocaleString('en', { useGrouping: true })} +`;
} else {
maxText.innerText = Number(upperValue).toLocaleString('en', { useGrouping: true });
}
};
updateTrackerBetween();
const moveAppropriateThumbToUpper = () => {
const lowerValue = Number(rangeLower.value);
const upperValue = Number(rangeUpper.value);
const closeValue = maxValue / 10;
if (
upperValue - lowerValue < closeValue
&& upperValue > maxValue * 0.9
) {
rangeLower.classList.add('display-upper');
rangeUpper.classList.remove('display-upper');
} else if (
upperValue - lowerValue < closeValue
&& lowerValue < maxValue * 0.1
) {
rangeLower.classList.remove('display-upper');
rangeUpper.classList.add('display-upper');
} else {
const middleValue = lowerValue + (upperValue - lowerValue) / 2;
if (containerHoverOnPercent < middleValue / maxValue) {
rangeLower.classList.add('display-upper');
rangeUpper.classList.remove('display-upper');
} else {
rangeLower.classList.remove('display-upper');
rangeUpper.classList.add('display-upper');
}
}
};
['mouseenter', 'mousemove', 'touchstart', 'touchmove'].forEach((eventName) => {
container.addEventListener(eventName, (event) => {
containerHoverOnPercent = event.offsetX / event.target.clientWidth;
moveAppropriateThumbToUpper();
}, false);
});
rangeLower.addEventListener('input', (event) => {
const lowerValue = Number(event.target.value);
const upperValue = Number(rangeUpper.value);
let adjustedValue = lowerValue;
if (lowerValue < minValue) {
adjustedValue = minValue;
} else if (lowerValue > upperValue) {
adjustedValue = upperValue;
}
Object.assign(event.target, {
value: String(adjustedValue),
});
// Use the updated value
moveAppropriateThumbToUpper();
updateTrackerBetween();
}, false);
rangeUpper.addEventListener('input', (event) => {
const lowerValue = Number(rangeLower.value);
const upperValue = Number(event.target.value);
let adjustedValue = upperValue;
if (upperValue > maxValue) {
adjustedValue = maxValue;
} else if (upperValue < lowerValue) {
adjustedValue = lowerValue;
}
// Create a new object and update the necessary properties
Object.assign(event.target, {
value: String(adjustedValue),
});
// Use the updated value
moveAppropriateThumbToUpper();
updateTrackerBetween();
}, false);
}
$input-range-thumb-height: 20px;
$input-range-tracker-height: 10px;
$input-range-lower-thumb-color: $color-black;
$input-range-upper-thumb-color: $color-black;
.slider {
&__range-container {
height: $input-range-thumb-height;
position: relative;
width: 70%;
&--with-bars {
margin-top: rem(150);
}
input[type="range"] {
appearance: none;
background: transparent;
display: block;
position: absolute;
width: 100%;
&.display-upper {
z-index: 1;
}
//webkit
&::-webkit-slider-thumb,
&::-webkit-media-slider-thumb {
appearance: none;
border: 0;
border-radius: 50%;
height: rem(20);
pointer-events: auto;
width: rem(20);
}
}
// Webkit
input[type="range"]:not(*:root) {
pointer-events: none;
}
input[type="range"]::-webkit-slider-runnable-track {
background: transparent;
}
input.slider__range-lower {
&::-webkit-slider-thumb,
&::-webkit-media-slider-thumb {
background: $input-range-lower-thumb-color;
}
}
input.slider__range-upper {
&::-webkit-slider-thumb,
&::-webkit-media-slider-thumb {
background: $input-range-upper-thumb-color;
}
}
// Firefox
input[type="range"]::-moz-range-track {
background: transparent;
}
input[type="range"]::-moz-range-thumb {
appearance: none;
border: 0;
border-radius: 50%;
height: rem(20);
pointer-events: auto;
width: 20px;
}
input.slider__range-lower::-moz-range-thumb {
background: $input-range-lower-thumb-color;
}
input.slider__range-upper::-moz-range-thumb {
background: $input-range-upper-thumb-color;
}
// Edge
input[type="range"]::-ms-track {
background: transparent;
border-color: transparent;
color: transparent;
z-index: 1;
}
input[type="range"]::-ms-thumb {
appearance: none;
border: 0;
border-radius: 50%;
height: rem(20);
width: rem(20);
}
input.slider__range-lower::-ms-thumb {
background: $input-range-lower-thumb-color;
}
input.slider__range-upper::-ms-thumb {
background: $input-range-upper-thumb-color;
}
}
&__range-tracker {
background: $color-steel-grey;
border-radius: rem(10);
height: $input-range-tracker-height;
left: 0;
margin-top: - rem(5);
pointer-events: none;
position: absolute;
top: 50%;
width: 100%;
}
&__range-tracker-between {
background: $color-black;
border-radius: rem(10);
height: $input-range-tracker-height;
left: 0;
margin-top: - rem(5);
pointer-events: none;
position: absolute;
top: 50%;
width: 0;
}
&__min-max-container {
display: flex;
justify-content: space-between;
padding: rem(20) 0;
width: 70%;
}
&__bar-container {
align-items: flex-end;
bottom: rem(20);
display: flex;
gap: rem(5);
height: rem(150);
justify-content: space-between;
overflow: hidden;
padding: 0 rem(20);
position: absolute;
width: 100%;
}
&__bar {
background: rgba($color-black, 0.2);
height: 100%;
transform-origin: top;
}
}
import LazyLoader from '../../../../js/utils/lazy-loader';
import PropertyListingList from './_property-listing-list';
import PropertyListingMap from './_property-listing-map';
import PropertyListingFilter from './_property-listing-filter';
import PropertyListingPropertyStore from './_propert-listing-property-store';
import debounce from '../../../../js/utils/debounce';
import { isSmallBreakpoint } from '../../../../js/utils/breakpoints';
import { getQueryStringParameter, setQueryParams } from '../../../../js/utils/routing';
const viewQueryStringParameterName = 'split-view';
export class PropertyListing {
constructor(component) {
this.component = component;
this.mapView = new PropertyListingMap(
this.component.querySelector('.js-map-view'),
(propertyData) => this.onMapPropertyClick(propertyData),
(properties) => this.onMapMultiPropertyClick(properties),
(properties) => this.onMapMove(properties),
);
this.propertyStore = new PropertyListingPropertyStore();
this.filter = new PropertyListingFilter(
this.component.querySelector('.js-filter-dialog'),
this.component.querySelectorAll('.js-filter-trigger'),
this.propertyStore,
(formData) => this.onFilter(formData),
);
this.listView = new PropertyListingList(
this.component.querySelector('.js-list-view'),
this.filter,
this.mapView,
);
this.toggleViewButton = this.component.querySelector('.js-view-toggle');
this.splitView = true;
this.propertyDataLoaded = false;
this.init();
// Check the query string value to see what view we need to display
const splitViewValue = getQueryStringParameter(viewQueryStringParameterName);
if (splitViewValue === 'false') {
this.toggleView();
}
}
init() {
const resultsContainer = this.component.querySelector('.js-results-container');
this.lazyLoader = new LazyLoader(
this.component.dataset.endpoint,
null,
null,
resultsContainer,
(res) => { this.onApiSuccess(res); },
this.component.dataset.method,
);
this.listView.showLoader();
this.lazyLoader.load();
if (this.toggleViewButton) {
this.toggleViewButton.addEventListener('click', (e) => {
e.preventDefault();
this.toggleView();
});
}
const resize = debounce(50, () => {
if (!isSmallBreakpoint()) {
// If we switch to a medium+ breakpoint then we need to clear the heights (flex basis) of the list and map view
this.listView.setHeight();
this.mapView.setHeight();
}
});
window.addEventListener('resize', resize);
}
onApiSuccess(json) {
this.listView.hideLoader();
const response = JSON.parse(json);
this.properties = response.properties;
this.propertyStore.setProperties(this.properties);
this.listView.setProperties(this.properties, true);
this.mapView.setAllProperties(this.properties);
this.mapView.setProperties(this.properties);
this.filter.setValues();
this.propertyDataLoaded = true;
}
onFilter(init) {
const formData = this.filter.getFormData();
const filteredProperties = this.propertyStore.filterProperties(formData);
this.listView.setProperties(filteredProperties, init);
this.listView.createRemoveFilters(formData);
this.mapView.setProperties(filteredProperties);
}
onMapPropertyClick(propertyData) {
if (isSmallBreakpoint()) {
this.mapView.setHeight(this.component.clientHeight);
this.listView.setHeight(0);
this.mapView.resize();
}
// We're on desktop (medium+) and already in map only view
this.mapView.showPropertyCard(propertyData);
}
onMapMultiPropertyClick(properties) {
if (isSmallBreakpoint()) {
this.mapView.setHeight(this.component.clientHeight);
this.listView.setHeight(0);
this.mapView.resize();
}
this.mapView.showMultiPropertyCard(properties);
}
// When the map has been moved we want to display the visible properties in the list view
onMapMove(visibleProperties) {
if (this.propertyDataLoaded) {
this.listView.setProperties(visibleProperties);
this.filter.setSubmitText();
}
}
toggleView() {
this.splitView = !this.splitView;
this.component.classList.toggle('show-full-map-view');
setQueryParams(viewQueryStringParameterName, this.splitView);
setTimeout(() => { this.mapView.resize(); }, 1000);
}
}
export default (module) => new PropertyListing(module);
@import 'source/scss/01-settings/_import';
@import 'source/scss/02-tools/_import';
@import './property-listing-placeholders';
@import './_map-view';
@import './_list-view';
@import './_filter';
@import './_no-results';
.property-listing {
@extend %spacer-large;
display: flex;
flex-direction: column-reverse;
height: calc(100vh - 60px);
width: 100%;
@include breakpoint(medium) {
flex-direction: row;
overflow: hidden;
position: relative;
}
&__toggle-button {
background-color: $color-white;
border-radius: rem(8);
left: rem(15);
padding: rem(10) rem(10) rem(10) rem(30);
position: absolute;
top: rem(15);
z-index: 10;
&::before {
background-image: url('data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 18L15 12L9 6" stroke="%230054A6" stroke-width="2"/></svg>');
background-repeat: no-repeat;
background-size: rem(24) rem(24);
content: '';
height: rem(30);
left: rem(17);
position: absolute;
top: rem(22);
transform: translate(-50%, -50%);
width: rem(24);
}
&:hover {
&::before {
background-image: url('data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 18L15 12L9 6" stroke="%232AACE2" stroke-width="2"/></svg>');
}
}
}
}
No notes defined.