1var woningGrid; 2var isLoading = false; 3var hasMoreData = true; 4var page = 1; 5var loadingType = 'load'; 6// hardcoded for now. Can be dynamic based on user selection 7var searchType = 'koop'; 8var isMapMode = false; 9var leafletMap = null; 10 11var geoData = []; 12var markers = {}; 13 14var markerCluster = L.markerClusterGroup({ 15 removeOutsideVisibleBounds: true, 16 spiderfyOnMaxZoom: false, 17 showCoverageOnHover: false, 18 zoomToBoundsOnClick: true, 19 animate: false, 20 animateAddingMarkers: false, 21 maxClusterRadius: 80, 22}); 23 24/** 25 * Default model for loading or filtering properties 26 */ 27var loadPropertiesData = { 28 'action': 'load_properties', 29 'http_referer': window.location.href, 30 'args': { 31 'page': page, 32 'filters': [ 33 { 34 'key': 'overdracht', 35 'action': 'BETWEEN_AND', 36 'node': (searchType == 'koop') ? 'koopprijs' : 'huurprijs', 37 'value': { 38 'from': 0, 39 'to': 10000000 40 } 41 }, 42 { 43 'key': 'overdracht', 44 'node': 'status', 45 'action': '=', 46 'value': ['BESCHIKBAAR','VERKOCHT_ONDER_VOORBEHOUD'] 47 } 48 ], 49 'sortby': { 50 'overdracht': { 51 'node': 'koopprijs', 52 'order': 'DESC', 53 'type': 'number' 54 }, 55 } 56 }, 57}; 58 59document.addEventListener('DOMContentLoaded', () => { 60 console.log('DOM fully loaded and parsed'); 61 62 const menu = document.querySelector('.header-custom'); 63 let lastScrollY = window.scrollY; 64 window.addEventListener('scroll', () => { 65 const y = window.scrollY; 66 const treshhold = 8; 67 const topGuard = 50; 68 69 if (y <= topGuard) { 70 menu.classList.remove('hide'); 71 lastScrollY = y; 72 return; 73 } 74 75 if (window.scrollY > lastScrollY) { 76 // scrolling down 77 menu.classList.add('hide'); 78 menu.parentElement.style.boxShadow = 'unset'; 79 } else { 80 // scrolling up 81 menu.classList.remove('hide'); 82 } 83 84 lastScrollY = window.scrollY; 85 }); 86 87 // Ensure we are on the correct page 88 const initPage = document.querySelector('body.page-id-129950'); 89 if (!initPage) { 90 console.log('Not the properties page, exiting script.'); 91 return; 92 } 93 94 setPriceSteps(jQuery); 95 filterPropertiesByOrder(jQuery); 96 97 woningGrid = document.querySelector('.woning-grid'); 98 if (woningGrid) { 99 loadProperties(jQuery, 1); 100 } 101 102 const search = document.querySelector('.apply-search'); 103 const propertiesContainer = document.querySelector('.properties-container'); 104 search && search.addEventListener('click', function() { 105 prepareFilters(jQuery, 1); 106 if (propertiesContainer) { 107 const y = propertiesContainer.getBoundingClientRect().top + window.pageYOffset; 108 const scroller = document.scrollingElement || document.documentElement; 109 110 scroller.scrollTo?.({ top: y, behavior: 'smooth' }) ?? (scroller.scrollTop = y); 111 document.body.scrollTop = y; 112 } 113 }); 114 115 /** 116 * Handle AJAX errors globally. 117 * Generic error handler for AJAX requests. Memory safe. 118 */ 119 jQuery(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError) { 120 switch(jqXHR.status) { 121 case 404: 122 jQuery(woningGrid).html(JSON.parse(jqXHR.responseText)); 123 break; 124 } 125 }); 126 127 const viewMapButton = document.getElementById('view-map'); 128 const viewGridButton = document.getElementById('view-grid'); 129 //const propertiesContainer = document.querySelector('.properties-container'); 130 131 /** 132 * Toggle Display mode 133 */ 134 viewMapButton && propertiesContainer && viewMapButton.addEventListener('click', function() { 135 isMapMode = !isMapMode; 136 if (isMapMode) { 137 // No need to keep observing when in map mode 138 observer.disconnect(); 139 140 propertiesContainer.innerHTML = `<div id="map"></div>`; 141 initMap();
142 prepareFilters(jQuery, 1); 143 144 viewMapButton.classList.toggle('_hidden'); 145 viewGridButton.classList.toggle('_hidden'); 146 } 147 }); 148 149 viewGridButton && viewGridButton.addEventListener('click', function() { 150 isMapMode = false; 151 console.log('List Mode'); 152 propertiesContainer.innerHTML = `<div class="woning-grid"></div>`; 153 woningGrid = document.querySelector('.woning-grid'); 154 if (! woningGrid) return; 155 prepareFilters(jQuery, 1); 156 initPropertiesContainerSwiper(); 157 158 viewMapButton.classList.toggle('_hidden'); 159 viewGridButton.classList.toggle('_hidden'); 160 }); 161}); 162 163function initPropertiesObserver() { 164 const gridContainer = document.querySelector('.woning-grid'); 165 if (gridContainer) { 166 console.log('Init Properties Observer'); 167 propertiesObserver.observe(gridContainer, { 168 childList: true, 169 }); 170 } 171} 172 173const propertiesObserver = new MutationObserver((mutations) => { 174 mutations.forEach((mutation) => { 175 if (mutation.type !== "childList" || mutation.addedNodes.length === 0) return; 176 mutation.addedNodes.forEach((node) => { 177 if (!(node instanceof HTMLElement)) return; 178 if (node.classList.contains("woningblokv2")) { 179 // Attach handler to swiper buttons specifically - let them work 180 const swiperButtons = node.querySelectorAll('.archive-swiper-button-prev, .archive-swiper-button-next'); 181 swiperButtons.forEach((button) => { 182 button.addEventListener('click', (e) => { 183 console.log('Swiper button clicked'); 184 e.stopPropagation(); 185 }); 186 }); 187 188 node.addEventListener('mouseover', () => { 189 console.log('Property block mouseover'); 190 swiperButtons.forEach((button) => { 191 button.style.display = 'flex'; 192 }); 193 }); 194 195 node.addEventListener('mouseleave', () => { 196 console.log('Property block mouseleave'); 197 swiperButtons.forEach((button) => { 198 button.style.display = 'none'; 199 }); 200 }); 201 } 202 }); 203 }); 204}); 205 206/** 207 * Initialize Intersection Observer for infinite scrolling 208 * Only initializes if not in map mode and if the scroller element exists. 209 */ 210function initObserver() { 211 const scroller = document.getElementById('scroller'); 212 if (!isMapMode && scroller) { 213 console.log('Init Observer'); 214 observer.observe(scroller); 215 } 216} 217 218/** 219 * Intersection Observer for lazy loading properties on scroll 220 */ 221const observer = new IntersectionObserver(entries => { 222 entries.forEach(entry => { 223 const intersecting = entry.isIntersecting; 224 if (intersecting && !isLoading && hasMoreData) { 225 isLoading = true; 226 setTimeout(function() { 227 (page == 1) ? page = 2 : page ++; 228 (loadingType == 'load') ? loadProperties(jQuery, page, true) : prepareFilters(jQuery, page, true); 229 }, 200); 230 } 231 }); 232}); 233 234/** 235 * Set the price steps for the property filters 236 * @param {jQuery} $ 237 * @param {string} search_type = 'koop' 238 * @deprecated 239 * @returns 240 */ 241function setPriceSteps($, search_type = 'koop') { 242 if (search_type == undefined) { 243 return; 244 } 245 246 let max_index = 0; 247 let step = 0; 248 249 (search_type == 'huur') ? max_index = 100 : max_index = 50; 250 (search_type == 'koop') ? step = 50000 : step = 100; 251 252 const data = { 253 'action': 'the_price_steps', 254 'data': { 255 'start_index': 0, 256 'max_index': max_index, 257 'step': step, 258 'search_type': search_type 259 } 260 }; 261 262 $.post(ajax_object.ajax_url, data, function (data, status, xhr) { 263 $('#property-min-price').html(data); 264 $('#property-max-price').html(data); 265 266 let first_val = $('#property-min-price option:first-child').val(); 267 $('#property-min-price').val(first_val); 268 269 let last_val = $('#property-max-price option:last-child').val(); 270 $('#property-max-price').val(last_val); 271 272 //When the price steps are set, we can load the properties 273 prepareFilters($, 1); 274 });
275} 276 277/** 278 * Filter properties by order 279 * @param {jQuery} $ 280 */ 281function filterPropertiesByOrder($) { 282 loadingType = 'filter'; 283 284 const sortMenuTrigger = document.querySelector('#_sort'); 285 const orderMenu = document.querySelector('.order-menu'); 286 287 if (sortMenuTrigger && orderMenu) { 288 sortMenuTrigger.addEventListener('click', () => { 289 orderMenu.classList.toggle('_hidden'); 290 }); 291 292 orderMenu.addEventListener('click', (menu) => { 293 if (menu.target.matches('div[order]') && !menu.target.classList.contains('selected')) { 294 document.querySelector('div[order].selected')?.classList.remove('selected'); 295 296 const target = menu.target; 297 const key = target.getAttribute('key'); 298 const node = target.getAttribute('node'); 299 const order = target.getAttribute('order'); 300 301 loadPropertiesData['args']['sortby'] = { 302 [key]: { 303 node: node, 304 order: order, 305 type: 'number' 306 } 307 }; 308 309 console.log('Sorting by: ', loadPropertiesData['args']['sortby']); 310 311 filterProperties(jQuery, 1); 312 313 sortMenuTrigger.querySelector('span').innerHTML = target.innerHTML; 314 orderMenu.classList.add('_hidden'); 315 target.classList.add('selected'); 316 } 317 }); 318 } 319} 320 321/** 322 * Prepare filters for property search. 323 * Modifies the global loadPropertiesData object based on selected filters. 324 * @param {jQuery} $ 325 * @param {number} page_number 326 */ 327function prepareFilters($, page_number) { 328 console.log('Prepare Filters'); 329 330 // Normalize filters 331 loadPropertiesData.args.filters = []; 332 333 page = page_number; 334 loadPropertiesData['args']['page'] = page_number; 335 // The method we use to load properties depending on the mode 336 (isMapMode) ? loadPropertiesData['action'] = 'load_geoData_properties' : loadPropertiesData['action'] = 'load_properties'; 337 338 // Debug info 339 console.log(loadPropertiesData); 340 console.log('Map mode: ', isMapMode); 341 342 const minPrice = parseInt(jQuery('#property-min-price').find(':selected').attr('value')); 343 const maxPrice = parseInt(jQuery('#property-max-price').find(':selected').attr('value')); 344 if (minPrice > 0 && maxPrice > 0 && minPrice <= maxPrice) { 345 loadPropertiesData['args']['filters'].push({ 346 key: 'overdracht', 347 action: 'BETWEEN_AND', 348 node: (searchType == 'koop') ? 'koopprijs' : 'huurprijs', 349 value: { 350 from: minPrice, 351 to: maxPrice 352 }, 353 type: 'number' 354 }); 355 } 356 357 let overdrachtStatus = $('#property-type-select').find(':selected').attr('value'); 358 if (overdrachtStatus != 'all') { 359 const overdrachtStatusValue = overdrachtStatus.includes(',') ? overdrachtStatus.split(',') : overdrachtStatus; 360 loadPropertiesData['args']['filters'].push({ 361 key: 'overdracht', 362 node: 'status', 363 action: '=', 364 value: overdrachtStatusValue 365 }); 366 } 367 368 let searchValue = $('#alfred-ajax-search').val(); 369 if (searchValue && searchValue.trim() !== '') { 370 loadPropertiesData['args']['filters'].push({ 371 key: 'adres', 372 node: 'ANY', 373 action: 'CONTAINS', 374 value: searchValue, 375 type: 'string' 376 }); 377 } 378 379 if (!isMapMode) { 380 filterProperties($, page_number, true); 381 } else { 382 getFilteredGeoProperties(() => { 383 prepareMarkers(); 384 }); 385 } 386}; 387 388/** 389 * Loads properties on initial page load, with propertiesData as is. 390 * @param {jQuery} $ 391 * @param {boolean} scrolling 392 */ 393function loadProperties($, scrolling = false) { 394 loadingType = 'load'; 395 console.log('Load Properties'); 396 397 $.post(ajax_object.ajax_url, loadPropertiesData, (data, status, xhr) => { 398 if (scrolling) { 399 if ( !data || data.trim() === '' || $(data).find('.no-results').length ) { 400 hasMoreData = false; 401 } else { 402 $(woningGrid).append(data); 403 } 404 } else { 405 $(woningGrid).html(data); 406 } 407 408 isLoading = false;
409 initObserver(); 410 initPropertiesObserver(); 411 }); 412} 413 414/** 415 * Fetches filtered properties using the (modified) loadPropertiesData object. 416 * Depending on the scrolling behavior, it either appends or replaces the content. 417 * @param {jQuery} $ 418 * @param {number} page_number 419 * @param {boolean} scrolling 420 */ 421function filterProperties($, page_number, scrolling = false) { 422 loadingType = 'filter'; 423 424 if (scrolling) loadPropertiesData['args']['page'] = page_number; 425 426 $.post(ajax_object.ajax_url, loadPropertiesData, function (result, status, xhr) { 427 if (scrolling && page_number > 1) { 428 if (!result || result.trim() === '' || $(result).find('.no-results').length) { 429 hasMoreData = false; 430 } else { 431 $(woningGrid).append(result); 432 } 433 } else { 434 $(woningGrid).html(result); 435 } 436 437 isLoading = false; 438 initObserver(); 439 }); 440} 441 442/** 443 * Initialize the Leaflet map. 444 * This function sets up the map container and initializes the map. 445 * Handles the display of properties on the map. 446 */ 447function initMap() { 448 const mapContainer = document.querySelector('#map'); 449 if (!mapContainer) { 450 console.log('Map container not found, exiting map initialization.'); 451 return; 452 } 453 454 // Initialize the map 455 leafletMap = L.map(mapContainer, { 456 center: [52.7842085, 6.065566], 457 zoom: 9.25 458 }); 459 460 const cartoApiKey = (typeof tpMapConfig !== 'undefined' && tpMapConfig.cartoApiKey) ? tpMapConfig.cartoApiKey : ''; 461 const tileUrl = cartoApiKey 462 ? `https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png?key=${cartoApiKey}` 463 : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; 464 L.tileLayer(tileUrl, { 465 attribution: cartoApiKey 466 ? '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>' 467 : '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', 468 }).addTo(leafletMap); 469 470 /*getInitialGeoData(() => { 471 prepareMarkers(); 472 });*/ 473 474 markerCluster.on('mouseover', (markerEvent) => { 475 displayPropertyPopupBox(markerEvent); 476 }); 477 478 /*==================================================================*/ 479 /* DEPRECATED - No need to load markers based on bounding box, */ 480 /* MarkerClustering handles both visibility and spidering. */ 481 /*==================================================================*/ 482 483 /*getInitialGeoData(() => { 484 displayPropertiesByBoundingBox(); 485 }); 486 487 leafletMap.on('moveend zoomend', () => { 488 displayPropertiesByBoundingBox(); 489 });*/ 490} 491 492/** 493 * Fetch initial geographic data for properties. 494 * @param {Function} callback 495 * @deprecated Use getFilteredGeoProperties instead. 496 */ 497function getInitialGeoData(callback) { 498 jQuery.ajax({ 499 url: ajax_object.ajax_url, 500 method: 'POST', 501 data: { 502 action: 'load_geoData_properties', 503 }, 504 success: (response) => { 505 let data = typeof response === 'string' ? JSON.parse(response) : response; 506 if (data && data.length) { 507 geoData = data; 508 if (typeof callback === 'function') { 509 callback(); 510 } 511 } 512 }, 513 error: (xhr, status, error) => { 514 console.error('Error loading properties:', error); 515 } 516 }); 517} 518 519/** 520 * Fetch geographic data for properties based on current filters. 521 * This function is called when filters are applied. 522 * @param {Function} callback 523 */ 524function getFilteredGeoProperties(callback) { 525 geoData = []; 526 markers = {}; 527 markerCluster.clearLayers(); 528 529 console.log('Fetch filtered geo properties'); 530 loadPropertiesData['action'] = 'load_geoData_properties'; 531 jQuery.post(ajax_object.ajax_url, loadPropertiesData, (result) => { 532 let data = typeof result === 'string' ? JSON.parse(result) : result; 533 if (data && data.length) { 534 geoData = data; 535 if (typeof callback === 'function') { 536 callback(); 537 } 538 } 539 });
540} 541 542/** 543 * Fetch geographic data for properties based on current filters. 544 * This function is called when filters are applied. 545 * @param {Array} geoData 546 */ 547function prepareMarkers() { 548 Array.from(geoData).forEach(property => { 549 if (property.geoData) { 550 const { lat, lon } = property.geoData; 551 var marker = L.marker([lat, lon]); 552 marker.wordpressId = property.wordpress_id; 553 marker.realworksId = property.realworks_id; 554 marker.html = property.html; 555 markerCluster.addLayer(marker); 556 } 557 }); 558 559 leafletMap.addLayer(markerCluster); 560} 561 562/** 563 * Display a popup box with property details. 564 * @param {string} htmlContent - The HTML content to display in the popup. 565 * @param {Object} markerEvent - The marker event containing location data. 566 */ 567function displayPropertyPopupBox(markerEvent) { 568 L.popup({ 569 maxWidth: 400, 570 minWidth: 200, 571 maxHeight: 600, 572 closeButton: true, 573 autoClose: true, 574 }) 575 .setLatLng(markerEvent.latlng) 576 .setContent(markerEvent.sourceTarget.html) 577 .openOn(leafletMap); 578 579 console.log(markerEvent.sourceTarget.html); 580 581 setTimeout(() => { 582 const el = document.querySelector('.properties-archive-swiper'); 583 if (!el) return; 584 const paginationEl = el.querySelector('.pas-swiper-pagination'); 585 new Swiper(el, { 586 slidesPerView: 1, 587 pagination: paginationEl ? { el: paginationEl } : {}, 588 }); 589 }, 50); 590 591 var popupContainer = document.querySelector('.leaflet-popup'); 592 if (popupContainer) { 593 popupContainer.addEventListener('mouseleave', function () { 594 leafletMap.closePopup(); 595 }, { once: true }); 596 } 597} 598 599/** 600 * Gets the current bounding box of the map. 601 * @deprecated 602 */ 603function displayPropertiesByBoundingBox() { 604 if (!geoData || geoData.length === 0) { 605 return; 606 } 607 608 const mapBounds = leafletMap.getBounds(); 609 const northEast = mapBounds.getNorthEast();
610 const southWest = mapBounds.getSouthWest(); 611 612 const boundingBox = { 613 minLat: southWest.lat, 614 minLng: southWest.lng, 615 maxLat: northEast.lat, 616 maxLng: northEast.lng 617 }; 618 619 addDefaultMarker(boundingBox); 620} 621 622/** 623 * Add a default marker to the map at the specified latitude and longitude. 624 * @param {Object} mapBoundingBox - The current bounding box of the map. 625 * @deprecated 626 */ 627function addDefaultMarker(mapBoundingBox) { 628 if (!leafletMap) return; 629 630 Object.values(markers).forEach(marker => marker.remove()); 631 markers = {}; 632 633 Array.from(geoData).forEach(property => { 634 if (property.geoData) { 635 const { lat, lon } = property.geoData; 636 if (lat >= mapBoundingBox.minLat && lat <= mapBoundingBox.maxLat && 637 lon >= mapBoundingBox.minLng && lon <= mapBoundingBox.maxLng) { 638 const key = `${lat},${lon}`; 639 if (!markers[key]) { 640 const marker = L.marker([lat, lon]); 641 markers[key] = marker; 642 } 643 } 644 } 645 }); 646 647 clusterMarkers(); 648} 649 650/** 651 * Cluster and display markers on the map. 652 * @deprecated 653 */ 654function clusterMarkers() { 655 markerCluster.clearLayers(); 656 Object.values(markers).forEach(marker => { 657 if (!markerCluster.hasLayer(marker)) { 658 markerCluster.addLayer(marker); 659 } 660 }); 661 662 leafletMap.addLayer(markerCluster); 663}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.