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 / assets / script.js

script.js 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/assets/script.js

473 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Site Preloader Frontend Script.
3 *
4 * Handles preloader initialization, hide strategies, and AJAX navigation support.
5 * Apple-inspired smooth interactions.
6 *
7 * @package King_Addons
8 * @since 1.0.0
9 */
10
11 (function () {
12 'use strict';
13
14 /**
15 * Main Preloader Controller
16 */
17 const KngPreloader = {
18
19 /**
20 * Configuration from backend
21 */
22 config: {
23 hideStrategy: 'window_load',
24 minDisplayTime: 500,
25 maxDisplayTime: 10000,
26 hideAnimation: 'fade',
27 animationDuration: 400,
28 triggerType: 'always',
29 enableAjax: false,
30 allowSkip: false,
31 skipMethod: 'click',
32 lockScroll: true,
33 cookieDays: 30
34 },
35
36 /**
37 * State management
38 */
39 state: {
40 isShowing: true,
41 startTime: 0,
42 hideTimer: null,
43 maxTimer: null
44 },
45
46 /**
47 * DOM elements
48 */
49 elements: {
50 preloader: null,
51 overlay: null,
52 content: null
53 },
54
55 /**
56 * Initialize the preloader
57 */
58 init: function () {
59 // Get preloader element
60 this.elements.preloader = document.querySelector('.kng-site-preloader');
61
62 if (!this.elements.preloader) {
63 return;
64 }
65
66 // Get child elements
67 this.elements.overlay = this.elements.preloader.querySelector('.kng-site-preloader__overlay');
68 this.elements.content = this.elements.preloader.querySelector('.kng-site-preloader__content');
69
70 // Parse configuration from data attributes
71 this.parseConfig();
72
73 // Record start time
74 this.state.startTime = performance.now();
75
76 // Lock scroll if needed
77 if (this.config.lockScroll) {
78 document.body.classList.add('kng-preloader-no-scroll');
79 }
80
81 // Set up skip key listener
82 this.setupSkipKey();
83
84 // Set up hide strategy
85 this.setupHideStrategy();
86
87 // Set up maximum display time failsafe
88 this.setupMaxTimeout();
89
90 // Set up AJAX navigation support (Pro feature)
91 if (this.config.enableAjax) {
92 this.setupAjaxNavigation();
93 }
94
95 // Expose API
96 window.KngPreloader = this;
97 },
98
99 /**
100 * Parse configuration from data attributes
101 */
102 parseConfig: function () {
103 const dataset = this.elements.preloader.dataset;
104
105 if (dataset.hideStrategy) {
106 this.config.hideStrategy = dataset.hideStrategy;
107 }
108 if (dataset.minDisplayTime) {
109 this.config.minDisplayTime = parseInt(dataset.minDisplayTime, 10);
110 }
111 if (dataset.maxDisplayTime) {
112 this.config.maxDisplayTime = parseInt(dataset.maxDisplayTime, 10);
113 }
114 if (dataset.hideAnimation) {
115 this.config.hideAnimation = dataset.hideAnimation;
116 }
117 if (dataset.animationDuration) {
118 this.config.animationDuration = parseInt(dataset.animationDuration, 10);
119 }
120 if (dataset.triggerType) {
121 this.config.triggerType = dataset.triggerType;
122 }
123 if (dataset.enableAjax) {
124 this.config.enableAjax = dataset.enableAjax === 'true';
125 }
126 if (dataset.allowSkip) {
127 this.config.allowSkip = dataset.allowSkip === 'true';
128 }
129 if (dataset.skipMethod) {
130 this.config.skipMethod = dataset.skipMethod;
131 }
132 if (dataset.lockScroll !== undefined) {
133 this.config.lockScroll = dataset.lockScroll === 'true';
134 }
135 },
136
137 /**
138 * Set up hide strategy based on configuration
139 */
140 setupHideStrategy: function () {
141 switch (this.config.hideStrategy) {
142 case 'dom_ready':
143 if (document.readyState === 'loading') {
144 document.addEventListener('DOMContentLoaded', () => this.requestHide());
145 } else {
146 this.requestHide();
147 }
148 break;
149
150 case 'window_load':
151 if (document.readyState === 'complete') {
152 this.requestHide();
153 } else {
154 window.addEventListener('load', () => this.requestHide());
155 }
156 break;
157
158 case 'timeout':
159 this.state.hideTimer = setTimeout(() => {
160 this.requestHide();
161 }, this.config.minDisplayTime);
162 break;
163
164 case 'custom':
165 // Wait for external call to hide()
166 break;
167
168 default:
169 window.addEventListener('load', () => this.requestHide());
170 }
171 },
172
173 /**
174 * Set up maximum display time failsafe
175 */
176 setupMaxTimeout: function () {
177 this.state.maxTimer = setTimeout(() => {
178 if (this.state.isShowing) {
179 console.warn('[KngPreloader] Max display time reached, forcing hide');
180 this.hide();
181 }
182 }, this.config.maxDisplayTime);
183 },
184
185 /**
186 * Set up skip functionality based on config
187 */
188 setupSkipKey: function () {
189 if (!this.config.allowSkip) {
190 return;
191 }
192
193 const skipMethod = this.config.skipMethod || 'click';
194
195 if (skipMethod === 'click') {
196 // Skip on click anywhere
197 const handleClick = () => {
198 if (this.state.isShowing) {
199 this.hide();
200 document.removeEventListener('click', handleClick);
201 }
202 };
203 document.addEventListener('click', handleClick);
204 } else if (skipMethod === 'escape') {
205 // Skip on Escape key
206 const handleKeyDown = (e) => {
207 if (e.key === 'Escape' && this.state.isShowing) {
208 this.hide();
209 document.removeEventListener('keydown', handleKeyDown);
210 }
211 };
212 document.addEventListener('keydown', handleKeyDown);
213 }
214 },
215
216 /**
217 * Request hide with minimum display time check
218 */
219 requestHide: function () {
220 const elapsed = performance.now() - this.state.startTime;
221 const remaining = this.config.minDisplayTime - elapsed;
222
223 if (remaining > 0) {
224 this.state.hideTimer = setTimeout(() => this.hide(), remaining);
225 } else {
226 this.hide();
227 }
228 },
229
230 /**
231 * Hide the preloader
232 */
233 hide: function () {
234 if (!this.state.isShowing || !this.elements.preloader) {
235 return;
236 }
237
238 this.state.isShowing = false;
239
240 // Clear timers
241 if (this.state.hideTimer) {
242 clearTimeout(this.state.hideTimer);
243 }
244 if (this.state.maxTimer) {
245 clearTimeout(this.state.maxTimer);
246 }
247
248 // Set CSS animation duration variable
249 this.elements.preloader.style.setProperty('--kng-preloader-transition', this.config.animationDuration + 'ms');
250
251 // Apply hide animation
252 const animationClass = this.getHideAnimationClass();
253 this.elements.preloader.classList.add('kng-site-preloader--hidden', animationClass);
254
255 // Unlock scroll after animation completes
256 setTimeout(() => {
257 document.body.classList.remove('kng-preloader-no-scroll');
258 this.elements.preloader.style.display = 'none';
259
260 // Set cookie based on trigger type
261 this.setTriggerCookie();
262
263 // Trigger custom event
264 document.dispatchEvent(new CustomEvent('kngPreloaderHidden', {
265 detail: {
266 displayTime: performance.now() - this.state.startTime
267 }
268 }));
269 }, this.config.animationDuration);
270 },
271
272 /**
273 * Get hide animation CSS class
274 */
275 getHideAnimationClass: function () {
276 const animations = {
277 'fade': 'kng-site-preloader--fade-out',
278 'slide_up': 'kng-site-preloader--slide-up',
279 'blur': 'kng-site-preloader--blur-out',
280 'scale': 'kng-site-preloader--scale-out'
281 };
282
283 return animations[this.config.hideAnimation] || animations['fade'];
284 },
285
286 /**
287 * Show the preloader (for AJAX navigation)
288 */
289 show: function () {
290 if (this.state.isShowing || !this.elements.preloader) {
291 return;
292 }
293
294 this.state.isShowing = true;
295 this.state.startTime = performance.now();
296
297 // Remove hide classes
298 this.elements.preloader.classList.remove(
299 'kng-site-preloader--hidden',
300 'kng-site-preloader--fade-out',
301 'kng-site-preloader--slide-up',
302 'kng-site-preloader--blur-out',
303 'kng-site-preloader--scale-out'
304 );
305
306 // Show preloader
307 this.elements.preloader.style.display = 'flex';
308 this.elements.preloader.classList.add('kng-site-preloader--fade-in');
309
310 // Lock scroll
311 document.body.classList.add('kng-preloader-no-scroll');
312
313 // Set up max timeout again
314 this.setupMaxTimeout();
315
316 // Trigger custom event
317 document.dispatchEvent(new CustomEvent('kngPreloaderShown'));
318 },
319
320 /**
321 * Set cookie based on trigger type
322 */
323 setTriggerCookie: function () {
324 if (this.config.triggerType === 'always') {
325 return;
326 }
327
328 const cookieName = 'kng_preloader_shown';
329 let expires = '';
330
331 if (this.config.triggerType === 'once') {
332 // Set cookie for specified days
333 const date = new Date();
334 date.setTime(date.getTime() + (this.config.cookieDays * 24 * 60 * 60 * 1000));
335 expires = '; expires=' + date.toUTCString();
336 } else if (this.config.triggerType === 'session') {
337 // Session cookie (no expires = session only)
338 expires = '';
339 }
340
341 document.cookie = cookieName + '=1' + expires + '; path=/; SameSite=Lax';
342 },
343
344 /**
345 * Check if preloader should be shown based on cookie
346 */
347 shouldShow: function () {
348 if (this.config.triggerType === 'always') {
349 return true;
350 }
351
352 const cookies = document.cookie.split(';');
353 for (let i = 0; i < cookies.length; i++) {
354 const cookie = cookies[i].trim();
355 if (cookie.startsWith('kng_preloader_shown=')) {
356 return false;
357 }
358 }
359 return true;
360 },
361
362 /**
363 * Set up AJAX navigation support (Pro feature)
364 */
365 setupAjaxNavigation: function () {
366 // Intercept link clicks
367 document.addEventListener('click', (e) => {
368 const link = e.target.closest('a');
369 if (!link || !this.isInternalLink(link.href)) {
370 return;
371 }
372
373 // Check for external link indicators
374 if (link.target === '_blank' || link.hasAttribute('download')) {
375 return;
376 }
377
378 // Exclude admin links and WP-specific URLs
379 const href = link.href;
380 if (href.includes('/wp-admin') ||
381 href.includes('/wp-login') ||
382 href.includes('#') ||
383 href.includes('?') && href.includes('action=')) {
384 return;
385 }
386
387 // Show preloader for internal navigation
388 e.preventDefault();
389 this.show();
390
391 // Navigate after short delay
392 setTimeout(() => {
393 window.location.href = href;
394 }, 200);
395 });
396
397 // Handle browser back/forward
398 window.addEventListener('popstate', () => {
399 this.show();
400 });
401
402 // Handle page show event (for bfcache)
403 window.addEventListener('pageshow', (e) => {
404 if (e.persisted) {
405 this.hide();
406 }
407 });
408 },
409
410 /**
411 * Check if URL is internal
412 */
413 isInternalLink: function (url) {
414 try {
415 const linkUrl = new URL(url);
416 return linkUrl.hostname === window.location.hostname;
417 } catch (e) {
418 return false;
419 }
420 },
421
422 /**
423 * Update progress (for custom progress bars)
424 */
425 setProgress: function (percent) {
426 const progressBar = this.elements.preloader.querySelector('[data-progress-bar]');
427 if (progressBar) {
428 progressBar.style.width = Math.min(100, Math.max(0, percent)) + '%';
429 }
430 },
431
432 /**
433 * Update loading text
434 */
435 setText: function (text) {
436 const textElement = this.elements.preloader.querySelector('.kng-site-preloader__text');
437 if (textElement) {
438 textElement.textContent = text;
439 }
440 },
441
442 /**
443 * Destroy preloader
444 */
445 destroy: function () {
446 if (this.state.hideTimer) {
447 clearTimeout(this.state.hideTimer);
448 }
449 if (this.state.maxTimer) {
450 clearTimeout(this.state.maxTimer);
451 }
452
453 if (this.elements.preloader) {
454 this.elements.preloader.remove();
455 }
456
457 document.body.classList.remove('kng-preloader-no-scroll');
458 this.elements.preloader = null;
459 }
460 };
461
462 // Initialize when DOM is ready
463 if (document.readyState === 'loading') {
464 document.addEventListener('DOMContentLoaded', () => KngPreloader.init());
465 } else {
466 KngPreloader.init();
467 }
468
469 // Expose globally for external control
470 window.KngPreloader = KngPreloader;
471
472 })();
473