3 * TClientScriptManager and TClientSideOptions class file.
5 * @author Qiang Xue <qiang.xue@gmail.com>
6 * @author Gabor Berczi <gabor.berczi@devworx.hu> (lazyload additions & progressive rendering)
7 * @link http://www.pradosoft.com/
8 * @copyright Copyright © 2005-2014 PradoSoft
9 * @license http://www.pradosoft.com/license/
10 * @package System.Web.UI
14 * TClientScriptManager class.
16 * TClientScriptManager manages javascript and CSS stylesheets for a page.
18 * @author Qiang Xue <qiang.xue@gmail.com>
19 * @author Gabor Berczi <gabor.berczi@devworx.hu> (lazyload additions & progressive rendering)
20 * @package System.Web.UI
23 class TClientScriptManager extends TApplicationComponent
26 * directory containing Prado javascript files
28 const SCRIPT_PATH='Web/Javascripts/source';
30 * file containing javascript packages and their cross dependencies
32 const PACKAGES_FILE='Web/Javascripts/packages.php';
34 * @var TPage page who owns this manager
38 * @var array registered hidden fields, indexed by hidden field names
40 private $_hiddenFields=array();
42 * @var array javascript blocks to be rendered at the beginning of the form
44 private $_beginScripts=array();
46 * @var array javascript blocks to be rendered at the end of the form
48 private $_endScripts=array();
50 * @var array javascript files to be rendered in the form
52 private $_scriptFiles=array();
54 * @var array javascript files to be rendered in page head section
56 private $_headScriptFiles=array();
58 * @var array javascript blocks to be rendered in page head section
60 private $_headScripts=array();
62 * @var array CSS files
64 private $_styleSheetFiles=array();
66 * @var array CSS declarations
68 private $_styleSheets=array();
70 * @var array registered PRADO script libraries
72 private $_registeredPradoScripts=array();
74 * Client-side javascript library dependencies, loads from PACKAGES_FILE;
77 private static $_pradoScripts;
79 * Client-side javascript library packages, loads from PACKAGES_FILE;
82 private static $_pradoPackages;
84 private $_renderedHiddenFields;
86 private $_renderedScriptFiles=array();
88 private $_expandedPradoScripts;
92 * @param TPage page that owns this client script manager
94 public function __construct(TPage $owner)
100 * @return boolean whether THead is required in order to render CSS and js within head
103 public function getRequiresHead()
105 return count($this->_styleSheetFiles) || count($this->_styleSheets)
106 || count($this->_headScriptFiles) || count($this->_headScripts);
109 public static function getPradoPackages()
111 return self::$_pradoPackages;
114 public static function getPradoScripts()
116 return self::$_pradoScripts;
120 * Registers Prado javascript by library name. See "Web/Javascripts/packages.php"
122 * @param string script library name.
124 public function registerPradoScript($name)
126 $this->registerPradoScriptInternal($name);
127 $params=func_get_args();
128 $this->_page->registerCachingAction('Page.ClientScript','registerPradoScript',$params);
132 * Registers a Prado javascript library to be loaded.
134 protected function registerPradoScriptInternal($name)
136 // $this->checkIfNotInRender();
137 if(!isset($this->_registeredPradoScripts[$name]))
139 if(self::$_pradoScripts === null)
141 $packageFile = Prado::getFrameworkPath().DIRECTORY_SEPARATOR.self::PACKAGES_FILE;
142 list($packages,$deps)= include($packageFile);
143 self::$_pradoScripts = $deps;
144 self::$_pradoPackages = $packages;
147 if (isset(self::$_pradoScripts[$name]))
148 $this->_registeredPradoScripts[$name]=true;
150 throw new TInvalidOperationException('csmanager_pradoscript_invalid',$name);
152 if(($packages=array_keys($this->_registeredPradoScripts))!==array())
154 $base = Prado::getFrameworkPath().DIRECTORY_SEPARATOR.self::SCRIPT_PATH;
155 list($path,$baseUrl)=$this->getPackagePathUrl($base);
156 $packagesUrl=array();
157 $isDebug=$this->getApplication()->getMode()===TApplicationMode::Debug;
158 foreach ($packages as $p)
160 foreach (self::$_pradoScripts[$p] as $dep)
162 foreach (self::$_pradoPackages[$dep] as $script)
163 if (!isset($this->_expandedPradoScripts[$script]))
165 $this->_expandedPradoScripts[$script] = true;
168 if (!in_array($url=$baseUrl.'/'.$script,$packagesUrl))
171 if (!in_array($url=$baseUrl.'/min/'.$script,$packagesUrl))
173 if(!is_file($filePath=$path.'/min/'.$script))
175 $dirPath=dirname($filePath);
176 if(!is_dir($dirPath))
177 mkdir($dirPath, PRADO_CHMOD, true);
178 file_put_contents($filePath, TJavaScript::JSMin(file_get_contents($base.'/'.$script)));
179 chmod($filePath, PRADO_CHMOD);
187 foreach($packagesUrl as $url)
188 $this->registerScriptFile($url,$url);
194 * @return string Prado javascript library base asset url.
196 public function getPradoScriptAssetUrl()
198 $base = Prado::getFrameworkPath().DIRECTORY_SEPARATOR.self::SCRIPT_PATH;
199 $assets = Prado::getApplication()->getAssetManager();
200 return $assets->getPublishedUrl($base);
204 * Returns the URLs of all script files referenced on the page
205 * @return array Combined list of all script urls used in the page
207 public function getScriptUrls()
209 $scripts = array_values($this->_headScriptFiles);
210 $scripts = array_merge($scripts, array_values($this->_scriptFiles));
211 $scripts = array_unique($scripts);
217 * @param string javascript package path.
218 * @return array tuple($path,$url).
220 protected function getPackagePathUrl($base)
222 $assets = Prado::getApplication()->getAssetManager();
223 if(strpos($base, $assets->getBaseUrl())===false)
225 if(($dir = Prado::getPathOfNameSpace($base)) !== null) {
228 return array($assets->getPublishedPath($base), $assets->publishFilePath($base));
232 return array($assets->getBasePath().str_replace($assets->getBaseUrl(),'',$base), $base);
237 * Returns javascript statement that create a new callback request object.
238 * @param ICallbackEventHandler callback response handler
239 * @param array additional callback options
240 * @return string javascript statement that creates a new callback request.
242 public function getCallbackReference(ICallbackEventHandler $callbackHandler, $options=null)
244 $options = !is_array($options) ? array() : $options;
245 $class = new ReflectionClass($callbackHandler);
246 $clientSide = $callbackHandler->getActiveControl()->getClientSide();
247 $options = array_merge($options, $clientSide->getOptions()->toArray());
248 $optionString = TJavaScript::encode($options);
249 $this->registerPradoScriptInternal('ajax');
250 $id = $callbackHandler->getUniqueID();
251 return "new Prado.CallbackRequest('{$id}',{$optionString})";
255 * Registers callback javascript for a control.
256 * @param string javascript class responsible for the control being registered for callback
257 * @param array callback options
259 public function registerCallbackControl($class, $options)
261 $optionString=TJavaScript::encode($options);
262 $code="new {$class}({$optionString});";
263 $this->_endScripts[sprintf('%08X', crc32($code))]=$code;
264 $this->registerPradoScriptInternal('ajax');
266 $params=func_get_args();
267 $this->_page->registerCachingAction('Page.ClientScript','registerCallbackControl',$params);
271 * Registers postback javascript for a control. A null class parameter will prevent
272 * the javascript code registration.
273 * @param string javascript class responsible for the control being registered for postback
274 * @param array postback options
276 public function registerPostBackControl($class,$options)
278 if($class === null) {
281 if(!isset($options['FormID']) && ($form=$this->_page->getForm())!==null)
282 $options['FormID']=$form->getClientID();
283 $optionString=TJavaScript::encode($options);
284 $code="new {$class}({$optionString});";
286 $this->_endScripts[sprintf('%08X', crc32($code))]=$code;
287 $this->_hiddenFields[TPage::FIELD_POSTBACK_TARGET]='';
288 $this->_hiddenFields[TPage::FIELD_POSTBACK_PARAMETER]='';
289 $this->registerPradoScriptInternal('prado');
291 $params=func_get_args();
292 $this->_page->registerCachingAction('Page.ClientScript','registerPostBackControl',$params);
296 * Register a default button to panel. When the $panel is in focus and
297 * the 'enter' key is pressed, the $button will be clicked.
298 * @param TControl|string panel (or its unique ID) to register the default button action
299 * @param TControl|string button (or its unique ID) to trigger a postback
301 public function registerDefaultButton($panel, $button)
303 $panelID=is_string($panel)?$panel:$panel->getUniqueID();
305 if(is_string($button))
309 $button->setIsDefaultButton(true);
310 $buttonID=$button->getUniqueID();
312 $options = TJavaScript::encode($this->getDefaultButtonOptions($panelID, $buttonID));
313 $code = "new Prado.WebUI.DefaultButton($options);";
315 $this->_endScripts['prado:'.$panelID]=$code;
316 $this->_hiddenFields[TPage::FIELD_POSTBACK_TARGET]='';
317 $this->registerPradoScriptInternal('prado');
319 $params=array($panelID,$buttonID);
320 $this->_page->registerCachingAction('Page.ClientScript','registerDefaultButton',$params);
324 * @param string the unique ID of the container control
325 * @param string the unique ID of the button control
326 * @return array default button options.
328 protected function getDefaultButtonOptions($panelID, $buttonID)
330 $options['ID'] = TControl::convertUniqueIdToClientId($panelID);
331 $options['Panel'] = TControl::convertUniqueIdToClientId($panelID);
332 $options['Target'] = TControl::convertUniqueIdToClientId($buttonID);
333 $options['EventTarget'] = $buttonID;
334 $options['Event'] = 'click';
339 * Registers the control to receive default focus.
340 * @param string the client ID of the control to receive default focus
342 public function registerFocusControl($target)
344 $this->registerPradoScriptInternal('effects');
345 if($target instanceof TControl)
346 $target=$target->getClientID();
347 $id = TJavaScript::quoteString($target);
348 $this->_endScripts['prado:focus'] = 'Prado.Element.focus('.$id.');';
350 $params=func_get_args();
351 $this->_page->registerCachingAction('Page.ClientScript','registerFocusControl',$params);
355 * Registers a CSS file to be rendered in the page head
357 * The CSS files in themes are registered in {@link OnPreRenderComplete onPreRenderComplete} if you want to override
358 * CSS styles in themes you need to register it after this event is completed.
363 * class BasePage extends TPage {
364 * public function onPreRenderComplete($param) {
365 * parent::onPreRenderComplete($param);
366 * $url = 'path/to/your/stylesheet.css';
367 * $this->Page->ClientScript->registerStyleSheetFile($url, $url);
372 * @param string a unique key identifying the file
373 * @param string URL to the CSS file
374 * @param string media type of the CSS (such as 'print', 'screen', etc.). Defaults to empty, meaning the CSS applies to all media types.
376 public function registerStyleSheetFile($key,$url,$media='')
379 $this->_styleSheetFiles[$key]=$url;
381 $this->_styleSheetFiles[$key]=array($url,$media);
383 $params=func_get_args();
384 $this->_page->registerCachingAction('Page.ClientScript','registerStyleSheetFile',$params);
388 * Registers a CSS block to be rendered in the page head
389 * @param string a unique key identifying the CSS block
390 * @param string CSS block
392 public function registerStyleSheet($key,$css,$media='')
394 $this->_styleSheets[$key]=$css;
396 $params=func_get_args();
397 $this->_page->registerCachingAction('Page.ClientScript','registerStyleSheet',$params);
401 * Returns the URLs of all stylesheet files referenced on the page
402 * @return array List of all stylesheet urls used in the page
404 public function getStyleSheetUrls()
406 $stylesheets = array_values(
408 create_function('$e', 'return is_array($e) ? $e[0] : $e;'),
409 $this->_styleSheetFiles)
412 foreach(Prado::getApplication()->getAssetManager()->getPublished() as $path=>$url)
413 if (substr($url,strlen($url)-4)=='.css')
414 $stylesheets[] = $url;
416 $stylesheets = array_unique($stylesheets);
422 * Returns all the stylesheet code snippets referenced on the page
423 * @return array List of all stylesheet snippets used in the page
425 public function getStyleSheetCodes()
427 return array_unique(array_values($this->_styleSheets));
431 * Registers a javascript file in the page head
432 * @param string a unique key identifying the file
433 * @param string URL to the javascript file
435 public function registerHeadScriptFile($key,$url)
437 $this->checkIfNotInRender();
438 $this->_headScriptFiles[$key]=$url;
440 $params=func_get_args();
441 $this->_page->registerCachingAction('Page.ClientScript','registerHeadScriptFile',$params);
445 * Registers a javascript block in the page head.
446 * @param string a unique key identifying the script block
447 * @param string javascript block
449 public function registerHeadScript($key,$script)
451 $this->checkIfNotInRender();
452 $this->_headScripts[$key]=$script;
454 $params=func_get_args();
455 $this->_page->registerCachingAction('Page.ClientScript','registerHeadScript',$params);
459 * Registers a javascript file to be rendered within the form
460 * @param string a unique key identifying the file
461 * @param string URL to the javascript file to be rendered
463 public function registerScriptFile($key, $url)
465 $this->_scriptFiles[$key]=$url;
467 $params=func_get_args();
468 $this->_page->registerCachingAction('Page.ClientScript','registerScriptFile',$params);
472 * Registers a javascript script block at the beginning of the form
473 * @param string a unique key identifying the script block
474 * @param string javascript block
476 public function registerBeginScript($key,$script)
478 $this->checkIfNotInRender();
479 $this->_beginScripts[$key]=$script;
481 $params=func_get_args();
482 $this->_page->registerCachingAction('Page.ClientScript','registerBeginScript',$params);
486 * Registers a javascript script block at the end of the form
487 * @param string a unique key identifying the script block
488 * @param string javascript block
490 public function registerEndScript($key,$script)
492 $this->_endScripts[$key]=$script;
494 $params=func_get_args();
495 $this->_page->registerCachingAction('Page.ClientScript','registerEndScript',$params);
499 * Registers a hidden field to be rendered in the form.
500 * @param string a unique key identifying the hidden field
501 * @param string|array hidden field value, if the value is an array, every element
502 * in the array will be rendered as a hidden field value.
504 public function registerHiddenField($name,$value)
506 $this->_hiddenFields[$name]=$value;
508 $params=func_get_args();
509 $this->_page->registerCachingAction('Page.ClientScript','registerHiddenField',$params);
513 * @param string a unique key
514 * @return boolean whether there is a CSS file registered with the specified key
516 public function isStyleSheetFileRegistered($key)
518 return isset($this->_styleSheetFiles[$key]);
522 * @param string a unique key
523 * @return boolean whether there is a CSS block registered with the specified key
525 public function isStyleSheetRegistered($key)
527 return isset($this->_styleSheets[$key]);
531 * @param string a unique key
532 * @return boolean whether there is a head javascript file registered with the specified key
534 public function isHeadScriptFileRegistered($key)
536 return isset($this->_headScriptFiles[$key]);
540 * @param string a unique key
541 * @return boolean whether there is a head javascript block registered with the specified key
543 public function isHeadScriptRegistered($key)
545 return isset($this->_headScripts[$key]);
549 * @param string a unique key
550 * @return boolean whether there is a javascript file registered with the specified key
552 public function isScriptFileRegistered($key)
554 return isset($this->_scriptFiles[$key]);
558 * @param string a unique key
559 * @return boolean whether there is a beginning javascript block registered with the specified key
561 public function isBeginScriptRegistered($key)
563 return isset($this->_beginScripts[$key]);
567 * @param string a unique key
568 * @return boolean whether there is an ending javascript block registered with the specified key
570 public function isEndScriptRegistered($key)
572 return isset($this->_endScripts[$key]);
576 * @return boolean true if any end scripts are registered.
578 public function hasEndScripts()
580 return count($this->_endScripts) > 0;
584 * @return boolean true if any begin scripts are registered.
586 public function hasBeginScripts()
588 return count($this->_beginScripts) > 0;
592 * @param string a unique key
593 * @return boolean whether there is a hidden field registered with the specified key
595 public function isHiddenFieldRegistered($key)
597 return isset($this->_hiddenFields[$key]);
601 * @param THtmlWriter writer for the rendering purpose
603 public function renderStyleSheetFiles($writer)
606 foreach($this->_styleSheetFiles as $url)
609 $str.="<link rel=\"stylesheet\" type=\"text/css\" media=\"{$url[1]}\" href=\"".THttpUtility::htmlEncode($url[0])."\" />\n";
611 $str.="<link rel=\"stylesheet\" type=\"text/css\" href=\"".THttpUtility::htmlEncode($url)."\" />\n";
613 $writer->write($str);
617 * @param THtmlWriter writer for the rendering purpose
619 public function renderStyleSheets($writer)
621 if(count($this->_styleSheets))
622 $writer->write("<style type=\"text/css\">\n/*<![CDATA[*/\n".implode("\n",$this->_styleSheets)."\n/*]]>*/\n</style>\n");
626 * @param THtmlWriter writer for the rendering purpose
628 public function renderHeadScriptFiles($writer)
630 $this->renderScriptFiles($writer,$this->_headScriptFiles);
634 * @param THtmlWriter writer for the rendering purpose
636 public function renderHeadScripts($writer)
638 $writer->write(TJavaScript::renderScriptBlocks($this->_headScripts));
641 public function renderScriptFilesBegin($writer)
643 $this->renderAllPendingScriptFiles($writer);
646 public function renderScriptFilesEnd($writer)
648 $this->renderAllPendingScriptFiles($writer);
651 public function markScriptFileAsRendered($url)
653 $this->_renderedScriptFiles[$url] = $url;
654 $params=func_get_args();
655 $this->_page->registerCachingAction('Page.ClientScript','markScriptFileAsRendered',$params);
658 protected function renderScriptFiles($writer, Array $scripts)
660 foreach($scripts as $script)
662 $writer->write(TJavaScript::renderScriptFile($script));
663 $this->markScriptFileAsRendered($script);
667 protected function getRenderedScriptFiles()
669 return $this->_renderedScriptFiles;
673 * @param THtmlWriter writer for the rendering purpose
675 public function renderAllPendingScriptFiles($writer)
677 if(!empty($this->_scriptFiles))
679 $addedScripts = array_diff($this->_scriptFiles,$this->getRenderedScriptFiles());
680 $this->renderScriptFiles($writer,$addedScripts);
685 * @param THtmlWriter writer for the rendering purpose
687 public function renderBeginScripts($writer)
689 $writer->write(TJavaScript::renderScriptBlocks($this->_beginScripts));
693 * @param THtmlWriter writer for the rendering purpose
695 public function renderEndScripts($writer)
697 $writer->write(TJavaScript::renderScriptBlocks($this->_endScripts));
700 public function renderHiddenFieldsBegin($writer)
702 $this->renderHiddenFieldsInt($writer,true);
705 public function renderHiddenFieldsEnd($writer)
707 $this->renderHiddenFieldsInt($writer,false);
711 * Flushes all pending script registrations
712 * @param THtmlWriter writer for the rendering purpose
713 * @param TControl the control forcing the flush (used only in error messages)
715 public function flushScriptFiles($writer, $control=null)
717 if(!$this->_page->getIsCallback())
719 $this->_page->ensureRenderInForm($control);
720 $this->renderAllPendingScriptFiles($writer);
725 * @param THtmlWriter writer for the rendering purpose
727 protected function renderHiddenFieldsInt($writer, $initial)
729 if ($initial) $this->_renderedHiddenFields = array();
731 foreach($this->_hiddenFields as $name=>$value)
733 if (in_array($name,$this->_renderedHiddenFields)) continue;
734 $id=strtr($name,':','_');
737 foreach($value as $v)
738 $str.='<input type="hidden" name="'.$name.'[]" id="'.$id.'" value="'.THttpUtility::htmlEncode($value)."\" />\n";
742 $str.='<input type="hidden" name="'.$name.'" id="'.$id.'" value="'.THttpUtility::htmlEncode($value)."\" />\n";
744 $this->_renderedHiddenFields[] = $name;
747 $writer->write("<div style=\"visibility:hidden;\">\n".$str."</div>\n");
750 public function getHiddenFields()
752 return $this->_hiddenFields;
756 * Checks whether page rendering has not begun yet
758 protected function checkIfNotInRender()
760 if ($form = $this->_page->InFormRender)
761 throw new Exception('Operation invalid when page is already rendering');
766 * TClientSideOptions abstract class.
768 * TClientSideOptions manages client-side options for components that have
769 * common client-side javascript behaviours and client-side events such as
770 * between ActiveControls and validators.
772 * @author <weizhuo[at]gmail[dot]com>
773 * @package System.Web.UI
776 abstract class TClientSideOptions extends TComponent
779 * @var TMap list of client-side options.
784 * Adds on client-side event handler by wrapping the code within a
785 * javascript function block. If the code begins with "javascript:", the
786 * code is assumed to be a javascript function block rather than arbiturary
787 * javascript statements.
788 * @param string option name
789 * @param string javascript statements.
791 protected function setFunction($name, $code)
793 if(!TJavaScript::isJsLiteral($code))
794 $code = TJavaScript::quoteJsLiteral($this->ensureFunction($code));
795 $this->setOption($name, $code);
799 * @return string gets a particular option, null if not set.
801 protected function getOption($name)
804 return $this->_options->itemAt($name);
810 * @param string option name
811 * @param mixed option value.
813 protected function setOption($name, $value)
815 $this->getOptions()->add($name, $value);
819 * @return TMap gets the list of options as TMap
821 public function getOptions()
823 if (!$this->_options)
824 $this->_options = Prado::createComponent('System.Collections.TMap');
825 return $this->_options;
829 * Ensure that the javascript statements are wrapped in a javascript
830 * function block as <code>function(sender, parameter){ //code }</code>.
832 protected function ensureFunction($javascript)
834 return "function(sender, parameter){ {$javascript} }";