PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.76
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.76
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 51.1.51 All 36 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.76, at includes/widgets/Dynamic_Posts_Grid/script.js

757 lines 33.8 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 // 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
51 init() {
52 this.wrapper = $scope.find('.king-addons-dpg-wrapper');
53 this.grid = $scope.find('.king-addons-dpg-grid');
54 this.filterBar = $scope.find('.king-addons-dpg-filter-bar');
55 this.filterSelect = $scope.find('.king-addons-dpg-posts-filter');
56 this.searchInput = $scope.find('.king-addons-dpg-posts-search');
57 this.searchBtn = $scope.find('.king-addons-dpg-search-btn');
58 this.loadMoreBtn = $scope.find('.king-addons-dpg-load-more-btn');
59 this.pagination = $scope.find('.king-addons-dpg-pagination');
60 this.loadingDiv = $scope.find('.king-addons-dpg-pagination-loading');
61 this.finishDiv = $scope.find('.king-addons-dpg-pagination-finish');
62
63 this.settings = this.getSettings();
64 this.currentPage = 1;
65 this.isLoading = false;
66 this.currentFilter = '*';
67 this.currentSearch = '';
68
69 this.bindEvents();
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 }
80 },
81
82 getSettings() {
83 return {
84 widgetId: this.wrapper.data('widget-id'),
85 postsPerPage: this.wrapper.data('posts-per-page'),
86 postTypes: this.wrapper.data('post-types'),
87 orderby: this.wrapper.data('orderby'),
88 order: this.wrapper.data('order'),
89 filterTaxonomy: this.wrapper.data('filter-taxonomy'),
90 showExcerpt: this.wrapper.data('show-excerpt'),
91 cardClickable: this.wrapper.data('card-clickable'),
92 cptActionsRaw: this.wrapper.attr('data-cpt-actions') || '',
93 cptIconsRaw: this.wrapper.attr('data-cpt-icons') || ''
94 };
95 },
96
97 bindEvents() {
98 // Filter dropdown change
99 this.filterSelect.on('change', (e) => {
100 this.currentFilter = $(e.target).val();
101 this.currentPage = 1;
102 this.filterAndSearch();
103 });
104
105 // Search input events
106 this.searchInput.on('keyup', this.debounce((e) => {
107 this.currentSearch = $(e.target).val();
108 this.currentPage = 1;
109 this.filterAndSearch();
110 }, 500));
111
112 this.searchBtn.on('click', () => {
113 this.currentSearch = this.searchInput.val();
114 this.currentPage = 1;
115 this.filterAndSearch();
116 });
117
118 // Search on Enter key
119 this.searchInput.on('keypress', (e) => {
120 if (e.which === 13) {
121 this.currentSearch = this.searchInput.val();
122 this.currentPage = 1;
123 this.filterAndSearch();
124 }
125 });
126
127 // Load More button
128 this.loadMoreBtn.on('click', () => {
129 this.loadMore();
130 });
131
132 // Card click functionality
133 this.bindCardClickEvents();
134
135 // Action button events
136 this.bindActionButtonEvents();
137 },
138
139 bindCardClickEvents() {
140 // Only enable card clicking if setting is enabled and not in Elementor editor
141 if (this.settings.cardClickable === 1 && !elementorFrontend.isEditMode()) {
142 // Use event delegation for dynamically loaded content
143 this.wrapper.on('click', '.king-addons-dpg-card', (e) => {
144 // Don't trigger if clicking on a link or button inside the card
145 if ($(e.target).closest('a, button, .king-addons-dpg-button').length > 0) {
146 return;
147 }
148
149 // Find the post link within the card
150 const postLink = $(e.currentTarget).find('.king-addons-dpg-title a');
151 if (postLink.length > 0) {
152 const postUrl = postLink.attr('href');
153 if (postUrl) {
154 const safePostUrl = this.normalizeSafeUrl(postUrl);
155 if (safePostUrl) {
156 window.location.href = safePostUrl;
157 }
158 }
159 }
160 });
161
162 // Add cursor pointer style to cards when clickable
163 this.wrapper.addClass('king-addons-dpg-cards-clickable');
164 }
165 },
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
384 initIsotope() {
385 // Skip Isotope initialization in Elementor editor
386 if (elementorFrontend.isEditMode()) {
387 return;
388 }
389
390 // Initialize isotope if available
391 if (typeof $.fn.isotopekng !== 'undefined') {
392 this.grid.isotopekng({
393 itemSelector: '.king-addons-dpg-card',
394 layoutMode: 'masonry',
395 masonry: {
396 columnWidth: '.king-addons-dpg-card'
397 },
398 transitionDuration: '0.3s'
399 });
400 }
401
402 // Images loaded callback (skip in editor)
403 if (typeof $.fn.imagesLoaded !== 'undefined' && !elementorFrontend.isEditMode()) {
404 this.grid.imagesLoaded(() => {
405 this.relayoutGrid();
406 });
407 }
408 },
409
410 relayoutGrid() {
411 // Skip in Elementor editor
412 if (elementorFrontend.isEditMode()) {
413 return;
414 }
415
416 if (typeof $.fn.isotopekng !== 'undefined') {
417 this.grid.isotopekng('layout');
418 }
419 },
420
421 filterAndSearch() {
422 // PRO: Use client-side filtering for CPT mode
423 if (widgetMode === 'custom_cpt') {
424 this.filterByPostType();
425 return;
426 }
427
428 // Skip AJAX calls in Elementor editor
429 if (elementorFrontend.isEditMode()) {
430 return;
431 }
432
433 if (this.isLoading) return;
434
435 this.isLoading = true;
436 this.showLoading();
437
438 const ajaxData = {
439 action: 'king_addons_dynamic_posts_grid_filter',
440 nonce: window.KingAddonsDynamicPostsGrid?.nonce || '',
441 widget_id: this.settings.widgetId,
442 posts_per_page: this.settings.postsPerPage,
443 post_types: this.settings.postTypes,
444 orderby: this.settings.orderby,
445 order: this.settings.order,
446 filter_taxonomy: this.settings.filterTaxonomy,
447 filter_term: this.currentFilter,
448 search_query: this.currentSearch,
449 page: this.currentPage,
450 show_excerpt: this.settings.showExcerpt,
451 cpt_actions: this.settings.cptActionsRaw
452 };
453
454 $.ajax({
455 url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php',
456 type: 'POST',
457 data: ajaxData,
458 success: (response) => {
459 this.handleFilterResponse(response);
460 },
461 error: (xhr, status, error) => {
462 console.error('Dynamic Posts Grid AJAX Error:', error);
463 this.hideLoading();
464 this.isLoading = false;
465 }
466 });
467 },
468
469 loadMore() {
470 // Skip AJAX calls in Elementor editor
471 if (elementorFrontend.isEditMode()) {
472 return;
473 }
474
475 if (this.isLoading) return;
476
477 this.currentPage++;
478 this.isLoading = true;
479 this.showLoading();
480
481 const ajaxData = {
482 action: 'king_addons_dynamic_posts_grid_load_more',
483 nonce: window.KingAddonsDynamicPostsGrid?.nonce || '',
484 widget_id: this.settings.widgetId,
485 posts_per_page: this.settings.postsPerPage,
486 post_types: this.settings.postTypes,
487 orderby: this.settings.orderby,
488 order: this.settings.order,
489 filter_taxonomy: this.settings.filterTaxonomy,
490 filter_term: this.currentFilter,
491 search_query: this.currentSearch,
492 page: this.currentPage,
493 show_excerpt: this.settings.showExcerpt,
494 cpt_actions: this.settings.cptActionsRaw
495 };
496
497 $.ajax({
498 url: window.KingAddonsDynamicPostsGrid?.ajaxUrl || '/wp-admin/admin-ajax.php',
499 type: 'POST',
500 data: ajaxData,
501 success: (response) => {
502 this.handleLoadMoreResponse(response);
503 },
504 error: (xhr, status, error) => {
505 console.error('Dynamic Posts Grid Load More Error:', error);
506 this.hideLoading();
507 this.isLoading = false;
508 this.currentPage--; // Revert page increment on error
509 }
510 });
511 },
512
513 handleFilterResponse(response) {
514 this.hideLoading();
515 this.isLoading = false;
516
517 if (response.success && response.data) {
518 // Fade out current content
519 this.grid.addClass('king-addons-dpg-zero-opacity');
520
521 setTimeout(() => {
522 // Replace grid content
523 if (typeof $.fn.isotopekng !== 'undefined' && !elementorFrontend.isEditMode()) {
524 this.grid.isotopekng('destroy');
525 }
526
527 this.grid.html(response.data.posts_html);
528
529 // Re-initialize isotope
530 this.initIsotope();
531
532 // Update pagination
533 this.updatePagination(response.data);
534
535 // Fade in new content
536 setTimeout(() => {
537 this.grid.removeClass('king-addons-dpg-zero-opacity');
538 this.animateNewItems();
539 }, 100);
540
541 }, 300);
542 } else {
543 this.showError(response.data?.message || 'Failed to load posts');
544 }
545 },
546
547 handleLoadMoreResponse(response) {
548 this.hideLoading();
549 this.isLoading = false;
550
551 if (response.success && response.data) {
552 const newItems = $(response.data.posts_html);
553
554 // Add new items to grid
555 this.grid.append(newItems);
556
557 // Animate new items
558 newItems.addClass('king-addons-dpg-fade-in');
559
560 // Re-layout isotope
561 if (typeof $.fn.isotopekng !== 'undefined' && !elementorFrontend.isEditMode()) {
562 this.grid.isotopekng('appended', newItems);
563
564 // Re-layout after images load
565 if (typeof $.fn.imagesLoaded !== 'undefined') {
566 newItems.imagesLoaded(() => {
567 this.relayoutGrid();
568 });
569 }
570 }
571
572 // Update pagination
573 this.updatePagination(response.data);
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
588 } else {
589 this.showError(response.data?.message || 'Failed to load more posts');
590 this.currentPage--; // Revert page increment
591 }
592 },
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
621 updatePagination(data) {
622 if (data.current_page >= data.max_pages) {
623 this.loadMoreBtn.hide();
624 this.showFinished(data.total_posts, data.current_count);
625 } else {
626 this.loadMoreBtn.attr('data-page', data.current_page);
627 this.loadMoreBtn.attr('data-max-pages', data.max_pages);
628 this.loadMoreBtn.show();
629 this.finishDiv.hide();
630 }
631
632 // Update counts
633 $scope.find('.king-addons-dpg-current-count').text(data.current_count);
634 $scope.find('.king-addons-dpg-total-count').text(data.total_posts);
635 },
636
637 showLoading() {
638 this.loadingDiv.show();
639 this.loadMoreBtn.prop('disabled', true);
640
641 // Add loading spinner to button
642 if (!this.loadMoreBtn.find('.king-addons-dpg-loading-spinner').length) {
643 this.loadMoreBtn.prepend('<span class="king-addons-dpg-loading-spinner"></span>');
644 }
645 },
646
647 hideLoading() {
648 this.loadingDiv.hide();
649 this.loadMoreBtn.prop('disabled', false);
650 this.loadMoreBtn.find('.king-addons-dpg-loading-spinner').remove();
651 },
652
653 showFinished(total, current) {
654 this.finishDiv.find('.king-addons-dpg-current-count').text(current);
655 this.finishDiv.find('.king-addons-dpg-total-count').text(total);
656 this.finishDiv.fadeIn(1000);
657
658 setTimeout(() => {
659 this.finishDiv.fadeOut(1000);
660 }, 3000);
661 },
662
663 showError(message) {
664 // Create and show error message
665 const errorDiv = $('<div class="king-addons-dpg-error-message">' + message + '</div>');
666 this.wrapper.prepend(errorDiv);
667
668 setTimeout(() => {
669 errorDiv.fadeOut(() => {
670 errorDiv.remove();
671 });
672 }, 5000);
673 },
674
675 animateNewItems() {
676 this.grid.find('.king-addons-dpg-card').each((index, element) => {
677 setTimeout(() => {
678 $(element).addClass('king-addons-dpg-fade-in');
679 }, index * 100);
680 });
681 },
682
683 debounce(func, wait, immediate) {
684 let timeout;
685 return function() {
686 const context = this;
687 const args = arguments;
688 const later = function() {
689 timeout = null;
690 if (!immediate) func.apply(context, args);
691 };
692 const callNow = immediate && !timeout;
693 clearTimeout(timeout);
694 timeout = setTimeout(later, wait);
695 if (callNow) func.apply(context, args);
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 }
731 }
732 };
733
734 // Initialize the grid handler
735 gridHandler.init();
736
737 // Handle responsive behavior
738 $(window).on('resize', gridHandler.debounce(() => {
739 gridHandler.relayoutGrid();
740 }, 250));
741
742 // Handle Elementor editor mode
743 if (window.elementorFrontend?.isEditMode()) {
744 // Re-initialize when settings change in editor
745 elementorFrontend.hooks.addAction('panel/open_editor/widget/king-addons-dynamic-posts-grid', () => {
746 setTimeout(() => {
747 gridHandler.init();
748 }, 100);
749 });
750 }
751 });
752
753 // PRO version uses the same hook name as Free version, no separate hook needed
754 });
755
756 })(jQuery);
757