PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.76
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.76
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Site_Preloader / Site_Preloader.php

Site_Preloader.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.76, at includes/extensions/Site_Preloader/Site_Preloader.php

1,414 lines 49.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Site Preloader Extension.
4 *
5 * Provides a customizable preloader/loading animation displayed during page load.
6 * Features include preset animations, custom styling, display rules, and Pro features
7 * like custom templates, advanced conditions, and AJAX navigation support.
8 *
9 * @package King_Addons
10 * @since 1.0.0
11 */
12
13 namespace King_Addons;
14
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 /**
20 * Site Preloader main class.
21 *
22 * Handles admin settings UI, frontend preloader rendering, and all related functionality.
23 */
24 final class Site_Preloader
25 {
26 /**
27 * Option key for storing preloader settings.
28 *
29 * @var string
30 */
31 public const OPTION_KEY = 'king_addons_site_preloader_settings';
32
33 /**
34 * Option key for storing preloader rules.
35 *
36 * @var string
37 */
38 public const RULES_OPTION_KEY = 'king_addons_site_preloader_rules';
39
40 /**
41 * Cookie name for tracking first visit.
42 *
43 * @var string
44 */
45 public const COOKIE_NAME = 'kng_preloader_shown';
46
47 /**
48 * Session storage key.
49 *
50 * @var string
51 */
52 public const SESSION_KEY = 'kng_preloader_session';
53
54 /**
55 * Singleton instance.
56 *
57 * @var Site_Preloader|null
58 */
59 private static ?Site_Preloader $instance = null;
60
61 /**
62 * Cached settings array.
63 *
64 * @var array<string, mixed>
65 */
66 private array $settings = [];
67
68 /**
69 * Available presets.
70 *
71 * @var array<string, array<string, mixed>>
72 */
73 private array $presets = [];
74
75 /**
76 * Returns the singleton instance.
77 *
78 * @return Site_Preloader
79 */
80 public static function instance(): Site_Preloader
81 {
82 if (is_null(self::$instance)) {
83 self::$instance = new self();
84 }
85 return self::$instance;
86 }
87
88 /**
89 * Constructor. Initializes hooks and loads settings.
90 */
91 public function __construct()
92 {
93 $this->settings = $this->get_settings();
94 $this->presets = $this->get_presets();
95
96 // Admin hooks
97 add_action('admin_menu', [$this, 'register_admin_menu'], 15);
98 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
99 add_action('admin_post_king_addons_site_preloader_save', [$this, 'handle_save_settings']);
100 add_action('admin_post_king_addons_site_preloader_export', [$this, 'handle_export']);
101 add_action('admin_post_king_addons_site_preloader_import', [$this, 'handle_import']);
102 add_action('admin_post_king_addons_site_preloader_reset', [$this, 'handle_reset']);
103
104 // Frontend hooks
105 add_action('wp_enqueue_scripts', [$this, 'maybe_enqueue_frontend_assets']);
106 add_action('wp_body_open', [$this, 'render_preloader_markup'], 1);
107 add_action('wp_footer', [$this, 'render_preloader_script'], 999);
108
109 // AJAX hooks
110 add_action('wp_ajax_king_addons_preloader_preview', [$this, 'ajax_preview']);
111 add_action('wp_ajax_king_addons_preloader_save_rule', [$this, 'ajax_save_rule']);
112 add_action('wp_ajax_king_addons_preloader_delete_rule', [$this, 'ajax_delete_rule']);
113 }
114
115 /**
116 * Registers the admin submenu page.
117 *
118 * @return void
119 */
120 public function register_admin_menu(): void
121 {
122 add_submenu_page(
123 'king-addons',
124 esc_html__('Site Preloader', 'king-addons'),
125 esc_html__('Site Preloader', 'king-addons'),
126 'manage_options',
127 'king-addons-site-preloader',
128 [$this, 'render_admin_page']
129 );
130 }
131
132 /**
133 * Enqueues admin assets for the settings page.
134 *
135 * @param string $hook Current admin page hook.
136 * @return void
137 */
138 public function enqueue_admin_assets(string $hook): void
139 {
140 if ($hook !== 'king-addons_page_king-addons-site-preloader') {
141 return;
142 }
143
144 // Shared V3 admin styles
145 wp_enqueue_style(
146 'king-addons-admin-v3',
147 KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css',
148 [],
149 KING_ADDONS_VERSION
150 );
151
152 // Frontend preloader animations (needed for admin preview)
153 wp_enqueue_style(
154 'king-addons-site-preloader-frontend',
155 KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/style.css',
156 [],
157 KING_ADDONS_VERSION
158 );
159
160 // Site Preloader specific admin styles
161 wp_enqueue_style(
162 'king-addons-site-preloader-admin',
163 KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/admin.css',
164 ['king-addons-admin-v3', 'king-addons-site-preloader-frontend'],
165 KING_ADDONS_VERSION
166 );
167
168 // Color picker
169 wp_enqueue_style('wp-color-picker');
170 wp_enqueue_script('wp-color-picker');
171
172 // Alpha-enabled WP Color Picker (same library used in Settings -> Lightbox Colors)
173 wp_enqueue_script(
174 'king-addons-wpcolorpicker-alpha',
175 KING_ADDONS_URL . 'includes/assets/libraries/wpcolorpicker/wpcolorpicker.js',
176 ['wp-color-picker'],
177 KING_ADDONS_VERSION,
178 true
179 );
180
181 // Media uploader
182 wp_enqueue_media();
183
184 // Admin JS
185 wp_enqueue_script(
186 'king-addons-site-preloader-admin',
187 KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/admin.js',
188 ['jquery', 'wp-color-picker', 'king-addons-wpcolorpicker-alpha'],
189 KING_ADDONS_VERSION,
190 true
191 );
192
193 wp_localize_script('king-addons-site-preloader-admin', 'kngPreloaderAdmin', [
194 'ajaxUrl' => admin_url('admin-ajax.php'),
195 'nonce' => wp_create_nonce('king_addons_site_preloader'),
196 'presets' => $this->presets,
197 'settings' => $this->settings,
198 'isPro' => $this->is_pro(),
199 'strings' => [
200 'selectImage' => esc_html__('Select Image', 'king-addons'),
201 'useImage' => esc_html__('Use This Image', 'king-addons'),
202 'saved' => esc_html__('Settings saved successfully!', 'king-addons'),
203 'error' => esc_html__('An error occurred. Please try again.', 'king-addons'),
204 'confirmReset' => esc_html__('Are you sure you want to reset all settings to defaults?', 'king-addons'),
205 'confirmDelete' => esc_html__('Are you sure you want to delete this rule?', 'king-addons'),
206 ],
207 ]);
208 }
209
210 /**
211 * Conditionally enqueues frontend assets.
212 *
213 * @return void
214 */
215 public function maybe_enqueue_frontend_assets(): void
216 {
217 if (!$this->should_show_preloader()) {
218 return;
219 }
220
221 wp_enqueue_style(
222 'king-addons-site-preloader',
223 KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/style.css',
224 [],
225 KING_ADDONS_VERSION
226 );
227
228 wp_enqueue_script(
229 'king-addons-site-preloader',
230 KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/script.js',
231 [],
232 KING_ADDONS_VERSION,
233 false // Load in header for immediate availability
234 );
235 }
236
237 /**
238 * Renders the admin settings page.
239 *
240 * @return void
241 */
242 public function render_admin_page(): void
243 {
244 if (!current_user_can('manage_options')) {
245 return;
246 }
247
248 $this->settings = $this->get_settings();
249 $settings = $this->settings; // For templates
250 $is_pro = $this->is_pro();
251 $presets = $this->presets;
252 $rules = $this->get_rules();
253 $current_tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'dashboard';
254
255 include __DIR__ . '/templates/admin-page.php';
256 }
257
258 /**
259 * Handles saving settings from the admin form.
260 *
261 * @return void
262 */
263 public function handle_save_settings(): void
264 {
265 if (!current_user_can('manage_options')) {
266 wp_die(esc_html__('Unauthorized access', 'king-addons'));
267 }
268
269 check_admin_referer('king_addons_site_preloader_save');
270
271 $sanitized = $this->sanitize_settings($_POST);
272 update_option(self::OPTION_KEY, $sanitized);
273 $this->settings = $sanitized;
274
275 wp_safe_redirect(add_query_arg([
276 'page' => 'king-addons-site-preloader',
277 'tab' => isset($_POST['current_tab']) ? sanitize_key($_POST['current_tab']) : 'settings',
278 'updated' => 'true',
279 ], admin_url('admin.php')));
280 exit;
281 }
282
283 /**
284 * Handles exporting settings as JSON.
285 *
286 * @return void
287 */
288 public function handle_export(): void
289 {
290 if (!current_user_can('manage_options')) {
291 wp_die(esc_html__('Unauthorized access', 'king-addons'));
292 }
293
294 if (!$this->is_pro()) {
295 wp_die(esc_html__('Export is a Pro feature', 'king-addons'));
296 }
297
298 check_admin_referer('king_addons_site_preloader_export');
299
300 $export_data = [
301 'version' => KING_ADDONS_VERSION,
302 'settings' => $this->settings,
303 'rules' => $this->get_rules(),
304 ];
305
306 $json = wp_json_encode($export_data, JSON_PRETTY_PRINT);
307
308 nocache_headers();
309 header('Content-Type: application/json; charset=utf-8');
310 header('Content-Disposition: attachment; filename=site-preloader-settings-' . gmdate('Y-m-d') . '.json');
311 echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
312 exit;
313 }
314
315 /**
316 * Handles importing settings from JSON.
317 *
318 * @return void
319 */
320 public function handle_import(): void
321 {
322 if (!current_user_can('manage_options')) {
323 wp_die(esc_html__('Unauthorized access', 'king-addons'));
324 }
325
326 if (!$this->is_pro()) {
327 wp_die(esc_html__('Import is a Pro feature', 'king-addons'));
328 }
329
330 check_admin_referer('king_addons_site_preloader_import');
331
332 if (empty($_FILES['import_file']['tmp_name'])) {
333 wp_safe_redirect(add_query_arg([
334 'page' => 'king-addons-site-preloader',
335 'tab' => 'import-export',
336 'error' => 'no-file',
337 ], admin_url('admin.php')));
338 exit;
339 }
340
341 $file_content = file_get_contents($_FILES['import_file']['tmp_name']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
342 $import_data = json_decode($file_content, true);
343
344 if (json_last_error() !== JSON_ERROR_NONE || !is_array($import_data)) {
345 wp_safe_redirect(add_query_arg([
346 'page' => 'king-addons-site-preloader',
347 'tab' => 'import-export',
348 'error' => 'invalid-json',
349 ], admin_url('admin.php')));
350 exit;
351 }
352
353 // Import settings
354 if (!empty($import_data['settings'])) {
355 $sanitized = $this->sanitize_settings($import_data['settings']);
356 update_option(self::OPTION_KEY, $sanitized);
357 }
358
359 // Import rules
360 if (!empty($import_data['rules']) && is_array($import_data['rules'])) {
361 update_option(self::RULES_OPTION_KEY, $import_data['rules']);
362 }
363
364 wp_safe_redirect(add_query_arg([
365 'page' => 'king-addons-site-preloader',
366 'tab' => 'import-export',
367 'imported' => 'true',
368 ], admin_url('admin.php')));
369 exit;
370 }
371
372 /**
373 * Handles resetting settings to defaults.
374 *
375 * @return void
376 */
377 public function handle_reset(): void
378 {
379 if (!current_user_can('manage_options')) {
380 wp_die(esc_html__('Unauthorized access', 'king-addons'));
381 }
382
383 check_admin_referer('king_addons_site_preloader_reset');
384
385 delete_option(self::OPTION_KEY);
386 delete_option(self::RULES_OPTION_KEY);
387
388 wp_safe_redirect(add_query_arg([
389 'page' => 'king-addons-site-preloader',
390 'tab' => 'import-export',
391 'reset' => 'true',
392 ], admin_url('admin.php')));
393 exit;
394 }
395
396 /**
397 * AJAX handler for live preview.
398 *
399 * @return void
400 */
401 public function ajax_preview(): void
402 {
403 check_ajax_referer('king_addons_site_preloader', 'nonce');
404
405 if (!current_user_can('manage_options')) {
406 wp_send_json_error(['message' => 'Unauthorized']);
407 }
408
409 $preset_id = isset($_POST['preset']) ? sanitize_key($_POST['preset']) : 'spinner-circle';
410 $settings = isset($_POST['settings']) ? $this->sanitize_settings($_POST['settings']) : [];
411
412 ob_start();
413 $this->render_preset_html($preset_id, $settings);
414 $html = ob_get_clean();
415
416 wp_send_json_success([
417 'html' => $html,
418 'css' => $this->generate_inline_css($settings),
419 ]);
420 }
421
422 /**
423 * AJAX handler for saving a display rule.
424 *
425 * @return void
426 */
427 public function ajax_save_rule(): void
428 {
429 check_ajax_referer('king_addons_site_preloader', 'nonce');
430
431 if (!current_user_can('manage_options')) {
432 wp_send_json_error(['message' => 'Unauthorized']);
433 }
434
435 $rules = $this->get_rules();
436 $rule_id = isset($_POST['rule_id']) ? sanitize_text_field($_POST['rule_id']) : '';
437
438 // Generate new ID if empty or starts with 'new_'
439 $is_new_rule = empty($rule_id) || strpos($rule_id, 'new_') === 0;
440 $final_rule_id = $is_new_rule ? 'rule_' . wp_generate_uuid4() : $rule_id;
441
442 // Parse pages array
443 $pages = [];
444 if (isset($_POST['pages'])) {
445 if (is_array($_POST['pages'])) {
446 $pages = array_map('absint', $_POST['pages']);
447 } else {
448 $pages = array_map('absint', explode(',', sanitize_text_field($_POST['pages'])));
449 }
450 $pages = array_filter($pages);
451 }
452
453 // Get action from 'rule_action' parameter (since 'action' is reserved for AJAX action name)
454 $rule_action = isset($_POST['rule_action']) ? sanitize_key($_POST['rule_action']) : 'show';
455
456 $rule = [
457 'id' => $final_rule_id,
458 'enabled' => isset($_POST['enabled']) && $_POST['enabled'] === '1',
459 'action' => $rule_action, // show / hide / override
460 'condition' => isset($_POST['condition']) ? sanitize_key($_POST['condition']) : 'specific_pages',
461 'condition_value' => isset($_POST['condition_value']) ? sanitize_text_field($_POST['condition_value']) : '',
462 'pages' => $pages,
463 'priority' => isset($_POST['priority']) ? absint($_POST['priority']) : 10,
464 'template' => isset($_POST['template']) ? sanitize_key($_POST['template']) : '',
465 'override_colors' => isset($_POST['override_colors']) && $_POST['override_colors'] === '1',
466 'bg_color' => $this->sanitize_color_value($_POST['bg_color'] ?? '', '#ffffff'),
467 'accent_color' => $this->sanitize_color_value($_POST['accent_color'] ?? '', '#0071e3'),
468 ];
469
470 if (!$is_new_rule) {
471 // Update existing rule
472 $found = false;
473 foreach ($rules as $key => $existing_rule) {
474 if ($existing_rule['id'] === $rule_id) {
475 $rules[$key] = $rule;
476 $found = true;
477 break;
478 }
479 }
480 // If not found, add as new
481 if (!$found) {
482 $rules[] = $rule;
483 }
484 } else {
485 // Add new rule
486 $rules[] = $rule;
487 }
488
489 update_option(self::RULES_OPTION_KEY, $rules);
490
491 wp_send_json_success(['rule' => $rule, 'rules' => $rules]);
492 }
493
494 /**
495 * AJAX handler for deleting a display rule.
496 *
497 * @return void
498 */
499 public function ajax_delete_rule(): void
500 {
501 check_ajax_referer('king_addons_site_preloader', 'nonce');
502
503 if (!current_user_can('manage_options')) {
504 wp_send_json_error(['message' => 'Unauthorized']);
505 }
506
507 $rule_id = isset($_POST['rule_id']) ? sanitize_key($_POST['rule_id']) : '';
508 if (empty($rule_id)) {
509 wp_send_json_error(['message' => 'No rule ID provided']);
510 }
511
512 $rules = $this->get_rules();
513 $rules = array_filter($rules, function ($rule) use ($rule_id) {
514 return $rule['id'] !== $rule_id;
515 });
516 $rules = array_values($rules);
517
518 update_option(self::RULES_OPTION_KEY, $rules);
519
520 wp_send_json_success(['rules' => $rules]);
521 }
522
523 /**
524 * Renders the preloader HTML markup in wp_body_open.
525 *
526 * @return void
527 */
528 public function render_preloader_markup(): void
529 {
530 if (!$this->should_show_preloader()) {
531 return;
532 }
533
534 $settings = $this->settings;
535
536 // Check if any rule overrides the settings
537 $rule_result = $this->get_rule_action();
538 $matched_rule = $rule_result['rule'];
539 $action = $rule_result['action'];
540
541 // Apply overrides only for "override" action
542 if ($action === 'override' && $matched_rule) {
543 // Override template if rule has one
544 if (!empty($matched_rule['template'])) {
545 $settings['template'] = $matched_rule['template'];
546 }
547 // Override colors if rule specifies
548 if (!empty($matched_rule['override_colors'])) {
549 if (!empty($matched_rule['bg_color'])) {
550 $settings['bg_color'] = $matched_rule['bg_color'];
551 }
552 if (!empty($matched_rule['accent_color'])) {
553 $settings['accent_color'] = $matched_rule['accent_color'];
554 }
555 }
556 }
557
558 $preset_id = $settings['template'] ?? 'spinner-circle';
559
560 $trigger_type = $settings['trigger_type'] ?? 'always';
561 $cookie_name = self::COOKIE_NAME . '_' . sanitize_key($trigger_type);
562
563 $data_attrs = [
564 'data-preset' => $preset_id,
565 'data-trigger-type' => $trigger_type,
566 'data-cookie-name' => $cookie_name,
567 'data-hide-strategy' => $settings['hide_strategy'] ?? 'window_load',
568 'data-min-display-time' => absint($settings['min_display_time'] ?? 500),
569 'data-max-display-time' => absint($settings['max_display_time'] ?? 5000),
570 'data-hide-animation' => $settings['hide_animation'] ?? 'fade',
571 'data-animation-duration' => absint($settings['transition_duration'] ?? 400),
572 'data-allow-skip' => !empty($settings['allow_skip']) ? 'true' : 'false',
573 'data-skip-method' => $settings['skip_method'] ?? 'click',
574 'data-lock-scroll' => !empty($settings['lock_scroll']) ? 'true' : 'false',
575 ];
576
577 $data_string = '';
578 foreach ($data_attrs as $attr => $value) {
579 $data_string .= ' ' . esc_attr($attr) . '="' . esc_attr($value) . '"';
580 }
581
582 // Debug: output matched rule info as HTML comment
583 $debug_info = '';
584 if (defined('WP_DEBUG') && WP_DEBUG && $matched_rule) {
585 $debug_info = sprintf(
586 '<!-- KNG Preloader: Matched rule ID=%s, action=%s, template=%s, condition=%s -->',
587 esc_html($matched_rule['id'] ?? 'unknown'),
588 esc_html($action),
589 esc_html($matched_rule['template'] ?? 'default'),
590 esc_html($matched_rule['condition'] ?? '')
591 );
592 }
593 echo $debug_info;
594 ?>
595 <div id="kng-site-preloader" class="kng-site-preloader"<?php echo $data_string; ?> style="<?php echo esc_attr($this->get_inline_style($settings)); ?>"><?php /* Using template: <?php echo esc_html($preset_id); ?> */ ?>
596 <div class="kng-site-preloader__overlay"></div>
597 <div class="kng-site-preloader__content">
598 <?php $this->render_preset_html($preset_id, $settings); ?>
599 </div>
600 </div>
601 <?php
602 }
603
604 /**
605 * Gets the first matching rule for current page.
606 * Rules are already sorted by priority (lower = higher priority).
607 *
608 * @return array|null The matching rule or null.
609 */
610 private function get_matching_rule(): ?array
611 {
612 $rules = $this->get_rules();
613
614 // Sort by priority (lower number = higher priority)
615 usort($rules, function($a, $b) {
616 $priority_a = intval($a['priority'] ?? 10);
617 $priority_b = intval($b['priority'] ?? 10);
618 return $priority_a - $priority_b;
619 });
620
621 foreach ($rules as $rule) {
622 // Skip disabled rules
623 if (empty($rule['enabled'])) {
624 continue;
625 }
626
627 // Check if rule matches current page
628 if ($this->evaluate_rule_condition($rule)) {
629 return $rule;
630 }
631 }
632
633 return null;
634 }
635
636 /**
637 * Get the action from a matching rule (show/hide/override).
638 *
639 * @return array{action: string, rule: array|null} Action and rule data.
640 */
641 private function get_rule_action(): array
642 {
643 $rule = $this->get_matching_rule();
644
645 if ($rule === null) {
646 return ['action' => 'default', 'rule' => null];
647 }
648
649 $action = $rule['action'] ?? 'show';
650
651 return ['action' => $action, 'rule' => $rule];
652 }
653
654 /**
655 * Renders the preloader initialization script.
656 *
657 * @return void
658 */
659 public function render_preloader_script(): void
660 {
661 // Script auto-initializes, no need for footer script
662 // Config is passed via data attributes on the preloader element
663 }
664
665 /**
666 * Generates inline CSS for preview styling.
667 *
668 * @param array<string,mixed> $settings Current settings.
669 * @return string Inline CSS styles.
670 */
671 public function get_preview_styles(array $settings): string
672 {
673 $bg_color = $settings['bg_color'] ?? '#ffffff';
674 $accent_color = $settings['accent_color'] ?? '#0071e3';
675 $text_color = $settings['text_color'] ?? '#1d1d1f';
676 $animation_size = absint($settings['spinner_size'] ?? 48);
677
678 return sprintf(
679 '--kng-preloader-bg: %s; --kng-preloader-accent: %s; --kng-preloader-text: %s; --kng-preloader-size: %dpx; background-color: %s;',
680 esc_attr($bg_color),
681 esc_attr($accent_color),
682 esc_attr($text_color),
683 $animation_size,
684 esc_attr($bg_color)
685 );
686 }
687
688 /**
689 * Renders the HTML for a specific preset.
690 *
691 * @param string $preset_id The preset identifier.
692 * @param array<string,mixed> $settings Current settings.
693 * @return void
694 */
695 public function render_preset_html(string $preset_id, array $settings): void
696 {
697 $logo_enabled = !empty($settings['logo_enabled']);
698 $logo_url = $settings['logo_url'] ?? '';
699 $text_enabled = !empty($settings['text_enabled']);
700 $text_content = $settings['text_content'] ?? '';
701
702 // Logo
703 if ($logo_enabled && !empty($logo_url)) {
704 echo '<div class="kng-site-preloader__logo">';
705 echo '<img src="' . esc_url($logo_url) . '" alt="' . esc_attr__('Loading', 'king-addons') . '" />';
706 echo '</div>';
707 }
708
709 // Preset animation
710 echo '<div class="kng-site-preloader__animation kng-site-preloader__animation--' . esc_attr($preset_id) . '">';
711
712 switch ($preset_id) {
713 case 'spinner-circle':
714 echo '<div class="kng-preloader-spinner-circle"></div>';
715 break;
716
717 case 'dual-ring':
718 echo '<div class="kng-preloader-dual-ring"></div>';
719 break;
720
721 case 'dots-bounce':
722 echo '<div class="kng-preloader-dots-bounce">';
723 echo '<span></span><span></span><span></span>';
724 echo '</div>';
725 break;
726
727 case 'bar-loader':
728 echo '<div class="kng-preloader-bar-loader">';
729 echo '<div class="kng-preloader-bar-loader__bar"></div>';
730 echo '</div>';
731 break;
732
733 case 'pulse-logo':
734 echo '<div class="kng-preloader-pulse-logo">';
735 if ($logo_enabled && !empty($logo_url)) {
736 echo '<img src="' . esc_url($logo_url) . '" alt="" />';
737 } else {
738 echo '<div class="kng-preloader-pulse-logo__circle"></div>';
739 }
740 echo '</div>';
741 break;
742
743 case 'minimal-line':
744 echo '<div class="kng-preloader-minimal-line">';
745 echo '<div class="kng-preloader-minimal-line__track">';
746 echo '<div class="kng-preloader-minimal-line__bar"></div>';
747 echo '</div>';
748 echo '</div>';
749 break;
750
751 case 'gradient-spinner':
752 echo '<div class="kng-preloader-gradient-spinner"></div>';
753 break;
754
755 case 'fade-text':
756 echo '<div class="kng-preloader-fade-text">';
757 $loading_text = !empty($text_content) ? $text_content : __('Loading', 'king-addons');
758 if (function_exists('mb_str_split')) {
759 $chars = mb_str_split($loading_text);
760 } else {
761 $chars = preg_split('//u', $loading_text, -1, PREG_SPLIT_NO_EMPTY);
762 }
763 if (!is_array($chars)) {
764 $chars = str_split($loading_text);
765 }
766 foreach ($chars as $index => $char) {
767 echo '<span style="animation-delay: ' . (0.1 * $index) . 's">' . esc_html($char) . '</span>';
768 }
769 echo '</div>';
770 break;
771
772 case 'cube-grid':
773 echo '<div class="kng-preloader-cube-grid">';
774 for ($i = 1; $i <= 9; $i++) {
775 echo '<div class="kng-preloader-cube-grid__cube"></div>';
776 }
777 echo '</div>';
778 break;
779
780 case 'wave-bars':
781 echo '<div class="kng-preloader-wave-bars">';
782 for ($i = 1; $i <= 5; $i++) {
783 echo '<div class="kng-preloader-wave-bars__bar"></div>';
784 }
785 echo '</div>';
786 break;
787
788 case 'rotating-squares':
789 echo '<div class="kng-preloader-rotating-squares">';
790 echo '<div class="kng-preloader-rotating-squares__square"></div>';
791 echo '<div class="kng-preloader-rotating-squares__square"></div>';
792 echo '</div>';
793 break;
794
795 case 'morphing-circle':
796 echo '<div class="kng-preloader-morphing-circle"></div>';
797 break;
798
799 default:
800 echo '<div class="kng-preloader-spinner-circle"></div>';
801 }
802
803 echo '</div>';
804
805 // Text
806 if ($text_enabled && !empty($text_content) && $preset_id !== 'fade-text') {
807 echo '<div class="kng-site-preloader__text">' . esc_html($text_content) . '</div>';
808 }
809 }
810
811 /**
812 * Generates inline CSS based on settings.
813 *
814 * @param array<string,mixed> $settings Current settings.
815 * @return string
816 */
817 private function generate_inline_css(array $settings): string
818 {
819 $css = '';
820
821 // Background
822 $bg_type = $settings['bg_type'] ?? 'solid';
823 $bg_color = $settings['bg_color'] ?? 'rgba(0,0,0,0)';
824 $bg_gradient_start = $settings['bg_gradient_start'] ?? '#ffffff';
825 $bg_gradient_end = $settings['bg_gradient_end'] ?? '#f5f5f7';
826 $bg_image = $settings['bg_image'] ?? '';
827 $overlay_opacity = isset($settings['overlay_opacity']) ? floatval($settings['overlay_opacity']) : 1;
828
829 $css .= '.kng-site-preloader__overlay {';
830
831 if ($bg_type === 'gradient') {
832 $css .= 'background: linear-gradient(135deg, ' . $bg_gradient_start . ' 0%, ' . $bg_gradient_end . ' 100%);';
833 } elseif ($bg_type === 'image' && !empty($bg_image)) {
834 $css .= 'background-image: url(' . esc_url($bg_image) . ');';
835 $css .= 'background-size: cover;';
836 $css .= 'background-position: center;';
837 } else {
838 $css .= 'background-color: ' . $bg_color . ';';
839 }
840
841 $css .= 'opacity: ' . $overlay_opacity . ';';
842 $css .= '}';
843
844 // Accent color
845 $accent_color = $settings['accent_color'] ?? '#0071e3';
846 $css .= '.kng-site-preloader { --kng-preloader-accent: ' . $accent_color . '; }';
847
848 // Spinner size
849 $spinner_size = isset($settings['spinner_size']) ? absint($settings['spinner_size']) : 48;
850 $css .= '.kng-site-preloader { --kng-preloader-size: ' . $spinner_size . 'px; }';
851
852 // Text styles
853 if (!empty($settings['text_enabled'])) {
854 $text_color = $settings['text_color'] ?? '#1d1d1f';
855 $text_size = isset($settings['text_size']) ? absint($settings['text_size']) : 16;
856 $text_weight = $settings['text_weight'] ?? '400';
857
858 $css .= '.kng-site-preloader__text {';
859 $css .= 'color: ' . $text_color . ';';
860 $css .= 'font-size: ' . $text_size . 'px;';
861 $css .= 'font-weight: ' . $text_weight . ';';
862 $css .= '}';
863 }
864
865 // Logo styles
866 if (!empty($settings['logo_enabled']) && !empty($settings['logo_max_width'])) {
867 $logo_max_width = absint($settings['logo_max_width']);
868 $css .= '.kng-site-preloader__logo img { max-width: ' . $logo_max_width . 'px; }';
869 }
870
871 return $css;
872 }
873
874 /**
875 * Gets inline style attribute value.
876 *
877 * @param array|null $settings Settings to use (optional, uses class settings if not provided).
878 * @return string
879 */
880 private function get_inline_style(?array $settings = null): string
881 {
882 $settings = $settings ?? $this->settings;
883 $z_index = isset($settings['z_index']) ? absint($settings['z_index']) : 999999;
884 $bg_color = $settings['bg_color'] ?? '#ffffff';
885 $accent_color = $settings['accent_color'] ?? '#0071e3';
886 $text_color = $settings['text_color'] ?? '#1d1d1f';
887 $spinner_size = absint($settings['spinner_size'] ?? 48);
888
889 return sprintf(
890 'z-index: %d; --kng-preloader-bg: %s; --kng-preloader-accent: %s; --kng-preloader-text: %s; --kng-preloader-size: %dpx;',
891 $z_index,
892 esc_attr($bg_color),
893 esc_attr($accent_color),
894 esc_attr($text_color),
895 $spinner_size
896 );
897 }
898
899 /**
900 * Determines if the preloader should be displayed.
901 *
902 * @return bool
903 */
904 private function should_show_preloader(): bool
905 {
906 $settings = $this->settings;
907
908 // Check if enabled
909 if (empty($settings['enabled'])) {
910 return false;
911 }
912
913 // Don't show in admin
914 if (is_admin()) {
915 return false;
916 }
917
918 // Don't show on Elementor editor/preview
919 if (
920 (isset($_GET['elementor-preview']) && $_GET['elementor-preview']) ||
921 (isset($_GET['preview']) && $_GET['preview'] === 'true') ||
922 (class_exists('\Elementor\Plugin') && \Elementor\Plugin::$instance->preview->is_preview_mode())
923 ) {
924 return false;
925 }
926
927 // Don't show on login page
928 if (strpos($_SERVER['REQUEST_URI'], 'wp-login.php') !== false) {
929 return false;
930 }
931
932 // Don't show on admin-ajax
933 if (defined('DOING_AJAX') && DOING_AJAX) {
934 return false;
935 }
936
937 // Don't show on REST API
938 if (defined('REST_REQUEST') && REST_REQUEST) {
939 return false;
940 }
941
942 // Check trigger type (cookie-based; avoids PHP session dependency)
943 $trigger_type = $settings['trigger_type'] ?? 'always';
944 $cookie_name = self::COOKIE_NAME . '_' . sanitize_key($trigger_type);
945
946 if ($trigger_type === 'first_visit') {
947 if (isset($_COOKIE[$cookie_name])) {
948 return false;
949 }
950 } elseif ($trigger_type === 'once_per_session') {
951 // Session cookie is handled on the frontend; we only need to check presence.
952 if (isset($_COOKIE[$cookie_name])) {
953 return false;
954 }
955 } elseif ($trigger_type === 'once_per_day') {
956 // Cookie value must be a unix timestamp (seconds).
957 $cookie_time = isset($_COOKIE[$cookie_name]) ? intval($_COOKIE[$cookie_name]) : 0;
958 if ($cookie_time && (time() - $cookie_time) < DAY_IN_SECONDS) {
959 return false;
960 }
961 }
962
963 // Check device visibility
964 $device_desktop = $settings['device_desktop'] ?? true;
965 $device_tablet = $settings['device_tablet'] ?? true;
966 $device_mobile = $settings['device_mobile'] ?? true;
967
968 if (!$device_desktop && !$device_tablet && !$device_mobile) {
969 return false;
970 }
971
972 // Check user visibility
973 $show_for = $settings['show_for'] ?? 'everyone';
974
975 if ($show_for === 'guests' && is_user_logged_in()) {
976 return false;
977 } elseif ($show_for === 'logged_in' && !is_user_logged_in()) {
978 return false;
979 }
980
981 // Check display rules
982 $show_on = $settings['show_on'] ?? 'all';
983
984 if ($show_on === 'selected') {
985 $selected_pages = $settings['selected_pages'] ?? [];
986 $current_page_id = get_queried_object_id();
987
988 if (!in_array($current_page_id, $selected_pages, true)) {
989 return false;
990 }
991 } elseif ($show_on === 'exclude') {
992 $excluded_pages = $settings['excluded_pages'] ?? [];
993 $current_page_id = get_queried_object_id();
994
995 if (in_array($current_page_id, $excluded_pages, true)) {
996 return false;
997 }
998 }
999
1000 // Check custom rules with new action system
1001 $rule_result = $this->get_rule_action();
1002 $action = $rule_result['action'];
1003
1004 // If a "hide" rule matches, don't show preloader
1005 if ($action === 'hide') {
1006 return false;
1007 }
1008
1009 // "show" and "override" actions allow the preloader to show
1010 // "default" means no matching rule, use global settings
1011
1012 return true;
1013 }
1014
1015 /**
1016 * Evaluates a rule condition.
1017 *
1018 * @param array<string,mixed> $rule The rule to evaluate.
1019 * @return bool
1020 */
1021 private function evaluate_rule_condition(array $rule): bool
1022 {
1023 $condition = $rule['condition'] ?? '';
1024 $value = $rule['condition_value'] ?? '';
1025 $pages = $rule['pages'] ?? [];
1026 $current_url = $_SERVER['REQUEST_URI'] ?? '';
1027
1028 switch ($condition) {
1029 // Specific pages by ID
1030 case 'specific_pages':
1031 if (empty($pages)) {
1032 return false;
1033 }
1034 $current_id = get_queried_object_id();
1035 return in_array($current_id, array_map('intval', $pages));
1036
1037 // All pages (not posts, not archives)
1038 case 'all_pages':
1039 return is_page();
1040
1041 // Front page
1042 case 'front_page':
1043 return is_front_page();
1044
1045 // Blog page (posts listing)
1046 case 'blog_page':
1047 return is_home();
1048
1049 // All single posts
1050 case 'all_posts':
1051 return is_single() && get_post_type() === 'post';
1052
1053 // Specific post type
1054 case 'post_type':
1055 return is_singular($value);
1056
1057 // Archive pages
1058 case 'archive':
1059 return is_archive();
1060
1061 // Search results
1062 case 'search':
1063 return is_search();
1064
1065 // 404 page
1066 case '404':
1067 return is_404();
1068
1069 // URL contains string
1070 case 'url_contains':
1071 return !empty($value) && strpos($current_url, $value) !== false;
1072
1073 // URL equals exactly
1074 case 'url_equals':
1075 return $current_url === $value || parse_url($current_url, PHP_URL_PATH) === $value;
1076
1077 default:
1078 return false;
1079 }
1080 }
1081
1082 /**
1083 * Gets the available presets.
1084 *
1085 * @return array<string, array<string, mixed>>
1086 */
1087 private function get_presets(): array
1088 {
1089 return [
1090 'spinner-circle' => [
1091 'id' => 'spinner-circle',
1092 'title' => __('Spinner Circle', 'king-addons'),
1093 'description' => __('Classic circular spinner animation', 'king-addons'),
1094 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-spinner-circle.svg',
1095 'pro' => false,
1096 ],
1097 'dual-ring' => [
1098 'id' => 'dual-ring',
1099 'title' => __('Dual Ring', 'king-addons'),
1100 'description' => __('Two concentric rotating rings', 'king-addons'),
1101 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-dual-ring.svg',
1102 'pro' => false,
1103 ],
1104 'dots-bounce' => [
1105 'id' => 'dots-bounce',
1106 'title' => __('Dots Bounce', 'king-addons'),
1107 'description' => __('Three bouncing dots animation', 'king-addons'),
1108 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-dots-bounce.svg',
1109 'pro' => false,
1110 ],
1111 'bar-loader' => [
1112 'id' => 'bar-loader',
1113 'title' => __('Bar Loader', 'king-addons'),
1114 'description' => __('Horizontal progress bar animation', 'king-addons'),
1115 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-bar-loader.svg',
1116 'pro' => false,
1117 ],
1118 'pulse-logo' => [
1119 'id' => 'pulse-logo',
1120 'title' => __('Pulse Logo', 'king-addons'),
1121 'description' => __('Pulsating logo or circle effect', 'king-addons'),
1122 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-pulse-logo.svg',
1123 'pro' => false,
1124 ],
1125 'minimal-line' => [
1126 'id' => 'minimal-line',
1127 'title' => __('Minimal Line', 'king-addons'),
1128 'description' => __('Sleek minimal line animation', 'king-addons'),
1129 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-minimal-line.svg',
1130 'pro' => false,
1131 ],
1132 'gradient-spinner' => [
1133 'id' => 'gradient-spinner',
1134 'title' => __('Gradient Spinner', 'king-addons'),
1135 'description' => __('Spinner with gradient color effect', 'king-addons'),
1136 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-gradient-spinner.svg',
1137 'pro' => false,
1138 ],
1139 'fade-text' => [
1140 'id' => 'fade-text',
1141 'title' => __('Fade Text', 'king-addons'),
1142 'description' => __('Loading text with letter fade animation', 'king-addons'),
1143 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-fade-text.svg',
1144 'pro' => false,
1145 ],
1146 'cube-grid' => [
1147 'id' => 'cube-grid',
1148 'title' => __('Cube Grid', 'king-addons'),
1149 'description' => __('3x3 grid of animated cubes', 'king-addons'),
1150 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-cube-grid.svg',
1151 'pro' => false,
1152 ],
1153 'wave-bars' => [
1154 'id' => 'wave-bars',
1155 'title' => __('Wave Bars', 'king-addons'),
1156 'description' => __('Vertical bars with wave animation', 'king-addons'),
1157 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-wave-bars.svg',
1158 'pro' => false,
1159 ],
1160 'rotating-squares' => [
1161 'id' => 'rotating-squares',
1162 'title' => __('Rotating Squares', 'king-addons'),
1163 'description' => __('Two squares rotating in sync', 'king-addons'),
1164 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-rotating-squares.svg',
1165 'pro' => false,
1166 ],
1167 'morphing-circle' => [
1168 'id' => 'morphing-circle',
1169 'title' => __('Morphing Circle', 'king-addons'),
1170 'description' => __('Shape-shifting circle animation', 'king-addons'),
1171 'thumbnail' => KING_ADDONS_URL . 'includes/extensions/Site_Preloader/assets/images/preset-morphing-circle.svg',
1172 'pro' => false,
1173 ],
1174 ];
1175 }
1176
1177 /**
1178 * Gets the default settings.
1179 *
1180 * @return array<string, mixed>
1181 */
1182 private function get_default_settings(): array
1183 {
1184 return [
1185 // General
1186 'enabled' => false,
1187 'template' => 'spinner-circle',
1188 'show_on' => 'all',
1189 'selected_pages' => [],
1190 'excluded_pages' => [],
1191 'show_for' => 'everyone',
1192 'device_desktop' => true,
1193 'device_tablet' => true,
1194 'device_mobile' => true,
1195
1196 // Appearance - Background
1197 'bg_type' => 'solid',
1198 'bg_color' => 'rgba(0,0,0,0)',
1199 'bg_gradient_start' => '#ffffff',
1200 'bg_gradient_end' => '#f5f5f7',
1201 'bg_image' => '',
1202 'overlay_opacity' => 1,
1203
1204 // Appearance - Logo
1205 'logo_enabled' => false,
1206 'logo_url' => '',
1207 'logo_max_width' => 120,
1208
1209 // Appearance - Text
1210 'text_enabled' => false,
1211 'text_content' => '',
1212 'text_size' => 16,
1213 'text_weight' => '400',
1214 'text_color' => '#1d1d1f',
1215
1216 // Appearance - Animation
1217 'accent_color' => '#0071e3',
1218 'spinner_size' => 48,
1219
1220 // Behavior
1221 'trigger_type' => 'always',
1222 'hide_strategy' => 'window_load',
1223 'min_display_time' => 500,
1224 'max_display_time' => 5000,
1225 'allow_skip' => false,
1226 'skip_method' => 'click',
1227 'lock_scroll' => true,
1228
1229 // Transitions
1230 'show_animation' => 'fade',
1231 'hide_animation' => 'fade',
1232 'transition_duration' => 400,
1233 'easing' => 'ease-out',
1234
1235 // Advanced
1236 'z_index' => 999999,
1237 'custom_css' => '',
1238 ];
1239 }
1240
1241 /**
1242 * Gets current settings with defaults merged.
1243 *
1244 * @return array<string, mixed>
1245 */
1246 private function get_settings(): array
1247 {
1248 $saved = get_option(self::OPTION_KEY, []);
1249 return wp_parse_args($saved, $this->get_default_settings());
1250 }
1251
1252 /**
1253 * Gets saved display rules.
1254 *
1255 * @return array<int, array<string, mixed>>
1256 */
1257 private function get_rules(): array
1258 {
1259 return get_option(self::RULES_OPTION_KEY, []);
1260 }
1261
1262 /**
1263 * Sanitizes settings input.
1264 *
1265 * @param array<string,mixed> $input Raw input data.
1266 * @return array<string,mixed>
1267 */
1268 private function sanitize_settings(array $input): array
1269 {
1270 $sanitized = [];
1271
1272 // General
1273 $sanitized['enabled'] = !empty($input['enabled']);
1274 $sanitized['template'] = isset($input['template']) ? sanitize_key($input['template']) : 'spinner-circle';
1275 $sanitized['show_on'] = isset($input['show_on']) ? sanitize_key($input['show_on']) : 'all';
1276 $sanitized['selected_pages'] = isset($input['selected_pages']) && is_array($input['selected_pages'])
1277 ? array_map('absint', $input['selected_pages'])
1278 : [];
1279 $sanitized['excluded_pages'] = isset($input['excluded_pages']) && is_array($input['excluded_pages'])
1280 ? array_map('absint', $input['excluded_pages'])
1281 : [];
1282 $sanitized['show_for'] = isset($input['show_for']) ? sanitize_key($input['show_for']) : 'everyone';
1283 $sanitized['device_desktop'] = !empty($input['device_desktop']);
1284 $sanitized['device_tablet'] = !empty($input['device_tablet']);
1285 $sanitized['device_mobile'] = !empty($input['device_mobile']);
1286
1287 // Background
1288 $sanitized['bg_type'] = isset($input['bg_type']) ? sanitize_key($input['bg_type']) : 'solid';
1289 $sanitized['bg_color'] = $this->sanitize_color_value($input['bg_color'] ?? '', 'rgba(0,0,0,0)');
1290 $sanitized['bg_gradient_start'] = $this->sanitize_color_value($input['bg_gradient_start'] ?? '', '#ffffff');
1291 $sanitized['bg_gradient_end'] = $this->sanitize_color_value($input['bg_gradient_end'] ?? '', '#f5f5f7');
1292 $sanitized['bg_image'] = isset($input['bg_image']) ? esc_url_raw($input['bg_image']) : '';
1293 $sanitized['overlay_opacity'] = isset($input['overlay_opacity']) ? max(0, min(1, floatval($input['overlay_opacity']))) : 1;
1294
1295 // Logo
1296 $sanitized['logo_enabled'] = !empty($input['logo_enabled']);
1297 $sanitized['logo_url'] = isset($input['logo_url']) ? esc_url_raw($input['logo_url']) : '';
1298 $sanitized['logo_max_width'] = isset($input['logo_max_width']) ? absint($input['logo_max_width']) : 120;
1299
1300 // Text
1301 $sanitized['text_enabled'] = !empty($input['text_enabled']);
1302 $sanitized['text_content'] = isset($input['text_content']) ? sanitize_text_field($input['text_content']) : '';
1303 $sanitized['text_size'] = isset($input['text_size']) ? absint($input['text_size']) : 16;
1304 $sanitized['text_weight'] = isset($input['text_weight']) ? sanitize_key($input['text_weight']) : '400';
1305 $sanitized['text_color'] = $this->sanitize_color_value($input['text_color'] ?? '', '#1d1d1f');
1306
1307 // Animation
1308 $sanitized['accent_color'] = $this->sanitize_color_value($input['accent_color'] ?? '', '#0071e3');
1309 $sanitized['spinner_size'] = isset($input['spinner_size']) ? absint($input['spinner_size']) : 48;
1310
1311 // Behavior
1312 $sanitized['trigger_type'] = isset($input['trigger_type']) ? sanitize_key($input['trigger_type']) : 'always';
1313 $sanitized['hide_strategy'] = isset($input['hide_strategy']) ? sanitize_key($input['hide_strategy']) : 'window_load';
1314 $sanitized['min_display_time'] = isset($input['min_display_time']) ? absint($input['min_display_time']) : 500;
1315 $sanitized['max_display_time'] = isset($input['max_display_time']) ? absint($input['max_display_time']) : 5000;
1316 $sanitized['allow_skip'] = !empty($input['allow_skip']);
1317 $sanitized['skip_method'] = isset($input['skip_method']) ? sanitize_key($input['skip_method']) : 'click';
1318 $sanitized['lock_scroll'] = !empty($input['lock_scroll']);
1319
1320 // Transitions
1321 $sanitized['show_animation'] = isset($input['show_animation']) ? sanitize_key($input['show_animation']) : 'fade';
1322 $sanitized['hide_animation'] = isset($input['hide_animation']) ? sanitize_key($input['hide_animation']) : 'fade';
1323 $sanitized['transition_duration'] = isset($input['transition_duration']) ? absint($input['transition_duration']) : 400;
1324 $sanitized['easing'] = isset($input['easing']) ? sanitize_key($input['easing']) : 'ease-out';
1325
1326 // Advanced
1327 $sanitized['z_index'] = isset($input['z_index']) ? absint($input['z_index']) : 999999;
1328
1329 // Custom CSS (Pro only)
1330 if ($this->is_pro() && isset($input['custom_css'])) {
1331 $sanitized['custom_css'] = wp_strip_all_tags($input['custom_css']);
1332 }
1333
1334 return $sanitized;
1335 }
1336
1337 /**
1338 * Sanitize a CSS color value coming from admin inputs.
1339 * Allows hex, 8-digit hex, rgb/rgba, and 'transparent'.
1340 *
1341 * @param mixed $value
1342 * @param string $fallback
1343 * @return string
1344 */
1345 private function sanitize_color_value($value, string $fallback): string
1346 {
1347 if (!is_string($value)) {
1348 return $fallback;
1349 }
1350
1351 $value = trim($value);
1352 if ($value === '') {
1353 return $fallback;
1354 }
1355
1356 if (strcasecmp($value, 'transparent') === 0) {
1357 return 'transparent';
1358 }
1359
1360 $hex = sanitize_hex_color($value);
1361 if (!empty($hex)) {
1362 return $hex;
1363 }
1364
1365 if (preg_match('/^#([0-9a-fA-F]{8})$/', $value, $m)) {
1366 return '#' . strtolower($m[1]);
1367 }
1368
1369 if (preg_match('/^rgba?\((.+)\)$/i', $value, $m)) {
1370 $parts = array_map('trim', explode(',', $m[1]));
1371 if (count($parts) < 3 || count($parts) > 4) {
1372 return $fallback;
1373 }
1374
1375 $r = is_numeric($parts[0]) ? (int) $parts[0] : -1;
1376 $g = is_numeric($parts[1]) ? (int) $parts[1] : -1;
1377 $b = is_numeric($parts[2]) ? (int) $parts[2] : -1;
1378 if ($r < 0 || $r > 255 || $g < 0 || $g > 255 || $b < 0 || $b > 255) {
1379 return $fallback;
1380 }
1381
1382 if (count($parts) === 3) {
1383 return 'rgb(' . $r . ',' . $g . ',' . $b . ')';
1384 }
1385
1386 $aRaw = $parts[3];
1387 if (!is_numeric($aRaw)) {
1388 return $fallback;
1389 }
1390 $a = (float) $aRaw;
1391 if ($a < 0) {
1392 $a = 0;
1393 } elseif ($a > 1) {
1394 $a = 1;
1395 }
1396
1397 $aStr = rtrim(rtrim(sprintf('%.3F', $a), '0'), '.');
1398 return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $aStr . ')';
1399 }
1400
1401 return $fallback;
1402 }
1403
1404 /**
1405 * Checks if Pro version is active.
1406 *
1407 * @return bool
1408 */
1409 private function is_pro(): bool
1410 {
1411 return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
1412 }
1413 }
1414