comparison cake/libs/view/helpers/javascript.php @ 0:261e66bd5a0c

hg init
author Shoshi TAMAKI <shoshi@cr.ie.u-ryukyu.ac.jp>
date Sun, 24 Jul 2011 21:08:31 +0900
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:261e66bd5a0c
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-2010, 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-2010, 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 */
30 class 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 }