source: Dev/branches/cakephp/cake/libs/view/helpers/javascript.php @ 126

Last change on this file since 126 was 126, checked in by fpvanagthoven, 14 years ago

Cakephp branch.

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