qcubed/framework

View on GitHub
includes/framework/QHtml.class.php

Summary

Maintainability
F
4 days
Test Coverage
<?php
    /**
     * An abstract utility class to handle Html tag rendering, as well as utilities to render
     * pieces of HTML and CSS code.  All methods are static.
     */
    abstract class QHtml {

        const IsVoid = true;

        // Common URL Protocols
        /** HTTP Protocol */
        const HTTP = 'http://';
        /** HTTPS Protocol */
        const HTTPS = 'https://';
        /** FTP Protocol */
        const FTP = 'ftp://';
        /** SFTP Protocol */
        const SFTP = 'sftp://';
        /** SMB Protocol */
        const SMB = 'smb://';


        /**
         * This faux constructor method throws a caller exception.
         * The Css object should never be instantiated, and this constructor
         * override simply guarantees it.
         *
         * @throws QCallerException
         * @return QHtml
         */
        public final function __construct() {
            throw new QCallerException('QHtml should never be instantiated.  All methods and variables are publicly statically accessible.');
        }

        /**
         * Renders an html tag with the given attributes and inner html.
         *
         * If the innerHtml is detected as being wrapped in an html tag of some sort, it will attempt to format the code so that
         * it has a structured view in a browser, with the inner html indented and on a new line in between the tags. You
         * can turn this off by setting __MINIMIZE__, or by passing in true to $blnNoSpace.
         *
         * There area a few special cases to consider:
         * - Void elements will not be formatted to avoid adding unnecessary white space since these are generally
         *   inline elements
         * - Non-void elements always use internal newlines, even in __MINIMIZE__ mode. This is to prevent different behavior
         *   from appearing in __MINIMIZE__ mode on inline elements, because inline elements with internal space will render with space to separate
         *   from surrounding elements. Usually, this is not an issue, but in the special situations where you really need inline
         *   elements to be right up against its siblings, set $blnNoSpace to true.
         *
         *
         * @param string         $strTag                The tag name
         * @param null|mixed     $mixAttributes         String of attribute values or array of attribute values.
         * @param null|string     $strInnerHtml         The html to print between the opening and closing tags. This will NOT be escaped.
         * @param boolean        $blnIsVoidElement     True to print as a tag with no closing tag.
         * @param boolean        $blnNoSpace             Renders with no white-space. Useful in special inline situations.
         * @return string                        The rendered html tag
         */
        public static function RenderTag($strTag, $mixAttributes, $strInnerHtml = null, $blnIsVoidElement = false, $blnNoSpace = false) {
            assert ('!empty($strTag)');
            $strToReturn = '<' . $strTag;
            if ($mixAttributes) {
                if (is_string($mixAttributes)) {
                    $strToReturn .=  ' ' . trim($mixAttributes);
                } else {
                    // assume array
                    $strToReturn .=  QHtml::RenderHtmlAttributes($mixAttributes);
                }
            };
            if ($blnIsVoidElement) {
                $strToReturn .= ' />'; // conforms to both XHTML and HTML5 for both normal and foreign elements
            }
            elseif ($blnNoSpace || substr (trim($strInnerHtml), 0, 1) !== '<') {
                $strToReturn .= '>' . $strInnerHtml . '</' . $strTag . '>';
            }
            else {
                // the hardcoded newlines below are important to prevent different drawing behavior in MINIMIZE mode
                $strToReturn .= '>' . "\n" . _indent(trim($strInnerHtml)) .  "\n" . '</' . $strTag . '>' . _nl();
            }
            return $strToReturn;
        }

        /**
         * Renders an input element with a label tag. Uses separate styling for the label and the input object.
         * In particular, this gives you the option of wrapping the input with a label (which is what Bootstrap
         * expects on checkboxes) or putting the label next to the object (which is what jQueryUI expects).
         *
         * Note that if you are not setting $blnWrapped, it is up to you to insert the "for" attribute into
         * the label attributes.
         *
         * @param $strLabel
         * @param $blnTextLeft
         * @param $strAttributes
         * @param $strLabelAttributes
         * @param $blnWrapped
         * @return string
         */
        public static function RenderLabeledInput($strLabel, $blnTextLeft, $strAttributes, $strLabelAttributes, $blnWrapped) {
            $strHtml = trim(self::RenderTag('input', $strAttributes, null, true));

            if ($blnWrapped) {
                if ($blnTextLeft) {
                    $strCombined = $strLabel .  $strHtml;
                } else {
                    $strCombined = $strHtml . $strLabel;
                }

                $strHtml = self::RenderTag('label', $strLabelAttributes, $strCombined);
            }
            else {
                $strLabel = trim(self::RenderTag('label', $strLabelAttributes, $strLabel));
                if ($blnTextLeft) {
                    $strHtml = $strLabel .  $strHtml;
                } else {
                    $strHtml = $strHtml . $strLabel;
                }
            }
            return $strHtml;
        }

        /**
         * Returns the formatted value of type <length>.
         * See http://www.w3.org/TR/CSS1/#units for more info.
         * @param     string     $strValue     The number or string to be formatted to the <length> compatible value.
         * @return     string     the formatted value of type <length>.
         */
        public final static function FormatLength($strValue) {
            if (is_numeric($strValue)) {
                if (0 == $strValue) {
                    if (!is_int($strValue)) {
                        $fltValue = floatval($strValue);
                        return sprintf('%s', $fltValue);
                    } else {
                        return sprintf('%s', $strValue);
                    }
                } else {
                    if (!is_int($strValue)) {
                        $fltValue = floatval($strValue);
                        return sprintf('%spx', $fltValue);
                    } else {
                        return sprintf('%spx', $strValue);
                    }
                }
            } else {
                return sprintf('%s', $strValue);
            }
        }

        /**
         * Sets the given length string to the new length value.
         * If the new length is preceded by a math operator (+-/*), then arithmetic is performed on the previous
         * value. Returns true if the length changed.
         * @param     string     $strOldLength
         * @param     string     $newLength
         * @return     bool    true if the length was changed
         */
        public static function SetLength(&$strOldLength, $newLength) {
            if ($newLength && preg_match('#^(\+|\-|/|\*)(.+)$#',$newLength, $matches)) { // do math operation
                $strOperator = $matches[1];
                $newValue = $matches[2];
                assert (is_numeric($newValue));
                if (!$strOldLength) {
                    $oldValue  = 0;
                    $oldUnits = 'px';
                } else {
                    $oldValue = filter_var ($strOldLength, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
                    if (preg_match('/([A-Z]+|[a-z]+|%)$/', $strOldLength, $matches)) {
                        $oldUnits = $matches[1];
                    } else {
                        $oldUnits = 'px';
                    }
                }

                switch ($strOperator) {
                    case '+':
                        $newValue = $oldValue + $newValue;
                        break;

                    case '-':
                        $newValue = $oldValue - $newValue;
                        break;

                    case '/':
                        $newValue = $oldValue / $newValue;
                        break;

                    case '*':
                        $newValue = $oldValue * $newValue;
                        break;
                }
                if ($newValue != $oldValue) {
                    $strOldLength = $newValue . $oldUnits; // update returned value
                    return true;
                } else {
                    return false; // nothing changed
                }
            } else { // no math operation
                $newLength = self::FormatLength($newLength);

                if ($strOldLength !== $newLength) {
                    $strOldLength = $newLength;
                    return true;
                } else {
                    return false;
                }
            }
        }


        /**
         * Helper to add a class or classes to a pre-existing space-separated list of classes. Checks to make sure the
         * class isn't already in the list. Returns true to indicate a change in the list.
         *
         * @param string     $strClassList    Current list of classes separated by a space
         * @param string     $strNewClasses     New class to add. Could be a list separated by spaces.
         * @return bool     true if the class list was changed.
         */
        public static function AddClass(&$strClassList, $strNewClasses) {
            $strNewClasses = trim($strNewClasses);
            if (empty($strNewClasses)) return false;

            if (empty ($strClassList)) {
                $strCurrentClasses = array();
            }
            else {
                $strCurrentClasses = explode(' ', $strClassList);
            }

            $blnChanged = false;
            foreach (explode (' ', $strNewClasses) as $strClass) {
                if ($strClass && !in_array ($strClass, $strCurrentClasses)) {
                    $blnChanged = true;
                    if (!empty ($strClassList)) {
                        $strClassList .= ' ';
                    }
                    $strClassList .= $strClass;
                }
            }

            return $blnChanged;
        }

        /**
         * Helper to remove a class or classes from a list of space-separated classes.
         *
         * @param string $strClassList        class list string to search
         * @param string $strCssNamesToRemove space separated list of names to remove
         *
         * @return bool    true if the class list was changed
         */
        public static function RemoveClass(&$strClassList, $strCssNamesToRemove) {
            $strNewCssClass = '';
            $blnRemoved = false;
            $strCssNamesToRemove = trim($strCssNamesToRemove);
            if (empty($strCssNamesToRemove)) return false;

            if (empty ($strClassList)) {
                $strCurrentClasses = array();
            }
            else {
                $strCurrentClasses = explode(' ', $strClassList);
            }
            $strRemoveArray = explode (' ', $strCssNamesToRemove);

            foreach ($strCurrentClasses as $strCssClass) {
                if ($strCssClass = trim($strCssClass)) {
                    if (in_array($strCssClass, $strRemoveArray)) {
                        $blnRemoved = true;
                    }
                    else {
                        $strNewCssClass .= $strCssClass . ' ';
                    }
                }
            }
            if ($blnRemoved) {
                $strClassList = trim($strNewCssClass);
            }
            return $blnRemoved;
        }

        /**
         * Many CSS frameworks use families of classes, which are built up from a base family name. For example,
         * Bootstrap uses 'col-lg-6' to represent a column that is 6 units wide on large screens and Foundation
         * uses 'large-6' to do the same thing. This utility removes classes that start with a particular prefix
         * to remove whatever sizing class was specified.
         *
         * @param  $strClassList
         * @param  $strPrefix
         * @return bool true if the class list changed
         */
        public static function RemoveClassesByPrefix (&$strClassList, $strPrefix) {
            $aRet = array();
            $blnChanged = false;
            if ($strClassList) foreach (explode (' ', $strClassList) as $strClass) {
                if (strpos($strClass, $strPrefix) !== 0) {
                    $aRet[] = $strClass;
                }
                else {
                    $blnChanged = true;
                }
            }
            $strClassList = implode (' ', $aRet);
            return $blnChanged;
        }

        /**
         * Render the given attribute array for html output. Escapes html entities enclosed in values. Uses
         * double-quotes to surround the value. Precedes the resulting text with a space character.
         *
         * @param array|null $attributes
         * @return string
         */
        public static function RenderHtmlAttributes ($attributes) {
            $strToReturn = '';
            if ($attributes) {
                foreach ($attributes as $strName=>$strValue) {
                    if ($strValue === false) {
                        $strToReturn .= (' ' . $strName);
                    } elseif (!is_null($strValue)) {
                        $strToReturn .= (' ' . $strName . '="' . htmlspecialchars($strValue, ENT_COMPAT | ENT_HTML5, QApplication::$EncodingType) . '"');
                    }
                }
            }
            return $strToReturn;
        }


        /**
         * Render the given array as a css style string. It will NOT be escaped.
         *
         * @param array     $styles        key/value array representing the styles.
         * @return string    a string suitable for including in a css 'style' property
         */
        public static function RenderStyles($styles) {
            if (!$styles) return '';
            return implode('; ', array_map(
                function ($v, $k) { return $k . ':' . $v; },
                $styles,
                array_keys($styles))
            );
        }

        /**
         * Returns the given string formatted as an html comment that will go on its own line.
         * @param string     $strText
         * @param bool         $blnRemoveOnMinimize
         * @return string
         */
        public static function Comment($strText, $blnRemoveOnMinimize = true) {
            if ($blnRemoveOnMinimize && QApplication::$Minimize) {
                return '';
            }
            return  _nl() . '<!-- ' . $strText . ' -->' . _nl();

        }

        /**
         * Generate a URL from components. This URL can be used in the QApplication::Redirect function, or applied to
         * an anchor tag by setting the href attribute.
         *
         * You can also use this to modify a URL by passing a complete URL in the location. The URL will be modified by the parameters given.
         *
         * @param string $strLocation            absolute or relative path to resource, depending on your protocol. If not needed, enter an empty string. Can be a complete URL.
         * @param array|null $queryParams        key->value array of query parameters to add to the location.
         * @param string|null $strAnchor        anchor to add to the url
         * @param string|null $strScheme        protocol if specifying a resource outside of the current server (i.e. http)
         * @param string|null $strHost            server that the resource is on. Required if specifying a scheme.
         * @param string|null $strUser            user name if needed. Some protocols like mailto and ftp need this
         * @param string|null $strPassword        password if needed. Note that password is sent in the clear.
         * @param string|null $intPort            port if different from default
         * @return string
         */
        public static function MakeUrl ($strLocation, $queryParams = null, $strAnchor = null, $strScheme = null, $strHost = null, $strUser = null, $strPassword = null, $intPort = null) {
            // Decompose
            if ($strLocation) {
                $params = parse_url($strLocation);
            }

            if (!empty($strLocation) && isset($params['path'])) {
                $strUrl = $params['path'];
            } else {
                $strUrl = '';
            }

            if (isset($params['query'])) {
                parse_str($params['query'], $queryParams2);
                if ($queryParams) {
                    $queryParams = array_merge($queryParams2, $queryParams);
                } else {
                    $queryParams = $queryParams2;
                }
            }

            if (empty($strAnchor) && isset($params['fragment'])) {
                $strAnchor = $params['fragment'];
            }

            if (empty($strScheme) && isset($params['scheme'])) {
                $strScheme = $params['scheme'];
            }

            if (empty($strHost) && isset($params['host'])) {
                $strHost = $params['host'];
            }

            if (empty($strUser) && isset($params['user'])) {
                $strUser = $params['user'];
            }
            if (empty($strPassword) && isset($params['pass'])) {
                $strPassword = $params['pass'];
            }
            if (empty($intPort) && isset($params['port'])) {
                $intPort = $params['port'];
            }

            if ($queryParams)  {
                $strUrl .= '?' . http_build_query($queryParams);
            }
            if ($strAnchor) {
                $strUrl .= '#' . urlencode($strAnchor);
            }

            // More complex URLs. Once you specify protocol, you will need to specify the server too.
            if ($strScheme) {
                assert(!empty($strHost));

                // We do not do any checking at this point since URLs can be complex. It is up to you to build a correct URL.
                // 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.

                // Build server portion.
                if ($intPort) {
                    $strHost .= ':' . $intPort;
                }
                if ($strUser) {
                    $strUser = rawurlencode($strUser);
                    if ($strPassword) {
                        $strUser = $strUser . ':' . rawurlencode($strPassword);
                    }
                    $strHost = $strUser . '@' . $strHost;
                }
                $strUrl = $strScheme . $strHost . $strUrl;
            }
            return $strUrl;
        }

        /**
         * Returns a MailTo url.
         *
         * @param string $strUser
         * @param string| null $strServer optional server. If missing, will assume server and "@" are already in strUser
         * @param array|null $queryParams
         * @param string|null $strName Optional name to associate with the email address. Some email clients will show this instead of the address.
         * @return string    The mailto url.
         */
        public static function MailToUrl ($strUser, $strServer = null, $queryParams = null, $strName = null) {
            if ($strServer) {
                $strUrl = $strUser . '@' . $strServer;
            } else {
                $strUrl = $strUser;
            }
            if ($strName) {
                $strUrl = '"' . $strName . '"' . '<' . $strUrl . '>';
            }
            $strUrl = rawurlencode($strUrl);
            if ($queryParams) {
                $strUrl .= '?' . http_build_query($queryParams, null, null, PHP_QUERY_RFC3986);
            }
            return $strUrl;
        }

        /**
         * Utility function to create a link, i.e. an "a" tag.
         *
         * @param string $strUrl URL to link to. Use MakeUrl or MailToUrl to create the URL.
         * @param string $strText The inner text. This WILL be escaped.
         * @param array $attributes Other html attributes to include in the tag
         * @param boolean $blnHtmlEntities False to prevent encoding
         */
        public static function RenderLink ($strUrl, $strText, $attributes = null, $blnHtmlEntities = true) {
            $attributes["href"] = $strUrl;
            if ($blnHtmlEntities) {
                $strText = QApplication::HtmlEntities($strText);
            }
            return self::RenderTag("a", $attributes, $strText);
        }

        /**
         * Renders a PHP string as HTML text. Makes sure special characters are encoded, and <br /> tags are substituted
         * for newlines.
         * @param $strText
         */
        public static function RenderString($strText) {
            return nl2br(htmlspecialchars($strText, ENT_COMPAT | ENT_HTML5, QApplication::$EncodingType));
        }

        /**
         * A quick way to render an HTML table from an array of data. For more control, or to automatically render
         * data that may change, see QHtmlTable and its subclasses.
         *
         * Example:
         * $data = [
         *                 ['name'=>'apple', 'type'=>'fruit'],
         *                 ['name'=>'carrot', 'type'=>'vegetable']
         *     ];
         *
         *     print(QHtml::RenderTable($data, ['name','type'], ['class'=>'mytable'], ['Name', 'Type']);
         *
         *
         * @param []mixed            $data                An array of objects, or an array of arrays
         * @param []string|null     $strFields            An array of fields to display from the data. If the data contains objects,
         *                                                 the fields will be accessed using $obj->$strFieldName. If an array of arrays,
         *                                                 it will be accessed using $obj[$strFieldName]. If no fields specified, it will
         *                                                 treat the data as an array of arrays and just create cells for whatever it finds.
         * @param array|null         $attributes            Optional array of attributes to be inserted into the table tag (like a class or id).
         * @param []string|null     $strHeaderTitles    Optional array of titles to be added as a header row.
         * @param int                 $intHeaderColumnCount    Optional count of the number of columns on the left that will be
         *                                                     rendered using a 'th' tag instead of a 'td' tag.
         * @param bool                 $blnHtmlEntities    True (default) to run all titles and text through the HTMLEntities renderer. Set this to
         *                                                 false if you are trying to display raw html.
         * @return string
         */
        public static function RenderTable(array $data, $strFields = null, $attributes = null, $strHeaderTitles = null, $intHeaderColumnCount = 0, $blnHtmlEntities = true) {
            if (!$data) {
                return '';
            }

            $strHeader = '';
            if ($strHeaderTitles) {
                foreach ($strHeaderTitles as $strHeaderTitle) {
                    if ($blnHtmlEntities) {
                        $strHeaderTitle = QApplication::HtmlEntities($strHeaderTitle);
                    }
                    $strHeader .= '<th>' . $strHeaderTitle . '</th>';
                }
                $strHeader = '<thead><tr>' . $strHeader . '</tr></thead>';
            }
            $strBody = '';
            foreach ($data as $row) {
                $intFieldNum = 0;
                $strRow = '';
                if ($strFields) {
                    foreach ($strFields as $strField) {
                        $intFieldNum ++;
                        $strItem = '';
                        if (is_object($row)) {
                            $strItem = $row->$strField;
                        } elseif (isset($row[$strField])) {
                            $strItem = $row[$strField];
                        }
                        if ($blnHtmlEntities) {
                            $strItem = QApplication::HtmlEntities($strItem);
                        }
                        if ($intFieldNum <= $intHeaderColumnCount) {
                            $strRow .= '<th>' . $strItem . '</th>';
                        } else {
                            $strRow .= '<td>' . $strItem . '</td>';
                        }
                    }
                } else {
                    foreach ($row as $strItem) {
                        $intFieldNum ++;
                        if ($blnHtmlEntities) {
                            $strItem = QApplication::HtmlEntities($strItem);
                        }
                        if ($intFieldNum <= $intHeaderColumnCount) {
                            $strRow .= '<th>' . $strItem . '</th>';
                        } else {
                            $strRow .= '<td>' . $strItem . '</td>';
                        }
                    }
                }
                $strRow = '<tr>' . $strRow . '</tr>';
                $strBody .= $strRow;
            }
            $strBody = '<tbody>' . $strBody . '</tbody>';
            $strTable = self::RenderTag('table', $attributes , $strHeader . $strBody);
            return $strTable;
        }

    }