PageSourceSearch

https://www.endangeredalphabets.net/wp-content/plugins/wp-google-m…?ver=10.1.08%2Bpro-10.1.05

js endangeredalphabets.net collected 2026-09-24 19:51:16 UTC 41,775 bytes, 1,150 lines download raw bytes

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.