N°2847 - Datatable modal dialogs

This commit is contained in:
Eric
2020-11-19 17:45:46 +01:00
parent 26d912f059
commit 32e0c8f9bf
24 changed files with 541 additions and 170 deletions

View File

@@ -368,7 +368,7 @@ EOF;
$oPage->add_linked_script($sFileAbsUrl);
}
$oPage->RenderInlineTemplatesRecursively($oBlock);
$oPage->RenderInlineScriptsAndCSSRecursively($oBlock);
return BlockRenderer::RenderBlockTemplates($oBlock);
}

View File

@@ -228,9 +228,9 @@ $(function()
_destroy: function()
{
this.element.removeClass('itop-datatable');
$('#sfl_'+this.options.sListId).remove();
$('#datatable_dlg_'+this.options.sListId).remove();
$('#sfl_' + this.options.sListId).remove();
$('#datatable_dlg_' + this.options.sListId).remove();
},
// _setOptions is called with a hash of all options that are changing
_setOptions: function()
@@ -274,32 +274,27 @@ $(function()
dlgElement.find('input[name=page_size]').val(iPageSize);
dlgElement.find(':itop-fieldsorter').fieldsorter('option', { fields: this.options.oColumns });
},
_saveDlgState: function()
{
this.originalState = {};
for(k in this.aDlgStateParams)
{
this.originalState[this.aDlgStateParams[k]] = this.options[this.aDlgStateParams[k]];
}
this.originalState.oFields = $('#datatable_dlg_'+this.options.sListId).find(':itop-fieldsorter').fieldsorter('get_params');
},
_restoreDlgState: function()
{
var dlgElement = $('#datatable_dlg_'+this.options.sListId);
_saveDlgState: function () {
this.originalState = {};
for (k in this.aDlgStateParams) {
this.originalState[this.aDlgStateParams[k]] = this.options[this.aDlgStateParams[k]];
}
this.originalState.oFields = $('#datatable_dlg_' + this.options.sListId).find(':itop-fieldsorter').fieldsorter('get_params');
},
_restoreDlgState: function () {
var dlgElement = $('#datatable_dlg_' + this.options.sListId);
for(k in this.aDlgStateParams)
{
this._setOption(this.aDlgStateParams[k], this.originalState[this.aDlgStateParams[k]]);
this._refresh();
}
dlgElement.find('input[name=page_size]').val(this.originalState.iDefaultPageSize);
dlgElement.find(':itop-fieldsorter').fieldsorter('option', { fields: this.originalState.oFields });
for (k in this.aDlgStateParams) {
this._setOption(this.aDlgStateParams[k], this.originalState[this.aDlgStateParams[k]]);
}
$('#datatable_dlg_'+this.options.sListId).unblock();
dlgElement.find('input[name=page_size]').val(this.originalState.iDefaultPageSize);
},
dlgElement.find(':itop-fieldsorter').fieldsorter('option', {fields: this.originalState.oFields});
dlgElement.unblock();
},
IsDialogOpen: function()
{
var oDlgOpen = $('#datatable_dlg_'+this.options.sListId+' :visible');

View File

@@ -338,6 +338,7 @@ return array(
'DashletProxy' => $baseDir . '/application/dashlet.class.inc.php',
'DashletUnknown' => $baseDir . '/application/dashlet.class.inc.php',
'DataTable' => $baseDir . '/application/datatable.class.inc.php',
'DataTableConfig' => $baseDir . '/sources/application/UI/Component/DataTable/DataTableConfig/DataTableConfig.php',
'Datamatrix' => $vendorDir . '/combodo/tcpdf/include/barcodes/datamatrix.php',
'DateTimeFormat' => $baseDir . '/core/datetimeformat.class.inc.php',
'DeadLockLog' => $baseDir . '/core/log.class.inc.php',

View File

@@ -568,6 +568,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'DashletProxy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletUnknown' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DataTable' => __DIR__ . '/../..' . '/application/datatable.class.inc.php',
'DataTableConfig' => __DIR__ . '/../..' . '/sources/application/UI/Component/DataTable/DataTableConfig/DataTableConfig.php',
'Datamatrix' => __DIR__ . '/..' . '/combodo/tcpdf/include/barcodes/datamatrix.php',
'DateTimeFormat' => __DIR__ . '/../..' . '/core/datetimeformat.class.inc.php',
'DeadLockLog' => __DIR__ . '/../..' . '/core/log.class.inc.php',

View File

@@ -17,7 +17,10 @@
* You should have received a copy of the GNU Affero General Public License
*/
use Combodo\iTop\Application\UI\Component\Button\ButtonFactory;
use Combodo\iTop\Application\UI\Component\Form\Form;
use Combodo\iTop\Application\UI\Component\GlobalSearch\GlobalSearchHelper;
use Combodo\iTop\Application\UI\Component\Input\InputFactory;
use Combodo\iTop\Application\UI\Component\QuickCreate\QuickCreateHelper;
use Combodo\iTop\Application\UI\Layout\PageContent\PageContentFactory;
@@ -274,33 +277,34 @@ function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '',
*/
function DisplayMultipleSelectionForm($oP, $oFilter, $sNextOperation, $oChecker, $aExtraFormParams = array())
{
$oAppContext = new ApplicationContext();
$iBulkActionAllowed = $oChecker->IsAllowed();
$aExtraParams = array('selection_type' => 'multiple', 'selection_mode' => true, 'display_limit' => false, 'menu' => false);
if ($iBulkActionAllowed == UR_ALLOWED_DEPENDS)
{
$aExtraParams['selection_enabled'] = $oChecker->GetAllowedIDs();
}
else if(UR_ALLOWED_NO)
{
$oAppContext = new ApplicationContext();
$iBulkActionAllowed = $oChecker->IsAllowed();
$aExtraParams = array('selection_type' => 'multiple', 'selection_mode' => true, 'display_limit' => false, 'menu' => false);
if ($iBulkActionAllowed == UR_ALLOWED_DEPENDS) {
$aExtraParams['selection_enabled'] = $oChecker->GetAllowedIDs();
} else {
if (UR_ALLOWED_NO) {
throw new ApplicationException(Dict::Format('UI:ActionNotAllowed'));
}
$oBlock = new DisplayBlock($oFilter, 'list', false);
$oP->add("<form method=\"post\" action=\"./UI.php\">\n");
$oP->add("<input type=\"hidden\" name=\"operation\" value=\"$sNextOperation\">\n");
$oP->add("<input type=\"hidden\" name=\"class\" value=\"".$oFilter->GetClass()."\">\n");
$oP->add("<input type=\"hidden\" name=\"filter\" value=\"".htmlentities($oFilter->Serialize(), ENT_QUOTES, 'UTF-8')."\">\n");
$oP->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::GetNewTransactionId()."\">\n");
foreach($aExtraFormParams as $sName => $sValue)
{
$oP->add("<input type=\"hidden\" name=\"$sName\" value=\"$sValue\">\n");
}
$oP->add($oAppContext->GetForForm());
$oBlock->Display($oP, 1, $aExtraParams);
$oP->add("<input type=\"button\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"window.history.back()\">&nbsp;&nbsp;<input type=\"submit\" value=\"".Dict::S('UI:Button:Next')."\">\n");
$oP->add("</form>\n");
$oP->add_ready_script("$('#1 table.listResults').trigger('check_all');");
}
$oForm = new Form();
$oForm->SetAction('./UI.php');
$oForm->AddSubBlock(InputFactory::MakeForHidden('operation', $sNextOperation));
$oForm->AddSubBlock(InputFactory::MakeForHidden('class', $oFilter->GetClass()));
$oForm->AddSubBlock(InputFactory::MakeForHidden('filter', utils::HtmlEntities($oFilter->Serialize())));
$oForm->AddSubBlock(InputFactory::MakeForHidden('transaction_id', utils::GetNewTransactionId()));
foreach ($aExtraFormParams as $sName => $sValue) {
$oForm->AddSubBlock(InputFactory::MakeForHidden($sName, $sValue));
}
$oForm->AddSubBlock($oAppContext->GetForFormBlock());
$oDisplayBlock = new DisplayBlock($oFilter, 'list', false);
$oForm->AddSubBlock($oDisplayBlock->GetDisplay($oP, 1, $aExtraParams));
$oForm->AddSubBlock(ButtonFactory::MakeNeutral(Dict::S('UI:Button:Cancel'), 'cancel')->SetOnClickJsCode('window.history.back()'));
$oForm->AddSubBlock(ButtonFactory::MakeForPrimaryAction(Dict::S('UI:Button:Next'), 'next', Dict::S('UI:Button:Next'), true));
$oP->AddUiBlock($oForm);
$oP->add_ready_script("$('#1 table.listResults').trigger('check_all');");
}
function DisplayNavigatorListTab($oP, $aResults, $sRelation, $sDirection, $oObj)

View File

@@ -87,8 +87,8 @@ class Extension
);
// Filter to sanitize an XML / HTML identifier
// Usage in twig: {{ 'identifier:to-sanitize' }}
$oTwigEnv->addFilter(new Twig_SimpleFilter('sanitize_identifier', function($sString) {
// Usage in twig: {{ 'identifier:to-sanitize'|sanitize_identifier }}
$oTwigEnv->addFilter(new Twig_SimpleFilter('sanitize_identifier', function ($sString) {
return utils::Sanitize($sString, '', utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
})
);
@@ -114,18 +114,23 @@ class Extension
$oTwigEnv->addFilter(new Twig_SimpleFilter('add_module_version', function ($sUrl, $sModuleName) {
$sModuleVersion = utils::GetCompiledModuleVersion($sModuleName);
if (strpos($sUrl, '?') === false)
{
if (strpos($sUrl, '?') === false) {
$sUrl = $sUrl."?moduleversion=".$sModuleVersion;
}
else
{
} else {
$sUrl = $sUrl."&moduleversion=".$sModuleVersion;
}
return $sUrl;
}));
// Filter to sanitize a string (escape ')
// Usage in twig: {{ 'string'|escape_quotes }}
$oTwigEnv->addFilter(new Twig_SimpleFilter('escape_for_js_string', function ($sString) {
return str_replace(["'", "\n"], ["\\'", " "], $sString);
})
);
// Function to check our current environment
// Usage in twig: {% if is_development_environment() %}
$oTwigEnv->addFunction(new Twig_SimpleFunction('is_development_environment', function () {

View File

@@ -8,6 +8,7 @@ namespace Combodo\iTop\Application\UI\Component\DataTable;
use Combodo\iTop\Application\UI\Layout\UIContentBlock;
use DataTableConfig;
/**
* Class DataTable
@@ -35,6 +36,7 @@ class DataTableBlock extends UIContentBlock
public function __construct(?string $sId = null)
{
parent::__construct($sId);
$this->AddDeferredBlock(new DataTableConfig($this));
}
/**

View File

@@ -0,0 +1,43 @@
<?php
use Combodo\iTop\Application\UI\Component\DataTable\DataTableBlock;
use Combodo\iTop\Application\UI\Layout\UIContentBlock;
/**
* @copyright Copyright (C) 2010-2020 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class DataTableConfig extends UIContentBlock
{
// Overloaded constants
public const BLOCK_CODE = 'ibo-datatableconfig';
public const HTML_TEMPLATE_REL_PATH = 'components/datatable/config/layout';
/** @var DataTableBlock */
private $oDataTable;
public function __construct(DataTableBlock $oDataTable, ?string $sId = null)
{
parent::__construct($sId);
$this->oDataTable = $oDataTable;
}
/**
* @return \Combodo\iTop\Application\UI\Component\DataTable\DataTableBlock
*/
private function GetDataTable(): DataTableBlock
{
return $this->oDataTable;
}
public function GetOption(string $sOption)
{
return $this->GetDataTable()->GetOption($sOption);
}
public function GetTableId()
{
return $this->GetDataTable()->GetId();
}
}

View File

@@ -10,7 +10,6 @@ use ApplicationException;
use appUserPreferences;
use AttributeLinkedSet;
use cmdbAbstractObject;
use CMDBObjectSet;
use Combodo\iTop\Application\UI\Component\DataTable\StaticTable\FormTable\FormTable;
use Combodo\iTop\Application\UI\Component\DataTable\StaticTable\StaticTable;
use Combodo\iTop\Application\UI\Component\Panel\PanelFactory;
@@ -35,7 +34,7 @@ class DataTableFactory
/**
* @param \WebPage $oPage
* @param string $sListId
* @param \CMDBObjectSet $oSet
* @param \DBObjectSet $oSet
* @param array $aExtraParams
*
* @return \Combodo\iTop\Application\UI\Component\Panel\Panel
@@ -50,7 +49,7 @@ class DataTableFactory
* @throws \OQLException
* @throws \ReflectionException
*/
public static function MakeForResult(WebPage $oPage, string $sListId, CMDBObjectSet $oSet, $aExtraParams = array())
public static function MakeForResult(WebPage $oPage, string $sListId, DBObjectSet $oSet, $aExtraParams = array())
{
$oPanel = PanelFactory::MakeForClass($oSet->GetClass(), "Result")->AddCSSClasses('ibo-datatable-panel');
$oDataTable = DataTableFactory::MakeForRendering($sListId, $oSet, $aExtraParams);
@@ -101,7 +100,7 @@ class DataTableFactory
* Make a basis Panel component
*
* @param string $sListId
* @param \CMDBObjectSet $oSet
* @param \DBObjectSet $oSet
* @param array $aExtraParams
*
* @return DataTableBlock
@@ -112,7 +111,7 @@ class DataTableFactory
* @throws \DictExceptionMissingString
* @throws \MySQLException
*/
public static function MakeForRendering(string $sListId, CMDBObjectSet $oSet, $aExtraParams = array())
public static function MakeForRendering(string $sListId, DBObjectSet $oSet, $aExtraParams = array())
{
$oDataTable = new DataTableBlock('datatable_'.$sListId);
///////////////////////////////////////////////////
@@ -474,7 +473,6 @@ class DataTableFactory
$aColumnDefinition = [];
$aSortOrder=[];
$aSortDatable=[];
$iIndexColumn=0;
if($sSelectMode!="") {
$iIndexColumn++;

View File

@@ -225,7 +225,8 @@ class PageContent extends UIBlock implements iUIContentBlock {
*
* @inheritDoc
*/
public function GetSubBlocks(): array {
public function GetSubBlocks(): array
{
$aSubBlocks = [];
foreach ($this->GetContentAreas() as $oContentArea) {
$aSubBlocks = array_merge($aSubBlocks, $oContentArea->GetSubBlocks());
@@ -233,4 +234,90 @@ class PageContent extends UIBlock implements iUIContentBlock {
return $aSubBlocks;
}
public function GetDeferredBlocks(): array
{
$aSubBlocks = [];
foreach ($this->GetContentAreas() as $oContentArea) {
$aSubBlocks = array_merge($aSubBlocks, $oContentArea->GetDeferredBlocks());
}
return $aSubBlocks;
}
/**
* Add the $oDeferredBlock directly in the main area
*
* @inheritDoc
*/
public function AddDeferredBlock(iUIBlock $oDeferredBlock)
{
$this->AddDeferredBlockToContentArea(static::ENUM_CONTENT_AREA_MAIN, $oDeferredBlock);
return $this;
}
/**
* Remove a specified subBlock from all the areas
*
* @param string $sId
*
* @return $this
*/
public function RemoveDeferredBlock(string $sId)
{
foreach ($this->GetContentAreas() as $oContentArea) {
$oContentArea->RemoveDeferredBlock($sId);
}
return $this;
}
/**
* Check if the specified subBlock is within one of all the areas
*
* @param string $sId
*
* @return bool
*/
public function HasDeferredBlock(string $sId): bool
{
foreach ($this->GetContentAreas() as $oContentArea) {
if ($oContentArea->HasDeferredBlock($sId)) {
return true;
}
}
return false;
}
/**
* Get a specific subBlock within all the areas
*
* @inheritDoc
*/
public function GetDeferredBlock(string $sId): ?iUIBlock
{
foreach ($this->GetContentAreas() as $oContentArea) {
$oDeferredBlock = $oContentArea->GetDeferredBlock($sId);
if (!is_null($oDeferredBlock)) {
return $oDeferredBlock;
}
}
return null;
}
/**
* Set the MAIN AREA subBlocks
*
* @inheritDoc
* @return $this|\Combodo\iTop\Application\UI\Layout\iUIContentBlock
*/
public function SetDeferredBlocks(array $aDeferredBlocks): iUIContentBlock
{
$this->SetContentAreaDeferredBlocks(self::ENUM_CONTENT_AREA_MAIN, $aDeferredBlocks);
return $this;
}
}

View File

@@ -33,6 +33,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
protected $aDataAttributes;
/** @var array */
protected $aSubBlocks;
/** @var array */
protected $aDeferredBlocks;
/**
* UIContentBlock constructor.
@@ -45,6 +47,7 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
parent::__construct($sName);
$this->aSubBlocks = [];
$this->aDeferredBlocks = [];
$this->aDataAttributes = [];
$this->SetCSSClasses($sContainerClass);
}
@@ -52,7 +55,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
/**
* @inheritDoc
*/
public function AddHtml(string $sHtml) {
public function AddHtml(string $sHtml)
{
$oBlock = new Html($sHtml);
$this->AddSubBlock($oBlock);
@@ -62,7 +66,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
/**
* @inheritDoc
*/
public function AddSubBlock(iUIBlock $oSubBlock) {
public function AddSubBlock(iUIBlock $oSubBlock)
{
$this->aSubBlocks[$oSubBlock->GetId()] = $oSubBlock;
return $this;
@@ -71,7 +76,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
/**
* @inheritDoc
*/
public function RemoveSubBlock(string $sId) {
public function RemoveSubBlock(string $sId)
{
if ($this->HasSubBlock($sId)) {
unset($this->aSubBlocks[$sId]);
}
@@ -82,28 +88,32 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
/**
* @inheritDoc
*/
public function HasSubBlock(string $sId): bool {
public function HasSubBlock(string $sId): bool
{
return array_key_exists($sId, $this->aSubBlocks);
}
/**
* @inheritDoc
*/
public function GetSubBlock(string $sId): ?iUIBlock {
public function GetSubBlock(string $sId): ?iUIBlock
{
return isset($this->aSubBlocks[$sId]) ? $this->aSubBlocks[$sId] : null;
}
/**
* @inheritDoc
*/
public function GetSubBlocks(): array {
public function GetSubBlocks(): array
{
return $this->aSubBlocks;
}
/**
* @inheritDoc
*/
public function SetSubBlocks(array $aSubBlocks) {
public function SetSubBlocks(array $aSubBlocks)
{
foreach ($aSubBlocks as $oSubBlock) {
$this->AddSubBlock($oSubBlock);
}
@@ -114,7 +124,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
/**
* @return string
*/
public function GetCSSClasses(): string {
public function GetCSSClasses(): string
{
return implode(' ', $this->aCSSClasses);
}
@@ -123,7 +134,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
*
* @return UIContentBlock
*/
public function SetCSSClasses(string $sCSSClasses) {
public function SetCSSClasses(string $sCSSClasses)
{
$this->aCSSClasses = [];
$this->AddCSSClasses($sCSSClasses);
@@ -135,7 +147,8 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
*
* @return $this
*/
public function AddCSSClasses(string $sCSSClasses) {
public function AddCSSClasses(string $sCSSClasses)
{
foreach (explode(' ', $sCSSClasses) as $sCSSClass) {
if (!empty($sCSSClass)) {
$this->aCSSClasses[$sCSSClass] = $sCSSClass;
@@ -177,4 +190,62 @@ class UIContentBlock extends UIBlock implements iUIContentBlock
return $this;
}
/**
* @inheritDoc
*/
public function AddDeferredBlock(iUIBlock $oDeferredBlock)
{
$this->aDeferredBlocks[$oDeferredBlock->GetId()] = $oDeferredBlock;
return $this;
}
/**
* @inheritDoc
*/
public function RemoveDeferredBlock(string $sId)
{
if ($this->HasDeferredBlock($sId)) {
unset($this->aDeferredBlocks[$sId]);
}
return $this;
}
/**
* @inheritDoc
*/
public function HasDeferredBlock(string $sId): bool
{
return array_key_exists($sId, $this->aDeferredBlocks);
}
/**
* @inheritDoc
*/
public function GetDeferredBlock(string $sId): ?iUIBlock
{
return isset($this->aDeferredBlocks[$sId]) ? $this->aDeferredBlocks[$sId] : null;
}
/**
* @inheritDoc
*/
public function GetDeferredBlocks(): array
{
return $this->aDeferredBlocks;
}
/**
* @inheritDoc
*/
public function SetDeferredBlocks(array $aDeferredBlocks)
{
foreach ($aDeferredBlocks as $oDeferredBlock) {
$this->AddDeferredBlock($oDeferredBlock);
}
return $this;
}
}

View File

@@ -79,4 +79,59 @@ interface iUIContentBlock {
* @return $this
*/
public function SetSubBlocks(array $aSubBlocks);
/**
* Add $oDeferredBlock, replacing any block with the same ID
*
* @param \Combodo\iTop\Application\UI\iUIBlock $oDeferredBlock
*
* @return $this
*/
public function AddDeferredBlock(iUIBlock $oDeferredBlock);
/**
* Remove the sub block identified by $sId.
* Note that if no sub block matches the ID, it proceeds silently.
*
* @param string $sId ID of the sub block to remove
*
* @return $this
*/
public function RemoveDeferredBlock(string $sId);
/**
* Return true if there is a sub block identified by $sId
*
* @param string $sId
*
* @return bool
*/
public function HasDeferredBlock(string $sId): bool;
/**
* Return the sub block identified by $sId or null if not found
*
* @param string $sId
*
* @return \Combodo\iTop\Application\UI\iUIBlock|null
*/
public function GetDeferredBlock(string $sId): ?iUIBlock;
/**
* Return an array of all the sub blocks
*
* @return \Combodo\iTop\Application\UI\iUIBlock[]
*/
public function GetDeferredBlocks(): array;
/**
* Set all sub blocks at once, replacing all existing ones
*
* @param \Combodo\iTop\Application\UI\iUIBlock[] $aDeferredBlocks
*
* @return $this
*/
public function SetDeferredBlocks(array $aDeferredBlocks);
}

View File

@@ -106,14 +106,16 @@ abstract class UIBlock implements iUIBlock
/**
* @inheritDoc
*/
public static function GetCssTemplateRelPath() {
public static function GetCssTemplateRelPath()
{
return static::CSS_TEMPLATE_REL_PATH;
}
/**
* @inheritDoc
*/
public static function GetCssFilesRelPaths() {
public static function GetCssFilesRelPaths()
{
return static::CSS_FILES_REL_PATH;
}
@@ -123,14 +125,16 @@ abstract class UIBlock implements iUIBlock
* @return string
* @see static::BLOCK_CODE
*/
public function GetBlockCode() {
public function GetBlockCode()
{
return static::BLOCK_CODE;
}
/**
* @inheritDoc
*/
public function GetId() {
public function GetId()
{
return $this->sId;
}
@@ -138,7 +142,17 @@ abstract class UIBlock implements iUIBlock
* @inheritDoc
* @return \Combodo\iTop\Application\UI\iUIBlock[]
*/
public function GetSubBlocks() {
public function GetSubBlocks()
{
return [];
}
/**
* @inheritDoc
* @return \Combodo\iTop\Application\UI\iUIBlock[]
*/
public function GetDeferredBlocks(): array
{
return [];
}
@@ -146,7 +160,8 @@ abstract class UIBlock implements iUIBlock
* @inheritDoc
* @throws \Exception
*/
public function GetJsFilesUrlRecursively(bool $bAbsoluteUrl = false) {
public function GetJsFilesUrlRecursively(bool $bAbsoluteUrl = false)
{
return $this->GetFilesUrlRecursively(static::ENUM_BLOCK_FILES_TYPE_JS, $bAbsoluteUrl);
}
@@ -154,7 +169,8 @@ abstract class UIBlock implements iUIBlock
* @inheritDoc
* @throws \Exception
*/
public function GetCssFilesUrlRecursively(bool $bAbsoluteUrl = false) {
public function GetCssFilesUrlRecursively(bool $bAbsoluteUrl = false)
{
return $this->GetFilesUrlRecursively(static::ENUM_BLOCK_FILES_TYPE_CSS, $bAbsoluteUrl);
}
@@ -162,7 +178,8 @@ abstract class UIBlock implements iUIBlock
* @return array
* @throws \Exception
*/
public function GetJsTemplateRelPathRecursively(): array {
public function GetJsTemplateRelPathRecursively(): array
{
return $this->GetUrlRecursively(static::ENUM_BLOCK_FILES_TYPE_JS, static::ENUM_BLOCK_FILES_TYPE_TEMPLATE, false);
}
@@ -170,7 +187,8 @@ abstract class UIBlock implements iUIBlock
* @return array
* @throws \Exception
*/
public function GetCssTemplateRelPathRecursively(): array {
public function GetCssTemplateRelPathRecursively(): array
{
return $this->GetUrlRecursively(static::ENUM_BLOCK_FILES_TYPE_CSS, static::ENUM_BLOCK_FILES_TYPE_TEMPLATE, false);
}

View File

@@ -86,6 +86,14 @@ interface iUIBlock {
*/
public function GetSubBlocks();
/**
* Return an array of iUIBlock to add at the end of the page
* Must be an associative array (<BLOCK_ID> => <BLOCK_INSTANCE>)
*
* @return \Combodo\iTop\Application\UI\iUIBlock[]
*/
public function GetDeferredBlocks(): array;
/**
* Return an array of the JS files URL necessary for the block and all its sub blocks.
* URLs are relative unless the $bAbsolutePath is set to true.

View File

@@ -63,7 +63,8 @@ trait tUIContentAreas {
*
* @return $this
*/
protected function SetContentAreaBlocks(string $sAreaId, array $aBlocks) {
protected function SetContentAreaBlocks(string $sAreaId, array $aBlocks)
{
if (!isset($this->aContentAreasBlocks[$sAreaId])) {
$this->aContentAreasBlocks[$sAreaId] = new UIContentBlock($sAreaId);
}
@@ -73,6 +74,25 @@ trait tUIContentAreas {
return $this;
}
/**
* Set all block for a content area at once, replacing all existing ones.
*
* @param string $sAreaId
* @param \Combodo\iTop\Application\UI\iUIBlock[] $aBlocks
*
* @return $this
*/
protected function SetContentAreaDeferredBlocks(string $sAreaId, array $aBlocks)
{
if (!isset($this->aContentAreasBlocks[$sAreaId])) {
$this->aContentAreasBlocks[$sAreaId] = new UIContentBlock($sAreaId);
}
$this->aContentAreasBlocks[$sAreaId]->SetDeferredBlocks($aBlocks);
return $this;
}
/**
* Return all blocks from the $sAreaId content area
*
@@ -81,7 +101,8 @@ trait tUIContentAreas {
* @return \Combodo\iTop\Application\UI\iUIBlock[]
* @throws \Combodo\iTop\Application\UI\UIException
*/
protected function GetContentAreaBlocks(string $sAreaId): array {
protected function GetContentAreaBlocks(string $sAreaId): array
{
if (!array_key_exists($sAreaId, $this->aContentAreasBlocks)) {
throw new UIException($this, Dict::Format('UIBlock:Error:CannotGetBlocks', $sAreaId, $this->GetId()));
}
@@ -98,7 +119,8 @@ trait tUIContentAreas {
*
* @return $this
*/
protected function AddBlockToContentArea(string $sAreaId, iUIBlock $oBlock) {
protected function AddBlockToContentArea(string $sAreaId, iUIBlock $oBlock)
{
if (!array_key_exists($sAreaId, $this->aContentAreasBlocks)) {
$this->aContentAreasBlocks[$sAreaId] = new UIContentBlock($sAreaId);
}
@@ -108,6 +130,26 @@ trait tUIContentAreas {
return $this;
}
/**
* Add $oBlock to the $sAreaId content area.
* Note that if the area doesn't exist yet, it is created. Also if a block with the same ID already exists, it will be replaced.
*
* @param string $sAreaId
* @param \Combodo\iTop\Application\UI\iUIBlock $oBlock
*
* @return $this
*/
protected function AddDeferredBlockToContentArea(string $sAreaId, iUIBlock $oBlock)
{
if (!array_key_exists($sAreaId, $this->aContentAreasBlocks)) {
$this->aContentAreasBlocks[$sAreaId] = new UIContentBlock($sAreaId);
}
$this->aContentAreasBlocks[$sAreaId]->AddDeferredBlock($oBlock);
return $this;
}
/**
* Remove the $sBlockId from the $sAreaId content area.
* Note that if the $sBlockId or the $sAreaId do not exist, it proceeds silently.

View File

@@ -165,11 +165,12 @@ EOF
}
$this->outputCollapsibleSectionInit();
$this->RenderInlineTemplatesRecursively($this->oContentLayout);
$this->RenderInlineScriptsAndCSSRecursively($this->oContentLayout);
$aData = [];
$aData['oLayout'] = $this->oContentLayout;
$aData['aDeferredBlocks'] = $this->GetDeferredBlocks($this->oContentLayout);
$aData['aPage'] = [
'sAbsoluteUrlAppRoot' => addslashes(utils::GetAbsoluteUrlAppRoot()),
@@ -341,7 +342,7 @@ EOF
/**
* @inheritDoc
*/
public function add_at_the_end($s_html, $sId = '')
public function add_at_the_end($s_html, $sId = null)
{
if ($sId != '') {
$this->add_script("$('#{$sId}').remove();"); // Remove any previous instance of the same Id

View File

@@ -18,9 +18,11 @@
*/
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
use Combodo\iTop\Application\UI\Component\Html\Html;
use Combodo\iTop\Application\UI\Component\PopoverMenu\PopoverMenu;
use Combodo\iTop\Application\UI\Component\PopoverMenu\PopoverMenuFactory;
use Combodo\iTop\Application\UI\iUIBlock;
use Combodo\iTop\Application\UI\Layout\iUIContentBlock;
use Combodo\iTop\Application\UI\Layout\UIContentBlock;
use Combodo\iTop\Renderer\BlockRenderer;
@@ -69,7 +71,7 @@ class WebPage implements Page
protected $bPrintable;
protected $bHasCollapsibleSection;
protected $bAddJSDict;
/** @var \Combodo\iTop\Application\UI\Layout\iUIContentBlock $oContentLayout */
/** @var iUIContentBlock $oContentLayout */
protected $oContentLayout;
protected $sTemplateRelPath;
@@ -170,9 +172,9 @@ class WebPage implements Page
* @param string $s_html
* @param string $sId
*/
public function add_at_the_end($s_html, $sId = '')
public function add_at_the_end($s_html, $sId = null)
{
$this->s_deferred_content .= $s_html;
$this->oContentLayout->AddDeferredBlock(new Html($s_html, $sId));
}
/**
@@ -711,7 +713,7 @@ class WebPage implements Page
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function RenderInlineTemplatesRecursively(iUIBlock $oBlock): void
public function RenderInlineScriptsAndCSSRecursively(iUIBlock $oBlock): void
{
$oBlockRenderer = new BlockRenderer($oBlock);
$sInlineScript = trim($oBlockRenderer->RenderJsInline());
@@ -725,8 +727,23 @@ class WebPage implements Page
}
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
$this->RenderInlineTemplatesRecursively($oSubBlock);
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
}
foreach ($oBlock->GetDeferredBlocks() as $oSubBlock) {
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
}
}
public function GetDeferredBlocks(iUIBlock $oBlock): array
{
$aDeferredBlocks = $oBlock->GetDeferredBlocks();
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
$aDeferredBlocks = array_merge($aDeferredBlocks, $this->GetDeferredBlocks($oSubBlock));
}
return $aDeferredBlocks;
}
/**
@@ -745,6 +762,7 @@ class WebPage implements Page
$aData = [];
$aData['oLayout'] = $this->oContentLayout;
$aData['aDeferredBlocks'] = $this->GetDeferredBlocks($this->oContentLayout);
// CSS files
foreach ($this->oContentLayout->GetCssFilesUrlRecursively(true) as $sFileAbsUrl) {
@@ -756,7 +774,7 @@ class WebPage implements Page
}
// Inline Templates
$this->RenderInlineTemplatesRecursively($this->oContentLayout);
$this->RenderInlineScriptsAndCSSRecursively($this->oContentLayout);
// Base structure of data to pass to the TWIG template
$aData['aPage'] = [

View File

@@ -401,7 +401,7 @@ JS
}
});
docWidth = $(document).width();
$('#ModalDlg').dialog({ autoOpen: false, modal: true, width: 0.8*docWidth, height: 'auto', maxHeight: $(window).height() - 50 }); // JQuery UI dialogs
// $('#ModalDlg').dialog({ autoOpen: false, modal: true, width: 0.8*docWidth, height: 'auto', maxHeight: $(window).height() - 50 }); // JQuery UI dialogs
ShowDebug();
$('#logOffBtn>ul').popupmenu();
@@ -770,14 +770,14 @@ EOF;
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function RenderInlineTemplatesRecursively(iUIBlock $oBlock): void
public function RenderInlineScriptsAndCSSRecursively(iUIBlock $oBlock): void
{
$oBlockRenderer = new BlockRenderer($oBlock);
$this->add_init_script($oBlockRenderer->RenderJsInline());
$this->add_style($oBlockRenderer->RenderCssInline());
foreach ($oBlock->GetSubBlocks() as $oSubBlock) {
$this->RenderInlineTemplatesRecursively($oSubBlock);
$this->RenderInlineScriptsAndCSSRecursively($oSubBlock);
}
}
@@ -839,6 +839,10 @@ EOF;
$aData['aLayouts']['oTopBar'] = $this->GetTopBarLayout();
// - Prepare content
$aData['aLayouts']['oPageContent'] = $this->GetContentLayout();
$aData['aDeferredBlocks'] = array_merge($this->GetDeferredBlocks($this->GetContentLayout()),
$this->GetDeferredBlocks($this->GetNavigationMenuLayout()),
$this->GetDeferredBlocks($this->GetTopBarLayout()));
// - Retrieve layouts linked files
// Note: Adding them now instead of in the template allow us to remove duplicates and lower the browser parsing time
/** @var \Combodo\iTop\Application\UI\UIBlock|string $oLayout */
@@ -856,7 +860,7 @@ EOF;
$this->add_linked_script($sFileAbsUrl);
}
$this->RenderInlineTemplatesRecursively($oLayout);
$this->RenderInlineScriptsAndCSSRecursively($oLayout);
}
// Components

View File

@@ -0,0 +1,44 @@
<div id="datatable_dlg_{{ oUIBlock.GetTableId() }}" style="display: none; background : white;" class="{{ oUIBlock.GetBlockCode() }}">
<input type="hidden" name="action" value="none"/>
<form id="form_{{ oUIBlock.GetTableId() }}" onsubmit="return false">
<p>
<input id="dtbl_dlg_settings_{{ oUIBlock.GetTableId() }}" type="radio" name="settings" {% if (oUIBlock.GetOption("bUseCustomSettings") == false) %} checked {% endif %} value="defaults">
<label for="dtbl_dlg_settings_{{ oUIBlock.GetTableId() }}">&nbsp;{{ 'UI:UseDefaultSettings'|dict_s }}</label>
</p>
<fieldset>
<legend class="transparent">
<input id="dtbl_dlg_specific_{{ oUIBlock.GetTableId() }}" type="radio" class="specific_settings" name="settings" {% if oUIBlock.GetOption("bUseCustomSettings") %} checked {% endif %} value="specific">
<label for="dtbl_dlg_specific_{{ oUIBlock.GetTableId() }}">&nbsp;&nbsp;{{ 'UI:UseSpecificSettings'|dict_s }}</label>
</legend>
{{ 'UI:ColumnsAndSortOrder'|dict_s }}<br/>
<ul class="sortable_field_list" id="sfl_{{ oUIBlock.GetTableId() }}">
</ul>
<p> {{ 'UI:Display_X_ItemsPerPage_prefix'|dict_s }}<input type="text" size="4" name="page_size" value="{{ oUIBlock.GetOption("iPageSize") }}">{{ 'UI:Display_X_ItemsPerPage_suffix'|dict_s }}</p>
</fieldset>
<fieldset>
<legend class="transparent">
<input id="dtbl_dlg_save_{{ oUIBlock.GetTableId() }}" type="checkbox" {% if oUIBlock.GetOption("sTableId") != null %}checked{% endif %} name="save_settings">
<label for="dtbl_dlg_save_{{ oUIBlock.GetTableId() }}">&nbsp;&nbsp;{{ 'UI:UseSavetheSettings'|dict_s }}</label>
</legend>
<p>
<input id="dtbl_dlg_this_list_{{ oUIBlock.GetTableId() }}" type="radio" name="scope" {% if oUIBlock.GetOption("sTableId") != null %} checked {% else %} disabled="disabled" stay-disabled="true"{% endif %} value="this_list">
<label for="dtbl_dlg_this_list_{{ oUIBlock.GetTableId() }}">&nbsp;&nbsp;{{ 'UI:OnlyForThisList'|dict_s }}</label>&nbsp;&nbsp;&nbsp;&nbsp;
<input id="dtbl_dlg_all_{{ oUIBlock.GetTableId() }}" type="radio" name="scope" {% if oUIBlock.GetOption("sTableId") == null %} checked {% endif %} value="defaults">
<label for="dtbl_dlg_all_{{ oUIBlock.GetTableId() }}">&nbsp;&nbsp;{{ 'UI:ForAllLists'|dict_s }}</label>
</p>
</fieldset>
<table style="width:100%">
<tr>
<td style="text-align:center;">
<button type="button" onclick="$('#datatable_dlg_{{ oUIBlock.GetTableId() }}').dialog('close')">&nbsp;{{ 'UI:Button:Cancel'|dict_s }}</button>
</td>
<td style="text-align:center;">
<button type="submit" onclick="$('#datatable_dlg_{{ oUIBlock.GetTableId() }}').DataTableSettings('onDlgOk'); ">&nbsp;{{ 'UI:Button:Ok'|dict_s }}</button>
</td>
</tr>
</table>
</form>
</div>

View File

@@ -1,62 +1,17 @@
{% for oSubBlock in oUIBlock.GetSubBlocks() %}{{ render_block(oSubBlock, {aPage: aPage}) }}{% endfor %}
{% if oUIBlock.GetOptions()["select_mode"] is defined %}
<input type="hidden" name="selectionMode" value="positive"/>
<input type="hidden" name="extra_params" value="{{ oUIBlock.GetAjaxData()["extra_params"]}}"/>
<input type="hidden" name="filter" value="{{ oUIBlock.GetAjaxData()["filter"]}}"/>
<input type="hidden" name="extra_params" value="{{ oUIBlock.GetAjaxData()["extra_params"] }}"/>
<input type="hidden" name="filter" value="{{ oUIBlock.GetAjaxData()["filter"] }}"/>
{% endif %}
<table id="{{ oUIBlock.GetId() }}" width="100%" class="ibo-datatable">
<thead>
{% if oUIBlock.GetOptions()["select_mode"] is defined %}
<th></th>
{% endif %}
{% for aColumn in oUIBlock.GetDisplayColumns() %}
<th class="ibo-datatable-header" title="{{ aColumn["description"] }}\">{{ aColumn["attribute_label"] }} </th>
{% endfor %}
</thead>
<table id="{{ oUIBlock.GetId() }}" width="100%" class="{{ oUIBlock.GetBlockCode() }}">
<thead>
{% if oUIBlock.GetOptions()["select_mode"] is defined %}
<th></th>
{% endif %}
{% for aColumn in oUIBlock.GetDisplayColumns() %}
<th class="ibo-datatable-header" title="{{ aColumn["description"] }}\">{{ aColumn["attribute_label"] }} </th>
{% endfor %}
</thead>
</table>
<div id="datatable_dlg_{{ oUIBlock.GetId() }}" style="display: none; background : white;" class="">
<input type="hidden" name="action" value="none"/>
<form id="form_{{ oUIBlock.GetId() }}" onsubmit="return false">
<p>
<input id="dtbl_dlg_settings_{{ oUIBlock.GetId() }}" type="radio" name="settings" {% if (oUIBlock.GetOptions()['bUseCustomSettings'] == false) %} checked {% endif %} value="defaults">
<label for="dtbl_dlg_settings_{{ oUIBlock.GetId() }}">&nbsp;{{ 'UI:UseDefaultSettings'|dict_s }}</label>
</p>
<fieldset>
<legend class="transparent">
<input id="dtbl_dlg_specific_{{ oUIBlock.GetId() }}" type="radio" class="specific_settings" name="settings" {% if oUIBlock.GetOptions()["bUseCustomSettings"] %} checked {% endif %} value="specific">
<label for="dtbl_dlg_specific_{{ oUIBlock.GetId() }}">&nbsp;&nbsp;{{ 'UI:UseSpecificSettings'|dict_s }}</label>
</legend>
{{'UI:ColumnsAndSortOrder'|dict_s}}<br/>
<ul class="sortable_field_list" id="sfl_{{ oUIBlock.GetId() }}">
</ul>
<p> {{ 'UI:Display_X_ItemsPerPage_prefix'|dict_s }}<input type="text" size="4" name="page_size" value="{{ oUIBlock.GetOptions()["iPageSize"]}}">{{ 'UI:Display_X_ItemsPerPage_suffix'|dict_s }}</p>
</fieldset>
<fieldset>
<legend class="transparent">
<input id="dtbl_dlg_save_{{ oUIBlock.GetId() }}" type="checkbox" {% if oUIBlock.GetOptions()["sTableId"] != null %}checked{% endif %} name="save_settings">
<label for="dtbl_dlg_save_{{ oUIBlock.GetId() }}">&nbsp;&nbsp;{{ 'UI:UseSavetheSettings'|dict_s }}</label>
</legend>
<p>
<input id="dtbl_dlg_this_list_{{ oUIBlock.GetId() }}" type="radio" name="scope" {% if oUIBlock.GetOptions()["sTableId"] != null %} checked {% else %} disabled="disabled" stay-disabled="true"{% endif %} value="this_list">
<label for="dtbl_dlg_this_list_{{ oUIBlock.GetId() }}">&nbsp;&nbsp;{{ 'UI:OnlyForThisList'|dict_s }}</label>&nbsp;&nbsp;&nbsp;&nbsp;
<input id="dtbl_dlg_all_{{ oUIBlock.GetId() }}" type="radio" name="scope" {% if oUIBlock.GetOptions()["sTableId"] == null %} checked {% endif %} value="defaults">
<label for="dtbl_dlg_all_{{ oUIBlock.GetId() }}">&nbsp;&nbsp;{{ 'UI:ForAllLists'|dict_s }}</label>
</p>
</fieldset>
<table style="width:100%">
<tr>
<td style="text-align:center;">
<button type="button" onclick="$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings('onDlgCancel'); $('#datatable_dlg_{{ oUIBlock.GetId() }}').dialog('close')">&nbsp;{{ 'UI:Button:Cancel'|dict_s }}</button>
</td>
<td style="text-align:center;">
<button type="submit" onclick="$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings('onDlgOk'); ">&nbsp;{{ 'UI:Button:Ok'|dict_s }}</button>
</td>
</tr>
</table>
</form>
</div>

View File

@@ -288,20 +288,16 @@ $('#datatable_dlg_{{ oUIBlock.GetId() }}').dialog(
$aOptions = {
sListId: '{{ oUIBlock.GetId() }}',
oColumns: {{ oUIBlock.GetResultColumnsAsJson()|raw }},
sSelectMode: "{{ oUIBlock.GetOption("select_mode") }}",
sSelectMode: "{{ oUIBlock.GetOption("select_mode") }}",
sViewLink: '{{ oUIBlock.GetOption("bViewLink") }}',
iPageSize: '{{ oUIBlock.GetOption("iPageSize") }}',
oClassAliases: JSON.parse('{{ oUIBlock.GetOption("oClassAliases") |raw}}'),
oClassAliases: JSON.parse('{{ oUIBlock.GetOption("oClassAliases") |raw }}'),
sTableId: '{{ oUIBlock.GetOption("sTableId") }}',
//oExtraParams
//oExtraParams
sRenderUrl: "{{ oUIBlock.GetAjaxUrl() }}",
oData: {{ oUIBlock.GetAjaxData() |raw }},//ttt
oDefaultSettings: {{ oUIBlock.GetOption("oDefaultSettings")|raw }},
oLabels: {moveup: "{{ 'UI:Button:MoveUp'|dict_s }}", movedown: "{{ 'UI:Button:MoveDown'|dict_s }}"},
};
//if (!typeof $('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings() !== "undefined")
if ($('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings()) {
$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings("destroy");
}
$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings($aOptions);

View File

@@ -1,6 +1,11 @@
<form method="post" enctype="multipart/form-data" id="{{ oUIBlock.GetId() }}"
{% if oUIBlock.GetOnSubmitJsCode() %}onSubmit="{{ oUIBlock.GetOnSubmitJsCode() }}"{% endif %}
{% if oUIBlock.GetAction() %}action="{{ oUIBlock.GetAction() }}"{% endif %}>
{% if oUIBlock.GetOnSubmitJsCode() %}
onSubmit="{{ oUIBlock.GetOnSubmitJsCode() }}"
{% endif %}
{% if oUIBlock.GetAction() %}
action="{{ oUIBlock.GetAction() }}"
{% endif %}
>
{% apply spaceless %}
{% block iboContentBlockContainer %}
{% for oSubBlock in oUIBlock.GetSubBlocks() %}

View File

@@ -7,6 +7,16 @@
{{ render_block(oLayout, {aPage: aPage}) }}
{% endif %}
{% if aDeferredBlocks is not empty %}
{# TODO 3.0.0 #}
{# <script type="text/javascript"> #}
{# $('body').append('{% for oBlock in aDeferredBlocks %}{{ render_block(oBlock, {aPage: aPage})|escape_for_js_string|raw }}{% endfor %}'); #}
{# </script> #}
{% for oBlock in aDeferredBlocks %}
{{ render_block(oBlock, {aPage: aPage})|raw }}
{% 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) #}
@@ -16,6 +26,12 @@
{% 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">
@@ -24,11 +40,6 @@
{% endfor %}
{% endblock %}
{% if sDeferredContent %}
<script type="text/javascript">
$('body').append('{{ sDeferredContent|raw }}');
</script>
{% endif %}
{% block iboPageJsInlineOnDomReady %}
{% for sJsInline in aPage.aJsInlineOnDomReady %}

View File

@@ -48,6 +48,13 @@
<div style="display:none" id="ajax_content"></div>
</div>
{% endblock %}
{% block iboDeferredBlocks %}
{% for oBlock in aDeferredBlocks %}
{{ render_block(oBlock, {aPage: aPage}) }}
{% endfor %}
{% endblock %}
{% block iboPageJsFiles %}
{% for sJsFile in aPage.aJsFiles %}
<script type="text/javascript" src="{{ sJsFile|add_itop_version }}"></script>