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 &copy; 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