PluginProbe
Send Users Email – Email Subscribers, Email Marketing Newsletter / trunk
Send Users Email – Email Subscribers, Email Marketing Newsletter vtrunk
2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 All 51 releases
send-users-email / includes / class-bootstrap-pagination.php

class-bootstrap-pagination.php in Send Users Email – Email Subscribers, Email Marketing Newsletter trunk, at includes/class-bootstrap-pagination.php

336 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Bootstrap_Pagination
4 * Handles the rendering of Bootstrap-styled pagination.
5 * This is not dependent of wpdb query results, it can be used with any pagination logic.
6 * This is a reusable class that can be used in any plugin or theme.
7 * Works with any data source (custom tables, REST results, arrays, etc.)
8 * Exposes limit() and offset() for your queries
9 * Renders Bootstrap 5 pagination (<ul class="pagination">…</ul>)
10 * Preserves any query args you pass in (e.g., search filters)
11 * Is safe and admin‑friendly (escapes URLs, adds ARIA, supports alignment + sizes)
12 */
13 class Bootstrap_Pagination
14 {
15 protected $total_items = 0;
16 protected $per_page = 10;
17 protected $current_page = 1;
18 protected $total_pages = 1;
19 protected $base_url = '';
20 protected $page_arg = 'paged';
21 protected $mid_size = 2; // pages to show on each side of current
22 protected $preserve_args = []; // extra args to keep in links
23
24 /**
25 * @param array $args {
26 * @type int $total_items Required. Total number of items.
27 * @type int $per_page Optional. Default 10.
28 * @type int|null $current_page Optional. Default from $_GET[$page_arg] or 1.
29 * @type string $base_url Required. Base URL without the 'paged' (or custom) arg.
30 * @type string $page_arg Optional. Query arg name for page. Default 'paged'.
31 * @type int $mid_size Optional. Pages around current. Default 2.
32 * @type array $preserve_args Optional. Extra query args to preserve in links.
33 * }
34 */
35 public function __construct( $args = [] ) {
36 $defaults = [
37 'total_items' => 0,
38 'per_page' => 10,
39 'current_page' => null,
40 'base_url' => '',
41 'page_arg' => 'paged',
42 'mid_size' => 2,
43 'preserve_args' => [],
44 ];
45 $args = function_exists('wp_parse_args') ? wp_parse_args( $args, $defaults ) : array_merge( $defaults, $args );
46
47 $this->total_items = max( 0, (int) $args['total_items'] );
48 $this->per_page = max( 1, (int) $args['per_page'] );
49 $this->page_arg = (string) $args['page_arg'];
50 $this->mid_size = max( 0, (int) $args['mid_size'] );
51
52 // Base URL is required for correct links.
53 $this->base_url = (string) $args['base_url'];
54
55 // Sanitize preserve args to only scalars.
56 $this->preserve_args = [];
57 if ( is_array( $args['preserve_args'] ) ) {
58 foreach ( $args['preserve_args'] as $k => $v ) {
59 if ( is_scalar( $v ) && $v !== '' && $v !== null ) {
60 $this->preserve_args[ $k ] = $v;
61 }
62 }
63 }
64
65 // Determine current page
66 if ( $args['current_page'] !== null ) {
67 $this->current_page = (int) $args['current_page'];
68 } else {
69 $from_get = isset( $_GET[ $this->page_arg ] ) ? (int) $_GET[ $this->page_arg ] : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
70 $this->current_page = $from_get;
71 }
72 $this->current_page = max( 1, $this->current_page );
73
74 // Compute total pages and clamp current page
75 $this->total_pages = max( 1, (int) ceil( $this->total_items / $this->per_page ) );
76 if ( $this->current_page > $this->total_pages ) {
77 $this->current_page = $this->total_pages;
78 }
79 }
80
81 /** Offset to use in queries. */
82 public function offset() {
83 return ( $this->current_page - 1 ) * $this->per_page;
84 }
85
86 /** Limit to use in queries. */
87 public function limit() {
88 return $this->per_page;
89 }
90
91 /** Current page number. */
92 public function currentPage() {
93 return $this->current_page;
94 }
95
96 /** Total pages. */
97 public function totalPages() {
98 return $this->total_pages;
99 }
100
101 /** Total items. */
102 public function totalItems() {
103 return $this->total_items;
104 }
105
106 /** Build URL for a specific page, preserving configured query args. */
107 public function pageUrl( $page ) {
108 $page = max( 1, (int) $page );
109 $args = $this->preserve_args;
110 $args[ $this->page_arg ] = $page;
111
112 if ( function_exists('add_query_arg') ) {
113 return esc_url( add_query_arg( $args, $this->base_url ) );
114 }
115
116 // Fallback (no WP helpers): append query string manually.
117 $glue = ( strpos( $this->base_url, '?' ) === false ) ? '?' : '&';
118 return htmlspecialchars( $this->base_url . $glue . http_build_query( $args ), ENT_QUOTES, 'UTF-8' );
119 }
120
121 /**
122 * Return a normalized list of "link items" you can render your own way.
123 * Each item is an associative array with keys:
124 * - type: 'prev'|'next'|'page'|'dots'|'first'|'last'
125 * - label: string
126 * - url: string|null
127 * - page: int|null
128 * - current: bool
129 * - disabled: bool
130 */
131 public function links( $args = [] ) {
132 $defaults = [
133 'show_prev_next' => true,
134 'show_first_last' => false,
135 ];
136 $args = function_exists('wp_parse_args') ? wp_parse_args( $args, $defaults ) : array_merge( $defaults, $args );
137
138 $items = [];
139
140 // Prev
141 if ( $args['show_prev_next'] ) {
142 $items[] = [
143 'type' => 'prev',
144 'label' => '«',
145 'url' => $this->current_page > 1 ? $this->pageUrl( $this->current_page - 1 ) : null,
146 'page' => $this->current_page - 1,
147 'current' => false,
148 'disabled' => $this->current_page <= 1,
149 ];
150 }
151
152 // First
153 if ( $args['show_first_last'] && $this->total_pages > 1 ) {
154 $items[] = [
155 'type' => 'first',
156 'label' => '1',
157 'url' => $this->current_page === 1 ? null : $this->pageUrl( 1 ),
158 'page' => 1,
159 'current' => $this->current_page === 1,
160 'disabled' => $this->current_page === 1,
161 ];
162 }
163
164 // Determine window
165 $start = max( 1, $this->current_page - $this->mid_size );
166 $end = min( $this->total_pages, $this->current_page + $this->mid_size );
167
168 // Leading dots if needed (and avoid duplicate "1" if showing First)
169 if ( $start > 2 && ! $args['show_first_last'] ) {
170 // show first page before dots
171 $items[] = [
172 'type' => 'page',
173 'label' => '1',
174 'url' => $this->pageUrl( 1 ),
175 'page' => 1,
176 'current' => false,
177 'disabled' => false,
178 ];
179 }
180 if ( $start > 2 ) {
181 $items[] = [
182 'type' => 'dots',
183 'label' => '',
184 'url' => null,
185 'page' => null,
186 'current' => false,
187 'disabled' => true,
188 ];
189 } elseif ( $start === 2 && ! $args['show_first_last'] ) {
190 // show page 1 if it's right before start and we didn't show 'first'
191 $items[] = [
192 'type' => 'page',
193 'label' => '1',
194 'url' => $this->pageUrl( 1 ),
195 'page' => 1,
196 'current' => false,
197 'disabled' => false,
198 ];
199 }
200
201 // Middle pages
202 for ( $i = $start; $i <= $end; $i++ ) {
203 // Avoid duplicating page 1/last if show_first_last true
204 if ( $args['show_first_last'] && ( $i === 1 || $i === $this->total_pages ) ) {
205 continue;
206 }
207 $items[] = [
208 'type' => 'page',
209 'label' => (string) $i,
210 'url' => $i === $this->current_page ? null : $this->pageUrl( $i ),
211 'page' => $i,
212 'current' => $i === $this->current_page,
213 'disabled' => $i === $this->current_page,
214 ];
215 }
216
217 // Trailing dots if needed
218 if ( $end < $this->total_pages - 1 ) {
219 $items[] = [
220 'type' => 'dots',
221 'label' => '',
222 'url' => null,
223 'page' => null,
224 'current' => false,
225 'disabled' => true,
226 ];
227 } elseif ( $end === $this->total_pages - 1 && ! $args['show_first_last'] ) {
228 // show last page if adjacent and we didn't show 'last'
229 $page = $this->total_pages;
230 $items[] = [
231 'type' => 'page',
232 'label' => (string) $page,
233 'url' => $this->pageUrl( $page ),
234 'page' => $page,
235 'current' => false,
236 'disabled' => false,
237 ];
238 }
239
240 // Last
241 if ( $args['show_first_last'] && $this->total_pages > 1 ) {
242 $page = $this->total_pages;
243 $items[] = [
244 'type' => 'last',
245 'label' => (string) $page,
246 'url' => $this->current_page === $page ? null : $this->pageUrl( $page ),
247 'page' => $page,
248 'current' => $this->current_page === $page,
249 'disabled' => $this->current_page === $page,
250 ];
251 }
252
253 // Next
254 if ( $args['show_prev_next'] ) {
255 $items[] = [
256 'type' => 'next',
257 'label' => '»',
258 'url' => $this->current_page < $this->total_pages ? $this->pageUrl( $this->current_page + 1 ) : null,
259 'page' => $this->current_page + 1,
260 'current' => false,
261 'disabled' => $this->current_page >= $this->total_pages,
262 ];
263 }
264
265 return $items;
266 }
267
268 /**
269 * Render Bootstrap 5 pagination.
270 *
271 * @param array $args {
272 * @type string $alignment '', 'center', or 'end' (uses justify-content-*).
273 * @type string $size '', 'sm', or 'lg'.
274 * @type string $aria_label ARIA label. Default 'Pagination'.
275 * @type bool $show_prev_next Show « and » links. Default true.
276 * @type bool $show_first_last Show first/last page numbers. Default false.
277 * }
278 * @return string HTML
279 */
280 public function render( $args = array() ) {
281 if ( $this->total_pages < 2 ) {
282 return ''; // No need to render
283 }
284 // this is the
285 $defaults = [
286 'alignment' => '',
287 'size' => '',
288 'aria_label' => 'Pagination',
289 'show_prev_next' => true,
290 'show_first_last' => false,
291 ];
292 $args = function_exists('wp_parse_args') ? wp_parse_args( $args, $defaults ) : array_merge( $defaults, $args );
293
294 $links = $this->links( $args );
295
296 // Classes for <ul>
297 $ul_classes = array( 'pagination', 'mb-0' );
298 if ( $args['size'] === 'sm' ) $ul_classes[] = 'pagination-sm';
299 if ( $args['size'] === 'lg' ) $ul_classes[] = 'pagination-lg';
300
301 $wrap_classes = array();
302 if ( $args['alignment'] === 'center' ) $wrap_classes[] = 'justify-content-center';
303 if ( $args['alignment'] === 'end' ) $wrap_classes[] = 'justify-content-end';
304
305 $html = '<nav aria-label="' . esc_attr( $args['aria_label'] ) . '">';
306 $html .= '<ul class="' . esc_attr( implode( ' ', $ul_classes ) ) . ' ' . esc_attr( implode( ' ', $wrap_classes ) ) . '">';
307
308 foreach ( $links as $item ) {
309 $li_classes = array( 'page-item' );
310 $link_attrs = '';
311 $content = '';
312
313 if ( $item['disabled'] ) {
314 $li_classes[] = 'disabled';
315 }
316 if ( $item['current'] ) {
317 $li_classes[] = 'active';
318 }
319
320 if ( $item['url'] ) {
321 $rel = '';
322 if ( $item['type'] === 'prev' ) $rel = ' rel="prev"';
323 if ( $item['type'] === 'next' ) $rel = ' rel="next"';
324 $content = '<a class="page-link" href="' . esc_url( $item['url'] ) . '"' . $rel . '>' . esc_html( $item['label'] ) . '</a>';
325 } else {
326 // current or disabled item
327 $content = '<span class="page-link">' . esc_html( $item['label'] ) . '</span>';
328 }
329
330 $html .= '<li class="' . esc_attr( implode( ' ', $li_classes ) ) . '">' . $content . '</li>';
331 }
332
333 $html .= '</ul></nav>';
334 return $html;
335 }
336 }