/** * @namespace WPGMZA * @module AtlasMajorMarkerList * @requires WPGMZA * * Custom marker list renderer for Atlas Major. * Replaces the DataTable in the markers tab with a clean list * matching the Concept C mockup design. * * Features: live search filtering, pagination, auto-sync with map events. */ jQuery(function($) { if(!document.querySelector('.wpgmza-atlas-major')) return; WPGMZA.AtlasMajorMarkerList = function(){ var self = this; this.container = document.querySelector('.am-marker-list-container'); this.paginationContainer = document.querySelector('.am-ml-pagination'); this.searchInput = document.querySelector('.am-ml-search'); this.countBadge = document.querySelector('.am-ml-count'); this.datatableContainer = document.querySelector('#wpgmza-table-container-Marker'); this.page = 1; this.perPage = 15; this.searchTerm = ''; this.debounceTimer = null; this.selectedIds = {}; if(!this.container) return; if(this.datatableContainer){ $(this.datatableContainer).addClass('am-dt-hidden-in-list'); } this.initBulkBar(); this.bindSearch(); this.initKebabDismiss(); this.render(); this.bindMapEvents(); this.bindMetaSync(); WPGMZA.AtlasMajorMarkerList.applySidebarLabels(); /* Initial load busy state — if the map hasn't finished placing * its initial marker batch, the list is empty until then. * Show the busy overlay so the user sees a clear "loading" * indicator instead of a deceptively empty list. bindMapEvents * above hooks `markersplaced` (or fires render() immediately * if it's already true); either path clears the busy state via * setBusy(false) at the end of render(). */ var map = WPGMZA.maps[0]; if(map && !map.markersPlaced){ this.setBusy(true); } } /** * Toggle the busy/loading overlay on the list container. * * Adds/removes the `am-marker-list-busy` class on the container, * which CSS (atlas-major.css, marker-list section) renders as a * dimmed list + centred spinner overlay + pointer-events: none * so the user can't click rows mid-action. Called by the action * handlers (duplicate / delete / approve / move-map) around their * AJAX calls, and once at constructor time while the map is still * fetching its initial marker batch. Cleared at the end of * render() too, as a safety net for code paths that forgot to * unset it after their AJAX completes. */ WPGMZA.AtlasMajorMarkerList.prototype.setBusy = function(busy){ if(!this.container) return; if(busy){ this.container.classList.add('am-marker-list-busy'); } else { this.container.classList.remove('am-marker-list-busy'); } } /** * Install document-level handlers (once) that dismiss any open * kebab menu on outside-click or Escape. Bound on the * constructor rather than bindItemEvents() so we don't stack * a fresh handler every render. */ WPGMZA.AtlasMajorMarkerList.prototype.initKebabDismiss = function(){ var self = this; $(document).on('click.ammkebab', function(e){ /* Click inside a kebab or its menu → leave alone, the per-row handlers in bindItemEvents() manage state. */ if($(e.target).closest('.am-mi-kebab, .am-mi-menu').length) return; $(self.container).find('.am-mi.am-mi-menu-open').removeClass('am-mi-menu-open') .find('.am-mi-kebab').attr('aria-expanded', 'false'); }); $(document).on('keydown.ammkebab', function(e){ if(e.key === 'Escape'){ $(self.container).find('.am-mi.am-mi-menu-open').removeClass('am-mi-menu-open') .find('.am-mi-kebab').attr('aria-expanded', 'false'); } }); } /** * Apply translated `data-am-label` attributes to sidebar * `.navigation` and `.feature-list` elements inside per-feature * groupings. These attributes are read by atlas-major.css via * `content: attr(data-am-label)` for the section heading labels * ("Add Marker", "Marker List", etc.). * * Previously those labels were hardcoded English in the CSS itself * (`::before { content: "Add Marker" }`), which made them * impossible to translate. This init runs once at editor load and * pushes the translated strings (from WPGMZA.localized_strings) * into the DOM as attributes so the CSS just reads them back. * * Runs idempotently (re-applies don't break anything) and is a * no-op outside the editor (no matching elements). */ WPGMZA.AtlasMajorMarkerList.applySidebarLabels = function() { var L = (WPGMZA.localized_strings) || {}; var addLabel = function(group) { if(group === 'map-markers') return L.am_sidebar_add_marker || 'Add Marker'; return L.am_sidebar_add_default || 'Add'; }; var listLabel = function(group) { switch(group) { case 'map-markers': return L.am_sidebar_list_marker || 'Marker List'; case 'map-polygons': return L.am_sidebar_list_polygon || 'Polygon List'; case 'map-polylines': return L.am_sidebar_list_polyline || 'Polyline List'; case 'map-circles': return L.am_sidebar_list_circle || 'Circle List'; case 'map-rectangles': return L.am_sidebar_list_rectangle || 'Rectangle List'; case 'map-heatmaps': return L.am_sidebar_list_heatmap || 'Heatmap List'; case 'map-point-labels': return L.am_sidebar_list_point_label || 'Point Label List'; case 'map-image-overlays': return L.am_sidebar_list_image_overlay || 'Image Overlay List'; default: return L.am_sidebar_list_default || 'List'; } }; jQuery('.wpgmza-atlas-major .wpgmza-editor .sidebar .grouping[data-group]').each(function() { var $grouping = jQuery(this); var group = $grouping.attr('data-group'); $grouping.children('.navigation').attr('data-am-label', addLabel(group)); $grouping.find('.feature-list').attr('data-am-label', listLabel(group)); }); }; WPGMZA.AtlasMajorMarkerList.PIN_COLORS = [ '#e8473f', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16' ]; /** * Keep title/address in sync with the hidden marker-list DataTable * (#wpgmza-table-container-Marker — never shown, see the constructor's * am-dt-hidden-in-list class, but still created by * MapEditPage::initDataTables and used for bulk-edit id lookups). * * Under Pro's `lazyload_info_window_content` setting, the map's own * marker fetch requests a reduced field set — id/lat/lng/icon/anim/ * categories/layergroup only (ProMap::getRESTParameters) — and defers * title/address/etc. until a marker's info-window is opened * (ProMarker::lazyloadContent). Reading marker.title/marker.address * directly off map.markers (as renderItem/getFilteredMarkers used to) * means those fields are still unset for any marker that hasn't been * clicked, so title falls back to address, which falls back to the * Marker class's placeholder default of "California". * * The hidden DataTable queries title/address straight from the DB on * every load, completely independent of that reduced fetch — it's the * same request Atlas Novus's real, visible table already uses to show * accurate titles. Rather than issue a second request for the same * data, this listens for that request's response (see the * 'wpgmza-datatable-loaded' event added in tables/datatable.js) and * builds an id -> {title, address, ...} lookup from its `meta` array * (the raw per-row fields DataTables keeps alongside the formatted * grid — AdminFeatureDataTable already relies on this same array for * bulk actions, e.g. row.wpgmzaFeatureData.id). * * That table defaults to a page of 10 rows, which wouldn't cover * every marker on a larger map. Its query is deliberately cheap * though — AdminMarkerDataTable::filterColumns blanks out * description/pic/category in SQL specifically to keep this listing * query light — so forcing it to "All" (the same length option Novus's * visible table already offers) is one extra lightweight request, not * a heavy one, and guarantees every marker gets an accurate title/ * address regardless of which page Atlas Major's own list is on. */ WPGMZA.AtlasMajorMarkerList.prototype.bindMetaSync = function(){ var self = this; var attach = function(){ var dt = WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable; if(!dt || !dt.dataTable){ setTimeout(attach, 250); return; } $(dt.element).on('wpgmza-datatable-loaded', function(event, response){ self.applyMetaResponse(response); }); if(dt.lastResponse){ self.applyMetaResponse(dt.lastResponse); } if(dt.dataTable.page.len() !== -1){ dt.dataTable.page.len(-1).draw(false); } }; attach(); } /** * Merge a DataTable response's raw `meta` rows into the id -> fields * lookup used by getDisplayTitle/getDisplayAddress, then re-render so * any row currently showing stale/placeholder text picks up the * correct value. */ WPGMZA.AtlasMajorMarkerList.prototype.applyMetaResponse = function(response){ if(!response || !(response.meta instanceof Array)) return; this._metaById = this._metaById || {}; for(var i = 0; i < response.meta.length; i++){ var row = response.meta[i]; if(row && typeof row.id !== 'undefined') this._metaById[row.id] = row; } this.render(); } WPGMZA.AtlasMajorMarkerList.prototype.getMetaFor = function(id){ return (this._metaById && this._metaById[id]) || null; } /** * Prefer the hidden DataTable's DB-accurate value; fall back to the * marker instance (correct once lazyloadContent has resolved, or * always correct when lazyload_info_window_content is off). */ WPGMZA.AtlasMajorMarkerList.prototype.getDisplayTitle = function(marker){ var meta = this.getMetaFor(marker.id); return (meta && meta.title) || marker.title || ''; } WPGMZA.AtlasMajorMarkerList.prototype.getDisplayAddress = function(marker){ var meta = this.getMetaFor(marker.id); return (meta && meta.address) || marker.address || ''; } /** * Get the filtered marker list based on search term */ WPGMZA.AtlasMajorMarkerList.prototype.getFilteredMarkers = function(){ var self = this; var map = WPGMZA.maps[0]; if(!map) return []; var markers = map.markers || []; var term = this.searchTerm.toLowerCase().trim(); if(!term) return markers; return markers.filter(function(m){ var title = self.getDisplayTitle(m).toLowerCase(); var address = self.getDisplayAddress(m).toLowerCase(); var id = String(m.id || ''); if(title.indexOf(term) !== -1 || address.indexOf(term) !== -1 || id.indexOf(term) !== -1) return true; /* Search description (strip HTML tags) */ if(m.description){ var desc = m.description.replace(/<[^>]*>/g, '').toLowerCase(); if(desc.indexOf(term) !== -1) return true; } /* Search category names */ if(m.categories && m.categories.length && WPGMZA.categories){ for(var i = 0; i < m.categories.length; i++){ var cat = WPGMZA.categories.getCategoryByID(parseInt(m.categories[i])); if(cat && cat.name && cat.name.toLowerCase().indexOf(term) !== -1) return true; } } return false; }); } /** * Render the list with pagination */ WPGMZA.AtlasMajorMarkerList.prototype.render = function(){ if(!this.container) return; var filtered = this.getFilteredMarkers(); var total = filtered.length; var totalPages = Math.ceil(total / this.perPage); if(this.page > totalPages) this.page = Math.max(1, totalPages); var start = (this.page - 1) * this.perPage; var pageItems = filtered.slice(start, start + this.perPage); /* Render list */ var html = ''; if(total === 0){ var msg = this.searchTerm ? (WPGMZA.localized_strings.no_results_found || 'No markers match your search') : (WPGMZA.localized_strings.no_markers_found || 'No markers yet'); html = '
' + msg + '
'; } else { html = ''; } this.container.innerHTML = html; /* Render pagination */ this.renderPagination(total, totalPages); /* Update count badge */ this.updateCount(total); /* Bind events */ this.bindItemEvents(); } /** * Coalesced render — collapses a burst of render requests into a * single actual render on the next animation frame. * * Why: map mutations fire `markeradded` / `markerremoved` ONE PER * MARKER. During a bulk operation (initial load of a large map, * a re-fetch, a multi-delete) that's potentially thousands of * synchronous events, and a naive `render()` per event re-runs an * O(n) filter over the whole marker set + rebuilds the page + re- * binds events every time — locking the tab on large maps. * requestAnimationFrame batches the whole synchronous burst into * one render after the burst settles. * * Also skips entirely while the map's initial marker load is still * in progress (`markersPlaced` false) — the single `markersplaced` * render at the end of load covers that case, so we don't render * thousands of times against a half-populated list during boot. */ WPGMZA.AtlasMajorMarkerList.prototype.scheduleRender = function(){ var self = this; var map = WPGMZA.maps[0]; /* Initial bulk load — defer to the end-of-load markersplaced render rather than re-rendering per marker. */ if(map && !map.markersPlaced) return; if(self._renderScheduled) return; self._renderScheduled = true; var run = function(){ self._renderScheduled = false; self.render(); }; if(typeof window.requestAnimationFrame === 'function') window.requestAnimationFrame(run); else setTimeout(run, 16); } /** * Render a single marker list item */ WPGMZA.AtlasMajorMarkerList.prototype.renderItem = function(marker, index){ var title = this.esc(this.getDisplayTitle(marker)); var address = this.esc(this.getDisplayAddress(marker)); var id = marker.id; if(!title && address){ title = address; address = ''; } if(!title) title = 'Marker #' + id; /* Resolve the actual icon URL — custom, map default, or global default */ var iconUrl = ''; var rawIcon = marker.icon; if(rawIcon && rawIcon !== ''){ if(typeof rawIcon === 'object' && rawIcon.url){ iconUrl = rawIcon.url; } else if(typeof rawIcon === 'string'){ try { var parsed = JSON.parse(rawIcon); iconUrl = parsed.url || rawIcon; } catch(e){ iconUrl = rawIcon; } } } else if(typeof marker.getIcon === 'function'){ var fetched = marker.getIcon(); if(typeof fetched === 'object' && fetched.url){ iconUrl = fetched.url; } else if(typeof fetched === 'string'){ iconUrl = fetched; } } if(!iconUrl && WPGMZA.defaultMarkerIcon){ iconUrl = WPGMZA.defaultMarkerIcon; } if(!iconUrl && WPGMZA.settings && WPGMZA.settings.default_marker_icon){ iconUrl = WPGMZA.settings.default_marker_icon; } var iconHtml = iconUrl ? '' : '
'; /* Resolve category pills (Pro only) */ var catHtml = ''; if(marker.categories && marker.categories.length && WPGMZA.categories){ var pills = []; for(var c = 0; c < marker.categories.length; c++){ var cat = WPGMZA.categories.getCategoryByID(parseInt(marker.categories[c])); if(cat && cat.name && cat.name.trim() !== ''){ pills.push('' + this.esc(cat.name) + ''); } } if(pills.length){ catHtml = '
' + pills.join('') + '
'; } } var checked = this.selectedIds[id] ? ' checked' : ''; /* Pending approval detection — UGM (visitor-generated markers) submits with approved=0 until an admin approves. The base marker model defaults approved=1, so a value of 0 (number or string) means "needs admin action". Pro's ProAdminMarkerDataTable injects an Approve action when UGM is loaded and approved=0; we mirror that here so VGM users keep this critical workflow in the Atlas Major list. */ var needsApproval = (typeof marker.approved !== 'undefined' && parseInt(marker.approved) === 0); /* Build the action menu items. Pro-only items (Duplicate, Move Map) are gated on WPGMZA.isProVersion(). Approve is gated on needsApproval and is the only item that carries its own red-dot indicator inside the menu (so users can spot the pending action without scanning every label). */ var isPro = WPGMZA.isProVersion(); var menuItems = ''; menuItems += this.renderMenuItem('edit-marker-id', id, 'Edit', ''); menuItems += this.renderMenuItem('adjust-marker-id', id, 'Adjust', ''); menuItems += this.renderMenuItem('center-marker-id', id, 'Center', ''); if(isPro){ menuItems += this.renderMenuItem('duplicate-feature-id', id, 'Duplicate', ''); menuItems += this.renderMenuItem('move-map-feature-id', id, 'Move Map', ''); } if(needsApproval){ menuItems += this.renderMenuItem('approve-marker-id', id, 'Approve', '', 'am-mi-mi-approve am-mi-mi-attention'); } menuItems += this.renderMenuItem('delete-marker-id', id, 'Delete', '', 'am-mi-mi-danger'); /* ---- Developer hook: row PARTS (filter-style) ---- Atlas Major has no DataTable, so the column-extension pattern third-party clients used under Atlas Novus (adding s / filtering cell content) doesn't apply. This is the supported replacement: handlers receive a single mutable `parts` object and can rewrite any rendered segment (title, address, icon, category pills, action-menu items) or read `parts.marker` for context. Mutations are read back below when the row is assembled. WPGMZA's JS has no applyFilters(), so we use the codebase's established jQuery-trigger-with-mutable-payload convention. Underscored event name (no jQuery namespace parsing), mirroring WP's PHP filter naming for familiarity. Example — append the marker ID to every title: jQuery(document.body).on('wpgmza_atlas_major_marker_list_item', function(e, parts){ parts.title += ' #' + parts.marker.id; }); Example — add a custom menu action (reuse renderMenuItem): jQuery(document.body).on('wpgmza_atlas_major_marker_list_item', function(e, parts){ parts.menuItems += WPGMZA.atlasMajorMarkerList.renderMenuItem( 'my-custom-id', parts.id, 'My Action', ''); }); */ var parts = { marker: marker, id: id, title: title, address: address, iconHtml: iconHtml, catHtml: catHtml, menuItems: menuItems, checked: checked, needsApproval: needsApproval }; $(document.body).trigger('wpgmza_atlas_major_marker_list_item', [parts]); var rowHtml = '
  • ' + '' + '
    ' + parts.iconHtml + '
    ' + '
    ' + '
    ' + parts.title + '
    ' + (parts.address ? '
    ' + parts.address + '
    ' : '') + parts.catHtml + '
    ' + '
    ' + /* Single kebab trigger replacing the previous inline icon strip. Matches Novus's "..." menu affordance. The red attention dot sits on this button (via the .am-mi-needs-approval class on the parent
  • ), mirroring exactly where Novus places it. */ '' + '' + '' + '
  • '; /* ---- Developer hook: whole-row HTML (filter-style) ---- Final escape hatch for clients that need to rewrite the row markup wholesale. Handlers receive a mutable `result` object; set `result.html` to replace. `result.marker` provided for context. Runs after the PARTS hook so part-level tweaks are already baked into `result.html`. NB: data-* attributes the list relies on for event delegation (data-marker-id, data-*-marker-id, etc.) must be preserved if you rebuild. */ var result = { html: rowHtml, marker: marker, id: id }; $(document.body).trigger('wpgmza_atlas_major_marker_list_item_html', [result]); return result.html; } /** * Render a single menu item button. SVG body is the inner * //etc. — we wrap with the standard SVG attrs * so menu items render consistently. */ WPGMZA.AtlasMajorMarkerList.prototype.renderMenuItem = function(dataKey, id, label, svgBody, extraClass){ var cls = 'am-mi-mi' + (extraClass ? ' ' + extraClass : ''); return ''; } /** * Render pagination controls */ WPGMZA.AtlasMajorMarkerList.prototype.renderPagination = function(total, totalPages){ if(!this.paginationContainer) return; if(totalPages <= 1){ this.paginationContainer.innerHTML = ''; return; } var self = this; var html = '
    '; /* Prev */ html += ''; /* Page numbers — show max 5 around current */ var startPage = Math.max(1, this.page - 2); var endPage = Math.min(totalPages, startPage + 4); if(endPage - startPage < 4) startPage = Math.max(1, endPage - 4); for(var p = startPage; p <= endPage; p++){ html += ''; } /* Next */ html += ''; html += '' + this.page + ' / ' + totalPages + ''; html += '
    '; this.paginationContainer.innerHTML = html; /* Bind pagination clicks */ $(this.paginationContainer).off('click.ampag').on('click.ampag', '.am-pag-btn:not(.disabled)', function(){ var page = parseInt($(this).attr('data-page')); if(page >= 1 && page <= totalPages){ self.page = page; self.render(); /* Scroll to top of list */ $(self.container).closest('.am-panel-scroll')[0].scrollTop = 0; } }); } /** * Bind search input with debounce */ WPGMZA.AtlasMajorMarkerList.prototype.bindSearch = function(){ var self = this; if(!this.searchInput) return; $(this.searchInput).on('input', function(){ clearTimeout(self.debounceTimer); self.debounceTimer = setTimeout(function(){ self.searchTerm = $(self.searchInput).val(); self.page = 1; self.render(); }, 200); }); } /** * Update the count badge next to "Markers" heading */ WPGMZA.AtlasMajorMarkerList.prototype.updateCount = function(filteredCount){ if(!this.countBadge) return; var map = WPGMZA.maps[0]; var total = (map && map.markers) ? map.markers.length : 0; if(this.searchTerm){ this.countBadge.textContent = filteredCount + ' / ' + total; } else { this.countBadge.textContent = total; } } /** * Bind click events on rendered list items */ WPGMZA.AtlasMajorMarkerList.prototype.bindItemEvents = function(){ var self = this; var container = $(this.container); container.off('click.amlist').on('click.amlist', '.am-mi', function(e){ if($(e.target).closest('.am-mi-ops').length) return; if($(e.target).closest('.am-mi-check').length) return; self.editMarker($(this).data('marker-id')); }); /* Kebab toggle — opens the per-row action menu. We close any other open menu first so only one is visible at a time. The menu closes via the document-level handler installed in initKebabDismiss(). */ container.on('click.amlist', '.am-mi-kebab', function(e){ e.stopPropagation(); var $kebab = $(this); var $li = $kebab.closest('.am-mi'); var alreadyOpen = $li.hasClass('am-mi-menu-open'); /* Close all other menus */ $(self.container).find('.am-mi.am-mi-menu-open').removeClass('am-mi-menu-open') .find('.am-mi-kebab').attr('aria-expanded', 'false'); if(!alreadyOpen){ $li.addClass('am-mi-menu-open'); $kebab.attr('aria-expanded', 'true'); } }); /* Close-on-action: clicking any menu item should close the menu after the action's own handler fires. */ container.on('click.amlist', '.am-mi-menu .am-mi-mi', function(){ $(this).closest('.am-mi').removeClass('am-mi-menu-open') .find('.am-mi-kebab').attr('aria-expanded', 'false'); }); self.bindBulkCheckboxes(); self.updateBulkBar(); container.on('click.amlist', '[data-edit-marker-id]', function(e){ e.stopPropagation(); self.editMarker($(this).attr('data-edit-marker-id')); }); container.on('click.amlist', '[data-center-marker-id]', function(e){ e.stopPropagation(); self.centerMarker($(this).attr('data-center-marker-id')); }); /* Adjust — do NOT stopPropagation. The core MarkerPanel listens on document.body for [data-adjust-marker-id] clicks. We just need to ensure the editor grouping opens. */ container.on('click.amlist', '[data-adjust-marker-id]', function(e){ /* Let the event bubble to MarkerPanel.onAdjustFeature on body */ }); container.on('click.amlist', '[data-duplicate-feature-id]', function(e){ e.stopPropagation(); self.duplicateMarker($(this).attr('data-duplicate-feature-id')); }); container.on('click.amlist', '[data-move-map-feature-id]', function(e){ e.stopPropagation(); self.moveMarkerToMap($(this).attr('data-move-map-feature-id')); }); container.on('click.amlist', '[data-approve-marker-id]', function(e){ e.stopPropagation(); self.approveMarker($(this).attr('data-approve-marker-id')); }); container.on('click.amlist', '[data-delete-marker-id]', function(e){ e.stopPropagation(); self.deleteMarker($(this).attr('data-delete-marker-id')); }); } /** * Bind to map events for live updates */ WPGMZA.AtlasMajorMarkerList.prototype.bindMapEvents = function(){ var self = this; var map = WPGMZA.maps[0]; if(!map) return; /* Coalesced (scheduleRender) rather than a direct render per event — on large maps these fire once per marker during bulk adds/removes and would otherwise re-render thousands of times. scheduleRender also no-ops while the initial load is still in progress; the markersplaced handler below renders once at the end of that load. */ map.on('markeradded', function(){ self.scheduleRender(); }); map.on('markerremoved', function(){ self.scheduleRender(); }); /* Authoritative "all initial markers are loaded" event. This * covers the case where the marker list constructor runs * BEFORE the map has finished fetching its initial marker * batch — constructor's render() sees 0 markers, tab shows * empty, and without this listener we'd miss the later load. * If `markersplaced` already fired (map.markersPlaced is true), * we render once right now instead of waiting. Direct render() * here (not scheduleRender) since it's a single end-of-load * call and scheduleRender would no-op if markersPlaced flips * true mid-frame. * Either path clears the initial-load busy state (set in the * constructor when !map.markersPlaced) — render is the real * "list is up to date" signal here. */ if(map.markersPlaced){ self.render(); self.setBusy(false); } else { map.on('markersplaced', function(){ self.render(); self.setBusy(false); }); } $(document.body).on('datatable.reload', function(){ setTimeout(function(){ self.render(); }, 500); }); } WPGMZA.AtlasMajorMarkerList.prototype.editMarker = function(id){ var panel = WPGMZA.mapEditPage ? WPGMZA.mapEditPage.markerPanel : null; if(panel && typeof panel.select === 'function'){ panel.select(id); } if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.sidebarGroupings){ WPGMZA.mapEditPage.sidebarGroupings.openTabByGroupId('map-markers-editor'); } /* Open the marker's infowindow on the Live Preview map so the user sees what they're editing in its rendered state. Safe in the editor because the viewportGroupings null-guards in pro-info-window.js (and siblings) make populatePanel tolerant of admin context where viewportGroupings is absent. */ var map = WPGMZA.maps[0]; var marker = map ? map.getMarkerByID(id) : null; if(marker && typeof marker.openInfoWindow === 'function'){ marker.openInfoWindow(); } } WPGMZA.AtlasMajorMarkerList.prototype.centerMarker = function(id){ var marker = WPGMZA.maps[0].getMarkerByID(id); if(marker){ WPGMZA.maps[0].setCenter(new WPGMZA.LatLng({ lat: marker.lat, lng: marker.lng })); } } /** * Duplicate a marker via REST API (Pro only) */ WPGMZA.AtlasMajorMarkerList.prototype.duplicateMarker = function(id){ var self = this; var map = WPGMZA.maps[0]; self.setBusy(true); WPGMZA.restAPI.call("/markers/", { method: "POST", data: { id: id, action: "duplicate" }, success: function(response, status, xhr) { /* The list renders from map.markers (no server-backed DataTable like Novus), so the freshly-created marker has to be pushed into the map. The duplicate endpoint now returns the new marker (see ProRestAPI::markers duplicate case); instantiate + addMarker, which fires `markeradded` -> render() via bindMapEvents. */ if(response && response.marker && map && typeof map.addMarker === 'function'){ var marker = WPGMZA.Marker.createInstance(response.marker); map.addMarker(marker); } else { /* Fallback for older Pro that doesn't return the marker — at least re-render from current state. */ self.render(); } /* Keep the hidden DataTable (bulk-edit modal source) in sync with the new row. */ if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ WPGMZA.mapEditPage.markerAdminDataTable.reload(); } self.setBusy(false); }, error: function(){ self.setBusy(false); } }); } /** * Move marker to a different map (Pro only) * Reuses the existing AdminFeatureDataTable.onMoveMap which * shows the .wpgmza-map-select-modal generic modal. The modal * is created during datatable construction; we just delegate * the call so the modal lifecycle stays in one place. */ WPGMZA.AtlasMajorMarkerList.prototype.moveMarkerToMap = function(id){ var self = this; var dt = WPGMZA.mapEditPage ? WPGMZA.mapEditPage.markerAdminDataTable : null; if(!dt || typeof dt.onMoveMap !== 'function') return; /* onMoveMap accepts either an event OR a bare id when event is undefined. Passing the id directly fires the modal flow. */ dt.onMoveMap(id); } /** * Approve a pending marker (Pro + UGM) * Mirrors MarkerPanel.onApproveMarker — POST /markers/{id} with * approved=1. Pro's existing handler updates the datatable; we * additionally flip the local marker.approved so the list * re-renders without the red-dot indicator + approve button. */ WPGMZA.AtlasMajorMarkerList.prototype.approveMarker = function(id){ var self = this; var map = WPGMZA.maps[0]; if(!map) return; self.setBusy(true); WPGMZA.restAPI.call("/markers/" + id, { method: "POST", data: { approved: "1" }, success: function(){ /* Update the local marker model so the list reflects the new state immediately without waiting for a full markers refetch. */ var marker = map.getMarkerByID(id); if(marker){ marker.approved = 1; } self.render(); /* Keep the hidden DataTable (used by bulk-edit modal lookups) in sync as well. */ if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ WPGMZA.mapEditPage.markerAdminDataTable.reload(); } self.setBusy(false); }, error: function(){ self.setBusy(false); } }); } WPGMZA.AtlasMajorMarkerList.prototype.deleteMarker = function(id){ var self = this; if(!confirm(WPGMZA.localized_strings.general_delete_prompt_text || 'Are you sure you want to delete this marker?')) return; var map = WPGMZA.maps[0]; var marker = map.getMarkerByID(id); /* Delete via the REST API (DELETE /markers/{id}) — the same path FeaturePanel.onDeleteFeature() uses (feature-panel.js ~L592). The previous legacy `$.post(ajaxurl, action: delete_marker)` call relied on `wp_ajax_delete_marker`, which is only registered by the pro-below-8.1 compatibility layer (hooking a deprecated empty stub); on current Pro no handler is registered, so admin-ajax returned 400 and the marker was never actually deleted. We also now remove the marker only AFTER the delete succeeds, rather than optimistically before a request that could fail. */ self.setBusy(true); WPGMZA.restAPI.call("/markers/" + id, { method: "DELETE", success: function(data, status, xhr){ if(marker) map.removeMarker(marker); /* fires markerremoved -> render() */ else map.removeMarkerByID(id); self.render(); if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ WPGMZA.mapEditPage.markerAdminDataTable.reload(); } self.setBusy(false); }, error: function(){ self.setBusy(false); } }); } /** * Create the bulk action bar (inserted after search field) */ WPGMZA.AtlasMajorMarkerList.prototype.initBulkBar = function(){ var self = this; var searchField = document.querySelector('.am-search-field'); if(!searchField) return; var bar = document.createElement('div'); bar.className = 'am-bulk-bar'; bar.style.display = 'none'; bar.innerHTML = '' + '' + '' + ''; searchField.after(bar); this.bulkBar = bar; $(bar).on('click', '[data-bulk-action]', function(e){ var action = $(this).attr('data-bulk-action'); if(action === 'select-all') self.bulkSelectAll(); else if(action === 'deselect-all') self.bulkDeselectAll(); else if(action === 'bulk-edit') self.bulkEdit(); else if(action === 'bulk-delete') self.bulkDelete(); }); } /** * Get the bulk edit modal (Pro only) * Reuses the modal already created by the hidden AdminFeatureDataTable */ WPGMZA.AtlasMajorMarkerList.prototype.getBulkEditModal = function(){ /* Try the datatable's lazy getter first */ var tables = WPGMZA.mapEditPage ? WPGMZA.mapEditPage.markerDataTable || WPGMZA.mapEditPage.markerAdminDataTable : null; if(tables && typeof tables.getBulkEditorModal === 'function'){ return tables.getBulkEditorModal(); } /* Fallback: find any AdminFeatureDataTable for markers */ if(WPGMZA.AdminFeatureDataTable){ var el = $('[data-wpgmza-table][data-wpgmza-feature-type="marker"]')[0]; if(el && el.wpgmzaDataTable && typeof el.wpgmzaDataTable.getBulkEditorModal === 'function'){ return el.wpgmzaDataTable.getBulkEditorModal(); } } return false; } /** * Update bulk bar visibility and info */ WPGMZA.AtlasMajorMarkerList.prototype.updateBulkBar = function(){ if(!this.bulkBar) return; var count = Object.keys(this.selectedIds).length; if(count > 0){ this.bulkBar.style.display = 'flex'; } else { this.bulkBar.style.display = 'none'; } } /** * Bind checkbox change events (called after render) */ WPGMZA.AtlasMajorMarkerList.prototype.bindBulkCheckboxes = function(){ var self = this; $(this.container).off('change.ambulk').on('change.ambulk', '[data-bulk-marker-id]', function(e){ e.stopPropagation(); var id = $(this).attr('data-bulk-marker-id'); if(this.checked){ self.selectedIds[id] = true; } else { delete self.selectedIds[id]; } $(this).closest('.am-mi').toggleClass('am-mi-selected', this.checked); self.updateBulkBar(); }); /* Prevent checkbox click from triggering marker edit */ $(this.container).off('click.ambulkstop').on('click.ambulkstop', '.am-mi-check', function(e){ e.stopPropagation(); }); } WPGMZA.AtlasMajorMarkerList.prototype.bulkSelectAll = function(){ var filtered = this.getFilteredMarkers(); for(var i = 0; i < filtered.length; i++){ this.selectedIds[filtered[i].id] = true; } this.render(); } WPGMZA.AtlasMajorMarkerList.prototype.bulkDeselectAll = function(){ this.selectedIds = {}; this.render(); } WPGMZA.AtlasMajorMarkerList.prototype.getSelectedIds = function(){ return Object.keys(this.selectedIds).map(function(id){ return parseInt(id); }); } WPGMZA.AtlasMajorMarkerList.prototype.bulkDelete = function(){ var self = this; var ids = this.getSelectedIds(); if(!ids.length) return; if(!confirm(WPGMZA.localized_strings.general_delete_prompt_text || 'Are you sure you want to delete these markers?')) return; var map = WPGMZA.maps[0]; ids.forEach(function(id){ try { map.removeMarkerByID(id); } catch(e){} }); WPGMZA.restAPI.call("/markers/", { method: "DELETE", data: { ids: ids }, complete: function(){ self.selectedIds = {}; self.render(); if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ WPGMZA.mapEditPage.markerAdminDataTable.reload(); } } }); } WPGMZA.AtlasMajorMarkerList.prototype.bulkEdit = function(){ var self = this; var ids = this.getSelectedIds(); if(!ids.length) return; var modal = this.getBulkEditModal(); if(modal){ modal.show(function(data){ data.ids = ids; data.action = "bulk_edit"; WPGMZA.restAPI.call("/markers/", { method: "POST", data: data, success: function(){ self.selectedIds = {}; self.render(); if(WPGMZA.mapEditPage && WPGMZA.mapEditPage.markerAdminDataTable){ WPGMZA.mapEditPage.markerAdminDataTable.reload(); } } }); }); } } WPGMZA.AtlasMajorMarkerList.prototype.esc = function(str){ var d = document.createElement('div'); d.textContent = str; return d.innerHTML; } /* Init */ function initMarkerList(){ if(WPGMZA.maps && WPGMZA.maps[0] && document.querySelector('.am-marker-list-container')){ if(!WPGMZA.atlasMajorMarkerList){ WPGMZA.atlasMajorMarkerList = new WPGMZA.AtlasMajorMarkerList(); } } } $(document.body).on('wpgmza_map_edit_page_created', function(){ setTimeout(initMarkerList, 1000); }); $(window).on('load', function(){ setTimeout(initMarkerList, 1500); }); $(document.body).on('markersfetched', function(){ if(WPGMZA.atlasMajorMarkerList){ WPGMZA.atlasMajorMarkerList.render(); } }); /* (Removed) Previously this file installed a sidebar-delegate-created * handler that scrolled the Add Marker panel back to the top and * refocused the address input — the old request was to keep users * on the Add Marker form for bulk-add workflows. That requirement * has been reversed: after a successful Add, the editor should now * return to the marker list panel (consistent with what Edit Marker * already does via sidebar-groupings.js's `sidebar-delegate-saved` * handler). The navigate-back is handled in sidebar-groupings.js's * `sidebar-delegate-created` handler — nothing Atlas-Major-specific * needs to run here anymore. */ });