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