View file rusnet-interactive-map/classes/class-admin.php

File size: 46.92Kb
<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Rusnetim_Admin {

    private $page_hook = 'rusnetim-options';

    public function __construct() {
        add_action( 'admin_menu', [ $this, 'add_admin_menu' ], 20 );
        add_action( 'admin_init', [ $this, 'register_settings' ] );
        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
        add_filter( 'mce_external_plugins', [ $this, 'add_tinymce_plugin' ] );
        add_filter( 'mce_buttons', [ $this, 'add_tinymce_button' ] );
        add_action( 'admin_head', [ $this, 'output_tinymce_template' ] );
        add_filter( 'parent_file', [ $this, 'set_parent_file' ] );
        add_filter( 'submenu_file', [ $this, 'set_submenu_file' ], 10, 2 );
    }

    public function add_admin_menu() {
        add_menu_page(
            'Rusnet Interactive Map',
            'Rusnet Map',
            'manage_options',
            $this->page_hook,
            [ $this, 'option_page' ],
            'dashicons-location-alt',
            30
        );
    }

    public function set_parent_file( $parent_file ) {
        global $current_screen;

        if ( ! $current_screen ) {
            return $parent_file;
        }

        if ( $current_screen->post_type === 'rusnetim_marker' ) {
            $parent_file = 'edit.php?post_type=rusnetim_marker';
        } elseif ( $current_screen->taxonomy === 'rusnetim_category' ) {
            $parent_file = 'edit.php?post_type=rusnetim_marker';
        }

        return $parent_file;
    }

    public function set_submenu_file( $submenu_file, $parent_file ) {
        global $current_screen;

        if ( ! $current_screen ) {
            return $submenu_file;
        }

        if ( $current_screen->post_type === 'rusnetim_marker' ) {
            if ( $current_screen->action === 'add' ) {
                $submenu_file = 'post-new.php?post_type=rusnetim_marker';
            } else {
                $submenu_file = 'edit.php?post_type=rusnetim_marker';
            }
        } elseif ( $current_screen->taxonomy === 'rusnetim_category' ) {
            $submenu_file = 'edit-tags.php?taxonomy=rusnetim_category&post_type=rusnetim_marker';
        }

        return $submenu_file;
    }

    public function register_settings() {
        register_setting( 'rusnetim_options', 'rusnetim_options', [ Rusnetim_Options::class, 'sanitize' ] );

        add_settings_section( 'general_section', esc_html__( 'Map options', 'rusnet-interactive-map' ), '', $this->page_hook . '_general' );

        $general_fields = [
            'center_map_option' => [
                'title' => esc_html__( 'Map center', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => esc_html__( 'Drag the map to set its default coordinates', 'rusnet-interactive-map' ),
            ],
            'zoom_map_option' => [
                'title' => esc_html__( 'Map zoom', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => esc_html__( 'Zoom the map to set its default scale', 'rusnet-interactive-map' ),
            ],
            'type_map_option' => [
                'title' => esc_html__( 'Map type', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => esc_html__( 'Choose default map type: yandex#map (scheme), yandex#satellite, yandex#hybrid.', 'rusnet-interactive-map' ),
            ],
            'height_map_option' => [
                'title' => esc_html__( 'Map height', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => __( 'Specify height with units: <code>400px</code>, <code>30rem</code>, <code>50vh</code>.', 'rusnet-interactive-map' ),
            ],
            'controls_map_option' => [
                'title' => esc_html__( 'Map controls', 'rusnet-interactive-map' ),
                'type'  => 'textarea',
                'desc'  => '<div id="addcontrol" style="text-align: left;"><a data-control="typeSelector">' . esc_html__( 'Map type', 'rusnet-interactive-map' ) . '</a>, <a data-control="zoomControl">' . esc_html__( 'Zoom', 'rusnet-interactive-map' ) . '</a>, <a data-control="searchControl">' . esc_html__( 'Search', 'rusnet-interactive-map' ) . '</a>, <a data-control="routeButtonControl">' . esc_html__( 'Route', 'rusnet-interactive-map' ) . '</a>, <a data-control="rulerControl">' . esc_html__( 'Ruler', 'rusnet-interactive-map' ) . '</a>, <a data-control="trafficControl">' . esc_html__( 'Traffic', 'rusnet-interactive-map' ) . '</a>, <a data-control="fullscreenControl">' . esc_html__( 'Full screen', 'rusnet-interactive-map' ) . '</a>, <a data-control="geolocationControl">' . esc_html__( 'Geolocation', 'rusnet-interactive-map' ) . '</a></div>'
            ],
            'wheelzoom_map_option' => [
                'title' => esc_html__( 'Wheel zoom', 'rusnet-interactive-map' ),
                'type'  => 'checkbox',
                'desc'  => esc_html__( 'The map can be scaled with mouse scroll', 'rusnet-interactive-map' ),
            ],
            'mobiledrag_map_option' => [
                'title' => esc_html__( 'Mobile drag', 'rusnet-interactive-map' ),
                'type'  => 'checkbox',
                'desc'  => esc_html__( 'The map can be dragged on mobile', 'rusnet-interactive-map' ),
            ],
        ];

        foreach ( $general_fields as $id => $field ) {
            add_settings_field(
                $id,
                $field['title'],
                [ $this, 'display_field' ],
                $this->page_hook . '_general',
                'general_section',
                [ 'id' => $id, 'type' => $field['type'], 'desc' => $field['desc'] ]
            );
        }

        add_settings_section( 'clustering_section', esc_html__( 'Clustering options', 'rusnet-interactive-map' ), '', $this->page_hook . '_clustering' );

        $clustering_fields = [
            'cluster_map_option' => [
                'title' => esc_html__( 'Clustering', 'rusnet-interactive-map' ),
                'type'  => 'checkbox',
                'desc'  => esc_html__( 'Enable marker clustering by default', 'rusnet-interactive-map' )
                      . '<br><span class="description">'
                      . esc_html__( 'Clustering automatically groups nearby markers into clusters. This improves map performance and readability when many markers are close together.', 'rusnet-interactive-map' )
                      . '</span>'
            ],
            'cluster_grid_option' => [
                'title' => esc_html__( 'Cluster grid size', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => esc_html__( 'Cluster grid size in pixels (2, 4, 8, 16, 32, 64, 128, 256).', 'rusnet-interactive-map' )
                      . '<br><span class="description">'
                      . esc_html__( 'Smaller values (e.g., 32) require markers to be very close to form a cluster; larger values (e.g., 128) create larger clusters, grouping markers that are farther apart.', 'rusnet-interactive-map' )
                      . '</span>',
            ],
            'cluster_color_option' => [
                'title' => esc_html__( 'Cluster color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Choose the color for cluster icons.', 'rusnet-interactive-map' ),
            ],
        ];

        foreach ( $clustering_fields as $id => $field ) {
            add_settings_field(
                $id,
                $field['title'],
                [ $this, 'display_field' ],
                $this->page_hook . '_clustering',
                'clustering_section',
                [ 'id' => $id, 'type' => $field['type'], 'desc' => $field['desc'] ]
            );
        }
        // ADDED HOOK: after clustering fields
        do_action( 'rusnetim_after_clustering_fields', $this->page_hook . '_clustering' );

        add_settings_section( 'infopanel_section', esc_html__( 'Info Panel options', 'rusnet-interactive-map' ), '', $this->page_hook . '_infopanel' );

        $infopanel_fields = [
            'info_panel_side' => [
                'title'   => esc_html__( 'Info panel side', 'rusnet-interactive-map' ),
                'type'    => 'select',
                'desc'    => esc_html__( 'Choose on which side the info panel should appear.', 'rusnet-interactive-map' ),
                'options' => apply_filters( 'rusnetim_info_panel_side_options', [
                    'right' => esc_html__( 'Right', 'rusnet-interactive-map' ),
                    'left'  => esc_html__( 'Left', 'rusnet-interactive-map' ),
                ] ),
            ],
            'info_panel_width' => [
                'title'   => esc_html__( 'Info panel width', 'rusnet-interactive-map' ),
                'type'    => 'text',
                'desc'    => esc_html__( 'Specify width with units: px, %, em, rem, vw, vh (e.g., 300px, 50%, 20rem). For modal, this will be the width of the content box.', 'rusnet-interactive-map' ),
            ],
            'info_panel_title_position' => [
                'title'   => esc_html__( 'Title position in panel', 'rusnet-interactive-map' ),
                'type'    => 'select',
                'desc'    => esc_html__( 'Where to display the marker title: above or below the gallery.', 'rusnet-interactive-map' ),
                'options' => [
                    'below' => esc_html__( 'Below gallery', 'rusnet-interactive-map' ),
                    'above' => esc_html__( 'Above gallery', 'rusnet-interactive-map' ),
                ],
            ],
            'info_title_tag' => [
                'title'   => esc_html__( 'Title HTML tag', 'rusnet-interactive-map' ),
                'type'    => 'select',
                'desc'    => esc_html__( 'Choose HTML element to wrap the title.', 'rusnet-interactive-map' ),
                'options' => [
                    'h1' => 'h1',
                    'h2' => 'h2',
                    'h3' => 'h3',
                    'h4' => 'h4',
                    'h5' => 'h5',
                    'h6' => 'h6',
                    'div' => 'div',
                    'span' => 'span',
                    'p'   => 'p',
                ],
            ],
            'info_title_font_size' => [
                'title' => esc_html__( 'Title font size', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => esc_html__( 'e.g., 18px, 1.2rem, 1.5em. Leave empty for default.', 'rusnet-interactive-map' ),
            ],
            'info_title_color' => [
                'title' => esc_html__( 'Title color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Select color for title.', 'rusnet-interactive-map' ),
            ],
            'info_panel_bg_color' => [
                'title' => esc_html__( 'Info panel background color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Select background color for the info panel.', 'rusnet-interactive-map' ),
            ],
        ];

        foreach ( $infopanel_fields as $id => $field ) {
            add_settings_field(
                $id,
                $field['title'],
                [ $this, 'display_field' ],
                $this->page_hook . '_infopanel',
                'infopanel_section',
                [
                    'id'      => $id,
                    'type'    => $field['type'],
                    'desc'    => $field['desc'],
                    'options' => isset( $field['options'] ) ? $field['options'] : []
                ]
            );
        }
        // ADDED HOOK: after infopanel fields
        do_action( 'rusnetim_after_infopanel_fields', $this->page_hook . '_infopanel' );

        add_settings_section( 'category_filter_section', esc_html__( 'Category Filter options', 'rusnet-interactive-map' ), '', $this->page_hook . '_category_filter' );

        $cat_filter_fields = [
            'cat_filter_container_justify' => [
                'title'   => esc_html__( 'Container alignment', 'rusnet-interactive-map' ),
                'type'    => 'select',
                'desc'    => esc_html__( 'How to align filter buttons within the container.', 'rusnet-interactive-map' ),
                'options' => [
                    'flex-start'    => esc_html__( 'Left', 'rusnet-interactive-map' ),
                    'center'        => esc_html__( 'Center', 'rusnet-interactive-map' ),
                    'flex-end'      => esc_html__( 'Right', 'rusnet-interactive-map' ),
                    'space-between' => esc_html__( 'Space between', 'rusnet-interactive-map' ),
                    'space-around'  => esc_html__( 'Space around', 'rusnet-interactive-map' ),
                    'space-evenly'  => esc_html__( 'Space evenly', 'rusnet-interactive-map' ),
                ],
            ],
            'cat_filter_button_tag' => [
                'title'   => esc_html__( 'Button tag', 'rusnet-interactive-map' ),
                'type'    => 'select',
                'desc'    => esc_html__( 'HTML element used for filter buttons.', 'rusnet-interactive-map' ),
                'options' => [
                    'button' => '&lt;button&gt;',
                    'a'      => '&lt;a&gt;',
                    'div'    => '&lt;div&gt;',
                    'span'   => '&lt;span&gt;',
                ],
            ],
            'cat_filter_button_font_size' => [
                'title' => esc_html__( 'Font size', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => esc_html__( 'e.g., 14px, 1rem, 1.2em. Leave empty for default.', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_color' => [
                'title' => esc_html__( 'Text color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Color of the button text (normal state).', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_bg_color' => [
                'title' => esc_html__( 'Background color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Background color of the button (normal state).', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_border' => [
                'title' => esc_html__( 'Border', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => __( 'CSS border property, e.g. <code>1px solid #ccc</code>.', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_box_shadow' => [
                'title' => esc_html__( 'Box shadow', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => __( 'CSS box-shadow, e.g. <code>0 2px 4px rgba(0,0,0,0.1)</code>.', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_padding' => [
                'title' => esc_html__( 'Padding', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => __( 'Button padding, e.g. <code>6px 12px</code>.', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_border_radius' => [
                'title' => esc_html__( 'Border radius', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => __( 'Border radius, e.g. <code>4px</code>.', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_active_color' => [
                'title' => esc_html__( 'Active text color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Text color when button is active (selected category).', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_active_bg_color' => [
                'title' => esc_html__( 'Active background color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'Background color when button is active.', 'rusnet-interactive-map' ),
            ],
            'cat_filter_button_active_border' => [
                'title' => esc_html__( 'Active border', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => __( 'Border when button is active <code>2px solid #ff0000</code>.', 'rusnet-interactive-map' ),
            ],
        ];

        foreach ( $cat_filter_fields as $id => $field ) {
            add_settings_field(
                $id,
                $field['title'],
                [ $this, 'display_field' ],
                $this->page_hook . '_category_filter',
                'category_filter_section',
                [
                    'id'      => $id,
                    'type'    => $field['type'],
                    'desc'    => $field['desc'],
                    'options' => $field['options'] ?? []
                ]
            );
        }
        // ADDED HOOK: after category filter fields
        do_action( 'rusnetim_after_category_filter_fields', $this->page_hook . '_category_filter' );

        add_settings_section( 'markers_section', esc_html__( 'Marker options', 'rusnet-interactive-map' ), '', $this->page_hook . '_markers' );

        $markers_fields = [
            'type_icon_option' => [
                'title' => esc_html__( 'Icon preset', 'rusnet-interactive-map' ),
                'type'  => 'text',
                'desc'  => '<a href="https://tech.yandex.com/maps/doc/jsapi/2.1/ref/reference/option.presetStorage-docpage/" target="_blank">' . esc_html__( 'Other icon types', 'rusnet-interactive-map' ) . '</a>',
            ],
            'color_icon_option' => [
                'title' => esc_html__( 'Marker color', 'rusnet-interactive-map' ),
                'type'  => 'color',
                'desc'  => esc_html__( 'For example:', 'rusnet-interactive-map' ) . ' #1e98ff',
            ],
            'custom_icon_option' => [
                'title' => esc_html__( 'Custom icon', 'rusnet-interactive-map' ),
                'type'  => 'custom_icon',
                'desc'  => esc_html__( 'Upload your own icon image', 'rusnet-interactive-map' ),
            ],
            'custom_icon_width_option' => [
                'title' => esc_html__( 'Icon width', 'rusnet-interactive-map' ),
                'type'  => 'number',
                'desc'  => esc_html__( 'Width in pixels (optional)', 'rusnet-interactive-map' ),
            ],
            'custom_icon_height_option' => [
                'title' => esc_html__( 'Icon height', 'rusnet-interactive-map' ),
                'type'  => 'number',
                'desc'  => esc_html__( 'Height in pixels (optional)', 'rusnet-interactive-map' ),
            ],
        ];

        foreach ( $markers_fields as $id => $field ) {
            add_settings_field(
                $id,
                $field['title'],
                [ $this, 'display_field' ],
                $this->page_hook . '_markers',
                'markers_section',
                [ 'id' => $id, 'type' => $field['type'], 'desc' => $field['desc'] ]
            );
        }
        // ADDED HOOK: after markers fields
        do_action( 'rusnetim_after_markers_fields', $this->page_hook . '_markers' );

        add_settings_section( 'apikey_section', esc_html__( 'Yandex.Maps API key', 'rusnet-interactive-map' ), '', $this->page_hook . '_general' );
        add_settings_field(
            'apikey_map_option',
            esc_html__( 'API key', 'rusnet-interactive-map' ),
            [ $this, 'display_field' ],
            $this->page_hook . '_general',
            'apikey_section',
            [
                'id'   => 'apikey_map_option',
                'type' => 'text',
                'desc' => __( '<a href="https://developer.tech.yandex.com/services/">Get a key</a> (JavaScript API & HTTP Geocoder) if necessary', 'rusnet-interactive-map' ),
            ]
        );

        add_settings_section( 'reset_section', esc_html__( 'Reset options', 'rusnet-interactive-map' ), '', $this->page_hook . '_general' );
        add_settings_field(
            'reset_maps_option',
            esc_html__( 'Reset options', 'rusnet-interactive-map' ),
            [ $this, 'display_field' ],
            $this->page_hook . '_general',
            'reset_section',
            [
                'id'   => 'reset_maps_option',
                'type' => 'checkbox',
                'desc' => esc_html__( 'Restore defaults', 'rusnet-interactive-map' ),
            ]
        );

        // ADDED HOOK: after all general fields (including apikey and reset)
        do_action( 'rusnetim_after_general_fields', $this->page_hook . '_general' );

        do_action( 'rusnetim_after_register_settings', $this->page_hook );
    }

    public function display_field( $args ) {
        $id = $args['id'];
        $type = $args['type'];
        $desc = $args['desc'] ?? '';

        $options = Rusnetim_Options::get_all();
        $value = $options[ $id ] ?? '';

        switch ( $type ) {
            case 'text':
                ?>
                <input class="regular-text" type="text" id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]" value="<?php echo esc_attr( $value ); ?>" />
                <?php if ( $desc ) echo '<br /><span class="description">' . wp_kses_post( $desc ) . '</span>'; ?>
                <?php
                break;
            case 'textarea':
                ?>
                <textarea class="code large-text" cols="50" rows="2" id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]"><?php echo esc_textarea( $value ); ?></textarea>
                <?php if ( $desc ) echo '<br /><span class="description">' . wp_kses_post( $desc ) . '</span>'; ?>
                <?php
                break;
            case 'checkbox':
                ?>
                <input type="hidden" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]" value="off" />
                <input type="checkbox" id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]" <?php checked( $value, 'on' ); ?> />
                <label for="<?php echo esc_attr( $id ); ?>"><?php echo wp_kses_post( $desc ); ?></label>
                <?php
                break;
            case 'color':
                ?>
                <input type="text" id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]" value="<?php echo esc_attr( $value ); ?>" class="rusnetim-color-field" data-default-color="#1e98ff" />
                <?php if ( $desc ) echo '<br /><span class="description">' . wp_kses_post( $desc ) . '</span>'; ?>
                <?php
                break;
            case 'custom_icon':
                ?>
                <div id="global-icon-wrapper">
                    <div style="display: flex; gap: 5px; align-items: center;">
                        <input type="text" id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]" value="<?php echo esc_attr( $value ); ?>" class="regular-text" style="flex:1;" />
                        <button type="button" class="button rusnetim-upload-icon-btn"><?php esc_html_e( 'Select Image', 'rusnet-interactive-map' ); ?></button>
                    </div>
                    <div id="global-icon-preview" style="margin-top: 10px;"></div>
                    <button type="button" id="global-icon-remove" class="button" style="display: none;"><?php esc_html_e( 'Remove', 'rusnet-interactive-map' ); ?></button>
                    <?php if ( $desc ) echo "<br /><span class='description'>" . wp_kses_post( $desc ) . "</span>"; ?>
                </div>
                <?php
                break;
            case 'number':
                ?>
                <input type="number" id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]" value="<?php echo esc_attr( $value ); ?>" min="0" step="1" />
                <?php if ( $desc ) echo '<br /><span class="description">' . wp_kses_post( $desc ) . '</span>'; ?>
                <?php
                break;
            case 'select':
                ?>
                <select id="<?php echo esc_attr( $id ); ?>" name="rusnetim_options[<?php echo esc_attr( $id ); ?>]">
                    <?php foreach ( $args['options'] as $opt_value => $opt_label ) : ?>
                        <option value="<?php echo esc_attr( $opt_value ); ?>" <?php selected( $value, $opt_value ); ?>>
                            <?php echo esc_html( $opt_label ); ?>
                        </option>
                    <?php endforeach; ?>
                </select>
                <?php if ( $desc ) echo '<br /><span class="description">' . wp_kses_post( $desc ) . '</span>'; ?>
                <?php
                break;
            case 'preset':
                $presets = rusnetim_get_category_filter_presets();
                ?>
                <div class="rusnetim-presets">
                    <div class="rusnetim-preset-buttons">
                        <?php foreach ( $presets as $key => $preset ) : ?>
                            <button type="button" class="rusnetim-preset-btn button" data-preset="<?php echo esc_attr( $key ); ?>">
                                <?php echo esc_html( $preset['label'] ); ?>
                            </button>
                        <?php endforeach; ?>
                    </div>

                    <div class="rusnetim-preview-box">
                        <button class="preview-btn preview-normal" style="display:inline-block;"><?php esc_html_e( 'Normal', 'rusnet-interactive-map' ); ?></button>
                        <button class="preview-btn preview-active" style="display:inline-block;"><?php esc_html_e( 'Active', 'rusnet-interactive-map' ); ?></button>
                    </div>

                    <?php if ( ! empty( $args['desc'] ) ) : ?>
                        <p class="description"><?php echo wp_kses_post( $args['desc'] ); ?></p>
                    <?php endif; ?>
                </div>
                <?php
                break;
        }
    }

    public function enqueue_scripts( $hook_suffix ) {
        $is_options_page = ( $hook_suffix === 'toplevel_page_' . $this->page_hook ) || ( isset( $_GET['page'] ) && $_GET['page'] === $this->page_hook );
        $is_marker_edit = ( $hook_suffix === 'post.php' || $hook_suffix === 'post-new.php' ) && get_post_type() === 'rusnetim_marker';
        $is_category_edit = ( $hook_suffix === 'edit-tags.php' || $hook_suffix === 'term.php' ) && isset( $_GET['taxonomy'] ) && $_GET['taxonomy'] === 'rusnetim_category';

        if ( $is_options_page || $is_marker_edit || $is_category_edit ) {
            wp_enqueue_media();
            wp_enqueue_style( 'wp-color-picker' );
            wp_enqueue_script( 'wp-color-picker' );

            $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
            if ( $is_options_page && $active_tab === 'general' ) {
                $defaults = Rusnetim_Options::get_all();
                $locale = Rusnetim_Options::sanitize_locale( get_locale() );
                $apikey = $defaults['apikey_map_option'];
                $api_url = Rusnetim_API::get_api_url( $locale, $apikey );
                wp_enqueue_script( 'yandex-maps-api', $api_url, [], null, true );

                $preview_data = [
                    'center'     => array_map( 'floatval', explode( ',', $defaults['center_map_option'] ) ),
                    'zoom'       => (int) $defaults['zoom_map_option'],
                    'type'       => $defaults['type_map_option'],
                    'controls'   => Rusnetim_Options::sanitize_controls( $defaults['controls_map_option'] ),
                    'icon_preset'=> $defaults['type_icon_option'],
                    'icon_color' => $defaults['color_icon_option'],
                    'apikey'     => $apikey,
                ];
                wp_enqueue_script( 'rusnetim-admin-preview', RUSNETIM_PLUGIN_URL . 'assets/js/admin/admin-map-preview.js', [ 'jquery' ], RUSNETIM_VERSION, true );
                wp_localize_script( 'rusnetim-admin-preview', 'rusnetim_admin_preview', $preview_data );

                $custom_css = "
                    #addcontrol a {
                        cursor: pointer;
                        color: #2271b1;
                        text-decoration: underline;
                    }
                    #addcontrol a:hover {
                        color: #135e96;
                    }
                ";
                wp_add_inline_style( 'common', $custom_css );
            }

            wp_localize_script( 'rusnetim-admin-iconset', 'rusnetim_admin', [
                'ajaxurl' => admin_url( 'admin-ajax.php' ),
                'nonce'   => wp_create_nonce( 'rusnetim_ajax_nonce' ),
                'i18n'    => [
                    '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();
                });
            ');

            if ( $is_options_page && isset( $_GET['tab'] ) && $_GET['tab'] === 'category_filter' ) {
                wp_enqueue_script(
                    'rusnetim-admin-presets',
                    RUSNETIM_PLUGIN_URL . 'assets/js/admin/admin-presets.js',
                    [ 'jquery' ],
                    RUSNETIM_VERSION,
                    true
                );
                $presets = rusnetim_get_category_filter_presets();
                wp_localize_script( 'rusnetim-admin-presets', 'rusnetimPresets', $presets );

                $preview_css = '
                .rusnetim-preset-buttons {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 8px;
                    margin-bottom: 15px;
                }
                .rusnetim-preset-btn {
                    padding: 6px 12px;
                    border: 1px solid #ccc;
                    background: #f7f7f7;
                    cursor: pointer;
                    border-radius: 3px;
                }
                .rusnetim-preset-btn:hover {
                    background: #eaeaea;
                }
                .rusnetim-preview-box {
                    display: flex;
                    gap: 20px;
                    align-items: center;
                    margin-top: 15px;
                    padding: 20px;
                    background: #f9f9f9;
                    border: 1px solid #ddd;
                    border-radius: 5px;
                }
                .preview-btn {
                    display: inline-block;
                    cursor: default;
                    pointer-events: none;
                    margin: 0;
                }';
                wp_register_style( 'rusnetim-admin-presets-styles', false );
                wp_enqueue_style( 'rusnetim-admin-presets-styles' );
                wp_add_inline_style( 'rusnetim-admin-presets-styles', $preview_css );
            }
        }
    }

    public function add_tinymce_plugin( $plugin_array ) {
        $handle = 'rusnetim-tinymce';
        $src = RUSNETIM_PLUGIN_URL . 'assets/js/tinymce/plugin.js';
        wp_register_script( $handle, $src, [], RUSNETIM_VERSION, true );
        wp_localize_script( $handle, 'rusnetim_defaults', Rusnetim_Options::get_all() );
        wp_localize_script( $handle, 'rusnetim_object', $this->get_tinymce_l10n() );
        wp_enqueue_script( $handle );
        $plugin_array['rusnetim_plugin'] = $src;
        return $plugin_array;
    }

    public function add_tinymce_button( $buttons ) {
        array_push( $buttons, 'rusnetim' );
        return $buttons;
    }

    public function output_tinymce_template() {
        include_once RUSNETIM_PLUGIN_DIR . 'templates/tmpl-editor-rusnetim.html';
    }

    private function get_tinymce_l10n() {
        return [
            'YaMap'            => esc_html__( 'Map', 'rusnet-interactive-map' ),
            'AddMap'           => esc_html__( 'Add map', 'rusnet-interactive-map' ),
            'EditMap'          => esc_html__( 'Edit map', 'rusnet-interactive-map' ),
            'PluginTitle'      => esc_html__( 'Rusnet Interactive Map', 'rusnet-interactive-map' ),
            'MarkerTab'        => esc_html__( 'Placemark', 'rusnet-interactive-map' ),
            'MapTab'           => esc_html__( 'Map', 'rusnet-interactive-map' ),
            'MarkerIcon'       => esc_html__( 'Icon', 'rusnet-interactive-map' ),
            'BlueOnly'         => esc_html__( 'Blue only', 'rusnet-interactive-map' ),
            'MarkerUrl'        => esc_html__( 'Link', 'rusnet-interactive-map' ),
            'MarkerUrlTip'     => esc_html__( 'Placemark hyperlink url or post ID', 'rusnet-interactive-map' ),
            'MapHeight'        => esc_html__( 'Map height', 'rusnet-interactive-map' ),
            'MarkerName'       => esc_html__( 'Placemark name', 'rusnet-interactive-map' ),
            'MarkerNameTip'    => esc_html__( 'Text for hint or icon content', 'rusnet-interactive-map' ),
            'MapControlsTip'   => esc_html__( 'Use the links below', 'rusnet-interactive-map' ),
            'MarkerCoord'      => esc_html__( 'Coordinates', 'rusnet-interactive-map' ),
            'NoCoord'          => esc_html__( 'Click on the map to choose or create the mark', 'rusnet-interactive-map' ),
            'MapControls'      => esc_html__( 'Map controls', 'rusnet-interactive-map' ),
            'MarkerDelete'     => esc_html__( 'Delete', 'rusnet-interactive-map' ),
            'type'             => esc_html__( 'Map type', 'rusnet-interactive-map' ),
            'zoom'             => esc_html__( 'Zoom', 'rusnet-interactive-map' ),
            'ScrollZoom'       => esc_html__( 'Wheel zoom', 'rusnet-interactive-map' ),
            'MobileDrag'       => esc_html__( 'Mobile drag', 'rusnet-interactive-map' ),
            'search'           => esc_html__( 'Search', 'rusnet-interactive-map' ),
            'route'            => esc_html__( 'Route', 'rusnet-interactive-map' ),
            'ruler'            => esc_html__( 'Ruler', 'rusnet-interactive-map' ),
            'traffic'          => esc_html__( 'Traffic', 'rusnet-interactive-map' ),
            'fullscreen'       => esc_html__( 'Full screen', 'rusnet-interactive-map' ),
            'geolocation'      => esc_html__( 'Geolocation', 'rusnet-interactive-map' ),
            'MarkerColor'      => esc_html__( 'Marker color', 'rusnet-interactive-map' ),
            'MapContainerID'   => esc_html__( 'Put in ID', 'rusnet-interactive-map' ),
            'MapContainerIDTip'=> esc_html__( 'Do not create a block in the content. Use the existing block of the WP theme with the specified ID', 'rusnet-interactive-map' ),
            'ClusterTab'       => esc_html__( 'Clustering', 'rusnet-interactive-map' ),
            'ClusterEnable'    => esc_html__( 'Enable clustering', 'rusnet-interactive-map' ),
            'ClusterGrid'      => esc_html__( 'Cluster grid size (px)', 'rusnet-interactive-map' ),
            'ClusterInfo'      => esc_html__( 'Clustering groups nearby markers into clusters.<br>The grid size determines how close markers need to be to form a cluster.', 'rusnet-interactive-map' ),
            'Extra'            => esc_html__( 'Extra', 'rusnet-interactive-map' ),
            'DeveloperInfoTab' => esc_html__( 'Design & Development', 'rusnet-interactive-map' ),
            'ExtraHTML'        => '',
        ];
    }

   public function option_page() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'rusnet-interactive-map' ) );
        }
    
        $tabs = array(
            'general'         => esc_html__( 'General', 'rusnet-interactive-map' ),
            'clustering'      => esc_html__( 'Clustering', 'rusnet-interactive-map' ),
            'infopanel'       => esc_html__( 'Info Panel', 'rusnet-interactive-map' ),
            'category_filter' => esc_html__( 'Category Filter', 'rusnet-interactive-map' ),
            'markers'         => esc_html__( 'Markers', 'rusnet-interactive-map' ),
            'info'            => esc_html__( 'Info', 'rusnet-interactive-map' ),
        );
        $tabs = apply_filters( 'rusnetim_settings_tabs', $tabs );
    
        $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
    
        $defaults = Rusnetim_Options::get_all();
        ?>
        <div class="wrap">
            <h1><?php esc_html_e( 'Rusnet Interactive Map', 'rusnet-interactive-map' ); ?></h1>
    
            <h2 class="nav-tab-wrapper">
                <?php foreach ( $tabs as $tab_key => $tab_label ) : ?>
                    <a href="?page=<?php echo esc_attr( $this->page_hook ); ?>&tab=<?php echo esc_attr( $tab_key ); ?>" 
                       class="nav-tab <?php echo $active_tab === $tab_key ? 'nav-tab-active' : ''; ?>">
                        <?php echo esc_html( $tab_label ); ?>
                    </a>
                <?php endforeach; ?>
            </h2>
    
            <?php settings_errors(); ?>
    
            <?php
            if ( array_key_exists( $active_tab, $tabs ) ) {
                switch ( $active_tab ) {
                    case 'general':
                        ?>
                        <form method="post" id="RusnetimOptions" enctype="multipart/form-data" action="options.php">
                            <?php wp_nonce_field( 'rusnetim_options_verify', 'rusnetim_options_nonce' ); ?>
                            <div id="yamap" style="position: relative; min-height: 15rem; margin-bottom: 1rem;"></div><br />
                            <?php
                            settings_fields( 'rusnetim_options' );
                            do_settings_sections( $this->page_hook . '_general' );
                            submit_button();
                            ?>
                        </form>
                        <?php
                        break;
                    case 'clustering':
                        ?>
                        <form method="post" action="options.php">
                            <?php
                            settings_fields( 'rusnetim_options' );
                            do_settings_sections( $this->page_hook . '_clustering' );
                            submit_button();
                            ?>
                        </form>
                        <?php
                        break;
                    case 'infopanel':
                        ?>
                        <form method="post" action="options.php">
                            <?php
                            settings_fields( 'rusnetim_options' );
                            do_settings_sections( $this->page_hook . '_infopanel' );
                            submit_button();
                            ?>
                        </form>
                        <?php
                        break;
                    case 'category_filter':
                        ?>
                        <form method="post" action="options.php">
                            <?php
                            settings_fields( 'rusnetim_options' );
                            do_settings_sections( $this->page_hook . '_category_filter' );
                            submit_button();
                            ?>
                        </form>
                        <?php
                        break;
                    case 'markers':
                        ?>
                        <form method="post" action="options.php">
                            <?php
                            settings_fields( 'rusnetim_options' );
                            do_settings_sections( $this->page_hook . '_markers' );
                            submit_button();
                            ?>
                        </form>
                        <?php
                        break;
                    case 'info':
                        ?>
                        <div class="thanks" style="border: 4px #ffdb4d solid; background: #fff; padding: 0 1rem 2rem 1rem; display: flex; flex-wrap: wrap; gap: 20px;">
                            <div style="flex:1; min-width: 280px;">
                                <?php
                                echo wp_kses_post(
                                    __(
                                        '<div style="position: relative; display: block; width: 100%; white-space: normal !important;">
                                            <h2 style="color: #444;font-size: 18px;font-weight: 600;line-height: 36px;">Need other icon types?</h2>
                                            Additional icon types can be found in the 
                                            <a href="https://tech.yandex.com/maps/doc/jsapi/2.1/ref/reference/option.presetStorage-docpage/" style="white-space: normal">Yandex.Maps documentation</a>.
                                        </div>
                                        <div style="position: relative; display: block; width: 100%; white-space: normal !important; margin-top: 20px;">
                                            <h2 style="color: #444;font-size: 18px;font-weight: 600;line-height: 36px;">Like the Rusnet Interactive Map plugin?</h2>
                                            You can support its development by making a donation 
                                            (<a href="https://rusnet.su/donate" style="white-space: normal">on the ANO "Rusnet" website</a>) 
                                            or leave a positive review in the 
                                            <a href="https://wordpress.org/support/plugin/rusnet-interactive-map/reviews/" style="white-space: normal">plugin repository</a>. 
                                            It\'s very motivating!
                                        </div>
                                        <div style="position: relative; display: block; width: 100%; white-space: normal !important; margin-top: 20px;">
                                            <h2 style="color: #444;font-size: 18px;font-weight: 600;line-height: 36px;">Any questions?</h2>
                                            Ask them on the 
                                            <a href="https://rusnet.su" style="white-space: normal">ANO "Rusnet" website</a> 
                                            or in the 
                                            <a href="https://wordpress.org/support/plugin/rusnet-interactive-map" style="white-space: normal">WordPress support forum</a>.
                                        </div>',
                                        'rusnet-interactive-map'
                                    )
                                );
                                ?>
                            </div>
                            <div style="flex:1; min-width: 280px;">
                                <?php
                                echo wp_kses_post(
                                    __(
                                        '<div style="position: relative; display: block; width: 100%; white-space: normal !important;">
                                            <h2 style="color: #444;font-size: 18px;font-weight: 600;line-height: 36px;">Support and development</h2>
                                            The Rusnet Interactive Map plugin is now maintained and developed by the ANO "Rusnet" team.<br>
                                            We are open to cooperation and custom modifications for your projects.
                                            <p style="margin-top: .5rem; text-align: center;">
                                                <b>Contacts:</b> 
                                                <a href="mailto:info@rusnet.su">info@rusnet.su</a>, 
                                                <a href="https://rusnet.su">https://rusnet.su</a>
                                            </p>
                                        </div>',
                                        'rusnet-interactive-map'
                                    )
                                );
                                ?>
                            </div>
                        </div>
                        <?php
                        break;
                    default:
                        do_action( 'rusnetim_admin_tab_content', $active_tab );
                        break;
                }
            } else {
                do_action( 'rusnetim_admin_tab_content', $active_tab );
            }
            ?>
        </div>
        <?php
    }
}

function rusnetim_get_category_filter_presets() {
    $presets = array(
        'default' => array(
            'label'  => esc_html__( 'Default', 'rusnet-interactive-map' ),
            'fields' => array(
                'cat_filter_button_font_size'      => '',
                'cat_filter_button_color'          => '',
                'cat_filter_button_bg_color'       => '',
                'cat_filter_button_border'         => '',
                'cat_filter_button_box_shadow'     => '',
                'cat_filter_button_padding'        => '6px 12px',
                'cat_filter_button_border_radius'  => '4px',
                'cat_filter_button_active_color'   => '',
                'cat_filter_button_active_bg_color' => '',
                'cat_filter_button_active_border'  => '',
            ),
        ),
        'modern' => array(
            'label'  => esc_html__( 'Modern', 'rusnet-interactive-map' ),
            'fields' => array(
                'cat_filter_button_font_size'      => '14px',
                'cat_filter_button_color'          => '#333333',
                'cat_filter_button_bg_color'       => '#f5f5f5',
                'cat_filter_button_border'         => '1px solid #cccccc',
                'cat_filter_button_box_shadow'     => '0 2px 4px rgba(0,0,0,0.1)',
                'cat_filter_button_padding'        => '8px 16px',
                'cat_filter_button_border_radius'  => '20px',
                'cat_filter_button_active_color'   => '#ffffff',
                'cat_filter_button_active_bg_color' => '#0073aa',
                'cat_filter_button_active_border'  => '1px solid #0073aa',
            ),
        ),
        'minimal' => array(
            'label'  => esc_html__( 'Minimal', 'rusnet-interactive-map' ),
            'fields' => array(
                'cat_filter_button_font_size'      => '13px',
                'cat_filter_button_color'          => '#555555',
                'cat_filter_button_bg_color'       => 'transparent',
                'cat_filter_button_border'         => 'none',
                'cat_filter_button_box_shadow'     => 'none',
                'cat_filter_button_padding'        => '4px 8px',
                'cat_filter_button_border_radius'  => '0',
                'cat_filter_button_active_color'   => '#000000',
                'cat_filter_button_active_bg_color' => '#e0e0e0',
                'cat_filter_button_active_border'  => 'none',
            ),
        ),
    );

    return apply_filters( 'rusnetim_category_filter_presets', $presets );
}