/** * Dynamic Posts Grid Widget JavaScript * King Addons for Elementor */ (function ($) { 'use strict'; $(window).on("elementor/frontend/init", () => { elementorFrontend.hooks.addAction("frontend/element_ready/king-addons-dynamic-posts-grid.default", ($scope) => { // Check if this is PRO mode by looking at widget-mode data attribute const $wrapper = $scope.find('.king-addons-dpg-wrapper'); // Prevent double initialization if ($wrapper.data('king-addons-initialized')) { return; } $wrapper.data('king-addons-initialized', true); const widgetMode = $wrapper.data('widget-mode'); const isPro = widgetMode === 'custom_cpt'; const gridHandler = { init() { this.wrapper = $scope.find('.king-addons-dpg-wrapper'); this.grid = $scope.find('.king-addons-dpg-grid'); this.filterBar = $scope.find('.king-addons-dpg-filter-bar'); this.filterSelect = $scope.find('.king-addons-dpg-posts-filter'); this.searchInput = $scope.find('.king-addons-dpg-posts-search'); this.searchBtn = $scope.find('.king-addons-dpg-search-btn'); this.loadMoreBtn = $scope.find('.king-addons-dpg-load-more-btn'); this.pagination = $scope.find('.king-addons-dpg-pagination'); this.loadingDiv = $scope.find('.king-addons-dpg-pagination-loading'); this.finishDiv = $scope.find('.king-addons-dpg-pagination-finish'); this.settings = this.getSettings(); this.currentPage = 1; this.isLoading = false; this.currentFilter = '*'; this.currentSearch = ''; this.bindEvents(); this.initIsotope(); // Prepare CPT icon map for client-side application after AJAX this.cptIcons = {}; try { const raw = this.settings.cptIconsRaw || ''; this.cptIcons = raw ? JSON.parse(raw) : {}; } catch (e) { this.cptIcons = {}; } }, getSettings() { return { widgetId: this.wrapper.data('widget-id'), postsPerPage: this.wrapper.data('posts-per-page'), postTypes: this.wrapper.data('post-types'), orderby: this.wrapper.data('orderby'), order: this.wrapper.data('order'), filterTaxonomy: this.wrapper.data('filter-taxonomy'), showExcerpt: this.wrapper.data('show-excerpt'), cardClickable: this.wrapper.data('card-clickable'), cptActionsRaw: this.wrapper.attr('data-cpt-actions') || '', cptIconsRaw: this.wrapper.attr('data-cpt-icons') || '' }; }, bindEvents() { // Filter dropdown change this.filterSelect.on('change', (e) => { this.currentFilter = $(e.target).val(); this.currentPage = 1; this.filterAndSearch(); }); // Search input events this.searchInput.on('keyup', this.debounce((e) => { this.currentSearch = $(e.target).val(); this.currentPage = 1; this.filterAndSearch(); }, 500)); this.searchBtn.on('click', () => { this.currentSearch = this.searchInput.val(); this.currentPage = 1; this.filterAndSearch(); }); // Search on Enter key this.searchInput.on('keypress', (e) => { if (e.which === 13) { this.currentSearch = this.searchInput.val(); this.currentPage = 1; this.filterAndSearch(); } }); // Load More button this.loadMoreBtn.on('click', () => { this.loadMore(); }); // Card click functionality this.bindCardClickEvents(); // Action button events this.bindActionButtonEvents(); }, bindCardClickEvents() { // Only enable card clicking if setting is enabled and not in Elementor editor if (this.settings.cardClickable === 1 && !elementorFrontend.isEditMode()) { // Use event delegation for dynamically loaded content this.wrapper.on('click', '.king-addons-dpg-card', (e) => { // Don't trigger if clicking on a link or button inside the card if ($(e.target).closest('a, button, .king-addons-dpg-button').length > 0) { return; } // Find the post link within the card const postLink = $(e.currentTarget).find('.king-addons-dpg-title a'); if (postLink.length > 0) { const postUrl = postLink.attr('href'); if (postUrl) { // Navigate to post window.location.href = postUrl; } } }); // Add cursor pointer style to cards when clickable this.wrapper.addClass('king-addons-dpg-cards-clickable'); } }, bindActionButtonEvents() { // Prevent multiple event bindings this.wrapper.off('click', '.king-addons-dpg-action-btn'); // Use event delegation for dynamically loaded content this.wrapper.on('click', '.king-addons-dpg-action-btn', (e) => { e.preventDefault(); e.stopPropagation(); const $button = $(e.target).closest('.king-addons-dpg-action-btn'); // Prevent double clicks and global lightbox blocking if ($button.data('clicking') || $('body').data('lightbox-opening')) { return; } $button.data('clicking', true); $('body').data('lightbox-opening', true); setTimeout(() => { $button.removeData('clicking'); $('body').removeData('lightbox-opening'); }, 1500); const action = $button.data('action'); const url = $button.data('url'); const title = $button.data('title') || ''; if (!url) { console.warn('No URL provided for action button'); return; } switch (action) { case 'lightbox_image': this.openImageLightbox(url, title); break; case 'lightbox_video': this.openVideoLightbox(url, title); break; case 'new_tab': default: window.open(url, '_blank'); break; } }); }, openImageLightbox(url, title) { // Prevent multiple lightboxes from opening simultaneously if ($('.lg-backdrop, .lg-outer, .king-addons-dpg-lightbox-temp, [data-lg-uid]').length > 0) { return; } // Check if lightGallery is already running if (window.lgCurrentInstance) { return; } // Remove any orphaned containers first $('.king-addons-dpg-lightbox-temp').remove(); // Create array with single image item for LightGallery const galleryItems = [{ src: url, subHtml: title || '' }]; // Initialize LightGallery directly with dynamic gallery if (typeof $.fn.lightGallery !== 'undefined') { // Create a temporary div just for gallery initialization const $tempDiv = $('
'); $('body').append($tempDiv); $tempDiv.lightGallery({ dynamic: true, dynamicEl: galleryItems, download: false, counter: false, zoom: true, fullScreen: true, controls: true, thumbnail: false, closable: true, escKey: true, keyPress: true }); // Clean up on close $tempDiv.on('onCloseAfter.lg', function() { window.lgCurrentInstance = false; setTimeout(() => { $tempDiv.remove(); }, 100); }); } else { console.error('LightGallery not available'); window.open(url, '_blank'); } }, openVideoLightbox(url, title) { // Check if LightGallery is available if (typeof $.fn.lightGallery === 'undefined') { console.warn('LightGallery not loaded, opening video in new tab'); window.open(url, '_blank'); return; } // For old LightGallery v1.6.12, let's create a manual video popup this.createYouTubePopup(url, title); }, createYouTubePopup(url, title) { // Process YouTube URL to get video ID const videoId = this.getYouTubeVideoId(url); if (!videoId) { console.warn('Invalid YouTube URL'); window.open(url, '_blank'); return; } // Create manual video popup similar to LightGallery structure const popupHtml = `
${title ? `
${title}
` : ''}
`; const $popup = $(popupHtml); $('body').append($popup); // Handle close events $popup.find('.king-addons-video-close').on('click', () => { this.closeVideoPopup($popup); }); $popup.on('click', (e) => { if (e.target === $popup[0]) { this.closeVideoPopup($popup); } }); // Handle ESC key $(document).on('keydown.video-popup', (e) => { if (e.keyCode === 27) { this.closeVideoPopup($popup); } }); // Animate in $popup.css('opacity', 0).animate({opacity: 1}, 300); }, getYouTubeVideoId(url) { // Extract video ID from various YouTube URL formats const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; const match = url.match(regExp); return (match && match[7].length === 11) ? match[7] : null; }, closeVideoPopup($popup) { // Stop video by removing iframe $popup.find('iframe').attr('src', ''); // Remove event listeners $(document).off('keydown.video-popup'); // Animate out and remove $popup.animate({opacity: 0}, 300, function() { $popup.remove(); }); }, processYouTubeUrl(url) { // Handle different YouTube URL formats if (url.includes('youtube.com/watch?v=')) { // Already in correct format return url; } else if (url.includes('youtu.be/')) { // Convert youtu.be/VIDEO_ID to youtube.com/watch?v=VIDEO_ID const videoId = url.split('youtu.be/')[1].split('?')[0].split('&')[0]; return `https://www.youtube.com/watch?v=${videoId}`; } else if (url.includes('youtube.com/embed/')) { // Convert youtube.com/embed/VIDEO_ID to youtube.com/watch?v=VIDEO_ID const videoId = url.split('/embed/')[1].split('?')[0].split('&')[0]; return `https://www.youtube.com/watch?v=${videoId}`; } // Return original URL if not YouTube or unknown format return url; }, initIsotope() { // Skip Isotope initialization in Elementor editor if (elementorFrontend.isEditMode()) { return; } // Initialize isotope if available if (typeof $.fn.isotopekng !== 'undefined') { this.grid.isotopekng({ itemSelector: '.king-addons-dpg-card', layoutMode: 'masonry', masonry: { columnWidth: '.king-addons-dpg-card' }, transitionDuration: '0.3s' }); } // Images loaded callback (skip in editor) if (typeof $.fn.imagesLoaded !== 'undefined' && !elementorFrontend.isEditMode()) { this.grid.imagesLoaded(() => { this.relayoutGrid(); }); } }, relayoutGrid() { // Skip in Elementor editor if (elementorFrontend.isEditMode()) { return; } if (typeof $.fn.isotopekng !== 'undefined') { this.grid.isotopekng('layout'); } }, filterAndSearch() { // PRO: Use client-side filtering for CPT mode if (widgetMode === 'custom_cpt') { this.filterByPostType(); return; } // Skip AJAX calls in Elementor editor if (elementorFrontend.isEditMode()) { return; } if (this.isLoading) return; this.isLoading = true; this.showLoading(); const ajaxData = { action: 'king_addons_dynamic_posts_grid_filter', nonce: window.KingAddonsDynamicPostsGrid?.nonce || '', widget_id: this.settings.widgetId, posts_per_page: this.settings.postsPerPage, post_types: this.settings.postTypes, orderby: this.settings.orderby, order: this.settings.order, filter_taxonomy: this.settings.filterTaxonomy, filter_term: this.currentFilter, search_query: this.currentSearch, page: this.currentPage, show_excerpt: this.settings.showExcerpt, cpt_actions: this.settings.cptActionsRaw }; $.ajax({ url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php', type: 'POST', data: ajaxData, success: (response) => { this.handleFilterResponse(response); }, error: (xhr, status, error) => { console.error('Dynamic Posts Grid AJAX Error:', error); this.hideLoading(); this.isLoading = false; } }); }, loadMore() { // Skip AJAX calls in Elementor editor if (elementorFrontend.isEditMode()) { return; } if (this.isLoading) return; this.currentPage++; this.isLoading = true; this.showLoading(); const ajaxData = { action: 'king_addons_dynamic_posts_grid_load_more', nonce: window.KingAddonsDynamicPostsGrid?.nonce || '', widget_id: this.settings.widgetId, posts_per_page: this.settings.postsPerPage, post_types: this.settings.postTypes, orderby: this.settings.orderby, order: this.settings.order, filter_taxonomy: this.settings.filterTaxonomy, filter_term: this.currentFilter, search_query: this.currentSearch, page: this.currentPage, show_excerpt: this.settings.showExcerpt, cpt_actions: this.settings.cptActionsRaw }; $.ajax({ url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php', type: 'POST', data: ajaxData, success: (response) => { this.handleLoadMoreResponse(response); }, error: (xhr, status, error) => { console.error('Dynamic Posts Grid Load More Error:', error); this.hideLoading(); this.isLoading = false; this.currentPage--; // Revert page increment on error } }); }, handleFilterResponse(response) { this.hideLoading(); this.isLoading = false; if (response.success && response.data) { // Fade out current content this.grid.addClass('king-addons-dpg-zero-opacity'); setTimeout(() => { // Replace grid content if (typeof $.fn.isotopekng !== 'undefined' && !elementorFrontend.isEditMode()) { this.grid.isotopekng('destroy'); } this.grid.html(response.data.posts_html); // Re-initialize isotope this.initIsotope(); // Update pagination this.updatePagination(response.data); // Fade in new content setTimeout(() => { this.grid.removeClass('king-addons-dpg-zero-opacity'); this.animateNewItems(); }, 100); }, 300); } else { this.showError(response.data?.message || 'Failed to load posts'); } }, handleLoadMoreResponse(response) { this.hideLoading(); this.isLoading = false; if (response.success && response.data) { const newItems = $(response.data.posts_html); // Add new items to grid this.grid.append(newItems); // Animate new items newItems.addClass('king-addons-dpg-fade-in'); // Re-layout isotope if (typeof $.fn.isotopekng !== 'undefined' && !elementorFrontend.isEditMode()) { this.grid.isotopekng('appended', newItems); // Re-layout after images load if (typeof $.fn.imagesLoaded !== 'undefined') { newItems.imagesLoaded(() => { this.relayoutGrid(); }); } } // Update pagination this.updatePagination(response.data); // Ensure clicks on links/buttons do not bubble to card-click handler this.wrapper.off('click', '.king-addons-dpg-card a, .king-addons-dpg-card button') .on('click', '.king-addons-dpg-card a, .king-addons-dpg-card button', (e) => { e.stopPropagation(); }); // If CPT mode, re-apply client-side filter (post type + search) if (widgetMode === 'custom_cpt') { // Apply CPT icons to newly added items this.applyCptIcons(newItems); this.filterByPostType(); } } else { this.showError(response.data?.message || 'Failed to load more posts'); this.currentPage--; // Revert page increment } }, applyCptIcons($scopeItems) { if (!this.cptIcons || typeof this.cptIcons !== 'object') return; $scopeItems.each((_, el) => { const $card = $(el); const postType = $card.data('post-type'); if (!postType) return; const conf = this.cptIcons[postType]; if (!conf) return; const $iconWrap = $card.find('.king-addons-dpg-icon'); if (conf.icon_type === 'image' && conf.image_url) { $iconWrap.html('' + postType + ''); } else if (conf.icon_class) { $iconWrap.html(''); } }); }, updatePagination(data) { if (data.current_page >= data.max_pages) { this.loadMoreBtn.hide(); this.showFinished(data.total_posts, data.current_count); } else { this.loadMoreBtn.attr('data-page', data.current_page); this.loadMoreBtn.attr('data-max-pages', data.max_pages); this.loadMoreBtn.show(); this.finishDiv.hide(); } // Update counts $scope.find('.king-addons-dpg-current-count').text(data.current_count); $scope.find('.king-addons-dpg-total-count').text(data.total_posts); }, showLoading() { this.loadingDiv.show(); this.loadMoreBtn.prop('disabled', true); // Add loading spinner to button if (!this.loadMoreBtn.find('.king-addons-dpg-loading-spinner').length) { this.loadMoreBtn.prepend(''); } }, hideLoading() { this.loadingDiv.hide(); this.loadMoreBtn.prop('disabled', false); this.loadMoreBtn.find('.king-addons-dpg-loading-spinner').remove(); }, showFinished(total, current) { this.finishDiv.find('.king-addons-dpg-current-count').text(current); this.finishDiv.find('.king-addons-dpg-total-count').text(total); this.finishDiv.fadeIn(1000); setTimeout(() => { this.finishDiv.fadeOut(1000); }, 3000); }, showError(message) { // Create and show error message const errorDiv = $('
' + message + '
'); this.wrapper.prepend(errorDiv); setTimeout(() => { errorDiv.fadeOut(() => { errorDiv.remove(); }); }, 5000); }, animateNewItems() { this.grid.find('.king-addons-dpg-card').each((index, element) => { setTimeout(() => { $(element).addClass('king-addons-dpg-fade-in'); }, index * 100); }); }, debounce(func, wait, immediate) { let timeout; return function() { const context = this; const args = arguments; const later = function() { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; }, // PRO Methods filterByPostType() { // Filter posts by post type (for CPT mode) if (widgetMode === 'custom_cpt') { const $cards = this.grid.find('.king-addons-dpg-item'); let $candidate = $cards; // Filter by selected post type first if (this.currentFilter !== '*') { $candidate = $candidate.filter('[data-post-type="' + this.currentFilter + '"]'); } // Then apply keyword search (title text) const query = (this.currentSearch || '').toString().trim().toLowerCase(); if (query.length > 0) { $candidate = $candidate.filter((_, el) => { const $el = $(el); const titleText = $el.find('.king-addons-dpg-title').text().toLowerCase(); const excerptText = $el.find('.king-addons-dpg-excerpt').text().toLowerCase(); return (titleText.indexOf(query) !== -1) || (excerptText.indexOf(query) !== -1); }); } // Show only matching cards $cards.hide(); $candidate.show(); // Re-layout if using Isotope if (this.grid.data('isotopekng')) { this.grid.isotopekng('layout'); } } } }; // Initialize the grid handler gridHandler.init(); // Handle responsive behavior $(window).on('resize', gridHandler.debounce(() => { gridHandler.relayoutGrid(); }, 250)); // Handle Elementor editor mode if (window.elementorFrontend?.isEditMode()) { // Re-initialize when settings change in editor elementorFrontend.hooks.addAction('panel/open_editor/widget/king-addons-dynamic-posts-grid', () => { setTimeout(() => { gridHandler.init(); }, 100); }); } }); // PRO version uses the same hook name as Free version, no separate hook needed }); })(jQuery);