1: <?php
2: /**
3: * An abstract utility class to handle Html tag rendering, as well as utilities to render
4: * pieces of HTML and CSS code. All methods are static.
5: */
6: abstract class QHtml {
7:
8: const IsVoid = true;
9:
10: // Common URL Protocols
11: /** HTTP Protocol */
12: const HTTP = 'http://';
13: /** HTTPS Protocol */
14: const HTTPS = 'https://';
15: /** FTP Protocol */
16: const FTP = 'ftp://';
17: /** SFTP Protocol */
18: const SFTP = 'sftp://';
19: /** SMB Protocol */
20: const SMB = 'smb://';
21:
22:
23: /**
24: * This faux constructor method throws a caller exception.
25: * The Css object should never be instantiated, and this constructor
26: * override simply guarantees it.
27: *
28: * @throws QCallerException
29: * @return QHtml
30: */
31: public final function __construct() {
32: throw new QCallerException('QHtml should never be instantiated. All methods and variables are publicly statically accessible.');
33: }
34:
35: /**
36: * Renders an html tag with the given attributes and inner html.
37: *
38: * If the innerHtml is detected as being wrapped in an html tag of some sort, it will attempt to format the code so that
39: * it has a structured view in a browser, with the inner html indented and on a new line in between the tags. You
40: * can turn this off by setting __MINIMIZE__, or by passing in true to $blnNoSpace.
41: *
42: * There area a few special cases to consider:
43: * - Void elements will not be formatted to avoid adding unnecessary white space since these are generally
44: * inline elements
45: * - Non-void elements always use internal newlines, even in __MINIMIZE__ mode. This is to prevent different behavior
46: * from appearing in __MINIMIZE__ mode on inline elements, because inline elements with internal space will render with space to separate
47: * from surrounding elements. Usually, this is not an issue, but in the special situations where you really need inline
48: * elements to be right up against its siblings, set $blnNoSpace to true.
49: *
50: *
51: * @param string $strTag The tag name
52: * @param null|mixed $mixAttributes String of attribute values or array of attribute values.
53: * @param null|string $strInnerHtml The html to print between the opening and closing tags. This will NOT be escaped.
54: * @param boolean $blnIsVoidElement True to print as a tag with no closing tag.
55: * @param boolean $blnNoSpace Renders with no white-space. Useful in special inline situations.
56: * @return string The rendered html tag
57: */
58: public static function RenderTag($strTag, $mixAttributes, $strInnerHtml = null, $blnIsVoidElement = false, $blnNoSpace = false) {
59: assert ('!empty($strTag)');
60: $strToReturn = '<' . $strTag;
61: if ($mixAttributes) {
62: if (is_string($mixAttributes)) {
63: $strToReturn .= ' ' . trim($mixAttributes);
64: } else {
65: // assume array
66: $strToReturn .= QHtml::RenderHtmlAttributes($mixAttributes);
67: }
68: };
69: if ($blnIsVoidElement) {
70: $strToReturn .= ' />'; // conforms to both XHTML and HTML5 for both normal and foreign elements
71: }
72: elseif ($blnNoSpace || substr (trim($strInnerHtml), 0, 1) !== '<') {
73: $strToReturn .= '>' . $strInnerHtml . '</' . $strTag . '>';
74: }
75: else {
76: // the hardcoded newlines below are important to prevent different drawing behavior in MINIMIZE mode
77: $strToReturn .= '>' . "\n" . _indent(trim($strInnerHtml)) . "\n" . '</' . $strTag . '>' . _nl();
78: }
79: return $strToReturn;
80: }
81:
82: /**
83: * Renders an input element with a label tag. Uses separate styling for the label and the input object.
84: * In particular, this gives you the option of wrapping the input with a label (which is what Bootstrap
85: * expects on checkboxes) or putting the label next to the object (which is what jQueryUI expects).
86: *
87: * Note that if you are not setting $blnWrapped, it is up to you to insert the "for" attribute into
88: * the label attributes.
89: *
90: * @param $strLabel
91: * @param $blnTextLeft
92: * @param $strAttributes
93: * @param $strLabelAttributes
94: * @param $blnWrapped
95: * @return string
96: */
97: public static function RenderLabeledInput($strLabel, $blnTextLeft, $strAttributes, $strLabelAttributes, $blnWrapped) {
98: $strHtml = trim(self::RenderTag('input', $strAttributes, null, true));
99:
100: if ($blnWrapped) {
101: if ($blnTextLeft) {
102: $strCombined = $strLabel . $strHtml;
103: } else {
104: $strCombined = $strHtml . $strLabel;
105: }
106:
107: $strHtml = self::RenderTag('label', $strLabelAttributes, $strCombined);
108: }
109: else {
110: $strLabel = trim(self::RenderTag('label', $strLabelAttributes, $strLabel));
111: if ($blnTextLeft) {
112: $strHtml = $strLabel . $strHtml;
113: } else {
114: $strHtml = $strHtml . $strLabel;
115: }
116: }
117: return $strHtml;
118: }
119:
120: /**
121: * Returns the formatted value of type <length>.
122: * See http://www.w3.org/TR/CSS1/#units for more info.
123: * @param string $strValue The number or string to be formatted to the <length> compatible value.
124: * @return string the formatted value of type <length>.
125: */
126: public final static function FormatLength($strValue) {
127: if (is_numeric($strValue)) {
128: if (0 == $strValue) {
129: if (!is_int($strValue)) {
130: $fltValue = floatval($strValue);
131: return sprintf('%s', $fltValue);
132: } else {
133: return sprintf('%s', $strValue);
134: }
135: } else {
136: if (!is_int($strValue)) {
137: $fltValue = floatval($strValue);
138: return sprintf('%spx', $fltValue);
139: } else {
140: return sprintf('%spx', $strValue);
141: }
142: }
143: } else {
144: return sprintf('%s', $strValue);
145: }
146: }
147:
148: /**
149: * Sets the given length string to the new length value.
150: * If the new length is preceded by a math operator (+-/*), then arithmetic is performed on the previous
151: * value. Returns true if the length changed.
152: * @param string $strOldLength
153: * @param string $newLength
154: * @return bool true if the length was changed
155: */
156: public static function SetLength(&$strOldLength, $newLength) {
157: if ($newLength && preg_match('#^(\+|\-|/|\*)(.+)$#',$newLength, $matches)) { // do math operation
158: $strOperator = $matches[1];
159: $newValue = $matches[2];
160: assert (is_numeric($newValue));
161: if (!$strOldLength) {
162: $oldValue = 0;
163: $oldUnits = 'px';
164: } else {
165: $oldValue = filter_var ($strOldLength, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
166: if (preg_match('/([A-Z]+|[a-z]+|%)$/', $strOldLength, $matches)) {
167: $oldUnits = $matches[1];
168: } else {
169: $oldUnits = 'px';
170: }
171: }
172:
173: switch ($strOperator) {
174: case '+':
175: $newValue = $oldValue + $newValue;
176: break;
177:
178: case '-':
179: $newValue = $oldValue - $newValue;
180: break;
181:
182: case '/':
183: $newValue = $oldValue / $newValue;
184: break;
185:
186: case '*':
187: $newValue = $oldValue * $newValue;
188: break;
189: }
190: if ($newValue != $oldValue) {
191: $strOldLength = $newValue . $oldUnits; // update returned value
192: return true;
193: } else {
194: return false; // nothing changed
195: }
196: } else { // no math operation
197: $newLength = self::FormatLength($newLength);
198:
199: if ($strOldLength !== $newLength) {
200: $strOldLength = $newLength;
201: return true;
202: } else {
203: return false;
204: }
205: }
206: }
207:
208:
209: /**
210: * Helper to add a class or classes to a pre-existing space-separated list of classes. Checks to make sure the
211: * class isn't already in the list. Returns true to indicate a change in the list.
212: *
213: * @param string $strClassList Current list of classes separated by a space
214: * @param string $strNewClasses New class to add. Could be a list separated by spaces.
215: * @return bool true if the class list was changed.
216: */
217: public static function AddClass(&$strClassList, $strNewClasses) {
218: $strNewClasses = trim($strNewClasses);
219: if (empty($strNewClasses)) return false;
220:
221: if (empty ($strClassList)) {
222: $strCurrentClasses = array();
223: }
224: else {
225: $strCurrentClasses = explode(' ', $strClassList);
226: }
227:
228: $blnChanged = false;
229: foreach (explode (' ', $strNewClasses) as $strClass) {
230: if ($strClass && !in_array ($strClass, $strCurrentClasses)) {
231: $blnChanged = true;
232: if (!empty ($strClassList)) {
233: $strClassList .= ' ';
234: }
235: $strClassList .= $strClass;
236: }
237: }
238:
239: return $blnChanged;
240: }
241:
242: /**
243: * Helper to remove a class or classes from a list of space-separated classes.
244: *
245: * @param string $strClassList class list string to search
246: * @param string $strCssNamesToRemove space separated list of names to remove
247: *
248: * @return bool true if the class list was changed
249: */
250: public static function RemoveClass(&$strClassList, $strCssNamesToRemove) {
251: $strNewCssClass = '';
252: $blnRemoved = false;
253: $strCssNamesToRemove = trim($strCssNamesToRemove);
254: if (empty($strCssNamesToRemove)) return false;
255:
256: if (empty ($strClassList)) {
257: $strCurrentClasses = array();
258: }
259: else {
260: $strCurrentClasses = explode(' ', $strClassList);
261: }
262: $strRemoveArray = explode (' ', $strCssNamesToRemove);
263:
264: foreach ($strCurrentClasses as $strCssClass) {
265: if ($strCssClass = trim($strCssClass)) {
266: if (in_array($strCssClass, $strRemoveArray)) {
267: $blnRemoved = true;
268: }
269: else {
270: $strNewCssClass .= $strCssClass . ' ';
271: }
272: }
273: }
274: if ($blnRemoved) {
275: $strClassList = trim($strNewCssClass);
276: }
277: return $blnRemoved;
278: }
279:
280: /**
281: * Many CSS frameworks use families of classes, which are built up from a base family name. For example,
282: * Bootstrap uses 'col-lg-6' to represent a column that is 6 units wide on large screens and Foundation
283: * uses 'large-6' to do the same thing. This utility removes classes that start with a particular prefix
284: * to remove whatever sizing class was specified.
285: *
286: * @param $strClassList
287: * @param $strPrefix
288: * @return bool true if the class list changed
289: */
290: public static function RemoveClassesByPrefix (&$strClassList, $strPrefix) {
291: $aRet = array();
292: $blnChanged = false;
293: if ($strClassList) foreach (explode (' ', $strClassList) as $strClass) {
294: if (strpos($strClass, $strPrefix) !== 0) {
295: $aRet[] = $strClass;
296: }
297: else {
298: $blnChanged = true;
299: }
300: }
301: $strClassList = implode (' ', $aRet);
302: return $blnChanged;
303: }
304:
305: /**
306: * Render the given attribute array for html output. Escapes html entities enclosed in values. Uses
307: * double-quotes to surround the value. Precedes the resulting text with a space character.
308: *
309: * @param array|null $attributes
310: * @return string
311: */
312: public static function RenderHtmlAttributes ($attributes) {
313: $strToReturn = '';
314: if ($attributes) {
315: foreach ($attributes as $strName=>$strValue) {
316: if ($strValue === false) {
317: $strToReturn .= (' ' . $strName);
318: } elseif (!is_null($strValue)) {
319: $strToReturn .= (' ' . $strName . '="' . htmlspecialchars($strValue, ENT_COMPAT | ENT_HTML5, QApplication::$EncodingType) . '"');
320: }
321: }
322: }
323: return $strToReturn;
324: }
325:
326:
327: /**
328: * Render the given array as a css style string. It will NOT be escaped.
329: *
330: * @param array $styles key/value array representing the styles.
331: * @return string a string suitable for including in a css 'style' property
332: */
333: public static function RenderStyles($styles) {
334: if (!$styles) return '';
335: return implode('; ', array_map(
336: function ($v, $k) { return $k . ':' . $v; },
337: $styles,
338: array_keys($styles))
339: );
340: }
341:
342: /**
343: * Returns the given string formatted as an html comment that will go on its own line.
344: * @param string $strText
345: * @param bool $blnRemoveOnMinimize
346: * @return string
347: */
348: public static function Comment($strText, $blnRemoveOnMinimize = true) {
349: if ($blnRemoveOnMinimize && QApplication::$Minimize) {
350: return '';
351: }
352: return _nl() . '<!-- ' . $strText . ' -->' . _nl();
353:
354: }
355:
356: /**
357: * Generate a URL from components. This URL can be used in the QApplication::Redirect function, or applied to
358: * an anchor tag by setting the href attribute.
359: *
360: * @param string $strLocation absolute or relative path to resource, depending on your protocol. If not needed, enter an empty string.
361: * @param array|null $queryParams key->value array of query parameters to add to the location.
362: * @param string|null $strAnchor anchor to add to the url
363: * @param string|null $strProtocol protocol if specifying a resource outside of the current server
364: * @param string|null $strServer server that the resource is on. Required if specifying a protocol.
365: * @param string|null $strUser user name if needed. Some protocols like mailto and ftp need this
366: * @param string|null $strPassword password if needed. Note that password is sent in the clear.
367: * @param string|null $intPort port if different from default
368: * @return string
369: */
370: public static function MakeUrl ($strLocation, $queryParams = null, $strAnchor = null, $strProtocol = null, $strServer = null, $strUser = null, $strPassword = null, $intPort = null) {
371: // Basic URLs that are pointing to our own server
372: $strUrl = $strLocation; // can be relative or absolute
373: if ($queryParams) {
374: $strUrl .= '?' . http_build_query($queryParams);
375: }
376: if ($strAnchor) {
377: $strUrl .= '#' . urlencode($strAnchor);
378: }
379:
380: // More complex URLs. Once you specify protocol, you will need to specify the server too.
381: if ($strProtocol) {
382: assert('!empty($strServer)');
383:
384: // We do not do any checking at this point since URLs can be complex. It is up to you to build a correct URL.
385: // If you use a protocol that expects an absolute path, you must start with a slash (http), or a relative path (mailto), leave the slash off.
386:
387: // Build server portion.
388: if ($intPort) {
389: $strServer .= ':' . $intPort;
390: }
391: if ($strUser) {
392: $strUser = rawurlencode($strUser);
393: if ($strPassword) {
394: $strUser = $strUser . ':' . rawurlencode($strPassword);
395: }
396: $strServer = $strUser . '@' . $strServer;
397: }
398: $strUrl = $strProtocol . $strServer . $strUrl;
399: }
400: return $strUrl;
401: }
402:
403: /**
404: * Returns a MailTo url.
405: *
406: * @param $strUser
407: * @param $strServer
408: * @param null $queryParams
409: * @return string
410: */
411: public static function MailToUrl ($strUser, $strServer, $queryParams = null) {
412: $strUrl = 'mailto:' . rawurlencode($strUser) . '@' . rawurlencode($strServer);
413: if ($queryParams) {
414: $strUrl .= '?' . http_build_query($queryParams, null, null, PHP_QUERY_RFC3986);
415: }
416: return $strUrl;
417: }
418:
419: /**
420: * Utility function to create a link, i.e. an "a" tag.
421: *
422: * @param string $strUrl URL to link to. Use MakeUrl or MailToUrl to create the URL.
423: * @param string $strText The inner text. This WILL be escaped.
424: * @param array $attributes Other html attributes to include in the tag
425: * @param boolean $blnHtmlEntities False to prevent encoding
426: */
427: public static function RenderLink ($strUrl, $strText, $attributes = null, $blnHtmlEntities = true) {
428: $attributes["href"] = $strUrl;
429: if ($blnHtmlEntities) {
430: $strText = QApplication::HtmlEntities($strText);
431: }
432: return self::RenderTag("a", $attributes, $strText);
433: }
434:
435: /**
436: * Renders a PHP string as HTML text. Makes sure special characters are encoded, and <br /> tags are substituted
437: * for newlines.
438: * @param $strText
439: */
440: public static function RenderString($strText) {
441: return nl2br(htmlspecialchars($strText, ENT_COMPAT | ENT_HTML5, QApplication::$EncodingType));
442: }
443:
444: }
445: