1/** 2 * @namespace WPGMZA 3 * @module AtlasMajorLivePreview 4 * @requires WPGMZA 5 * 6 * Live preview of frontend map components inside the Atlas Major map editor. 7 * Base plugin handles: Store Locator. 8 * Pro components are added by atlas-major-live-preview-pro.js via registerComponent(). 9 */ 10jQuery(function($) { 11 12 if(!document.querySelector('.wpgmza-atlas-major')) 13 return; 14 15 /* ======================================================== 16 ANCHOR MAP â mirrors ComponentAnchorControl PHP constants 17 ======================================================== */ 18 var ANCHOR_NAMES = { 19 0: 'top', 20 1: 'left', 21 2: 'center', 22 3: 'right', 23 4: 'bottom', 24 5: 'top_left', 25 6: 'top_right', 26 7: 'bottom_left', 27 8: 'bottom_right' 28 }; 29 var ANCHOR_ABOVE = 9; 30 var ANCHOR_BELOW = 10; 31 32 /* ======================================================== 33 COMPONENT REGISTRY â base plugin only registers store locator. 34 Pro adds its own via registerComponent(). 35 ======================================================== */ 36 var COMPONENTS = { 37 'store-locator': { 38 enabledCheck: function(){ return $('input[name="store_locator_enabled"]').is(':checked'); }, 39 anchorField: 'select[name="store_locator_component_anchor"]', 40 defaultAnchor: 0, 41 proOnly: false, 42 onInit: function(map, clone){ 43 /* Set data-id so initStoreLocator() finds it */ 44 clone.find('.wpgmza-store-locator').attr('data-id', map.id); 45 /* Call the real init */ 46 map.initStoreLocator(); 47 /* The map "init" event has already fired, so the filteringcomplete 48 listener inside the StoreLocator constructor never bound. 49 Bind it manually now. */ 50 if(map.storeLocator && map.markerFilter){ 51 map.markerFilter.on("filteringcomplete", function(event){ 52 map.storeLocator.onFilteringComplete(event); 53 }); 54 } 55 }, 56 onTeardown: function(map){ 57 if(map.storeLocator){ 58 /* Destroy the search-radius circle before dropping the 59 * storeLocator reference. Otherwise the next render 60 * leaves the circle drawn on the leaflet/google map 61 * with no live storeLocator owning it â visible but 62 * orphaned, can't be cleared by reset/search. Mirrors 63 * the cleanup in initStoreLocatorSettingsSync's 64 * destroyAndRedrawCircle() â handles both classic 65 * (removeCircle on the engine map) and modern 66 * (canvas overlay needs explicit destroy) circle 67 * subclasses. */ 68 var circle = map.storeLocator._circle; 69 if(circle){ 70 try { circle.setVisible(false); } catch(e){} 71 if(!(circle instanceof WPGMZA.ModernStoreLocatorCircle) && circle.map){ 72 try { circle.map.removeCircle(circle); } catch(e){} 73 } else if(circle instanceof WPGMZA.ModernStoreLocatorCircle && typeof circle.destroy === 'function'){ 74 try { circle.destroy(); } catch(e){} 75 } 76 map.storeLocator._circle = null; 77 } 78 map.storeLocator = null; 79 } 80 } 81 } 82 }; 83 84 /* ======================================================== 85 CONSTRUCTOR 86 ======================================================== */ 87 WPGMZA.AtlasMajorLivePreview = function(){ 88 var self = this; 89 90 this.map = WPGMZA.maps[0]; 91 if(!this.map || !this.map.element) return; 92 93 this.mapElement = $(this.map.element); 94 this.templates = $('#wpgmza-live-preview-templates'); 95 this.previewFrame = $('.am-preview-frame'); 96 this.slotAbove = $('.am-preview-slot-above'); 97 this.slotBelow = $('.am-preview-slot-below'); 98 this.enabled = localStorage.getItem('wpgmza-live-preview') !== '0'; 99 this._activeComponents = {}; 100 101 if(!this.templates.length) return; 102 103 /* Create inner-stack scaffold */ 104 this.stacks = {}; 105 this.createInnerStacks(); 106 107 /* Set viewport CSS variables */ 108 this.setViewportVariables(); 109 $(window).on('resize', function(){ self.setViewportVariables(); }); 110 111 /* Toggle */ 112 this.previewFrame.toggleClass('am-preview-off', !this.enabled); 113 this.initToggle(); 114 115 /* Initial render */ 116 this.render(); 117 118 /* Bind reactivity */ 119 this.bindEvents(); 120 121 /* Store locator live settings sync */ 122 this.initStoreLocatorSettingsSync(); 123 124 /* Map init/start state sync (center, zoom, bounds, streetview) */ 125 this.initMapStateSync(); 126 127 /* Marker click/interaction behaviour sync */ 128 this.initMarkerBehaviourSync(); 129 130 /* Advanced: map controls & layers */ 131 this.initControlsAndLayersSync(); 132 133 /* Save reminder â show when any setting changes */ 134 this.initSaveReminder(); 135 136 /* Click outside to close color pickers */ 137 $(document).on('mousedown', function(e){ 138 if(!$(e.target).closest('.wpgmza-color-input-wrapper, .wpgmza-color-input-host').length){ 139 $('.wpgmza-color-picker.active').removeClass('active'); 140 } 141 }); 142 } 143 144 /* ======================================================== 145 INNER-STACK SCAFFOLD 146 ======================================================== */ 147 /** 148 * Register a newly-created inner-stack element so clicks/drags on 149 * it don't propagate down to the underlying Leaflet map (which 150 * would interpret the drag as a map pan). Mirrors the 151 * `L.DomEvent.disableClickPropagation` calls in 152 * leaflet-map.js:288-295 (which only run on inner-stacks present at 153 * map-construction time â dynamic stacks created later by the live 154 * preview need the same treatment manually). 155 * 156 * No-op for non-Leaflet engines (Google Maps, OpenLayers) â Google 157 * Maps' own overlay layer handles event isolation; OpenLayers' map 158 * surface only listens on its own canvas. Kept on the public 159 * AtlasMajorLivePreview namespace so Pro can call it too. 160 * 161 * @param {jQuery|HTMLElement} stack â the inner-stack element. 162 */ 163 WPGMZA.AtlasMajorLivePreview.registerInnerStack = function(stack){ 164 if(!stack) return; 165 if(typeof window.L === 'undefined' || !window.L.DomEvent) return; 166 var el = stack instanceof jQuery ? stack[0] : stack; 167 if(!el) return; 168 try { 169 L.DomEvent.disableClickPropagation(el); 170 L.DomEvent.disableScrollPropagation(el); 171 L.DomEvent.on(el, 'mousedown', L.DomEvent.stopPropagation); 172 } catch(e){ } 173 }; 174 175 WPGMZA.AtlasMajorLivePreview.prototype.createInnerStacks = function(){ 176 var mapEl = this.mapElement; 177 178 for(var code in ANCHOR_NAMES){ 179 var name = ANCHOR_NAMES[code]; 180 var existing = mapEl.children('.wpgmza-inner-stack.' + name); 181 if(existing.length){ 182 this.stacks[code] = existing; 183 } else {
184 var stack = $('<div class="wpgmza-inner-stack ' + name + ' wpgmza-live-preview-stack"></div>'); 185 mapEl.append(stack); 186 WPGMZA.AtlasMajorLivePreview.registerInnerStack(stack); 187 this.stacks[code] = stack; 188 } 189 } 190 } 191 192 /* ======================================================== 193 VIEWPORT CSS VARIABLES 194 ======================================================== */ 195 WPGMZA.AtlasMajorLivePreview.prototype.setViewportVariables = function(){ 196 var el = this.mapElement[0]; 197 if(!el) return; 198 var w = el.offsetWidth || 800; 199 var oMult = w < 760 ? 1 : (w < 960 ? 0.7 : 0.5); 200 var pMult = w < 760 ? 1 : (w < 960 ? 0.5 : 0.3); 201 el.style.setProperty('--wpgmza--viewport-overlays-max-width', (oMult * 100) + '%'); 202 el.style.setProperty('--wpgmza--viewport-panels-max-width', (pMult * 100) + '%'); 203 el.style.setProperty('--wpgmza--viewport-container-width', w + 'px'); 204 el.style.setProperty('--wpgmza--viewport-container-height', (el.offsetHeight || 500) + 'px'); 205 } 206 207 /* ======================================================== 208 TOGGLE 209 ======================================================== */ 210 WPGMZA.AtlasMajorLivePreview.prototype.initToggle = function(){ 211 var self = this; 212 var checkbox = $('#am-live-preview-toggle'); 213 checkbox.prop('checked', this.enabled); 214 checkbox.on('change', function(){ 215 self.enabled = $(this).is(':checked'); 216 self.previewFrame.toggleClass('am-preview-off', !self.enabled); 217 localStorage.setItem('wpgmza-live-preview', self.enabled ? '1' : '0'); 218 self.render(); 219 220 /* The map container's dimensions change when the mockup 221 * frame is toggled (the mockup wraps it to ~1000px wide vs 222 * full editor width in the off state). Leaflet / OpenLayers 223 * cache viewport size and only recalculate on explicit 224 * resize â without this the map tiles stay sized to the 225 * old container and subsequent height changes look broken. 226 * The CSS transition needs ~250ms to settle, so defer the 227 * resize slightly. */ 228 setTimeout(function(){ 229 if(self.map && typeof self.map.onElementResized === 'function'){ 230 self.map.onElementResized(); 231 } 232 /* Leaflet-specific: force an invalidateSize as a belt- 233 * and-suspenders for any engine that doesn't fully 234 * resize from onElementResized. */ 235 if(self.map && self.map.leafletMap && typeof self.map.leafletMap.invalidateSize === 'function'){ 236 self.map.leafletMap.invalidateSize(); 237 } 238 }, 300); 239 }); 240 } 241 242 /* ======================================================== 243 STATE CHECKS 244 ======================================================== */ 245 WPGMZA.AtlasMajorLivePreview.prototype.isComponentEnabled = function(key){ 246 var def = COMPONENTS[key]; 247 if(!def) return false; 248 if(def.proOnly && (!WPGMZA.isProVersion || !WPGMZA.isProVersion())) 249 return false; 250 return def.enabledCheck(); 251 } 252 253 WPGMZA.AtlasMajorLivePreview.prototype.getComponentAnchor = function(key){ 254 var def = COMPONENTS[key]; 255 if(!def) return 0; 256 var field = $(def.anchorField); 257 if(field.length){ 258 var val = parseInt(field.val()); 259 if(!isNaN(val)) return val; 260 } 261 return def.defaultAnchor; 262 } 263 264 /* ======================================================== 265 RENDER â places components and calls init methods 266 ======================================================== */ 267 WPGMZA.AtlasMajorLivePreview.prototype.render = function(){ 268 var self = this; 269 var map = this.map; 270 271 /* Teardown all active components */ 272 for(var activeKey in this._activeComponents){ 273 this.teardownComponent(activeKey); 274 } 275 276 /* Clear DOM */ 277 $('.wpgmza-live-preview-component').remove(); 278 this.slotAbove.empty(); 279 this.slotBelow.empty(); 280 281 if(!this.enabled){ 282 $(document.body).trigger('wpgmza_live_preview_rendered', [this]); 283 return; 284 } 285 286 for(var key in COMPONENTS){ 287 if(!this.isComponentEnabled(key)) continue; 288 289 var anchor = this.getComponentAnchor(key); 290 var templateEl = this.templates.find('[data-preview-component="' + key + '"]'); 291 if(!templateEl.length || !templateEl.children().length) continue; 292 293 var clone = templateEl.children().first().clone(); 294 clone.addClass('wpgmza-live-preview-component'); 295 clone.attr('data-live-preview-key', key); 296 297 /* Place in correct position */ 298 var target; 299 if(anchor === ANCHOR_ABOVE){ 300 this.slotAbove.append(clone); 301 target = this.slotAbove; 302 } else if(anchor === ANCHOR_BELOW){ 303 this.slotBelow.append(clone); 304 target = this.slotBelow; 305 } else { 306 var stack = this.stacks[anchor] || this.stacks[0]; 307 if(stack && stack.length){ 308 stack.append(clone); 309 target = stack; 310 } 311 } 312 313 /* Ensure inner stacks with content are visible */ 314 if(target && target.hasClass('wpgmza-live-preview-stack')){ 315 target.css('display', 'flex'); 316 } 317 318 /* Call component init */ 319 var def = COMPONENTS[key]; 320 if(def.onInit){
321 try { 322 def.onInit(map, clone); 323 } catch(e){ 324 console.warn('AtlasMajorLivePreview: init failed for ' + key, e); 325 } 326 } 327 328 this._activeComponents[key] = true; 329 } 330 331 /* Hide empty stacks */ 332 this.mapElement.find('.wpgmza-live-preview-stack').each(function(){ 333 if(!$(this).children('.wpgmza-live-preview-component').length){ 334 $(this).css('display', ''); 335 } 336 }); 337 338 /* Fire event for Pro to hook into */ 339 $(document.body).trigger('wpgmza_live_preview_rendered', [this]); 340 } 341 342 /* ======================================================== 343 TEARDOWN â removes component and cleans up JS instance 344 ======================================================== */ 345 WPGMZA.AtlasMajorLivePreview.prototype.teardownComponent = function(key){ 346 var def = COMPONENTS[key]; 347 if(def && def.onTeardown){ 348 try { 349 def.onTeardown(this.map); 350 } catch(e){ 351 console.warn('AtlasMajorLivePreview: teardown failed for ' + key, e); 352 } 353 } 354 delete this._activeComponents[key]; 355 } 356 357 /* ======================================================== 358 EVENT BINDING 359 ======================================================== */ 360 WPGMZA.AtlasMajorLivePreview.prototype.bindEvents = function(){ 361 var self = this; 362 var timer = null; 363 364 function debouncedRender(){ 365 clearTimeout(timer); 366 timer = setTimeout(function(){ self.render(); }, 200); 367 } 368 369 /* Watch all known component fields */ 370 var selectors = []; 371 for(var key in COMPONENTS){ 372 var def = COMPONENTS[key]; 373 if(def.anchorField) selectors.push(def.anchorField); 374 } 375 /* Toggle checkboxes */ 376 selectors.push('input[name="store_locator_enabled"]'); 377 378 $(document.body).on('change', selectors.join(','), debouncedRender); 379 380 /* cmn-toggle labels */ 381 $(document.body).on('click', 'label[for="store_locator_enabled"]', function(){ 382 setTimeout(debouncedRender, 50); 383 }); 384 385 /* Allow Pro to add more watched fields */ 386 $(document.body).on('wpgmza_live_preview_watch_field', function(e, selector){ 387 $(document.body).on('change', selector, debouncedRender); 388 }); 389 } 390 391 /* ======================================================== 392 STORE LOCATOR SETTINGS SYNC 393 Updates live-changeable settings without full rebuild. 394 Structural changes trigger a full rebuild via render(). 395 ======================================================== */ 396 WPGMZA.AtlasMajorLivePreview.prototype.initStoreLocatorSettingsSync = function(){ 397 var self = this; 398 var map = this.map; 399 400 /* --- Circle settings (color, opacity, radius style) --- 401 Sync value into map.settings, destroy cached circle, 402 re-trigger filtering to redraw with new values */ 403 /* Destroy circle and re-trigger filtering to recreate it */ 404 function destroyAndRedrawCircle(){ 405 if(map.storeLocator && map.storeLocator._circle){ 406 var circle = map.storeLocator._circle; 407 circle.setVisible(false); 408 if(!(circle instanceof WPGMZA.ModernStoreLocatorCircle) && circle.map){ 409 /* Classic circle â remove from map cleanly. */ 410 try { circle.map.removeCircle(circle); } catch(e){} 411 } else if(circle instanceof WPGMZA.ModernStoreLocatorCircle && typeof circle.destroy === 'function'){ 412 /* Modern circle is a canvas overlay â setVisible(false) 413 * only hides, leaving the canvas attached to the DOM. 414 * Without destroy(), a subsequent modernâmodern cycle 415 * (radarâclassicâradar) stacks orphaned canvases which 416 * block the new canvas's paint. Engine-specific 417 * subclasses all implement destroy() to detach canvas 418 * + unbind map events. */ 419 try { circle.destroy(); } catch(e){} 420 } 421 map.storeLocator._circle = null; 422 } 423 if(map.storeLocator && map.storeLocator.state === WPGMZA.StoreLocator.STATE_APPLIED && map.storeLocator._center){ 424 map.markerFilter.update({}, map.storeLocator); 425 } 426 } 427 428 /* Update modern circle color/draw without destroying */ 429 function updateModernCircleColor(){ 430 if(map.storeLocator && map.storeLocator._circle && map.storeLocator._circle instanceof WPGMZA.ModernStoreLocatorCircle){ 431 map.storeLocator._circle.settings.color = map.storeLocator.circleStrokeColor; 432 if(typeof map.storeLocator._circle.draw === 'function'){ 433 map.storeLocator._circle.draw(); 434 } 435 return true; 436 } 437 return false;
438 } 439 440 /* Color fields â live update for modern circle, destroy+redraw for classic */ 441 ['sl_stroke_color', 'sl_fill_color'].forEach(function(name){ 442 $(document.body).on('change input', '[name="' + name + '"]', function(){ 443 map.settings[name] = $(this).val(); 444 if(!updateModernCircleColor()){ 445 destroyAndRedrawCircle(); 446 } 447 }); 448 }); 449 450 /* Opacity fields â only affect classic circle */ 451 ['sl_stroke_opacity', 'sl_fill_opacity'].forEach(function(name){ 452 $(document.body).on('change input', '[name="' + name + '"]', function(){ 453 map.settings[name] = $(this).val(); 454 destroyAndRedrawCircle(); 455 }); 456 }); 457 458 /* Radius style switch â always needs full destroy+recreate (different circle class) */ 459 $(document.body).on('change input', '[name="wpgmza_store_locator_radius_style"]', function(){ 460 map.settings.wpgmza_store_locator_radius_style = $('[name="wpgmza_store_locator_radius_style"]:checked').val(); 461 destroyAndRedrawCircle(); 462 }); 463 464 /* --- Live-updatable simple settings (just sync into map.settings) --- 465 * Some fields (store_locator_button_style, store_locator_style) 466 * are ALSO in rebuildFields below â the rebuildFields handler 467 * triggers a render but doesn't sync map.settings. We include 468 * them here too so map.settings has the new value by the time 469 * render() finishes and wpgmza_live_preview_rendered fires 470 * (post-render transforms like applyButtonStyleToPreview read 471 * from map.settings). Both handlers fire on change; simpleFields 472 * is bound first so the sync lands before render is scheduled. */ 473 var simpleFields = [ 474 'wpgmza_store_locator_bounce', 475 'store_locator_distance', 476 'store_locator_auto_area_max_zoom', 477 'store_locator_show_distance', 478 'wpgmza_store_locator_use_their_location', 479 'wpgmza_store_locator_hide_before_search', 480 'store_locator_nearby_searches', 481 'wpgmza_store_locator_restrict', 482 'store_locator_button_style', 483 'store_locator_style', 484 /* Info window settings â handled separately below */ 485 'close_infowindow_on_map_click' 486 ]; 487 488 simpleFields.forEach(function(name){ 489 $(document.body).on('change input', '[name="' + name + '"]', function(){ 490 var el = $(this); 491 if(el.is(':checkbox')){ 492 /* Unchecked = '' (falsy) NOT '0' â the string '0' is 493 * truthy in JS so downstream `if(map.settings.foo)` 494 * checks treat unchecked checkboxes as enabled. */ 495 map.settings[name] = el.is(':checked') ? '1' : ''; 496 } else if(el.is(':radio')){ 497 map.settings[name] = $('[name="' + name + '"]:checked').val(); 498 } else { 499 map.settings[name] = el.val(); 500 } 501 }); 502 }); 503 504 /* --- Live text updates â modify the cloned DOM directly --- 505 Fallbacks route through WPGMZA.localized_strings so translated 506 copy shows when the user hasn't entered a custom override. 507 Strings are registered in class.strings.php::getLocalizedStrings(). */ 508 var LS = (WPGMZA && WPGMZA.localized_strings) || {}; 509 var textFieldMap = { 510 'store_locator_query_string': function(val){ 511 $('.wpgmza-live-preview-component.wpgmza-store-locator label.wpgmza-address, .wpgmza-live-preview-component .wpgmza-store-locator label.wpgmza-address').text(val || LS.atlas_major_sl_address_label || 'ZIP / Address:'); 512 }, 513 'store_locator_location_placeholder': function(val){ 514 $('.wpgmza-live-preview-component.wpgmza-store-locator input.wpgmza-address, .wpgmza-live-preview-component .wpgmza-store-locator input.wpgmza-address').attr('placeholder', val || LS.autcomplete_placeholder || 'Enter a location'); 515 }, 516 'store_locator_name_string': function(val){ 517 $('.wpgmza-live-preview-component.wpgmza-store-locator label.wpgmza-keywords, .wpgmza-live-preview-component .wpgmza-store-locator label.wpgmza-keywords').text(val || LS.atlas_major_sl_keywords_label || 'Title / Description:'); 518 $('.wpgmza-live-preview-component.wpgmza-store-locator input.wpgmza-keywords, .wpgmza-live-preview-component .wpgmza-store-locator input.wpgmza-keywords').attr('placeholder', val || LS.atlas_major_sl_keywords_placeholder || 'Enter a title'); 519 }, 520 'store_locator_not_found_message': function(val){ 521 map.settings.store_locator_not_found_message = val; 522 }, 523 'store_locator_default_address': function(val){ 524 map.settings.store_locator_default_address = val; 525 $('.wpgmza-live-preview-component.wpgmza-store-locator input.wpgmza-address, .wpgmza-live-preview-component .wpgmza-store-locator input.wpgmza-address').val(val || ''); 526 }, 527 'wpgmza_store_locator_default_radius': function(val){ 528 map.settings.wpgmza_store_locator_default_radius = val; 529 $('.wpgmza-live-preview-component.wpgmza-store-locator select.wpgmza-radius, .wpgmza-live-preview-component .wpgmza-store-locator select.wpgmza-radius').val(val); 530 } 531 }; 532 533 for(var tfName in textFieldMap){ 534 (function(name, handler){ 535 $(document.body).on('change input', '[name="' + name + '"]', function(){ 536 handler($(this).val()); 537 }); 538 })(tfName, textFieldMap[tfName]); 539 } 540 541 /* Re-apply every textFieldMap handler from the current form 542 * value. Used after a full live-preview render() â toggling a 543 * structural field (Enable Title Search, Enable Categories, 544 * etc.) reclones the store locator template, which resets all
545 * the user's customised labels/placeholders/default address/ 546 * default radius back to template defaults. Re-running each 547 * handler against the current form state restores those 548 * customisations on the freshly-cloned DOM. */ 549 function reapplyTextFieldValues(){ 550 for(var name in textFieldMap){ 551 var $input = $('[name="' + name + '"]'); 552 if(!$input.length) continue; 553 try { 554 textFieldMap[name]($input.val()); 555 } catch(e){ } 556 } 557 } 558 559 /* Distance unit DOM sync â bring the SL DOM (suffix span + 560 * radius option text) in line with the current form-state 561 * unit. Idempotent: safe to call any time, including 562 * immediately after a fresh template clone whose option 563 * text was server-rendered with the SAVED unit baked in. 564 * 565 * The `oldSuffix` derivation reads from the live form value 566 * (`isMiles ? 'km' : 'mi'`) and the regex only matches 567 * options ending with that opposite suffix â so calling 568 * this on a DOM that already matches the form is a no-op 569 * (the regex fails to match, nothing changes). 570 * 571 * Localised strings aren't available client-side, so we 572 * fall back to English â acceptable because this is the 573 * admin-only live preview. */ 574 function applyDistanceUnitToDOM(){ 575 var isMiles = $('input[name="store_locator_distance"]').is(':checked'); 576 var oldSuffix = isMiles ? 'km' : 'mi'; 577 var newSuffix = isMiles ? 'mi' : 'km'; 578 map.settings.store_locator_distance = isMiles ? '1' : '0'; 579 580 /* Swap the suffix text. class.map-edit-page.php wraps the 581 * suffix in <span class="wpgmza-distance-unit-suffix">. */ 582 $('.wpgmza-distance-unit-suffix').text(newSuffix); 583 584 /* Each <option> in the live-preview store locator's radius 585 * dropdown has its unit baked into the text ("10km", 586 * "25km", etc.) because StoreLocator::populateRadiusSelect 587 * writes `$radius . $suffix` at server render. Swap the 588 * trailing unit on every option so the dropdown reflects 589 * the current toggle state. The underlying `value` 590 * attribute (just the number) is unchanged. */ 591 $('.wpgmza-live-preview-component .wpgmza-radius option, .wpgmza-live-preview-component select.wpgmza-radius option').each(function(){ 592 var $opt = $(this); 593 var text = $opt.text(); 594 /* Only rewrite options that end with the old suffix, 595 * so we don't double-suffix on repeat toggles. */ 596 var re = new RegExp('(\\d+)\\s*' + oldSuffix + '\\s*$', 'i'); 597 if(re.test(text)){ 598 $opt.text(text.replace(re, '$1' + newSuffix)); 599 } 600 }); 601 } 602 603 /* User toggled the distance unit â full update including 604 * circle redraw and info-window clear (in case "X miles 605 * away" is currently showing). */ 606 $(document.body).on('change', 'input[name="store_locator_distance"]', function(){ 607 applyDistanceUnitToDOM(); 608 609 /* Rebuild the circle so its radius recalculates under the 610 * new unit. Modern circles redraw in-place via draw(); 611 * classic circles need a full destroy+recreate. */ 612 if(!updateModernCircleColor()){ 613 destroyAndRedrawCircle(); 614 } 615 616 /* Info window "X miles/km away" text is cached in the open 617 * popup â rebuild it so the unit flips too. info-window.js 618 * reads map.settings.store_locator_distance live in 619 * getContent, so reopening the popup regenerates the text 620 * with the new unit. */ 621 clearCachedInfoWindows(); 622 }); 623 624 /* Live preview re-rendered (most importantly: SL toggled 625 * back on after being off). The freshly-cloned SL template 626 * was server-rendered with the SAVED unit baked into the 627 * radius option text + suffix span, so if the user changed 628 * the unit while SL was disabled, the re-enabled template 629 * would still display the saved unit. Re-applying the DOM 630 * sync brings it in line with the current form value. 631 * 632 * Lightweight only â no circle redraw, no info-window 633 * clear, since the re-init path handles those naturally 634 * (no circle yet on a fresh re-enable, info windows already 635 * cleared by the marker teardown/rebuild). Mirrors the 636 * pattern used by `applyKeywordSearchVisibility` in 637 * atlas-major-live-preview-pro.js. */ 638 $(document.body).on('wpgmza_live_preview_rendered', function(){ 639 applyDistanceUnitToDOM(); 640 }); 641 642 /* --- Info window settings â sync + clear cached info windows so new style applies --- */ 643 var iwFields = ['wpgmza_iw_type', 'iw_primary_color', 'iw_accent_color', 'iw_text_color']; 644 645 function clearCachedInfoWindows(){ 646 if(!map.markers) return; 647 648 /* Find which marker has an open info window */ 649 var openMarker = null; 650 for(var i = 0; i < map.markers.length; i++){ 651 var m = map.markers[i]; 652 if(m.infoWindow && m.infoWindow.element && $(m.infoWindow.element).is(':visible')){ 653 openMarker = m; 654 } 655 } 656 if(!openMarker && map.lastInteractedMarker && map.lastInteractedMarker.infoWindow){ 657 openMarker = map.lastInteractedMarker; 658 } 659 660 /* Close and delete all cached info windows */ 661 for(var i = 0; i < map.markers.length; i++){ 662 if(map.markers[i].infoWindow){ 663 try { map.markers[i].infoWindow.close(); } catch(e){} 664 delete map.markers[i].infoWindow; 665 } 666 } 667 668 /* Clear lastInteractedMarker so openInfoWindow doesn't try to close a deleted infoWindow */ 669 map.lastInteractedMarker = null; 670 671 /* Reopen on the same marker with new style */ 672 if(openMarker){ 673 setTimeout(function(){ 674 openMarker.openInfoWindow(); 675 }, 100); 676 } 677 } 678 679 iwFields.forEach(function(name){ 680 $(document.body).on('change input', '[name="' + name + '"]', function(){ 681 var el = $(this); 682 if(el.is(':radio')){ 683 map.settings[name] = $('[name="' + name + '"]:checked').val(); 684 } else { 685 map.settings[name] = el.val(); 686 } 687 clearCachedInfoWindows(); 688 }); 689 }); 690 691 /* "Show distance from search" â toggles whether marker info windows 692 * include "X miles/km away" after a store locator search. 693 * 694 * Two problems `simpleFields` above introduces that we fix here: 695 * 696 * 1. simpleFields writes a STRING '1'/'0' into map.settings. The 697 * info-window.js check `if(map.settings.store_locator_show_distance 698 * && ...)` is ALWAYS truthy for a non-empty string â the string 699 * '0' reads as true, leaving the distance visible after OFF.
700 * We overwrite with an integer 0/1 so the truthy check works. 701 * 702 * 2. simpleFields binds on `'change input'` â a checkbox click 703 * fires BOTH events. simpleFields thus writes the string value 704 * twice (once per event). If we only listen for 'change', our 705 * integer gets clobbered by the subsequent 'input' event 706 * writing the string back. We bind on both events too so our 707 * integer wins reliably. 708 * 709 * clearCachedInfoWindows is called once (on the 'change' event 710 * only) so we close + reopen the info window exactly once per 711 * user toggle â not twice per click. */ 712 $(document.body).on('change input', 'input[name="store_locator_show_distance"]', function(e){ 713 map.settings.store_locator_show_distance = $(this).is(':checked') ? 1 : 0; 714 if(e.type === 'change'){ 715 clearCachedInfoWindows(); 716 } 717 }); 718 719 /* Enable Store Locator toggle â when turned OFF mid-search, tear 720 * down the side-effects of the previous search: destroy the 721 * circle, reset state to INITIAL, and refresh info windows so 722 * they drop the "X miles/km away" line (the check in 723 * info-window.js gates on storeLocator.state === STATE_APPLIED, 724 * which no longer holds after the reset). Without this teardown 725 * the circle stayed painted on the map and info windows kept 726 * the stale distance text after the toggle flipped off. */ 727 $(document.body).on('change', 'input[name="store_locator_enabled"]', function(){ 728 if($(this).is(':checked')) return; // turning ON â render() handles it via bindEvents 729 730 if(map.storeLocator){ 731 /* Destroy circle (same logic as destroyAndRedrawCircle 732 * but without the follow-up redraw). */ 733 if(map.storeLocator._circle){ 734 var circle = map.storeLocator._circle; 735 circle.setVisible(false); 736 if(!(circle instanceof WPGMZA.ModernStoreLocatorCircle) && circle.map){ 737 try { circle.map.removeCircle(circle); } catch(e){} 738 } else if(circle instanceof WPGMZA.ModernStoreLocatorCircle && typeof circle.destroy === 'function'){ 739 try { circle.destroy(); } catch(e){} 740 } 741 map.storeLocator._circle = null; 742 } 743 /* Reset search state so info-window.js' distance check 744 * (storeLocator.state === STATE_APPLIED) no longer 745 * passes. We don't touch map.storeLocator._center / 746 * _bounds â if the user toggles SL back on the state 747 * goes back to INITIAL and they'll need to search 748 * again, which matches the expected reset UX. */ 749 map.storeLocator.state = WPGMZA.StoreLocator.STATE_INITIAL; 750 } 751 752 clearCachedInfoWindows(); 753 }); 754 755 /* --- Map width/height â apply to the map wrapper (NOT the mockup 756 * page). Previously this wrote `max-width` onto 757 * `.am-preview-page-inner`, which shrank the entire mockup 758 * "website" frame instead of just the map. It also caused a 759 * visual shift because .am-preview-page-inner has 760 * `margin-left: max(70px, calc((100% - 1000px) / 2))` that 761 * still assumes max-width: 1000px â once JS overwrote that 762 * with 100%, the margin calc went off-grid. Now we target the 763 * map wrapper, which sits inside the page-inner and is what 764 * the frontend equivalent (.wpgmza_map inline style) also 765 * sizes. */ 766 function updateMapDimensions(){ 767 var width = $('input[name="map_width"]').val(); 768 var widthType = $('select[name="map_width_type"]').val() || '%'; 769 var height = $('input[name="map_height"]').val(); 770 var heightType = $('select[name="map_height_type"]').val() || 'px'; 771 772 var mapWrapper = $('.am-preview-page-inner .map_wrapper'); 773 var mapEl = $(map.element); 774 775 if(width && widthType){ 776 mapWrapper.css({ 777 'width': width + widthType, 778 'max-width': '100%' // so % values remain parent-bound 779 }); 780 } 781 if(height && heightType){ 782 mapWrapper.css({ 783 'height': height + heightType, 784 'min-height': height + heightType 785 }); 786 mapEl.css({ 787 'height': height + heightType, 788 'min-height': height + heightType 789 }); 790 } 791 792 /* Tell the map engine to resize to fit its new container. 793 * onElementResized handles most engines; Leaflet needs an 794 * explicit invalidateSize() because it caches viewport 795 * dimensions and doesn't recalc unless told to. */ 796 setTimeout(function(){ 797 if(map.onElementResized){ 798 map.onElementResized(); 799 } 800 if(map.leafletMap && typeof map.leafletMap.invalidateSize === 'function'){ 801 map.leafletMap.invalidateSize(); 802 } 803 self.setViewportVariables(); 804 }, 100); 805 } 806 807 $(document.body).on('change input', 'input[name="map_width"], select[name="map_width_type"], input[name="map_height"], select[name="map_height_type"]', function(){ 808 updateMapDimensions(); 809 }); 810 811 /* Apply the saved width/height once on initial load too â the 812 * change handler only fires on user interaction, so without 813 * this initial call the preview shows the map at its CSS 814 * default (100% width) regardless of the saved map_width value 815 * (e.g. 500px). Defer slightly so the form values have been 816 * populated by the upstream WPGMZA init. */ 817 setTimeout(updateMapDimensions, 500); 818 819 /* Apply store_locator_button_style (SVG icons vs. text buttons) 820 * to any currently-rendered live preview component. The hidden 821 * template always holds the icon (SVG) version â we transform 822 * to text buttons here when the setting calls for it. Mirrors 823 * the PHP logic in class.store-locator.php that does the same 824 * transformation at server render time. */ 825 function applyButtonStyleToPreview(){ 826 var style = (map.settings && map.settings.store_locator_button_style) || ''; 827 $('.wpgmza-live-preview-component.wpgmza-store-locator, .wpgmza-live-preview-component .wpgmza-store-locator').each(function(){ 828 var wrapper = this; 829 if(style === 'text'){ 830 $(wrapper).find('svg[role="button"]').each(function(){ 831 var svg = this; 832 var btn = document.createElement('button'); 833 btn.textContent = svg.getAttribute('title') || ''; 834 var attrs = ['class', 'aria-label']; 835 for(var i = 0; i < attrs.length; i++){ 836 if(svg.hasAttribute(attrs[i])){ 837 btn.setAttribute(attrs[i], svg.getAttribute(attrs[i])); 838 } 839 } 840 /* The PHP strips .wpgmza-aria-bridge on buttons */ 841 btn.classList.remove('wpgmza-aria-bridge'); 842 svg.parentNode.insertBefore(btn, svg.nextSibling); 843 svg.parentNode.removeChild(svg); 844 }); 845 } 846 /* For 'icons' (default) we don't need to do anything â the 847 * template clone already has SVG buttons. The reverse 848 * transform (text â SVG) isn't possible without a 849 * template copy, which is why we force the PHP template 850 * render to always use icons. */ 851 }); 852 } 853 854 /* Re-apply button style after every live-preview render. The 855 * render() flow clones the template (always icons) and replaces 856 * existing components, so we need to re-transform to 'text' 857 * after each render. Same reasoning for reapplyTextFieldValues: 858 * a structural rebuild (Enable Title Search etc.) reclones the 859 * store locator template, dropping any user-customised labels/
860 * placeholders/defaults. Re-applying the textFieldMap handlers 861 * from the current form state restores them. */ 862 $(document.body).on('wpgmza_live_preview_rendered', function(){ 863 applyButtonStyleToPreview(); 864 reapplyTextFieldValues(); 865 }); 866 867 /* Also apply right now â the constructor calls render() BEFORE 868 * initStoreLocatorSettingsSync binds listeners, so the initial 869 * render's `wpgmza_live_preview_rendered` event fires before 870 * we're listening. Running once here covers that initial pass; 871 * subsequent renders go through the listener above. */ 872 applyButtonStyleToPreview(); 873 874 /* --- Structural changes â need full store locator rebuild --- */ 875 var rebuildFields = [ 876 'store_locator_name_search', 877 'store_locator_category', 878 'store_locator_search_area', 879 'store_locator_button_style', 880 'store_locator_style' 881 ]; 882 883 var rebuildTimer = null; 884 rebuildFields.forEach(function(name){ 885 $(document.body).on('change', '[name="' + name + '"]', function(){ 886 clearTimeout(rebuildTimer); 887 rebuildTimer = setTimeout(function(){ self.render(); }, 300); 888 }); 889 }); 890 891 /* Toggle labels for structural fields (need full rebuild) */ 892 $(document.body).on('click', 893 'label[for="wpgmza_store_locator_name_search"], label[for="wpgmza_store_locator_category_enabled"]', 894 function(){ 895 clearTimeout(rebuildTimer); 896 rebuildTimer = setTimeout(function(){ self.render(); }, 300); 897 } 898 ); 899 900 /* Toggle labels for simple settings (just sync). 901 * 902 * NOTE: `store_locator_show_distance` was intentionally REMOVED 903 * from this selector list. The dedicated change handler above 904 * writes an INTEGER (0/1) so the truthy check in 905 * info-window.js works correctly; this label-click handler 906 * fires a 50ms-delayed STRING write ('0'/'1') that ran AFTER 907 * change/input events and clobbered the integer with a string 908 * (which reads as truthy â causing the distance line to stick 909 * even when the toggle was off). Clicking a <label for="X"> 910 * natively toggles the checkbox and fires change/input events 911 * on the input, so `simpleFields` already syncs â this 912 * click-delayed write is redundant for all four toggles. */ 913 $(document.body).on('click', 914 'label[for="wpgmza_store_locator_use_their_location"], label[for="wpgmza_store_locator_hide_before_search"], label[for="store_locator_nearby_searches"]', 915 function(){ 916 var forId = $(this).attr('for'); 917 var checkbox = $('#' + forId); 918 setTimeout(function(){ 919 /* Unchecked = '' (falsy), NOT '0' (truthy string) */ 920 map.settings[checkbox.attr('name')] = checkbox.is(':checked') ? '1' : ''; 921 }, 50); 922 } 923 ); 924 } 925 926 /* ======================================================== 927 MAP STATE SYNC 928 Apply map-level settings (start position, zoom, bounds, 929 streetview) directly to the map via existing APIs â no 930 component rebuild needed. 931 ======================================================== */ 932 WPGMZA.AtlasMajorLivePreview.prototype.initMapStateSync = function(){ 933 var self = this; 934 var map = this.map; 935 936 function readFloat(sel){ 937 var v = parseFloat($(sel).val()); 938 return isNaN(v) ? null : v; 939 } 940 941 function readInt(sel){ 942 var v = parseInt($(sel).val()); 943 return isNaN(v) ? null : v; 944 } 945 946 /* Note: map_start_lat/lng/zoom and wpgmza_start_location/zoom are 947 implicit hidden fields â they auto-update as the user pans/zooms 948 the map. No explicit watcher needed. */ 949 950 /* --- Max/min zoom limits --- 951 NOTE: the form field names are misleading. Per the UI labels: 952 map_max_zoom = "Maximum Zoom Out Level" â Google minZoom 953 map_min_zoom = "Maximum Zoom In Level" â Google maxZoom */ 954 function applyZoomLimits(){ 955 var outLimit = readInt('input[name="map_max_zoom"]'); /* most zoomed out */ 956 var inLimit = readInt('input[name="map_min_zoom"]'); /* most zoomed in */ 957 958 map.settings.map_max_zoom = outLimit; 959 map.settings.map_min_zoom = inLimit; 960 961 /* Engine-specific â Google Maps is the primary engine */ 962 if(map.googleMap && typeof map.googleMap.setOptions === 'function'){ 963 var opts = {};
964 if(outLimit !== null) opts.minZoom = outLimit; 965 if(inLimit !== null) opts.maxZoom = inLimit; 966 map.googleMap.setOptions(opts); 967 } 968 /* Leaflet */ 969 if(map.leafletMap){ 970 if(outLimit !== null && typeof map.leafletMap.setMinZoom === 'function') map.leafletMap.setMinZoom(outLimit); 971 if(inLimit !== null && typeof map.leafletMap.setMaxZoom === 'function') map.leafletMap.setMaxZoom(inLimit); 972 } 973 } 974 975 $(document.body).on('change input', 'input[name="map_max_zoom"], input[name="map_min_zoom"]', applyZoomLimits); 976 977 /* --- Start in streetview (Google Maps only) --- 978 * The editor workflow (Atlas Novus behaviour) is: toggle ON â .streetview-starting-editor 979 * fieldset fades in and WPGMZA.StreetViewEditor enters add mode. The user drags the 980 * native Google pegman from the map's top-right control onto a location; position/ 981 * heading/pitch then populate. All of that is handled by Pro's ProMapEditPage + 982 * StreetViewEditor (pro-map-edit-page.js + streetview-editor.js) bound to the single 983 * admin map instance â the live preview shares that same map, so no extra wiring is 984 * needed here. We only mirror the raw setting value onto map.settings. 985 */ 986 $(document.body).on('change', 'input[name="map_starts_in_streetview"]', function(){ 987 var enabled = $(this).is(':checked'); 988 map.settings.map_starts_in_streetview = enabled ? '1' : '0'; 989 990 /* If the map currently has streetview open (e.g. loaded with this setting 991 already enabled + a saved location) and the user toggles OFF, close it 992 so the user isn't stuck looking at streetview. */ 993 if(!enabled && typeof map.closeStreetView === 'function'){ 994 map.closeStreetView(); 995 } 996 }); 997 998 /* --- Jump to nearest marker on init â trigger the effect on toggle --- */ 999 $(document.body).on('change', 'input[name="jump_to_nearest_marker_on_initialization"]', function(){ 1000 map.settings.jump_to_nearest_marker_on_initialization = $(this).is(':checked') ? '1' : ''; 1001 if(!$(this).is(':checked')) return; 1002 1003 /* Find nearest marker to current center and pan to it */ 1004 if(!map.markers || !map.markers.length) return; 1005 var center = map.getCenter(); 1006 if(!center) return; 1007 1008 var nearest = null; 1009 var nearestDist = Infinity; 1010 for(var i = 0; i < map.markers.length; i++){ 1011 var m = map.markers[i]; 1012 if(!m.lat || !m.lng) continue; 1013 var dLat = m.lat - center.lat; 1014 var dLng = m.lng - center.lng; 1015 var dist = dLat*dLat + dLng*dLng; 1016 if(dist < nearestDist){ 1017 nearestDist = dist; 1018 nearest = m; 1019 } 1020 } 1021 if(nearest && WPGMZA.LatLng){ 1022 map.setCenter(new WPGMZA.LatLng({lat: nearest.lat, lng: nearest.lng})); 1023 } 1024 }); 1025 1026 /* --- Fit bounds to markers --- */ 1027 function fitBoundsToMarkers(){ 1028 if(!map.markers || !map.markers.length) return; 1029 if(typeof map.fitBoundsToVisibleMarkers === 'function'){ 1030 map.fitBoundsToVisibleMarkers(); 1031 return; 1032 } 1033 /* Fallback */ 1034 if(WPGMZA.LatLngBounds){ 1035 var bounds = new WPGMZA.LatLngBounds(); 1036 for(var i = 0; i < map.markers.length; i++){ 1037 var m = map.markers[i]; 1038 if(m.lat && m.lng) bounds.extend(new WPGMZA.LatLng({lat: m.lat, lng: m.lng})); 1039 } 1040 if(typeof map.fitBounds === 'function') map.fitBounds(bounds); 1041 } 1042 } 1043 1044 $(document.body).on('change', 'input[name="fit_maps_bounds_to_markers"]', function(){ 1045 map.settings.fit_maps_bounds_to_markers = $(this).is(':checked') ? '1' : ''; 1046 if($(this).is(':checked')) fitBoundsToMarkers(); 1047 }); 1048 1049 /* --- Fit bounds to markers after filtering --- */ 1050 var fitOnFilterHandler = null; 1051 function setFitOnFilterHandler(enable){ 1052 if(fitOnFilterHandler){ 1053 $(document.body).off('filteringcomplete.wpgmza.fitOnFilter'); 1054 fitOnFilterHandler = null; 1055 } 1056 if(enable){ 1057 fitOnFilterHandler = function(event){ 1058 if(event.map && event.map.id === map.id){ 1059 fitBoundsToMarkers(); 1060 } 1061 }; 1062 $(document.body).on('filteringcomplete.wpgmza.fitOnFilter', fitOnFilterHandler); 1063 } 1064 } 1065 1066 $(document.body).on('change', 'input[name="fit_maps_bounds_to_markers_after_filtering"]', function(){ 1067 var enabled = $(this).is(':checked'); 1068 map.settings.fit_maps_bounds_to_markers_after_filtering = enabled ? '1' : '0'; 1069 setFitOnFilterHandler(enabled); 1070 }); 1071 1072 /* Initial state on load */ 1073 if($('input[name="fit_maps_bounds_to_markers_after_filtering"]').is(':checked')){ 1074 setFitOnFilterHandler(true); 1075 } 1076 1077 /* --- Map alignment --- 1078 * Align the map within the live preview's mockup page. 1079 * Values mirror the select options: 1080 * 1 = Left (default), 2 = Center, 3 = Right, 4 = None 1081 * We apply margin-* on the .map_wrapper so the map element 1082 * sits in the requested position when its width is < 100%. 1083 * For 100%-wide maps the alignment is visually a no-op. */ 1084 function applyMapAlignment(){ 1085 var v = parseInt($('select[name="wpgmza_map_align"]').val(), 10); 1086 var wrapper = $('.am-preview-page-inner .map_wrapper'); 1087 if(!wrapper.length) return; 1088 1089 /* Reset alignment-related margins first so we don't stack */ 1090 wrapper.css({ 'margin-left': '', 'margin-right': '' }); 1091 1092 switch(v){ 1093 case 2: /* Center */ 1094 wrapper.css({ 'margin-left': 'auto', 'margin-right': 'auto' }); 1095 break; 1096 case 3: /* Right */ 1097 wrapper.css({ 'margin-left': 'auto', 'margin-right': '0' }); 1098 break; 1099 case 4: /* None â no alignment, sits at default flow position */
1100 case 1: /* Left â default */ 1101 default: 1102 wrapper.css({ 'margin-left': '0', 'margin-right': 'auto' }); 1103 break; 1104 } 1105 } 1106 1107 $(document.body).on('change input', 'select[name="wpgmza_map_align"]', applyMapAlignment); 1108 /* Apply once on load so the saved alignment takes effect without 1109 * requiring user interaction. Slight delay so the form value 1110 * is populated. */ 1111 setTimeout(applyMapAlignment, 500); 1112 } 1113 1114 /* ======================================================== 1115 MARKER BEHAVIOUR SYNC 1116 Mirror marker click / info-window click-behaviour flags into 1117 map.settings so Pro's runtime code (ProMarker.onClick, 1118 ProMap.onMapClick) picks them up on the next interaction. 1119 For enable_marker_labels, iterate existing markers and apply 1120 setLabel() immediately so the change is visible without a reload. 1121 ======================================================== */ 1122 WPGMZA.AtlasMajorLivePreview.prototype.initMarkerBehaviourSync = function(){ 1123 var map = WPGMZA.maps[0]; 1124 if(!map) return; 1125 1126 /* --- Click marker opens link --- */ 1127 $(document.body).on('change', 'input[name="click_open_link"]', function(){ 1128 map.settings.click_open_link = $(this).is(':checked') ? 1 : 0; 1129 }); 1130 1131 /* --- Zoom on marker click (+ zoom level from slider) --- 1132 * The slider writes its value to input[name="wpgmza_zoom_on_marker_click_slider"]; 1133 * the existing zoom-slider UI fires change events on that input already. */ 1134 $(document.body).on('change', 'input[name="wpgmza_zoom_on_marker_click"]', function(){ 1135 map.settings.wpgmza_zoom_on_marker_click = $(this).is(':checked') ? 1 : 0; 1136 }); 1137 1138 $(document.body).on('change input', 'input[name="wpgmza_zoom_on_marker_click_slider"]', function(){ 1139 var v = parseInt($(this).val(), 10); 1140 if(!isNaN(v)) map.settings.wpgmza_zoom_on_marker_click_slider = v; 1141 }); 1142 1143 /* close_infowindow_on_map_click is already synced by initStoreLocatorSettingsSync's 1144 simpleFields array â no handler needed here. */ 1145 1146 /* --- Marker labels --- 1147 * ProMarker applies labels at onAdd/setVisible time using map.settings.enable_marker_labels 1148 * and the marker's own title/subText. The label itself is a Pointlabel instance owned by 1149 * the marker (see WPGMZA.Marker.prototype.setLabel), styled entirely from the 1150 * map.settings.marker_label_* fields below. Iterate existing markers and refresh them 1151 * live on any relevant setting change. */ 1152 function refreshMarkerLabels(){ 1153 if(!map.markers) return; 1154 for(var i = 0; i < map.markers.length; i++){ 1155 var m = map.markers[i]; 1156 if(typeof m.setLabel !== 'function') continue; 1157 if(map.settings.enable_marker_labels){ 1158 m.setLabel(); 1159 } else { 1160 m.setLabel(null); 1161 } 1162 } 1163 } 1164 1165 $(document.body).on('change', 'input[name="enable_marker_labels"]', function(){ 1166 map.settings.enable_marker_labels = $(this).is(':checked') ? 1 : 0; 1167 refreshMarkerLabels(); 1168 }); 1169 1170 $(document.body).on('change', 'select[name="marker_label_style"]', function(){ 1171 map.settings.marker_label_style = $(this).val(); 1172 refreshMarkerLabels(); 1173 }); 1174 1175 $(document.body).on('change', 'input[name="marker_label_above_icons"]', function(){ 1176 map.settings[$(this).attr('name')] = $(this).is(':checked') ? 1 : 0; 1177 refreshMarkerLabels(); 1178 }); 1179 1180 $(document.body).on('change', 'input[name="marker_label_hidden_by_default"]', function(){ 1181 map.settings[$(this).attr('name')] = $(this).is(':checked') ? 1 : 0; 1182 refreshMarkerLabels(); 1183 }); 1184 1185 $(document.body).on('change', 'input[name="marker_label_click_opens_infowindow"]', function(){ 1186 map.settings[$(this).attr('name')] = $(this).is(':checked') ? 1 : 0; 1187 refreshMarkerLabels(); 1188 }); 1189 1190 /* marker_label_icon is a single global icon URL (ImageInputSingle widget) - it 1191 * fires a plain 'change' on its underlying text input when picked or reset. */ 1192 $(document.body).on('change input', 'input[name="marker_label_font_size"], input[name="marker_label_offset_x"], input[name="marker_label_offset_y"], input[name="marker_label_icon"]', function(){ 1193 map.settings[$(this).attr('name')] = $(this).val(); 1194 refreshMarkerLabels(); 1195 }); 1196 } 1197 1198 /* ======================================================== 1199 CONTROLS & LAYERS SYNC (Map Settings â Advanced) 1200 Live-apply layer toggles and scale control via the existing 1201 engine methods. All of these are idempotent â calling them 1202 with the same enable state is a no-op. 1203 ======================================================== */ 1204 WPGMZA.AtlasMajorLivePreview.prototype.initControlsAndLayersSync = function(){ 1205 var map = WPGMZA.maps[0]; 1206 if(!map) return; 1207 1208 function syncAndApply(name, method){ 1209 $(document.body).on('change', 'input[name="' + name + '"]', function(){ 1210 var enabled = $(this).is(':checked'); 1211 map.settings[name] = enabled ? '1' : '0'; 1212 if(typeof map[method] === 'function'){ 1213 map[method](enabled); 1214 } 1215 }); 1216 } 1217 1218 syncAndApply('bicycle', 'enableBicycleLayer'); 1219 syncAndApply('traffic', 'enableTrafficLayer'); 1220 syncAndApply('transport_layer', 'enablePublicTransportLayer'); 1221 syncAndApply('enable_scale_control', 'enableScaleControl'); 1222 } 1223 1224 /* ======================================================== 1225 SAVE REMINDER 1226 ======================================================== */ 1227 WPGMZA.AtlasMajorLivePreview.prototype.initSaveReminder = function(){ 1228 var self = this; 1229 var shown = false;
1230 1231 var fadeTimer = null; 1232 var frontendOnlyFadeTimer = null; 1233 1234 /* Settings whose changes do NOT affect the live preview. When these 1235 change, show a "frontend only" notice instead of the save reminder. */ 1236 var FRONTEND_ONLY_SETTINGS = [ 1237 'only_load_markers_within_viewport', 1238 'lazyload_info_window_content', 1239 /* Mobile overrides â preview frame is fixed-size desktop, 1240 these only take effect on a real mobile viewport. */ 1241 'zoom_level_mobile_override_enabled', 1242 'zoom_level_mobile_override', 1243 'map_dimensions_mobile_override_enabled', 1244 'map_mobile_width', 1245 'map_mobile_width_type', 1246 'map_mobile_height', 1247 'map_mobile_height_type', 1248 /* Advanced tab â frontend-only behaviours */ 1249 'disable_lightbox_images', /* takes effect on marker image click on frontend */ 1250 'use_Raw_Jpeg_Coordinates', /* admin-side JPEG exif reader, no live preview effect */ 1251 /* Marker Listing â click-triggered effects only */ 1252 'marker_listing_nudge_marker_center', 1253 'marker_listing_nudge_x', 1254 'marker_listing_nudge_y', 1255 'zoom_level_on_marker_listing_override', 1256 'zoom_level_on_marker_listing_click', 1257 'marker_listing_disable_zoom', 1258 /* Directions â fires only when user runs directions */ 1259 'directions_behaviour', 1260 'force_google_directions_app', 1261 /* UGM / VGM â all settings only affect the frontend visitor- 1262 facing submission form (rendered via shortcode on public 1263 pages). None has a live preview effect inside the editor. */ 1264 'wpgmza_ugm_enbaled', 1265 'wpgmza_ugm_access', 1266 'wpgmza_ugm_desc_enabled', 1267 'wpgmza_ugm_category_enbaled', 1268 'wpgmza_ugm_upload_images', 1269 'wpgmza_ugm_link_enabled', 1270 'zoom_after_ugm_submission', 1271 'wpgmza_ugm_form_header', 1272 'wpgmza_ugm_form_title', 1273 'wpgmza_ugm_form_title_ph', 1274 'wpgmza_ugm_form_address', 1275 'wpgmza_ugm_form_address_ph', 1276 'wpgmza_ugm_form_address_help', 1277 'wpgmza_ugm_form_desc', 1278 'wpgmza_ugm_form_link', 1279 'wpgmza_ugm_form_link_ph', 1280 'wpgmza_ugm_form_image', 1281 'wpgmza_ugm_form_category' 1282 ]; 1283 1284 function showReminder(){ 1285 if(shown) return; 1286 shown = true; 1287 1288 if(self.enabled){ 1289 $('.am-save-reminder-preview').show(); 1290 $('.am-save-reminder-toolbar').hide(); 1291 } else { 1292 $('.am-save-reminder-toolbar').show(); 1293 $('.am-save-reminder-preview').hide(); 1294 } 1295 1296 /* Fade out after 8 seconds */ 1297 clearTimeout(fadeTimer); 1298 fadeTimer = setTimeout(function(){ 1299 $('.am-save-reminder').fadeOut(400, function(){ 1300 shown = false; 1301 }); 1302 }, 8000); 1303 } 1304 1305 function showFrontendOnlyNotice(){ 1306 if(self.enabled){ 1307 $('.am-frontend-only-notice-preview').stop(true, true).show(); 1308 $('.am-frontend-only-notice-toolbar').hide(); 1309 } else { 1310 $('.am-frontend-only-notice-toolbar').stop(true, true).show(); 1311 $('.am-frontend-only-notice-preview').hide(); 1312 } 1313 1314 clearTimeout(frontendOnlyFadeTimer); 1315 frontendOnlyFadeTimer = setTimeout(function(){ 1316 $('.am-frontend-only-notice').fadeOut(400); 1317 }, 8000); 1318 } 1319 1320 function isFrontendOnlySetting(el){ 1321 var name = $(el).attr('name'); 1322 if(!name) return false; 1323 return FRONTEND_ONLY_SETTINGS.indexOf(name) !== -1; 1324 } 1325 1326 /* Listen for ANY form change inside the sidebar */ 1327 $('.wpgmza-editor .sidebar').on('change input', 'input, select, textarea', function(){ 1328 if(isFrontendOnlySetting(this)){ 1329 showFrontendOnlyNotice(); 1330 } else { 1331 showReminder(); 1332 } 1333 }); 1334 1335 /* Also catch cmn-toggle label clicks */ 1336 $('.wpgmza-editor .sidebar').on('click', 'label[for]', function(){ 1337 var targetId = $(this).attr('for'); 1338 var targetEl = document.getElementById(targetId); 1339 if(targetEl && FRONTEND_ONLY_SETTINGS.indexOf($(targetEl).attr('name')) !== -1){ 1340 setTimeout(showFrontendOnlyNotice, 50); 1341 } else { 1342 setTimeout(showReminder, 50); 1343 } 1344 }); 1345 1346 /* Map move/zoom triggers save reminder */ 1347 if(self.map){ 1348 self.map.on('dragend', function(){ showReminder(); }); 1349 self.map.on('zoomchanged', function(){ showReminder(); }); 1350 } 1351 1352 /* Update visibility when preview is toggled */ 1353 $('#am-live-preview-toggle').on('change', function(){ 1354 if(shown){ 1355 if(self.enabled){ 1356 $('.am-save-reminder-preview').show(); 1357 $('.am-save-reminder-toolbar').hide(); 1358 } else { 1359 $('.am-save-reminder-toolbar').show(); 1360 $('.am-save-reminder-preview').hide(); 1361 } 1362 } 1363 }); 1364 } 1365 1366 /* ======================================================== 1367 PUBLIC API â for Pro to register components 1368 ======================================================== */ 1369 WPGMZA.AtlasMajorLivePreview.prototype.registerComponent = function(key, config){ 1370 COMPONENTS[key] = config; 1371 1372 /* Watch the enabled field and anchor field */ 1373 var self = this; 1374 if(config.anchorField){ 1375 $(document.body).trigger('wpgmza_live_preview_watch_field', [config.anchorField]); 1376 } 1377 } 1378 1379 WPGMZA.AtlasMajorLivePreview.prototype.getTemplatesContainer = function(){ 1380 return this.templates; 1381 } 1382 1383 WPGMZA.AtlasMajorLivePreview.prototype.getMap = function(){ 1384 return this.map; 1385 } 1386 1387 /* ======================================================== 1388 LIVE TILESET SWAP â Themes > Tileset card click applies 1389 the new tileset to the live preview map without needing a 1390 save. Map editor under Atlas Major previously showed only 1391 "Save map to apply tileset!" notification; this hooks the 1392 click and reloads the leaflet/maplibre tile layer in place. 1393 1394 Pulls config from the public tileservers.json (same source 1395 the PHP-side `TileServers::getList()` reads). One-shot
1396 fetch, cached on `WPGMZA.atlasMajorTileServers` so repeat 1397 clicks don't re-hit the network. 1398 1399 Engine guard: only runs on Leaflet-family engines (Google 1400 Maps tilesets are managed by Google's own type selector, 1401 not the OpenFreeMap/OSM tileset cards in this panel). 1402 ======================================================== */ 1403 jQuery(function($){ 1404 var tilesetCache = null; 1405 1406 function fetchTileServers(callback){ 1407 if(WPGMZA.atlasMajorTileServers){ 1408 callback(WPGMZA.atlasMajorTileServers); 1409 return; 1410 } 1411 if(tilesetCache){ 1412 callback(tilesetCache); 1413 return; 1414 } 1415 $.ajax({ 1416 url: WPGMZA.pluginDirURL + 'js/tileservers.json', 1417 dataType: 'json', 1418 cache: true, 1419 success: function(data){ 1420 /* Stamp slug into each entry, mirroring the PHP 1421 getList() behaviour. */ 1422 for(var slug in data){ 1423 if(data[slug] && typeof data[slug] === 'object'){ 1424 data[slug].slug = slug; 1425 } 1426 } 1427 tilesetCache = data; 1428 WPGMZA.atlasMajorTileServers = data; 1429 callback(data); 1430 }, 1431 error: function(){ 1432 /* Silent fail â tileset just won't update live; the 1433 pre-existing "Save map to apply" notification 1434 already covers the fallback path. */ 1435 } 1436 }); 1437 } 1438 1439 /* CRS is locked at leaflet map construction (leaflet-map.js:29-38 1440 sets it from `customTileMode` at init only). Trying to swap 1441 the layer type (custom image overlay <-> XYZ tiles) into a 1442 map whose CRS doesn't match the new layer renders the map 1443 grey â XYZ tiles request URLs in Web Mercator coords that 1444 don't translate inside L.CRS.Simple, and L.imageOverlay 1445 bounds get interpreted as lat/lng inside EPSG3857. The 1446 extended Simple CRS preserves `infinite: true`; EPSG3857 1447 doesn't set it â that's our reliable signal. */ 1448 function mapHasSimpleCRS(map){ 1449 return !!(map && map.leafletMap && map.leafletMap.options.crs && map.leafletMap.options.crs.infinite); 1450 } 1451 1452 /* Reload the leaflet/maplibre tile layer in place using the 1453 current map.settings values. Used by both the tileset card 1454 click handler and the custom-image field handlers â anything 1455 that changes a tile-layer-relevant setting calls this to 1456 visualise the change without a save. */ 1457 function reloadLiveTileLayer(){ 1458 if(!WPGMZA.atlasMajorLivePreview) return; 1459 var lp = WPGMZA.atlasMajorLivePreview; 1460 var map = lp.getMap && lp.getMap(); 1461 if(!map || !map.leafletMap) return; /* Leaflet-only path */ 1462 if(typeof map.getTileLayers !== 'function') return; 1463 1464 /* CRS-compat guard: only proceed when the map's locked CRS 1465 is appropriate for the layer we're about to add. The 1466 modal that fires on custom_tile_enabled toggle already 1467 tells the user a save+reload is needed â a fresh map 1468 construction will reset the CRS to match. Skipping the 1469 reload here keeps the visual stable (last good state) 1470 instead of going grey/blank. */ 1471 var wantSimple = !!(map.settings && map.settings.custom_tile_enabled); 1472 if(mapHasSimpleCRS(map) !== wantSimple) return; 1473 1474 if(map.tileLayers && map.tileLayers.length){ 1475 map.tileLayers.forEach(function(layer){ 1476 try { map.leafletMap.removeLayer(layer); } catch(e){} 1477 }); 1478 } 1479 1480 try { 1481 map.tileLayers = map.getTileLayers(); 1482 map.tileLayers.forEach(function(layer){ 1483 layer.addTo(map.leafletMap); 1484 }); 1485 1486 /* Deliberately NOT calling fitBounds(customTileBounds) 1487 here. The custom-image branch of getTileLayers sets 1488 customTileBounds to `[[0,0],[height,width]]` which is 1489 only valid in L.CRS.Simple â but the map was already 1490 constructed in EPSG3857 (Mercator) and CRS is locked 1491 at construction. Calling fitBounds against those 1492 bounds in Mercator pans the map to invalid lat/lng, 1493 which onCenterChanged writes into map_start_lat / 1494 map_start_lng form fields. Saving then sends garbage 1495 coords to the REST endpoint. The custom image 1496 overlay visually requires save+reload to display 1497 correctly anyway (so the map can be reconstructed 1498 with CRS.Simple), so live preview just no-ops the 1499 view shift here â the toggle still applies cleanly 1500 and the save path stays valid. */ 1501 } catch(e){ 1502 /* If something throws, fall back silently. */ 1503 } 1504 } 1505 1506 function applyTilesetToLivePreview(slug){ 1507 if(!WPGMZA.atlasMajorLivePreview) return; 1508 var lp = WPGMZA.atlasMajorLivePreview; 1509 var map = lp.getMap && lp.getMap(); 1510 if(!map) return; 1511 1512 fetchTileServers(function(servers){ 1513 var config = servers[slug]; 1514 if(!config){ 1515 /* Empty slug = "Global Default" â fall back to whatever 1516 the engine's default tile server URL is. */ 1517 if(!slug){ 1518 config = WPGMZA.tileServer || null; 1519 } 1520 if(!config) return; 1521 } 1522
1523 /* Update the in-memory settings so configureTileServer 1524 picks up the new override. tile_server_override is 1525 the slug; tile_server_override_config is the hydrated 1526 definition that getTileLayers reads via 1527 configureTileServer (map-settings.js:472-482). */ 1528 map.settings.tile_server_override = slug || ''; 1529 map.settings.tile_server_override_config = config; 1530 1531 /* For zerocost engine the URL also lives on the global 1532 settings under tile_server_url_leaflet_zerocost. Keep 1533 it in sync so getTileLayers' switch/case path returns 1534 the right base URL. */ 1535 if(WPGMZA.settings && WPGMZA.settings.engine === 'leaflet-zerocost' && config.url){ 1536 WPGMZA.settings.tile_server_url_leaflet_zerocost = config.url; 1537 } 1538 1539 reloadLiveTileLayer(); 1540 }); 1541 } 1542 1543 /* Bind the tileset card click. Body-delegated so it survives 1544 the panel re-renders that SidebarGroupings can do. */ 1545 $(document.body).on('click', '#wpgmza-tileset-panel .tileset-option', function(){ 1546 if(!document.querySelector('.wpgmza-atlas-major .am-preview-frame')) return; 1547 var slug = $(this).find('input[name="tile_server_override"]').val(); 1548 applyTilesetToLivePreview(slug); 1549 }); 1550 1551 /* Custom Image overlay â Themes > Custom Image tab. 1552 `configureTileServer` (map-settings.js:644-676) flips into 1553 `type: 'custom'` mode when `custom_tile_enabled` is true and 1554 `custom_tile_image` is set. getTileLayers then renders an 1555 L.imageOverlay over the explicit bounds derived from 1556 width/height. We mirror each form field into map.settings 1557 and reload the tile layer so the image appears immediately. 1558 1559 Toggling OFF reverts to the active tileset (via the same 1560 reload path â when custom_tile_enabled=false, configureTileServer 1561 skips the custom branch and returns the regular tile config). 1562 Field debounced so dragging width/height inputs doesn't 1563 spam reloads on every keystroke. */ 1564 var customTileTimer = null; 1565 function syncCustomTileFromForm(){ 1566 if(!WPGMZA.atlasMajorLivePreview) return; 1567 var lp = WPGMZA.atlasMajorLivePreview; 1568 var map = lp.getMap && lp.getMap(); 1569 if(!map || !map.settings) return; 1570 1571 map.settings.custom_tile_enabled = $('input[name="custom_tile_enabled"]').is(':checked') ? 1 : 0; 1572 map.settings.custom_tile_image = $('input[name="custom_tile_image"]').val() || ''; 1573 map.settings.custom_tile_image_width = $('input[name="custom_tile_image_width"]').val() || ''; 1574 map.settings.custom_tile_image_height = $('input[name="custom_tile_image_height"]').val() || ''; 1575 map.settings.custom_tile_image_attribution = $('input[name="custom_tile_image_attribution"]').val() || ''; 1576 1577 /* getTileLayers' custom branch only ever sets customTileMode 1578 and customTileBounds â never resets them. If the user 1579 toggles custom_tile_enabled off, those flags would stay 1580 true from the previous enable, leaking custom-tile state 1581 into subsequent renders (and into the saved form payload 1582 where leaflet-map.js reads them at construction). Reset 1583 them explicitly here whenever the toggle is off. */ 1584 if(!map.settings.custom_tile_enabled){ 1585 map.customTileMode = false; 1586 map.customTileBounds = null; 1587 map.customTileDimensions = null; 1588 1589 /* If the saved map_start_lat / map_start_lng came from a 1590 previous custom-tile session they're in L.CRS.Simple 1591 pixel coords (e.g., 400, 600 for an 800x600 image), 1592 not lat/lng. Saving those would break the map on 1593 next reload (leaflet-map.js constructs in EPSG3857 1594 without the customTileMode bounds-check at line 1595 40-44). Detect out-of-range values and reset to the 1596 plugin defaults from class.map.php:195-200 + 1597 class.database.php:334 (zoom 4). The setView call 1598 triggers MapEditPage.onCenterChanged which writes 1599 the new values into map_start_lat / map_start_lng / 1600 map_start_zoom inputs. */ 1601 var lat = parseFloat($('input[name="map_start_lat"]').val()); 1602 var lng = parseFloat($('input[name="map_start_lng"]').val()); 1603 var coordsInvalid = isNaN(lat) || isNaN(lng) || Math.abs(lat) > 90 || Math.abs(lng) > 180; 1604 1605 if(coordsInvalid && map.leafletMap){ 1606 var defaultLat = 36.778261; 1607 var defaultLng = -119.4179323999; 1608 var defaultZoom = 4; 1609 1610 try {
1611 map.leafletMap.setView([defaultLat, defaultLng], defaultZoom); 1612 } catch(e){} 1613 1614 /* Force-write in case setView's moveend doesn't fire 1615 in time before a save click. */ 1616 $('input[name="map_start_lat"]').val(defaultLat).trigger('change'); 1617 $('input[name="map_start_lng"]').val(defaultLng).trigger('change'); 1618 $('input[name="map_start_zoom"]').val(defaultZoom).trigger('change'); 1619 } 1620 } 1621 } 1622 1623 function applyCustomTileLive(){ 1624 clearTimeout(customTileTimer); 1625 customTileTimer = setTimeout(function(){ 1626 syncCustomTileFromForm(); 1627 reloadLiveTileLayer(); 1628 }, 200); 1629 } 1630 1631 /* Modal that intercepts custom-image enable/disable. Because 1632 the leaflet CRS is locked at map construction 1633 (leaflet-map.js:29-38), live preview can't visually swap 1634 between image overlay (CRS.Simple) and XYZ tiles (EPSG3857) 1635 without rebuilding the entire engine. Instead, we ask the 1636 user to confirm a save+reload â clicking the savemap submit 1637 button POSTs the form and the page reloads with the new 1638 CRS. Cancel reverts the toggle so the form state stays 1639 consistent with what's on screen. */ 1640 var customTileModal = null; 1641 var customTilePreviousState = null; 1642 1643 /* Full-screen blocking overlay shown while the editor saves and 1644 reloads. Inline-styled so we don't have to ship CSS through 1645 the build pipeline for a one-off element. z-index sits above 1646 modals (99999) and Leaflet's panes. The reload typically 1647 takes <2s; the overlay is removed automatically when the 1648 page navigates. */ 1649 function showLivePreviewReloadingMask(){ 1650 if(document.getElementById('wpgmza-live-preview-reloading-mask')) return; 1651 1652 var mask = document.createElement('div'); 1653 mask.id = 'wpgmza-live-preview-reloading-mask'; 1654 mask.style.cssText = [ 1655 'position:fixed', 1656 'top:0', 1657 'left:0', 1658 'width:100vw', 1659 'height:100vh', 1660 'background:rgba(0,0,0,0.55)', 1661 'z-index:999999', 1662 'display:flex', 1663 'align-items:center', 1664 'justify-content:center', 1665 'cursor:wait', 1666 'font-family:"Plus Jakarta Sans",system-ui,sans-serif' 1667 ].join(';'); 1668 1669 var inner = document.createElement('div'); 1670 inner.style.cssText = [ 1671 'background:#fff', 1672 'color:#1f2937', 1673 'padding:20px 28px', 1674 'border-radius:10px', 1675 'font-size:14px', 1676 'font-weight:600', 1677 'box-shadow:0 8px 32px rgba(0,0,0,0.25)', 1678 'display:flex', 1679 'align-items:center', 1680 'gap:12px' 1681 ].join(';'); 1682 1683 var spinner = document.createElement('div'); 1684 spinner.style.cssText = [ 1685 'width:18px', 1686 'height:18px', 1687 'border:2px solid #e5e7eb', 1688 'border-top-color:#e8473f', 1689 'border-radius:50%', 1690 'animation:wpgmza-mask-spin 0.7s linear infinite' 1691 ].join(';'); 1692 1693 /* Inject the keyframes once. Scoped via the unique id so 1694 we don't collide with anything else. */ 1695 if(!document.getElementById('wpgmza-mask-keyframes')){ 1696 var style = document.createElement('style'); 1697 style.id = 'wpgmza-mask-keyframes'; 1698 style.textContent = '@keyframes wpgmza-mask-spin{to{transform:rotate(360deg)}}'; 1699 document.head.appendChild(style); 1700 } 1701 1702 inner.appendChild(spinner); 1703 inner.appendChild(document.createTextNode('Saving and reloading editorâ¦')); 1704 mask.appendChild(inner); 1705 document.body.appendChild(mask); 1706 } 1707 1708 function buildCustomTileModal(){ 1709 /* Must inject inside the .wpgmza-atlas-major wrapper â 1710 the generic-modal CSS in input.css (~line 2548) is 1711 scoped under that selector, so appending to <body> 1712 leaves the modal unstyled and stuck in document flow 1713 below the page. */ 1714 var $host = $('.wpgmza-atlas-major').first(); 1715 if(!$host.length) $host = $('body'); 1716 1717 var html = '' 1718 + '<div class="wpgmza-generic-modal wpgmza-custom-tile-modal" data-type="custom_tile_save_reload">' 1719 + '<div class="wpgmza-generic-modal-inner">' 1720 + '<div class="wpgmza-generic-modal-title">' 1721 + 'Save and reload required' 1722 + '</div>' 1723 + '<div class="wpgmza-generic-modal-content">' 1724 + 'Disabling or enabling this feature requires the map to be saved and the editor reloaded. Click <strong>Save & Reload</strong> to apply this change now, or <strong>Cancel</strong> to revert.' 1725 + '</div>' 1726 + '<div class="wpgmza-generic-modal-actions">'
1727 + '<div class="wpgmza-row">' 1728 + '<div class="wpgmza-col">' 1729 + '<div class="wpgmza-button" data-action="cancel"><span>Cancel</span></div>' 1730 + '<div class="wpgmza-button" data-action="complete"><span>Save & Reload</span></div>' 1731 + '</div>' 1732 + '</div>' 1733 + '</div>' 1734 + '</div>' 1735 + '</div>'; 1736 $host.append(html); 1737 1738 return WPGMZA.GenericModal.createInstance( 1739 $host.find('.wpgmza-custom-tile-modal').get(0), 1740 /* complete -> save+reload */ 1741 function(){ 1742 /* Atlas Major's autosave hijacks form submit and 1743 saves via REST instead of POST, so a plain 1744 submit-button click only saves â the page 1745 never reloads. Set the reloadAfterSave flag 1746 on the autosave so its REST success handler 1747 triggers location.reload() (atlas-major- 1748 autosave.js, success branch). */ 1749 if(WPGMZA.atlasMajorAutoSave){ 1750 WPGMZA.atlasMajorAutoSave.reloadAfterSave = true; 1751 } 1752 1753 /* Cover the editor with a non-interactive overlay 1754 so the user can't fire other actions during the 1755 save+reload window. */ 1756 showLivePreviewReloadingMask(); 1757 1758 /* Trigger the same submit input the visible Save 1759 Map label points at â keeps the save flow 1760 identical to a manual click. */ 1761 var $btn = $('input[name="wpgmza_savemap"]'); 1762 if($btn.length){ 1763 $btn.trigger('click'); 1764 } 1765 }, 1766 /* cancel -> revert checkbox */ 1767 function(){ 1768 if(customTilePreviousState !== null){ 1769 $('input[name="custom_tile_enabled"]') 1770 .prop('checked', customTilePreviousState) 1771 .trigger('change.revert'); 1772 } 1773 } 1774 ); 1775 } 1776 1777 function showCustomTileSaveReloadModal(){ 1778 if(!customTileModal){ 1779 customTileModal = buildCustomTileModal(); 1780 } 1781 customTileModal.show(); 1782 } 1783 1784 $(document.body).on('change', 1785 'input[name="custom_tile_enabled"]', 1786 function(event){ 1787 if(!document.querySelector('.wpgmza-atlas-major .am-preview-frame')) return; 1788 1789 /* Skip when the change is our own revert â namespaced 1790 `change.revert` lets us put the checkbox back without 1791 re-firing the modal. */ 1792 if(event && event.namespace === 'revert') return; 1793 1794 /* Stash the inverse of the new state so cancel can 1795 revert. The change event fires AFTER the checkbox 1796 value flipped, so previous = !current. */ 1797 customTilePreviousState = !$(this).is(':checked'); 1798 1799 applyCustomTileLive(); 1800 showCustomTileSaveReloadModal(); 1801 } 1802 ); 1803 1804 $(document.body).on('change', 1805 'input[name="custom_tile_image"]', 1806 function(){ 1807 if(!document.querySelector('.wpgmza-atlas-major .am-preview-frame')) return; 1808 applyCustomTileLive(); 1809 } 1810 ); 1811 $(document.body).on('change input', 1812 'input[name="custom_tile_image_width"], input[name="custom_tile_image_height"], input[name="custom_tile_image_attribution"]', 1813 function(){ 1814 if(!document.querySelector('.wpgmza-atlas-major .am-preview-frame')) return; 1815 applyCustomTileLive(); 1816 } 1817 ); 1818 }); 1819 1820 /* ======================================================== 1821 BOOTSTRAP 1822 ======================================================== */ 1823 function tryInitLivePreview(){ 1824 if(WPGMZA.atlasMajorLivePreview) return; 1825 if(WPGMZA.maps && WPGMZA.maps[0] && document.querySelector('.am-preview-frame')){ 1826 WPGMZA.atlasMajorLivePreview = new WPGMZA.AtlasMajorLivePreview(); 1827 } 1828 } 1829 1830 $(document.body).on('wpgmza_map_edit_page_created', function(){ 1831 setTimeout(tryInitLivePreview, 1200); 1832 }); 1833 1834 /* Fallback poll */ 1835 var pollCount = 0; 1836 var pollInterval = setInterval(function(){ 1837 pollCount++; 1838 if(WPGMZA.atlasMajorLivePreview || pollCount > 30){ 1839 clearInterval(pollInterval); 1840 return; 1841 } 1842 tryInitLivePreview(); 1843 }, 1000); 1844 1845 1846 /* ======================================================== 1847 COLOR PICKER REPARENT (Live Preview mode) 1848 -------------------------------------------------------- 1849 Lives in this file (rather than a standalone module) so 1850 it inherits a known-good loading position in the base 1851 combined bundle. A previous attempt as its own file got 1852 misrouted into the Pro bundle by the build CLI, throwing 1853 WPGMZA-is-not-defined and breaking the whole Pro bundle. 1854 -------------------------------------------------------- 1855 Why this exists: the color picker is appended to 1856 `.map_wrapper` via data-container, ending up nested in 1857 the map engine's DOM. The map engine applies transforms 1858 on inner layers, which hijack `position: fixed` 1859 (containing block becomes the transformed ancestor 1860 instead of the viewport). Plus `.content` and 1861 `.am-preview-page` both clip overflow. CSS-only fixes 1862 can't escape both. 1863 -------------------------------------------------------- 1864 When LP is on: detach picker from map DOM, append to a 1865 body-level host wrapper, position via getBoundingClientRect 1866 relative to the trigger swatch. Host carries 1867 `.wpgmza-color-input-host` so the existing picker CSS 1868 (sizing, palette layout, .active display toggle) still 1869 applies. When LP is off (or toggled off mid-session): 1870 restore picker to original parent on next open so the
1871 base `left: 10px inside .map_wrapper` rule applies. */ 1872 if(document.querySelector('.wpgmza-atlas-major') && WPGMZA.currentPage === 'map-edit'){ 1873 var $lpColorHost = null; 1874 1875 var ensureLpColorHost = function(){ 1876 if(!$lpColorHost){ 1877 $lpColorHost = $('<div class="wpgmza-color-input-host wpgmza-atlas-major am-lp-color-host"></div>'); 1878 $lpColorHost.css({ 1879 position: 'fixed', 1880 zIndex: 100000, 1881 width: '200px' 1882 }); 1883 $('body').append($lpColorHost); 1884 } 1885 return $lpColorHost; 1886 }; 1887 1888 var repositionLpColorHostNear = function(triggerEl){ 1889 var host = ensureLpColorHost(); 1890 var rect = triggerEl.getBoundingClientRect(); 1891 var $picker = host.find('.wpgmza-color-picker').first(); 1892 var pickerHeight = ($picker.length && $picker[0].offsetHeight) || 370; 1893 var vh = window.innerHeight; 1894 var vw = window.innerWidth; 1895 1896 /* Default placement: below the trigger swatch, aligned 1897 to its left edge. Standard popover behaviour. */ 1898 var topPos = rect.bottom + 4; 1899 var leftPos = rect.left; 1900 1901 /* Flip above the trigger if there's no room below. */ 1902 if(topPos + pickerHeight > vh - 10){ 1903 var aboveTop = rect.top - pickerHeight - 4; 1904 if(aboveTop >= 10){ 1905 topPos = aboveTop; 1906 } else { 1907 topPos = Math.max(10, vh - pickerHeight - 10); 1908 } 1909 } 1910 1911 /* Clamp horizontally so the 200px-wide picker can't 1912 render off-screen. */ 1913 if(leftPos + 200 > vw - 10) leftPos = vw - 210; 1914 if(leftPos < 10) leftPos = 10; 1915 1916 host.css({ 1917 top: topPos + 'px', 1918 left: leftPos + 'px' 1919 }); 1920 }; 1921 1922 var restoreLpPicker = function($picker){ 1923 var origParent = $picker.data('wpgmza-orig-parent'); 1924 if(origParent && document.body.contains(origParent)){ 1925 $(origParent).append($picker); 1926 } 1927 }; 1928 1929 /* Track the currently-open ColorInput instance so the 1930 outside-click handler below can close it on any tab / 1931 panel click that would otherwise leave the picker 1932 stranded. color-input.js binds its own body-click 1933 autoClose listener, but tab navigation in Atlas Major 1934 (sidebar groupings) sometimes stops propagation, so the 1935 picker's own listener doesn't fire and the picker stays 1936 visible â and because the host gets re-evaluated against 1937 a stale/hidden trigger, it can shoot to viewport-top-left. 1938 This defensive document-level handler ensures the picker 1939 closes whenever the user clicks anywhere outside the 1940 picker and its trigger swatch. */ 1941 var lpActiveColorInstance = null; 1942 1943 $(document.body).on('colorpicker.open.wpgmza', function(event){ 1944 if(!event.instance || !event.instance.picker) return; 1945 1946 lpActiveColorInstance = event.instance; 1947 1948 var $picker = $(event.instance.picker); 1949 var trigger = event.instance.preview; 1950 if(!trigger || !trigger.length) return; 1951 1952 var lpActive = !!document.querySelector('.am-preview-frame:not(.am-preview-off)'); 1953 var inHost = $picker.closest('.am-lp-color-host').length > 0; 1954 1955 if(lpActive){ 1956 if(!inHost){ 1957 if(!$picker.data('wpgmza-orig-parent')){ 1958 $picker.data('wpgmza-orig-parent', $picker.parent()[0]); 1959 } 1960 ensureLpColorHost().append($picker); 1961 } 1962 repositionLpColorHostNear(trigger[0]); 1963 } else if(inHost){ 1964 /* LP toggled off between open events â return picker 1965 to original parent so the base `left: 10px inside 1966 .map_wrapper` CSS rule applies again. */ 1967 restoreLpPicker($picker); 1968 } 1969 }); 1970 1971 /* Defensive outside-click handler â runs on mousedown 1972 (earlier than click) so we close the picker even if the 1973 downstream click handler (e.g. a sidebar tab switch) 1974 later stops propagation and prevents color-input.js's 1975 own body listener from firing. 1976 Bind once on document; uses lpActiveColorInstance tracked 1977 above to know which instance to close. Safe to fire even 1978 if the picker's own listener also closes it â onTogglePicker 1979 is gated on `state.open` so the second call is a no-op. */ 1980 $(document).on('mousedown.amlpcolor', function(e){ 1981 if(!lpActiveColorInstance) return; 1982 if(!lpActiveColorInstance.state || !lpActiveColorInstance.state.open) return; 1983 1984 var $target = $(e.target); 1985 /* Clicks inside the body-level host (where the picker 1986 itself lives once reparented) should NOT close. */ 1987 if($target.closest('.am-lp-color-host').length) return; 1988 /* Clicks on any color preview / swatch should NOT close 1989 either â the preview's own click handler manages the 1990 toggle and stopPropagation. */ 1991 if($target.closest('.wpgmza-color-preview').length) return; 1992 /* Clicks inside the picker (if not reparented, e.g. 1993 LP off) should also be ignored. */ 1994 if($target.closest('.wpgmza-color-picker').length) return; 1995 1996 try { 1997 lpActiveColorInstance.onTogglePicker(); 1998 } catch(e){} 1999 lpActiveColorInstance = null; 2000 }); 2001 } 2002 2003});
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.