Plugin Directory

Changeset 3451372


Ignore:
Timestamp:
02/01/2026 12:51:22 PM (5 weeks ago)
Author:
creativewebui
Message:

Update plugin to version 1.1.0

Location:
flexi-post-grid
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • flexi-post-grid/trunk/ajax-handler.php

    r3363422 r3451372  
    304304                // Last resort: plugin placeholder (also saved as a real file for custom)
    305305                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
    307313                        $uploads = wp_upload_dir();
    308314                        if ( ! empty( $uploads['path'] ) && ! file_exists( $uploads['path'] ) ) {
     
    310316                        }
    311317
    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;
    339352                                }
    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 );
    348422                                }
    349423                            }
    350 
    351                             if ( ! $image_url ) { $image_url = $default_image_url; }
    352                         } else {
    353                             $image_url = $default_image_url;
    354424                        }
     425
    355426                    } 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 );
    357429                    }
    358430                }
     
    384456            }
    385457
     458            // Decode title first (fix &amp;)
     459            $clean_title = html_entity_decode( get_the_title(), ENT_QUOTES, 'UTF-8' );
    386460            // Title HTML
    387461            $title_html = '';
    388462            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}>";
    390464            }
    391465
     
    395469            // Collect
    396470            $response['posts'][] = array(
    397                 'title'          => get_the_title(),
     471                'title' => $clean_title,
    398472                'title_html'     => $title_html,
    399473                'excerpt'        => $show_excerpt ? wp_trim_words( get_the_excerpt(), $excerpt_length ) : '',
  • flexi-post-grid/trunk/assets/blog-grid-filter.js

    r3363422 r3451372  
    3636        const paginationContainer = $(`#pagination-${uniqueId}`);
    3737        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
    3843
    3944        function setContainerHeight() {
     
    4752
    4853        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;
    4957          // Prevent loading in Elementor editor preview mode
    5058          if (typeof elementor !== 'undefined' && elementor.editMode) {
     
    8189          let hoverAnimation = postsContainer.data('hover-animation');
    8290
    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          }
    8897
    8998          $.ajax({
     
    270279                }
    271280
    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                }
    273363
    274364                //Sequential Lazy Load Animation
     
    277367                  setTimeout(() => {
    278368                    post.classList.add('loaded');  // Add animation with delay
    279                   }, index * 150);  // Delay of 150ms between each post
     369                  }, index * 30);  // Delay of 150ms between each post
    280370                });
     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);
    281379
    282380                $this.removeClass('loading'); //Remove loader after content loads
    283381                resetContainerHeight(); //Reset height after load
    284382
     383               
    285384                // Pagination
    286385                let paginationHtml = '<div class="pagination">';
     
    288387                  paginationHtml += `<button class="prev-page" data-page="${page - 1}">Prev</button>`;
    289388                }
    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();
    297435                }
    298436
    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);
    309437              }
    310438
     
    341469          filterContainer.find('li').removeClass('active');
    342470          $(this).addClass('active');
    343           $this.removeClass('loading');
    344471          let category = $(this).data('category');
    345           loadPosts(category);
     472          lastLoadedPage = 1;
     473
     474          if (io) { io.disconnect(); io = null; }
     475
     476          loadPosts(category, 1);
    346477        });
     478
     479
    347480
    348481        // Pagination click event
     
    352485          loadPosts(category, page);
    353486        });
     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        }
    354496
    355497        //Initial load
  • flexi-post-grid/trunk/assets/style.css

    r3363422 r3451372  
    351351/* Disable meta link if data-disable-meta-click="yes" */
    352352[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  
    44 * Plugin URI: https://creativewebui.com/flexi-post-grid/
    55 * 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.0
     6 * Version: 1.1.0
    77 * Author: CreativeWebUI
    88 * Author URI: https://creativewebui.com
     
    1212 * Domain Path: /languages
    1313 * Requires at least: 5.6
    14  * Tested up to: 6.8
     14 * Tested up to: 6.9
    1515 * Requires PHP: 7.4
    1616 */
  • flexi-post-grid/trunk/readme.txt

    r3369120 r3451372  
    11=== Flexi Post Grid ===
    22Contributors: creativewebui
    3 Tags: elementor, post grid, ajax, filters, blog grid
     3Tags: elementor grid, event grid, blog grid, post slider, product grid
    44Requires at least: 5.6
    5 Tested up to: 6.8
     5Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.0
     7Stable tag: 1.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 AJAX-powered grid widget for Elementor with filters, pagination, and custom fields.
     11AJAX-powered Post Grid widget for Elementor with preset layouts, filters, pagination types, and slider support.
    1212
    1313== Description ==
     14
    1415Flexi 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 easily customize layouts, typography, colors, spacing, and more — starting from a preset style or building their own.
     16Our 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.
    1617
    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/)
     18With 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
     20Flexi 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
     22You 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** 
     25Watch the introduction video to see how Flexi Post Grid works with Elementor:
     26https://www.youtube.com/watch?v=OiRiQo9zrlo
     27
     28---
     29
     30== Free Version Highlights ==
    2131
    2232**Highlights**
    2333- 10+ responsive preset layouts (Blog, News, Portfolio, Team, Event)
    2434- 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))
    2636- Image overlays, gradients, and hover animations
    2737- Fine control over image sizing (built-in sizes or custom), object-fit, and wrapper height
     
    3242This plugin integrates with **Elementor** and adds a “Flexi Post Grid” widget you can place anywhere.
    3343
     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
    3469== Screenshots ==
    35701. Settings — Manually include post types (Posts, Pages & CPTs) with one-click Save and quick Docs access.
     
    37723. Image settings — resolution, crop/fit, wrapper height, spacing, borders, and radius.
    38734. 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.
     745. Preset grid styles — pick from Blog, News, Portfolio, Team, and Product Grid layouts.
     756. Customizable filters & pagination — Classic Pagination, Load More Button (AJAX), and Infinite Scroll (AJAX).
     767. Grid Slider / Carousel — enable slider from grid settings and control autoplay, arrows, and speed.
     778. WooCommerce Product Grid — price, color swatches, hover image, wishlist, and custom buttons.
     78
     79---
    4180
    4281== Installation ==
     
    45843. Activate **Flexi Post Grid** from **Plugins → Installed Plugins**.
    46854. 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.
     865. Configure your grid (layout, filters, pagination, slider, custom fields, etc.) and publish.
     87
     88---
    4889
    4990== Frequently Asked Questions ==
     
    5596Yes. This plugin adds a widget for Elementor.
    5697
     98= What pagination types are available? =
     99Flexi 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
    57101= Is it translation-ready? =
    58102Yes. Text domain is `flexi-post-grid`. When installed from WordPress.org, language packs load automatically. A `.pot` file is included in `/languages`.
     
    61105Yes. Filtering and pagination are AJAX-driven for a smooth browsing experience.
    62106
    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---
    69108
    70109== 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., &nbsp;) in headings.
    71115
    72116= 1.0.0 =
    73117* Initial release: Elementor grid widget with AJAX filters, pagination, overlays, animations, and custom fields.
    74118
     119---
     120
    75121== Upgrade Notice ==
     122
     123= 1.1.0 =
     124New pagination types and performance improvements added.
    76125
    77126= 1.0.0 =
  • flexi-post-grid/trunk/widgets/blog-grid-controls-content.php

    r3363422 r3451372  
    275275);
    276276
     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
    277295$this->add_control(
    278296    'show_filter',
  • flexi-post-grid/trunk/widgets/blog-grid-controls-style.php

    r3363422 r3451372  
    32993299        'selectors' => [
    33003300            '{{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);',
    33013302        ],
    33023303    ]
     
    33053306$this->end_controls_section();
    33063307
     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  
    150150        $hover_animation_class = !empty($settings['button_hover_animation']) ? 'elementor-animation-' . $settings['button_hover_animation'] : '';
    151151       
     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
    152157       
    153158        // ===== Dynamic Category Options Update ===== //
     
    182187            $cols, $cols_tablet, $cols_mobile, $gutter_px
    183188        );
     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
    184197       
    185198        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  
    4242}
    4343
     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
    4453// Container with data attributes
    4554echo '<div id="' . esc_attr( $unique_id ) . '" class="blog-posts-container ' . esc_attr( $grid_style ) . '"
     
    5665        data-custom-image-height="' . esc_attr( isset( $settings['custom_image_height'] ) ? $settings['custom_image_height'] : '' ) . '"
    5766        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 ) . '"
    5970        data-show-meta="' . esc_attr( isset( $settings['show_meta'] ) ? $settings['show_meta'] : '' ) . '"
    6071        data-show-pagination="' . esc_attr( isset( $settings['show_pagination'] ) ? $settings['show_pagination'] : '' ) . '"
     
    152163    </div>';
    153164?>
     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  
    3232}
    3333
     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
    3443echo '<div id="' . esc_attr( $unique_id ) . '" class="blog-posts-container ' . esc_attr( $grid_style ) . '"
    3544        data-unique-id="' . esc_attr( $unique_id ) . '"
     
    4655        data-show-excerpt="' . esc_attr( $show_excerpt ) . '"
    4756        data-excerpt-length="' . esc_attr( $excerpt_length ) . '"
     57        data-pagination-mode="' . esc_attr( $__pagination_mode ) . '"
     58        data-load-more-text="' . esc_attr( $__load_label ) . '"
    4859        data-show-meta="' . esc_attr( isset( $settings['show_meta'] ) ? $settings['show_meta'] : '' ) . '"
    4960        data-show-pagination="' . esc_attr( isset( $settings['show_pagination'] ) ? $settings['show_pagination'] : '' ) . '"
     
    8192    </div>';
    8293
    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.