PageRenderTime 75ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/cake/libs/view/helpers/javascript.php

https://github.com/msadouni/cakephp2x
PHP | 718 lines | 452 code | 50 blank | 216 comment | 83 complexity | b50d0dbb1f005ab48fa85fb1b1efd45d MD5 | raw file
  1. <?php
  2. /**
  3. * Javascript Helper class file.
  4. *
  5. * PHP Version 5.x
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package cake
  16. * @subpackage cake.cake.libs.view.helpers
  17. * @since CakePHP(tm) v 0.10.0.1076
  18. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  19. */
  20. /**
  21. * Javascript Helper class for easy use of JavaScript.
  22. *
  23. * JavascriptHelper encloses all methods needed while working with JavaScript.
  24. *
  25. * @package cake
  26. * @subpackage cake.cake.libs.view.helpers
  27. */
  28. class JavascriptHelper extends AppHelper {
  29. /**
  30. * Determines whether native JSON extension is used for encoding. Set by object constructor.
  31. *
  32. * @var boolean
  33. * @access public
  34. */
  35. public $useNative = false;
  36. /**
  37. * If true, automatically writes events to the end of a script or to an external JavaScript file
  38. * at the end of page execution
  39. *
  40. * @var boolean
  41. * @access public
  42. */
  43. public $enabled = true;
  44. /**
  45. * Indicates whether <script /> blocks should be written 'safely,' i.e. wrapped in CDATA blocks
  46. *
  47. * @var boolean
  48. * @access public
  49. */
  50. public $safe = false;
  51. /**
  52. * HTML tags used by this helper.
  53. *
  54. * @var array
  55. * @access public
  56. */
  57. public $tags = array(
  58. 'javascriptblock' => '<script type="text/javascript">%s</script>',
  59. 'javascriptlink' => '<script type="text/javascript" src="%s"></script>'
  60. );
  61. /**
  62. * Holds options passed to codeBlock(), saved for when block is dumped to output
  63. *
  64. * @var array
  65. * @access protected
  66. * @see JavascriptHelper::codeBlock()
  67. */
  68. protected $_blockOptions = array();
  69. /**
  70. * Caches events written by event() for output at the end of page execution
  71. *
  72. * @var array
  73. * @access protected
  74. * @see JavascriptHelper::event()
  75. */
  76. protected $_cachedEvents = array();
  77. /**
  78. * Indicates whether generated events should be cached for later output (can be written at the
  79. * end of the page, in the <head />, or to an external file).
  80. *
  81. * @var boolean
  82. * @access protected
  83. * @see JavascriptHelper::event()
  84. * @see JavascriptHelper::writeEvents()
  85. */
  86. protected $_cacheEvents = false;
  87. /**
  88. * Indicates whether cached events should be written to an external file
  89. *
  90. * @var boolean
  91. * @access protected
  92. * @see JavascriptHelper::event()
  93. * @see JavascriptHelper::writeEvents()
  94. */
  95. protected $_cacheToFile = false;
  96. /**
  97. * Indicates whether *all* generated JavaScript should be cached for later output
  98. *
  99. * @var boolean
  100. * @access protected
  101. * @see JavascriptHelper::codeBlock()
  102. * @see JavascriptHelper::blockEnd()
  103. */
  104. protected $_cacheAll = false;
  105. /**
  106. * Contains event rules attached with CSS selectors. Used with the event:Selectors JavaScript
  107. * library.
  108. *
  109. * @var array
  110. * @access protected
  111. * @see JavascriptHelper::event()
  112. * @link http://alternateidea.com/event-selectors/
  113. */
  114. protected $_rules = array();
  115. /**
  116. * @var string
  117. * @access private
  118. */
  119. private $__scriptBuffer = null;
  120. /**
  121. * Constructor. Checks for presence of native PHP JSON extension to use for object encoding
  122. *
  123. * @return array
  124. * @access public
  125. */
  126. public function __construct($options = array()) {
  127. if (!empty($options)) {
  128. foreach ($options as $key => $val) {
  129. if (is_numeric($key)) {
  130. $key = $val;
  131. $val = true;
  132. }
  133. switch ($key) {
  134. case 'cache':
  135. break;
  136. case 'safe':
  137. $this->safe = $val;
  138. break;
  139. }
  140. }
  141. }
  142. $this->useNative = function_exists('json_encode');
  143. return parent::__construct($options);
  144. }
  145. /**
  146. * Returns a JavaScript script tag.
  147. *
  148. * Options:
  149. *
  150. * - allowCache: boolean, designates whether this block is cacheable using the
  151. * current cache settings.
  152. * - safe: boolean, whether this block should be wrapped in CDATA tags. Defaults
  153. * to helper's object configuration.
  154. * - inline: whether the block should be printed inline, or written
  155. * to cached for later output (i.e. $scripts_for_layout).
  156. *
  157. * @param string $script The JavaScript to be wrapped in SCRIPT tags.
  158. * @param array $options Set of options:
  159. * @return string The full SCRIPT element, with the JavaScript inside it, or null,
  160. * if 'inline' is set to false.
  161. */
  162. public function codeBlock($script = null, $options = array()) {
  163. if (!empty($options) && !is_array($options)) {
  164. $options = array('allowCache' => $options);
  165. } elseif (empty($options)) {
  166. $options = array();
  167. }
  168. $defaultOptions = array('allowCache' => true, 'safe' => true, 'inline' => true);
  169. $options = array_merge($defaultOptions, $options);
  170. if (empty($script)) {
  171. $this->__scriptBuffer = @ob_get_contents();
  172. $this->_blockOptions = $options;
  173. $this->inBlock = true;
  174. @ob_end_clean();
  175. ob_start();
  176. return null;
  177. }
  178. if ($this->_cacheEvents && $this->_cacheAll && $options['allowCache']) {
  179. $this->_cachedEvents[] = $script;
  180. return null;
  181. }
  182. if ($options['safe'] || $this->safe) {
  183. $script = "\n" . '//<![CDATA[' . "\n" . $script . "\n" . '//]]>' . "\n";
  184. }
  185. if ($options['inline']) {
  186. return sprintf($this->tags['javascriptblock'], $script);
  187. } else {
  188. $view = ClassRegistry::getObject('view');
  189. $view->addScript(sprintf($this->tags['javascriptblock'], $script));
  190. }
  191. }
  192. /**
  193. * Ends a block of cached JavaScript code
  194. *
  195. * @return mixed
  196. */
  197. public function blockEnd() {
  198. if (!isset($this->inBlock) || !$this->inBlock) {
  199. return;
  200. }
  201. $script = @ob_get_contents();
  202. @ob_end_clean();
  203. ob_start();
  204. echo $this->__scriptBuffer;
  205. $this->__scriptBuffer = null;
  206. $options = $this->_blockOptions;
  207. $this->_blockOptions = array();
  208. $this->inBlock = false;
  209. if (empty($script)) {
  210. return null;
  211. }
  212. return $this->codeBlock($script, $options);
  213. }
  214. /**
  215. * Returns a JavaScript include tag (SCRIPT element). If the filename is prefixed with "/",
  216. * the path will be relative to the base path of your application. Otherwise, the path will
  217. * be relative to your JavaScript path, usually webroot/js.
  218. *
  219. * @param mixed $url String URL to JavaScript file, or an array of URLs.
  220. * @param boolean $inline If true, the <script /> tag will be printed inline,
  221. * otherwise it will be printed in the <head />, using $scripts_for_layout
  222. * @see JS_URL
  223. * @return string
  224. */
  225. public function link($url, $inline = true) {
  226. if (is_array($url)) {
  227. $out = '';
  228. foreach ($url as $i) {
  229. $out .= "\n\t" . $this->link($i, $inline);
  230. }
  231. if ($inline) {
  232. return $out . "\n";
  233. }
  234. return;
  235. }
  236. if (strpos($url, '://') === false) {
  237. if ($url[0] !== '/') {
  238. $url = JS_URL . $url;
  239. }
  240. if (strpos($url, '?') === false) {
  241. if (substr($url, -3) !== '.js') {
  242. $url .= '.js';
  243. }
  244. }
  245. $url = $this->webroot($this->assetTimestamp($url));
  246. if (Configure::read('Asset.filter.js')) {
  247. $pos = strpos($url, JS_URL);
  248. if ($pos !== false) {
  249. $url = substr($url, 0, $pos) . 'cjs/' . substr($url, $pos + strlen(JS_URL));
  250. }
  251. }
  252. }
  253. $out = sprintf($this->tags['javascriptlink'], $url);
  254. if ($inline) {
  255. return $out;
  256. } else {
  257. $view = ClassRegistry::getObject('view');
  258. $view->addScript($out);
  259. }
  260. }
  261. /**
  262. * Escape carriage returns and single and double quotes for JavaScript segments.
  263. *
  264. * @param string $script string that might have javascript elements
  265. * @return string escaped string
  266. */
  267. public function escapeScript($script) {
  268. $script = str_replace(array("\r\n", "\n", "\r"), '\n', $script);
  269. $script = str_replace(array('"', "'"), array('\"', "\\'"), $script);
  270. return $script;
  271. }
  272. /**
  273. * Escape a string to be JavaScript friendly.
  274. *
  275. * List of escaped ellements:
  276. * + "\r\n" => '\n'
  277. * + "\r" => '\n'
  278. * + "\n" => '\n'
  279. * + '"' => '\"'
  280. * + "'" => "\\'"
  281. *
  282. * @param string $script String that needs to get escaped.
  283. * @return string Escaped string.
  284. */
  285. public function escapeString($string) {
  286. App::import('Core', 'Multibyte');
  287. $escape = array("\r\n" => "\n", "\r" => "\n");
  288. $string = str_replace(array_keys($escape), array_values($escape), $string);
  289. return $this->_utf8ToHex($string);
  290. }
  291. /**
  292. * Encode a string into JSON. Converts and escapes necessary characters.
  293. *
  294. * @return void
  295. */
  296. function _utf8ToHex($string) {
  297. $length = strlen($string);
  298. $return = '';
  299. for ($i = 0; $i < $length; ++$i) {
  300. $ord = ord($string{$i});
  301. switch (true) {
  302. case $ord == 0x08:
  303. $return .= '\b';
  304. break;
  305. case $ord == 0x09:
  306. $return .= '\t';
  307. break;
  308. case $ord == 0x0A:
  309. $return .= '\n';
  310. break;
  311. case $ord == 0x0C:
  312. $return .= '\f';
  313. break;
  314. case $ord == 0x0D:
  315. $return .= '\r';
  316. break;
  317. case $ord == 0x22:
  318. case $ord == 0x2F:
  319. case $ord == 0x5C:
  320. case $ord == 0x27:
  321. $return .= '\\' . $string{$i};
  322. break;
  323. case (($ord >= 0x20) && ($ord <= 0x7F)):
  324. $return .= $string{$i};
  325. break;
  326. case (($ord & 0xE0) == 0xC0):
  327. if ($i + 1 >= $length) {
  328. $i += 1;
  329. $return .= '?';
  330. break;
  331. }
  332. $charbits = $string{$i} . $string{$i + 1};
  333. $char = Multibyte::utf8($charbits);
  334. $return .= sprintf('\u%04s', dechex($char[0]));
  335. $i += 1;
  336. break;
  337. case (($ord & 0xF0) == 0xE0):
  338. if ($i + 2 >= $length) {
  339. $i += 2;
  340. $return .= '?';
  341. break;
  342. }
  343. $charbits = $string{$i} . $string{$i + 1} . $string{$i + 2};
  344. $char = Multibyte::utf8($charbits);
  345. $return .= sprintf('\u%04s', dechex($char[0]));
  346. $i += 2;
  347. break;
  348. case (($ord & 0xF8) == 0xF0):
  349. if ($i + 3 >= $length) {
  350. $i += 3;
  351. $return .= '?';
  352. break;
  353. }
  354. $charbits = $string{$i} . $string{$i + 1} . $string{$i + 2} . $string{$i + 3};
  355. $char = Multibyte::utf8($charbits);
  356. $return .= sprintf('\u%04s', dechex($char[0]));
  357. $i += 3;
  358. break;
  359. case (($ord & 0xFC) == 0xF8):
  360. if ($i + 4 >= $length) {
  361. $i += 4;
  362. $return .= '?';
  363. break;
  364. }
  365. $charbits = $string{$i} . $string{$i + 1} . $string{$i + 2} . $string{$i + 3} . $string{$i + 4};
  366. $char = Multibyte::utf8($charbits);
  367. $return .= sprintf('\u%04s', dechex($char[0]));
  368. $i += 4;
  369. break;
  370. case (($ord & 0xFE) == 0xFC):
  371. if ($i + 5 >= $length) {
  372. $i += 5;
  373. $return .= '?';
  374. break;
  375. }
  376. $charbits = $string{$i} . $string{$i + 1} . $string{$i + 2} . $string{$i + 3} . $string{$i + 4} . $string{$i + 5};
  377. $char = Multibyte::utf8($charbits);
  378. $return .= sprintf('\u%04s', dechex($char[0]));
  379. $i += 5;
  380. break;
  381. }
  382. }
  383. return $return;
  384. }
  385. /**
  386. * Attach an event to an element. Used with the Prototype library.
  387. *
  388. * @param string $object Object to be observed
  389. * @param string $event event to observe
  390. * @param string $observer function to call
  391. * @param array $options Set options: useCapture, allowCache, safe
  392. * @return boolean true on success
  393. */
  394. public function event($object, $event, $observer = null, $options = array()) {
  395. if (!empty($options) && !is_array($options)) {
  396. $options = array('useCapture' => $options);
  397. } else if (empty($options)) {
  398. $options = array();
  399. }
  400. $defaultOptions = array('useCapture' => false);
  401. $options = array_merge($defaultOptions, $options);
  402. if ($options['useCapture'] == true) {
  403. $options['useCapture'] = 'true';
  404. } else {
  405. $options['useCapture'] = 'false';
  406. }
  407. $isObject = (
  408. strpos($object, 'window') !== false || strpos($object, 'document') !== false ||
  409. strpos($object, '$(') !== false || strpos($object, '"') !== false ||
  410. strpos($object, '\'') !== false
  411. );
  412. if ($isObject) {
  413. $b = "Event.observe({$object}, '{$event}', function(event) { {$observer} }, ";
  414. $b .= "{$options['useCapture']});";
  415. } elseif ($object[0] == '\'') {
  416. $b = "Event.observe(" . substr($object, 1) . ", '{$event}', function(event) { ";
  417. $b .= "{$observer} }, {$options['useCapture']});";
  418. } else {
  419. $chars = array('#', ' ', ', ', '.', ':');
  420. $found = false;
  421. foreach ($chars as $char) {
  422. if (strpos($object, $char) !== false) {
  423. $found = true;
  424. break;
  425. }
  426. }
  427. if ($found) {
  428. $this->_rules[$object] = $event;
  429. } else {
  430. $b = "Event.observe(\$('{$object}'), '{$event}', function(event) { ";
  431. $b .= "{$observer} }, {$options['useCapture']});";
  432. }
  433. }
  434. if (isset($b) && !empty($b)) {
  435. if ($this->_cacheEvents === true) {
  436. $this->_cachedEvents[] = $b;
  437. return;
  438. } else {
  439. return $this->codeBlock($b, array_diff_key($options, $defaultOptions));
  440. }
  441. }
  442. }
  443. /**
  444. * Cache JavaScript events created with event()
  445. *
  446. * @param boolean $file If true, code will be written to a file
  447. * @param boolean $all If true, all code written with JavascriptHelper will be sent to a file
  448. * @return null
  449. */
  450. public function cacheEvents($file = false, $all = false) {
  451. $this->_cacheEvents = true;
  452. $this->_cacheToFile = $file;
  453. $this->_cacheAll = $all;
  454. }
  455. /**
  456. * Gets (and clears) the current JavaScript event cache
  457. *
  458. * @param boolean $clear
  459. * @return string
  460. */
  461. public function getCache($clear = true) {
  462. $out = '';
  463. $rules = array();
  464. if (!empty($this->_rules)) {
  465. foreach ($this->_rules as $sel => $event) {
  466. $rules[] = "\t'{$sel}': function(element, event) {\n\t\t{$event}\n\t}";
  467. }
  468. }
  469. $data = implode("\n", $this->_cachedEvents);
  470. if (!empty($rules)) {
  471. $data .= "\nvar Rules = {\n" . implode(",\n\n", $rules) . "\n}";
  472. $data .= "\nEventSelectors.start(Rules);\n";
  473. }
  474. if ($clear) {
  475. $this->_rules = array();
  476. $this->_cacheEvents = false;
  477. $this->_cachedEvents = array();
  478. }
  479. return $data;
  480. }
  481. /**
  482. * Write cached JavaScript events
  483. *
  484. * @param boolean $inline If true, returns JavaScript event code. Otherwise it is added to the
  485. * output of $scripts_for_layout in the layout.
  486. * @param array $options Set options for codeBlock
  487. * @return string
  488. */
  489. public function writeEvents($inline = true, $options = array()) {
  490. $out = '';
  491. $rules = array();
  492. if (!$this->_cacheEvents) {
  493. return;
  494. }
  495. $data = $this->getCache();
  496. if (empty($data)) {
  497. return;
  498. }
  499. if ($this->_cacheToFile) {
  500. $filename = md5($data);
  501. if (!file_exists(JS . $filename . '.js')) {
  502. cache(str_replace(WWW_ROOT, '', JS) . $filename . '.js', $data, '+999 days', 'public');
  503. }
  504. $out = $this->link($filename);
  505. } else {
  506. $out = $this->codeBlock("\n" . $data . "\n", $options);
  507. }
  508. if ($inline) {
  509. return $out;
  510. } else {
  511. $view = ClassRegistry::getObject('view');
  512. $view->addScript($out);
  513. }
  514. }
  515. /**
  516. * Includes the Prototype Javascript library (and anything else) inside a single script tag.
  517. *
  518. * Note: The recommended approach is to copy the contents of
  519. * javascripts into your application's
  520. * public/javascripts/ directory, and use @see javascriptIncludeTag() to
  521. * create remote script links.
  522. *
  523. * @param string $script Script file to include
  524. * @param array $options Set options for codeBlock
  525. * @return string script with all javascript in/javascripts folder
  526. */
  527. public function includeScript($script = "", $options = array()) {
  528. if ($script == "") {
  529. $files = scandir(JS);
  530. $javascript = '';
  531. foreach ($files as $file) {
  532. if (substr($file, -3) == '.js') {
  533. $javascript .= file_get_contents(JS . "{$file}") . "\n\n";
  534. }
  535. }
  536. } else {
  537. $javascript = file_get_contents(JS . "$script.js") . "\n\n";
  538. }
  539. return $this->codeBlock("\n\n" . $javascript, $options);
  540. }
  541. /**
  542. * Generates a JavaScript object in JavaScript Object Notation (JSON)
  543. * from an array
  544. *
  545. * ### Options
  546. *
  547. * - block - Wraps the return value in a script tag if true. Default is false
  548. * - prefix - Prepends the string to the returned data. Default is ''
  549. * - postfix - Appends the string to the returned data. Default is ''
  550. * - stringKeys - A list of array keys to be treated as a string.
  551. * - quoteKeys - If false treats $stringKeys as a list of keys **not** to be quoted. Default is true.
  552. * - q - The type of quote to use. Default is '"'. This option only affects the keys, not the values.
  553. *
  554. * @param array $data Data to be converted
  555. * @param array $options Set of options: block, prefix, postfix, stringKeys, quoteKeys, q
  556. * @return string A JSON code block
  557. * @access public
  558. */
  559. public function object($data = array(), $options = array()) {
  560. if (!empty($options) && !is_array($options)) {
  561. $options = array('block' => $options);
  562. } else if (empty($options)) {
  563. $options = array();
  564. }
  565. $defaultOptions = array(
  566. 'block' => false, 'prefix' => '', 'postfix' => '',
  567. 'stringKeys' => array(), 'quoteKeys' => true, 'q' => '"'
  568. );
  569. $options = array_merge($defaultOptions, $options, array_filter(compact(array_keys($defaultOptions))));
  570. if (is_object($data)) {
  571. $data = get_object_vars($data);
  572. }
  573. $out = $keys = array();
  574. $numeric = true;
  575. if ($this->useNative) {
  576. $rt = json_encode($data);
  577. } else {
  578. if (is_null($data)) {
  579. return 'null';
  580. }
  581. if (is_bool($data)) {
  582. return $data ? 'true' : 'false';
  583. }
  584. if (is_array($data)) {
  585. $keys = array_keys($data);
  586. }
  587. if (!empty($keys)) {
  588. $numeric = (array_values($keys) === array_keys(array_values($keys)));
  589. }
  590. foreach ($data as $key => $val) {
  591. if (is_array($val) || is_object($val)) {
  592. $val = $this->object($val, array_merge($options, array('block' => false)));
  593. } else {
  594. $quoteStrings = (
  595. !count($options['stringKeys']) ||
  596. ($options['quoteKeys'] && in_array($key, $options['stringKeys'], true)) ||
  597. (!$options['quoteKeys'] && !in_array($key, $options['stringKeys'], true))
  598. );
  599. $val = $this->value($val, $quoteStrings);
  600. }
  601. if (!$numeric) {
  602. $val = $options['q'] . $this->value($key, false) . $options['q'] . ':' . $val;
  603. }
  604. $out[] = $val;
  605. }
  606. if (!$numeric) {
  607. $rt = '{' . implode(',', $out) . '}';
  608. } else {
  609. $rt = '[' . implode(',', $out) . ']';
  610. }
  611. }
  612. $rt = $options['prefix'] . $rt . $options['postfix'];
  613. if ($options['block']) {
  614. $rt = $this->codeBlock($rt, array_diff_key($options, $defaultOptions));
  615. }
  616. return $rt;
  617. }
  618. /**
  619. * Converts a PHP-native variable of any type to a JSON-equivalent representation
  620. *
  621. * @param mixed $val A PHP variable to be converted to JSON
  622. * @param boolean $quoteStrings If false, leaves string values unquoted
  623. * @return string a JavaScript-safe/JSON representation of $val
  624. */
  625. public function value($val, $quoteStrings = true) {
  626. switch (true) {
  627. case (is_array($val) || is_object($val)):
  628. $val = $this->object($val);
  629. break;
  630. case ($val === null):
  631. $val = 'null';
  632. break;
  633. case (is_bool($val)):
  634. $val = !empty($val) ? 'true' : 'false';
  635. break;
  636. case (is_int($val)):
  637. $val = $val;
  638. break;
  639. case (is_float($val)):
  640. $val = sprintf("%.11f", $val);
  641. break;
  642. default:
  643. $val = $this->escapeString($val);
  644. if ($quoteStrings) {
  645. $val = '"' . $val . '"';
  646. }
  647. break;
  648. }
  649. return $val;
  650. }
  651. /**
  652. * AfterRender callback. Writes any cached events to the view, or to a temp file.
  653. *
  654. * @return null
  655. */
  656. public function afterRender() {
  657. if (!$this->enabled) {
  658. return;
  659. }
  660. echo $this->writeEvents(true);
  661. }
  662. }
  663. ?>