<?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 <sam.shull@jhspecialty.com>
* @version 0.5
*
* @copyright Copyright (c) 2009 Sam Shull <sam.shull@jhspeicalty.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();
}
?>
|