. * * Field type implementation for the ACF 5 / 6 field API * (`acf/include_field_types`). The ACF 4 implementation lives in fields.php. */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'acfmc_field_multiple_coordinates' ) ) : class acfmc_field_multiple_coordinates extends acf_field { /** * Field type setup. Called by acf_field::__construct() on ACF 5 and later. */ public function initialize() { // The field type name must never change: it is what ties existing // field groups and existing postmeta values to this plugin. $this->name = 'multiple-coordinates-field'; // A wrapping label used to render left aligned in ACF's field type // browser; css/field-group.css supplies the `text-align` ACF leaves // unset, so the length of this label no longer matters. $this->label = __( 'Coordinates map', 'advanced-custom-fields-multiple-coordinates' ); // 'content' is an ACF field type category key (see acf_get_field_type_categories()). $this->category = 'content'; // Default values center the map around Fliseryd/Mönsterås, Småland. $this->defaults = array( 'center' => array( 'lat' => 57.13485135696844, 'lng' => 16.292744633789084, ), 'zoom' => 11, ); $this->l10n = array(); } /** * Enqueues the map assets on screens where the field is rendered. */ public function input_admin_enqueue_scripts() { // Google Maps API key — the site owner configures it through ACF's // global setting (`acf_get_setting('google_api_key')`, the same key // ACF's own Google Map field uses), or through a filter: // // add_filter('acfmc_gmaps_key', function () { return 'AIza…'; }); // // Without a key Google Maps loads in limited mode (watermark + modal). $gmaps_url = 'https://maps.googleapis.com/maps/api/js'; $gmaps_key = function_exists( 'acf_get_setting' ) ? (string) acf_get_setting( 'google_api_key' ) : ''; $gmaps_key = (string) apply_filters( 'acfmc_gmaps_key', $gmaps_key ); if ( '' !== $gmaps_key ) { $gmaps_url = add_query_arg( 'key', rawurlencode( $gmaps_key ), $gmaps_url ); } $url = ACFMC_URL; $version = ACFMC_VERSION; // wp-color-picker is part of WordPress core; it colours the shape drawn // through the points. dashicons carries the toolbar icons, and // jquery-ui-sortable (also core) is what reorders the point list. wp_register_style( 'acf-multiple-coordinates-field', $url . 'css/fields.css', array( 'wp-color-picker', 'dashicons' ), $version ); wp_register_script( 'googlemaps-api', $gmaps_url, array(), null, false ); wp_register_script( 'acf-multiple-coordinates-map', $url . 'js/map.js', array( 'jquery', 'jquery-ui-sortable', 'wp-color-picker', 'googlemaps-api' ), $version, false ); wp_localize_script( 'acf-multiple-coordinates-map', 'acfmcL10n', acfmc_l10n_strings() ); wp_enqueue_style( 'acf-multiple-coordinates-field' ); wp_enqueue_script( 'googlemaps-api' ); wp_enqueue_script( 'acf-multiple-coordinates-map' ); } /** * Enqueues a stylesheet on the field group editor screen. * * ACF's field type browser lays every card out with flexbox and centers the * card contents, but never sets `text-align` on the label itself. A label * that fits on one line looks centered because its box is only as wide as * the text; a label that wraps gets a wider box and the lines inside it fall * back to `start`, so it renders left aligned next to all the other cards. * That is ACF's own default and it hits every field type with a long enough * label (the built-in oEmbed card does the same thing), so we simply set the * missing `text-align` — scoped to the field browser and nothing else. */ public function field_group_admin_enqueue_scripts() { wp_enqueue_style( 'acf-multiple-coordinates-field-group', ACFMC_URL . 'css/field-group.css', array(), ACFMC_VERSION ); } /** * Renders the field interface. ACF 5/6 replacement for create_field(). * * @param array $field the field array holding all the field options. */ public function render_field( $field ) { $center = isset( $field['center'] ) && is_array( $field['center'] ) ? $field['center'] : $this->defaults['center']; $lat = isset( $center['lat'] ) ? (float) $center['lat'] : (float) $this->defaults['center']['lat']; $lng = isset( $center['lng'] ) ? (float) $center['lng'] : (float) $this->defaults['center']['lng']; $zoom = isset( $field['zoom'] ) ? (int) $field['zoom'] : (int) $this->defaults['zoom']; // Generate a unique id from the field name. The field name includes // [ and ] so we replace them with characters valid in an html attribute. $uid = preg_replace( array( '/\[/', '/\]/' ), array( '_', '' ), $field['name'] ); // Normalize the stored value. If it is missing or unusable, fall back // to a single marker on the configured center so the map can render. $data = $this->decode_value( isset( $field['value'] ) ? $field['value'] : '' ); if ( null === $data ) { $data = array( 'zoom' => $zoom, 'coords' => array( array( 'lat' => $lat, 'lng' => $lng, ), ), ); } // Shape, style, colour and the marker switch live in the same value as // the points, and every one of them is optional. Missing means "no // shape", which is what every value saved before 1.4.0 looks like. acfmc_render_field_markup( array( 'uid' => $uid, 'name' => $field['name'], 'values' => wp_json_encode( $data ), 'shape' => isset( $data['shape'] ) ? $data['shape'] : 'none', 'style' => isset( $data['style'] ) ? $data['style'] : 'sharp', 'color' => isset( $data['color'] ) ? $data['color'] : ACFMC_DEFAULT_COLOR, 'markers' => ! isset( $data['markers'] ) || (bool) $data['markers'], ) ); } /** * Normalizes the value on the way into the database. * * The value is stored exactly as it always has been: a JSON string in * postmeta, so values saved by earlier versions keep working. * * @param mixed $value the value about to be saved. * @param int|string $post_id the post the value belongs to. * @param array $field the field array holding all the field options. * @return mixed */ public function update_value( $value, $post_id, $field ) { if ( null === $value || '' === $value ) { return $value; } $data = $this->decode_value( $value ); // Never throw away a value we do not understand — store it untouched. if ( null === $data ) { return $value; } return wp_json_encode( $data ); } /** * ACF 5/6 replacement for format_value_for_api(). * * The value is stored JSON encoded, so decode it into an array that is * directly usable from a template. * * @param mixed $value the value loaded from the database. * @param int|string $post_id the id of the loaded post. * @param array $field the field array holding all the field options. * @return mixed */ public function format_value( $value, $post_id, $field ) { if ( is_array( $value ) ) { return $value; } if ( ! is_string( $value ) || '' === $value ) { return $value; } $decoded = json_decode( $value, true ); return is_array( $decoded ) ? $decoded : $value; } /** * Decodes and sanitizes a stored/posted value. * * @param mixed $value raw value, JSON string or array. * @return array|null normalized array, or null when unusable. */ private function decode_value( $value ) { if ( is_string( $value ) ) { $decoded = json_decode( $value, true ); // Values coming from $_POST can still be slashed. if ( ! is_array( $decoded ) ) { $decoded = json_decode( wp_unslash( $value ), true ); } } elseif ( is_array( $value ) ) { $decoded = $value; } else { $decoded = null; } if ( ! is_array( $decoded ) || empty( $decoded['coords'] ) || ! is_array( $decoded['coords'] ) ) { return null; } $coords = array(); foreach ( $decoded['coords'] as $coord ) { if ( ! is_array( $coord ) || ! isset( $coord['lat'] ) || ! isset( $coord['lng'] ) ) { continue; } $point = array( 'lat' => (float) $coord['lat'], 'lng' => (float) $coord['lng'], ); // `label` is optional and only present on points that came from the // search field. Points without one are stored exactly as before, so // values written by earlier versions are unchanged by a save. if ( isset( $coord['label'] ) && is_scalar( $coord['label'] ) ) { $label = sanitize_text_field( (string) $coord['label'] ); if ( '' !== $label ) { $point['label'] = function_exists( 'mb_substr' ) ? mb_substr( $label, 0, 200 ) : substr( $label, 0, 200 ); } } $coords[] = $point; } if ( empty( $coords ) ) { return null; } $data = array( 'coords' => $coords, 'zoom' => isset( $decoded['zoom'] ) ? (int) $decoded['zoom'] : (int) $this->defaults['zoom'], ); // What is drawn through the points, how it is drawn, in what colour, // and whether the markers are drawn with it. None of these keys is // written unless a shape is actually chosen, so a value with no shape // is stored exactly the way earlier versions stored it. $shape = isset( $decoded['shape'] ) && is_string( $decoded['shape'] ) ? sanitize_key( $decoded['shape'] ) : ''; if ( in_array( $shape, acfmc_shapes(), true ) ) { $data['shape'] = $shape; $style = isset( $decoded['style'] ) && is_string( $decoded['style'] ) ? sanitize_key( $decoded['style'] ) : ''; $data['style'] = in_array( $style, acfmc_styles(), true ) ? $style : 'sharp'; $color = isset( $decoded['color'] ) && is_string( $decoded['color'] ) ? trim( $decoded['color'] ) : ''; if ( preg_match( '/^#[0-9a-fA-F]{6}$/', $color ) ) { $data['color'] = strtolower( $color ); } // Only "off" is stored. Markers are drawn unless the value says // otherwise, so every value without the key behaves as before. if ( isset( $decoded['markers'] ) && ! filter_var( $decoded['markers'], FILTER_VALIDATE_BOOLEAN ) ) { $data['markers'] = false; } } return $data; } } acf_register_field_type( 'acfmc_field_multiple_coordinates' ); endif;