Changeset 3451372
- Timestamp:
- 02/01/2026 12:51:22 PM (5 weeks ago)
- Location:
- flexi-post-grid
- Files:
-
- 11 edited
-
assets/screenshot-6.png (modified) (previous)
-
trunk/ajax-handler.php (modified) (4 diffs)
-
trunk/assets/blog-grid-filter.js (modified) (8 diffs)
-
trunk/assets/style.css (modified) (1 diff)
-
trunk/flexipostgridbuilder.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (6 diffs)
-
trunk/widgets/blog-grid-controls-content.php (modified) (1 diff)
-
trunk/widgets/blog-grid-controls-style.php (modified) (2 diffs)
-
trunk/widgets/blog-grid-widget.php (modified) (2 diffs)
-
trunk/widgets/partials/editor-static-output.php (modified) (3 diffs)
-
trunk/widgets/partials/frontend-ajax-output.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
flexi-post-grid/trunk/ajax-handler.php
r3363422 r3451372 304 304 // Last resort: plugin placeholder (also saved as a real file for custom) 305 305 if ( ! $image_url ) { 306 if ( file_exists( $default_image_full_path ) ) { 306 307 // If user set a custom fallback image in the widget, prefer that directly 308 if ( ! empty( $fallback_image ) ) { 309 $image_url = esc_url_raw( $fallback_image ); 310 311 } elseif ( $default_image_full_path && file_exists( $default_image_full_path ) ) { 312 307 313 $uploads = wp_upload_dir(); 308 314 if ( ! empty( $uploads['path'] ) && ! file_exists( $uploads['path'] ) ) { … … 310 316 } 311 317 312 $editor = wp_get_image_editor( $default_image_full_path ); 313 if ( ! is_wp_error( $editor ) ) { 314 $wph = 0; $hph = 0; 315 switch ( $ir ) { 316 case 'thumbnail': $wph = 150; $hph = 150; break; 317 case 'medium': $wph = 300; $hph = 300; break; 318 case 'large': $wph = 1024; $hph = 1024; break; 319 case 'custom_500': 320 case '500x500': $wph = 500; $hph = 500; break; 321 case 'custom': 322 case 'custom size': 323 case 'custom_size': 324 case 'custom-size': 325 $wph = max( 1, (int) $custom_width ); 326 $hph = max( 1, (int) $custom_height ); 327 if ( $wph && ! $hph ) { $hph = $wph; } 328 if ( $hph && ! $wph ) { $wph = $hph; } 329 break; 330 case 'full': 331 default: 332 // no resize 333 break; 334 } 335 336 if ( $wph && $hph ) { 337 if ( method_exists( $editor, 'set_quality' ) ) { 338 $editor->set_quality( 90 ); 318 // ----- Decide target size for placeholder (you wanted this block) ----- 319 $wph = 0; 320 $hph = 0; 321 322 switch ( $ir ) { 323 case 'thumbnail': 324 $wph = 150; 325 $hph = 150; 326 break; 327 328 case 'medium': 329 $wph = 300; 330 $hph = 300; 331 break; 332 333 case 'large': 334 $wph = 1024; 335 $hph = 1024; 336 break; 337 338 case 'custom_500': 339 case '500x500': 340 $wph = 500; 341 $hph = 500; 342 break; 343 344 case 'custom': 345 case 'custom size': 346 case 'custom_size': 347 case 'custom-size': 348 $wph = max( 1, (int) $custom_width ); 349 $hph = max( 1, (int) $custom_height ); 350 if ( $wph && ! $hph ) { 351 $hph = $wph; 339 352 } 340 $editor->resize( $wph, $hph, true ); 341 $saved = $editor->save( trailingslashit( $uploads['path'] ) . 'wppostgrid-ph-' . $wph . 'x' . $hph . '.jpg' ); 342 if ( ! is_wp_error( $saved ) && ! empty( $saved['path'] ) ) { 343 $image_url = esc_url( add_query_arg( 344 'v', 345 filemtime( $saved['path'] ), 346 str_replace( $uploads['basedir'], $uploads['baseurl'], $saved['path'] ) 347 ) ); 353 if ( $hph && ! $wph ) { 354 $wph = $hph; 355 } 356 break; 357 358 case 'full': 359 default: 360 // no resize, use original placeholder 361 break; 362 } 363 364 // If no resize requested (full or invalid) → use original placeholder directly 365 if ( ! $wph || ! $hph ) { 366 $image_url = esc_url_raw( $default_image_url ); 367 368 } else { 369 370 $dest_name = 'wppostgrid-ph-' . $wph . 'x' . $hph . '.jpg'; 371 $dest_path = trailingslashit( $uploads['path'] ) . $dest_name; 372 $dest_url = trailingslashit( $uploads['url'] ) . $dest_name; 373 374 // If file already exists and is a valid (non-empty) image, reuse it 375 if ( file_exists( $dest_path ) && filesize( $dest_path ) > 5000 ) { 376 377 $image_url = esc_url_raw( 378 add_query_arg( 'v', filemtime( $dest_path ), $dest_url ) 379 ); 380 381 } else { 382 383 // Generate / regenerate resized placeholder safely 384 $editor = wp_get_image_editor( $default_image_full_path ); 385 386 if ( ! is_wp_error( $editor ) ) { 387 if ( method_exists( $editor, 'set_quality' ) ) { 388 $editor->set_quality( 90 ); 389 } 390 391 $editor->resize( $wph, $hph, true ); 392 $saved = $editor->save( $dest_path ); 393 394 if ( ! is_wp_error( $saved ) && ! empty( $saved['path'] ) ) { 395 $generated = $saved['path']; 396 397 // Wait a tiny bit until file is fully written (avoid 0-byte image) 398 $max_wait_ms = 200; // total wait ~200ms max 399 $step_ms = 10; 400 401 while ( $max_wait_ms > 0 && file_exists( $generated ) && filesize( $generated ) < 5000 ) { 402 usleep( $step_ms * 1000 ); // microseconds 403 clearstatcache(); 404 $max_wait_ms -= $step_ms; 405 } 406 407 if ( file_exists( $generated ) && filesize( $generated ) > 5000 ) { 408 $image_url = esc_url_raw( 409 add_query_arg( 410 'v', 411 filemtime( $generated ), 412 str_replace( $uploads['basedir'], $uploads['baseurl'], $generated ) 413 ) 414 ); 415 } 416 } 417 } 418 419 // If something failed, fall back to original placeholder URL 420 if ( empty( $image_url ) ) { 421 $image_url = esc_url_raw( $default_image_url ); 348 422 } 349 423 } 350 351 if ( ! $image_url ) { $image_url = $default_image_url; }352 } else {353 $image_url = $default_image_url;354 424 } 425 355 426 } else { 356 $image_url = $default_image_url; 427 // If default placeholder file is missing, still send a safe URL value 428 $image_url = esc_url_raw( $default_image_url ); 357 429 } 358 430 } … … 384 456 } 385 457 458 // Decode title first (fix &) 459 $clean_title = html_entity_decode( get_the_title(), ENT_QUOTES, 'UTF-8' ); 386 460 // Title HTML 387 461 $title_html = ''; 388 462 if ( $show_title ) { 389 $title_html = "<{$title_tag} class='post-title'><a href='" . esc_url( get_permalink() ) . "'>" . esc_html( get_the_title()) . "</a></{$title_tag}>";463 $title_html = "<{$title_tag} class='post-title'><a href='" . esc_url( get_permalink() ) . "'>" . esc_html( $clean_title ) . "</a></{$title_tag}>"; 390 464 } 391 465 … … 395 469 // Collect 396 470 $response['posts'][] = array( 397 'title' => get_the_title(),471 'title' => $clean_title, 398 472 'title_html' => $title_html, 399 473 'excerpt' => $show_excerpt ? wp_trim_words( get_the_excerpt(), $excerpt_length ) : '', -
flexi-post-grid/trunk/assets/blog-grid-filter.js
r3363422 r3451372 36 36 const paginationContainer = $(`#pagination-${uniqueId}`); 37 37 const filterContainer = $this.find(`.category-filter`); 38 const paginationMode = (postsContainer.data('pagination-mode') || 'pagination'); // 'pagination' | 'load_more' | 'infinite' 39 const showPaginationFlag = (postsContainer.data('show-pagination') === 'yes'); 40 let lastLoadedPage = 1; // track page for load more / infinite 41 let io = null; // IntersectionObserver ref 42 38 43 39 44 function setContainerHeight() { … … 47 52 48 53 function loadPosts(category = 'all', page = 1) { 54 const effectiveMode = showPaginationFlag ? paginationMode : 'none'; 55 const isAppend = (effectiveMode !== 'pagination' && effectiveMode !== 'none' && page > 1); 56 const nextPage = page; 49 57 // Prevent loading in Elementor editor preview mode 50 58 if (typeof elementor !== 'undefined' && elementor.editMode) { … … 81 89 let hoverAnimation = postsContainer.data('hover-animation'); 82 90 83 setContainerHeight(); //Lock height before clearing content 84 $this.addClass('loading'); //Add lazy load effect 85 86 postsContainer.html(''); // Clear content without showing "Loading..." text 87 paginationContainer.html(''); 91 if (!isAppend) { 92 setContainerHeight(); 93 $this.addClass('loading'); 94 postsContainer.html(''); 95 paginationContainer.html(''); 96 } 88 97 89 98 $.ajax({ … … 270 279 } 271 280 272 postsContainer.html(html); 281 if (isAppend) { 282 postsContainer.append(html); 283 } else { 284 postsContainer.html(html); 285 286 // ---- FIXED INFINITE SCROLL (IO + SCROLL-BOTTOM, NO autoInfinite) ---- 287 if (paginationMode === 'infinite' && showPaginationFlag) { 288 289 // ensure sentinel exists 290 if (!$this.find('.fpg-infinite-sentinel').length) { 291 postsContainer.after('<div class="fpg-infinite-sentinel"></div>'); 292 } 293 294 // reset previous observer for this grid 295 if (io) { io.disconnect(); io = null; } 296 297 let isLoadingMore = false; 298 299 // helper: load next page safely 300 const triggerNextPage = () => { 301 const totalPages = parseInt(postsContainer.data('total-pages') || 1, 10); 302 const nextPage = lastLoadedPage + 1; 303 304 if (isLoadingMore) return; 305 if (nextPage > totalPages) return; 306 307 isLoadingMore = true; 308 309 const category = filterContainer.find('li.active').data('category') || 'all'; 310 loadPosts(category, nextPage); 311 312 // small delay lock to avoid double-fire 313 setTimeout(() => { isLoadingMore = false; }, 500); 314 }; 315 316 // IntersectionObserver → when sentinel enters viewport 317 setTimeout(() => { 318 const $sentinel = $this.find('.fpg-infinite-sentinel'); 319 if (!$sentinel.length || !('IntersectionObserver' in window)) return; 320 321 io = new IntersectionObserver((entries) => { 322 entries.forEach(entry => { 323 324 // grid itself must be visible 325 const rect = postsContainer[0].getBoundingClientRect(); 326 const gridVisible = rect.top < window.innerHeight && rect.bottom > 0; 327 if (!gridVisible) return; 328 329 if (entry.isIntersecting) { 330 triggerNextPage(); 331 } 332 }); 333 }, { 334 root: null, 335 rootMargin: '10px', // start loading a bit before bottom 336 threshold: 0.01 337 }); 338 339 io.observe($sentinel[0]); 340 }, 150); 341 342 // Fallback: when scrollbar hits bottom of page (like Pexels) 343 $(window) 344 .off('scroll.fpgBottom-' + uniqueId) 345 .on('scroll.fpgBottom-' + uniqueId, function () { 346 347 const scrollBottom = window.innerHeight + window.scrollY; 348 const pageBottom = document.body.offsetHeight - 120; 349 350 if (scrollBottom >= pageBottom) { 351 triggerNextPage(); 352 } 353 }); 354 } 355 356 357 } 358 359 // === Insert Loader (bottom) if not exists === 360 if (!$this.find('.fpg-loader-bottom').length) { 361 postsContainer.after('<div class="fpg-loader-bottom"><div class="fpg-spinner"></div></div>'); 362 } 273 363 274 364 //Sequential Lazy Load Animation … … 277 367 setTimeout(() => { 278 368 post.classList.add('loaded'); // Add animation with delay 279 }, index * 150); // Delay of 150ms between each post369 }, index * 30); // Delay of 150ms between each post 280 370 }); 371 372 // Wait until animation complete then remove loader 373 setTimeout(() => { 374 const remaining = $this.find(`.blog-post.blogPosts-${uniqueId}:not(.loaded)`).length; 375 if (remaining === 0) { 376 $this.find('.fpg-loader-bottom').remove(); 377 } 378 }, blogPostsEls.length * 30 + 200); 281 379 282 380 $this.removeClass('loading'); //Remove loader after content loads 283 381 resetContainerHeight(); //Reset height after load 284 382 383 285 384 // Pagination 286 385 let paginationHtml = '<div class="pagination">'; … … 288 387 paginationHtml += `<button class="prev-page" data-page="${page - 1}">Prev</button>`; 289 388 } 290 291 let totalPages = parseInt(response.total_pages, 10) || 1; 292 let startPage = Math.max(page - 1, 1); 293 let endPage = Math.min(startPage + 3, totalPages); 294 295 if (endPage - startPage < 3) { 296 startPage = Math.max(endPage - 3, 1); 389 390 // ===== Pagination UI handling (mode-aware) ===== 391 const totalPages = parseInt(response.total_pages, 10) || 1; 392 postsContainer.data('total-pages', totalPages); // <-- NEW 393 lastLoadedPage = nextPage; 394 395 if (effectiveMode === 'pagination') { 396 let paginationHtml = '<div class="pagination">'; 397 if (nextPage > 1) paginationHtml += `<button class="prev-page" data-page="${nextPage - 1}">Prev</button>`; 398 399 let startPage = Math.max(nextPage - 1, 1); 400 let endPage = Math.min(startPage + 3, totalPages); 401 if (endPage - startPage < 3) startPage = Math.max(endPage - 3, 1); 402 403 for (let i = startPage; i <= endPage; i++) { 404 paginationHtml += `<button class="page-number ${i === nextPage ? 'active' : ''}" data-page="${i}">${i}</button>`; 405 } 406 407 if (nextPage < totalPages) paginationHtml += `<button class="next-page" data-page="${nextPage + 1}">Next</button>`; 408 paginationHtml += '</div>'; 409 410 paginationContainer.html(paginationHtml).toggle(showPaginationFlag); 411 412 } else if (effectiveMode === 'load_more') { 413 const $btn = $this.find('.fpg-load-more'); 414 if (nextPage >= totalPages) { 415 $btn.prop('disabled', true).addClass('is-hidden'); 416 } else { 417 $btn.prop('disabled', false).removeClass('is-hidden'); 418 } 419 paginationContainer.empty(); 420 421 } else if (effectiveMode === 'infinite') { 422 paginationContainer.empty(); 423 424 if (nextPage >= totalPages) { 425 // all pages loaded → cleanup 426 if (io) { io.disconnect(); io = null; } 427 $this.find('.fpg-infinite-sentinel').remove(); 428 $(window).off('scroll.fpgBottom-' + uniqueId); 429 } 430 431 } else { 432 paginationContainer.empty(); 433 $this.find('.fpg-load-more').closest('.fpg-load-more-wrap').remove(); 434 $this.find('.fpg-infinite-sentinel').remove(); 297 435 } 298 436 299 for (let i = startPage; i <= endPage; i++) {300 paginationHtml += `<button class="page-number ${i === page ? 'active' : ''}" data-page="${i}">${i}</button>`;301 }302 303 if (page < totalPages) {304 paginationHtml += `<button class="next-page" data-page="${page + 1}">Next</button>`;305 }306 307 paginationHtml += '</div>';308 paginationContainer.html(paginationHtml);309 437 } 310 438 … … 341 469 filterContainer.find('li').removeClass('active'); 342 470 $(this).addClass('active'); 343 $this.removeClass('loading');344 471 let category = $(this).data('category'); 345 loadPosts(category); 472 lastLoadedPage = 1; 473 474 if (io) { io.disconnect(); io = null; } 475 476 loadPosts(category, 1); 346 477 }); 478 479 347 480 348 481 // Pagination click event … … 352 485 loadPosts(category, page); 353 486 }); 487 488 // LOAD MORE (only when enabled) 489 if (showPaginationFlag && paginationMode === 'load_more') { 490 $this.off('click.fpg').on('click.fpg', '.fpg-load-more', function (e) { 491 e.preventDefault(); 492 const category = filterContainer.find('li.active').data('category') || 'all'; 493 loadPosts(category, lastLoadedPage + 1); 494 }); 495 } 354 496 355 497 //Initial load -
flexi-post-grid/trunk/assets/style.css
r3363422 r3451372 351 351 /* Disable meta link if data-disable-meta-click="yes" */ 352 352 [data-disable-meta-click="yes"] .post-meta a { pointer-events: none; cursor: default; } 353 354 .fpg-load-more { cursor:pointer; } 355 .fpg-load-more.is-hidden { display:none; } 356 357 358 359 360 /* ------------------------------------------------- 361 Bottom loader for infinite scroll: .fpg-loader-bottom 362 (Keeps existing overlay loader untouched) 363 ------------------------------------------------- */ 364 365 /* 1) Base style: make it behave like the last grid item */ 366 .fpg-loader-bottom { 367 display: none; /* hidden when not loading */ 368 position: relative; 369 min-height: 70px; /* space for spinner */ 370 width: 100%; 371 top: 0 !important; 372 373 /* For CSS grid layouts: force full-row item at bottom */ 374 grid-column: 1 / -1; 375 376 /* For flex layouts: push to the very end */ 377 order: 9999; 378 } 379 380 /* 2) Only show when the widget is in "loading" state */ 381 .fpg-loader-bottom { 382 display: block; 383 } 384 385 /* 3) Spinner inside .fpg-loader-bottom 386 Reuse the SAME animation style as your existing loader 387 (uses your existing @keyframes l32-1 and l32-2) 388 */ 389 .fpg-loader-bottom::before { 390 content: ""; 391 position: absolute; 392 top: 50%; 393 left: 50%; 394 transform: translate(-50%, -50%); 395 396 /* same look as .blog-grid-filter.loading::after */ 397 --c: no-repeat linear-gradient(#090275 0 0); 398 background: 399 var(--c), var(--c), var(--c), 400 var(--c), var(--c), var(--c), 401 var(--c), var(--c), var(--c); 402 background-size: 12px 12px; 403 border-radius: 4px; 404 animation: l32-1 1s infinite, l32-2 1s infinite; 405 } 406 407 /* Optional: tiny top margin so it doesn't stick to last card */ 408 .fpg-loader-bottom { 409 margin-top: 12px; 410 } 411 412 .blog-posts-container .blog-post{ 413 visibility: hidden; 414 height: 0; 415 opacity: 0; 416 } 417 418 .blog-posts-container .blog-post:not(.loaded) { 419 padding: 0 !important; 420 } 421 422 .blog-posts-container .blog-post.loaded{ 423 opacity: 1; 424 visibility: visible; 425 height: auto; 426 } -
flexi-post-grid/trunk/flexipostgridbuilder.php
r3363422 r3451372 4 4 * Plugin URI: https://creativewebui.com/flexi-post-grid/ 5 5 * Description: Create customizable post grids with advanced filtering, pagination, and Elementor integration. Includes preset grid styles so users can quickly design layouts without extra effort. 6 * Version: 1. 0.06 * Version: 1.1.0 7 7 * Author: CreativeWebUI 8 8 * Author URI: https://creativewebui.com … … 12 12 * Domain Path: /languages 13 13 * Requires at least: 5.6 14 * Tested up to: 6. 814 * Tested up to: 6.9 15 15 * Requires PHP: 7.4 16 16 */ -
flexi-post-grid/trunk/readme.txt
r3369120 r3451372 1 1 === Flexi Post Grid === 2 2 Contributors: creativewebui 3 Tags: elementor , post grid, ajax, filters, bloggrid3 Tags: elementor grid, event grid, blog grid, post slider, product grid 4 4 Requires at least: 5.6 5 Tested up to: 6. 85 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.07 Stable tag: 1.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 AJAX-powered grid widget for Elementor with filters, pagination, and custom fields.11 AJAX-powered Post Grid widget for Elementor with preset layouts, filters, pagination types, and slider support. 12 12 13 13 == Description == 14 14 15 Flexi Post Grid helps you build professional post grids in Elementor with ease. 15 Our most important feature is the **preset grid styles**, so users can instantly apply modern designs without spending time on manual customization. At the same time, we provide **complete design controls inside the widget**, allowing users to easilycustomize layouts, typography, colors, spacing, and more — starting from a preset style or building their own.16 Our most important feature is the **preset grid styles**, so users can instantly apply modern designs without spending time on manual customization. At the same time, we provide **complete design controls inside the widget**, allowing users to customize layouts, typography, colors, spacing, and more — starting from a preset style or building their own. 16 17 17 **Live Links** 18 - [🌐 Plugin Demo](https://creativewebui.com/flexi-post-grid/preset-grids/) 19 - [📘 Documentation](https://creativewebui.com/flexi-post-grid/documentation/) 20 - [⭐ Get Pro Version](https://creativewebui.com/flexi-post-grid/pricing/) 18 With Flexi Post Grid, you can also turn any grid layout into a **Post Grid Slider / Carousel** by simply enabling the Slider option from the Grid Settings. No extra widget or complex setup is required. 19 20 Flexi Post Grid supports multiple pagination types including **Classic Pagination**, **Load More Button (AJAX)**, and **Infinite Scroll (AJAX)**, giving you full control over how posts are loaded and displayed. 21 22 You can use Flexi Post Grid to create grids for blogs, news posts, portfolios, team members, events, and custom post types with smooth filtering and pagination. 23 24 **Video Demo** 25 Watch the introduction video to see how Flexi Post Grid works with Elementor: 26 https://www.youtube.com/watch?v=OiRiQo9zrlo 27 28 --- 29 30 == Free Version Highlights == 21 31 22 32 **Highlights** 23 33 - 10+ responsive preset layouts (Blog, News, Portfolio, Team, Event) 24 34 - Native Elementor controls (drag & drop, live preview) 25 - AJAX filters and pagination 35 - AJAX filters and pagination (Classic Pagination, Load More Button (AJAX), and Infinite Scroll (AJAX)) 26 36 - Image overlays, gradients, and hover animations 27 37 - Fine control over image sizing (built-in sizes or custom), object-fit, and wrapper height … … 32 42 This plugin integrates with **Elementor** and adds a “Flexi Post Grid” widget you can place anywhere. 33 43 44 --- 45 46 == Pro Version Highlights == 47 48 **Live Links** 49 - [🌐 Plugin Demo](https://creativewebui.com/flexi-post-grid/preset-grids/) 50 - [📘 Documentation](https://creativewebui.com/flexi-post-grid/documentation/) 51 - [⭐ Get Pro Version](https://creativewebui.com/flexi-post-grid/pricing/) 52 53 **Highlights** 54 - 16+ responsive preset layouts (Blog, News, Portfolio, Team, Event, Product Grid) 55 - WooCommerce Product Grid with price, swatches, hover image & wishlist 56 - Grid Slider / Carousel (enable slider from grid settings) 57 - Slider controls: slides per view, autoplay, arrows, dots, speed & spacing 58 - Slider arrows styling: color, background, size, border radius & hover effects 59 - Native Elementor controls (drag & drop, live preview) 60 - AJAX filters and pagination (Classic Pagination, Load More Button (AJAX), and Infinite Scroll (AJAX)) 61 - Image overlays, gradients, and hover animations 62 - Fine control over image sizing (built-in sizes or custom), object-fit, and wrapper height 63 - Per-device columns and ordering (desktop / tablet / mobile) 64 - Optional custom CSS per grid 65 - Accessible and translation-ready (`flexi-post-grid-pro` text domain) 66 67 --- 68 34 69 == Screenshots == 35 70 1. Settings — Manually include post types (Posts, Pages & CPTs) with one-click Save and quick Docs access. … … 37 72 3. Image settings — resolution, crop/fit, wrapper height, spacing, borders, and radius. 38 73 4. Style panel — tune buttons, titles, descriptions, meta, overlays, filters, and pagination. 39 5. Preset grid styles — pick from 10+ ready layouts such as Blog, News, Portfolio, and List. 40 6. Customizable filters & pagination — category tabs and AJAX pagination on the front end. 74 5. Preset grid styles — pick from Blog, News, Portfolio, Team, and Product Grid layouts. 75 6. Customizable filters & pagination — Classic Pagination, Load More Button (AJAX), and Infinite Scroll (AJAX). 76 7. Grid Slider / Carousel — enable slider from grid settings and control autoplay, arrows, and speed. 77 8. WooCommerce Product Grid — price, color swatches, hover image, wishlist, and custom buttons. 78 79 --- 41 80 42 81 == Installation == … … 45 84 3. Activate **Flexi Post Grid** from **Plugins → Installed Plugins**. 46 85 4. Open Elementor, search for **Flexi Post Grid**, and drop it into your layout. 47 5. Configure your grid (layout, filters, custom fields, meta, etc.) and publish. 86 5. Configure your grid (layout, filters, pagination, slider, custom fields, etc.) and publish. 87 88 --- 48 89 49 90 == Frequently Asked Questions == … … 55 96 Yes. This plugin adds a widget for Elementor. 56 97 98 = What pagination types are available? = 99 Flexi Post Grid supports three pagination types: Classic Pagination, Load More Button (AJAX), and Infinite Scroll (AJAX). These pagination options are available in both Free and Pro versions. 100 57 101 = Is it translation-ready? = 58 102 Yes. Text domain is `flexi-post-grid`. When installed from WordPress.org, language packs load automatically. A `.pot` file is included in `/languages`. … … 61 105 Yes. Filtering and pagination are AJAX-driven for a smooth browsing experience. 62 106 63 == Third-party Libraries == 64 This plugin bundles the following third-party assets: 65 66 - lazysizes (https://github.com/aFarkas/lazysizes) – MIT License. 67 The minified file is included at `vendor/lazysizes/lazysizes.min.js` (license text in `vendor/lazysizes/LICENSE`). 68 <!-- If your path is different, update these two paths to match your zip. --> 107 --- 69 108 70 109 == Changelog == 110 111 = 1.1.0 = 112 * Added three pagination types: Classic Pagination, Load More Button (AJAX), and Infinite Scroll (AJAX). 113 * UI and performance improvements. 114 * Fixed special character display issue (e.g., ) in headings. 71 115 72 116 = 1.0.0 = 73 117 * Initial release: Elementor grid widget with AJAX filters, pagination, overlays, animations, and custom fields. 74 118 119 --- 120 75 121 == Upgrade Notice == 122 123 = 1.1.0 = 124 New pagination types and performance improvements added. 76 125 77 126 = 1.0.0 = -
flexi-post-grid/trunk/widgets/blog-grid-controls-content.php
r3363422 r3451372 275 275 ); 276 276 277 // Pagination Mode (Select) 278 $this->add_control( 279 'pagination_mode', 280 [ 281 'label' => esc_html__('Pagination Mode', 'flexi-post-grid'), 282 'type' => \Elementor\Controls_Manager::SELECT, 283 'default' => 'pagination', // pagination | load_more | infinite 284 'options' => [ 285 'pagination' => esc_html__('Classic Pagination', 'flexi-post-grid'), 286 'load_more' => esc_html__('Load More Button (AJAX)', 'flexi-post-grid'), 287 'infinite' => esc_html__('Infinite Scroll (AJAX)', 'flexi-post-grid'), 288 ], 289 'condition' => [ // 👈 NEW 290 'show_pagination' => 'yes', 291 ], 292 ] 293 ); 294 277 295 $this->add_control( 278 296 'show_filter', -
flexi-post-grid/trunk/widgets/blog-grid-controls-style.php
r3363422 r3451372 3299 3299 'selectors' => [ 3300 3300 '{{WRAPPER}} .blog-grid-filter.loading::after' => '--c:no-repeat linear-gradient({{VALUE}} 0 0);', 3301 '{{WRAPPER}} .fpg-loader-bottom::before' => '--c:no-repeat linear-gradient({{VALUE}} 0 0);', 3301 3302 ], 3302 3303 ] … … 3305 3306 $this->end_controls_section(); 3306 3307 3308 3309 // Load More Button Styles 3310 $this->start_controls_section( 3311 'fpg_load_more_style_section', 3312 [ 3313 'label' => __( 'Load More Button', 'flexi-post-grid' ), 3314 'tab' => \Elementor\Controls_Manager::TAB_STYLE, 3315 'condition' => [ 'pagination_mode' => 'load_more' ], 3316 ] 3317 ); 3318 3319 // Typography 3320 $this->add_group_control( 3321 \Elementor\Group_Control_Typography::get_type(), 3322 [ 3323 'name' => 'fpg_load_more_typography', 3324 'selector' => '{{WRAPPER}} .fpg-load-more', 3325 ] 3326 ); 3327 3328 // Padding 3329 $this->add_responsive_control( 3330 'fpg_load_more_padding', 3331 [ 3332 'label' => __( 'Padding', 'flexi-post-grid' ), 3333 'type' => \Elementor\Controls_Manager::DIMENSIONS, 3334 'size_units' => [ 'px', 'em', '%' ], 3335 'selectors' => [ 3336 '{{WRAPPER}} .fpg-load-more' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', 3337 ], 3338 ] 3339 ); 3340 3341 // Margin 3342 $this->add_responsive_control( 3343 'fpg_load_more_margin', 3344 [ 3345 'label' => __( 'Margin', 'flexi-post-grid' ), 3346 'type' => \Elementor\Controls_Manager::DIMENSIONS, 3347 'size_units' => [ 'px', 'em', '%' ], 3348 'selectors' => [ 3349 '{{WRAPPER}} .fpg-load-more-wrap' => 'margin: {{TOP}}{{UNIT}} auto {{BOTTOM}}{{UNIT}} auto;', 3350 ], 3351 ] 3352 ); 3353 3354 // Border 3355 $this->add_group_control( 3356 \Elementor\Group_Control_Border::get_type(), 3357 [ 3358 'name' => 'fpg_load_more_border', 3359 'selector' => '{{WRAPPER}} .fpg-load-more', 3360 ] 3361 ); 3362 3363 // Border Radius 3364 $this->add_responsive_control( 3365 'fpg_load_more_border_radius', 3366 [ 3367 'label' => __( 'Border Radius', 'flexi-post-grid' ), 3368 'type' => \Elementor\Controls_Manager::DIMENSIONS, 3369 'size_units' => [ 'px', '%' ], 3370 'selectors' => [ 3371 '{{WRAPPER}} .fpg-load-more' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', 3372 ], 3373 ] 3374 ); 3375 3376 // Full Width Toggle 3377 $this->add_control( 3378 'fpg_load_more_fullwidth', 3379 [ 3380 'label' => __( 'Full Width Button', 'flexi-post-grid' ), 3381 'type' => \Elementor\Controls_Manager::SWITCHER, 3382 'selectors' => [ 3383 '{{WRAPPER}} .fpg-load-more' => 'width: 100%; display:block;', 3384 ], 3385 ] 3386 ); 3387 3388 // Alignment 3389 $this->add_responsive_control( 3390 'fpg_load_more_alignment', 3391 [ 3392 'label' => __( 'Alignment', 'flexi-post-grid' ), 3393 'type' => \Elementor\Controls_Manager::CHOOSE, 3394 'options' => [ 3395 'left' => [ 3396 'title' => __( 'Left', 'flexi-post-grid' ), 3397 'icon' => 'eicon-text-align-left', 3398 ], 3399 'center' => [ 3400 'title' => __( 'Center', 'flexi-post-grid' ), 3401 'icon' => 'eicon-text-align-center', 3402 ], 3403 'right' => [ 3404 'title' => __( 'Right', 'flexi-post-grid' ), 3405 'icon' => 'eicon-text-align-right', 3406 ], 3407 ], 3408 'default' => 'center', 3409 'selectors' => [ 3410 '{{WRAPPER}} .fpg-load-more-wrap' => 'text-align: {{VALUE}};', 3411 ], 3412 ] 3413 ); 3414 3415 /* ------------------------------------------------------- 3416 * Normal + Hover Tabs (Text + Background Colors Only) 3417 * ------------------------------------------------------*/ 3418 $this->start_controls_tabs( 3419 'fpg_load_more_color_tabs' 3420 ); 3421 3422 /* --------------------------------- 3423 * Normal Tab 3424 * ---------------------------------*/ 3425 $this->start_controls_tab( 3426 'fpg_load_more_tab_normal', 3427 [ 3428 'label' => __( 'Normal', 'flexi-post-grid' ), 3429 ] 3430 ); 3431 3432 // Normal Text Color 3433 $this->add_control( 3434 'fpg_load_more_text_color', 3435 [ 3436 'label' => __( 'Text Color', 'flexi-post-grid' ), 3437 'type' => \Elementor\Controls_Manager::COLOR, 3438 'selectors' => [ 3439 '{{WRAPPER}} .fpg-load-more' => 'color: {{VALUE}};', 3440 ], 3441 ] 3442 ); 3443 3444 // Normal Background Color 3445 $this->add_control( 3446 'fpg_load_more_bg_color', 3447 [ 3448 'label' => __( 'Background Color', 'flexi-post-grid' ), 3449 'type' => \Elementor\Controls_Manager::COLOR, 3450 'selectors' => [ 3451 '{{WRAPPER}} .fpg-load-more' => 'background-color: {{VALUE}};', 3452 ], 3453 ] 3454 ); 3455 3456 $this->end_controls_tab(); 3457 3458 /* --------------------------------- 3459 * Hover Tab 3460 * ---------------------------------*/ 3461 $this->start_controls_tab( 3462 'fpg_load_more_tab_hover', 3463 [ 3464 'label' => __( 'Hover', 'flexi-post-grid' ), 3465 ] 3466 ); 3467 3468 // Hover Text Color 3469 $this->add_control( 3470 'fpg_load_more_hover_text_color', 3471 [ 3472 'label' => __( 'Text Color (Hover)', 'flexi-post-grid' ), 3473 'type' => \Elementor\Controls_Manager::COLOR, 3474 'selectors' => [ 3475 '{{WRAPPER}} .fpg-load-more:hover' => 'color: {{VALUE}};', 3476 ], 3477 ] 3478 ); 3479 3480 // Hover Background Color 3481 $this->add_control( 3482 'fpg_load_more_hover_bg_color', 3483 [ 3484 'label' => __( 'Background Color (Hover)', 'flexi-post-grid' ), 3485 'type' => \Elementor\Controls_Manager::COLOR, 3486 'selectors' => [ 3487 '{{WRAPPER}} .fpg-load-more:hover' => 'background-color: {{VALUE}};', 3488 ], 3489 ] 3490 ); 3491 3492 // Hover Border Color (Unique — No Conflict) 3493 $this->add_control( 3494 'fpg_load_more_hover_border_color', 3495 [ 3496 'label' => __( 'Border Color (Hover)', 'flexi-post-grid' ), 3497 'type' => \Elementor\Controls_Manager::COLOR, 3498 'selectors' => [ 3499 '{{WRAPPER}} .fpg-load-more:hover' => 'border-color: {{VALUE}};', 3500 ], 3501 ] 3502 ); 3503 3504 $this->end_controls_tab(); 3505 $this->end_controls_tabs(); 3506 3507 $this->end_controls_section(); 3508 -
flexi-post-grid/trunk/widgets/blog-grid-widget.php
r3363422 r3451372 150 150 $hover_animation_class = !empty($settings['button_hover_animation']) ? 'elementor-animation-' . $settings['button_hover_animation'] : ''; 151 151 152 // NEW: Pagination Mode values for partials 153 $pagination_mode = $settings['pagination_mode'] ?? 'pagination'; // pagination | load_more | infinite 154 $load_more_text = !empty($settings['load_more_text']) ? $settings['load_more_text'] : esc_html__('Load More', 'flexi-post-grid'); 155 156 152 157 153 158 // ===== Dynamic Category Options Update ===== // … … 182 187 $cols, $cols_tablet, $cols_mobile, $gutter_px 183 188 ); 189 190 // NEW: make available to partials without changing structure 191 $fpg_runtime_pagination_mode = $pagination_mode; 192 $fpg_runtime_load_more_text = $load_more_text; 193 $fpg_runtime_style_vars = $style_vars; 194 $fpg_runtime_title_tag = $title_tag; 195 $fpg_runtime_fallback_image = $settings['fallback_image']['url'] ?? ''; 196 184 197 185 198 echo '<div class="blog-grid-filter" data-unique-id="' . esc_attr($unique_id) . '">'; -
flexi-post-grid/trunk/widgets/partials/editor-static-output.php
r3363422 r3451372 42 42 } 43 43 44 // Decide effective mode based on Show Pagination 45 $__show_pagination = isset( $settings['show_pagination'] ) && $settings['show_pagination'] === 'yes'; 46 $__pagination_mode = $__show_pagination 47 ? ( isset( $settings['pagination_mode'] ) ? $settings['pagination_mode'] : 'pagination' ) 48 : 'none'; // disable all pagination UIs when switch is off 49 $__load_label = ! empty( $settings['load_more_text'] ) 50 ? $settings['load_more_text'] 51 : esc_html__( 'Load More', 'flexi-post-grid' ); 52 44 53 // Container with data attributes 45 54 echo '<div id="' . esc_attr( $unique_id ) . '" class="blog-posts-container ' . esc_attr( $grid_style ) . '" … … 56 65 data-custom-image-height="' . esc_attr( isset( $settings['custom_image_height'] ) ? $settings['custom_image_height'] : '' ) . '" 57 66 data-show-excerpt="' . esc_attr( $show_excerpt ) . '" 58 data-excerpt-length="' . esc_attr( $excerpt_length ) . '" 67 data-excerpt-length="' . esc_attr( $excerpt_length ) . '" 68 data-pagination-mode="' . esc_attr( $__pagination_mode ) . '" 69 data-load-more-text="' . esc_attr( $__load_label ) . '" 59 70 data-show-meta="' . esc_attr( isset( $settings['show_meta'] ) ? $settings['show_meta'] : '' ) . '" 60 71 data-show-pagination="' . esc_attr( isset( $settings['show_pagination'] ) ? $settings['show_pagination'] : '' ) . '" … … 152 163 </div>'; 153 164 ?> 165 166 <?php if ( $__show_pagination && 'load_more' === $__pagination_mode ) : ?> 167 <div class="fpg-load-more-wrap"> 168 <button type="button" class="fpg-load-more"><?php echo esc_html( $__load_label ); ?></button> 169 </div> 170 171 <?php elseif ( $__show_pagination && 'infinite' === $__pagination_mode ) : ?> 172 <div class="fpg-infinite-sentinel" aria-hidden="true"></div> 173 174 <?php endif; ?> 175 176 <?php if ( $__show_pagination && 'pagination' === $__pagination_mode ) : ?> 177 <div id="pagination-<?php echo esc_attr( $unique_id ); ?>" class="pagination1"></div> 178 <?php endif; ?> -
flexi-post-grid/trunk/widgets/partials/frontend-ajax-output.php
r3363422 r3451372 32 32 } 33 33 34 // Decide effective mode based on Show Pagination 35 $__show_pagination = isset( $settings['show_pagination'] ) && $settings['show_pagination'] === 'yes'; 36 $__pagination_mode = $__show_pagination 37 ? ( isset( $settings['pagination_mode'] ) ? $settings['pagination_mode'] : 'pagination' ) 38 : 'none'; // disable all pagination UIs when switch is off 39 $__load_label = ! empty( $settings['load_more_text'] ) 40 ? $settings['load_more_text'] 41 : esc_html__( 'Load More', 'flexi-post-grid' ); 42 34 43 echo '<div id="' . esc_attr( $unique_id ) . '" class="blog-posts-container ' . esc_attr( $grid_style ) . '" 35 44 data-unique-id="' . esc_attr( $unique_id ) . '" … … 46 55 data-show-excerpt="' . esc_attr( $show_excerpt ) . '" 47 56 data-excerpt-length="' . esc_attr( $excerpt_length ) . '" 57 data-pagination-mode="' . esc_attr( $__pagination_mode ) . '" 58 data-load-more-text="' . esc_attr( $__load_label ) . '" 48 59 data-show-meta="' . esc_attr( isset( $settings['show_meta'] ) ? $settings['show_meta'] : '' ) . '" 49 60 data-show-pagination="' . esc_attr( isset( $settings['show_pagination'] ) ? $settings['show_pagination'] : '' ) . '" … … 81 92 </div>'; 82 93 83 // Empty pagination wrapper for dynamic controls 84 echo '<div id="pagination-' . esc_attr( $unique_id ) . '" class="pagination1"></div>'; 85 ?> 94 if ( $__show_pagination && 'load_more' === $__pagination_mode ) : ?> 95 <div class="fpg-load-more-wrap"> 96 <button type="button" class="fpg-load-more"><?php echo esc_html( $__load_label ); ?></button> 97 </div> 98 99 <?php elseif ( $__show_pagination && 'infinite' === $__pagination_mode ) : ?> 100 <div class="fpg-infinite-sentinel" aria-hidden="true"></div> 101 102 <?php endif; ?> 103 104 <?php if ( $__show_pagination && 'pagination' === $__pagination_mode ) : ?> 105 <div id="pagination-<?php echo esc_attr( $unique_id ); ?>" class="pagination1"></div> 106 <?php endif; ?>
Note: See TracChangeset
for help on using the changeset viewer.