Location: PHPKode > scripts > jQueryP > jqueryp/jQueryP.class.php
<?php
/**
 *	@package jQueryP
 *
 *	This package contains the tools to enable the manipulation of
 *	client-side elements from the server side using jQuery. 
 *	Additionally, the class provides a proxy interface for server side 
 *	callbacks on client-side elements using jQuery events
 *
 *	04/29/2009
 *	@author Sam Shull <hide@address.com>
 *	@version 0.5
 *
 *	@copyright Copyright (c) 2009 Sam Shull <hide@address.com>
 *	@license <http://www.opensource.org/licenses/mit-license.html>
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *	
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *	
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 *
 *	CHANGES -
 *			
 */

/****************************************************************************************************
 *
 *										----  NOTICE -----
 *
 *		This file must be included before any output occurs or it will not work appropriately
 *
 *
/****************************************************************************************************/

/**
 *	Enables jQuery like object intialization
 *
 *	@param mixed $selector
 *	@param string $context
 *
 *	@return jQueryP
 */
function jQueryP($selector, $context=null)
{
	return new jQueryP($selector, $context);
}

/**
 *	This class uses the magic methods to enable ease of access
 *	to jQuery methods from the server side
 *
 *	NOTE - use the jQueryP::createProxy method if you would like to make a server side callback
 *
 *	<code>
 *		<html>
 *			<?php
 *				$begin_opacity = 0;
 *				$end_opacity = 0.5;
 *				$first_eleemnt = '0';
 *
 *				function scroll ()
 *				{
 *					$arguments = func_get_args();
 *					return 'document.body.innerHTML += "<br>"+' . json_encode(print_r($arguments, true));
 *				}
 *			
 *				require 'jQueryP.php';
 *			?>
 *			<body style="height:6000px;">
 *				<input id="test" style="display:none"/>
 *				<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
 *				<script type="text/javascript">
 *					<?php
 *						print jQueryP(JSVar('window'))
 *								->bind('scroll', jQueryP::createProxy('scroll'));
 *
 *						print 'var test = ' . 
 *								jQueryP('#test')
 *										//example of PHP bracket notation access to a method
 *									->{(true ? 'fadeIn' : 'show')}(500)
 *									->css(array('border' => '1px solid red', 'opacity' => $begin_opacity))
 *									->animate(array('opacity' => $end_opacity), 500, JSVar('function(){alert("animation complete");}'))
 *										//example of PHP bracket notation access to a property
 *									->{0}; // or ->$first_element; or ->{'example' . $first_element};
 *										//since jQueryP implements ArrayAccess you can also use:
 *										//[0] or [$first_element] or ['example' . $first_element]
 *										//but not for method calls and not after other method calls
 *										//so use curly braces not square brackets in PHP, much better
 *					?>
 *				</script>
 *			</body>
 *		</html>
 *	</code>
 */
class jQueryP implements ArrayAccess
{
	
	/**
	 *	Pick which one of the valid HTTP verbs, 
	 *	preferrably not POST, GET, PUT, OPTIONS, TRACE, HEAD, or DELETE,
	 *	that should be used as a way to simplify call back
	 *	http://annevankesteren.nl/2007/10/http-methods
	 *
	 *	Depends on web server configuration
	 *	@const string
	 */
	const HTTP_METHOD = 'MKCOL';
	
	/**
	 *	the function to use for hashing the callback function
	 *	in jQueryP::createProxy
	 *
	 *	@const string
	 */
	const HASH_FUNCTION = 'md5'; //md5|sha1
	
	/**
	 *	String value of the object
	 *
	 *	@var string
	 */
	public $_str;
	
	/**
	 *	just an identifier for ensuring that
	 *	the jQueryP_ARGUMENTS_ENCODER javascript
	 *	function has been included before the 
	 *	jQueryP::createProxy method is called
	 *
	 *	@var boolean
	 */
	private static $included = false;
	
	/**
	 *	
	 *
	 *	@var array
	 */
	private static $input = array();
	
	/**
	 *	Establish the base string
	 *
	 *	@param mixed $selector
	 *	@param string $context
	 */
	public function __construct ($selector, $context=null)
	{
		$this->_str = sprintf('jQuery(%s%s)', 
						$selector instanceof JSVar ? $selector : json_encode($selector), 
						($context ? ",{$context}" : '')
					);
		return;
	}
	
	/**
	 *	Magic method simplifies adding methods 
	 *	onto the string value of the object
	 *
	 *	@param string name
	 *	@param array $args
	 *
	 *	@return jQueryP
	 */
	public function __call ($name, array $args)
	{
		foreach ($args as $n => $arg)
		{
			//if the arg is an instance of JSVar leave it that way
			$args[$n] = self::js_encode($arg);
		}
		
		$this->_str .= sprintf("\n\t.%s(%s)", $name, implode(',', $args));
		
		return $this;
	}
	
	/**
	 *	Get the string value of the object
	 *	joined to the desired property
	 *
	 *	@return string
	 */
	public function __get ($name)
	{
		return sprintf("{$this->_str}[%s]\n", json_encode($name));
	}
	
	/**
	 *	Dont try it. Too hard to keep track of
	 *
	 *	@throws BadMethodCallException
	 */
	public function __set ($name, $value)
	{
		throw new BadMethodCallException('The server side jQueryP object has no setters.');
	}
	
	/**
	 *	Get the string value of the object
	 *
	 *	@return string
	 */
	public function __toString ()
	{
		return "{$this->_str}\n";
	}
	
	/**
	 *	Get the string value of the object
	 *	adds a semi-colon for safety
	 *
	 *	@return string
	 */
	public function toString ($comma=true)
	{
		return $this->_str . ($comma ? ";\n" : "\n");
	}
	
	/**
	 *	Get the string value of the object
	 *	joined to the desired property
	 *
	 *	@return string
	 */
	public function offsetGet ($offset)
	{
		return is_numeric($offset) ? "{$this->_str}[{$offset}]\n" : $this->__get($offset);
	}
	
	/**
	 *	Dont try it.
	 *
	 *	@throws BadMethodCallException
	 */
	public function offsetSet ($offset, $value)
	{
		throw new BadMethodCallException('The server side jQueryP object has no setters.');
	}
	
	/**
	 *	Dont try it.
	 *
	 *	@throws Exception
	 */
	public function offsetUnset ($offset)
	{
		throw new BadMethodCallException('The server side jQueryP object has no setters.');
	}
	
	/**
	 *	Dont try it. Too hard to keep track of
	 *
	 *	@throws Exception
	 */
	public function offsetExists ($offset)
	{
		throw new BadMethodCallException('The server side jQueryP object has no setters.');
	}
	
	/**
	 *	
	 *
	 *	@return mixed
	 */
	public static function & extend (&$object)
	{
		$args = func_get_args();
		
		if (is_array($object))
		{
			for ($i = 1, $l = count($args);$i < $l; ++$i)
			{
				foreach ($args[$i] as $name => $value)
				{
					if (isset($object[$name]) && !is_scalar($object[$name]) && !is_scalar($value))
					{
						$object[$name] = self::extend($object[$name], $value);
					}
					elseif (!is_null($value))
					{
						$object[$name] = $value;
					}
				}
			}
			
			return $object;
		}
		
		for ($i = 1, $l = count($args);$i < $l; ++$i)
		{
			foreach ($args[$i] as $name => $value)
			{
				if (isset($object->$name) && !is_scalar($object->$name) && !is_scalar($value))
				{
					$object->$name = self::extend($object->$name, $value);
				}
				elseif (!is_null($value))
				{
					$object->$name = $value;
				}
			}
		}
		
		return $object;
	}
	
	/**
	 *	Need special support for encoding JSVar in 
	 *	into javascript instead of JSON
	 *
	 *	@return string
	 */
	public static function js_encode ($object)
	{
		if ($object instanceof JSVar)
		{
			return $object->__toString();
		}
		
		if (!is_scalar($object))
		{
			$ret = array();
			
			if (is_array($object) && !array_diff(array_keys($object), range(0, count($var) - 1)))
			{
				foreach ($object as $property => $value)
				{
					$ret[] = (
								$value instanceof JSVar ? 
								$value->__toString() : 
								self::js_encode($value)
							 );
				}
				
				return '[' . implode(',', $ret) . ']';
			}
			
			foreach ($object as $property => $value)
			{
				$ret[] = json_encode($property) . ":" . 
						 (
						 	$value instanceof JSVar ? 
							$value->__toString() : 
							self::js_encode($value)
						 );
			}
			
			return '{' . implode(',', $ret) . '}';
		}
		
		return json_encode($object);
	}
	
	/**
	 *	Returns the jQueryP_ARGUMENTS_ENCODER javascript
	 *	function string for inclusion in the script
	 *
	 *	@return string
	 */
	public static function setupProxies ()
	{
		self::$included = true;
		return jQueryP_ARGUMENTS_ENCODER;
	}
	
	/**
	 *	
	 *
	 *	@return array
	 */
	protected static function getInput ()
	{
		if (self::$input)
		{
			return self::$input;
		}
		
		switch ($_SERVER['REQUEST_METHOD'])
		{
			case 'GET':
			{
				self::$input = $_GET;
				break;
			}
			case 'POST':
			{
				self::$input = $_POST;
				break;
			}
			default:
			{
				$source = function_exists('stream_get_wrappers') && 
							in_array('php', stream_get_wrappers()) ? 
							file_get_contents('php://input') :
							(
								isset($_SERVER['HTTP_RAW_POST_DATA']) ? 
								$_SERVER['HTTP_RAW_POST_DATA'] :
								(
									isset($GLOBALS['HTTP_RAW_POST_DATA']) ?
									$GLOBALS['HTTP_RAW_POST_DATA'] : 
									null
								)
							);
				
				parse_str($source, $output);
				
				self::$input = $output;
			}
		}
		
		return (array)self::$input;
	}
	
	/**
	 *	Create a JavaScript proxy to a PHP function.
	 *	A javascript success function can be specified to enable different
	 *	handling of the server side data on the client side, but the default
	 *	is that if the callback returns data it is expected to be executable javascript.
	 *
	 *	Element references in the arguments will return a string representation 
	 *	of a jQuery selector string that will allow you to access that element
	 *
	 *	This is not the most efficient way to perform a server side callback,
	 *	but it is the easiest to implement into a general use class like this one.
	 *
	 *	@param array $options
	 *
	 *	@return JSFunction
	 */
	public static function createProxy($options)
	{
		if (!self::$included)
		{
			throw new LogicException('Please call jQueryP::setupProxies before creating a proxy.');
		}
		
		if (
			!isset($options['callback']) ||
			!is_callable($options['callback']) || 
			(
				is_object($options['callback']) && 
				!method_exists($options['callback'], '__invoke')
			)
		)
		{
			throw new InvalidArgumentException('jQueryP::createProxy requires that the "callback" element be set in the options argument.');
		}
		
		if ($options['callback'] instanceof JSVar)
		{
			return $options['callback'];
		}
		
		$func = self::HASH_FUNCTION;
		
		//create an identifier - always use spl_object_hash on a Closure
		$hash = is_object($options['callback']) && 
				method_exists($options['callback'], '__invoke') && 
				function_exists('spl_object_hash') ?
				spl_object_hash($options['callback']) : 
				$func(serialize($options['callback']));
		
		//execute the callback if appropriate
		if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == jQueryP::HTTP_METHOD)
		{
			$input = self::getInput();
		
			//testing server had magic_quotes - i hate magic quotes
			$magic_quotes = get_magic_quotes_gpc();
			
			if (isset($input['callback'], $input['arguments']) && $hash == ($magic_quotes ? stripslashes($input['callback']) : $input['callback']))
			{
				//get the arguments into an array
				$args = json_decode($magic_quotes ? stripslashes($input['arguments']) : $input['arguments']);
				
				//clean up the output buffer
				$x = ob_get_clean();
				
				//end the process if the callback is being called
				die(
						call_user_func_array(
												is_object($options['callback']) && 
												method_exists($options['callback'], '__invoke') ? 
												//just in case
												array($options['callback'], '__invoke') :  
												$options['callback'], 
												is_array($args) ? 
												$args : 
												array($args)
											)
				);
			}
		}
										
		unset($options['callback']);
		
		$array = array(
			'type'		=> jQueryP::HTTP_METHOD,
			'success'	=> JSVar('function (data, status)
							{
								if (status === "success" && jQuery.trim(data+""))
								{
									//if the callback returns data
									//it is expected to be javascript
									jQuery.globalEval(data);
								}
							}'),
			'data'		=> array(
									'callback'	=> $hash,
									'arguments'	=> JSVar('jQuery._encodeArguments(jQuery.makeArray(arguments))'),
								)
		);
		//this is a javascript callback that will send an XHR
		//to the script that is being requested now ($_SERVER['PHP_SELF']), 
		//allowing this function to execute the callback as shown above
		return new JSFunction( sprintf('jQuery.ajax(%s);', self::js_encode( self::extend($array, $options) )) );
	}
}

/**
 *	A utility function
 *
 *
 */
function JSVar ($v)
{
	return new JSVar($v);
}

/**
 *	Ensure that data is stored as javascript
 *	not JSON
 *
 *
 */
class JSVar extends jQueryP
{
	/**
	 *
	 *
	 *
	 */
	public function __construct($var)
	{
		$this->_str = $var;
	}
	
	/**
	 *
	 *
	 *
	 */
	public function __toString()
	{
		return (string)$this->_str;
	}
}

/**
 *	A utility function
 *
 *
 */
function JSFunction ($args=null, $code=null)
{
	if (is_null($code))
	{
		$code = $args;
		$args = null;
	}
	
	return new JSFunction($args, $code);
}

/**
 *	Create an anonymous javascript function
 *	from a code string and optional argument
 *	string
 *
 *
 */
class JSFunction extends JSVar
{
	
	/**
	 *
	 *
	 *
	 */
	public function __construct($args, $code=null)
	{
		if (is_null($code))
		{
			$code = $args;
			$args = null;
		}
		
		if (is_array($args))
		{
			foreach ($args as $n => $arg)
			{
				//if the arg is an instance of JSVar leave it that way
				$args[$n] = jQueryP::js_encode($arg);
			}
			
			$args = implode(",", $args);
		}
		
		$this->_str = "function({$args}){{$code}}";
	}
}

/**
 *	A utility function for calling a utility class
 *
 *
 */
function JSScopingFunction ($args=null, $code=null)
{
	if (is_null($code))
	{
		$code = $args;
		$args = null;
	}
	
	return new JSScopingFunction($args, $code);
}

/**
 *	A utility class for storing information that must NOT be json encoded
 *
 *
 */
class JSScopingFunction extends JSVar
{
	
	/**
	 *
	 *
	 *
	 */
	public function __construct($args=null, $code=null)
	{
		if (is_null($code))
		{
			$code = $args;
			$args = null;
		}
		
		if (is_array($args))
		{
			foreach ($args as $n => $arg)
			{
				//if the arg is an instance of JSVar leave it that way
				$args[$n] = jQueryP::js_encode($arg);
			}
			
			$args = implode(",", $args);
		}
		
		$this->_str = "(function(){{$code}})({$args})";
	}
}

/**
 *	A utility function for calling a utility class
 *
 *
 */
function JSTimeout ($func, $time=13)
{
	return new JSTimeout($func, $time);
}

/**
 *	A utility class for storing information that must NOT be json encoded
 *
 *
 */
class JSTimeout extends JSVar
{
	/**
	 *
	 *
	 *
	 */
	public function __construct($func, $time=13)
	{
		$this->_str = "setTimeout({$func},{$time})";
	}
}

/**
 *	JSON.stringify with support for element selectors
 *	for use by a jQueryP_PROXY_FUNCTION to encode data
 *
 */
define('jQueryP_ARGUMENTS_ENCODER', <<<EOD
	jQuery._encodeArguments = function (args)
	{
		try{
			//a reference to this anonymous function
			var callee = arguments.callee, 
				//and an undefined object that can be used later
				temporary;
			
			//return "null" if null or undefined
			if (args === null || args === temporary)
			{
				return "null";
			}
			
			if (jQuery.isFunction(args))
			{
				return '"[object Function]"';
			}
			
			//figure out an object type
			switch (Object.prototype.toString.call(args))
			{
				case "[object Boolean]":
				case "[object Number]":
					return args+"";
				
				case "[object Array]":	
					temporary = [];
					jQuery.each(args, function(){temporary[temporary.length] = callee(this);});
					return "[" + temporary.join(",") + "]";
				
				case "[object Object]":	
					temporary = [];
					jQuery.each(args, function(n,v)
					{
						if (jQuery.isFunction(v))
						{
							//helper for methods like isPropagationStopped
							if (n.substr(0,2) != 'is')
							{
								return;
							}
							
							v = v();
						}
						
						temporary[temporary.length] = callee(n+"") + ":" + callee(v);
					});
					return "{" + temporary.join(",") + "}";
				
				case "[object String]":
				case "[object Date]":
				case "[object RegExp]":
					return '"' + 
							args.toString()
							 .replace(/([\\"\\r\\n\\t\\x00-\\x19])/g,//"-because this messes up my editor highlighting
								function ($0,$1)
								{
									switch($1)
									{
										case '\\r': return "\\\\r";
										case '\\n': return "\\\\n";
										case '\\t':	return "\\\\t";
										case "'":
										case '"': 	return "\\\\" + $1;
										default: 	return "";
									}
								}) + 
							'"';
					
			}
			//is it an element?
			
			if (args === window)
			{
				return '"window"';
			}
			
			if (args === document)
			{
				return '"document"';
			}
			
			if (args === document.body)
			{
				return '"body"';
			}
			
			if ("nodeType" in args)
			{ 
					//get a jQuery selector string for the object
				return (function ()
						{
							var str = [],
								element = args,
								isXML = jQuery.isXMLDoc(args),
								temp, temp2, i, l;
							
							while (element)
							{
								//break on an id
								if (element.id && element.nodeType == 1)
								{
									str.unshift("#" + element.id);
									break;
								}
								
								temp = path();
								
								if (temp)
								{
									str.unshift(temp);
								}
								
								temp = null;
								element = element.parentNode;
							}
							
							return str.length > 0 ?
									callee(str.join(">")) :
									args.outerHTML || args.innerHTML || callee(args+"");
							
							function path ()
							{
								var temp;
								
								//an attribute returns a string representing its parent
								if (element.nodeType == 2)
								{
									temp = ([
												arguments.callee(element.parentNode), 
												"[",
													element.nodeName, 
													"=", 
													callee(element.value+""), 
												"]"
											]).join("");
									
									//increment element
									element = element.parentNode;
								}
								else if (element.nodeType == 1)
								{
									temp = element.nodeName || element.tagName || element.localName;
									
									if (element.parentNode)
									{
										temp2 = element.parentNode.getElementsByTagName(temp);
										
										if (temp2.length > 1)
										{
											l = temp2.length;
											
											temp += ":eq(";//the CSS3 selector - ":nth-of-type("; - xpath [
											
											for (i=0;i<l;++i)
											{
												if (temp2[i] === element)
												{
													temp += ""+i;
													break;
												}
											}
											
											temp += ")";
										}
									}
								}
								
								return temp;
							}
						})();
			}
			
			return '"undefined"';
		}
		catch(e)
		{
			console.log(e, args);
			// Error: Permission denied to access property 'nodeType' from a non-chrome context <div class="anonymous-div">
		}
		
		return "null";
		//arguments should 
	};
	
EOD
);

/*
 *	jQuery natively uses the $ as an alias function
 *	this is a simple identifier for jQuery, but the simplest
 *	function name that PHP will support is _
 *	but I found at least one extension (gettext) that uses 
 *	that function name, so I used __
 *
 *	this could potentially cause problems and is not recommended though
 *
 *	<code>
 *		__('#test')
 *			->css(array('backgroundColor' => 'red'));
 *	</code>
 *
 *	@param mixed $selector
 *	@param string $context
 *
 *	@return jQueryP
 * /
if (!function_exists('__'))
{
	function __ ($selector, $context=null)
	{
		return new jQueryP($selector, $context);
	}
}

/**
 *	Another option is to create an anonymous function
 *	and assign it to $_ if that name isn't already taken
 *	this could potentially cause problems and is not recommended though
 * /
if (!isset($GLOBALS['_']))
{
	$GLOBALS['_'] = create_function('$selector, $context=null', 'return new jQueryP($selector, $context);');
	//$_('#test')->css('opacity', 0.9);
}
*/

if (isset($_SERVER['HTTP_METHOD']))
{
	$_SERVER['REQUEST_METHOD'] = $_SERVER['HTTP_METHOD'];
}

//use output bufferring in order to use callback function appropriately
//but only if the HTTP method is being used
if (
	isset($_SERVER['REQUEST_METHOD']) && 
	//check for the HTTP method
	$_SERVER['REQUEST_METHOD'] == jQueryP::HTTP_METHOD
)
{
	ob_start();
}

?>
Return current item: jQueryP