1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
11: * @link http://cakephp.org CakePHP(tm) Project
12: * @since 0.9.1
13: * @license http://www.opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Helper;
16:
17: use Cake\Core\Configure;
18: use Cake\Network\Response;
19: use Cake\View\Helper;
20: use Cake\View\StringTemplateTrait;
21: use Cake\View\View;
22:
23: /**
24: * Html Helper class for easy use of HTML widgets.
25: *
26: * HtmlHelper encloses all methods needed while working with HTML pages.
27: *
28: * @property UrlHelper $Url
29: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html
30: */
31: class HtmlHelper extends Helper
32: {
33:
34: use StringTemplateTrait;
35:
36: /**
37: * List of helpers used by this helper
38: *
39: * @var array
40: */
41: public $helpers = ['Url'];
42:
43: /**
44: * Reference to the Response object
45: *
46: * @var \Cake\Network\Response
47: */
48: public $response;
49:
50: /**
51: * Default config for this class
52: *
53: * @var array
54: */
55: protected $_defaultConfig = [
56: 'templates' => [
57: 'meta' => '<meta{{attrs}}/>',
58: 'metalink' => '<link href="{{url}}"{{attrs}}/>',
59: 'link' => '<a href="{{url}}"{{attrs}}>{{content}}</a>',
60: 'mailto' => '<a href="mailto:{{url}}"{{attrs}}>{{content}}</a>',
61: 'image' => '<img src="{{url}}"{{attrs}}/>',
62: 'tableheader' => '<th{{attrs}}>{{content}}</th>',
63: 'tableheaderrow' => '<tr{{attrs}}>{{content}}</tr>',
64: 'tablecell' => '<td{{attrs}}>{{content}}</td>',
65: 'tablerow' => '<tr{{attrs}}>{{content}}</tr>',
66: 'block' => '<div{{attrs}}>{{content}}</div>',
67: 'blockstart' => '<div{{attrs}}>',
68: 'blockend' => '</div>',
69: 'tag' => '<{{tag}}{{attrs}}>{{content}}</{{tag}}>',
70: 'tagstart' => '<{{tag}}{{attrs}}>',
71: 'tagend' => '</{{tag}}>',
72: 'tagselfclosing' => '<{{tag}}{{attrs}}/>',
73: 'para' => '<p{{attrs}}>{{content}}</p>',
74: 'parastart' => '<p{{attrs}}>',
75: 'css' => '<link rel="{{rel}}" href="{{url}}"{{attrs}}/>',
76: 'style' => '<style{{attrs}}>{{content}}</style>',
77: 'charset' => '<meta http-equiv="Content-Type" content="text/html; charset={{charset}}"/>',
78: 'ul' => '<ul{{attrs}}>{{content}}</ul>',
79: 'ol' => '<ol{{attrs}}>{{content}}</ol>',
80: 'li' => '<li{{attrs}}>{{content}}</li>',
81: 'javascriptblock' => '<script{{attrs}}>{{content}}</script>',
82: 'javascriptstart' => '<script>',
83: 'javascriptlink' => '<script src="{{url}}"{{attrs}}></script>',
84: 'javascriptend' => '</script>'
85: ]
86: ];
87:
88: /**
89: * Breadcrumbs.
90: *
91: * @var array
92: */
93: protected $_crumbs = [];
94:
95: /**
96: * Names of script & css files that have been included once
97: *
98: * @var array
99: */
100: protected $_includedAssets = [];
101:
102: /**
103: * Options for the currently opened script block buffer if any.
104: *
105: * @var array
106: */
107: protected $_scriptBlockOptions = [];
108:
109: /**
110: * Document type definitions
111: *
112: * @var array
113: */
114: protected $_docTypes = [
115: 'html4-strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
116: 'html4-trans' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
117: 'html4-frame' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
118: 'html5' => '<!DOCTYPE html>',
119: 'xhtml-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
120: 'xhtml-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
121: 'xhtml-frame' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
122: 'xhtml11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
123: ];
124:
125: /**
126: * Constructor
127: *
128: * ### Settings
129: *
130: * - `templates` Either a filename to a config containing templates.
131: * Or an array of templates to load. See Cake\View\StringTemplate for
132: * template formatting.
133: *
134: * ### Customizing tag sets
135: *
136: * Using the `templates` option you can redefine the tag HtmlHelper will use.
137: *
138: * @param \Cake\View\View $View The View this helper is being attached to.
139: * @param array $config Configuration settings for the helper.
140: */
141: public function __construct(View $View, array $config = [])
142: {
143: parent::__construct($View, $config);
144: $this->response = $this->_View->response ?: new Response();
145: }
146:
147: /**
148: * Adds a link to the breadcrumbs array.
149: *
150: * @param string $name Text for link
151: * @param string|null $link URL for link (if empty it won't be a link)
152: * @param string|array $options Link attributes e.g. ['id' => 'selected']
153: * @return $this
154: * @see HtmlHelper::link() for details on $options that can be used.
155: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
156: */
157: public function addCrumb($name, $link = null, array $options = [])
158: {
159: $this->_crumbs[] = [$name, $link, $options];
160: return $this;
161: }
162:
163: /**
164: * Returns a doctype string.
165: *
166: * Possible doctypes:
167: *
168: * - html4-strict: HTML4 Strict.
169: * - html4-trans: HTML4 Transitional.
170: * - html4-frame: HTML4 Frameset.
171: * - html5: HTML5. Default value.
172: * - xhtml-strict: XHTML1 Strict.
173: * - xhtml-trans: XHTML1 Transitional.
174: * - xhtml-frame: XHTML1 Frameset.
175: * - xhtml11: XHTML1.1.
176: *
177: * @param string $type Doctype to use.
178: * @return string|null Doctype string
179: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-doctype-tags
180: */
181: public function docType($type = 'html5')
182: {
183: if (isset($this->_docTypes[$type])) {
184: return $this->_docTypes[$type];
185: }
186: return null;
187: }
188:
189: /**
190: * Creates a link to an external resource and handles basic meta tags
191: *
192: * Create a meta tag that is output inline:
193: *
194: * `$this->Html->meta('icon', 'favicon.ico');
195: *
196: * Append the meta tag to custom view block "meta":
197: *
198: * ```
199: * $this->Html->meta('description', 'A great page', ['block' => true]);
200: * ```
201: *
202: * Append the meta tag to custom view block:
203: *
204: * ```
205: * $this->Html->meta('description', 'A great page', ['block' => 'metaTags']);
206: * ```
207: *
208: * Create a custom meta tag:
209: *
210: * ```
211: * $this->Html->meta(['property' => 'og:site_name', 'content' => 'CakePHP']);
212: * ```
213: *
214: * ### Options
215: *
216: * - `block` - Set to true to append output to view block "meta" or provide
217: * custom block name.
218: *
219: * @param string|array $type The title of the external resource
220: * @param string|array|null $content The address of the external resource or string for content attribute
221: * @param array $options Other attributes for the generated tag. If the type attribute is html,
222: * rss, atom, or icon, the mime-type is returned.
223: * @return string A completed `<link />` element.
224: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-meta-tags
225: */
226: public function meta($type, $content = null, array $options = [])
227: {
228: $options += ['block' => null];
229:
230: if (!is_array($type)) {
231: $types = [
232: 'rss' => ['type' => 'application/rss+xml', 'rel' => 'alternate', 'title' => $type, 'link' => $content],
233: 'atom' => ['type' => 'application/atom+xml', 'title' => $type, 'link' => $content],
234: 'icon' => ['type' => 'image/x-icon', 'rel' => 'icon', 'link' => $content],
235: 'keywords' => ['name' => 'keywords', 'content' => $content],
236: 'description' => ['name' => 'description', 'content' => $content],
237: 'robots' => ['name' => 'robots', 'content' => $content],
238: 'viewport' => ['name' => 'viewport', 'content' => $content],
239: ];
240:
241: if ($type === 'icon' && $content === null) {
242: $types['icon']['link'] = 'favicon.ico';
243: }
244:
245: if (isset($types[$type])) {
246: $type = $types[$type];
247: } elseif (!isset($options['type']) && $content !== null) {
248: if (is_array($content) && isset($content['_ext'])) {
249: $type = $types[$content['_ext']];
250: } else {
251: $type = ['name' => $type, 'content' => $content];
252: }
253: } elseif (isset($options['type']) && isset($types[$options['type']])) {
254: $type = $types[$options['type']];
255: unset($options['type']);
256: } else {
257: $type = [];
258: }
259: }
260:
261: $options += $type;
262: $out = null;
263:
264: if (isset($options['link'])) {
265: $options['link'] = $this->Url->assetUrl($options['link']);
266: if (isset($options['rel']) && $options['rel'] === 'icon') {
267: $out = $this->formatTemplate('metalink', [
268: 'url' => $options['link'],
269: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'link'])
270: ]);
271: $options['rel'] = 'shortcut icon';
272: }
273: $out .= $this->formatTemplate('metalink', [
274: 'url' => $options['link'],
275: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'link'])
276: ]);
277: } else {
278: $out = $this->formatTemplate('meta', [
279: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'type'])
280: ]);
281: }
282:
283: if (empty($options['block'])) {
284: return $out;
285: }
286: if ($options['block'] === true) {
287: $options['block'] = __FUNCTION__;
288: }
289: $this->_View->append($options['block'], $out);
290: }
291:
292: /**
293: * Returns a charset META-tag.
294: *
295: * @param string|null $charset The character set to be used in the meta tag. If empty,
296: * The App.encoding value will be used. Example: "utf-8".
297: * @return string A meta tag containing the specified character set.
298: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-charset-tags
299: */
300: public function charset($charset = null)
301: {
302: if (empty($charset)) {
303: $charset = strtolower(Configure::read('App.encoding'));
304: }
305: return $this->formatTemplate('charset', [
306: 'charset' => (!empty($charset) ? $charset : 'utf-8')
307: ]);
308: }
309:
310: /**
311: * Creates an HTML link.
312: *
313: * If $url starts with "http://" this is treated as an external link. Else,
314: * it is treated as a path to controller/action and parsed with the
315: * UrlHelper::url() method.
316: *
317: * If the $url is empty, $title is used instead.
318: *
319: * ### Options
320: *
321: * - `escape` Set to false to disable escaping of title and attributes.
322: * - `escapeTitle` Set to false to disable escaping of title. Takes precedence
323: * over value of `escape`)
324: * - `confirm` JavaScript confirmation message.
325: *
326: * @param string $title The content to be wrapped by <a> tags.
327: * @param string|array|null $url Cake-relative URL or array of URL parameters, or
328: * external URL (starts with http://)
329: * @param array $options Array of options and HTML attributes.
330: * @return string An `<a />` element.
331: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-links
332: */
333: public function link($title, $url = null, array $options = [])
334: {
335: $escapeTitle = true;
336: if ($url !== null) {
337: $url = $this->Url->build($url);
338: } else {
339: $url = $this->Url->build($title);
340: $title = htmlspecialchars_decode($url, ENT_QUOTES);
341: $title = h(urldecode($title));
342: $escapeTitle = false;
343: }
344:
345: if (isset($options['escapeTitle'])) {
346: $escapeTitle = $options['escapeTitle'];
347: unset($options['escapeTitle']);
348: } elseif (isset($options['escape'])) {
349: $escapeTitle = $options['escape'];
350: }
351:
352: if ($escapeTitle === true) {
353: $title = h($title);
354: } elseif (is_string($escapeTitle)) {
355: $title = htmlentities($title, ENT_QUOTES, $escapeTitle);
356: }
357:
358: $confirmMessage = null;
359: if (isset($options['confirm'])) {
360: $confirmMessage = $options['confirm'];
361: unset($options['confirm']);
362: }
363: if ($confirmMessage) {
364: $options['onclick'] = $this->_confirm($confirmMessage, 'return true;', 'return false;', $options);
365: }
366:
367: $templater = $this->templater();
368: return $templater->format('link', [
369: 'url' => $url,
370: 'attrs' => $templater->formatAttributes($options),
371: 'content' => $title
372: ]);
373: }
374:
375: /**
376: * Creates a link element for CSS stylesheets.
377: *
378: * ### Usage
379: *
380: * Include one CSS file:
381: *
382: * ```
383: * echo $this->Html->css('styles.css');
384: * ```
385: *
386: * Include multiple CSS files:
387: *
388: * ```
389: * echo $this->Html->css(['one.css', 'two.css']);
390: * ```
391: *
392: * Add the stylesheet to view block "css":
393: *
394: * ```
395: * $this->Html->css('styles.css', ['block' => true]);
396: * ```
397: *
398: * Add the stylesheet to a custom block:
399: *
400: * ```
401: * $this->Html->css('styles.css', ['block' => 'layoutCss']);
402: * ```
403: *
404: * ### Options
405: *
406: * - `block` Set to true to append output to view block "css" or provide
407: * custom block name.
408: * - `once` Whether or not the css file should be checked for uniqueness. If true css
409: * files will only be included once, use false to allow the same
410: * css to be included more than once per request.
411: * - `plugin` False value will prevent parsing path as a plugin
412: * - `rel` Defaults to 'stylesheet'. If equal to 'import' the stylesheet will be imported.
413: * - `fullBase` If true the URL will get a full address for the css file.
414: *
415: * @param string|array $path The name of a CSS style sheet or an array containing names of
416: * CSS stylesheets. If `$path` is prefixed with '/', the path will be relative to the webroot
417: * of your application. Otherwise, the path will be relative to your CSS path, usually webroot/css.
418: * @param array $options Array of options and HTML arguments.
419: * @return string CSS `<link />` or `<style />` tag, depending on the type of link.
420: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#linking-to-css-files
421: */
422: public function css($path, array $options = [])
423: {
424: $options += ['once' => true, 'block' => null, 'rel' => 'stylesheet'];
425:
426: if (is_array($path)) {
427: $out = '';
428: foreach ($path as $i) {
429: $out .= "\n\t" . $this->css($i, $options);
430: }
431: if (empty($options['block'])) {
432: return $out . "\n";
433: }
434: return;
435: }
436:
437: if (strpos($path, '//') !== false) {
438: $url = $path;
439: } else {
440: $url = $this->Url->assetUrl($path, $options + ['pathPrefix' => Configure::read('App.cssBaseUrl'), 'ext' => '.css']);
441: $options = array_diff_key($options, ['fullBase' => null, 'pathPrefix' => null]);
442: }
443:
444: if ($options['once'] && isset($this->_includedAssets[__METHOD__][$path])) {
445: return '';
446: }
447: unset($options['once']);
448: $this->_includedAssets[__METHOD__][$path] = true;
449: $templater = $this->templater();
450:
451: if ($options['rel'] === 'import') {
452: $out = $templater->format('style', [
453: 'attrs' => $templater->formatAttributes($options, ['rel', 'block']),
454: 'content' => '@import url(' . $url . ');',
455: ]);
456: } else {
457: $out = $templater->format('css', [
458: 'rel' => $options['rel'],
459: 'url' => $url,
460: 'attrs' => $templater->formatAttributes($options, ['rel', 'block']),
461: ]);
462: }
463:
464: if (empty($options['block'])) {
465: return $out;
466: }
467: if ($options['block'] === true) {
468: $options['block'] = __FUNCTION__;
469: }
470: $this->_View->append($options['block'], $out);
471: }
472:
473: /**
474: * Returns one or many `<script>` tags depending on the number of scripts given.
475: *
476: * If the filename is prefixed with "/", the path will be relative to the base path of your
477: * application. Otherwise, the path will be relative to your JavaScript path, usually webroot/js.
478: *
479: * ### Usage
480: *
481: * Include one script file:
482: *
483: * ```
484: * echo $this->Html->script('styles.js');
485: * ```
486: *
487: * Include multiple script files:
488: *
489: * ```
490: * echo $this->Html->script(['one.js', 'two.js']);
491: * ```
492: *
493: * Add the script file to a custom block:
494: *
495: * ```
496: * $this->Html->script('styles.js', ['block' => 'bodyScript']);
497: * ```
498: *
499: * ### Options
500: *
501: * - `block` Set to true to append output to view block "script" or provide
502: * custom block name.
503: * - `once` Whether or not the script should be checked for uniqueness. If true scripts will only be
504: * included once, use false to allow the same script to be included more than once per request.
505: * - `plugin` False value will prevent parsing path as a plugin
506: * - `fullBase` If true the url will get a full address for the script file.
507: *
508: * @param string|array $url String or array of javascript files to include
509: * @param array $options Array of options, and html attributes see above.
510: * @return mixed String of `<script />` tags or null if block is specified in options
511: * or if $once is true and the file has been included before.
512: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#linking-to-javascript-files
513: */
514: public function script($url, array $options = [])
515: {
516: $defaults = ['block' => null, 'once' => true];
517: $options += $defaults;
518:
519: if (is_array($url)) {
520: $out = '';
521: foreach ($url as $i) {
522: $out .= "\n\t" . $this->script($i, $options);
523: }
524: if (empty($options['block'])) {
525: return $out . "\n";
526: }
527: return null;
528: }
529:
530: if (strpos($url, '//') === false) {
531: $url = $this->Url->assetUrl($url, $options + ['pathPrefix' => Configure::read('App.jsBaseUrl'), 'ext' => '.js']);
532: $options = array_diff_key($options, ['fullBase' => null, 'pathPrefix' => null]);
533: }
534: if ($options['once'] && isset($this->_includedAssets[__METHOD__][$url])) {
535: return null;
536: }
537: $this->_includedAssets[__METHOD__][$url] = true;
538:
539: $out = $this->formatTemplate('javascriptlink', [
540: 'url' => $url,
541: 'attrs' => $this->templater()->formatAttributes($options, ['block', 'once']),
542: ]);
543:
544: if (empty($options['block'])) {
545: return $out;
546: }
547: if ($options['block'] === true) {
548: $options['block'] = __FUNCTION__;
549: }
550: $this->_View->append($options['block'], $out);
551: }
552:
553: /**
554: * Wrap $script in a script tag.
555: *
556: * ### Options
557: *
558: * - `safe` (boolean) Whether or not the $script should be wrapped in <![CDATA[ ]]>
559: * - `block` Set to true to append output to view block "script" or provide
560: * custom block name.
561: *
562: * @param string $script The script to wrap
563: * @param array $options The options to use. Options not listed above will be
564: * treated as HTML attributes.
565: * @return mixed string or null depending on the value of `$options['block']`
566: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-inline-javascript-blocks
567: */
568: public function scriptBlock($script, array $options = [])
569: {
570: $options += ['safe' => true, 'block' => null];
571: if ($options['safe']) {
572: $script = "\n" . '//<![CDATA[' . "\n" . $script . "\n" . '//]]>' . "\n";
573: }
574: unset($options['safe']);
575:
576: $out = $this->formatTemplate('javascriptblock', [
577: 'attrs' => $this->templater()->formatAttributes($options, ['block']),
578: 'content' => $script
579: ]);
580:
581: if (empty($options['block'])) {
582: return $out;
583: }
584: if ($options['block'] === true) {
585: $options['block'] = 'script';
586: }
587: $this->_View->append($options['block'], $out);
588: }
589:
590: /**
591: * Begin a script block that captures output until HtmlHelper::scriptEnd()
592: * is called. This capturing block will capture all output between the methods
593: * and create a scriptBlock from it.
594: *
595: * ### Options
596: *
597: * - `safe` Whether the code block should contain a CDATA
598: * - `block` Set to true to append output to view block "script" or provide
599: * custom block name.
600: *
601: * @param array $options Options for the code block.
602: * @return void
603: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-javascript-blocks
604: */
605: public function scriptStart(array $options = [])
606: {
607: $options += ['safe' => true, 'block' => null];
608: $this->_scriptBlockOptions = $options;
609: ob_start();
610: }
611:
612: /**
613: * End a Buffered section of JavaScript capturing.
614: * Generates a script tag inline or appends to specified view block depending on
615: * the settings used when the scriptBlock was started
616: *
617: * @return mixed depending on the settings of scriptStart() either a script tag or null
618: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-javascript-blocks
619: */
620: public function scriptEnd()
621: {
622: $buffer = ob_get_clean();
623: $options = $this->_scriptBlockOptions;
624: $this->_scriptBlockOptions = [];
625: return $this->scriptBlock($buffer, $options);
626: }
627:
628: /**
629: * Builds CSS style data from an array of CSS properties
630: *
631: * ### Usage:
632: *
633: * ```
634: * echo $this->Html->style(['margin' => '10px', 'padding' => '10px'], true);
635: *
636: * // creates
637: * 'margin:10px;padding:10px;'
638: * ```
639: *
640: * @param array $data Style data array, keys will be used as property names, values as property values.
641: * @param bool $oneLine Whether or not the style block should be displayed on one line.
642: * @return string CSS styling data
643: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-css-programatically
644: */
645: public function style(array $data, $oneLine = true)
646: {
647: $out = [];
648: foreach ($data as $key => $value) {
649: $out[] = $key . ':' . $value . ';';
650: }
651: if ($oneLine) {
652: return implode(' ', $out);
653: }
654: return implode("\n", $out);
655: }
656:
657: /**
658: * Returns the breadcrumb trail as a sequence of »-separated links.
659: *
660: * If `$startText` is an array, the accepted keys are:
661: *
662: * - `text` Define the text/content for the link.
663: * - `url` Define the target of the created link.
664: *
665: * All other keys will be passed to HtmlHelper::link() as the `$options` parameter.
666: *
667: * @param string $separator Text to separate crumbs.
668: * @param string|array|bool $startText This will be the first crumb, if false it defaults to first crumb in array. Can
669: * also be an array, see above for details.
670: * @return string|null Composed bread crumbs
671: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
672: */
673: public function getCrumbs($separator = '»', $startText = false)
674: {
675: $crumbs = $this->_prepareCrumbs($startText);
676: if (!empty($crumbs)) {
677: $out = [];
678: foreach ($crumbs as $crumb) {
679: if (!empty($crumb[1])) {
680: $out[] = $this->link($crumb[0], $crumb[1], $crumb[2]);
681: } else {
682: $out[] = $crumb[0];
683: }
684: }
685: return implode($separator, $out);
686: }
687: return null;
688: }
689:
690: /**
691: * Returns breadcrumbs as a (x)html list
692: *
693: * This method uses HtmlHelper::tag() to generate list and its elements. Works
694: * similar to HtmlHelper::getCrumbs(), so it uses options which every
695: * crumb was added with.
696: *
697: * ### Options
698: *
699: * - `separator` Separator content to insert in between breadcrumbs, defaults to ''
700: * - `firstClass` Class for wrapper tag on the first breadcrumb, defaults to 'first'
701: * - `lastClass` Class for wrapper tag on current active page, defaults to 'last'
702: *
703: * @param array $options Array of HTML attributes to apply to the generated list elements.
704: * @param string|array|bool $startText This will be the first crumb, if false it defaults to first crumb in array. Can
705: * also be an array, see `HtmlHelper::getCrumbs` for details.
706: * @return string|null Breadcrumbs HTML list.
707: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-breadcrumb-trails-with-htmlhelper
708: */
709: public function getCrumbList(array $options = [], $startText = false)
710: {
711: $defaults = ['firstClass' => 'first', 'lastClass' => 'last', 'separator' => '', 'escape' => true];
712: $options += $defaults;
713: $firstClass = $options['firstClass'];
714: $lastClass = $options['lastClass'];
715: $separator = $options['separator'];
716: $escape = $options['escape'];
717: unset($options['firstClass'], $options['lastClass'], $options['separator'], $options['escape']);
718:
719: $crumbs = $this->_prepareCrumbs($startText, $escape);
720: if (empty($crumbs)) {
721: return null;
722: }
723:
724: $result = '';
725: $crumbCount = count($crumbs);
726: $ulOptions = $options;
727: foreach ($crumbs as $which => $crumb) {
728: $options = [];
729: if (empty($crumb[1])) {
730: $elementContent = $crumb[0];
731: } else {
732: $elementContent = $this->link($crumb[0], $crumb[1], $crumb[2]);
733: }
734: if (!$which && $firstClass !== false) {
735: $options['class'] = $firstClass;
736: } elseif ($which == $crumbCount - 1 && $lastClass !== false) {
737: $options['class'] = $lastClass;
738: }
739: if (!empty($separator) && ($crumbCount - $which >= 2)) {
740: $elementContent .= $separator;
741: }
742: $result .= $this->formatTemplate('li', [
743: 'content' => $elementContent,
744: 'attrs' => $this->templater()->formatAttributes($options)
745: ]);
746: }
747: return $this->formatTemplate('ul', [
748: 'content' => $result,
749: 'attrs' => $this->templater()->formatAttributes($ulOptions)
750: ]);
751: }
752:
753: /**
754: * Prepends startText to crumbs array if set
755: *
756: * @param string|array|bool $startText Text to prepend
757: * @param bool $escape If the output should be escaped or not
758: * @return array Crumb list including startText (if provided)
759: */
760: protected function _prepareCrumbs($startText, $escape = true)
761: {
762: $crumbs = $this->_crumbs;
763: if ($startText) {
764: if (!is_array($startText)) {
765: $startText = [
766: 'url' => '/',
767: 'text' => $startText
768: ];
769: }
770: $startText += ['url' => '/', 'text' => __d('cake', 'Home')];
771: list($url, $text) = [$startText['url'], $startText['text']];
772: unset($startText['url'], $startText['text']);
773: array_unshift($crumbs, [$text, $url, $startText + ['escape' => $escape]]);
774: }
775: return $crumbs;
776: }
777:
778: /**
779: * Creates a formatted IMG element.
780: *
781: * This method will set an empty alt attribute if one is not supplied.
782: *
783: * ### Usage:
784: *
785: * Create a regular image:
786: *
787: * ```
788: * echo $this->Html->image('cake_icon.png', ['alt' => 'CakePHP']);
789: * ```
790: *
791: * Create an image link:
792: *
793: * ```
794: * echo $this->Html->image('cake_icon.png', ['alt' => 'CakePHP', 'url' => 'http://cakephp.org']);
795: * ```
796: *
797: * ### Options:
798: *
799: * - `url` If provided an image link will be generated and the link will point at
800: * `$options['url']`.
801: * - `fullBase` If true the src attribute will get a full address for the image file.
802: * - `plugin` False value will prevent parsing path as a plugin
803: *
804: * @param string $path Path to the image file, relative to the app/webroot/img/ directory.
805: * @param array $options Array of HTML attributes. See above for special options.
806: * @return string completed img tag
807: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#linking-to-images
808: */
809: public function image($path, array $options = [])
810: {
811: $path = $this->Url->assetUrl($path, $options + ['pathPrefix' => Configure::read('App.imageBaseUrl')]);
812: $options = array_diff_key($options, ['fullBase' => null, 'pathPrefix' => null]);
813:
814: if (!isset($options['alt'])) {
815: $options['alt'] = '';
816: }
817:
818: $url = false;
819: if (!empty($options['url'])) {
820: $url = $options['url'];
821: unset($options['url']);
822: }
823:
824: $templater = $this->templater();
825: $image = $templater->format('image', [
826: 'url' => $path,
827: 'attrs' => $templater->formatAttributes($options),
828: ]);
829:
830: if ($url) {
831: return $templater->format('link', [
832: 'url' => $this->Url->build($url),
833: 'attrs' => null,
834: 'content' => $image
835: ]);
836: }
837: return $image;
838: }
839:
840: /**
841: * Returns a row of formatted and named TABLE headers.
842: *
843: * @param array $names Array of tablenames. Each tablename also can be a key that points to an array with a set
844: * of attributes to its specific tag
845: * @param array|null $trOptions HTML options for TR elements.
846: * @param array|null $thOptions HTML options for TH elements.
847: * @return string Completed table headers
848: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-table-headings
849: */
850: public function tableHeaders(array $names, array $trOptions = null, array $thOptions = null)
851: {
852: $out = [];
853: foreach ($names as $arg) {
854: if (!is_array($arg)) {
855: $out[] = $this->formatTemplate('tableheader', [
856: 'attrs' => $this->templater()->formatAttributes($thOptions),
857: 'content' => $arg
858: ]);
859: } else {
860: $out[] = $this->formatTemplate('tableheader', [
861: 'attrs' => $this->templater()->formatAttributes(current($arg)),
862: 'content' => key($arg)
863: ]);
864: }
865: }
866: return $this->formatTemplate('tablerow', [
867: 'attrs' => $this->templater()->formatAttributes($trOptions),
868: 'content' => implode(' ', $out)
869: ]);
870: }
871:
872: /**
873: * Returns a formatted string of table rows (TR's with TD's in them).
874: *
875: * @param array|string $data Array of table data
876: * @param array|bool|null $oddTrOptions HTML options for odd TR elements if true useCount is used
877: * @param array|bool|null $evenTrOptions HTML options for even TR elements
878: * @param bool $useCount adds class "column-$i"
879: * @param bool $continueOddEven If false, will use a non-static $count variable,
880: * so that the odd/even count is reset to zero just for that call.
881: * @return string Formatted HTML
882: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-table-cells
883: */
884: public function tableCells($data, $oddTrOptions = null, $evenTrOptions = null, $useCount = false, $continueOddEven = true)
885: {
886: if (empty($data[0]) || !is_array($data[0])) {
887: $data = [$data];
888: }
889:
890: if ($oddTrOptions === true) {
891: $useCount = true;
892: $oddTrOptions = null;
893: }
894:
895: if ($evenTrOptions === false) {
896: $continueOddEven = false;
897: $evenTrOptions = null;
898: }
899:
900: if ($continueOddEven) {
901: static $count = 0;
902: } else {
903: $count = 0;
904: }
905:
906: foreach ($data as $line) {
907: $count++;
908: $cellsOut = [];
909: $i = 0;
910: foreach ($line as $cell) {
911: $cellOptions = [];
912:
913: if (is_array($cell)) {
914: $cellOptions = $cell[1];
915: $cell = $cell[0];
916: }
917:
918: if ($useCount) {
919: if (isset($cellOptions['class'])) {
920: $cellOptions['class'] .= ' column-' . ++$i;
921: } else {
922: $cellOptions['class'] = 'column-' . ++$i;
923: }
924: }
925:
926: $cellsOut[] = $this->formatTemplate('tablecell', [
927: 'attrs' => $this->templater()->formatAttributes($cellOptions),
928: 'content' => $cell
929: ]);
930: }
931: $opts = $count % 2 ? $oddTrOptions : $evenTrOptions;
932: $out[] = $this->formatTemplate('tablerow', [
933: 'attrs' => $this->templater()->formatAttributes($opts),
934: 'content' => implode(' ', $cellsOut),
935: ]);
936: }
937: return implode("\n", $out);
938: }
939:
940: /**
941: * Returns a formatted block tag, i.e DIV, SPAN, P.
942: *
943: * ### Options
944: *
945: * - `escape` Whether or not the contents should be html_entity escaped.
946: *
947: * @param string $name Tag name.
948: * @param string $text String content that will appear inside the div element.
949: * If null, only a start tag will be printed
950: * @param array $options Additional HTML attributes of the DIV tag, see above.
951: * @return string The formatted tag element
952: */
953: public function tag($name, $text = null, array $options = [])
954: {
955: if (empty($name)) {
956: return $text;
957: }
958: if (isset($options['escape']) && $options['escape']) {
959: $text = h($text);
960: unset($options['escape']);
961: }
962: if ($text === null) {
963: $tag = 'tagstart';
964: } else {
965: $tag = 'tag';
966: }
967: return $this->formatTemplate($tag, [
968: 'attrs' => $this->templater()->formatAttributes($options),
969: 'tag' => $name,
970: 'content' => $text,
971: ]);
972: }
973:
974: /**
975: * Returns a formatted DIV tag for HTML FORMs.
976: *
977: * ### Options
978: *
979: * - `escape` Whether or not the contents should be html_entity escaped.
980: *
981: * @param string $class CSS class name of the div element.
982: * @param string $text String content that will appear inside the div element.
983: * If null, only a start tag will be printed
984: * @param array $options Additional HTML attributes of the DIV tag
985: * @return string The formatted DIV element
986: */
987: public function div($class = null, $text = null, array $options = [])
988: {
989: if (!empty($class)) {
990: $options['class'] = $class;
991: }
992: return $this->tag('div', $text, $options);
993: }
994:
995: /**
996: * Returns a formatted P tag.
997: *
998: * ### Options
999: *
1000: * - `escape` Whether or not the contents should be html_entity escaped.
1001: *
1002: * @param string $class CSS class name of the p element.
1003: * @param string $text String content that will appear inside the p element.
1004: * @param array $options Additional HTML attributes of the P tag
1005: * @return string The formatted P element
1006: */
1007: public function para($class, $text, array $options = [])
1008: {
1009: if (isset($options['escape'])) {
1010: $text = h($text);
1011: }
1012: if ($class && !empty($class)) {
1013: $options['class'] = $class;
1014: }
1015: $tag = 'para';
1016: if ($text === null) {
1017: $tag = 'parastart';
1018: }
1019: return $this->formatTemplate($tag, [
1020: 'attrs' => $this->templater()->formatAttributes($options),
1021: 'content' => $text,
1022: ]);
1023: }
1024:
1025: /**
1026: * Returns an audio/video element
1027: *
1028: * ### Usage
1029: *
1030: * Using an audio file:
1031: *
1032: * ```
1033: * echo $this->Html->media('audio.mp3', ['fullBase' => true]);
1034: * ```
1035: *
1036: * Outputs:
1037: *
1038: * `<video src="http://www.somehost.com/files/audio.mp3">Fallback text</video>`
1039: *
1040: * Using a video file:
1041: *
1042: * ```
1043: * echo $this->Html->media('video.mp4', ['text' => 'Fallback text']);
1044: * ```
1045: *
1046: * Outputs:
1047: *
1048: * `<video src="/files/video.mp4">Fallback text</video>`
1049: *
1050: * Using multiple video files:
1051: *
1052: * ```
1053: * echo $this->Html->media(
1054: * ['video.mp4', ['src' => 'video.ogv', 'type' => "video/ogg; codecs='theora, vorbis'"]],
1055: * ['tag' => 'video', 'autoplay']
1056: * );
1057: * ```
1058: *
1059: * Outputs:
1060: *
1061: * ```
1062: * <video autoplay="autoplay">
1063: * <source src="/files/video.mp4" type="video/mp4"/>
1064: * <source src="/files/video.ogv" type="video/ogv; codecs='theora, vorbis'"/>
1065: * </video>
1066: * ```
1067: *
1068: * ### Options
1069: *
1070: * - `tag` Type of media element to generate, either "audio" or "video".
1071: * If tag is not provided it's guessed based on file's mime type.
1072: * - `text` Text to include inside the audio/video tag
1073: * - `pathPrefix` Path prefix to use for relative URLs, defaults to 'files/'
1074: * - `fullBase` If provided the src attribute will get a full address including domain name
1075: *
1076: * @param string|array $path Path to the video file, relative to the webroot/{$options['pathPrefix']} directory.
1077: * Or an array where each item itself can be a path string or an associate array containing keys `src` and `type`
1078: * @param array $options Array of HTML attributes, and special options above.
1079: * @return string Generated media element
1080: */
1081: public function media($path, array $options = [])
1082: {
1083: $options += [
1084: 'tag' => null,
1085: 'pathPrefix' => 'files/',
1086: 'text' => ''
1087: ];
1088:
1089: if (!empty($options['tag'])) {
1090: $tag = $options['tag'];
1091: } else {
1092: $tag = null;
1093: }
1094:
1095: if (is_array($path)) {
1096: $sourceTags = '';
1097: foreach ($path as &$source) {
1098: if (is_string($source)) {
1099: $source = [
1100: 'src' => $source,
1101: ];
1102: }
1103: if (!isset($source['type'])) {
1104: $ext = pathinfo($source['src'], PATHINFO_EXTENSION);
1105: $source['type'] = $this->response->getMimeType($ext);
1106: }
1107: $source['src'] = $this->Url->assetUrl($source['src'], $options);
1108: $sourceTags .= $this->formatTemplate('tagselfclosing', [
1109: 'tag' => 'source',
1110: 'attrs' => $this->templater()->formatAttributes($source)
1111: ]);
1112: }
1113: unset($source);
1114: $options['text'] = $sourceTags . $options['text'];
1115: unset($options['fullBase']);
1116: } else {
1117: if (empty($path) && !empty($options['src'])) {
1118: $path = $options['src'];
1119: }
1120: $options['src'] = $this->Url->assetUrl($path, $options);
1121: }
1122:
1123: if ($tag === null) {
1124: if (is_array($path)) {
1125: $mimeType = $path[0]['type'];
1126: } else {
1127: $mimeType = $this->response->getMimeType(pathinfo($path, PATHINFO_EXTENSION));
1128: }
1129: if (preg_match('#^video/#', $mimeType)) {
1130: $tag = 'video';
1131: } else {
1132: $tag = 'audio';
1133: }
1134: }
1135:
1136: if (isset($options['poster'])) {
1137: $options['poster'] = $this->Url->assetUrl($options['poster'], ['pathPrefix' => Configure::read('App.imageBaseUrl')] + $options);
1138: }
1139: $text = $options['text'];
1140:
1141: $options = array_diff_key($options, [
1142: 'tag' => null,
1143: 'fullBase' => null,
1144: 'pathPrefix' => null,
1145: 'text' => null
1146: ]);
1147: return $this->tag($tag, $text, $options);
1148: }
1149:
1150: /**
1151: * Build a nested list (UL/OL) out of an associative array.
1152: *
1153: * Options for $options:
1154: *
1155: * - `tag` - Type of list tag to use (ol/ul)
1156: *
1157: * Options for $itemOptions:
1158: *
1159: * - `even` - Class to use for even rows.
1160: * - `odd` - Class to use for odd rows.
1161: *
1162: * @param array $list Set of elements to list
1163: * @param array $options Options and additional HTML attributes of the list (ol/ul) tag.
1164: * @param array $itemOptions Options and additional HTML attributes of the list item (LI) tag.
1165: * @return string The nested list
1166: * @link http://book.cakephp.org/3.0/en/views/helpers/html.html#creating-nested-lists
1167: */
1168: public function nestedList(array $list, array $options = [], array $itemOptions = [])
1169: {
1170: $options += ['tag' => 'ul'];
1171: $items = $this->_nestedListItem($list, $options, $itemOptions);
1172: return $this->formatTemplate($options['tag'], [
1173: 'attrs' => $this->templater()->formatAttributes($options, ['tag']),
1174: 'content' => $items
1175: ]);
1176: }
1177:
1178: /**
1179: * Internal function to build a nested list (UL/OL) out of an associative array.
1180: *
1181: * @param array $items Set of elements to list.
1182: * @param array $options Additional HTML attributes of the list (ol/ul) tag.
1183: * @param array $itemOptions Options and additional HTML attributes of the list item (LI) tag.
1184: * @return string The nested list element
1185: * @see HtmlHelper::nestedList()
1186: */
1187: protected function _nestedListItem($items, $options, $itemOptions)
1188: {
1189: $out = '';
1190:
1191: $index = 1;
1192: foreach ($items as $key => $item) {
1193: if (is_array($item)) {
1194: $item = $key . $this->nestedList($item, $options, $itemOptions);
1195: }
1196: if (isset($itemOptions['even']) && $index % 2 === 0) {
1197: $itemOptions['class'] = $itemOptions['even'];
1198: } elseif (isset($itemOptions['odd']) && $index % 2 !== 0) {
1199: $itemOptions['class'] = $itemOptions['odd'];
1200: }
1201: $out .= $this->formatTemplate('li', [
1202: 'attrs' => $this->templater()->formatAttributes($itemOptions, ['even', 'odd']),
1203: 'content' => $item
1204: ]);
1205: $index++;
1206: }
1207: return $out;
1208: }
1209:
1210: /**
1211: * Event listeners.
1212: *
1213: * @return array
1214: */
1215: public function implementedEvents()
1216: {
1217: return [];
1218: }
1219: }
1220: