Changeset 3447021
- Timestamp:
- 01/26/2026 10:42:45 AM (2 months ago)
- Location:
- readmo-ai
- Files:
-
- 14 added
- 14 edited
- 28 copied
-
tags/1.2.0 (copied) (copied from readmo-ai/trunk)
-
tags/1.2.0/Controller (copied) (copied from readmo-ai/trunk/Controller)
-
tags/1.2.0/Controller/admin/class-readmo-ai-admin-settings.php (copied) (copied from readmo-ai/trunk/Controller/admin/class-readmo-ai-admin-settings.php) (3 diffs)
-
tags/1.2.0/Controller/frontend/class-readmo-ai-ajax-handler.php (copied) (copied from readmo-ai/trunk/Controller/frontend/class-readmo-ai-ajax-handler.php)
-
tags/1.2.0/Controller/frontend/class-readmo-ai-auto-insert.php (added)
-
tags/1.2.0/Controller/frontend/class-readmo-ai-block-handler.php (added)
-
tags/1.2.0/Controller/frontend/class-readmo-ai-frontend-assets.php (copied) (copied from readmo-ai/trunk/Controller/frontend/class-readmo-ai-frontend-assets.php) (8 diffs)
-
tags/1.2.0/Controller/frontend/class-readmo-ai-shortcode-handler.php (copied) (copied from readmo-ai/trunk/Controller/frontend/class-readmo-ai-shortcode-handler.php) (1 diff)
-
tags/1.2.0/Controller/frontend/class-readmo-ai-tracking-handler.php (copied) (copied from readmo-ai/trunk/Controller/frontend/class-readmo-ai-tracking-handler.php)
-
tags/1.2.0/Controller/frontend/strategies (added)
-
tags/1.2.0/Infrastructure (copied) (copied from readmo-ai/trunk/Infrastructure)
-
tags/1.2.0/Infrastructure/api/class-readmo-ai-api-client.php (copied) (copied from readmo-ai/trunk/Infrastructure/api/class-readmo-ai-api-client.php)
-
tags/1.2.0/Infrastructure/api/class-readmo-ai-tracking-client.php (copied) (copied from readmo-ai/trunk/Infrastructure/api/class-readmo-ai-tracking-client.php)
-
tags/1.2.0/Infrastructure/config/class-readmo-ai-config.php (copied) (copied from readmo-ai/trunk/Infrastructure/config/class-readmo-ai-config.php)
-
tags/1.2.0/Infrastructure/dao/class-readmo-ai-settings-dao.php (copied) (copied from readmo-ai/trunk/Infrastructure/dao/class-readmo-ai-settings-dao.php) (3 diffs)
-
tags/1.2.0/Service (copied) (copied from readmo-ai/trunk/Service)
-
tags/1.2.0/View (copied) (copied from readmo-ai/trunk/View)
-
tags/1.2.0/View/admin/class-readmo-ai-admin-settings-view.php (copied) (copied from readmo-ai/trunk/View/admin/class-readmo-ai-admin-settings-view.php) (3 diffs)
-
tags/1.2.0/View/frontend/class-readmo-ai-shortcode-view.php (modified) (1 diff)
-
tags/1.2.0/assets (copied) (copied from readmo-ai/trunk/assets)
-
tags/1.2.0/assets/css/admin.css (modified) (7 diffs)
-
tags/1.2.0/assets/js/admin.js (copied) (copied from readmo-ai/trunk/assets/js/admin.js) (6 diffs)
-
tags/1.2.0/assets/js/polling.js (copied) (copied from readmo-ai/trunk/assets/js/polling.js)
-
tags/1.2.0/assets/js/tracking.js (copied) (copied from readmo-ai/trunk/assets/js/tracking.js)
-
tags/1.2.0/blocks (added)
-
tags/1.2.0/blocks/readmo-ai-articles (added)
-
tags/1.2.0/blocks/readmo-ai-articles/block.json (added)
-
tags/1.2.0/blocks/readmo-ai-articles/index.js (added)
-
tags/1.2.0/class-readmo-ai-plugin.php (copied) (copied from readmo-ai/trunk/class-readmo-ai-plugin.php) (4 diffs)
-
tags/1.2.0/languages (copied) (copied from readmo-ai/trunk/languages)
-
tags/1.2.0/languages/readmo-ai-zh_CN.po (copied) (copied from readmo-ai/trunk/languages/readmo-ai-zh_CN.po)
-
tags/1.2.0/languages/readmo-ai-zh_HK.po (copied) (copied from readmo-ai/trunk/languages/readmo-ai-zh_HK.po)
-
tags/1.2.0/languages/readmo-ai-zh_TW.po (copied) (copied from readmo-ai/trunk/languages/readmo-ai-zh_TW.po)
-
tags/1.2.0/languages/readmo-ai.pot (copied) (copied from readmo-ai/trunk/languages/readmo-ai.pot)
-
tags/1.2.0/readme.txt (copied) (copied from readmo-ai/trunk/readme.txt) (1 diff)
-
tags/1.2.0/readmo-ai.php (copied) (copied from readmo-ai/trunk/readmo-ai.php) (2 diffs)
-
tags/1.2.0/uninstall.php (copied) (copied from readmo-ai/trunk/uninstall.php) (1 diff)
-
trunk/Controller/admin/class-readmo-ai-admin-settings.php (modified) (3 diffs)
-
trunk/Controller/frontend/class-readmo-ai-auto-insert.php (added)
-
trunk/Controller/frontend/class-readmo-ai-block-handler.php (added)
-
trunk/Controller/frontend/class-readmo-ai-frontend-assets.php (modified) (8 diffs)
-
trunk/Controller/frontend/class-readmo-ai-shortcode-handler.php (modified) (1 diff)
-
trunk/Controller/frontend/strategies (added)
-
trunk/Infrastructure/dao/class-readmo-ai-settings-dao.php (modified) (3 diffs)
-
trunk/View/admin/class-readmo-ai-admin-settings-view.php (modified) (3 diffs)
-
trunk/View/frontend/class-readmo-ai-shortcode-view.php (modified) (1 diff)
-
trunk/assets/css/admin.css (modified) (7 diffs)
-
trunk/assets/js/admin.js (modified) (6 diffs)
-
trunk/blocks (added)
-
trunk/blocks/readmo-ai-articles (added)
-
trunk/blocks/readmo-ai-articles/block.json (added)
-
trunk/blocks/readmo-ai-articles/index.js (added)
-
trunk/class-readmo-ai-plugin.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (1 diff)
-
trunk/readmo-ai.php (modified) (2 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
readmo-ai/tags/1.2.0/Controller/admin/class-readmo-ai-admin-settings.php
r3417155 r3447021 111 111 // Register AJAX handlers. 112 112 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' ) ); 113 115 } 114 116 … … 290 292 // Prepare view data. 291 293 $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(), 293 297 ); 294 298 … … 393 397 } 394 398 } 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 } 395 675 } -
readmo-ai/tags/1.2.0/Controller/frontend/class-readmo-ai-frontend-assets.php
r3417155 r3447021 27 27 */ 28 28 class 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; 29 38 30 39 /** … … 59 68 protected function register_hooks() { 60 69 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; 61 94 } 62 95 … … 64 97 * Enqueue frontend assets 65 98 * 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. 67 102 * 68 103 * @since 1.0.0 … … 70 105 */ 71 106 public function enqueue_frontend_assets() { 72 // Enqueue frontend CSS .107 // Enqueue frontend CSS (always needed for styling). 73 108 wp_enqueue_style( 74 109 'readmo-ai-frontend', … … 79 114 ); 80 115 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( 86 118 'readmo-ai-tracking', 87 119 READMO_AI_PLUGIN_URL . 'assets/js/tracking.js', … … 91 123 ); 92 124 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( 95 127 'readmo-ai-polling', 96 128 READMO_AI_PLUGIN_URL . 'assets/js/polling.js', … … 108 140 } 109 141 110 // Localize script with AJAX URL, settings, SVG, translations, and tracking nonce.142 // Localize script data (will be available when scripts are enqueued). 111 143 wp_localize_script( 112 144 'readmo-ai-polling', … … 120 152 ); 121 153 } 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 } 122 171 } -
readmo-ai/tags/1.2.0/Controller/frontend/class-readmo-ai-shortcode-handler.php
r3417155 r3447021 90 90 */ 91 91 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(); 107 94 108 95 // Generate unique container ID. -
readmo-ai/tags/1.2.0/Infrastructure/dao/class-readmo-ai-settings-dao.php
r3417155 r3447021 25 25 26 26 /** 27 * WordPress option name 27 * WordPress option name for main settings 28 28 * 29 29 * @since 1.0.0 … … 31 31 */ 32 32 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'; 33 41 34 42 /** … … 99 107 return delete_option( self::OPTION_NAME ); 100 108 } 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 } 101 174 } -
readmo-ai/tags/1.2.0/View/admin/class-readmo-ai-admin-settings-view.php
r3417155 r3447021 34 34 */ 35 35 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(); 37 47 38 48 ?> … … 73 83 74 84 <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"> 76 86 <?php echo esc_html( __( 'Save Changes', 'readmo-ai' ) ); ?> 77 87 </button> … … 79 89 </form> 80 90 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> 81 283 82 284 <div class="readmo-ai-help-info"> -
readmo-ai/tags/1.2.0/View/frontend/class-readmo-ai-shortcode-view.php
r3411004 r3447021 38 38 $from = isset( $data['from'] ) ? esc_js( $data['from'] ) : ''; 39 39 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 40 45 ob_start(); 41 46 ?> -
readmo-ai/tags/1.2.0/assets/css/admin.css
r3411004 r3447021 16 16 flex-direction: column; 17 17 gap: 12px; 18 margin: 32px 0;19 18 font-family: Noto Sans TC, PingFang TC, Arial, Helvetica, LiHei Pro, Microsoft JhengHei, MingLiU, sans-serif; 20 19 } … … 64 63 display: flex; 65 64 flex-direction: column; 66 gap: 2 0px;65 gap: 28px; 67 66 padding: 32px; 68 67 border-bottom: 1px solid rgba(221, 221, 221, 0.5); … … 149 148 font-weight: 500; 150 149 font-size: 16px; 151 line-height: 1 00%;150 line-height: 1.5; 152 151 vertical-align: middle; 153 152 color: var(--Text-Neutral-400, rgba(74, 75, 88, 1)); … … 174 173 175 174 .btn-secondary { 176 width: 200px;177 175 background: rgba(0, 134, 168, 1); 178 176 color: #ffffff; … … 182 180 183 181 .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)); 187 184 border: none; 188 185 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; 189 209 } 190 210 … … 241 261 } 242 262 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 495 input[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 511 input[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 516 input[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 528 input[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 533 input[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 544 input[type="checkbox"].readmo-ai-tree-input:hover { 545 border-color: rgba(0, 134, 168, 0.6); 546 } 547 548 input[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 243 653 @media screen and (max-width: 767px) { 244 654 .readmo-ai-settings-container { … … 268 678 padding: 12px 16px; 269 679 } 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 28 28 this.storeOriginalValues(); 29 29 this.initializeSvgIcons(); 30 // Ensure button is disabled initially. 31 this.$saveButton.prop( 'disabled', true ); 30 32 }, 31 33 … … 35 37 cacheElements: function () { 36 38 this.$apiKeyInput = $( '#readmo-ai-api-key' ); 37 this.$saveButton = $( ' .readmo-ai-action .btn' );39 this.$saveButton = $( '#readmo-ai-save-api-key' ); 38 40 this.$toggleButton = $( '#readmo-ai-toggle-password' ); 39 41 this.$form = $( '.readmo-ai-settings-container' ); … … 108 110 // Enable button and change to secondary style. 109 111 this.$saveButton 110 .removeClass( 'btn-tertiary' ) 111 .addClass( 'btn-secondary' ); 112 .removeClass( 'btn-ban' ) 113 .addClass( 'btn-secondary' ) 114 .prop( 'disabled', false ); 112 115 } else { 113 // Disable button and change to tertiarystyle.116 // Disable button and change to ban style. 114 117 this.$saveButton 115 118 .removeClass( 'btn-secondary' ) 116 .addClass( 'btn-tertiary' ); 119 .addClass( 'btn-ban' ) 120 .prop( 'disabled', true ); 117 121 } 118 122 }, … … 125 129 var apiKey = this.$apiKeyInput.val(); 126 130 127 // Check if button is disabled ( tertiarystate).128 if (this.$saveButton.hasClass( 'btn- tertiary' )) {131 // Check if button is disabled (ban state). 132 if (this.$saveButton.hasClass( 'btn-ban' )) { 129 133 return; 130 134 } … … 150 154 self.$saveButton 151 155 .removeClass( 'btn-secondary' ) 152 .addClass( 'btn- tertiary' )156 .addClass( 'btn-ban' ) 153 157 .prop( 'disabled', false ) 154 158 .text( readmoAiAdminData.i18n.saveChanges ); … … 231 235 }; 232 236 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 233 711 // Initialize on document ready. 234 712 $( document ).ready( 235 713 function () { 236 714 ReadmoAiAdmin.init(); 715 ReadmoAiAutoInsert.init(); 237 716 } 238 717 ); -
readmo-ai/tags/1.2.0/class-readmo-ai-plugin.php
r3417155 r3447021 106 106 */ 107 107 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; 108 124 109 125 /** … … 158 174 require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-shortcode-handler.php'; 159 175 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'; 160 178 161 179 // View Layer loaded on-demand by Controllers. … … 174 192 $this->ajax_handler = new Readmo_Ai_Ajax_Handler( $this->encryption, $this->admin_settings, $this->api_client ); 175 193 $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(); 176 195 177 196 // Initialize frontend if not in admin. … … 179 198 $this->frontend_assets = new Readmo_Ai_Frontend_Assets( $this->admin_settings ); 180 199 $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 ); 181 201 } 182 202 } -
readmo-ai/tags/1.2.0/readme.txt
r3445269 r3447021 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 1.17 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html -
readmo-ai/tags/1.2.0/readmo-ai.php
r3445269 r3447021 13 13 * Plugin Name: Readmo AI 14 14 * Description: AI-powered content analysis and optimization for WordPress with analytics tracking 15 * Version: 1. 1.115 * Version: 1.2.0 16 16 * Requires at least: 5.9 17 17 * Requires PHP: 7.4 … … 33 33 */ 34 34 if ( ! defined( 'READMO_AI_VERSION' ) ) { 35 define( 'READMO_AI_VERSION', '1. 1.1' );35 define( 'READMO_AI_VERSION', '1.2.0' ); 36 36 } 37 37 -
readmo-ai/tags/1.2.0/uninstall.php
r3411004 r3447021 31 31 delete_option( 'readmo_ai_settings' ); 32 32 33 // Delete auto-insert settings option. 34 delete_option( 'readmo_ai_auto_insert' ); 35 33 36 // For multisite installations, delete site options as well. 34 37 if ( is_multisite() ) { 35 38 delete_site_option( 'readmo_ai_settings' ); 39 delete_site_option( 'readmo_ai_auto_insert' ); 36 40 } 37 41 } -
readmo-ai/trunk/Controller/admin/class-readmo-ai-admin-settings.php
r3417155 r3447021 111 111 // Register AJAX handlers. 112 112 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' ) ); 113 115 } 114 116 … … 290 292 // Prepare view data. 291 293 $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(), 293 297 ); 294 298 … … 393 397 } 394 398 } 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 } 395 675 } -
readmo-ai/trunk/Controller/frontend/class-readmo-ai-frontend-assets.php
r3417155 r3447021 27 27 */ 28 28 class 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; 29 38 30 39 /** … … 59 68 protected function register_hooks() { 60 69 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; 61 94 } 62 95 … … 64 97 * Enqueue frontend assets 65 98 * 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. 67 102 * 68 103 * @since 1.0.0 … … 70 105 */ 71 106 public function enqueue_frontend_assets() { 72 // Enqueue frontend CSS .107 // Enqueue frontend CSS (always needed for styling). 73 108 wp_enqueue_style( 74 109 'readmo-ai-frontend', … … 79 114 ); 80 115 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( 86 118 'readmo-ai-tracking', 87 119 READMO_AI_PLUGIN_URL . 'assets/js/tracking.js', … … 91 123 ); 92 124 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( 95 127 'readmo-ai-polling', 96 128 READMO_AI_PLUGIN_URL . 'assets/js/polling.js', … … 108 140 } 109 141 110 // Localize script with AJAX URL, settings, SVG, translations, and tracking nonce.142 // Localize script data (will be available when scripts are enqueued). 111 143 wp_localize_script( 112 144 'readmo-ai-polling', … … 120 152 ); 121 153 } 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 } 122 171 } -
readmo-ai/trunk/Controller/frontend/class-readmo-ai-shortcode-handler.php
r3417155 r3447021 90 90 */ 91 91 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(); 107 94 108 95 // Generate unique container ID. -
readmo-ai/trunk/Infrastructure/dao/class-readmo-ai-settings-dao.php
r3417155 r3447021 25 25 26 26 /** 27 * WordPress option name 27 * WordPress option name for main settings 28 28 * 29 29 * @since 1.0.0 … … 31 31 */ 32 32 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'; 33 41 34 42 /** … … 99 107 return delete_option( self::OPTION_NAME ); 100 108 } 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 } 101 174 } -
readmo-ai/trunk/View/admin/class-readmo-ai-admin-settings-view.php
r3417155 r3447021 34 34 */ 35 35 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(); 37 47 38 48 ?> … … 73 83 74 84 <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"> 76 86 <?php echo esc_html( __( 'Save Changes', 'readmo-ai' ) ); ?> 77 87 </button> … … 79 89 </form> 80 90 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> 81 283 82 284 <div class="readmo-ai-help-info"> -
readmo-ai/trunk/View/frontend/class-readmo-ai-shortcode-view.php
r3411004 r3447021 38 38 $from = isset( $data['from'] ) ? esc_js( $data['from'] ) : ''; 39 39 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 40 45 ob_start(); 41 46 ?> -
readmo-ai/trunk/assets/css/admin.css
r3411004 r3447021 16 16 flex-direction: column; 17 17 gap: 12px; 18 margin: 32px 0;19 18 font-family: Noto Sans TC, PingFang TC, Arial, Helvetica, LiHei Pro, Microsoft JhengHei, MingLiU, sans-serif; 20 19 } … … 64 63 display: flex; 65 64 flex-direction: column; 66 gap: 2 0px;65 gap: 28px; 67 66 padding: 32px; 68 67 border-bottom: 1px solid rgba(221, 221, 221, 0.5); … … 149 148 font-weight: 500; 150 149 font-size: 16px; 151 line-height: 1 00%;150 line-height: 1.5; 152 151 vertical-align: middle; 153 152 color: var(--Text-Neutral-400, rgba(74, 75, 88, 1)); … … 174 173 175 174 .btn-secondary { 176 width: 200px;177 175 background: rgba(0, 134, 168, 1); 178 176 color: #ffffff; … … 182 180 183 181 .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)); 187 184 border: none; 188 185 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; 189 209 } 190 210 … … 241 261 } 242 262 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 495 input[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 511 input[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 516 input[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 528 input[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 533 input[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 544 input[type="checkbox"].readmo-ai-tree-input:hover { 545 border-color: rgba(0, 134, 168, 0.6); 546 } 547 548 input[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 243 653 @media screen and (max-width: 767px) { 244 654 .readmo-ai-settings-container { … … 268 678 padding: 12px 16px; 269 679 } 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 28 28 this.storeOriginalValues(); 29 29 this.initializeSvgIcons(); 30 // Ensure button is disabled initially. 31 this.$saveButton.prop( 'disabled', true ); 30 32 }, 31 33 … … 35 37 cacheElements: function () { 36 38 this.$apiKeyInput = $( '#readmo-ai-api-key' ); 37 this.$saveButton = $( ' .readmo-ai-action .btn' );39 this.$saveButton = $( '#readmo-ai-save-api-key' ); 38 40 this.$toggleButton = $( '#readmo-ai-toggle-password' ); 39 41 this.$form = $( '.readmo-ai-settings-container' ); … … 108 110 // Enable button and change to secondary style. 109 111 this.$saveButton 110 .removeClass( 'btn-tertiary' ) 111 .addClass( 'btn-secondary' ); 112 .removeClass( 'btn-ban' ) 113 .addClass( 'btn-secondary' ) 114 .prop( 'disabled', false ); 112 115 } else { 113 // Disable button and change to tertiarystyle.116 // Disable button and change to ban style. 114 117 this.$saveButton 115 118 .removeClass( 'btn-secondary' ) 116 .addClass( 'btn-tertiary' ); 119 .addClass( 'btn-ban' ) 120 .prop( 'disabled', true ); 117 121 } 118 122 }, … … 125 129 var apiKey = this.$apiKeyInput.val(); 126 130 127 // Check if button is disabled ( tertiarystate).128 if (this.$saveButton.hasClass( 'btn- tertiary' )) {131 // Check if button is disabled (ban state). 132 if (this.$saveButton.hasClass( 'btn-ban' )) { 129 133 return; 130 134 } … … 150 154 self.$saveButton 151 155 .removeClass( 'btn-secondary' ) 152 .addClass( 'btn- tertiary' )156 .addClass( 'btn-ban' ) 153 157 .prop( 'disabled', false ) 154 158 .text( readmoAiAdminData.i18n.saveChanges ); … … 231 235 }; 232 236 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 233 711 // Initialize on document ready. 234 712 $( document ).ready( 235 713 function () { 236 714 ReadmoAiAdmin.init(); 715 ReadmoAiAutoInsert.init(); 237 716 } 238 717 ); -
readmo-ai/trunk/class-readmo-ai-plugin.php
r3417155 r3447021 106 106 */ 107 107 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; 108 124 109 125 /** … … 158 174 require_once READMO_AI_PLUGIN_DIR . 'Controller/frontend/class-readmo-ai-shortcode-handler.php'; 159 175 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'; 160 178 161 179 // View Layer loaded on-demand by Controllers. … … 174 192 $this->ajax_handler = new Readmo_Ai_Ajax_Handler( $this->encryption, $this->admin_settings, $this->api_client ); 175 193 $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(); 176 195 177 196 // Initialize frontend if not in admin. … … 179 198 $this->frontend_assets = new Readmo_Ai_Frontend_Assets( $this->admin_settings ); 180 199 $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 ); 181 201 } 182 202 } -
readmo-ai/trunk/readme.txt
r3445269 r3447021 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 1.17 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html -
readmo-ai/trunk/readmo-ai.php
r3445269 r3447021 13 13 * Plugin Name: Readmo AI 14 14 * Description: AI-powered content analysis and optimization for WordPress with analytics tracking 15 * Version: 1. 1.115 * Version: 1.2.0 16 16 * Requires at least: 5.9 17 17 * Requires PHP: 7.4 … … 33 33 */ 34 34 if ( ! defined( 'READMO_AI_VERSION' ) ) { 35 define( 'READMO_AI_VERSION', '1. 1.1' );35 define( 'READMO_AI_VERSION', '1.2.0' ); 36 36 } 37 37 -
readmo-ai/trunk/uninstall.php
r3411004 r3447021 31 31 delete_option( 'readmo_ai_settings' ); 32 32 33 // Delete auto-insert settings option. 34 delete_option( 'readmo_ai_auto_insert' ); 35 33 36 // For multisite installations, delete site options as well. 34 37 if ( is_multisite() ) { 35 38 delete_site_option( 'readmo_ai_settings' ); 39 delete_site_option( 'readmo_ai_auto_insert' ); 36 40 } 37 41 }
Note: See TracChangeset
for help on using the changeset viewer.