render_map( $slug, array( 'height' => '100vh', 'focus_spot' => $focus_spot ) ); ?> > > post_status ) { return; // unknown/unpublished id — let WP continue to its own normal 404. } $config = Map_Types::get_by_post_type( $post->post_type ); if ( ! $config ) { return; // not one of this plugin's own Spots. } $this->enqueue_card_assets(); $categories = $this->spot_categories( $post, $config ); // Same reasoning as maybe_render_preview()'s own $map_html above. $card_html = $this->render_spot_card( $post, $config, $categories, true ); ?> > <?php echo esc_html( get_the_title( $post ) ); ?> >
to Mapbox on pages that render a map — doesn't * remove the round trip to fetch the style JSON (loadMapboxStyle() in * frontend.js), but starts the DNS/TLS handshake earlier, in parallel with page load. */ public function add_resource_hints( array $hints, string $relation_type ): array { if ( 'preconnect' !== $relation_type || ! $this->current_page_has_map() ) { return $hints; } $hints[] = 'https://api.mapbox.com'; $hints[] = 'https://events.mapbox.com'; // Mapbox's own telemetry, called by every style/tile request return $hints; } /** * Best-effort: only catches a map in the current singular post/page's own * content — wp_resource_hints fires before content renders, so that's the * only case knowable early enough. A theme-embedded map just misses the * hint (never breaks). Irrelevant to the auto-appended Spot card, which * never loads MapLibre. */ private function current_page_has_map(): bool { if ( ! is_singular() ) { return false; } $post = get_post(); if ( ! $post instanceof \WP_Post ) { return false; } return has_shortcode( $post->post_content, 'mapfield_spots_map' ) || has_block( 'mapfield-spots/map', $post ); } // ── Shortcode ──────────────────────────────────────────────────────── public function shortcode_map( $atts ): string { $atts = shortcode_atts( array( 'type' => '', 'list' => 'true', 'height' => '', 'category' => '', // Other Map Types (id/slug, comma-separated) whose Spots also show as // read-only pins (PRO) — see render_map()'s docblock. 'overlay' => '', 'title' => 'false', ), $atts, 'mapfield_spots_map' ); return $this->render_map( sanitize_key( $atts['type'] ), array( 'list' => $this->shortcode_bool( $atts['list'] ), 'height' => sanitize_text_field( $atts['height'] ), 'category' => sanitize_text_field( $atts['category'] ), 'overlay' => sanitize_text_field( $atts['overlay'] ), 'title' => $this->shortcode_bool( $atts['title'] ), ) ); } /** Shortcode attributes are always strings — "false"/"0"/"no" read as false, anything else as true. */ private function shortcode_bool( string $value ): bool { return ! in_array( strtolower( trim( $value ) ), array( 'false', '0', 'no' ), true ); } /** * Replaces $content with the Spot's info card on its own single page. * Always on: the theme still renders the title, but the card takes over * the featured image + description for its own two-column layout * (add_single_spot_body_class() hides the theme's featured-image block). * in_the_loop()+is_main_query() restrict this to the real page render. * doing_filter('get_the_excerpt') additionally excludes wp_trim_excerpt() * (WP 4.5+ reruns 'the_content' before trimming for shortcodes/embeds) — * without it the whole flattened card leaked into excerpts (/embed/, RSS, * SEO meta descriptions). */ public function maybe_append_spot_card( string $content ): string { if ( ! is_singular() || ! in_the_loop() || ! is_main_query() || doing_filter( 'get_the_excerpt' ) ) { return $content; } $post = get_post(); if ( ! $post instanceof \WP_Post ) { return $content; } $config = Map_Types::get_by_post_type( $post->post_type ); if ( ! $config ) { return $content; } $this->enqueue_card_assets(); $categories = $this->spot_categories( $post, $config ); // $show_map_link=true: only this, the Spot's real own page — not the // small popup-preview render below, which reuses this same $native // layout but is already looking at the map right behind it. return $this->render_spot_card( $post, $config, $categories, true, true ); } /** * Marks a Spot's single page so frontend.css can hide the theme's own * featured-image block — render_spot_card() already renders that photo * itself. Scoped to Map Types with the photo field enabled. */ public function add_single_spot_body_class( array $classes ): array { if ( ! is_singular() ) { return $classes; } $config = Map_Types::get_by_post_type( get_post_type() ); if ( $config && ! empty( $config['fields']['photo'] ) ) { $classes[] = 'mapfield_spots-single'; } return $classes; } /** * True on a term archive for one of this plugin's category taxonomies, or * a search results page including a Spot (WP includes all public post * types in search; a block theme's search template reuses the taxonomy * archive's own Query Loop markup, confirmed on Twenty Twenty-Five, so * the same CSS fixes both). Shared by add_archive_body_class() and * maybe_enqueue_archive_assets(). */ private function is_own_archive(): bool { if ( is_tax() ) { $term = get_queried_object(); if ( ! $term instanceof \WP_Term ) { return false; } foreach ( Map_Types::get_all() as $slug => $config ) { if ( CPT::taxonomy_slug( $slug ) === $term->taxonomy ) { return true; } } return false; } if ( is_search() ) { global $wp_query; foreach ( $wp_query->posts as $post ) { if ( $post instanceof \WP_Post && Map_Types::get_by_post_type( $post->post_type ) ) { return true; } } } return false; } /** Marks a category archive or Spot-including search page so frontend.css can reflow the theme's Query Loop into two columns. */ public function add_archive_body_class( array $classes ): array { if ( $this->is_own_archive() ) { $classes[] = 'mapfield_spots-archive'; } return $classes; } /** Just the stylesheet — archives/search never need enqueue_card_assets()'s gallery/description JS. */ public function maybe_enqueue_archive_assets(): void { if ( $this->is_own_archive() ) { wp_enqueue_style( 'mapfield-spots-frontend', MAPFIELD_SPOTS_URL . 'assets/frontend.css', array( 'mapfield-spots-palette' ), MAPFIELD_SPOTS_VERSION ); } } // ── Gutenberg block ────────────────────────────────────────────────── public function register_block(): void { wp_register_script( 'mapfield-spots-block', MAPFIELD_SPOTS_URL . 'assets/block.js', array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-hooks' ), MAPFIELD_SPOTS_VERSION, true ); // block.js's wp.i18n.__() calls need this — unlike PHP's __()/_e(), JS reads // a separate compiled JSON catalog per script handle, auto-discovered off // this plugin's own Domain Path header. wp_set_script_translations( 'mapfield-spots-block', 'mapfield-spots' ); $choices = array(); foreach ( Map_Types::get_all() as $slug => $config ) { $choices[] = array( 'slug' => $slug, 'label' => $config['label_plural'] ?: $slug, // Only when categories are enabled — also determines whether the // block's Category control renders at all (see block.js). 'categories' => ! empty( $config['fields']['category'] ) ? $config['categories'] : array(), ); } wp_localize_script( 'mapfield-spots-block', 'MapfieldSpotsBlockData', array( 'mapTypes' => $choices, ) ); register_block_type( 'mapfield-spots/map', array( 'editor_script' => 'mapfield-spots-block', 'render_callback' => function ( $attributes ) { return $this->render_map( sanitize_key( $attributes['mapType'] ?? '' ), array( 'list' => ! empty( $attributes['showList'] ), 'height' => sanitize_text_field( $attributes['height'] ?? '' ), 'category' => sanitize_text_field( $attributes['category'] ?? '' ), 'overlay' => sanitize_text_field( $attributes['overlay'] ?? '' ), 'title' => ! empty( $attributes['title'] ), 'align' => sanitize_key( $attributes['align'] ?? '' ), ) ); }, 'attributes' => array( 'mapType' => array( 'type' => 'string', 'default' => '' ), 'showList' => array( 'type' => 'boolean', 'default' => true ), 'height' => array( 'type' => 'string', 'default' => '' ), 'category' => array( 'type' => 'string', 'default' => '' ), 'overlay' => array( 'type' => 'string', 'default' => '' ), 'title' => array( 'type' => 'boolean', 'default' => false ), ), // Matched by block.js's `supports: { align: [...] }` (shows the toolbar // control) and makes WordPress recognize $attributes['align'] for a // plain register_block_type() array registration (no block.json). // Does NOT auto-apply an align class to render_callback's output — // that's a block.json+render.php mechanism; render_map() applies the // class by hand instead (see $align_class there). 'supports' => array( 'align' => array( 'wide', 'full' ), ), ) ); } // ── Shared rendering ───────────────────────────────────────────────── /** * @param array{list?: bool, height?: string, category?: string, overlay?: string, title?: bool, align?: string} $args * list: show the locations list panel (default true). * height: CSS length overriding the wrap's default 640px height. * category: comma-separated category slugs to filter Spots by. * overlay: comma-separated other Map Types (PRO, by id/public_slug) * whose Spots also show as read-only pins (mapfield_spots_overlay_layers, * see Mapfield_Spots_Pro\Overlay) — a per-embed choice, not a stored * Map Type setting. * title: show this map's own name as a heading above it (default false). * align: 'wide'|'full'|'' — the block's alignment toolbar choice; no shortcode equivalent. */ public function render_map( string $map_type_slug, array $args = array() ): string { $config = Map_Types::get( $map_type_slug ); if ( ! $config ) { if ( current_user_can( 'manage_options' ) ) { return '

' . esc_html__( 'Mapfield Spots: unknown map type.', 'mapfield-spots' ) . '

'; } return ''; } // $map_type_slug can be id or public_slug; $id below is always the real // internal id (post_type/taxonomy/Zones keys use that). $id = $config['slug']; if ( $this->is_page_builder_edit_mode() ) { return $this->render_editor_placeholder( $config ); } $args = wp_parse_args( $args, array( 'list' => true, 'height' => '', 'category' => '', 'overlay' => '', 'title' => false, 'align' => '', 'focus_spot' => 0 ) ); $height = $this->sanitize_css_length( $args['height'] ); $categories_filter = array_filter( array_map( 'sanitize_key', explode( ',', $args['category'] ) ) ); $show_map_title = ! empty( $args['title'] ); // The block's "Wide/Full width" toolbar control (shortcode has no // equivalent). Sanitized against the fixed set so a crafted $args can't // emit an arbitrary class. $align_class = in_array( $args['align'], array( 'wide', 'full' ), true ) ? ' align' . $args['align'] : ''; // Per-embed choice, not a stored Map_Types setting. Resolution (token // parsing) and layer-building are both PRO (mapfield_spots_overlay_layers, // hooked by Mapfield_Spots_Pro\Overlay) — no filter result is an empty array. $overlay_layers = $args['overlay'] ? apply_filters( 'mapfield_spots_overlay_layers', array(), $args['overlay'], $id ) : array(); $this->enqueue_assets( $config ); // Extensibility hook PRO fills (Mapfield_Spots_Pro\Routing::maybe_enqueue()), // fired before /footer print so PRO can enqueue its own assets in time. do_action( 'mapfield_spots_render_map', $id, $config ); // Unconditional: needed for overlay Spots, list-off, and the mobile compact // card layout's own expand button — every path can need it now. wp_enqueue_script( 'mapfield-spots-spot-preview-modal', MAPFIELD_SPOTS_URL . 'assets/spot-preview-modal.js', array( 'pimp-scrollbar' ), MAPFIELD_SPOTS_VERSION, true ); $query_args = array( 'post_type' => CPT::post_type_slug( $id ), 'posts_per_page' => -1, 'post_status' => 'publish', // Shown as soon as real coordinates exist — position is a normal post // field, no separate "confirmed" state (see Location_Field's docblock). // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- there's no alternative for "has real coordinates" without a dedicated indexed table; a Map Type's location count is small (typically tens to low hundreds), so the plain meta_query is an acceptable tradeoff here. 'meta_query' => array( 'relation' => 'AND', array( 'key' => '_mapfield_spots_lat', 'value' => '', 'compare' => '!=' ), array( 'key' => '_mapfield_spots_lng', 'value' => '', 'compare' => '!=' ), ), ); if ( $categories_filter && ! empty( $config['fields']['category'] ) ) { $query_args['tax_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- one embed's worth of Spots, same small scale as the meta_query above array( 'taxonomy' => CPT::taxonomy_slug( $id ), 'field' => 'slug', 'terms' => $categories_filter, // 'terms' as an array already means "any of these" (OR), no separate 'operator' needed for that ), ); } $posts = get_posts( $query_args ); $features = array(); $list_html = ''; foreach ( $posts as $post ) { $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; } $categories = $this->spot_categories( $post, $config ); if ( $categories_filter ) { // Move filter-matched categories to the front so the pin colors by the // filtered category, not some other lower-ID one the Spot also has // (e.g. filtering "Bistrot" but the pin still shows Gastronomique-red). // array_filter() not usort(): guaranteed order-preserving on PHP 7.4 // (usort() only got that in PHP 8). $matched = array_filter( $categories, function ( $cat ) use ( $categories_filter ) { return in_array( $cat['slug'], $categories_filter, true ); } ); $categories = array_merge( $matched, array_diff_key( $categories, $matched ) ); } $color = $categories[0]['color'] ?? '#E03137'; // == categoryColors[0] when there are any — kept as its own key since it's also the no-category fallback. Matches --mapfield_spots-alert. $features[] = array( 'id' => $post->ID, 'lat' => $lat, 'lng' => $lng, // html_entity_decode(): get_the_title() runs through wptexturize() // (apostrophe → ’), fine for the list (HTML) but this title is // set via JS textContent for the marker label, which doesn't parse entities. 'title' => html_entity_decode( get_the_title( $post ), ENT_QUOTES, 'UTF-8' ), // shown above the pin once it's the active/selected marker 'color' => $color, 'categorySlugs' => wp_list_pluck( $categories, 'slug' ), 'categoryColors' => wp_list_pluck( $categories, 'color' ), // Only used by frontend.js when the list panel is off; built for every // Spot regardless (negligible payload vs. extra branching). 'previewUrl' => home_url( '/?mapfield_spots_spot_preview=' . $post->ID ), ); if ( $args['list'] ) { $list_html .= $this->render_spot_card( $post, $config, $categories, false ); } } $container_id = 'mapfield_spots-map-' . $id . '-' . wp_unique_id(); $mapbox_settings = Settings::get_mapbox_settings(); $marker = Marker_Library::resolve( $config['map'] ); wp_add_inline_script( 'mapfield-spots-frontend', sprintf( 'window.MapfieldSpotsMaps = window.MapfieldSpotsMaps || []; window.MapfieldSpotsMaps.push(%s);', wp_json_encode( array( 'containerId' => $container_id, // Read by startMap()'s style-load failure handler to pick the right // i18n string — never trust a client-side check for this. 'isAdmin' => current_user_can( 'manage_options' ), 'token' => $mapbox_settings['token'], 'style' => $config['map']['style_url'] ?: $mapbox_settings['style_url'], 'center' => array( $config['map']['center'][1], $config['map']['center'][0] ), 'zoom' => $config['map']['zoom'], // When true, frontend.js skips auto-fitting bounds to the Spots/Zones // shown and keeps this center/zoom as the real starting view instead. 'lockInitialView' => ! empty( $config['map']['initial_view_enabled'] ), // Per-map default now (was a site-wide Settings toggle) — the Map // Type's own "3D buildings" toolbar checkbox. 'showBuildings' => ! empty( $config['toolbar']['buildings3d'] ), // Default true — matches the plugin's existing behaviour before this toggle existed. 'zonePopup' => ! empty( $config['fields']['zone_popup'] ), 'markerSvg' => $marker['markup'], // A custom photo/logo has no corner for dots like a drawn pin — // buildMarkerElement() skips them for custom markers. 'markerIsCustom' => $marker['is_custom'], // Read by frontend.js: with the list off, a marker click also opens // the small preview popup (+ "View details"), otherwise the only way // to reach a Spot's details. 'showList' => (bool) $args['list'], // A Spot's own single page links here ("View on map") to fly // straight to it instead of landing on the full overview. 'focusSpotId' => (int) $args['focus_spot'], // Read by PRO's routing.js to offer Circuit/travel-times controls per // map instance (a page can mix maps with Directions on/off). 'directionsEnabled' => ! empty( $config['fields']['directions'] ), // Front-end map tools this Map Type's admin enabled (see "Map // toolbar" tab). toolbarCircuit + directionsEnabled must both be // true for PRO's routing.js. 'toolbarFullscreen' => ! empty( $config['toolbar']['fullscreen'] ), 'toolbarBuildings3d' => ! empty( $config['toolbar']['buildings3d'] ), 'toolbarSatellite' => ! empty( $config['toolbar']['satellite'] ), 'toolbarTerrain' => ! empty( $config['toolbar']['terrain'] ), 'toolbarCircuit' => ! empty( $config['toolbar']['circuit'] ), 'features' => $features, 'overlayLayers' => $overlay_layers, // Same extensibility-hook pattern as above (Mapfield_Spots_Pro\Zones::get_zones_geojson()). 'zones' => apply_filters( 'mapfield_spots_map_zones', array(), $id ), ) ) ) ); $show_list = $args['list'] && $features; // A category filter already excludes other categories from the query, so a // legend that "filters" among them would be broken — skip it. $show_legend = ! $categories_filter && ! empty( $config['fields']['category'] ) && ! empty( $config['categories'] ) && ! empty( $config['toolbar']['category_legend'] ); // Map_Types' own theme config, applied as inline custom properties (see // Map_Types docblock); only emitted when customized so untouched Map // Types keep identical markup. $theme_defaults = Map_Types::default_config()['theme']; $wrap_style = array(); if ( $height ) { $wrap_style[] = '--mapfield_spots-map-height:' . $height; } if ( $config['theme']['list_bg'] !== $theme_defaults['list_bg'] ) { $wrap_style[] = '--mapfield_spots-list-bg:' . $config['theme']['list_bg']; } if ( $config['theme']['item_bg'] !== $theme_defaults['item_bg'] ) { $wrap_style[] = '--mapfield_spots-list-item-bg:' . $config['theme']['item_bg']; } if ( $config['theme']['link_color'] !== $theme_defaults['link_color'] ) { $wrap_style[] = '--mapfield_spots-link:' . $config['theme']['link_color']; } if ( $config['theme']['border_color'] !== $theme_defaults['border_color'] ) { $wrap_style[] = '--mapfield_spots-medium:' . $config['theme']['border_color']; } if ( $config['theme']['accent_color'] !== $theme_defaults['accent_color'] ) { $wrap_style[] = '--mapfield_spots-alert:' . $config['theme']['accent_color']; } if ( $config['theme']['text_color'] !== $theme_defaults['text_color'] ) { $wrap_style[] = '--mapfield_spots-text:' . $config['theme']['text_color']; } if ( (int) $config['theme']['radius'] !== (int) $theme_defaults['radius'] ) { $wrap_style[] = '--mapfield_spots-radius:' . (int) $config['theme']['radius'] . 'px'; } // sanitize() already restricts marker_size to these 3 presets + 'custom' // (PRO); only emitted when it differs from the default, same as above. $marker_sizes = array( 'small' => 25, 'medium' => 35, 'large' => 45, ); if ( 'custom' === $config['map']['marker_size'] ) { // The declaration itself (not just whether it's allowed) comes from // PRO, re-checked so a config saved under PRO still falls back // cleanly if PRO is later deactivated — same pattern as the custom // marker/rating icon resolution above. $custom_css = apply_filters( 'mapfield_spots_custom_marker_size_css', '', (int) ( $config['map']['marker_size_custom_px'] ?? 0 ) ); if ( $custom_css ) { $wrap_style[] = $custom_css; } } elseif ( 'medium' !== $config['map']['marker_size'] ) { $wrap_style[] = '--mapfield_spots-marker-size:' . $marker_sizes[ $config['map']['marker_size'] ] . 'px'; } ob_start(); ?> isn't always there. ?>

>

*/ private function spot_categories( \WP_Post $post, array $config ): array { $categories = array(); if ( empty( $config['fields']['category'] ) ) { return $categories; } $terms = get_the_terms( $post->ID, CPT::taxonomy_slug( $config['slug'] ) ); if ( empty( $terms ) || is_wp_error( $terms ) ) { return $categories; } // Deterministic order (term_id): a Spot can have several categories, but a // marker only shows one colour, so the lowest term_id wins. usort( $terms, function ( $a, $b ) { return $a->term_id <=> $b->term_id; } ); foreach ( $terms as $term ) { $categories[] = array( 'slug' => $term->slug, // matches a Map Type category's own 'slug' — see CPT::register_taxonomy(), which creates each term with that exact slug 'label' => $term->name, 'color' => get_term_meta( $term->term_id, 'mapfield_spots_color', true ) ?: '#E03137', ); } return $categories; } /** * Renders one Spot's info card, every field its Map Type has enabled. * Shared by both rendering paths so a new field only needs markup in one place. * * $native=false: list item in render_map()'s panel — clickable, synced to * its marker via data-post-id, truncated description with "Read more". * $native=true: auto-appended to the Spot's single page — theme still * renders the title; photo/description are rendered by the card itself * (theme's own featured-image block hidden via add_single_spot_body_class()) * into two columns (.__media / .__info, see frontend.css). */ private function render_spot_card( \WP_Post $post, array $config, array $categories, bool $native = false, bool $show_map_link = false ): string { $show_title = ! $native; $thumb = ! empty( $config['fields']['photo'] ) && has_post_thumbnail( $post ) ? get_the_post_thumbnail_url( $post, $native ? 'large' : 'medium' ) : ''; // Full-res for the lightbox's data-full — deliberately not $thumb itself, // which is the smaller 'medium' size for the inline list display. $thumb_full = $thumb ? get_the_post_thumbnail_url( $post, 'large' ) : ''; $website = ! empty( $config['fields']['website'] ) ? get_post_meta( $post->ID, '_mapfield_spots_website', true ) : ''; $phone = ! empty( $config['fields']['phone'] ) ? get_post_meta( $post->ID, '_mapfield_spots_phone', true ) : ''; $cta_label = ! empty( $config['fields']['cta'] ) ? get_post_meta( $post->ID, '_mapfield_spots_cta_label', true ) : ''; $cta_url = ! empty( $config['fields']['cta'] ) ? get_post_meta( $post->ID, '_mapfield_spots_cta_url', true ) : ''; $gallery_ids = ! empty( $config['fields']['gallery'] ) ? Gallery_Field::sanitize_ids( get_post_meta( $post->ID, '_mapfield_spots_gallery', true ) ) : array(); $hours_groups = array(); if ( ! empty( $config['fields']['hours'] ) ) { $hours = Hours_Field::sanitize_hours( get_post_meta( $post->ID, '_mapfield_spots_hours', true ) ); if ( Hours_Field::has_any_data( $hours ) ) { $hours_groups = Hours_Field::grouped_for_display( $hours ); } } $social_links = ! empty( $config['fields']['social'] ) ? Social_Field::sanitize_links( get_post_meta( $post->ID, '_mapfield_spots_social', true ) ) : array(); $rating_level = ! empty( $config['fields']['rating'] ) ? Rating_Field::sanitize_level( get_post_meta( $post->ID, '_mapfield_spots_rating', true ) ) : 0; $rating_symbol_key = $config['rating']['symbol'] ?? 'star'; $rating_label = $config['rating']['label'] ?: __( 'Rating', 'mapfield-spots' ); $lat = (float) get_post_meta( $post->ID, '_mapfield_spots_lat', true ); $lng = (float) get_post_meta( $post->ID, '_mapfield_spots_lng', true ); // Generated from the same lat/lng as the marker — nothing extra for the editor to fill in. $directions_url = ! empty( $config['fields']['directions'] ) && $lat && $lng ? sprintf( 'https://www.google.com/maps/dir/?api=1&destination=%s', rawurlencode( $lat . ',' . $lng ) ) : ''; $gallery_items = array(); foreach ( $gallery_ids as $gallery_id ) { $gallery_thumb = wp_get_attachment_image_url( $gallery_id, Gallery_Field::THUMB_SIZE ); $gallery_full = wp_get_attachment_image_url( $gallery_id, 'large' ); if ( $gallery_thumb && $gallery_full ) { $gallery_items[] = array( 'thumb' => $gallery_thumb, 'full' => $gallery_full ); } } // Plain-text truncation avoids cutting mid-tag; full HTML only shown via // "Read more" ($native skips the description entirely). $desc_limit = 150; $desc_plain = wp_strip_all_tags( get_the_content( null, false, $post ) ); $desc_html = wp_kses_post( wpautop( get_the_content( null, false, $post ) ) ); $needs_more = ! $native && mb_strlen( $desc_plain ) > $desc_limit; $wrapper_class = $native ? 'mapfield_spots-spot-card' : 'mapfield_spots-map-list__item'; // Built once, echoed wherever the layout needs it — inline for the list // card, pulled into its own media column for $native. $gallery_html = ''; if ( $gallery_items ) { ob_start(); ?>
> for no-JS/new-tab; spot-map-modal.js intercepts the click to open it in a popup instead of navigating away. ?>
enqueue_card_assets(); $this->enqueue_map_only_assets(); } /** Card CSS/JS — needed everywhere a card can appear, with or without a map. */ private function enqueue_card_assets(): void { // Not loaded on the front end by default — needed here for the Website/Phone icons. wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'mapfield-spots-frontend', MAPFIELD_SPOTS_URL . 'assets/frontend.css', array( 'mapfield-spots-palette' ), MAPFIELD_SPOTS_VERSION ); // Shared lightbox for every card's gallery thumbnails — standalone since // it's unrelated to the map itself. wp_enqueue_script( 'mapfield-spots-gallery-lightbox', MAPFIELD_SPOTS_URL . 'assets/gallery-lightbox.js', array(), MAPFIELD_SPOTS_VERSION, true ); wp_enqueue_script( 'mapfield-spots-description-toggle', MAPFIELD_SPOTS_URL . 'assets/description-toggle.js', array(), MAPFIELD_SPOTS_VERSION, true ); // "View on map" popup (only rendered with $show_map_link, but cheap enough to always enqueue here). wp_enqueue_script( 'mapfield-spots-spot-map-modal', MAPFIELD_SPOTS_URL . 'assets/spot-map-modal.js', array(), MAPFIELD_SPOTS_VERSION, true ); } /** * MapLibre + the map canvas/list JS — only needed for render_map(), not * for the auto-appended Spot page card, which never draws a map at * all. Kept separate so that card doesn't pull in MapLibre's own * JS/CSS for nothing. */ private function enqueue_map_only_assets(): void { wp_enqueue_style( 'maplibre-gl', MAPFIELD_SPOTS_URL . 'assets/vendor/maplibre-gl/maplibre-gl.css', array(), '4.7.1' ); wp_enqueue_script( 'maplibre-gl', MAPFIELD_SPOTS_URL . 'assets/vendor/maplibre-gl/maplibre-gl.js', array(), '4.7.1', true ); // Reusable across the plugin (any screen, admin or front-end) — // just add the pimp-scrollbar class to an element and enqueue // these two files, no other wiring needed. wp_enqueue_style( 'pimp-scrollbar', MAPFIELD_SPOTS_URL . 'assets/pimp-scrollbar.css', array(), MAPFIELD_SPOTS_VERSION ); wp_enqueue_script( 'pimp-scrollbar', MAPFIELD_SPOTS_URL . 'assets/pimp-scrollbar.js', array(), MAPFIELD_SPOTS_VERSION, true ); wp_enqueue_script( 'mapfield-spots-frontend', MAPFIELD_SPOTS_URL . 'assets/frontend.js', array( 'maplibre-gl', 'pimp-scrollbar' ), MAPFIELD_SPOTS_VERSION, true ); wp_localize_script( 'mapfield-spots-frontend', 'MapfieldSpotsFrontendI18n', array( 'rotateHint' => __( 'Hold and drag to rotate in 3D', 'mapfield-spots' ), // Same strings as the sidebar list card's own Read more/less // toggle (see render_map() below) — a zone popup's description // (PRO) reuses them for its own truncation, built in JS since // that popup's HTML doesn't exist until a zone is clicked. 'readMore' => __( 'Read more', 'mapfield-spots' ), 'readLess' => __( 'Read less', 'mapfield-spots' ), // Overlay Spot popup's own "open full page" button + its modal // (PRO — see Mapfield_Spots_Pro\Overlay, spot-preview-modal.js). 'viewFullPage' => __( 'View details', 'mapfield-spots' ), 'close' => __( 'Close', 'mapfield-spots' ), // startMap()'s own style-load failure (e.g. an invalid/expired // Mapbox token) — %s is the raw fetch()/MapLibre error text, // left untranslated on purpose (it's the same technical detail // an admin would need to search for, e.g. an HTTP status code). // Everyone else gets the plain, translated one instead — never // this raw detail, same admin-vs-everyone split already used // for the "unknown map type"/"map inactive" messages above. /* translators: %s: the raw fetch()/MapLibre error message (e.g. an HTTP status code or network error) */ 'mapLoadErrorAdmin' => __( 'Mapfield Spots: could not load the map style (%s).', 'mapfield-spots' ), 'mapLoadErrorVisitor' => __( 'This map is temporarily unavailable.', 'mapfield-spots' ), ) ); } }