PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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 / assets / script.js

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

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