1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui;
6:
7: use Atk4\Ui\Js\JsExpression;
8: use Atk4\Ui\Js\JsReload;
9:
10: class Paginator extends View
11: {
12: public $ui = 'pagination menu';
13: public $defaultTemplate = 'paginator.html';
14:
15: /** Specify how many pages this paginator has in total. */
16: public int $total;
17:
18: /**
19: * Override what is the current page. If not set, Paginator will look inside
20: * $_GET[self::$urlTrigger]. If page > total, then page = total.
21: */
22: public ?int $page = null;
23:
24: /**
25: * When there are more than ($range * 2 + 1) items, then current page will be surrounded by $range pages
26: * followed by spacer ..., for example if range=2, then.
27: *
28: * 1, ..., 5, 6, *7*, 8, 9, ..., 34
29: */
30: public int $range = 4;
31:
32: /** @var string|null Set this if you want GET argument name to look beautifully. */
33: public $urlTrigger;
34:
35: /**
36: * If specified, must be instance of a view which will be reloaded on click.
37: * Otherwise will use link to current page.
38: *
39: * @var View|null
40: */
41: public $reload;
42:
43: /**
44: * Add extra parameter to the reload view
45: * as JsReload urlOptions.
46: */
47: public array $reloadArgs = [];
48:
49: #[\Override]
50: protected function init(): void
51: {
52: parent::init();
53:
54: if ($this->urlTrigger === null) {
55: $this->urlTrigger = $this->name;
56: }
57:
58: if (!$this->page) {
59: $this->page = $this->getCurrentPage();
60: }
61: }
62:
63: /**
64: * Set total number of pages.
65: */
66: public function setTotal(int $total): void
67: {
68: $this->total = $total < 1 ? 1 : $total;
69:
70: if ($this->page < 1) {
71: $this->page = 1;
72: } elseif ($this->page > $this->total) {
73: $this->page = $this->total;
74: }
75: }
76:
77: /**
78: * Determine and return the current page. You can extend this method for
79: * the advanced logic.
80: */
81: public function getCurrentPage(): int
82: {
83: return (int) ($this->getApp()->tryGetRequestQueryParam($this->urlTrigger) ?? 1);
84: }
85:
86: /**
87: * Calculate logical sequence of items in a paginator. Responds with array
88: * containing recipe for HTML augmenting:.
89: *
90: * [ '[', '...', 10, 11, 12 ]
91: *
92: * Array will contain '[', ']', denoting "first", "last" items, '...' for the spacer and any
93: * other integer value for a regular page link.
94: */
95: public function getPaginatorItems(): array
96: {
97: if ($this->page < 1) {
98: $this->page = 1;
99: } elseif ($this->page > $this->total) {
100: $this->page = $this->total;
101: }
102:
103: $start = $this->page - $this->range;
104: $end = $this->page + $this->range;
105:
106: // see if we are close to the edge
107: if ($start < 1) {
108: // shift by ($start-1);
109: $end += (1 - $start);
110: $start = 1;
111: }
112: if ($end > $this->total) {
113: $start -= ($end - $this->total);
114: $end = $this->total;
115: }
116:
117: if ($start < 1) {
118: $start = 1; // shifted twice
119: }
120:
121: $p = [];
122:
123: if ($start > 1) {
124: $p[] = '[';
125: }
126:
127: if ($start > 2) {
128: $p[] = '...';
129: }
130:
131: for ($i = $start; $i <= $end; ++$i) {
132: $p[] = $i;
133: }
134:
135: if ($end < $this->total - 1) {
136: $p[] = '...';
137: }
138:
139: if ($end < $this->total) {
140: $p[] = ']';
141: }
142:
143: return $p;
144: }
145:
146: /**
147: * Return URL for displaying a certain page.
148: *
149: * @param int|string $page
150: */
151: protected function getPageUrl($page): string
152: {
153: return $this->url([$this->urlTrigger => $page]);
154: }
155:
156: /**
157: * Add extra argument to the reload view.
158: * These arguments will be set as urlOptions to JsReload.
159: *
160: * @param array $args
161: */
162: public function addReloadArgs($args): void
163: {
164: $this->reloadArgs = array_merge($this->reloadArgs, $args);
165: }
166:
167: /**
168: * Render page item using template $t for the page number $page.
169: *
170: * @param HtmlTemplate $t
171: * @param int|string $page
172: */
173: public function renderItem($t, $page = null): void
174: {
175: if ($page) {
176: $t->set('page', (string) $page);
177: $t->set('link', $this->getPageUrl($page));
178:
179: $t->trySet('active', $page === $this->page ? 'active' : '');
180: }
181:
182: $this->template->dangerouslyAppendHtml('rows', $t->renderToHtml());
183: }
184:
185: #[\Override]
186: protected function renderView(): void
187: {
188: $tItem = $this->template->cloneRegion('Item');
189: $tFirst = $this->template->hasTag('FirstItem') ? $this->template->cloneRegion('FirstItem') : $tItem;
190: $tLast = $this->template->hasTag('LastItem') ? $this->template->cloneRegion('LastItem') : $tItem;
191: $tSpacer = $this->template->cloneRegion('Spacer');
192:
193: $this->template->del('rows');
194:
195: foreach ($this->getPaginatorItems() as $item) {
196: if ($item === '[') {
197: $this->renderItem($tFirst, 1);
198: } elseif ($item === '...') {
199: $this->renderItem($tSpacer);
200: } elseif ($item === ']') {
201: $this->renderItem($tLast, $this->total);
202: } else {
203: $this->renderItem($tItem, $item);
204: }
205: }
206:
207: if ($this->reload) {
208: $this->on('click', '.item', new JsReload($this->reload, array_merge([$this->urlTrigger => new JsExpression('$(this).data(\'page\')')], $this->reloadArgs)));
209: }
210:
211: parent::renderView();
212: }
213: }
214: