Plugin Directory

Changeset 3447021


Ignore:
Timestamp:
01/26/2026 10:42:45 AM (2 months ago)
Author:
ailab
Message:

feat: 新增 Hook 腳本

Location:
readmo-ai
Files:
14 added
14 edited
28 copied

Legend:

Unmodified
Added
Removed
  • readmo-ai/tags/1.2.0/Controller/admin/class-readmo-ai-admin-settings.php

    r3417155 r3447021  
    111111        // Register AJAX handlers.
    112112        add_action( 'wp_ajax_readmo_ai_save_settings', array( $this, 'ajax_save_settings' ) );
     113        add_action( 'wp_ajax_readmo_ai_save_auto_insert_settings', array( $this, 'ajax_save_auto_insert_settings' ) );
     114        add_action( 'wp_ajax_readmo_ai_delete_auto_insert_settings', array( $this, 'ajax_delete_auto_insert_settings' ) );
    113115    }
    114116
     
    290292        // Prepare view data.
    291293        $view_data = array(
    292             'api_key' => $this->settings_dao->get_api_key(),
     294            'api_key'              => $this->settings_dao->get_api_key(),
     295            'auto_insert_settings' => $this->settings_dao->get_auto_insert_settings(),
     296            'content_tree'         => $this->build_content_tree(),
    293297        );
    294298
     
    393397        }
    394398    }
     399
     400    /**
     401     * AJAX handler to save auto-insert settings
     402     *
     403     * Handles AJAX requests to save auto-insert configuration.
     404     *
     405     * @since 1.2.0
     406     * @return void
     407     */
     408    public function ajax_save_auto_insert_settings() {
     409        // Verify nonce.
     410        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'readmo_ai_admin_nonce' ) ) {
     411            wp_send_json_error(
     412                array(
     413                    'message' => __( 'Security check failed', 'readmo-ai' ),
     414                )
     415            );
     416        }
     417
     418        // Check user capabilities.
     419        if ( ! current_user_can( 'manage_options' ) ) {
     420            wp_send_json_error(
     421                array(
     422                    'message' => __( 'Insufficient permissions', 'readmo-ai' ),
     423                )
     424            );
     425        }
     426
     427        // Sanitize and validate settings.
     428        $auto_insert_settings = $this->sanitize_auto_insert_settings( $_POST );
     429
     430        // Save settings.
     431        $saved = $this->settings_dao->save_auto_insert_settings( $auto_insert_settings );
     432
     433        // Read back to verify.
     434        $verified_settings = $this->settings_dao->get_auto_insert_settings();
     435
     436        if ( $saved ) {
     437            wp_send_json_success(
     438                array(
     439                    'message'          => __( 'Auto-insert settings saved successfully', 'readmo-ai' ),
     440                    'saved_settings'   => $auto_insert_settings,
     441                    'verified_settings' => $verified_settings,
     442                )
     443            );
     444        } else {
     445            wp_send_json_error(
     446                array(
     447                    'message'          => __( 'Failed to save auto-insert settings', 'readmo-ai' ),
     448                    'attempted_settings' => $auto_insert_settings,
     449                )
     450            );
     451        }
     452    }
     453
     454    /**
     455     * AJAX handler to delete auto-insert settings
     456     *
     457     * Handles AJAX requests to remove auto-insert configuration.
     458     *
     459     * @since 1.2.0
     460     * @return void
     461     */
     462    public function ajax_delete_auto_insert_settings() {
     463        // Verify nonce.
     464        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'readmo_ai_admin_nonce' ) ) {
     465            wp_send_json_error(
     466                array(
     467                    'message' => __( 'Security check failed', 'readmo-ai' ),
     468                )
     469            );
     470        }
     471
     472        // Check user capabilities.
     473        if ( ! current_user_can( 'manage_options' ) ) {
     474            wp_send_json_error(
     475                array(
     476                    'message' => __( 'Insufficient permissions', 'readmo-ai' ),
     477                )
     478            );
     479        }
     480
     481        // Delete settings.
     482        $deleted = $this->settings_dao->delete_auto_insert_settings();
     483
     484        if ( $deleted ) {
     485            wp_send_json_success(
     486                array(
     487                    'message' => __( 'Auto-insert settings removed successfully', 'readmo-ai' ),
     488                )
     489            );
     490        } else {
     491            wp_send_json_error(
     492                array(
     493                    'message' => __( 'Failed to remove auto-insert settings', 'readmo-ai' ),
     494                )
     495            );
     496        }
     497    }
     498
     499    /**
     500     * Sanitize auto-insert settings
     501     *
     502     * Validates and sanitizes auto-insert settings from POST data.
     503     * Uses "excluded" storage model: stores only unchecked items.
     504     *
     505     * @since 1.2.0
     506     * @param array $post_data The POST data to sanitize.
     507     * @return array Sanitized settings.
     508     */
     509    private function sanitize_auto_insert_settings( $post_data ) {
     510        $settings = array();
     511
     512        // Enabled flag.
     513        $settings['enabled'] = ! empty( $post_data['enabled'] );
     514
     515        // Position.
     516        $allowed_positions    = array( 'before_content', 'after_content', 'footer' );
     517        $settings['position'] = 'after_content';
     518        if ( isset( $post_data['position'] ) && in_array( $post_data['position'], $allowed_positions, true ) ) {
     519            $settings['position'] = sanitize_text_field( $post_data['position'] );
     520        }
     521
     522        // Excluded post types (array of post type names).
     523        $settings['excluded_post_types'] = array();
     524        if ( isset( $post_data['excluded_post_types'] ) && is_array( $post_data['excluded_post_types'] ) ) {
     525            $settings['excluded_post_types'] = array_map( 'sanitize_key', $post_data['excluded_post_types'] );
     526            $settings['excluded_post_types'] = array_filter( $settings['excluded_post_types'] );
     527        }
     528
     529        // Excluded categories (array of IDs).
     530        $settings['excluded_categories'] = array();
     531        if ( isset( $post_data['excluded_categories'] ) && is_array( $post_data['excluded_categories'] ) ) {
     532            $settings['excluded_categories'] = array_map( 'absint', $post_data['excluded_categories'] );
     533            $settings['excluded_categories'] = array_filter( $settings['excluded_categories'] );
     534        }
     535
     536        // Excluded posts/pages (array of IDs).
     537        $settings['excluded_posts'] = array();
     538        if ( isset( $post_data['excluded_posts'] ) && is_array( $post_data['excluded_posts'] ) ) {
     539            $settings['excluded_posts'] = array_map( 'absint', $post_data['excluded_posts'] );
     540            $settings['excluded_posts'] = array_filter( $settings['excluded_posts'] );
     541        }
     542
     543        return $settings;
     544    }
     545
     546    /**
     547     * Get auto-insert settings
     548     *
     549     * Retrieves the auto-insert configuration.
     550     *
     551     * @since 1.2.0
     552     * @return array Auto-insert settings.
     553     */
     554    public function get_auto_insert_settings() {
     555        return $this->settings_dao->get_auto_insert_settings();
     556    }
     557
     558    /**
     559     * Build content tree structure
     560     *
     561     * Builds hierarchical tree data for the tree selector UI.
     562     * Structure: Post Type → Category → Post/Page
     563     *
     564     * @since 1.2.0
     565     * @return array Content tree structure.
     566     */
     567    private function build_content_tree() {
     568        $tree = array();
     569
     570        // Get public post types.
     571        $post_types = get_post_types(
     572            array(
     573                'public' => true,
     574            ),
     575            'objects'
     576        );
     577
     578        // Remove attachment post type.
     579        unset( $post_types['attachment'] );
     580
     581        foreach ( $post_types as $post_type ) {
     582            $type_data = array(
     583                'name'  => $post_type->name,
     584                'label' => $post_type->labels->name,
     585            );
     586
     587            // For 'post' type, include categories as children.
     588            if ( 'post' === $post_type->name ) {
     589                $type_data['categories'] = $this->get_categories_with_posts();
     590            } else {
     591                // For other types (page, custom), list posts directly.
     592                $type_data['posts'] = $this->get_posts_by_type( $post_type->name );
     593            }
     594
     595            $tree[ $post_type->name ] = $type_data;
     596        }
     597
     598        return $tree;
     599    }
     600
     601    /**
     602     * Get categories with their posts
     603     *
     604     * Retrieves all categories with their associated posts.
     605     *
     606     * @since 1.2.0
     607     * @return array Categories with posts.
     608     */
     609    private function get_categories_with_posts() {
     610        $categories_data = array();
     611
     612        $categories = get_categories(
     613            array(
     614                'hide_empty' => false,
     615                'orderby'    => 'name',
     616                'order'      => 'ASC',
     617            )
     618        );
     619
     620        foreach ( $categories as $category ) {
     621            $posts = get_posts(
     622                array(
     623                    'post_type'      => 'post',
     624                    'post_status'    => 'publish',
     625                    'category'       => $category->term_id,
     626                    'posts_per_page' => 100,
     627                    'orderby'        => 'title',
     628                    'order'          => 'ASC',
     629                )
     630            );
     631
     632            $posts_data = array();
     633            foreach ( $posts as $post ) {
     634                $posts_data[ $post->ID ] = $post->post_title;
     635            }
     636
     637            $categories_data[ $category->term_id ] = array(
     638                'name'  => $category->name,
     639                'count' => $category->count,
     640                'posts' => $posts_data,
     641            );
     642        }
     643
     644        return $categories_data;
     645    }
     646
     647    /**
     648     * Get posts by type
     649     *
     650     * Retrieves all posts of a specific post type.
     651     *
     652     * @since 1.2.0
     653     * @param string $post_type The post type to retrieve.
     654     * @return array Posts data (ID => title).
     655     */
     656    private function get_posts_by_type( $post_type ) {
     657        $posts_data = array();
     658
     659        $posts = get_posts(
     660            array(
     661                'post_type'      => $post_type,
     662                'post_status'    => 'publish',
     663                'posts_per_page' => 100,
     664                'orderby'        => 'title',
     665                'order'          => 'ASC',
     666            )
     667        );
     668
     669        foreach ( $posts as $post ) {
     670            $posts_data[ $post->ID ] = $post->post_title;
     671        }
     672
     673        return $posts_data;
     674    }
    395675}
  • readmo-ai/tags/1.2.0/Controller/frontend/class-readmo-ai-frontend-assets.php

    r3417155 r3447021  
    2727 */
    2828class Readmo_Ai_Frontend_Assets {
     29
     30    /**
     31     * Flag to track if Readmo AI content is present on the page.
     32     * Used for conditional loading of tracking script.
     33     *
     34     * @since 1.2.0
     35     * @var bool
     36     */
     37    private static $has_readmo_content = false;
    2938
    3039    /**
     
    5968    protected function register_hooks() {
    6069        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
     70        // Conditionally enqueue tracking script in footer (after content is rendered).
     71        add_action( 'wp_footer', array( $this, 'maybe_enqueue_tracking_script' ), 5 );
     72    }
     73
     74    /**
     75     * Mark that Readmo AI content is present on the page.
     76     *
     77     * Called by shortcode/block renderers to enable tracking script loading.
     78     *
     79     * @since 1.2.0
     80     * @return void
     81     */
     82    public static function mark_has_content() {
     83        self::$has_readmo_content = true;
     84    }
     85
     86    /**
     87     * Check if Readmo AI content is present on the page.
     88     *
     89     * @since 1.2.0
     90     * @return bool True if content is present.
     91     */
     92    public static function has_content() {
     93        return self::$has_readmo_content;
    6194    }
    6295
     
    6497     * Enqueue frontend assets
    6598     *
    66      * Enqueues CSS and JavaScript files required for the shortcode functionality.
     99     * Registers CSS and JavaScript files required for the shortcode functionality.
     100     * Note: Scripts are conditionally loaded via maybe_enqueue_tracking_script()
     101     * to prevent tracking on pages without Readmo AI content.
    67102     *
    68103     * @since 1.0.0
     
    70105     */
    71106    public function enqueue_frontend_assets() {
    72         // Enqueue frontend CSS.
     107        // Enqueue frontend CSS (always needed for styling).
    73108        wp_enqueue_style(
    74109            'readmo-ai-frontend',
     
    79114        );
    80115
    81         // Enqueue jQuery (WordPress standard library).
    82         wp_enqueue_script( 'jquery' );
    83 
    84         // Enqueue tracking JavaScript.
    85         wp_enqueue_script(
     116        // Register tracking script (will be enqueued conditionally in footer).
     117        wp_register_script(
    86118            'readmo-ai-tracking',
    87119            READMO_AI_PLUGIN_URL . 'assets/js/tracking.js',
     
    91123        );
    92124
    93         // Enqueue polling JavaScript (depends on tracking for sendTrackingEvent).
    94         wp_enqueue_script(
     125        // Register polling JavaScript (will be enqueued conditionally in footer).
     126        wp_register_script(
    95127            'readmo-ai-polling',
    96128            READMO_AI_PLUGIN_URL . 'assets/js/polling.js',
     
    108140        }
    109141
    110         // Localize script with AJAX URL, settings, SVG, translations, and tracking nonce.
     142        // Localize script data (will be available when scripts are enqueued).
    111143        wp_localize_script(
    112144            'readmo-ai-polling',
     
    120152        );
    121153    }
     154
     155    /**
     156     * Conditionally enqueue scripts
     157     *
     158     * Only loads tracking.js and polling.js if Readmo AI content is present on the page.
     159     * This prevents unnecessary tracking on pages without Readmo AI content.
     160     *
     161     * @since 1.2.0
     162     * @return void
     163     */
     164    public function maybe_enqueue_tracking_script() {
     165        if ( self::$has_readmo_content ) {
     166            // Enqueue polling script (which depends on tracking script).
     167            // WordPress will automatically load tracking.js as a dependency.
     168            wp_enqueue_script( 'readmo-ai-polling' );
     169        }
     170    }
    122171}
  • readmo-ai/tags/1.2.0/Controller/frontend/class-readmo-ai-shortcode-handler.php

    r3417155 r3447021  
    9090     */
    9191    public function render_shortcode( $atts = array(), $content = '' ) {
    92         // Parse shortcode attributes.
    93         $atts = shortcode_atts(
    94             array(
    95                 'from' => '',
    96             ),
    97             $atts,
    98             'readmo_ai_articles'
    99         );
    100 
    101         // Determine source URL: use 'url' or 'from' parameter, or fallback to current page URL.
    102         if ( ! empty( $atts['from'] ) ) {
    103             $from = $atts['from'];
    104         } else {
    105             $from = $this->get_current_page_url();
    106         }
     92        // Always use current page URL as source.
     93        $from = $this->get_current_page_url();
    10794
    10895        // Generate unique container ID.
  • readmo-ai/tags/1.2.0/Infrastructure/dao/class-readmo-ai-settings-dao.php

    r3417155 r3447021  
    2525
    2626    /**
    27      * WordPress option name
     27     * WordPress option name for main settings
    2828     *
    2929     * @since 1.0.0
     
    3131     */
    3232    const OPTION_NAME = 'readmo_ai_settings';
     33
     34    /**
     35     * WordPress option name for auto-insert settings
     36     *
     37     * @since 1.2.0
     38     * @var string
     39     */
     40    const AUTO_INSERT_OPTION_NAME = 'readmo_ai_auto_insert';
    3341
    3442    /**
     
    99107        return delete_option( self::OPTION_NAME );
    100108    }
     109
     110    /**
     111     * Get auto-insert settings
     112     *
     113     * Retrieves the auto-insert configuration settings from dedicated option.
     114     * Uses "excluded" storage model: empty arrays mean all items are checked (apply to all).
     115     *
     116     * @since 1.2.0
     117     * @return array Auto-insert settings with defaults.
     118     */
     119    public function get_auto_insert_settings() {
     120        // Default: all empty arrays = all checked = apply to all content.
     121        $defaults = array(
     122            'enabled'              => false,
     123            'position'             => 'after_content',
     124            'excluded_post_types'  => array(),
     125            'excluded_categories'  => array(),
     126            'excluded_posts'       => array(),
     127        );
     128
     129        $settings = get_option( self::AUTO_INSERT_OPTION_NAME, array() );
     130
     131        if ( empty( $settings ) ) {
     132            return $defaults;
     133        }
     134
     135        $settings = wp_parse_args( $settings, $defaults );
     136
     137        // Ensure ID arrays are integers for strict comparison in should_insert().
     138        // WordPress get_option() may return serialized integers as strings.
     139        if ( ! empty( $settings['excluded_categories'] ) && is_array( $settings['excluded_categories'] ) ) {
     140            $settings['excluded_categories'] = array_map( 'intval', $settings['excluded_categories'] );
     141        }
     142        if ( ! empty( $settings['excluded_posts'] ) && is_array( $settings['excluded_posts'] ) ) {
     143            $settings['excluded_posts'] = array_map( 'intval', $settings['excluded_posts'] );
     144        }
     145
     146        return $settings;
     147    }
     148
     149    /**
     150     * Save auto-insert settings
     151     *
     152     * Saves the auto-insert configuration to dedicated WordPress option.
     153     *
     154     * @since 1.2.0
     155     * @param array $auto_insert_settings The auto-insert settings to save.
     156     * @return bool True on success, false on failure.
     157     */
     158    public function save_auto_insert_settings( $auto_insert_settings ) {
     159        // Use update_option with autoload = true for better performance.
     160        return update_option( self::AUTO_INSERT_OPTION_NAME, $auto_insert_settings, true );
     161    }
     162
     163    /**
     164     * Delete auto-insert settings
     165     *
     166     * Removes the auto-insert settings option entirely.
     167     *
     168     * @since 1.2.0
     169     * @return bool True on success, false on failure.
     170     */
     171    public function delete_auto_insert_settings() {
     172        return delete_option( self::AUTO_INSERT_OPTION_NAME );
     173    }
    101174}
  • readmo-ai/tags/1.2.0/View/admin/class-readmo-ai-admin-settings-view.php

    r3417155 r3447021  
    3434     */
    3535    public function render( $data ) {
    36         $api_key = isset( $data['api_key'] ) ? $data['api_key'] : '';
     36        $api_key              = isset( $data['api_key'] ) ? $data['api_key'] : '';
     37        $auto_insert_settings = isset( $data['auto_insert_settings'] ) ? $data['auto_insert_settings'] : array();
     38        $content_tree         = isset( $data['content_tree'] ) ? $data['content_tree'] : array();
     39
     40        // Default values for auto-insert settings.
     41        // Using "excluded" storage: empty arrays = all checked (apply to all).
     42        $ai_enabled             = ! empty( $auto_insert_settings['enabled'] );
     43        $ai_position            = isset( $auto_insert_settings['position'] ) ? $auto_insert_settings['position'] : 'after_content';
     44        $ai_excluded_post_types = isset( $auto_insert_settings['excluded_post_types'] ) ? $auto_insert_settings['excluded_post_types'] : array();
     45        $ai_excluded_categories = isset( $auto_insert_settings['excluded_categories'] ) ? $auto_insert_settings['excluded_categories'] : array();
     46        $ai_excluded_posts      = isset( $auto_insert_settings['excluded_posts'] ) ? $auto_insert_settings['excluded_posts'] : array();
    3747
    3848        ?>
     
    7383
    7484                <div class="readmo-ai-action">
    75                     <button type="button" class="btn btn-tertiary">
     85                    <button type="button" id="readmo-ai-save-api-key" class="btn btn-ban">
    7686                        <?php echo esc_html( __( 'Save Changes', 'readmo-ai' ) ); ?>
    7787                    </button>
     
    7989            </form>
    8090
     91
     92            <!-- Auto-Insert Settings Panel -->
     93            <div class="readmo-ai-panel">
     94                <div class="readmo-ai-field">
     95                    <span class="readmo-ai-title"><?php echo esc_html( __( 'Auto-Insert Settings', 'readmo-ai' ) ); ?></span>
     96
     97                    <!-- Enable Toggle -->
     98                    <div class="readmo-ai-setting-row">
     99                        <label class="readmo-ai-label">
     100                            <span class="readmo-ai-text"><?php echo esc_html( __( 'Enable auto-insert', 'readmo-ai' ) ); ?></span>
     101                            <span class="readmo-ai-comment"><?php echo esc_html( __( 'Automatically display Readmo AI on all pages matching the criteria below.', 'readmo-ai' ) ); ?></span>
     102                        </label>
     103                        <div class="readmo-ai-toggle">
     104                            <input
     105                                type="checkbox"
     106                                id="readmo-ai-auto-insert-enabled"
     107                                name="auto_insert_enabled"
     108                                value="1"
     109                                <?php checked( $ai_enabled ); ?>
     110                            />
     111                            <label for="readmo-ai-auto-insert-enabled" class="readmo-ai-toggle-slider"></label>
     112                        </div>
     113                    </div>
     114
     115                    <!-- Insert Position -->
     116                    <div class="readmo-ai-setting-column">
     117                        <label class="readmo-ai-label">
     118                            <span class="readmo-ai-text"><?php echo esc_html( __( 'Insert Position', 'readmo-ai' ) ); ?></span>
     119                        </label>
     120                        <div class="readmo-ai-radio-group">
     121                            <label class="readmo-ai-radio-label">
     122                                <input
     123                                    type="radio"
     124                                    name="auto_insert_position"
     125                                    value="after_content"
     126                                    <?php checked( $ai_position, 'after_content' ); ?>
     127                                />
     128                                <span class="readmo-ai-radio-dot"></span>
     129                                <span class="readmo-ai-radio-text"><?php echo esc_html( __( 'After content', 'readmo-ai' ) ); ?></span>
     130                            </label>
     131                            <label class="readmo-ai-radio-label">
     132                                <input
     133                                    type="radio"
     134                                    name="auto_insert_position"
     135                                    value="footer"
     136                                    <?php checked( $ai_position, 'footer' ); ?>
     137                                />
     138                                <span class="readmo-ai-radio-dot"></span>
     139                                <span class="readmo-ai-radio-text"><?php echo esc_html( __( 'Page footer', 'readmo-ai' ) ); ?></span>
     140                            </label>
     141                        </div>
     142                    </div>
     143
     144                    <!-- Content Tree Selector -->
     145                    <div class="readmo-ai-setting-column">
     146                        <label class="readmo-ai-label">
     147                            <span class="readmo-ai-text"><?php echo esc_html( __( 'Apply to Content', 'readmo-ai' ) ); ?></span>
     148                            <span class="readmo-ai-comment"><?php echo esc_html( __( 'Check items to display Readmo AI. Unchecked items will not show Readmo AI.', 'readmo-ai' ) ); ?></span>
     149                        </label>
     150                        <div class="readmo-ai-content-tree" id="readmo-ai-content-tree">
     151                            <?php foreach ( $content_tree as $type_name => $type_data ) : ?>
     152                                <?php
     153                                $type_excluded = in_array( $type_name, $ai_excluded_post_types, true );
     154                                $type_checked  = ! $type_excluded;
     155                                ?>
     156                                <div class="readmo-ai-tree-node" data-type="post-type" data-value="<?php echo esc_attr( $type_name ); ?>">
     157                                    <div class="readmo-ai-tree-item">
     158                                        <span class="readmo-ai-tree-toggle">
     159                                            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
     160                                                <path d="M6 4l4 4-4 4V4z"/>
     161                                            </svg>
     162                                        </span>
     163                                        <label class="readmo-ai-tree-checkbox">
     164                                            <input
     165                                                type="checkbox"
     166                                                class="readmo-ai-tree-input"
     167                                                data-type="post-type"
     168                                                data-value="<?php echo esc_attr( $type_name ); ?>"
     169                                                <?php checked( $type_checked ); ?>
     170                                            />
     171                                            <span class="readmo-ai-tree-label"><?php echo esc_html( $type_data['label'] ); ?></span>
     172                                        </label>
     173                                    </div>
     174                                    <div class="readmo-ai-tree-children">
     175                                        <?php if ( isset( $type_data['categories'] ) ) : ?>
     176                                            <!-- Post type with categories -->
     177                                            <?php foreach ( $type_data['categories'] as $cat_id => $cat_data ) : ?>
     178                                                <?php
     179                                                $cat_excluded = in_array( (int) $cat_id, $ai_excluded_categories, true );
     180                                                $cat_checked  = ! $cat_excluded && $type_checked;
     181                                                ?>
     182                                                <div class="readmo-ai-tree-node" data-type="category" data-value="<?php echo esc_attr( $cat_id ); ?>">
     183                                                    <div class="readmo-ai-tree-item">
     184                                                        <span class="readmo-ai-tree-toggle">
     185                                                            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
     186                                                                <path d="M6 4l4 4-4 4V4z"/>
     187                                                            </svg>
     188                                                        </span>
     189                                                        <label class="readmo-ai-tree-checkbox">
     190                                                            <input
     191                                                                type="checkbox"
     192                                                                class="readmo-ai-tree-input"
     193                                                                data-type="category"
     194                                                                data-value="<?php echo esc_attr( $cat_id ); ?>"
     195                                                                <?php checked( $cat_checked ); ?>
     196                                                            />
     197                                                            <span class="readmo-ai-tree-label">
     198                                                                <?php echo esc_html( $cat_data['name'] ); ?>
     199                                                                <small>(<?php echo esc_html( $cat_data['count'] ); ?> <?php echo esc_html( __( 'posts', 'readmo-ai' ) ); ?>)</small>
     200                                                            </span>
     201                                                        </label>
     202                                                    </div>
     203                                                    <div class="readmo-ai-tree-children">
     204                                                        <?php foreach ( $cat_data['posts'] as $post_id => $post_title ) : ?>
     205                                                            <?php
     206                                                            $post_excluded = in_array( (int) $post_id, $ai_excluded_posts, true );
     207                                                            $post_checked  = ! $post_excluded && $cat_checked;
     208                                                            ?>
     209                                                            <div class="readmo-ai-tree-node" data-type="post" data-value="<?php echo esc_attr( $post_id ); ?>">
     210                                                                <div class="readmo-ai-tree-item readmo-ai-tree-leaf">
     211                                                                    <label class="readmo-ai-tree-checkbox">
     212                                                                        <input
     213                                                                            type="checkbox"
     214                                                                            class="readmo-ai-tree-input"
     215                                                                            data-type="post"
     216                                                                            data-value="<?php echo esc_attr( $post_id ); ?>"
     217                                                                            <?php checked( $post_checked ); ?>
     218                                                                        />
     219                                                                        <span class="readmo-ai-tree-label"><?php echo esc_html( $post_title ); ?></span>
     220                                                                    </label>
     221                                                                </div>
     222                                                            </div>
     223                                                        <?php endforeach; ?>
     224                                                    </div>
     225                                                </div>
     226                                            <?php endforeach; ?>
     227                                        <?php elseif ( isset( $type_data['posts'] ) ) : ?>
     228                                            <!-- Post type without categories (e.g., pages) -->
     229                                            <?php foreach ( $type_data['posts'] as $post_id => $post_title ) : ?>
     230                                                <?php
     231                                                $post_excluded = in_array( (int) $post_id, $ai_excluded_posts, true );
     232                                                $post_checked  = ! $post_excluded && $type_checked;
     233                                                ?>
     234                                                <div class="readmo-ai-tree-node" data-type="post" data-value="<?php echo esc_attr( $post_id ); ?>">
     235                                                    <div class="readmo-ai-tree-item readmo-ai-tree-leaf">
     236                                                        <label class="readmo-ai-tree-checkbox">
     237                                                            <input
     238                                                                type="checkbox"
     239                                                                class="readmo-ai-tree-input"
     240                                                                data-type="post"
     241                                                                data-value="<?php echo esc_attr( $post_id ); ?>"
     242                                                                <?php checked( $post_checked ); ?>
     243                                                            />
     244                                                            <span class="readmo-ai-tree-label"><?php echo esc_html( $post_title ); ?></span>
     245                                                        </label>
     246                                                    </div>
     247                                                </div>
     248                                            <?php endforeach; ?>
     249                                        <?php endif; ?>
     250                                    </div>
     251                                </div>
     252                            <?php endforeach; ?>
     253                        </div>
     254                    </div>
     255                </div>
     256
     257                <div class="readmo-ai-action readmo-ai-auto-insert-actions">
     258                    <button type="button" id="readmo-ai-save-auto-insert" class="btn btn-ban">
     259                        <?php echo esc_html( __( 'Save Settings', 'readmo-ai' ) ); ?>
     260                    </button>
     261                    <button type="button" id="readmo-ai-remove-auto-insert" class="btn btn-danger">
     262                        <?php echo esc_html( __( 'Disable and Remove', 'readmo-ai' ) ); ?>
     263                    </button>
     264                </div>
     265            </div>
     266
     267            <!-- Remove Confirmation Modal -->
     268            <div id="readmo-ai-confirm-modal" class="readmo-ai-modal" style="display: none;">
     269                <div class="readmo-ai-modal-overlay"></div>
     270                <div class="readmo-ai-modal-content">
     271                    <span class="readmo-ai-title"><?php echo esc_html( __( 'Confirm Remove', 'readmo-ai' ) ); ?></span>
     272                    <span class="readmo-ai-text"><?php echo esc_html( __( 'Are you sure you want to disable and remove all auto-insert settings? This action cannot be undone.', 'readmo-ai' ) ); ?></span>
     273                    <div class="readmo-ai-modal-actions">
     274                        <button type="button" id="readmo-ai-modal-cancel" class="btn btn-tertiary">
     275                            <?php echo esc_html( __( 'Cancel', 'readmo-ai' ) ); ?>
     276                        </button>
     277                        <button type="button" id="readmo-ai-modal-confirm" class="btn btn-danger">
     278                            <?php echo esc_html( __( 'Confirm Remove', 'readmo-ai' ) ); ?>
     279                        </button>
     280                    </div>
     281                </div>
     282            </div>
    81283
    82284            <div class="readmo-ai-help-info">
  • readmo-ai/tags/1.2.0/View/frontend/class-readmo-ai-shortcode-view.php

    r3411004 r3447021  
    3838        $from         = isset( $data['from'] ) ? esc_js( $data['from'] ) : '';
    3939
     40        // Mark that Readmo AI content is present for conditional tracking script loading.
     41        if ( class_exists( 'Readmo_Ai_Frontend_Assets' ) ) {
     42            Readmo_Ai_Frontend_Assets::mark_has_content();
     43        }
     44
    4045        ob_start();
    4146        ?>
  • readmo-ai/tags/1.2.0/assets/css/admin.css

    r3411004 r3447021  
    1616    flex-direction: column;
    1717    gap: 12px;
    18     margin: 32px 0;
    1918    font-family: Noto Sans TC, PingFang TC, Arial, Helvetica, LiHei Pro, Microsoft JhengHei, MingLiU, sans-serif;
    2019}
     
    6463    display: flex;
    6564    flex-direction: column;
    66     gap: 20px;
     65    gap: 28px;
    6766    padding: 32px;
    6867    border-bottom: 1px solid rgba(221, 221, 221, 0.5);
     
    149148    font-weight: 500;
    150149    font-size: 16px;
    151     line-height: 100%;
     150    line-height: 1.5;
    152151    vertical-align: middle;
    153152    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     
    174173
    175174.btn-secondary {
    176     width: 200px;
    177175    background: rgba(0, 134, 168, 1);
    178176    color: #ffffff;
     
    182180
    183181.btn-tertiary {
    184     width: 200px;
    185     background: var(--UI-Disable, rgba(221, 221, 221, 0.2));
    186     color: var(--Text-Neutral-100, rgba(221, 221, 221, 1));
     182    background: var(--UI-Disable, rgba(221, 221, 221, 0.465));
     183    color: var(--Text-Neutral-100, rgb(154, 154, 154));
    187184    border: none;
    188185    padding: 16px 20px;
     186}
     187
     188/* Danger Button */
     189.btn-danger {
     190    background: rgb(235, 67, 67);
     191    color: #ffffff;
     192    border: none;
     193    padding: 16px 20px;
     194}
     195
     196.btn-danger:hover {
     197    background: rgb(220, 38, 38);
     198}
     199
     200/* Ban Button (Disabled State) */
     201.btn-ban,
     202.btn:disabled {
     203    background: rgb(246, 246, 246);
     204    color: var(--Text-Neutral-200, rgb(220, 220, 220));
     205    border: none;
     206    padding: 16px 20px;
     207    cursor: not-allowed;
     208    pointer-events: none;
    189209}
    190210
     
    241261}
    242262
     263/* Auto-Insert Settings Panel */
     264.readmo-ai-setting-row {
     265    display: flex;
     266    justify-content: space-between;
     267    align-items: center;
     268    gap: 2px;
     269}
     270
     271.readmo-ai-setting-column {
     272    display: flex;
     273    flex-direction: column;
     274    gap: 12px;
     275}
     276
     277.readmo-ai-comment {
     278    font-size: 14px;
     279    line-height: 1.5;
     280    color: var(--Text-Neutral-300, rgb(155, 155, 155));
     281}
     282
     283/* Text Input Styles */
     284.readmo-ai-text-input {
     285    width: 100%;
     286    padding: 10px 14px;
     287    font-size: 14px;
     288    border: 1px solid rgba(221, 221, 221, 1);
     289    border-radius: 8px;
     290    background: var(--Text-White, rgba(255, 255, 255, 1));
     291    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     292    transition: border-color 0.2s ease;
     293}
     294
     295.readmo-ai-text-input:focus {
     296    outline: none;
     297    border-color: rgba(0, 134, 168, 1);
     298    box-shadow: 0 0 0 2px rgba(0, 134, 168, 0.1);
     299}
     300
     301.readmo-ai-text-input::placeholder {
     302    color: var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     303}
     304
     305/* Toggle Switch Styles */
     306.readmo-ai-label {
     307    display: flex;
     308    flex-direction: column;
     309    gap: 2px;
     310}
     311
     312.readmo-ai-toggle {
     313    position: relative;
     314    width: 44px;
     315    height: 24px;
     316    flex-shrink: 0;
     317}
     318
     319.readmo-ai-toggle input {
     320    opacity: 0;
     321    width: 0;
     322    height: 0;
     323}
     324
     325.readmo-ai-toggle-slider {
     326    position: absolute;
     327    cursor: pointer;
     328    top: 0;
     329    left: 0;
     330    right: 0;
     331    bottom: 0;
     332    background-color: var(--UI-Disable, rgba(221, 221, 221, 1));
     333    transition: 0.3s ease;
     334    border-radius: 24px;
     335}
     336
     337.readmo-ai-toggle-slider::before {
     338    position: absolute;
     339    content: "";
     340    height: 18px;
     341    width: 18px;
     342    left: 3px;
     343    bottom: 3px;
     344    background-color: white;
     345    transition: 0.3s ease;
     346    border-radius: 50%;
     347    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
     348}
     349
     350.readmo-ai-toggle input:checked + .readmo-ai-toggle-slider {
     351    background-color: rgba(0, 134, 168, 1);
     352}
     353
     354.readmo-ai-toggle input:checked + .readmo-ai-toggle-slider::before {
     355    transform: translateX(20px);
     356}
     357
     358.readmo-ai-toggle input:focus + .readmo-ai-toggle-slider {
     359    box-shadow: 0 0 0 2px rgba(0, 134, 168, 0.2);
     360}
     361
     362/* Checkbox and Radio Styles */
     363.readmo-ai-checkbox-group,
     364.readmo-ai-radio-group {
     365    display: flex;
     366    flex-wrap: wrap;
     367    gap: 16px;
     368}
     369
     370.readmo-ai-checkbox-label,
     371.readmo-ai-radio-label {
     372    display: flex;
     373    align-items: center;
     374    gap: 8px;
     375    cursor: pointer;
     376    font-size: 14px;
     377    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     378}
     379
     380.readmo-ai-checkbox-label input[type="checkbox"] {
     381    width: 18px;
     382    height: 18px;
     383    cursor: pointer;
     384    accent-color: rgba(0, 134, 168, 1);
     385}
     386
     387/* Custom Radio Button */
     388.readmo-ai-radio-label input[type="radio"] {
     389    position: absolute;
     390    opacity: 0;
     391    width: 0;
     392    height: 0;
     393}
     394
     395.readmo-ai-radio-dot {
     396    display: flex;
     397    align-items: center;
     398    justify-content: center;
     399    width: 18px;
     400    height: 18px;
     401    flex-shrink: 0;
     402    border: 2px solid var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     403    border-radius: 50%;
     404    background: #ffffff;
     405    transition: border-color 0.2s ease;
     406}
     407
     408.readmo-ai-radio-dot::after {
     409    content: "";
     410    width: 10px;
     411    height: 10px;
     412    border-radius: 50%;
     413    background: transparent;
     414    transition: background-color 0.2s ease;
     415}
     416
     417.readmo-ai-radio-label input[type="radio"]:checked + .readmo-ai-radio-dot {
     418    border-color: rgba(0, 134, 168, 1);
     419}
     420
     421.readmo-ai-radio-label input[type="radio"]:checked + .readmo-ai-radio-dot::after {
     422    background: rgba(0, 134, 168, 1);
     423}
     424
     425.readmo-ai-radio-label:hover .readmo-ai-radio-dot {
     426    border-color: rgba(0, 134, 168, 0.6);
     427}
     428
     429.readmo-ai-radio-text {
     430    line-height: 1;
     431}
     432
     433/* Content Tree Selector Styles */
     434.readmo-ai-content-tree {
     435    max-height: 400px;
     436    overflow: auto;
     437    padding: 12px;
     438    border: 1px solid rgba(221, 221, 221, 1);
     439    border-radius: 8px;
     440    background: var(--Text-White, rgba(255, 255, 255, 1));
     441}
     442
     443.readmo-ai-tree-node {
     444    user-select: none;
     445}
     446
     447.readmo-ai-tree-item {
     448    display: flex;
     449    align-items: center;
     450    gap: 12px;
     451    padding: 12px 8px;
     452    border-radius: 4px;
     453    transition: background-color 0.15s ease;
     454}
     455
     456.readmo-ai-tree-item:hover {
     457    background-color: rgba(0, 134, 168, 0.05);
     458}
     459
     460.readmo-ai-tree-toggle {
     461    display: flex;
     462    align-items: center;
     463    justify-content: center;
     464    width: 20px;
     465    height: 20px;
     466    cursor: pointer;
     467    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     468    transition: transform 0.2s ease;
     469}
     470
     471.readmo-ai-tree-toggle svg {
     472    transition: transform 0.2s ease;
     473}
     474
     475.readmo-ai-tree-node.expanded > .readmo-ai-tree-item > .readmo-ai-tree-toggle svg {
     476    transform: rotate(90deg);
     477}
     478
     479.readmo-ai-tree-leaf .readmo-ai-tree-toggle {
     480    visibility: hidden;
     481}
     482
     483.readmo-ai-tree-leaf {
     484    padding-left: 32px;
     485}
     486
     487.readmo-ai-tree-checkbox {
     488    display: flex;
     489    align-items: center;
     490    gap: 12px;
     491    cursor: pointer;
     492    flex: 1;
     493}
     494
     495input[type="checkbox"].readmo-ai-tree-input {
     496    appearance: none;
     497    -webkit-appearance: none;
     498    width: 20px;
     499    height: 20px;
     500    margin: 0;
     501    padding: 0;
     502    flex-shrink: 0;
     503    border: 2px solid var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     504    border-radius: 4px;
     505    background: #ffffff;
     506    cursor: pointer;
     507    position: relative;
     508    transition: border-color 0.2s ease, background-color 0.2s ease;
     509}
     510
     511input[type="checkbox"].readmo-ai-tree-input:checked {
     512    background-color: rgba(0, 134, 168, 1);
     513    border-color: rgba(0, 134, 168, 1);
     514}
     515
     516input[type="checkbox"].readmo-ai-tree-input:checked::after {
     517    content: "";
     518    position: absolute;
     519    top: 50%;
     520    left: 50%;
     521    width: 5px;
     522    height: 10px;
     523    border: solid #ffffff;
     524    border-width: 0 2px 2px 0;
     525    transform: translate(-50%, -60%) rotate(45deg);
     526}
     527
     528input[type="checkbox"].readmo-ai-tree-input:indeterminate {
     529    background-color: rgba(0, 134, 168, 1);
     530    border-color: rgba(0, 134, 168, 1);
     531}
     532
     533input[type="checkbox"].readmo-ai-tree-input:indeterminate::after {
     534    content: "";
     535    position: absolute;
     536    top: 50%;
     537    left: 50%;
     538    width: 10px;
     539    height: 2px;
     540    background: #ffffff;
     541    transform: translate(-50%, -50%);
     542}
     543
     544input[type="checkbox"].readmo-ai-tree-input:hover {
     545    border-color: rgba(0, 134, 168, 0.6);
     546}
     547
     548input[type="checkbox"].readmo-ai-tree-input:focus {
     549    outline: none;
     550    box-shadow: none;
     551}
     552
     553.readmo-ai-tree-label {
     554    font-size: 16px;
     555    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     556    transition: color 0.15s ease;
     557}
     558
     559.readmo-ai-tree-label small {
     560    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     561}
     562
     563/* Gray style for unchecked items */
     564.readmo-ai-tree-node.unchecked > .readmo-ai-tree-item .readmo-ai-tree-label {
     565    color: var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     566}
     567
     568.readmo-ai-tree-node.unchecked > .readmo-ai-tree-item .readmo-ai-tree-label small {
     569    color: var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     570}
     571
     572/* Children container */
     573.readmo-ai-tree-children {
     574    margin-left: 18px;
     575    display: none;
     576    border-left: 1px solid rgba(221, 221, 221, 0.5);
     577    padding-left: 12px;
     578}
     579
     580.readmo-ai-tree-node.expanded > .readmo-ai-tree-children {
     581    display: block;
     582}
     583
     584.readmo-ai-empty-msg {
     585    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     586    font-size: 14px;
     587    font-style: italic;
     588}
     589
     590/* Auto-Insert Actions */
     591.readmo-ai-auto-insert-actions {
     592    justify-content: space-between;
     593}
     594
     595/* Modal Styles */
     596.readmo-ai-modal {
     597    position: fixed;
     598    top: 0;
     599    left: 0;
     600    width: 100%;
     601    height: 100%;
     602    z-index: 100000;
     603    display: flex;
     604    align-items: center;
     605    justify-content: center;
     606}
     607
     608.readmo-ai-modal-overlay {
     609    position: absolute;
     610    top: 0;
     611    left: 0;
     612    width: 100%;
     613    height: 100%;
     614    background: rgba(0, 0, 0, 0.5);
     615}
     616
     617.readmo-ai-modal-content {
     618    display: flex;
     619    flex-direction: column;
     620    gap: 24px;
     621    position: relative;
     622    background: #ffffff;
     623    padding: 24px;
     624    border-radius: 12px;
     625    max-width: 400px;
     626    width: 90%;
     627    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
     628}
     629
     630.readmo-ai-modal-content h3 {
     631    font-size: 18px;
     632    font-weight: 600;
     633    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     634}
     635
     636.readmo-ai-modal-content p {
     637    font-size: 14px;
     638    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     639    line-height: 1.5;
     640}
     641
     642.readmo-ai-modal-actions {
     643    display: flex;
     644    justify-content: flex-end;
     645    gap: 12px;
     646}
     647
     648.readmo-ai-modal-actions .btn {
     649    width: auto;
     650    padding: 10px 20px;
     651}
     652
    243653@media screen and (max-width: 767px) {
    244654    .readmo-ai-settings-container {
     
    268678        padding: 12px 16px;
    269679    }
    270 }
     680
     681    .btn-danger {
     682        width: auto;
     683        padding: 12px 16px;
     684    }
     685
     686    .readmo-ai-auto-insert-actions {
     687        flex-direction: column;
     688        gap: 12px;
     689    }
     690
     691    .readmo-ai-auto-insert-actions .btn {
     692        width: 100%;
     693    }
     694
     695    .readmo-ai-checkbox-group,
     696    .readmo-ai-radio-group {
     697        flex-direction: column;
     698        gap: 12px;
     699    }
     700}
  • readmo-ai/tags/1.2.0/assets/js/admin.js

    r3417155 r3447021  
    2828            this.storeOriginalValues();
    2929            this.initializeSvgIcons();
     30            // Ensure button is disabled initially.
     31            this.$saveButton.prop( 'disabled', true );
    3032        },
    3133
     
    3537        cacheElements: function () {
    3638            this.$apiKeyInput  = $( '#readmo-ai-api-key' );
    37             this.$saveButton   = $( '.readmo-ai-action .btn' );
     39            this.$saveButton   = $( '#readmo-ai-save-api-key' );
    3840            this.$toggleButton = $( '#readmo-ai-toggle-password' );
    3941            this.$form         = $( '.readmo-ai-settings-container' );
     
    108110                // Enable button and change to secondary style.
    109111                this.$saveButton
    110                     .removeClass( 'btn-tertiary' )
    111                     .addClass( 'btn-secondary' );
     112                    .removeClass( 'btn-ban' )
     113                    .addClass( 'btn-secondary' )
     114                    .prop( 'disabled', false );
    112115            } else {
    113                 // Disable button and change to tertiary style.
     116                // Disable button and change to ban style.
    114117                this.$saveButton
    115118                    .removeClass( 'btn-secondary' )
    116                     .addClass( 'btn-tertiary' );
     119                    .addClass( 'btn-ban' )
     120                    .prop( 'disabled', true );
    117121            }
    118122        },
     
    125129            var apiKey = this.$apiKeyInput.val();
    126130
    127             // Check if button is disabled (tertiary state).
    128             if (this.$saveButton.hasClass( 'btn-tertiary' )) {
     131            // Check if button is disabled (ban state).
     132            if (this.$saveButton.hasClass( 'btn-ban' )) {
    129133                return;
    130134            }
     
    150154                            self.$saveButton
    151155                            .removeClass( 'btn-secondary' )
    152                             .addClass( 'btn-tertiary' )
     156                            .addClass( 'btn-ban' )
    153157                            .prop( 'disabled', false )
    154158                            .text( readmoAiAdminData.i18n.saveChanges );
     
    231235    };
    232236
     237    /**
     238     * Auto-Insert Settings Handler
     239     */
     240    var ReadmoAiAutoInsert = {
     241        /**
     242         * Initialize auto-insert functionality.
     243         */
     244        init: function () {
     245            this.cacheElements();
     246            this.bindEvents();
     247            this.initializeTree();
     248            this.storeOriginalValues();
     249            // Ensure button is disabled initially.
     250            this.$saveButton.prop( 'disabled', true );
     251        },
     252
     253        /**
     254         * Cache DOM elements.
     255         */
     256        cacheElements: function () {
     257            this.$enabledCheckbox = $( '#readmo-ai-auto-insert-enabled' );
     258            this.$positionRadios  = $( 'input[name="auto_insert_position"]' );
     259            this.$fromUrlInput    = $( '#readmo-ai-from-url' );
     260            this.$contentTree     = $( '#readmo-ai-content-tree' );
     261            this.$saveButton      = $( '#readmo-ai-save-auto-insert' );
     262            this.$removeButton    = $( '#readmo-ai-remove-auto-insert' );
     263            this.$modal           = $( '#readmo-ai-confirm-modal' );
     264            this.$modalCancel     = $( '#readmo-ai-modal-cancel' );
     265            this.$modalConfirm    = $( '#readmo-ai-modal-confirm' );
     266            this.$modalOverlay    = $( '.readmo-ai-modal-overlay' );
     267        },
     268
     269        /**
     270         * Initialize tree state.
     271         */
     272        initializeTree: function () {
     273            var self = this;
     274
     275            // Expand all post type nodes by default.
     276            this.$contentTree.find( '.readmo-ai-tree-node[data-type="post-type"]' ).addClass( 'expanded' );
     277
     278            // Update all checkbox states and gray styling.
     279            this.$contentTree.find( '.readmo-ai-tree-node' ).each( function () {
     280                self.updateNodeState( $( this ) );
     281            });
     282        },
     283
     284        /**
     285         * Store original form values.
     286         */
     287        storeOriginalValues: function () {
     288            this.originalValues = this.getFormValues();
     289        },
     290
     291        /**
     292         * Get current form values.
     293         * Returns excluded items (unchecked = excluded).
     294         */
     295        getFormValues: function () {
     296            var excludedPostTypes  = [];
     297            var excludedCategories = [];
     298            var excludedPosts      = [];
     299
     300            // Get unchecked post types.
     301            this.$contentTree.find( '.readmo-ai-tree-input[data-type="post-type"]' ).each( function () {
     302                if ( ! $( this ).is( ':checked' ) && ! $( this ).prop( 'indeterminate' ) ) {
     303                    excludedPostTypes.push( $( this ).data( 'value' ) );
     304                }
     305            });
     306
     307            // Get unchecked categories.
     308            this.$contentTree.find( '.readmo-ai-tree-input[data-type="category"]' ).each( function () {
     309                if ( ! $( this ).is( ':checked' ) && ! $( this ).prop( 'indeterminate' ) ) {
     310                    excludedCategories.push( String( $( this ).data( 'value' ) ) );
     311                }
     312            });
     313
     314            // Get unchecked posts.
     315            this.$contentTree.find( '.readmo-ai-tree-input[data-type="post"]' ).each( function () {
     316                if ( ! $( this ).is( ':checked' ) ) {
     317                    excludedPosts.push( String( $( this ).data( 'value' ) ) );
     318                }
     319            });
     320
     321            return {
     322                enabled: this.$enabledCheckbox.is( ':checked' ),
     323                position: $( 'input[name="auto_insert_position"]:checked' ).val(),
     324                fromUrl: this.$fromUrlInput.val(),
     325                excludedPostTypes: excludedPostTypes,
     326                excludedCategories: excludedCategories,
     327                excludedPosts: excludedPosts
     328            };
     329        },
     330
     331        /**
     332         * Check if form has changes.
     333         */
     334        hasChanges: function () {
     335            var current  = this.getFormValues();
     336            var original = this.originalValues;
     337
     338            return JSON.stringify( current ) !== JSON.stringify( original );
     339        },
     340
     341        /**
     342         * Update save button state.
     343         */
     344        updateSaveButtonState: function () {
     345            if ( this.hasChanges() ) {
     346                this.$saveButton
     347                    .removeClass( 'btn-ban' )
     348                    .addClass( 'btn-secondary' )
     349                    .prop( 'disabled', false );
     350            } else {
     351                this.$saveButton
     352                    .removeClass( 'btn-secondary' )
     353                    .addClass( 'btn-ban' )
     354                    .prop( 'disabled', true );
     355            }
     356        },
     357
     358        /**
     359         * Bind event listeners.
     360         */
     361        bindEvents: function () {
     362            var self = this;
     363
     364            // Monitor form changes.
     365            this.$enabledCheckbox.on( 'change', function () {
     366                self.updateSaveButtonState();
     367            });
     368
     369            this.$positionRadios.on( 'change', function () {
     370                self.updateSaveButtonState();
     371            });
     372
     373            this.$fromUrlInput.on( 'input', function () {
     374                self.updateSaveButtonState();
     375            });
     376
     377            // Tree toggle (expand/collapse).
     378            this.$contentTree.on( 'click', '.readmo-ai-tree-toggle', function ( e ) {
     379                e.stopPropagation();
     380                var $node = $( this ).closest( '.readmo-ai-tree-node' );
     381                $node.toggleClass( 'expanded' );
     382            });
     383
     384            // Tree checkbox change.
     385            this.$contentTree.on( 'change', '.readmo-ai-tree-input', function () {
     386                var $checkbox = $( this );
     387                var $node     = $checkbox.closest( '.readmo-ai-tree-node' );
     388                var isChecked = $checkbox.is( ':checked' );
     389
     390                // Cascade to children.
     391                self.cascadeToChildren( $node, isChecked );
     392
     393                // Update parent states.
     394                self.updateParentStates( $node );
     395
     396                // Update save button.
     397                self.updateSaveButtonState();
     398            });
     399
     400            // Save button click.
     401            this.$saveButton.on( 'click', function ( e ) {
     402                e.preventDefault();
     403                self.saveSettings();
     404            });
     405
     406            // Remove button click.
     407            this.$removeButton.on( 'click', function ( e ) {
     408                e.preventDefault();
     409                self.showModal();
     410            });
     411
     412            // Modal cancel.
     413            this.$modalCancel.on( 'click', function ( e ) {
     414                e.preventDefault();
     415                self.hideModal();
     416            });
     417
     418            // Modal overlay click.
     419            this.$modalOverlay.on( 'click', function () {
     420                self.hideModal();
     421            });
     422
     423            // Modal confirm.
     424            this.$modalConfirm.on( 'click', function ( e ) {
     425                e.preventDefault();
     426                self.removeSettings();
     427            });
     428
     429            // ESC key to close modal.
     430            $( document ).on( 'keydown', function ( e ) {
     431                if ( e.key === 'Escape' && self.$modal.is( ':visible' ) ) {
     432                    self.hideModal();
     433                }
     434            });
     435        },
     436
     437        /**
     438         * Cascade checkbox state to all children.
     439         */
     440        cascadeToChildren: function ( $node, isChecked ) {
     441            var self = this;
     442
     443            $node.find( '.readmo-ai-tree-children .readmo-ai-tree-input' ).each( function () {
     444                $( this ).prop( 'checked', isChecked ).prop( 'indeterminate', false );
     445            });
     446
     447            // Update gray styling for all descendant nodes.
     448            $node.find( '.readmo-ai-tree-node' ).each( function () {
     449                self.updateNodeGrayStyle( $( this ) );
     450            });
     451
     452            // Update current node gray style.
     453            self.updateNodeGrayStyle( $node );
     454        },
     455
     456        /**
     457         * Update parent checkbox states (indeterminate).
     458         */
     459        updateParentStates: function ( $node ) {
     460            var self    = this;
     461            var $parent = $node.parent().closest( '.readmo-ai-tree-node' );
     462
     463            if ( $parent.length === 0 ) {
     464                return;
     465            }
     466
     467            var $parentCheckbox = $parent.find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     468            var $children       = $parent.find( '> .readmo-ai-tree-children > .readmo-ai-tree-node' );
     469
     470            var checkedCount       = 0;
     471            var uncheckedCount     = 0;
     472            var indeterminateCount = 0;
     473
     474            $children.each( function () {
     475                var $childCheckbox = $( this ).find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     476                if ( $childCheckbox.prop( 'indeterminate' ) ) {
     477                    indeterminateCount++;
     478                } else if ( $childCheckbox.is( ':checked' ) ) {
     479                    checkedCount++;
     480                } else {
     481                    uncheckedCount++;
     482                }
     483            });
     484
     485            if ( indeterminateCount > 0 || ( checkedCount > 0 && uncheckedCount > 0 ) ) {
     486                // Partial selection - indeterminate state.
     487                $parentCheckbox.prop( 'checked', false ).prop( 'indeterminate', true );
     488            } else if ( checkedCount === $children.length ) {
     489                // All checked.
     490                $parentCheckbox.prop( 'checked', true ).prop( 'indeterminate', false );
     491            } else {
     492                // All unchecked.
     493                $parentCheckbox.prop( 'checked', false ).prop( 'indeterminate', false );
     494            }
     495
     496            // Update gray styling for parent.
     497            self.updateNodeGrayStyle( $parent );
     498
     499            // Recursively update grandparent.
     500            self.updateParentStates( $parent );
     501        },
     502
     503        /**
     504         * Update node state (checkbox and gray styling).
     505         */
     506        updateNodeState: function ( $node ) {
     507            var self            = this;
     508            var $checkbox       = $node.find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     509            var $childrenNodes  = $node.find( '> .readmo-ai-tree-children > .readmo-ai-tree-node' );
     510
     511            if ( $childrenNodes.length > 0 ) {
     512                // Has children - calculate state from children.
     513                var checkedCount       = 0;
     514                var uncheckedCount     = 0;
     515                var indeterminateCount = 0;
     516
     517                $childrenNodes.each( function () {
     518                    var $childCheckbox = $( this ).find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     519                    if ( $childCheckbox.prop( 'indeterminate' ) ) {
     520                        indeterminateCount++;
     521                    } else if ( $childCheckbox.is( ':checked' ) ) {
     522                        checkedCount++;
     523                    } else {
     524                        uncheckedCount++;
     525                    }
     526                });
     527
     528                if ( indeterminateCount > 0 || ( checkedCount > 0 && uncheckedCount > 0 ) ) {
     529                    $checkbox.prop( 'checked', false ).prop( 'indeterminate', true );
     530                } else if ( checkedCount === $childrenNodes.length ) {
     531                    $checkbox.prop( 'checked', true ).prop( 'indeterminate', false );
     532                } else {
     533                    $checkbox.prop( 'checked', false ).prop( 'indeterminate', false );
     534                }
     535            }
     536
     537            // Update gray styling.
     538            self.updateNodeGrayStyle( $node );
     539        },
     540
     541        /**
     542         * Update gray styling for a node.
     543         * Unchecked nodes are gray, but parents with checked children are NOT gray.
     544         */
     545        updateNodeGrayStyle: function ( $node ) {
     546            var $checkbox = $node.find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     547            var isChecked = $checkbox.is( ':checked' );
     548            var isIndeterminate = $checkbox.prop( 'indeterminate' );
     549
     550            if ( isChecked || isIndeterminate ) {
     551                // Checked or has some checked children - not gray.
     552                $node.removeClass( 'unchecked' );
     553            } else {
     554                // Completely unchecked - gray.
     555                $node.addClass( 'unchecked' );
     556            }
     557        },
     558
     559        /**
     560         * Save auto-insert settings via AJAX.
     561         */
     562        saveSettings: function () {
     563            var self   = this;
     564            var values = this.getFormValues();
     565
     566            // Check if button is disabled.
     567            if ( this.$saveButton.prop( 'disabled' ) ) {
     568                return;
     569            }
     570
     571            // Disable button during save.
     572            this.$saveButton.prop( 'disabled', true ).text( readmoAiAdminData.i18n.saving );
     573
     574            // Debug: log values being sent.
     575            console.log( 'Readmo AI - Sending auto-insert settings:', values );
     576
     577            $.ajax({
     578                type: 'POST',
     579                url: readmoAiAdminData.ajaxUrl,
     580                data: {
     581                    action: 'readmo_ai_save_auto_insert_settings',
     582                    nonce: readmoAiAdminData.nonce,
     583                    enabled: values.enabled ? 1 : 0,
     584                    position: values.position,
     585                    from_url: values.fromUrl,
     586                    excluded_post_types: values.excludedPostTypes,
     587                    excluded_categories: values.excludedCategories,
     588                    excluded_posts: values.excludedPosts
     589                },
     590                success: function ( response ) {
     591                    // Debug: log response.
     592                    console.log( 'Readmo AI - Save response:', response );
     593                    if ( response.success ) {
     594                        // Update original values.
     595                        self.originalValues = values;
     596
     597                        // Reset button state.
     598                        self.$saveButton
     599                            .removeClass( 'btn-secondary' )
     600                            .addClass( 'btn-ban' )
     601                            .prop( 'disabled', false )
     602                            .text( readmoAiAdminData.i18n.saveChanges );
     603
     604                        // Show success message.
     605                        ReadmoAiAdmin.showNotice( 'success', response.data.message );
     606                    } else {
     607                        // Show error message.
     608                        ReadmoAiAdmin.showNotice( 'error', response.data.message );
     609
     610                        // Re-enable button.
     611                        self.$saveButton
     612                            .prop( 'disabled', false )
     613                            .text( readmoAiAdminData.i18n.saveChanges );
     614                    }
     615                },
     616                error: function () {
     617                    // Show error message.
     618                    ReadmoAiAdmin.showNotice( 'error', 'An error occurred while saving settings.' );
     619
     620                    // Re-enable button.
     621                    self.$saveButton
     622                        .prop( 'disabled', false )
     623                        .text( readmoAiAdminData.i18n.saveChanges );
     624                }
     625            });
     626        },
     627
     628        /**
     629         * Show confirmation modal.
     630         */
     631        showModal: function () {
     632            this.$modal.fadeIn( 200 );
     633        },
     634
     635        /**
     636         * Hide confirmation modal.
     637         */
     638        hideModal: function () {
     639            this.$modal.fadeOut( 200 );
     640        },
     641
     642        /**
     643         * Remove auto-insert settings via AJAX.
     644         */
     645        removeSettings: function () {
     646            var self = this;
     647
     648            // Disable confirm button.
     649            this.$modalConfirm.prop( 'disabled', true ).text( readmoAiAdminData.i18n.saving );
     650
     651            $.ajax({
     652                type: 'POST',
     653                url: readmoAiAdminData.ajaxUrl,
     654                data: {
     655                    action: 'readmo_ai_delete_auto_insert_settings',
     656                    nonce: readmoAiAdminData.nonce
     657                },
     658                success: function ( response ) {
     659                    if ( response.success ) {
     660                        // Reset form to defaults (all checked).
     661                        self.$enabledCheckbox.prop( 'checked', false );
     662                        $( 'input[name="auto_insert_position"][value="after_content"]' ).prop( 'checked', true );
     663                        self.$fromUrlInput.val( '' );
     664
     665                        // Check all tree items.
     666                        self.$contentTree.find( '.readmo-ai-tree-input' ).prop( 'checked', true ).prop( 'indeterminate', false );
     667                        self.$contentTree.find( '.readmo-ai-tree-node' ).removeClass( 'unchecked' );
     668
     669                        // Update original values.
     670                        self.originalValues = self.getFormValues();
     671
     672                        // Reset button state.
     673                        self.$saveButton
     674                            .removeClass( 'btn-secondary' )
     675                            .addClass( 'btn-ban' )
     676                            .prop( 'disabled', true );
     677
     678                        // Hide modal.
     679                        self.hideModal();
     680
     681                        // Re-enable confirm button.
     682                        self.$modalConfirm.prop( 'disabled', false ).text( readmoAiAdminData.i18n.confirmRemove || 'Confirm Remove' );
     683
     684                        // Show success message.
     685                        ReadmoAiAdmin.showNotice( 'success', response.data.message );
     686                    } else {
     687                        // Show error message.
     688                        ReadmoAiAdmin.showNotice( 'error', response.data.message );
     689
     690                        // Hide modal.
     691                        self.hideModal();
     692
     693                        // Re-enable confirm button.
     694                        self.$modalConfirm.prop( 'disabled', false ).text( readmoAiAdminData.i18n.confirmRemove || 'Confirm Remove' );
     695                    }
     696                },
     697                error: function () {
     698                    // Show error message.
     699                    ReadmoAiAdmin.showNotice( 'error', 'An error occurred while removing settings.' );
     700
     701                    // Hide modal.
     702                    self.hideModal();
     703
     704                    // Re-enable confirm button.
     705                    self.$modalConfirm.prop( 'disabled', false ).text( readmoAiAdminData.i18n.confirmRemove || 'Confirm Remove' );
     706                }
     707            });
     708        }
     709    };
     710
    233711    // Initialize on document ready.
    234712    $( document ).ready(
    235713        function () {
    236714            ReadmoAiAdmin.init();
     715            ReadmoAiAutoInsert.init();
    237716        }
    238717    );
  • readmo-ai/tags/1.2.0/class-readmo-ai-plugin.php

    r3417155 r3447021  
    106106     */
    107107    protected $tracking_handler = null;
     108
     109    /**
     110     * Block handler instance (Controller Layer)
     111     *
     112     * @since 1.0.0
     113     * @var Readmo_Ai_Block_Handler
     114     */
     115    protected $block_handler = null;
     116
     117    /**
     118     * Auto-insert handler instance (Controller Layer)
     119     *
     120     * @since 1.2.0
     121     * @var Readmo_Ai_Auto_Insert
     122     */
     123    protected $auto_insert = null;
    108124
    109125    /**
     
    158174        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-shortcode-handler.php';
    159175        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-tracking-handler.php';
     176        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-block-handler.php';
     177        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-auto-insert.php';
    160178
    161179        // View Layer loaded on-demand by Controllers.
     
    174192        $this->ajax_handler     = new Readmo_Ai_Ajax_Handler( $this->encryption, $this->admin_settings, $this->api_client );
    175193        $this->tracking_handler = new Readmo_Ai_Tracking_Handler( $this->settings_dao, $this->api_client, $this->tracking_client );
     194        $this->block_handler    = new Readmo_Ai_Block_Handler();
    176195
    177196        // Initialize frontend if not in admin.
     
    179198            $this->frontend_assets   = new Readmo_Ai_Frontend_Assets( $this->admin_settings );
    180199            $this->shortcode_handler = new Readmo_Ai_Shortcode_Handler( $this->encryption, $this->admin_settings, $this->api_client );
     200            $this->auto_insert       = new Readmo_Ai_Auto_Insert( $this->settings_dao );
    181201        }
    182202    }
  • readmo-ai/tags/1.2.0/readme.txt

    r3445269 r3447021  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.1.1
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
  • readmo-ai/tags/1.2.0/readmo-ai.php

    r3445269 r3447021  
    1313 * Plugin Name:       Readmo AI
    1414 * Description:       AI-powered content analysis and optimization for WordPress with analytics tracking
    15  * Version:           1.1.1
     15 * Version:           1.2.0
    1616 * Requires at least: 5.9
    1717 * Requires PHP:      7.4
     
    3333 */
    3434if ( ! defined( 'READMO_AI_VERSION' ) ) {
    35     define( 'READMO_AI_VERSION', '1.1.1' );
     35    define( 'READMO_AI_VERSION', '1.2.0' );
    3636}
    3737
  • readmo-ai/tags/1.2.0/uninstall.php

    r3411004 r3447021  
    3131    delete_option( 'readmo_ai_settings' );
    3232
     33    // Delete auto-insert settings option.
     34    delete_option( 'readmo_ai_auto_insert' );
     35
    3336    // For multisite installations, delete site options as well.
    3437    if ( is_multisite() ) {
    3538        delete_site_option( 'readmo_ai_settings' );
     39        delete_site_option( 'readmo_ai_auto_insert' );
    3640    }
    3741}
  • readmo-ai/trunk/Controller/admin/class-readmo-ai-admin-settings.php

    r3417155 r3447021  
    111111        // Register AJAX handlers.
    112112        add_action( 'wp_ajax_readmo_ai_save_settings', array( $this, 'ajax_save_settings' ) );
     113        add_action( 'wp_ajax_readmo_ai_save_auto_insert_settings', array( $this, 'ajax_save_auto_insert_settings' ) );
     114        add_action( 'wp_ajax_readmo_ai_delete_auto_insert_settings', array( $this, 'ajax_delete_auto_insert_settings' ) );
    113115    }
    114116
     
    290292        // Prepare view data.
    291293        $view_data = array(
    292             'api_key' => $this->settings_dao->get_api_key(),
     294            'api_key'              => $this->settings_dao->get_api_key(),
     295            'auto_insert_settings' => $this->settings_dao->get_auto_insert_settings(),
     296            'content_tree'         => $this->build_content_tree(),
    293297        );
    294298
     
    393397        }
    394398    }
     399
     400    /**
     401     * AJAX handler to save auto-insert settings
     402     *
     403     * Handles AJAX requests to save auto-insert configuration.
     404     *
     405     * @since 1.2.0
     406     * @return void
     407     */
     408    public function ajax_save_auto_insert_settings() {
     409        // Verify nonce.
     410        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'readmo_ai_admin_nonce' ) ) {
     411            wp_send_json_error(
     412                array(
     413                    'message' => __( 'Security check failed', 'readmo-ai' ),
     414                )
     415            );
     416        }
     417
     418        // Check user capabilities.
     419        if ( ! current_user_can( 'manage_options' ) ) {
     420            wp_send_json_error(
     421                array(
     422                    'message' => __( 'Insufficient permissions', 'readmo-ai' ),
     423                )
     424            );
     425        }
     426
     427        // Sanitize and validate settings.
     428        $auto_insert_settings = $this->sanitize_auto_insert_settings( $_POST );
     429
     430        // Save settings.
     431        $saved = $this->settings_dao->save_auto_insert_settings( $auto_insert_settings );
     432
     433        // Read back to verify.
     434        $verified_settings = $this->settings_dao->get_auto_insert_settings();
     435
     436        if ( $saved ) {
     437            wp_send_json_success(
     438                array(
     439                    'message'          => __( 'Auto-insert settings saved successfully', 'readmo-ai' ),
     440                    'saved_settings'   => $auto_insert_settings,
     441                    'verified_settings' => $verified_settings,
     442                )
     443            );
     444        } else {
     445            wp_send_json_error(
     446                array(
     447                    'message'          => __( 'Failed to save auto-insert settings', 'readmo-ai' ),
     448                    'attempted_settings' => $auto_insert_settings,
     449                )
     450            );
     451        }
     452    }
     453
     454    /**
     455     * AJAX handler to delete auto-insert settings
     456     *
     457     * Handles AJAX requests to remove auto-insert configuration.
     458     *
     459     * @since 1.2.0
     460     * @return void
     461     */
     462    public function ajax_delete_auto_insert_settings() {
     463        // Verify nonce.
     464        if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'readmo_ai_admin_nonce' ) ) {
     465            wp_send_json_error(
     466                array(
     467                    'message' => __( 'Security check failed', 'readmo-ai' ),
     468                )
     469            );
     470        }
     471
     472        // Check user capabilities.
     473        if ( ! current_user_can( 'manage_options' ) ) {
     474            wp_send_json_error(
     475                array(
     476                    'message' => __( 'Insufficient permissions', 'readmo-ai' ),
     477                )
     478            );
     479        }
     480
     481        // Delete settings.
     482        $deleted = $this->settings_dao->delete_auto_insert_settings();
     483
     484        if ( $deleted ) {
     485            wp_send_json_success(
     486                array(
     487                    'message' => __( 'Auto-insert settings removed successfully', 'readmo-ai' ),
     488                )
     489            );
     490        } else {
     491            wp_send_json_error(
     492                array(
     493                    'message' => __( 'Failed to remove auto-insert settings', 'readmo-ai' ),
     494                )
     495            );
     496        }
     497    }
     498
     499    /**
     500     * Sanitize auto-insert settings
     501     *
     502     * Validates and sanitizes auto-insert settings from POST data.
     503     * Uses "excluded" storage model: stores only unchecked items.
     504     *
     505     * @since 1.2.0
     506     * @param array $post_data The POST data to sanitize.
     507     * @return array Sanitized settings.
     508     */
     509    private function sanitize_auto_insert_settings( $post_data ) {
     510        $settings = array();
     511
     512        // Enabled flag.
     513        $settings['enabled'] = ! empty( $post_data['enabled'] );
     514
     515        // Position.
     516        $allowed_positions    = array( 'before_content', 'after_content', 'footer' );
     517        $settings['position'] = 'after_content';
     518        if ( isset( $post_data['position'] ) && in_array( $post_data['position'], $allowed_positions, true ) ) {
     519            $settings['position'] = sanitize_text_field( $post_data['position'] );
     520        }
     521
     522        // Excluded post types (array of post type names).
     523        $settings['excluded_post_types'] = array();
     524        if ( isset( $post_data['excluded_post_types'] ) && is_array( $post_data['excluded_post_types'] ) ) {
     525            $settings['excluded_post_types'] = array_map( 'sanitize_key', $post_data['excluded_post_types'] );
     526            $settings['excluded_post_types'] = array_filter( $settings['excluded_post_types'] );
     527        }
     528
     529        // Excluded categories (array of IDs).
     530        $settings['excluded_categories'] = array();
     531        if ( isset( $post_data['excluded_categories'] ) && is_array( $post_data['excluded_categories'] ) ) {
     532            $settings['excluded_categories'] = array_map( 'absint', $post_data['excluded_categories'] );
     533            $settings['excluded_categories'] = array_filter( $settings['excluded_categories'] );
     534        }
     535
     536        // Excluded posts/pages (array of IDs).
     537        $settings['excluded_posts'] = array();
     538        if ( isset( $post_data['excluded_posts'] ) && is_array( $post_data['excluded_posts'] ) ) {
     539            $settings['excluded_posts'] = array_map( 'absint', $post_data['excluded_posts'] );
     540            $settings['excluded_posts'] = array_filter( $settings['excluded_posts'] );
     541        }
     542
     543        return $settings;
     544    }
     545
     546    /**
     547     * Get auto-insert settings
     548     *
     549     * Retrieves the auto-insert configuration.
     550     *
     551     * @since 1.2.0
     552     * @return array Auto-insert settings.
     553     */
     554    public function get_auto_insert_settings() {
     555        return $this->settings_dao->get_auto_insert_settings();
     556    }
     557
     558    /**
     559     * Build content tree structure
     560     *
     561     * Builds hierarchical tree data for the tree selector UI.
     562     * Structure: Post Type → Category → Post/Page
     563     *
     564     * @since 1.2.0
     565     * @return array Content tree structure.
     566     */
     567    private function build_content_tree() {
     568        $tree = array();
     569
     570        // Get public post types.
     571        $post_types = get_post_types(
     572            array(
     573                'public' => true,
     574            ),
     575            'objects'
     576        );
     577
     578        // Remove attachment post type.
     579        unset( $post_types['attachment'] );
     580
     581        foreach ( $post_types as $post_type ) {
     582            $type_data = array(
     583                'name'  => $post_type->name,
     584                'label' => $post_type->labels->name,
     585            );
     586
     587            // For 'post' type, include categories as children.
     588            if ( 'post' === $post_type->name ) {
     589                $type_data['categories'] = $this->get_categories_with_posts();
     590            } else {
     591                // For other types (page, custom), list posts directly.
     592                $type_data['posts'] = $this->get_posts_by_type( $post_type->name );
     593            }
     594
     595            $tree[ $post_type->name ] = $type_data;
     596        }
     597
     598        return $tree;
     599    }
     600
     601    /**
     602     * Get categories with their posts
     603     *
     604     * Retrieves all categories with their associated posts.
     605     *
     606     * @since 1.2.0
     607     * @return array Categories with posts.
     608     */
     609    private function get_categories_with_posts() {
     610        $categories_data = array();
     611
     612        $categories = get_categories(
     613            array(
     614                'hide_empty' => false,
     615                'orderby'    => 'name',
     616                'order'      => 'ASC',
     617            )
     618        );
     619
     620        foreach ( $categories as $category ) {
     621            $posts = get_posts(
     622                array(
     623                    'post_type'      => 'post',
     624                    'post_status'    => 'publish',
     625                    'category'       => $category->term_id,
     626                    'posts_per_page' => 100,
     627                    'orderby'        => 'title',
     628                    'order'          => 'ASC',
     629                )
     630            );
     631
     632            $posts_data = array();
     633            foreach ( $posts as $post ) {
     634                $posts_data[ $post->ID ] = $post->post_title;
     635            }
     636
     637            $categories_data[ $category->term_id ] = array(
     638                'name'  => $category->name,
     639                'count' => $category->count,
     640                'posts' => $posts_data,
     641            );
     642        }
     643
     644        return $categories_data;
     645    }
     646
     647    /**
     648     * Get posts by type
     649     *
     650     * Retrieves all posts of a specific post type.
     651     *
     652     * @since 1.2.0
     653     * @param string $post_type The post type to retrieve.
     654     * @return array Posts data (ID => title).
     655     */
     656    private function get_posts_by_type( $post_type ) {
     657        $posts_data = array();
     658
     659        $posts = get_posts(
     660            array(
     661                'post_type'      => $post_type,
     662                'post_status'    => 'publish',
     663                'posts_per_page' => 100,
     664                'orderby'        => 'title',
     665                'order'          => 'ASC',
     666            )
     667        );
     668
     669        foreach ( $posts as $post ) {
     670            $posts_data[ $post->ID ] = $post->post_title;
     671        }
     672
     673        return $posts_data;
     674    }
    395675}
  • readmo-ai/trunk/Controller/frontend/class-readmo-ai-frontend-assets.php

    r3417155 r3447021  
    2727 */
    2828class Readmo_Ai_Frontend_Assets {
     29
     30    /**
     31     * Flag to track if Readmo AI content is present on the page.
     32     * Used for conditional loading of tracking script.
     33     *
     34     * @since 1.2.0
     35     * @var bool
     36     */
     37    private static $has_readmo_content = false;
    2938
    3039    /**
     
    5968    protected function register_hooks() {
    6069        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
     70        // Conditionally enqueue tracking script in footer (after content is rendered).
     71        add_action( 'wp_footer', array( $this, 'maybe_enqueue_tracking_script' ), 5 );
     72    }
     73
     74    /**
     75     * Mark that Readmo AI content is present on the page.
     76     *
     77     * Called by shortcode/block renderers to enable tracking script loading.
     78     *
     79     * @since 1.2.0
     80     * @return void
     81     */
     82    public static function mark_has_content() {
     83        self::$has_readmo_content = true;
     84    }
     85
     86    /**
     87     * Check if Readmo AI content is present on the page.
     88     *
     89     * @since 1.2.0
     90     * @return bool True if content is present.
     91     */
     92    public static function has_content() {
     93        return self::$has_readmo_content;
    6194    }
    6295
     
    6497     * Enqueue frontend assets
    6598     *
    66      * Enqueues CSS and JavaScript files required for the shortcode functionality.
     99     * Registers CSS and JavaScript files required for the shortcode functionality.
     100     * Note: Scripts are conditionally loaded via maybe_enqueue_tracking_script()
     101     * to prevent tracking on pages without Readmo AI content.
    67102     *
    68103     * @since 1.0.0
     
    70105     */
    71106    public function enqueue_frontend_assets() {
    72         // Enqueue frontend CSS.
     107        // Enqueue frontend CSS (always needed for styling).
    73108        wp_enqueue_style(
    74109            'readmo-ai-frontend',
     
    79114        );
    80115
    81         // Enqueue jQuery (WordPress standard library).
    82         wp_enqueue_script( 'jquery' );
    83 
    84         // Enqueue tracking JavaScript.
    85         wp_enqueue_script(
     116        // Register tracking script (will be enqueued conditionally in footer).
     117        wp_register_script(
    86118            'readmo-ai-tracking',
    87119            READMO_AI_PLUGIN_URL . 'assets/js/tracking.js',
     
    91123        );
    92124
    93         // Enqueue polling JavaScript (depends on tracking for sendTrackingEvent).
    94         wp_enqueue_script(
     125        // Register polling JavaScript (will be enqueued conditionally in footer).
     126        wp_register_script(
    95127            'readmo-ai-polling',
    96128            READMO_AI_PLUGIN_URL . 'assets/js/polling.js',
     
    108140        }
    109141
    110         // Localize script with AJAX URL, settings, SVG, translations, and tracking nonce.
     142        // Localize script data (will be available when scripts are enqueued).
    111143        wp_localize_script(
    112144            'readmo-ai-polling',
     
    120152        );
    121153    }
     154
     155    /**
     156     * Conditionally enqueue scripts
     157     *
     158     * Only loads tracking.js and polling.js if Readmo AI content is present on the page.
     159     * This prevents unnecessary tracking on pages without Readmo AI content.
     160     *
     161     * @since 1.2.0
     162     * @return void
     163     */
     164    public function maybe_enqueue_tracking_script() {
     165        if ( self::$has_readmo_content ) {
     166            // Enqueue polling script (which depends on tracking script).
     167            // WordPress will automatically load tracking.js as a dependency.
     168            wp_enqueue_script( 'readmo-ai-polling' );
     169        }
     170    }
    122171}
  • readmo-ai/trunk/Controller/frontend/class-readmo-ai-shortcode-handler.php

    r3417155 r3447021  
    9090     */
    9191    public function render_shortcode( $atts = array(), $content = '' ) {
    92         // Parse shortcode attributes.
    93         $atts = shortcode_atts(
    94             array(
    95                 'from' => '',
    96             ),
    97             $atts,
    98             'readmo_ai_articles'
    99         );
    100 
    101         // Determine source URL: use 'url' or 'from' parameter, or fallback to current page URL.
    102         if ( ! empty( $atts['from'] ) ) {
    103             $from = $atts['from'];
    104         } else {
    105             $from = $this->get_current_page_url();
    106         }
     92        // Always use current page URL as source.
     93        $from = $this->get_current_page_url();
    10794
    10895        // Generate unique container ID.
  • readmo-ai/trunk/Infrastructure/dao/class-readmo-ai-settings-dao.php

    r3417155 r3447021  
    2525
    2626    /**
    27      * WordPress option name
     27     * WordPress option name for main settings
    2828     *
    2929     * @since 1.0.0
     
    3131     */
    3232    const OPTION_NAME = 'readmo_ai_settings';
     33
     34    /**
     35     * WordPress option name for auto-insert settings
     36     *
     37     * @since 1.2.0
     38     * @var string
     39     */
     40    const AUTO_INSERT_OPTION_NAME = 'readmo_ai_auto_insert';
    3341
    3442    /**
     
    99107        return delete_option( self::OPTION_NAME );
    100108    }
     109
     110    /**
     111     * Get auto-insert settings
     112     *
     113     * Retrieves the auto-insert configuration settings from dedicated option.
     114     * Uses "excluded" storage model: empty arrays mean all items are checked (apply to all).
     115     *
     116     * @since 1.2.0
     117     * @return array Auto-insert settings with defaults.
     118     */
     119    public function get_auto_insert_settings() {
     120        // Default: all empty arrays = all checked = apply to all content.
     121        $defaults = array(
     122            'enabled'              => false,
     123            'position'             => 'after_content',
     124            'excluded_post_types'  => array(),
     125            'excluded_categories'  => array(),
     126            'excluded_posts'       => array(),
     127        );
     128
     129        $settings = get_option( self::AUTO_INSERT_OPTION_NAME, array() );
     130
     131        if ( empty( $settings ) ) {
     132            return $defaults;
     133        }
     134
     135        $settings = wp_parse_args( $settings, $defaults );
     136
     137        // Ensure ID arrays are integers for strict comparison in should_insert().
     138        // WordPress get_option() may return serialized integers as strings.
     139        if ( ! empty( $settings['excluded_categories'] ) && is_array( $settings['excluded_categories'] ) ) {
     140            $settings['excluded_categories'] = array_map( 'intval', $settings['excluded_categories'] );
     141        }
     142        if ( ! empty( $settings['excluded_posts'] ) && is_array( $settings['excluded_posts'] ) ) {
     143            $settings['excluded_posts'] = array_map( 'intval', $settings['excluded_posts'] );
     144        }
     145
     146        return $settings;
     147    }
     148
     149    /**
     150     * Save auto-insert settings
     151     *
     152     * Saves the auto-insert configuration to dedicated WordPress option.
     153     *
     154     * @since 1.2.0
     155     * @param array $auto_insert_settings The auto-insert settings to save.
     156     * @return bool True on success, false on failure.
     157     */
     158    public function save_auto_insert_settings( $auto_insert_settings ) {
     159        // Use update_option with autoload = true for better performance.
     160        return update_option( self::AUTO_INSERT_OPTION_NAME, $auto_insert_settings, true );
     161    }
     162
     163    /**
     164     * Delete auto-insert settings
     165     *
     166     * Removes the auto-insert settings option entirely.
     167     *
     168     * @since 1.2.0
     169     * @return bool True on success, false on failure.
     170     */
     171    public function delete_auto_insert_settings() {
     172        return delete_option( self::AUTO_INSERT_OPTION_NAME );
     173    }
    101174}
  • readmo-ai/trunk/View/admin/class-readmo-ai-admin-settings-view.php

    r3417155 r3447021  
    3434     */
    3535    public function render( $data ) {
    36         $api_key = isset( $data['api_key'] ) ? $data['api_key'] : '';
     36        $api_key              = isset( $data['api_key'] ) ? $data['api_key'] : '';
     37        $auto_insert_settings = isset( $data['auto_insert_settings'] ) ? $data['auto_insert_settings'] : array();
     38        $content_tree         = isset( $data['content_tree'] ) ? $data['content_tree'] : array();
     39
     40        // Default values for auto-insert settings.
     41        // Using "excluded" storage: empty arrays = all checked (apply to all).
     42        $ai_enabled             = ! empty( $auto_insert_settings['enabled'] );
     43        $ai_position            = isset( $auto_insert_settings['position'] ) ? $auto_insert_settings['position'] : 'after_content';
     44        $ai_excluded_post_types = isset( $auto_insert_settings['excluded_post_types'] ) ? $auto_insert_settings['excluded_post_types'] : array();
     45        $ai_excluded_categories = isset( $auto_insert_settings['excluded_categories'] ) ? $auto_insert_settings['excluded_categories'] : array();
     46        $ai_excluded_posts      = isset( $auto_insert_settings['excluded_posts'] ) ? $auto_insert_settings['excluded_posts'] : array();
    3747
    3848        ?>
     
    7383
    7484                <div class="readmo-ai-action">
    75                     <button type="button" class="btn btn-tertiary">
     85                    <button type="button" id="readmo-ai-save-api-key" class="btn btn-ban">
    7686                        <?php echo esc_html( __( 'Save Changes', 'readmo-ai' ) ); ?>
    7787                    </button>
     
    7989            </form>
    8090
     91
     92            <!-- Auto-Insert Settings Panel -->
     93            <div class="readmo-ai-panel">
     94                <div class="readmo-ai-field">
     95                    <span class="readmo-ai-title"><?php echo esc_html( __( 'Auto-Insert Settings', 'readmo-ai' ) ); ?></span>
     96
     97                    <!-- Enable Toggle -->
     98                    <div class="readmo-ai-setting-row">
     99                        <label class="readmo-ai-label">
     100                            <span class="readmo-ai-text"><?php echo esc_html( __( 'Enable auto-insert', 'readmo-ai' ) ); ?></span>
     101                            <span class="readmo-ai-comment"><?php echo esc_html( __( 'Automatically display Readmo AI on all pages matching the criteria below.', 'readmo-ai' ) ); ?></span>
     102                        </label>
     103                        <div class="readmo-ai-toggle">
     104                            <input
     105                                type="checkbox"
     106                                id="readmo-ai-auto-insert-enabled"
     107                                name="auto_insert_enabled"
     108                                value="1"
     109                                <?php checked( $ai_enabled ); ?>
     110                            />
     111                            <label for="readmo-ai-auto-insert-enabled" class="readmo-ai-toggle-slider"></label>
     112                        </div>
     113                    </div>
     114
     115                    <!-- Insert Position -->
     116                    <div class="readmo-ai-setting-column">
     117                        <label class="readmo-ai-label">
     118                            <span class="readmo-ai-text"><?php echo esc_html( __( 'Insert Position', 'readmo-ai' ) ); ?></span>
     119                        </label>
     120                        <div class="readmo-ai-radio-group">
     121                            <label class="readmo-ai-radio-label">
     122                                <input
     123                                    type="radio"
     124                                    name="auto_insert_position"
     125                                    value="after_content"
     126                                    <?php checked( $ai_position, 'after_content' ); ?>
     127                                />
     128                                <span class="readmo-ai-radio-dot"></span>
     129                                <span class="readmo-ai-radio-text"><?php echo esc_html( __( 'After content', 'readmo-ai' ) ); ?></span>
     130                            </label>
     131                            <label class="readmo-ai-radio-label">
     132                                <input
     133                                    type="radio"
     134                                    name="auto_insert_position"
     135                                    value="footer"
     136                                    <?php checked( $ai_position, 'footer' ); ?>
     137                                />
     138                                <span class="readmo-ai-radio-dot"></span>
     139                                <span class="readmo-ai-radio-text"><?php echo esc_html( __( 'Page footer', 'readmo-ai' ) ); ?></span>
     140                            </label>
     141                        </div>
     142                    </div>
     143
     144                    <!-- Content Tree Selector -->
     145                    <div class="readmo-ai-setting-column">
     146                        <label class="readmo-ai-label">
     147                            <span class="readmo-ai-text"><?php echo esc_html( __( 'Apply to Content', 'readmo-ai' ) ); ?></span>
     148                            <span class="readmo-ai-comment"><?php echo esc_html( __( 'Check items to display Readmo AI. Unchecked items will not show Readmo AI.', 'readmo-ai' ) ); ?></span>
     149                        </label>
     150                        <div class="readmo-ai-content-tree" id="readmo-ai-content-tree">
     151                            <?php foreach ( $content_tree as $type_name => $type_data ) : ?>
     152                                <?php
     153                                $type_excluded = in_array( $type_name, $ai_excluded_post_types, true );
     154                                $type_checked  = ! $type_excluded;
     155                                ?>
     156                                <div class="readmo-ai-tree-node" data-type="post-type" data-value="<?php echo esc_attr( $type_name ); ?>">
     157                                    <div class="readmo-ai-tree-item">
     158                                        <span class="readmo-ai-tree-toggle">
     159                                            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
     160                                                <path d="M6 4l4 4-4 4V4z"/>
     161                                            </svg>
     162                                        </span>
     163                                        <label class="readmo-ai-tree-checkbox">
     164                                            <input
     165                                                type="checkbox"
     166                                                class="readmo-ai-tree-input"
     167                                                data-type="post-type"
     168                                                data-value="<?php echo esc_attr( $type_name ); ?>"
     169                                                <?php checked( $type_checked ); ?>
     170                                            />
     171                                            <span class="readmo-ai-tree-label"><?php echo esc_html( $type_data['label'] ); ?></span>
     172                                        </label>
     173                                    </div>
     174                                    <div class="readmo-ai-tree-children">
     175                                        <?php if ( isset( $type_data['categories'] ) ) : ?>
     176                                            <!-- Post type with categories -->
     177                                            <?php foreach ( $type_data['categories'] as $cat_id => $cat_data ) : ?>
     178                                                <?php
     179                                                $cat_excluded = in_array( (int) $cat_id, $ai_excluded_categories, true );
     180                                                $cat_checked  = ! $cat_excluded && $type_checked;
     181                                                ?>
     182                                                <div class="readmo-ai-tree-node" data-type="category" data-value="<?php echo esc_attr( $cat_id ); ?>">
     183                                                    <div class="readmo-ai-tree-item">
     184                                                        <span class="readmo-ai-tree-toggle">
     185                                                            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
     186                                                                <path d="M6 4l4 4-4 4V4z"/>
     187                                                            </svg>
     188                                                        </span>
     189                                                        <label class="readmo-ai-tree-checkbox">
     190                                                            <input
     191                                                                type="checkbox"
     192                                                                class="readmo-ai-tree-input"
     193                                                                data-type="category"
     194                                                                data-value="<?php echo esc_attr( $cat_id ); ?>"
     195                                                                <?php checked( $cat_checked ); ?>
     196                                                            />
     197                                                            <span class="readmo-ai-tree-label">
     198                                                                <?php echo esc_html( $cat_data['name'] ); ?>
     199                                                                <small>(<?php echo esc_html( $cat_data['count'] ); ?> <?php echo esc_html( __( 'posts', 'readmo-ai' ) ); ?>)</small>
     200                                                            </span>
     201                                                        </label>
     202                                                    </div>
     203                                                    <div class="readmo-ai-tree-children">
     204                                                        <?php foreach ( $cat_data['posts'] as $post_id => $post_title ) : ?>
     205                                                            <?php
     206                                                            $post_excluded = in_array( (int) $post_id, $ai_excluded_posts, true );
     207                                                            $post_checked  = ! $post_excluded && $cat_checked;
     208                                                            ?>
     209                                                            <div class="readmo-ai-tree-node" data-type="post" data-value="<?php echo esc_attr( $post_id ); ?>">
     210                                                                <div class="readmo-ai-tree-item readmo-ai-tree-leaf">
     211                                                                    <label class="readmo-ai-tree-checkbox">
     212                                                                        <input
     213                                                                            type="checkbox"
     214                                                                            class="readmo-ai-tree-input"
     215                                                                            data-type="post"
     216                                                                            data-value="<?php echo esc_attr( $post_id ); ?>"
     217                                                                            <?php checked( $post_checked ); ?>
     218                                                                        />
     219                                                                        <span class="readmo-ai-tree-label"><?php echo esc_html( $post_title ); ?></span>
     220                                                                    </label>
     221                                                                </div>
     222                                                            </div>
     223                                                        <?php endforeach; ?>
     224                                                    </div>
     225                                                </div>
     226                                            <?php endforeach; ?>
     227                                        <?php elseif ( isset( $type_data['posts'] ) ) : ?>
     228                                            <!-- Post type without categories (e.g., pages) -->
     229                                            <?php foreach ( $type_data['posts'] as $post_id => $post_title ) : ?>
     230                                                <?php
     231                                                $post_excluded = in_array( (int) $post_id, $ai_excluded_posts, true );
     232                                                $post_checked  = ! $post_excluded && $type_checked;
     233                                                ?>
     234                                                <div class="readmo-ai-tree-node" data-type="post" data-value="<?php echo esc_attr( $post_id ); ?>">
     235                                                    <div class="readmo-ai-tree-item readmo-ai-tree-leaf">
     236                                                        <label class="readmo-ai-tree-checkbox">
     237                                                            <input
     238                                                                type="checkbox"
     239                                                                class="readmo-ai-tree-input"
     240                                                                data-type="post"
     241                                                                data-value="<?php echo esc_attr( $post_id ); ?>"
     242                                                                <?php checked( $post_checked ); ?>
     243                                                            />
     244                                                            <span class="readmo-ai-tree-label"><?php echo esc_html( $post_title ); ?></span>
     245                                                        </label>
     246                                                    </div>
     247                                                </div>
     248                                            <?php endforeach; ?>
     249                                        <?php endif; ?>
     250                                    </div>
     251                                </div>
     252                            <?php endforeach; ?>
     253                        </div>
     254                    </div>
     255                </div>
     256
     257                <div class="readmo-ai-action readmo-ai-auto-insert-actions">
     258                    <button type="button" id="readmo-ai-save-auto-insert" class="btn btn-ban">
     259                        <?php echo esc_html( __( 'Save Settings', 'readmo-ai' ) ); ?>
     260                    </button>
     261                    <button type="button" id="readmo-ai-remove-auto-insert" class="btn btn-danger">
     262                        <?php echo esc_html( __( 'Disable and Remove', 'readmo-ai' ) ); ?>
     263                    </button>
     264                </div>
     265            </div>
     266
     267            <!-- Remove Confirmation Modal -->
     268            <div id="readmo-ai-confirm-modal" class="readmo-ai-modal" style="display: none;">
     269                <div class="readmo-ai-modal-overlay"></div>
     270                <div class="readmo-ai-modal-content">
     271                    <span class="readmo-ai-title"><?php echo esc_html( __( 'Confirm Remove', 'readmo-ai' ) ); ?></span>
     272                    <span class="readmo-ai-text"><?php echo esc_html( __( 'Are you sure you want to disable and remove all auto-insert settings? This action cannot be undone.', 'readmo-ai' ) ); ?></span>
     273                    <div class="readmo-ai-modal-actions">
     274                        <button type="button" id="readmo-ai-modal-cancel" class="btn btn-tertiary">
     275                            <?php echo esc_html( __( 'Cancel', 'readmo-ai' ) ); ?>
     276                        </button>
     277                        <button type="button" id="readmo-ai-modal-confirm" class="btn btn-danger">
     278                            <?php echo esc_html( __( 'Confirm Remove', 'readmo-ai' ) ); ?>
     279                        </button>
     280                    </div>
     281                </div>
     282            </div>
    81283
    82284            <div class="readmo-ai-help-info">
  • readmo-ai/trunk/View/frontend/class-readmo-ai-shortcode-view.php

    r3411004 r3447021  
    3838        $from         = isset( $data['from'] ) ? esc_js( $data['from'] ) : '';
    3939
     40        // Mark that Readmo AI content is present for conditional tracking script loading.
     41        if ( class_exists( 'Readmo_Ai_Frontend_Assets' ) ) {
     42            Readmo_Ai_Frontend_Assets::mark_has_content();
     43        }
     44
    4045        ob_start();
    4146        ?>
  • readmo-ai/trunk/assets/css/admin.css

    r3411004 r3447021  
    1616    flex-direction: column;
    1717    gap: 12px;
    18     margin: 32px 0;
    1918    font-family: Noto Sans TC, PingFang TC, Arial, Helvetica, LiHei Pro, Microsoft JhengHei, MingLiU, sans-serif;
    2019}
     
    6463    display: flex;
    6564    flex-direction: column;
    66     gap: 20px;
     65    gap: 28px;
    6766    padding: 32px;
    6867    border-bottom: 1px solid rgba(221, 221, 221, 0.5);
     
    149148    font-weight: 500;
    150149    font-size: 16px;
    151     line-height: 100%;
     150    line-height: 1.5;
    152151    vertical-align: middle;
    153152    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     
    174173
    175174.btn-secondary {
    176     width: 200px;
    177175    background: rgba(0, 134, 168, 1);
    178176    color: #ffffff;
     
    182180
    183181.btn-tertiary {
    184     width: 200px;
    185     background: var(--UI-Disable, rgba(221, 221, 221, 0.2));
    186     color: var(--Text-Neutral-100, rgba(221, 221, 221, 1));
     182    background: var(--UI-Disable, rgba(221, 221, 221, 0.465));
     183    color: var(--Text-Neutral-100, rgb(154, 154, 154));
    187184    border: none;
    188185    padding: 16px 20px;
     186}
     187
     188/* Danger Button */
     189.btn-danger {
     190    background: rgb(235, 67, 67);
     191    color: #ffffff;
     192    border: none;
     193    padding: 16px 20px;
     194}
     195
     196.btn-danger:hover {
     197    background: rgb(220, 38, 38);
     198}
     199
     200/* Ban Button (Disabled State) */
     201.btn-ban,
     202.btn:disabled {
     203    background: rgb(246, 246, 246);
     204    color: var(--Text-Neutral-200, rgb(220, 220, 220));
     205    border: none;
     206    padding: 16px 20px;
     207    cursor: not-allowed;
     208    pointer-events: none;
    189209}
    190210
     
    241261}
    242262
     263/* Auto-Insert Settings Panel */
     264.readmo-ai-setting-row {
     265    display: flex;
     266    justify-content: space-between;
     267    align-items: center;
     268    gap: 2px;
     269}
     270
     271.readmo-ai-setting-column {
     272    display: flex;
     273    flex-direction: column;
     274    gap: 12px;
     275}
     276
     277.readmo-ai-comment {
     278    font-size: 14px;
     279    line-height: 1.5;
     280    color: var(--Text-Neutral-300, rgb(155, 155, 155));
     281}
     282
     283/* Text Input Styles */
     284.readmo-ai-text-input {
     285    width: 100%;
     286    padding: 10px 14px;
     287    font-size: 14px;
     288    border: 1px solid rgba(221, 221, 221, 1);
     289    border-radius: 8px;
     290    background: var(--Text-White, rgba(255, 255, 255, 1));
     291    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     292    transition: border-color 0.2s ease;
     293}
     294
     295.readmo-ai-text-input:focus {
     296    outline: none;
     297    border-color: rgba(0, 134, 168, 1);
     298    box-shadow: 0 0 0 2px rgba(0, 134, 168, 0.1);
     299}
     300
     301.readmo-ai-text-input::placeholder {
     302    color: var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     303}
     304
     305/* Toggle Switch Styles */
     306.readmo-ai-label {
     307    display: flex;
     308    flex-direction: column;
     309    gap: 2px;
     310}
     311
     312.readmo-ai-toggle {
     313    position: relative;
     314    width: 44px;
     315    height: 24px;
     316    flex-shrink: 0;
     317}
     318
     319.readmo-ai-toggle input {
     320    opacity: 0;
     321    width: 0;
     322    height: 0;
     323}
     324
     325.readmo-ai-toggle-slider {
     326    position: absolute;
     327    cursor: pointer;
     328    top: 0;
     329    left: 0;
     330    right: 0;
     331    bottom: 0;
     332    background-color: var(--UI-Disable, rgba(221, 221, 221, 1));
     333    transition: 0.3s ease;
     334    border-radius: 24px;
     335}
     336
     337.readmo-ai-toggle-slider::before {
     338    position: absolute;
     339    content: "";
     340    height: 18px;
     341    width: 18px;
     342    left: 3px;
     343    bottom: 3px;
     344    background-color: white;
     345    transition: 0.3s ease;
     346    border-radius: 50%;
     347    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
     348}
     349
     350.readmo-ai-toggle input:checked + .readmo-ai-toggle-slider {
     351    background-color: rgba(0, 134, 168, 1);
     352}
     353
     354.readmo-ai-toggle input:checked + .readmo-ai-toggle-slider::before {
     355    transform: translateX(20px);
     356}
     357
     358.readmo-ai-toggle input:focus + .readmo-ai-toggle-slider {
     359    box-shadow: 0 0 0 2px rgba(0, 134, 168, 0.2);
     360}
     361
     362/* Checkbox and Radio Styles */
     363.readmo-ai-checkbox-group,
     364.readmo-ai-radio-group {
     365    display: flex;
     366    flex-wrap: wrap;
     367    gap: 16px;
     368}
     369
     370.readmo-ai-checkbox-label,
     371.readmo-ai-radio-label {
     372    display: flex;
     373    align-items: center;
     374    gap: 8px;
     375    cursor: pointer;
     376    font-size: 14px;
     377    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     378}
     379
     380.readmo-ai-checkbox-label input[type="checkbox"] {
     381    width: 18px;
     382    height: 18px;
     383    cursor: pointer;
     384    accent-color: rgba(0, 134, 168, 1);
     385}
     386
     387/* Custom Radio Button */
     388.readmo-ai-radio-label input[type="radio"] {
     389    position: absolute;
     390    opacity: 0;
     391    width: 0;
     392    height: 0;
     393}
     394
     395.readmo-ai-radio-dot {
     396    display: flex;
     397    align-items: center;
     398    justify-content: center;
     399    width: 18px;
     400    height: 18px;
     401    flex-shrink: 0;
     402    border: 2px solid var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     403    border-radius: 50%;
     404    background: #ffffff;
     405    transition: border-color 0.2s ease;
     406}
     407
     408.readmo-ai-radio-dot::after {
     409    content: "";
     410    width: 10px;
     411    height: 10px;
     412    border-radius: 50%;
     413    background: transparent;
     414    transition: background-color 0.2s ease;
     415}
     416
     417.readmo-ai-radio-label input[type="radio"]:checked + .readmo-ai-radio-dot {
     418    border-color: rgba(0, 134, 168, 1);
     419}
     420
     421.readmo-ai-radio-label input[type="radio"]:checked + .readmo-ai-radio-dot::after {
     422    background: rgba(0, 134, 168, 1);
     423}
     424
     425.readmo-ai-radio-label:hover .readmo-ai-radio-dot {
     426    border-color: rgba(0, 134, 168, 0.6);
     427}
     428
     429.readmo-ai-radio-text {
     430    line-height: 1;
     431}
     432
     433/* Content Tree Selector Styles */
     434.readmo-ai-content-tree {
     435    max-height: 400px;
     436    overflow: auto;
     437    padding: 12px;
     438    border: 1px solid rgba(221, 221, 221, 1);
     439    border-radius: 8px;
     440    background: var(--Text-White, rgba(255, 255, 255, 1));
     441}
     442
     443.readmo-ai-tree-node {
     444    user-select: none;
     445}
     446
     447.readmo-ai-tree-item {
     448    display: flex;
     449    align-items: center;
     450    gap: 12px;
     451    padding: 12px 8px;
     452    border-radius: 4px;
     453    transition: background-color 0.15s ease;
     454}
     455
     456.readmo-ai-tree-item:hover {
     457    background-color: rgba(0, 134, 168, 0.05);
     458}
     459
     460.readmo-ai-tree-toggle {
     461    display: flex;
     462    align-items: center;
     463    justify-content: center;
     464    width: 20px;
     465    height: 20px;
     466    cursor: pointer;
     467    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     468    transition: transform 0.2s ease;
     469}
     470
     471.readmo-ai-tree-toggle svg {
     472    transition: transform 0.2s ease;
     473}
     474
     475.readmo-ai-tree-node.expanded > .readmo-ai-tree-item > .readmo-ai-tree-toggle svg {
     476    transform: rotate(90deg);
     477}
     478
     479.readmo-ai-tree-leaf .readmo-ai-tree-toggle {
     480    visibility: hidden;
     481}
     482
     483.readmo-ai-tree-leaf {
     484    padding-left: 32px;
     485}
     486
     487.readmo-ai-tree-checkbox {
     488    display: flex;
     489    align-items: center;
     490    gap: 12px;
     491    cursor: pointer;
     492    flex: 1;
     493}
     494
     495input[type="checkbox"].readmo-ai-tree-input {
     496    appearance: none;
     497    -webkit-appearance: none;
     498    width: 20px;
     499    height: 20px;
     500    margin: 0;
     501    padding: 0;
     502    flex-shrink: 0;
     503    border: 2px solid var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     504    border-radius: 4px;
     505    background: #ffffff;
     506    cursor: pointer;
     507    position: relative;
     508    transition: border-color 0.2s ease, background-color 0.2s ease;
     509}
     510
     511input[type="checkbox"].readmo-ai-tree-input:checked {
     512    background-color: rgba(0, 134, 168, 1);
     513    border-color: rgba(0, 134, 168, 1);
     514}
     515
     516input[type="checkbox"].readmo-ai-tree-input:checked::after {
     517    content: "";
     518    position: absolute;
     519    top: 50%;
     520    left: 50%;
     521    width: 5px;
     522    height: 10px;
     523    border: solid #ffffff;
     524    border-width: 0 2px 2px 0;
     525    transform: translate(-50%, -60%) rotate(45deg);
     526}
     527
     528input[type="checkbox"].readmo-ai-tree-input:indeterminate {
     529    background-color: rgba(0, 134, 168, 1);
     530    border-color: rgba(0, 134, 168, 1);
     531}
     532
     533input[type="checkbox"].readmo-ai-tree-input:indeterminate::after {
     534    content: "";
     535    position: absolute;
     536    top: 50%;
     537    left: 50%;
     538    width: 10px;
     539    height: 2px;
     540    background: #ffffff;
     541    transform: translate(-50%, -50%);
     542}
     543
     544input[type="checkbox"].readmo-ai-tree-input:hover {
     545    border-color: rgba(0, 134, 168, 0.6);
     546}
     547
     548input[type="checkbox"].readmo-ai-tree-input:focus {
     549    outline: none;
     550    box-shadow: none;
     551}
     552
     553.readmo-ai-tree-label {
     554    font-size: 16px;
     555    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     556    transition: color 0.15s ease;
     557}
     558
     559.readmo-ai-tree-label small {
     560    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     561}
     562
     563/* Gray style for unchecked items */
     564.readmo-ai-tree-node.unchecked > .readmo-ai-tree-item .readmo-ai-tree-label {
     565    color: var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     566}
     567
     568.readmo-ai-tree-node.unchecked > .readmo-ai-tree-item .readmo-ai-tree-label small {
     569    color: var(--Text-Neutral-200, rgba(183, 183, 183, 1));
     570}
     571
     572/* Children container */
     573.readmo-ai-tree-children {
     574    margin-left: 18px;
     575    display: none;
     576    border-left: 1px solid rgba(221, 221, 221, 0.5);
     577    padding-left: 12px;
     578}
     579
     580.readmo-ai-tree-node.expanded > .readmo-ai-tree-children {
     581    display: block;
     582}
     583
     584.readmo-ai-empty-msg {
     585    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     586    font-size: 14px;
     587    font-style: italic;
     588}
     589
     590/* Auto-Insert Actions */
     591.readmo-ai-auto-insert-actions {
     592    justify-content: space-between;
     593}
     594
     595/* Modal Styles */
     596.readmo-ai-modal {
     597    position: fixed;
     598    top: 0;
     599    left: 0;
     600    width: 100%;
     601    height: 100%;
     602    z-index: 100000;
     603    display: flex;
     604    align-items: center;
     605    justify-content: center;
     606}
     607
     608.readmo-ai-modal-overlay {
     609    position: absolute;
     610    top: 0;
     611    left: 0;
     612    width: 100%;
     613    height: 100%;
     614    background: rgba(0, 0, 0, 0.5);
     615}
     616
     617.readmo-ai-modal-content {
     618    display: flex;
     619    flex-direction: column;
     620    gap: 24px;
     621    position: relative;
     622    background: #ffffff;
     623    padding: 24px;
     624    border-radius: 12px;
     625    max-width: 400px;
     626    width: 90%;
     627    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
     628}
     629
     630.readmo-ai-modal-content h3 {
     631    font-size: 18px;
     632    font-weight: 600;
     633    color: var(--Text-Neutral-400, rgba(74, 75, 88, 1));
     634}
     635
     636.readmo-ai-modal-content p {
     637    font-size: 14px;
     638    color: var(--Text-Neutral-300, rgba(113, 113, 113, 1));
     639    line-height: 1.5;
     640}
     641
     642.readmo-ai-modal-actions {
     643    display: flex;
     644    justify-content: flex-end;
     645    gap: 12px;
     646}
     647
     648.readmo-ai-modal-actions .btn {
     649    width: auto;
     650    padding: 10px 20px;
     651}
     652
    243653@media screen and (max-width: 767px) {
    244654    .readmo-ai-settings-container {
     
    268678        padding: 12px 16px;
    269679    }
    270 }
     680
     681    .btn-danger {
     682        width: auto;
     683        padding: 12px 16px;
     684    }
     685
     686    .readmo-ai-auto-insert-actions {
     687        flex-direction: column;
     688        gap: 12px;
     689    }
     690
     691    .readmo-ai-auto-insert-actions .btn {
     692        width: 100%;
     693    }
     694
     695    .readmo-ai-checkbox-group,
     696    .readmo-ai-radio-group {
     697        flex-direction: column;
     698        gap: 12px;
     699    }
     700}
  • readmo-ai/trunk/assets/js/admin.js

    r3417155 r3447021  
    2828            this.storeOriginalValues();
    2929            this.initializeSvgIcons();
     30            // Ensure button is disabled initially.
     31            this.$saveButton.prop( 'disabled', true );
    3032        },
    3133
     
    3537        cacheElements: function () {
    3638            this.$apiKeyInput  = $( '#readmo-ai-api-key' );
    37             this.$saveButton   = $( '.readmo-ai-action .btn' );
     39            this.$saveButton   = $( '#readmo-ai-save-api-key' );
    3840            this.$toggleButton = $( '#readmo-ai-toggle-password' );
    3941            this.$form         = $( '.readmo-ai-settings-container' );
     
    108110                // Enable button and change to secondary style.
    109111                this.$saveButton
    110                     .removeClass( 'btn-tertiary' )
    111                     .addClass( 'btn-secondary' );
     112                    .removeClass( 'btn-ban' )
     113                    .addClass( 'btn-secondary' )
     114                    .prop( 'disabled', false );
    112115            } else {
    113                 // Disable button and change to tertiary style.
     116                // Disable button and change to ban style.
    114117                this.$saveButton
    115118                    .removeClass( 'btn-secondary' )
    116                     .addClass( 'btn-tertiary' );
     119                    .addClass( 'btn-ban' )
     120                    .prop( 'disabled', true );
    117121            }
    118122        },
     
    125129            var apiKey = this.$apiKeyInput.val();
    126130
    127             // Check if button is disabled (tertiary state).
    128             if (this.$saveButton.hasClass( 'btn-tertiary' )) {
     131            // Check if button is disabled (ban state).
     132            if (this.$saveButton.hasClass( 'btn-ban' )) {
    129133                return;
    130134            }
     
    150154                            self.$saveButton
    151155                            .removeClass( 'btn-secondary' )
    152                             .addClass( 'btn-tertiary' )
     156                            .addClass( 'btn-ban' )
    153157                            .prop( 'disabled', false )
    154158                            .text( readmoAiAdminData.i18n.saveChanges );
     
    231235    };
    232236
     237    /**
     238     * Auto-Insert Settings Handler
     239     */
     240    var ReadmoAiAutoInsert = {
     241        /**
     242         * Initialize auto-insert functionality.
     243         */
     244        init: function () {
     245            this.cacheElements();
     246            this.bindEvents();
     247            this.initializeTree();
     248            this.storeOriginalValues();
     249            // Ensure button is disabled initially.
     250            this.$saveButton.prop( 'disabled', true );
     251        },
     252
     253        /**
     254         * Cache DOM elements.
     255         */
     256        cacheElements: function () {
     257            this.$enabledCheckbox = $( '#readmo-ai-auto-insert-enabled' );
     258            this.$positionRadios  = $( 'input[name="auto_insert_position"]' );
     259            this.$fromUrlInput    = $( '#readmo-ai-from-url' );
     260            this.$contentTree     = $( '#readmo-ai-content-tree' );
     261            this.$saveButton      = $( '#readmo-ai-save-auto-insert' );
     262            this.$removeButton    = $( '#readmo-ai-remove-auto-insert' );
     263            this.$modal           = $( '#readmo-ai-confirm-modal' );
     264            this.$modalCancel     = $( '#readmo-ai-modal-cancel' );
     265            this.$modalConfirm    = $( '#readmo-ai-modal-confirm' );
     266            this.$modalOverlay    = $( '.readmo-ai-modal-overlay' );
     267        },
     268
     269        /**
     270         * Initialize tree state.
     271         */
     272        initializeTree: function () {
     273            var self = this;
     274
     275            // Expand all post type nodes by default.
     276            this.$contentTree.find( '.readmo-ai-tree-node[data-type="post-type"]' ).addClass( 'expanded' );
     277
     278            // Update all checkbox states and gray styling.
     279            this.$contentTree.find( '.readmo-ai-tree-node' ).each( function () {
     280                self.updateNodeState( $( this ) );
     281            });
     282        },
     283
     284        /**
     285         * Store original form values.
     286         */
     287        storeOriginalValues: function () {
     288            this.originalValues = this.getFormValues();
     289        },
     290
     291        /**
     292         * Get current form values.
     293         * Returns excluded items (unchecked = excluded).
     294         */
     295        getFormValues: function () {
     296            var excludedPostTypes  = [];
     297            var excludedCategories = [];
     298            var excludedPosts      = [];
     299
     300            // Get unchecked post types.
     301            this.$contentTree.find( '.readmo-ai-tree-input[data-type="post-type"]' ).each( function () {
     302                if ( ! $( this ).is( ':checked' ) && ! $( this ).prop( 'indeterminate' ) ) {
     303                    excludedPostTypes.push( $( this ).data( 'value' ) );
     304                }
     305            });
     306
     307            // Get unchecked categories.
     308            this.$contentTree.find( '.readmo-ai-tree-input[data-type="category"]' ).each( function () {
     309                if ( ! $( this ).is( ':checked' ) && ! $( this ).prop( 'indeterminate' ) ) {
     310                    excludedCategories.push( String( $( this ).data( 'value' ) ) );
     311                }
     312            });
     313
     314            // Get unchecked posts.
     315            this.$contentTree.find( '.readmo-ai-tree-input[data-type="post"]' ).each( function () {
     316                if ( ! $( this ).is( ':checked' ) ) {
     317                    excludedPosts.push( String( $( this ).data( 'value' ) ) );
     318                }
     319            });
     320
     321            return {
     322                enabled: this.$enabledCheckbox.is( ':checked' ),
     323                position: $( 'input[name="auto_insert_position"]:checked' ).val(),
     324                fromUrl: this.$fromUrlInput.val(),
     325                excludedPostTypes: excludedPostTypes,
     326                excludedCategories: excludedCategories,
     327                excludedPosts: excludedPosts
     328            };
     329        },
     330
     331        /**
     332         * Check if form has changes.
     333         */
     334        hasChanges: function () {
     335            var current  = this.getFormValues();
     336            var original = this.originalValues;
     337
     338            return JSON.stringify( current ) !== JSON.stringify( original );
     339        },
     340
     341        /**
     342         * Update save button state.
     343         */
     344        updateSaveButtonState: function () {
     345            if ( this.hasChanges() ) {
     346                this.$saveButton
     347                    .removeClass( 'btn-ban' )
     348                    .addClass( 'btn-secondary' )
     349                    .prop( 'disabled', false );
     350            } else {
     351                this.$saveButton
     352                    .removeClass( 'btn-secondary' )
     353                    .addClass( 'btn-ban' )
     354                    .prop( 'disabled', true );
     355            }
     356        },
     357
     358        /**
     359         * Bind event listeners.
     360         */
     361        bindEvents: function () {
     362            var self = this;
     363
     364            // Monitor form changes.
     365            this.$enabledCheckbox.on( 'change', function () {
     366                self.updateSaveButtonState();
     367            });
     368
     369            this.$positionRadios.on( 'change', function () {
     370                self.updateSaveButtonState();
     371            });
     372
     373            this.$fromUrlInput.on( 'input', function () {
     374                self.updateSaveButtonState();
     375            });
     376
     377            // Tree toggle (expand/collapse).
     378            this.$contentTree.on( 'click', '.readmo-ai-tree-toggle', function ( e ) {
     379                e.stopPropagation();
     380                var $node = $( this ).closest( '.readmo-ai-tree-node' );
     381                $node.toggleClass( 'expanded' );
     382            });
     383
     384            // Tree checkbox change.
     385            this.$contentTree.on( 'change', '.readmo-ai-tree-input', function () {
     386                var $checkbox = $( this );
     387                var $node     = $checkbox.closest( '.readmo-ai-tree-node' );
     388                var isChecked = $checkbox.is( ':checked' );
     389
     390                // Cascade to children.
     391                self.cascadeToChildren( $node, isChecked );
     392
     393                // Update parent states.
     394                self.updateParentStates( $node );
     395
     396                // Update save button.
     397                self.updateSaveButtonState();
     398            });
     399
     400            // Save button click.
     401            this.$saveButton.on( 'click', function ( e ) {
     402                e.preventDefault();
     403                self.saveSettings();
     404            });
     405
     406            // Remove button click.
     407            this.$removeButton.on( 'click', function ( e ) {
     408                e.preventDefault();
     409                self.showModal();
     410            });
     411
     412            // Modal cancel.
     413            this.$modalCancel.on( 'click', function ( e ) {
     414                e.preventDefault();
     415                self.hideModal();
     416            });
     417
     418            // Modal overlay click.
     419            this.$modalOverlay.on( 'click', function () {
     420                self.hideModal();
     421            });
     422
     423            // Modal confirm.
     424            this.$modalConfirm.on( 'click', function ( e ) {
     425                e.preventDefault();
     426                self.removeSettings();
     427            });
     428
     429            // ESC key to close modal.
     430            $( document ).on( 'keydown', function ( e ) {
     431                if ( e.key === 'Escape' && self.$modal.is( ':visible' ) ) {
     432                    self.hideModal();
     433                }
     434            });
     435        },
     436
     437        /**
     438         * Cascade checkbox state to all children.
     439         */
     440        cascadeToChildren: function ( $node, isChecked ) {
     441            var self = this;
     442
     443            $node.find( '.readmo-ai-tree-children .readmo-ai-tree-input' ).each( function () {
     444                $( this ).prop( 'checked', isChecked ).prop( 'indeterminate', false );
     445            });
     446
     447            // Update gray styling for all descendant nodes.
     448            $node.find( '.readmo-ai-tree-node' ).each( function () {
     449                self.updateNodeGrayStyle( $( this ) );
     450            });
     451
     452            // Update current node gray style.
     453            self.updateNodeGrayStyle( $node );
     454        },
     455
     456        /**
     457         * Update parent checkbox states (indeterminate).
     458         */
     459        updateParentStates: function ( $node ) {
     460            var self    = this;
     461            var $parent = $node.parent().closest( '.readmo-ai-tree-node' );
     462
     463            if ( $parent.length === 0 ) {
     464                return;
     465            }
     466
     467            var $parentCheckbox = $parent.find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     468            var $children       = $parent.find( '> .readmo-ai-tree-children > .readmo-ai-tree-node' );
     469
     470            var checkedCount       = 0;
     471            var uncheckedCount     = 0;
     472            var indeterminateCount = 0;
     473
     474            $children.each( function () {
     475                var $childCheckbox = $( this ).find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     476                if ( $childCheckbox.prop( 'indeterminate' ) ) {
     477                    indeterminateCount++;
     478                } else if ( $childCheckbox.is( ':checked' ) ) {
     479                    checkedCount++;
     480                } else {
     481                    uncheckedCount++;
     482                }
     483            });
     484
     485            if ( indeterminateCount > 0 || ( checkedCount > 0 && uncheckedCount > 0 ) ) {
     486                // Partial selection - indeterminate state.
     487                $parentCheckbox.prop( 'checked', false ).prop( 'indeterminate', true );
     488            } else if ( checkedCount === $children.length ) {
     489                // All checked.
     490                $parentCheckbox.prop( 'checked', true ).prop( 'indeterminate', false );
     491            } else {
     492                // All unchecked.
     493                $parentCheckbox.prop( 'checked', false ).prop( 'indeterminate', false );
     494            }
     495
     496            // Update gray styling for parent.
     497            self.updateNodeGrayStyle( $parent );
     498
     499            // Recursively update grandparent.
     500            self.updateParentStates( $parent );
     501        },
     502
     503        /**
     504         * Update node state (checkbox and gray styling).
     505         */
     506        updateNodeState: function ( $node ) {
     507            var self            = this;
     508            var $checkbox       = $node.find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     509            var $childrenNodes  = $node.find( '> .readmo-ai-tree-children > .readmo-ai-tree-node' );
     510
     511            if ( $childrenNodes.length > 0 ) {
     512                // Has children - calculate state from children.
     513                var checkedCount       = 0;
     514                var uncheckedCount     = 0;
     515                var indeterminateCount = 0;
     516
     517                $childrenNodes.each( function () {
     518                    var $childCheckbox = $( this ).find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     519                    if ( $childCheckbox.prop( 'indeterminate' ) ) {
     520                        indeterminateCount++;
     521                    } else if ( $childCheckbox.is( ':checked' ) ) {
     522                        checkedCount++;
     523                    } else {
     524                        uncheckedCount++;
     525                    }
     526                });
     527
     528                if ( indeterminateCount > 0 || ( checkedCount > 0 && uncheckedCount > 0 ) ) {
     529                    $checkbox.prop( 'checked', false ).prop( 'indeterminate', true );
     530                } else if ( checkedCount === $childrenNodes.length ) {
     531                    $checkbox.prop( 'checked', true ).prop( 'indeterminate', false );
     532                } else {
     533                    $checkbox.prop( 'checked', false ).prop( 'indeterminate', false );
     534                }
     535            }
     536
     537            // Update gray styling.
     538            self.updateNodeGrayStyle( $node );
     539        },
     540
     541        /**
     542         * Update gray styling for a node.
     543         * Unchecked nodes are gray, but parents with checked children are NOT gray.
     544         */
     545        updateNodeGrayStyle: function ( $node ) {
     546            var $checkbox = $node.find( '> .readmo-ai-tree-item .readmo-ai-tree-input' );
     547            var isChecked = $checkbox.is( ':checked' );
     548            var isIndeterminate = $checkbox.prop( 'indeterminate' );
     549
     550            if ( isChecked || isIndeterminate ) {
     551                // Checked or has some checked children - not gray.
     552                $node.removeClass( 'unchecked' );
     553            } else {
     554                // Completely unchecked - gray.
     555                $node.addClass( 'unchecked' );
     556            }
     557        },
     558
     559        /**
     560         * Save auto-insert settings via AJAX.
     561         */
     562        saveSettings: function () {
     563            var self   = this;
     564            var values = this.getFormValues();
     565
     566            // Check if button is disabled.
     567            if ( this.$saveButton.prop( 'disabled' ) ) {
     568                return;
     569            }
     570
     571            // Disable button during save.
     572            this.$saveButton.prop( 'disabled', true ).text( readmoAiAdminData.i18n.saving );
     573
     574            // Debug: log values being sent.
     575            console.log( 'Readmo AI - Sending auto-insert settings:', values );
     576
     577            $.ajax({
     578                type: 'POST',
     579                url: readmoAiAdminData.ajaxUrl,
     580                data: {
     581                    action: 'readmo_ai_save_auto_insert_settings',
     582                    nonce: readmoAiAdminData.nonce,
     583                    enabled: values.enabled ? 1 : 0,
     584                    position: values.position,
     585                    from_url: values.fromUrl,
     586                    excluded_post_types: values.excludedPostTypes,
     587                    excluded_categories: values.excludedCategories,
     588                    excluded_posts: values.excludedPosts
     589                },
     590                success: function ( response ) {
     591                    // Debug: log response.
     592                    console.log( 'Readmo AI - Save response:', response );
     593                    if ( response.success ) {
     594                        // Update original values.
     595                        self.originalValues = values;
     596
     597                        // Reset button state.
     598                        self.$saveButton
     599                            .removeClass( 'btn-secondary' )
     600                            .addClass( 'btn-ban' )
     601                            .prop( 'disabled', false )
     602                            .text( readmoAiAdminData.i18n.saveChanges );
     603
     604                        // Show success message.
     605                        ReadmoAiAdmin.showNotice( 'success', response.data.message );
     606                    } else {
     607                        // Show error message.
     608                        ReadmoAiAdmin.showNotice( 'error', response.data.message );
     609
     610                        // Re-enable button.
     611                        self.$saveButton
     612                            .prop( 'disabled', false )
     613                            .text( readmoAiAdminData.i18n.saveChanges );
     614                    }
     615                },
     616                error: function () {
     617                    // Show error message.
     618                    ReadmoAiAdmin.showNotice( 'error', 'An error occurred while saving settings.' );
     619
     620                    // Re-enable button.
     621                    self.$saveButton
     622                        .prop( 'disabled', false )
     623                        .text( readmoAiAdminData.i18n.saveChanges );
     624                }
     625            });
     626        },
     627
     628        /**
     629         * Show confirmation modal.
     630         */
     631        showModal: function () {
     632            this.$modal.fadeIn( 200 );
     633        },
     634
     635        /**
     636         * Hide confirmation modal.
     637         */
     638        hideModal: function () {
     639            this.$modal.fadeOut( 200 );
     640        },
     641
     642        /**
     643         * Remove auto-insert settings via AJAX.
     644         */
     645        removeSettings: function () {
     646            var self = this;
     647
     648            // Disable confirm button.
     649            this.$modalConfirm.prop( 'disabled', true ).text( readmoAiAdminData.i18n.saving );
     650
     651            $.ajax({
     652                type: 'POST',
     653                url: readmoAiAdminData.ajaxUrl,
     654                data: {
     655                    action: 'readmo_ai_delete_auto_insert_settings',
     656                    nonce: readmoAiAdminData.nonce
     657                },
     658                success: function ( response ) {
     659                    if ( response.success ) {
     660                        // Reset form to defaults (all checked).
     661                        self.$enabledCheckbox.prop( 'checked', false );
     662                        $( 'input[name="auto_insert_position"][value="after_content"]' ).prop( 'checked', true );
     663                        self.$fromUrlInput.val( '' );
     664
     665                        // Check all tree items.
     666                        self.$contentTree.find( '.readmo-ai-tree-input' ).prop( 'checked', true ).prop( 'indeterminate', false );
     667                        self.$contentTree.find( '.readmo-ai-tree-node' ).removeClass( 'unchecked' );
     668
     669                        // Update original values.
     670                        self.originalValues = self.getFormValues();
     671
     672                        // Reset button state.
     673                        self.$saveButton
     674                            .removeClass( 'btn-secondary' )
     675                            .addClass( 'btn-ban' )
     676                            .prop( 'disabled', true );
     677
     678                        // Hide modal.
     679                        self.hideModal();
     680
     681                        // Re-enable confirm button.
     682                        self.$modalConfirm.prop( 'disabled', false ).text( readmoAiAdminData.i18n.confirmRemove || 'Confirm Remove' );
     683
     684                        // Show success message.
     685                        ReadmoAiAdmin.showNotice( 'success', response.data.message );
     686                    } else {
     687                        // Show error message.
     688                        ReadmoAiAdmin.showNotice( 'error', response.data.message );
     689
     690                        // Hide modal.
     691                        self.hideModal();
     692
     693                        // Re-enable confirm button.
     694                        self.$modalConfirm.prop( 'disabled', false ).text( readmoAiAdminData.i18n.confirmRemove || 'Confirm Remove' );
     695                    }
     696                },
     697                error: function () {
     698                    // Show error message.
     699                    ReadmoAiAdmin.showNotice( 'error', 'An error occurred while removing settings.' );
     700
     701                    // Hide modal.
     702                    self.hideModal();
     703
     704                    // Re-enable confirm button.
     705                    self.$modalConfirm.prop( 'disabled', false ).text( readmoAiAdminData.i18n.confirmRemove || 'Confirm Remove' );
     706                }
     707            });
     708        }
     709    };
     710
    233711    // Initialize on document ready.
    234712    $( document ).ready(
    235713        function () {
    236714            ReadmoAiAdmin.init();
     715            ReadmoAiAutoInsert.init();
    237716        }
    238717    );
  • readmo-ai/trunk/class-readmo-ai-plugin.php

    r3417155 r3447021  
    106106     */
    107107    protected $tracking_handler = null;
     108
     109    /**
     110     * Block handler instance (Controller Layer)
     111     *
     112     * @since 1.0.0
     113     * @var Readmo_Ai_Block_Handler
     114     */
     115    protected $block_handler = null;
     116
     117    /**
     118     * Auto-insert handler instance (Controller Layer)
     119     *
     120     * @since 1.2.0
     121     * @var Readmo_Ai_Auto_Insert
     122     */
     123    protected $auto_insert = null;
    108124
    109125    /**
     
    158174        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-shortcode-handler.php';
    159175        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-tracking-handler.php';
     176        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-block-handler.php';
     177        require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-auto-insert.php';
    160178
    161179        // View Layer loaded on-demand by Controllers.
     
    174192        $this->ajax_handler     = new Readmo_Ai_Ajax_Handler( $this->encryption, $this->admin_settings, $this->api_client );
    175193        $this->tracking_handler = new Readmo_Ai_Tracking_Handler( $this->settings_dao, $this->api_client, $this->tracking_client );
     194        $this->block_handler    = new Readmo_Ai_Block_Handler();
    176195
    177196        // Initialize frontend if not in admin.
     
    179198            $this->frontend_assets   = new Readmo_Ai_Frontend_Assets( $this->admin_settings );
    180199            $this->shortcode_handler = new Readmo_Ai_Shortcode_Handler( $this->encryption, $this->admin_settings, $this->api_client );
     200            $this->auto_insert       = new Readmo_Ai_Auto_Insert( $this->settings_dao );
    181201        }
    182202    }
  • readmo-ai/trunk/readme.txt

    r3445269 r3447021  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.1.1
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
  • readmo-ai/trunk/readmo-ai.php

    r3445269 r3447021  
    1313 * Plugin Name:       Readmo AI
    1414 * Description:       AI-powered content analysis and optimization for WordPress with analytics tracking
    15  * Version:           1.1.1
     15 * Version:           1.2.0
    1616 * Requires at least: 5.9
    1717 * Requires PHP:      7.4
     
    3333 */
    3434if ( ! defined( 'READMO_AI_VERSION' ) ) {
    35     define( 'READMO_AI_VERSION', '1.1.1' );
     35    define( 'READMO_AI_VERSION', '1.2.0' );
    3636}
    3737
  • readmo-ai/trunk/uninstall.php

    r3411004 r3447021  
    3131    delete_option( 'readmo_ai_settings' );
    3232
     33    // Delete auto-insert settings option.
     34    delete_option( 'readmo_ai_auto_insert' );
     35
    3336    // For multisite installations, delete site options as well.
    3437    if ( is_multisite() ) {
    3538        delete_site_option( 'readmo_ai_settings' );
     39        delete_site_option( 'readmo_ai_auto_insert' );
    3640    }
    3741}
Note: See TracChangeset for help on using the changeset viewer.