1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 1.2.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Helper;
16:
17: use Cake\Utility\Hash;
18: use Cake\Utility\Inflector;
19: use Cake\View\Helper;
20: use Cake\View\StringTemplateTrait;
21: use Cake\View\View;
22:
23: /**
24: * Pagination Helper class for easy generation of pagination links.
25: *
26: * PaginationHelper encloses all methods needed when working with pagination.
27: *
28: * @property \Cake\View\Helper\UrlHelper $Url
29: * @property \Cake\View\Helper\NumberHelper $Number
30: * @property \Cake\View\Helper\HtmlHelper $Html
31: * @property \Cake\View\Helper\FormHelper $Form
32: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html
33: */
34: class PaginatorHelper extends Helper
35: {
36:
37: use StringTemplateTrait;
38:
39: /**
40: * List of helpers used by this helper
41: *
42: * @var array
43: */
44: public $helpers = ['Url', 'Number', 'Html', 'Form'];
45:
46: /**
47: * Default config for this class
48: *
49: * Options: Holds the default options for pagination links
50: *
51: * The values that may be specified are:
52: *
53: * - `url` Url of the action. See Router::url()
54: * - `url['sort']` the key that the recordset is sorted.
55: * - `url['direction']` Direction of the sorting (default: 'asc').
56: * - `url['page']` Page number to use in links.
57: * - `model` The name of the model.
58: * - `escape` Defines if the title field for the link should be escaped (default: true).
59: *
60: * Templates: the templates used by this class
61: *
62: * @var array
63: */
64: protected $_defaultConfig = [
65: 'options' => [],
66: 'templates' => [
67: 'nextActive' => '<li class="next"><a rel="next" href="{{url}}">{{text}}</a></li>',
68: 'nextDisabled' => '<li class="next disabled"><a href="" onclick="return false;">{{text}}</a></li>',
69: 'prevActive' => '<li class="prev"><a rel="prev" href="{{url}}">{{text}}</a></li>',
70: 'prevDisabled' => '<li class="prev disabled"><a href="" onclick="return false;">{{text}}</a></li>',
71: 'counterRange' => '{{start}} - {{end}} of {{count}}',
72: 'counterPages' => '{{page}} of {{pages}}',
73: 'first' => '<li class="first"><a href="{{url}}">{{text}}</a></li>',
74: 'last' => '<li class="last"><a href="{{url}}">{{text}}</a></li>',
75: 'number' => '<li><a href="{{url}}">{{text}}</a></li>',
76: 'current' => '<li class="active"><a href="">{{text}}</a></li>',
77: 'ellipsis' => '<li class="ellipsis">…</li>',
78: 'sort' => '<a href="{{url}}">{{text}}</a>',
79: 'sortAsc' => '<a class="asc" href="{{url}}">{{text}}</a>',
80: 'sortDesc' => '<a class="desc" href="{{url}}">{{text}}</a>',
81: 'sortAscLocked' => '<a class="asc locked" href="{{url}}">{{text}}</a>',
82: 'sortDescLocked' => '<a class="desc locked" href="{{url}}">{{text}}</a>',
83: ]
84: ];
85:
86: /**
87: * Default model of the paged sets
88: *
89: * @var string
90: */
91: protected $_defaultModel;
92:
93: /**
94: * Constructor. Overridden to merge passed args with URL options.
95: *
96: * @param \Cake\View\View $View The View this helper is being attached to.
97: * @param array $config Configuration settings for the helper.
98: */
99: public function __construct(View $View, array $config = [])
100: {
101: parent::__construct($View, $config);
102:
103: $query = $this->_View->getRequest()->getQueryParams();
104: unset($query['page'], $query['limit'], $query['sort'], $query['direction']);
105: $this->setConfig(
106: 'options.url',
107: array_merge($this->_View->getRequest()->getParam('pass', []), ['?' => $query])
108: );
109: }
110:
111: /**
112: * Gets the current paging parameters from the resultset for the given model
113: *
114: * @param string|null $model Optional model name. Uses the default if none is specified.
115: * @return array The array of paging parameters for the paginated resultset.
116: */
117: public function params($model = null)
118: {
119: $request = $this->_View->getRequest();
120:
121: if (empty($model)) {
122: $model = $this->defaultModel();
123: }
124: if (!$request->getParam('paging') || !$request->getParam('paging.' . $model)) {
125: return [];
126: }
127:
128: return $request->getParam('paging.' . $model);
129: }
130:
131: /**
132: * Convenience access to any of the paginator params.
133: *
134: * @param string $key Key of the paginator params array to retrieve.
135: * @param string|null $model Optional model name. Uses the default if none is specified.
136: * @return mixed Content of the requested param.
137: */
138: public function param($key, $model = null)
139: {
140: $params = $this->params($model);
141: if (!isset($params[$key])) {
142: return null;
143: }
144:
145: return $params[$key];
146: }
147:
148: /**
149: * Sets default options for all pagination links
150: *
151: * @param array $options Default options for pagination links.
152: * See PaginatorHelper::$options for list of keys.
153: * @return void
154: */
155: public function options(array $options = [])
156: {
157: $request = $this->_View->getRequest();
158:
159: if (!empty($options['paging'])) {
160: $request = $request->withParam(
161: 'paging',
162: $options['paging'] + $request->getParam('paging', [])
163: );
164: unset($options['paging']);
165: }
166:
167: $model = $this->defaultModel();
168: if (!empty($options[$model])) {
169: $request = $request->withParam(
170: 'paging.' . $model,
171: $options[$model] + (array)$request->getParam('paging.' . $model, [])
172: );
173: unset($options[$model]);
174: }
175:
176: $this->_View->setRequest($request);
177:
178: $this->_config['options'] = array_filter($options + $this->_config['options']);
179: if (empty($this->_config['options']['url'])) {
180: $this->_config['options']['url'] = [];
181: }
182: if (!empty($this->_config['options']['model'])) {
183: $this->defaultModel($this->_config['options']['model']);
184: }
185: }
186:
187: /**
188: * Gets the current page of the recordset for the given model
189: *
190: * @param string|null $model Optional model name. Uses the default if none is specified.
191: * @return int The current page number of the recordset.
192: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#checking-the-pagination-state
193: */
194: public function current($model = null)
195: {
196: $params = $this->params($model);
197:
198: if (isset($params['page'])) {
199: return $params['page'];
200: }
201:
202: return 1;
203: }
204:
205: /**
206: * Gets the total number of pages in the recordset for the given model.
207: *
208: * @param string|null $model Optional model name. Uses the default if none is specified.
209: * @return int The total pages for the recordset.
210: */
211: public function total($model = null)
212: {
213: $params = $this->params($model);
214:
215: if (isset($params['pageCount'])) {
216: return $params['pageCount'];
217: }
218:
219: return 0;
220: }
221:
222: /**
223: * Gets the current key by which the recordset is sorted
224: *
225: * @param string|null $model Optional model name. Uses the default if none is specified.
226: * @param array $options Options for pagination links. See #options for list of keys.
227: * @return string|null The name of the key by which the recordset is being sorted, or
228: * null if the results are not currently sorted.
229: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-sort-links
230: */
231: public function sortKey($model = null, array $options = [])
232: {
233: if (empty($options)) {
234: $options = $this->params($model);
235: }
236: if (!empty($options['sort'])) {
237: return $options['sort'];
238: }
239:
240: return null;
241: }
242:
243: /**
244: * Gets the current direction the recordset is sorted
245: *
246: * @param string|null $model Optional model name. Uses the default if none is specified.
247: * @param array $options Options for pagination links. See #options for list of keys.
248: * @return string The direction by which the recordset is being sorted, or
249: * null if the results are not currently sorted.
250: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-sort-links
251: */
252: public function sortDir($model = null, array $options = [])
253: {
254: $dir = null;
255:
256: if (empty($options)) {
257: $options = $this->params($model);
258: }
259:
260: if (isset($options['direction'])) {
261: $dir = strtolower($options['direction']);
262: }
263:
264: if ($dir === 'desc') {
265: return 'desc';
266: }
267:
268: return 'asc';
269: }
270:
271: /**
272: * Generate an active/inactive link for next/prev methods.
273: *
274: * @param string|bool $text The enabled text for the link.
275: * @param bool $enabled Whether or not the enabled/disabled version should be created.
276: * @param array $options An array of options from the calling method.
277: * @param array $templates An array of templates with the 'active' and 'disabled' keys.
278: * @return string Generated HTML
279: */
280: protected function _toggledLink($text, $enabled, $options, $templates)
281: {
282: $template = $templates['active'];
283: if (!$enabled) {
284: $text = $options['disabledTitle'];
285: $template = $templates['disabled'];
286: }
287:
288: if (!$enabled && $text === false) {
289: return '';
290: }
291: $text = $options['escape'] ? h($text) : $text;
292:
293: $templater = $this->templater();
294: $newTemplates = !empty($options['templates']) ? $options['templates'] : false;
295: if ($newTemplates) {
296: $templater->push();
297: $templateMethod = is_string($options['templates']) ? 'load' : 'add';
298: $templater->{$templateMethod}($options['templates']);
299: }
300:
301: if (!$enabled) {
302: $out = $templater->format($template, [
303: 'text' => $text,
304: ]);
305:
306: if ($newTemplates) {
307: $templater->pop();
308: }
309:
310: return $out;
311: }
312: $paging = $this->params($options['model']);
313:
314: $url = array_merge(
315: $options['url'],
316: ['page' => $paging['page'] + $options['step']]
317: );
318: $url = $this->generateUrl($url, $options['model']);
319:
320: $out = $templater->format($template, [
321: 'url' => $url,
322: 'text' => $text,
323: ]);
324:
325: if ($newTemplates) {
326: $templater->pop();
327: }
328:
329: return $out;
330: }
331:
332: /**
333: * Generates a "previous" link for a set of paged records
334: *
335: * ### Options:
336: *
337: * - `disabledTitle` The text to used when the link is disabled. This
338: * defaults to the same text at the active link. Setting to false will cause
339: * this method to return ''.
340: * - `escape` Whether you want the contents html entity encoded, defaults to true
341: * - `model` The model to use, defaults to PaginatorHelper::defaultModel()
342: * - `url` An array of additional URL options to use for link generation.
343: * - `templates` An array of templates, or template file name containing the
344: * templates you'd like to use when generating the link for previous page.
345: * The helper's original templates will be restored once prev() is done.
346: *
347: * @param string $title Title for the link. Defaults to '<< Previous'.
348: * @param array $options Options for pagination link. See above for list of keys.
349: * @return string A "previous" link or a disabled link.
350: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-jump-links
351: */
352: public function prev($title = '<< Previous', array $options = [])
353: {
354: $defaults = [
355: 'url' => [],
356: 'model' => $this->defaultModel(),
357: 'disabledTitle' => $title,
358: 'escape' => true,
359: ];
360: $options += $defaults;
361: $options['step'] = -1;
362:
363: $enabled = $this->hasPrev($options['model']);
364: $templates = [
365: 'active' => 'prevActive',
366: 'disabled' => 'prevDisabled'
367: ];
368:
369: return $this->_toggledLink($title, $enabled, $options, $templates);
370: }
371:
372: /**
373: * Generates a "next" link for a set of paged records
374: *
375: * ### Options:
376: *
377: * - `disabledTitle` The text to used when the link is disabled. This
378: * defaults to the same text at the active link. Setting to false will cause
379: * this method to return ''.
380: * - `escape` Whether you want the contents html entity encoded, defaults to true
381: * - `model` The model to use, defaults to PaginatorHelper::defaultModel()
382: * - `url` An array of additional URL options to use for link generation.
383: * - `templates` An array of templates, or template file name containing the
384: * templates you'd like to use when generating the link for next page.
385: * The helper's original templates will be restored once next() is done.
386: *
387: * @param string $title Title for the link. Defaults to 'Next >>'.
388: * @param array $options Options for pagination link. See above for list of keys.
389: * @return string A "next" link or $disabledTitle text if the link is disabled.
390: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-jump-links
391: */
392: public function next($title = 'Next >>', array $options = [])
393: {
394: $defaults = [
395: 'url' => [],
396: 'model' => $this->defaultModel(),
397: 'disabledTitle' => $title,
398: 'escape' => true,
399: ];
400: $options += $defaults;
401: $options['step'] = 1;
402:
403: $enabled = $this->hasNext($options['model']);
404: $templates = [
405: 'active' => 'nextActive',
406: 'disabled' => 'nextDisabled'
407: ];
408:
409: return $this->_toggledLink($title, $enabled, $options, $templates);
410: }
411:
412: /**
413: * Generates a sorting link. Sets named parameters for the sort and direction. Handles
414: * direction switching automatically.
415: *
416: * ### Options:
417: *
418: * - `escape` Whether you want the contents html entity encoded, defaults to true.
419: * - `model` The model to use, defaults to PaginatorHelper::defaultModel().
420: * - `direction` The default direction to use when this link isn't active.
421: * - `lock` Lock direction. Will only use the default direction then, defaults to false.
422: *
423: * @param string $key The name of the key that the recordset should be sorted.
424: * @param string|null $title Title for the link. If $title is null $key will be used
425: * for the title and will be generated by inflection.
426: * @param array $options Options for sorting link. See above for list of keys.
427: * @return string A link sorting default by 'asc'. If the resultset is sorted 'asc' by the specified
428: * key the returned link will sort by 'desc'.
429: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-sort-links
430: */
431: public function sort($key, $title = null, array $options = [])
432: {
433: $options += ['url' => [], 'model' => null, 'escape' => true];
434: $url = $options['url'];
435: unset($options['url']);
436:
437: if (empty($title)) {
438: $title = $key;
439:
440: if (strpos($title, '.') !== false) {
441: $title = str_replace('.', ' ', $title);
442: }
443:
444: $title = __(Inflector::humanize(preg_replace('/_id$/', '', $title)));
445: }
446:
447: $defaultDir = isset($options['direction']) ? strtolower($options['direction']) : 'asc';
448: unset($options['direction']);
449:
450: $locked = isset($options['lock']) ? $options['lock'] : false;
451: unset($options['lock']);
452:
453: $sortKey = $this->sortKey($options['model']);
454: $defaultModel = $this->defaultModel();
455: $model = $options['model'] ?: $defaultModel;
456: list($table, $field) = explode('.', $key . '.');
457: if (!$field) {
458: $field = $table;
459: $table = $model;
460: }
461: $isSorted = (
462: $sortKey === $table . '.' . $field ||
463: $sortKey === $model . '.' . $key ||
464: $table . '.' . $field === $model . '.' . $sortKey
465: );
466:
467: $template = 'sort';
468: $dir = $defaultDir;
469: if ($isSorted) {
470: if ($locked) {
471: $template = $dir === 'asc' ? 'sortDescLocked' : 'sortAscLocked';
472: } else {
473: $dir = $this->sortDir($options['model']) === 'asc' ? 'desc' : 'asc';
474: $template = $dir === 'asc' ? 'sortDesc' : 'sortAsc';
475: }
476: }
477: if (is_array($title) && array_key_exists($dir, $title)) {
478: $title = $title[$dir];
479: }
480:
481: $url = array_merge(
482: ['sort' => $key, 'direction' => $dir, 'page' => 1],
483: $url,
484: ['order' => null]
485: );
486: $vars = [
487: 'text' => $options['escape'] ? h($title) : $title,
488: 'url' => $this->generateUrl($url, $options['model']),
489: ];
490:
491: return $this->templater()->format($template, $vars);
492: }
493:
494: /**
495: * Merges passed URL options with current pagination state to generate a pagination URL.
496: *
497: * ### Url options:
498: *
499: * - `escape`: If false, the URL will be returned unescaped, do only use if it is manually
500: * escaped afterwards before being displayed.
501: * - `fullBase`: If true, the full base URL will be prepended to the result
502: *
503: * @param array $options Pagination/URL options array
504: * @param string|null $model Which model to paginate on
505: * @param array $urlOptions Array of options
506: * The bool version of this argument is *deprecated* and will be removed in 4.0.0
507: * @return string By default, returns a full pagination URL string for use in non-standard contexts (i.e. JavaScript)
508: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#generating-pagination-urls
509: */
510: public function generateUrl(array $options = [], $model = null, $urlOptions = [])
511: {
512: if (is_bool($urlOptions)) {
513: $urlOptions = ['fullBase' => $urlOptions];
514: deprecationWarning(
515: 'Passing a boolean value as third argument into PaginatorHelper::generateUrl() is deprecated ' .
516: 'and will be removed in 4.0.0 . ' .
517: 'Pass an array instead.'
518: );
519: }
520: $urlOptions += [
521: 'escape' => true,
522: 'fullBase' => false
523: ];
524:
525: return $this->Url->build($this->generateUrlParams($options, $model), $urlOptions);
526: }
527:
528: /**
529: * Merges passed URL options with current pagination state to generate a pagination URL.
530: *
531: * @param array $options Pagination/URL options array
532: * @param string|null $model Which model to paginate on
533: * @return array An array of URL parameters
534: */
535: public function generateUrlParams(array $options = [], $model = null)
536: {
537: $paging = $this->params($model);
538: $paging += ['page' => null, 'sort' => null, 'direction' => null, 'limit' => null];
539:
540: if (!empty($paging['sort']) && !empty($options['sort']) && strpos($options['sort'], '.') === false) {
541: $paging['sort'] = $this->_removeAlias($paging['sort'], null);
542: }
543: if (!empty($paging['sortDefault']) && !empty($options['sort']) && strpos($options['sort'], '.') === false) {
544: $paging['sortDefault'] = $this->_removeAlias($paging['sortDefault'], $model);
545: }
546:
547: $url = [
548: 'page' => $paging['page'],
549: 'limit' => $paging['limit'],
550: 'sort' => $paging['sort'],
551: 'direction' => $paging['direction'],
552: ];
553:
554: if (!empty($this->_config['options']['url'])) {
555: $key = implode('.', array_filter(['options.url', Hash::get($paging, 'scope', null)]));
556: $url = array_merge($url, Hash::get($this->_config, $key, []));
557: }
558:
559: $url = array_filter($url, function ($value) {
560: return ($value || is_numeric($value) || $value === false);
561: });
562: $url = array_merge($url, $options);
563:
564: if (!empty($url['page']) && $url['page'] == 1) {
565: $url['page'] = false;
566: }
567:
568: if (isset($paging['sortDefault'], $paging['directionDefault'], $url['sort'], $url['direction']) &&
569: $url['sort'] === $paging['sortDefault'] &&
570: strtolower($url['direction']) === strtolower($paging['directionDefault'])
571: ) {
572: $url['sort'] = $url['direction'] = null;
573: }
574:
575: if (!empty($paging['scope'])) {
576: $scope = $paging['scope'];
577: $currentParams = $this->_config['options']['url'];
578:
579: if (isset($url['#'])) {
580: $currentParams['#'] = $url['#'];
581: unset($url['#']);
582: }
583:
584: // Merge existing query parameters in the scope.
585: if (isset($currentParams['?'][$scope]) && is_array($currentParams['?'][$scope])) {
586: $url += $currentParams['?'][$scope];
587: unset($currentParams['?'][$scope]);
588: }
589: $url = [$scope => $url] + $currentParams;
590: if (empty($url[$scope]['page'])) {
591: unset($url[$scope]['page']);
592: }
593: }
594:
595: return $url;
596: }
597:
598: /**
599: * Remove alias if needed.
600: *
601: * @param string $field Current field
602: * @param string|null $model Current model alias
603: * @return string Unaliased field if applicable
604: */
605: protected function _removeAlias($field, $model = null)
606: {
607: $currentModel = $model ?: $this->defaultModel();
608:
609: if (strpos($field, '.') === false) {
610: return $field;
611: }
612:
613: list ($alias, $currentField) = explode('.', $field);
614:
615: if ($alias === $currentModel) {
616: return $currentField;
617: }
618:
619: return $field;
620: }
621:
622: /**
623: * Returns true if the given result set is not at the first page
624: *
625: * @param string|null $model Optional model name. Uses the default if none is specified.
626: * @return bool True if the result set is not at the first page.
627: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#checking-the-pagination-state
628: */
629: public function hasPrev($model = null)
630: {
631: return $this->_hasPage($model, 'prev');
632: }
633:
634: /**
635: * Returns true if the given result set is not at the last page
636: *
637: * @param string|null $model Optional model name. Uses the default if none is specified.
638: * @return bool True if the result set is not at the last page.
639: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#checking-the-pagination-state
640: */
641: public function hasNext($model = null)
642: {
643: return $this->_hasPage($model, 'next');
644: }
645:
646: /**
647: * Returns true if the given result set has the page number given by $page
648: *
649: * @param string|null $model Optional model name. Uses the default if none is specified.
650: * @param int $page The page number - if not set defaults to 1.
651: * @return bool True if the given result set has the specified page number.
652: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#checking-the-pagination-state
653: */
654: public function hasPage($model = null, $page = 1)
655: {
656: if (is_numeric($model)) {
657: $page = $model;
658: $model = null;
659: }
660: $paging = $this->params($model);
661: if ($paging === []) {
662: return false;
663: }
664:
665: return $page <= $paging['pageCount'];
666: }
667:
668: /**
669: * Does $model have $page in its range?
670: *
671: * @param string $model Model name to get parameters for.
672: * @param int $page Page number you are checking.
673: * @return bool Whether model has $page
674: */
675: protected function _hasPage($model, $page)
676: {
677: $params = $this->params($model);
678:
679: return !empty($params) && $params[$page . 'Page'];
680: }
681:
682: /**
683: * Gets or sets the default model of the paged sets
684: *
685: * @param string|null $model Model name to set
686: * @return string|null Model name or null if the pagination isn't initialized.
687: */
688: public function defaultModel($model = null)
689: {
690: if ($model !== null) {
691: $this->_defaultModel = $model;
692: }
693: if ($this->_defaultModel) {
694: return $this->_defaultModel;
695: }
696: if (!$this->_View->getRequest()->getParam('paging')) {
697: return null;
698: }
699: list($this->_defaultModel) = array_keys($this->_View->getRequest()->getParam('paging'));
700:
701: return $this->_defaultModel;
702: }
703:
704: /**
705: * Returns a counter string for the paged result set
706: *
707: * ### Options
708: *
709: * - `model` The model to use, defaults to PaginatorHelper::defaultModel();
710: * - `format` The format string you want to use, defaults to 'pages' Which generates output like '1 of 5'
711: * set to 'range' to generate output like '1 - 3 of 13'. Can also be set to a custom string, containing
712: * the following placeholders `{{page}}`, `{{pages}}`, `{{current}}`, `{{count}}`, `{{model}}`, `{{start}}`, `{{end}}` and any
713: * custom content you would like.
714: *
715: * @param string|array $options Options for the counter string. See #options for list of keys.
716: * If string it will be used as format.
717: * @return string Counter string.
718: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-a-page-counter
719: */
720: public function counter($options = [])
721: {
722: if (is_string($options)) {
723: $options = ['format' => $options];
724: }
725:
726: $options += [
727: 'model' => $this->defaultModel(),
728: 'format' => 'pages',
729: ];
730:
731: $paging = $this->params($options['model']);
732: if (!$paging['pageCount']) {
733: $paging['pageCount'] = 1;
734: }
735:
736: switch ($options['format']) {
737: case 'range':
738: case 'pages':
739: $template = 'counter' . ucfirst($options['format']);
740: break;
741: default:
742: $template = 'counterCustom';
743: $this->templater()->add([$template => $options['format']]);
744: }
745: $map = array_map([$this->Number, 'format'], [
746: 'page' => $paging['page'],
747: 'pages' => $paging['pageCount'],
748: 'current' => $paging['current'],
749: 'count' => $paging['count'],
750: 'start' => $paging['start'],
751: 'end' => $paging['end']
752: ]);
753:
754: $map += [
755: 'model' => strtolower(Inflector::humanize(Inflector::tableize($options['model'])))
756: ];
757:
758: return $this->templater()->format($template, $map);
759: }
760:
761: /**
762: * Returns a set of numbers for the paged result set
763: * uses a modulus to decide how many numbers to show on each side of the current page (default: 8).
764: *
765: * ```
766: * $this->Paginator->numbers(['first' => 2, 'last' => 2]);
767: * ```
768: *
769: * Using the first and last options you can create links to the beginning and end of the page set.
770: *
771: * ### Options
772: *
773: * - `before` Content to be inserted before the numbers, but after the first links.
774: * - `after` Content to be inserted after the numbers, but before the last links.
775: * - `model` Model to create numbers for, defaults to PaginatorHelper::defaultModel()
776: * - `modulus` How many numbers to include on either side of the current page, defaults to 8.
777: * Set to `false` to disable and to show all numbers.
778: * - `first` Whether you want first links generated, set to an integer to define the number of 'first'
779: * links to generate. If a string is set a link to the first page will be generated with the value
780: * as the title.
781: * - `last` Whether you want last links generated, set to an integer to define the number of 'last'
782: * links to generate. If a string is set a link to the last page will be generated with the value
783: * as the title.
784: * - `templates` An array of templates, or template file name containing the templates you'd like to
785: * use when generating the numbers. The helper's original templates will be restored once
786: * numbers() is done.
787: * - `url` An array of additional URL options to use for link generation.
788: *
789: * The generated number links will include the 'ellipsis' template when the `first` and `last` options
790: * and the number of pages exceed the modulus. For example if you have 25 pages, and use the first/last
791: * options and a modulus of 8, ellipsis content will be inserted after the first and last link sets.
792: *
793: * @param array $options Options for the numbers.
794: * @return string|false Numbers string.
795: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-page-number-links
796: */
797: public function numbers(array $options = [])
798: {
799: $defaults = [
800: 'before' => null, 'after' => null, 'model' => $this->defaultModel(),
801: 'modulus' => 8, 'first' => null, 'last' => null, 'url' => []
802: ];
803: $options += $defaults;
804:
805: $params = (array)$this->params($options['model']) + ['page' => 1];
806: if ($params['pageCount'] <= 1) {
807: return false;
808: }
809:
810: $templater = $this->templater();
811: if (isset($options['templates'])) {
812: $templater->push();
813: $method = is_string($options['templates']) ? 'load' : 'add';
814: $templater->{$method}($options['templates']);
815: }
816:
817: if ($options['modulus'] !== false && $params['pageCount'] > $options['modulus']) {
818: $out = $this->_modulusNumbers($templater, $params, $options);
819: } else {
820: $out = $this->_numbers($templater, $params, $options);
821: }
822:
823: if (isset($options['templates'])) {
824: $templater->pop();
825: }
826:
827: return $out;
828: }
829:
830: /**
831: * Calculates the start and end for the pagination numbers.
832: *
833: * @param array $params Params from the numbers() method.
834: * @param array $options Options from the numbers() method.
835: * @return array An array with the start and end numbers.
836: */
837: protected function _getNumbersStartAndEnd($params, $options)
838: {
839: $half = (int)($options['modulus'] / 2);
840: $end = max(1 + $options['modulus'], $params['page'] + $half);
841: $start = min($params['pageCount'] - $options['modulus'], $params['page'] - $half - $options['modulus'] % 2);
842:
843: if ($options['first']) {
844: $first = is_int($options['first']) ? $options['first'] : 1;
845:
846: if ($start <= $first + 2) {
847: $start = 1;
848: }
849: }
850:
851: if ($options['last']) {
852: $last = is_int($options['last']) ? $options['last'] : 1;
853:
854: if ($end >= $params['pageCount'] - $last - 1) {
855: $end = $params['pageCount'];
856: }
857: }
858:
859: $end = min($params['pageCount'], $end);
860: $start = max(1, $start);
861:
862: return [$start, $end];
863: }
864:
865: /**
866: * Formats a number for the paginator number output.
867: *
868: * @param \Cake\View\StringTemplate $templater StringTemplate instance.
869: * @param array $options Options from the numbers() method.
870: * @return string
871: */
872: protected function _formatNumber($templater, $options)
873: {
874: $url = array_merge($options['url'], ['page' => $options['page']]);
875: $vars = [
876: 'text' => $options['text'],
877: 'url' => $this->generateUrl($url, $options['model']),
878: ];
879:
880: return $templater->format('number', $vars);
881: }
882:
883: /**
884: * Generates the numbers for the paginator numbers() method.
885: *
886: * @param \Cake\View\StringTemplate $templater StringTemplate instance.
887: * @param array $params Params from the numbers() method.
888: * @param array $options Options from the numbers() method.
889: * @return string Markup output.
890: */
891: protected function _modulusNumbers($templater, $params, $options)
892: {
893: $out = '';
894: $ellipsis = $templater->format('ellipsis', []);
895:
896: list($start, $end) = $this->_getNumbersStartAndEnd($params, $options);
897:
898: $out .= $this->_firstNumber($ellipsis, $params, $start, $options);
899: $out .= $options['before'];
900:
901: for ($i = $start; $i < $params['page']; $i++) {
902: $out .= $this->_formatNumber($templater, [
903: 'text' => $this->Number->format($i),
904: 'page' => $i,
905: 'model' => $options['model'],
906: 'url' => $options['url'],
907: ]);
908: }
909:
910: $url = array_merge($options['url'], ['page' => $params['page']]);
911: $out .= $templater->format('current', [
912: 'text' => $this->Number->format($params['page']),
913: 'url' => $this->generateUrl($url, $options['model']),
914: ]);
915:
916: $start = $params['page'] + 1;
917: $i = $start;
918: while ($i < $end) {
919: $out .= $this->_formatNumber($templater, [
920: 'text' => $this->Number->format($i),
921: 'page' => $i,
922: 'model' => $options['model'],
923: 'url' => $options['url'],
924: ]);
925: $i++;
926: }
927:
928: if ($end != $params['page']) {
929: $out .= $this->_formatNumber($templater, [
930: 'text' => $this->Number->format($i),
931: 'page' => $end,
932: 'model' => $options['model'],
933: 'url' => $options['url'],
934: ]);
935: }
936:
937: $out .= $options['after'];
938: $out .= $this->_lastNumber($ellipsis, $params, $end, $options);
939:
940: return $out;
941: }
942:
943: /**
944: * Generates the first number for the paginator numbers() method.
945: *
946: * @param string $ellipsis Ellipsis character.
947: * @param array $params Params from the numbers() method.
948: * @param int $start Start number.
949: * @param array $options Options from the numbers() method.
950: * @return string Markup output.
951: */
952: protected function _firstNumber($ellipsis, $params, $start, $options)
953: {
954: $out = '';
955: $first = is_int($options['first']) ? $options['first'] : 0;
956: if ($options['first'] && $start > 1) {
957: $offset = ($start <= $first) ? $start - 1 : $options['first'];
958: $out .= $this->first($offset, $options);
959: if ($first < $start - 1) {
960: $out .= $ellipsis;
961: }
962: }
963:
964: return $out;
965: }
966:
967: /**
968: * Generates the last number for the paginator numbers() method.
969: *
970: * @param string $ellipsis Ellipsis character.
971: * @param array $params Params from the numbers() method.
972: * @param int $end End number.
973: * @param array $options Options from the numbers() method.
974: * @return string Markup output.
975: */
976: protected function _lastNumber($ellipsis, $params, $end, $options)
977: {
978: $out = '';
979: $last = is_int($options['last']) ? $options['last'] : 0;
980: if ($options['last'] && $end < $params['pageCount']) {
981: $offset = ($params['pageCount'] < $end + $last) ? $params['pageCount'] - $end : $options['last'];
982: if ($offset <= $options['last'] && $params['pageCount'] - $end > $last) {
983: $out .= $ellipsis;
984: }
985: $out .= $this->last($offset, $options);
986: }
987:
988: return $out;
989: }
990:
991: /**
992: * Generates the numbers for the paginator numbers() method.
993: *
994: * @param \Cake\View\StringTemplate $templater StringTemplate instance.
995: * @param array $params Params from the numbers() method.
996: * @param array $options Options from the numbers() method.
997: * @return string Markup output.
998: */
999: protected function _numbers($templater, $params, $options)
1000: {
1001: $out = '';
1002: $out .= $options['before'];
1003: for ($i = 1; $i <= $params['pageCount']; $i++) {
1004: $url = array_merge($options['url'], ['page' => $i]);
1005: if ($i == $params['page']) {
1006: $out .= $templater->format('current', [
1007: 'text' => $this->Number->format($params['page']),
1008: 'url' => $this->generateUrl($url, $options['model']),
1009: ]);
1010: } else {
1011: $vars = [
1012: 'text' => $this->Number->format($i),
1013: 'url' => $this->generateUrl($url, $options['model']),
1014: ];
1015: $out .= $templater->format('number', $vars);
1016: }
1017: }
1018: $out .= $options['after'];
1019:
1020: return $out;
1021: }
1022:
1023: /**
1024: * Returns a first or set of numbers for the first pages.
1025: *
1026: * ```
1027: * echo $this->Paginator->first('< first');
1028: * ```
1029: *
1030: * Creates a single link for the first page. Will output nothing if you are on the first page.
1031: *
1032: * ```
1033: * echo $this->Paginator->first(3);
1034: * ```
1035: *
1036: * Will create links for the first 3 pages, once you get to the third or greater page. Prior to that
1037: * nothing will be output.
1038: *
1039: * ### Options:
1040: *
1041: * - `model` The model to use defaults to PaginatorHelper::defaultModel()
1042: * - `escape` Whether or not to HTML escape the text.
1043: * - `url` An array of additional URL options to use for link generation.
1044: *
1045: * @param string|int $first if string use as label for the link. If numeric, the number of page links
1046: * you want at the beginning of the range.
1047: * @param array $options An array of options.
1048: * @return string|false Numbers string.
1049: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-jump-links
1050: */
1051: public function first($first = '<< first', array $options = [])
1052: {
1053: $options += [
1054: 'url' => [],
1055: 'model' => $this->defaultModel(),
1056: 'escape' => true
1057: ];
1058:
1059: $params = $this->params($options['model']);
1060:
1061: if ($params['pageCount'] <= 1) {
1062: return false;
1063: }
1064:
1065: $out = '';
1066:
1067: if (is_int($first) && $params['page'] >= $first) {
1068: for ($i = 1; $i <= $first; $i++) {
1069: $url = array_merge($options['url'], ['page' => $i]);
1070: $out .= $this->templater()->format('number', [
1071: 'url' => $this->generateUrl($url, $options['model']),
1072: 'text' => $this->Number->format($i)
1073: ]);
1074: }
1075: } elseif ($params['page'] > 1 && is_string($first)) {
1076: $first = $options['escape'] ? h($first) : $first;
1077: $out .= $this->templater()->format('first', [
1078: 'url' => $this->generateUrl(['page' => 1], $options['model']),
1079: 'text' => $first
1080: ]);
1081: }
1082:
1083: return $out;
1084: }
1085:
1086: /**
1087: * Returns a last or set of numbers for the last pages.
1088: *
1089: * ```
1090: * echo $this->Paginator->last('last >');
1091: * ```
1092: *
1093: * Creates a single link for the last page. Will output nothing if you are on the last page.
1094: *
1095: * ```
1096: * echo $this->Paginator->last(3);
1097: * ```
1098: *
1099: * Will create links for the last 3 pages. Once you enter the page range, no output will be created.
1100: *
1101: * ### Options:
1102: *
1103: * - `model` The model to use defaults to PaginatorHelper::defaultModel()
1104: * - `escape` Whether or not to HTML escape the text.
1105: * - `url` An array of additional URL options to use for link generation.
1106: *
1107: * @param string|int $last if string use as label for the link, if numeric print page numbers
1108: * @param array $options Array of options
1109: * @return string|false Numbers string.
1110: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-jump-links
1111: */
1112: public function last($last = 'last >>', array $options = [])
1113: {
1114: $options += [
1115: 'model' => $this->defaultModel(),
1116: 'escape' => true,
1117: 'url' => []
1118: ];
1119: $params = $this->params($options['model']);
1120:
1121: if ($params['pageCount'] <= 1) {
1122: return false;
1123: }
1124:
1125: $out = '';
1126: $lower = (int)$params['pageCount'] - (int)$last + 1;
1127:
1128: if (is_int($last) && $params['page'] <= $lower) {
1129: for ($i = $lower; $i <= $params['pageCount']; $i++) {
1130: $url = array_merge($options['url'], ['page' => $i]);
1131: $out .= $this->templater()->format('number', [
1132: 'url' => $this->generateUrl($url, $options['model']),
1133: 'text' => $this->Number->format($i)
1134: ]);
1135: }
1136: } elseif ($params['page'] < $params['pageCount'] && is_string($last)) {
1137: $last = $options['escape'] ? h($last) : $last;
1138: $out .= $this->templater()->format('last', [
1139: 'url' => $this->generateUrl(['page' => $params['pageCount']], $options['model']),
1140: 'text' => $last
1141: ]);
1142: }
1143:
1144: return $out;
1145: }
1146:
1147: /**
1148: * Returns the meta-links for a paginated result set.
1149: *
1150: * ```
1151: * echo $this->Paginator->meta();
1152: * ```
1153: *
1154: * Echos the links directly, will output nothing if there is neither a previous nor next page.
1155: *
1156: * ```
1157: * $this->Paginator->meta(['block' => true]);
1158: * ```
1159: *
1160: * Will append the output of the meta function to the named block - if true is passed the "meta"
1161: * block is used.
1162: *
1163: * ### Options:
1164: *
1165: * - `model` The model to use defaults to PaginatorHelper::defaultModel()
1166: * - `block` The block name to append the output to, or false/absent to return as a string
1167: * - `prev` (default True) True to generate meta for previous page
1168: * - `next` (default True) True to generate meta for next page
1169: * - `first` (default False) True to generate meta for first page
1170: * - `last` (default False) True to generate meta for last page
1171: *
1172: * @param array $options Array of options
1173: * @return string|null Meta links
1174: */
1175: public function meta(array $options = [])
1176: {
1177: $options += [
1178: 'model' => null,
1179: 'block' => false,
1180: 'prev' => true,
1181: 'next' => true,
1182: 'first' => false,
1183: 'last' => false
1184: ];
1185:
1186: $model = isset($options['model']) ? $options['model'] : null;
1187: $params = $this->params($model);
1188: $links = [];
1189:
1190: if ($options['prev'] && $this->hasPrev()) {
1191: $links[] = $this->Html->meta('prev', $this->generateUrl(['page' => $params['page'] - 1], null, ['fullBase' => true]));
1192: }
1193:
1194: if ($options['next'] && $this->hasNext()) {
1195: $links[] = $this->Html->meta('next', $this->generateUrl(['page' => $params['page'] + 1], null, ['fullBase' => true]));
1196: }
1197:
1198: if ($options['first']) {
1199: $links[] = $this->Html->meta('first', $this->generateUrl(['page' => 1], null, ['fullBase' => true]));
1200: }
1201:
1202: if ($options['last']) {
1203: $links[] = $this->Html->meta('last', $this->generateUrl(['page' => $params['pageCount']], null, ['fullBase' => true]));
1204: }
1205:
1206: $out = implode($links);
1207:
1208: if ($options['block'] === true) {
1209: $options['block'] = __FUNCTION__;
1210: }
1211:
1212: if ($options['block']) {
1213: $this->_View->append($options['block'], $out);
1214:
1215: return null;
1216: }
1217:
1218: return $out;
1219: }
1220:
1221: /**
1222: * Event listeners.
1223: *
1224: * @return array
1225: */
1226: public function implementedEvents()
1227: {
1228: return [];
1229: }
1230:
1231: /**
1232: * Dropdown select for pagination limit.
1233: * This will generate a wrapping form.
1234: *
1235: * @param array $limits The options array.
1236: * @param int|null $default Default option for pagination limit. Defaults to `$this->param('perPage')`.
1237: * @param array $options Options for Select tag attributes like class, id or event
1238: * @return string html output.
1239: */
1240: public function limitControl(array $limits = [], $default = null, array $options = [])
1241: {
1242: $out = $this->Form->create(null, ['type' => 'get']);
1243:
1244: if (empty($default) || !is_numeric($default)) {
1245: $default = $this->param('perPage');
1246: }
1247:
1248: if (empty($limits)) {
1249: $limits = [
1250: '20' => '20',
1251: '50' => '50',
1252: '100' => '100'
1253: ];
1254: }
1255:
1256: $out .= $this->Form->control('limit', $options + [
1257: 'type' => 'select',
1258: 'label' => __('View'),
1259: 'value' => $default,
1260: 'options' => $limits,
1261: 'onChange' => 'this.form.submit()'
1262: ]);
1263: $out .= $this->Form->end();
1264:
1265: return $out;
1266: }
1267: }
1268: