1: <?php
2:
3: /**
4: * This file contains the base class cHTML for all HTML classes.
5: *
6: * @package Core
7: * @subpackage GUI_HTML
8: *
9: * @author Simon Sprankel
10: * @copyright four for business AG <www.4fb.de>
11: * @license http://www.contenido.org/license/LIZENZ.txt
12: * @link http://www.4fb.de
13: * @link http://www.contenido.org
14: */
15:
16: defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.');
17:
18: /**
19: * Base class for all CONTENIDO HTML classes
20: *
21: * @package Core
22: * @subpackage GUI_HTML
23: */
24: class cHTML {
25:
26: /**
27: * Id attribute counter, used to generate unique values for id-attributes
28: *
29: * @var int
30: */
31: protected static $_idCounter = 0;
32:
33: /**
34: * Flag to generate XHTML valid elements
35: *
36: * @var bool
37: */
38: protected static $_generateXHTML;
39:
40: /**
41: * Storage of the open SGML tag template
42: *
43: * @var string
44: */
45: protected $_skeletonOpen = '<%s%s>';
46:
47: /**
48: * Storage of a single SGML tag template
49: *
50: * @var string
51: */
52: protected $_skeletonSingle;
53:
54: /**
55: * Storage of the close SGML tag
56: *
57: * @var string
58: */
59: protected $_skeletonClose = '</%s>';
60:
61: /**
62: * Defines which tag to use
63: *
64: * @var string
65: */
66: protected $_tag;
67:
68: /**
69: * Defines the style definitions
70: *
71: * @var array
72: */
73: protected $_styleDefs = array();
74:
75: /**
76: * Defines all scripts which are required by the current element
77: *
78: * @var array
79: */
80: protected $_requiredScripts = array();
81:
82: /**
83: * Defines if the current tag is a contentless tag
84: *
85: * @var bool
86: */
87: protected $_contentlessTag = true;
88:
89: /**
90: * Defines which JS events contain which scripts
91: *
92: * @var array
93: */
94: protected $_eventDefinitions = array();
95:
96: /**
97: * Style definitions
98: *
99: * @var array
100: */
101: protected $_styleDefinitions = array();
102:
103: /**
104: * Attributes
105: *
106: * @var array
107: */
108: protected $_attributes;
109:
110: /**
111: * The content itself
112: *
113: * @var string
114: */
115: protected $_content;
116:
117: /**
118: * Constructor to create an instance of this class.
119: *
120: * @param array $attributes [optional]
121: * Associative array of table tag attributes
122: */
123: public function __construct(array $attributes = NULL) {
124: if (!is_null($attributes)) {
125: $this->setAttributes($attributes);
126: }
127:
128: if (self::$_generateXHTML === NULL) {
129: if (getEffectiveSetting('generator', 'xhtml', 'false') == 'true') {
130: self::$_generateXHTML = true;
131: } else {
132: self::$_generateXHTML = false;
133: }
134: }
135:
136: if (self::$_generateXHTML === true) {
137: $this->_skeletonSingle = '<%s%s />';
138: } else {
139: $this->_skeletonSingle = '<%s%s>';
140: }
141:
142: if (isset($attributes['id']) === false) {
143: $this->advanceID();
144: }
145: }
146:
147: /**
148: * Setter for static $_generateXHTML property
149: *
150: * @param bool $value
151: */
152: public static function setGenerateXHTML($value) {
153: self::$_generateXHTML = (bool) $value;
154: }
155:
156: /**
157: * Advances to the next ID available in the system.
158: *
159: * This function is useful if you need to use HTML elements
160: * in a loop, but don't want to re-create new objects each time.
161: *
162: * @return cHTML
163: * $this for chaining
164: */
165: public function advanceID() {
166: self::$_idCounter++;
167: return $this->updateAttribute('id', 'm' . self::$_idCounter);
168: }
169:
170: /**
171: * Returns the current ID
172: *
173: * @return string
174: * current ID
175: */
176: public function getID() {
177: return $this->getAttribute('id');
178: }
179:
180: /**
181: * Sets the HTML tag to $tag
182: *
183: * @param string $tag
184: * The new tag
185: * @return cHTML
186: * $this for chaining
187: */
188: public function setTag($tag) {
189: $this->_tag = $tag;
190:
191: return $this;
192: }
193:
194: /**
195: * Sets the alt and title attributes
196: *
197: * Sets the "alt" and "title" tags. Usually, "alt" is used
198: * for accessibility and "title" for mouse overs.
199: *
200: * The title attribute is not overwritten if there is already a set value for it.
201: *
202: * To set the text for all browsers for mouse over, set "alt"
203: * and "title". IE behaves incorrectly and shows "alt" on
204: * mouse over. Mozilla browsers only show "title" as mouse over.
205: *
206: * @param string $alt
207: * Text to set as the "alt" and "title" attribute
208: * @param bool $setTitle [optional]
209: * Whether title attribute should be set, too (optional, default: true)
210: * @return cHTML
211: * $this for chaining
212: */
213: public function setAlt($alt, $setTitle = true) {
214: $attributes = array('alt' => $alt, 'title' => $alt);
215:
216: if ($setTitle === false || isset($this->_attributes['title']) === true) {
217: unset($attributes['title']);
218: }
219:
220: return $this->updateAttributes($attributes);
221: }
222:
223: /**
224: * Sets the ID class
225: *
226: * @param string $id
227: * Text to set as the "id"
228: * @return cHTML
229: * $this for chaining
230: */
231: public function setID($id) {
232: if ($id != "") {
233: return $this->updateAttribute('id', $id);
234: } else {
235: return $this;
236: }
237:
238: }
239:
240: /**
241: * Sets the CSS class
242: *
243: * @param string $class
244: * Text to set as the "class" attribute
245: * @return cHTML
246: * $this for chaining
247: */
248: public function setClass($class) {
249: if ($class != "") {
250: return $this->updateAttribute('class', $class);
251: } else {
252: return $this;
253: }
254: }
255:
256: /**
257: * Sets the CSS style
258: *
259: * @param string $style
260: * Text to set as the "style" attribute
261: * @return cHTML
262: * $this for chaining
263: */
264: public function setStyle($style) {
265: return $this->updateAttribute('style', $style);
266: }
267:
268: /**
269: * Adds an "on???" javascript event handler
270: *
271: * example:
272: * $item->setEvent('change', 'document.forms[0].submit');
273: *
274: * @param string $event
275: * Type of the event, e.g. "change" for "onchange"
276: * @param string $action
277: * Function or action to call (JavaScript Code)
278: * @return cHTML
279: * $this for chaining
280: */
281: public function setEvent($event, $action) {
282: if (substr($event, 0, 2) !== 'on' && $event != 'disabled') {
283: return $this->updateAttribute('on' . $event, conHtmlSpecialChars($action));
284: } else {
285: return $this->updateAttribute($event, conHtmlSpecialChars($action));
286: }
287: }
288:
289: /**
290: * Removes an event handler
291: *
292: * example:
293: * $item->unsetEvent('change');
294: *
295: * @param string $event
296: * Type of the event
297: * @return cHTML
298: * $this for chaining
299: */
300: public function unsetEvent($event) {
301: if (substr($event, 0, 2) !== 'on') {
302: return $this->removeAttribute('on' . $event);
303: } else {
304: return $this->removeAttribute($event);
305: }
306: }
307:
308: /**
309: * Fills the open SGML tag skeleton
310: *
311: * fillSkeleton fills the SGML opener tag with the
312: * specified attributes. Attributes need to be passed
313: * in the stringyfied variant.
314: *
315: * @param string $attributes
316: * Attributes to set
317: * @return string
318: * filled SGML opener skeleton
319: */
320: public function fillSkeleton($attributes) {
321: if ($this->_contentlessTag == true) {
322: return sprintf($this->_skeletonSingle, $this->_tag, $attributes);
323: } else {
324: return sprintf($this->_skeletonOpen, $this->_tag, $attributes);
325: }
326: }
327:
328: /**
329: * Fills the close skeleton
330: *
331: * @return string
332: * filled SGML closer skeleton
333: */
334: public function fillCloseSkeleton() {
335: return sprintf($this->_skeletonClose, $this->_tag);
336: }
337:
338: /**
339: * Appends the given style definition to the HTML element.
340: * Example usage:
341: * $element->appendStyleDefinition('margin', '5px');
342: *
343: * @param string $property
344: * the property name, e.g. 'margin'
345: * @param string $value
346: * the value of the property, e.g. '5px'
347: * @return cHTML
348: * $this for chaining
349: */
350: public function appendStyleDefinition($property, $value) {
351: if (substr($value, -1) === ';') {
352: $value = substr($value, 0, strlen($value) - 1);
353: }
354: $this->_styleDefinitions[$property] = $value;
355:
356: return $this;
357: }
358:
359: /**
360: * Appends the given style definitions to the HTML element.
361: * Example usage:
362: * $element->appendStyleDefinitions(array(
363: * 'margin' => '5px',
364: * 'padding' => '0'
365: * ));
366: *
367: * @param string $styles
368: * the styles to append
369: * @return cHTML
370: * $this for chaining
371: */
372: public function appendStyleDefinitions(array $styles) {
373: foreach ($styles as $property => $value) {
374: $this->appendStyleDefinition($property, $value);
375: }
376:
377: return $this;
378: }
379:
380: /**
381: * Adds a required script to the current element.
382: * Anyway, scripts are not included twice.
383: *
384: * @param string $script
385: * the script to include
386: * @return cHTML
387: * $this for chaining
388: */
389: public function addRequiredScript($script) {
390: if (!is_array($this->_requiredScripts)) {
391: $this->_requiredScripts = array();
392: }
393:
394: $this->_requiredScripts[] = $script;
395: $this->_requiredScripts = array_unique($this->_requiredScripts);
396:
397: return $this;
398: }
399:
400: /**
401: * Sets the content of the object
402: *
403: * @param string|object|array $content
404: * String with the content or a cHTML object to render or an array
405: * of strings / objects.
406: * @return cHTMLContentElement
407: * $this for chaining
408: */
409: protected function _setContent($content) {
410: $this->_contentlessTag = false;
411: if (is_array($content)) {
412: // reset content
413: $this->_content = '';
414: // content is an array, so iterate over it and append the elements
415: foreach ($content as $item) {
416: if (is_object($item)) {
417: if (method_exists($item, 'render')) {
418: $this->_content .= $item->render();
419: }
420: if (count($item->_requiredScripts) > 0) {
421: $this->_requiredScripts = array_merge($this->_requiredScripts, $item->_requiredScripts);
422: }
423: } else {
424: $this->_content .= $item;
425: }
426: }
427: } else {
428: // content is an object or a string, so just set the rendered
429: // content
430: if (is_object($content)) {
431: if (method_exists($content, 'render')) {
432: $this->_content = $content->render();
433: }
434: if (count($content->_requiredScripts) > 0) {
435: $this->_requiredScripts = array_merge($this->_requiredScripts, $content->_requiredScripts);
436: }
437: } else {
438: $this->_content = $content;
439: }
440: }
441:
442: return $this;
443: }
444:
445: /**
446: * Adds the given content to the already existing content of this object.
447: *
448: * @param string|object|array $content
449: * String with the content or an object to render
450: * or an array of strings/objects.
451: * @return cHTML
452: * $this for chaining
453: */
454: protected function _appendContent($content) {
455: if (!is_string($this->_content)) {
456: $this->_content = '';
457: }
458: $this->_contentlessTag = false;
459: if (is_array($content)) {
460: // content is an array, so iterate over it and append the elements
461: foreach ($content as $item) {
462: if (is_object($item)) {
463: if (method_exists($item, 'render')) {
464: $this->_content .= $item->render();
465: }
466: } else {
467: $this->_content .= $item;
468: }
469: }
470: } else {
471: // content is an object or a string, so just append the rendered
472: // content
473: if (is_object($content)) {
474: if (method_exists($content, 'render')) {
475: $this->_content .= $content->render();
476: }
477: } else {
478: $this->_content .= $content;
479: }
480: }
481:
482: return $this;
483: }
484:
485: /**
486: * Attaches the code for an event
487: *
488: * Example to attach an onClick handler:
489: * attachEventDefinition('foo', 'onClick', 'alert("foo");');
490: *
491: * @param string $name
492: * Defines the name of the event
493: * @param string $event
494: * Defines the event (e.g. onClick)
495: * @param string $code
496: * Defines the code
497: * @return cHTML
498: * $this for chaining
499: */
500: public function attachEventDefinition($name, $event, $code) {
501: $this->_eventDefinitions[strtolower($event)][$name] = $code;
502:
503: return $this;
504: }
505:
506: /**
507: * Sets a specific attribute
508: *
509: * @param string $attributeName
510: * Name of the attribute
511: * @param string $value [optional]
512: * Value of the attribute
513: * @return cHTML
514: * $this for chaining
515: */
516: public function setAttribute($attributeName, $value = NULL) {
517: $attributeName = strtolower($attributeName);
518: if (is_null($value)) {
519: $value = $attributeName;
520: }
521: $this->_attributes[$attributeName] = $value;
522:
523: return $this;
524: }
525:
526: /**
527: * Sets the HTML attributes
528: *
529: * @param array $attributes
530: * Associative array with attributes
531: * @return cHTML
532: * $this for chaining
533: */
534: public function setAttributes(array $attributes) {
535: $this->_attributes = $this->_parseAttributes($attributes);
536:
537: return $this;
538: }
539:
540: /**
541: * Returns a valid atrributes array.
542: *
543: * @param array $attributes
544: * Associative array with attributes
545: * @return array
546: * the parsed attributes
547: */
548: protected function _parseAttributes(array $attributes) {
549: $return = array();
550: foreach ($attributes as $key => $value) {
551: if (is_int($key)) {
552: $key = $value = strtolower($value);
553: } else {
554: $key = strtolower($key);
555: }
556:
557: $return[$key] = $value;
558: }
559:
560: return $return;
561: }
562:
563: /**
564: * Removes an attribute
565: *
566: * @param string $attributeName
567: * Attribute name
568: * @return cHTML
569: * $this for chaining
570: */
571: public function removeAttribute($attributeName) {
572: if (isset($this->_attributes[$attributeName])) {
573: unset($this->_attributes[$attributeName]);
574: }
575:
576: return $this;
577: }
578:
579: /**
580: * Returns the value of the given attribute.
581: *
582: * @param string $attributeName
583: * Attribute name
584: * @return string
585: * NULL value or NULL if the attribute does not exist
586: */
587: public function getAttribute($attributeName) {
588: $attributeName = strtolower($attributeName);
589:
590: if (isset($this->_attributes[$attributeName])) {
591: return $this->_attributes[$attributeName];
592: }
593:
594: return NULL;
595: }
596:
597: /**
598: * Updates the passed attribute without changing the other existing
599: * attributes
600: *
601: * @param string $name
602: * the name of the attribute
603: * @param string $value
604: * the value of the attribute with the given name
605: * @return cHTML
606: * $this for chaining
607: */
608: public function updateAttribute($name, $value) {
609: return $this->updateAttributes(array(
610: $name => $value
611: ));
612: }
613:
614: /**
615: * Updates the passed attributes without changing the other existing
616: * attributes
617: *
618: * @param array $attributes
619: * Associative array with attributes
620: * @return cHTML
621: * $this for chaining
622: */
623: public function updateAttributes(array $attributes) {
624: $attributes = $this->_parseAttributes($attributes);
625:
626: foreach ($attributes as $key => $value) {
627: if (!is_null($value)) {
628: $this->_attributes[$key] = $value;
629: }
630: }
631:
632: return $this;
633: }
634:
635: /**
636: * Returns an HTML formatted attribute string
637: *
638: * @param array $attributes
639: * Associative array with attributes
640: * @return string
641: * Attribute string in HTML format
642: */
643: protected function _getAttrString(array $attributes) {
644: $attrString = '';
645:
646: if (!is_array($attributes)) {
647: return '';
648: }
649:
650: foreach ($attributes as $key => $value) {
651: $attrString .= ' ' . $key . '="' . $value . '"';
652: }
653:
654: return $attrString;
655: }
656:
657: /**
658: * Returns the assoc array(default) or string of attributes
659: *
660: * @param bool $returnAsString [optional]
661: * Whether to return the attributes as string
662: * @return array|string
663: */
664: public function getAttributes($returnAsString = false) {
665: if ($returnAsString) {
666: return $this->_getAttrString($this->_attributes);
667: } else {
668: return $this->_attributes;
669: }
670: }
671:
672: /**
673: * Generates the markup of the element.
674: *
675: * @return string
676: * generated markup
677: */
678: public function toHtml() {
679: // Fill style definition
680: $style = $this->getAttribute('style');
681:
682: // If the style doesn't end with a semicolon, append one
683: if (is_string($style)) {
684: $style = trim($style);
685: if (substr($style, -1) !== ';') {
686: $style .= ';';
687: }
688: }
689:
690: // append the defined styles
691: foreach ($this->_styleDefinitions as $property => $value) {
692: $style .= "$property: $value;";
693: }
694:
695: if ($style != '') {
696: $this->setStyle($style);
697: }
698:
699: foreach ($this->_eventDefinitions as $eventName => $entry) {
700: $fullCode = array();
701: foreach ($entry as $code) {
702: $fullCode[] = $code;
703: }
704: $this->setAttribute($eventName, $this->getAttribute($eventName) . implode(' ', $fullCode));
705: }
706:
707: $attributes = $this->getAttributes(true);
708: if (!empty($this->_content) || $this->_contentlessTag === false) {
709: return $this->fillSkeleton($attributes) . $this->_content . $this->fillCloseSkeleton();
710: } else {
711: // This is a single style tag
712: return $this->fillSkeleton($attributes);
713: }
714: }
715:
716: /**
717: * Alias for toHtml
718: *
719: * @return string
720: * generated markup
721: */
722: public function render() {
723: return $this->toHtml();
724: }
725:
726: /**
727: * Direct call of object as string will return it's generated markup.
728: *
729: * @return string
730: * Generated markup
731: */
732: public function __toString() {
733: return $this->render();
734: }
735:
736: /**
737: * Outputs the generated markup
738: */
739: public function display() {
740: echo $this->render();
741: }
742:
743: }
744: