rails_pagination_ultimate 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 465576ccd1cde103ef4befb850236d17d3105ef14c61cbba792f8ceff7f2aa6e
4
+ data.tar.gz: 9f35198d69e339ea5fd7c6535198cdb3bdd068338d65d074afd00cd33e3429fc
5
+ SHA512:
6
+ metadata.gz: 77e93931d1fbf86e6e3476438a726aaa15aeb6482183040ca7b2f57324c2334bf621ac060dc64a9da78aa981c57f45ca75041abc1ab6508b97fdbed0c7a49ed9
7
+ data.tar.gz: b70adad1aab1788d8626ffb6605d30bd64e1464c9ed79c6c17a4783939923fe262e5d405e7b4feb3ff5a677b3638bbfef944adc3a783566af0d3f2df25aa49a2
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-06-03
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shiboshree Roy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # 💎 RailsPaginationUltimate
2
+
3
+ **The ultra-premium, high-performance pagination powerhouse for modern Rails applications.**
4
+
5
+ RailsPaginationUltimate is built for developers who need more than just simple page links. It is a feature-rich, "Best-in-Class" replacement for Kaminari and WillPaginate, offering pro-grade UI components, native Tailwind CSS support, and advanced interactivity out of the box.
6
+
7
+ ---
8
+
9
+ ## 🚀 Key Features
10
+
11
+ * **⚡ Pro Performance**: `without_count` mode for lightning-fast paging on multi-million row tables.
12
+ * **🎨 Premium UI Styles**: Standard numbers, "Load More" buttons, and automated "Infinite Scroll".
13
+ * **🌊 Native Tailwind Support**: First-class support for Tailwind CSS utility classes.
14
+ * **⌨️ Keyboard Navigation**: Use Left/Right arrows to flip through pages.
15
+ * **🔄 History API**: AJAX updates automatically sync with the browser's URL and Back button.
16
+ * **🌍 100% Localizable**: Full I18n support for all labels, icons, and result strings.
17
+ * **🛠 Advanced Helpers**: Integrated `page_entries_info` and `per_page_select` components.
18
+ * **🧩 Total Customization**: Easy-to-use generator to export and modify HTML templates.
19
+
20
+ ---
21
+
22
+ ## 📦 Installation
23
+
24
+ Add this line to your application's `Gemfile`:
25
+
26
+ ```ruby
27
+ gem 'rails_pagination_ultimate', github: 'shiboshreeroy/rails_pagination_ultimate'
28
+ ```
29
+
30
+ And then execute:
31
+ ```bash
32
+ bundle install
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 📖 Usage
38
+
39
+ ### 1. Basic Pagination
40
+ In your controller:
41
+ ```ruby
42
+ @posts = Post.page(params[:page]).per(25)
43
+ ```
44
+
45
+ In your view:
46
+ ```erb
47
+ <%= paginate @posts %>
48
+ ```
49
+
50
+ ### 2. Pro Navigation Modes
51
+
52
+ #### 📱 Infinite Scroll (Modern Feed)
53
+ Automatically loads content as the user scrolls. Requires a `target` CSS selector.
54
+ ```erb
55
+ <div id="posts-container">
56
+ <%= render @posts %>
57
+ </div>
58
+
59
+ <%= paginate @posts, type: :infinite_scroll, target: "#posts-container" %>
60
+ ```
61
+
62
+ #### 🖱 "Load More" Button
63
+ A premium button that appends results without a full page reload.
64
+ ```erb
65
+ <%= paginate @posts, type: :load_more, target: "#posts-container" %>
66
+ ```
67
+
68
+ #### ⌨️ Keyboard & History Support
69
+ Enable pro-level accessibility and shareable URLs for AJAX pagination:
70
+ ```erb
71
+ <%= paginate @posts, theme: :tailwind, keyboard: true, history: true %>
72
+ ```
73
+
74
+ ---
75
+
76
+ ## ⚡ Performance Optimization
77
+
78
+ ### Fast Mode (`without_count`)
79
+ Traditional pagination runs a `COUNT(*)` query which can be slow on large tables. RailsPaginationUltimate's "Fast Mode" skips this count entirely and uses an efficient look-ahead check for the next page.
80
+
81
+ ```ruby
82
+ # Controller
83
+ @posts = Post.without_count.page(params[:page]).per(50)
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 🎨 Customization & Theming
89
+
90
+ ### A. Tailwind CSS (Recommended)
91
+ Use pro-grade Tailwind components by simply passing `theme: :tailwind`. No extra CSS files required!
92
+ ```erb
93
+ <%= paginate @posts, theme: :tailwind, first_last: true, window: 3 %>
94
+ ```
95
+
96
+ ### B. CSS Variables (Default Theme)
97
+ If you're not using Tailwind, the default theme is built with clean CSS variables for instant global styling:
98
+ ```css
99
+ :root {
100
+ --rp-primary-color: #3b82f6; /* Your Brand Color */
101
+ --rp-border-radius: 0.5rem;
102
+ --rp-text-color: #1f2937;
103
+ --rp-bg-color: #ffffff;
104
+ }
105
+ ```
106
+
107
+ ### C. Total UI Control (Generator)
108
+ Need to change the HTML? Export the Pro Tailwind template to your app:
109
+ ```bash
110
+ rails generate rails_pagination_ultimate:tailwind
111
+ ```
112
+ This creates `app/views/rails_pagination_ultimate/_pagination.html.erb`. You can then use it like this:
113
+ ```erb
114
+ <%= paginate @posts, template: "rails_pagination_ultimate/pagination" %>
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 🛠 Results Management
120
+
121
+ ### Detailed Information
122
+ Displays "Showing 1-25 of 150 results".
123
+ ```erb
124
+ <%= page_entries_info @posts %>
125
+ ```
126
+
127
+ ### Dynamic "Per Page" Selector
128
+ Let users choose how many items they want to see.
129
+ ```erb
130
+ <%= per_page_select @posts, choices: [25, 50, 100], label: "Items per page:" %>
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 🌍 Localization (I18n)
136
+
137
+ Everything is translatable in `config/locales/en.yml`:
138
+
139
+ ```yaml
140
+ en:
141
+ rails_pagination_ultimate:
142
+ previous: "← Previous"
143
+ next: "Next →"
144
+ first: "« First"
145
+ last: "Last »"
146
+ info:
147
+ other: "Showing <b>%{first}-%{last}</b> of <b>%{total}</b> results"
148
+ without_count: "Showing <b>%{first}-%{last}</b> results"
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 📋 Options Reference
154
+
155
+ | Option | Type | Default | Description |
156
+ | :--- | :--- | :--- | :--- |
157
+ | `theme` | Symbol | `nil` | Set to `:tailwind` for Tailwind UI. |
158
+ | `type` | Symbol | `:standard` | `:standard`, `:load_more`, or `:infinite_scroll`. |
159
+ | `target` | String | `nil` | CSS selector for where to append new items (AJAX). |
160
+ | `window` | Integer | `2` | Number of page links around the current page. |
161
+ | `first_last`| Boolean | `false` | Show "First" and "Last" page links. |
162
+ | `keyboard` | Boolean | `false` | Enable Left/Right arrow key navigation. |
163
+ | `history` | Boolean | `false` | Update browser URL/history on AJAX paging. |
164
+ | `template` | String | `nil` | Path to a custom partial for rendering. |
165
+
166
+ ---
167
+
168
+ ## ⚖️ Why switch from Kaminari?
169
+
170
+ | Feature | RailsPaginationUltimate | Kaminari |
171
+ | :--- | :---: | :---: |
172
+ | **Native Tailwind UI** | ✅ Built-in | ❌ No |
173
+ | **Infinite Scroll** | ✅ Built-in | ❌ No |
174
+ | **Fast Mode (No Count)** | ✅ Native | ❌ Plugin required |
175
+ | **Keyboard Shortcuts** | ✅ Built-in | ❌ No |
176
+ | **History API Sync** | ✅ Built-in | ❌ No |
177
+ | **Interactive Per-Page** | ✅ Built-in | ❌ No |
178
+ | **Modern Flexbox CSS** | ✅ Yes | ❌ No |
179
+
180
+ ---
181
+
182
+ ## 👨‍💻 Author & Credits
183
+
184
+ **RailsPaginationUltimate** is developed and maintained by **Shiboshree Roy**.
185
+
186
+ - **Developer & Author**: [Shiboshree Roy](https://github.com/shiboshreeroy)
187
+ - **Powered By**: Shiboshree Roy
188
+
189
+ Built with ❤️ for the Rails community.
190
+
191
+ ## 📄 License
192
+
193
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,133 @@
1
+ (function() {
2
+ document.addEventListener("click", function(event) {
3
+ const target = event.target.closest("[data-type='load-more']");
4
+ if (!target) return;
5
+
6
+ event.preventDefault();
7
+ const url = target.href;
8
+ const targetSelector = target.dataset.target;
9
+
10
+ if (!targetSelector) {
11
+ console.warn("RailsPaginationUltimate: No target selector provided for load-more button.");
12
+ return;
13
+ }
14
+
15
+ target.classList.add("loading");
16
+ target.innerText = "Loading...";
17
+
18
+ fetch(url, {
19
+ headers: {
20
+ "Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
21
+ }
22
+ })
23
+ .then(response => response.text())
24
+ .then(html => {
25
+ const parser = new DOMParser();
26
+ const doc = parser.parseFromString(html, "text/html");
27
+ const newItems = doc.querySelector(targetSelector);
28
+ const newButton = doc.querySelector(".rp-load-more-container");
29
+
30
+ if (newItems) {
31
+ document.querySelector(targetSelector).insertAdjacentHTML("beforeend", newItems.innerHTML);
32
+ }
33
+
34
+ const container = target.closest(".rp-load-more-container");
35
+ if (newButton) {
36
+ container.outerHTML = newButton.outerHTML;
37
+ } else {
38
+ container.remove();
39
+ }
40
+ })
41
+ .catch(error => {
42
+ console.error("RailsPaginationUltimate error:", error);
43
+ target.classList.remove("loading");
44
+ target.innerText = "Load More";
45
+ });
46
+ });
47
+
48
+ // Infinite Scroll Implementation
49
+ const initInfiniteScroll = () => {
50
+ const containers = document.querySelectorAll("[data-controller='rp-infinite-scroll']");
51
+
52
+ containers.forEach(container => {
53
+ if (container.dataset.observed) return;
54
+
55
+ const url = container.dataset.rpInfiniteScrollUrlValue;
56
+ const targetSelector = container.dataset.rpInfiniteScrollTargetValue;
57
+
58
+ const observer = new IntersectionObserver((entries) => {
59
+ if (entries[0].isIntersecting) {
60
+ observer.unobserve(container);
61
+
62
+ fetch(url, {
63
+ headers: { "Accept": "text/html" }
64
+ })
65
+ .then(response => response.text())
66
+ .then(html => {
67
+ const parser = new DOMParser();
68
+ const doc = parser.parseFromString(html, "text/html");
69
+ const newItems = doc.querySelector(targetSelector);
70
+ const newContainer = doc.querySelector("[data-controller='rp-infinite-scroll']");
71
+
72
+ if (newItems) {
73
+ document.querySelector(targetSelector).insertAdjacentHTML("beforeend", newItems.innerHTML);
74
+ }
75
+
76
+ if (newContainer) {
77
+ container.outerHTML = newContainer.outerHTML;
78
+ initInfiniteScroll(); // Re-init for the new container
79
+ } else {
80
+ container.remove();
81
+ }
82
+ });
83
+ }
84
+ }, { threshold: 0.1 });
85
+
86
+ observer.observe(container);
87
+ container.dataset.observed = "true";
88
+ });
89
+ };
90
+
91
+ // Keyboard Navigation & History Support
92
+ const initAdvancedFeatures = () => {
93
+ const navs = document.querySelectorAll("[data-controller='rails-pagination']");
94
+
95
+ navs.forEach(nav => {
96
+ if (nav.dataset.advancedObserved) return;
97
+
98
+ // Keyboard Navigation
99
+ if (nav.dataset.keyboard === "true") {
100
+ document.addEventListener("keydown", (event) => {
101
+ if (event.key === "ArrowLeft") {
102
+ const prev = nav.querySelector("[data-rel='prev']");
103
+ if (prev) prev.click();
104
+ } else if (event.key === "ArrowRight") {
105
+ const next = nav.querySelector("[data-rel='next']");
106
+ if (next) next.click();
107
+ }
108
+ });
109
+ }
110
+
111
+ // History API for AJAX links
112
+ if (nav.dataset.history === "true") {
113
+ nav.querySelectorAll("a").forEach(link => {
114
+ link.addEventListener("click", () => {
115
+ if (!link.dataset.remote && !link.dataset.turbo) return;
116
+ window.history.pushState({}, "", link.href);
117
+ });
118
+ });
119
+ }
120
+
121
+ nav.dataset.advancedObserved = "true";
122
+ });
123
+ };
124
+
125
+ document.addEventListener("turbo:load", () => {
126
+ initInfiniteScroll();
127
+ initAdvancedFeatures();
128
+ });
129
+ document.addEventListener("DOMContentLoaded", () => {
130
+ initInfiniteScroll();
131
+ initAdvancedFeatures();
132
+ });
133
+ })();
@@ -0,0 +1,113 @@
1
+ :root {
2
+ --rp-primary-color: #3b82f6;
3
+ --rp-primary-hover: #2563eb;
4
+ --rp-text-color: #374151;
5
+ --rp-bg-color: #ffffff;
6
+ --rp-border-color: #e5e7eb;
7
+ --rp-border-radius: 0.5rem;
8
+ --rp-gap-color: #9ca3af;
9
+ --rp-disabled-color: #d1d5db;
10
+ }
11
+
12
+ .rp-pagination {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ gap: 0.5rem;
17
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
18
+ margin: 2rem 0;
19
+ user-select: none;
20
+ }
21
+
22
+ .rp-page {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ min-width: 2.5rem;
27
+ height: 2.5rem;
28
+ padding: 0 0.5rem;
29
+ border: 1px solid var(--rp-border-color);
30
+ border-radius: var(--rp-border-radius);
31
+ background-color: var(--rp-bg-color);
32
+ color: var(--rp-text-color);
33
+ text-decoration: none;
34
+ font-size: 0.875rem;
35
+ font-weight: 500;
36
+ transition: all 0.2s;
37
+ cursor: pointer;
38
+ }
39
+
40
+ .rp-page:hover:not(.disabled):not(.current):not(.gap) {
41
+ border-color: var(--rp-primary-color);
42
+ color: var(--rp-primary-color);
43
+ background-color: #f8fafc;
44
+ }
45
+
46
+ .rp-page.current {
47
+ background-color: var(--rp-primary-color);
48
+ border-color: var(--rp-primary-color);
49
+ color: white;
50
+ cursor: default;
51
+ }
52
+
53
+ .rp-page.disabled {
54
+ color: var(--rp-disabled-color);
55
+ border-color: var(--rp-border-color);
56
+ cursor: not-allowed;
57
+ }
58
+
59
+ .rp-page.gap {
60
+ border: none;
61
+ color: var(--rp-gap-color);
62
+ cursor: default;
63
+ }
64
+
65
+ .rp-page-links {
66
+ display: flex;
67
+ gap: 0.25rem;
68
+ }
69
+
70
+ .rp-prev, .rp-next {
71
+ padding: 0 1rem;
72
+ }
73
+
74
+ .rp-load-more-container {
75
+ display: flex;
76
+ justify-content: center;
77
+ margin: 2rem 0;
78
+ }
79
+
80
+ .rp-load-more-button {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ padding: 0.75rem 2rem;
85
+ background-color: var(--rp-primary-color);
86
+ color: white;
87
+ border-radius: var(--rp-border-radius);
88
+ font-weight: 600;
89
+ text-decoration: none;
90
+ transition: background-color 0.2s;
91
+ border: none;
92
+ cursor: pointer;
93
+ }
94
+
95
+ .rp-load-more-button:hover {
96
+ background-color: var(--rp-primary-hover);
97
+ }
98
+
99
+ .rp-load-more-button.loading {
100
+ opacity: 0.7;
101
+ cursor: wait;
102
+ }
103
+
104
+ @media (max-width: 640px) {
105
+ .rp-page-links {
106
+ display: none;
107
+ }
108
+
109
+ .rp-pagination {
110
+ justify-content: space-between;
111
+ width: 100%;
112
+ }
113
+ }
@@ -0,0 +1,35 @@
1
+ <nav class="flex items-center justify-between border-t border-gray-200 px-4 sm:px-0">
2
+ <div class="-mt-px flex w-0 flex-1">
3
+ <% unless collection.first_page? %>
4
+ <%= link_to template.url_for(template.params.to_unsafe_h.merge(page: collection.prev_page)), class: "inline-flex items-center border-t-2 border-transparent pr-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" do %>
5
+ <svg class="mr-3 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
6
+ <path fill-rule="evenodd" d="M18 10a.75.75 0 01-.75.75H4.66l2.1 1.95a.75.75 0 11-1.02 1.1l-3.5-3.25a.75.75 0 010-1.1l3.5-3.25a.75.75 0 111.02 1.1l-2.1 1.95h12.59A.75.75 0 0118 10z" clip-rule="evenodd" />
7
+ </svg>
8
+ Previous
9
+ <% end %>
10
+ <% end %>
11
+ </div>
12
+
13
+ <div class="hidden md:-mt-px md:flex">
14
+ <% windowed_page_numbers.each do |n| %>
15
+ <% if n == :gap %>
16
+ <span class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500">...</span>
17
+ <% elsif n == collection.current_page %>
18
+ <span class="inline-flex items-center border-t-2 border-blue-500 px-4 pt-4 text-sm font-medium text-blue-600" aria-current="page"><%= n %></span>
19
+ <% else %>
20
+ <%= link_to n, template.url_for(template.params.to_unsafe_h.merge(page: n)), class: "inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" %>
21
+ <% end %>
22
+ <% end %>
23
+ </div>
24
+
25
+ <div class="-mt-px flex w-0 flex-1 justify-end">
26
+ <% unless collection.last_page? %>
27
+ <%= link_to template.url_for(template.params.to_unsafe_h.merge(page: collection.next_page)), class: "inline-flex items-center border-t-2 border-transparent pl-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" do %>
28
+ Next
29
+ <svg class="ml-3 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
30
+ <path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd" />
31
+ </svg>
32
+ <% end %>
33
+ <% end %>
34
+ </div>
35
+ </nav>
@@ -0,0 +1,12 @@
1
+ en:
2
+ rails_pagination_ultimate:
3
+ previous: "&laquo; Previous"
4
+ next: "Next &raquo;"
5
+ first: "&laquo; First"
6
+ last: "Last &raquo;"
7
+ truncate: "..."
8
+ info:
9
+ one: "Displaying <b>1</b> %{entry_name}"
10
+ other: "Displaying %{entry_name} <b>%{first}&nbsp;-&nbsp;%{last}</b> of <b>%{total}</b>"
11
+ all: "Displaying <b>all %{total}</b> %{entry_name}"
12
+ without_count: "Displaying %{entry_name} <b>%{first}&nbsp;-&nbsp;%{last}</b>"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsPaginationUltimate
6
+ module Generators
7
+ class TailwindGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../../../app/views/rails_pagination_ultimate", __dir__)
9
+
10
+ desc "Copies RailsPaginationUltimate Tailwind views to your application."
11
+
12
+ def copy_views
13
+ copy_file "_tailwind_pro.html.erb", "app/views/rails_pagination_ultimate/_pagination.html.erb"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPaginationUltimate
4
+ module ActionViewHelper
5
+ def paginate(collection, options = {})
6
+ return unless collection.respond_to?(:total_pages)
7
+
8
+ paginator = Paginator.new(self, collection, options)
9
+ paginator.render
10
+ end
11
+
12
+ def page_entries_info(collection, options = {})
13
+ entry_name = options[:entry_name] || collection.model_name.human.downcase
14
+ entry_name = entry_name.pluralize if collection.total_count != 1
15
+
16
+ if collection.total_pages == Float::INFINITY
17
+ first = collection.offset_value + 1
18
+ last = collection.offset_value + collection.length
19
+ return I18n.t("rails_pagination_ultimate.info.without_count", first: first, last: last, entry_name: entry_name).html_safe
20
+ end
21
+
22
+ if collection.total_pages < 2
23
+ I18n.t("rails_pagination_ultimate.info.all", total: collection.total_count, entry_name: entry_name).html_safe
24
+ else
25
+ first = collection.offset_value + 1
26
+ last = [collection.offset_value + collection.limit_value, collection.total_count].min
27
+ I18n.t("rails_pagination_ultimate.info.other", first: first, last: last, total: collection.total_count, entry_name: entry_name).html_safe
28
+ end
29
+ end
30
+
31
+ def per_page_select(collection, options = {})
32
+ choices = options[:choices] || [10, 25, 50, 100]
33
+ current_per = collection.limit_value
34
+
35
+ template.content_tag(:div, class: "rp-per-page-container #{options[:class]}") do
36
+ template.concat template.content_tag(:span, options[:label] || "Show", class: "rp-label")
37
+ template.concat template.select_tag(:per, template.options_for_select(choices, current_per),
38
+ class: "rp-select",
39
+ onchange: "const url = new URL(window.location.href); url.searchParams.set('per', this.value); url.searchParams.set('page', 1); window.location.href = url.toString();")
40
+ end
41
+ end
42
+ end
43
+
44
+ class Paginator
45
+ attr_reader :template, :collection, :options, :current_page, :total_pages
46
+
47
+ def initialize(template, collection, options = {})
48
+ @template = template
49
+ @collection = collection
50
+ @options = options
51
+ @current_page = collection.current_page
52
+ @total_pages = collection.total_pages
53
+ end
54
+
55
+ def render
56
+ return "" if total_pages <= 1 && total_pages != Float::INFINITY
57
+
58
+ if options[:template]
59
+ return template.render(partial: options[:template], locals: {
60
+ collection: collection,
61
+ options: options,
62
+ template: template,
63
+ windowed_page_numbers: windowed_page_numbers
64
+ })
65
+ end
66
+
67
+ case options[:type]
68
+ when :load_more
69
+ render_load_more
70
+ when :infinite_scroll
71
+ render_infinite_scroll
72
+ else
73
+ render_standard
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def render_standard
80
+ nav_class = options[:theme] == :tailwind ? "flex items-center justify-center space-x-2 my-8" : "rp-pagination rp-standard #{options[:class]}"
81
+
82
+ data = {
83
+ controller: "rails-pagination",
84
+ keyboard: options[:keyboard],
85
+ history: options[:history]
86
+ }
87
+
88
+ template.content_tag(:nav, class: nav_class, aria: { label: "Pagination" }, data: data) do
89
+ template.concat first_link if options[:first_last] && total_pages != Float::INFINITY
90
+ template.concat prev_link
91
+ template.concat page_links if total_pages != Float::INFINITY
92
+ template.concat next_link
93
+ template.concat last_link if options[:first_last] && total_pages != Float::INFINITY
94
+ end
95
+ end
96
+
97
+ def first_link
98
+ label = options[:first_label] || I18n.t("rails_pagination_ultimate.first").html_safe
99
+ if collection.first_page?
100
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-400 cursor-not-allowed hidden sm:inline-flex" : "rp-page rp-first disabled"
101
+ template.content_tag(:span, label, class: cls)
102
+ else
103
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-700 hover:bg-gray-50 transition hidden sm:inline-flex" : "rp-page rp-first"
104
+ template.link_to(label, template.url_for(template.params.to_unsafe_h.merge(page: 1)), class: cls, data: { rel: "first" })
105
+ end
106
+ end
107
+
108
+ def last_link
109
+ label = options[:last_label] || I18n.t("rails_pagination_ultimate.last").html_safe
110
+ if collection.last_page?
111
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-400 cursor-not-allowed hidden sm:inline-flex" : "rp-page rp-last disabled"
112
+ template.content_tag(:span, label, class: cls)
113
+ else
114
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-700 hover:bg-gray-50 transition hidden sm:inline-flex" : "rp-page rp-last"
115
+ template.link_to(label, template.url_for(template.params.to_unsafe_h.merge(page: total_pages)), class: cls, data: { rel: "last" })
116
+ end
117
+ end
118
+
119
+ def render_load_more
120
+ return if collection.last_page?
121
+
122
+ template.content_tag(:div, class: "rp-pagination rp-load-more-container #{options[:class]}") do
123
+ template.link_to("Load More", template.url_for(template.params.to_unsafe_h.merge(page: collection.next_page)),
124
+ class: "rp-load-more-button",
125
+ remote: true,
126
+ data: {
127
+ type: "load-more",
128
+ target: options[:target],
129
+ turbo_frame: options[:turbo_frame]
130
+ }
131
+ )
132
+ end
133
+ end
134
+
135
+ def render_infinite_scroll
136
+ return if collection.last_page?
137
+
138
+ template.content_tag(:div, class: "rp-pagination rp-infinite-scroll-container #{options[:class]}",
139
+ data: {
140
+ controller: "rp-infinite-scroll",
141
+ rp_infinite_scroll_url_value: template.url_for(template.params.to_unsafe_h.merge(page: collection.next_page)),
142
+ rp_infinite_scroll_target_value: options[:target]
143
+ }) do
144
+ template.content_tag(:div, options[:loading_text] || "Loading more...", class: "rp-infinite-scroll-loader")
145
+ end
146
+ end
147
+
148
+ def prev_link
149
+ label = options[:prev_label] || I18n.t("rails_pagination_ultimate.previous").html_safe
150
+ if collection.first_page?
151
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-400 cursor-not-allowed" : "rp-page rp-prev disabled"
152
+ template.content_tag(:span, label, class: cls)
153
+ else
154
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-700 hover:bg-gray-50 transition" : "rp-page rp-prev"
155
+ template.link_to(label, template.url_for(template.params.to_unsafe_h.merge(page: collection.prev_page)), class: cls, data: { turbo_frame: options[:turbo_frame], rel: "prev" })
156
+ end
157
+ end
158
+
159
+ def next_link
160
+ label = options[:next_label] || I18n.t("rails_pagination_ultimate.next").html_safe
161
+ if collection.last_page?
162
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-400 cursor-not-allowed" : "rp-page rp-next disabled"
163
+ template.content_tag(:span, label, class: cls)
164
+ else
165
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-700 hover:bg-gray-50 transition" : "rp-page rp-next"
166
+ template.link_to(label, template.url_for(template.params.to_unsafe_h.merge(page: collection.next_page)), class: cls, data: { turbo_frame: options[:turbo_frame], rel: "next" })
167
+ end
168
+ end
169
+
170
+ def page_links
171
+ container_cls = options[:theme] == :tailwind ? "flex space-x-1" : "rp-page-links"
172
+ template.content_tag(:div, class: container_cls) do
173
+ windowed_page_numbers.each do |n|
174
+ if n == :gap
175
+ cls = options[:theme] == :tailwind ? "px-4 py-2 text-gray-400" : "rp-page gap"
176
+ template.concat template.content_tag(:span, "...", class: cls)
177
+ elsif n == current_page
178
+ cls = options[:theme] == :tailwind ? "px-4 py-2 bg-blue-600 text-white rounded-md font-medium" : "rp-page current"
179
+ template.concat template.content_tag(:span, n, class: cls)
180
+ else
181
+ cls = options[:theme] == :tailwind ? "px-4 py-2 border rounded-md text-gray-700 hover:border-blue-600 hover:text-blue-600 transition" : "rp-page"
182
+ template.concat template.link_to(n, template.url_for(template.params.to_unsafe_h.merge(page: n)), class: cls, data: { turbo_frame: options[:turbo_frame] })
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ def windowed_page_numbers
189
+ return [] if total_pages == Float::INFINITY
190
+ window = options[:window] || 2
191
+ pages = []
192
+
193
+ (1..total_pages).each do |n|
194
+ if n <= 1 || n >= total_pages || (n - current_page).abs <= window
195
+ pages << n
196
+ else
197
+ pages << :gap if pages.last != :gap
198
+ end
199
+ end
200
+ pages
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module RailsPaginationUltimate
6
+ module ActiveRecordExtension
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ scope :page, ->(num) {
11
+ num = num.to_i
12
+ num = 1 if num < 1
13
+ limit(default_per_page).offset(default_per_page * (num - 1)).extending(ActiveRecordModelExtension)
14
+ }
15
+
16
+ scope :without_count, -> {
17
+ extending(WithoutCountExtension)
18
+ }
19
+ end
20
+
21
+ module WithoutCountExtension
22
+ end
23
+
24
+ module ClassMethods
25
+ def default_per_page
26
+ RailsPaginationUltimate.config.default_per_page
27
+ end
28
+ end
29
+
30
+ module ActiveRecordModelExtension
31
+ def is_without_count?
32
+ singleton_class.included_modules.include?(WithoutCountExtension)
33
+ end
34
+
35
+ def per(num)
36
+ num = num.to_i
37
+ limit(num).offset(num * (current_page - 1))
38
+ end
39
+
40
+ def current_page
41
+ limit_val = limit_value || RailsPaginationUltimate.config.default_per_page
42
+ offset_val = offset_value || 0
43
+ (offset_val / limit_val) + 1
44
+ rescue StandardError
45
+ 1
46
+ end
47
+
48
+ def total_pages
49
+ return Float::INFINITY if is_without_count?
50
+
51
+ limit_val = limit_value || RailsPaginationUltimate.config.default_per_page
52
+ (total_count.to_f / limit_val).ceil
53
+ rescue StandardError
54
+ 0
55
+ end
56
+
57
+ def total_count
58
+ return 0 if is_without_count?
59
+
60
+ @total_count ||= begin
61
+ c = except(:offset, :limit, :order)
62
+ c = c.count
63
+ c.is_a?(Hash) ? c.count : c
64
+ end
65
+ end
66
+
67
+ def first_page?
68
+ current_page == 1
69
+ end
70
+
71
+ def last_page?
72
+ if is_without_count?
73
+ return !has_next_page?
74
+ end
75
+ current_page >= total_pages
76
+ end
77
+
78
+ def next_page
79
+ current_page + 1 unless last_page?
80
+ end
81
+
82
+ def prev_page
83
+ current_page - 1 unless first_page?
84
+ end
85
+
86
+ private
87
+
88
+ def has_next_page?
89
+ @_has_next_page ||= begin
90
+ limit_val = limit_value || RailsPaginationUltimate.config.default_per_page
91
+ # Fetch one extra record to see if there is a next page
92
+ records_count = except(:limit).limit(limit_val + 1).offset(offset_value).to_a.length
93
+ records_count > limit_val
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPaginationUltimate
4
+ class Configuration
5
+ attr_accessor :default_per_page
6
+
7
+ def initialize
8
+ @default_per_page = 25
9
+ end
10
+ end
11
+
12
+ class << self
13
+ attr_accessor :configuration
14
+
15
+ def configure
16
+ self.configuration ||= Configuration.new
17
+ yield(configuration)
18
+ end
19
+
20
+ def config
21
+ self.configuration ||= Configuration.new
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPaginationUltimate
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RailsPaginationUltimate
6
+
7
+ initializer "rails_pagination_ultimate.active_record" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ include RailsPaginationUltimate::ActiveRecordExtension
10
+ end
11
+ end
12
+
13
+ initializer "rails_pagination_ultimate.action_view" do
14
+ ActiveSupport.on_load(:action_view) do
15
+ require_relative "action_view_helper"
16
+ include RailsPaginationUltimate::ActionViewHelper
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsPaginationUltimate
4
+ VERSION = "1.0.1"
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rails_pagination_ultimate/version"
4
+ require_relative "rails_pagination_ultimate/configuration"
5
+ require_relative "rails_pagination_ultimate/active_record_extension"
6
+ require_relative "rails_pagination_ultimate/engine" if defined?(Rails)
7
+
8
+ module RailsPaginationUltimate
9
+ class Error < StandardError; end
10
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_pagination_ultimate
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Shiboshree Roy
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: actionview
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: railties
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ description: RailsPaginationUltimate provides a powerful and easy-to-use pagination
55
+ solution with modern UI/UX, responsive designs, and seamless Turbo/Hotwire integration.
56
+ email:
57
+ - shiboshreeroy169@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - app/assets/javascripts/rails_pagination_ultimate/loader.js
66
+ - app/assets/stylesheets/rails_pagination_ultimate/main.css
67
+ - app/views/rails_pagination_ultimate/_tailwind_pro.html.erb
68
+ - config/locales/en.yml
69
+ - lib/generators/rails_pagination_ultimate/tailwind_generator.rb
70
+ - lib/rails_pagination_ultimate.rb
71
+ - lib/rails_pagination_ultimate/action_view_helper.rb
72
+ - lib/rails_pagination_ultimate/active_record_extension.rb
73
+ - lib/rails_pagination_ultimate/configuration.rb
74
+ - lib/rails_pagination_ultimate/engine.rb
75
+ - lib/rails_pagination_ultimate/version.rb
76
+ homepage: https://github.com/shiboshreeroy/rails_pagination_ultimate
77
+ licenses:
78
+ - MIT
79
+ metadata:
80
+ homepage_uri: https://github.com/shiboshreeroy/rails_pagination_ultimate
81
+ source_code_uri: https://github.com/shiboshreeroy/rails_pagination_ultimate
82
+ changelog_uri: https://github.com/shiboshreeroy/rails_pagination_ultimate/blob/main/CHANGELOG.md
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.0.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 4.0.11
98
+ specification_version: 4
99
+ summary: A premium, responsive, and advanced pagination gem for Rails.
100
+ test_files: []