<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Rusnetim_CPT_Marker {
public function __construct() {
add_action( 'init', [ $this, 'register_post_type' ] );
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
add_action( 'save_post_rusnetim_marker', [ $this, 'save_meta' ] );
add_filter( 'manage_rusnetim_marker_posts_columns', [ $this, 'custom_columns' ] );
add_action( 'manage_rusnetim_marker_posts_custom_column', [ $this, 'custom_column_content' ], 10, 2 );
add_action( 'restrict_manage_posts', [ $this, 'admin_posts_filter' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'wp_ajax_rusnetim_toggle_visibility', [ $this, 'ajax_toggle_visibility' ] );
}
public function register_post_type() {
$labels = array(
'name' => esc_html__( 'Markers', 'rusnet-interactive-map' ),
'singular_name' => esc_html__( 'Marker', 'rusnet-interactive-map' ),
'menu_name' => esc_html__( 'Markers', 'rusnet-interactive-map' ),
'add_new' => esc_html__( 'Add New', 'rusnet-interactive-map' ),
'add_new_item' => esc_html__( 'Add New Marker', 'rusnet-interactive-map' ),
'edit_item' => esc_html__( 'Edit Marker', 'rusnet-interactive-map' ),
'new_item' => esc_html__( 'New Marker', 'rusnet-interactive-map' ),
'view_item' => esc_html__( 'View Marker', 'rusnet-interactive-map' ),
'search_items' => esc_html__( 'Search Markers', 'rusnet-interactive-map' ),
'not_found' => esc_html__( 'No markers found', 'rusnet-interactive-map' ),
'not_found_in_trash' => esc_html__( 'No markers found in Trash', 'rusnet-interactive-map' ),
);
$args = array(
'labels' => $labels,
'public' => false,
'show_ui' => true,
'show_in_nav_menus' => false,
'supports' => array( 'title', 'editor' ),
'has_archive' => false,
'rewrite' => false,
'query_var' => false,
'capability_type' => 'post',
'map_meta_cap' => true,
'taxonomies' => array( 'rusnetim_category' ),
);
register_post_type( 'rusnetim_marker', $args );
}
public function add_meta_boxes() {
add_meta_box(
'marker-coords',
esc_html__( 'Coordinates', 'rusnet-interactive-map' ),
[ $this, 'meta_box_coords' ],
'rusnetim_marker',
'normal',
'high'
);
add_meta_box(
'marker-icon',
esc_html__( 'Icon and Color', 'rusnet-interactive-map' ),
[ $this, 'meta_box_icon' ],
'rusnetim_marker',
'normal',
'high'
);
add_meta_box(
'marker-url',
esc_html__( 'Link', 'rusnet-interactive-map' ),
[ $this, 'meta_box_url' ],
'rusnetim_marker',
'normal',
'default'
);
add_meta_box(
'marker-image',
esc_html__( 'Image Gallery', 'rusnet-interactive-map' ),
[ $this, 'meta_box_image' ],
'rusnetim_marker',
'normal',
'default'
);
}
public function meta_box_coords( $post ) {
wp_nonce_field( 'rusnetim_save_marker', 'rusnetim_marker_nonce' );
$coord = get_post_meta( $post->ID, '_marker_coord', true );
$coord = $coord ?: '53.334554,83.786980';
$visible = get_post_meta( $post->ID, '_marker_visible', true );
if ( '' === $visible ) {
$visible = '1';
}
?>
<p>
<label for="marker_coord"><?php esc_html_e( 'Latitude, longitude:', 'rusnet-interactive-map' ); ?></label>
<input type="text" id="marker_coord" name="marker_coord" value="<?php echo esc_attr( $coord ); ?>" class="widefat" />
<span class="description"><?php esc_html_e( 'Example: 53.334554,83.786980', 'rusnet-interactive-map' ); ?></span>
</p>
<p>
<label for="marker_visible">
<input type="checkbox" id="marker_visible" name="marker_visible" value="1" <?php checked( $visible, '1' ); ?> />
<?php esc_html_e( 'Show marker on maps', 'rusnet-interactive-map' ); ?>
</label>
</p>
<?php
// ADDED HOOK: after coordinates and visibility fields
do_action( 'rusnetim_marker_coords_meta_box', $post );
}
public function meta_box_icon( $post ) {
$icon = get_post_meta( $post->ID, '_marker_icon', true );
$color = get_post_meta( $post->ID, '_marker_color', true );
$custom_icon = get_post_meta( $post->ID, '_marker_custom_icon', true );
$icon_width = get_post_meta( $post->ID, '_marker_icon_width', true );
$icon_height = get_post_meta( $post->ID, '_marker_icon_height', true );
$defaults = Rusnetim_Options::get_all();
$my_iconset = $defaults['my_iconset'];
?>
<p>
<label for="marker_icon"><?php esc_html_e( 'Icon type (preset or myset:slug):', 'rusnet-interactive-map' ); ?></label>
<input type="text" id="marker_icon" name="marker_icon" value="<?php echo esc_attr( $icon ); ?>" class="widefat" placeholder="islands#dotIcon or myset:office" />
<span class="description">
<?php esc_html_e( 'You can use standard presets (islands#dotIcon) or icons from your own set in the format myset:slug (e.g., myset:office).', 'rusnet-interactive-map' ); ?>
<a href="https://tech.yandex.com/maps/doc/jsapi/2.1/ref/reference/option.presetStorage-docpage/" target="_blank"><?php esc_html_e( 'Documentation', 'rusnet-interactive-map' ); ?></a>
</span>
</p>
<p>
<label for="marker_color"><?php esc_html_e( 'Color:', 'rusnet-interactive-map' ); ?></label>
<input type="text" id="marker_color" name="marker_color" value="<?php echo esc_attr( $color ); ?>" class="widefat rusnetim-color-field" style="width: 100px;" data-default-color="" />
<span class="description"><?php esc_html_e( 'HEX (e.g., #1e98ff). If empty, category color or global color will be used.', 'rusnet-interactive-map' ); ?></span>
</p>
<h4><?php esc_html_e( 'Custom icon (image)', 'rusnet-interactive-map' ); ?></h4>
<div id="marker-icon-custom-wrapper">
<div style="display: flex; gap: 5px; align-items: center;">
<input type="text" id="marker_custom_icon" name="marker_custom_icon" value="<?php echo esc_attr( $custom_icon ); ?>" class="widefat" placeholder="https://... or attachment ID" style="flex:1;" />
<button type="button" class="button rusnetim-upload-icon-btn"><?php esc_html_e( 'Select', 'rusnet-interactive-map' ); ?></button>
</div>
<div id="marker-icon-preview" style="margin-top: 10px;"></div>
<button type="button" id="marker-icon-remove" class="button" style="display: none;"><?php esc_html_e( 'Remove', 'rusnet-interactive-map' ); ?></button>
<span class="description"><?php esc_html_e( 'Upload your own image (SVG, PNG). If specified, it will be used instead of the preset icon.', 'rusnet-interactive-map' ); ?></span>
</div>
<p style="display: flex; gap: 10px; flex-wrap: wrap;">
<span>
<label for="marker_icon_width"><?php esc_html_e( 'Width (px):', 'rusnet-interactive-map' ); ?></label>
<input type="number" id="marker_icon_width" name="marker_icon_width" value="<?php echo esc_attr( $icon_width ); ?>" min="0" step="1" style="width: 80px;" />
</span>
<span>
<label for="marker_icon_height"><?php esc_html_e( 'Height (px):', 'rusnet-interactive-map' ); ?></label>
<input type="number" id="marker_icon_height" name="marker_icon_height" value="<?php echo esc_attr( $icon_height ); ?>" min="0" step="1" style="width: 80px;" />
</span>
<span class="description"><?php esc_html_e( 'Leave empty for auto-detection.', 'rusnet-interactive-map' ); ?></span>
</p>
<?php
do_action( 'rusnetim_marker_icon_meta_box', $post );
}
public function meta_box_url( $post ) {
$url = get_post_meta( $post->ID, '_marker_url', true );
?>
<p>
<label for="marker_url"><?php esc_html_e( 'URL or post ID:', 'rusnet-interactive-map' ); ?></label>
<input type="text" id="marker_url" name="marker_url" value="<?php echo esc_attr( $url ); ?>" class="widefat" />
<span class="description"><?php esc_html_e( 'Clicking on the marker will open the link. You can specify page/post ID.', 'rusnet-interactive-map' ); ?></span>
</p>
<?php
// ADDED HOOK: after URL field
do_action( 'rusnetim_marker_url_meta_box', $post );
}
public function meta_box_image( $post ) {
$gallery_ids = get_post_meta( $post->ID, '_marker_gallery', true );
if ( ! is_array( $gallery_ids ) ) {
$gallery_ids = array();
}
$single_image = get_post_meta( $post->ID, '_marker_image', true );
?>
<div id="marker-gallery-wrapper">
<div style="margin-bottom: 10px;">
<button type="button" class="button" id="rusnetim-add-gallery-btn"><?php esc_html_e( 'Select images', 'rusnet-interactive-map' ); ?></button>
<button type="button" class="button" id="rusnetim-clear-gallery-btn" style="display:<?php echo empty( $gallery_ids ) ? 'none' : 'inline-block'; ?>;"><?php esc_html_e( 'Clear', 'rusnet-interactive-map' ); ?></button>
</div>
<div id="marker-gallery-preview" style="display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px;">
<?php
foreach ( $gallery_ids as $attachment_id ) {
$image = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
if ( $image ) {
echo '<div class="gallery-thumb" data-id="' . esc_attr( $attachment_id ) . '" style="position: relative;">';
echo '<img src="' . esc_url( $image[0] ) . '" style="max-width: 80px; max-height: 80px; border: 1px solid #ddd; padding: 2px; border-radius: 4px;">';
echo '<span class="remove-gallery-thumb" style="position: absolute; top: -5px; right: -5px; background: #f00; color: #fff; border-radius: 50%; width: 18px; height: 18px; text-align: center; line-height: 18px; font-size: 14px; cursor: pointer;">×</span>';
echo '</div>';
}
}
?>
</div>
<input type="hidden" name="marker_gallery" id="marker_gallery" value="<?php echo esc_attr( implode( ',', $gallery_ids ) ); ?>" />
<span class="description"><?php esc_html_e( 'Select images for the marker gallery.', 'rusnet-interactive-map' ); ?></span>
<p><em><?php esc_html_e( 'Old "Image" field is kept for compatibility, but it\'s recommended to use the new gallery.', 'rusnet-interactive-map' ); ?></em></p>
<input type="hidden" name="marker_image_old" id="marker_image_old" value="<?php echo esc_attr( $single_image ); ?>" />
</div>
<?php
// ADDED HOOK: after gallery fields
do_action( 'rusnetim_marker_image_meta_box', $post );
}
public function save_meta( $post_id ) {
if ( ! isset( $_POST['rusnetim_marker_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['rusnetim_marker_nonce'] ) ), 'rusnetim_save_marker' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( isset( $_POST['marker_coord'] ) ) {
$coord = Rusnetim_Options::sanitize_coords( sanitize_text_field( wp_unslash( $_POST['marker_coord'] ) ) );
update_post_meta( $post_id, '_marker_coord', $coord );
}
$visible = isset( $_POST['marker_visible'] ) ? '1' : '0';
update_post_meta( $post_id, '_marker_visible', $visible );
if ( isset( $_POST['marker_icon'] ) ) {
$icon = sanitize_text_field( wp_unslash( $_POST['marker_icon'] ) );
update_post_meta( $post_id, '_marker_icon', $icon );
}
if ( isset( $_POST['marker_color'] ) ) {
$color = sanitize_hex_color( wp_unslash( $_POST['marker_color'] ) );
update_post_meta( $post_id, '_marker_color', $color );
}
if ( isset( $_POST['marker_custom_icon'] ) ) {
$custom_icon = sanitize_text_field( wp_unslash( $_POST['marker_custom_icon'] ) );
update_post_meta( $post_id, '_marker_custom_icon', $custom_icon );
}
if ( isset( $_POST['marker_icon_width'] ) ) {
$width = intval( wp_unslash( $_POST['marker_icon_width'] ) );
update_post_meta( $post_id, '_marker_icon_width', $width ?: '' );
}
if ( isset( $_POST['marker_icon_height'] ) ) {
$height = intval( wp_unslash( $_POST['marker_icon_height'] ) );
update_post_meta( $post_id, '_marker_icon_height', $height ?: '' );
}
if ( isset( $_POST['marker_url'] ) ) {
$url = esc_url_raw( wp_unslash( $_POST['marker_url'] ) );
update_post_meta( $post_id, '_marker_url', $url );
}
if ( isset( $_POST['marker_gallery'] ) ) {
$gallery = sanitize_text_field( wp_unslash( $_POST['marker_gallery'] ) );
$ids = array_filter( array_map( 'intval', explode( ',', $gallery ) ) );
update_post_meta( $post_id, '_marker_gallery', $ids );
} else {
delete_post_meta( $post_id, '_marker_gallery' );
}
if ( isset( $_POST['marker_image_old'] ) ) {
$image = sanitize_text_field( wp_unslash( $_POST['marker_image_old'] ) );
update_post_meta( $post_id, '_marker_image', $image );
}
do_action( 'rusnetim_save_marker_meta', $post_id );
}
public function custom_columns( $columns ) {
$new_columns = array();
foreach ( $columns as $key => $value ) {
if ( $key === 'title' ) {
$new_columns[ $key ] = $value;
$new_columns['marker_visible'] = esc_html__( 'Visibility', 'rusnet-interactive-map' );
} else {
$new_columns[ $key ] = $value;
}
}
$new_columns['marker_coord'] = esc_html__( 'Coordinates', 'rusnet-interactive-map' );
$new_columns['taxonomy-rusnetim_category'] = esc_html__( 'Category', 'rusnet-interactive-map' );
$new_columns['marker_shortcode'] = esc_html__( 'Shortcode', 'rusnet-interactive-map' );
unset( $new_columns['date'] );
return $new_columns;
}
public function custom_column_content( $column, $post_id ) {
switch ( $column ) {
case 'marker_coord':
$coord = get_post_meta( $post_id, '_marker_coord', true );
echo esc_html( $coord ?: '—' );
break;
case 'marker_shortcode':
echo '<input type="text" readonly value="' . esc_attr( '[rusnetim_map marker_id="' . $post_id . '"]' ) . '" class="widefat" onclick="this.select();" />';
break;
case 'marker_visible':
$visible = get_post_meta( $post_id, '_marker_visible', true );
$visible = ( $visible === '1' || $visible === '' ) ? '1' : '0';
$icon = ( $visible === '1' ) ? 'dashicons-visibility' : 'dashicons-hidden';
$title = ( $visible === '1' ) ? esc_attr__( 'Hide marker', 'rusnet-interactive-map' ) : esc_attr__( 'Show marker', 'rusnet-interactive-map' );
echo '<span class="rusnetim-toggle-visibility dashicons ' . esc_attr( $icon ) . '" data-id="' . esc_attr( $post_id ) . '" data-nonce="' . esc_attr( wp_create_nonce( 'rusnetim_toggle_' . $post_id ) ) . '" title="' . esc_attr( $title ) . '" style="cursor: pointer;"></span>';
break;
}
}
public function ajax_toggle_visibility() {
if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['post_id'] ) ) {
wp_die( -1 );
}
$post_id = intval( wp_unslash( $_POST['post_id'] ) );
$nonce = sanitize_text_field( wp_unslash( $_POST['nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'rusnetim_toggle_' . $post_id ) ) {
wp_die( -1 );
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( -1 );
}
$current = get_post_meta( $post_id, '_marker_visible', true );
$new = ( $current === '0' ) ? '1' : '0';
update_post_meta( $post_id, '_marker_visible', $new );
wp_send_json_success( [ 'visible' => $new ] );
}
public function admin_posts_filter() {
global $typenow;
if ( 'rusnetim_marker' === $typenow ) {
$taxonomy = 'rusnetim_category';
$selected = isset( $_GET[ $taxonomy ] ) ? sanitize_text_field( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
wp_dropdown_categories( array(
'show_option_all' => esc_html__( 'All categories', 'rusnet-interactive-map' ),
'taxonomy' => $taxonomy,
'name' => $taxonomy,
'value_field' => 'slug',
'selected' => $selected,
'show_count' => false,
'hide_empty' => true,
) );
}
}
public function enqueue_scripts( $hook_suffix ) {
$screen = get_current_screen();
if ( $screen && ( 'rusnetim_marker' === $screen->post_type ) ) {
wp_enqueue_media();
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_script(
'rusnetim-admin-iconset',
RUSNETIM_PLUGIN_URL . 'assets/js/admin/admin-iconset.js',
array( 'jquery', 'wp-color-picker' ),
RUSNETIM_VERSION,
true
);
wp_enqueue_script(
'rusnetim-admin-gallery',
RUSNETIM_PLUGIN_URL . 'assets/js/admin/admin-gallery.js',
array( 'jquery', 'wp-color-picker' ),
RUSNETIM_VERSION,
true
);
wp_localize_script( 'rusnetim-admin-iconset', 'rusnetim_admin', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'rusnetim_ajax_nonce' ),
'i18n' => array(
'selectImage' => __( 'Select image', 'rusnet-interactive-map' ),
'useImage' => __( 'Use this image', 'rusnet-interactive-map' ),
'enterSlug' => __( 'Please enter a slug for the new icon.', 'rusnet-interactive-map' ),
'invalidSlug' => __( 'Slug may only contain latin letters, digits and hyphen.', 'rusnet-interactive-map' ),
'slugExists' => __( 'An icon with this slug already exists.', 'rusnet-interactive-map' ),
'remove' => __( 'Remove', 'rusnet-interactive-map' ),
)
) );
wp_add_inline_script( 'wp-color-picker', '
jQuery(document).ready(function($) {
$(".rusnetim-color-field").wpColorPicker();
});
' );
$custom_css = '
.post-type-rusnetim_marker .inside .widefat,
.post-type-rusnetim_marker .inside input[type="text"],
.post-type-rusnetim_marker .inside input[type="number"],
.post-type-rusnetim_marker .inside textarea {
width: 100% !important;
box-sizing: border-box;
max-width: 100%;
}
.post-type-rusnetim_marker .inside span[style*="display: flex"] {
flex-wrap: wrap;
}
.post-type-rusnetim_marker .inside span[style*="display: flex"] input {
min-width: 200px;
flex: 1 1 auto;
}
.post-type-rusnetim_marker .inside p {
max-width: 100%;
}
.column-marker_visible {
width: 80px;
text-align: center;
}
.rusnetim-toggle-visibility {
font-size: 20px;
width: auto;
height: auto;
}
.rusnetim-toggle-visibility.dashicons-hidden {
opacity: 0.5;
}
';
wp_register_style( 'rusnetim-admin-marker-styles', false );
wp_enqueue_style( 'rusnetim-admin-marker-styles' );
wp_add_inline_style( 'rusnetim-admin-marker-styles', $custom_css );
if ( $screen->base === 'edit' ) {
$toggle_script = '
jQuery(document).ready(function($) {
$(".rusnetim-toggle-visibility").on("click", function() {
var $this = $(this);
var post_id = $this.data("id");
var nonce = $this.data("nonce");
$.post(ajaxurl, {
action: "rusnetim_toggle_visibility",
post_id: post_id,
nonce: nonce
}, function(response) {
if (response.success) {
var visible = response.data.visible;
if (visible === "1") {
$this.removeClass("dashicons-hidden").addClass("dashicons-visibility");
$this.attr("title", "' . esc_js( __( 'Hide marker', 'rusnet-interactive-map' ) ) . '");
} else {
$this.removeClass("dashicons-visibility").addClass("dashicons-hidden");
$this.attr("title", "' . esc_js( __( 'Show marker', 'rusnet-interactive-map' ) ) . '");
}
}
});
});
});
';
wp_add_inline_script( 'jquery', $toggle_script );
}
}
}
}