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: use StringTemplateTrait;
37:
38: /**
39: * List of helpers used by this helper
40: *
41: * @var array
42: */
43: public $helpers = ['Url', 'Number', 'Html', 'Form'];
44:
45: /**
46: * Default config for this class
47: *
48: * Options: Holds the default options for pagination links
49: *
50: * The values that may be specified are:
51: *
52: * - `url` Url of the action. See Router::url()
53: * - `url['sort']` the key that the recordset is sorted.
54: * - `url['direction']` Direction of the sorting (default: 'asc').
55: * - `url['page']` Page number to use in links.
56: * - `model` The name of the model.
57: * - `escape` Defines if the title field for the link should be escaped (default: true).
58: *
59: * Templates: the templates used by this class
60: *
61: * @var array
62: */
63: protected $_defaultConfig = [
64: 'options' => [],
65: 'templates' => [
66: 'nextActive' => '<li class="next"><a rel="next" href="{{url}}">{{text}}</a></li>',
67: 'nextDisabled' => '<li class="next disabled"><a href="" onclick="return false;">{{text}}</a></li>',
68: 'prevActive' => '<li class="prev"><a rel="prev" href="{{url}}">{{text}}</a></li>',
69: 'prevDisabled' => '<li class="prev disabled"><a href="" onclick="return false;">{{text}}</a></li>',
70: 'counterRange' => '{{start}} - {{end}} of {{count}}',
71: 'counterPages' => '{{page}} of {{pages}}',
72: 'first' => '<li class="first"><a href="{{url}}">{{text}}</a></li>',
73: 'last' => '<li class="last"><a href="{{url}}">{{text}}</a></li>',
74: 'number' => '<li><a href="{{url}}">{{text}}</a></li>',
75: 'current' => '<li class="active"><a href="">{{text}}</a></li>',
76: 'ellipsis' => '<li class="ellipsis">…</li>',
77: 'sort' => '<a href="{{url}}">{{text}}</a>',
78: 'sortAsc' => '<a class="asc" href="{{url}}">{{text}}</a>',
79: 'sortDesc' => '<a class="desc" href="{{url}}">{{text}}</a>',
80: 'sortAscLocked' => '<a class="asc locked" href="{{url}}">{{text}}</a>',
81: 'sortDescLocked' => '<a class="desc locked" href="{{url}}">{{text}}</a>',
82: ]
83: ];
84:
85: /**
86: * Default model of the paged sets
87: *
88: * @var string
89: */
90: protected $_defaultModel;
91:
92: /**
93: * Constructor. Overridden to merge passed args with URL options.
94: *
95: * @param \Cake\View\View $View The View this helper is being attached to.
96: * @param array $config Configuration settings for the helper.
97: */
98: public function __construct(View $View, array $config = [])
99: {
100: parent::__construct($View, $config);
101:
102: $query = $this->_View->getRequest()->getQueryParams();
103: unset($query['page'], $query['limit'], $query['sort'], $query['direction']);
104: $this->setConfig(
105: 'options.url',
106: array_merge($this->_View->getRequest()->getParam('pass', []), ['?' => $query])
107: );
108: }
109:
110: /**
111: * Gets the current paging parameters from the resultset for the given model
112: *
113: * @param string|null $model Optional model name. Uses the default if none is specified.
114: * @return array The array of paging parameters for the paginated resultset.
115: */
116: public function params($model = null)
117: {
118: $request = $this->_View->getRequest();
119:
120: if (empty($model)) {
121: $model = $this->defaultModel();
122: }
123: if (!$request->getParam('paging') || !$request->getParam('paging.' . $model)) {
124: return [];
125: }
126:
127: return $request->getParam('paging.' . $model);
128: }
129:
130: /**
131: * Convenience access to any of the paginator params.
132: *
133: * @param string $key Key of the paginator params array to retrieve.
134: * @param string|null $model Optional model name. Uses the default if none is specified.
135: * @return mixed Content of the requested param.
136: */
137: public function param($key, $model = null)
138: {
139: $params = $this->params($model);
140: if (!isset($params[$key])) {
141: return null;
142: }
143:
144: return $params[$key];
145: }
146:
147: /**
148: * Sets default options for all pagination links
149: *
150: * @param array $options Default options for pagination links.
151: * See PaginatorHelper::$options for list of keys.
152: * @return void
153: */
154: public function options(array $options = [])
155: {
156: $request = $this->_View->getRequest();
157:
158: if (!empty($options['paging'])) {
159: $request = $request->withParam(
160: 'paging',
161: $options['paging'] + $request->getParam('paging', [])
162: );
163: unset($options['paging']);
164: }
165:
166: $model = $this->defaultModel();
167: if (!empty($options[$model])) {
168: $request = $request->withParam(
169: 'paging.' . $model,
170: $options[$model] + (array)$request->getParam('paging.' . $model, [])
171: );
172: unset($options[$model]);
173: }
174:
175: $this->_View->setRequest($request);
176:
177: $this->_config['options'] = array_filter($options + $this->_config['options']);
178: if (empty($this->_config['options']['url'])) {
179: $this->_config['options']['url'] = [];
180: }
181: if (!empty($this->_config['options']['model'])) {
182: $this->defaultModel($this->_config['options']['model']);
183: }
184: }
185:
186: /**
187: * Gets the current page of the recordset for the given model
188: *
189: * @param string|null $model Optional model name. Uses the default if none is specified.
190: * @return int The current page number of the recordset.
191: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#checking-the-pagination-state
192: */
193: public function current($model = null)
194: {
195: $params = $this->params($model);
196:
197: if (isset($params['page'])) {
198: return $params['page'];
199: }
200:
201: return 1;
202: }
203:
204: /**
205: * Gets the total number of pages in the recordset for the given model.
206: *
207: * @param string|null $model Optional model name. Uses the default if none is specified.
208: * @return int The total pages for the recordset.
209: */
210: public function total($model = null)
211: {
212: $params = $this->params($model);
213:
214: if (isset($params['pageCount'])) {
215: return $params['pageCount'];
216: }
217:
218: return 0;
219: }
220:
221: /**
222: * Gets the current key by which the recordset is sorted
223: *
224: * @param string|null $model Optional model name. Uses the default if none is specified.
225: * @param array $options Options for pagination links. See #options for list of keys.
226: * @return string|null The name of the key by which the recordset is being sorted, or
227: * null if the results are not currently sorted.
228: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-sort-links
229: */
230: public function sortKey($model = null, array $options = [])
231: {
232: if (empty($options)) {
233: $options = $this->params($model);
234: }
235: if (!empty($options['sort'])) {
236: return $options['sort'];
237: }
238:
239: return null;
240: }
241:
242: /**
243: * Gets the current direction the recordset is sorted
244: *
245: * @param string|null $model Optional model name. Uses the default if none is specified.
246: * @param array $options Options for pagination links. See #options for list of keys.
247: * @return string The direction by which the recordset is being sorted, or
248: * null if the results are not currently sorted.
249: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-sort-links
250: */
251: public function sortDir($model = null, array $options = [])
252: {
253: $dir = null;
254:
255: if (empty($options)) {
256: $options = $this->params($model);
257: }
258:
259: if (isset($options['direction'])) {
260: $dir = strtolower($options['direction']);
261: }
262:
263: if ($dir === 'desc') {
264: return 'desc';
265: }
266:
267: return 'asc';
268: }
269:
270: /**
271: * Generate an active/inactive link for next/prev methods.
272: *
273: * @param string|bool $text The enabled text for the link.
274: * @param bool $enabled Whether or not the enabled/disabled version should be created.
275: * @param array $options An array of options from the calling method.
276: * @param array $templates An array of templates with the 'active' and 'disabled' keys.
277: * @return string Generated HTML
278: */
279: protected function _toggledLink($text, $enabled, $options, $templates)
280: {
281: $template = $templates['active'];
282: if (!$enabled) {
283: $text = $options['disabledTitle'];
284: $template = $templates['disabled'];
285: }
286:
287: if (!$enabled && $text === false) {
288: return '';
289: }
290: $text = $options['escape'] ? h($text) : $text;
291:
292: $templater = $this->templater();
293: $newTemplates = !empty($options['templates']) ? $options['templates'] : false;
294: if ($newTemplates) {
295: $templater->push();
296: $templateMethod = is_string($options['templates']) ? 'load' : 'add';
297: $templater->{$templateMethod}($options['templates']);
298: }
299:
300: if (!$enabled) {
301: $out = $templater->format($template, [
302: 'text' => $text,
303: ]);
304:
305: if ($newTemplates) {
306: $templater->pop();
307: }
308:
309: return $out;
310: }
311: $paging = $this->params($options['model']);
312:
313: $url = array_merge(
314: $options['url'],
315: ['page' => $paging['page'] + $options['step']]
316: );
317: $url = $this->generateUrl($url, $options['model']);
318:
319: $out = $templater->format($template, [
320: 'url' => $url,
321: 'text' => $text,
322: ]);
323:
324: if ($newTemplates) {
325: $templater->pop();
326: }
327:
328: return $out;
329: }
330:
331: /**
332: * Generates a "previous" link for a set of paged records
333: *
334: * ### Options:
335: *
336: * - `disabledTitle` The text to used when the link is disabled. This
337: * defaults to the same text at the active link. Setting to false will cause
338: * this method to return ''.
339: * - `escape` Whether you want the contents html entity encoded, defaults to true
340: * - `model` The model to use, defaults to PaginatorHelper::defaultModel()
341: * - `url` An array of additional URL options to use for link generation.
342: * - `templates` An array of templates, or template file name containing the
343: * templates you'd like to use when generating the link for previous page.
344: * The helper's original templates will be restored once prev() is done.
345: *
346: * @param string $title Title for the link. Defaults to '<< Previous'.
347: * @param array $options Options for pagination link. See above for list of keys.
348: * @return string A "previous" link or a disabled link.
349: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-jump-links
350: */
351: public function prev($title = '<< Previous', array $options = [])
352: {
353: $defaults = [
354: 'url' => [],
355: 'model' => $this->defaultModel(),
356: 'disabledTitle' => $title,
357: 'escape' => true,
358: ];
359: $options += $defaults;
360: $options['step'] = -1;
361:
362: $enabled = $this->hasPrev($options['model']);
363: $templates = [
364: 'active' => 'prevActive',
365: 'disabled' => 'prevDisabled'
366: ];
367:
368: return $this->_toggledLink($title, $enabled, $options, $templates);
369: }
370:
371: /**
372: * Generates a "next" link for a set of paged records
373: *
374: * ### Options:
375: *
376: * - `disabledTitle` The text to used when the link is disabled. This
377: * defaults to the same text at the active link. Setting to false will cause
378: * this method to return ''.
379: * - `escape` Whether you want the contents html entity encoded, defaults to true
380: * - `model` The model to use, defaults to PaginatorHelper::defaultModel()
381: * - `url` An array of additional URL options to use for link generation.
382: * - `templates` An array of templates, or template file name containing the
383: * templates you'd like to use when generating the link for next page.
384: * The helper's original templates will be restored once next() is done.
385: *
386: * @param string $title Title for the link. Defaults to 'Next >>'.
387: * @param array $options Options for pagination link. See above for list of keys.
388: * @return string A "next" link or $disabledTitle text if the link is disabled.
389: * @link https://book.cakephp.org/3.0/en/views/helpers/paginator.html#creating-jump-links
390: */
391: public function next($title = 'Next >>', array $options = [])
392: {
393: $defaults = [
394: 'url' => [],
395: 'model' => $this->defaultModel(),
396: 'disabledTitle' => $title,
397: 'escape' => true,
398: ];
399: $options += $defaults;
400: $options['step'] = 1;
401:
402: $enabled = $this->hasNext($options['model']);
403: $templates = [
404: 'active' => 'nextActive',
405: 'disabled' => 'nextDisabled'
406: ];
407:
408: return $this->_toggledLink($title, $enabled, $options, $templates);
409: }
410:
411: /**
412: * Generates a sorting link. Sets named parameters for the sort and direction. Handles
413: * direction switching automatically.
414: *
415: * ### Options:
416: *
417: * - `escape` Whether you want the contents html entity encoded, defaults to true.
418: * - `model` The model to use, defaults to PaginatorHelper::defaultModel().
419: * - `direction` The default direction to use when this link isn't active.
420: * - `lock` Lock direction. Will only use the default direction then, defaults to false.
421: *
422: * @param string $key The name of the key that the recordset should be sorted.
423: * @param string|array|null $title Title for the link. If $title is null $key will be used
424: * for the title and will be generated by inflection. It can also be an array
425: * with keys `asc` and `desc` for specifying separate titles based on the direction.
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(
1192: 'prev',
1193: $this->generateUrl(['page' => $params['page'] - 1], null, ['escape' => false, 'fullBase' => true])
1194: );
1195: }
1196:
1197: if ($options['next'] && $this->hasNext()) {
1198: $links[] = $this->Html->meta(
1199: 'next',
1200: $this->generateUrl(['page' => $params['page'] + 1], null, ['escape' => false, 'fullBase' => true])
1201: );
1202: }
1203:
1204: if ($options['first']) {
1205: $links[] = $this->Html->meta(
1206: 'first',
1207: $this->generateUrl(['page' => 1], null, ['escape' => false, 'fullBase' => true])
1208: );
1209: }
1210:
1211: if ($options['last']) {
1212: $links[] = $this->Html->meta(
1213: 'last',
1214: $this->generateUrl(['page' => $params['pageCount']], null, ['escape' => false, 'fullBase' => true])
1215: );
1216: }
1217:
1218: $out = implode($links);
1219:
1220: if ($options['block'] === true) {
1221: $options['block'] = __FUNCTION__;
1222: }
1223:
1224: if ($options['block']) {
1225: $this->_View->append($options['block'], $out);
1226:
1227: return null;
1228: }
1229:
1230: return $out;
1231: }
1232:
1233: /**
1234: * Event listeners.
1235: *
1236: * @return array
1237: */
1238: public function implementedEvents()
1239: {
1240: return [];
1241: }
1242:
1243: /**
1244: * Dropdown select for pagination limit.
1245: * This will generate a wrapping form.
1246: *
1247: * @param array $limits The options array.
1248: * @param int|null $default Default option for pagination limit. Defaults to `$this->param('perPage')`.
1249: * @param array $options Options for Select tag attributes like class, id or event
1250: * @return string html output.
1251: */
1252: public function limitControl(array $limits = [], $default = null, array $options = [])
1253: {
1254: $out = $this->Form->create(null, ['type' => 'get']);
1255:
1256: if (empty($default) || !is_numeric($default)) {
1257: $default = $this->param('perPage');
1258: }
1259:
1260: if (empty($limits)) {
1261: $limits = [
1262: '20' => '20',
1263: '50' => '50',
1264: '100' => '100'
1265: ];
1266: }
1267:
1268: $out .= $this->Form->control('limit', $options + [
1269: 'type' => 'select',
1270: 'label' => __('View'),
1271: 'default' => $default,
1272: 'value' => $this->_View->getRequest()->getQuery('limit'),
1273: 'options' => $limits,
1274: 'onChange' => 'this.form.submit()'
1275: ]);
1276: $out .= $this->Form->end();
1277:
1278: return $out;
1279: }
1280: }
1281: