PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 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 All 41 releases
← All changes | includes/widgets/Dynamic_Posts_Grid/script.js +363 -8 51.1.14 → 51.1.87 View file →
@@ -7,10 +7,48 @@
7 7 'use strict';
8 8
9 9 $(window).on("elementor/frontend/init", () => {
10 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');
11 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 +
12 23 const gridHandler = {
24 + // Sanitize HTML using DOMPurify for XSS protection
25 + sanitizeHtml(value) {
26 + if (typeof DOMPurify !== 'undefined') {
27 + return DOMPurify.sanitize(String(value), {ALLOWED_TAGS: [], ALLOWED_ATTR: []});
28 + }
29 + // Fallback
30 + return String(value)
31 + .replace(/&/g, "&")
32 + .replace(/</g, "&lt;")
33 + .replace(/>/g, "&gt;")
34 + .replace(/"/g, "&quot;")
35 + .replace(/'/g, "&#039;");
36 + },
37 +
38 + normalizeSafeUrl(rawUrl, allowedProtocols = ["http:", "https:"]) {
39 + if (!rawUrl || typeof rawUrl !== "string") return null;
40 + try {
41 + const url = new URL(rawUrl, window.location.href);
42 + if (allowedProtocols.includes(url.protocol)) {
43 + return url.href;
44 + }
45 + } catch (e) {
46 + // ignore
47 + }
48 + return null;
49 + },
50 +
13 51 init() {
14 52 this.wrapper = $scope.find('.king-addons-dpg-wrapper');
15 53 this.grid = $scope.find('.king-addons-dpg-grid');
16 54 this.filterBar = $scope.find('.king-addons-dpg-filter-bar');
@@ -29,8 +67,17 @@
29 67 this.currentSearch = '';
30 68
31 69 this.bindEvents();
32 70 this.initIsotope();
71 +
72 + // Prepare CPT icon map for client-side application after AJAX
73 + this.cptIcons = {};
74 + try {
75 + const raw = this.settings.cptIconsRaw || '';
76 + this.cptIcons = raw ? JSON.parse(raw) : {};
77 + } catch (e) {
78 + this.cptIcons = {};
79 + }
33 80 },
34 81
35 82 getSettings() {
36 83 return {
@@ -40,9 +87,11 @@
40 87 orderby: this.wrapper.data('orderby'),
41 88 order: this.wrapper.data('order'),
42 89 filterTaxonomy: this.wrapper.data('filter-taxonomy'),
43 90 showExcerpt: this.wrapper.data('show-excerpt'),
44 - cardClickable: this.wrapper.data('card-clickable')
91 + cardClickable: this.wrapper.data('card-clickable'),
92 + cptActionsRaw: this.wrapper.attr('data-cpt-actions') || '',
93 + cptIconsRaw: this.wrapper.attr('data-cpt-icons') || ''
45 94 };
46 95 },
47 96
48 97 bindEvents() {
@@ -81,8 +130,11 @@
81 130 });
82 131
83 132 // Card click functionality
84 133 this.bindCardClickEvents();
134 +
135 + // Action button events
136 + this.bindActionButtonEvents();
85 137 },
86 138
87 139 bindCardClickEvents() {
88 140 // Only enable card clicking if setting is enabled and not in Elementor editor
@@ -98,10 +150,12 @@
98 150 const postLink = $(e.currentTarget).find('.king-addons-dpg-title a');
99 151 if (postLink.length > 0) {
100 152 const postUrl = postLink.attr('href');
101 153 if (postUrl) {
102 - // Navigate to post
103 - window.location.href = postUrl;
154 + const safePostUrl = this.normalizeSafeUrl(postUrl);
155 + if (safePostUrl) {
156 + window.location.href = safePostUrl;
157 + }
104 158 }
105 159 }
106 160 });
107 161
@@ -109,8 +163,225 @@
109 163 this.wrapper.addClass('king-addons-dpg-cards-clickable');
110 164 }
111 165 },
112 166
167 + bindActionButtonEvents() {
168 + // Prevent multiple event bindings
169 + this.wrapper.off('click', '.king-addons-dpg-action-btn');
170 +
171 + // Use event delegation for dynamically loaded content
172 + this.wrapper.on('click', '.king-addons-dpg-action-btn', (e) => {
173 + e.preventDefault();
174 + e.stopPropagation();
175 +
176 + const $button = $(e.target).closest('.king-addons-dpg-action-btn');
177 +
178 + // Prevent double clicks and global lightbox blocking
179 + if ($button.data('clicking') || $('body').data('lightbox-opening')) {
180 + return;
181 + }
182 +
183 + $button.data('clicking', true);
184 + $('body').data('lightbox-opening', true);
185 +
186 + setTimeout(() => {
187 + $button.removeData('clicking');
188 + $('body').removeData('lightbox-opening');
189 + }, 1500);
190 +
191 + const action = $button.data('action');
192 + const url = $button.data('url');
193 + const title = $button.data('title') || '';
194 +
195 + if (!url) {
196 + // console.warn('No URL provided for action button');
197 + return;
198 + }
199 +
200 + switch (action) {
201 + case 'lightbox_image':
202 + this.openImageLightbox(url, title);
203 + break;
204 + case 'lightbox_video':
205 + this.openVideoLightbox(url, title);
206 + break;
207 + case 'new_tab':
208 + default:
209 + window.open(url, '_blank');
210 + break;
211 + }
212 + });
213 + },
214 +
215 + openImageLightbox(url, title) {
216 + const safeUrl = this.normalizeSafeUrl(url);
217 + if (!safeUrl) {
218 + return;
219 + }
220 +
221 + // Prevent multiple lightboxes from opening simultaneously
222 + if ($('.lg-backdrop, .lg-outer, .king-addons-dpg-lightbox-temp, [data-lg-uid]').length > 0) {
223 + return;
224 + }
225 +
226 + // Check if lightGallery is already running
227 + if (window.lgCurrentInstance) {
228 + return;
229 + }
230 +
231 + // Remove any orphaned containers first
232 + $('.king-addons-dpg-lightbox-temp').remove();
233 +
234 + // Create array with single image item for LightGallery
235 + const galleryItems = [{
236 + src: safeUrl,
237 + subHtml: title ? '<div>' + this.sanitizeHtml(title) + '</div>' : ''
238 + }];
239 +
240 + // Initialize LightGallery directly with dynamic gallery
241 + if (typeof $.fn.lightGallery !== 'undefined') {
242 + // Create a temporary div just for gallery initialization
243 + const $tempDiv = $('<div style="display:none;"></div>');
244 + $('body').append($tempDiv);
245 +
246 + $tempDiv.lightGallery({
247 + dynamic: true,
248 + dynamicEl: galleryItems,
249 + download: false,
250 + counter: false,
251 + zoom: true,
252 + fullScreen: true,
253 + controls: true,
254 + thumbnail: false,
255 + closable: true,
256 + escKey: true,
257 + keyPress: true
258 + });
259 +
260 + // Clean up on close
261 + $tempDiv.on('onCloseAfter.lg', function() {
262 + window.lgCurrentInstance = false;
263 + setTimeout(() => {
264 + $tempDiv.remove();
265 + }, 100);
266 + });
267 + } else {
268 + // console.error('LightGallery not available');
269 + window.open(safeUrl, '_blank');
270 + }
271 + },
272 +
273 + openVideoLightbox(url, title) {
274 + // Check if LightGallery is available
275 + if (typeof $.fn.lightGallery === 'undefined') {
276 + // console.warn('LightGallery not loaded, opening video in new tab');
277 + const safeUrl = this.normalizeSafeUrl(url);
278 + if (safeUrl) {
279 + window.open(safeUrl, '_blank');
280 + }
281 + return;
282 + }
283 +
284 + // For old LightGallery v1.6.12, let's create a manual video popup
285 + this.createYouTubePopup(url, title);
286 + },
287 +
288 + createYouTubePopup(url, title) {
289 + const safeUrl = this.normalizeSafeUrl(url);
290 + if (!safeUrl) {
291 + return;
292 + }
293 +
294 + // Process YouTube URL to get video ID
295 + const videoId = this.getYouTubeVideoId(safeUrl);
296 + if (!videoId) {
297 + // console.warn('Invalid YouTube URL');
298 + window.open(safeUrl, '_blank');
299 + return;
300 + }
301 +
302 + // Create manual video popup similar to LightGallery structure
303 + const popupHtml = `
304 + <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;">
305 + <div class="king-addons-video-container" style="position: relative; width: 90%; max-width: 1200px; max-height: 90%;">
306 + <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>
307 + <div class="king-addons-video-wrapper" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
308 + <iframe
309 + src="https://www.youtube.com/embed/${videoId}?autoplay=1&modestbranding=1&rel=0&showinfo=0&controls=1"
310 + style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;"
311 + allowfullscreen
312 + allow="autoplay; encrypted-media">
313 + </iframe>
314 + </div>
315 + ${title ? `<div style="color: white; text-align: center; margin-top: 10px;">${title}</div>` : ''}
316 + </div>
317 + </div>
318 + `;
319 +
320 + const $popup = $(popupHtml);
321 + $('body').append($popup);
322 +
323 + // Handle close events
324 + $popup.find('.king-addons-video-close').on('click', () => {
325 + this.closeVideoPopup($popup);
326 + });
327 +
328 + $popup.on('click', (e) => {
329 + if (e.target === $popup[0]) {
330 + this.closeVideoPopup($popup);
331 + }
332 + });
333 +
334 + // Handle ESC key
335 + $(document).on('keydown.video-popup', (e) => {
336 + if (e.keyCode === 27) {
337 + this.closeVideoPopup($popup);
338 + }
339 + });
340 +
341 + // Animate in
342 + $popup.css('opacity', 0).animate({opacity: 1}, 300);
343 + },
344 +
345 + getYouTubeVideoId(url) {
346 + // Extract video ID from various YouTube URL formats
347 + const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
348 + const match = url.match(regExp);
349 + return (match && match[7].length === 11) ? match[7] : null;
350 + },
351 +
352 + closeVideoPopup($popup) {
353 + // Stop video by removing iframe
354 + $popup.find('iframe').attr('src', '');
355 +
356 + // Remove event listeners
357 + $(document).off('keydown.video-popup');
358 +
359 + // Animate out and remove
360 + $popup.animate({opacity: 0}, 300, function() {
361 + $popup.remove();
362 + });
363 + },
364 +
365 + processYouTubeUrl(url) {
366 + // Handle different YouTube URL formats
367 + if (url.includes('youtube.com/watch?v=')) {
368 + // Already in correct format
369 + return url;
370 + } else if (url.includes('youtu.be/')) {
371 + // Convert youtu.be/VIDEO_ID to youtube.com/watch?v=VIDEO_ID
372 + const videoId = url.split('youtu.be/')[1].split('?')[0].split('&')[0];
373 + return `https://www.youtube.com/watch?v=${videoId}`;
374 + } else if (url.includes('youtube.com/embed/')) {
375 + // Convert youtube.com/embed/VIDEO_ID to youtube.com/watch?v=VIDEO_ID
376 + const videoId = url.split('/embed/')[1].split('?')[0].split('&')[0];
377 + return `https://www.youtube.com/watch?v=${videoId}`;
378 + }
379 +
380 + // Return original URL if not YouTube or unknown format
381 + return url;
382 + },
383 +
113 384 initIsotope() {
114 385 // Skip Isotope initialization in Elementor editor
115 386 if (elementorFrontend.isEditMode()) {
116 387 return;
@@ -147,8 +418,14 @@
147 418 }
148 419 },
149 420
150 421 filterAndSearch() {
422 + // PRO: Use client-side filtering for CPT mode
423 + if (widgetMode === 'custom_cpt') {
424 + this.filterByPostType();
425 + return;
426 + }
427 +
151 428 // Skip AJAX calls in Elementor editor
152 429 if (elementorFrontend.isEditMode()) {
153 430 return;
154 431 }
@@ -169,9 +446,10 @@
169 446 filter_taxonomy: this.settings.filterTaxonomy,
170 447 filter_term: this.currentFilter,
171 448 search_query: this.currentSearch,
172 449 page: this.currentPage,
173 - show_excerpt: this.settings.showExcerpt
450 + show_excerpt: this.settings.showExcerpt,
451 + cpt_actions: this.settings.cptActionsRaw
174 452 };
175 453
176 454 $.ajax({
177 455 url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php',
@@ -180,9 +458,9 @@
180 458 success: (response) => {
181 459 this.handleFilterResponse(response);
182 460 },
183 461 error: (xhr, status, error) => {
184 - console.error('Dynamic Posts Grid AJAX Error:', error);
462 + // console.error('Dynamic Posts Grid AJAX Error:', error);
185 463 this.hideLoading();
186 464 this.isLoading = false;
187 465 }
188 466 });
@@ -211,9 +489,10 @@
211 489 filter_taxonomy: this.settings.filterTaxonomy,
212 490 filter_term: this.currentFilter,
213 491 search_query: this.currentSearch,
214 492 page: this.currentPage,
215 - show_excerpt: this.settings.showExcerpt
493 + show_excerpt: this.settings.showExcerpt,
494 + cpt_actions: this.settings.cptActionsRaw
216 495 };
217 496
218 497 $.ajax({
219 498 url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php',
@@ -222,9 +501,9 @@
222 501 success: (response) => {
223 502 this.handleLoadMoreResponse(response);
224 503 },
225 504 error: (xhr, status, error) => {
226 - console.error('Dynamic Posts Grid Load More Error:', error);
505 + // console.error('Dynamic Posts Grid Load More Error:', error);
227 506 this.hideLoading();
228 507 this.isLoading = false;
229 508 this.currentPage--; // Revert page increment on error
230 509 }
@@ -292,8 +571,21 @@
292 571
293 572 // Update pagination
294 573 this.updatePagination(response.data);
295 574
575 + // Ensure clicks on links/buttons do not bubble to card-click handler
576 + this.wrapper.off('click', '.king-addons-dpg-card a, .king-addons-dpg-card button')
577 + .on('click', '.king-addons-dpg-card a, .king-addons-dpg-card button', (e) => {
578 + e.stopPropagation();
579 + });
580 +
581 + // If CPT mode, re-apply client-side filter (post type + search)
582 + if (widgetMode === 'custom_cpt') {
583 + // Apply CPT icons to newly added items
584 + this.applyCptIcons(newItems);
585 + this.filterByPostType();
586 + }
587 +
296 588 } else {
297 589 this.showError(response.data?.message || 'Failed to load more posts');
298 590 this.currentPage--; // Revert page increment
299 591 }
@@ -298,8 +590,35 @@
298 590 this.currentPage--; // Revert page increment
299 591 }
300 592 },
301 593
594 + applyCptIcons($scopeItems) {
595 + if (!this.cptIcons || typeof this.cptIcons !== 'object') return;
596 + $scopeItems.each((_, el) => {
597 + const $card = $(el);
598 + const postType = $card.data('post-type');
599 + if (!postType) return;
600 + const conf = this.cptIcons[postType];
601 + if (!conf) return;
602 +
603 + const $iconWrap = $card.find('.king-addons-dpg-icon');
604 + if (conf.icon_type === 'image' && conf.image_url) {
605 + const safeImgUrl = this.normalizeSafeUrl(conf.image_url);
606 + if (!safeImgUrl) return;
607 + $iconWrap.empty().append(
608 + $("<img />", {
609 + src: safeImgUrl,
610 + alt: String(postType || ""),
611 + })
612 + );
613 + } else if (conf.icon_class) {
614 + $iconWrap.empty().append(
615 + $("<i />").addClass(String(conf.icon_class || ""))
616 + );
617 + }
618 + });
619 + },
620 +
302 621 updatePagination(data) {
303 622 if (data.current_page >= data.max_pages) {
304 623 this.loadMoreBtn.hide();
305 624 this.showFinished(data.total_posts, data.current_count);
@@ -374,8 +693,42 @@
374 693 clearTimeout(timeout);
375 694 timeout = setTimeout(later, wait);
376 695 if (callNow) func.apply(context, args);
377 696 };
697 + },
698 +
699 + // PRO Methods
700 + filterByPostType() {
701 + // Filter posts by post type (for CPT mode)
702 + if (widgetMode === 'custom_cpt') {
703 + const $cards = this.grid.find('.king-addons-dpg-item');
704 + let $candidate = $cards;
705 +
706 + // Filter by selected post type first
707 + if (this.currentFilter !== '*') {
708 + $candidate = $candidate.filter('[data-post-type="' + this.currentFilter + '"]');
709 + }
710 +
711 + // Then apply keyword search (title text)
712 + const query = (this.currentSearch || '').toString().trim().toLowerCase();
713 + if (query.length > 0) {
714 + $candidate = $candidate.filter((_, el) => {
715 + const $el = $(el);
716 + const titleText = $el.find('.king-addons-dpg-title').text().toLowerCase();
717 + const excerptText = $el.find('.king-addons-dpg-excerpt').text().toLowerCase();
718 + return (titleText.indexOf(query) !== -1) || (excerptText.indexOf(query) !== -1);
719 + });
720 + }
721 +
722 + // Show only matching cards
723 + $cards.hide();
724 + $candidate.show();
725 +
726 + // Re-layout if using Isotope
727 + if (this.grid.data('isotopekng')) {
728 + this.grid.isotopekng('layout');
729 + }
730 + }
378 731 }
379 732 };
380 733
381 734 // Initialize the grid handler
@@ -395,7 +748,9 @@
395 748 }, 100);
396 749 });
397 750 }
398 751 });
752 +
753 + // PRO version uses the same hook name as Free version, no separate hook needed
399 754 });
400 755
401 -})(jQuery);
756 +})(jQuery);