/** * @namespace WPGMZA * @module AtlasMajorLivePreviewPro * @requires WPGMZA.AtlasMajorLivePreview * * Pro extension for the Atlas Major live preview. * Adds: Directions Box, Marker Listings, Category Filter, Category Legends. * * Uses the same JS classes as the frontend — no code duplication. * Components are initialized by calling the real Pro init methods after * placing the correct DOM elements into the preview. */ jQuery(function($) { if(!document.querySelector('.wpgmza-atlas-major')) return; /* PHP class name for each marker listing style */ var LISTING_STYLE_CLASS = { 1: 'WPGMZA\\MarkerListing\\BasicTable', 2: 'WPGMZA\\MarkerListing\\AdvancedTable', 3: 'WPGMZA\\MarkerListing\\Carousel', 4: 'WPGMZA\\MarkerListing\\BasicList', 6: 'WPGMZA\\MarkerListing\\Modern', 7: 'WPGMZA\\MarkerListing\\Grid', 8: 'WPGMZA\\MarkerListing\\Panel', 9: 'WPGMZA\\MarkerListing\\CategoryGroupedList', 10: 'WPGMZA\\MarkerListing\\CategoryGroupedTabs' }; /* CSS classes that PHP adds to the listing element per style */ var LISTING_STYLE_EXTRA_CLASSES = { 3: 'owl-carousel owl-theme wpgmza_marker_carousel', 7: 'wpgmza-marker-grid' }; function initProLivePreview(){ var lp = WPGMZA.atlasMajorLivePreview; if(!lp){ return; } var map = lp.getMap(); if(!map){ return; } var templates = lp.getTemplatesContainer(); var mapId = map.id; /* ======================================================== INJECT PRO COMPONENT TEMPLATES ======================================================== */ /* Directions Box — real PHP template injected by class.pro-map-edit-page.php */ /* Build the ajax-parameters JSON for the marker listing. Starts with the required map_id and layers on the live-preview override params (order_markers_by/choice) so the server sorts per the live form values instead of the saved map record. Server reads the overrides in class.marker-listing.php filterOrderBy/Direction and class.advanced-table.php getOrderBy/getOrderDirection. */ function buildMarkerListingAjaxParams(){ var params = {map_id: String(mapId)}; var orderBy = $('select[name="order_markers_by"]').val(); var orderDir = $('select[name="order_markers_choice"]').val(); if(orderBy) params.override_order_by = orderBy; if(orderDir) params.override_order_dir = orderDir; /* Always force-render the image-placeholder element into the preview's listing rows. Its visibility is then toggled live via the .wpgmza-image-placeholders-off class (see syncImagePlaceholderPreview) when the "Image Placeholders" dropdown changes, with no re-fetch needed. */ params.image_placeholder_preview = '1'; return params; } /* Marker Listing — container with AJAX attributes. Rebuilt each render because the listing style (and thus PHP class) can change. */ function buildMarkerListingTemplate(){ templates.find('[data-preview-component="marker-listing"]').remove(); var listStyle = parseInt($('input[name="wpgmza_listmarkers_by"]:checked').val()) || 0; var phpClass = LISTING_STYLE_CLASS[listStyle] || 'WPGMZA\\MarkerListing\\BasicList'; var extraClasses = LISTING_STYLE_EXTRA_CLASSES[listStyle] || ''; var ajaxParams = buildMarkerListingAjaxParams(); /* Advanced Table: use PHP-rendered table if available, fallback to JS-built */ if(listStyle === 2){ var advTableSrc = templates.find('[data-preview-advanced-table]'); if(advTableSrc.length && advTableSrc.children().length){ var mlDiv = $('
'); var clone = advTableSrc.children().first().clone(); /* Overwrite the PHP-rendered ajax-parameters so the override params are included on the live-preview clone. */ clone.find('[data-wpgmza-ajax-parameters]').addBack('[data-wpgmza-ajax-parameters]') .attr('data-wpgmza-ajax-parameters', JSON.stringify(ajaxParams)); mlDiv.append(clone); templates.append(mlDiv); return; } /* Fallback: build a basic table structure */ var mlDiv = $('
'); var mlInner = $('
'); mlInner.attr('data-wpgmza-marker-listing', 'true'); mlInner.attr('id', 'wpgmza_marker_list_' + mapId); mlInner.attr('data-map-id', mapId); mlInner.attr('data-wpgmza-php-class', phpClass); mlInner.attr('data-wpgmza-ajax-parameters', JSON.stringify(ajaxParams)); mlInner.attr('data-wpgmza-rest-api-route', '/datatables/'); mlInner.attr('data-wpgmza-datatable', 'true'); mlInner.attr('data-wpgmza-table', mapId); mlInner.addClass('wpgmza_marker_list_class wpgmza_marker_holder'); var tableHtml = ''; tableHtml += ''; tableHtml += ''; tableHtml += ''; tableHtml += ''; tableHtml += ''; tableHtml += ''; tableHtml += ''; tableHtml += '
' + (WPGMZA.localized_strings.title || 'Title') + '' + (WPGMZA.localized_strings.category || 'Category') + '' + (WPGMZA.localized_strings.address || 'Address') + '' + (WPGMZA.localized_strings.description || 'Description') + '
'; mlInner.append(tableHtml); mlDiv.append(mlInner); templates.append(mlDiv); return; } var mlDiv = $('
'); var mlInner = $('
'); mlInner.attr('data-wpgmza-marker-listing', 'true'); mlInner.attr('id', 'wpgmza_marker_list_' + mapId); mlInner.attr('data-map-id', mapId); mlInner.attr('data-wpgmza-php-class', phpClass); mlInner.attr('data-wpgmza-ajax-parameters', JSON.stringify(ajaxParams)); mlInner.attr('data-wpgmza-rest-api-route', '/marker-listing/'); /* Add the base class that PHP always adds + any style-specific classes */ mlInner.addClass('wpgmza_marker_list_class'); if(extraClasses){ mlInner.addClass(extraClasses); } mlDiv.append(mlInner); templates.append(mlDiv); } buildMarkerListingTemplate(); /* Rebuild template when listing style changes */ $(document.body).on('change', 'input[name="wpgmza_listmarkers_by"]', function(){ buildMarkerListingTemplate(); }); /* Category Filter — PHP-rendered template injected by class.pro-map-edit-page.php using CategoryFilterWidget. Respects the global filterbycat_type setting (dropdown vs checkboxes). */ /* Category Legends — built from live category tree data */ if(!templates.find('[data-preview-component="category-legends"]').length){ var legendsHtml = buildCategoryLegendsHtml(mapId); templates.append( '
' + legendsHtml + '
' ); } /* MarkerListing looks up `.wpgmza-marker-listing-category-filter[data-map-id="X"]` globally. The PHP-rendered filter-widget template also has data-map-id, so it would match alongside any live clone and cause duplicate-handler wiring. Strip data-map-id from the template source once; each onInit re-adds it to the clone. */ templates.find('[data-preview-component="category-filter"] .wpgmza-marker-listing-category-filter').removeAttr('data-map-id'); /* ======================================================== REGISTER PRO COMPONENTS ======================================================== */ lp.registerComponent('directions-box', { enabledCheck: function(){ return $('input[name="directions_enabled"]').is(':checked'); }, anchorField: 'select[name="directions_box_component_anchor"]', defaultAnchor: 1, proOnly: true, onInit: function(map, clone){ map.directionsEnabled = true; map.settings.directions_enabled = "1"; if(map.directionsBox) map.directionsBox = null; /* Set the ID that DirectionsBox constructor looks for */ clone.find('.wpgmza-directions-box').addBack('.wpgmza-directions-box') .attr('id', 'wpgmaps_directions_edit_' + map.id); /* For left/right anchors, wrap in viewport-grouping structure */ var anchor = lp.getComponentAnchor('directions-box'); var anchorName = {0:'top',1:'left',2:'center',3:'right',4:'bottom',5:'top_left',6:'top_right',7:'bottom_left',8:'bottom_right'}[anchor] || 'left'; if(anchorName === 'left' || anchorName === 'right'){ var mapEl = $(map.element); var stack = mapEl.children('.wpgmza-inner-stack.' + anchorName); /* Remove the directly-placed clone from base render */ clone.detach(); if(!stack.length){ stack = $('
'); mapEl.append(stack); WPGMZA.AtlasMajorLivePreview.registerInnerStack(stack); } /* Check if open by default (dbox setting: 1=No, 6=Yes). Also support live form state — the dropdown may change before map.settings is synced */ var dboxVal = $('select[name="dbox"]').val() || map.settings.dbox; var openByDefault = (!map.settings.dbox_open_external && parseInt(dboxVal) === WPGMZA.DirectionsBox.OPEN_BY_DEFAULT); var visibleClass = openByDefault ? ' visible' : ''; stack.addClass('viewport-grouping'); if(openByDefault) stack.addClass('expanded'); var grouping = $('
'); var groupingItem = $('
'); var groupingHandle = $('
'); groupingItem.append(clone); grouping.append(groupingItem); stack.append(grouping); stack.append(groupingHandle); stack.css('display', 'flex'); if(map.viewportGroupings){ /* Unbind any existing click handlers on grouping-handles before re-init, or initGroups stacks duplicate handlers */ $(map.element).find('.grouping-handle').off('click'); map.viewportGroupings.initGroups(); } } /* Set IDs that the DirectionsBox JS expects — the PHP DirectionsBox class normally sets these, but the live preview template is loaded via raw loadPHPFile without going through the class constructor */ clone.find('.wpgmza-directions-notifications').attr('id', 'wpgmaps_directions_notification_' + map.id); clone.find('.wpgmza-directions-output-panel').attr('id', 'directions_panel_' + map.id); var origAdmin = WPGMZA.is_admin; WPGMZA.is_admin = "0"; try { WPGMZA.ProMap.prototype.initDirectionsBox.call(map, clone[0]); } catch(e){ console.warn('LP-PRO: initDirectionsBox failed', e); } WPGMZA.is_admin = origAdmin; }, onTeardown: function(map){ if(map.directionsBox){ map.directionsBox = null; } map.directionsEnabled = false; /* Remove the grouping wrapper */ $('.wpgmza-lp-directions-grouping').closest('.wpgmza-inner-stack').each(function(){ $(this).find('.wpgmza-lp-directions-grouping, .wpgmza-lp-directions-grouping ~ .grouping-handle').remove(); $(this).removeClass('viewport-grouping expanded'); }); /* Remove orphaned autocomplete list divs from body */ $('body > .wpgmza-internal-autocomplete-list').remove(); } }); /* ======================================================== CENTRALIZED SETTINGS SYNC + REBUILD Fields are classified as "rebuild" (structural changes requiring full teardown/re-render) or "light" (value synced to map.settings only — existing sync handlers in the base live preview handle the UI update). ======================================================== */ var REBUILD_SELECTORS = [ 'input[name="directions_enabled"]', 'select[name="dbox"]', 'input[name="wpgmza_listmarkers_by"]', 'input[name="filterbycat"]', 'input[name="category_legends_enabled"]', 'input.wpgmza-enable-custom-field-filter', 'input[name="wpgmza_iw_type"]', 'select[name="iw_anchor_panel_card"]', 'select[name="dataTable_pagination_style"]', 'input[name="remove_search_box_datables"]', 'select[name="category_legends_style"]', 'input[name="category_legends_label_string"]', 'input[name="store_locator_nearby_searches"]', 'input[name="marker_share_links"]', 'input[name="wpgmza_store_locator_use_their_location"]', 'input[name="store_locator_category"]', 'input[name="wpgmza_store_locator_hide_before_search"]', '.wpgmza-anchor-control', /* Marker Listing — General */ 'input[name="marker_listing_component_auto_open"]', /* Ordering — rebuild updates the AJAX params so the server sorts per live values */ 'select[name="order_markers_by"]', 'select[name="order_markers_choice"]', /* Marker Listing — Filtering */ 'input[name="category_filter_label_string"]', /* Marker Listing — DataTables text strings (rebuild reinitialises the DataTable with new language) */ 'input[name="datatable_no_result_message"]', 'input[name="datatable_search_string"]', 'input[name="datatable_result_start"]', 'input[name="datatable_result_of"]', 'input[name="datatable_result_to"]', 'input[name="datatable_result_total"]', 'input[name="datatable_result_show"]', 'input[name="datatable_result_entries"]' ]; function syncSettingsFromForm(){ /* Form ID is 'wpgmaps_options', name attribute is 'wpgmza_map_form' */ $('#wpgmaps_options').find('input, select, textarea').each(function(){ var name = $(this).attr('name'); if(!name) return; if($(this).is(':radio') && !$(this).is(':checked')) return; if($(this).is(':checkbox')){ /* Unchecked must be '' (falsy) — NOT '0'. The string * '0' is truthy in JS (`if('0')` === true), so any * downstream code that does `if(map.settings.foo)` * treats unchecked checkboxes as enabled. Notably * pro-info-window.js:509 / 513 do exactly that for * store_locator_nearby_searches and marker_share_links, * which is why "Find Nearby" / "Share Location" links * appeared in live preview info windows even when * the user had unchecked the corresponding settings * — frontend was correct because PHP localizes * unchecked as int 0 (falsy in JS). */ map.settings[name] = $(this).is(':checked') ? '1' : ''; } else { map.settings[name] = $(this).val(); } }); /* Sync convenience properties */ map.directionsEnabled = $('input[name="directions_enabled"]').is(':checked'); } var rebuildTimer = null; function debouncedRebuild(){ clearTimeout(rebuildTimer); rebuildTimer = setTimeout(function(){ syncSettingsFromForm(); rebuildLegendsTemplate(); buildMarkerListingTemplate(); lp.render(); }, 250); } /* Bind rebuild triggers */ $(document.body).on('change input', REBUILD_SELECTORS.join(','), debouncedRebuild); /* ------------------------------------------------------------------ Image Placeholders (per-map override) — live toggle ------------------------------------------------------------------ The placeholder element is force-rendered into both the info-window clone templates and the listing rows (see class.pro-map-edit-page.php and buildMarkerListingAjaxParams), so we only need to show/hide it. Resolve the effective value (per-map dropdown overrides the global setting) and toggle the .wpgmza-image-placeholders-off class on the editor root — the CSS hides every placeholder beneath it. No rebuild/re-fetch. */ function imagePlaceholdersEnabled(){ var override = $('select[name="image_placeholder"]').val(); if(override === 'enabled') return true; if(override === 'disabled') return false; /* Default — follow the global setting. */ var g = (WPGMZA.settings && typeof WPGMZA.settings.image_placeholder_enabled !== 'undefined') ? WPGMZA.settings.image_placeholder_enabled : false; return g === true || g === 1 || g === '1' || g === 'yes'; } function syncImagePlaceholderPreview(){ $('.wpgmza-editor').toggleClass('wpgmza-image-placeholders-off', !imagePlaceholdersEnabled()); } $(document.body).on('change', 'select[name="image_placeholder"]', syncImagePlaceholderPreview); /* Initial state from the loaded form value. */ syncImagePlaceholderPreview(); /* ------------------------------------------------------------------ Advanced tab — Pro/Gold controls with custom live wiring (map reset control, KML URL, marker ratings) ------------------------------------------------------------------ */ /* Remove any existing reset-map control DOM across all engines */ function removeResetMapControl(){ /* Google — iterate the LEFT_BOTTOM controls MVCArray */ if(map.googleMap && map.googleMap.controls && typeof google !== 'undefined' && google.maps){ var pos = google.maps.ControlPosition.LEFT_BOTTOM; var arr = map.googleMap.controls[pos]; if(arr && typeof arr.getLength === 'function'){ for(var i = arr.getLength() - 1; i >= 0; i--){ var el = arr.getAt(i); if(el && el.classList && el.classList.contains('google-maps-control-reset-map')){ arr.removeAt(i); } } } } /* Leaflet / OpenLayers — DOM removal is enough; the control wrapper is a plain element appended into the map DOM. */ $(map.element).find('.leaflet-control-reset-map, .ol-control-reset-map').each(function(){ /* Walk up to the Leaflet control container if present */ var $leafletWrap = $(this).closest('.leaflet-control'); if($leafletWrap.length){ $leafletWrap.remove(); } else { $(this).remove(); } }); } $(document.body).on('change', 'input[name="enable_map_reset_control"]', function(){ var enabled = $(this).is(':checked'); map.settings.enable_map_reset_control = enabled ? 1 : 0; if(enabled){ /* Guard against double-add if already present */ removeResetMapControl(); if(typeof map.addResetMapControl === 'function'){ map.addResetMapControl(); } } else { removeResetMapControl(); } }); /* KML/GeoRSS URL — loadKMLLayers() already handles both add and remove based on map.settings.kml. Debounce so we don't refetch on every keystroke. */ var kmlTimer = null; $(document.body).on('input change', 'input[name="kml"]', function(){ var val = $(this).val(); clearTimeout(kmlTimer); kmlTimer = setTimeout(function(){ map.settings.kml = val; if(typeof map.loadKMLLayers === 'function'){ map.loadKMLLayers(); } }, 500); }); /* Marker ratings (Gold) — read at infowindow open and markerlistingupdated events. Light sync into map.settings; next IW open / listing refresh picks up the change. */ $(document.body).on('change', 'input[name="enable_marker_ratings"]', function(){ map.settings.enable_marker_ratings = $(this).is(':checked') ? 1 : 0; }); /* ------------------------------------------------------------------ Store Locator → Style: center-point marker live update The marker is placed by the store locator after a user search (onGeocodeComplete → onFilteringComplete). These handlers update the marker in place when its settings change AFTER a search has been performed — changing the icon or animation immediately. If no search has run yet, nothing happens; the next search picks up the new settings via the lazy marker getter. Mirrors the existing destroyAndRedrawCircle pattern. ------------------------------------------------------------------ */ function destroyAndRedrawCenterMarker(){ if(!map.storeLocator) return; /* Drop cached marker so the lazy `marker` getter rebuilds it with the current icon + animation on next filter cycle. */ if(map.storeLocator._marker){ try { map.storeLocator._marker.setVisible(false); } catch(e){} try { if(map.removeMarker) map.removeMarker(map.storeLocator._marker); } catch(e){} map.storeLocator._marker = null; } /* Sync current form values into map.settings so the lazy getter builds the new marker correctly. */ map.settings.wpgmza_store_locator_bounce = $('input[name="wpgmza_store_locator_bounce"]').is(':checked') ? 1 : 0; map.settings.upload_default_sl_marker = $('.wpgmza-store-locator-marker-icon-picker-container input.wpgmza-marker-icon-url').val() || ''; map.settings.wpgmza_sl_animation = parseInt($('select[name="wpgmza_sl_animation"]').val(), 10) || 0; /* Only re-trigger the filter cycle if a search has actually been applied — otherwise there's nothing to place. */ if(map.storeLocator.state === WPGMZA.StoreLocator.STATE_APPLIED && map.markerFilter){ map.markerFilter.update({}, map.storeLocator); } } $(document.body).on('change', 'input[name="wpgmza_store_locator_bounce"]', destroyAndRedrawCenterMarker); $(document.body).on('change', 'select[name="wpgmza_sl_animation"]', destroyAndRedrawCenterMarker); $(document.body).on('change input', '.wpgmza-store-locator-marker-icon-picker-container input.wpgmza-marker-icon-url', destroyAndRedrawCenterMarker); /* ------------------------------------------------------------------ Store Locator → Enable Title Search (keyword container). The .wpgmza-keywords container is injected into the store locator template by PHP at render time (class.pro-store-locator.php:47), based on the SAVED `store_locator_name_search` value. Since the live-preview template is stale once the page has loaded, toggling the setting in the editor wouldn't add or remove the input on its own. We inject/remove it client-side so the live preview reflects the live toggle state. Re-applied after each rebuild via the `wpgmza_live_preview_rendered` event. ------------------------------------------------------------------ */ function applyKeywordSearchVisibility(){ var enabled = $('input[name="store_locator_name_search"]').is(':checked'); map.settings.store_locator_name_search = enabled ? '1' : '0'; $('.wpgmza-live-preview-component.wpgmza-store-locator, .wpgmza-live-preview-component .wpgmza-store-locator').each(function(){ var $sl = $(this); var $existing = $sl.children('.wpgmza-keywords'); if(enabled){ if($existing.length) return; /* Build the same HTML the PHP fragment produces. Keyword id + label "for" follow the `nameInput_{mapId}` pattern from class.pro-store-locator.php:204-205. */ var labelText = $('input[name="store_locator_name_string"]').val() || ''; /* "Enter a title" is already registered for translation via esc_attr_e() in the map editor template, so it resolves through the plugin's PO catalog at runtime. */ var placeholderText = labelText || 'Enter a title'; var $wrap = $( '
' + '' + '' + '
' ); $wrap.children('label').text(labelText); $wrap.children('input').attr('placeholder', placeholderText); /* Insert before the radius container if present, else at the top */ var $radius = $sl.children('.wpgmza-radius-container').first(); if($radius.length){ $radius.before($wrap); } else { $sl.prepend($wrap); } } else { $existing.remove(); } }); /* If the StoreLocator instance is already constructed, refresh its cached categoryDropdown/categoryCheckboxes — and in this case, grab the new keyword input too — so its filter-change handler binds correctly. */ if(map.storeLocator){ try { var $keywordInput = $('.wpgmza-live-preview-component.wpgmza-store-locator input.wpgmza-keywords'); map.storeLocator.keywordSearchInput = $keywordInput[0]; } catch(e){} } } $(document.body).on('change', 'input[name="store_locator_name_search"]', applyKeywordSearchVisibility); $(document.body).on('click', 'label[for="wpgmza_store_locator_name_search"]', function(){ setTimeout(applyKeywordSearchVisibility, 50); }); /* Re-apply after every rebuild so the clone reflects live state */ $(document.body).on('wpgmza_live_preview_rendered', function(){ applyKeywordSearchVisibility(); }); /* ------------------------------------------------------------------ Directions → live route updates - default_to / default_from: mirror into the directions-box inputs so the user sees the pre-fill without saving. - stroke color / weight / opacity: if a route is currently drawn, call setOptions on Google's DirectionsRenderer (or on the polyline for ORS/Route renderers) to restyle it live. - origin / destination icons: if a route is drawn, setIcon on the cached start/end markers. - fit_bounds_to_route: if toggled on while a route is active, re-fit. ------------------------------------------------------------------ */ /* Keep the directions-box input fields in sync with default_to/from. Only prefill when the input is currently empty so we don't clobber what the user is actively editing. */ function applyDefaultDirectionsAddresses(){ var defaultTo = $('input[name="default_to"]').val() || ''; var defaultFrom = $('input[name="default_from"]').val() || ''; map.settings.default_to = defaultTo; map.settings.default_from = defaultFrom; /* Find the directions box inputs inside the live preview */ var $box = $('.wpgmza-directions-box, #wpgmaps_directions_edit_' + map.id); var $toInput = $box.find('input[data-name="directions_to"], input.wpgmza-address.wpgmza-address-to, input.wpgmza-directions-to-input').first(); var $fromInput = $box.find('input[data-name="directions_from"], input.wpgmza-address.wpgmza-address-from, input.wpgmza-directions-from-input').first(); if($toInput.length && !$toInput.val()) $toInput.val(defaultTo); if($fromInput.length && !$fromInput.val()) $fromInput.val(defaultFrom); } $(document.body).on('change input', 'input[name="default_to"], input[name="default_from"]', applyDefaultDirectionsAddresses); /* Live route styling — stroke + icons + fit bounds */ function getDirectionsRenderer(){ if(!map.directionsBox) return null; return map.directionsBox.renderer || null; } function isRouteDrawn(renderer){ if(!renderer) return false; /* Google keeps directionStartMarker cached after setDirections; ORS/Route create polyline + markers. Either signals an active route. */ return !!(renderer.directionStartMarker || renderer.polyline); } function applyDirectionsPolylineOptions(){ var renderer = getDirectionsRenderer(); if(!isRouteDrawn(renderer)) return; var color = $('input[name="directions_route_stroke_color"]').val(); var weight = $('input[name="directions_route_stroke_weight"]').val(); var opacity = $('input[name="directions_route_stroke_opacity"]').val(); if(color !== undefined) map.settings.directions_route_stroke_color = color; if(weight !== undefined) map.settings.directions_route_stroke_weight = weight; if(opacity !== undefined) map.settings.directions_route_stroke_opacity = opacity; var polyOpts = {}; if(color) polyOpts.strokeColor = color; if(weight) polyOpts.strokeWeight = parseFloat(weight); if(opacity) polyOpts.strokeOpacity = parseFloat(opacity); /* Google DirectionsRenderer — restyle via setOptions({polylineOptions}) */ if(renderer.googleDirectionsDisplay && typeof renderer.googleDirectionsDisplay.setOptions === 'function'){ renderer.googleDirectionsDisplay.setOptions({polylineOptions: polyOpts}); } /* Route/ORS renderers — the polyline is a WPGMZA.Polyline that supports setOptions */ if(renderer.polyline && typeof renderer.polyline.setOptions === 'function'){ renderer.polyline.setOptions(polyOpts); } } $(document.body).on('change input', 'input[name="directions_route_stroke_color"], input[name="directions_route_stroke_weight"], input[name="directions_route_stroke_opacity"]', applyDirectionsPolylineOptions ); function applyDirectionsMarkerIcons(){ var renderer = getDirectionsRenderer(); if(!renderer) return; var originIcon = $('#directions_origin_icon_picker_container input.wpgmza-marker-icon-url').val() || ''; var destIcon = $('#directions_destination_icon_picker_container input.wpgmza-marker-icon-url').val() || ''; map.settings.directions_route_origin_icon = originIcon; map.settings.directions_route_destination_icon = destIcon; if(renderer.directionStartMarker && typeof renderer.directionStartMarker.setIcon === 'function'){ renderer.directionStartMarker.setIcon(originIcon); } if(renderer.directionEndMarker && typeof renderer.directionEndMarker.setIcon === 'function'){ renderer.directionEndMarker.setIcon(destIcon); } } $(document.body).on('change input', '#directions_origin_icon_picker_container input.wpgmza-marker-icon-url, #directions_destination_icon_picker_container input.wpgmza-marker-icon-url', applyDirectionsMarkerIcons ); /* ------------------------------------------------------------------ User Location — live wiring - show_user_location: toggle ON triggers navigator.geolocation (one-shot), drops the marker with the configured icon at the user's real lat/lng (no faking). Toggle OFF removes the marker. Uses getCurrentPosition (not watchPosition) so we have clean teardown without leaving a background watcher running during editing. - User-location icon: change destroys + re-places the marker. - enable_user_location_control: add/remove the native map control button (same pattern as the reset-map control). Note: the ProMap constructor's initUserLocationMarker is explicitly no-op'd on the admin page (is_admin guard + no-op in pro-map.js:604), so we don't double-place. ------------------------------------------------------------------ */ function removeUserLocationMarker(){ if(map.userLocationMarker){ try { if(map.removeMarker) map.removeMarker(map.userLocationMarker); } catch(e){} try { map.userLocationMarker.setVisible(false); } catch(e){} map.userLocationMarker = null; } } function placeUserLocationMarker(){ removeUserLocationMarker(); map.settings.show_user_location = $('input[name="show_user_location"]').is(':checked') ? 1 : 0; map.settings.upload_default_ul_marker = $('#wpgmza_show_user_location_conditional input.wpgmza-marker-icon-url').val() || ''; if(!map.settings.show_user_location) return; WPGMZA.getCurrentPosition(function(position){ /* Guard: user may have toggled OFF (or changed icon) while the geolocation request was in flight. Only place if still on. */ if(!map.settings.show_user_location) return; if(map.userLocationMarker) return; /* Already placed by a later call */ var icon = map.settings.upload_default_ul_marker; var options = { id: WPGMZA.guid(), animation: WPGMZA.Marker.ANIMATION_DROP, user_location: true }; if(icon && icon.length) options.icon = icon; if(map.settings.upload_default_ul_marker_retina) options.retina = true; var marker = WPGMZA.Marker.createInstance(options); marker.isFilterable = false; marker.setOptions({zIndex: 999999}); if(marker._icon) marker._icon.retina = marker.retina; marker.setPosition({ lat: position.coords.latitude, lng: position.coords.longitude }); map.addMarker(marker); map.userLocationMarker = marker; }, function(err){ /* Permission denied or unavailable — no marker. User will see no visual change; browser console carries the reason. */ }); } $(document.body).on('change', 'input[name="show_user_location"]', function(){ var enabled = $(this).is(':checked'); map.settings.show_user_location = enabled ? 1 : 0; if(enabled) placeUserLocationMarker(); else removeUserLocationMarker(); }); $(document.body).on('change input', '#wpgmza_show_user_location_conditional input.wpgmza-marker-icon-url', function(){ if($('input[name="show_user_location"]').is(':checked')){ placeUserLocationMarker(); } } ); /* Initial state on load — if the saved map had show_user_location on, place the marker (requests browser geolocation the same way the frontend would). */ if($('input[name="show_user_location"]').is(':checked')){ placeUserLocationMarker(); } /* Enable user-location map control — add/remove the native control button */ function removeUserLocationControl(){ if(map.googleMap && map.googleMap.controls && typeof google !== 'undefined' && google.maps){ var pos = google.maps.ControlPosition.RIGHT; var arr = map.googleMap.controls[pos]; if(arr && typeof arr.getLength === 'function'){ for(var i = arr.getLength() - 1; i >= 0; i--){ var el = arr.getAt(i); if(el && el.classList && el.classList.contains('google-maps-control-user-location')){ arr.removeAt(i); } } } } $(map.element).find('.leaflet-control-user-location, .ol-control-user-location').each(function(){ var $leafletWrap = $(this).closest('.leaflet-control'); if($leafletWrap.length) $leafletWrap.remove(); else $(this).remove(); }); } $(document.body).on('change', 'input[name="enable_user_location_control"]', function(){ var enabled = $(this).is(':checked'); map.settings.enable_user_location_control = enabled ? 1 : 0; if(enabled){ removeUserLocationControl(); if(typeof map.addUserLocationControl === 'function'){ map.addUserLocationControl(); } } else { removeUserLocationControl(); } }); /* Shared getter: reuse already-known user position (from #1 marker or earlier geolocation call) before re-prompting the browser. */ function getCachedUserLocation(){ if(map.userLocation && typeof map.userLocation.lat !== 'undefined') return map.userLocation; if(map.userLocationMarker && typeof map.userLocationMarker.getPosition === 'function'){ return map.userLocationMarker.getPosition(); } return null; } function withUserLocation(callback){ var cached = getCachedUserLocation(); if(cached){ callback(cached); return; } WPGMZA.getCurrentPosition(function(position){ var loc = new WPGMZA.LatLng({ lat: position.coords.latitude, lng: position.coords.longitude }); map.userLocation = loc; callback(loc); }); } /* Automatically pan to user location (+ override zoom level). Mirrors pro-map.js:199-218 (which is admin-guarded). Pans only once per "enable" event — doesn't keep hijacking the viewport as the user interacts with the preview. */ function applyAutoPan(){ if(!$('input[name="automatically_pan_to_users_location"]').is(':checked')) return; withUserLocation(function(loc){ map.setCenter(loc); if($('input[name="override_users_location_zoom_level"]').is(':checked')){ var zoom = parseInt($('input[name="override_users_location_zoom_levels"]').val(), 10); if(!isNaN(zoom)) map.setZoom(zoom); } }); } $(document.body).on('change', 'input[name="automatically_pan_to_users_location"]', function(){ map.settings.automatically_pan_to_users_location = $(this).is(':checked') ? '1' : ''; if($(this).is(':checked')) applyAutoPan(); }); $(document.body).on('change', 'input[name="override_users_location_zoom_level"]', function(){ map.settings.override_users_location_zoom_level = $(this).is(':checked') ? 1 : 0; applyAutoPan(); }); $(document.body).on('change input', 'input[name="override_users_location_zoom_levels"]', function(){ var v = parseInt($(this).val(), 10); if(!isNaN(v)) map.settings.override_users_location_zoom_levels = v; applyAutoPan(); }); /* If saved map had auto-pan on, apply on init (after a brief delay so the map centre/zoom from the saved state has already been applied). */ if($('input[name="automatically_pan_to_users_location"]').is(':checked')){ setTimeout(applyAutoPan, 300); } /* Show distance from user location (in info windows + marker listings). Mirrors pro-map.js:268-304. */ function applyShowDistance(){ var enabled = $('input[name="show_distance_from_location"]').is(':checked'); map.settings.show_distance_from_location = enabled ? 1 : 0; if(enabled){ withUserLocation(function(loc){ map.userLocation = loc; if(WPGMZA.ProMap && WPGMZA.ProMap.SHOW_DISTANCE_FROM_USER_LOCATION){ map.userLocation.source = WPGMZA.ProMap.SHOW_DISTANCE_FROM_USER_LOCATION; } map.showDistanceFromLocation = loc; if(typeof map.updateInfoWindowDistances === 'function'){ map.updateInfoWindowDistances(); } if(map.markerListing && typeof map.markerListing.reload === 'function'){ map.markerListing.reload(); } }); } else { map.showDistanceFromLocation = null; if(map.markerListing && typeof map.markerListing.reload === 'function'){ map.markerListing.reload(); } } } $(document.body).on('change', 'input[name="show_distance_from_location"]', applyShowDistance); if($('input[name="show_distance_from_location"]').is(':checked')){ setTimeout(applyShowDistance, 300); } /* Fit bounds to route — when toggled on while a route exists, re-fit immediately */ $(document.body).on('change', 'input[name="directions_fit_bounds_to_route"]', function(){ var enabled = $(this).is(':checked'); map.settings.directions_fit_bounds_to_route = enabled ? 1 : 0; if(!enabled) return; var renderer = getDirectionsRenderer(); if(!isRouteDrawn(renderer)) return; if(renderer.directionStartMarker && renderer.directionEndMarker && typeof renderer.fitBoundsToRoute === 'function'){ renderer.fitBoundsToRoute( renderer.directionStartMarker.getPosition(), renderer.directionEndMarker.getPosition() ); } }); /* cmn-toggle labels (click fires before the checkbox change) */ $(document.body).on('click', 'label[for="directions_enabled"], label[for="filterbycat"], label[for="category_legends_enabled"], label[for="store_locator_enabled"]', function(){ setTimeout(debouncedRebuild, 50); } ); /* After each render, toggle store locator elements based on live settings. The template was pre-rendered with whatever state was saved. For settings we can hide/show without re-rendering the template, do it here using live form values. */ $(document.body).on('wpgmza_live_preview_rendered', function(){ var catEnabled = $('input[name="store_locator_category"]').is(':checked'); $('.wpgmza-store-locator .wpgmza-category-filter-container, .wpgmza-store-locator .wpgmza-category-filter-toggle').each(function(){ $(this).toggleClass('wpgmza-hidden', !catEnabled); }); /* If any grouping is visible (open-by-default), ensure its handle is un-hidden. ViewportGroupingPanel.findComponents hides the handle when the default component requires a feature — but for open-by-default we need the user to be able to click it to close. */ $(map.element).find('.wpgmza-inner-stack .grouping.visible').each(function(){ $(this).siblings('.grouping-handle').removeClass('wpgmza-hidden'); }); }); /* Delegated close handler for .wpgmza-clear inside the live preview IW panel. Bound on document.body once, survives populatePanel's $(el).off('click') calls. Uses capture-first priority via namespaced event + stopPropagation to ensure we always close. */ $(document.body).on('click.lp-iw-close', '.wpgmza-live-preview-iw-panel .wpgmza-clear, .wpgmza-inner-stack .wpgmza-lp-iw-grouping-item .wpgmza-clear', function(e){ e.stopPropagation(); var thisStack = $(this).closest('.wpgmza-inner-stack'); thisStack.removeClass('expanded'); thisStack.find('.grouping').removeClass('visible'); $(this).closest('.wpgmza-panel-info-window').removeClass('wpgmza-panel-info-window-populated'); }); /* Auto-close panel info window when its Delete button is * clicked. AdminMarkerDataTable's onDeleteMarker fires the AJAX * deletion + removes the marker from the map, but leaves the * panel open showing the now-deleted marker's content. Trigger * the panel's own .wpgmza-clear button so the close goes * through the established close path (same DOM mutations as a * user-initiated close). setTimeout(0) so the existing * delete-handler click fires first and we don't suppress the * AJAX request. */ $(document.body).on('click', '.wpgmza-panel-info-window .wpgmza_del_btn', function(){ var $panel = $(this).closest('.wpgmza-panel-info-window'); var $clear = $panel.find('.wpgmza-clear').first(); if(!$clear.length) return; setTimeout(function(){ $clear.trigger('click'); }, 0); }); /* Auto-refresh panel info window when the underlying marker is * saved from the sidebar editor. * * `sidebar-delegate-saved` is fired by FeaturePanel.onSave * AFTER the save flow has: * 1. removed the old marker from the map * 2. created a new marker from the REST response * 3. added the new marker * 4. reloaded the datatable * * IMPORTANT: the event's `feature` property is the feature * type STRING ("marker"), not the marker instance — see * feature-panel.js:771-774's `sidebarTriggerDelegate(type)` * which does `$(el).trigger({type, feature: this.featureType})`. * Using `event.feature` as a marker reference doesn't work. * * Approach: find the populated panel by its data-marker-id * attribute (set during populatePanel), look up the newly- * created marker via map.getMarkerByID, and re-run * populatePanel on its infoWindow. The new marker has a fresh * infoWindow but populatePanel finds the existing * `.wpgmza-panel-info-window-populated[data-map="X"]` element * in the DOM and updates its content in-place. */ $('.wpgmza-feature-accordion[data-wpgmza-feature-type="marker"]').on('sidebar-delegate-saved', function(event){ var $panel = $('.wpgmza-panel-info-window-populated[data-marker-id]').first(); if(!$panel.length) return; var markerId = parseInt($panel.attr('data-marker-id'), 10); if(!markerId) return; var liveMap = (WPGMZA.maps && WPGMZA.maps[0]) || (WPGMZA.mapEditPage && WPGMZA.mapEditPage.map); if(!liveMap || typeof liveMap.getMarkerByID !== 'function') return; /* Small delay so the save flow's marker reconciliation * (remove old → addMarker new → addMarker triggers icon * resolution etc.) settles before we re-populate. */ setTimeout(function(){ var marker = liveMap.getMarkerByID(markerId); if(!marker) return; /* Fresh marker may not have an infoWindow lazily-created * yet — the original instance only got one when the user * clicked the marker. Force-create via the engine's * createInfoWindow path the same way a click would. */ if(!marker.infoWindow && typeof WPGMZA.InfoWindow !== 'undefined' && typeof WPGMZA.InfoWindow.createInstance === 'function'){ marker.infoWindow = WPGMZA.InfoWindow.createInstance(marker); } if(marker.infoWindow && typeof marker.infoWindow.populatePanel === 'function'){ /* feature ref needed by populatePanel internals — * normally set by InfoWindow.open(map, feature). */ marker.infoWindow.feature = marker; marker.infoWindow.populatePanel(); } }, 150); }); /* Register BEFORE marker-listing so the filter clone is in the DOM by the time MarkerListing's constructor scans for it. MarkerListing binds change handlers on `.wpgmza-marker-listing-category-filter [data-map-id="X"] select/input[type="checkbox"]` in its ctor, and caches the jQuery collection — if the clone isn't placed yet, the collection is empty and the handlers are never bound, which is why category clicks did nothing in the live preview. */ lp.registerComponent('category-filter', { enabledCheck: function(){ return $('input[name="filterbycat"]').is(':checked'); }, anchorField: 'select[name="category_filter_component_anchor"]', defaultAnchor: 0, proOnly: true, onInit: function(map, clone){ /* Re-apply data-map-id (stripped from the template source at LP init so only the live clone matches MarkerListing's selector). */ var $filter = clone.find('.wpgmza-marker-listing-category-filter') .addBack('.wpgmza-marker-listing-category-filter'); $filter.attr('data-map-id', map.id); /* Override the label from the live form value. The PHP template bakes the saved value in at page-load time, so rebuild alone doesn't reflect edits to "Filter Label" — we patch it here. */ var label = $('input[name="category_filter_label_string"]').val() || 'Filter by'; $filter.children('label[for^="wpgmza_filter_select_"]').first().text(label); }, onTeardown: function(map){ /* Cleanup handled by marker listing teardown */ } }); lp.registerComponent('marker-listing', { enabledCheck: function(){ var val = $('input[name="wpgmza_listmarkers_by"]:checked').val(); return val && parseInt(val) !== 0; }, anchorField: 'select[name="marker_listing_component_anchor"]', defaultAnchor: 10, // BELOW (matches data-default="BELOW" on the select in map-edit-page.html.php) proOnly: true, onInit: function(map, clone){ /* Sync form value into map.settings so MarkerListing.createInstance reads the right style */ var listStyle = $('input[name="wpgmza_listmarkers_by"]:checked').val(); if(listStyle !== undefined){ map.settings.wpgmza_listmarkers_by = parseInt(listStyle); } if(map.markerListing){ /* Clean up previous listing */ try { if(map.markerListing.paginationElement){ $(map.markerListing.paginationElement).pagination('destroy'); } if(map.markerListing.dataTable && map.markerListing.dataTable.dataTable){ map.markerListing.dataTable.dataTable.destroy(true); } } catch(e){} map.markerListing = null; } /* Rebuild the template with the correct PHP class for this style */ buildMarkerListingTemplate(); /* Ensure the visible clone's listing element has the correct attributes. initMarkerListing() and AdvancedTableMarkerListing both do global DOM searches by ID, so we must de-dupe: only the visible clone should match. Note: clone IS the listing element itself (not a wrapper around it), so .find() won't match it — use .addBack() to include the clone. */ var listEl = clone.find('[data-wpgmza-table], [data-wpgmza-marker-listing]') .addBack('[data-wpgmza-table], [data-wpgmza-marker-listing]').first(); if(!listEl.length) listEl = clone; listEl.attr('data-wpgmza-marker-listing', 'true'); listEl.attr('id', 'wpgmza_marker_list_' + mapId); listEl.css('width', '100%'); /* Remove duplicate IDs from hidden templates container */ templates.find('#wpgmza_marker_list_' + mapId).removeAttr('id'); templates.find('#wpgmza_marker_holder_' + mapId).removeAttr('id'); /* Ensure elements have wpgmza_table_* classes — the AdvancedTableDataTable drawCallback expects them but initTableDOM only adds them in legacy HTML mode */ listEl.find('thead th[data-wpgmza-column-name]').each(function(){ var name = $(this).attr('data-wpgmza-column-name'); if(name === 'icon') name = 'marker'; if(name === 'title') $(this).addClass('all'); if(!$(this).hasClass('wpgmza_table_' + name)){ $(this).addClass('wpgmza_table_' + name); } }); /* STYLE_PANEL (8) + LEFT/RIGHT anchor → route into the grouped panel (viewport-grouping), mirroring the frontend PHP shortcode compiler logic in class.pro-shortcodes.php:306-311 which puts $panels->left/right['listing'] into the inner-stack panel system. Live preview was placing it in the regular `.wpgmza-inner-stack.{left|right}` slot instead, which doesn't match the frontend rendering. Same pattern as directions-box (line ~185 above) and iw-panel Panel style (line ~1238 below) — when those three components share LEFT (or RIGHT), they all merge into the same `.grouping` with a single shared toggle, matching the frontend stacked-panel UX. */ var lpListStyle = parseInt($('input[name="wpgmza_listmarkers_by"]:checked').val()) || 0; var lpAnchor = lp.getComponentAnchor('marker-listing'); var lpAnchorName = ({1: 'left', 3: 'right'})[lpAnchor]; if(lpListStyle === 8 && lpAnchorName){ /* Detach the clone from wherever base render() placed it */ clone.detach(); var lpMapEl = $(map.element); var lpStack = lpMapEl.children('.wpgmza-inner-stack.' + lpAnchorName); if(!lpStack.length){ lpStack = $('
'); lpMapEl.append(lpStack); WPGMZA.AtlasMajorLivePreview.registerInnerStack(lpStack); } lpStack.addClass('viewport-grouping'); /* IMPORTANT: marker listing is ALWAYS-VISIBLE (unlike directions / iw-panel which are feature-triggered). Intentionally OMIT `data-requires-feature` here — `ViewportGroupingPanel.findComponents()` adds `.wpgmza-hidden` to the toggle handle when the FIRST grouping-item has data-requires-feature=true. The listing wants the handle always visible, so we skip the attribute. See viewport-grouping-panel.js lines ~76-80. */ var lpGroupingItem = $('
'); lpGroupingItem.append(clone); /* If a grouping already exists (created by directions / iw-panel earlier in this render's component loop), PREPEND our item so it becomes the FIRST child. ViewportGroupingPanel picks the first grouping-item as the defaultView — we want listing to be the default since it's always-visible. Without the prepend, directions/iw-panel would win that slot and the handle would be hidden again. */ var lpExistingGrouping = lpStack.children('.grouping').first(); if(lpExistingGrouping.length){ lpExistingGrouping.prepend(lpGroupingItem); } else { var lpGrouping = $('
'); var lpGroupingHandle = $('
'); lpGrouping.append(lpGroupingItem); lpStack.append(lpGrouping); lpStack.append(lpGroupingHandle); } lpStack.css('display', 'flex'); /* Defensive: clear `wpgmza-hidden` on the handle before re-init. A previous render (or a directions/iw-panel onInit running earlier this render) could have added it via their requires-feature path; the new ViewportGroupingPanel instance only ADDS the class conditionally, it never removes a stale one. With listing now prepended as the first grouping-item the new instance won't re-add it, so wiping here makes the handle reliably visible. */ lpStack.find('.grouping-handle').removeClass('wpgmza-hidden'); if(map.viewportGroupings){ /* Unbind existing handlers before re-init, or initGroups stacks duplicate handlers (same defensive cleanup directions/iw-panel do). */ $(map.element).find('.grouping-handle').off('click'); map.viewportGroupings.initGroups(); } } var origAdmin = WPGMZA.is_admin; WPGMZA.is_admin = "0"; try { WPGMZA.ProMap.prototype.initMarkerListing.call(map); } catch(e){ console.warn('LP-PRO: initMarkerListing failed', e); } WPGMZA.is_admin = origAdmin; }, onTeardown: function(map){ if(map.markerListing){ try { if(map.markerListing.dataTable && map.markerListing.dataTable.dataTable){ map.markerListing.dataTable.dataTable.destroy(true); } if(map.markerListing.paginationElement){ $(map.markerListing.paginationElement).pagination('destroy'); } } catch(e){} /* Neuter the orphaned filteringcomplete listener — the MarkerListing constructor binds an anonymous handler on document.body that we can't unbind by reference. Nulling the element prevents reload() from sending requests. */ map.markerListing.element = null; map.markerListing = null; } /* Remove Panel-style listing grouping-items so the next render doesn't double-up when directions / iw-panel recreate the shared grouping. Mirrors the cleanup iw-panel does for `.wpgmza-lp-iw-grouping-item`. */ $('.wpgmza-lp-listing-grouping-item').remove(); } }); /* Marker listing radio — handled by centralized rebuild */ /* category-filter is registered earlier (before marker-listing) so the clone is in the DOM by the time MarkerListing's constructor runs its global `.wpgmza-marker-listing-category-filter[data-map-id="X"]` lookup. See registration block above the marker-listing component. */ /* Rebuild legends template from current settings */ function rebuildLegendsTemplate(){ templates.find('[data-preview-component="category-legends"]').remove(); var legendsHtml = buildCategoryLegendsHtml(mapId); templates.append( '
' + legendsHtml + '
' ); } lp.registerComponent('category-legends', { enabledCheck: function(){ return $('input[name="category_legends_enabled"]').is(':checked'); }, anchorField: 'select[name="category_legends_component_anchor"]', defaultAnchor: 8, proOnly: true, onInit: function(map, clone){ if(map.categoryLegends) map.categoryLegends = null; if(WPGMZA.CategoryLegends){ /* Find only the clone in the live preview, not the hidden template */ var els = clone.find('.wpgmza-category-legends').addBack('.wpgmza-category-legends'); if(!els.length){ els = clone.filter('.wpgmza-category-legends'); } if(els.length){ els.attr('data-map-id', map.id); map.categoryLegends = []; els.each(function(i, el){ map.categoryLegends.push(new WPGMZA.CategoryLegends(el)); }); } } }, onTeardown: function(map){ if(map.categoryLegends){ map.categoryLegends = null; } } }); /* Category legends toggle — handled by centralized rebuild */ /* ======================================================== MARKER FIELDS (Custom Field Filters) ======================================================== */ lp.registerComponent('marker-fields', { enabledCheck: function(){ return $('input.wpgmza-enable-custom-field-filter:checked').length > 0; }, anchorField: 'select[name="marker_fields_component_anchor"]', defaultAnchor: 9, /* ABOVE */ proOnly: true, onInit: function(map, clone){ /* Show only widgets for fields that are checked in the editor settings */ clone.find('[data-field-id]').each(function(){ var fieldId = $(this).attr('data-field-id'); var isEnabled = $('input[name="enable_filter_custom_field_' + fieldId + '"]').is(':checked'); $(this).toggle(isEnabled); }); /* Init the custom field filter controller — it auto-discovers [data-wpgmza-filter-widget-class] elements in the DOM */ if(map.customFieldFilterController){ map.customFieldFilterController = null; } var origAdmin = WPGMZA.is_admin; WPGMZA.is_admin = "0"; try { WPGMZA.ProMap.prototype.initCustomFieldFilterController.call(map); } catch(e){ console.warn('LP-PRO: initCustomFieldFilterController failed', e); } WPGMZA.is_admin = origAdmin; }, onTeardown: function(map){ if(map.customFieldFilterController){ /* Remove the