TAPMAP_URL . 'assets/img/demo-building.svg', 'demoParking' => TAPMAP_URL . 'assets/img/demo-parking.svg', 'demoPlan' => TAPMAP_URL . 'assets/img/demo-floorplan.svg', ) ) . ';', 'before' ); wp_register_style( 'tapmap-editor', TAPMAP_URL . 'assets/css/editor.css', array( 'wp-edit-blocks' ), TAPMAP_VERSION ); wp_register_style( 'tapmap', TAPMAP_URL . 'assets/css/tapmap.css', array(), TAPMAP_VERSION ); wp_register_script( 'tapmap-view', TAPMAP_URL . 'assets/js/view.js', array(), TAPMAP_VERSION, array( 'in_footer' => true, 'strategy' => 'defer' ) ); register_block_type( self::NAME, array( 'api_version' => 3, 'title' => __( 'Tapmap', 'tapmap' ), 'category' => 'media', 'editor_script' => 'tapmap-editor', 'editor_style' => 'tapmap-editor', 'style' => 'tapmap', 'view_script' => 'tapmap-view', 'render_callback' => array( __CLASS__, 'render' ), 'supports' => array( 'align' => array( 'wide', 'full' ), 'anchor' => true, 'spacing' => array( 'margin' => true ), ), 'attributes' => array( 'imageId' => array( 'type' => 'number', 'default' => 0 ), 'imageUrl' => array( 'type' => 'string', 'default' => '' ), 'imageAlt' => array( 'type' => 'string', 'default' => '' ), 'imageWidth' => array( 'type' => 'number', 'default' => 0 ), 'imageHeight' => array( 'type' => 'number', 'default' => 0 ), 'mode' => array( 'type' => 'string', 'default' => 'hotspots' ), 'areas' => array( 'type' => 'array', 'default' => array() ), 'fieldLabels' => array( 'type' => 'array', 'default' => self::DEFAULT_FIELDS ), 'accent' => array( 'type' => 'string', 'default' => '#7b61ff' ), 'caption' => array( 'type' => 'string', 'default' => '' ), ), ) ); } /** * Clean one area coming from block attributes. * * @param array $area Raw area. * * @return array|null */ public static function clean_area( $area ) { if ( ! is_array( $area ) ) { return null; } $num = static function ( $v, $min = 0, $max = 100 ) { return max( $min, min( $max, round( (float) $v, 3 ) ) ); }; $shape = isset( $area['shape'] ) && in_array( $area['shape'], array( 'rect', 'poly' ), true ) ? $area['shape'] : 'point'; $status = isset( $area['status'] ) && in_array( $area['status'], array( 'available', 'reserved', 'sold' ), true ) ? $area['status'] : 'available'; $points = array(); if ( 'poly' === $shape ) { foreach ( (array) ( $area['points'] ?? array() ) as $p ) { if ( is_array( $p ) && count( $p ) >= 2 ) { $points[] = array( $num( $p[0] ), $num( $p[1] ) ); } } if ( count( $points ) < 3 ) { return null; } } // 0.1 stored apartment fields under fixed names; 0.2 uses f1–f4. $legacy = array( 'rooms', 'size', 'floor', 'price' ); $fields = array(); for ( $i = 1; $i <= 4; $i++ ) { $fields[ 'f' . $i ] = sanitize_text_field( $area[ 'f' . $i ] ?? ( $area[ $legacy[ $i - 1 ] ] ?? '' ) ); } $clean = array( 'id' => isset( $area['id'] ) ? sanitize_key( $area['id'] ) : wp_generate_uuid4(), 'shape' => $shape, 'x' => $num( $area['x'] ?? 0 ), 'y' => $num( $area['y'] ?? 0 ), 'w' => 'rect' === $shape ? $num( $area['w'] ?? 10 ) : 0, 'h' => 'rect' === $shape ? $num( $area['h'] ?? 10 ) : 0, 'points' => $points, 'title' => sanitize_text_field( $area['title'] ?? '' ), 'text' => sanitize_textarea_field( $area['text'] ?? '' ), 'url' => esc_url_raw( $area['url'] ?? '' ), 'linkText' => sanitize_text_field( $area['linkText'] ?? '' ), 'status' => $status, ) + $fields; if ( 'poly' === $shape ) { $xs = array_column( $points, 0 ); $ys = array_column( $points, 1 ); $clean['x'] = min( $xs ); $clean['y'] = min( $ys ); $clean['w'] = max( 0.1, max( $xs ) - min( $xs ) ); $clean['h'] = max( 0.1, max( $ys ) - min( $ys ) ); } /** * Filter one cleaned area. Add-ons use it to keep their own keys. * * @param array $clean Cleaned area. * @param array $area Raw area from the block attributes. */ return apply_filters( 'tapmap_clean_area', $clean, $area ); } /** * Field labels, always four strings. * * @param array $attrs Block attributes. * * @return string[] */ private static function labels( $attrs ) { $labels = isset( $attrs['fieldLabels'] ) && is_array( $attrs['fieldLabels'] ) ? array_values( $attrs['fieldLabels'] ) : self::DEFAULT_FIELDS; $out = array(); for ( $i = 0; $i < 4; $i++ ) { $out[] = sanitize_text_field( $labels[ $i ] ?? '' ); } return $out; } /** * Accessible name for an area. * * @param array $a Area. * @param bool $unit Unit picker mode. * @param string[] $labels Field labels. * * @return string */ private static function area_label( $a, $unit, $labels ) { $name = '' !== $a['title'] ? $a['title'] : __( 'Untitled', 'tapmap' ); if ( ! $unit ) { return $name; } $parts = array( $name ); for ( $i = 0; $i < 3; $i++ ) { $value = $a[ 'f' . ( $i + 1 ) ]; if ( '' !== $labels[ $i ] && '' !== $value ) { $parts[] = $labels[ $i ] . ' ' . $value; } } $parts[] = self::status_label( $a['status'] ); return implode( ', ', $parts ); } /** * Classes for a freeform outline. * * @param array $a Area. * @param bool $unit Unit picker mode. * @param string $base Base class. * * @return string */ private static function area_classes( $a, $unit, $base ) { /** * Filter the classes of a freeform outline. * * @param string[] $classes Classes. * @param array $a Area. * @param bool $unit Unit picker mode. */ $classes = apply_filters( 'tapmap_shape_classes', array( $base, $unit ? 'is-' . $a['status'] : '' ), $a, $unit ); return implode( ' ', array_filter( (array) $classes ) ); } /** * Status label. * * @param string $status Status key. * * @return string */ private static function status_label( $status ) { $labels = array( 'available' => __( 'Available', 'tapmap' ), 'reserved' => __( 'Reserved', 'tapmap' ), 'sold' => __( 'Sold', 'tapmap' ), ); return $labels[ $status ] ?? $labels['available']; } /** * The images of a map. The free plugin has one; add-ons can add more. * * @param array $attrs Block attributes. * * @return array[] */ public static function views( $attrs ) { $views = array( array( 'id' => 'main', 'label' => '', 'imageUrl' => $attrs['imageUrl'] ?? '', 'imageAlt' => $attrs['imageAlt'] ?? '', 'imageWidth' => (int) ( $attrs['imageWidth'] ?? 0 ), 'imageHeight' => (int) ( $attrs['imageHeight'] ?? 0 ), 'areas' => (array) ( $attrs['areas'] ?? array() ), ), ); /** * Filter the images of a map. Each view: id, label, imageUrl, imageAlt, * imageWidth, imageHeight and areas (raw). The first one is the block's own image. * * @param array[] $views Views. * @param array $attrs Block attributes. */ $views = array_values( (array) apply_filters( 'tapmap_views', $views, $attrs ) ); foreach ( $views as $k => &$view ) { if ( '' === $view['label'] ) { /* translators: %d: view number. */ $view['label'] = 0 === $k ? __( 'Overview', 'tapmap' ) : sprintf( __( 'View %d', 'tapmap' ), $k + 1 ); } $view['areas'] = array_values( array_filter( array_map( array( __CLASS__, 'clean_area' ), $view['areas'] ) ) ); } unset( $view ); return $views; } /** * Front-end markup. Every point and area is a real button; freeform * shapes are buttons clipped to the shape, with the outline drawn in an * SVG layer underneath. view.js adds the info card; add-ons can add more. * * @param array $attrs Block attributes. * * @return string */ public static function render( $attrs ) { if ( empty( $attrs['imageUrl'] ) ) { return ''; } $unit = 'units' === ( $attrs['mode'] ?? '' ); $labels = self::labels( $attrs ); $views = self::views( $attrs ); $id = 'tapmap-' . wp_unique_id(); $multi = count( $views ) > 1; // One flat list for the script; each area knows its view. $areas = array(); foreach ( $views as $k => &$v ) { foreach ( $v['areas'] as &$a ) { $a['view'] = $k; /** * Filter an area before it is rendered, e.g. to drop links to views that are gone. * * @param array $a Area. * @param array[] $views All views. * @param array $attrs Block attributes. */ $a = apply_filters( 'tapmap_render_area', $a, $views, $attrs ); $areas[] = $a; } unset( $a ); } unset( $v ); $accent = sanitize_hex_color( $attrs['accent'] ?? '' ); $style = $accent ? '--tapmap-accent:' . $accent . ';' : ''; $wrapper = get_block_wrapper_attributes( array( 'class' => 'tapmap tapmap--' . ( $unit ? 'units' : 'hotspots' ), 'style' => $style, 'id' => $id, ) ); $config = array( 'mode' => $unit ? 'units' : 'hotspots', 'labels' => $labels, 'areas' => $areas, 'i18n' => array( 'close' => __( 'Close', 'tapmap' ), 'available' => __( 'Available', 'tapmap' ), 'reserved' => __( 'Reserved', 'tapmap' ), 'sold' => __( 'Sold', 'tapmap' ), 'readMore' => __( 'Read more', 'tapmap' ), 'interest' => __( 'Register interest', 'tapmap' ), ), ); /** * Filter the settings the front-end script gets. Add-ons that add their * own script can enqueue it here, so it only loads where a map is shown. * * @param array $config Script settings. * @param array $attrs Block attributes. * @param array[] $views Views. */ $config = apply_filters( 'tapmap_config', $config, $attrs, $views ); ob_start(); ?>
data-tapmap=""> $v ) : $has_poly = false; foreach ( $v['areas'] as $a ) { $has_poly = $has_poly || 'poly' === $a['shape']; } ?>
role="tabpanel" aria-label="" >
<?php echo esc_attr( $v['imageAlt'] ); ?> width="" height="" decoding="async" loading="lazy"> self::area_label( $a, $unit, $labels ), 'classes' => array( 'tapmap__area', 'tapmap__area--' . $a['shape'], $unit ? 'is-' . $a['status'] : '' ), 'expands' => true, 'tag' => $unit, ), $a, $unit ); ?>