Rearranging scripts to reduce the hassle of updating local application whenever scrip...
[akelos.git] / lib / AkActionView / helpers / prototype_helper.php
blob11ef98f0e04cafa121636a8ccc4fa6ccdd03e616
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
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 // +----------------------------------------------------------------------+
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>
21 require_once(AK_LIB_DIR.DS.'AkActionView'.DS.'helpers'.DS.'javascript_helper.php');
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.
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.
37 * See link_to_remote for documentation of options common to all Ajax
38 * helpers.
40 * See also ActionView/helpers/scriptaculous_helper.php for helpers which work
41 * with the Scriptaculous controls and visual effects library.
43 * See JavaScriptGenerator for information on updating multiple elements
44 * on the page in an Ajax response.
46 class PrototypeHelper extends AkActionViewHelper
48 function getCallbacks()
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;
57 $this->callbacks = $callbacks;
59 return $this->callbacks;
62 function getAjaxOptions()
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;
72 $this->ajax_options = $ajax_options;
74 return $this->ajax_options;
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.
86 * Examples:
87 * $prototype_helper->link_to_remote('Delete this post', array('url' => array('action' => 'destroy', 'id' => $_POST['id']), array('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'));
90 * You can also specify a hash 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:
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');
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>.
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.
106 * To access the server response, use <tt>request.responseText</tt>, to
107 * find out the HTTP status, use <tt>request.status</tt>.
109 * Example:
111 * $prototype_helper->link_to_remote($word, array('url' => array('action' => 'undo', 'n' => $word_counter) , 'complete' => 'undoRequestCompleted(request)');
113 * The callbacks that may be specified are (in order):
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).
131 * You can further refine <tt>'success'</tt> and <tt>'failure'</tt> by
132 * adding additional callbacks for specific status codes.
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 + '!')"));
137 * A status code callback overrides the success/failure handlers if present.
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>.
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:
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.
161 function link_to_remote($name, $options = array(), $html_options = array())
163 return $this->_controller->javascript_helper->link_to_function($name, $this->remote_function($options), $html_options);
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.
175 function periodically_call_remote($options = array())
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);
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.
191 * A 'fall-through' target for browsers that doesn't do JavaScript can be
192 * specified with the 'action'/'method' options on 'html'.
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'));
198 * The Hash passed to the 'html' key is equivalent to the options (2nd)
199 * argument in the FormTagHelper.form_tag method.
201 * By default the fall-through action is the same as the one specified in
202 * the 'url' (and the default method is 'post').
204 function form_remote_tag($options = array())
207 $options['url'] = empty($options['url']) ? array() : $options['url'];
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';
215 return $this->_controller->tag_helper->tag('form', $options['html'], true);
220 * Works like form_remote_tag, but uses form_for semantics.
222 function remote_form_for($object_name, $object, $options = array(), $proc)
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);
229 /* Alias: remote_form_for */
230 function form_remote_for($object_name, $object, $options = array(), $proc)
232 return $this->remote_form_for($object_name, $object, $options, $proc);
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>.
240 function submit_to_remote($name, $value, $options = array())
242 $options['with'] = !empty($options['with']) ? $options['with'] : 'Form.serialize(this.form)';
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;
250 return $this->_controller->tag_helper->tag('input', $options['html'], false);
254 * Returns a JavaScript function (or expression) that'll update a DOM
255 * element according to the options passed.
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'.
262 * Examples:
263 * <?= $javascript_helper->javascript_tag($prototype_helper->update_element_function('products', array('position' => 'bottom'), array('content' => '<p>New product!</p>')) ?>
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) %>
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:
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...
279 * * Controller action
280 * function buy(){
281 * $this->_controller->product = $this->_controller->Product->find(1);
284 * * Returning view
285 * <?= $prototype_helper->update_element_function('cart', array('action' => 'update', 'position' => 'bottom', 'content' => "<p>New Product: {$product.name}</p>")) ?>
287 * <% update_element_function("status", :binding => binding) do %>
288 * You've bought a new product!
289 * <% end %>
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).
296 * See also JavaScriptGenerator and update_page.
298 function update_element_function($element_id, $options = array())
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';
304 switch ($action) {
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}'";
312 break;
314 case 'empty':
315 $javascript_function = "$('{$element_id}').innerHTML = ''";
316 break;
318 case 'remove':
319 $javascript_function = "Element.remove('{$element_id}')";
320 break;
322 default:
323 trigger_error(Ak::t('Invalid action, choose one of update, remove, empty'), E_USER_WARNING);
324 break;
327 $javascript_function .= ";\n";
328 return !empty($options['binding']) ? $this->_controller->text_helper->concat($javascript_function, $options['binding']) : $javascript_function;
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.
336 function evaluate_remote_response()
338 return "eval(request.responseText)";
342 * Returns the JavaScript needed for a remote function.
343 * Takes the same arguments as link_to_remote.
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>
351 function remote_function($options = array())
354 $javascript_options = $this->_optionsForAjax($options);
356 if (!empty($options['update'])) {
358 if (is_array($options['update'])) {
359 $update = array();
360 if (!empty($options['update']['success'])) {
361 $update[] = "success:'{$options['update']['success']}'";
363 if (!empty($options['update']['failure'])) {
364 $update[] = "failure:'{$options['update']['failure']}'";
366 $update = '{'. join(',',$update). '}';
367 }else{
368 $update = '';
369 $update .= "'{$options['update']}'";
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})";
376 if (!empty($options['before'])) {
377 $function = "{$options['before']}; {$function}";
379 if (!empty($options['after'])) {
380 $function = "{$function}; {$options['after']}";
382 if (!empty($options['condition'])) {
383 $function = "if ({$options['condition']}) { {$function}; }";
385 if (!empty($options['confirm'])) {
386 $function = "if (confirm('".$this->_controller->javascript_helper->escape_javascript($options['confirm'])."')) { {$function}; }";
389 return $function;
393 * Observes the field with the DOM ID specified by +field_id+ and makes
394 * an Ajax call when its contents have changed.
396 * Required +options+ are:
397 * <tt>'url</tt>:: +url_for+-style options for the action to call
398 * when the field has changed.
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.
419 * Additionally, you may specify any of the options documented in
420 * link_to_remote.
422 function observe_field($field_id, $options = array())
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);
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.
437 function observe_form($form_id, $options = array())
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);
447 function _buildObserver($class, $name, $options = array())
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'];
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 .= ")";
462 return $this->_controller->javascript_helper->javascript_tag($javascript);
465 function _buildCallbacks($options)
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};}";
475 return $callbacks;
478 function _optionsForAjax($options)
480 $js_options = $this->_buildCallbacks($options);
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;
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'];
495 return $this->_controller->javascript_helper->_options_for_javascript($js_options);
498 function _methodOptionToString($method)
500 return is_string($method) && substr($method,0,1) == "'" ? $method : "'$method'";