mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 18:48:51 +02:00
N°3123 : Improved JavaScript management in web pages and ajax pages
This commit is contained in:
@@ -85,6 +85,12 @@ class utils
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public const ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER = 'element_identifier';
|
||||
/**
|
||||
* @var string For variables names
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public const ENUM_SANITIZATION_FILTER_VARIABLE_NAME = 'variable_name';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 3.0.0
|
||||
@@ -411,6 +417,10 @@ class utils
|
||||
$retValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
|
||||
break;
|
||||
|
||||
case static::ENUM_SANITIZATION_FILTER_VARIABLE_NAME:
|
||||
$retValue = preg_replace('/[^a-zA-Z0-9_]/', '', $value);
|
||||
break;
|
||||
|
||||
default:
|
||||
case static::ENUM_SANITIZATION_FILTER_RAW_DATA:
|
||||
$retValue = $value;
|
||||
|
||||
@@ -127,26 +127,27 @@ class BlockRenderer
|
||||
/**
|
||||
* Return the raw output of the JS template
|
||||
*
|
||||
* @param string $sType javascript type only JS_TYPE_ON_INIT / JS_TYPE_ON_READY / JS_TYPE_LIVE
|
||||
*
|
||||
* @return string
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function RenderJsInline()
|
||||
public function RenderJsInline(string $sType)
|
||||
{
|
||||
$sOutput = '';
|
||||
if(!empty($this->oBlock->GetJsTemplateRelPath()))
|
||||
if(!empty($this->oBlock->GetJsTemplateRelPath($sType)))
|
||||
{
|
||||
$sOutput = TwigHelper::RenderTemplate(
|
||||
static::$oTwigEnv,
|
||||
$this->GetTemplateParameters(),
|
||||
$this->oBlock->GetJsTemplateRelPath(),
|
||||
TwigHelper::ENUM_FILE_TYPE_JS
|
||||
$this->oBlock->GetJsTemplateRelPath($sType),
|
||||
$sType
|
||||
);
|
||||
}
|
||||
|
||||
return $sOutput;
|
||||
return trim($sOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +172,7 @@ class BlockRenderer
|
||||
);
|
||||
}
|
||||
|
||||
return $sOutput;
|
||||
return trim($sOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -93,6 +93,12 @@ class Extension
|
||||
})
|
||||
);
|
||||
|
||||
// Filter to sanitize a variable name
|
||||
// Usage in twig: {{ 'variable_name:to-sanitize'|variable_name }}
|
||||
$oTwigEnv->addFilter(new Twig_SimpleFilter('variable_name', function ($sString) {
|
||||
return utils::Sanitize($sString, '', utils::ENUM_SANITIZATION_FILTER_VARIABLE_NAME);
|
||||
})
|
||||
);
|
||||
// Filter to add a parameter at the end of the URL to force cache invalidation after an upgrade.
|
||||
// Previously we put the iTop version but now it's the last setup/toolkit timestamp to avoid cache issues when building several times the same version during tests
|
||||
//
|
||||
|
||||
@@ -22,6 +22,16 @@ class DataTableBlock extends UIContentBlock
|
||||
public const BLOCK_CODE = 'ibo-datatable';
|
||||
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/datatable/layout';
|
||||
public const DEFAULT_JS_TEMPLATE_REL_PATH = 'base/components/datatable/layout';
|
||||
public const DEFAULT_JS_FILES_REL_PATH = [
|
||||
'lib/datatables/js/jquery.dataTables.min.js',
|
||||
'lib/datatables/js/dataTables.bootstrap.min.js',
|
||||
'lib/datatables/js/dataTables.fixedHeader.min.js',
|
||||
'lib/datatables/js/dataTables.responsive.min.js',
|
||||
'lib/datatables/js/dataTables.scroller.min.js',
|
||||
'lib/datatables/js/dataTables.select.min.js',
|
||||
'js/dataTables.settings.js',
|
||||
'js/dataTables.pipeline.js',
|
||||
];
|
||||
|
||||
protected $aOptions;//list of specific options for display datatable
|
||||
protected $sAjaxUrl;
|
||||
|
||||
@@ -45,8 +45,12 @@ abstract class UIBlock implements iUIBlock
|
||||
public const DEFAULT_HTML_TEMPLATE_REL_PATH = null;
|
||||
/** @var array JS_FILES_REL_PATH Relative paths (from <ITOP>/) to the JS files */
|
||||
public const DEFAULT_JS_FILES_REL_PATH = [];
|
||||
/** @var string|null JS_TEMPLATE_REL_PATH Relative path (from <ITOP>/templates/) to the JS template */
|
||||
/** @var string|null JS_TEMPLATE_REL_PATH Relative path (from <ITOP>/templates/) to the JS template on dom ready*/
|
||||
public const DEFAULT_JS_TEMPLATE_REL_PATH = null;
|
||||
/** @var string|null Relative path (from <ITOP>/templates/) to the JS template not deferred */
|
||||
public const DEFAULT_JS_LIVE_TEMPLATE_REL_PATH = null;
|
||||
/** @var string|null Relative path (from <ITOP>/templates/) to the JS template after DEFAULT_JS_TEMPLATE_REL_PATH */
|
||||
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = null;
|
||||
/** @var array CSS_FILES_REL_PATH Relative paths (from <ITOP>/) to the CSS files */
|
||||
public const DEFAULT_CSS_FILES_REL_PATH = [];
|
||||
/** @var string|null CSS_TEMPLATE_REL_PATH Relative path (from <ITOP>/templates/) to the CSS template */
|
||||
@@ -68,8 +72,8 @@ abstract class UIBlock implements iUIBlock
|
||||
protected $sGlobalTemplateRelPath;
|
||||
/** @var string */
|
||||
protected $sHtmlTemplateRelPath;
|
||||
/** @var string */
|
||||
protected $sJsTemplateRelPath;
|
||||
/** @var array */
|
||||
protected $aJsTemplateRelPath;
|
||||
/** @var string */
|
||||
protected $sCssTemplateRelPath;
|
||||
/** @var array */
|
||||
@@ -88,7 +92,9 @@ abstract class UIBlock implements iUIBlock
|
||||
$this->aJsFilesRelPath = static::DEFAULT_JS_FILES_REL_PATH;
|
||||
$this->aCssFilesRelPath = static::DEFAULT_CSS_FILES_REL_PATH;
|
||||
$this->sHtmlTemplateRelPath = static::DEFAULT_HTML_TEMPLATE_REL_PATH;
|
||||
$this->sJsTemplateRelPath = static::DEFAULT_JS_TEMPLATE_REL_PATH;
|
||||
$this->aJsTemplateRelPath[self::JS_TYPE_LIVE] = static::DEFAULT_JS_LIVE_TEMPLATE_REL_PATH;
|
||||
$this->aJsTemplateRelPath[self::JS_TYPE_ON_INIT] = static::DEFAULT_JS_TEMPLATE_REL_PATH;
|
||||
$this->aJsTemplateRelPath[self::JS_TYPE_ON_READY] = static::DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH;
|
||||
$this->sCssTemplateRelPath = static::DEFAULT_CSS_TEMPLATE_REL_PATH;
|
||||
$this->sGlobalTemplateRelPath = static::DEFAULT_GLOBAL_TEMPLATE_REL_PATH;
|
||||
}
|
||||
@@ -111,8 +117,11 @@ abstract class UIBlock implements iUIBlock
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetJsTemplateRelPath() {
|
||||
return $this->sJsTemplateRelPath;
|
||||
public function GetJsTemplateRelPath(string $sType) {
|
||||
if ($sType != self::JS_TYPE_LIVE && $sType != self::JS_TYPE_ON_INIT && $sType != self::JS_TYPE_ON_READY){
|
||||
throw new UIException($this, "Type of javascript $sType not supported");
|
||||
}
|
||||
return $this->aJsTemplateRelPath[$sType];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base;
|
||||
use Combodo\iTop\Application\UI\Base\UIException;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,9 @@ namespace Combodo\iTop\Application\UI\Base;
|
||||
* @since 3.0.0
|
||||
*/
|
||||
interface iUIBlock {
|
||||
public const JS_TYPE_ON_INIT = "js";
|
||||
public const JS_TYPE_LIVE = "live.js";
|
||||
public const JS_TYPE_ON_READY = "ready.js";
|
||||
/**
|
||||
* Return the relative path (from <ITOP>/templates/) of the global template (HTML, JS, CSS) to use or null if it's not provided. Should not be used to often as JS/CSS files would be duplicated making the browser parsing time way longer.
|
||||
*
|
||||
@@ -46,9 +50,11 @@ interface iUIBlock {
|
||||
/**
|
||||
* Return the relative path (from <ITOP>/templates/) of the JS template to use or null if there is no inline JS to render
|
||||
*
|
||||
* @param string $sType javascript type only JS_TYPE_ON_INIT / JS_TYPE_ON_READY / JS_TYPE_LIVE
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetJsTemplateRelPath();
|
||||
public function GetJsTemplateRelPath(string $sType) ;
|
||||
|
||||
/**
|
||||
* Return an array of the relative paths (from <ITOP>/) of the JS files to use for the block itself
|
||||
|
||||
@@ -151,6 +151,15 @@ class AjaxPage extends WebPage implements iTabbedPage
|
||||
header($s_header);
|
||||
}
|
||||
|
||||
// CSS files
|
||||
foreach ($this->oContentLayout->GetCssFilesUrlRecursively(true) as $sFileAbsUrl) {
|
||||
$this->add_linked_stylesheet($sFileAbsUrl);
|
||||
}
|
||||
// JS files
|
||||
foreach ($this->oContentLayout->GetJsFilesUrlRecursively(true) as $sFileAbsUrl) {
|
||||
$this->add_linked_script($sFileAbsUrl);
|
||||
}
|
||||
|
||||
// Render the blocks
|
||||
// Additional UI widgets to be activated inside the ajax fragment
|
||||
// Important: Testing the content type is not enough because some ajax handlers have not correctly positionned the flag (e.g json response corrupted by the script)
|
||||
@@ -350,6 +359,26 @@ EOF
|
||||
$this->s_deferred_content .= $s_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\Application\UI\Base\iUIBlock $oBlock
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function RenderInlineScriptsAndCSSRecursively(iUIBlock $oBlock): void
|
||||
{
|
||||
$oBlockRenderer = new BlockRenderer($oBlock);
|
||||
$this->add_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_LIVE));
|
||||
$this->add_ready_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_INIT));
|
||||
$this->add_ready_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_READY));
|
||||
$this->add_style($oBlockRenderer->RenderCssInline());
|
||||
|
||||
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
|
||||
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\iUIBlock;
|
||||
use Combodo\iTop\Renderer\BlockRenderer;
|
||||
|
||||
/**
|
||||
* Web page with some associated CSS and scripts (jquery) for a fancier display
|
||||
*/
|
||||
@@ -50,21 +53,6 @@ class NiceWebPage extends WebPage
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.pager.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablehover.js');
|
||||
//TODO end deprecated in 3.0.0
|
||||
// Datatables added in 3.0.0
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'lib/datatables/js/jquery.dataTables.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'lib/datatables/js/dataTables.bootstrap.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'lib/datatables/js/dataTables.fixedHeader.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'lib/datatables/js/dataTables.responsive.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'lib/datatables/js/dataTables.scroller.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'lib/datatables/js/dataTables.select.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/dataTables.settings.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/dataTables.pipeline.js');
|
||||
/*$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'lib/datatables/css/dataTables.bootstrap.min.css');
|
||||
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'lib/datatables/css/fixedHeader.bootstrap.min.css');
|
||||
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'lib/datatables/css/responsive.bootstrap.min.css');
|
||||
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'lib/datatables/css/scroller.bootstrap.min.css');
|
||||
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'lib/datatables/css/select.bootstrap.min.css');
|
||||
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'lib/datatables/css/select.dataTables.min.css');*/
|
||||
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.positionBy.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.popupmenu.js');
|
||||
@@ -210,7 +198,31 @@ EOF
|
||||
$this->m_aReadyScripts[] = $sScript;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\Application\UI\Base\iUIBlock $oBlock
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function RenderInlineScriptsAndCSSRecursively(iUIBlock $oBlock): void
|
||||
{
|
||||
$oBlockRenderer = new BlockRenderer($oBlock);
|
||||
$this->add_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_LIVE));
|
||||
$this->add_ready_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_INIT));
|
||||
$this->add_ready_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_READY));
|
||||
|
||||
$this->add_style($oBlockRenderer->RenderCssInline());
|
||||
|
||||
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
|
||||
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
|
||||
}
|
||||
|
||||
foreach ($oBlock->GetDeferredBlocks() as $oSubBlock) {
|
||||
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Outputs (via some echo) the complete HTML page by assembling all its elements
|
||||
*/
|
||||
|
||||
@@ -716,15 +716,10 @@ class WebPage implements Page
|
||||
public function RenderInlineScriptsAndCSSRecursively(iUIBlock $oBlock): void
|
||||
{
|
||||
$oBlockRenderer = new BlockRenderer($oBlock);
|
||||
$sInlineScript = trim($oBlockRenderer->RenderJsInline());
|
||||
if (!empty($sInlineScript)) {
|
||||
$this->add_script($sInlineScript);
|
||||
}
|
||||
$this->add_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_INIT));
|
||||
$this->add_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_LIVE));
|
||||
|
||||
$sInlineStyle = trim($oBlockRenderer->RenderCssInline());
|
||||
if (!empty($sInlineStyle)) {
|
||||
$this->add_style($sInlineStyle);
|
||||
}
|
||||
$this->add_style($oBlockRenderer->RenderCssInline());
|
||||
|
||||
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
|
||||
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
|
||||
|
||||
@@ -832,7 +832,9 @@ EOF;
|
||||
public function RenderInlineScriptsAndCSSRecursively(iUIBlock $oBlock): void
|
||||
{
|
||||
$oBlockRenderer = new BlockRenderer($oBlock);
|
||||
$this->add_init_script($oBlockRenderer->RenderJsInline());
|
||||
$this->add_init_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_INIT));
|
||||
$this->add_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_LIVE));
|
||||
$this->add_ready_script($oBlockRenderer->RenderJsInline(iUIBlock::JS_TYPE_ON_READY));
|
||||
$this->add_style($oBlockRenderer->RenderCssInline());
|
||||
|
||||
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
|
||||
|
||||
@@ -7,6 +7,60 @@
|
||||
{{ render_block(oLayout, {aPage: aPage}) }}
|
||||
{% endif %}
|
||||
|
||||
{% block iboPageJsInlineLive %}
|
||||
{% for sJsInline in aPage.aJsInlineLive %}
|
||||
{# We put each scripts in a dedicated script tag to prevent massive failure if 1 script is broken (eg. missing semi-colon or non closed multi-line comment) #}
|
||||
<script type="text/javascript">
|
||||
{{ sJsInline|raw }}
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% if aPage.aJsFiles is not empty %}
|
||||
{% set sId = oUIBlock.GetId() | variable_name %}
|
||||
{% block iboPageJsFiles %}
|
||||
<script type="text/javascript">
|
||||
var aFilesToLoad{{ sId }} = [];
|
||||
{% for sJsFile in aPage.aJsFiles %}
|
||||
aFilesToLoad{{ sId }}.push('{{ sJsFile|raw }}');
|
||||
{% endfor %}
|
||||
var iCurrentIdx{{ sId }} = 0;
|
||||
var iFilesToLoadCount{{ sId }} = aFilesToLoad{{ sId }}.length;
|
||||
var fLoadScript{{ sId }} = function(){
|
||||
$.when(
|
||||
$.ajax({
|
||||
url: aFilesToLoad{{ sId }}[iCurrentIdx{{ sId }}],
|
||||
dataType: 'script',
|
||||
cache: true
|
||||
})
|
||||
)
|
||||
.then(function(){
|
||||
iCurrentIdx{{ sId }}++;
|
||||
if (iCurrentIdx{{ sId }} !== iFilesToLoadCount{{ sId }})
|
||||
{
|
||||
fLoadScript{{ sId }}();
|
||||
}
|
||||
else
|
||||
{
|
||||
{% for sJsInlineOnDomReady in aPage.aJsInlineOnDomReady %}
|
||||
{{ sJsInlineOnDomReady|raw }}
|
||||
{% endfor %}
|
||||
}
|
||||
});
|
||||
};
|
||||
fLoadScript{{ sId }}();
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% else %}
|
||||
{% block iboPageJsInlineOnDomReady %}
|
||||
{% for sJsInlineOnDomReady in aPage.aJsInlineOnDomReady %}
|
||||
<script type="text/javascript">
|
||||
{{ sJsInlineOnDomReady|raw }}
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% if aDeferredBlocks is not empty %}
|
||||
{# TODO 3.0.0 #}
|
||||
{# <script type="text/javascript"> #}
|
||||
@@ -17,41 +71,27 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block iboPageJsInlineLive %}
|
||||
{% for sJsInline in aPage.aJsInlineLive %}
|
||||
{# We put each scripts in a dedicated script tag to prevent massive failure if 1 script is broken (eg. missing semi-colon or non closed multi-line comment) #}
|
||||
<script type="text/javascript">
|
||||
{{ sJsInline|raw }}
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% if sDeferredContent %}
|
||||
<script type="text/javascript">
|
||||
$('body').append('{{ sDeferredContent|raw }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% block iboPageJsFiles %}
|
||||
{% for sJsFile in aPage.aJsFiles %}
|
||||
<script type="text/javascript">
|
||||
$.getScript('{{ sJsFile|raw }}');
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block iboPageJsInlineOnDomReady %}
|
||||
{% for sJsInline in aPage.aJsInlineOnDomReady %}
|
||||
<script type="text/javascript">
|
||||
{{ sJsInline|raw }}
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block iboPageCssFiles %}
|
||||
{% for aCssFileData in aPage.aCssFiles %}
|
||||
<script type="text/javascript">
|
||||
/* $.ajax({
|
||||
url: aKBFilesToLoad[iKBCurrentIdx].url,
|
||||
dataType: aKBFilesToLoad[iKBCurrentIdx].type,
|
||||
cache: true
|
||||
})
|
||||
.done(function(){
|
||||
if ( (aKBFilesToLoad[iKBCurrentIdx].type === 'text') && ($('head link[type="text/css"][href="' + aKBFilesToLoad[iKBCurrentIdx].url + '"]').length === 0) )
|
||||
{
|
||||
$('<link rel="stylesheet" type="text/css" href="' + aKBFilesToLoad[iKBCurrentIdx].url + '" />').appendTo('head');
|
||||
}
|
||||
})
|
||||
)*/
|
||||
if (!$('link[href="{{ aCssFileData.link }}"]').length) $('<link href="{{ aCssFileData.link }}" rel="stylesheet">').appendTo('head');
|
||||
</script>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user