N°3123 : Improved JavaScript management in web pages and ajax pages

This commit is contained in:
acognet
2020-12-08 08:59:01 +01:00
parent 0808a76226
commit 5ccb12453a
11 changed files with 185 additions and 65 deletions

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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
//

View File

@@ -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;

View File

@@ -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];
}
/**

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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 %}