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

script.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.37, at includes/widgets/Dynamic_Posts_Grid/script.js

706 lines 31.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Dynamic Posts Grid Widget JavaScript
3 * King Addons for Elementor
4 */
5
6 (function ($) {
7 'use strict';
8
9 $(window).on("elementor/frontend/init", () => {
10 elementorFrontend.hooks.addAction("frontend/element_ready/king-addons-dynamic-posts-grid.default", ($scope) => {
11 // Check if this is PRO mode by looking at widget-mode data attribute
12 const $wrapper = $scope.find('.king-addons-dpg-wrapper');
13
14 // Prevent double initialization
15 if ($wrapper.data('king-addons-initialized')) {
16 return;
17 }
18 $wrapper.data('king-addons-initialized', true);
19
20 const widgetMode = $wrapper.data('widget-mode');
21 const isPro = widgetMode === 'custom_cpt';
22
23 const gridHandler = {
24 init() {
25 this.wrapper = $scope.find('.king-addons-dpg-wrapper');
26 this.grid = $scope.find('.king-addons-dpg-grid');
27 this.filterBar = $scope.find('.king-addons-dpg-filter-bar');
28 this.filterSelect = $scope.find('.king-addons-dpg-posts-filter');
29 this.searchInput = $scope.find('.king-addons-dpg-posts-search');
30 this.searchBtn = $scope.find('.king-addons-dpg-search-btn');
31 this.loadMoreBtn = $scope.find('.king-addons-dpg-load-more-btn');
32 this.pagination = $scope.find('.king-addons-dpg-pagination');
33 this.loadingDiv = $scope.find('.king-addons-dpg-pagination-loading');
34 this.finishDiv = $scope.find('.king-addons-dpg-pagination-finish');
35
36 this.settings = this.getSettings();
37 this.currentPage = 1;
38 this.isLoading = false;
39 this.currentFilter = '*';
40 this.currentSearch = '';
41
42 this.bindEvents();
43 this.initIsotope();
44
45 // Prepare CPT icon map for client-side application after AJAX
46 this.cptIcons = {};
47 try {
48 const raw = this.settings.cptIconsRaw || '';
49 this.cptIcons = raw ? JSON.parse(raw) : {};
50 } catch (e) {
51 this.cptIcons = {};
52 }
53 },
54
55 getSettings() {
56 return {
57 widgetId: this.wrapper.data('widget-id'),
58 postsPerPage: this.wrapper.data('posts-per-page'),
59 postTypes: this.wrapper.data('post-types'),
60 orderby: this.wrapper.data('orderby'),
61 order: this.wrapper.data('order'),
62 filterTaxonomy: this.wrapper.data('filter-taxonomy'),
63 showExcerpt: this.wrapper.data('show-excerpt'),
64 cardClickable: this.wrapper.data('card-clickable'),
65 cptActionsRaw: this.wrapper.attr('data-cpt-actions') || '',
66 cptIconsRaw: this.wrapper.attr('data-cpt-icons') || ''
67 };
68 },
69
70 bindEvents() {
71 // Filter dropdown change
72 this.filterSelect.on('change', (e) => {
73 this.currentFilter = $(e.target).val();
74 this.currentPage = 1;
75 this.filterAndSearch();
76 });
77
78 // Search input events
79 this.searchInput.on('keyup', this.debounce((e) => {
80 this.currentSearch = $(e.target).val();
81 this.currentPage = 1;
82 this.filterAndSearch();
83 }, 500));
84
85 this.searchBtn.on('click', () => {
86 this.currentSearch = this.searchInput.val();
87 this.currentPage = 1;
88 this.filterAndSearch();
89 });
90
91 // Search on Enter key
92 this.searchInput.on('keypress', (e) => {
93 if (e.which === 13) {
94 this.currentSearch = this.searchInput.val();
95 this.currentPage = 1;
96 this.filterAndSearch();
97 }
98 });
99
100 // Load More button
101 this.loadMoreBtn.on('click', () => {
102 this.loadMore();
103 });
104
105 // Card click functionality
106 this.bindCardClickEvents();
107
108 // Action button events
109 this.bindActionButtonEvents();
110 },
111
112 bindCardClickEvents() {
113 // Only enable card clicking if setting is enabled and not in Elementor editor
114 if (this.settings.cardClickable === 1 && !elementorFrontend.isEditMode()) {
115 // Use event delegation for dynamically loaded content
116 this.wrapper.on('click', '.king-addons-dpg-card', (e) => {
117 // Don't trigger if clicking on a link or button inside the card
118 if ($(e.target).closest('a, button, .king-addons-dpg-button').length > 0) {
119 return;
120 }
121
122 // Find the post link within the card
123 const postLink = $(e.currentTarget).find('.king-addons-dpg-title a');
124 if (postLink.length > 0) {
125 const postUrl = postLink.attr('href');
126 if (postUrl) {
127 // Navigate to post
128 window.location.href = postUrl;
129 }
130 }
131 });
132
133 // Add cursor pointer style to cards when clickable
134 this.wrapper.addClass('king-addons-dpg-cards-clickable');
135 }
136 },
137
138 bindActionButtonEvents() {
139 // Prevent multiple event bindings
140 this.wrapper.off('click', '.king-addons-dpg-action-btn');
141
142 // Use event delegation for dynamically loaded content
143 this.wrapper.on('click', '.king-addons-dpg-action-btn', (e) => {
144 e.preventDefault();
145 e.stopPropagation();
146
147 const $button = $(e.target).closest('.king-addons-dpg-action-btn');
148
149 // Prevent double clicks and global lightbox blocking
150 if ($button.data('clicking') || $('body').data('lightbox-opening')) {
151 return;
152 }
153
154 $button.data('clicking', true);
155 $('body').data('lightbox-opening', true);
156
157 setTimeout(() => {
158 $button.removeData('clicking');
159 $('body').removeData('lightbox-opening');
160 }, 1500);
161
162 const action = $button.data('action');
163 const url = $button.data('url');
164 const title = $button.data('title') || '';
165
166 if (!url) {
167 console.warn('No URL provided for action button');
168 return;
169 }
170
171 switch (action) {
172 case 'lightbox_image':
173 this.openImageLightbox(url, title);
174 break;
175 case 'lightbox_video':
176 this.openVideoLightbox(url, title);
177 break;
178 case 'new_tab':
179 default:
180 window.open(url, '_blank');
181 break;
182 }
183 });
184 },
185
186 openImageLightbox(url, title) {
187 // Prevent multiple lightboxes from opening simultaneously
188 if ($('.lg-backdrop, .lg-outer, .king-addons-dpg-lightbox-temp, [data-lg-uid]').length > 0) {
189 return;
190 }
191
192 // Check if lightGallery is already running
193 if (window.lgCurrentInstance) {
194 return;
195 }
196
197 // Remove any orphaned containers first
198 $('.king-addons-dpg-lightbox-temp').remove();
199
200 // Create array with single image item for LightGallery
201 const galleryItems = [{
202 src: url,
203 subHtml: title || ''
204 }];
205
206 // Initialize LightGallery directly with dynamic gallery
207 if (typeof $.fn.lightGallery !== 'undefined') {
208 // Create a temporary div just for gallery initialization
209 const $tempDiv = $('<div style="display:none;"></div>');
210 $('body').append($tempDiv);
211
212 $tempDiv.lightGallery({
213 dynamic: true,
214 dynamicEl: galleryItems,
215 download: false,
216 counter: false,
217 zoom: true,
218 fullScreen: true,
219 controls: true,
220 thumbnail: false,
221 closable: true,
222 escKey: true,
223 keyPress: true
224 });
225
226 // Clean up on close
227 $tempDiv.on('onCloseAfter.lg', function() {
228 window.lgCurrentInstance = false;
229 setTimeout(() => {
230 $tempDiv.remove();
231 }, 100);
232 });
233 } else {
234 console.error('LightGallery not available');
235 window.open(url, '_blank');
236 }
237 },
238
239 openVideoLightbox(url, title) {
240 // Check if LightGallery is available
241 if (typeof $.fn.lightGallery === 'undefined') {
242 console.warn('LightGallery not loaded, opening video in new tab');
243 window.open(url, '_blank');
244 return;
245 }
246
247 // For old LightGallery v1.6.12, let's create a manual video popup
248 this.createYouTubePopup(url, title);
249 },
250
251 createYouTubePopup(url, title) {
252 // Process YouTube URL to get video ID
253 const videoId = this.getYouTubeVideoId(url);
254 if (!videoId) {
255 console.warn('Invalid YouTube URL');
256 window.open(url, '_blank');
257 return;
258 }
259
260 // Create manual video popup similar to LightGallery structure
261 const popupHtml = `
262 <div class="king-addons-video-popup" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 9999; display: flex; align-items: center; justify-content: center;">
263 <div class="king-addons-video-container" style="position: relative; width: 90%; max-width: 1200px; max-height: 90%;">
264 <button class="king-addons-video-close" style="position: absolute; top: -40px; right: 0; background: none; border: none; color: white; font-size: 30px; cursor: pointer; z-index: 10000;">&times;</button>
265 <div class="king-addons-video-wrapper" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
266 <iframe
267 src="https://www.youtube.com/embed/${videoId}?autoplay=1&modestbranding=1&rel=0&showinfo=0&controls=1"
268 style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;"
269 allowfullscreen
270 allow="autoplay; encrypted-media">
271 </iframe>
272 </div>
273 ${title ? `<div style="color: white; text-align: center; margin-top: 10px;">${title}</div>` : ''}
274 </div>
275 </div>
276 `;
277
278 const $popup = $(popupHtml);
279 $('body').append($popup);
280
281 // Handle close events
282 $popup.find('.king-addons-video-close').on('click', () => {
283 this.closeVideoPopup($popup);
284 });
285
286 $popup.on('click', (e) => {
287 if (e.target === $popup[0]) {
288 this.closeVideoPopup($popup);
289 }
290 });
291
292 // Handle ESC key
293 $(document).on('keydown.video-popup', (e) => {
294 if (e.keyCode === 27) {
295 this.closeVideoPopup($popup);
296 }
297 });
298
299 // Animate in
300 $popup.css('opacity', 0).animate({opacity: 1}, 300);
301 },
302
303 getYouTubeVideoId(url) {
304 // Extract video ID from various YouTube URL formats
305 const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
306 const match = url.match(regExp);
307 return (match && match[7].length === 11) ? match[7] : null;
308 },
309
310 closeVideoPopup($popup) {
311 // Stop video by removing iframe
312 $popup.find('iframe').attr('src', '');
313
314 // Remove event listeners
315 $(document).off('keydown.video-popup');
316
317 // Animate out and remove
318 $popup.animate({opacity: 0}, 300, function() {
319 $popup.remove();
320 });
321 },
322
323 processYouTubeUrl(url) {
324 // Handle different YouTube URL formats
325 if (url.includes('youtube.com/watch?v=')) {
326 // Already in correct format
327 return url;
328 } else if (url.includes('youtu.be/')) {
329 // Convert youtu.be/VIDEO_ID to youtube.com/watch?v=VIDEO_ID
330 const videoId = url.split('youtu.be/')[1].split('?')[0].split('&')[0];
331 return `https://www.youtube.com/watch?v=${videoId}`;
332 } else if (url.includes('youtube.com/embed/')) {
333 // Convert youtube.com/embed/VIDEO_ID to youtube.com/watch?v=VIDEO_ID
334 const videoId = url.split('/embed/')[1].split('?')[0].split('&')[0];
335 return `https://www.youtube.com/watch?v=${videoId}`;
336 }
337
338 // Return original URL if not YouTube or unknown format
339 return url;
340 },
341
342 initIsotope() {
343 // Skip Isotope initialization in Elementor editor
344 if (elementorFrontend.isEditMode()) {
345 return;
346 }
347
348 // Initialize isotope if available
349 if (typeof $.fn.isotopekng !== 'undefined') {
350 this.grid.isotopekng({
351 itemSelector: '.king-addons-dpg-card',
352 layoutMode: 'masonry',
353 masonry: {
354 columnWidth: '.king-addons-dpg-card'
355 },
356 transitionDuration: '0.3s'
357 });
358 }
359
360 // Images loaded callback (skip in editor)
361 if (typeof $.fn.imagesLoaded !== 'undefined' && !elementorFrontend.isEditMode()) {
362 this.grid.imagesLoaded(() => {
363 this.relayoutGrid();
364 });
365 }
366 },
367
368 relayoutGrid() {
369 // Skip in Elementor editor
370 if (elementorFrontend.isEditMode()) {
371 return;
372 }
373
374 if (typeof $.fn.isotopekng !== 'undefined') {
375 this.grid.isotopekng('layout');
376 }
377 },
378
379 filterAndSearch() {
380 // PRO: Use client-side filtering for CPT mode
381 if (widgetMode === 'custom_cpt') {
382 this.filterByPostType();
383 return;
384 }
385
386 // Skip AJAX calls in Elementor editor
387 if (elementorFrontend.isEditMode()) {
388 return;
389 }
390
391 if (this.isLoading) return;
392
393 this.isLoading = true;
394 this.showLoading();
395
396 const ajaxData = {
397 action: 'king_addons_dynamic_posts_grid_filter',
398 nonce: window.KingAddonsDynamicPostsGrid?.nonce || '',
399 widget_id: this.settings.widgetId,
400 posts_per_page: this.settings.postsPerPage,
401 post_types: this.settings.postTypes,
402 orderby: this.settings.orderby,
403 order: this.settings.order,
404 filter_taxonomy: this.settings.filterTaxonomy,
405 filter_term: this.currentFilter,
406 search_query: this.currentSearch,
407 page: this.currentPage,
408 show_excerpt: this.settings.showExcerpt,
409 cpt_actions: this.settings.cptActionsRaw
410 };
411
412 $.ajax({
413 url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php',
414 type: 'POST',
415 data: ajaxData,
416 success: (response) => {
417 this.handleFilterResponse(response);
418 },
419 error: (xhr, status, error) => {
420 console.error('Dynamic Posts Grid AJAX Error:', error);
421 this.hideLoading();
422 this.isLoading = false;
423 }
424 });
425 },
426
427 loadMore() {
428 // Skip AJAX calls in Elementor editor
429 if (elementorFrontend.isEditMode()) {
430 return;
431 }
432
433 if (this.isLoading) return;
434
435 this.currentPage++;
436 this.isLoading = true;
437 this.showLoading();
438
439 const ajaxData = {
440 action: 'king_addons_dynamic_posts_grid_load_more',
441 nonce: window.KingAddonsDynamicPostsGrid?.nonce || '',
442 widget_id: this.settings.widgetId,
443 posts_per_page: this.settings.postsPerPage,
444 post_types: this.settings.postTypes,
445 orderby: this.settings.orderby,
446 order: this.settings.order,
447 filter_taxonomy: this.settings.filterTaxonomy,
448 filter_term: this.currentFilter,
449 search_query: this.currentSearch,
450 page: this.currentPage,
451 show_excerpt: this.settings.showExcerpt,
452 cpt_actions: this.settings.cptActionsRaw
453 };
454
455 $.ajax({
456 url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php',
457 type: 'POST',
458 data: ajaxData,
459 success: (response) => {
460 this.handleLoadMoreResponse(response);
461 },
462 error: (xhr, status, error) => {
463 console.error('Dynamic Posts Grid Load More Error:', error);
464 this.hideLoading();
465 this.isLoading = false;
466 this.currentPage--; // Revert page increment on error
467 }
468 });
469 },
470
471 handleFilterResponse(response) {
472 this.hideLoading();
473 this.isLoading = false;
474
475 if (response.success && response.data) {
476 // Fade out current content
477 this.grid.addClass('king-addons-dpg-zero-opacity');
478
479 setTimeout(() => {
480 // Replace grid content
481 if (typeof $.fn.isotopekng !== 'undefined' && !elementorFrontend.isEditMode()) {
482 this.grid.isotopekng('destroy');
483 }
484
485 this.grid.html(response.data.posts_html);
486
487 // Re-initialize isotope
488 this.initIsotope();
489
490 // Update pagination
491 this.updatePagination(response.data);
492
493 // Fade in new content
494 setTimeout(() => {
495 this.grid.removeClass('king-addons-dpg-zero-opacity');
496 this.animateNewItems();
497 }, 100);
498
499 }, 300);
500 } else {
501 this.showError(response.data?.message || 'Failed to load posts');
502 }
503 },
504
505 handleLoadMoreResponse(response) {
506 this.hideLoading();
507 this.isLoading = false;
508
509 if (response.success && response.data) {
510 const newItems = $(response.data.posts_html);
511
512 // Add new items to grid
513 this.grid.append(newItems);
514
515 // Animate new items
516 newItems.addClass('king-addons-dpg-fade-in');
517
518 // Re-layout isotope
519 if (typeof $.fn.isotopekng !== 'undefined' && !elementorFrontend.isEditMode()) {
520 this.grid.isotopekng('appended', newItems);
521
522 // Re-layout after images load
523 if (typeof $.fn.imagesLoaded !== 'undefined') {
524 newItems.imagesLoaded(() => {
525 this.relayoutGrid();
526 });
527 }
528 }
529
530 // Update pagination
531 this.updatePagination(response.data);
532
533 // Ensure clicks on links/buttons do not bubble to card-click handler
534 this.wrapper.off('click', '.king-addons-dpg-card a, .king-addons-dpg-card button')
535 .on('click', '.king-addons-dpg-card a, .king-addons-dpg-card button', (e) => {
536 e.stopPropagation();
537 });
538
539 // If CPT mode, re-apply client-side filter (post type + search)
540 if (widgetMode === 'custom_cpt') {
541 // Apply CPT icons to newly added items
542 this.applyCptIcons(newItems);
543 this.filterByPostType();
544 }
545
546 } else {
547 this.showError(response.data?.message || 'Failed to load more posts');
548 this.currentPage--; // Revert page increment
549 }
550 },
551
552 applyCptIcons($scopeItems) {
553 if (!this.cptIcons || typeof this.cptIcons !== 'object') return;
554 $scopeItems.each((_, el) => {
555 const $card = $(el);
556 const postType = $card.data('post-type');
557 if (!postType) return;
558 const conf = this.cptIcons[postType];
559 if (!conf) return;
560
561 const $iconWrap = $card.find('.king-addons-dpg-icon');
562 if (conf.icon_type === 'image' && conf.image_url) {
563 $iconWrap.html('<img src="' + conf.image_url + '" alt="' + postType + '" />');
564 } else if (conf.icon_class) {
565 $iconWrap.html('<i class="' + conf.icon_class + '"></i>');
566 }
567 });
568 },
569
570 updatePagination(data) {
571 if (data.current_page >= data.max_pages) {
572 this.loadMoreBtn.hide();
573 this.showFinished(data.total_posts, data.current_count);
574 } else {
575 this.loadMoreBtn.attr('data-page', data.current_page);
576 this.loadMoreBtn.attr('data-max-pages', data.max_pages);
577 this.loadMoreBtn.show();
578 this.finishDiv.hide();
579 }
580
581 // Update counts
582 $scope.find('.king-addons-dpg-current-count').text(data.current_count);
583 $scope.find('.king-addons-dpg-total-count').text(data.total_posts);
584 },
585
586 showLoading() {
587 this.loadingDiv.show();
588 this.loadMoreBtn.prop('disabled', true);
589
590 // Add loading spinner to button
591 if (!this.loadMoreBtn.find('.king-addons-dpg-loading-spinner').length) {
592 this.loadMoreBtn.prepend('<span class="king-addons-dpg-loading-spinner"></span>');
593 }
594 },
595
596 hideLoading() {
597 this.loadingDiv.hide();
598 this.loadMoreBtn.prop('disabled', false);
599 this.loadMoreBtn.find('.king-addons-dpg-loading-spinner').remove();
600 },
601
602 showFinished(total, current) {
603 this.finishDiv.find('.king-addons-dpg-current-count').text(current);
604 this.finishDiv.find('.king-addons-dpg-total-count').text(total);
605 this.finishDiv.fadeIn(1000);
606
607 setTimeout(() => {
608 this.finishDiv.fadeOut(1000);
609 }, 3000);
610 },
611
612 showError(message) {
613 // Create and show error message
614 const errorDiv = $('<div class="king-addons-dpg-error-message">' + message + '</div>');
615 this.wrapper.prepend(errorDiv);
616
617 setTimeout(() => {
618 errorDiv.fadeOut(() => {
619 errorDiv.remove();
620 });
621 }, 5000);
622 },
623
624 animateNewItems() {
625 this.grid.find('.king-addons-dpg-card').each((index, element) => {
626 setTimeout(() => {
627 $(element).addClass('king-addons-dpg-fade-in');
628 }, index * 100);
629 });
630 },
631
632 debounce(func, wait, immediate) {
633 let timeout;
634 return function() {
635 const context = this;
636 const args = arguments;
637 const later = function() {
638 timeout = null;
639 if (!immediate) func.apply(context, args);
640 };
641 const callNow = immediate && !timeout;
642 clearTimeout(timeout);
643 timeout = setTimeout(later, wait);
644 if (callNow) func.apply(context, args);
645 };
646 },
647
648 // PRO Methods
649 filterByPostType() {
650 // Filter posts by post type (for CPT mode)
651 if (widgetMode === 'custom_cpt') {
652 const $cards = this.grid.find('.king-addons-dpg-item');
653 let $candidate = $cards;
654
655 // Filter by selected post type first
656 if (this.currentFilter !== '*') {
657 $candidate = $candidate.filter('[data-post-type="' + this.currentFilter + '"]');
658 }
659
660 // Then apply keyword search (title text)
661 const query = (this.currentSearch || '').toString().trim().toLowerCase();
662 if (query.length > 0) {
663 $candidate = $candidate.filter((_, el) => {
664 const $el = $(el);
665 const titleText = $el.find('.king-addons-dpg-title').text().toLowerCase();
666 const excerptText = $el.find('.king-addons-dpg-excerpt').text().toLowerCase();
667 return (titleText.indexOf(query) !== -1) || (excerptText.indexOf(query) !== -1);
668 });
669 }
670
671 // Show only matching cards
672 $cards.hide();
673 $candidate.show();
674
675 // Re-layout if using Isotope
676 if (this.grid.data('isotopekng')) {
677 this.grid.isotopekng('layout');
678 }
679 }
680 }
681 };
682
683 // Initialize the grid handler
684 gridHandler.init();
685
686 // Handle responsive behavior
687 $(window).on('resize', gridHandler.debounce(() => {
688 gridHandler.relayoutGrid();
689 }, 250));
690
691 // Handle Elementor editor mode
692 if (window.elementorFrontend?.isEditMode()) {
693 // Re-initialize when settings change in editor
694 elementorFrontend.hooks.addAction('panel/open_editor/widget/king-addons-dynamic-posts-grid', () => {
695 setTimeout(() => {
696 gridHandler.init();
697 }, 100);
698 });
699 }
700 });
701
702 // PRO version uses the same hook name as Free version, no separate hook needed
703 });
704
705 })(jQuery);
706