| 1 |
<?php |
| 2 |
/** |
| 3 |
* Standalone (theme_id 990) pagination component. |
| 4 |
* |
| 5 |
* One source of pager markup for every listings surface: the AJAX listings grid |
| 6 |
* (Mlsimport_Standalone_Render::render_grid + the AJAX repaint) and the GET-based |
| 7 |
* Search Results page block. Given a total, a page size and the current page it |
| 8 |
* returns a <nav> of page controls (or '' for a single page). |
| 9 |
* |
| 10 |
* Each control is an anchor carrying BOTH a real ?page= URL (shareable / no-JS / |
| 11 |
* the GET block) and a data-page hook (the listings JS intercepts these to repaint |
| 12 |
* the grid via AJAX). The page list is windowed so a large set collapses to |
| 13 |
* first … current±2 … last instead of printing every page. |
| 14 |
* |
| 15 |
* @package Mlsimport |
| 16 |
*/ |
| 17 |
|
| 18 |
if ( ! defined( 'ABSPATH' ) ) { |
| 19 |
exit; |
| 20 |
} |
| 21 |
|
| 22 |
/** |
| 23 |
* Renders the shared listings pager. |
| 24 |
*/ |
| 25 |
class Mlsimport_Pagination { |
| 26 |
|
| 27 |
/** |
| 28 |
* Page numbers to keep either side of the current page before collapsing the |
| 29 |
* run to an ellipsis. |
| 30 |
*/ |
| 31 |
const WINDOW = 2; |
| 32 |
|
| 33 |
/** |
| 34 |
* Render the pager. |
| 35 |
* |
| 36 |
* @param int $total Full match count (not the page count). |
| 37 |
* @param int $per_page Page size. |
| 38 |
* @param int $current Current page (1-based). |
| 39 |
* @param array $opts { @type string $label aria-label for the nav. @type string $param |
| 40 |
* query-arg key for the page links (default 'page'). Singular pages |
| 41 |
* must use a non-reserved key — WP's redirect_canonical strips ?page= |
| 42 |
* on a single post that has no <!--nextpage--> content. } |
| 43 |
* @return string Pager HTML, or '' when there is a single page. |
| 44 |
*/ |
| 45 |
public static function render( int $total, int $per_page, int $current, array $opts = array() ): string { |
| 46 |
$per_page = max( 1, $per_page ); |
| 47 |
$pages = (int) ceil( $total / $per_page ); |
| 48 |
if ( $pages < 2 ) { |
| 49 |
return ''; |
| 50 |
} |
| 51 |
$current = min( max( 1, $current ), $pages ); |
| 52 |
$label = isset( $opts['label'] ) ? (string) $opts['label'] : __( 'Listings pagination', 'mlsimport' ); |
| 53 |
$param = isset( $opts['param'] ) ? (string) $opts['param'] : 'page'; |
| 54 |
|
| 55 |
$out = '<nav class="mlsimport-pager" role="navigation" aria-label="' . esc_attr( $label ) . '">'; |
| 56 |
if ( $current > 1 ) { |
| 57 |
$out .= self::link( $current - 1, __( 'Previous', 'mlsimport' ), $param, 'mlsimport-pager__prev', 'prev' ); |
| 58 |
} |
| 59 |
foreach ( self::page_list( $pages, $current ) as $page ) { |
| 60 |
if ( 0 === $page ) { |
| 61 |
$out .= '<span class="mlsimport-pager__gap" aria-hidden="true">…</span>'; |
| 62 |
} elseif ( $page === $current ) { |
| 63 |
$out .= '<span class="mlsimport-pager__btn is-active" aria-current="page">' . esc_html( (string) $page ) . '</span>'; |
| 64 |
} else { |
| 65 |
$out .= self::link( $page, (string) $page, $param ); |
| 66 |
} |
| 67 |
} |
| 68 |
if ( $current < $pages ) { |
| 69 |
$out .= self::link( $current + 1, __( 'Next', 'mlsimport' ), $param, 'mlsimport-pager__next', 'next' ); |
| 70 |
} |
| 71 |
$out .= '</nav>'; |
| 72 |
return $out; |
| 73 |
} |
| 74 |
|
| 75 |
/** |
| 76 |
* One page control: a real ?<param>= link that the AJAX layer hooks via data-page. |
| 77 |
* |
| 78 |
* @param int $page Target page. |
| 79 |
* @param string $label Visible label. |
| 80 |
* @param string $param Query-arg key for the page number. |
| 81 |
* @param string $extra_class Extra BEM modifier class. |
| 82 |
* @param string $rel Optional rel attribute (prev/next). |
| 83 |
* @return string |
| 84 |
*/ |
| 85 |
private static function link( int $page, string $label, string $param = 'page', string $extra_class = '', string $rel = '' ): string { |
| 86 |
$class = 'mlsimport-pager__btn' . ( '' !== $extra_class ? ' ' . $extra_class : '' ); |
| 87 |
$rel = '' !== $rel ? ' rel="' . esc_attr( $rel ) . '"' : ''; |
| 88 |
return '<a class="' . esc_attr( $class ) . '" href="' . esc_url( add_query_arg( $param, $page ) ) . '"' |
| 89 |
. ' data-page="' . esc_attr( (string) $page ) . '"' . $rel . '>' . esc_html( $label ) . '</a>'; |
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* The page-number sequence to render: page 1, the window around the current |
| 94 |
* page, and the last page — with 0 marking an ellipsis gap where pages are |
| 95 |
* skipped. E.g. pages=20 current=10 -> [1,0,8,9,10,11,12,0,20]. |
| 96 |
* |
| 97 |
* @param int $pages Total pages. |
| 98 |
* @param int $current Current page. |
| 99 |
* @return int[] |
| 100 |
*/ |
| 101 |
private static function page_list( int $pages, int $current ): array { |
| 102 |
$keep = array(); |
| 103 |
for ( $page = 1; $page <= $pages; $page++ ) { |
| 104 |
if ( 1 === $page || $pages === $page || ( $page >= $current - self::WINDOW && $page <= $current + self::WINDOW ) ) { |
| 105 |
$keep[] = $page; |
| 106 |
} |
| 107 |
} |
| 108 |
|
| 109 |
$list = array(); |
| 110 |
$prev = 0; |
| 111 |
foreach ( $keep as $page ) { |
| 112 |
if ( $prev && $page - $prev > 1 ) { |
| 113 |
$list[] = 0; // Gap marker. |
| 114 |
} |
| 115 |
$list[] = $page; |
| 116 |
$prev = $page; |
| 117 |
} |
| 118 |
return $list; |
| 119 |
} |
| 120 |
} |
| 121 |
|