JavascriptHelper.php 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. <?php
  2. /*
  3. * This file is part of the symfony package.
  4. * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
  5. * (c) 2004 David Heinemeier Hansson
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. /**
  11. * JavascriptHelper.
  12. *
  13. * @package symfony
  14. * @subpackage helper
  15. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  16. * @author John Christopher <john.christopher@symfony-project.com>
  17. * @author David Heinemeier Hansson
  18. * @author Fabian Lange <fabian.lange@symfony-project.com>
  19. * @version SVN: $Id: JavascriptHelper.php 17385 2009-04-17 01:11:22Z dwhittle $
  20. */
  21. /*
  22. * Provides a set of helpers for calling JavaScript functions and, most importantly,
  23. * to call remote methods using what has been labelled AJAX[http://www.adaptivepath.com/publications/essays/archives/000385.php].
  24. * This means that you can call actions in your controllers without reloading the page,
  25. * but still update certain parts of it using injections into the DOM.
  26. * The common use case is having a form that adds a new element to a list without reloading the page.
  27. *
  28. * To be able to use the JavaScript helpers, you must include the Prototype JavaScript Framework
  29. * and for some functions script.aculo.us (which both come with symfony) on your pages.
  30. * Choose one of these options:
  31. *
  32. * * Use <tt><?php echo javascript_include_tag :defaults ?></tt> in the HEAD section of your page (recommended):
  33. * The function will return references to the JavaScript files created by the +rails+ command in your
  34. * <tt>public/javascripts</tt> directory. Using it is recommended as the browser can then cache the libraries
  35. * instead of fetching all the functions anew on every request.
  36. * * Use <tt><?php echo javascript_include_tag 'prototype' ?></tt>: As above, but will only include the Prototype core library,
  37. * which means you are able to use all basic AJAX functionality. For the script.aculo.us-based JavaScript helpers,
  38. * like visual effects, autocompletion, drag and drop and so on, you should use the method described above.
  39. * * Use <tt><?php echo define_javascript_functions ?></tt>: this will copy all the JavaScript support functions within a single
  40. * script block.
  41. *
  42. * For documentation on +javascript_include_tag+ see ActionView::Helpers::AssetTagHelper.
  43. *
  44. * If you're the visual type, there's an AJAX movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating
  45. * the use of form_remote_tag.
  46. */
  47. function get_callbacks()
  48. {
  49. static $callbacks;
  50. if (!$callbacks)
  51. {
  52. $callbacks = array_merge(array(
  53. 'uninitialized', 'loading', 'loaded', 'interactive', 'complete', 'failure', 'success'
  54. ), range(100, 599));
  55. }
  56. return $callbacks;
  57. }
  58. function get_ajax_options()
  59. {
  60. static $ajax_options;
  61. if (!$ajax_options)
  62. {
  63. $ajax_options = array_merge(array(
  64. 'before', 'after', 'condition', 'url', 'asynchronous', 'method',
  65. 'insertion', 'position', 'form', 'with', 'update', 'script'
  66. ), get_callbacks());
  67. }
  68. return $ajax_options;
  69. }
  70. /**
  71. * Returns a link that'll trigger a javascript function using the
  72. * onclick handler and return false after the fact.
  73. *
  74. * Examples:
  75. * <?php echo link_to_function('Greeting', "alert('Hello world!')") ?>
  76. * <?php echo link_to_function(image_tag('delete'), "do_delete()", array('confirm' => 'Really?')) ?>
  77. */
  78. function link_to_function($name, $function, $html_options = array())
  79. {
  80. $html_options = _parse_attributes($html_options);
  81. $html_options['href'] = isset($html_options['href']) ? $html_options['href'] : '#';
  82. if ( isset($html_options['confirm']) )
  83. {
  84. $confirm = escape_javascript($html_options['confirm']);
  85. $html_options['onclick'] = "if(window.confirm('$confirm')){ $function;}; return false;";
  86. }
  87. else
  88. {
  89. $html_options['onclick'] = $function.'; return false;';
  90. }
  91. return content_tag('a', $name, $html_options);
  92. }
  93. /**
  94. * Returns a button that'll trigger a javascript function using the
  95. * onclick handler and return false after the fact.
  96. *
  97. * Examples:
  98. * <?php echo button_to_function('Greeting', "alert('Hello world!')") ?>
  99. */
  100. function button_to_function($name, $function, $html_options = array())
  101. {
  102. $html_options = _parse_attributes($html_options);
  103. $html_options['onclick'] = $function.'; return false;';
  104. $html_options['type'] = 'button';
  105. $html_options['value'] = $name;
  106. return tag('input', $html_options);
  107. }
  108. /**
  109. * Returns an html button to a remote action defined by 'url' (using the
  110. * 'url_for()' format) that's called in the background using XMLHttpRequest.
  111. *
  112. * See link_to_remote() for details.
  113. *
  114. */
  115. function button_to_remote($name, $options = array(), $html_options = array())
  116. {
  117. return button_to_function($name, remote_function($options), $html_options);
  118. }
  119. /**
  120. * Returns a link to a remote action defined by 'url'
  121. * (using the 'url_for()' format) that's called in the background using
  122. * XMLHttpRequest. The result of that request can then be inserted into a
  123. * DOM object whose id can be specified with 'update'.
  124. * Usually, the result would be a partial prepared by the controller with
  125. * either 'render_partial()'.
  126. *
  127. * Examples:
  128. * <?php echo link_to_remote('Delete this post'), array(
  129. * 'update' => 'posts',
  130. * 'url' => 'destroy?id='.$post.id,
  131. * )) ?>
  132. * <?php echo link_to_remote(image_tag('refresh'), array(
  133. * 'update' => 'emails',
  134. * 'url' => '@list_emails',
  135. * )) ?>
  136. *
  137. * You can also specify a hash for 'update' to allow for
  138. * easy redirection of output to an other DOM element if a server-side error occurs:
  139. *
  140. * Example:
  141. * <?php echo link_to_remote('Delete this post', array(
  142. * 'update' => array('success' => 'posts', 'failure' => 'error'),
  143. * 'url' => 'destroy?id='.$post.id,
  144. * )) ?>
  145. *
  146. * Optionally, you can use the 'position' parameter to influence
  147. * how the target DOM element is updated. It must be one of
  148. * 'before', 'top', 'bottom', or 'after'.
  149. *
  150. * By default, these remote requests are processed asynchronous during
  151. * which various JavaScript callbacks can be triggered (for progress indicators and
  152. * the likes). All callbacks get access to the 'request' object,
  153. * which holds the underlying XMLHttpRequest.
  154. *
  155. * To access the server response, use 'request.responseText', to
  156. * find out the HTTP status, use 'request.status'.
  157. *
  158. * Example:
  159. * <?php echo link_to_remote($word, array(
  160. * 'url' => '@undo?n='.$word_counter,
  161. * 'complete' => 'undoRequestCompleted(request)'
  162. * )) ?>
  163. *
  164. * The callbacks that may be specified are (in order):
  165. *
  166. * 'loading' Called when the remote document is being
  167. * loaded with data by the browser.
  168. * 'loaded' Called when the browser has finished loading
  169. * the remote document.
  170. * 'interactive' Called when the user can interact with the
  171. * remote document, even though it has not
  172. * finished loading.
  173. * 'success' Called when the XMLHttpRequest is completed,
  174. * and the HTTP status code is in the 2XX range.
  175. * 'failure' Called when the XMLHttpRequest is completed,
  176. * and the HTTP status code is not in the 2XX
  177. * range.
  178. * 'complete' Called when the XMLHttpRequest is complete
  179. * (fires after success/failure if they are present).,
  180. *
  181. * You can further refine 'success' and 'failure' by adding additional
  182. * callbacks for specific status codes:
  183. *
  184. * Example:
  185. * <?php echo link_to_remote($word, array(
  186. * 'url' => '@rule',
  187. * '404' => "alert('Not found...? Wrong URL...?')",
  188. * 'failure' => "alert('HTTP Error ' + request.status + '!')",
  189. * )) ?>
  190. *
  191. * A status code callback overrides the success/failure handlers if present.
  192. *
  193. * If you for some reason or another need synchronous processing (that'll
  194. * block the browser while the request is happening), you can specify
  195. * 'type' => 'synchronous'.
  196. *
  197. * You can customize further browser side call logic by passing
  198. * in JavaScript code snippets via some optional parameters. In
  199. * their order of use these are:
  200. *
  201. * 'confirm' Adds confirmation dialog.
  202. * 'condition' Perform remote request conditionally
  203. * by this expression. Use this to
  204. * describe browser-side conditions when
  205. * request should not be initiated.
  206. * 'before' Called before request is initiated.
  207. * 'after' Called immediately after request was
  208. * initiated and before 'loading'.
  209. * 'submit' Specifies the DOM element ID that's used
  210. * as the parent of the form elements. By
  211. * default this is the current form, but
  212. * it could just as well be the ID of a
  213. * table row or any other DOM element.
  214. */
  215. function link_to_remote($name, $options = array(), $html_options = array())
  216. {
  217. return link_to_function($name, remote_function($options), $html_options);
  218. }
  219. /**
  220. * Periodically calls the specified url ('url') every 'frequency' seconds (default is 10).
  221. * Usually used to update a specified div ('update') with the results of the remote call.
  222. * The options for specifying the target with 'url' and defining callbacks is the same as 'link_to_remote()'.
  223. */
  224. function periodically_call_remote($options = array())
  225. {
  226. $frequency = isset($options['frequency']) ? $options['frequency'] : 10; // every ten seconds by default
  227. $code = 'new PeriodicalExecuter(function() {'.remote_function($options).'}, '.$frequency.')';
  228. return javascript_tag($code);
  229. }
  230. /**
  231. * Returns a form tag that will submit using XMLHttpRequest in the background instead of the regular
  232. * reloading POST arrangement. Even though it's using JavaScript to serialize the form elements, the form submission
  233. * will work just like a regular submission as viewed by the receiving side (all elements available in 'params').
  234. * The options for specifying the target with 'url' and defining callbacks are the same as 'link_to_remote()'.
  235. *
  236. * A "fall-through" target for browsers that don't do JavaScript can be specified
  237. * with the 'action'/'method' options on '$options_html'
  238. *
  239. * Example:
  240. * <?php echo form_remote_tag(array(
  241. * 'url' => '@tag_add',
  242. * 'update' => 'question_tags',
  243. * 'loading' => "Element.show('indicator'); \$('tag').value = ''",
  244. * 'complete' => "Element.hide('indicator');".visual_effect('highlight', 'question_tags'),
  245. * )) ?>
  246. *
  247. * The hash passed as a second argument is equivalent to the options (2nd) argument in the form_tag() helper.
  248. *
  249. * By default the fall-through action is the same as the one specified in the 'url'
  250. * (and the default method is 'post').
  251. */
  252. function form_remote_tag($options = array(), $options_html = array())
  253. {
  254. $options = _parse_attributes($options);
  255. $options_html = _parse_attributes($options_html);
  256. $options['form'] = true;
  257. $options_html['onsubmit'] = remote_function($options).' return false;';
  258. $options_html['action'] = isset($options_html['action']) ? $options_html['action'] : url_for($options['url']);
  259. $options_html['method'] = isset($options_html['method']) ? $options_html['method'] : 'post';
  260. return tag('form', $options_html, true);
  261. }
  262. /**
  263. * Returns a button input tag that will submit form using XMLHttpRequest in the background instead of regular
  264. * reloading POST arrangement. The '$options' argument is the same as in 'form_remote_tag()'.
  265. */
  266. function submit_to_remote($name, $value, $options = array(), $options_html = array())
  267. {
  268. $options = _parse_attributes($options);
  269. $options_html = _parse_attributes($options_html);
  270. if (!isset($options['with']))
  271. {
  272. $options['with'] = 'Form.serialize(this.form)';
  273. }
  274. $options_html['type'] = 'button';
  275. $options_html['onclick'] = remote_function($options).' return false;';
  276. $options_html['name'] = $name;
  277. $options_html['value'] = $value;
  278. return tag('input', $options_html, false);
  279. }
  280. /**
  281. * Returns a image submit tag that will submit form using XMLHttpRequest in the background instead of regular
  282. * reloading POST arrangement. The '$options' argument is the same as in 'form_remote_tag()'.
  283. */
  284. function submit_image_to_remote($name, $source, $options = array(), $options_html = array())
  285. {
  286. $options = _parse_attributes($options);
  287. $options_html = _parse_attributes($options_html);
  288. if (!isset($options['with']))
  289. {
  290. $options['with'] = 'Form.serialize(this.form)';
  291. }
  292. $options_html['type'] = 'image';
  293. $options_html['onclick'] = remote_function($options).' return false;';
  294. $options_html['name'] = $name;
  295. $options_html['src'] = image_path($source);
  296. if (!isset($options_html['alt']))
  297. {
  298. $path_pos = strrpos($source, '/');
  299. $dot_pos = strrpos($source, '.');
  300. $begin = $path_pos ? $path_pos + 1 : 0;
  301. $nb_str = ($dot_pos ? $dot_pos : strlen($source)) - $begin;
  302. $options_html['alt'] = ucfirst(substr($source, $begin, $nb_str));
  303. }
  304. return tag('input', $options_html, false);
  305. }
  306. /**
  307. * Returns a Javascript function (or expression) that will update a DOM element '$element_id'
  308. * according to the '$options' passed.
  309. *
  310. * Possible '$options' are:
  311. * 'content' The content to use for updating. Can be left out if using block, see example.
  312. * 'action' Valid options are 'update' (assumed by default), 'empty', 'remove'
  313. * 'position' If the 'action' is 'update', you can optionally specify one of the following positions:
  314. * 'before', 'top', 'bottom', 'after'.
  315. *
  316. * Example:
  317. * <?php echo javascript_tag(
  318. * update_element_function('products', array(
  319. * 'position' => 'bottom',
  320. * 'content' => "<p>New product!</p>",
  321. * ))
  322. * ) ?>
  323. *
  324. *
  325. * This method can also be used in combination with remote method call
  326. * where the result is evaluated afterwards to cause multiple updates on a page.
  327. *
  328. * Example:
  329. *
  330. * # Calling view
  331. * <?php echo form_remote_tag(array(
  332. * 'url' => '@buy',
  333. * 'complete' => evaluate_remote_response()
  334. * )) ?>
  335. * all the inputs here...
  336. *
  337. * # Target action
  338. * public function executeBuy()
  339. * {
  340. * $this->product = ProductPeer::retrieveByPk(1);
  341. * }
  342. *
  343. * # Returning view
  344. * <php echo update_element_function('cart', array(
  345. * 'action' => 'update',
  346. * 'position' => 'bottom',
  347. * 'content' => '<p>New Product: '.$product->getName().'</p>',
  348. * )) ?>
  349. */
  350. function update_element_function($element_id, $options = array())
  351. {
  352. sfContext::getInstance()->getResponse()->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  353. $content = escape_javascript(isset($options['content']) ? $options['content'] : '');
  354. $value = isset($options['action']) ? $options['action'] : 'update';
  355. switch ($value)
  356. {
  357. case 'update':
  358. if (isset($options['position']) && $options['position'])
  359. {
  360. $javascript_function = "\$('$element_id').insert('$content','".$options['position']."')";
  361. }
  362. else
  363. {
  364. $javascript_function = "\$('$element_id').update('$content')";
  365. }
  366. break;
  367. case 'empty':
  368. $javascript_function = "\$('$element_id').innerHTML = ''";
  369. break;
  370. case 'remove':
  371. $javascript_function = "\$('$element_id').remove()";
  372. break;
  373. default:
  374. throw new sfException('Invalid action, choose one of update, remove, empty.');
  375. }
  376. $javascript_function .= ";\n";
  377. return (isset($options['binding']) ? $javascript_function.$options['binding'] : $javascript_function);
  378. }
  379. /**
  380. * Returns 'eval(request.responseText)', which is the Javascript function that
  381. * 'form_remote_tag()' can call in 'complete' to evaluate a multiple update return document
  382. * using 'update_element_function()' calls.
  383. */
  384. function evaluate_remote_response()
  385. {
  386. return 'eval(request.responseText)';
  387. }
  388. /**
  389. * Returns the javascript needed for a remote function.
  390. * Takes the same arguments as 'link_to_remote()'.
  391. *
  392. * Example:
  393. * <select id="options" onchange="<?php echo remote_function(array('update' => 'options', 'url' => '@update_options')) ?>">
  394. * <option value="0">Hello</option>
  395. * <option value="1">World</option>
  396. * </select>
  397. */
  398. function remote_function($options)
  399. {
  400. sfContext::getInstance()->getResponse()->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  401. $javascript_options = _options_for_ajax($options);
  402. $update = '';
  403. if (isset($options['update']) && is_array($options['update']))
  404. {
  405. $update = array();
  406. if (isset($options['update']['success']))
  407. {
  408. $update[] = "success:'".$options['update']['success']."'";
  409. }
  410. if (isset($options['update']['failure']))
  411. {
  412. $update[] = "failure:'".$options['update']['failure']."'";
  413. }
  414. $update = '{'.join(',', $update).'}';
  415. }
  416. else if (isset($options['update']))
  417. {
  418. $update .= "'".$options['update']."'";
  419. }
  420. $function = !$update ? "new Ajax.Request(" : "new Ajax.Updater($update, ";
  421. $function .= '\''.url_for($options['url']).'\'';
  422. $function .= ', '.$javascript_options.')';
  423. if (isset($options['before']))
  424. {
  425. $function = $options['before'].'; '.$function;
  426. }
  427. if (isset($options['after']))
  428. {
  429. $function = $function.'; '.$options['after'];
  430. }
  431. if (isset($options['condition']))
  432. {
  433. $function = 'if ('.$options['condition'].') { '.$function.'; }';
  434. }
  435. if (isset($options['confirm']))
  436. {
  437. $function = "if (confirm('".escape_javascript($options['confirm'])."')) { $function; }";
  438. if (isset($options['cancel']))
  439. {
  440. $function = $function.' else { '.$options['cancel'].' }';
  441. }
  442. }
  443. return $function.';';
  444. }
  445. /**
  446. * Observes the field with the DOM ID specified by '$field_id' and makes
  447. * an AJAX call when its contents have changed.
  448. *
  449. * Required '$options' are:
  450. * 'url' 'url_for()'-style options for the action to call
  451. * when the field has changed.
  452. *
  453. * Additional options are:
  454. * 'frequency' The frequency (in seconds) at which changes to
  455. * this field will be detected. Not setting this
  456. * option at all or to a value equal to or less than
  457. * zero will use event based observation instead of
  458. * time based observation.
  459. * 'update' Specifies the DOM ID of the element whose
  460. * innerHTML should be updated with the
  461. * XMLHttpRequest response text.
  462. * 'with' A JavaScript expression specifying the
  463. * parameters for the XMLHttpRequest. This defaults
  464. * to 'value', which in the evaluated context
  465. * refers to the new field value.
  466. *
  467. * Additionally, you may specify any of the options documented in
  468. * link_to_remote().
  469. */
  470. function observe_field($field_id, $options = array())
  471. {
  472. sfContext::getInstance()->getResponse()->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  473. if (isset($options['frequency']) && $options['frequency'] > 0)
  474. {
  475. return _build_observer('Form.Element.Observer', $field_id, $options);
  476. }
  477. else
  478. {
  479. return _build_observer('Form.Element.EventObserver', $field_id, $options);
  480. }
  481. }
  482. /**
  483. * Like 'observe_field()', but operates on an entire form identified by the
  484. * DOM ID '$form_id'. '$options' are the same as 'observe_field()', except
  485. * the default value of the 'with' option evaluates to the
  486. * serialized (request string) value of the form.
  487. */
  488. function observe_form($form_id, $options = array())
  489. {
  490. sfContext::getInstance()->getResponse()->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  491. if (isset($options['frequency']) && $options['frequency'] > 0)
  492. {
  493. return _build_observer('Form.Observer', $form_id, $options);
  494. }
  495. else
  496. {
  497. return _build_observer('Form.EventObserver', $form_id, $options);
  498. }
  499. }
  500. /**
  501. * Returns a JavaScript snippet to be used on the AJAX callbacks for starting
  502. * visual effects.
  503. *
  504. * Example:
  505. * <?php echo link_to_remote('Reload', array(
  506. * 'update' => 'posts',
  507. * 'url' => '@reload',
  508. * 'complete => visual_effect('highlight', 'posts', array('duration' => 0.5 )),
  509. * )) ?>
  510. *
  511. * If no '$element_id' is given, it assumes "element" which should be a local
  512. * variable in the generated JavaScript execution context. This can be used
  513. * for example with drop_receiving_element():
  514. *
  515. * <?php echo drop_receving_element( ..., array(
  516. * ...
  517. * 'loading' => visual_effect('fade'),
  518. * )) ?>
  519. *
  520. * This would fade the element that was dropped on the drop receiving element.
  521. *
  522. * You can change the behaviour with various options, see
  523. * http://script.aculo.us for more documentation.
  524. */
  525. function visual_effect($name, $element_id = false, $js_options = array())
  526. {
  527. $response = sfContext::getInstance()->getResponse();
  528. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  529. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/builder');
  530. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  531. $element = $element_id ? "'$element_id'" : 'element';
  532. if (in_array($name, array('toggle_appear', 'toggle_blind', 'toggle_slide')))
  533. {
  534. return "new Effect.toggle($element, '".substr($name, 7)."', "._options_for_javascript($js_options).");";
  535. }
  536. else
  537. {
  538. return "new Effect.".sfInflector::camelize($name)."($element, "._options_for_javascript($js_options).");";
  539. }
  540. }
  541. /**
  542. * Makes the elements with the DOM ID specified by '$element_id' sortable
  543. * by drag-and-drop and if an 'url' is specified make an AJAX call whenever
  544. * the sort order has changed. By default, the action called gets the
  545. * serialized sortable element as parameters.
  546. *
  547. * Example:
  548. * <php echo sortable_element($my_list, array(
  549. * 'url' => '@order',
  550. * )) ?>
  551. *
  552. * In the example, the action gets a '$my_list' array parameter
  553. * containing the values of the ids of elements the sortable consists
  554. * of, in the current order.
  555. *
  556. * You can change the behaviour with various options, see
  557. * http://script.aculo.us for more documentation.
  558. */
  559. function sortable_element($element_id, $options = array())
  560. {
  561. $response = sfContext::getInstance()->getResponse();
  562. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  563. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/builder');
  564. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  565. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/dragdrop');
  566. if (!isset($options['with']))
  567. {
  568. $options['with'] = "Sortable.serialize('$element_id')";
  569. }
  570. if (!isset($options['onUpdate']) && isset($options['url']))
  571. {
  572. $options['onUpdate'] = "function(){".remote_function($options)."}";
  573. }
  574. foreach (get_ajax_options() as $key)
  575. {
  576. unset($options[$key]);
  577. }
  578. foreach (array('tag', 'overlap', 'constraint', 'handle') as $option)
  579. {
  580. if (isset($options[$option]))
  581. {
  582. $options[$option] = "'{$options[$option]}'";
  583. }
  584. }
  585. if (isset($options['containment']))
  586. {
  587. $options['containment'] = _array_or_string_for_javascript($options['containment']);
  588. }
  589. if (isset($options['hoverclass']))
  590. {
  591. $options['hoverclass'] = "'{$options['hoverclass']}'";
  592. }
  593. if (isset($options['only']))
  594. {
  595. $options['only'] = _array_or_string_for_javascript($options['only']);
  596. }
  597. $scrollPosition = "";
  598. if (isset($options['scroll']))
  599. {
  600. $options['scroll'] = _array_or_string_for_javascript($options['scroll']);
  601. $scrollPosition = "Position.includeScrollOffsets = true;";
  602. }
  603. return javascript_tag($scrollPosition."Sortable.create('$element_id', "._options_for_javascript($options).")");
  604. }
  605. /**
  606. * Makes the element with the DOM ID specified by '$element_id' draggable.
  607. *
  608. * Example:
  609. * <?php echo draggable_element('my_image', array(
  610. * 'revert' => true,
  611. * )) ?>
  612. *
  613. * You can change the behaviour with various options, see
  614. * http://script.aculo.us for more documentation.
  615. */
  616. function draggable_element($element_id, $options = array())
  617. {
  618. $response = sfContext::getInstance()->getResponse();
  619. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  620. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/builder');
  621. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  622. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/dragdrop');
  623. return javascript_tag("new Draggable('$element_id', "._options_for_javascript($options).")");
  624. }
  625. /**
  626. * Makes the element with the DOM ID specified by '$element_id' receive
  627. * dropped draggable elements (created by 'draggable_element()') and make an AJAX call.
  628. * By default, the action called gets the DOM ID of the element as parameter.
  629. *
  630. * Example:
  631. * <?php drop_receiving_element('my_cart', array(
  632. * 'url' => 'cart/add',
  633. * )) ?>
  634. *
  635. * You can change the behaviour with various options, see
  636. * http://script.aculo.us for more documentation.
  637. */
  638. function drop_receiving_element($element_id, $options = array())
  639. {
  640. $response = sfContext::getInstance()->getResponse();
  641. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  642. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/builder');
  643. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  644. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/dragdrop');
  645. if (!isset($options['with']))
  646. {
  647. $options['with'] = "'id=' + encodeURIComponent(element.id)";
  648. }
  649. if (!isset($options['onDrop']))
  650. {
  651. $options['onDrop'] = "function(element){".remote_function($options)."}";
  652. }
  653. foreach (get_ajax_options() as $key)
  654. {
  655. unset($options[$key]);
  656. }
  657. if (isset($options['accept']))
  658. {
  659. $options['accept'] = _array_or_string_for_javascript($options['accept']);
  660. }
  661. if (isset($options['hoverclass']))
  662. {
  663. $options['hoverclass'] = "'{$options['hoverclass']}'";
  664. }
  665. return javascript_tag("Droppables.add('$element_id', "._options_for_javascript($options).")");
  666. }
  667. /**
  668. * Returns a JavaScript tag with the '$content' inside.
  669. * Example:
  670. * <?php echo javascript_tag("alert('All is good')") ?>
  671. * => <script type="text/javascript">alert('All is good')</script>
  672. */
  673. function javascript_tag($content)
  674. {
  675. return content_tag('script', javascript_cdata_section($content), array('type' => 'text/javascript'));
  676. }
  677. function javascript_cdata_section($content)
  678. {
  679. return "\n//".cdata_section("\n$content\n//")."\n";
  680. }
  681. /**
  682. * wrapper for script.aculo.us/prototype Ajax.Autocompleter.
  683. * @param string name value of input field
  684. * @param string default value for input field
  685. * @param array input tag options. (size, autocomplete, etc...)
  686. * @param array completion options. (use_style, etc...)
  687. *
  688. * @return string input field tag, div for completion results, and
  689. * auto complete javascript tags
  690. */
  691. function input_auto_complete_tag($name, $value, $url, $tag_options = array(), $completion_options = array())
  692. {
  693. $context = sfContext::getInstance();
  694. $tag_options = _convert_options($tag_options);
  695. $response = $context->getResponse();
  696. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  697. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  698. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/controls');
  699. $comp_options = _convert_options($completion_options);
  700. if (isset($comp_options['use_style']) && $comp_options['use_style'] == true)
  701. {
  702. $response->addStylesheet(sfConfig::get('sf_prototype_web_dir').'/css/input_auto_complete_tag');
  703. }
  704. $tag_options['id'] = get_id_from_name(isset($tag_options['id']) ? $tag_options['id'] : $name);
  705. $javascript = tag('input', array_merge(array('type' => 'text', 'name' => $name, 'value' => $value), _convert_options($tag_options)));
  706. $javascript .= content_tag('div', '' , array('id' => $tag_options['id'].'_auto_complete', 'class' => 'auto_complete'));
  707. $javascript .= _auto_complete_field($tag_options['id'], $url, $comp_options);
  708. return $javascript;
  709. }
  710. /**
  711. * wrapper for script.aculo.us/prototype Ajax.InPlaceEditor.
  712. * @param string name id of field that can be edited
  713. * @param string url of module/action to be called when ok is clicked
  714. * @param array editor tag options. (rows, cols, highlightcolor, highlightendcolor, etc...)
  715. *
  716. * @return string javascript to manipulate the id field to allow click and edit functionality
  717. */
  718. function input_in_place_editor_tag($name, $url, $editor_options = array())
  719. {
  720. $response = sfContext::getInstance()->getResponse();
  721. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  722. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  723. $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/controls');
  724. $editor_options = _convert_options($editor_options);
  725. return _in_place_editor($name, $url, $editor_options);
  726. }
  727. /**
  728. * Mark the start of a block that should only be shown in the browser if JavaScript
  729. * is switched on.
  730. */
  731. function if_javascript()
  732. {
  733. ob_start();
  734. }
  735. /**
  736. * Mark the end of a block that should only be shown in the browser if JavaScript
  737. * is switched on.
  738. */
  739. function end_if_javascript()
  740. {
  741. $content = ob_get_clean();
  742. echo javascript_tag("document.write('" . esc_js_no_entities($content) . "');");
  743. }
  744. /*
  745. * Makes an HTML element specified by the DOM ID '$field_id' become an in-place
  746. * editor of a property.
  747. *
  748. * A form is automatically created and displayed when the user clicks the element,
  749. * something like this:
  750. * <form id="myElement-in-place-edit-form" target="specified url">
  751. * <input name="value" text="The content of myElement"/>
  752. * <input type="submit" value="ok"/>
  753. * <a onclick="javascript to cancel the editing">cancel</a>
  754. * </form>
  755. *
  756. * The form is serialized and sent to the server using an AJAX call, the action on
  757. * the server should process the value and return the updated value in the body of
  758. * the reponse. The element will automatically be updated with the changed value
  759. * (as returned from the server).
  760. *
  761. * Required '$options' are:
  762. * 'url' Specifies the url where the updated value should
  763. * be sent after the user presses "ok".
  764. *
  765. * Some additional '$options' are:
  766. * 'rows' Number of rows (more than 1 will use a TEXTAREA)
  767. * 'cancel_text' The text on the cancel link. (default: "cancel")
  768. * 'save_text' The text on the save link. (default: "ok")
  769. * 'external_control' The id of an external control used to enter edit mode.
  770. * 'options' Pass through options to the AJAX call (see prototype's Ajax.Updater)
  771. * 'with' JavaScript snippet that should return what is to be sent
  772. * in the AJAX call, 'form' is an implicit parameter
  773. *
  774. * for details see: http://mir.aculo.us/2007/7/17/in-place-editing-the-summer-2007-rewrite/
  775. */
  776. function _in_place_editor($field_id, $url, $options = array())
  777. {
  778. $javascript = "new Ajax.InPlaceEditor(";
  779. $javascript .= "'$field_id', ";
  780. $javascript .= _array_or_string_for_javascript(url_for($url));
  781. // translate symfony option names to InPlaceEditor options
  782. if (isset($options['cancel_text']))
  783. {
  784. $options['cancelText'] = _array_or_string_for_javascript($options['cancel_text']);
  785. unset($options['cancel_text']);
  786. }
  787. if (isset($options['save_text']))
  788. {
  789. $options['okText'] = _array_or_string_for_javascript($options['save_text']);
  790. unset($options['save_text']);
  791. }
  792. if (isset($options['external_control']))
  793. {
  794. $options['externalControl'] = _array_or_string_for_javascript($options['external_control']);
  795. unset($options['external_control']);
  796. }
  797. if (isset($options['options']))
  798. {
  799. $options['ajaxOptions'] = $options['options'];
  800. unset($options['options']);
  801. }
  802. if (isset($options['with']))
  803. {
  804. $options['callback'] = "function(form, value) { return ".$options['with']." }";
  805. unset($options['with']);
  806. }
  807. if (isset($options['highlightcolor']))
  808. {
  809. $options['highlightColor'] = _array_or_string_for_javascript($options['highlightcolor']);
  810. unset($options['highlightcolor']);
  811. }
  812. if (isset($options['highlightendcolor']))
  813. {
  814. $options['highlightEndColor'] = _array_or_string_for_javascript($options['highlightendcolor']);
  815. unset($options['highlightendcolor']);
  816. }
  817. if (isset($options['loadTextURL']))
  818. {
  819. $options['loadTextURL'] = _array_or_string_for_javascript($options['loadTextURL']);
  820. }
  821. $javascript .= ', '._options_for_javascript($options);
  822. $javascript .= ');';
  823. return javascript_tag($javascript);
  824. }
  825. /**
  826. * wrapper for script.aculo.us/prototype Ajax.Autocompleter.
  827. * @param string id value of input field
  828. * @param string url of module/action to execute for autocompletion
  829. * @param array completion options
  830. * @return string javascript tag for Ajax.Autocompleter
  831. */
  832. function _auto_complete_field($field_id, $url, $options = array())
  833. {
  834. $javascript = "new Ajax.Autocompleter(";
  835. $javascript .= "'".get_id_from_name($field_id)."', ";
  836. if (isset($options['update']))
  837. {
  838. $javascript .= "'".$options['update']."', ";
  839. }
  840. else
  841. {
  842. $javascript .= "'".get_id_from_name($field_id)."_auto_complete', ";
  843. }
  844. $javascript .= _array_or_string_for_javascript(url_for($url));
  845. $js_options = array();
  846. if (isset($options['tokens']))
  847. {
  848. $js_options['tokens'] = _array_or_string_for_javascript($options['tokens']);
  849. }
  850. if (isset ($options['with']))
  851. {
  852. $js_options['callback'] = "function(element, value) { return ".$options['with']."}";
  853. }
  854. if (isset($options['indicator']))
  855. {
  856. $js_options['indicator'] = _array_or_string_for_javascript($options['indicator']);
  857. }
  858. if (isset($options['on_show']))
  859. {
  860. $js_options['onShow'] = $options['on_show'];
  861. }
  862. if (isset($options['on_hide']))
  863. {
  864. $js_options['onHide'] = $options['on_hide'];
  865. }
  866. if (isset($options['min_chars']))
  867. {
  868. $js_options['minChars'] = $options['min_chars'];
  869. }
  870. if (isset($options['frequency']))
  871. {
  872. $js_options['frequency'] = $options['frequency'];
  873. }
  874. if (isset($options['update_element']))
  875. {
  876. $js_options['updateElement'] = $options['update_element'];
  877. }
  878. if (isset($options['after_update_element']))
  879. {
  880. $js_options['afterUpdateElement'] = $options['after_update_element'];
  881. }
  882. if (isset($options['param_name']))
  883. {
  884. $js_options['paramName'] = "'".$options['param_name']."'";
  885. }
  886. $javascript .= ', '._options_for_javascript($js_options).');';
  887. return javascript_tag($javascript);
  888. }
  889. function _options_for_javascript($options)
  890. {
  891. $opts = array();
  892. foreach ($options as $key => $value)
  893. {
  894. $opts[] = $key.":"._boolean_for_javascript($value);
  895. }
  896. sort($opts);
  897. return '{'.join(', ', $opts).'}';
  898. }
  899. /**
  900. * converts the given PHP array or string to the corresponding javascript array or string.
  901. * javascript strings need to be single quoted.
  902. *
  903. * @param option (typically from option array)
  904. * @return string javascript string or array equivalent
  905. */
  906. function _array_or_string_for_javascript($option)
  907. {
  908. if (is_array($option))
  909. {
  910. return "['".join('\',\'', $option)."']";
  911. }
  912. else if (is_string($option) && $option[0] != "'")
  913. {
  914. return "'$option'";
  915. }
  916. return $option;
  917. }
  918. /**
  919. * converts the given PHP boolean to the corresponding javascript boolean.
  920. * booleans need to be true or false (php will print 1 or nothing).
  921. *
  922. * @param bool (typically from option array)
  923. * @return string javascript boolean equivalent
  924. */
  925. function _boolean_for_javascript($bool)
  926. {
  927. if (is_bool($bool))
  928. {
  929. return ($bool===true ? 'true' : 'false');
  930. }
  931. return $bool;
  932. }
  933. function _options_for_ajax($options)
  934. {
  935. $js_options = _build_callbacks($options);
  936. $js_options['asynchronous'] = (isset($options['type']) && ($options['type'] == 'synchronous')) ? false : true;
  937. if (isset($options['method'])) $js_options['method'] = _array_or_string_for_javascript($options['method']);
  938. if (isset($options['position'])) $js_options['insertion'] = "Insertion.".sfInflector::camelize($options['position']);
  939. $js_options['evalScripts'] = (!isset($options['script']) || $options['script'] == '0' || $options['script'] == false) ? false : true;
  940. if (isset($options['form']))
  941. {
  942. $js_options['parameters'] = 'Form.serialize(this)';
  943. }
  944. else if (isset($options['submit']))
  945. {
  946. $js_options['parameters'] = "Form.serialize(document.getElementById('{$options['submit']}'))";
  947. }
  948. else if (isset($options['with']))
  949. {
  950. $js_options['parameters'] = $options['with'];
  951. }
  952. return _options_for_javascript($js_options);
  953. }
  954. function _build_observer($klass, $name, $options = array())
  955. {
  956. if (!isset($options['with']) && isset($options['update']))
  957. {
  958. $options['with'] = 'value';
  959. }
  960. $callback = remote_function($options);
  961. $javascript = 'new '.$klass.'("'.$name.'", ';
  962. if (isset($options['frequency']) && $options['frequency'] > 0)
  963. {
  964. $javascript .= $options['frequency'].", ";
  965. }
  966. $javascript .= "function(element, value) {";
  967. $javascript .= $callback.'});';
  968. return javascript_tag($javascript);
  969. }
  970. function _build_callbacks($options)
  971. {
  972. $callbacks = array();
  973. foreach (get_callbacks() as $callback)
  974. {
  975. if (isset($options[$callback]))
  976. {
  977. $name = 'on'.ucfirst($callback);
  978. $code = $options[$callback];
  979. $callbacks[$name] = 'function(request, json){'.$code.'}';
  980. }
  981. }
  982. return $callbacks;
  983. }