1/** 2 * @namespace WPGMZA 3 * @module AtlasMajorMarkerList 4 * @requires WPGMZA 5 * 6 * Custom marker list renderer for Atlas Major. 7 * Replaces the DataTable in the markers tab with a clean list 8 * matching the Concept C mockup design. 9 * 10 * Features: live search filtering, pagination, auto-sync with map events. 11 */ 12jQuery(function($) { 13 14 if(!document.querySelector('.wpgmza-atlas-major')) 15 return; 16 17 WPGMZA.AtlasMajorMarkerList = function(){ 18 var self = this; 19 20 this.container = document.querySelector('.am-marker-list-container'); 21 this.paginationContainer = document.querySelector('.am-ml-pagination'); 22 this.searchInput = document.querySelector('.am-ml-search'); 23 this.countBadge = document.querySelector('.am-ml-count'); 24 this.datatableContainer = document.querySelector('#wpgmza-table-container-Marker'); 25 26 this.page = 1; 27 this.perPage = 15; 28 this.searchTerm = ''; 29 this.debounceTimer = null; 30 this.selectedIds = {}; 31 32 if(!this.container) 33 return; 34 35 if(this.datatableContainer){ 36 $(this.datatableContainer).addClass('am-dt-hidden-in-list'); 37 } 38 39 this.initBulkBar(); 40 this.bindSearch(); 41 this.initKebabDismiss(); 42 this.render(); 43 this.bindMapEvents(); 44 this.bindMetaSync(); 45 WPGMZA.AtlasMajorMarkerList.applySidebarLabels(); 46 47 /* Initial load busy state â if the map hasn't finished placing 48 * its initial marker batch, the list is empty until then. 49 * Show the busy overlay so the user sees a clear "loading" 50 * indicator instead of a deceptively empty list. bindMapEvents 51 * above hooks `markersplaced` (or fires render() immediately 52 * if it's already true); either path clears the busy state via 53 * setBusy(false) at the end of render(). */ 54 var map = WPGMZA.maps[0]; 55 if(map && !map.markersPlaced){ 56 this.setBusy(true); 57 } 58 } 59 60 /** 61 * Toggle the busy/loading overlay on the list container. 62 * 63 * Adds/removes the `am-marker-list-busy` class on the container, 64 * which CSS (atlas-major.css, marker-list section) renders as a 65 * dimmed list + centred spinner overlay + pointer-events: none 66 * so the user can't click rows mid-action. Called by the action 67 * handlers (duplicate / delete / approve / move-map) around their 68 * AJAX calls, and once at constructor time while the map is still 69 * fetching its initial marker batch. Cleared at the end of 70 * render() too, as a safety net for code paths that forgot to 71 * unset it after their AJAX completes. 72 */ 73 WPGMZA.AtlasMajorMarkerList.prototype.setBusy = function(busy){ 74 if(!this.container) return; 75 if(busy){ 76 this.container.classList.add('am-marker-list-busy'); 77 } else { 78 this.container.classList.remove('am-marker-list-busy'); 79 } 80 } 81 82 /** 83 * Install document-level handlers (once) that dismiss any open 84 * kebab menu on outside-click or Escape. Bound on the 85 * constructor rather than bindItemEvents() so we don't stack 86 * a fresh handler every render. 87 */ 88 WPGMZA.AtlasMajorMarkerList.prototype.initKebabDismiss = function(){ 89 var self = this; 90 91 $(document).on('click.ammkebab', function(e){ 92 /* Click inside a kebab or its menu â leave alone, the 93 per-row handlers in bindItemEvents() manage state. */ 94 if($(e.target).closest('.am-mi-kebab, .am-mi-menu').length) return; 95 $(self.container).find('.am-mi.am-mi-menu-open').removeClass('am-mi-menu-open') 96 .find('.am-mi-kebab').attr('aria-expanded', 'false'); 97 }); 98 99 $(document).on('keydown.ammkebab', function(e){ 100 if(e.key === 'Escape'){ 101 $(self.container).find('.am-mi.am-mi-menu-open').removeClass('am-mi-menu-open') 102 .find('.am-mi-kebab').attr('aria-expanded', 'false'); 103 } 104 }); 105 } 106 107 /** 108 * Apply translated `data-am-label` attributes to sidebar 109 * `.navigation` and `.feature-list` elements inside per-feature 110 * groupings. These attributes are read by atlas-major.css via 111 * `content: attr(data-am-label)` for the section heading labels 112 * ("Add Marker", "Marker List", etc.). 113 * 114 * Previously those labels were hardcoded English in the CSS itself 115 * (`::before { content: "Add Marker" }`), which made them 116 * impossible to translate. This init runs once at editor load and 117 * pushes the translated strings (from WPGMZA.localized_strings) 118 * into the DOM as attributes so the CSS just reads them back. 119 * 120 * Runs idempotently (re-applies don't break anything) and is a 121 * no-op outside the editor (no matching elements). 122 */ 123 WPGMZA.AtlasMajorMarkerList.applySidebarLabels = function() { 124 var L = (WPGMZA.localized_strings) || {}; 125 126 var addLabel = function(group) { 127 if(group === 'map-markers') return L.am_sidebar_add_marker || 'Add Marker'; 128 return L.am_sidebar_add_default || 'Add'; 129 }; 130 131 var listLabel = function(group) { 132 switch(group) { 133 case 'map-markers': return L.am_sidebar_list_marker || 'Marker List';
134 case 'map-polygons': return L.am_sidebar_list_polygon || 'Polygon List'; 135 case 'map-polylines': return L.am_sidebar_list_polyline || 'Polyline List'; 136 case 'map-circles': return L.am_sidebar_list_circle || 'Circle List'; 137 case 'map-rectangles': return L.am_sidebar_list_rectangle || 'Rectangle List'; 138 case 'map-heatmaps': return L.am_sidebar_list_heatmap || 'Heatmap List'; 139 case 'map-point-labels': return L.am_sidebar_list_point_label || 'Point Label List'; 140 case 'map-image-overlays': return L.am_sidebar_list_image_overlay || 'Image Overlay List'; 141 default: return L.am_sidebar_list_default || 'List'; 142 } 143 }; 144 145 jQuery('.wpgmza-atlas-major .wpgmza-editor .sidebar .grouping[data-group]').each(function() { 146 var $grouping = jQuery(this); 147 var group = $grouping.attr('data-group'); 148 149 $grouping.children('.navigation').attr('data-am-label', addLabel(group)); 150 $grouping.find('.feature-list').attr('data-am-label', listLabel(group)); 151 }); 152 }; 153 154 WPGMZA.AtlasMajorMarkerList.PIN_COLORS = [ 155 '#e8473f', '#3b82f6', '#10b981', '#f59e0b', 156 '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16' 157 ]; 158 159 /** 160 * Keep title/address in sync with the hidden marker-list DataTable 161 * (#wpgmza-table-container-Marker â never shown, see the constructor's 162 * am-dt-hidden-in-list class, but still created by 163 * MapEditPage::initDataTables and used for bulk-edit id lookups). 164 * 165 * Under Pro's `lazyload_info_window_content` setting, the map's own 166 * marker fetch requests a reduced field set â id/lat/lng/icon/anim/ 167 * categories/layergroup only (ProMap::getRESTParameters) â and defers 168 * title/address/etc. until a marker's info-window is opened 169 * (ProMarker::lazyloadContent). Reading marker.title/marker.address 170 * directly off map.markers (as renderItem/getFilteredMarkers used to) 171 * means those fields are still unset for any marker that hasn't been 172 * clicked, so title falls back to address, which falls back to the 173 * Marker class's placeholder default of "California". 174 * 175 * The hidden DataTable queries title/address straight from the DB on 176 * every load, completely independent of that reduced fetch â it's the 177 * same request Atlas Novus's real, visible table already uses to show 178 * accurate titles. Rather than issue a second request for the same 179 * data, this listens for that request's response (see the 180 * 'wpgmza-datatable-loaded' event added in tables/datatable.js) and 181 * builds an id -> {title, address, ...} lookup from its `meta` array 182 * (the raw per-row fields DataTables keeps alongside the formatted 183 * grid â AdminFeatureDataTable already relies on this same array for 184 * bulk actions, e.g. row.wpgmzaFeatureData.id). 185 * 186 * That table defaults to a page of 10 rows, which wouldn't cover 187 * every marker on a larger map. Its query is deliberately cheap 188 * though â AdminMarkerDataTable::filterColumns blanks out 189 * description/pic/category in SQL specifically to keep this listing 190 * query light â so forcing it to "All" (the same length option Novus's 191 * visible table already offers) is one extra lightweight request, not 192 * a heavy one, and guarantees every marker gets an accurate title/ 193 * address regardless of which page Atlas Major's own list is on. 194 */ 195 WPGMZA.AtlasMajorMarkerList.prototype.bindMetaSync = function(){ 196 var self = this; 197 198 var attach = function(){ 199 var dt = WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable; 200 if(!dt || !dt.dataTable){ 201 setTimeout(attach, 250); 202 return; 203 } 204 205 $(dt.element).on('wpgmza-datatable-loaded', function(event, response){ 206 self.applyMetaResponse(response); 207 }); 208 209 if(dt.lastResponse){ 210 self.applyMetaResponse(dt.lastResponse); 211 } 212 213 if(dt.dataTable.page.len() !== -1){ 214 dt.dataTable.page.len(-1).draw(false); 215 } 216 }; 217 218 attach(); 219 } 220 221 /** 222 * Merge a DataTable response's raw `meta` rows into the id -> fields 223 * lookup used by getDisplayTitle/getDisplayAddress, then re-render so 224 * any row currently showing stale/placeholder text picks up the 225 * correct value. 226 */ 227 WPGMZA.AtlasMajorMarkerList.prototype.applyMetaResponse = function(response){ 228 if(!response || !(response.meta instanceof Array)) 229 return; 230 231 this._metaById = this._metaById || {}; 232 233 for(var i = 0; i < response.meta.length; i++){ 234 var row = response.meta[i]; 235 if(row && typeof row.id !== 'undefined') 236 this._metaById[row.id] = row; 237 } 238 239 this.render(); 240 } 241 242 WPGMZA.AtlasMajorMarkerList.prototype.getMetaFor = function(id){ 243 return (this._metaById && this._metaById[id]) || null; 244 } 245 246 /** 247 * Prefer the hidden DataTable's DB-accurate value; fall back to the 248 * marker instance (correct once lazyloadContent has resolved, or 249 * always correct when lazyload_info_window_content is off). 250 */ 251 WPGMZA.AtlasMajorMarkerList.prototype.getDisplayTitle = function(marker){ 252 var meta = this.getMetaFor(marker.id); 253 return (meta && meta.title) || marker.title || ''; 254 } 255 256 WPGMZA.AtlasMajorMarkerList.prototype.getDisplayAddress = function(marker){ 257 var meta = this.getMetaFor(marker.id); 258 return (meta && meta.address) || marker.address || ''; 259 } 260 261 /** 262 * Get the filtered marker list based on search term 263 */ 264 WPGMZA.AtlasMajorMarkerList.prototype.getFilteredMarkers = function(){ 265 var self = this; 266 var map = WPGMZA.maps[0]; 267 if(!map) return []; 268 269 var markers = map.markers || []; 270 var term = this.searchTerm.toLowerCase().trim(); 271 272 if(!term) 273 return markers; 274 275 return markers.filter(function(m){ 276 var title = self.getDisplayTitle(m).toLowerCase(); 277 var address = self.getDisplayAddress(m).toLowerCase(); 278 var id = String(m.id || ''); 279
280 if(title.indexOf(term) !== -1 || address.indexOf(term) !== -1 || id.indexOf(term) !== -1) 281 return true; 282 283 /* Search description (strip HTML tags) */ 284 if(m.description){ 285 var desc = m.description.replace(/<[^>]*>/g, '').toLowerCase(); 286 if(desc.indexOf(term) !== -1) 287 return true; 288 } 289 290 /* Search category names */ 291 if(m.categories && m.categories.length && WPGMZA.categories){ 292 for(var i = 0; i < m.categories.length; i++){ 293 var cat = WPGMZA.categories.getCategoryByID(parseInt(m.categories[i])); 294 if(cat && cat.name && cat.name.toLowerCase().indexOf(term) !== -1) 295 return true; 296 } 297 } 298 299 return false; 300 }); 301 } 302 303 /** 304 * Render the list with pagination 305 */ 306 WPGMZA.AtlasMajorMarkerList.prototype.render = function(){ 307 if(!this.container) return; 308 309 var filtered = this.getFilteredMarkers(); 310 var total = filtered.length; 311 var totalPages = Math.ceil(total / this.perPage); 312 313 if(this.page > totalPages) this.page = Math.max(1, totalPages); 314 315 var start = (this.page - 1) * this.perPage; 316 var pageItems = filtered.slice(start, start + this.perPage); 317 318 /* Render list */ 319 var html = ''; 320 if(total === 0){ 321 var msg = this.searchTerm 322 ? (WPGMZA.localized_strings.no_results_found || 'No markers match your search') 323 : (WPGMZA.localized_strings.no_markers_found || 'No markers yet'); 324 html = '<div class="am-ml-empty"><div class="am-empty-text">' + msg + '</div></div>'; 325 } else { 326 html = '<ul class="am-ml">'; 327 for(var i = 0; i < pageItems.length; i++){ 328 html += this.renderItem(pageItems[i], start + i); 329 } 330 html += '</ul>'; 331 } 332 this.container.innerHTML = html; 333 334 /* Render pagination */ 335 this.renderPagination(total, totalPages); 336 337 /* Update count badge */ 338 this.updateCount(total); 339 340 /* Bind events */ 341 this.bindItemEvents(); 342 } 343 344 /** 345 * Coalesced render â collapses a burst of render requests into a 346 * single actual render on the next animation frame. 347 * 348 * Why: map mutations fire `markeradded` / `markerremoved` ONE PER 349 * MARKER. During a bulk operation (initial load of a large map, 350 * a re-fetch, a multi-delete) that's potentially thousands of 351 * synchronous events, and a naive `render()` per event re-runs an 352 * O(n) filter over the whole marker set + rebuilds the page + re- 353 * binds events every time â locking the tab on large maps. 354 * requestAnimationFrame batches the whole synchronous burst into 355 * one render after the burst settles. 356 * 357 * Also skips entirely while the map's initial marker load is still 358 * in progress (`markersPlaced` false) â the single `markersplaced` 359 * render at the end of load covers that case, so we don't render 360 * thousands of times against a half-populated list during boot. 361 */ 362 WPGMZA.AtlasMajorMarkerList.prototype.scheduleRender = function(){ 363 var self = this; 364 var map = WPGMZA.maps[0]; 365 366 /* Initial bulk load â defer to the end-of-load markersplaced 367 render rather than re-rendering per marker. */ 368 if(map && !map.markersPlaced) 369 return; 370 371 if(self._renderScheduled) 372 return; 373 self._renderScheduled = true; 374 375 var run = function(){ 376 self._renderScheduled = false; 377 self.render(); 378 }; 379 380 if(typeof window.requestAnimationFrame === 'function') 381 window.requestAnimationFrame(run); 382 else 383 setTimeout(run, 16); 384 } 385 386 /** 387 * Render a single marker list item 388 */ 389 WPGMZA.AtlasMajorMarkerList.prototype.renderItem = function(marker, index){ 390 var title = this.esc(this.getDisplayTitle(marker)); 391 var address = this.esc(this.getDisplayAddress(marker)); 392 var id = marker.id; 393 394 if(!title && address){ title = address; address = ''; } 395 if(!title) title = 'Marker #' + id; 396 397 /* Resolve the actual icon URL â custom, map default, or global default */ 398 var iconUrl = ''; 399 var rawIcon = marker.icon; 400 if(rawIcon && rawIcon !== ''){ 401 if(typeof rawIcon === 'object' && rawIcon.url){ 402 iconUrl = rawIcon.url; 403 } else if(typeof rawIcon === 'string'){ 404 try { 405 var parsed = JSON.parse(rawIcon); 406 iconUrl = parsed.url || rawIcon; 407 } catch(e){ 408 iconUrl = rawIcon; 409 } 410 } 411 } else if(typeof marker.getIcon === 'function'){ 412 var fetched = marker.getIcon(); 413 if(typeof fetched === 'object' && fetched.url){ 414 iconUrl = fetched.url; 415 } else if(typeof fetched === 'string'){ 416 iconUrl = fetched; 417 } 418 } 419 if(!iconUrl && WPGMZA.defaultMarkerIcon){ 420 iconUrl = WPGMZA.defaultMarkerIcon; 421 } 422 if(!iconUrl && WPGMZA.settings && WPGMZA.settings.default_marker_icon){ 423 iconUrl = WPGMZA.settings.default_marker_icon; 424 } 425 426 var iconHtml = iconUrl 427 ? '<img src="' + this.esc(iconUrl) + '" alt="" />' 428 : '<div style="width:10px;height:10px;border-radius:50%;background:#e8473f;"></div>'; 429 430 /* Resolve category pills (Pro only) */ 431 var catHtml = ''; 432 if(marker.categories && marker.categories.length && WPGMZA.categories){ 433 var pills = []; 434 for(var c = 0; c < marker.categories.length; c++){ 435 var cat = WPGMZA.categories.getCategoryByID(parseInt(marker.categories[c])); 436 if(cat && cat.name && cat.name.trim() !== ''){ 437 pills.push('<span class="am-mi-cat">' + this.esc(cat.name) + '</span>'); 438 } 439 } 440 if(pills.length){ 441 catHtml = '<div class="am-mi-cats">' + pills.join('') + '</div>'; 442 } 443 } 444
445 var checked = this.selectedIds[id] ? ' checked' : ''; 446 447 /* Pending approval detection â UGM (visitor-generated markers) 448 submits with approved=0 until an admin approves. The base 449 marker model defaults approved=1, so a value of 0 (number 450 or string) means "needs admin action". Pro's 451 ProAdminMarkerDataTable injects an Approve action when UGM 452 is loaded and approved=0; we mirror that here so VGM users 453 keep this critical workflow in the Atlas Major list. */ 454 var needsApproval = (typeof marker.approved !== 'undefined' && parseInt(marker.approved) === 0); 455 456 /* Build the action menu items. Pro-only items (Duplicate, 457 Move Map) are gated on WPGMZA.isProVersion(). Approve is 458 gated on needsApproval and is the only item that carries 459 its own red-dot indicator inside the menu (so users can 460 spot the pending action without scanning every label). */ 461 var isPro = WPGMZA.isProVersion(); 462 var menuItems = ''; 463 menuItems += this.renderMenuItem('edit-marker-id', id, 'Edit', 464 '<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>'); 465 menuItems += this.renderMenuItem('adjust-marker-id', id, 'Adjust', 466 '<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/>'); 467 menuItems += this.renderMenuItem('center-marker-id', id, 'Center', 468 '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/>'); 469 if(isPro){ 470 menuItems += this.renderMenuItem('duplicate-feature-id', id, 'Duplicate', 471 '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'); 472 menuItems += this.renderMenuItem('move-map-feature-id', id, 'Move Map', 473 '<polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/>'); 474 } 475 if(needsApproval){ 476 menuItems += this.renderMenuItem('approve-marker-id', id, 'Approve', 477 '<polyline points="20 6 9 17 4 12"/>', 478 'am-mi-mi-approve am-mi-mi-attention'); 479 } 480 menuItems += this.renderMenuItem('delete-marker-id', id, 'Delete', 481 '<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>', 482 'am-mi-mi-danger'); 483 484 /* ---- Developer hook: row PARTS (filter-style) ---- 485 Atlas Major has no DataTable, so the column-extension pattern 486 third-party clients used under Atlas Novus (adding <td>s / 487 filtering cell content) doesn't apply. This is the supported 488 replacement: handlers receive a single mutable `parts` object 489 and can rewrite any rendered segment (title, address, icon, 490 category pills, action-menu items) or read `parts.marker` for 491 context. Mutations are read back below when the row is 492 assembled. WPGMZA's JS has no applyFilters(), so we use the 493 codebase's established jQuery-trigger-with-mutable-payload 494 convention. Underscored event name (no jQuery namespace 495 parsing), mirroring WP's PHP filter naming for familiarity. 496 497 Example â append the marker ID to every title: 498 jQuery(document.body).on('wpgmza_atlas_major_marker_list_item', function(e, parts){ 499 parts.title += ' #' + parts.marker.id; 500 }); 501 Example â add a custom menu action (reuse renderMenuItem): 502 jQuery(document.body).on('wpgmza_atlas_major_marker_list_item', function(e, parts){ 503 parts.menuItems += WPGMZA.atlasMajorMarkerList.renderMenuItem( 504 'my-custom-id', parts.id, 'My Action', '<circle cx="12" cy="12" r="10"/>'); 505 }); */ 506 var parts = { 507 marker: marker, 508 id: id, 509 title: title, 510 address: address, 511 iconHtml: iconHtml, 512 catHtml: catHtml, 513 menuItems: menuItems, 514 checked: checked, 515 needsApproval: needsApproval 516 }; 517 $(document.body).trigger('wpgmza_atlas_major_marker_list_item', [parts]); 518 519 var rowHtml = '<li class="am-mi' + (parts.checked ? ' am-mi-selected' : '') + (parts.needsApproval ? ' am-mi-needs-approval' : '') + '" data-marker-id="' + id + '">' + 520 '<label class="am-mi-check"><input type="checkbox" data-bulk-marker-id="' + id + '"' + parts.checked + '/></label>' + 521 '<div class="am-mi-pin">' + parts.iconHtml + '</div>' + 522 '<div class="am-mi-body">' + 523 '<div class="am-mi-name">' + parts.title + '</div>' + 524 (parts.address ? '<div class="am-mi-addr">' + parts.address + '</div>' : '') + 525 parts.catHtml + 526 '</div>' + 527 '<div class="am-mi-ops">' + 528 /* Single kebab trigger replacing the previous inline 529 icon strip. Matches Novus's "..." menu affordance. 530 The red attention dot sits on this button (via the 531 .am-mi-needs-approval class on the parent <li>), 532 mirroring exactly where Novus places it. */ 533 '<button class="am-mi-kebab" data-marker-menu-id="' + id + '" aria-label="Marker actions" aria-haspopup="true" aria-expanded="false">' + 534 '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>
534' + 535 '</button>' + 536 '<div class="am-mi-menu" data-marker-menu-for="' + id + '" role="menu">' + 537 parts.menuItems + 538 '</div>' + 539 '</div>' + 540 '</li>'; 541 542 /* ---- Developer hook: whole-row HTML (filter-style) ---- 543 Final escape hatch for clients that need to rewrite the row 544 markup wholesale. Handlers receive a mutable `result` object; 545 set `result.html` to replace. `result.marker` provided for 546 context. Runs after the PARTS hook so part-level tweaks are 547 already baked into `result.html`. NB: data-* attributes the 548 list relies on for event delegation (data-marker-id, 549 data-*-marker-id, etc.) must be preserved if you rebuild. */ 550 var result = { html: rowHtml, marker: marker, id: id }; 551 $(document.body).trigger('wpgmza_atlas_major_marker_list_item_html', [result]); 552 553 return result.html; 554 } 555 556 /** 557 * Render a single menu item button. SVG body is the inner 558 * <path>/<circle>/etc. â we wrap with the standard SVG attrs 559 * so menu items render consistently. 560 */ 561 WPGMZA.AtlasMajorMarkerList.prototype.renderMenuItem = function(dataKey, id, label, svgBody, extraClass){ 562 var cls = 'am-mi-mi' + (extraClass ? ' ' + extraClass : ''); 563 return '<button class="' + cls + '" data-' + dataKey + '="' + id + '" role="menuitem">' + 564 '<svg class="am-mi-mi-icon" viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + svgBody + '</svg>' + 565 '<span class="am-mi-mi-label">' + label + '</span>' + 566 '</button>'; 567 } 568 569 /** 570 * Render pagination controls 571 */ 572 WPGMZA.AtlasMajorMarkerList.prototype.renderPagination = function(total, totalPages){ 573 if(!this.paginationContainer) return; 574 575 if(totalPages <= 1){ 576 this.paginationContainer.innerHTML = ''; 577 return; 578 } 579 580 var self = this; 581 var html = '<div class="am-pag">'; 582 583 /* Prev */ 584 html += '<button class="am-pag-btn' + (this.page <= 1 ? ' disabled' : '') + '" data-page="' + (this.page - 1) + '">' + 585 '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>' + 586 '</button>'; 587 588 /* Page numbers â show max 5 around current */ 589 var startPage = Math.max(1, this.page - 2); 590 var endPage = Math.min(totalPages, startPage + 4); 591 if(endPage - startPage < 4) startPage = Math.max(1, endPage - 4); 592 593 for(var p = startPage; p <= endPage; p++){ 594 html += '<button class="am-pag-btn am-pag-num' + (p === this.page ? ' on' : '') + '" data-page="' + p + '">' + p + '</button>'; 595 } 596 597 /* Next */ 598 html += '<button class="am-pag-btn' + (this.page >= totalPages ? ' disabled' : '') + '" data-page="' + (this.page + 1) + '">' + 599 '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2"><polyline points="9 6 15 12 9 18"/></svg>' + 600 '</button>'; 601 602 html += '<span class="am-pag-info">' + this.page + ' / ' + totalPages + '</span>'; 603 html += '</div>'; 604 605 this.paginationContainer.innerHTML = html; 606 607 /* Bind pagination clicks */ 608 $(this.paginationContainer).off('click.ampag').on('click.ampag', '.am-pag-btn:not(.disabled)', function(){ 609 var page = parseInt($(this).attr('data-page')); 610 if(page >= 1 && page <= totalPages){ 611 self.page = page; 612 self.render(); 613 /* Scroll to top of list */ 614 $(self.container).closest('.am-panel-scroll')[0].scrollTop = 0; 615 } 616 }); 617 } 618 619 /** 620 * Bind search input with debounce 621 */ 622 WPGMZA.AtlasMajorMarkerList.prototype.bindSearch = function(){ 623 var self = this; 624 if(!this.searchInput) return; 625 626 $(this.searchInput).on('input', function(){ 627 clearTimeout(self.debounceTimer); 628 self.debounceTimer = setTimeout(function(){ 629 self.searchTerm = $(self.searchInput).val(); 630 self.page = 1; 631 self.render(); 632 }, 200); 633 }); 634 } 635 636 /** 637 * Update the count badge next to "Markers" heading 638 */ 639 WPGMZA.AtlasMajorMarkerList.prototype.updateCount = function(filteredCount){ 640 if(!this.countBadge) return; 641 642 var map = WPGMZA.maps[0]; 643 var total = (map && map.markers) ? map.markers.length : 0; 644 645 if(this.searchTerm){ 646 this.countBadge.textContent = filteredCount + ' / ' + total; 647 } else { 648 this.countBadge.textContent = total; 649 } 650 } 651
652 /** 653 * Bind click events on rendered list items 654 */ 655 WPGMZA.AtlasMajorMarkerList.prototype.bindItemEvents = function(){ 656 var self = this; 657 var container = $(this.container); 658 659 container.off('click.amlist').on('click.amlist', '.am-mi', function(e){ 660 if($(e.target).closest('.am-mi-ops').length) return; 661 if($(e.target).closest('.am-mi-check').length) return; 662 self.editMarker($(this).data('marker-id')); 663 }); 664 665 /* Kebab toggle â opens the per-row action menu. We close any 666 other open menu first so only one is visible at a time. 667 The menu closes via the document-level handler installed 668 in initKebabDismiss(). */ 669 container.on('click.amlist', '.am-mi-kebab', function(e){ 670 e.stopPropagation(); 671 var $kebab = $(this); 672 var $li = $kebab.closest('.am-mi'); 673 var alreadyOpen = $li.hasClass('am-mi-menu-open'); 674 675 /* Close all other menus */ 676 $(self.container).find('.am-mi.am-mi-menu-open').removeClass('am-mi-menu-open') 677 .find('.am-mi-kebab').attr('aria-expanded', 'false'); 678 679 if(!alreadyOpen){ 680 $li.addClass('am-mi-menu-open'); 681 $kebab.attr('aria-expanded', 'true'); 682 } 683 }); 684 685 /* Close-on-action: clicking any menu item should close the 686 menu after the action's own handler fires. */ 687 container.on('click.amlist', '.am-mi-menu .am-mi-mi', function(){ 688 $(this).closest('.am-mi').removeClass('am-mi-menu-open') 689 .find('.am-mi-kebab').attr('aria-expanded', 'false'); 690 }); 691 692 self.bindBulkCheckboxes(); 693 self.updateBulkBar(); 694 695 container.on('click.amlist', '[data-edit-marker-id]', function(e){ 696 e.stopPropagation(); 697 self.editMarker($(this).attr('data-edit-marker-id')); 698 }); 699 700 container.on('click.amlist', '[data-center-marker-id]', function(e){ 701 e.stopPropagation(); 702 self.centerMarker($(this).attr('data-center-marker-id')); 703 }); 704 705 /* Adjust â do NOT stopPropagation. The core MarkerPanel listens on 706 document.body for [data-adjust-marker-id] clicks. We just need 707 to ensure the editor grouping opens. */ 708 container.on('click.amlist', '[data-adjust-marker-id]', function(e){ 709 /* Let the event bubble to MarkerPanel.onAdjustFeature on body */ 710 }); 711 712 container.on('click.amlist', '[data-duplicate-feature-id]', function(e){ 713 e.stopPropagation(); 714 self.duplicateMarker($(this).attr('data-duplicate-feature-id')); 715 }); 716 717 container.on('click.amlist', '[data-move-map-feature-id]', function(e){ 718 e.stopPropagation(); 719 self.moveMarkerToMap($(this).attr('data-move-map-feature-id')); 720 }); 721 722 container.on('click.amlist', '[data-approve-marker-id]', function(e){ 723 e.stopPropagation(); 724 self.approveMarker($(this).attr('data-approve-marker-id')); 725 }); 726 727 container.on('click.amlist', '[data-delete-marker-id]', function(e){ 728 e.stopPropagation(); 729 self.deleteMarker($(this).attr('data-delete-marker-id')); 730 }); 731 } 732 733 /** 734 * Bind to map events for live updates 735 */ 736 WPGMZA.AtlasMajorMarkerList.prototype.bindMapEvents = function(){ 737 var self = this; 738 var map = WPGMZA.maps[0]; 739 if(!map) return; 740 741 /* Coalesced (scheduleRender) rather than a direct render per 742 event â on large maps these fire once per marker during bulk 743 adds/removes and would otherwise re-render thousands of times. 744 scheduleRender also no-ops while the initial load is still in 745 progress; the markersplaced handler below renders once at the 746 end of that load. */ 747 map.on('markeradded', function(){ self.scheduleRender(); }); 748 map.on('markerremoved', function(){ self.scheduleRender(); }); 749 750 /* Authoritative "all initial markers are loaded" event. This 751 * covers the case where the marker list constructor runs 752 * BEFORE the map has finished fetching its initial marker 753 * batch â constructor's render() sees 0 markers, tab shows 754 * empty, and without this listener we'd miss the later load. 755 * If `markersplaced` already fired (map.markersPlaced is true), 756 * we render once right now instead of waiting. Direct render() 757 * here (not scheduleRender) since it's a single end-of-load 758 * call and scheduleRender would no-op if markersPlaced flips 759 * true mid-frame. 760 * Either path clears the initial-load busy state (set in the 761 * constructor when !map.markersPlaced) â render is the real 762 * "list is up to date" signal here. */ 763 if(map.markersPlaced){ 764 self.render(); 765 self.setBusy(false); 766 } else { 767 map.on('markersplaced', function(){ 768 self.render(); 769 self.setBusy(false); 770 }); 771 } 772 773 $(document.body).on('datatable.reload', function(){ 774 setTimeout(function(){ self.render(); }, 500); 775 }); 776 } 777 778 WPGMZA.AtlasMajorMarkerList.prototype.editMarker = function(id){ 779 var panel = WPGMZA.mapEditPage ? WPGMZA.mapEditPage.markerPanel : null; 780 781 if(panel && typeof panel.select === 'function'){ 782 panel.select(id); 783 } 784 785 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.sidebarGroupings){ 786 WPGMZA.mapEditPage.sidebarGroupings.openTabByGroupId('map-markers-editor'); 787 } 788 789 /* Open the marker's infowindow on the Live Preview map so the 790 user sees what they're editing in its rendered state. Safe 791 in the editor because the viewportGroupings null-guards in 792 pro-info-window.js (and siblings) make populatePanel 793 tolerant of admin context where viewportGroupings is absent. */ 794 var map = WPGMZA.maps[0]; 795 var marker = map ? map.getMarkerByID(id) : null; 796 if(marker && typeof marker.openInfoWindow === 'function'){ 797 marker.openInfoWindow(); 798 } 799 } 800 801 WPGMZA.AtlasMajorMarkerList.prototype.centerMarker = function(id){ 802 var marker = WPGMZA.maps[0].getMarkerByID(id); 803 if(marker){ 804 WPGMZA.maps[0].setCenter(new WPGMZA.LatLng({ lat: marker.lat, lng: marker.lng })); 805 } 806 } 807
808 /** 809 * Duplicate a marker via REST API (Pro only) 810 */ 811 WPGMZA.AtlasMajorMarkerList.prototype.duplicateMarker = function(id){ 812 var self = this; 813 var map = WPGMZA.maps[0]; 814 815 self.setBusy(true); 816 817 WPGMZA.restAPI.call("/markers/", { 818 method: "POST", 819 data: { 820 id: id, 821 action: "duplicate" 822 }, 823 success: function(response, status, xhr) { 824 /* The list renders from map.markers (no server-backed 825 DataTable like Novus), so the freshly-created marker 826 has to be pushed into the map. The duplicate endpoint 827 now returns the new marker (see ProRestAPI::markers 828 duplicate case); instantiate + addMarker, which fires 829 `markeradded` -> render() via bindMapEvents. */ 830 if(response && response.marker && map && typeof map.addMarker === 'function'){ 831 var marker = WPGMZA.Marker.createInstance(response.marker); 832 map.addMarker(marker); 833 } else { 834 /* Fallback for older Pro that doesn't return the 835 marker â at least re-render from current state. */ 836 self.render(); 837 } 838 839 /* Keep the hidden DataTable (bulk-edit modal source) in 840 sync with the new row. */ 841 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ 842 WPGMZA.mapEditPage.markerAdminDataTable.reload(); 843 } 844 845 self.setBusy(false); 846 }, 847 error: function(){ 848 self.setBusy(false); 849 } 850 }); 851 } 852 853 /** 854 * Move marker to a different map (Pro only) 855 * Reuses the existing AdminFeatureDataTable.onMoveMap which 856 * shows the .wpgmza-map-select-modal generic modal. The modal 857 * is created during datatable construction; we just delegate 858 * the call so the modal lifecycle stays in one place. 859 */ 860 WPGMZA.AtlasMajorMarkerList.prototype.moveMarkerToMap = function(id){ 861 var self = this; 862 863 var dt = WPGMZA.mapEditPage ? WPGMZA.mapEditPage.markerAdminDataTable : null; 864 if(!dt || typeof dt.onMoveMap !== 'function') 865 return; 866 867 /* onMoveMap accepts either an event OR a bare id when event is 868 undefined. Passing the id directly fires the modal flow. */ 869 dt.onMoveMap(id); 870 } 871 872 /** 873 * Approve a pending marker (Pro + UGM) 874 * Mirrors MarkerPanel.onApproveMarker â POST /markers/{id} with 875 * approved=1. Pro's existing handler updates the datatable; we 876 * additionally flip the local marker.approved so the list 877 * re-renders without the red-dot indicator + approve button. 878 */ 879 WPGMZA.AtlasMajorMarkerList.prototype.approveMarker = function(id){ 880 var self = this; 881 var map = WPGMZA.maps[0]; 882 if(!map) return; 883 884 self.setBusy(true); 885 886 WPGMZA.restAPI.call("/markers/" + id, { 887 method: "POST", 888 data: { approved: "1" }, 889 success: function(){ 890 /* Update the local marker model so the list reflects 891 the new state immediately without waiting for a 892 full markers refetch. */ 893 var marker = map.getMarkerByID(id); 894 if(marker){ 895 marker.approved = 1; 896 } 897 self.render(); 898 899 /* Keep the hidden DataTable (used by bulk-edit modal 900 lookups) in sync as well. */ 901 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ 902 WPGMZA.mapEditPage.markerAdminDataTable.reload(); 903 } 904 905 self.setBusy(false); 906 }, 907 error: function(){ 908 self.setBusy(false); 909 } 910 }); 911 } 912 913 WPGMZA.AtlasMajorMarkerList.prototype.deleteMarker = function(id){ 914 var self = this; 915 if(!confirm(WPGMZA.localized_strings.general_delete_prompt_text || 'Are you sure you want to delete this marker?')) 916 return; 917 918 var map = WPGMZA.maps[0]; 919 var marker = map.getMarkerByID(id); 920 921 /* Delete via the REST API (DELETE /markers/{id}) â the same 922 path FeaturePanel.onDeleteFeature() uses (feature-panel.js 923 ~L592). The previous legacy `$.post(ajaxurl, action: 924 delete_marker)` call relied on `wp_ajax_delete_marker`, 925 which is only registered by the pro-below-8.1 compatibility 926 layer (hooking a deprecated empty stub); on current Pro no 927 handler is registered, so admin-ajax returned 400 and the 928 marker was never actually deleted. We also now remove the 929 marker only AFTER the delete succeeds, rather than 930 optimistically before a request that could fail. */ 931 self.setBusy(true); 932 933 WPGMZA.restAPI.call("/markers/" + id, { 934 method: "DELETE", 935 success: function(data, status, xhr){ 936 if(marker) 937 map.removeMarker(marker); /* fires markerremoved -> render() */ 938 else 939 map.removeMarkerByID(id); 940 941 self.render(); 942 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ 943 WPGMZA.mapEditPage.markerAdminDataTable.reload(); 944 } 945 946 self.setBusy(false); 947 }, 948 error: function(){ 949 self.setBusy(false); 950 } 951 }); 952 } 953 954 /**
955 * Create the bulk action bar (inserted after search field) 956 */ 957 WPGMZA.AtlasMajorMarkerList.prototype.initBulkBar = function(){ 958 var self = this; 959 var searchField = document.querySelector('.am-search-field'); 960 if(!searchField) return; 961 962 var bar = document.createElement('div'); 963 bar.className = 'am-bulk-bar'; 964 bar.style.display = 'none'; 965 bar.innerHTML = 966 '<button type="button" class="am-bulk-btn" data-bulk-action="select-all">' + (WPGMZA.localized_strings.select_all || 'Select All') + '</button>' + 967 '<button type="button" class="am-bulk-btn" data-bulk-action="deselect-all">' + (WPGMZA.localized_strings.deselect_all || 'Deselect All') + '</button>' + 968 '<button type="button" class="am-bulk-btn" data-bulk-action="bulk-edit">' + (WPGMZA.localized_strings.bulk_edit || 'Bulk Edit') + '</button>' + 969 '<button type="button" class="am-bulk-btn am-bulk-btn-danger" data-bulk-action="bulk-delete">' + (WPGMZA.localized_strings.bulk_delete || 'Bulk Delete') + '</button>'; 970 971 searchField.after(bar); 972 this.bulkBar = bar; 973 974 $(bar).on('click', '[data-bulk-action]', function(e){ 975 var action = $(this).attr('data-bulk-action'); 976 if(action === 'select-all') self.bulkSelectAll(); 977 else if(action === 'deselect-all') self.bulkDeselectAll(); 978 else if(action === 'bulk-edit') self.bulkEdit(); 979 else if(action === 'bulk-delete') self.bulkDelete(); 980 }); 981 } 982 983 /** 984 * Get the bulk edit modal (Pro only) 985 * Reuses the modal already created by the hidden AdminFeatureDataTable 986 */ 987 WPGMZA.AtlasMajorMarkerList.prototype.getBulkEditModal = function(){ 988 /* Try the datatable's lazy getter first */ 989 var tables = WPGMZA.mapEditPage ? WPGMZA.mapEditPage.markerDataTable || WPGMZA.mapEditPage.markerAdminDataTable : null; 990 if(tables && typeof tables.getBulkEditorModal === 'function'){ 991 return tables.getBulkEditorModal(); 992 } 993 994 /* Fallback: find any AdminFeatureDataTable for markers */ 995 if(WPGMZA.AdminFeatureDataTable){ 996 var el = $('[data-wpgmza-table][data-wpgmza-feature-type="marker"]')[0]; 997 if(el && el.wpgmzaDataTable && typeof el.wpgmzaDataTable.getBulkEditorModal === 'function'){ 998 return el.wpgmzaDataTable.getBulkEditorModal(); 999 } 1000 } 1001 1002 return false; 1003 } 1004 1005 /** 1006 * Update bulk bar visibility and info 1007 */ 1008 WPGMZA.AtlasMajorMarkerList.prototype.updateBulkBar = function(){ 1009 if(!this.bulkBar) return; 1010 1011 var count = Object.keys(this.selectedIds).length; 1012 if(count > 0){ 1013 this.bulkBar.style.display = 'flex'; 1014 } else { 1015 this.bulkBar.style.display = 'none'; 1016 } 1017 } 1018 1019 /** 1020 * Bind checkbox change events (called after render) 1021 */ 1022 WPGMZA.AtlasMajorMarkerList.prototype.bindBulkCheckboxes = function(){ 1023 var self = this; 1024 $(this.container).off('change.ambulk').on('change.ambulk', '[data-bulk-marker-id]', function(e){ 1025 e.stopPropagation(); 1026 var id = $(this).attr('data-bulk-marker-id'); 1027 if(this.checked){ 1028 self.selectedIds[id] = true; 1029 } else { 1030 delete self.selectedIds[id]; 1031 } 1032 $(this).closest('.am-mi').toggleClass('am-mi-selected', this.checked); 1033 self.updateBulkBar(); 1034 }); 1035 1036 /* Prevent checkbox click from triggering marker edit */ 1037 $(this.container).off('click.ambulkstop').on('click.ambulkstop', '.am-mi-check', function(e){ 1038 e.stopPropagation(); 1039 }); 1040 } 1041 1042 WPGMZA.AtlasMajorMarkerList.prototype.bulkSelectAll = function(){ 1043 var filtered = this.getFilteredMarkers(); 1044 for(var i = 0; i < filtered.length; i++){ 1045 this.selectedIds[filtered[i].id] = true; 1046 } 1047 this.render(); 1048 } 1049 1050 WPGMZA.AtlasMajorMarkerList.prototype.bulkDeselectAll = function(){ 1051 this.selectedIds = {}; 1052 this.render(); 1053 } 1054 1055 WPGMZA.AtlasMajorMarkerList.prototype.getSelectedIds = function(){ 1056 return Object.keys(this.selectedIds).map(function(id){ return parseInt(id); }); 1057 } 1058 1059 WPGMZA.AtlasMajorMarkerList.prototype.bulkDelete = function(){ 1060 var self = this; 1061 var ids = this.getSelectedIds(); 1062 if(!ids.length) return; 1063 1064 if(!confirm(WPGMZA.localized_strings.general_delete_prompt_text || 'Are you sure you want to delete these markers?')) 1065 return; 1066 1067 var map = WPGMZA.maps[0]; 1068 ids.forEach(function(id){ 1069 try { map.removeMarkerByID(id); } catch(e){} 1070 }); 1071 1072 WPGMZA.restAPI.call("/markers/", { 1073 method: "DELETE", 1074 data: { ids: ids }, 1075 complete: function(){ 1076 self.selectedIds = {}; 1077 self.render(); 1078 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ 1079 WPGMZA.mapEditPage.markerAdminDataTable.reload(); 1080 } 1081 } 1082 }); 1083 } 1084 1085 WPGMZA.AtlasMajorMarkerList.prototype.bulkEdit = function(){ 1086 var self = this;
1087 var ids = this.getSelectedIds(); 1088 if(!ids.length) return; 1089 1090 var modal = this.getBulkEditModal(); 1091 if(modal){ 1092 modal.show(function(data){ 1093 data.ids = ids; 1094 data.action = "bulk_edit"; 1095 1096 WPGMZA.restAPI.call("/markers/", { 1097 method: "POST", 1098 data: data, 1099 success: function(){ 1100 self.selectedIds = {}; 1101 self.render(); 1102 if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ 1103 WPGMZA.mapEditPage.markerAdminDataTable.reload(); 1104 } 1105 } 1106 }); 1107 }); 1108 } 1109 } 1110 1111 WPGMZA.AtlasMajorMarkerList.prototype.esc = function(str){ 1112 var d = document.createElement('div'); 1113 d.textContent = str; 1114 return d.innerHTML; 1115 } 1116 1117 /* Init */ 1118 function initMarkerList(){ 1119 if(WPGMZA.maps && WPGMZA.maps[0] && document.querySelector('.am-marker-list-container')){ 1120 if(!WPGMZA.atlasMajorMarkerList){ 1121 WPGMZA.atlasMajorMarkerList = new WPGMZA.AtlasMajorMarkerList(); 1122 } 1123 } 1124 } 1125 1126 $(document.body).on('wpgmza_map_edit_page_created', function(){ 1127 setTimeout(initMarkerList, 1000); 1128 }); 1129 1130 $(window).on('load', function(){ 1131 setTimeout(initMarkerList, 1500); 1132 }); 1133 1134 $(document.body).on('markersfetched', function(){ 1135 if(WPGMZA.atlasMajorMarkerList){ 1136 WPGMZA.atlasMajorMarkerList.render(); 1137 } 1138 }); 1139 1140 /* (Removed) Previously this file installed a sidebar-delegate-created 1141 * handler that scrolled the Add Marker panel back to the top and 1142 * refocused the address input â the old request was to keep users 1143 * on the Add Marker form for bulk-add workflows. That requirement 1144 * has been reversed: after a successful Add, the editor should now 1145 * return to the marker list panel (consistent with what Edit Marker 1146 * already does via sidebar-groupings.js's `sidebar-delegate-saved` 1147 * handler). The navigate-back is handled in sidebar-groupings.js's 1148 * `sidebar-delegate-created` handler â nothing Atlas-Major-specific 1149 * needs to run here anymore. */ 1150});
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.