1/** 2 * @namespace WPGMZA 3 * @module AtlasMajorTabs 4 * @requires WPGMZA.SidebarGroupings 5 * 6 * Bridges the Atlas Major tab bar to the existing SidebarGroupings navigation system. 7 * Tabs carry data-am-group attributes that map to data-group values on groupings. 8 */ 9jQuery(function($) { 10 11 if(WPGMZA.currentPage != "map-edit") 12 return; 13 14 if(!document.querySelector('.wpgmza-atlas-major')) 15 return; 16 17 WPGMZA.AtlasMajorTabs = function(){ 18 var self = this; 19 this.tabBar = $('.am-tabs'); 20 this.tabs = this.tabBar.find('.am-tab[data-am-group]'); 21 this.activeGroup = null; 22 23 this.tabs.on('click', function(){ 24 var groupId = $(this).data('am-group'); 25 if(groupId){ 26 /* Belt-and-braces: discard any in-progress drawing 27 BEFORE switching tabs. The `grouping-closed` 28 listener further down also handles this path (via 29 closeAll â grouping-closed â our synthesized 30 feature-block-closed), but firing here too 31 guarantees the discard runs regardless of the 32 closeAll timing or any race with the tab switch. 33 discardChanges is gated on `this.feature` so a 34 double-discard is a no-op. */ 35 $('.grouping[data-feature-discard]').each(function(){ 36 $(this).trigger('feature-block-closed'); 37 }); 38 39 self.activateTab($(this), groupId); 40 } 41 }); 42 43 /* Make the entire .heading.has-back row clickable, not just 44 the small caret-left arrow on its left edge. The core 45 SidebarGroupings handler binds to `.grouping .item` clicks 46 only, so anywhere else in the heading row was inert. 47 This caused a confusing UX: the whole row hover-highlights 48 (signalling clickability) but only the arrow column actually 49 navigated back. We forward clicks anywhere on the row to 50 the inner .item.caret-left, which then bubbles through the 51 existing handler. Scoped to .wpgmza-atlas-major so Atlas 52 Novus behaviour is unchanged. */ 53 $(document.body).on('click', '.wpgmza-atlas-major .heading.has-back', function(event){ 54 /* If the click hit the .item directly, the existing 55 SidebarGroupings delegation will handle it â don't 56 double-fire. */ 57 if($(event.target).closest('.item').length) return; 58 $(this).find('> .item').first().trigger('click'); 59 }); 60 61 /* Release the drawing manager when a feature grouping is 62 being closed by navigation away (e.g. clicking a settings 63 tab while in the Circles / Polygons / etc. feature panel 64 with the drawing manager active). 65 Background: sidebar-groupings.js:openTabByGroupId fires 66 `feature-block-closed` ONLY when the TARGET grouping has 67 `data-feature-discard="true"` â that's the 68 feature-to-feature switch path (Circles â Polygons fires 69 discard on the polygon grouping; all FeaturePanels 70 listen on `.grouping` and react). Settings groupings 71 don't carry `data-feature-discard`, so the 72 feature-to-settings path NEVER fires the trigger â 73 drawing manager stays active, the in-progress shape stays 74 parked, and the user can't return to that feature panel 75 without a page reload. 76 Fix: listen for `grouping-closed` (fired by 77 sidebar-groupings.js:closeAll for every closing 78 grouping) and synthesize a `feature-block-closed` 79 event on any closing grouping that carries 80 `data-feature-discard`. The existing 81 feature-panel.js:51 listener then triggers 82 onTabDeactivated â discardChanges and resets drawing 83 mode to MODE_NONE. Works regardless of navigation path 84 (tab bar, back arrow, programmatic switch). */ 85 $(document.body).on('grouping-closed', function(event, groupId){ 86 if(!groupId) return; 87 var closingGrouping = $('.grouping[data-group="' + groupId + '"]').first(); 88 if(!closingGrouping.length) return; 89 if(closingGrouping.data('feature-discard')){ 90 closingGrouping.trigger('feature-block-closed'); 91 } 92 }); 93 94 /* Listen for grouping changes from other sources (context menu, quick actions) */ 95 $(document.body).on('grouping-opened', function(event, groupId){ 96 self.syncTabState(groupId); 97 }); 98 99 /* Also catch the event on .grouping elements directly (jQuery trigger doesn't always bubble) */ 100 $('.grouping').on('grouping-opened', function(event, groupId){ 101 self.syncTabState(groupId); 102 }); 103 104 /* Activate the first tab on load. Retry if 105 * WPGMZA.mapEditPage.sidebarGroupings isn't ready yet â the 106 * AtlasMajorTabs constructor runs on window.load, but 107 * sidebarGroupings is created during MapEditPage's own init 108 * which can lag. Without the retry we'd silently bail and the 109 * markers grouping would stay `display:none` until the user 110 * clicks the tab themselves. */ 111 var firstTab = this.tabs.filter('.on').first(); 112 if(firstTab.length){ 113 var initialGroup = firstTab.data('am-group'); 114 if(initialGroup){ 115 var attempts = 0; 116 var tryActivate = function(){ 117 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.sidebarGroupings){ 118 self.activateTab(firstTab, initialGroup); 119 return true; 120 } 121 return false;
122 }; 123 if(!tryActivate()){ 124 var retry = setInterval(function(){ 125 attempts++; 126 if(tryActivate() || attempts > 30){ 127 clearInterval(retry); 128 } 129 }, 100); 130 } 131 } 132 } 133 134 /* Sub-tab navigation (horizontal tabs within panels like Store Locator) */ 135 $('.am-subnav').on('click', '.am-subnav-item', function(){ 136 var subnav = $(this).closest('.am-subnav'); 137 subnav.find('.am-subnav-item').removeClass('on'); 138 $(this).addClass('on'); 139 }); 140 141 /* Sync sub-tab active state when groupings open from other sources */ 142 $(document.body).on('grouping-opened', function(event, groupId){ 143 $('.am-subnav .am-subnav-item').each(function(){ 144 var itemGroup = $(this).data('group'); 145 if(itemGroup === groupId){ 146 $(this).closest('.am-subnav').find('.am-subnav-item').removeClass('on'); 147 $(this).addClass('on'); 148 } 149 }); 150 }); 151 152 /* Auto-focus the Address/GPS input when the marker editor opens in 153 Add mode. FeaturePanel flips the visibility of 154 .wpgmza-feature-drawing-instructions vs 155 .wpgmza-feature-editing-instructions (showInstructions) on mode 156 change â we defer one tick so that DOM swap has landed before 157 we check. */ 158 $(document.body).on('grouping-opened', function(event, groupId){ 159 if(groupId !== 'map-markers-editor'){ 160 return; 161 } 162 setTimeout(function(){ 163 var grouping = $('[data-group="map-markers-editor"]'); 164 var drawing = grouping.find('.wpgmza-feature-drawing-instructions'); 165 if(drawing.length && drawing.is(':visible')){ 166 var addressInput = grouping.find('#wpgmza_add_address_map_editor'); 167 if(addressInput.length && !addressInput.is(':disabled')){ 168 addressInput.trigger('focus'); 169 } 170 } 171 }, 0); 172 }); 173 174 /* Sync toolbar map name with the form field */ 175 this.syncToolbarFields(); 176 } 177 178 /* Groupings that are navigation hubs â when opened, auto-navigate 179 to the first visible child item */ 180 WPGMZA.AtlasMajorTabs.AUTO_NAV_HUBS = [ 181 'map-settings-themes' 182 ]; 183 184 WPGMZA.AtlasMajorTabs.prototype.activateTab = function(tabElement, groupId){ 185 /* Update tab active state */ 186 this.tabs.removeClass('on'); 187 tabElement.addClass('on'); 188 this.activeGroup = groupId; 189 190 /* Open the grouping via SidebarGroupings */ 191 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.sidebarGroupings){ 192 WPGMZA.mapEditPage.sidebarGroupings.openTabByGroupId(groupId); 193 194 /* For nav hubs, auto-click the first visible child sub-tab */ 195 if(WPGMZA.AtlasMajorTabs.AUTO_NAV_HUBS.indexOf(groupId) !== -1){ 196 setTimeout(function(){ 197 var grouping = $('[data-group="' + groupId + '"]'); 198 /* Try visible am-subnav-item first (Atlas Major sub-tabs) */ 199 var firstVisible = grouping.find('.am-subnav .am-subnav-item:visible').first(); 200 if(!firstVisible.length){ 201 /* Fallback to hidden nav items */ 202 firstVisible = grouping.find('.am-hidden-nav .item.caret-right').first(); 203 } 204 if(firstVisible.length){ 205 firstVisible.trigger('click'); 206 } 207 }, 50); 208 } 209 } 210 } 211 212 WPGMZA.AtlasMajorTabs.prototype.syncTabState = function(groupId){ 213 /* Find the tab that owns this group or a parent of it */ 214 var matched = false; 215 var self = this; 216 217 this.tabs.each(function(){ 218 var tabGroup = $(this).data('am-group'); 219 220 /* Direct match */ 221 if(tabGroup === groupId){ 222 self.tabs.removeClass('on'); 223 $(this).addClass('on'); 224 self.activeGroup = groupId; 225 matched = true; 226 return false; 227 } 228 229 /* Prefix match â e.g. tab targets "map-settings-store-locator-general" 230 but opened group is "map-settings-store-locator-style". 231 Strip the last segment of both and compare the prefix. 232 Only applies when prefix has 3+ segments to avoid false matches 233 like "map-markers" matching "map-datasets". */ 234 var tabPrefix = tabGroup.replace(/-[^-]+$/, ''); 235 var groupPrefix = groupId.replace(/-[^-]+$/, ''); 236 var segmentCount = tabPrefix.split('-').length; 237 if(tabPrefix === groupPrefix && tabPrefix !== tabGroup && segmentCount >= 3){ 238 self.tabs.removeClass('on'); 239 $(this).addClass('on'); 240 self.activeGroup = tabGroup; 241 matched = true; 242 return false;
243 } 244 }); 245 246 if(!matched){ 247 /* Check if the opened group is a child of a tab's group */ 248 this.tabs.each(function(){ 249 var tabGroup = $(this).data('am-group'); 250 var openedGrouping = $('[data-group="' + groupId + '"]'); 251 252 /* Walk up the back-navigation chain to find parent */ 253 var backItem = openedGrouping.find('.heading .item.caret-left[data-group]'); 254 while(backItem.length){ 255 var parentGroup = backItem.data('group'); 256 if(parentGroup === tabGroup){ 257 self.tabs.removeClass('on'); 258 $(this).addClass('on'); 259 self.activeGroup = tabGroup; 260 matched = true; 261 return false; 262 } 263 var parentGrouping = $('[data-group="' + parentGroup + '"]'); 264 backItem = parentGrouping.find('.heading .item.caret-left[data-group]'); 265 } 266 }); 267 } 268 } 269 270 WPGMZA.AtlasMajorTabs.prototype.syncToolbarFields = function(){ 271 var titleInput = $('#wpgmza_title'); 272 var toolbarTitle = $('#am_wpgmza_title'); 273 var shortcodeInput = $('#shortcode_input'); 274 var toolbarShortcode = $('#am_shortcode_display'); 275 276 if(titleInput.length && toolbarTitle.length){ 277 /* Initial sync */ 278 toolbarTitle.val(titleInput.val()); 279 280 /* Two-way bind */ 281 toolbarTitle.on('input change', function(){ 282 titleInput.val($(this).val()).trigger('change'); 283 }); 284 titleInput.on('input change', function(){ 285 toolbarTitle.val($(this).val()); 286 }); 287 } 288 289 if(shortcodeInput.length && toolbarShortcode.length){ 290 /* Sync shortcode display â PHP sets the value attribute server-side */ 291 var updateShortcode = function(){ 292 /* Try property first, then attribute, then raw DOM */ 293 var val = shortcodeInput.val() 294 || shortcodeInput.attr('value') 295 || shortcodeInput[0].getAttribute('value') 296 || ''; 297 if(val && val.trim()){ 298 toolbarShortcode.text(val); 299 return true; 300 } 301 return false; 302 }; 303 304 shortcodeInput.on('input change', updateShortcode); 305 306 /* Immediate attempt */ 307 if(!updateShortcode()){ 308 /* Poll until value appears */ 309 var pollCount = 0; 310 var pollInterval = setInterval(function(){ 311 if(updateShortcode() || pollCount > 30){ 312 clearInterval(pollInterval); 313 } 314 pollCount++; 315 }, 500); 316 } 317 } 318 319 /* Copy shortcode on pill click */ 320 $('.am-sc-pill').on('click', function(){ 321 var text = (shortcodeInput.val() || toolbarShortcode.text() || '').trim(); 322 if(!text) return; 323 324 /* Use temp input + execCommand as primary (works in all contexts) */ 325 var $temp = $('<input>'); 326 $('body').append($temp); 327 $temp.val(text).select(); 328 document.execCommand('copy'); 329 $temp.remove(); 330 331 if(WPGMZA.notification){ 332 WPGMZA.notification("Shortcode Copied"); 333 } 334 }); 335 336 /* Override the core .wpgmza_copy_shortcode handler â unbind then rebind with centered notification */ 337 $('body').off('click', '.wpgmza_copy_shortcode'); 338 $('body').on('click', '.wpgmza_copy_shortcode', function(){ 339 var text = $(this).val(); 340 if(!text) return; 341 342 var $temp = $('<input>'); 343 $('body').append($temp); 344 $temp.val(text).select(); 345 document.execCommand('copy'); 346 $temp.remove(); 347 348 if(WPGMZA.notification){ 349 WPGMZA.notification("Shortcode Copied"); 350 } 351 }); 352 } 353 354 /** 355 * Quick-add marker: when user types address in the markers tab bar 356 * and clicks Add, populate the editor's address field and navigate to editor. 357 */ 358 WPGMZA.AtlasMajorTabs.prototype.initQuickAddMarker = function(){ 359 var self = this; 360 var quickInput = $('#am-quick-add-address'); 361 var quickBtn = $('.am-marker-quick-add'); 362 var resultsContainer = $('#am-quick-add-autocomplete-results'); 363 364 if(!quickInput.length || !quickBtn.length) 365 return; 366 367 /* Add wpgmza-address class NOW (after MapEditPage init has finished 368 its $("input.wpgmza-address").each loop) so the body-level keypress 369 delegation picks up our input, but no AddressInput/Google autocomplete 370 gets created on it. */ 371 quickInput.addClass('wpgmza-address'); 372 373 /* Override shouldAddressFieldUseEnhancedAutocomplete so the core 374 keypress handler on .wpgmza-address also fires for our input. 375 We wrap the original method â no core code changes needed. */ 376 if(WPGMZA.mapEditPage){ 377 var originalCheck = WPGMZA.mapEditPage.shouldAddressFieldUseEnhancedAutocomplete; 378 WPGMZA.mapEditPage.shouldAddressFieldUseEnhancedAutocomplete = function(element){
379 if(element && element.id === 'am-quick-add-address'){ 380 return true; 381 } 382 return originalCheck.call(this, element); 383 }; 384 } 385 386 /* The core renders results into #wpgmza_autocomplete_search_results 387 which lives inside the editor accordion. We observe that element 388 and mirror its content into our local results container. */ 389 var coreResults = $('#wpgmza_autocomplete_search_results'); 390 var quickAddActive = false; 391 392 quickInput.on('focusin', function(){ quickAddActive = true; }); 393 quickInput.on('focusout', function(){ 394 setTimeout(function(){ 395 quickAddActive = false; 396 resultsContainer.hide(); 397 }, 400); 398 }); 399 400 var coreDisabled = $('#wpgmza_autoc_disabled'); 401 402 function mirrorCoreElement(coreEl, localContainer){ 403 if(!coreEl.length) return; 404 405 var observer = new MutationObserver(function(){ 406 if(!quickAddActive) return; 407 var html = coreEl.html(); 408 if(html && html.trim().length > 0){ 409 localContainer.html(html).show(); 410 coreEl.hide(); 411 } else { 412 localContainer.hide(); 413 } 414 }); 415 observer.observe(coreEl[0], { childList: true, subtree: true, characterData: true }); 416 417 var styleObserver = new MutationObserver(function(){ 418 if(!quickAddActive) return; 419 if(coreEl.is(':visible') && coreEl.html().trim().length > 0){ 420 localContainer.html(coreEl.html()).show(); 421 coreEl.hide(); 422 } 423 }); 424 styleObserver.observe(coreEl[0], { attributes: true, attributeFilter: ['style'] }); 425 } 426 427 /* Mirror both the results and the disabled/warning div */ 428 mirrorCoreElement(coreResults, resultsContainer); 429 430 if(coreDisabled.length){
431 var disabledContainer = $('<div id="am-quick-add-autoc-disabled"></div>'); 432 quickInput.closest('.am-ml-bar').append(disabledContainer); 433 disabledContainer.hide(); 434 mirrorCoreElement(coreDisabled, disabledContainer); 435 } 436 437 /* When a result is clicked in our container, fill our input */ 438 resultsContainer.on('click', '.wpgmza_ac_result', function(e){ 439 e.preventDefault(); 440 e.stopPropagation(); 441 var index = $(this).data('id'); 442 var lat = $(this).data('lat'); 443 var lng = $(this).data('lng'); 444 var name = $(this).find('[id^="wpgmza_item_address_"]').html() || $(this).text(); 445 quickInput.val(name.trim()); 446 resultsContainer.hide(); 447 }); 448 449 /* When "Add" is clicked, copy the address to the editor input 450 and navigate to the editor grouping */ 451 quickBtn.on('click', function(e){ 452 var address = quickInput.val().trim(); 453 454 setTimeout(function(){ 455 if(address){ 456 var editorAddress = $('#wpgmza_add_address_map_editor'); 457 if(editorAddress.length){ 458 editorAddress.val(address).trigger('change').trigger('input'); 459 } 460 } 461 quickInput.val(''); 462 resultsContainer.hide(); 463 }, 200); 464 }); 465 466 /* Enter key in quick input triggers Add */ 467 quickInput.on('keydown', function(e){ 468 if(e.key === 'Enter'){ 469 e.preventDefault(); 470 quickBtn.trigger('click'); 471 } 472 }); 473 } 474 475 /* Initialize after map edit page is ready */ 476 $(document.body).on('wpgmza_map_edit_page_created', function(){ 477 if(!WPGMZA.atlasMajorTabs){ 478 WPGMZA.atlasMajorTabs = new WPGMZA.AtlasMajorTabs(); 479 WPGMZA.atlasMajorTabs.initQuickAddMarker(); 480 } 481 }); 482 483 /* Fallback initialization */ 484 $(window).on('load', function(){ 485 if(!WPGMZA.atlasMajorTabs && document.querySelector('.wpgmza-atlas-major')){ 486 WPGMZA.atlasMajorTabs = new WPGMZA.AtlasMajorTabs(); 487 WPGMZA.atlasMajorTabs.initQuickAddMarker(); 488 } 489 initDualZoomSliders(); 490 }); 491 492 /* ======================================================== 493 DUAL-HANDLE ZOOM RANGE SLIDER 494 Visual wrapper around the hidden map_max_zoom / map_min_zoom 495 inputs. Two overlapping range inputs share a track; the fill 496 bar shows the allowed zoom range between them. 497 ======================================================== */ 498 function initDualZoomSliders(){ 499 $('.wpgmza-dual-zoom-slider').each(function(){ 500 var $slider = $(this); 501 if($slider.data('wpgmza-dual-zoom-inited')) return; 502 $slider.data('wpgmza-dual-zoom-inited', true); 503 504 var $outInput = $slider.find('.wpgmza-dual-zoom-out-input'); 505 var $inInput = $slider.find('.wpgmza-dual-zoom-in-input'); 506 var $outLabel = $slider.find('.wpgmza-dual-zoom-out-label'); 507 var $inLabel = $slider.find('.wpgmza-dual-zoom-in-label'); 508 var $fill = $slider.find('.wpgmza-dual-zoom-fill'); 509 510 /* Sync from hidden inputs (holds saved values) into the range inputs */ 511 var $hiddenOut = $slider.closest('fieldset').find('input[name="map_max_zoom"]'); 512 var $hiddenIn = $slider.closest('fieldset').find('input[name="map_min_zoom"]'); 513 514 var savedOut = parseInt($hiddenOut.val()); 515 var savedIn = parseInt($hiddenIn.val()); 516 if(!isNaN(savedOut)) $outInput.val(savedOut); 517 if(!isNaN(savedIn)) $inInput.val(savedIn); 518 519 var min = parseInt($slider.data('min')) || 0; 520 var max = parseInt($slider.data('max')) || 22; 521 522 function update(fromInput){ 523 var outVal = parseInt($outInput.val()); 524 var inVal = parseInt($inInput.val()); 525 526 /* Enforce outVal <= inVal, push whichever handle is driving */ 527 if(outVal > inVal){ 528 if(fromInput === 'out'){ 529 $inInput.val(outVal); 530 inVal = outVal; 531 } else { 532 $outInput.val(inVal); 533 outVal = inVal; 534 } 535 } 536 537 $outLabel.text(outVal); 538 $inLabel.text(inVal); 539 $hiddenOut.val(outVal); 540 $hiddenIn.val(inVal); 541 542 var range = max - min; 543 var leftPct = ((outVal - min) / range) * 100; 544 var rightPct = ((inVal - min) / range) * 100; 545 $fill.css({left: leftPct + '%', width: (rightPct - leftPct) + '%'}); 546 547 /* Trigger change on the hidden inputs so live preview picks it up */ 548 $hiddenOut.trigger('change'); 549 $hiddenIn.trigger('change'); 550 } 551 552 $outInput.on('input', function(){ update('out'); }); 553 $inInput.on('input', function(){ update('in'); }); 554 555 /* Initial render */ 556 update(); 557 }); 558 } 559});
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.