| [ Index ] |
PHP Cross Reference of Akelos Framework |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 3 4 // +----------------------------------------------------------------------+ 5 // | Akelos Framework - http://www.akelos.org | 6 // +----------------------------------------------------------------------+ 7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez | 8 // | Released under the GNU Lesser General Public License, see LICENSE.txt| 9 // +----------------------------------------------------------------------+ 10 11 /** 12 * @package ActionView 13 * @subpackage Helpers 14 * @author Bermi Ferrer <bermi a.t akelos c.om> 15 * @author Jerome Loyet 16 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org 17 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html> 18 */ 19 20 21 require_once (AK_LIB_DIR.DS.'AkActionView'.DS.'helpers'.DS.'javascript_helper.php'); 22 23 /** 24 * Provides a set of helpers for calling Prototype JavaScript functions, 25 * including functionality to call remote methods using 26 * Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]. 27 * This means that you can call actions in your controllers without 28 * reloading the page, but still update certain parts of it using 29 * injections into the DOM. The common use case is having a form that adds 30 * a new element to a list without reloading the page. 31 * 32 * To be able to use these helpers, you must include the Prototype 33 * JavaScript framework in your pages. See the documentation for 34 * ActionView/helpers/javascript_helper.php for more information on including 35 * the necessary JavaScript. 36 * 37 * See link_to_remote for documentation of options common to all Ajax 38 * helpers. 39 * 40 * See also ActionView/helpers/scriptaculous_helper.php for helpers which work 41 * with the Scriptaculous controls and visual effects library. 42 * 43 * See JavaScriptGenerator for information on updating multiple elements 44 * on the page in an Ajax response. 45 */ 46 class PrototypeHelper extends AkActionViewHelper 47 { 48 function getCallbacks() 49 { 50 if(empty($this->callbacks)){ 51 $callbacks = array_merge(array('uninitialized', 'loading', 'loaded', 52 'interactive', 'complete', 'failure', 'success'), 53 range(100,599)); 54 if(empty($this)) { 55 return $callbacks; 56 } 57 $this->callbacks = $callbacks; 58 } 59 return $this->callbacks; 60 } 61 62 function getAjaxOptions() 63 { 64 if(empty($this->ajax_options)){ 65 $ajax_options = array_merge(array('before', 'after', 'condition', 'url', 66 'asynchronous', 'method', 'insertion', 'position', 67 'form', 'with', 'update', 'script'), 68 $this->getCallbacks()); 69 if(empty($this)) { 70 return $ajax_options; 71 } 72 $this->ajax_options = $ajax_options; 73 } 74 return $this->ajax_options; 75 } 76 77 78 /** 79 * Returns a link to a remote action defined by <tt>options['url']</tt> 80 * (using the url_for format) that's called in the background using 81 * XMLHttpRequest. The result of that request can then be inserted into a 82 * DOM object whose id can be specified with <tt>options['update']</tt>. 83 * Usually, the result would be a partial prepared by the controller with 84 * either render_partial or render_partial_collection. 85 * 86 * Examples: 87 * $prototype_helper->link_to_remote('Delete this post', array('url' => array('action' => 'destroy', 'id' => $_POST['id']), 'update' => 'posts')); 88 * $prototype_helper->link_to_remote(Asset$this->_controller->tag_helper->image_tag('refresh'), array('url' => array('action' => 'list_emails'), array('update => 'emails')); 89 * 90 * You can also specify an array for <tt>options['update']</tt> to allow for 91 * easy redirection of output to an other DOM element if a server-side 92 * error occurs: 93 * 94 * Example: 95 * $prototype_helper->link_to_remote('Delete this post', array('url' => array('action' => 'destroy', 'id' => $_POST['id']), array('update' => array('success' => 'posts', 'failure' => 'error'); 96 * 97 * Optionally, you can use the <tt>options['position']</tt> parameter to 98 * influence how the target DOM element is updated. It must be one of 99 * <tt>'before'</tt>, <tt>'top'</tt>, <tt>'bottom'</tt>, or <tt>'after'</tt>. 100 * 101 * By default, these remote requests are processed asynchronous during 102 * which various JavaScript callbacks can be triggered (for progress 103 * indicators and the likes). All callbacks get access to the 104 * <tt>request</tt> object, which holds the underlying XMLHttpRequest. 105 * 106 * To access the server response, use <tt>request.responseText</tt>, to 107 * find out the HTTP status, use <tt>request.status</tt>. 108 * 109 * Example: 110 * 111 * $prototype_helper->link_to_remote($word, array('url' => array('action' => 'undo', 'n' => $word_counter) , 'complete' => 'undoRequestCompleted(request)'); 112 * 113 * The callbacks that may be specified are (in order): 114 * 115 * <tt>'loading'</tt>:: Called when the remote document is being 116 * loaded with data by the browser. 117 * <tt>'loaded'</tt>:: Called when the browser has finished loading 118 * the remote document. 119 * <tt>'interactive'</tt>:: Called when the user can interact with the 120 * remote document, even though it has not 121 * finished loading. 122 * <tt>'success'</tt>:: Called when the XMLHttpRequest is completed, 123 * and the HTTP status code is in the 2XX range. 124 * <tt>'failure'</tt>:: Called when the XMLHttpRequest is completed, 125 * and the HTTP status code is not in the 2XX 126 * range. 127 * <tt>'complete'</tt>:: Called when the XMLHttpRequest is complete 128 * (fires after success/failure if they are 129 * present). 130 * 131 * You can further refine <tt>'success'</tt> and <tt>'failure'</tt> by 132 * adding additional callbacks for specific status codes. 133 * 134 * Example: 135 * $this->link_to_remote($word, array('url' => array('action' => 'action')), array('404' => "alert('Not found...? Wrong URL...?')"), array('failure' => "alert('HTTP Error ' + request.status + '!')")); 136 * 137 * A status code callback overrides the success/failure handlers if present. 138 * 139 * If you for some reason or another need synchronous processing (that'll 140 * block the browser while the request is happening), you can specify 141 * <tt>options['type'] = 'synchronous'</tt>. 142 * 143 * You can customize further browser side call logic by passing in 144 * JavaScript code snippets via some optional parameters. In their order 145 * of use these are: 146 * 147 * <tt>'confirm'</tt>:: Adds confirmation dialog. 148 * <tt>'condition'</tt>:: Perform remote request conditionally 149 * by this expression. Use this to 150 * describe browser-side conditions when 151 * request should not be initiated. 152 * <tt>'before'</tt>:: Called before request is initiated. 153 * <tt>'after'</tt>:: Called immediately after request was 154 * initiated and before <tt>'loading'</tt>. 155 * <tt>'submit'</tt>:: Specifies the DOM element ID that's used 156 * as the parent of the form elements. By 157 * default this is the current form, but 158 * it could just as well be the ID of a 159 * table row or any other DOM element. 160 */ 161 function link_to_remote($name, $options = array(), $html_options = array()) 162 { 163 return $this->_controller->javascript_helper->link_to_function($name, $this->remote_function($options), $html_options); 164 165 } 166 167 168 /** 169 * Periodically calls the specified url (<tt>options[:url]</tt>) every 170 * <tt>options['frequency']</tt> seconds (default is 10). Usually used to 171 * update a specified div (<tt>options['update']</tt>) with the results 172 * of the remote call. The options for specifying the target with 'url' 173 * and defining callbacks is the same as link_to_remote. 174 */ 175 function periodically_call_remote($options = array()) 176 { 177 $frequency = !empty($options['frequency']) ? $options['frequency'] : 10; // every ten seconds by default 178 $code = "new PeriodicalExecuter(function() {".$this->remote_function($options)."}, {$frequency})"; 179 return $this->_controller->javascript_helper->javascript_tag($code); 180 } 181 182 /** 183 * Returns a form tag that will submit using XMLHttpRequest in the 184 * background instead of the regular reloading POST arrangement. Even 185 * though it's using JavaScript to serialize the form elements, the form 186 * submission will work just like a regular submission as viewed by the 187 * receiving side (all elements available in @params). The options for 188 * specifying the target with 'url' and defining callbacks is the same as 189 * link_to_remote. 190 * 191 * A 'fall-through' target for browsers that doesn't do JavaScript can be 192 * specified with the 'action'/'method' options on 'html'. 193 * 194 * Example: 195 * $prototype_helper->form_remote_tag('html' => array('action' => $this->_controller->url_helper->url_for(array('controller' => 'some', 'action' => 'place'))); 196 * $prototype_helper->form_remote_tag('url' => array('controller' => 'foo', 'action' => 'bar'), 'update' => 'div_to_update', html => array('id' => 'form_id')); 197 * 198 * The Hash passed to the 'html' key is equivalent to the options (2nd) 199 * argument in the FormTagHelper.form_tag method. 200 * 201 * By default the fall-through action is the same as the one specified in 202 * the 'url' (and the default method is 'post'). 203 */ 204 function form_remote_tag($options = array()) 205 { 206 207 $options['url'] = empty($options['url']) ? array() : $options['url']; 208 209 $options['form'] = true; 210 $options['html'] = empty($options['html']) ? array() : $options['html']; 211 $options['html']['onsubmit'] = $this->remote_function($options).'; return false;'; 212 $options['html']['action'] = !empty($options['html']['action']) ? $options['html']['action'] : (is_array($options['url']) ? $this->_controller->url_helper->url_for($options['url']) : $options['url']); 213 $options['html']['method'] = !empty($options['html']['method']) ? $options['html']['method'] : 'post'; 214 215 return $this->_controller->tag_helper->tag('form', $options['html'], true); 216 217 } 218 219 /** 220 * Works like form_remote_tag, but uses form_for semantics. 221 */ 222 function remote_form_for($object_name, $object, $options = array(), $proc) 223 { 224 //$this->_controller->text_helper->concat($this->_controller->form_remote_tag($options),proc.binding); 225 return $this->_controller->form_helper->fields_for($object_name,$object,$proc); 226 //return $this->_controller->text_helper->concat('</form>', proc.binding); 227 } 228 229 /* Alias: remote_form_for */ 230 function form_remote_for($object_name, $object, $options = array(), $proc) 231 { 232 return $this->remote_form_for($object_name, $object, $options, $proc); 233 } 234 235 /** 236 * Returns a button input tag that will submit form using XMLHttpRequest 237 * in the background instead of regular reloading POST arrangement. 238 * <tt>options</tt> argument is the same as in <tt>form_remote_tag</tt>. 239 */ 240 function submit_to_remote($name, $value, $options = array()) 241 { 242 $options['with'] = !empty($options['with']) ? $options['with'] : 'Form.serialize(this.form)'; 243 244 $options['html'] = empty($options['html']) ? array() : $options['html']; 245 $options['html']['type'] = 'button'; 246 $options['html']['onclick'] = $this->remote_function($options).'; return false;'; 247 $options['html']['name'] = $name; 248 $options['html']['value'] = $value; 249 250 return $this->_controller->tag_helper->tag('input', $options['html'], false); 251 } 252 253 /** 254 * Returns a JavaScript function (or expression) that'll update a DOM 255 * element according to the options passed. 256 * 257 * * <tt>'content'</tt>: The content to use for updating. Can be left out if using block, see example. 258 * * <tt>'action'</tt>: Valid options are 'update' (assumed by default), 'empty', 'remove' 259 * * <tt>'position'</tt> If the 'action' is 'update', you can optionally 260 * specify one of the following positions: 'before', 'top', 'bottom', 'after'. 261 * 262 * Examples: 263 * <?= $javascript_helper->javascript_tag($prototype_helper->update_element_function('products', array('position' => 'bottom'), array('content' => '<p>New product!</p>')) ?> 264 * 265 * <% replacement_function = update_element_function("products") do %> 266 * <p>Product 1</p> 267 * <p>Product 2</p> 268 * <% end %> 269 * <%= javascript_tag(replacement_function) %> 270 * 271 * This method can also be used in combination with remote method call 272 * where the result is evaluated afterwards to cause multiple updates on 273 * a page. Example: 274 * 275 * * Calling view 276 * <?= $this->_controller->form_helper->form_remote_tag(array('url' => array('action' => 'buy')), array('complete' => evaluate_remote_response)) ?> 277 * all the inputs here... 278 * 279 * * Controller action 280 * function buy(){ 281 * $this->_controller->product = $this->_controller->Product->find(1); 282 * } 283 * 284 * * Returning view 285 * <?= $prototype_helper->update_element_function('cart', array('action' => 'update', 'position' => 'bottom', 'content' => "<p>New Product: {$product.name}</p>")) ?> 286 * 287 * <% update_element_function("status", :binding => binding) do %> 288 * You've bought a new product! 289 * <% end %> 290 * 291 * Notice how the second call doesn't need to be in an ERb output block 292 * since it uses a block and passes in the binding to render directly. 293 * This trick will however only work in ERb (not Builder or other 294 * template forms). 295 * 296 * See also JavaScriptGenerator and update_page. 297 */ 298 function update_element_function($element_id, $options = array()) 299 { 300 $content = !empty($options['content']) ? $this->_controller->javascript_helper->escape_javascript($options['content']) : ''; 301 $content = empty($content) && func_num_args() == 3 ? func_get_arg(2) : (is_string($options) ? $options : $content); 302 $action = !empty($options['action']) ? $options['action'] : 'update'; 303 304 switch ($action) { 305 306 case 'update': 307 if (!empty($options['position'])){ 308 $javascript_function = "new Insertion.".AkInflector::camelize($options['position'])."('{$element_id}','{$content}')"; 309 }else{ 310 $javascript_function = "$('{$element_id}').innerHTML = '{$content}'"; 311 } 312 break; 313 314 case 'empty': 315 $javascript_function = "$('{$element_id}').innerHTML = ''"; 316 break; 317 318 case 'remove': 319 $javascript_function = "Element.remove('{$element_id}')"; 320 break; 321 322 default: 323 trigger_error(Ak::t('Invalid action, choose one of update, remove, empty'), E_USER_WARNING); 324 break; 325 } 326 327 $javascript_function .= ";\n"; 328 return !empty($options['binding']) ? $this->_controller->text_helper->concat($javascript_function, $options['binding']) : $javascript_function; 329 } 330 331 /** 332 * Returns 'eval(request.responseText)' which is the JavaScript function 333 * that form_remote_tag can call in 'complete' to evaluate a multiple 334 * update return document using update_element_function calls. 335 */ 336 function evaluate_remote_response() 337 { 338 return "eval(request.responseText)"; 339 } 340 341 /** 342 * Returns the JavaScript needed for a remote function. 343 * Takes the same arguments as link_to_remote. 344 * 345 * Example: 346 * <select id="options" onchange="<?= $prototype_helper->remote_function(array('update' => 'options', 'url' => array('action' => 'update_options' )) ?>"> 347 * <option value="0">Hello</option> 348 * <option value="1">World</option> 349 * </select> 350 */ 351 function remote_function($options = array()) 352 { 353 354 $javascript_options = $this->_optionsForAjax($options); 355 356 if (!empty($options['update'])) { 357 358 if (is_array($options['update'])) { 359 $update = array(); 360 if (!empty($options['update']['success'])) { 361 $update[] = "success:'{$options['update']['success']}'"; 362 } 363 if (!empty($options['update']['failure'])) { 364 $update[] = "failure:'{$options['update']['failure']}'"; 365 } 366 $update = '{'. join(',',$update). '}'; 367 }else{ 368 $update = ''; 369 $update .= "'{$options['update']}'"; 370 } 371 } 372 $function = empty($update) ? "new Ajax.Request(" : "new Ajax.Updater({$update}, "; 373 $function .= "'".(is_array($options['url'])?$this->_controller->url_helper->url_for($options['url']):$options['url'])."'"; 374 $function .= ", {$javascript_options})"; 375 376 if (!empty($options['before'])) { 377 $function = "{$options['before']}; {$function}"; 378 } 379 if (!empty($options['after'])) { 380 $function = "{$function}; {$options['after']}"; 381 } 382 if (!empty($options['condition'])) { 383 $function = "if ({$options['condition']}) { {$function}; }"; 384 } 385 if (!empty($options['confirm'])) { 386 $function = "if (confirm('".$this->_controller->javascript_helper->escape_javascript($options['confirm'])."')) { {$function}; }"; 387 } 388 389 return $function; 390 } 391 392 /** 393 * Observes the field with the DOM ID specified by +field_id+ and makes 394 * an Ajax call when its contents have changed. 395 * 396 * Required +options+ are: 397 * <tt>'url</tt>:: +url_for+-style options for the action to call 398 * when the field has changed. 399 * 400 * Additional options are: 401 * <tt>frequency</tt>:: The frequency (in seconds) at which changes to 402 * this field will be detected. Not setting this 403 * option at all or to a value equal to or less than 404 * zero will use event based observation instead of 405 * time based observation. 406 * <tt>update</tt>:: Specifies the DOM ID of the element whose 407 * innerHTML should be updated with the 408 * XMLHttpRequest response text. 409 * <tt>with</tt>:: A JavaScript expression specifying the 410 * parameters for the XMLHttpRequest. This defaults 411 * to 'value', which in the evaluated context 412 * refers to the new field value. 413 * <tt>on</tt>:: Specifies which event handler to observe. By default, 414 * it's set to "changed" for text fields and areas and 415 * "click" for radio buttons and checkboxes. With this, 416 * you can specify it instead to be "blur" or "focus" or 417 * any other event. 418 * 419 * Additionally, you may specify any of the options documented in 420 * link_to_remote. 421 */ 422 function observe_field($field_id, $options = array()) 423 { 424 if (!empty($options['frequency']) && $options['frequency']>0) { 425 return $this->_buildObserver('Form.Element.Observer', $field_id, $options); 426 }else{ 427 return $this->_buildObserver('Form.Element.EventObserver', $field_id, $options); 428 } 429 } 430 431 /** 432 * Like +observe_field+, but operates on an entire form identified by the 433 * DOM ID +form_id+. +options+ are the same as +observe_field+, except 434 * the default value of the <tt>with</tt> option evaluates to the 435 * serialized (request string) value of the form. 436 */ 437 function observe_form($form_id, $options = array()) 438 { 439 if (!empty($options['frequency']) && $options['frequency']>0) { 440 return $this->_buildObserver('Form.Observer', $form_id, $options); 441 }else{ 442 return $this->_buildObserver('Form.EventObserver', $form_id, $options); 443 } 444 } 445 446 447 function _buildObserver($class, $name, $options = array()) 448 { 449 if(!empty($options['with']) && !strstr($options['with'],'=')){ 450 $options['with'] = "'{$options['with']}=' + value"; 451 }elseif(!empty($options['update'])){ 452 $options['with'] = empty($options['with']) ? 'value' : $options['with']; 453 } 454 $callback = empty($options['function']) ? $this->remote_function($options) : $options['function']; 455 $javascript = "new {$class}('{$name}', "; 456 $javascript .= empty($options['frequency']) ? '' : "{$options['frequency']}, "; 457 $javascript .= "function(element, value) {"; 458 $javascript .= "{$callback}}"; 459 $javascript .= empty($options['on']) ? '' : ", '{$options['on']}'"; 460 $javascript .= ")"; 461 462 return $this->_controller->javascript_helper->javascript_tag($javascript); 463 } 464 465 function _buildCallbacks($options) 466 { 467 $callbacks = array(); 468 $this->callbacks = $this->getCallbacks(); 469 foreach ($options as $callback=>$code){ 470 if(in_array($callback, $this->callbacks)){ 471 $name = 'on' . ucfirst($callback); 472 $callbacks[$name] = "function(request){{$code};}"; 473 } 474 } 475 return $callbacks; 476 } 477 478 function _optionsForAjax($options) 479 { 480 $js_options = $this->_buildCallbacks($options); 481 482 empty($options['type']) ? null : ($js_options['asynchronous'] = $options['type'] != 'synchronous' ? 'asynchronous' : 'synchronous'); 483 empty($options['method']) ? null : $js_options['method'] = $this->_methodOptionToString($options['method']); 484 empty($options['position']) ? null : $js_options['insertion'] = "Insertion.".AkInflector::camelize($options['position']); 485 isset($options['script']) ? $js_options['evalScripts'] = 'true' : null; 486 487 if(!empty($options['form'])){ 488 $js_options['parameters'] = 'Form.serialize(this)'; 489 }elseif(!empty($options['submit'])){ 490 $js_options['parameters'] = "Form.serialize('{$options['submit']}')"; 491 }elseif(!empty($options['with'])){ 492 $js_options['parameters'] = $options['with']; 493 } 494 495 return $this->_controller->javascript_helper->_options_for_javascript($js_options); 496 } 497 498 function _methodOptionToString($method) 499 { 500 return is_string($method) && substr($method,0,1) == "'" ? $method : "'$method'"; 501 } 502 } 503 504 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Mon Oct 27 12:43:49 2008 | Cross-referenced by PHPXref 0.6 |