PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.44
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.44
51.1.86 51.1.84 51.1.85 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 All 40 releases
king-addons / includes / extensions / Site_Preloader / Site_Preloader.php

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

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