$preview_config['map']['style_url'], // MapLibre wants [lng, lat]; stored config is [lat, lng] — see class-frontend.php's own swap. 'center' => array( $preview_config['map']['center'][1], $preview_config['map']['center'][0] ), 'zoom' => $preview_config['map']['zoom'], // When "Customize the starting view" isn't on, the preview opens on // this instead of the raw stored center/zoom, so it shows what // visitors actually see (the real front-end auto-fit) rather than // an arbitrary default — null when the map has no located Spots yet. 'fitBounds' => $preview_slug ? self::spot_bounds( $preview_slug ) : null, 'i18n' => array( 'validate' => __( 'Validate', 'mapfield-spots' ), 'changeView' => __( 'Change starting view', 'mapfield-spots' ), ), ) ); // PRO enqueues its own custom-image-picker.js (+ wp_enqueue_media()) and // zones-preview.js here (Mapfield_Spots_Pro\Customization, \Zones) — // both are no-ops without PRO's own markup for them to attach to. do_action( 'mapfield_spots_map_type_admin_assets' ); } public function render(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $editing = isset( $_GET['edit'] ) ? sanitize_key( $_GET['edit'] ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view routing, not state-changing. $tool = isset( $_GET['tool'] ) ? sanitize_key( $_GET['tool'] ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- same. $slug = isset( $_GET['slug'] ) ? sanitize_key( $_GET['slug'] ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- same. echo '
'; if ( $editing || isset( $_GET['new'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- same. $this->render_form( $editing ); } elseif ( $tool && $slug ) { $this->render_tool( $tool, $slug ); } else { $this->render_list(); } echo '
'; } /** * Dispatch point for a PRO add-on's admin tool screen (currently just * Import/Export). Free plugin renders the shell chrome and hands the * body off via the `mapfield_spots_render_admin_tool` action — same * extension-point pattern as the Zones filter; never references PRO directly. */ private function render_tool( string $tool, string $slug ): void { $config = Map_Types::get( $slug ); if ( ! $config ) { wp_die( esc_html__( 'Unknown map.', 'mapfield-spots' ) ); } ?>

' . esc_html__( 'PRO version', 'mapfield-spots' ) . '' ) ); ?>

' . esc_html( $group['title'] ) . '' ) ); ?>

1 ) : ?>
$config ) : $page = (int) floor( $i / self::CARDS_PER_PAGE ) + 1; $this->render_map_card( $slug, $config, $page ); ++$i; endforeach; $total_pages = (int) ceil( count( $types ) / self::CARDS_PER_PAGE ); ?>
1 ) { // Hides page 2+ cards as early as possible, before admin.js's own // DOMContentLoaded pagination logic would (avoids a flash of all cards). wp_add_inline_script( 'mapfield-spots-admin', "document.querySelectorAll( '.mapfield_spots-map-card' ).forEach( function ( card ) {\n" . "\tif ( '1' !== card.dataset.page ) card.hidden = true;\n" . '} );', 'before' ); } ?> render_pagination( $total_pages ); ?>

  • —
  • / —
  • —
  • —

render_howto_shared_params(); ?>
[mapfield_spots_map type="…" list="false" height="500px" category="restaurants,cafes" title="true"]

mapfield_spots_render_map()' ); ?>

render_howto_shared_params(); ?> , // this file's own tab-indentation would otherwise leak into the output. $theme_template_example = "echo mapfield_spots_render_map( 'restaurants', array(\n" . " 'list' => false,\n" . " 'height' => '500px',\n" . " 'category' => 'restaurants,cafes',\n" . " 'title' => true,\n" . ') );'; ?>
render_preview_modal(); ?>
data-title-template="">
publish ?? 0 ); $mapbox = Settings::get_mapbox_settings(); $style = $config['map']['style_url'] ?: $mapbox['style_url']; $marker = Marker_Library::resolve( $config['map'] ); // Centered on a real Spot when one exists, built from its lat/lng meta // directly in MapLibre's [lng, lat] order (config['map']['center'] is // stored [lat, lng] instead) to avoid a swap. orderby=rand so the card // doesn't always show the same corner of the map. $sample_spot_id = get_posts( array( 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => 1, 'orderby' => 'rand', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_orderby -- small scale, same tradeoff already accepted elsewhere in this codebase (e.g. Zones::get_zones_geojson()'s own meta_query) 'fields' => 'ids', ) ); $center = null; $marker_color = null; if ( $sample_spot_id ) { $lat = (float) get_post_meta( $sample_spot_id[0], '_mapfield_spots_lat', true ); $lng = (float) get_post_meta( $sample_spot_id[0], '_mapfield_spots_lng', true ); if ( $lat && $lng ) { $center = array( $lng, $lat ); // Same "lowest term_id wins" primary-category rule as // Frontend::spot_categories(), so this stand-in marker matches // the real one, including the fallback colour. $marker_color = '#E03137'; if ( ! empty( $config['fields']['category'] ) ) { $terms = get_the_terms( $sample_spot_id[0], CPT::taxonomy_slug( $slug ) ); if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) { usort( $terms, function ( $a, $b ) { return $a->term_id <=> $b->term_id; } ); $marker_color = get_term_meta( $terms[0]->term_id, 'mapfield_spots_color', true ) ?: '#E03137'; } } } } $zoom = $center ? 12 : 5; // France's geographic centre, fallback when no Spot exists yet — no // marker_color is set in that case, so no marker shows. $center = $center ?: array( 2.4, 46.6 ); // Zones are PRO-only; free plugin only sees them via this filter // (same one render_map() uses for the front-end zones layer). $zones = apply_filters( 'mapfield_spots_map_zones', array(), $slug ); $zone_count = is_array( $zones ) && ! empty( $zones['features'] ) ? count( $zones['features'] ) : 0; ?>
[mapfield_spots_map type=""]

__( 'Map name', 'mapfield-spots' ), 'categories' => __( 'Spot categories', 'mapfield-spots' ), 'spots' => __( 'Spot contents', 'mapfield-spots' ), 'style' => __( 'Style', 'mapfield-spots' ), 'toolbar' => __( 'Map toolbar', 'mapfield-spots' ), ) ); ?>
$tab_label ) : ?>

: browser extensions (password managers etc.) // can still set .value via JS on those. A hidden input isn't targeted. ?>
wrapper — see the tabs array above, no // upsell fallback needed since the tab button only exists when PRO added it. do_action( 'mapfield_spots_map_type_tab_sections', $slug, $config ); ?>
document.querySelector('input[name="fields[rating]"]').addEventListener('change', function () { document.getElementById('mapfield_spots-rating-settings-row').hidden = ! this.checked; }); document.getElementById('mapfield_spots-initial-view-enabled').addEventListener('change', function () { document.getElementById('mapfield_spots-initial-view-fields').hidden = ! this.checked; }); (function() { var singularInput = document.getElementById('label_singular'); var slugInput = document.getElementById('slug'); var slugManuallyEdited = false; function slugify(str) { return str.toLowerCase().trim() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') // Matches the input's own maxlength; trims a trailing dash the cut might leave. .slice(0, ) .replace(/-+$/, ''); } slugInput.addEventListener('input', function() { slugManuallyEdited = true; }); singularInput.addEventListener('input', function() { if (slugManuallyEdited) return; slugInput.value = slugify(singularInput.value); }); })(); document.getElementById('mapfield_spots-add-category').addEventListener('click', function() { var wrap = document.getElementById('mapfield_spots-categories-repeater'); var i = wrap.children.length; var row = document.createElement('div'); row.className = 'mapfield_spots-cat-row'; row.innerHTML = '' + '
' + '' + '
' + '
' + '
' + '' + '
#
' + '
' + ''; wrap.appendChild(row); var colorWrap = row.querySelector('.mapfield_spots-color-picker-wrap'); if (window.MapfieldSpotsColorPicker) { MapfieldSpotsColorPicker.initOne(colorWrap); } // Lets color-palettes.js fill this new row with the active theme's next colour. document.dispatchEvent(new CustomEvent('mapfield_spots:category-row-added', { detail: { wrap: colorWrap, index: i } })); }); // Delegated so it also works for rows added after this script ran. document.getElementById('mapfield_spots-categories-repeater').addEventListener('click', function(e) { if (e.target.classList.contains('mapfield_spots-cat-row__remove')) { if (!confirm('')) { return; } e.target.closest('.mapfield_spots-cat-row').remove(); } });

>
>
>
>
>

>
  • 9:00 – 18:00
>
> >

render_color_field( 'theme[list_bg]', $theme['list_bg'] ); ?>
render_color_field( 'theme[item_bg]', $theme['item_bg'] ); ?>
render_color_field( 'theme[link_color]', $theme['link_color'] ); ?>
render_color_field( 'theme[border_color]', $theme['border_color'] ); ?>
render_color_field( 'theme[accent_color]', $theme['accent_color'] ); ?>
render_color_field( 'theme[text_color]', $theme['text_color'] ); ?>
#
CPT::post_type_slug( $slug ), 'posts_per_page' => -1, 'post_status' => 'publish', 'fields' => 'ids', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- same query shape as Frontend::render_map(), admin-only and once per page load. 'relation' => 'AND', array( 'key' => '_mapfield_spots_lat', 'value' => '', 'compare' => '!=' ), array( 'key' => '_mapfield_spots_lng', 'value' => '', 'compare' => '!=' ), ), ) ); $min_lat = null; $max_lat = null; $min_lng = null; $max_lng = null; foreach ( $post_ids as $post_id ) { $lat = (float) get_post_meta( $post_id, '_mapfield_spots_lat', true ); $lng = (float) get_post_meta( $post_id, '_mapfield_spots_lng', true ); if ( ! $lat || ! $lng ) { continue; } $min_lat = null === $min_lat ? $lat : min( $min_lat, $lat ); $max_lat = null === $max_lat ? $lat : max( $max_lat, $lat ); $min_lng = null === $min_lng ? $lng : min( $min_lng, $lng ); $max_lng = null === $max_lng ? $lng : max( $max_lng, $lng ); } // Zones are PRO-only; free plugin only sees them via this filter — same // one the "Your maps" card list and render_map() use. A map with Zones // but no (or few) point Spots, e.g. a country outline map, still needs // its real extent — not just whatever the Spots alone cover. $zones = apply_filters( 'mapfield_spots_map_zones', array(), $slug ); if ( is_array( $zones ) && ! empty( $zones['features'] ) ) { foreach ( $zones['features'] as $feature ) { $ring = $feature['geometry']['coordinates'][0] ?? array(); foreach ( $ring as $point ) { if ( ! isset( $point[0], $point[1] ) ) { continue; } list( $lng, $lat ) = $point; // GeoJSON order, unlike the Spot meta above. $min_lat = null === $min_lat ? $lat : min( $min_lat, $lat ); $max_lat = null === $max_lat ? $lat : max( $max_lat, $lat ); $min_lng = null === $min_lng ? $lng : min( $min_lng, $lng ); $max_lng = null === $max_lng ? $lng : max( $max_lng, $lng ); } } } if ( null === $min_lat ) { return null; } return array( array( $min_lng, $min_lat ), array( $max_lng, $max_lat ) ); } /** * Illustrative-only value per ACF field type for the live preview card. * Mirrors PRO's Acf_Display::format_value() markup shape (real anchors, * swatches, tags) so the preview matches actual front-end rendering. */ private static function acf_preview_value( string $type ): string { switch ( $type ) { case 'number': return '42'; case 'true_false': return esc_html__( 'Yes', 'mapfield-spots' ); case 'date_picker': return esc_html( gmdate( 'F j, Y' ) ); case 'email': return 'name@example.com'; case 'url': case 'link': return 'example.com'; case 'color_picker': return ''; case 'checkbox': case 'select': return '' . '' . esc_html__( 'Option A', 'mapfield-spots' ) . '' . '' . esc_html__( 'Option B', 'mapfield-spots' ) . '' . ''; default: return esc_html__( 'Example value', 'mapfield-spots' ); } } private function render_category_row( int $i, string $label, string $color ): void { ?>
#
array( 'label' => __( 'Pastel', 'mapfield-spots' ), 'colors' => array( '#FFADAD', '#FFD6A5', '#FDFFB6', '#CAFFBF', '#9BF6FF', '#A0C4FF' ), ), 'sunset' => array( 'label' => __( 'Sunset', 'mapfield-spots' ), 'colors' => array( '#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#43AA8B' ), ), 'seaside' => array( 'label' => __( 'Seaside', 'mapfield-spots' ), 'colors' => array( '#264653', '#2A9D8F', '#8AB17D', '#E9C46A', '#F4A261', '#E76F51' ), ), 'vivid' => array( 'label' => __( 'Vivid', 'mapfield-spots' ), 'colors' => array( '#FF595E', '#FFCA3A', '#8AC926', '#1982C4', '#6A4C93', '#52B69A' ), ), 'autumn' => array( 'label' => __( 'Autumn', 'mapfield-spots' ), 'colors' => array( '#9B2226', '#CA6702', '#E0A458', '#606C38', '#5C4D7D', '#774936' ), ), 'neon' => array( 'label' => __( 'Neon', 'mapfield-spots' ), 'colors' => array( '#FF006E', '#FB5607', '#FFBE0B', '#8338EC', '#3A86FF', '#06D6A0' ), ), 'ocean' => array( 'label' => __( 'Ocean', 'mapfield-spots' ), 'colors' => array( '#03045E', '#0077B6', '#52B69A', '#F2C14E', '#E9724C', '#90E0EF' ), ), 'forest' => array( 'label' => __( 'Forest', 'mapfield-spots' ), 'colors' => array( '#1B4332', '#6B8F71', '#7A5C3E', '#D4B483', '#9D2449', '#4A6FA5' ), ), 'berry' => array( 'label' => __( 'Berry', 'mapfield-spots' ), 'colors' => array( '#590D22', '#4A1942', '#277DA1', '#F72585', '#90BE6D', '#F9C74F' ), ), 'tropical' => array( 'label' => __( 'Tropical', 'mapfield-spots' ), 'colors' => array( '#073B4C', '#118AB2', '#06D6A0', '#FFD166', '#EF476F', '#7209B7' ), ), 'lavender' => array( 'label' => __( 'Lavender', 'mapfield-spots' ), 'colors' => array( '#7B2CBF', '#F4ACB7', '#84A98C', '#A2D2FF', '#FFE8A3', '#D68C45' ), ), 'nordic' => array( 'label' => __( 'Nordic', 'mapfield-spots' ), 'colors' => array( '#293241', '#3D5A80', '#98C1D9', '#B8B8FF', '#EE6C4D', '#E0FBFC' ), ), 'candy' => array( 'label' => __( 'Candy', 'mapfield-spots' ), 'colors' => array( '#FF6B9D', '#C44FE7', '#6C5CE7', '#74B9FF', '#55EFC4', '#FFEAA7' ), ), 'citrus' => array( 'label' => __( 'Citrus', 'mapfield-spots' ), 'colors' => array( '#FFD60A', '#FFC300', '#FF8500', '#FB8B24', '#9EF01A', '#70E000' ), ), 'slate' => array( 'label' => __( 'Slate', 'mapfield-spots' ), 'colors' => array( '#415A77', '#6B9080', '#BC6C25', '#8A817C', '#5C4D7D', '#A4AC96' ), ), 'wine' => array( 'label' => __( 'Wine', 'mapfield-spots' ), 'colors' => array( '#6A040F', '#1B4332', '#CA6702', '#3C096C', '#283618', '#9C6644' ), ), 'sky' => array( 'label' => __( 'Sky', 'mapfield-spots' ), 'colors' => array( '#90E0EF', '#FFD60A', '#CED4DA', '#80B918', '#FF8FA3', '#9B5DE5' ), ), 'desert' => array( 'label' => __( 'Desert', 'mapfield-spots' ), 'colors' => array( '#D4A373', '#588157', '#BC4749', '#40A9A7', '#8661C1', '#F2B134' ), ), 'teal' => array( 'label' => __( 'Teal', 'mapfield-spots' ), 'colors' => array( '#0B6E5C', '#FF6B6B', '#F4E285', '#14213D', '#A8DF8E', '#FF8FAB' ), ), 'jewel' => array( 'label' => __( 'Jewel', 'mapfield-spots' ), 'colors' => array( '#0B132B', '#5A189A', '#9D0208', '#007F5F', '#FFB703', '#003566' ), ), 'spectrum' => array( 'label' => __( 'Spectrum', 'mapfield-spots' ), 'colors' => array( '#D62828', '#F77F00', '#FCBF49', '#52796F', '#003049', '#8338EC' ), ), 'blossom' => array( 'label' => __( 'Blossom', 'mapfield-spots' ), 'colors' => array( '#FF8FAB', '#588157', '#FFD60A', '#90E0EF', '#C77DFF', '#F4845F' ), ), 'midnight' => array( 'label' => __( 'Midnight', 'mapfield-spots' ), 'colors' => array( '#0D1321', '#FFD60A', '#748CAB', '#3C096C', '#52B788', '#BB3E03' ), ), 'meadow' => array( 'label' => __( 'Meadow', 'mapfield-spots' ), 'colors' => array( '#588157', '#E63946', '#FFD60A', '#89C2D9', '#9D4EDD', '#F4A6CD' ), ), 'rose' => array( 'label' => __( 'Rose', 'mapfield-spots' ), 'colors' => array( '#FB6F92', '#588157', '#6F4E37', '#4A6FA5', '#FFD166', '#6A040F' ), ), 'amber' => array( 'label' => __( 'Amber', 'mapfield-spots' ), 'colors' => array( '#FFB000', '#1D7874', '#9E2A2B', '#606C38', '#5C4D7D', '#F2E8CF' ), ), 'arctic' => array( 'label' => __( 'Arctic', 'mapfield-spots' ), 'colors' => array( '#A8DADC', '#52B788', '#1D3557', '#F4ACB7', '#6C757D', '#FFF3B0' ), ), 'carnival' => array( 'label' => __( 'Carnival', 'mapfield-spots' ), 'colors' => array( '#E63946', '#F1A208', '#2A9134', '#1D7874', '#2E5A6E', '#7B2D8E' ), ), 'peach' => array( 'label' => __( 'Peach', 'mapfield-spots' ), 'colors' => array( '#FFB997', '#95D5B2', '#F4ACB7', '#89C2D9', '#FFC971', '#6D4C7D' ), ), 'plum' => array( 'label' => __( 'Plum', 'mapfield-spots' ), 'colors' => array( '#6A2C70', '#8AC926', '#FFCA3A', '#6A9FB5', '#B23A48', '#5C4033' ), ), ); } private function render_palette_picker(): void { ?>
color_palettes() as $key => $palette ) : ?>
sanitize_title( $cat['label'] ), 'label' => sanitize_text_field( $cat['label'] ), 'color' => sanitize_hex_color( $cat['color'] ?? '' ) ?: '#E03137', // Matches --mapfield_spots-alert. ); } } // CPT::register_taxonomy() only adds/updates configured terms, never // deletes ones removed from the list — must be done explicitly here. if ( $original_id ) { $previous_config = Map_Types::get( $original_id ); if ( $previous_config ) { $new_slugs = wp_list_pluck( $categories, 'slug' ); $removed_slugs = array_diff( wp_list_pluck( $previous_config['categories'], 'slug' ), $new_slugs ); $taxonomy = CPT::taxonomy_slug( $original_id ); foreach ( $removed_slugs as $removed_slug ) { $term = get_term_by( 'slug', $removed_slug, $taxonomy ); if ( $term ) { wp_delete_term( $term->term_id, $taxonomy ); } } } } $config = array( 'public_slug' => $public_slug, 'label_singular' => sanitize_text_field( wp_unslash( $_POST['label_singular'] ?? '' ) ), 'label_plural' => sanitize_text_field( wp_unslash( $_POST['label_plural'] ?? '' ) ), 'fields' => array( 'category_badges' => ! empty( $_POST['fields']['category_badges'] ), 'photo' => ! empty( $_POST['fields']['photo'] ), 'description' => ! empty( $_POST['fields']['description'] ), 'category' => ! empty( $categories ), // derived: adding a category enables the field 'website' => ! empty( $_POST['fields']['website'] ), 'phone' => ! empty( $_POST['fields']['phone'] ), 'cta' => ! empty( $_POST['fields']['cta'] ), 'gallery' => ! empty( $_POST['fields']['gallery'] ), 'hours' => ! empty( $_POST['fields']['hours'] ), 'social' => ! empty( $_POST['fields']['social'] ), 'rating' => ! empty( $_POST['fields']['rating'] ), 'directions' => ! empty( $_POST['fields']['directions'] ), ), 'toolbar' => array( 'category_legend' => ! empty( $_POST['toolbar']['category_legend'] ), 'fullscreen' => ! empty( $_POST['toolbar']['fullscreen'] ), 'buildings3d' => ! empty( $_POST['toolbar']['buildings3d'] ), 'satellite' => ! empty( $_POST['toolbar']['satellite'] ), 'terrain' => ! empty( $_POST['toolbar']['terrain'] ), ), 'categories' => $categories, 'map' => array( 'style_url' => Settings::sanitize_style_url( wp_unslash( $_POST['style_url'] ?? '' ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_style_url() IS the sanitizer; analyzer only recognizes core functions by name. // center/zoom below only matter on the front end when this is true // (see Frontend::localize_map() and frontend.js's startMap()) — when // false, stick with the plain default rather than keep whatever was // last picked, since it's otherwise unused. 'initial_view_enabled' => ! empty( $_POST['initial_view_enabled'] ), 'center' => ! empty( $_POST['initial_view_enabled'] ) ? array( self::clamp_float( wp_unslash( $_POST['initial_view_lat'] ?? 0 ), -90, 90 ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clamp_float() IS the sanitizer; analyzer only recognizes core functions by name. self::clamp_float( wp_unslash( $_POST['initial_view_lng'] ?? 0 ), -180, 180 ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clamp_float() IS the sanitizer; analyzer only recognizes core functions by name. ) : Map_Types::default_config()['map']['center'], 'zoom' => ! empty( $_POST['initial_view_enabled'] ) ? self::clamp_float( wp_unslash( $_POST['initial_view_zoom'] ?? 5 ), 1, 20 ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clamp_float() IS the sanitizer; analyzer only recognizes core functions by name. : Map_Types::default_config()['map']['zoom'], // Real validation (allowed values, PRO-only 'custom') happens in sanitize(). 'marker' => sanitize_key( $_POST['marker'] ?? '' ), // Real validation (PRO-only + [20,100] clamp) happens in sanitize(). 'marker_size' => sanitize_key( $_POST['marker_size'] ?? '' ), ), 'rating' => array( 'label' => sanitize_text_field( wp_unslash( $_POST['rating_label'] ?? '' ) ), // Real validation (allowed values, PRO-only 'custom') happens in sanitize(). 'symbol' => sanitize_key( $_POST['rating_symbol'] ?? '' ), ), // Real validation (hex/range checks) happens in Map_Types::sanitize(). 'theme' => array( 'list_bg' => sanitize_text_field( wp_unslash( $_POST['theme']['list_bg'] ?? '' ) ), 'item_bg' => sanitize_text_field( wp_unslash( $_POST['theme']['item_bg'] ?? '' ) ), 'link_color' => sanitize_text_field( wp_unslash( $_POST['theme']['link_color'] ?? '' ) ), 'border_color' => sanitize_text_field( wp_unslash( $_POST['theme']['border_color'] ?? '' ) ), 'accent_color' => sanitize_text_field( wp_unslash( $_POST['theme']['accent_color'] ?? '' ) ), 'text_color' => sanitize_text_field( wp_unslash( $_POST['theme']['text_color'] ?? '' ) ), 'radius' => sanitize_text_field( wp_unslash( $_POST['theme']['radius'] ?? '' ) ), ), ); // Map_Types::save() resolves the real id (reuses $original_id if // existing, else generates one) — use its return value, not // $original_id, so the redirect targets the Map Type actually saved. $id = Map_Types::save( $original_id, $config ); // Back to the edit form (not the list), matching WP's own post-edit // behaviour. &created=1 (new only) triggers the "add your first Spot" banner. $redirect_args = '&updated=1' . ( $original_id ? '' : '&created=1' ); $redirect_url = admin_url( 'admin.php?page=' . Settings::MENU_SLUG . '&edit=' . $id . $redirect_args ); // Reopens whichever tab was showing when Save was clicked. $active_tab = sanitize_key( wp_unslash( $_POST['active_tab'] ?? '' ) ); if ( $active_tab ) { $redirect_url .= '#' . $active_tab; } wp_safe_redirect( $redirect_url ); exit; } public function handle_delete(): void { $slug = sanitize_key( $_GET['slug'] ?? '' ); $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( $nonce, 'mapfield_spots_delete_' . $slug ) ) { wp_die( esc_html__( 'Action not allowed.', 'mapfield-spots' ) ); } // Must run before Map_Types::delete() removes the config, while the // post type/taxonomy (registered on 'init') are still queryable — // otherwise locations/categories are left as invisible orphaned rows. $post_type = CPT::post_type_slug( $slug ); $taxonomy = CPT::taxonomy_slug( $slug ); $post_ids = get_posts( array( 'post_type' => $post_type, // 'auto-draft' included explicitly: get_posts() excludes it by default, // but an unsaved "Add New" Spot is still a real row to clean up. 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private', 'trash', 'auto-draft' ), 'posts_per_page' => -1, 'fields' => 'ids', ) ); foreach ( $post_ids as $post_id ) { // Force-delete (bypass trash): removes meta/term relationships but // leaves the featured image attachment — photos stay in the media library. wp_delete_post( $post_id, true ); } $term_ids = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'fields' => 'ids', ) ); if ( ! is_wp_error( $term_ids ) ) { foreach ( $term_ids as $term_id ) { wp_delete_term( $term_id, $taxonomy ); } } // PRO-only data (e.g. Zones) lives outside this plugin; same // "free exposes a hook, PRO reacts" convention as elsewhere. Fired // before delete() while the config still exists, for any listener that needs it. do_action( 'mapfield_spots_map_type_deleted', $slug ); Map_Types::delete( $slug ); wp_safe_redirect( admin_url( 'admin.php?page=' . Settings::MENU_SLUG ) ); exit; } /** "I don't want to reuse these fields" — silences get_orphaned_field_groups()'s notice for one group key, permanently (no undo UI; re-pointing the group's Location in ACF also naturally clears it). */ public function handle_dismiss_acf_orphan(): void { $group_key = isset( $_GET['group_key'] ) ? sanitize_key( $_GET['group_key'] ) : ''; $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( ! current_user_can( 'manage_options' ) || ! $group_key || ! wp_verify_nonce( $nonce, 'mapfield_spots_dismiss_acf_orphan_' . $group_key ) ) { wp_die( esc_html__( 'Action not allowed.', 'mapfield-spots' ) ); } $dismissed = get_option( self::DISMISSED_ACF_ORPHANS_OPTION, array() ); if ( ! in_array( $group_key, $dismissed, true ) ) { $dismissed[] = $group_key; update_option( self::DISMISSED_ACF_ORPHANS_OPTION, $dismissed, false ); } wp_safe_redirect( admin_url( 'admin.php?page=' . Settings::MENU_SLUG ) ); exit; } }