includes/framework/JavaScriptHelper.class.php
<?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();
}
}