qcubed/framework

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

Summary

Maintainability
B
5 hrs
Test Coverage
<?php

    /**
     * Class QJsClosure
     *
     * An object which represents a javascript closure (annonymous function). Use this to embed a
     * function into a PHP array or object that eventually will get turned into javascript.
     */
    class QJsClosure implements JsonSerializable {
        /** @var  string The js code for the function. */
        protected $strBody;
        /** @var array parameter names for the function call that get passed into the function. */
        protected $strParamsArray;

        /**
         * @param string     $strBody        The function body
         * @param array|null $strParamsArray The names of the parameters passed in the function call
         */
        public function __construct($strBody, $strParamsArray = null) {
            $this->strBody = $strBody;
            $this->strParamsArray = $strParamsArray;
        }

        /**
         * Return a javascript enclosure. Enclosures cannot be included in JSON, so we need to create a custom
         * encoding to include in the json that will get decoded at the other side.
         *
         * @return string
         */
        public function toJsObject() {
            $strParams = $this->strParamsArray ? implode(', ', $this->strParamsArray) : '';
            return  'function('.$strParams.') {' . $this->strBody . '}';
        }

        /**
         * Converts the object into something serializable by json_encode. Will get decoded in qcubed.unpackObj
         * @return mixed
         */
        public function jsonSerialize() {
            // Encode in a way to decode in qcubed.js
            $a[JavaScriptHelper::ObjectType] = 'qClosure';
            $a['func'] = $this->strBody;
            $a['params'] = $this->strParamsArray;
            return JavaScriptHelper::MakeJsonEncodable($a);
        }
    }

    /**
     * Wrapper class for arrays to control whether the key in the array is quoted.
     * In some situations, a quoted key has a different meaning from a non-quoted key.
     * For example, when making a list of parameters to pass when calling the jQuery $() command,
     * (i.e. $j(selector, params)), quoted words are turned into parameters, and non-quoted words
     * are turned into functions. For example, "size" will set the size attribute of the object, and
     * size (no quotes), will call the size() function on the object.
     *
     * To use it, simply wrap the value part of the array with this class.
     * @usage: $a = array ("click", new QJsNoQuoteKey (new QJsClosure('alert ("I was clicked")')));
     */
    class QJsNoQuoteKey implements JsonSerializable {
        protected $mixContent;

        public function __construct ($mixContent) {
            $this->mixContent = $mixContent;
        }

        public function toJsObject() {
            return JavaScriptHelper::toJsObject($this->mixContent);
        }

        public function jsonSerialize() {
            return $this->mixContent;
        }

    }

    /**
     * Class QJsVarName
     * Outputs a string without quotes to specify a global variable name. Strings are normally quoted. Dot notation
     * can be used to specify items within globals.
     */
    class QJsVarName implements JsonSerializable {
        protected $strContent;

        public function __construct($strContent) {
            $this->strContent = $strContent;
        }

        public function toJsObject() {
            return $this->strContent;
        }

        public function jsonSerialize() {
            $a[JavaScriptHelper::ObjectType] = 'qVarName';
            $a['varName'] = $this->strContent;
            return JavaScriptHelper::MakeJsonEncodable($a);
        }
    }

    /**
     * Class QJsFunction
     * Outputs a function call to a global function or function in an object referenced from global space. The purpose
     * of this is to immediately use the results of the function call, as opposed to a closure, which stores a pointer
     * to a function that is used later.
     */
    class QJsFunction implements JsonSerializable {
        /** @var  string|null */
        protected $strContext;
        /** @var  string */
        protected $strFunctionName;
        /** @var  array|null */
        protected $params;

        /**
         * QJsFunction constructor.
         * @param string $strFunctionName The name of the function call.
         * @param null|array $strParamsArray If given, the parameters to send to the function call
         * @param null $strContext If given, the object in the window object which contains the function and is the context for the function.
         *   Use dot '.' notation to traverse the object tree. i.e. "obj1.obj2" refers to window.obj1.obj2 in javascript.
         */
        public function __construct($strFunctionName, $params = null, $strContext = null) {
            $this->strFunctionName = $strFunctionName;
            $this->params = $params;
            $this->strContext = $strContext;
        }

        /**
         * Returns this as a javascript string to be included in the end script of the page.
         * @return string
         */
        public function toJsObject() {
            if ($this->params) {
                foreach ($this->params as $param) {
                    $strParams[] = JavaScriptHelper::toJsObject($param);
                }
                $strParams = implode (",", $strParams);
            }
            else {
                $strParams = '';
            }
            $strFuncName = $this->strFunctionName;
            if ($this->strContext) {
                $strFuncName = $this->strContext . '.' . $strFuncName;
            }
             return  $strFuncName . '('.$strParams.')';
        }

        /**
         * Returns this as a json object to be sent to qcubed.js during ajax drawing.
         * @return mixed
         */
        public function jsonSerialize() {
            $a[JavaScriptHelper::ObjectType] = 'qFunc';
            $a['func'] = $this->strFunctionName;
            if ($this->strContext) {
                $a['context'] = $this->strContext;
            }
            if ($this->params) {
                $a['params'] = $this->params;
            }

            return JavaScriptHelper::MakeJsonEncodable($a);
        }
    }



    /**
     * Class QJsonParameterList
     * A Wrapper class that will render an array without the brackets, so that it becomes a variable length parameter list.
     */
    class QJsParameterList {
        protected $arrContent;

        public function __construct ($arrContent) {
            $this->arrContent = $arrContent;
        }

        public function toJsObject() {
            $strList = '';
            foreach ($this->arrContent as $objItem) {
                if (strlen($strList) > 0) $strList .= ',';
                $strList .= JavaScriptHelper::toJsObject($objItem);
            }
            return $strList;
        }
    }

    /**
     * A wrapper class for generating an ajax action with no script. This is helpful in situations where data
     * will be returned to the javascript object later.
     *
     * Class QNoScriptAjaxAction
     */
    class QNoScriptAjaxAction extends QAjaxAction {
        private $objTargetAction;

        function __construct(QAction $objTargetAction) {
            $this->objTargetAction = $objTargetAction;
        }

        public function __get($strName) {
            if ($strName == 'Event')
                return parent::__get($strName);
            return $this->objTargetAction->__get($strName);
        }

        public function RenderScript(QControl $objControl) {
            return '';
        }
    }

    /**
     * Class JavaScriptHelper: used to help with generating javascript code
     */
    abstract class JavaScriptHelper {

        const ObjectType = 'qObjType';    // Identifies a JSON object as an object we want handle specially in qcubed.js
        /**
         * Returns javascript that on execution will insert the value $strValue into the DOM element corresponding to
         * the $objControl using the key $strKey
         *
         * @static
         * @param QControl $objControl
         * @param string $strKey
         * @param string $strValue any javascript variable or object
         * @return string data insertion javascript
         */
        public static function customDataInsertion(QControl $objControl, $strKey, $strValue) {
            return 'jQuery("#'.$objControl->ControlId.'").data("'.$strKey.'", '.$strValue.');';
        }

        /**
         * Returns javascript that on execution will retrieve the value from the DOM element corresponding to
         * the $objControl using the key $strKey and assign it to the javascript variable $strValue.
         *
         * @static
         * @param QControl $objControl
         * @param string $strKey
         * @param string $strValue
         * @return string data retrieval javascript
         */
        public static function customDataRetrieval(QControl $objControl, $strKey, $strValue) {
            return 'var '.$strValue.' = jQuery("#'.$objControl->ControlId.'").data("'.$strKey.'");';
        }

        /**
         * Helper class to convert a name from camel case to using dashes to separated words.
         * data-* html attributes have special conversion rules. Key names should always be lower case. Dashes in the
         * name get converted to camel case javascript variable names by jQuery.
         * For example, if you want to pass the value with key name "testVar" from PHP to javascript by printing it in
         * the html, you would use this function to help convert it to "data-test-var", after which you can retrieve
         * in in javascript by calling ".data('testVar')". on the object.
         * @param $strName
         * @return string
         * @throws QCallerException
         */
        public static function dataNameFromCamelCase($strName) {
            if (preg_match('/[A-Z][A-Z]/', $strName)) {
                throw new QCallerException ('Not a camel case string');
            }
            return preg_replace_callback('/([A-Z])/',
                function ($matches) {
                    return '-' . strtolower($matches[1]);
                },
                $strName
            );

        }

        /**
         * Converts an html data attribute name to camelCase.
         *
         * @param $strName
         * @return string
         */
        public static function dataNameToCamelCase($strName) {
            return preg_replace_callback('/-([a-z])/',
                function ($matches) {
                    return ucfirst($matches[1]);
                },
                $strName
            );
        }

        public static function jsEncodeString($objValue) {
            // default to string if not specified
            static $search = array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"');
            static $replace = array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"');
            return '"' . str_replace($search, $replace, $objValue) . '"';
        }

        /**
         * Recursively convert a php object to a javascript object.
         * If the $objValue is an object other than Date and has a toJsObject() method, the method will be called
         * to perform the conversion. Array values are recursively converted as well.
         *
         * This string is designed to create the object if it was directly output to the browser. See toJSON below
         * for an equivalent version that is passable through a json interface.
         *
         * @static
         * @param mixed $objValue the php object to convert
         * @return string javascript representation of the php object
         */
        public static function toJsObject($objValue) {
            $strRet = '';

            switch (gettype($objValue)) {
                case 'double':
                case 'integer':
                    $strRet = (string)$objValue;
                    break;

                case 'boolean':
                    $strRet = $objValue? 'true' : 'false';
                    break;

                case 'string':
                    $strRet = self::jsEncodeString($objValue);
                    break;

                case 'NULL':
                    $strRet = 'null';
                    break;

                case 'object':
                    if (method_exists($objValue, 'toJsObject')) {
                        $strRet = $objValue->toJsObject();
                    }
                    break;

                case 'array':
                    $array = (array)$objValue;
                    if (0 !== count(array_diff_key($array, array_keys(array_keys($array))))) {
                        // associative array - create a hash
                        $strHash = '';
                        foreach ($array as $objKey => $objItem) {
                            if ($strHash) $strHash .= ',';
                            if ($objItem instanceof QJsNoQuoteKey) {
                                $strHash .= $objKey.': '.self::toJsObject($objItem);
                            } else {
                                $strHash .= self::toJsObject($objKey).': '.self::toJsObject($objItem);
                            }
                        }
                        $strRet = '{'.$strHash.'}';
                    }
                    else {
                        // simple array - create a list
                        $strList = '';
                        foreach ($array as $objItem) {
                            if (strlen($strList) > 0) $strList .= ',';
                            $strList .= self::toJsObject($objItem);
                        }
                        $strRet = '['.$strList.']';
                    }

                    break;

                default:
                    $strRet = self::jsEncodeString((string)$objValue);
                    break;

            }
            return $strRet;
        }

        /**
         * Our specialized json encoder. Strings will be converted to UTF-8. Arrays will be recursively searched and
         * both keys and values made UTF-8. Objects will be converted with json_encode, and so objects that need a special
         * encoding should implement the jsonSerializable interface. See below
         * @param mixed $objValue
         * @return string
         */
        public static function toJSON($objValue) {
            assert ('is_array($objValue) || is_object($objValue)');    // json spec says only arrays or objects can be encoded
            $objValue = JavaScriptHelper::MakeJsonEncodable($objValue);
            $strRet = json_encode($objValue);
            if ($strRet === false) {
                throw new QCallerException ('Json Encoding Error: ' . json_last_error_msg());
            }
            return $strRet;
        }

        /**
         * Convert an object to a structure that we can call json_encode on. This is particularly meant for the purpose of
         * sending json data to qcubed.js through ajax, but can be used for other things as well.
         *
         * PHP 5.4 has a new jsonSerializable interface that objects should use to modify their encoding if needed. Otherwise,
         * public member variables will be encoded. The goal of object serialization should be to be able to send it
         * to qcubed.unpackParams in qcubed.js to create the javascript form of the object. This decoder will look for objects
         * that have the 'qObjType' key set and send the object to the special unpacker.
         *
         * DateTime handling is absent below. DateTime objects will get converted, but not in a very useful way. If you
         * are using strict DateTime objects (not likely since the framework normally uses QDateTime for all date objects),
         * you should convert them to QDateTime objects before sending them here.
         *
         * @param mixed $objValue
         * @return mixed
         */
        public static function MakeJsonEncodable($objValue) {
            if (QApplication::$EncodingType && QApplication::$EncodingType == 'UTF-8') {
                return $objValue; // Nothing to do, since all strings are already UTF-8 and objects can take care of themselves.
            }

            switch (gettype($objValue)) {
                case 'string':
                    $objValue = mb_convert_encoding($objValue, 'UTF-8', QApplication::$EncodingType);
                    return $objValue;

                case 'array':
                    $newArray = array();
                    foreach ($objValue as $key=>$val) {
                        $key = self::makeJsonEncodable($key);
                        $val = self::makeJsonEncodable($val);
                        $newArray[$key] = $val;
                    }
                    return $newArray;

                default:
                    return $objValue;

            }
        }

        /**
         * Utility function to make sure a script is terminated with a semicolon.
         *
         * @param $strScript
         * @return string
         */
        public static function TerminateScript($strScript) {
            if (!$strScript) return '';
            if (!($strScript = trim ($strScript))) return '';
            if (substr($strScript, -1) != ';') {
                $strScript .= ';';
            }
            return $strScript . _nl();
        }

    }