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 3.3.6
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Helper;
16:
17: use Cake\View\Helper;
18: use Cake\View\StringTemplateTrait;
19: use LogicException;
20:
21: /**
22: * BreadcrumbsHelper to register and display a breadcrumb trail for your views
23: *
24: * @property \Cake\View\Helper\UrlHelper $Url
25: */
26: class BreadcrumbsHelper extends Helper
27: {
28: use StringTemplateTrait;
29:
30: /**
31: * Other helpers used by BreadcrumbsHelper.
32: *
33: * @var array
34: */
35: public $helpers = ['Url'];
36:
37: /**
38: * Default config for the helper.
39: *
40: * @var array
41: */
42: protected $_defaultConfig = [
43: 'templates' => [
44: 'wrapper' => '<ul{{attrs}}>{{content}}</ul>',
45: 'item' => '<li{{attrs}}><a href="{{url}}"{{innerAttrs}}>{{title}}</a></li>{{separator}}',
46: 'itemWithoutLink' => '<li{{attrs}}><span{{innerAttrs}}>{{title}}</span></li>{{separator}}',
47: 'separator' => '<li{{attrs}}><span{{innerAttrs}}>{{separator}}</span></li>'
48: ]
49: ];
50:
51: /**
52: * The crumb list.
53: *
54: * @var array
55: */
56: protected $crumbs = [];
57:
58: /**
59: * Add a crumb to the end of the trail.
60: *
61: * @param string|array $title If provided as a string, it represents the title of the crumb.
62: * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a
63: * single crumb. Arrays are expected to be of this form:
64: * - *title* The title of the crumb
65: * - *link* The link of the crumb. If not provided, no link will be made
66: * - *options* Options of the crumb. See description of params option of this method.
67: * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
68: * Url::build() or null / empty if the crumb does not have a link.
69: * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
70: * be rendered in (a <li> tag by default). It accepts two special keys:
71: * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
72: * the link)
73: * - *templateVars*: Specific template vars in case you override the templates provided.
74: * @return $this
75: */
76: public function add($title, $url = null, array $options = [])
77: {
78: if (is_array($title)) {
79: foreach ($title as $crumb) {
80: $this->crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []];
81: }
82:
83: return $this;
84: }
85:
86: $this->crumbs[] = compact('title', 'url', 'options');
87:
88: return $this;
89: }
90:
91: /**
92: * Prepend a crumb to the start of the queue.
93: *
94: * @param string $title If provided as a string, it represents the title of the crumb.
95: * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a
96: * single crumb. Arrays are expected to be of this form:
97: * - *title* The title of the crumb
98: * - *link* The link of the crumb. If not provided, no link will be made
99: * - *options* Options of the crumb. See description of params option of this method.
100: * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
101: * Url::build() or null / empty if the crumb does not have a link.
102: * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
103: * be rendered in (a <li> tag by default). It accepts two special keys:
104: * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
105: * the link)
106: * - *templateVars*: Specific template vars in case you override the templates provided.
107: * @return $this
108: */
109: public function prepend($title, $url = null, array $options = [])
110: {
111: if (is_array($title)) {
112: $crumbs = [];
113: foreach ($title as $crumb) {
114: $crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []];
115: }
116:
117: array_splice($this->crumbs, 0, 0, $crumbs);
118:
119: return $this;
120: }
121:
122: array_unshift($this->crumbs, compact('title', 'url', 'options'));
123:
124: return $this;
125: }
126:
127: /**
128: * Insert a crumb at a specific index.
129: *
130: * If the index already exists, the new crumb will be inserted,
131: * and the existing element will be shifted one index greater.
132: * If the index is out of bounds, it will throw an exception.
133: *
134: * @param int $index The index to insert at.
135: * @param string $title Title of the crumb.
136: * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
137: * Url::build() or null / empty if the crumb does not have a link.
138: * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
139: * be rendered in (a <li> tag by default). It accepts two special keys:
140: * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
141: * the link)
142: * - *templateVars*: Specific template vars in case you override the templates provided.
143: * @return $this
144: * @throws \LogicException In case the index is out of bound
145: */
146: public function insertAt($index, $title, $url = null, array $options = [])
147: {
148: if (!isset($this->crumbs[$index])) {
149: throw new LogicException(sprintf("No crumb could be found at index '%s'", $index));
150: }
151:
152: array_splice($this->crumbs, $index, 0, [compact('title', 'url', 'options')]);
153:
154: return $this;
155: }
156:
157: /**
158: * Insert a crumb before the first matching crumb with the specified title.
159: *
160: * Finds the index of the first crumb that matches the provided class,
161: * and inserts the supplied callable before it.
162: *
163: * @param string $matchingTitle The title of the crumb you want to insert this one before.
164: * @param string $title Title of the crumb.
165: * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
166: * Url::build() or null / empty if the crumb does not have a link.
167: * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
168: * be rendered in (a <li> tag by default). It accepts two special keys:
169: * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
170: * the link)
171: * - *templateVars*: Specific template vars in case you override the templates provided.
172: * @return $this
173: * @throws \LogicException In case the matching crumb can not be found
174: */
175: public function insertBefore($matchingTitle, $title, $url = null, array $options = [])
176: {
177: $key = $this->findCrumb($matchingTitle);
178:
179: if ($key === null) {
180: throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle));
181: }
182:
183: return $this->insertAt($key, $title, $url, $options);
184: }
185:
186: /**
187: * Insert a crumb after the first matching crumb with the specified title.
188: *
189: * Finds the index of the first crumb that matches the provided class,
190: * and inserts the supplied callable before it.
191: *
192: * @param string $matchingTitle The title of the crumb you want to insert this one after.
193: * @param string $title Title of the crumb.
194: * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
195: * Url::build() or null / empty if the crumb does not have a link.
196: * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
197: * be rendered in (a <li> tag by default). It accepts two special keys:
198: * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
199: * the link)
200: * - *templateVars*: Specific template vars in case you override the templates provided.
201: * @return $this
202: * @throws \LogicException In case the matching crumb can not be found.
203: */
204: public function insertAfter($matchingTitle, $title, $url = null, array $options = [])
205: {
206: $key = $this->findCrumb($matchingTitle);
207:
208: if ($key === null) {
209: throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle));
210: }
211:
212: return $this->insertAt($key + 1, $title, $url, $options);
213: }
214:
215: /**
216: * Returns the crumb list.
217: *
218: * @return array
219: */
220: public function getCrumbs()
221: {
222: return $this->crumbs;
223: }
224:
225: /**
226: * Removes all existing crumbs.
227: *
228: * @return $this
229: */
230: public function reset()
231: {
232: $this->crumbs = [];
233:
234: return $this;
235: }
236:
237: /**
238: * Renders the breadcrumbs trail.
239: *
240: * @param array $attributes Array of attributes applied to the `wrapper` template. Accepts the `templateVars` key to
241: * allow the insertion of custom template variable in the template.
242: * @param array $separator Array of attributes for the `separator` template.
243: * Possible properties are :
244: * - *separator* The string to be displayed as a separator
245: * - *templateVars* Allows the insertion of custom template variable in the template
246: * - *innerAttrs* To provide attributes in case your separator is divided in two elements.
247: * All other properties will be converted as HTML attributes and will replace the *attrs* key in the template.
248: * If you use the default for this option (empty), it will not render a separator.
249: * @return string The breadcrumbs trail
250: */
251: public function render(array $attributes = [], array $separator = [])
252: {
253: if (!$this->crumbs) {
254: return '';
255: }
256:
257: $crumbs = $this->crumbs;
258: $crumbsCount = count($crumbs);
259: $templater = $this->templater();
260: $separatorString = '';
261:
262: if ($separator) {
263: if (isset($separator['innerAttrs'])) {
264: $separator['innerAttrs'] = $templater->formatAttributes($separator['innerAttrs']);
265: }
266:
267: $separator['attrs'] = $templater->formatAttributes(
268: $separator,
269: ['innerAttrs', 'separator']
270: );
271:
272: $separatorString = $this->formatTemplate('separator', $separator);
273: }
274:
275: $crumbTrail = '';
276: foreach ($crumbs as $key => $crumb) {
277: $url = $crumb['url'] ? $this->Url->build($crumb['url']) : null;
278: $title = $crumb['title'];
279: $options = $crumb['options'];
280:
281: $optionsLink = [];
282: if (isset($options['innerAttrs'])) {
283: $optionsLink = $options['innerAttrs'];
284: unset($options['innerAttrs']);
285: }
286:
287: $template = 'item';
288: $templateParams = [
289: 'attrs' => $templater->formatAttributes($options, ['templateVars']),
290: 'innerAttrs' => $templater->formatAttributes($optionsLink),
291: 'title' => $title,
292: 'url' => $url,
293: 'separator' => '',
294: 'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : []
295: ];
296:
297: if (!$url) {
298: $template = 'itemWithoutLink';
299: }
300:
301: if ($separatorString && $key !== ($crumbsCount - 1)) {
302: $templateParams['separator'] = $separatorString;
303: }
304:
305: $crumbTrail .= $this->formatTemplate($template, $templateParams);
306: }
307:
308: $crumbTrail = $this->formatTemplate('wrapper', [
309: 'content' => $crumbTrail,
310: 'attrs' => $templater->formatAttributes($attributes, ['templateVars']),
311: 'templateVars' => isset($attributes['templateVars']) ? $attributes['templateVars'] : []
312: ]);
313:
314: return $crumbTrail;
315: }
316:
317: /**
318: * Search a crumb in the current stack which title matches the one provided as argument.
319: * If found, the index of the matching crumb will be returned.
320: *
321: * @param string $title Title to find.
322: * @return int|null Index of the crumb found, or null if it can not be found.
323: */
324: protected function findCrumb($title)
325: {
326: foreach ($this->crumbs as $key => $crumb) {
327: if ($crumb['title'] === $title) {
328: return $key;
329: }
330: }
331:
332: return null;
333: }
334: }
335: