1<?php 2/** 3 * CClientScript class file. 4 * 5 * @author Qiang Xue <qiang.xue@gmail.com> 6 * @link http://www.yiiframework.com/ 7 * @copyright Copyright © 2008-2010 Yii Software LLC 8 * @license http://www.yiiframework.com/license/ 9 */ 10 11/** 12 * CClientScript manages JavaScript and CSS stylesheets for views. 13 * 14 * @author Qiang Xue <qiang.xue@gmail.com> 15 * @version $Id: CClientScript.php 2406 2010-09-01 15:47:06Z mdomba $ 16 * @package system.web 17 * @since 1.0 18 */ 19class CClientScript extends CApplicationComponent 20{ 21 /** 22 * The script is rendered in the head section right before the title element. 23 */ 24 const POS_HEAD=0; 25 /** 26 * The script is rendered at the beginning of the body section. 27 */ 28 const POS_BEGIN=1; 29 /** 30 * The script is rendered at the end of the body section. 31 */ 32 const POS_END=2; 33 /** 34 * The script is rendered inside window onload function. 35 */ 36 const POS_LOAD=3; 37 /** 38 * The body script is rendered inside a jQuery ready function. 39 */ 40 const POS_READY=4; 41 42 /** 43 * @var boolean whether JavaScript should be enabled. Defaults to true. 44 */ 45 public $enableJavaScript=true; 46 /** 47 * @var array the mapping between script file names and the corresponding script URLs. 48 * The array keys are script file names (without directory part) and the array values are the corresponding URLs. 49 * If an array value is false, the corresponding script file will not be rendered. 50 * If an array key is '*.js' or '*.css', the corresponding URL will replace all 51 * all JavaScript files or CSS files, respectively. 52 * 53 * This property is mainly used to optimize the generated HTML pages 54 * by merging different scripts files into fewer and optimized script files. 55 * @since 1.0.3 56 */ 57 public $scriptMap=array(); 58 /** 59 * @var array the registered CSS files (CSS URL=>media type). 60 * @since 1.0.4 61 */ 62 protected $cssFiles=array(); 63 /** 64 * @var array the registered JavaScript files (position, key => URL) 65 * @since 1.0.4 66 */ 67 protected $scriptFiles=array(); 68 /** 69 * @var array the registered JavaScript code blocks (position, key => code) 70 * @since 1.0.5 71 */ 72 protected $scripts=array(); 73 /** 74 * @var array the register head meta tags. Each array element represents an option array 75 * that will be passed as the last parameter of {@link CHtml::metaTag}. 76 * @since 1.1.3 77 */ 78 protected $metaTags=array(); 79 /** 80 * @var array the register head link tags. Each array element represents an option array 81 * that will be passed as the last parameter of {@link CHtml::linkTag}. 82 * @since 1.1.3 83 */ 84 protected $linkTags=array(); 85 /** 86 * @var array the register css code blocks (key => array(CSS code, media type)). 87 * @since 1.1.3 88 */ 89 protected $css=array(); 90 /** 91 * @var integer Where the core scripts will be inserted in the page. 92 * This can be one of the CClientScript::POS_* constants. 93 * Defaults to CClientScript::POS_HEAD. 94 * @since 1.1.3 95 */ 96 public $coreScriptPosition=self::POS_HEAD; 97 98 private $_hasScripts=false; 99 private $_packages; 100 private $_dependencies; 101 private $_baseUrl; 102 private $_coreScripts=array(); 103 104 /** 105 * Cleans all registered scripts. 106 */ 107 public function reset() 108 { 109 $this->_hasScripts=false; 110 $this->_coreScripts=array(); 111 $this->cssFiles=array(); 112 $this->css=array(); 113 $this->scriptFiles=array(); 114 $this->scripts=array(); 115 $this->metaTags=array(); 116 $this->linkTags=array(); 117 118 $this->recordCachingAction('clientScript','reset',array()); 119 } 120 121 /** 122 * Renders the registered scripts. 123 * This method is called in {@link CController::render} when it finishes 124 * rendering content. CClientScript thus gets a chance to insert script tags 125 * at <code>head</code> and <code>body</code> sections in the HTML output. 126 * @param string the existing output that needs to be inserted with script tags 127 */ 128 public function render(&$output) 129 { 130 if(!$this->_hasScripts) 131 return; 132 133 $this->renderCoreScripts(); 134 135 if(!empty($this->scriptMap)) 136 $this->remapScripts(); 137 138 $this->renderHead($output); 139 if($this->enableJavaScript) 140 { 141 $this->renderBodyBegin($output); 142 $this->renderBodyEnd($output); 143 } 144 } 145 146 /** 147 * Uses {@link scriptMap} to re-map the registered scripts. 148 * @since 1.0.3 149 */ 150 protected function remapScripts() 151 { 152 $cssFiles=array(); 153 foreach($this->cssFiles as $url=>$media) 154 { 155 $name=basename($url); 156 if(isset($this->scriptMap[$name])) 157 { 158 if($this->scriptMap[$name]!==false) 159 $cssFiles[$this->scriptMap[$name]]=$media; 160 } 161 else if(isset($this->scriptMap['*.css'])) 162 { 163 if($this->scriptMap['*.css']!==false) 164 $cssFiles[$this->scriptMap['*.css']]=$media; 165 } 166 else 167 $cssFiles[$url]=$media; 168 } 169 $this->cssFiles=$cssFiles; 170 171 $jsFiles=array(); 172 foreach($this->scriptFiles as $position=>$scripts) 173 { 174 $jsFiles[$position]=array(); 175 foreach($scripts as $key=>$script) 176 { 177 $name=basename($script); 178 if(isset($this->scriptMap[$name])) 179 { 180 if($this->scriptMap[$name]!==false) 181 $jsFiles[$position][$this->scriptMap[$name]]=$this->scriptMap[$name]; 182 } 183 else if(isset($this->scriptMap['*.js'])) 184 { 185 if($this->scriptMap['*.js']!==false) 186 $jsFiles[$position][$this->scriptMap['*.js']]=$this->scriptMap['*.js']; 187 } 188 else 189 $jsFiles[$position][$key]=$script; 190 } 191 } 192 $this->scriptFiles=$jsFiles; 193 } 194 195 /** 196 * Renders the specified core javascript library. 197 * @since 1.0.3 198 */ 199 public function renderCoreScripts() 200 { 201 if($this->_packages===null) 202 return; 203 $baseUrl=$this->getCoreScriptUrl(); 204 $cssFiles=array(); 205 $jsFiles=array(); 206 foreach($this->_coreScripts as $name) 207 { 208 foreach($this->_packages[$name] as $path) 209 { 210 $url=$baseUrl.'/'.$path; 211 if(substr($path,-4)==='.css') 212 $cssFiles[$url]=''; 213 else 214 $jsFiles[$url]=$url; 215 } 216 } 217 // merge in place 218 if($cssFiles!==array()) 219 { 220 foreach($this->cssFiles as $cssFile=>$media) 221 $cssFiles[$cssFile]=$media; 222 $this->cssFiles=$cssFiles; 223 } 224 if($jsFiles!==array()) 225 { 226 if(isset($this->scriptFiles[$this->coreScriptPosition])) 227 { 228 foreach($this->scriptFiles[$this->coreScriptPosition] as $url) 229 $jsFiles[$url]=$url; 230 } 231 $this->scriptFiles[$this->coreScriptPosition]=$jsFiles; 232 } 233 } 234 235 /** 236 * Inserts the scripts in the head section. 237 * @param string the output to be inserted with scripts. 238 */ 239 public function renderHead(&$output) 240 { 241 $html=''; 242 foreach($this->metaTags as $meta) 243 $html.=CHtml::metaTag($meta['content'],null,null,$meta)."\n"; 244 foreach($this->linkTags as $link) 245 $html.=CHtml::linkTag(null,null,null,null,$link)."\n"; 246 foreach($this->cssFiles as $url=>$media) 247 $html.=CHtml::cssFile($url,$media)."\n"; 248 foreach($this->css as $css) 249 $html.=CHtml::css($css[0],$css[1])."\n"; 250 if($this->enableJavaScript) 251 { 252 if(isset($this->scriptFiles[self::POS_HEAD])) 253 { 254 foreach($this->scriptFiles[self::POS_HEAD] as $scriptFile) 255 $html.=CHtml::scriptFile($scriptFile)."\n"; 256 } 257 258 if(isset($this->scripts[self::POS_HEAD])) 259 $html.=CHtml::script(implode("\n",$this->scripts[self::POS_HEAD]))."\n"; 260 } 261 262 if($html!=='') 263 { 264 $count=0; 265 $output=preg_replace('/(<title\b[^>]*>|<\\/head\s*>)/is','<###head###>$1',$output,1,$count); 266 if($count) 267 $output=str_replace('<###head###>',$html,$output); 268 else 269 $output=$html.$output; 270 } 271 } 272 273 /** 274 * Inserts the scripts at the beginning of the body section. 275 * @param string the output to be inserted with scripts. 276 */ 277 public function renderBodyBegin(&$output) 278 { 279 $html=''; 280 if(isset($this->scriptFiles[self::POS_BEGIN])) 281 { 282 foreach($this->scriptFiles[self::POS_BEGIN] as $scriptFile) 283 $html.=CHtml::scriptFile($scriptFile)."\n"; 284 } 285 if(isset($this->scripts[self::POS_BEGIN])) 286 $html.=CHtml::script(implode("\n",$this->scripts[self::POS_BEGIN]))."\n"; 287 288 if($html!=='') 289 { 290 $count=0; 291 $output=preg_replace('/(<body\b[^>]*>)/is','$1<###begin###>',$output,1,$count); 292 if($count) 293 $output=str_replace('<###begin###>',$html,$output); 294 else 295 $output=$html.$output; 296 } 297 } 298 299 /** 300 * Inserts the scripts at the end of the body section. 301 * @param string the output to be inserted with scripts. 302 */ 303 public function renderBodyEnd(&$output) 304 { 305 if(!isset($this->scriptFiles[self::POS_END]) && !isset($this->scripts[self::POS_END]) 306 && !isset($this->scripts[self::POS_READY]) && !isset($this->scripts[self::POS_LOAD])) 307 return; 308 309 $fullPage=0; 310 $output=preg_replace('/(<\\/body\s*>)/is','<###end###>$1',$output,1,$fullPage); 311 $html=''; 312 if(isset($this->scriptFiles[self::POS_END])) 313 { 314 foreach($this->scriptFiles[self::POS_END] as $scriptFile) 315 $html.=CHtml::scriptFile($scriptFile)."\n"; 316 } 317 $scripts=isset($this->scripts[self::POS_END]) ? $this->scripts[self::POS_END] : array(); 318 if(isset($this->scripts[self::POS_READY])) 319 { 320 if($fullPage) 321 $scripts[]="jQuery(function($) {\n".implode("\n",$this->scripts[self::POS_READY])."\n});"; 322 else 323 $scripts[]=implode("\n",$this->scripts[self::POS_READY]); 324 } 325 if(isset($this->scripts[self::POS_LOAD])) 326 { 327 if($fullPage) 328 $scripts[]="window.onload=function() {\n".implode("\n",$this->scripts[self::POS_LOAD])."\n};"; 329 else 330 $scripts[]=implode("\n",$this->scripts[self::POS_LOAD]); 331 } 332 if(!empty($scripts)) 333 $html.=CHtml::script(implode("\n",$scripts))."\n"; 334 335 if($fullPage) 336 $output=str_replace('<###end###>',$html,$output); 337 else 338 $output=$output.$html; 339 } 340 341 /** 342 * Returns the base URL of all core javascript files. 343 * If the base URL is not explicitly set, this method will publish the whole directory 344 * 'framework/web/js/source' and return the corresponding URL. 345 * @return string the base URL of all core javascript files 346 */ 347 public function getCoreScriptUrl() 348 { 349 if($this->_baseUrl!==null) 350 return $this->_baseUrl; 351 else 352 return $this->_baseUrl=Yii::app()->getAssetManager()->publish(YII_PATH.'/web/js/source'); 353 } 354 355 /** 356 * Sets the base URL of all core javascript files. 357 * This setter is provided in case when core javascript files are manually published 358 * to a pre-specified location. This may save asset publishing time for large-scale applications. 359 * @param string the base URL of all core javascript files. 360 */ 361 public function setCoreScriptUrl($value) 362 { 363 $this->_baseUrl=$value; 364 } 365 366 /** 367 * Registers a core javascript library. 368 * @param string the core javascript library name 369 * @see renderCoreScript 370 */ 371 public function registerCoreScript($name) 372 { 373 if(isset($this->_coreScripts[$name])) 374 return; 375 376 if($this->_packages===null) 377 { 378 $config=require(YII_PATH.'/web/js/packages.php'); 379 $this->_packages=$config[0]; 380 $this->_dependencies=$config[1]; 381 } 382 if(!isset($this->_packages[$name])) 383 return; 384 if(isset($this->_dependencies[$name])) 385 { 386 foreach($this->_dependencies[$name] as $depName) 387 $this->registerCoreScript($depName); 388 } 389 390 $this->_hasScripts=true; 391 $this->_coreScripts[$name]=$name; 392 $params=func_get_args(); 393 $this->recordCachingAction('clientScript','registerCoreScript',$params); 394 } 395 396 /** 397 * Registers a CSS file 398 * @param string URL of the CSS file 399 * @param string media that the CSS file should be applied to. If empty, it means all media types. 400 */ 401 public function registerCssFile($url,$media='') 402 { 403 $this->_hasScripts=true; 404 $this->cssFiles[$url]=$media; 405 $params=func_get_args(); 406 $this->recordCachingAction('clientScript','registerCssFile',$params); 407 } 408 409 /** 410 * Registers a piece of CSS code. 411 * @param string ID that uniquely identifies this piece of CSS code 412 * @param string the CSS code 413 * @param string media that the CSS code should be applied to. If empty, it means all media types. 414 */ 415 public function registerCss($id,$css,$media='') 416 { 417 $this->_hasScripts=true; 418 $this->css[$id]=array($css,$media); 419 $params=func_get_args(); 420 $this->recordCachingAction('clientScript','registerCss',$params); 421 } 422 423 /** 424 * Registers a javascript file. 425 * @param string URL of the javascript file 426 * @param integer the position of the JavaScript code. Valid values include the following: 427 * <ul> 428 * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li> 429 * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li> 430 * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li> 431 * </ul> 432 */ 433 public function registerScriptFile($url,$position=self::POS_HEAD) 434 { 435 $this->_hasScripts=true; 436 $this->scriptFiles[$position][$url]=$url; 437 $params=func_get_args(); 438 $this->recordCachingAction('clientScript','registerScriptFile',$params); 439 } 440 441 /** 442 * Registers a piece of javascript code. 443 * @param string ID that uniquely identifies this piece of JavaScript code 444 * @param string the javascript code 445 * @param integer the position of the JavaScript code. Valid values include the following: 446 * <ul> 447 * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li> 448 * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li> 449 * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li> 450 * <li>CClientScript::POS_LOAD : the script is inserted in the window.onload() function.</li> 451 * <li>CClientScript::POS_READY : the script is inserted in the jQuery's ready function.</li> 452 * </ul> 453 */ 454 public function registerScript($id,$script,$position=self::POS_READY) 455 { 456 $this->_hasScripts=true; 457 $this->scripts[$position][$id]=$script; 458 if($position===self::POS_READY) 459 $this->registerCoreScript('jquery'); 460 $params=func_get_args(); 461 $this->recordCachingAction('clientScript','registerScript',$params); 462 } 463 464 /** 465 * Registers a meta tag that will be inserted in the head section (right before the title element) of the resulting page. 466 * @param string content attribute of the meta tag 467 * @param string name attribute of the meta tag. If null, the attribute will not be generated 468 * @param string http-equiv attribute of the meta tag. If null, the attribute will not be generated 469 * @param array other options in name-value pairs (e.g. 'scheme', 'lang') 470 * @since 1.0.1 471 */ 472 public function registerMetaTag($content,$name=null,$httpEquiv=null,$options=array()) 473 { 474 $this->_hasScripts=true; 475 if($name!==null) 476 $options['name']=$name; 477 if($httpEquiv!==null) 478 $options['http-equiv']=$httpEquiv; 479 $options['content']=$content; 480 $this->metaTags[serialize($options)]=$options; 481 $params=func_get_args(); 482 $this->recordCachingAction('clientScript','registerMetaTag',$params); 483 } 484 485 /** 486 * Registers a link tag that will be inserted in the head section (right before the title element) of the resulting page. 487 * @param string rel attribute of the link tag. If null, the attribute will not be generated. 488 * @param string type attribute of the link tag. If null, the attribute will not be generated. 489 * @param string href attribute of the link tag. If null, the attribute will not be generated. 490 * @param string media attribute of the link tag. If null, the attribute will not be generated. 491 * @param array other options in name-value pairs 492 * @since 1.0.1 493 */ 494 public function registerLinkTag($relation=null,$type=null,$href=null,$media=null,$options=array()) 495 { 496 $this->_hasScripts=true; 497 if($relation!==null) 498 $options['rel']=$relation; 499 if($type!==null) 500 $options['type']=$type; 501 if($href!==null) 502 $options['href']=$href; 503 if($media!==null) 504 $options['media']=$media; 505 $this->linkTags[serialize($options)]=$options; 506 $params=func_get_args(); 507 $this->recordCachingAction('clientScript','registerLinkTag',$params); 508 } 509 510 /** 511 * Checks whether the CSS file has been registered. 512 * @param string URL of the CSS file 513 * @return boolean whether the CSS file is already registered 514 */ 515 public function isCssFileRegistered($url) 516 { 517 return isset($this->cssFiles[$url]); 518 } 519 520 /** 521 * Checks whether the CSS code has been registered. 522 * @param string ID that uniquely identifies the CSS code 523 * @return boolean whether the CSS code is already registered 524 */ 525 public function isCssRegistered($id) 526 { 527 return isset($this->css[$id]); 528 } 529 530 /** 531 * Checks whether the JavaScript file has been registered. 532 * @param string URL of the javascript file 533 * @param integer the position of the JavaScript code. Valid values include the following: 534 * <ul> 535 * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li> 536 * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li> 537 * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li> 538 * </ul> 539 * @return boolean whether the javascript file is already registered 540 */ 541 public function isScriptFileRegistered($url,$position=self::POS_HEAD) 542 { 543 return isset($this->scriptFiles[$position][$url]); 544 } 545 546 /** 547 * Checks whether the JavaScript code has been registered. 548 * @param string ID that uniquely identifies the JavaScript code 549 * @param integer the position of the JavaScript code. Valid values include the following: 550 * <ul> 551 * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li> 552 * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li> 553 * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li> 554 * <li>CClientScript::POS_LOAD : the script is inserted in the window.onload() function.</li> 555 * <li>CClientScript::POS_READY : the script is inserted in the jQuery's ready function.</li> 556 * </ul> 557 * @return boolean whether the javascript code is already registered 558 */ 559 public function isScriptRegistered($id,$position=self::POS_READY) 560 { 561 return isset($this->scripts[$position][$id]); 562 } 563 564 /** 565 * Records a method call when an output cache is in effect. 566 * This is a shortcut to Yii::app()->controller->recordCachingAction. 567 * In case when controller is absent, nothing is recorded. 568 * @param string a property name of the controller. It refers to an object 569 * whose method is being called. If empty it means the controller itself. 570 * @param string the method name 571 * @param array parameters passed to the method 572 * @see COutputCache 573 * @since 1.0.5 574 */ 575 protected function recordCachingAction($context,$method,$params) 576 { 577 if(($controller=Yii::app()->getController())!==null) 578 $controller->recordCachingAction($context,$method,$params); 579 } 580} 581