$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 '
';
}
/**
* 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' ) );
}
?>
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=""]
list —
height —
category —
title —
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();
}
});