/mvc/helpers/html
[return to app]1
<?php
2 /**
3 * HTML helper tools
4 */
5 class htmlHelper {
6 /**
7 * Internal storage of the link-prefix and hypertext protocol values
8 * @var string
9 */
10 protected $_linkPrefix, $_protocol;
11
12 /**
13 * Internal list of included CSS & JS files used by $this->_tagBuilder() to assure that files are not included
twice
14 * @var array
15 */
16 protected $_includedFiles = array();
17
18 /**
19 * Flag array to avoid defining singleton JavaScript & CSS snippets more than once
20 * @var array
21 */
22 protected $_jsSingleton = array(), $_cssSingleton = array();
23
24 /**
25 * Data to load at the end of the output, just before </body></html>
26 * @var array
27 */
28 public $eof = array();
29
30 /**
31 * Sets the protocol (http/https)
32 */
33 public function __construct() {
34 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
35 $this->_linkPrefix = 'http://' . $_SERVER['HTTP_HOST'];
36 $this->_protocol = 'https://';
37 } else {
38 $this->_protocol = 'http://';
39 }
40 }
41
42 /**
43 * Creates simple HTML wrappers, accessed via $this->__call()
44 *
45 * JS and CSS files are never included more than once even if requested twice. If DEBUG mode is enabled than
the
46 * second request will be added to the debug log as a duplicate. The jsSingleton and cssSingleton methods
operate
47 * the same as the js & css methods except that they will silently skip duplicate requests instead of logging
them.
48 *
49 * jsInlineSingleton and cssInlineSingleton makes sure a JavaScript or CSS snippet will only be output once,
even
50 * if echoed out multiple times. Eg.:
51 * $helloJs = "function helloWorld() {alert('Hello World');}";
52 * echo $html->jsInlineSingleton($helloJs);
53 *
54 * Adding an optional extra argument to jsInlineSingleton/cssInlineSingleton will return the inline code bare
(plus
55 * a trailing linebreak), this is used for joint JS/CSS statements:
56 * echo $html->jsInline($html->jsInlineSingleton($helloJs, true) . 'helloWorld();');
57 *
58 * @param string $tagType
59 * @param array $args
60 * @return string
61 */
62 protected function _tagBuilder($tagType, $args = array()) {
63 $arg = current($args);
64 if (DEBUG_MODE && ($arg === '' || (is_array($arg) && empty($arg)))) {
65 $errorMsg = 'Missing argument for ' . __CLASS__ . '::' . $tagType . '()';
66 debug::log($errorMsg, 'warn');
67 }
68
69 if (is_array($arg)) {
70 $baseArray = $args; //maintain potential-existence of $args[1]...[n]
71 foreach ($arg as $thisArg) {
72 $baseArray[0] = $thisArg;
73 $return[] = $this->_tagBuilder($tagType, $baseArray);
74 }
75 $return = implode(PHP_EOL, $return);
76 } else {
77 switch ($tagType) {
78 case 'js': //Optional extra argument to delay output until the end of the file
79 case 'jsSingleton':
80 case 'css': //Optional extra argument to define CSS media type
81 case 'cssSingleton':
82 case 'jqueryTheme':
83 if ($tagType == 'jqueryTheme') {
84 $arg = 'http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/'
85 . str_replace(' ', '-', strtolower($arg)) . '/jquery-ui.css';
86 $tagType = 'css';
87 }
88 if (!isset($this->_includedFiles[$tagType][$arg])) {
89 if ($tagType == 'css' || $tagType == 'cssSingleton') {
90 $return = '<link rel="stylesheet" type="text/css" href="' . $arg . '"'
91 . ' media="' . (isset($args[1]) ? $args[1] : 'all') . '" />';
92 } else {
93 $return = '<script type="text/javascript" src="' . $arg . '"></script>';
94 if (isset($args[1])) {
95 $this->eof[] = $return;
96 $return = null;
97 }
98 }
99 $this->_includedFiles[$tagType][$arg] = true;
100 } else {
101 $return = null;
102 if (DEBUG_MODE && ($tagType == 'js' || $tagType == 'css')) {
103 debug::log($arg . $tagType . ' file has already been included', 'warn');
104 }
105 }
106 break;
107 case 'cssInline': //Optional extra argument to define CSS media type
108 $return = '<style type="text/css" media="' . (isset($args[1]) ? $args[1] : 'all') . '">'
109 . PHP_EOL . '/*<![CDATA[*/'
110 . PHP_EOL . '<!--'
111 . PHP_EOL . $arg
112 . PHP_EOL . '//-->'
113 . PHP_EOL . '/*]]>*/'
114 . PHP_EOL . '</style>';
115 break;
116 case 'jsInline': //Optional extra argument to delay output until the end of the file
117 $return = '<script type="text/javascript">'
118 . PHP_EOL . '//<![CDATA['
119 . PHP_EOL . '<!--'
120 . PHP_EOL . $arg
121 . PHP_EOL . '//-->'
122 . PHP_EOL . '//]]>'
123 . PHP_EOL . '</script>';
124 if (isset($args[1])) {
125 $this->eof[] = $return;
126 $return = null;
127 }
128 break;
129 case 'jsInlineSingleton': //Optional extra argument to supress adding of inline JS/CSS wrapper
130 case 'cssInlineSingleton':
131 $tagTypeBase = substr($tagType, 0, -15);
132 $return = null;
133 $md5 = md5($arg);
134 if (!isset($this->{'_' . $tagTypeBase . 'Singleton'}[$md5])) {
135 $this->{'_' . $tagTypeBase . 'Singleton'}[$md5] = true;
136 $return = (!isset($args[1]) || !$args[1] ? $this->{$tagTypeBase . 'Inline'}($arg)
137 : $arg . PHP_EOL);
138 }
139 break;
140 case 'div':
141 case 'li':
142 case 'p':
143 case 'h1':
144 case 'h2':
145 case 'h3':
146 case 'h4':
147 case 'ul':
148 case 'ol':
149 $return = '<' . $tagType;
150 if (isset($args[1]) && is_array($args[1]) && $args[1]) {
151 $return .= ' ' . self::formatProperties($args[1]);
152 }
153 $return .= '>' . $arg . '</' . $tagType . '>' . PHP_EOL;
154 break;
155 default:
156 $errorMsg = 'TagType ' . $tagType . ' not valid in ' . __CLASS__ . '::' . __METHOD__;
157 throw new Exception($errorMsg);
158 break;
159 }
160 }
161 return $return;
162 }
163
164 /**
165 * Creates virtual wrapper methods via $this->_tagBuilder() for the simple wrapper functions including:
166 * $html->css, js, cssInline, jsInline, div, li, p and h1-h4
167 *
168 * @param string $method
169 * @param array $arg
170 * @return string
171 */
172 public function __call($method, $args) {
173 $validTags = array('css', 'js', 'cssSingleton', 'jsSingleton', 'jqueryTheme',
174 'cssInline', 'jsInline', 'jsInlineSingleton', 'cssInlineSingleton',
175 'div', 'li', 'p', 'h1', 'h2', 'h3', 'h4', 'ul', 'ol');
176 if (in_array($method, $validTags)) {
177 return $this->_tagBuilder($method, $args);
178 } else {
179 $errorMsg = 'Call to undefined method ' . __CLASS__ . '::' . $method . '()';
180 trigger_error($errorMsg, E_USER_ERROR);
181 }
182 }
183
184 /**
185 * Flag to make sure that header() can only be opened one-at-a-time and footer() can only be used after
header()
186 * @var boolean
187 */
188 private $_bodyOpen = false;
189
190 /**
191 * Silently updates common XHTML 1.1 invalidations with the proper XHTML 1.1 markup
192 * @var Boolean Default true
193 */
194 public $xhtmlMode = true;
195
196 /**
197 * Internal cache for the doctype data
198 * @var string
199 */
200 protected $_docTypeDeclaration, $_isHtml5;
201
202 /**
203 * Enables modification of the docType
204 *
205 * Can either set to an actual doctype definition or to one of the presets (case-insensitive):
206 *
207 * HTML 5 (this is the default DTD, there is no need to explicitely call setDocType() for HTML 5)
208 * XHTML 1.1
209 * XHTML (Alias for XHTML 1.1)
210 * XHTML 1.0 Strict
211 * XHTML 1.0 Transitional
212 * XHTML 1.0 Frameset
213 * XHTML 1.0 (Alias for XHTML 1.0 Strict)
214 * HTML 4.01
215 * HTML (Alias for HTML 4.01)
216 * XHTML Mobile 1.2
217 * XHTML Mobile 1.1
218 * XHTML Mobile 1.0
219 * Mobile 1.2 (alias for XHTML Mobile 1.2)
220 * Mobile 1.1 (alias for XHTML Mobile 1.1)
221 * Mobile 1.0 (alias for XHTML Mobile 1.0)
222 * Mobile (alias for the most-strict Mobile DTD, currently 1.2)
223 *
224 * @param string $docType
225 */
226 public function setDocType($docType) {
227 $docType = str_replace(' ', '', strtolower($docType));
228 if ($docType == 'xhtml1.0') {
229 $docType = 'strict';
230 }
231 $docType = str_replace(array('xhtmlmobile', 'xhtml1.0'), array('mobile', ''), $docType);
232 $this->_isHtml5 = ($docType == 'html5');
233 $docTypes = array(
234 'html5' => '<!DOCTYPE html>',
235 'xhtml1.1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" '
236 . '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
237 'strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '
238 . '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
239 'transitional' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
240 . '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
241 'frameset' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" '
242 . '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
243 'html4.01' => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" '
244 . '"http://www.w3.org/TR/html4/strict.dtd">',
245 'mobile1.2' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" '
246 . '"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
247 'mobile1.1' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.1//EN '
248 . '"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile11.dtd">',
249 'mobile1.0' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" '
250 . '"http://www.wapforum.org/DTD/xhtml-mobile10.dtd">'
251 );
252 //create greatest-version aliases for mobile, xhtml and html
253 $docTypes['mobile'] = $docTypes['mobile1.2'];
254 $docTypes['xhtml'] = $docTypes['xhtml1.1'];
255 $docTypes['html'] = $docTypes['html4.01'];
256 $this->_docTypeDeclaration = (isset($docTypes[$docType]) ? $docTypes[$docType] : $docType);
257 }
258
259 /**
260 * Array used internally by Vork to cache JavaScript and CSS snippets and place them in the head section
261 * Changing the contents of this property may cause Vork components to be rendered incorrectly.
262 * @var array
263 */
264 public $vorkHead = array();
265
266 /**
267 * Adds a JavaScript or CSS snippet to the head of the document if it has not yet been rendered
268 *
269 * @param string $container only valid options are: jsInline, cssInline
270 * @param string $obj
271 * @return string Returns an empty string if snippet is added to head, returns snippet if head is already
rendered
272 */
273 public function addSnippetToHead($container, $obj) {
274 $this->vorkHead[$container][] = $obj;
275 return (!$this->_bodyOpen ? '' : $obj);
276 }
277
278 /**
279 * Returns an HTML header and opens the body container
280 * This method will trigger an error if executed more than once without first calling
281 * the footer() method on the prior usage
282 * This is meant to be utilized within layouts, not views (but will work in either)
283 *
284 * @param array $args Options: (array) metaheader, (array) meta, favicon, animatedFavicon, (string/array)
icon,
285 * head, showBrowserBars
286 * @return string
287 */
288 public function header(array $args) {
289 if (!$this->_bodyOpen) {
290 $this->_bodyOpen = true;
291 extract($args);
292 if (!$this->_docTypeDeclaration) {
293 $this->setDocType('html5');
294 }
295 $return = $this->_docTypeDeclaration
296 . PHP_EOL . '<html xmlns="http://www.w3.org/1999/xhtml">'
297 . PHP_EOL . '<head>'
298 . PHP_EOL . '<title>' . $title . '</title>';
299
300 if (!isset($metaheader['Content-Type'])) {
301 $metaheader['Content-Type'] = ($this->_isHtml5 ? 'text/html' : 'application/xhtml+xml')
302 . '; charset=utf-8';
303 }
304 foreach ($metaheader as $name => $content) {
305 $return .= PHP_EOL . '<meta http-equiv="' . $name . '" content="' . $content . '" />';
306 }
307
308 if ($this->_isHtml5 && empty($showBrowserBars)) { //makes iOS utilize fullscreen
309 $meta['apple-mobile-web-app-capable'] = 'yes';
310 }
311 $meta['generator'] = 'Vork 3.00';
312 foreach ($meta as $name => $content) {
313 $return .= PHP_EOL . '<meta name="' . $name . '" content="' . $content . '" />';
314 }
315
316 if (isset($favicon)) {
317 $return .= PHP_EOL . '<link rel="shortcut icon" href="' . $favicon . '" type="image/x-icon" />';
318 }
319 if (isset($animatedFavicon)) {
320 $return .= PHP_EOL . '<link rel="icon" href="' . $animatedFavicon . '" type="image/gif" />';
321 }
322 if (isset($icon)) {
323 if (!is_array($icon)) {
324 $icon[144] = $icon;
325 }
326 krsort($icon);
327 $largestIcon = current($icon);
328 foreach ($icon as $size => $iconImage) {
329 $return .= PHP_EOL . '<link rel="apple-touch-icon-precomposed" href="' . $iconImage
330 . '" sizes="' . $size . 'x' . $size . '">';
331 }
332 $return .= PHP_EOL . '<link rel="apple-touch-icon-precomposed" href="' . $largestIcon . '">';
333 }
334
335 if ($this->vorkHead) { //used internally by Vork tools
336 foreach ($this->vorkHead as $container => $objArray) { //works only for inline code, not external
files
337 $return .= PHP_EOL . $this->$container(implode(PHP_EOL, $objArray));
338 }
339 }
340
341 if ($this->_isHtml5 && is::mobile() && empty($showAddressBar)) {
342 $hideAddressBarJs = 'window.addEventListener("load", function(e) {
343 setTimeout(function() {
344 window.scrollTo(0, 1);
345 }, 1);
346 }, false);';
347 $return .= PHP_EOL . $this->jsInline($hideAddressBarJs);
348 }
349 $containers = array('css', 'cssInline', 'js', 'jsInline', 'jqueryTheme');
350 foreach ($containers as $container) {
351 if (isset($$container)) {
352 $return .= PHP_EOL . $this->$container($$container);
353 }
354 }
355
356 if (isset($head)) {
357 $return .= PHP_EOL . (is_array($head) ? implode(PHP_EOL, $head) : $head);
358 }
359
360 $return .= PHP_EOL . '</head>' . PHP_EOL . '<body>';
361 return $return;
362 } else {
363 $errorMsg = 'Invalid usage of ' . __METHOD__ . '() - the header has already been returned';
364 trigger_error($errorMsg, E_USER_NOTICE);
365 }
366 }
367
368 /**
369 * Returns an HTML footer and optional Google Analytics
370 * This method will trigger an error if executed without first calling the header() method
371 * This is meant to be utilized within layouts, not views (but will work in either)
372 *
373 * @param array $args
374 * @return string
375 */
376 public function footer(array $args = array()) {
377 if ($this->_bodyOpen) {
378 $this->_bodyOpen = false;
379 $return = '</body></html>';
380
381 if (isset($args['GoogleAnalytics'])) {
382 $return = $this->jsInline('var _gaq = _gaq || []; _gaq.push(["_setAccount", "'
383 . $args['GoogleAnalytics'] . '"]); _gaq.push(["_trackPageview"]); (function() {
384 var ga = document.createElement("script"); ga.type = "text/javascript"; ga.async = true;
385 ga.src = ("https:" == document.location.protocol ? "https://ssl" : "http://www") +
".google-analytics.com/ga.js";
386 (document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(ga);'
387 . '})();') . $return;
388 }
389
390 if ($this->eof) {
391 if (!is_array($this->eof)) {
392 $this->eof = array($this->eof);
393 }
394 $return = implode($this->eof) . $return;
395 }
396 return $return;
397 } else {
398 $errorMsg = 'Invalid usage of ' . __METHOD__ . '() - header() has not been called';
399 trigger_error($errorMsg, E_USER_NOTICE);
400 }
401 }
402
403 /**
404 * Establishes a basic set of JavaScript tools, just echo $html->jsTools() before any JavaScript code that
405 * will use the tools.
406 *
407 * This method will only operate from the first occurrence in your code, subsequent calls will not output
anything
408 * but you should add it anyway as it will make sure that your code continues to work if you later remove a
409 * previous call to jsTools.
410 *
411 * Tools provided:
412 *
413 * dom() method is a direct replacement for document.getElementById() that works in all JS-capable
414 * browsers Y2k and newer.
415 *
416 * vork object - defines a global vork storage space; use by appending your own properties, eg.:
vork.widgetCount
417 *
418 * @param Boolean $noJsWrapper set to True if calling from within a $html->jsInline() wrapper
419 * @return string
420 */
421 public function jsTools($noJsWrapper = false) {
422 return $this->addSnippetToHead('jsInline', $this->jsInlineSingleton("var vork = function() {}
423 var dom = function(id) {
424 if (typeof document.getElementById != 'undefined') {
425 dom = function(id) {return document.getElementById(id);}
426 } else if (typeof document.all != 'undefined') {
427 dom = function(id) {return document.all[id];}
428 } else {
429 return false;
430 }
431 return dom(id);
432 }", $noJsWrapper));
433 }
434
435 /**
436 * Load a JavaScript library via Google's AJAX API
437 * http://code.google.com/apis/ajaxlibs/documentation/
438 *
439 * Version is optional and can be exact (1.8.2) or just version-major (1 or 1.8)
440 *
441 * Usage:
442 * echo $html->jsLoad('jquery');
443 * echo $html->jsLoad(array('yui', 'mootools'));
444 * echo $html->jsLoad(array('yui' => 2.7, 'jquery', 'dojo' => '1.3.1', 'scriptaculous'));
445 *
446 * //You can also use the Google API format JSON-decoded in which case version is required & name must be
lowercase
447 * $jsLibs = array(array('name' => 'mootools', 'version' => 1.2, 'base_domain' => 'ditu.google.cn'),
array(...));
448 * echo $html->jsLoad($jsLibs);
449 *
450 * @param mixed $library Can be a string, array(str1, str2...) or , array(name1 => version1, name2 =>
version2...)
451 * or JSON-decoded Google API syntax array(array('name' => 'yui', 'version' => 2),
array(...))
452 * @param mixed $version Optional, int or str, this is only used if $library is a string
453 * @param array $options Optional, passed to Google "optionalSettings" argument, only used if $library == str
454 * @return str
455 */
456 public function jsLoad($library, $version = null, array $options = array()) {
457 $versionDefaults = array('swfobject' => 2, 'yui' => 2, 'ext-core' => 3, 'mootools' => 1.2);
458 if (!is_array($library)) { //jsLoad('yui')
459 $library = strtolower($library);
460 if (!$version) {
461 $version = (!isset($versionDefaults[$library]) ? 1 : $versionDefaults[$library]);
462 }
463 $library = array('name' => $library, 'version' => $version);
464 $library = array(!$options ? $library : array_merge($library, $options));
465 } else {
466 foreach ($library as $key => $val) {
467 if (!is_array($val)) {
468 if (is_int($key)) { //jsLoad(array('yui', 'prototype'))
469 $val = strtolower($val);
470 $version = (!isset($versionDefaults[$val]) ? 1 : $versionDefaults[$val]);
471 $library[$key] = array('name' => $val, 'version' => $version);
472 } else if (!is_array($val)) { // //jsLoad(array('yui' => '2.8.0r4', 'prototype' => 1.6))
473 $library[$key] = array('name' => strtolower($key), 'version' => $val);
474 }
475 }
476 }
477 }
478 $url = $this->_protocol . 'www.google.com/jsapi';
479 if (!isset($this->_includedFiles['js'][$url])) { //autoload library
480 $this->_includedFiles['js'][$url] = true;
481 $url .= '?autoload=' . urlencode(json_encode(array('modules' => array_values($library))));
482 $return = $this->js($url);
483 } else { //load inline
484 foreach ($library as $lib) {
485 $js = 'google.load("' . $lib['name'] . '", "' . $lib['version'] . '"';
486 if (count($lib) > 2) {
487 unset($lib['name'], $lib['version']);
488 $js .= ', ' . json_encode($lib);
489 }
490 $jsLoads[] = $js . ');';
491 }
492 $return = $this->jsInline(implode(PHP_EOL, $jsLoads));
493 }
494 return $return;
495 }
496
497 /**
498 * Takes an array of key-value pairs and formats them in the syntax of HTML-container properties
499 *
500 * @param array $properties
501 * @return string
502 */
503 public static function formatProperties(array $properties) {
504 $return = array();
505 foreach ($properties as $name => $value) {
506 $return[] = $name . '="' . get::htmlentities($value) . '"';
507 }
508 return implode(' ', $return);
509 }
510
511 /**
512 * Creates an anchor or link container
513 *
514 * @param array $args
515 * @return string
516 */
517 public function anchor(array $args) {
518 if (!isset($args['text']) && isset($args['href'])) {
519 $args['text'] = $args['href'];
520 }
521 if (!isset($args['title']) && isset($args['text'])) {
522 $args['title'] = $args['text'];
523 }
524 if (isset($args['title'])) {
525 $args['title'] = str_replace(array("\n", "\r"), ' ', strip_tags($args['title']));
526 }
527 $return = '';
528 if (isset($args['ajax'])) {
529 $return = $this->jsSingleton('/js/ajax.js');
530 $onclick = "return ajax.load('" . $args['ajax'] . "', this.href);";
531 $args['onclick'] = (!isset($args['onclick']) ? $onclick : $args['onclick'] . '; ' . $onclick);
532 unset($args['ajax']);
533 }
534 if (isset($args['target'])) {
535 if ($args['target'] == '_auto') {
536 if (isset($args['href']) && strpos($args['href'], '://')
537 && !preg_match('#^..?tps?\:\/\/.*' . get::$config->SITE_DOMAIN . '(\/.*)?$#i', $args['href']))
{
538 $args['target'] = '_blank';
539 }
540 }
541 if ($args['target'] == '_blank' && $this->xhtmlMode) {
542 $onclick = 'window.open(this.href); return false;';
543 $args['onclick'] = (!isset($args['onclick']) ? $onclick : $args['onclick'] . '; ' . $onclick);
544 }
545 if (isset($onclick) || $args['target'] == '_auto') {
546 unset($args['target']);
547 }
548 }
549 $text = (isset($args['text']) ? $args['text'] : null);
550 unset($args['text']);
551 return $return . '<a ' . self::formatProperties($args) . '>' . $text . '</a>';
552 }
553
554 /**
555 * Shortcut to access the anchor method
556 *
557 * @param str $href
558 * @param str $text
559 * @param array $args
560 * @return str
561 */
562 public function link($href, $text = null, array $args = array()) {
563 if (strpos($href, 'http') !== 0) {
564 $href = $this->_linkPrefix . $href;
565 }
566 $args['href'] = $href;
567 if ($text !== null) {
568 $args['text'] = $text;
569 }
570 return $this->anchor($args);
571 }
572
573 /**
574 * Returns an image in accessible-XHTML syntax
575 *
576 * @param array $args
577 * @return string
578 */
579 public function img(array $args) {
580 $args['alt'] = (isset($args['alt']) ? str_replace(array("\n", "\r"), ' ', strip_tags($args['alt'])) :
'');
581 return '<img ' . self::formatProperties($args) . ' />';
582 }
583
584 /**
585 * Convenience function to simplify access the img() helper method
586 *
587 * @param string $src
588 * @param int $width
589 * @param int $height
590 * @param string $alt
591 * @param array $args
592 * @return string
593 */
594 public function image($src, $width = null, $height = null, $alt = '', array $args = array()) {
595 $args['src'] = $src;
596 $options = array('width', 'height', 'alt');
597 foreach ($options as $option) {
598 if ($$option) {
599 $args[$option] = $$option;
600 }
601 }
602 return $this->img($args);
603 }
604
605 /**
606 * Adds PHP syntax highlighting - if no PHP-open <? tag is found the entire string gets treated as PHP
607 *
608 * @param string $str
609 * @return string
610 */
611 public function phpcode($str) {
612 if (strpos($str, '<?') === false) {
613 $return = substr(strstr(highlight_string("<?php\n" . $str, true), '<br />'), 6);
614 if (substr($return, 0, 7) == '</span>') {
615 $return = '<code><span style="color: #000000">' . substr($return, 7);
616 } else {
617 $return = '<code><span style="color: #0000BB">' . $return;
618 }
619 } else {
620 $return = highlight_string($str, true);
621 }
622 return $return;
623 }
624
625 /**
626 * Wrapper display computer-code samples
627 *
628 * @param str $str
629 * @return str
630 */
631 public function code($str) {
632 return '<code>' . str_replace(' ', ' ', nl2br(get::htmlentities($str))) . '</code>';
633 }
634
635 /**
636 * Creates a list from an array with automatic nesting and linking.
637 * If the keys are URLs then the elements will be linked; be sure that elements to remain unlinked have
numeric keys
638 * Note: the word "list" is a reserved word in PHP, so thus the name "linkList"
639 *
640 * @param array $links
641 * @param string $listType Optional, if ommitted then an unordered (ul) list will be returned, if an empty
string or
642 * Bool-false is used then no list-wrapper is used (do not mix this with nested
lists)
643 * @return string
644 */
645 public function linkList(array $links, $listType = 'ul', $linkArgs = array()) {
646 $return = ($listType ? '<' . $listType . '>' : '');
647 foreach ($links as $url => $title) {
648 $class = array('class' => ($this->alternator() ? 'odd' : 'even'));
649 $return .= $this->li(!is_int($url) ? $this->link($url, $title, $linkArgs) :
650 (is_array($title) ? $this->linkList($title, $listType) : $title),
$class);
651 }
652 $return .= ($listType ? '</' . $listType . '>' : '');
653 return $return;
654 }
655
656 /**
657 * Display a definition list
658 *
659 * @param array $definitions Array of key-val definitions, both val can be a string or an array of
descriptions
660 * if a "dt" key exists within the array-val then it will be used as the DT value
instead
661 * of the array-key. The DT value can be either a string or an array of strings.
662 * @return string
663 */
664 public function dl(array $definitions) {
665 foreach ($definitions as $term => $desc) {
666 if (is_array($desc) && isset($desc['dt'])) {
667 $term = (is_array($desc['dt']) ? implode('</dt>' . PHP_EOL . '<dt>', $desc['dt']) : $desc['dt']);
668 unset($desc['dt']);
669 }
670 $return[] = '<dl class="' . ($this->alternator() ? 'odd' : 'even') . '">';
671 $return[] = '<dt>' . $term . '</dt>';
672 $return[] = '<dd>' . (!is_array($desc) ? $desc : implode('</dd>' . PHP_EOL . '<dd>', $desc)) .
'</dd>';
673 $return[] = '</dl>';
674 }
675 return implode(PHP_EOL, $return);
676 }
677
678 /**
679 * Creates a "breadcrumb trail" of links
680 *
681 * @param array $links
682 * @param string $delimiter Optional, greater-than sign is used if ommitted
683 * @return string
684 */
685 public function crumbs(array $links, $delimiter = ' >> ') {
686 if ($links) {
687 foreach ($links as $url => $title) {
688 $return[] = (!is_int($url) ? $this->link($url, $title) : get::htmlentities($title));
689 }
690 return implode($delimiter, $return);
691 }
692 }
693
694 /**
695 * Create an embedded Flash movie
696 *
697 * @param string $filename
698 * @param array $args
699 * @return string
700 */
701 public function flash($filename, array $args = array()) {
702 $args['object']['type'] = 'application/x-shockwave-flash';
703 $args['params']['movie'] = $args['object']['data'] = $filename;
704 $args['params']['wmode'] = (!isset($args['wmode']) ? 'opaque' : $args['wmode']);
705
706 $dimensions = array('height', 'width');
707 foreach ($dimensions as $dimension) {
708 if (isset($args[$dimension])) {
709 $args['params'][$dimension] = $args['object'][$dimension] = $args[$dimension];
710 }
711 }
712
713 $objectProperties = array('id', 'class', 'style');
714 foreach ($objectProperties as $property) {
715 if (isset($args[$property])) {
716 $args['object'][$property] = $args[$property];
717 }
718 }
719
720 $return = '<object ' . self::formatProperties($args['object']) . '>';
721 foreach ($args['params'] as $key => $val) {
722 $return .= PHP_EOL . '<param name="' . $key . '" ' . 'value="' . $val . '" />';
723 }
724
725 if (!isset($args['noFlash'])) {
726 $args['noFlash'] = 'Flash file is missing or Flash plugin is not installed';
727 }
728 $return .= PHP_EOL . $args['noFlash'] . '</object>';
729 return $return;
730 }
731
732 /**
733 * Embeds a PDF file
734 *
735 * @param string $filename
736 * @param array $args
737 * @return string
738 */
739 public function pdf($filename, array $args = array()) {
740 $defaults = array('height' => 400, 'width' => 400, 'noPdf' => $this->link($filename, 'Download PDF'));
741 $urlParams = array_diff_key($args, $defaults);
742 $urlParams = array_merge(array('navpanes' => 0, 'toolbar' => 0), $urlParams); //defaults
743 if (isset($urlParams['search'])) {
744 $urlParams['search'] = '"' . urlencode($urlParams['search']) . '"';
745 }
746 if (isset($urlParams['fdf'])) { //fdf must be last in the URL
747 $fdf = $urlParams['fdf'];
748 unset($urlParams['fdf']);
749 $urlParams['fdf'] = $fdf;
750 }
751 $filename .= '#' . str_replace('%2B', ' ', http_build_query($urlParams, '', '&'));
752 $args = array_merge($defaults, $args);
753 $return = '<object type="application/pdf" data="' . $filename
754 . '" width="' . $args['width'] . '" height="' . $args['height'] . '">'
755 . '<param name="src" value="' . $filename . '" />' . $args['noPdf'] . '</object>';
756 return $return;
757 }
758
759 public function video($filename, array $args = array()) {
760 $args['src'] = $filename;
761 if (!isset($args['autostart'])) {
762 $args['autostart'] = 'false';
763 }
764 $return = '<object id="MediaPlayer" classid="CLSID:22D6F312-B0F6-11D0-94AB-0080C74C7E95"
type="application/x-oleobject">';
765 foreach ($args as $key => $val) {
766 $return .= '<param name="' . $key . '" value="' . $val . '" />';
767 }
768 $return .= '</object>';
769 return $return;
770 }
771
772 /**
773 * Will return true if the number passed in is even, false if odd.
774 *
775 * @param int $number
776 * @return boolean
777 */
778 public function isEven($number) {
779 return (Boolean) ($number % 2 == 0);
780 }
781
782 /**
783 * Internal incrementing integar for the alternator() method
784 * @var int
785 */
786 private $alternator = 1;
787
788 /**
789 * Returns an alternating Boolean, useful to generate alternating background colors
790 * Eg.:
791 * $colors = array(true => 'gray', false => 'white');
792 * echo '<div style="background: ' . $colors[$html->alternator()] . ';">...</div>'; //gray background
793 * echo '<div style="background: ' . $colors[$html->alternator()] . ';">...</div>'; //white background
794 * echo '<div style="background: ' . $colors[$html->alternator()] . ';">...</div>'; //gray background
795 *
796 * @return Boolean
797 */
798 public function alternator() {
799 return $this->isEven(++$this->alternator);
800 }
801
802 /**
803 * Converts a string to its ascii equivalent
804 *
805 * @param str $str
806 * @return str
807 */
808 public function str2ascii($str) {
809 $ascii = '';
810 $strLen = strlen($str);
811 for ($i = 0; $i < $strLen; $i++) {
812 $ascii .= '&#' . ord($str[$i]) . ';';
813 }
814 return $ascii;
815 }
816
817 /**
818 * Assures a unique ID is used for all the IDs used in the email() method
819 * @var int
820 */
821 private $_emailCounter = 0;
822
823 /**
824 * Returns a spam-resistant email link
825 *
826 * @param str $email Must be a valid email address
827 * @return str
828 */
829 public function email($email) {
830 $var = (!$this->_emailCounter ? 'var ' : '');
831 $emailHalves = explode('@', $email);
832 if (!isset($emailHalves[1])) {
833 return $email;
834 }
835 $emailDomainParts = explode('.', $emailHalves[1]);
836 if (!$this->_emailCounter) {
837 $initJs = $this->addSnippetToHead('jsInline', 'var doar, noSpm; var doarlink = function(doartext,
noSpm) {
838 doartext.style.cursor = "pointer";
839 doartext.onclick = function() {window.onerror = function() {return true;}; '
840 . 'window.location = "mai" + "lto:" + noSpm.replace("@", "@");};
841 doartext.onmouseover = function() {doartext.style.textDecoration = "underline";}
842 doartext.onmouseout = function() {doartext.style.textDecoration = "none";}
843 }');
844 }
845 $id = ++$this->_emailCounter;
846 $noScript = $emailHalves[0] . ' -@- ' . implode('.', $emailDomainParts);
847 if (!$this->_isHtml5) {
848 $noScript = '<div style="display: inline;">' . $noScript . '</div>';
849 }
850 return '<span id="doar' . $id . '" class="textLink"></span><noscript>' . $noScript . '</noscript>'
851 . $this->jsInline($this->jsTools(true) . (isset($initJs) ? $initJs : '') . '
852 doar = dom("doar' . $id . '");
853 if (doar) {
854 noSpm = "' . $emailHalves[0] . '";
855 noSpm += "@";
856 noSpm += "' . implode('." + "', $emailDomainParts) . '";
857 doar.innerHTML = "<span id=\"doartext' . $id . '\">" + noSpm + "</span>";
858 doarlink(dom("doartext' . $id . '"), noSpm);
859 }');
860 }
861
862 /**
863 * Returns a list of notifications if there are any - similar to the Flash feature of Ruby on Rails
864 *
865 * @param mixed $messages String or an array of strings
866 * @param string $class
867 * @return string Returns null if there are no notifications to return
868 */
869 public function getNotifications($messages, $class = 'errormessage') {
870 if ($messages) {
871 return $this->div((is_array($messages) ? implode('<br />', $messages) : $messages),
compact('class'));
872 }
873 }
874
875 /**
876 * Formats a timestamp in human-readable proximity-since/until format
877 *
878 * @param int $ts Timestamp or a date/time that is in a format that can be cast into a timestamp
879 * @return string
880 */
881 public function howLongAgo($ts) {
882 $now = time();
883 if (!is_numeric($ts)) {
884 $ts = strtotime($ts);
885 }
886 $ago = ($now > $ts ? 'ago' : 'from now');
887 $secondsAgo = abs($now - $ts);
888 if ($secondsAgo < 5) {
889 $return = 'just now';
890 } else if ($secondsAgo < 91) { //5 to 90-seconds
891 $return = $secondsAgo . ' seconds ' . $ago;
892 } else if ($secondsAgo < 5400) { //2-90 minutes
893 $return = round($secondsAgo / 60) . ' minutes ' . $ago;
894 } else if ($secondsAgo < 86400) { //2-24 hours
895 $return = round($secondsAgo / 3600) . ' hours ' . $ago;
896 } else if ($secondsAgo < 172800) { //24-48 hours
897 $return = ($now > $ts ? 'yesterday' : 'tomorrow');
898 } else if ($secondsAgo < 31536000) { //up to 1-year
899 $return = round($secondsAgo / 86400) . ' days ' . $ago;
900 } else { //1+ year
901 $return = date('M. j, Y', $ts);
902 }
903 return $return;
904 }
905
906 /**
907 * Results of last usage of maxlength()
908 * @var boolean
909 */
910 public $withinMaxLength;
911
912 /**
913 * Returns a string no longer than a fixed length
914 * If the original string exceeds the set length then it is trimmed and then $append string is appended to it
915 *
916 * @param string $str
917 * @param int $len
918 * @param string $append Optional, defaults to ...
919 * @return string
920 */
921 public function maxlength($str, $len, $append = '...') {
922 $strLen = strlen($str);
923 $this->withinMaxLength = ($strLen <= $len);
924 if (!$this->withinMaxLength) {
925 $appendLen = strlen($append);
926 $str = substr($str, 0, ($len - $appendLen));
927 $str = substr($str, 0, strrpos($str, ' ')) . $append;
928 }
929 return $str;
930 }
931
932 /**
933 * Formats price, including reducing to two-digit precision
934 *
935 * @param mixed $price
936 * @return float (values under 1,000) or string (values 1,000+) - this is due to the comma-separator
937 */
938 public function price($price) {
939 return number_format(round((float) $price, 2), 2);
940 }
941
942 /**
943 * Formats telephone numbers containing 9 or 10 numeric digits, all other numbers pass through unmodified
944 *
945 * @param mixed $telephone
946 * @return string
947 */
948 public function telephone($telephone) {
949 $tel = preg_replace('/\D/', '', $telephone);
950 $telLength = strlen($tel);
951 switch ($telLength) {
952 case 11:
953 if (substring($tel, 0, 1) == '1') { //1-800-555-1212 format
954 $tel = substring($tel, 1);
955 } else {
956 break;
957 }
958 case 10: // N. American format
959 $telephone = '(' . substr($tel, 0, 3) . ') ' . substr($tel, 3, 3) . '-' . substr($tel, 6);
960 break;
961 case 9: // S. American/Mid-East landline format
962 $telephone = '(' . substr($tel, 0, 2) . ') ' . substr($tel, 2, 3) . '-' . substr($tel, 5);
963 break;
964 }
965 return $telephone;
966 }
967 }