mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 23:44:11 +01:00
Compare commits
106 Commits
develop
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24110bd880 | ||
|
|
c11fd6e348 | ||
|
|
dc89c4705e | ||
|
|
863dd67302 | ||
|
|
f9272ac2ed | ||
|
|
a1d5acadf8 | ||
|
|
0b17e1f9b3 | ||
|
|
b2041e3b63 | ||
|
|
73e6a0af8a | ||
|
|
2ff183a78a | ||
|
|
3aba8c5aa3 | ||
|
|
d7451fe2ea | ||
|
|
bb1c4f865a | ||
|
|
3fa658edb5 | ||
|
|
814f91bd48 | ||
|
|
3092e9d5d4 | ||
|
|
f3fddfe54e | ||
|
|
bfa7a209d6 | ||
|
|
390b5c0bc3 | ||
|
|
e9a5d1a65e | ||
|
|
07675778d1 | ||
|
|
ceda1d40f8 | ||
|
|
1bb7ddd488 | ||
|
|
4abf948a03 | ||
|
|
f4a0a2f2ff | ||
|
|
eb60dd3bb1 | ||
|
|
e9b8e088fe | ||
|
|
5fe9b1fe41 | ||
|
|
1d100f1727 | ||
|
|
7e91e01ad8 | ||
|
|
890d1ece8d | ||
|
|
2d4c5b8b2a | ||
|
|
ce263ae97b | ||
|
|
fe4b2a0d93 | ||
|
|
140c0a960f | ||
|
|
e909c41445 | ||
|
|
945d2f32c5 | ||
|
|
28b74bb489 | ||
|
|
8581ec0bf6 | ||
|
|
7be77949ac | ||
|
|
c85e0373b1 | ||
|
|
096dfd7dec | ||
|
|
23a4dd5bb6 | ||
|
|
ab5394ef83 | ||
|
|
bb752debaa | ||
|
|
7ffddbb633 | ||
|
|
5346ccd13d | ||
|
|
2fdb09173a | ||
|
|
6e1c2e9f51 | ||
|
|
ccf0ec626b | ||
|
|
cb61ae8d61 | ||
|
|
6aa8ac7ff1 | ||
|
|
0b6cbc1fef | ||
|
|
805e306712 | ||
|
|
8e16d24d85 | ||
|
|
344854a0d0 | ||
|
|
3c80c93b4f | ||
|
|
efc6e6730b | ||
|
|
34fa1dba4b | ||
|
|
542d33a040 | ||
|
|
3769bc1024 | ||
|
|
e5338f28eb | ||
|
|
5cfe7fa6eb | ||
|
|
27c16a782c | ||
|
|
7c21178e1d | ||
|
|
3a6e148c11 | ||
|
|
4d9e18890a | ||
|
|
50ffaa2b55 | ||
|
|
15c8f5903b | ||
|
|
0bc6f5d56a | ||
|
|
d248524cc8 | ||
|
|
410dc152d7 | ||
|
|
423413e3a0 | ||
|
|
8896c576d7 | ||
|
|
975046da17 | ||
|
|
e4a281c3ff | ||
|
|
90729f84b6 | ||
|
|
c075a5c145 | ||
|
|
cd6d130bcb | ||
|
|
7ca2c56dad | ||
|
|
dd0ac58643 | ||
|
|
df943ec8b5 | ||
|
|
076a6d0495 | ||
|
|
02e59c906b | ||
|
|
49f91961e7 | ||
|
|
441519d71d | ||
|
|
5c75d0ce7c | ||
|
|
2efe80265d | ||
|
|
b58a64e143 | ||
|
|
ff11aec7fe | ||
|
|
f79bb9d51c | ||
|
|
3c365fc103 | ||
|
|
7b193dd737 | ||
|
|
1538596db8 | ||
|
|
9d4fc345bc | ||
|
|
08c9309572 | ||
|
|
d072aa05f1 | ||
|
|
85c1f091e2 | ||
|
|
be6a8abdf4 | ||
|
|
60bcf0c85f | ||
|
|
4a8804b8ac | ||
|
|
b014b9f638 | ||
|
|
1c633c6173 | ||
|
|
cdc95aca7b | ||
|
|
b4460999ef | ||
|
|
a713e1b56e |
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
use Combodo\iTop\Application\Dashlet\Service\DashletService;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardColumn;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout as DashboardLayoutUIBlock;
|
||||
@@ -30,7 +31,7 @@ use Combodo\iTop\Application\WebPage\WebPage;
|
||||
*/
|
||||
abstract class DashboardLayout
|
||||
{
|
||||
abstract public function Render($oPage, $aDashlets, $bEditMode = false);
|
||||
abstract public function Render($oPage, $aDashlets, $bEditMode = false, array $aExtraParams = []);
|
||||
|
||||
/**
|
||||
* @param int $iCellIdx
|
||||
@@ -43,8 +44,8 @@ abstract class DashboardLayout
|
||||
public static function GetInfo()
|
||||
{
|
||||
return [
|
||||
'label' => '',
|
||||
'icon' => '',
|
||||
'label' => '',
|
||||
'icon' => '',
|
||||
'description' => '',
|
||||
];
|
||||
}
|
||||
@@ -74,6 +75,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
|
||||
}
|
||||
$idx++;
|
||||
}
|
||||
|
||||
return $aDashlets;
|
||||
}
|
||||
|
||||
@@ -94,6 +96,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
|
||||
}
|
||||
$idx++;
|
||||
}
|
||||
|
||||
return $aCells;
|
||||
|
||||
}
|
||||
@@ -106,26 +109,39 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
|
||||
*/
|
||||
public function Render($oPage, $aCells, $bEditMode = false, $aExtraParams = [])
|
||||
{
|
||||
/** @var DashletService $oDashletService */
|
||||
$oDashletService = MetaModel::GetService('DashletService');
|
||||
// Trim the list of cells to remove the invisible/empty ones at the end of the array
|
||||
$aCells = $this->TrimCellsArray($aCells);
|
||||
|
||||
$oDashboardLayout = new DashboardLayoutUIBlock();
|
||||
//$oPage->AddUiBlock($oDashboardLayout);
|
||||
$oDashboardLayout = new DashboardLayoutUIBlock($aExtraParams['dashboard_div_id']);
|
||||
|
||||
$iCellIdx = 0;
|
||||
$iNbRows = ceil(count($aCells) / $this->iNbCols);
|
||||
|
||||
// GRID LAYOUT: Global positioning
|
||||
$iGridCurrentX = 0;
|
||||
$iGridCurrentY = 0;
|
||||
$iGridColWidth = (int)(12 / $this->iNbCols);
|
||||
|
||||
//Js given by each dashlet to reload
|
||||
$sJSReload = "";
|
||||
|
||||
$oDashboardGrid = new \Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardGrid();
|
||||
$oDashboardLayout->SetGrid($oDashboardGrid);
|
||||
for ($iRows = 0; $iRows < $iNbRows; $iRows++) {
|
||||
$oDashboardRow = new DashboardRow();
|
||||
$oDashboardLayout->AddDashboardRow($oDashboardRow);
|
||||
|
||||
// GRID LAYOUT: Store the maximum column Y in this row
|
||||
$iGridMaxColY = -1;
|
||||
|
||||
for ($iCols = 0; $iCols < $this->iNbCols; $iCols++) {
|
||||
$oDashboardColumn = new DashboardColumn($bEditMode);
|
||||
$oDashboardColumn->SetCellIndex($iCellIdx);
|
||||
$oDashboardRow->AddDashboardColumn($oDashboardColumn);
|
||||
|
||||
// GRID LAYOUT: Column positioning
|
||||
$iGridCurrentColX = 0;
|
||||
$iGridCurrentColY = 0;
|
||||
$iGridMaxHeightDashlet = -1;
|
||||
|
||||
if (array_key_exists($iCellIdx, $aCells)) {
|
||||
$aDashlets = $aCells[$iCellIdx];
|
||||
@@ -133,7 +149,41 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
|
||||
/** @var \Dashlet $oDashlet */
|
||||
foreach ($aDashlets as $oDashlet) {
|
||||
if ($oDashlet::IsVisible()) {
|
||||
$oDashboardColumn->AddUIBlock($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams));
|
||||
$sDashletId = $oDashlet->GetID();
|
||||
$sDashletClass = $oDashlet->GetDashletType();
|
||||
$aDashletDenormalizedProperties = $oDashlet->GetModelData();
|
||||
$aDashletsInfo = $oDashletService->GetDashletDefinition($sDashletClass);
|
||||
|
||||
// GRID LAYOUT: Set position relative to grid
|
||||
$iPositionX = $iGridCurrentX + $iGridCurrentColX;
|
||||
$iPositionY = $iGridCurrentY + $iGridCurrentColY;
|
||||
$iWidth = array_key_exists('preferred_width', $aDashletsInfo) ? $aDashletsInfo['preferred_width'] : 1;
|
||||
// GRID LAYOUT: Limit dashlet width to fit column width
|
||||
if ($iWidth > $iGridColWidth) {
|
||||
$iWidth = $iGridColWidth;
|
||||
}
|
||||
$iHeight = array_key_exists('preferred_height', $aDashletsInfo) ? $aDashletsInfo['preferred_height'] : 1;
|
||||
// GRID LAYOUT: Store max height of dashlets in this current column
|
||||
if ($iHeight > $iGridMaxHeightDashlet) {
|
||||
$iGridMaxHeightDashlet = $iHeight;
|
||||
}
|
||||
// GRID LAYOUT: Ensure that dashlet fits in the current row of the column
|
||||
if ($iGridCurrentColX + $iWidth > $iGridColWidth) {
|
||||
$iPositionX = $iGridCurrentX;
|
||||
$iPositionY++;
|
||||
}
|
||||
|
||||
$oDashboardGrid->AddDashlet($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams), $sDashletId, $sDashletClass, $aDashletDenormalizedProperties, $iPositionX, $iPositionY, $iWidth, $iHeight);
|
||||
|
||||
// GRID LAYOUT: Update column cursor
|
||||
$iGridCurrentColX += $iWidth;
|
||||
if ($iGridCurrentColX >= $iGridColWidth) {
|
||||
$iGridCurrentColX = 0;
|
||||
$iGridCurrentColY += $iGridMaxHeightDashlet;
|
||||
$iGridMaxHeightDashlet = -1;
|
||||
}
|
||||
|
||||
//$oDashboardColumn->AddUIBlock($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -143,10 +193,25 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
|
||||
$oDashboardColumn->AddUIBlock(new Html(' '));
|
||||
}
|
||||
$iCellIdx++;
|
||||
|
||||
// GRID LAYOUT: Store max y in this current row
|
||||
if ($iGridCurrentColY > $iGridMaxColY) {
|
||||
$iGridMaxColY = $iGridCurrentColY;
|
||||
}
|
||||
|
||||
// GRID LAYOUT: Next column
|
||||
$iGridCurrentX += $iGridColWidth;
|
||||
|
||||
}
|
||||
|
||||
// GRID LAYOUT: Next Row
|
||||
$iGridCurrentY += ($iGridMaxColY + 1);
|
||||
$iGridCurrentX = 0;
|
||||
|
||||
$sJSReload .= $oDashboardRow->GetJSRefreshCallback()." ";
|
||||
}
|
||||
|
||||
// TODO 3.3 We can probably do better with the new dashboard
|
||||
$oPage->add_script("function updateDashboard".$aExtraParams['dashboard_div_id']."(){".$sJSReload."}");
|
||||
|
||||
if ($bEditMode) { // Add one row for extensibility
|
||||
@@ -168,8 +233,8 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
|
||||
*/
|
||||
public function GetDashletCoordinates($iCellIdx)
|
||||
{
|
||||
$iColNumber = (int) $iCellIdx % $this->iNbCols;
|
||||
$iRowNumber = (int) floor($iCellIdx / $this->iNbCols);
|
||||
$iColNumber = (int)$iCellIdx % $this->iNbCols;
|
||||
$iRowNumber = (int)floor($iCellIdx / $this->iNbCols);
|
||||
|
||||
return [$iColNumber, $iRowNumber];
|
||||
}
|
||||
@@ -182,11 +247,12 @@ class DashboardLayoutOneCol extends DashboardLayoutMultiCol
|
||||
parent::__construct();
|
||||
$this->iNbCols = 1;
|
||||
}
|
||||
|
||||
public static function GetInfo()
|
||||
{
|
||||
return [
|
||||
'label' => 'One Column',
|
||||
'icon' => 'images/layout_1col.png',
|
||||
'label' => 'One Column',
|
||||
'icon' => 'images/layout_1col.png',
|
||||
'description' => '',
|
||||
];
|
||||
}
|
||||
@@ -199,11 +265,12 @@ class DashboardLayoutTwoCols extends DashboardLayoutMultiCol
|
||||
parent::__construct();
|
||||
$this->iNbCols = 2;
|
||||
}
|
||||
|
||||
public static function GetInfo()
|
||||
{
|
||||
return [
|
||||
'label' => 'Two Columns',
|
||||
'icon' => 'images/layout_2col.png',
|
||||
'label' => 'Two Columns',
|
||||
'icon' => 'images/layout_2col.png',
|
||||
'description' => '',
|
||||
];
|
||||
}
|
||||
@@ -216,11 +283,12 @@ class DashboardLayoutThreeCols extends DashboardLayoutMultiCol
|
||||
parent::__construct();
|
||||
$this->iNbCols = 3;
|
||||
}
|
||||
|
||||
public static function GetInfo()
|
||||
{
|
||||
return [
|
||||
'label' => 'Two Columns',
|
||||
'icon' => 'images/layout_3col.png',
|
||||
'label' => 'Two Columns',
|
||||
'icon' => 'images/layout_3col.png',
|
||||
'description' => '',
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<class id="AbstractResource" _delta="define">
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
<properties>
|
||||
<comment>/* Resource access control abstraction. Can be herited by abstract resource access control classes. Generally controlled using UR_ACTION_MODIFY access right. */</comment>
|
||||
<comment>/* Resource access control abstraction. Can be inherited by abstract resource access control classes. Generally controlled using UR_ACTION_MODIFY access right. */</comment>
|
||||
<abstract>true</abstract>
|
||||
</properties>
|
||||
<presentation/>
|
||||
@@ -851,9 +851,302 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
|
||||
</methods>
|
||||
</class>
|
||||
</classes>
|
||||
<dashlets>
|
||||
<dashlet id="DashletGroupByTable" _delta="define">
|
||||
<label>UI:DashletGroupByTable:Label</label>
|
||||
<icon>images/dashlets/icons8-transaction-list-48.png</icon>
|
||||
<description>UI:DashletGroupByTable:Description</description>
|
||||
<min_width>3</min_width>
|
||||
<min_height>2</min_height>
|
||||
<preferred_width>6</preferred_width>
|
||||
<preferred_height>3</preferred_height>
|
||||
<can_create_by_oql>true</can_create_by_oql>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletGroupByBars" _delta="define">
|
||||
<label>UI:DashletGroupByBars:Label</label>
|
||||
<icon>images/dashlets/icons8-bar-chart-48.png</icon>
|
||||
<description>UI:DashletGroupByBars:Description</description>
|
||||
<min_width>2</min_width>
|
||||
<min_height>2</min_height>
|
||||
<preferred_width>6</preferred_width>
|
||||
<preferred_height>3</preferred_height>
|
||||
<can_create_by_oql>true</can_create_by_oql>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletGroupByPie" _delta="define">
|
||||
<label>UI:DashletGroupByPie:Label</label>
|
||||
<icon>images/dashlets/icons8-pie-chart-48.png</icon>
|
||||
<description>UI:DashletGroupByPie:Description</description>
|
||||
<min_width>2</min_width>
|
||||
<min_height>2</min_height>
|
||||
<preferred_width>6</preferred_width>
|
||||
<preferred_height>3</preferred_height>
|
||||
<can_create_by_oql>true</can_create_by_oql>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletBadge" _delta="define">
|
||||
<label>UI:DashletBadge:Label</label>
|
||||
<icon>images/dashlets/icons8-badge-48.png</icon>
|
||||
<description>UI:DashletBadge:Description</description>
|
||||
<min_width>2</min_width>
|
||||
<min_height>1</min_height>
|
||||
<preferred_width>3</preferred_width>
|
||||
<preferred_height>1</preferred_height>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletHeaderDynamic" _delta="define">
|
||||
<label>UI:DashletHeaderDynamic:Label</label>
|
||||
<icon>images/dashlets/icons8-header-altered-48.png</icon>
|
||||
<description>UI:DashletHeaderDynamic:Description</description>
|
||||
<min_width>2</min_width>
|
||||
<min_height>1</min_height>
|
||||
<preferred_width>12</preferred_width>
|
||||
<preferred_height>2</preferred_height>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletHeaderStatic" _delta="define">
|
||||
<label>UI:DashletHeaderStatic:Label</label>
|
||||
<icon>images/dashlets/icons8-header-48.png</icon>
|
||||
<description>UI:DashletHeaderStatic:Description</description>
|
||||
<min_width>4</min_width>
|
||||
<min_height>1</min_height>
|
||||
<preferred_width>12</preferred_width>
|
||||
<preferred_height>1</preferred_height>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletObjectList" _delta="define">
|
||||
<label>UI:DashletObjectList:Label</label>
|
||||
<icon>images/dashlets/icons8-list-48.png</icon>
|
||||
<description>UI:DashletObjectList:Description</description>
|
||||
<min_width>2</min_width>
|
||||
<min_height>1</min_height>
|
||||
<preferred_width>12</preferred_width>
|
||||
<preferred_height>3</preferred_height>
|
||||
<can_create_by_oql>true</can_create_by_oql>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletPlainText" _delta="define">
|
||||
<label>UI:DashletPlainText:Label</label>
|
||||
<icon>images/dashlets/icons8-text-box-48.png</icon>
|
||||
<description>UI:DashletPlainText:Description</description>
|
||||
<min_width>2</min_width>
|
||||
<min_height>1</min_height>
|
||||
<preferred_width>2</preferred_width>
|
||||
<preferred_height>1</preferred_height>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
<dashlet id="DashletUnknown" _delta="define">
|
||||
<label>UI:DashletUnknown:Label</label>
|
||||
<icon>images/dashlet-unknown.png</icon>
|
||||
<description>UI:DashletUnknown:Description</description>
|
||||
<can_be_created>false</can_be_created>
|
||||
<configuration/>
|
||||
</dashlet>
|
||||
</dashlets>
|
||||
<property_types _delta="define">
|
||||
<property_type id="Dashboard" xsi:type="Combodo-AbstractPropertyType"/>
|
||||
<property_type id="DashboardGrid" xsi:type="Combodo-PropertyType">
|
||||
<extends>Dashboard</extends>
|
||||
<definition xsi:type="Combodo-ValueType-PropertyTree">
|
||||
<label>Dashboard</label>
|
||||
<nodes>
|
||||
<node id="title" xsi:type="Combodo-ValueType-String">
|
||||
<label>UI:DashboardEdit:DashboardTitle</label>
|
||||
</node>
|
||||
<node id="refresh" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
|
||||
<label>UI:DashboardEdit:AutoReload</label>
|
||||
<values>
|
||||
<value id="0">
|
||||
<label>No auto-refresh</label>
|
||||
</value>
|
||||
<value id="30">
|
||||
<label>Every 30 seconds</label>
|
||||
</value>
|
||||
<value id="60">
|
||||
<label>Every 1 minute</label>
|
||||
</value>
|
||||
<value id="300">
|
||||
<label>Every 5 minutes</label>
|
||||
</value>
|
||||
<value id="600">
|
||||
<label>Every 10 minutes</label>
|
||||
</value>
|
||||
<value id="1800">
|
||||
<label>Every 30 minutes</label>
|
||||
</value>
|
||||
<value id="3600">
|
||||
<label>Every 1 hour</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
<node id="pos_dashlets" xsi:type="Combodo-ValueType-Collection">
|
||||
<label>Dashlet List</label>
|
||||
<xml-format xsi:type="Combodo-XMLFormat-CollectionWithId">
|
||||
<tag-name>pos_dashlet</tag-name>
|
||||
</xml-format>
|
||||
<prototype>
|
||||
<node id="position_x" xsi:type="Combodo-ValueType-Integer">
|
||||
<label>X</label>
|
||||
</node>
|
||||
<node id="position_y" xsi:type="Combodo-ValueType-Integer">
|
||||
<label>Y</label>
|
||||
</node>
|
||||
<node id="width" xsi:type="Combodo-ValueType-Integer">
|
||||
<label>W</label>
|
||||
</node>
|
||||
<node id="height" xsi:type="Combodo-ValueType-Integer">
|
||||
<label>H</label>
|
||||
</node>
|
||||
<node id="dashlet" xsi:type="Combodo-ValueType-Polymorphic">
|
||||
<label>Dashlet</label>
|
||||
<allowed-types>
|
||||
<allowed-type>Dashlet</allowed-type>
|
||||
</allowed-types>
|
||||
</node>
|
||||
</prototype>
|
||||
</node>
|
||||
</nodes>
|
||||
</definition>
|
||||
</property_type>
|
||||
<property_type id="Dashlet" xsi:type="Combodo-AbstractPropertyType"/>
|
||||
<property_type id="DashletGroupBy" xsi:type="Combodo-PropertyType">
|
||||
<property_type id="DashletGroupByTable" xsi:type="Combodo-PropertyType">
|
||||
<extends>Dashlet</extends>
|
||||
<definition xsi:type="Combodo-ValueType-PropertyTree">
|
||||
<label>UI:DashletGroupBy:Title</label>
|
||||
<nodes>
|
||||
<node id="title" xsi:type="Combodo-ValueType-Label">
|
||||
<label>UI:DashletGroupBy:Prop-Title</label>
|
||||
</node>
|
||||
<node id="query" xsi:type="Combodo-ValueType-OQL">
|
||||
<label>UI:DashletGroupBy:Prop-Query</label>
|
||||
</node>
|
||||
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
|
||||
<label>UI:DashletGroupBy:Prop-GroupBy</label>
|
||||
<class>{{query.selected_class}}</class>
|
||||
</node>
|
||||
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
|
||||
<label>UI:DashletGroupBy:Prop-Style</label>
|
||||
<values>
|
||||
<value id="bars">
|
||||
<label>UI:DashletGroupByBars:Label</label>
|
||||
</value>
|
||||
<value id="pie">
|
||||
<label>UI:DashletGroupByPie:Label</label>
|
||||
</value>
|
||||
<value id="table">
|
||||
<label>UI:DashletGroupByTable:Label</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
|
||||
<label>UI:DashletGroupBy:Prop-Function</label>
|
||||
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
|
||||
</node>
|
||||
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
|
||||
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
|
||||
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
|
||||
<class>{{query.selected_class}}</class>
|
||||
<category>numeric</category>
|
||||
</node>
|
||||
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
|
||||
<label>UI:DashletGroupBy:Prop-OrderField</label>
|
||||
<values>
|
||||
<value id="attribute">
|
||||
<label>{{aggregation_attribute.label}}</label>
|
||||
</value>
|
||||
<value id="function">
|
||||
<label>{{aggregation_function.label}}</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
<node id="limit" xsi:type="Combodo-ValueType-Integer">
|
||||
<label>UI:DashletGroupBy:Prop-Limit</label>
|
||||
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
|
||||
</node>
|
||||
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
|
||||
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
|
||||
<values>
|
||||
<value id="asc">
|
||||
<label>UI:DashletGroupBy:Order:asc</label>
|
||||
</value>
|
||||
<value id="desc">
|
||||
<label>UI:DashletGroupBy:Order:desc</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
</nodes>
|
||||
</definition>
|
||||
</property_type>
|
||||
<property_type id="DashletGroupByBars" xsi:type="Combodo-PropertyType">
|
||||
<extends>Dashlet</extends>
|
||||
<definition xsi:type="Combodo-ValueType-PropertyTree">
|
||||
<label>UI:DashletGroupBy:Title</label>
|
||||
<nodes>
|
||||
<node id="title" xsi:type="Combodo-ValueType-Label">
|
||||
<label>UI:DashletGroupBy:Prop-Title</label>
|
||||
</node>
|
||||
<node id="query" xsi:type="Combodo-ValueType-OQL">
|
||||
<label>UI:DashletGroupBy:Prop-Query</label>
|
||||
</node>
|
||||
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
|
||||
<label>UI:DashletGroupBy:Prop-GroupBy</label>
|
||||
<class>{{query.selected_class}}</class>
|
||||
</node>
|
||||
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
|
||||
<label>UI:DashletGroupBy:Prop-Style</label>
|
||||
<values>
|
||||
<value id="bars">
|
||||
<label>UI:DashletGroupByBars:Label</label>
|
||||
</value>
|
||||
<value id="pie">
|
||||
<label>UI:DashletGroupByPie:Label</label>
|
||||
</value>
|
||||
<value id="table">
|
||||
<label>UI:DashletGroupByTable:Label</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
|
||||
<label>UI:DashletGroupBy:Prop-Function</label>
|
||||
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
|
||||
</node>
|
||||
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
|
||||
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
|
||||
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
|
||||
<class>{{query.selected_class}}</class>
|
||||
<category>numeric</category>
|
||||
</node>
|
||||
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
|
||||
<label>UI:DashletGroupBy:Prop-OrderField</label>
|
||||
<values>
|
||||
<value id="attribute">
|
||||
<label>{{aggregation_attribute.label}}</label>
|
||||
</value>
|
||||
<value id="function">
|
||||
<label>{{aggregation_function.label}}</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
<node id="limit" xsi:type="Combodo-ValueType-Integer">
|
||||
<label>UI:DashletGroupBy:Prop-Limit</label>
|
||||
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
|
||||
</node>
|
||||
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
|
||||
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
|
||||
<values>
|
||||
<value id="asc">
|
||||
<label>UI:DashletGroupBy:Order:asc</label>
|
||||
</value>
|
||||
<value id="desc">
|
||||
<label>UI:DashletGroupBy:Order:desc</label>
|
||||
</value>
|
||||
</values>
|
||||
</node>
|
||||
</nodes>
|
||||
</definition>
|
||||
</property_type>
|
||||
<property_type id="DashletGroupByPie" xsi:type="Combodo-PropertyType">
|
||||
<extends>Dashlet</extends>
|
||||
<definition xsi:type="Combodo-ValueType-PropertyTree">
|
||||
<label>UI:DashletGroupBy:Title</label>
|
||||
|
||||
@@ -984,7 +984,7 @@ JS
|
||||
$aFunctions = [$sFctVar => $oFctExpr];
|
||||
}
|
||||
|
||||
if (!empty($sAggregationAttr)) {
|
||||
if (!empty(trim($sAggregationAttr))) {
|
||||
$sClass = $this->m_oFilter->GetClass();
|
||||
$sAggregationAttr = MetaModel::GetLabel($sClass, $sAggregationAttr);
|
||||
}
|
||||
@@ -1608,10 +1608,9 @@ JS
|
||||
$iTotalCount = 0;
|
||||
$aURLs = [];
|
||||
|
||||
foreach ($aRes as $iRow => $aRow) {
|
||||
foreach ($aRes as $aRow) {
|
||||
$sValue = $aRow['grouped_by_1'];
|
||||
$sHtmlValue = $oGroupByExp->MakeValueLabel($this->m_oFilter, $sValue, $sValue);
|
||||
$iTotalCount += $aRow['_itop_count_'];
|
||||
$aValues[] = [
|
||||
'label' => html_entity_decode(strip_tags($sHtmlValue), ENT_QUOTES, 'UTF-8'),
|
||||
'label_html' => $sHtmlValue,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Helper\WebResourcesHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
@@ -1218,7 +1219,6 @@ class NewObjectMenuNode extends MenuNode
|
||||
}
|
||||
}
|
||||
|
||||
require_once(APPROOT.'application/dashboard.class.inc.php');
|
||||
/**
|
||||
* This class defines a menu item which content is based on XML dashboard.
|
||||
*/
|
||||
@@ -1279,13 +1279,14 @@ class DashboardMenuNode extends MenuNode
|
||||
if ($oDashboard != null) {
|
||||
WebResourcesHelper::EnableC3JSToWebPage($oPage);
|
||||
|
||||
// TODO 3.3 this works for dashboard menu, what about other places ?
|
||||
$oPageLayout = PageContentFactory::MakeForDashboard();
|
||||
$oPage->SetContentLayout($oPageLayout, $oPage);
|
||||
$sDivId = utils::Sanitize($this->sMenuId, '', 'element_identifier');
|
||||
$oPage->add('<div id="'.$sDivId.'" class="ibo-dashboard" data-role="ibo-dashboard">');
|
||||
$aExtraParams['dashboard_div_id'] = $sDivId;
|
||||
$aExtraParams['from_dashboard_page'] = true;
|
||||
$oDashboard->SetReloadURL($this->GetHyperlink($aExtraParams));
|
||||
$oDashboard->Render($oPage, false, $aExtraParams);
|
||||
$oPage->add('</div>');
|
||||
|
||||
$bEdit = utils::ReadParam('edit', false);
|
||||
if ($bEdit) {
|
||||
@@ -1311,37 +1312,6 @@ class DashboardMenuNode extends MenuNode
|
||||
$oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @throws CoreException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function RenderEditor(WebPage $oPage)
|
||||
{
|
||||
$oDashboard = $this->GetDashboard();
|
||||
if ($oDashboard != null) {
|
||||
$oDashboard->RenderEditor($oPage);
|
||||
} else {
|
||||
$oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $oDashlet
|
||||
* @throws Exception
|
||||
*/
|
||||
public function AddDashlet($oDashlet)
|
||||
{
|
||||
$oDashboard = $this->GetDashboard();
|
||||
if ($oDashboard != null) {
|
||||
$oDashboard->AddDashlet($oDashlet);
|
||||
$oDashboard->Save();
|
||||
} else {
|
||||
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1520,7 +1520,7 @@ class utils
|
||||
$sUploadDashboardTransactId = utils::GetNewTransactionId();
|
||||
$aResult = [
|
||||
new SeparatorPopupMenuItem(),
|
||||
new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sDashboardId.'&file='.$sDashboardFileURL),
|
||||
new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?route=dashboard.export&id='.$sDashboardId.'&file='.$sDashboardFileURL),
|
||||
new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sDashboardId', file: '$sDashboardFileJS', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn', transaction: '$sUploadDashboardTransactId' })"),
|
||||
];
|
||||
if ($oDashboard->GetReloadURL()) {
|
||||
@@ -2988,6 +2988,12 @@ TXT
|
||||
return $sAcronym;
|
||||
}
|
||||
|
||||
public static function IsTrue(mixed $value): bool
|
||||
{
|
||||
$bVal = (is_string($value) ? filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : (bool) $value);
|
||||
return ($bVal === null ? false : $bVal);
|
||||
}
|
||||
|
||||
//----------------------------------------------
|
||||
// Text manipulation
|
||||
//----------------------------------------------
|
||||
|
||||
@@ -42,6 +42,20 @@ define('ITOP_DEFAULT_ENV', 'production');
|
||||
define('MAINTENANCE_MODE_FILE', APPROOT.'data/.maintenance');
|
||||
define('READONLY_MODE_FILE', APPROOT.'data/.readonly');
|
||||
|
||||
// TODO 3.3 To deprecate
|
||||
/**
|
||||
* Exclude the parent class from the list
|
||||
*
|
||||
* @package iTopORM
|
||||
*/
|
||||
define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1);
|
||||
/**
|
||||
* Include the parent class in the list
|
||||
*
|
||||
* @package iTopORM
|
||||
*/
|
||||
define('ENUM_CHILD_CLASSES_ALL', 2);
|
||||
|
||||
$fItopStarted = microtime(true);
|
||||
$iItopInitialMemory = memory_get_usage(true);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
|
||||
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Service\Events\EventService;
|
||||
use Combodo\iTop\Service\ServiceLocator\ServiceLocator;
|
||||
|
||||
require_once APPROOT.'core/modulehandler.class.inc.php';
|
||||
require_once APPROOT.'core/querymodifier.class.inc.php';
|
||||
@@ -142,6 +143,8 @@ abstract class MetaModel
|
||||
*/
|
||||
protected static array $m_aReentranceProtection = [];
|
||||
|
||||
private static ServiceLocator $oServiceLocator;
|
||||
|
||||
/**
|
||||
* MetaModel constructor.
|
||||
*/
|
||||
@@ -7026,6 +7029,46 @@ abstract class MetaModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)Init the global service locator with a configuration file
|
||||
*
|
||||
* @param string|null $sRelativeConfigFileName default to the runtime config file when null
|
||||
*
|
||||
* @return void
|
||||
* @api
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public static function InitServiceLocator(string $sRelativeConfigFileName = null): void
|
||||
{
|
||||
if (!isset(self::$oServiceLocator)) {
|
||||
self::$oServiceLocator = new ServiceLocator();
|
||||
}
|
||||
|
||||
// Read the runtime service locator configuration
|
||||
self::$oServiceLocator->Init($sRelativeConfigFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured service using a service locator
|
||||
*
|
||||
* Example: $oModelReflection = \MetaModel::GetService('ModelReflexion');
|
||||
*
|
||||
* @param string $sServiceName Name of the service to get
|
||||
*
|
||||
* @return mixed The service object instance corresponding to the service name
|
||||
* @throws \Combodo\iTop\Service\ServiceLocator\ServiceLocatorException
|
||||
* @api
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public static function GetService(string $sServiceName): mixed
|
||||
{
|
||||
if (!isset(self::$oServiceLocator)) {
|
||||
// Read the runtime service locator configuration
|
||||
self::InitServiceLocator();
|
||||
}
|
||||
return self::$oServiceLocator->get($sServiceName);
|
||||
}
|
||||
}
|
||||
|
||||
// Standard attribute lists
|
||||
|
||||
@@ -24,19 +24,6 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exclude the parent class from the list
|
||||
*
|
||||
* @package iTopORM
|
||||
*/
|
||||
define('ENUM_CHILD_CLASSES_EXCLUDETOP', 1);
|
||||
/**
|
||||
* Include the parent class in the list
|
||||
*
|
||||
* @package iTopORM
|
||||
*/
|
||||
define('ENUM_CHILD_CLASSES_ALL', 2);
|
||||
|
||||
abstract class ModelReflection
|
||||
{
|
||||
abstract public function GetClassIcon($sClass, $bImgTag = true);
|
||||
@@ -89,9 +76,17 @@ abstract class ModelReflection
|
||||
* @param string $defaultValue
|
||||
*
|
||||
* @return \RunTimeIconSelectionField
|
||||
* @deprecated since 3.3.0 replaced by GetAvailableIcons
|
||||
*/
|
||||
abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '');
|
||||
|
||||
/**
|
||||
* Find available icons for the current context
|
||||
*
|
||||
* @return array of ['value', 'label', 'icon'] where 'value' is the relative path on disk, 'label' the name to display and 'icon' is the URL to get the image
|
||||
*/
|
||||
abstract public function GetAvailableIcons(): array;
|
||||
|
||||
abstract public function GetRootClass($sClass);
|
||||
abstract public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP);
|
||||
}
|
||||
@@ -109,6 +104,8 @@ abstract class QueryReflection
|
||||
|
||||
class ModelReflectionRuntime extends ModelReflection
|
||||
{
|
||||
private static array $aAllIcons = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
@@ -255,6 +252,52 @@ class ModelReflectionRuntime extends ModelReflection
|
||||
return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue);
|
||||
}
|
||||
|
||||
public function GetAvailableIcons(): array
|
||||
{
|
||||
$aFolderList = [
|
||||
APPROOT.'env-'.utils::GetCurrentEnvironment() => utils::GetAbsoluteUrlModulesRoot(),
|
||||
APPROOT.'images/icons' => utils::GetAbsoluteUrlAppRoot().'images/icons',
|
||||
];
|
||||
if (count(self::$aAllIcons) == 0) {
|
||||
foreach ($aFolderList as $sFolderPath => $sUrlPrefix) {
|
||||
$aIcons = self::FindIconsOnDisk($sFolderPath);
|
||||
ksort($aIcons);
|
||||
|
||||
foreach ($aIcons as $sFilePath) {
|
||||
self::$aAllIcons[] = ['value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => $sUrlPrefix.$sFilePath];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::$aAllIcons;
|
||||
}
|
||||
|
||||
private static function FindIconsOnDisk(string $sBaseDir, string $sDir = '', array &$aFilesSpecs = []): array
|
||||
{
|
||||
$aResult = [];
|
||||
// Populate automatically the list of icon files
|
||||
if ($hDir = @opendir($sBaseDir.'/'.$sDir)) {
|
||||
while (($sFile = readdir($hDir)) !== false) {
|
||||
$aMatches = [];
|
||||
if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile)) {
|
||||
$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
|
||||
$aResult = array_merge($aResult, self::FindIconsOnDisk($sBaseDir, $sDirSubPath, $aFilesSpecs));
|
||||
}
|
||||
$sSize = filesize($sBaseDir.'/'.$sDir.'/'.$sFile);
|
||||
if (isset($aFilesSpecs[$sFile]) && $aFilesSpecs[$sFile] == $sSize) {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/\.(png|jpg|jpeg|gif|svg)$/i', $sFile, $aMatches)) { // png, jp(e)g, gif and svg are considered valid
|
||||
$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
|
||||
$aFilesSpecs[$sFile] = $sSize;
|
||||
}
|
||||
}
|
||||
closedir($hDir);
|
||||
}
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
public function GetRootClass($sClass)
|
||||
{
|
||||
return MetaModel::GetRootClass($sClass);
|
||||
|
||||
@@ -20,4 +20,213 @@ $ibo-dashlet-within-dashboard--dashlet-header-static--margin-top--is-not-first-d
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 3.3: This css is for dashboard editor demo purpose and needs to be updated
|
||||
|
||||
ibo-dashboard[data-edit-mode="edit"]{
|
||||
ibo-dashlet .ibo-dashlet{
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ibo-dashlet{
|
||||
// do not apply to this dashlets
|
||||
container-type: inline-size;
|
||||
|
||||
&:not([data-dashlet-type="DashletBadge"]):not([data-dashlet-type="DashletHeaderStatic"]){
|
||||
border: 1px solid #ccd4db;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
padding: 16px;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
background-color: $ibo-color-blue-800;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the dashlet body take all the available height to allow scrollbars when needed
|
||||
.ibo-dashlet > .ibo-content-block{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
row-gap: 10px;
|
||||
.ibo-panel--header{
|
||||
align-items: flex-start;
|
||||
}
|
||||
> .ibo-panel--body{
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ibo-dashlet[data-dashlet-type="DashletBadge"] {
|
||||
.ibo-dashlet-badge{
|
||||
max-width: unset;
|
||||
}
|
||||
.ibo-dashlet-badge--body{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@container (width < 175px) {
|
||||
.ibo-dashlet-badge--action-list-label, .ibo-dashlet-badge--action-create-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ibo-dashboard[data-edit-mode="edit"] ibo-dashlet[data-dashlet-type="DashletHeaderStatic"]{
|
||||
border: 1px solid #ccd4db;
|
||||
background-color: $ibo-color-grey-100;
|
||||
border-radius: 3px;
|
||||
> .ibo-content-block{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> .ibo-dashlet-header-static{
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ibo-dashlet[data-dashlet-type="DashletHeaderDynamic"] {
|
||||
> .ibo-content-block > .ibo-content-block{
|
||||
.ibo-panel--body{
|
||||
border: none;
|
||||
padding: 0;
|
||||
&:before{
|
||||
display: none;
|
||||
}
|
||||
.ibo-panel-boy{
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ibo-dashlet[data-dashlet-type="DashletObjectList"] {
|
||||
overflow-y: hidden!important;
|
||||
|
||||
> .ibo-dashlet > .ibo-content-block > .ibo-content-block{
|
||||
display: flex;
|
||||
max-height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.dataTables_wrapper{
|
||||
height: 100%;
|
||||
|
||||
.dataTables_scroll{
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.dataTables_scrollHead{
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 0px;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dataTables_scrollBody{
|
||||
max-height: unset!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-panel--body{
|
||||
margin: 0 -16px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
padding-top: 16px;
|
||||
&:before{
|
||||
display: none;
|
||||
}
|
||||
.ibo-datatable{
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ibo-dashlet[data-dashlet-type="DashletGroupByTable"] {
|
||||
overflow-y: hidden!important;
|
||||
|
||||
> .ibo-dashlet > .ibo-content-block > .ibo-content-block{
|
||||
display: flex;
|
||||
max-height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.dataTables_wrapper{
|
||||
height: 100%;
|
||||
|
||||
.dataTables_scroll{
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.dataTables_scrollHead{
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dataTables_scrollBody{
|
||||
max-height: unset!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-panel--body{
|
||||
margin: 0 -16px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
padding-top: 16px;
|
||||
&:before{
|
||||
display: none;
|
||||
}
|
||||
.ibo-datatable{
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ibo-dashlet[data-dashlet-type="DashletGroupByPie"] {
|
||||
.ibo-panel--body {
|
||||
border: none;
|
||||
padding: 0;
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ibo-dashlet[data-dashlet-type="DashletGroupByBars"] {
|
||||
.ibo-panel--body {
|
||||
border: none;
|
||||
padding: 0;
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,3 +75,27 @@ collection-entry-element {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.ibo-form-compact{
|
||||
|
||||
.ibo-field{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.ibo-field--label{
|
||||
min-width: 100px;
|
||||
max-width: unset;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.ibo-input-type-checkbox{
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.ibo-input-type-date{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ $ibo-dashlet-blocker--height: 100% !default;
|
||||
.ibo-dashlet {
|
||||
position: relative;
|
||||
width: calc(#{$ibo-dashlet--width} - #{$ibo-dashlet--elements-spacing-x});
|
||||
margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
|
||||
//margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
|
||||
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
|
||||
&.dashlet-selected {
|
||||
position: relative;
|
||||
@@ -38,4 +41,21 @@ $ibo-dashlet-blocker--height: 100% !default;
|
||||
width: $ibo-dashlet-blocker--width;
|
||||
height: $ibo-dashlet-blocker--height;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ibo-dashlet--actions {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
display: none;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
background-color: $ibo-color-white-100;
|
||||
@extend %ibo-elevation-100;
|
||||
}
|
||||
|
||||
ibo-dashlet[data-edit-mode="edit"] {
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -54,3 +54,32 @@ $ibo-input-select-icon--menu--icon--margin-right: 10px !default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ts-control > .ibo-input-select-icon--menu--item{
|
||||
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
|
||||
> img{
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ts-dropdown > .ts-dropdown-content > .ibo-input-select-icon--menu--item{
|
||||
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
|
||||
> img{
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
@import "wizard-container/wizard-container";
|
||||
@import "object/all";
|
||||
@import "activity-panel/all";
|
||||
@import "dashlet-panel/all";
|
||||
|
||||
@@ -175,3 +175,78 @@ input:checked + .ibo-dashboard--slider:before {
|
||||
input:checked + .ibo-dashboard--slider:after {
|
||||
content: $ibo-dashboard--slider--before--content;
|
||||
}
|
||||
|
||||
// TODO 3.3 Cleanup variables
|
||||
// TODO 3.3 Move to vendor what's from gridstack
|
||||
|
||||
|
||||
.grid-stack {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ibo-dashboard[data-edit-mode="edit"] .grid-stack{
|
||||
background-size: calc(100% / 12) var(--gs-cell-height);
|
||||
background-color: $ibo-color-white-100;
|
||||
background-image: linear-gradient(to right, $ibo-color-white-200 8px, transparent 8px), linear-gradient(to bottom, $ibo-color-white-200 8px, transparent 8px);
|
||||
--gs-item-margin-top: 8px;
|
||||
--gs-item-margin-bottom: 0;
|
||||
--gs-item-margin-right: 0;
|
||||
--gs-item-margin-left: 8px;
|
||||
}
|
||||
|
||||
ibo-dashboard[data-edit-mode="view"] {
|
||||
.ibo-dashboard--form {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-dashboard--form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 55px;
|
||||
background-color: $ibo-color-blue-200;
|
||||
margin: -16px -36px 24px -36px;
|
||||
padding: 0 36px;
|
||||
}
|
||||
|
||||
.ibo-dashboard--form--inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
@extend %common-font-ral-med-250;
|
||||
|
||||
> [name="dashboard_title"] {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ibo-dashboard[data-edit-mode="edit"] ibo-dashlet:not([data-edit-mode="edit"]):hover .ibo-dashlet--actions {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ibo-dashboard[data-edit-mode="error"] .grid-stack{
|
||||
background-image: url($approot-relative + '/images/alpha-fatal-error.gif');
|
||||
}
|
||||
|
||||
// Our edit mode dashboard already has its own header, so we hide the standard one
|
||||
#ibo-page-header:has(+ ibo-dashboard[data-edit-mode="edit"]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ibo-dashboard[data-edit-mode="edit"] .ibo-dashboard--grid:has(ibo-dashboard-grid-slot > ibo-dashlet[data-edit-mode="edit"]) .ibo-dashboard--grid--backdrop {
|
||||
|
||||
position: absolute;
|
||||
height: calc(100% + 24px);
|
||||
// 36px is $ibo-page-container--elements-padding-x, handle variable resolution
|
||||
width: calc(100% + 36px + 36px);
|
||||
margin: -24px -#{36px} 0 -#{36px};
|
||||
background-color: $ibo-color-grey-400;
|
||||
z-index: 2;
|
||||
opacity: 60%;
|
||||
}
|
||||
2
css/backoffice/layout/dashlet-panel/_all.scss
Normal file
2
css/backoffice/layout/dashlet-panel/_all.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "dashlet-entry";
|
||||
@import "dashlet-panel";
|
||||
51
css/backoffice/layout/dashlet-panel/_dashlet-entry.scss
Normal file
51
css/backoffice/layout/dashlet-panel/_dashlet-entry.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
// TODO 3.3 Cleanup variables
|
||||
|
||||
.ibo-dashlet-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
|
||||
|
||||
border: 1px solid $ibo-color-grey-300;
|
||||
border-radius: 5px;
|
||||
padding: 12px;
|
||||
background-color: $ibo-color-grey-100;
|
||||
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: $ibo-color-grey-200;
|
||||
border-color: $ibo-color-grey-400;
|
||||
}
|
||||
&:active {
|
||||
background-color: $ibo-color-grey-50;
|
||||
border-color: $ibo-color-grey-500;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-dashlet-entry--icon {
|
||||
flex-shrink: 0;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.ibo-dashlet-entry--content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ibo-dashlet-entry--title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $ibo-color-grey-900;
|
||||
}
|
||||
|
||||
.ibo-dashlet-entry--description {
|
||||
font-size: 12px;
|
||||
color: $ibo-color-grey-700;
|
||||
|
||||
}
|
||||
66
css/backoffice/layout/dashlet-panel/_dashlet-panel.scss
Normal file
66
css/backoffice/layout/dashlet-panel/_dashlet-panel.scss
Normal file
@@ -0,0 +1,66 @@
|
||||
// TODO 3.3 Cleanup variables
|
||||
|
||||
.ibo-dashlet-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 326px;
|
||||
background-color: $ibo-color-white-100;
|
||||
}
|
||||
|
||||
.ibo-dashlet-panel--title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
height: 55px;
|
||||
background-color: $ibo-color-white-200;
|
||||
padding: 0 16px;
|
||||
flex-grow: 0;
|
||||
@extend %common-font-ral-med-300;
|
||||
}
|
||||
|
||||
.ibo-dashlet-panel--entries, .ibo-dashlet-panel--form-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
&.ibo-is-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-center-container:has(ibo-dashboard[data-edit-mode="view"]) .ibo-dashlet-panel{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ibo-dashlet-panel--form-container turbo-frame {
|
||||
height: 100%;
|
||||
}
|
||||
.ibo-dashlet-panel--form-container .ibo-form {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
> .form {
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-dashlet-panel--form-container--buttons {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
|
||||
margin: 0 -16px -16px -16px;
|
||||
padding: 0 16px;
|
||||
min-height: 60px;
|
||||
|
||||
background-color: $ibo-color-grey-50;
|
||||
border-top: solid 1px $ibo-color-grey-400;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -466,6 +466,7 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
|
||||
'UI:UserPref:DoNotShowAgain' => 'Ne plus afficher ce message',
|
||||
'UI:InputFile:NoFileSelected' => 'Aucun fichier sélectionné',
|
||||
'UI:InputFile:SelectFile' => 'Sélectionner un fichier',
|
||||
'UI:InputFile:SelectImage' => 'Sélectionner une image',
|
||||
'UI:SearchToggle' => 'Recherche',
|
||||
'UI:ClickToCreateNew' => 'Créer un(e) %1$s',
|
||||
'UI:SearchFor_Class' => 'Rechercher des objets de type %1$s',
|
||||
@@ -863,6 +864,7 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
|
||||
'UI:LinksWidget:Autocomplete+' => 'Tapez les 3 premiers caractères...',
|
||||
'UI:Edit:SearchQuery' => 'Sélectionner une requête prédéfinie',
|
||||
'UI:Edit:TestQuery' => 'Tester la requête',
|
||||
'UI:Edit:QueryBook' => 'Predefined query',
|
||||
'UI:Combo:SelectValue' => '--- choisissez une valeur ---',
|
||||
'UI:Label:SelectedObjects' => 'Objets sélectionnés: ',
|
||||
'UI:Label:AvailableObjects' => 'Objets disponibles: ',
|
||||
|
||||
19
git-split.md
Normal file
19
git-split.md
Normal file
@@ -0,0 +1,19 @@
|
||||
git copy of one file into two files without loosing history
|
||||
|
||||
```
|
||||
git mv foo bar
|
||||
git commit
|
||||
|
||||
SAVED=`git rev-parse HEAD`
|
||||
git reset --hard HEAD^
|
||||
git mv foo copy
|
||||
git commit
|
||||
|
||||
git merge $SAVED # This will generate conflicts
|
||||
git commit -a # Trivially resolved like this
|
||||
|
||||
git mv copy foo
|
||||
git commit
|
||||
```
|
||||
|
||||
DO NOT SQUASH the branch NEVER !
|
||||
@@ -387,7 +387,7 @@ $(function()
|
||||
text: 'Select a dashboard file to import',
|
||||
title: 'Dahsboard Import',
|
||||
close_btn: 'Close',
|
||||
submit_to: GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=import_dashboard'
|
||||
submit_to: GetAbsoluteUrlAppRoot()+'pages/UI.php?route=dashboard.import'
|
||||
},
|
||||
|
||||
// the constructor
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
/**
|
||||
* Image renderer plugin for TomSelect
|
||||
*/
|
||||
TomSelect.define('image_renderer', function(options) {
|
||||
this.settings.render.option = function(data, escape) {
|
||||
const image = data['$option'].dataset['image'];
|
||||
if(image === ''){
|
||||
return `<div style="padding-left: 46px;">${escape(data.text)}</div>`;
|
||||
}
|
||||
else return `<div class="ibo-input-select-icon--menu--item">
|
||||
<img src="${image}" alt="${escape(data.text)}" />${escape(data.text)}
|
||||
</div>`;
|
||||
};
|
||||
this.settings.render.item = function(data, escape) {
|
||||
const image = data['$option'].dataset['image'];
|
||||
if(image === ''){
|
||||
return `<div>${escape(data.text)}</div>`;
|
||||
}
|
||||
else return `<div class="ibo-input-select-icon--menu--item">
|
||||
<img src="${image}" alt="${escape(data.text)}"/>${escape(data.text)}
|
||||
</div>`;
|
||||
};
|
||||
});
|
||||
|
||||
class ChoicesElement extends HTMLSelectElement {
|
||||
|
||||
// register the custom element
|
||||
@@ -16,6 +40,14 @@ class ChoicesElement extends HTMLSelectElement {
|
||||
this.plugins.push('remove_button');
|
||||
}
|
||||
|
||||
// plugins
|
||||
if(this.hasAttribute('data-plugins')){
|
||||
const aPlugins = JSON.parse(this.getAttribute('data-plugins'));
|
||||
Array.from(aPlugins).values().forEach(plugin => {
|
||||
this.plugins.push(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
plugins: this.plugins,
|
||||
wrapperClass: 'ts-wrapper ibo-input-wrapper ibo-input-select-wrapper--with-buttons ibo-input-select-autocomplete-wrapper',
|
||||
@@ -43,3 +75,4 @@ class ChoicesElement extends HTMLSelectElement {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class TurboStreamEvent extends HTMLElement {
|
||||
|
||||
this.style.display = 'none';
|
||||
|
||||
const event = new CustomEvent("itop:TurboStreamEvent", {
|
||||
const event = new CustomEvent(`itop:TurboStreamEvent:${this.dataset.type}`, {
|
||||
detail: {
|
||||
id: this.getAttribute('id'),
|
||||
form_id: this.dataset.formId,
|
||||
@@ -20,6 +20,8 @@ class TurboStreamEvent extends HTMLElement {
|
||||
},
|
||||
});
|
||||
|
||||
console.log(event);
|
||||
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -453,7 +453,7 @@
|
||||
//
|
||||
// Method: jQuery.deparam
|
||||
//
|
||||
// Deserialize a params string into an object, optionally coercing numbers,
|
||||
// DeserializeFromDOMNode a params string into an object, optionally coercing numbers,
|
||||
// booleans, null and undefined values; this method is the counterpart to the
|
||||
// internal jQuery.param method.
|
||||
//
|
||||
|
||||
83
js/layouts/dashboard/dashboard-grid-slot.js
Normal file
83
js/layouts/dashboard/dashboard-grid-slot.js
Normal file
@@ -0,0 +1,83 @@
|
||||
class IboGridSlot extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
connectedCallback() {
|
||||
|
||||
/** @type {string} unique cell id */
|
||||
this.id = '';
|
||||
|
||||
/** @type {number} */
|
||||
this.iPosX = this.getAttribute('gs-x') ? parseInt(this.getAttribute('gs-x'), 10) : 0;
|
||||
|
||||
/** @type {number} */
|
||||
this.iPostY = this.getAttribute('gs-y') ? parseInt(this.getAttribute('gs-y'), 10) : 0;
|
||||
|
||||
/** @type {number} */
|
||||
this.iWidth = this.getAttribute('gs-w') ? parseInt(this.getAttribute('gs-w'), 10) : 1;
|
||||
|
||||
/** @type {number} */
|
||||
this.iHeight = this.getAttribute('gs-h') ? parseInt(this.getAttribute('gs-h'), 10) : 1;
|
||||
|
||||
/** @type {IboDashlet|null} contained dashlet id */
|
||||
this.sDashletId = null;
|
||||
/** @type {IboDashlet|null} contained dashlet element */
|
||||
this.oDashlet = this.querySelector('ibo-dashlet') || null;
|
||||
|
||||
/** @type {Object} freeform metadata */
|
||||
this.meta = {};
|
||||
}
|
||||
|
||||
static MakeNew(oDashletElem, aOptions = {}) {
|
||||
const oSlot = document.createElement('ibo-dashboard-grid-slot');
|
||||
oDashletElem.classList.add("grid-stack-item-content");
|
||||
|
||||
oSlot.appendChild(oDashletElem);
|
||||
oSlot.classList.add("grid-stack-item");
|
||||
oSlot.oDashlet = oDashletElem;
|
||||
|
||||
return oSlot;
|
||||
}
|
||||
|
||||
Serialize(bIncludeHtml = false) {
|
||||
const oDashlet = this.oDashlet;
|
||||
|
||||
const aSlotData = {
|
||||
position_x: this.iPosX,
|
||||
position_y: this.iPostY,
|
||||
width: this.iWidth,
|
||||
height: this.iHeight
|
||||
};
|
||||
|
||||
const aDashletData = oDashlet ? oDashlet.Serialize() : {};
|
||||
|
||||
return {
|
||||
...aSlotData,
|
||||
dashlet: {...aDashletData}
|
||||
};
|
||||
}
|
||||
|
||||
static observedAttributes = ['gs-x', 'gs-y', 'gs-w', 'gs-h'];
|
||||
|
||||
|
||||
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
switch (name) {
|
||||
case 'gs-x':
|
||||
this.iPosX = parseInt(newValue, 10) || 0;
|
||||
break;
|
||||
case 'gs-y':
|
||||
this.iPostY = parseInt(newValue, 10) || 0;
|
||||
break;
|
||||
case 'gs-w':
|
||||
this.iWidth = parseInt(newValue, 10) || 1;
|
||||
break;
|
||||
case 'gs-h':
|
||||
this.iHeight = parseInt(newValue, 10) || 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashboard-grid-slot', IboGridSlot);
|
||||
194
js/layouts/dashboard/dashboard-grid.js
Normal file
194
js/layouts/dashboard/dashboard-grid.js
Normal file
@@ -0,0 +1,194 @@
|
||||
class IboGrid extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
||||
/** @type {number} */
|
||||
this.columnsCount = 12;
|
||||
/** @type {boolean} unused yet*/
|
||||
this.bEditable = false;
|
||||
/** @type {GridStack|null} GridStack instance */
|
||||
this.oGrid = null;
|
||||
/** @type {Array<IboGridSlot>} */
|
||||
this.aSlots = [];
|
||||
|
||||
|
||||
this.SetupGrid();
|
||||
}
|
||||
|
||||
SetupGrid() {
|
||||
let aCandidateSlots = Array.from(this.querySelectorAll('ibo-dashboard-grid-slot'));
|
||||
aCandidateSlots.forEach(oSlot => {
|
||||
const aAttrs = ['gs-x', 'gs-y', 'gs-w', 'gs-h', 'id'];
|
||||
aAttrs.forEach(sAttr => {
|
||||
const sVal = oSlot.getAttribute(sAttr) || oSlot.getAttribute('data-'+sAttr);
|
||||
if (sVal !== null) {
|
||||
oSlot.setAttribute(sAttr, sVal);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.oGrid = GridStack.init({
|
||||
column: this.columnsCount,
|
||||
marginTop: 8,
|
||||
marginLeft: 8,
|
||||
marginRight: 0,
|
||||
marginBottom: 0,
|
||||
resizable: {handles: 'all'},
|
||||
disableDrag: true,
|
||||
disableResize: true,
|
||||
float: true
|
||||
}, this);
|
||||
}
|
||||
|
||||
getSlots() {
|
||||
return this.oGrid.getGridItems();
|
||||
}
|
||||
|
||||
SetEditable(bIsEditable) {
|
||||
this.bEditable = bIsEditable;
|
||||
if (this.oGrid !== null) {
|
||||
this.oGrid.enableMove(bIsEditable);
|
||||
this.oGrid.enableResize(bIsEditable);
|
||||
}
|
||||
}
|
||||
|
||||
GetDashletElement(sDashletId) {
|
||||
const aSlots = this.getSlots();
|
||||
|
||||
for (let oSlot of aSlots) {
|
||||
|
||||
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
|
||||
return oSlot.oDashlet;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
AddDashlet(sDashlet, aOptions = {}) {
|
||||
// Get the dashlet as an object
|
||||
const oParser = new DOMParser();
|
||||
const oDocument = oParser.parseFromString(sDashlet, 'text/html');
|
||||
const oDashlet = oDocument.body.querySelector('ibo-dashlet');
|
||||
const aScripts = oDocument.body.querySelectorAll('script');
|
||||
|
||||
const oSlot = IboGridSlot.MakeNew(oDashlet);
|
||||
|
||||
this.append(oSlot);
|
||||
|
||||
let aDefaultOptions = {
|
||||
autoPosition: !(aOptions.hasOwnProperty('x') || aOptions.hasOwnProperty('y')),
|
||||
}
|
||||
this.oGrid.makeWidget(oSlot, Object.assign(aDefaultOptions, aOptions));
|
||||
|
||||
oSlot.scrollIntoView({ behavior: "smooth"});
|
||||
|
||||
aScripts.forEach( oScript => {
|
||||
if (oScript) {
|
||||
const oNewScriptElement = document.createElement('script');
|
||||
|
||||
// copy attributes
|
||||
[...oScript.attributes].forEach(attr =>
|
||||
oNewScriptElement.setAttribute(attr.name, attr.value)
|
||||
);
|
||||
|
||||
oNewScriptElement.text = oScript.textContent;
|
||||
oSlot.querySelector('ibo-dashlet').after(oNewScriptElement);
|
||||
}
|
||||
});
|
||||
|
||||
return oDashlet.sDashletId;
|
||||
}
|
||||
|
||||
RefreshDashlet (sDashlet, aOptions = {}) {
|
||||
const oParser = new DOMParser();
|
||||
const oDocument = oParser.parseFromString(sDashlet, 'text/html');
|
||||
const oNewDashlet = oDocument.body.querySelector('ibo-dashlet');
|
||||
const aNewScripts = oDocument.body.querySelectorAll('script');
|
||||
|
||||
// Can't use oNewDashet.sDashletId as it's not in the DOM yet and connectedCallback hasn't been called yet
|
||||
const oExistingDashlet = this.GetDashletElement(oNewDashlet.getAttribute('data-dashlet-id') );
|
||||
|
||||
// Copy attributes
|
||||
for (const sAttr of oNewDashlet.attributes) {
|
||||
oExistingDashlet.setAttribute(sAttr.name, sAttr.value);
|
||||
}
|
||||
|
||||
// If we refresh a dashlet, its parent slot remains the same, we just need to update its content
|
||||
let oSlotForExistingDashlet = null;
|
||||
const aSlots = this.getSlots();
|
||||
for (let oSlot of aSlots) {
|
||||
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === oNewDashlet.getAttribute('data-dashlet-id')) {
|
||||
oSlotForExistingDashlet = oSlot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// There must be a slot for the existing dashlet
|
||||
if(oSlotForExistingDashlet !== null) {
|
||||
this.oGrid.removeWidget(oSlotForExistingDashlet);
|
||||
oSlotForExistingDashlet.innerHTML = oNewDashlet.outerHTML;
|
||||
this.oGrid.makeWidget(oSlotForExistingDashlet);
|
||||
}
|
||||
|
||||
// Append scripts to body so they are executed
|
||||
aNewScripts.forEach( oScript => {
|
||||
if (oScript) {
|
||||
const oNewScriptElement = document.createElement('script');
|
||||
|
||||
// copy attributes
|
||||
[...oScript.attributes].forEach(attr =>
|
||||
oNewScriptElement.setAttribute(attr.name, attr.value)
|
||||
);
|
||||
|
||||
oNewScriptElement.text = oScript.textContent;
|
||||
oSlotForExistingDashlet.querySelector('ibo-dashlet').after(oNewScriptElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CloneDashlet(sDashletId) {
|
||||
const aSlots = this.getSlots();
|
||||
for (let oSlot of aSlots) {
|
||||
|
||||
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
|
||||
const sWidth = oSlot.iWidth;
|
||||
const sHeight = oSlot.iHeight;
|
||||
|
||||
// Ask a new rendered dashlet to avoid duplicating IDs
|
||||
// Still we'll copy width and height
|
||||
this.closest('ibo-dashboard')?.AddNewDashlet(oSlot.oDashlet.sType, oSlot.oDashlet.formData, {
|
||||
'w': sWidth,
|
||||
'h': sHeight
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveDashlet(sDashletId) {
|
||||
const aSlots = this.getSlots();
|
||||
for (let oSlot of aSlots) {
|
||||
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
|
||||
this.oGrid.removeWidget(oSlot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClearGrid() {
|
||||
this.oGrid.removeAll();
|
||||
}
|
||||
|
||||
Serialize() {
|
||||
const aSlots = this.getSlots();
|
||||
|
||||
return aSlots.reduce((aAccumulator, oSlot) => {
|
||||
aAccumulator[oSlot.oDashlet.sDashletId] = oSlot.Serialize();
|
||||
return aAccumulator;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashboard-grid', IboGrid);
|
||||
461
js/layouts/dashboard/dashboard.js
Normal file
461
js/layouts/dashboard/dashboard.js
Normal file
@@ -0,0 +1,461 @@
|
||||
class IboDashboard extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/** @type {string} */
|
||||
this.sId = "";
|
||||
/** @type {string} */
|
||||
this.sTitle = "";
|
||||
/** @type {boolean} unused yet */
|
||||
this.isEditable = false;
|
||||
/** @type {boolean} */
|
||||
this.bEditMode = false;
|
||||
/** @type {boolean} unused yet */
|
||||
this.bAutoRefresh = false;
|
||||
/** @type {number} unused yet */
|
||||
this.iRefreshRate = 0;
|
||||
/** @type {IboGrid|null} */
|
||||
this.oGrid = null;
|
||||
/** @type {string|null} unused yet */
|
||||
this.refreshUrl = null;
|
||||
/** @type {string|null} unused yet */
|
||||
this.csrfToken = null;
|
||||
/** @type {number} Payload schema version */
|
||||
this.schemaVersion = 2;
|
||||
/** @type {boolean} Define is the current is dashboard is custom or not */
|
||||
this.bIsCustomDashboard = false;
|
||||
// TODO 3.3 Do not use file that come from frontend
|
||||
/** @type {string} File for the default dashboard */
|
||||
this.sFile = '';
|
||||
|
||||
/** @type {object|null} Last saved state for cancel functionality, unused yet */
|
||||
this.aLastSavedState = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.sId = this.getAttribute("id");
|
||||
this.bEditMode = (this.getAttribute("data-edit-mode") === "edit")
|
||||
this.bIsCustomDashboard = this.getAttribute("data-is-custom") === "true";
|
||||
this.sFile = this.getAttribute("data-file") || '';
|
||||
|
||||
this.SetupGrid();
|
||||
|
||||
this.BindEvents();
|
||||
}
|
||||
|
||||
BindEvents() {
|
||||
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.ToggleEditMode();
|
||||
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.classList.toggle('active', this.GetEditMode());
|
||||
});
|
||||
|
||||
this.querySelector('[data-role="ibo-button"][name="save"]')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.Save()
|
||||
});
|
||||
|
||||
this.querySelector('[data-role="ibo-button"][name="cancel"]')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (this.aLastSavedState) {
|
||||
this.Load(this.aLastSavedState);
|
||||
}
|
||||
this.SetEditMode(false);
|
||||
});
|
||||
|
||||
document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]')?.addEventListener('change', (e) => {
|
||||
const bIsCustomDashboard = e.target.checked;
|
||||
this.SetIsCustomDashboard(bIsCustomDashboard);
|
||||
});
|
||||
// TODO 3.3 Add event listener to dashboard toggler to get custom/default dashboard switching
|
||||
// TODO 3.3 require load method that's not finished yet
|
||||
|
||||
// Bind IboDashboard object to these listener so we can access current instance
|
||||
this._ListenToDashletFormSubmission = this._ListenToDashletFormSubmission.bind(this);
|
||||
this._ListenToDashletFormCancellation = this._ListenToDashletFormCancellation.bind(this);
|
||||
}
|
||||
SetupGrid() {
|
||||
if(this.oGrid !== null){
|
||||
return;
|
||||
}
|
||||
|
||||
this.oGrid = this.querySelector('ibo-dashboard-grid');
|
||||
}
|
||||
async SetIsCustomDashboard(bIsCustom) {
|
||||
this.bIsCustomDashboard = bIsCustom;
|
||||
this.setAttribute("data-custom-dashboard", bIsCustom ? "true" : "false");
|
||||
SetUserPreference(`display_original_dashboard_${this.sId}`, !bIsCustom, true);
|
||||
console.log(document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]'));
|
||||
let checkbox = document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = bIsCustom;
|
||||
}
|
||||
return this.ReloadFromBackend(bIsCustom);
|
||||
}
|
||||
|
||||
GetEditMode() {
|
||||
return this.bEditMode;
|
||||
}
|
||||
|
||||
ToggleEditMode(){
|
||||
return this.SetEditMode(!this.bEditMode);
|
||||
}
|
||||
|
||||
async SetEditMode(bEditMode) {
|
||||
if (this.bIsCustomDashboard === false && bEditMode === true) {
|
||||
await this.SetIsCustomDashboard(true);
|
||||
}
|
||||
|
||||
this.bEditMode = bEditMode;
|
||||
|
||||
this.oGrid.SetEditable(this.bEditMode);
|
||||
if (this.bEditMode) {
|
||||
// TODO 3.3 If we are in default dashboard display, change to custom to allow editing
|
||||
// TODO 3.3 Get the custom dashboard and load it, show a tooltip on the dashboard toggler to explain that we switched to custom mode
|
||||
this.aLastSavedState = this.Serialize();
|
||||
this.setAttribute("data-edit-mode", "edit");
|
||||
} else {
|
||||
this.setAttribute("data-edit-mode", "view");
|
||||
}
|
||||
}
|
||||
|
||||
AddNewDashlet(sDashletClass, sDashletValues, aDashletOptions = {}) {
|
||||
let oGetDashletPromise = this.GetDashlet(sDashletClass, '', sDashletValues);
|
||||
|
||||
oGetDashletPromise.then(async data => {
|
||||
|
||||
const sDashletId = this.oGrid.AddDashlet(await data.text(), aDashletOptions);
|
||||
|
||||
// Specify that this dashlet is new
|
||||
this.EditDashlet(sDashletId, true);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
GetDashlet(sDashletClass, sDashletId = '', sDashletValues = '') {
|
||||
let sGetDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet&dashlet_class='+encodeURIComponent(sDashletClass);
|
||||
|
||||
if(sDashletId.length > 0) {
|
||||
sGetDashletUrl += '&dashlet_id=' + encodeURIComponent(sDashletId);
|
||||
}
|
||||
|
||||
if(sDashletValues.length > 0) {
|
||||
sGetDashletUrl += '&values=' + encodeURIComponent(sDashletValues);
|
||||
}
|
||||
|
||||
return fetch(sGetDashletUrl);
|
||||
}
|
||||
RefreshDashlet(oDashlet) {
|
||||
let oGetDashletPromise = this.GetDashlet(oDashlet.sType, oDashlet.sDashletId, oDashlet.formData);
|
||||
|
||||
|
||||
return oGetDashletPromise.then(async data => {
|
||||
|
||||
this.oGrid.RefreshDashlet(await data.text());
|
||||
});
|
||||
}
|
||||
|
||||
HideDashletTogglers() {
|
||||
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
|
||||
aTogglers.classList.add('ibo-is-hidden');
|
||||
}
|
||||
|
||||
ShowDashletTogglers() {
|
||||
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
|
||||
aTogglers.classList.remove('ibo-is-hidden');
|
||||
}
|
||||
|
||||
SetDashletForm(sFormData) {
|
||||
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
|
||||
oFormContainer.innerHTML = sFormData;
|
||||
oFormContainer.classList.remove('ibo-is-hidden');
|
||||
}
|
||||
|
||||
ClearDashletForm() {
|
||||
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
|
||||
oFormContainer.innerHTML = '';
|
||||
oFormContainer.classList.add('ibo-is-hidden');
|
||||
}
|
||||
|
||||
DisableFormButtons() {
|
||||
const aButtons = this.querySelectorAll('.ibo-dashboard--form--actions button');
|
||||
aButtons.forEach( (oButton) => {
|
||||
oButton.setAttribute('disabled', 'disabled');
|
||||
});
|
||||
}
|
||||
|
||||
EnableFormButtons() {
|
||||
const aButtons = this.querySelectorAll('.ibo-dashboard--form--actions button');
|
||||
aButtons.forEach( (oButton) => {
|
||||
oButton.removeAttribute('disabled');
|
||||
});
|
||||
}
|
||||
|
||||
EditDashlet(sDashletId, bIsNew = false) {
|
||||
const oDashlet = this.oGrid.GetDashletElement(sDashletId);
|
||||
const me = this;
|
||||
|
||||
// Create backdrop to block interactions with other dashlets
|
||||
if(this.oGrid.querySelector('.ibo-dashboard--grid--backdrop') === null) {
|
||||
const oBackdrop = document.createElement("div");
|
||||
oBackdrop.classList.add('ibo-dashboard--grid--backdrop');
|
||||
this.oGrid.append(oBackdrop);
|
||||
}
|
||||
|
||||
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'edit');
|
||||
|
||||
const oPanelElement = document.querySelector('.ibo-dashlet-panel');
|
||||
// Choose what we'll write as title
|
||||
// Also store this information in a data attribute to be able to differentiate between addition and edition on form submission/cancellation
|
||||
if(bIsNew) {
|
||||
this.SetDashletPanelTitle('Add a dashlet ' + oDashlet.sType);
|
||||
oPanelElement.setAttribute('data-dashlet-form-mode', 'add');
|
||||
}
|
||||
else {
|
||||
this.SetDashletPanelTitle('Edit dashlet ' + oDashlet.sType);
|
||||
oPanelElement.setAttribute('data-dashlet-form-mode', 'edit');
|
||||
}
|
||||
|
||||
// Disable dashboard buttons so we need to finish this edition first
|
||||
this.DisableFormButtons();
|
||||
|
||||
// Fetch dashlet form from server
|
||||
let sGetashletFormUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet_form&dashlet_class='+encodeURIComponent(oDashlet.sType);
|
||||
|
||||
if(oDashlet.formData.length > 0) {
|
||||
sGetashletFormUrl += '&values=' + encodeURIComponent(oDashlet.formData);
|
||||
}
|
||||
|
||||
fetch(sGetashletFormUrl)
|
||||
.then(async formData => {
|
||||
const sFormData = await formData.text();
|
||||
|
||||
this.HideDashletTogglers();
|
||||
this.SetDashletForm(sFormData);
|
||||
|
||||
// Listen to form submission event and cancellation
|
||||
document.addEventListener('itop:TurboStreamEvent:Complete', me._ListenToDashletFormSubmission);
|
||||
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').addEventListener('click', me._ListenToDashletFormCancellation);
|
||||
});
|
||||
}
|
||||
|
||||
_ListenToDashletFormSubmission(event) {
|
||||
const oDashlet = this.querySelector('ibo-dashlet[data-edit-mode="edit"]');
|
||||
const sDashletId = oDashlet.GetDashletId();
|
||||
|
||||
if(event.detail.id === oDashlet.sType + '-turbo-stream-event' && event.detail.valid === "1") {
|
||||
// Remove events
|
||||
document.addEventListener('itop:TurboStreamEvent:Complete', this._ListenToDashletFormSubmission);
|
||||
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation);
|
||||
|
||||
// Notify it all went well
|
||||
CombodoToast.OpenToast('Dashlet created/updated', 'success');
|
||||
|
||||
// Clean edit mode
|
||||
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view');
|
||||
document.querySelector('.ibo-dashlet-panel').removeAttribute('data-dashlet-form-mode');
|
||||
this.ShowDashletTogglers();
|
||||
this.ClearDashletForm();
|
||||
this.SetDashletPanelTitle();
|
||||
|
||||
// Update local dashlet and refresh it
|
||||
oDashlet.formData = event.detail.view_data;
|
||||
this.RefreshDashlet(oDashlet);
|
||||
|
||||
// Re-enable dashboard buttons
|
||||
this.EnableFormButtons();
|
||||
}
|
||||
}
|
||||
|
||||
_ListenToDashletFormCancellation(event) {
|
||||
const oDashlet = this.querySelector('ibo-dashlet[data-edit-mode="edit"]');
|
||||
const sDashletId = oDashlet.GetDashletId();
|
||||
|
||||
// If we are cancelling an addition, remove the dashlet from the grid
|
||||
const oPanelElement = document.querySelector('.ibo-dashlet-panel');
|
||||
const sDashletFormMode = oPanelElement.getAttribute('data-dashlet-form-mode');
|
||||
|
||||
if(sDashletFormMode === 'add') {
|
||||
this.oGrid.RemoveDashlet(sDashletId);
|
||||
}
|
||||
else if(sDashletFormMode === 'edit') {
|
||||
// Just exit edit mode
|
||||
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view');
|
||||
|
||||
// TODO 3.3 If we refresh dashlet view in edit mode, we should restore previous form data + rendering
|
||||
}
|
||||
|
||||
// Remove events
|
||||
document.addEventListener('itop:TurboStreamEvent:Complete', this._ListenToDashletFormSubmission);
|
||||
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation);
|
||||
|
||||
// Clean edit mode
|
||||
this.ShowDashletTogglers();
|
||||
this.ClearDashletForm();
|
||||
this.SetDashletPanelTitle();
|
||||
|
||||
// Re-enable dashboard buttons
|
||||
this.EnableFormButtons();
|
||||
}
|
||||
|
||||
SetDashletPanelTitle(sTitle = '') {
|
||||
const oTitleElement = document.querySelector('.ibo-dashlet-panel .ibo-dashlet-panel--title');
|
||||
|
||||
if (sTitle === '') {
|
||||
sTitle = 'Add a dashlet';
|
||||
}
|
||||
if (oTitleElement) {
|
||||
oTitleElement.innerText = sTitle;
|
||||
}
|
||||
}
|
||||
|
||||
CloneDashlet(sDashletId) {
|
||||
this.oGrid.CloneDashlet(sDashletId);
|
||||
}
|
||||
|
||||
RemoveDashlet(sDashletId) {
|
||||
this.oGrid.RemoveDashlet(sDashletId);
|
||||
}
|
||||
|
||||
ReloadFromBackend(bCustomDashboard = false) {
|
||||
let sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&is_custom=${bCustomDashboard ? 'true' : 'false'}`;
|
||||
if(!bCustomDashboard && this.sFile.length > 0) {
|
||||
sLoadDashboardUrl += `&file=${encodeURIComponent(this.sFile)}`;
|
||||
}
|
||||
|
||||
fetch(sLoadDashboardUrl)
|
||||
.then(async oResponse => {
|
||||
const oDashletData = await oResponse.json();
|
||||
this.Load(oDashletData.data);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Serialize() {
|
||||
const sDashboardTitle = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]').value;
|
||||
const sDashboardRefreshRate = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]').value;
|
||||
|
||||
const aSerializedGrid = this.oGrid.Serialize();
|
||||
return {
|
||||
schema_version: this.schemaVersion,
|
||||
id: this.sId,
|
||||
title: sDashboardTitle,
|
||||
refresh: sDashboardRefreshRate,
|
||||
pos_dashlets: aSerializedGrid,
|
||||
_token: ":)"
|
||||
};
|
||||
}
|
||||
|
||||
Save() {
|
||||
// This payload shape is expected by the server
|
||||
const aPayload = this.Serialize();
|
||||
|
||||
let sSaveUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.save';
|
||||
|
||||
fetch(sSaveUrl, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({ values: JSON.stringify(aPayload) }),
|
||||
})
|
||||
.then(async data => {
|
||||
const res = await data.json();
|
||||
if(res.status === 'ok') {
|
||||
CombodoToast.OpenToast(res.message, 'success');
|
||||
this.aLastSavedState = this.Serialize();
|
||||
await this.SetEditMode(false);
|
||||
} else {
|
||||
CombodoToast.OpenToast(res.message, 'error');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Load(aSaveState) {
|
||||
try {
|
||||
// TODO 3.3 Maybe we won't need to validate schema version right now as we control both sides
|
||||
// Validate schema version
|
||||
if (false && aSaveState.schema_version !== this.schemaVersion) {
|
||||
CombodoToast.OpenToast('Somehow, we got an incompatible dashboard schema version.', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update dashboard data
|
||||
this.sTitle = aSaveState.title || "";
|
||||
this.iRefreshRate = parseInt(aSaveState.refresh, 10) || 0;
|
||||
|
||||
// Update form inputs if they exist
|
||||
const oTitleInput = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]');
|
||||
if (oTitleInput) {
|
||||
oTitleInput.value = this.sTitle;
|
||||
}
|
||||
|
||||
const oRefreshSelect = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]');
|
||||
if (oRefreshSelect) {
|
||||
oRefreshSelect.value = aSaveState.refresh;
|
||||
}
|
||||
|
||||
// Clear existing grid
|
||||
this.ClearGrid();
|
||||
|
||||
// Load dashlets
|
||||
const aDashletSlots = aSaveState.pos_dashlets || {};
|
||||
|
||||
for (const [sDashletId, aDashletData] of Object.entries(aDashletSlots)) {
|
||||
const iPosX = aDashletData.position_x;
|
||||
const iPosY = aDashletData.position_y;
|
||||
const iWidth = aDashletData.width;
|
||||
const iHeight = aDashletData.height;
|
||||
const aDashlet = aDashletData.dashlet;
|
||||
let sDashletHtml = '';
|
||||
// Check if the dashlet state has HTML content
|
||||
|
||||
// TODO 3.3 Is there a way to avoid duplicating AddDashlet call but keep the promise to avoid waiting for fetch result in this loop ?
|
||||
if(aDashletData.html && aDashletData.html.length > 0) {
|
||||
sDashletHtml = aDashletData.html;
|
||||
this.oGrid.AddDashlet(sDashletHtml, {
|
||||
x: iPosX,
|
||||
y: iPosY,
|
||||
w: iWidth,
|
||||
h: iHeight,
|
||||
autoPosition: false
|
||||
});
|
||||
} else {
|
||||
// We need to fetch dashlet HTML from server as scripts need to be executed again
|
||||
let oGetDashletPromise = this.GetDashlet(aDashlet.type, aDashlet.id, JSON.stringify(aDashlet.properties));
|
||||
|
||||
oGetDashletPromise.then(async data => {
|
||||
let sDashletHtml = await data.text();
|
||||
// Add dashlet to grid with its position and size
|
||||
this.oGrid.AddDashlet(sDashletHtml, {
|
||||
x: iPosX,
|
||||
y: iPosY,
|
||||
w: iWidth,
|
||||
h: iHeight,
|
||||
autoPosition: false
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update last saved state
|
||||
this.aLastSavedState = aSaveState;
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard state:', error);
|
||||
CombodoToast.OpenToast('Error loading dashboard state', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ClearGrid() {
|
||||
this.oGrid.ClearGrid();
|
||||
}
|
||||
|
||||
DisplayError(sMessage, sSeverity = 'error') {
|
||||
// TODO 3.3: Make this real
|
||||
this.setAttribute("data-edit-mode", "error");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashboard', IboDashboard);
|
||||
64
js/layouts/dashboard/dashlet.js
Normal file
64
js/layouts/dashboard/dashlet.js
Normal file
@@ -0,0 +1,64 @@
|
||||
class IboDashlet extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
/** @type {string} */
|
||||
this.sDashletId = this.GetDashletId();
|
||||
/** @type {string} */
|
||||
this.sType = this.GetDashletType();
|
||||
/** @type {Object} */
|
||||
this.formData = this.GetFormData();
|
||||
/** @type {Object} unused yet */
|
||||
this.meta = {};
|
||||
|
||||
this.BindEvents();
|
||||
}
|
||||
BindEvents() {
|
||||
// Bind any dashlet-specific events here
|
||||
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-edit"]')?.addEventListener('click', (e) => {
|
||||
this.closest('ibo-dashboard')?.EditDashlet(this.sDashletId);
|
||||
});
|
||||
|
||||
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-clone"]')?.addEventListener('click', (e) => {
|
||||
this.closest('ibo-dashboard')?.CloneDashlet(this.sDashletId);
|
||||
});
|
||||
|
||||
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-remove"]')?.addEventListener('click', (e) => {
|
||||
this.closest('ibo-dashboard')?.RemoveDashlet(this.sDashletId);
|
||||
});
|
||||
}
|
||||
|
||||
GetDashletId() {
|
||||
return this.getAttribute('data-dashlet-id');
|
||||
}
|
||||
|
||||
GetDashletType() {
|
||||
return this.getAttribute("data-dashlet-type") || "";
|
||||
}
|
||||
|
||||
GetFormData() {
|
||||
return this.getAttribute("data-form-view-data") || "";
|
||||
}
|
||||
|
||||
static MakeNew(sDashlet) {
|
||||
const oDashlet = document.createElement('ibo-dashlet');
|
||||
oDashlet.innerHTML = sDashlet;
|
||||
|
||||
return oDashlet;
|
||||
}
|
||||
|
||||
Serialize() {
|
||||
// TODO 3.3 Should we use getters ?
|
||||
let aDashletData = {
|
||||
id: this.sDashletId,
|
||||
type: this.sType,
|
||||
properties: JSON.parse(this.formData),
|
||||
};
|
||||
|
||||
return aDashletData;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashlet', IboDashlet);
|
||||
@@ -130,6 +130,28 @@ return array(
|
||||
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
|
||||
'Collator' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php',
|
||||
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/Application/Branding.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\Controller\\DashboardController' => $baseDir . '/sources/Application/Dashboard/Controller/DashboardController.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\Dashboard' => $baseDir . '/sources/Application/Dashboard/Dashboard.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\DashboardException' => $baseDir . '/sources/Application/Dashboard/DashboardException.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashboardFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashboardFormBlock.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashletFormBlock.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletPropertiesFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashletPropertiesFormBlock.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\Layout\\DashboardLayoutGrid' => $baseDir . '/sources/Application/Dashboard/Layout/DashboardLayoutGrid.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletBadge' => $baseDir . '/sources/Application/Dashlet/Base/DashletBadge.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupBy' => $baseDir . '/sources/Application/Dashlet/Base/DashletGroupBy.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupByBars' => $baseDir . '/sources/Application/Dashlet/Base/DashletGroupByBars.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupByPie' => $baseDir . '/sources/Application/Dashlet/Base/DashletGroupByPie.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupByTable' => $baseDir . '/sources/Application/Dashlet/Base/DashletGroupByTable.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletHeaderDynamic' => $baseDir . '/sources/Application/Dashlet/Base/DashletHeaderDynamic.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletHeaderStatic' => $baseDir . '/sources/Application/Dashlet/Base/DashletHeaderStatic.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletObjectList' => $baseDir . '/sources/Application/Dashlet/Base/DashletObjectList.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletPlainText' => $baseDir . '/sources/Application/Dashlet/Base/DashletPlainText.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletProxy' => $baseDir . '/sources/Application/Dashlet/Base/DashletProxy.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletUnknown' => $baseDir . '/sources/Application/Dashlet/Base/DashletUnknown.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Dashlet' => $baseDir . '/sources/Application/Dashlet/Dashlet.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\DashletException' => $baseDir . '/sources/Application/Dashlet/DashletException.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\DashletFactory' => $baseDir . '/sources/Application/Dashlet/DashletFactory.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Service\\DashletService' => $baseDir . '/sources/Application/Dashlet/Service/DashletService.php',
|
||||
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
|
||||
@@ -170,6 +192,7 @@ return array(
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletFactory' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletHeaderStatic' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletHeaderStatic.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletPlainText' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletPlainText.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletWrapper' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletWrapper.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTable' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/DataTable.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableConfig\\DataTableConfig' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/DataTableConfig/DataTableConfig.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableSettings' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php',
|
||||
@@ -270,8 +293,13 @@ return array(
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryForm' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryFormFactory' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardColumn' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardColumn.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGrid' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGrid.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGridSlot' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGridSlot.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardLayout' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardLayout.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardRow' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardRow.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletEntry' => $baseDir . '/sources/Application/UI/Base/Layout/DashletPanel/DashletEntry.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanel' => $baseDir . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanel.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanelFactory' => $baseDir . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\Column' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/Column/Column.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\ColumnUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/Column/ColumnUIBlockFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\MultiColumn' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/MultiColumn.php',
|
||||
@@ -483,13 +511,16 @@ return array(
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\CheckboxFormBlock' => $baseDir . '/sources/Forms/Block/Base/CheckboxFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFormBlock' => $baseDir . '/sources/Forms/Block/Base/ChoiceFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFromInputsBlock' => $baseDir . '/sources/Forms/Block/Base/ChoiceFromInputsBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceImageFormBlock' => $baseDir . '/sources/Forms/Block/Base/ChoiceImageFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\CollectionBlock' => $baseDir . '/sources/Forms/Block/Base/CollectionBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\DateFormBlock' => $baseDir . '/sources/Forms/Block/Base/DateFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\DateTimeFormBlock' => $baseDir . '/sources/Forms/Block/Base/DateTimeFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\FileFormBlock' => $baseDir . '/sources/Forms/Block/Base/FileFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\FormBlock' => $baseDir . '/sources/Forms/Block/Base/FormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\HiddenFormBlock' => $baseDir . '/sources/Forms/Block/Base/HiddenFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\IntegerFormBlock' => $baseDir . '/sources/Forms/Block/Base/IntegerFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\NumberFormBlock' => $baseDir . '/sources/Forms/Block/Base/NumberFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\PolymorphicFormBlock' => $baseDir . '/sources/Forms/Block/Base/PolymorphicFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\TextAreaFormBlock' => $baseDir . '/sources/Forms/Block/Base/TextAreaFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\TextFormBlock' => $baseDir . '/sources/Forms/Block/Base/TextFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeChoiceFormBlock' => $baseDir . '/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php',
|
||||
@@ -512,6 +543,7 @@ return array(
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\DependencyMap' => $baseDir . '/sources/Forms/FormBuilder/DependencyMap.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilder' => $baseDir . '/sources/Forms/FormBuilder/FormBuilder.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilderException' => $baseDir . '/sources/Forms/FormBuilder/FormBuilderException.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormFactoryBuilderService' => $baseDir . '/sources/Forms/FormBuilder/FormFactoryBuilderService.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormHelper' => $baseDir . '/sources/Forms/FormBuilder/FormHelper.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormTypeExtension' => $baseDir . '/sources/Forms/FormBuilder/FormTypeExtension.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\ResolvedFormType' => $baseDir . '/sources/Forms/FormBuilder/ResolvedFormType.php',
|
||||
@@ -559,6 +591,7 @@ return array(
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\SerializerException' => $baseDir . '/sources/PropertyType/Serializer/SerializerException.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\AbstractXMLFormat' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/AbstractXMLFormat.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCSV' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCSV.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCollectionWithId' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCollectionWithId.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFactory' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFactory.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFlatArray' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFlatArray.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatValueAsId' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatValueAsId.php',
|
||||
@@ -566,6 +599,7 @@ return array(
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\AbstractValueType' => $baseDir . '/sources/PropertyType/ValueType/AbstractValueType.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\AbstractBranchValueType' => $baseDir . '/sources/PropertyType/ValueType/Branch/AbstractBranchValueType.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypeCollection' => $baseDir . '/sources/PropertyType/ValueType/Branch/ValueTypeCollection.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePolymorphic' => $baseDir . '/sources/PropertyType/ValueType/Branch/ValueTypePolymorphic.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePropertyTree' => $baseDir . '/sources/PropertyType/ValueType/Branch/ValueTypePropertyTree.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\AbstractLeafValueType' => $baseDir . '/sources/PropertyType/ValueType/Leaf/AbstractLeafValueType.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\ValueTypeAggregateFunction' => $baseDir . '/sources/PropertyType/ValueType/Leaf/ValueTypeAggregateFunction.php',
|
||||
@@ -607,8 +641,6 @@ return array(
|
||||
'Combodo\\iTop\\Service\\Base\\ObjectRepository' => $baseDir . '/sources/Service/Base/ObjectRepository.php',
|
||||
'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => $baseDir . '/sources/Service/Base/iDataPostProcessor.php',
|
||||
'Combodo\\iTop\\Service\\Cache\\DataModelDependantCache' => $baseDir . '/sources/Service/Cache/DataModelDependantCache.php',
|
||||
'Combodo\\iTop\\Service\\DependencyInjection\\DIException' => $baseDir . '/sources/Service/DependencyInjection/DIException.php',
|
||||
'Combodo\\iTop\\Service\\DependencyInjection\\ServiceLocator' => $baseDir . '/sources/Service/DependencyInjection/ServiceLocator.php',
|
||||
'Combodo\\iTop\\Service\\Events\\Description\\EventDataDescription' => $baseDir . '/sources/Service/Events/Description/EventDataDescription.php',
|
||||
'Combodo\\iTop\\Service\\Events\\Description\\EventDescription' => $baseDir . '/sources/Service/Events/Description/EventDescription.php',
|
||||
'Combodo\\iTop\\Service\\Events\\EventData' => $baseDir . '/sources/Service/Events/EventData.php',
|
||||
@@ -631,6 +663,9 @@ return array(
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php',
|
||||
'Combodo\\iTop\\Service\\ServiceLocator\\NotFoundException' => $baseDir . '/sources/Service/ServiceLocator/NotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\ServiceLocator\\ServiceLocator' => $baseDir . '/sources/Service/ServiceLocator/ServiceLocator.php',
|
||||
'Combodo\\iTop\\Service\\ServiceLocator\\ServiceLocatorException' => $baseDir . '/sources/Service/ServiceLocator/ServiceLocatorException.php',
|
||||
'Combodo\\iTop\\Service\\SummaryCard\\SummaryCardService' => $baseDir . '/sources/Service/SummaryCard/SummaryCardService.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectConfig' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectConfig.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectGC' => $baseDir . '/sources/Service/TemporaryObjects/TemporaryObjectGC.php',
|
||||
@@ -669,26 +704,12 @@ return array(
|
||||
'DBUnionSearch' => $baseDir . '/core/dbunionsearch.class.php',
|
||||
'DOMSanitizer' => $baseDir . '/core/htmlsanitizer.class.inc.php',
|
||||
'DailyRotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
|
||||
'Dashboard' => $baseDir . '/application/dashboard.class.inc.php',
|
||||
'DashboardLayout' => $baseDir . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutMultiCol' => $baseDir . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutOneCol' => $baseDir . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutThreeCols' => $baseDir . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutTwoCols' => $baseDir . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardMenuNode' => $baseDir . '/application/menunode.class.inc.php',
|
||||
'Dashlet' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletBadge' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletEmptyCell' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupBy' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupByBars' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupByPie' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupByTable' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletHeaderDynamic' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletHeaderStatic' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletObjectList' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletPlainText' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletProxy' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'DashletUnknown' => $baseDir . '/application/dashlet.class.inc.php',
|
||||
'Datamatrix' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php',
|
||||
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||
@@ -1420,7 +1441,7 @@ return array(
|
||||
'RowStatus_NewObj' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||
'RowStatus_NoChange' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||
'RunTimeIconSelectionField' => $baseDir . '/application/forms.class.inc.php',
|
||||
'RuntimeDashboard' => $baseDir . '/application/dashboard.class.inc.php',
|
||||
'RuntimeDashboard' => $baseDir . '/sources/Application/Dashboard/RuntimeDashboard.php',
|
||||
'SQLExpression' => $baseDir . '/core/oql/expression.class.inc.php',
|
||||
'SQLObjectQuery' => $baseDir . '/core/sqlobjectquery.class.inc.php',
|
||||
'SQLObjectQueryBuilder' => $baseDir . '/core/sqlobjectquerybuilder.class.inc.php',
|
||||
|
||||
@@ -516,6 +516,28 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
|
||||
'Collator' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php',
|
||||
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/Application/Branding.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\Controller\\DashboardController' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Controller/DashboardController.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\Dashboard' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Dashboard.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\DashboardException' => __DIR__ . '/../..' . '/sources/Application/Dashboard/DashboardException.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashboardFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashboardFormBlock.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashletFormBlock.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletPropertiesFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashletPropertiesFormBlock.php',
|
||||
'Combodo\\iTop\\Application\\Dashboard\\Layout\\DashboardLayoutGrid' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Layout/DashboardLayoutGrid.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletBadge' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletBadge.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupBy' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletGroupBy.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupByBars' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletGroupByBars.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupByPie' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletGroupByPie.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletGroupByTable' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletGroupByTable.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletHeaderDynamic' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletHeaderDynamic.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletHeaderStatic' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletHeaderStatic.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletObjectList' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletObjectList.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletPlainText' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletPlainText.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletProxy' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletProxy.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Base\\DashletUnknown' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Base/DashletUnknown.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Dashlet' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Dashlet.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\DashletException' => __DIR__ . '/../..' . '/sources/Application/Dashlet/DashletException.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\DashletFactory' => __DIR__ . '/../..' . '/sources/Application/Dashlet/DashletFactory.php',
|
||||
'Combodo\\iTop\\Application\\Dashlet\\Service\\DashletService' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Service/DashletService.php',
|
||||
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
|
||||
@@ -556,6 +578,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletHeaderStatic' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletHeaderStatic.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletPlainText' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletPlainText.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletWrapper' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletWrapper.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTable' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/DataTable.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableConfig\\DataTableConfig' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/DataTableConfig/DataTableConfig.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableSettings' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php',
|
||||
@@ -656,8 +679,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryForm' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryFormFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardColumn' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardColumn.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGrid' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGrid.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGridSlot' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGridSlot.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardLayout' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardLayout.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardRow' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardRow.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletEntry' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/DashletPanel/DashletEntry.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanel.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanelFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\Column' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/Column/Column.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\ColumnUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/Column/ColumnUIBlockFactory.php',
|
||||
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\MultiColumn' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/MultiColumn.php',
|
||||
@@ -869,13 +897,16 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\CheckboxFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/CheckboxFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ChoiceFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFromInputsBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ChoiceFromInputsBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceImageFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ChoiceImageFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\CollectionBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/CollectionBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\DateFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/DateFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\DateTimeFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/DateTimeFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\FileFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/FileFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\FormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/FormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\HiddenFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/HiddenFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\IntegerFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/IntegerFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\NumberFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/NumberFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\PolymorphicFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/PolymorphicFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\TextAreaFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/TextAreaFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\Base\\TextFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/TextFormBlock.php',
|
||||
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeChoiceFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php',
|
||||
@@ -898,6 +929,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\DependencyMap' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/DependencyMap.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilder' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormBuilder.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilderException' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormBuilderException.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormFactoryBuilderService' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormFactoryBuilderService.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormHelper' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormHelper.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\FormTypeExtension' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormTypeExtension.php',
|
||||
'Combodo\\iTop\\Forms\\FormBuilder\\ResolvedFormType' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/ResolvedFormType.php',
|
||||
@@ -945,6 +977,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\SerializerException' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/SerializerException.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\AbstractXMLFormat' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/AbstractXMLFormat.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCSV' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCSV.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCollectionWithId' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCollectionWithId.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFactory' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFactory.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFlatArray' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFlatArray.php',
|
||||
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatValueAsId' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatValueAsId.php',
|
||||
@@ -952,6 +985,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\AbstractValueType' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/AbstractValueType.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\AbstractBranchValueType' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/AbstractBranchValueType.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypeCollection' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/ValueTypeCollection.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePolymorphic' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/ValueTypePolymorphic.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePropertyTree' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/ValueTypePropertyTree.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\AbstractLeafValueType' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Leaf/AbstractLeafValueType.php',
|
||||
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\ValueTypeAggregateFunction' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Leaf/ValueTypeAggregateFunction.php',
|
||||
@@ -993,8 +1027,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Service\\Base\\ObjectRepository' => __DIR__ . '/../..' . '/sources/Service/Base/ObjectRepository.php',
|
||||
'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Base/iDataPostProcessor.php',
|
||||
'Combodo\\iTop\\Service\\Cache\\DataModelDependantCache' => __DIR__ . '/../..' . '/sources/Service/Cache/DataModelDependantCache.php',
|
||||
'Combodo\\iTop\\Service\\DependencyInjection\\DIException' => __DIR__ . '/../..' . '/sources/Service/DependencyInjection/DIException.php',
|
||||
'Combodo\\iTop\\Service\\DependencyInjection\\ServiceLocator' => __DIR__ . '/../..' . '/sources/Service/DependencyInjection/ServiceLocator.php',
|
||||
'Combodo\\iTop\\Service\\Events\\Description\\EventDataDescription' => __DIR__ . '/../..' . '/sources/Service/Events/Description/EventDataDescription.php',
|
||||
'Combodo\\iTop\\Service\\Events\\Description\\EventDescription' => __DIR__ . '/../..' . '/sources/Service/Events/Description/EventDescription.php',
|
||||
'Combodo\\iTop\\Service\\Events\\EventData' => __DIR__ . '/../..' . '/sources/Service/Events/EventData.php',
|
||||
@@ -1017,6 +1049,9 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php',
|
||||
'Combodo\\iTop\\Service\\ServiceLocator\\NotFoundException' => __DIR__ . '/../..' . '/sources/Service/ServiceLocator/NotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\ServiceLocator\\ServiceLocator' => __DIR__ . '/../..' . '/sources/Service/ServiceLocator/ServiceLocator.php',
|
||||
'Combodo\\iTop\\Service\\ServiceLocator\\ServiceLocatorException' => __DIR__ . '/../..' . '/sources/Service/ServiceLocator/ServiceLocatorException.php',
|
||||
'Combodo\\iTop\\Service\\SummaryCard\\SummaryCardService' => __DIR__ . '/../..' . '/sources/Service/SummaryCard/SummaryCardService.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectConfig' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectConfig.php',
|
||||
'Combodo\\iTop\\Service\\TemporaryObjects\\TemporaryObjectGC' => __DIR__ . '/../..' . '/sources/Service/TemporaryObjects/TemporaryObjectGC.php',
|
||||
@@ -1055,26 +1090,12 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'DBUnionSearch' => __DIR__ . '/../..' . '/core/dbunionsearch.class.php',
|
||||
'DOMSanitizer' => __DIR__ . '/../..' . '/core/htmlsanitizer.class.inc.php',
|
||||
'DailyRotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
|
||||
'Dashboard' => __DIR__ . '/../..' . '/application/dashboard.class.inc.php',
|
||||
'DashboardLayout' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutMultiCol' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutOneCol' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutThreeCols' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardLayoutTwoCols' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
|
||||
'DashboardMenuNode' => __DIR__ . '/../..' . '/application/menunode.class.inc.php',
|
||||
'Dashlet' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletBadge' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletEmptyCell' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupBy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupByBars' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupByPie' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletGroupByTable' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletHeaderDynamic' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletHeaderStatic' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletObjectList' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletPlainText' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletProxy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'DashletUnknown' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
|
||||
'Datamatrix' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php',
|
||||
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
|
||||
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
|
||||
@@ -1806,7 +1827,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'RowStatus_NewObj' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||
'RowStatus_NoChange' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||
'RunTimeIconSelectionField' => __DIR__ . '/../..' . '/application/forms.class.inc.php',
|
||||
'RuntimeDashboard' => __DIR__ . '/../..' . '/application/dashboard.class.inc.php',
|
||||
'RuntimeDashboard' => __DIR__ . '/../..' . '/sources/Application/Dashboard/RuntimeDashboard.php',
|
||||
'SQLExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
|
||||
'SQLObjectQuery' => __DIR__ . '/../..' . '/core/sqlobjectquery.class.inc.php',
|
||||
'SQLObjectQueryBuilder' => __DIR__ . '/../..' . '/core/sqlobjectquerybuilder.class.inc.php',
|
||||
|
||||
562
lib/symfony/cache/Adapter/ArrayAdapter.php
vendored
562
lib/symfony/cache/Adapter/ArrayAdapter.php
vendored
@@ -28,341 +28,341 @@ use Symfony\Contracts\Cache\CacheInterface;
|
||||
*/
|
||||
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private bool $storeSerialized;
|
||||
private array $values = [];
|
||||
private array $tags = [];
|
||||
private array $expiries = [];
|
||||
private int $defaultLifetime;
|
||||
private float $maxLifetime;
|
||||
private int $maxItems;
|
||||
private bool $storeSerialized;
|
||||
private array $values = [];
|
||||
private array $tags = [];
|
||||
private array $expiries = [];
|
||||
private int $defaultLifetime;
|
||||
private float $maxLifetime;
|
||||
private int $maxItems;
|
||||
|
||||
private static \Closure $createCacheItem;
|
||||
private static \Closure $createCacheItem;
|
||||
|
||||
/**
|
||||
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
|
||||
*/
|
||||
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, float $maxLifetime = 0, int $maxItems = 0)
|
||||
{
|
||||
if (0 > $maxLifetime) {
|
||||
throw new InvalidArgumentException(\sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime));
|
||||
}
|
||||
/**
|
||||
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
|
||||
*/
|
||||
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, float $maxLifetime = 0, int $maxItems = 0)
|
||||
{
|
||||
if (0 > $maxLifetime) {
|
||||
throw new InvalidArgumentException(\sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime));
|
||||
}
|
||||
|
||||
if (0 > $maxItems) {
|
||||
throw new InvalidArgumentException(\sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
|
||||
}
|
||||
if (0 > $maxItems) {
|
||||
throw new InvalidArgumentException(\sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
|
||||
}
|
||||
|
||||
$this->defaultLifetime = $defaultLifetime;
|
||||
$this->storeSerialized = $storeSerialized;
|
||||
$this->maxLifetime = $maxLifetime;
|
||||
$this->maxItems = $maxItems;
|
||||
self::$createCacheItem ??= \Closure::bind(
|
||||
static function ($key, $value, $isHit, $tags) {
|
||||
$item = new CacheItem();
|
||||
$item->key = $key;
|
||||
$item->value = $value;
|
||||
$item->isHit = $isHit;
|
||||
if (null !== $tags) {
|
||||
$item->metadata[CacheItem::METADATA_TAGS] = $tags;
|
||||
}
|
||||
$this->defaultLifetime = $defaultLifetime;
|
||||
$this->storeSerialized = $storeSerialized;
|
||||
$this->maxLifetime = $maxLifetime;
|
||||
$this->maxItems = $maxItems;
|
||||
self::$createCacheItem ??= \Closure::bind(
|
||||
static function ($key, $value, $isHit, $tags) {
|
||||
$item = new CacheItem();
|
||||
$item->key = $key;
|
||||
$item->value = $value;
|
||||
$item->isHit = $isHit;
|
||||
if (null !== $tags) {
|
||||
$item->metadata[CacheItem::METADATA_TAGS] = $tags;
|
||||
}
|
||||
|
||||
return $item;
|
||||
},
|
||||
null,
|
||||
CacheItem::class
|
||||
);
|
||||
}
|
||||
return $item;
|
||||
},
|
||||
null,
|
||||
CacheItem::class
|
||||
);
|
||||
}
|
||||
|
||||
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
|
||||
{
|
||||
$item = $this->getItem($key);
|
||||
$metadata = $item->getMetadata();
|
||||
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
|
||||
{
|
||||
$item = $this->getItem($key);
|
||||
$metadata = $item->getMetadata();
|
||||
|
||||
// ArrayAdapter works in memory, we don't care about stampede protection
|
||||
if (\INF === $beta || !$item->isHit()) {
|
||||
$save = true;
|
||||
$item->set($callback($item, $save));
|
||||
if ($save) {
|
||||
$this->save($item);
|
||||
}
|
||||
}
|
||||
// ArrayAdapter works in memory, we don't care about stampede protection
|
||||
if (\INF === $beta || !$item->isHit()) {
|
||||
$save = true;
|
||||
$item->set($callback($item, $save));
|
||||
if ($save) {
|
||||
$this->save($item);
|
||||
}
|
||||
}
|
||||
|
||||
return $item->get();
|
||||
}
|
||||
return $item->get();
|
||||
}
|
||||
|
||||
public function delete(string $key): bool
|
||||
{
|
||||
return $this->deleteItem($key);
|
||||
}
|
||||
public function delete(string $key): bool
|
||||
{
|
||||
return $this->deleteItem($key);
|
||||
}
|
||||
|
||||
public function hasItem(mixed $key): bool
|
||||
{
|
||||
if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
|
||||
if ($this->maxItems) {
|
||||
// Move the item last in the storage
|
||||
$value = $this->values[$key];
|
||||
unset($this->values[$key]);
|
||||
$this->values[$key] = $value;
|
||||
}
|
||||
public function hasItem(mixed $key): bool
|
||||
{
|
||||
if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
|
||||
if ($this->maxItems) {
|
||||
// Move the item last in the storage
|
||||
$value = $this->values[$key];
|
||||
unset($this->values[$key]);
|
||||
$this->values[$key] = $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
\assert('' !== CacheItem::validateKey($key));
|
||||
return true;
|
||||
}
|
||||
\assert('' !== CacheItem::validateKey($key));
|
||||
|
||||
return isset($this->expiries[$key]) && !$this->deleteItem($key);
|
||||
}
|
||||
return isset($this->expiries[$key]) && !$this->deleteItem($key);
|
||||
}
|
||||
|
||||
public function getItem(mixed $key): CacheItem
|
||||
{
|
||||
if (!$isHit = $this->hasItem($key)) {
|
||||
$value = null;
|
||||
public function getItem(mixed $key): CacheItem
|
||||
{
|
||||
if (!$isHit = $this->hasItem($key)) {
|
||||
$value = null;
|
||||
|
||||
if (!$this->maxItems) {
|
||||
// Track misses in non-LRU mode only
|
||||
$this->values[$key] = null;
|
||||
}
|
||||
} else {
|
||||
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
|
||||
}
|
||||
if (!$this->maxItems) {
|
||||
// Track misses in non-LRU mode only
|
||||
$this->values[$key] = null;
|
||||
}
|
||||
} else {
|
||||
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
|
||||
}
|
||||
|
||||
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
|
||||
}
|
||||
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
|
||||
}
|
||||
|
||||
public function getItems(array $keys = []): iterable
|
||||
{
|
||||
\assert(self::validateKeys($keys));
|
||||
public function getItems(array $keys = []): iterable
|
||||
{
|
||||
\assert(self::validateKeys($keys));
|
||||
|
||||
return $this->generateItems($keys, microtime(true), self::$createCacheItem);
|
||||
}
|
||||
return $this->generateItems($keys, microtime(true), self::$createCacheItem);
|
||||
}
|
||||
|
||||
public function deleteItem(mixed $key): bool
|
||||
{
|
||||
\assert('' !== CacheItem::validateKey($key));
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
|
||||
public function deleteItem(mixed $key): bool
|
||||
{
|
||||
\assert('' !== CacheItem::validateKey($key));
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteItems(array $keys): bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
$this->deleteItem($key);
|
||||
}
|
||||
public function deleteItems(array $keys): bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
$this->deleteItem($key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function save(CacheItemInterface $item): bool
|
||||
{
|
||||
if (!$item instanceof CacheItem) {
|
||||
return false;
|
||||
}
|
||||
$item = (array) $item;
|
||||
$key = $item["\0*\0key"];
|
||||
$value = $item["\0*\0value"];
|
||||
$expiry = $item["\0*\0expiry"];
|
||||
public function save(CacheItemInterface $item): bool
|
||||
{
|
||||
if (!$item instanceof CacheItem) {
|
||||
return false;
|
||||
}
|
||||
$item = (array) $item;
|
||||
$key = $item["\0*\0key"];
|
||||
$value = $item["\0*\0value"];
|
||||
$expiry = $item["\0*\0expiry"];
|
||||
|
||||
$now = microtime(true);
|
||||
$now = microtime(true);
|
||||
|
||||
if (null !== $expiry) {
|
||||
if (!$expiry) {
|
||||
$expiry = \PHP_INT_MAX;
|
||||
} elseif ($expiry <= $now) {
|
||||
$this->deleteItem($key);
|
||||
if (null !== $expiry) {
|
||||
if (!$expiry) {
|
||||
$expiry = \PHP_INT_MAX;
|
||||
} elseif ($expiry <= $now) {
|
||||
$this->deleteItem($key);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
|
||||
return false;
|
||||
}
|
||||
if (null === $expiry && 0 < $this->defaultLifetime) {
|
||||
$expiry = $this->defaultLifetime;
|
||||
$expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
|
||||
} elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
|
||||
$expiry = $now + $this->maxLifetime;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
|
||||
return false;
|
||||
}
|
||||
if (null === $expiry && 0 < $this->defaultLifetime) {
|
||||
$expiry = $this->defaultLifetime;
|
||||
$expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
|
||||
} elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
|
||||
$expiry = $now + $this->maxLifetime;
|
||||
}
|
||||
|
||||
if ($this->maxItems) {
|
||||
unset($this->values[$key], $this->tags[$key]);
|
||||
if ($this->maxItems) {
|
||||
unset($this->values[$key], $this->tags[$key]);
|
||||
|
||||
// Iterate items and vacuum expired ones while we are at it
|
||||
foreach ($this->values as $k => $v) {
|
||||
if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
|
||||
break;
|
||||
}
|
||||
// Iterate items and vacuum expired ones while we are at it
|
||||
foreach ($this->values as $k => $v) {
|
||||
if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
|
||||
break;
|
||||
}
|
||||
|
||||
unset($this->values[$k], $this->tags[$k], $this->expiries[$k]);
|
||||
}
|
||||
}
|
||||
unset($this->values[$k], $this->tags[$k], $this->expiries[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->values[$key] = $value;
|
||||
$this->expiries[$key] = $expiry ?? \PHP_INT_MAX;
|
||||
$this->values[$key] = $value;
|
||||
$this->expiries[$key] = $expiry ?? \PHP_INT_MAX;
|
||||
|
||||
if (null === $this->tags[$key] = $item["\0*\0newMetadata"][CacheItem::METADATA_TAGS] ?? null) {
|
||||
unset($this->tags[$key]);
|
||||
}
|
||||
if (null === $this->tags[$key] = $item["\0*\0newMetadata"][CacheItem::METADATA_TAGS] ?? null) {
|
||||
unset($this->tags[$key]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function saveDeferred(CacheItemInterface $item): bool
|
||||
{
|
||||
return $this->save($item);
|
||||
}
|
||||
public function saveDeferred(CacheItemInterface $item): bool
|
||||
{
|
||||
return $this->save($item);
|
||||
}
|
||||
|
||||
public function commit(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public function commit(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(string $prefix = ''): bool
|
||||
{
|
||||
if ('' !== $prefix) {
|
||||
$now = microtime(true);
|
||||
public function clear(string $prefix = ''): bool
|
||||
{
|
||||
if ('' !== $prefix) {
|
||||
$now = microtime(true);
|
||||
|
||||
foreach ($this->values as $key => $value) {
|
||||
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || str_starts_with($key, $prefix)) {
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
|
||||
}
|
||||
}
|
||||
foreach ($this->values as $key => $value) {
|
||||
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || str_starts_with($key, $prefix)) {
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->values) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ($this->values) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->values = $this->tags = $this->expiries = [];
|
||||
$this->values = $this->tags = $this->expiries = [];
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cached values, with cache miss as null.
|
||||
*/
|
||||
public function getValues(): array
|
||||
{
|
||||
if (!$this->storeSerialized) {
|
||||
return $this->values;
|
||||
}
|
||||
/**
|
||||
* Returns all cached values, with cache miss as null.
|
||||
*/
|
||||
public function getValues(): array
|
||||
{
|
||||
if (!$this->storeSerialized) {
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
$values = $this->values;
|
||||
foreach ($values as $k => $v) {
|
||||
if (null === $v || 'N;' === $v) {
|
||||
continue;
|
||||
}
|
||||
if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) {
|
||||
$values[$k] = serialize($v);
|
||||
}
|
||||
}
|
||||
$values = $this->values;
|
||||
foreach ($values as $k => $v) {
|
||||
if (null === $v || 'N;' === $v) {
|
||||
continue;
|
||||
}
|
||||
if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) {
|
||||
$values[$k] = serialize($v);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->clear();
|
||||
}
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
private function generateItems(array $keys, float $now, \Closure $f): \Generator
|
||||
{
|
||||
foreach ($keys as $i => $key) {
|
||||
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
|
||||
$value = null;
|
||||
private function generateItems(array $keys, float $now, \Closure $f): \Generator
|
||||
{
|
||||
foreach ($keys as $i => $key) {
|
||||
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
|
||||
$value = null;
|
||||
|
||||
if (!$this->maxItems) {
|
||||
// Track misses in non-LRU mode only
|
||||
$this->values[$key] = null;
|
||||
}
|
||||
} else {
|
||||
if ($this->maxItems) {
|
||||
// Move the item last in the storage
|
||||
$value = $this->values[$key];
|
||||
unset($this->values[$key]);
|
||||
$this->values[$key] = $value;
|
||||
}
|
||||
if (!$this->maxItems) {
|
||||
// Track misses in non-LRU mode only
|
||||
$this->values[$key] = null;
|
||||
}
|
||||
} else {
|
||||
if ($this->maxItems) {
|
||||
// Move the item last in the storage
|
||||
$value = $this->values[$key];
|
||||
unset($this->values[$key]);
|
||||
$this->values[$key] = $value;
|
||||
}
|
||||
|
||||
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
|
||||
}
|
||||
unset($keys[$i]);
|
||||
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
|
||||
}
|
||||
unset($keys[$i]);
|
||||
|
||||
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
|
||||
}
|
||||
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
yield $key => $f($key, null, false);
|
||||
}
|
||||
}
|
||||
foreach ($keys as $key) {
|
||||
yield $key => $f($key, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private function freeze($value, string $key): string|int|float|bool|array|\UnitEnum|null
|
||||
{
|
||||
if (null === $value) {
|
||||
return 'N;';
|
||||
}
|
||||
if (\is_string($value)) {
|
||||
// Serialize strings if they could be confused with serialized objects or arrays
|
||||
if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
|
||||
return serialize($value);
|
||||
}
|
||||
} elseif (!\is_scalar($value)) {
|
||||
try {
|
||||
$serialized = serialize($value);
|
||||
} catch (\Exception $e) {
|
||||
if (!isset($this->expiries[$key])) {
|
||||
unset($this->values[$key]);
|
||||
}
|
||||
$type = get_debug_type($value);
|
||||
$message = \sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
|
||||
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
|
||||
private function freeze($value, string $key): string|int|float|bool|array|\UnitEnum|null
|
||||
{
|
||||
if (null === $value) {
|
||||
return 'N;';
|
||||
}
|
||||
if (\is_string($value)) {
|
||||
// SerializeToDOMNode strings if they could be confused with serialized objects or arrays
|
||||
if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
|
||||
return serialize($value);
|
||||
}
|
||||
} elseif (!\is_scalar($value)) {
|
||||
try {
|
||||
$serialized = serialize($value);
|
||||
} catch (\Exception $e) {
|
||||
if (!isset($this->expiries[$key])) {
|
||||
unset($this->values[$key]);
|
||||
}
|
||||
$type = get_debug_type($value);
|
||||
$message = \sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
|
||||
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
|
||||
|
||||
return null;
|
||||
}
|
||||
// Keep value serialized if it contains any objects or any internal references
|
||||
if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) {
|
||||
return $serialized;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Keep value serialized if it contains any objects or any internal references
|
||||
if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) {
|
||||
return $serialized;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function unfreeze(string $key, bool &$isHit): mixed
|
||||
{
|
||||
if ('N;' === $value = $this->values[$key]) {
|
||||
return null;
|
||||
}
|
||||
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
|
||||
try {
|
||||
$value = unserialize($value);
|
||||
} catch (\Exception $e) {
|
||||
CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
|
||||
$value = false;
|
||||
}
|
||||
if (false === $value) {
|
||||
$value = null;
|
||||
$isHit = false;
|
||||
private function unfreeze(string $key, bool &$isHit): mixed
|
||||
{
|
||||
if ('N;' === $value = $this->values[$key]) {
|
||||
return null;
|
||||
}
|
||||
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
|
||||
try {
|
||||
$value = unserialize($value);
|
||||
} catch (\Exception $e) {
|
||||
CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
|
||||
$value = false;
|
||||
}
|
||||
if (false === $value) {
|
||||
$value = null;
|
||||
$isHit = false;
|
||||
|
||||
if (!$this->maxItems) {
|
||||
$this->values[$key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$this->maxItems) {
|
||||
$this->values[$key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function validateKeys(array $keys): bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (!\is_string($key) || !isset($this->expiries[$key])) {
|
||||
CacheItem::validateKey($key);
|
||||
}
|
||||
}
|
||||
private function validateKeys(array $keys): bool
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (!\is_string($key) || !isset($this->expiries[$key])) {
|
||||
CacheItem::validateKey($key);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17291,7 +17291,7 @@ class TCPDF {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize data to be used with TCPDF tag in HTML code.
|
||||
* SerializeToDOMNode data to be used with TCPDF tag in HTML code.
|
||||
* @param string $method TCPDF method name
|
||||
* @param array $params Method parameters
|
||||
* @return string Serialized data
|
||||
|
||||
16
node_modules/.package-lock.json
generated
vendored
16
node_modules/.package-lock.json
generated
vendored
@@ -186,6 +186,22 @@
|
||||
"delegate": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gridstack": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-12.4.2.tgz",
|
||||
"integrity": "sha512-aXbJrQpi3LwpYXYOr4UriPM5uc/dPcjK01SdOE5PDpx2vi8tnLhU7yBg/1i4T59UhNkG/RBfabdFUObuN+gMnw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://www.paypal.me/alaind831"
|
||||
},
|
||||
{
|
||||
"type": "venmo",
|
||||
"url": "https://www.venmo.com/adumesny"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
|
||||
21
node_modules/gridstack/LICENSE
generated
vendored
Normal file
21
node_modules/gridstack/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2025 Alain Dumesny. v0.4.0 and older (c) 2014-2018 Pavel Reznikov, Dylan Weiss
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
565
node_modules/gridstack/README.md
generated
vendored
Normal file
565
node_modules/gridstack/README.md
generated
vendored
Normal file
@@ -0,0 +1,565 @@
|
||||
# gridstack.js
|
||||
|
||||
[](https://www.npmjs.com/package/gridstack)
|
||||
[](https://coveralls.io/github/gridstack/gridstack.js?branch=develop)
|
||||
[](https://www.npmjs.com/package/gridstack)
|
||||
|
||||
Mobile-friendly modern Typescript library for dashboard layout and creation. Making a drag-and-drop, multi-column responsive dashboard has never been easier. Has multiple bindings and works great with [Angular](https://angular.io/) (included), [React](https://reactjs.org/), [Vue](https://vuejs.org/), [Knockout.js](http://knockoutjs.com), [Ember](https://www.emberjs.com/) and others (see [frameworks](#specific-frameworks) section).
|
||||
|
||||
Inspired by no-longer maintained gridster, built with love.
|
||||
|
||||
Check http://gridstackjs.com and [these demos](http://gridstackjs.com/demo/).
|
||||
|
||||
If you find this lib useful, please donate [PayPal](https://www.paypal.me/alaind831) (use **“send to a friend”** to avoid 3% fee) or [Venmo](https://www.venmo.com/adumesny) (adumesny) and help support it!
|
||||
|
||||
[](https://www.paypal.me/alaind831)
|
||||
[](https://www.venmo.com/adumesny)
|
||||
|
||||
Join us on Slack: [https://gridstackjs.slack.com](https://join.slack.com/t/gridstackjs/shared_invite/zt-3978nsff6-HDNE_N45DydP36NBSV9JFQ)
|
||||
|
||||
<!-- [](https://gridstackjs.slack.com) -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
|
||||
|
||||
- [Demo and API Documentation](#demo-and-api-documentation)
|
||||
- [Usage](#usage)
|
||||
- [Install](#install)
|
||||
- [Include](#include)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Requirements](#requirements)
|
||||
- [Specific frameworks](#specific-frameworks)
|
||||
- [Extend Library](#extend-library)
|
||||
- [Extend Engine](#extend-engine)
|
||||
- [Change grid columns](#change-grid-columns)
|
||||
- [Custom columns CSS (OLD, not needed with v12+)](#custom-columns-css-old-not-needed-with-v12)
|
||||
- [Override resizable/draggable options](#override-resizabledraggable-options)
|
||||
- [Touch devices support](#touch-devices-support)
|
||||
- [Migrating](#migrating)
|
||||
- [Migrating to v0.6](#migrating-to-v06)
|
||||
- [Migrating to v1](#migrating-to-v1)
|
||||
- [Migrating to v2](#migrating-to-v2)
|
||||
- [Migrating to v3](#migrating-to-v3)
|
||||
- [Migrating to v4](#migrating-to-v4)
|
||||
- [Migrating to v5](#migrating-to-v5)
|
||||
- [Migrating to v6](#migrating-to-v6)
|
||||
- [Migrating to v7](#migrating-to-v7)
|
||||
- [Migrating to v8](#migrating-to-v8)
|
||||
- [Migrating to v9](#migrating-to-v9)
|
||||
- [Migrating to v10](#migrating-to-v10)
|
||||
- [Migrating to v11](#migrating-to-v11)
|
||||
- [Migrating to v12](#migrating-to-v12)
|
||||
- [jQuery Application](#jquery-application)
|
||||
- [Changes](#changes)
|
||||
- [Usage Trend](#usage-trend)
|
||||
- [The Team](#the-team)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
# Demo and API Documentation
|
||||
|
||||
Please visit http://gridstackjs.com and [these demos](http://gridstackjs.com/demo/), and complete [API documentation](https://gridstack.github.io/gridstack.js/doc/html/) ([markdown](https://github.com/gridstack/gridstack.js/tree/master/doc/API.md))
|
||||
|
||||
# Usage
|
||||
|
||||
## Install
|
||||
[](https://www.npmjs.com/package/gridstack)
|
||||
|
||||
```js
|
||||
yarn add gridstack
|
||||
// or
|
||||
npm install --save gridstack
|
||||
```
|
||||
|
||||
## Include
|
||||
|
||||
ES6 or Typescript
|
||||
|
||||
```js
|
||||
import 'gridstack/dist/gridstack.min.css';
|
||||
import { GridStack } from 'gridstack';
|
||||
```
|
||||
|
||||
Alternatively (single combined file, notice the -all.js) in html
|
||||
|
||||
```html
|
||||
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
|
||||
<script src="node_modules/gridstack/dist/gridstack-all.js"></script>
|
||||
```
|
||||
|
||||
**Note**: IE support was dropped in v2, but restored in v4.4 by an external contributor (I have no interest in testing+supporting obsolete browser so this likely will break again in the future) and DROPPED again in v12 (css variable needed).
|
||||
You can use the es5 files and polyfill (larger) for older browser instead. For example:
|
||||
```html
|
||||
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
|
||||
<script src="node_modules/gridstack/dist/es5/gridstack-poly.js"></script>
|
||||
<script src="node_modules/gridstack/dist/es5/gridstack-all.js"></script>
|
||||
```
|
||||
|
||||
|
||||
## Basic usage
|
||||
|
||||
creating items dynamically...
|
||||
|
||||
```js
|
||||
// ...in your HTML
|
||||
<div class="grid-stack"></div>
|
||||
|
||||
// ...in your script
|
||||
var grid = GridStack.init();
|
||||
grid.addWidget({w: 2, content: 'item 1'});
|
||||
```
|
||||
|
||||
... or creating from list
|
||||
|
||||
```js
|
||||
// using serialize data instead of .addWidget()
|
||||
const serializedData = [
|
||||
{x: 0, y: 0, w: 2, h: 2},
|
||||
{x: 2, y: 3, w: 3, content: 'item 2'},
|
||||
{x: 1, y: 3}
|
||||
];
|
||||
|
||||
grid.load(serializedData);
|
||||
```
|
||||
|
||||
... or DOM created items
|
||||
|
||||
```js
|
||||
// ...in your HTML
|
||||
<div class="grid-stack">
|
||||
<div class="grid-stack-item">
|
||||
<div class="grid-stack-item-content">Item 1</div>
|
||||
</div>
|
||||
<div class="grid-stack-item" gs-w="2">
|
||||
<div class="grid-stack-item-content">Item 2 wider</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// ...in your script
|
||||
GridStack.init();
|
||||
```
|
||||
|
||||
...or see list of all [API and options](https://github.com/gridstack/gridstack.js/tree/master/doc) available.
|
||||
|
||||
see [stackblitz sample](https://stackblitz.com/edit/gridstack-demo) as running example too.
|
||||
|
||||
## Requirements
|
||||
|
||||
GridStack no longer requires external dependencies as of v1 (lodash was removed in v0.5 and jquery API in v1). v3 is a complete HTML5 re-write removing need for jquery. v6 is native mouse and touch event for mobile support, and no longer have jquery-ui version. All you need to include now is `gridstack-all.js` and `gridstack.min.css` (layouts are done using CSS column based %).
|
||||
|
||||
## Specific frameworks
|
||||
|
||||
search for ['gridstack' under NPM](https://www.npmjs.com/search?q=gridstack&ranking=popularity) for latest, more to come...
|
||||
|
||||
- **Angular**: we ship out of the box an Angular wrapper - see <a href="https://github.com/gridstack/gridstack.js/tree/master/angular" target="_blank">Angular Component</a>.
|
||||
- **Angular9**: [lb-gridstack](https://github.com/pfms84/lb-gridstack) Note: very old v0.3 gridstack instance so recommend for **concept ONLY if you wish to use directive instead**. Code has **not been vented** at as I use components.
|
||||
- **AngularJS**: [gridstack-angular](https://github.com/kdietrich/gridstack-angular)
|
||||
- **Ember**: [ember-gridstack](https://github.com/yahoo/ember-gridstack)
|
||||
- **knockout**: see [demo](https://gridstackjs.com/demo/knockout.html) using component, but check [custom bindings ticket](https://github.com/gridstack/gridstack.js/issues/465) which is likely better approach.
|
||||
- **Rails**: [gridstack-js-rails](https://github.com/randoum/gridstack-js-rails)
|
||||
- **React**: work in progress to have wrapper code: see <a href="https://github.com/gridstack/gridstack.js/tree/master/react" target="_blank">React Component</a>. But also see [demo](https://gridstackjs.com/demo/react.html) with [src](https://github.com/gridstack/gridstack.js/tree/master/demo/react.html), or [react-gridstack-example](https://github.com/Inder2108/react-gridstack-example/tree/master/src/App.js), or read on what [hooks to use](https://github.com/gridstack/gridstack.js/issues/735#issuecomment-329888796)
|
||||
- **Vue**: see [demo](https://gridstackjs.com/demo/vue3js.html) with [v3 src](https://github.com/gridstack/gridstack.js/tree/master/demo/vue3js.html) or [v2 src](https://github.com/gridstack/gridstack.js/tree/master/demo/vue2js.html)
|
||||
- **Aurelia**: [aurelia-gridstack](https://github.com/aurelia-ui-toolkits/aurelia-gridstack), see [demo](https://aurelia-ui-toolkits.github.io/aurelia-gridstack/)
|
||||
|
||||
## Extend Library
|
||||
|
||||
You can easily extend or patch gridstack with code like this:
|
||||
|
||||
```js
|
||||
// extend gridstack with our own custom method
|
||||
GridStack.prototype.printCount = function() {
|
||||
console.log('grid has ' + this.engine.nodes.length + ' items');
|
||||
};
|
||||
|
||||
let grid = GridStack.init();
|
||||
|
||||
// you can now call
|
||||
grid.printCount();
|
||||
```
|
||||
|
||||
## Extend Engine
|
||||
|
||||
You can now (5.1+) easily create your own layout engine to further customize your usage. Here is a typescript example
|
||||
|
||||
```ts
|
||||
import { GridStack, GridStackEngine, GridStackNode, GridStackMoveOpts } from 'gridstack';
|
||||
|
||||
class CustomEngine extends GridStackEngine {
|
||||
|
||||
/** refined this to move the node to the given new location */
|
||||
public override moveNode(node: GridStackNode, o: GridStackMoveOpts): boolean {
|
||||
// keep the same original X and Width and let base do it all...
|
||||
o.x = node.x;
|
||||
o.w = node.w;
|
||||
return super.moveNode(node, o);
|
||||
}
|
||||
}
|
||||
|
||||
GridStack.registerEngine(CustomEngine); // globally set our custom class
|
||||
```
|
||||
|
||||
## Change grid columns
|
||||
|
||||
GridStack makes it very easy if you need [1-12] columns out of the box (default is 12), but you always need **2 things** if you need to customize this:
|
||||
|
||||
1) Change the `column` grid option when creating a grid to your number N
|
||||
```js
|
||||
GridStack.init( {column: N} );
|
||||
```
|
||||
|
||||
NOTE: step 2 is OLD and not needed with v12+ which uses CSS variables instead of classes
|
||||
|
||||
2) also include `gridstack-extra.css` if **N < 12** (else custom CSS - see next). Without these, things will not render/work correctly.
|
||||
```html
|
||||
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
|
||||
<link href="node_modules/gridstack/dist/gridstack-extra.min.css" rel="stylesheet"/>
|
||||
|
||||
<div class="grid-stack">...</div>
|
||||
```
|
||||
|
||||
Note: class `.grid-stack-N` will automatically be added and we include `gridstack-extra.min.css` which defines CSS for grids with custom [2-11] columns. Anything more and you'll need to generate the SASS/CSS yourself (see next).
|
||||
|
||||
See example: [2 grids demo](http://gridstack.github.io/gridstack.js/demo/two.html) with 6 columns
|
||||
|
||||
## Custom columns CSS (OLD, not needed with v12+)
|
||||
|
||||
NOTE: step is OLD and not needed with v12+ which uses CSS variables instead of classes
|
||||
|
||||
If you need > 12 columns or want to generate the CSS manually you will need to generate CSS rules for `.grid-stack-item[gs-w="X"]` and `.grid-stack-item[gs-x="X"]`.
|
||||
|
||||
For instance for 4-column grid you need CSS to be:
|
||||
|
||||
```css
|
||||
.gs-4 > .grid-stack-item[gs-x="1"] { left: 25% }
|
||||
.gs-4 > .grid-stack-item[gs-x="2"] { left: 50% }
|
||||
.gs-4 > .grid-stack-item[gs-x="3"] { left: 75% }
|
||||
|
||||
.gs-4 > .grid-stack-item { width: 25% }
|
||||
.gs-4 > .grid-stack-item[gs-w="2"] { width: 50% }
|
||||
.gs-4 > .grid-stack-item[gs-w="3"] { width: 75% }
|
||||
.gs-4 > .grid-stack-item[gs-w="4"] { width: 100% }
|
||||
```
|
||||
|
||||
Better yet, here is a SCSS code snippet, you can use sites like [sassmeister.com](https://www.sassmeister.com/) to generate the CSS for you instead:
|
||||
|
||||
```scss
|
||||
$columns: 20;
|
||||
@function fixed($float) {
|
||||
@return round($float * 1000) / 1000; // total 2+3 digits being %
|
||||
}
|
||||
.gs-#{$columns} > .grid-stack-item {
|
||||
|
||||
width: fixed(100% / $columns);
|
||||
|
||||
@for $i from 1 through $columns - 1 {
|
||||
&[gs-x='#{$i}'] { left: fixed((100% / $columns) * $i); }
|
||||
&[gs-w='#{$i+1}'] { width: fixed((100% / $columns) * ($i+1)); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
you can also use the SCSS [src/gridstack-extra.scss](https://github.com/gridstack/gridstack.js/tree/master/src/gridstack-extra.scss) included in NPM package and modify to add more columns.
|
||||
|
||||
Sample gulp command for 30 columns:
|
||||
```js
|
||||
gulp.src('node_modules/gridstack/dist/src/gridstack-extra.scss')
|
||||
.pipe(replace('$start: 2 !default;','$start: 30;'))
|
||||
.pipe(replace('$end: 11 !default;','$end: 30;'))
|
||||
.pipe(sass({outputStyle: 'compressed'}))
|
||||
.pipe(rename({extname: '.min.css'}))
|
||||
.pipe(gulp.dest('dist/css'))
|
||||
```
|
||||
|
||||
## Override resizable/draggable options
|
||||
|
||||
You can override default `resizable`/`draggable` options. For instance to enable other then bottom right resizing handle
|
||||
you can init gridstack like:
|
||||
|
||||
```js
|
||||
GridStack.init({
|
||||
resizable: {
|
||||
handles: 'e,se,s,sw,w'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Touch devices support
|
||||
|
||||
gridstack v6+ now support mobile out of the box, with the addition of native touch event (along with mouse event) for drag&drop and resize.
|
||||
Older versions (3.2+) required the jq version with added touch punch, but doesn't work well with nested grids.
|
||||
|
||||
This option is now the default:
|
||||
|
||||
```js
|
||||
let options = {
|
||||
alwaysShowResizeHandle: 'mobile' // true if we're on mobile devices
|
||||
};
|
||||
GridStack.init(options);
|
||||
```
|
||||
|
||||
See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html).
|
||||
|
||||
# Migrating
|
||||
## Migrating to v0.6
|
||||
|
||||
starting in 0.6.x `change` event are no longer sent (for pretty much most nodes!) when an item is just added/deleted unless it also changes other nodes (was incorrect and causing inefficiencies). You may need to track `added|removed` [events](https://github.com/gridstack/gridstack.js/tree/master/doc#events) if you didn't and relied on the old broken behavior.
|
||||
|
||||
## Migrating to v1
|
||||
|
||||
v1.0.0 removed Jquery from the API and external dependencies, which will require some code changes. Here is a list of the changes:
|
||||
|
||||
0. see previous step if not on v0.6 already
|
||||
|
||||
1. your code only needs to `import GridStack from 'gridstack'` or include `gridstack.all.js` and `gristack.css` (don't include other JS) and is recommended you do that as internal dependencies will change over time. If you are jquery based, see [jquery app](#jquery-application) section.
|
||||
|
||||
2. code change:
|
||||
|
||||
**OLD** initializing code + adding a widget + adding an event:
|
||||
```js
|
||||
// initialization returned Jquery element, requiring second call to get GridStack var
|
||||
var grid = $('.grid-stack').gridstack(opts?).data('gridstack');
|
||||
|
||||
// returned Jquery element
|
||||
grid.addWidget($('<div><div class="grid-stack-item-content"> test </div></div>'), undefined, undefined, 2, undefined, true);
|
||||
|
||||
// jquery event handler
|
||||
$('.grid-stack').on('added', function(e, items) {/* items contains info */});
|
||||
|
||||
// grid access after init
|
||||
var grid = $('.grid-stack').data('gridstack');
|
||||
```
|
||||
**NEW**
|
||||
```js
|
||||
// element identifier defaults to '.grid-stack', returns the grid
|
||||
// Note: for Typescript use window.GridStack.init() until next native 2.x TS version
|
||||
var grid = GridStack.init(opts?, element?);
|
||||
|
||||
// returns DOM element
|
||||
grid.addWidget('<div><div class="grid-stack-item-content"> test </div></div>', {width: 2});
|
||||
// Note: in 3.x it's ever simpler
|
||||
// grid.addWidget({w:2, content: 'test'})
|
||||
|
||||
// event handler
|
||||
grid.on('added', function(e, items) {/* items contains info */});
|
||||
|
||||
// grid access after init
|
||||
var grid = el.gridstack; // where el = document.querySelector('.grid-stack') or other ways...
|
||||
```
|
||||
Other rename changes
|
||||
|
||||
```js
|
||||
`GridStackUI` --> `GridStack`
|
||||
`GridStackUI.GridStackEngine` --> `GridStack.Engine`
|
||||
`grid.container` (jquery grid wrapper) --> `grid.el` // (grid DOM element)
|
||||
`grid.grid` (GridStackEngine) --> `grid.engine`
|
||||
`grid.setColumn(N)` --> `grid.column(N)` and `grid.column()` // to get value, old API still supported though
|
||||
```
|
||||
|
||||
Recommend looking at the [many samples](./demo) for more code examples.
|
||||
|
||||
## Migrating to v2
|
||||
|
||||
make sure to read v1 migration first!
|
||||
|
||||
v2 is a Typescript rewrite of 1.x, removing all jquery events, using classes and overall code cleanup to support ES6 modules. Your code might need to change from 1.x
|
||||
|
||||
1. In general methods that used no args (getter) vs setter can't be used in TS when the arguments differ (set/get are also not function calls so API would have changed). Instead we decided to have <b>all set methods return</b> `GridStack` to they can be chain-able (ex: `grid.float(true).cellHeight(10).column(6)`). Also legacy methods that used to take many parameters will now take a single object (typically `GridStackOptions` or `GridStackWidget`).
|
||||
|
||||
```js
|
||||
`addWidget(el, x, y, width, height)` --> `addWidget(el, {with: 2})`
|
||||
// Note: in 2.1.x you can now just do addWidget({with: 2, content: "text"})
|
||||
`float()` --> `getFloat()` // to get value
|
||||
`cellHeight()` --> `getCellHeight()` // to get value
|
||||
`verticalMargin` --> `margin` // grid options and API that applies to all 4 sides.
|
||||
`verticalMargin()` --> `getMargin()` // to get value
|
||||
```
|
||||
|
||||
2. event signatures are generic and not jquery-ui dependent anymore. `gsresizestop` has been removed as `resizestop|dragstop` are now called **after** the DOM attributes have been updated.
|
||||
|
||||
3. `oneColumnMode` would trigger when `window.width` < 768px by default. We now check for grid width instead (more correct and supports nesting). You might need to adjust grid `oneColumnSize` or `disableOneColumnMode`.
|
||||
|
||||
**Note:** 2.x no longer support legacy IE11 and older due to using more compact ES6 output and typecsript native code. You will need to stay at 1.x
|
||||
|
||||
## Migrating to v3
|
||||
|
||||
make sure to read v2 migration first!
|
||||
|
||||
v3 has a new HTML5 drag&drop plugging (63k total, all native code), while still allowing you to use the legacy jquery-ui version instead (188k), or a new static grid version (34k, no user drag&drop but full API support). You will need to decide which version to use as `gridstack.all.js` no longer exist (same is now `gridstack-jq.js`) - see [include info](#include).
|
||||
|
||||
**NOTE**: HTML5 version is almost on parity with the old jquery-ui based drag&drop. the `containment` (prevent a child from being dragged outside it's parent) and `revert` (not clear what it is for yet) are not yet implemented in initial release of v3.0.0.<br>
|
||||
Also mobile devices don't support h5 `drag` events (will need to handle `touch`) whereas v3.2 jq version now now supports out of the box (see [v3.2 release](https://github.com/gridstack/gridstack.js/releases/tag/v3.2.0))
|
||||
|
||||
Breaking changes:
|
||||
|
||||
1. include (as mentioned) need to change
|
||||
|
||||
2. `GridStack.update(el, opt)` now takes single `GridStackWidget` options instead of only supporting (x,y,w,h) BUT legacy call in JS will continue working the same for now. That method is a complete re-write and does the right constrain and updates now for all the available params.
|
||||
|
||||
3. `locked()`, `move()`, `resize()`, `minWidth()`, `minHeight()`, `maxWidth()`, `maxHeight()` methods are hidden from Typescript (JS can still call for now) as they are just 1 liner wrapper around `update(el, opt)` anyway and will go away soon. (ex: `move(el, x, y)` => `update(el, {x, y})`)
|
||||
|
||||
4. item attribute like `data-gs-min-width` is now `gs-min-w`. We removed 'data-' from all attributes, and shorten 'width|height' to just 'w|h' to require less typing and more efficient (2k saved in .js alone!).
|
||||
|
||||
5. `GridStackWidget` used in most API `width|height|minWidth|minHeight|maxWidth|maxHeight` are now shorter `w|h|minW|minH|maxW|maxH` as well
|
||||
|
||||
## Migrating to v4
|
||||
|
||||
make sure to read v3 migration first!
|
||||
|
||||
v4 is a complete re-write of the collision and drag in/out heuristics to fix some very long standing request & bugs. It also greatly improved usability. Read the release notes for more detail.
|
||||
|
||||
**Unlikely** Breaking Changes (internal usage):
|
||||
|
||||
1. `removeTimeout` was removed (feedback over trash will be immediate - actual removal still on mouse up)
|
||||
|
||||
2. the following `GridStackEngine` methods changed (used internally, doesn't affect `GridStack` public API)
|
||||
|
||||
```js
|
||||
// moved to 3 methods with new option params to support new code and pixel coverage check
|
||||
`collision()` -> `collide(), collideAll(), collideCoverage()`
|
||||
`moveNodeCheck(node, x, y, w, h)` -> `moveNodeCheck(node, opt: GridStackMoveOpts)`
|
||||
`isNodeChangedPosition(node, x, y, w, h)` -> `changedPosConstrain(node, opt: GridStackMoveOpts)`
|
||||
`moveNode(node, x, y, w, h, noPack)` -> `moveNode(node, opt: GridStackMoveOpts)`
|
||||
```
|
||||
|
||||
3. removed old obsolete (v0.6-v1 methods/attrs) `getGridHeight()`, `verticalMargin`, `data-gs-current-height`,
|
||||
`locked()`, `maxWidth()`, `minWidth()`, `maxHeight()`, `minHeight()`, `move()`, `resize()`
|
||||
|
||||
|
||||
## Migrating to v5
|
||||
|
||||
make sure to read v4 migration first!
|
||||
|
||||
v5 does not have any breaking changes from v4, but a focus on nested grids in h5 mode:
|
||||
You can now drag in/out of parent into nested child, with new API parameters values. See the release notes.
|
||||
|
||||
## Migrating to v6
|
||||
|
||||
the API did not really change from v5, but a complete re-write of Drag&Drop to use native `mouseevent` (instead of HTML draggable=true which is buggy on Mac Safari, and doesn't work on mobile devices) and `touchevent` (mobile), and we no longer have jquery ui option (wasn't working well for nested grids, didn't want to maintain legacy lib).
|
||||
|
||||
The main difference is you only need to include gridstack.js and get D&D (desktop and mobile) out of the box for the same size as h5 version.
|
||||
|
||||
## Migrating to v7
|
||||
|
||||
New addition, no API breakage per say. See release notes about creating sub-grids on the fly.
|
||||
|
||||
## Migrating to v8
|
||||
|
||||
Possible breaking change if you use nested grid JSON format, or original Angular wrapper, or relied on specific CSS paths. Also target is now ES2020 (see release notes).
|
||||
* `GridStackOptions.subGrid` -> `GridStackOptions.subGridOpts` rename. We now have `GridStackWidget.subGridOpts` vs `GridStackNode.subGrid` (was both types which is error prone)
|
||||
* `GridStackOptions.addRemoveCB` -> `GridStack.addRemoveCB` is now global instead of grid option
|
||||
* removed `GridStackOptions.dragInOptions` since `GridStack.setupDragIn()` has it replaced since 4.0
|
||||
* remove `GridStackOptions.minWidth` obsolete since 5.1, use `oneColumnSize` instead
|
||||
* CSS rules removed `.grid-stack` prefix for anything already gs based, 12 column (default) now uses `.gs-12`, extra.css is less than 1/4th it original size!, `gs-min|max_w|h` attribute no longer written (but read)
|
||||
|
||||
## Migrating to v9
|
||||
|
||||
New addition - see release notes about `sizeToContent` feature.
|
||||
Possible break:
|
||||
* `GridStack.onParentResize()` is now called `onResize()` as grid now directly track size change, no longer involving parent per say to tell us anything. Note sure why it was public.
|
||||
|
||||
## Migrating to v10
|
||||
|
||||
we now support much richer responsive behavior with `GridStackOptions.columnOpts` including any breakpoint width:column pairs, or automatic column sizing.
|
||||
|
||||
breaking change:
|
||||
* `disableOneColumnMode`, `oneColumnSize` have been removed (thought we temporary convert if you have them). use `columnOpts: { breakpoints: [{w:768, c:1}] }` for the same behavior.
|
||||
* 1 column mode switch is no longer by default (`columnOpts` is not defined) as too many new users had issues. Instead set it explicitly (see above).
|
||||
* `oneColumnModeDomSort` has been removed. Planning to support per column layouts at some future times. TBD
|
||||
|
||||
## Migrating to v11
|
||||
|
||||
* All instances of `el.innerHTML = 'some content'` have been removed for security reason as it opens up some potential for accidental XSS.
|
||||
|
||||
* Side panel drag&drop complete rewrite.
|
||||
|
||||
* new lazy loading option
|
||||
|
||||
**Breaking change:**
|
||||
|
||||
* V11 add new `GridStack.renderCB` that is called for you to create the widget content (entire GridStackWidget is passed so you can use id or some other field as logic) while GS creates the 2 needed parent divs + classes, unlike `GridStack.addRemoveCB` which doesn't create anything for you. Both can be handy for Angular/React/Vue frameworks.
|
||||
* `addWidget(w: GridStackWidget)` is now the only supported format, no more string content passing. You will need to create content yourself as shown below, OR use `GridStack.createWidgetDivs()` to create parent divs, do the innerHtml, then call `makeWidget(el)` instead.
|
||||
* if your code relies on `GridStackWidget.content` with real HTML (like a few demos) it is up to you to do this:
|
||||
```ts
|
||||
// NOTE: REAL apps would sanitize-html or DOMPurify before blinding setting innerHTML. see #2736
|
||||
GridStack.renderCB = function(el: HTMLElement, w: GridStackNode) {
|
||||
el.innerHTML = w.content;
|
||||
};
|
||||
|
||||
// now you can create widgets like this again
|
||||
let gridWidget = grid.addWidget({x, y, w, h, content: '<div>My html content</div>'});
|
||||
```
|
||||
|
||||
**Potential breaking change:**
|
||||
|
||||
* BIG overall to how sidepanel helper drag&drop is done:
|
||||
1. `clone()` helper is now passed full HTML element dragged, not an event on `grid-stack-item-content` so you can clone or set attr at the top.
|
||||
2. use any class/structure you want for side panel items (see two.html)
|
||||
3. `GridStack.setupDragIn()` now support associating a `GridStackWidget` for each sidepanel that will be used to define what to create on drop!
|
||||
4. if no `GridStackWidget` is defined, the helper will now be inserted as is, and NOT original sidepanel item.
|
||||
5. support DOM gs- attr as well as gridstacknode JSON (see two.html) alternatives.
|
||||
|
||||
## Migrating to v12
|
||||
|
||||
* column and cell height code has been re-writen to use browser CSS variables, and we no longer need a tons of custom CSS classes!
|
||||
this fixes a long standing issue where people forget to include the right CSS for non 12 columns layouts, and a big speedup in many cases (many columns, or small cellHeight values).
|
||||
|
||||
**Potential breaking change:**
|
||||
* `gridstack-extra.min.css` no longer exist, nor is custom column CSS classes needed. API/options hasn't changed.
|
||||
* (v12.1) `ES5` folder content has been removed - was for IE support, which has been dropped.
|
||||
* (v12.1) nested grid events are now sent to the main grid. You might have to adjust your workaround of this missing feature. nested.html demo has been adjusted.
|
||||
|
||||
# jQuery Application
|
||||
|
||||
This is **old and no longer apply to v6+**. You'll need to use v5.1.1 and before
|
||||
|
||||
```js
|
||||
import 'gridstack/dist/gridstack.min.css';
|
||||
import { GridStack } from 'gridstack';
|
||||
import 'gridstack/dist/jq/gridstack-dd-jqueryui';
|
||||
```
|
||||
**Note**: `jquery` & `jquery-ui` are imported by name, so you will have to specify their location in your webpack (or equivalent) config file,
|
||||
which means you can possibly bring your own version
|
||||
```js
|
||||
alias: {
|
||||
'jquery': 'gridstack/dist/jq/jquery.js',
|
||||
'jquery-ui': 'gridstack/dist/jq/jquery-ui.js',
|
||||
'jquery.ui': 'gridstack/dist/jq/jquery-ui.js',
|
||||
'jquery.ui.touch-punch': 'gridstack/dist/jq/jquery.ui.touch-punch.js',
|
||||
},
|
||||
```
|
||||
Alternatively (single combined file) in html
|
||||
|
||||
```html
|
||||
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
|
||||
<!-- HTML5 drag&drop (70k) -->
|
||||
<script src="node_modules/gridstack/dist/gridstack-h5.js"></script>
|
||||
<!-- OR jquery-ui drag&drop (195k) -->
|
||||
<script src="node_modules/gridstack/dist/gridstack-jq.js"></script>
|
||||
<!-- OR static grid (40k) -->
|
||||
<script src="node_modules/gridstack/dist/gridstack-static.js"></script>
|
||||
```
|
||||
|
||||
We have a native HTML5 drag'n'drop through the plugin system (default), but the jquery-ui version can be used instead. It will bundle `jquery` (3.5.1) + `jquery-ui` (1.13.1 minimal drag|drop|resize) + `jquery-ui-touch-punch` (1.0.8 for mobile support) in `gridstack-jq.js`.
|
||||
|
||||
**NOTE: in v4, v3**: we ES6 module import jquery & jquery-ui by name, so you need to specify location of those .js files, which means you might be able to bring your own version as well. See the include instructions.
|
||||
|
||||
**NOTE: in v1.x** IFF you want to use gridstack-jq instead and your app needs to bring your own JQ version, you should **instead** include `gridstack-poly.min.js` (optional IE support) + `gridstack.min.js` + `gridstack.jQueryUI.min.js` after you import your JQ libs. But note that there are issue with jQuery and ES6 import (see [1306](https://github.com/gridstack/gridstack.js/issues/1306)).
|
||||
|
||||
As for events, you can still use `$(".grid-stack").on(...)` for the version that uses jquery-ui for things we don't support.
|
||||
|
||||
# Changes
|
||||
|
||||
View our change log [here](https://github.com/gridstack/gridstack.js/tree/master/doc/CHANGES.md).
|
||||
|
||||
# Usage Trend
|
||||
|
||||
[Usage Trend of gridstack](https://npm-compare.com/gridstack#timeRange=THREE_YEARS)
|
||||
|
||||
<a href="https://npm-compare.com/gridstack#timeRange=THREE_YEARS" target="_blank">
|
||||
<img src="https://npm-compare.com/img/npm-trend/THREE_YEARS/gridstack.png" width="70%" alt="NPM Usage Trend of gridstack" />
|
||||
</a>
|
||||
|
||||
# The Team
|
||||
|
||||
gridstack.js is currently maintained by [Alain Dumesny](https://github.com/adumesny), before that [Dylan Weiss](https://github.com/radiolips), originally created by [Pavel Reznikov](https://github.com/troolee). We appreciate [all contributors](https://github.com/gridstack/gridstack.js/graphs/contributors) for help.
|
||||
69
node_modules/gridstack/dist/dd-base-impl.d.ts
generated
vendored
Normal file
69
node_modules/gridstack/dist/dd-base-impl.d.ts
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* dd-base-impl.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
/**
|
||||
* Type for event callback functions used in drag & drop operations.
|
||||
* Can return boolean to indicate if the event should continue propagation.
|
||||
*/
|
||||
export type EventCallback = (event: Event) => boolean | void;
|
||||
/**
|
||||
* Abstract base class for all drag & drop implementations.
|
||||
* Provides common functionality for event handling, enable/disable state,
|
||||
* and lifecycle management used by draggable, droppable, and resizable implementations.
|
||||
*/
|
||||
export declare abstract class DDBaseImplement {
|
||||
/**
|
||||
* Returns the current disabled state.
|
||||
* Note: Use enable()/disable() methods to change state as other operations need to happen.
|
||||
*/
|
||||
get disabled(): boolean;
|
||||
/**
|
||||
* Register an event callback for the specified event.
|
||||
*
|
||||
* @param event - Event name to listen for
|
||||
* @param callback - Function to call when event occurs
|
||||
*/
|
||||
on(event: string, callback: EventCallback): void;
|
||||
/**
|
||||
* Unregister an event callback for the specified event.
|
||||
*
|
||||
* @param event - Event name to stop listening for
|
||||
*/
|
||||
off(event: string): void;
|
||||
/**
|
||||
* Enable this drag & drop implementation.
|
||||
* Subclasses should override to perform additional setup.
|
||||
*/
|
||||
enable(): void;
|
||||
/**
|
||||
* Disable this drag & drop implementation.
|
||||
* Subclasses should override to perform additional cleanup.
|
||||
*/
|
||||
disable(): void;
|
||||
/**
|
||||
* Destroy this drag & drop implementation and clean up resources.
|
||||
* Removes all event handlers and clears internal state.
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Trigger a registered event callback if one exists and the implementation is enabled.
|
||||
*
|
||||
* @param eventName - Name of the event to trigger
|
||||
* @param event - DOM event object to pass to the callback
|
||||
* @returns Result from the callback function, if any
|
||||
*/
|
||||
triggerEvent(eventName: string, event: Event): boolean | void;
|
||||
}
|
||||
/**
|
||||
* Interface for HTML elements extended with drag & drop options.
|
||||
* Used to associate DD configuration with DOM elements.
|
||||
*/
|
||||
export interface HTMLElementExtendOpt<T> {
|
||||
/** The HTML element being extended */
|
||||
el: HTMLElement;
|
||||
/** The drag & drop options/configuration */
|
||||
option: T;
|
||||
/** Method to update the options and return the DD implementation */
|
||||
updateOption(T: any): DDBaseImplement;
|
||||
}
|
||||
70
node_modules/gridstack/dist/dd-base-impl.js
generated
vendored
Normal file
70
node_modules/gridstack/dist/dd-base-impl.js
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* dd-base-impl.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
/**
|
||||
* Abstract base class for all drag & drop implementations.
|
||||
* Provides common functionality for event handling, enable/disable state,
|
||||
* and lifecycle management used by draggable, droppable, and resizable implementations.
|
||||
*/
|
||||
export class DDBaseImplement {
|
||||
constructor() {
|
||||
/** @internal */
|
||||
this._eventRegister = {};
|
||||
}
|
||||
/**
|
||||
* Returns the current disabled state.
|
||||
* Note: Use enable()/disable() methods to change state as other operations need to happen.
|
||||
*/
|
||||
get disabled() { return this._disabled; }
|
||||
/**
|
||||
* Register an event callback for the specified event.
|
||||
*
|
||||
* @param event - Event name to listen for
|
||||
* @param callback - Function to call when event occurs
|
||||
*/
|
||||
on(event, callback) {
|
||||
this._eventRegister[event] = callback;
|
||||
}
|
||||
/**
|
||||
* Unregister an event callback for the specified event.
|
||||
*
|
||||
* @param event - Event name to stop listening for
|
||||
*/
|
||||
off(event) {
|
||||
delete this._eventRegister[event];
|
||||
}
|
||||
/**
|
||||
* Enable this drag & drop implementation.
|
||||
* Subclasses should override to perform additional setup.
|
||||
*/
|
||||
enable() {
|
||||
this._disabled = false;
|
||||
}
|
||||
/**
|
||||
* Disable this drag & drop implementation.
|
||||
* Subclasses should override to perform additional cleanup.
|
||||
*/
|
||||
disable() {
|
||||
this._disabled = true;
|
||||
}
|
||||
/**
|
||||
* Destroy this drag & drop implementation and clean up resources.
|
||||
* Removes all event handlers and clears internal state.
|
||||
*/
|
||||
destroy() {
|
||||
delete this._eventRegister;
|
||||
}
|
||||
/**
|
||||
* Trigger a registered event callback if one exists and the implementation is enabled.
|
||||
*
|
||||
* @param eventName - Name of the event to trigger
|
||||
* @param event - DOM event object to pass to the callback
|
||||
* @returns Result from the callback function, if any
|
||||
*/
|
||||
triggerEvent(eventName, event) {
|
||||
if (!this.disabled && this._eventRegister && this._eventRegister[eventName])
|
||||
return this._eventRegister[eventName](event);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=dd-base-impl.js.map
|
||||
1
node_modules/gridstack/dist/dd-base-impl.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-base-impl.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"dd-base-impl.js","sourceRoot":"","sources":["../src/dd-base-impl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;GAIG;AACH,MAAM,OAAgB,eAAe;IAArC;QASE,gBAAgB;QACN,mBAAc,GAEpB,EAAE,CAAC;IAwDT,CAAC;IAnEC;;;OAGG;IACH,IAAW,QAAQ,KAAgB,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAS3D;;;;;OAKG;IACI,EAAE,CAAC,KAAa,EAAE,QAAuB;QAC9C,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACI,GAAG,CAAC,KAAa;QACtB,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAED;;;OAGG;IACI,MAAM;QACX,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACzB,CAAC;IAED;;;OAGG;IACI,OAAO;QACZ,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;IAED;;;OAGG;IACI,OAAO;QACZ,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACI,YAAY,CAAC,SAAiB,EAAE,KAAY;QACjD,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC;YACzE,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;CACF","sourcesContent":["/**\n * dd-base-impl.ts 12.4.2\n * Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license\n */\n\n/**\n * Type for event callback functions used in drag & drop operations.\n * Can return boolean to indicate if the event should continue propagation.\n */\nexport type EventCallback = (event: Event) => boolean|void;\n\n/**\n * Abstract base class for all drag & drop implementations.\n * Provides common functionality for event handling, enable/disable state,\n * and lifecycle management used by draggable, droppable, and resizable implementations.\n */\nexport abstract class DDBaseImplement {\n /**\n * Returns the current disabled state.\n * Note: Use enable()/disable() methods to change state as other operations need to happen.\n */\n public get disabled(): boolean { return this._disabled; }\n\n /** @internal */\n protected _disabled: boolean; // initial state to differentiate from false\n /** @internal */\n protected _eventRegister: {\n [eventName: string]: EventCallback;\n } = {};\n\n /**\n * Register an event callback for the specified event.\n *\n * @param event - Event name to listen for\n * @param callback - Function to call when event occurs\n */\n public on(event: string, callback: EventCallback): void {\n this._eventRegister[event] = callback;\n }\n\n /**\n * Unregister an event callback for the specified event.\n *\n * @param event - Event name to stop listening for\n */\n public off(event: string): void {\n delete this._eventRegister[event];\n }\n\n /**\n * Enable this drag & drop implementation.\n * Subclasses should override to perform additional setup.\n */\n public enable(): void {\n this._disabled = false;\n }\n\n /**\n * Disable this drag & drop implementation.\n * Subclasses should override to perform additional cleanup.\n */\n public disable(): void {\n this._disabled = true;\n }\n\n /**\n * Destroy this drag & drop implementation and clean up resources.\n * Removes all event handlers and clears internal state.\n */\n public destroy(): void {\n delete this._eventRegister;\n }\n\n /**\n * Trigger a registered event callback if one exists and the implementation is enabled.\n *\n * @param eventName - Name of the event to trigger\n * @param event - DOM event object to pass to the callback\n * @returns Result from the callback function, if any\n */\n public triggerEvent(eventName: string, event: Event): boolean|void {\n if (!this.disabled && this._eventRegister && this._eventRegister[eventName])\n return this._eventRegister[eventName](event);\n }\n}\n\n/**\n * Interface for HTML elements extended with drag & drop options.\n * Used to associate DD configuration with DOM elements.\n */\nexport interface HTMLElementExtendOpt<T> {\n /** The HTML element being extended */\n el: HTMLElement;\n /** The drag & drop options/configuration */\n option: T;\n /** Method to update the options and return the DD implementation */\n updateOption(T): DDBaseImplement;\n}\n"]}
|
||||
20
node_modules/gridstack/dist/dd-draggable.d.ts
generated
vendored
Normal file
20
node_modules/gridstack/dist/dd-draggable.d.ts
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* dd-draggable.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
|
||||
import { GridItemHTMLElement, DDDragOpt } from './types';
|
||||
type DDDragEvent = 'drag' | 'dragstart' | 'dragstop';
|
||||
export declare class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt<DDDragOpt> {
|
||||
el: GridItemHTMLElement;
|
||||
option: DDDragOpt;
|
||||
helper: HTMLElement;
|
||||
constructor(el: GridItemHTMLElement, option?: DDDragOpt);
|
||||
on(event: DDDragEvent, callback: (event: DragEvent) => void): void;
|
||||
off(event: DDDragEvent): void;
|
||||
enable(): void;
|
||||
disable(forDestroy?: boolean): void;
|
||||
destroy(): void;
|
||||
updateOption(opts: DDDragOpt): DDDraggable;
|
||||
}
|
||||
export {};
|
||||
367
node_modules/gridstack/dist/dd-draggable.js
generated
vendored
Normal file
367
node_modules/gridstack/dist/dd-draggable.js
generated
vendored
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* dd-draggable.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDManager } from './dd-manager';
|
||||
import { Utils } from './utils';
|
||||
import { DDBaseImplement } from './dd-base-impl';
|
||||
import { isTouch, touchend, touchmove, touchstart, pointerdown, DDTouch } from './dd-touch';
|
||||
// make sure we are not clicking on known object that handles mouseDown
|
||||
const skipMouseDown = 'input,textarea,button,select,option,[contenteditable="true"],.ui-resizable-handle';
|
||||
// let count = 0; // TEST
|
||||
class DDDraggable extends DDBaseImplement {
|
||||
constructor(el, option = {}) {
|
||||
super();
|
||||
this.el = el;
|
||||
this.option = option;
|
||||
/** @internal */
|
||||
this.dragTransform = {
|
||||
xScale: 1,
|
||||
yScale: 1,
|
||||
xOffset: 0,
|
||||
yOffset: 0
|
||||
};
|
||||
// get the element that is actually supposed to be dragged by
|
||||
const handleName = option?.handle?.substring(1);
|
||||
const n = el.gridstackNode;
|
||||
this.dragEls = !handleName || el.classList.contains(handleName) ? [el] : (n?.subGrid ? [el.querySelector(option.handle) || el] : Array.from(el.querySelectorAll(option.handle)));
|
||||
if (this.dragEls.length === 0) {
|
||||
this.dragEls = [el];
|
||||
}
|
||||
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
|
||||
this._mouseDown = this._mouseDown.bind(this);
|
||||
this._mouseMove = this._mouseMove.bind(this);
|
||||
this._mouseUp = this._mouseUp.bind(this);
|
||||
this._keyEvent = this._keyEvent.bind(this);
|
||||
this.enable();
|
||||
}
|
||||
on(event, callback) {
|
||||
super.on(event, callback);
|
||||
}
|
||||
off(event) {
|
||||
super.off(event);
|
||||
}
|
||||
enable() {
|
||||
if (this.disabled === false)
|
||||
return;
|
||||
super.enable();
|
||||
this.dragEls.forEach(dragEl => {
|
||||
dragEl.addEventListener('mousedown', this._mouseDown);
|
||||
if (isTouch) {
|
||||
dragEl.addEventListener('touchstart', touchstart);
|
||||
dragEl.addEventListener('pointerdown', pointerdown);
|
||||
// dragEl.style.touchAction = 'none'; // not needed unlike pointerdown doc comment
|
||||
}
|
||||
});
|
||||
this.el.classList.remove('ui-draggable-disabled');
|
||||
}
|
||||
disable(forDestroy = false) {
|
||||
if (this.disabled === true)
|
||||
return;
|
||||
super.disable();
|
||||
this.dragEls.forEach(dragEl => {
|
||||
dragEl.removeEventListener('mousedown', this._mouseDown);
|
||||
if (isTouch) {
|
||||
dragEl.removeEventListener('touchstart', touchstart);
|
||||
dragEl.removeEventListener('pointerdown', pointerdown);
|
||||
}
|
||||
});
|
||||
if (!forDestroy)
|
||||
this.el.classList.add('ui-draggable-disabled');
|
||||
}
|
||||
destroy() {
|
||||
if (this.dragTimeout)
|
||||
window.clearTimeout(this.dragTimeout);
|
||||
delete this.dragTimeout;
|
||||
if (this.mouseDownEvent)
|
||||
this._mouseUp(this.mouseDownEvent);
|
||||
this.disable(true);
|
||||
delete this.el;
|
||||
delete this.helper;
|
||||
delete this.option;
|
||||
super.destroy();
|
||||
}
|
||||
updateOption(opts) {
|
||||
Object.keys(opts).forEach(key => this.option[key] = opts[key]);
|
||||
return this;
|
||||
}
|
||||
/** @internal call when mouse goes down before a dragstart happens */
|
||||
_mouseDown(e) {
|
||||
// if real brower event (trusted:true vs false for our simulated ones) and we didn't correctly clear the last touch event, clear things up
|
||||
if (DDTouch.touchHandled && e.isTrusted)
|
||||
DDTouch.touchHandled = false;
|
||||
// don't let more than one widget handle mouseStart
|
||||
if (DDManager.mouseHandled)
|
||||
return;
|
||||
if (e.button !== 0)
|
||||
return true; // only left click
|
||||
// make sure we are not clicking on known object that handles mouseDown, or ones supplied by the user
|
||||
if (!this.dragEls.find(el => el === e.target) && e.target.closest(skipMouseDown))
|
||||
return true;
|
||||
if (this.option.cancel) {
|
||||
if (e.target.closest(this.option.cancel))
|
||||
return true;
|
||||
}
|
||||
this.mouseDownEvent = e;
|
||||
delete this.dragging;
|
||||
delete DDManager.dragElement;
|
||||
delete DDManager.dropElement;
|
||||
// document handler so we can continue receiving moves as the item is 'fixed' position, and capture=true so WE get a first crack
|
||||
document.addEventListener('mousemove', this._mouseMove, { capture: true, passive: true }); // true=capture, not bubble
|
||||
document.addEventListener('mouseup', this._mouseUp, true);
|
||||
if (isTouch) {
|
||||
e.currentTarget.addEventListener('touchmove', touchmove);
|
||||
e.currentTarget.addEventListener('touchend', touchend);
|
||||
}
|
||||
e.preventDefault();
|
||||
// preventDefault() prevents blur event which occurs just after mousedown event.
|
||||
// if an editable content has focus, then blur must be call
|
||||
if (document.activeElement)
|
||||
document.activeElement.blur();
|
||||
DDManager.mouseHandled = true;
|
||||
return true;
|
||||
}
|
||||
/** @internal method to call actual drag event */
|
||||
_callDrag(e) {
|
||||
if (!this.dragging)
|
||||
return;
|
||||
const ev = Utils.initEvent(e, { target: this.el, type: 'drag' });
|
||||
if (this.option.drag) {
|
||||
this.option.drag(ev, this.ui());
|
||||
}
|
||||
this.triggerEvent('drag', ev);
|
||||
}
|
||||
/** @internal called when the main page (after successful mousedown) receives a move event to drag the item around the screen */
|
||||
_mouseMove(e) {
|
||||
// console.log(`${count++} move ${e.x},${e.y}`)
|
||||
const s = this.mouseDownEvent;
|
||||
this.lastDrag = e;
|
||||
if (this.dragging) {
|
||||
this._dragFollow(e);
|
||||
// delay actual grid handling drag until we pause for a while if set
|
||||
if (DDManager.pauseDrag) {
|
||||
const pause = Number.isInteger(DDManager.pauseDrag) ? DDManager.pauseDrag : 100;
|
||||
if (this.dragTimeout)
|
||||
window.clearTimeout(this.dragTimeout);
|
||||
this.dragTimeout = window.setTimeout(() => this._callDrag(e), pause);
|
||||
}
|
||||
else {
|
||||
this._callDrag(e);
|
||||
}
|
||||
}
|
||||
else if (Math.abs(e.x - s.x) + Math.abs(e.y - s.y) > 3) {
|
||||
/**
|
||||
* don't start unless we've moved at least 3 pixels
|
||||
*/
|
||||
this.dragging = true;
|
||||
DDManager.dragElement = this;
|
||||
// if we're dragging an actual grid item, set the current drop as the grid (to detect enter/leave)
|
||||
const grid = this.el.gridstackNode?.grid;
|
||||
if (grid) {
|
||||
DDManager.dropElement = grid.el.ddElement.ddDroppable;
|
||||
}
|
||||
else {
|
||||
delete DDManager.dropElement;
|
||||
}
|
||||
this.helper = this._createHelper();
|
||||
this._setupHelperContainmentStyle();
|
||||
this.dragTransform = Utils.getValuesFromTransformedElement(this.helperContainment);
|
||||
this.dragOffset = this._getDragOffset(e, this.el, this.helperContainment);
|
||||
this._setupHelperStyle(e);
|
||||
const ev = Utils.initEvent(e, { target: this.el, type: 'dragstart' });
|
||||
if (this.option.start) {
|
||||
this.option.start(ev, this.ui());
|
||||
}
|
||||
this.triggerEvent('dragstart', ev);
|
||||
// now track keyboard events to cancel or rotate
|
||||
document.addEventListener('keydown', this._keyEvent);
|
||||
}
|
||||
// e.preventDefault(); // passive = true. OLD: was needed otherwise we get text sweep text selection as we drag around
|
||||
return true;
|
||||
}
|
||||
/** @internal call when the mouse gets released to drop the item at current location */
|
||||
_mouseUp(e) {
|
||||
document.removeEventListener('mousemove', this._mouseMove, true);
|
||||
document.removeEventListener('mouseup', this._mouseUp, true);
|
||||
if (isTouch && e.currentTarget) { // destroy() during nested grid call us again wit fake _mouseUp
|
||||
e.currentTarget.removeEventListener('touchmove', touchmove, true);
|
||||
e.currentTarget.removeEventListener('touchend', touchend, true);
|
||||
}
|
||||
if (this.dragging) {
|
||||
delete this.dragging;
|
||||
delete this.el.gridstackNode?._origRotate;
|
||||
document.removeEventListener('keydown', this._keyEvent);
|
||||
// reset the drop target if dragging over ourself (already parented, just moving during stop callback below)
|
||||
if (DDManager.dropElement?.el === this.el.parentElement) {
|
||||
delete DDManager.dropElement;
|
||||
}
|
||||
this.helperContainment.style.position = this.parentOriginStylePosition || null;
|
||||
if (this.helper !== this.el)
|
||||
this.helper.remove(); // hide now
|
||||
this._removeHelperStyle();
|
||||
const ev = Utils.initEvent(e, { target: this.el, type: 'dragstop' });
|
||||
if (this.option.stop) {
|
||||
this.option.stop(ev); // NOTE: destroy() will be called when removing item, so expect NULL ptr after!
|
||||
}
|
||||
this.triggerEvent('dragstop', ev);
|
||||
// call the droppable method to receive the item
|
||||
if (DDManager.dropElement) {
|
||||
DDManager.dropElement.drop(e);
|
||||
}
|
||||
}
|
||||
delete this.helper;
|
||||
delete this.mouseDownEvent;
|
||||
delete DDManager.dragElement;
|
||||
delete DDManager.dropElement;
|
||||
delete DDManager.mouseHandled;
|
||||
e.preventDefault();
|
||||
}
|
||||
/** @internal call when keys are being pressed - use Esc to cancel, R to rotate */
|
||||
_keyEvent(e) {
|
||||
const n = this.el.gridstackNode;
|
||||
const grid = n?.grid || DDManager.dropElement?.el?.gridstack;
|
||||
if (e.key === 'Escape') {
|
||||
if (n && n._origRotate) {
|
||||
n._orig = n._origRotate;
|
||||
delete n._origRotate;
|
||||
}
|
||||
grid?.cancelDrag();
|
||||
this._mouseUp(this.mouseDownEvent);
|
||||
}
|
||||
else if (n && grid && (e.key === 'r' || e.key === 'R')) {
|
||||
if (!Utils.canBeRotated(n))
|
||||
return;
|
||||
n._origRotate = n._origRotate || { ...n._orig }; // store the real orig size in case we Esc after doing rotation
|
||||
delete n._moving; // force rotate to happen (move waits for >50% coverage otherwise)
|
||||
grid.setAnimation(false) // immediate rotate so _getDragOffset() gets the right dom size below
|
||||
.rotate(n.el, { top: -this.dragOffset.offsetTop, left: -this.dragOffset.offsetLeft })
|
||||
.setAnimation();
|
||||
n._moving = true;
|
||||
this.dragOffset = this._getDragOffset(this.lastDrag, n.el, this.helperContainment);
|
||||
this.helper.style.width = this.dragOffset.width + 'px';
|
||||
this.helper.style.height = this.dragOffset.height + 'px';
|
||||
Utils.swap(n._orig, 'w', 'h');
|
||||
delete n._rect;
|
||||
this._mouseMove(this.lastDrag);
|
||||
}
|
||||
}
|
||||
/** @internal create a clone copy (or user defined method) of the original drag item if set */
|
||||
_createHelper() {
|
||||
let helper = this.el;
|
||||
if (typeof this.option.helper === 'function') {
|
||||
helper = this.option.helper(this.el);
|
||||
}
|
||||
else if (this.option.helper === 'clone') {
|
||||
helper = Utils.cloneNode(this.el);
|
||||
}
|
||||
if (!helper.parentElement) {
|
||||
Utils.appendTo(helper, this.option.appendTo === 'parent' ? this.el.parentElement : this.option.appendTo);
|
||||
}
|
||||
this.dragElementOriginStyle = DDDraggable.originStyleProp.map(prop => this.el.style[prop]);
|
||||
return helper;
|
||||
}
|
||||
/** @internal set the fix position of the dragged item */
|
||||
_setupHelperStyle(e) {
|
||||
this.helper.classList.add('ui-draggable-dragging');
|
||||
this.el.gridstackNode?.grid?.el.classList.add('grid-stack-dragging');
|
||||
// TODO: set all at once with style.cssText += ... ? https://stackoverflow.com/questions/3968593
|
||||
const style = this.helper.style;
|
||||
style.pointerEvents = 'none'; // needed for over items to get enter/leave
|
||||
// style.cursor = 'move'; // TODO: can't set with pointerEvents=none ! (no longer in CSS either as no-op)
|
||||
style.width = this.dragOffset.width + 'px';
|
||||
style.height = this.dragOffset.height + 'px';
|
||||
style.willChange = 'left, top';
|
||||
style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative'
|
||||
this._dragFollow(e); // now position it
|
||||
style.transition = 'none'; // show up instantly
|
||||
setTimeout(() => {
|
||||
if (this.helper) {
|
||||
style.transition = null; // recover animation
|
||||
}
|
||||
}, 0);
|
||||
return this;
|
||||
}
|
||||
/** @internal restore back the original style before dragging */
|
||||
_removeHelperStyle() {
|
||||
this.helper.classList.remove('ui-draggable-dragging');
|
||||
this.el.gridstackNode?.grid?.el.classList.remove('grid-stack-dragging');
|
||||
const node = this.helper?.gridstackNode;
|
||||
// don't bother restoring styles if we're gonna remove anyway...
|
||||
if (!node?._isAboutToRemove && this.dragElementOriginStyle) {
|
||||
const helper = this.helper;
|
||||
// don't animate, otherwise we animate offseted when switching back to 'absolute' from 'fixed'.
|
||||
// TODO: this also removes resizing animation which doesn't have this issue, but others.
|
||||
// Ideally both would animate ('move' would immediately restore 'absolute' and adjust coordinate to match,
|
||||
// then trigger a delay (repaint) to restore to final dest with animate) but then we need to make sure 'resizestop'
|
||||
// is called AFTER 'transitionend' event is received (see https://github.com/gridstack/gridstack.js/issues/2033)
|
||||
const transition = this.dragElementOriginStyle['transition'] || null;
|
||||
helper.style.transition = this.dragElementOriginStyle['transition'] = 'none'; // can't be NULL #1973
|
||||
DDDraggable.originStyleProp.forEach(prop => helper.style[prop] = this.dragElementOriginStyle[prop] || null);
|
||||
setTimeout(() => helper.style.transition = transition, 50); // recover animation from saved vars after a pause (0 isn't enough #1973)
|
||||
}
|
||||
delete this.dragElementOriginStyle;
|
||||
return this;
|
||||
}
|
||||
/** @internal updates the top/left position to follow the mouse */
|
||||
_dragFollow(e) {
|
||||
const containmentRect = { left: 0, top: 0 };
|
||||
// if (this.helper.style.position === 'absolute') { // we use 'fixed'
|
||||
// const { left, top } = this.helperContainment.getBoundingClientRect();
|
||||
// containmentRect = { left, top };
|
||||
// }
|
||||
const style = this.helper.style;
|
||||
const offset = this.dragOffset;
|
||||
style.left = (e.clientX + offset.offsetLeft - containmentRect.left) * this.dragTransform.xScale + 'px';
|
||||
style.top = (e.clientY + offset.offsetTop - containmentRect.top) * this.dragTransform.yScale + 'px';
|
||||
}
|
||||
/** @internal */
|
||||
_setupHelperContainmentStyle() {
|
||||
this.helperContainment = this.helper.parentElement;
|
||||
if (this.helper.style.position !== 'fixed') {
|
||||
this.parentOriginStylePosition = this.helperContainment.style.position;
|
||||
if (getComputedStyle(this.helperContainment).position.match(/static/)) {
|
||||
this.helperContainment.style.position = 'relative';
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_getDragOffset(event, el, parent) {
|
||||
// in case ancestor has transform/perspective css properties that change the viewpoint
|
||||
let xformOffsetX = 0;
|
||||
let xformOffsetY = 0;
|
||||
if (parent) {
|
||||
xformOffsetX = this.dragTransform.xOffset;
|
||||
xformOffsetY = this.dragTransform.yOffset;
|
||||
}
|
||||
const targetOffset = el.getBoundingClientRect();
|
||||
return {
|
||||
left: targetOffset.left,
|
||||
top: targetOffset.top,
|
||||
offsetLeft: -event.clientX + targetOffset.left - xformOffsetX,
|
||||
offsetTop: -event.clientY + targetOffset.top - xformOffsetY,
|
||||
width: targetOffset.width * this.dragTransform.xScale,
|
||||
height: targetOffset.height * this.dragTransform.yScale
|
||||
};
|
||||
}
|
||||
/** @internal TODO: set to public as called by DDDroppable! */
|
||||
ui() {
|
||||
const containmentEl = this.el.parentElement;
|
||||
const containmentRect = containmentEl.getBoundingClientRect();
|
||||
const offset = this.helper.getBoundingClientRect();
|
||||
return {
|
||||
position: {
|
||||
top: (offset.top - containmentRect.top) * this.dragTransform.yScale,
|
||||
left: (offset.left - containmentRect.left) * this.dragTransform.xScale
|
||||
}
|
||||
/* not used by GridStack for now...
|
||||
helper: [this.helper], //The object arr representing the helper that's being dragged.
|
||||
offset: { top: offset.top, left: offset.left } // Current offset position of the helper as { top, left } object.
|
||||
*/
|
||||
};
|
||||
}
|
||||
}
|
||||
/** @internal properties we change during dragging, and restore back */
|
||||
DDDraggable.originStyleProp = ['width', 'height', 'transform', 'transform-origin', 'transition', 'pointerEvents', 'position', 'left', 'top', 'minWidth', 'willChange'];
|
||||
export { DDDraggable };
|
||||
//# sourceMappingURL=dd-draggable.js.map
|
||||
1
node_modules/gridstack/dist/dd-draggable.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-draggable.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
26
node_modules/gridstack/dist/dd-droppable.d.ts
generated
vendored
Normal file
26
node_modules/gridstack/dist/dd-droppable.d.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* dd-droppable.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
|
||||
import { DDUIData } from './types';
|
||||
export interface DDDroppableOpt {
|
||||
accept?: string | ((el: HTMLElement) => boolean);
|
||||
drop?: (event: DragEvent, ui: DDUIData) => void;
|
||||
over?: (event: DragEvent, ui: DDUIData) => void;
|
||||
out?: (event: DragEvent, ui: DDUIData) => void;
|
||||
}
|
||||
export declare class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt<DDDroppableOpt> {
|
||||
el: HTMLElement;
|
||||
option: DDDroppableOpt;
|
||||
accept: (el: HTMLElement) => boolean;
|
||||
constructor(el: HTMLElement, option?: DDDroppableOpt);
|
||||
on(event: 'drop' | 'dropover' | 'dropout', callback: (event: DragEvent) => void): void;
|
||||
off(event: 'drop' | 'dropover' | 'dropout'): void;
|
||||
enable(): void;
|
||||
disable(forDestroy?: boolean): void;
|
||||
destroy(): void;
|
||||
updateOption(opts: DDDroppableOpt): DDDroppable;
|
||||
/** item is being dropped on us - called by the drag mouseup handler - this calls the client drop event */
|
||||
drop(e: MouseEvent): void;
|
||||
}
|
||||
153
node_modules/gridstack/dist/dd-droppable.js
generated
vendored
Normal file
153
node_modules/gridstack/dist/dd-droppable.js
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* dd-droppable.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDManager } from './dd-manager';
|
||||
import { DDBaseImplement } from './dd-base-impl';
|
||||
import { Utils } from './utils';
|
||||
import { DDTouch, isTouch, pointerenter, pointerleave } from './dd-touch';
|
||||
// let count = 0; // TEST
|
||||
export class DDDroppable extends DDBaseImplement {
|
||||
constructor(el, option = {}) {
|
||||
super();
|
||||
this.el = el;
|
||||
this.option = option;
|
||||
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
|
||||
this._mouseEnter = this._mouseEnter.bind(this);
|
||||
this._mouseLeave = this._mouseLeave.bind(this);
|
||||
this.enable();
|
||||
this._setupAccept();
|
||||
}
|
||||
on(event, callback) {
|
||||
super.on(event, callback);
|
||||
}
|
||||
off(event) {
|
||||
super.off(event);
|
||||
}
|
||||
enable() {
|
||||
if (this.disabled === false)
|
||||
return;
|
||||
super.enable();
|
||||
this.el.classList.add('ui-droppable');
|
||||
this.el.classList.remove('ui-droppable-disabled');
|
||||
this.el.addEventListener('mouseenter', this._mouseEnter);
|
||||
this.el.addEventListener('mouseleave', this._mouseLeave);
|
||||
if (isTouch) {
|
||||
this.el.addEventListener('pointerenter', pointerenter);
|
||||
this.el.addEventListener('pointerleave', pointerleave);
|
||||
}
|
||||
}
|
||||
disable(forDestroy = false) {
|
||||
if (this.disabled === true)
|
||||
return;
|
||||
super.disable();
|
||||
this.el.classList.remove('ui-droppable');
|
||||
if (!forDestroy)
|
||||
this.el.classList.add('ui-droppable-disabled');
|
||||
this.el.removeEventListener('mouseenter', this._mouseEnter);
|
||||
this.el.removeEventListener('mouseleave', this._mouseLeave);
|
||||
if (isTouch) {
|
||||
this.el.removeEventListener('pointerenter', pointerenter);
|
||||
this.el.removeEventListener('pointerleave', pointerleave);
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
this.disable(true);
|
||||
this.el.classList.remove('ui-droppable');
|
||||
this.el.classList.remove('ui-droppable-disabled');
|
||||
super.destroy();
|
||||
}
|
||||
updateOption(opts) {
|
||||
Object.keys(opts).forEach(key => this.option[key] = opts[key]);
|
||||
this._setupAccept();
|
||||
return this;
|
||||
}
|
||||
/** @internal called when the cursor enters our area - prepare for a possible drop and track leaving */
|
||||
_mouseEnter(e) {
|
||||
// console.log(`${count++} Enter ${this.el.id || (this.el as GridHTMLElement).gridstack.opts.id}`); // TEST
|
||||
if (!DDManager.dragElement)
|
||||
return;
|
||||
// During touch drag operations, ignore real browser-generated mouseenter events (isTrusted:true) vs our simulated ones (isTrusted:false).
|
||||
// The browser can fire spurious mouseenter events when we dispatch simulated mousemove events.
|
||||
if (DDTouch.touchHandled && e.isTrusted)
|
||||
return;
|
||||
if (!this._canDrop(DDManager.dragElement.el))
|
||||
return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// make sure when we enter this, that the last one gets a leave FIRST to correctly cleanup as we don't always do
|
||||
if (DDManager.dropElement && DDManager.dropElement !== this) {
|
||||
DDManager.dropElement._mouseLeave(e, true); // calledByEnter = true
|
||||
}
|
||||
DDManager.dropElement = this;
|
||||
const ev = Utils.initEvent(e, { target: this.el, type: 'dropover' });
|
||||
if (this.option.over) {
|
||||
this.option.over(ev, this._ui(DDManager.dragElement));
|
||||
}
|
||||
this.triggerEvent('dropover', ev);
|
||||
this.el.classList.add('ui-droppable-over');
|
||||
// console.log('tracking'); // TEST
|
||||
}
|
||||
/** @internal called when the item is leaving our area, stop tracking if we had moving item */
|
||||
_mouseLeave(e, calledByEnter = false) {
|
||||
// console.log(`${count++} Leave ${this.el.id || (this.el as GridHTMLElement).gridstack.opts.id}`); // TEST
|
||||
if (!DDManager.dragElement || DDManager.dropElement !== this)
|
||||
return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const ev = Utils.initEvent(e, { target: this.el, type: 'dropout' });
|
||||
if (this.option.out) {
|
||||
this.option.out(ev, this._ui(DDManager.dragElement));
|
||||
}
|
||||
this.triggerEvent('dropout', ev);
|
||||
if (DDManager.dropElement === this) {
|
||||
delete DDManager.dropElement;
|
||||
// console.log('not tracking'); // TEST
|
||||
// if we're still over a parent droppable, send it an enter as we don't get one from leaving nested children
|
||||
if (!calledByEnter) {
|
||||
let parentDrop;
|
||||
let parent = this.el.parentElement;
|
||||
while (!parentDrop && parent) {
|
||||
parentDrop = parent.ddElement?.ddDroppable;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
if (parentDrop) {
|
||||
parentDrop._mouseEnter(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** item is being dropped on us - called by the drag mouseup handler - this calls the client drop event */
|
||||
drop(e) {
|
||||
e.preventDefault();
|
||||
const ev = Utils.initEvent(e, { target: this.el, type: 'drop' });
|
||||
if (this.option.drop) {
|
||||
this.option.drop(ev, this._ui(DDManager.dragElement));
|
||||
}
|
||||
this.triggerEvent('drop', ev);
|
||||
}
|
||||
/** @internal true if element matches the string/method accept option */
|
||||
_canDrop(el) {
|
||||
return el && (!this.accept || this.accept(el));
|
||||
}
|
||||
/** @internal */
|
||||
_setupAccept() {
|
||||
if (!this.option.accept)
|
||||
return this;
|
||||
if (typeof this.option.accept === 'string') {
|
||||
this.accept = (el) => el.classList.contains(this.option.accept) || el.matches(this.option.accept);
|
||||
}
|
||||
else {
|
||||
this.accept = this.option.accept;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_ui(drag) {
|
||||
return {
|
||||
draggable: drag.el,
|
||||
...drag.ui()
|
||||
};
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=dd-droppable.js.map
|
||||
1
node_modules/gridstack/dist/dd-droppable.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-droppable.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
27
node_modules/gridstack/dist/dd-element.d.ts
generated
vendored
Normal file
27
node_modules/gridstack/dist/dd-element.d.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* dd-elements.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDResizable, DDResizableOpt } from './dd-resizable';
|
||||
import { DDDragOpt, GridItemHTMLElement } from './types';
|
||||
import { DDDraggable } from './dd-draggable';
|
||||
import { DDDroppable, DDDroppableOpt } from './dd-droppable';
|
||||
export interface DDElementHost extends GridItemHTMLElement {
|
||||
ddElement?: DDElement;
|
||||
}
|
||||
export declare class DDElement {
|
||||
el: DDElementHost;
|
||||
static init(el: DDElementHost): DDElement;
|
||||
ddDraggable?: DDDraggable;
|
||||
ddDroppable?: DDDroppable;
|
||||
ddResizable?: DDResizable;
|
||||
constructor(el: DDElementHost);
|
||||
on(eventName: string, callback: (event: MouseEvent) => void): DDElement;
|
||||
off(eventName: string): DDElement;
|
||||
setupDraggable(opts: DDDragOpt): DDElement;
|
||||
cleanDraggable(): DDElement;
|
||||
setupResizable(opts: DDResizableOpt): DDElement;
|
||||
cleanResizable(): DDElement;
|
||||
setupDroppable(opts: DDDroppableOpt): DDElement;
|
||||
cleanDroppable(): DDElement;
|
||||
}
|
||||
91
node_modules/gridstack/dist/dd-element.js
generated
vendored
Normal file
91
node_modules/gridstack/dist/dd-element.js
generated
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* dd-elements.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDResizable } from './dd-resizable';
|
||||
import { DDDraggable } from './dd-draggable';
|
||||
import { DDDroppable } from './dd-droppable';
|
||||
export class DDElement {
|
||||
static init(el) {
|
||||
if (!el.ddElement) {
|
||||
el.ddElement = new DDElement(el);
|
||||
}
|
||||
return el.ddElement;
|
||||
}
|
||||
constructor(el) {
|
||||
this.el = el;
|
||||
}
|
||||
on(eventName, callback) {
|
||||
if (this.ddDraggable && ['drag', 'dragstart', 'dragstop'].indexOf(eventName) > -1) {
|
||||
this.ddDraggable.on(eventName, callback);
|
||||
}
|
||||
else if (this.ddDroppable && ['drop', 'dropover', 'dropout'].indexOf(eventName) > -1) {
|
||||
this.ddDroppable.on(eventName, callback);
|
||||
}
|
||||
else if (this.ddResizable && ['resizestart', 'resize', 'resizestop'].indexOf(eventName) > -1) {
|
||||
this.ddResizable.on(eventName, callback);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
off(eventName) {
|
||||
if (this.ddDraggable && ['drag', 'dragstart', 'dragstop'].indexOf(eventName) > -1) {
|
||||
this.ddDraggable.off(eventName);
|
||||
}
|
||||
else if (this.ddDroppable && ['drop', 'dropover', 'dropout'].indexOf(eventName) > -1) {
|
||||
this.ddDroppable.off(eventName);
|
||||
}
|
||||
else if (this.ddResizable && ['resizestart', 'resize', 'resizestop'].indexOf(eventName) > -1) {
|
||||
this.ddResizable.off(eventName);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setupDraggable(opts) {
|
||||
if (!this.ddDraggable) {
|
||||
this.ddDraggable = new DDDraggable(this.el, opts);
|
||||
}
|
||||
else {
|
||||
this.ddDraggable.updateOption(opts);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
cleanDraggable() {
|
||||
if (this.ddDraggable) {
|
||||
this.ddDraggable.destroy();
|
||||
delete this.ddDraggable;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setupResizable(opts) {
|
||||
if (!this.ddResizable) {
|
||||
this.ddResizable = new DDResizable(this.el, opts);
|
||||
}
|
||||
else {
|
||||
this.ddResizable.updateOption(opts);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
cleanResizable() {
|
||||
if (this.ddResizable) {
|
||||
this.ddResizable.destroy();
|
||||
delete this.ddResizable;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setupDroppable(opts) {
|
||||
if (!this.ddDroppable) {
|
||||
this.ddDroppable = new DDDroppable(this.el, opts);
|
||||
}
|
||||
else {
|
||||
this.ddDroppable.updateOption(opts);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
cleanDroppable() {
|
||||
if (this.ddDroppable) {
|
||||
this.ddDroppable.destroy();
|
||||
delete this.ddDroppable;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=dd-element.js.map
|
||||
1
node_modules/gridstack/dist/dd-element.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-element.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
82
node_modules/gridstack/dist/dd-gridstack.d.ts
generated
vendored
Normal file
82
node_modules/gridstack/dist/dd-gridstack.d.ts
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* dd-gridstack.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { GridItemHTMLElement, GridStackElement, DDDragOpt } from './types';
|
||||
import { DDElementHost } from './dd-element';
|
||||
/**
|
||||
* Drag & Drop options for drop targets.
|
||||
* Configures which elements can be dropped onto a grid.
|
||||
*/
|
||||
export type DDDropOpt = {
|
||||
/** Function to determine if an element can be dropped (see GridStackOptions.acceptWidgets) */
|
||||
accept?: (el: GridItemHTMLElement) => boolean;
|
||||
};
|
||||
/**
|
||||
* Drag & Drop operation types used throughout the DD system.
|
||||
* Can be control commands or configuration objects.
|
||||
*/
|
||||
export type DDOpts = 'enable' | 'disable' | 'destroy' | 'option' | string | any;
|
||||
/**
|
||||
* Keys for DD configuration options that can be set via the 'option' command.
|
||||
*/
|
||||
export type DDKey = 'minWidth' | 'minHeight' | 'maxWidth' | 'maxHeight' | 'maxHeightMoveUp' | 'maxWidthMoveLeft';
|
||||
/**
|
||||
* Values for DD configuration options (numbers or strings with units).
|
||||
*/
|
||||
export type DDValue = number | string;
|
||||
/**
|
||||
* Callback function type for drag & drop events.
|
||||
*
|
||||
* @param event - The DOM event that triggered the callback
|
||||
* @param arg2 - The grid item element being dragged/dropped
|
||||
* @param helper - Optional helper element used during drag operations
|
||||
*/
|
||||
export type DDCallback = (event: Event, arg2: GridItemHTMLElement, helper?: GridItemHTMLElement) => void;
|
||||
/**
|
||||
* HTML Native Mouse and Touch Events Drag and Drop functionality.
|
||||
*
|
||||
* This class provides the main drag & drop implementation for GridStack,
|
||||
* handling resizing, dragging, and dropping of grid items using native HTML5 events.
|
||||
* It manages the interaction between different DD components and the grid system.
|
||||
*/
|
||||
export declare class DDGridStack {
|
||||
/**
|
||||
* Enable/disable/configure resizing for grid elements.
|
||||
*
|
||||
* @param el - Grid item element(s) to configure
|
||||
* @param opts - Resize options or command ('enable', 'disable', 'destroy', 'option', or config object)
|
||||
* @param key - Option key when using 'option' command
|
||||
* @param value - Option value when using 'option' command
|
||||
* @returns this instance for chaining
|
||||
*
|
||||
* @example
|
||||
* dd.resizable(element, 'enable'); // Enable resizing
|
||||
* dd.resizable(element, 'option', 'minWidth', 100); // Set minimum width
|
||||
*/
|
||||
resizable(el: GridItemHTMLElement, opts: DDOpts, key?: DDKey, value?: DDValue): DDGridStack;
|
||||
/**
|
||||
* Enable/disable/configure dragging for grid elements.
|
||||
*
|
||||
* @param el - Grid item element(s) to configure
|
||||
* @param opts - Drag options or command ('enable', 'disable', 'destroy', 'option', or config object)
|
||||
* @param key - Option key when using 'option' command
|
||||
* @param value - Option value when using 'option' command
|
||||
* @returns this instance for chaining
|
||||
*
|
||||
* @example
|
||||
* dd.draggable(element, 'enable'); // Enable dragging
|
||||
* dd.draggable(element, {handle: '.drag-handle'}); // Configure drag handle
|
||||
*/
|
||||
draggable(el: GridItemHTMLElement, opts: DDOpts, key?: DDKey, value?: DDValue): DDGridStack;
|
||||
dragIn(el: GridStackElement, opts: DDDragOpt): DDGridStack;
|
||||
droppable(el: GridItemHTMLElement, opts: DDOpts | DDDropOpt, key?: DDKey, value?: DDValue): DDGridStack;
|
||||
/** true if element is droppable */
|
||||
isDroppable(el: DDElementHost): boolean;
|
||||
/** true if element is draggable */
|
||||
isDraggable(el: DDElementHost): boolean;
|
||||
/** true if element is draggable */
|
||||
isResizable(el: DDElementHost): boolean;
|
||||
on(el: GridItemHTMLElement, name: string, callback: DDCallback): DDGridStack;
|
||||
off(el: GridItemHTMLElement, name: string): DDGridStack;
|
||||
}
|
||||
165
node_modules/gridstack/dist/dd-gridstack.js
generated
vendored
Normal file
165
node_modules/gridstack/dist/dd-gridstack.js
generated
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* dd-gridstack.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { Utils } from './utils';
|
||||
import { DDManager } from './dd-manager';
|
||||
import { DDElement } from './dd-element';
|
||||
// let count = 0; // TEST
|
||||
/**
|
||||
* HTML Native Mouse and Touch Events Drag and Drop functionality.
|
||||
*
|
||||
* This class provides the main drag & drop implementation for GridStack,
|
||||
* handling resizing, dragging, and dropping of grid items using native HTML5 events.
|
||||
* It manages the interaction between different DD components and the grid system.
|
||||
*/
|
||||
export class DDGridStack {
|
||||
/**
|
||||
* Enable/disable/configure resizing for grid elements.
|
||||
*
|
||||
* @param el - Grid item element(s) to configure
|
||||
* @param opts - Resize options or command ('enable', 'disable', 'destroy', 'option', or config object)
|
||||
* @param key - Option key when using 'option' command
|
||||
* @param value - Option value when using 'option' command
|
||||
* @returns this instance for chaining
|
||||
*
|
||||
* @example
|
||||
* dd.resizable(element, 'enable'); // Enable resizing
|
||||
* dd.resizable(element, 'option', 'minWidth', 100); // Set minimum width
|
||||
*/
|
||||
resizable(el, opts, key, value) {
|
||||
this._getDDElements(el, opts).forEach(dEl => {
|
||||
if (opts === 'disable' || opts === 'enable') {
|
||||
dEl.ddResizable && dEl.ddResizable[opts](); // can't create DD as it requires options for setupResizable()
|
||||
}
|
||||
else if (opts === 'destroy') {
|
||||
dEl.ddResizable && dEl.cleanResizable();
|
||||
}
|
||||
else if (opts === 'option') {
|
||||
dEl.setupResizable({ [key]: value });
|
||||
}
|
||||
else {
|
||||
const n = dEl.el.gridstackNode;
|
||||
const grid = n.grid;
|
||||
let handles = dEl.el.getAttribute('gs-resize-handles') || grid.opts.resizable.handles || 'e,s,se';
|
||||
if (handles === 'all')
|
||||
handles = 'n,e,s,w,se,sw,ne,nw';
|
||||
// NOTE: keep the resize handles as e,w don't have enough space (10px) to show resize corners anyway. limit during drag instead
|
||||
// restrict vertical resize if height is done to match content anyway... odd to have it spring back
|
||||
// if (Utils.shouldSizeToContent(n, true)) {
|
||||
// const doE = handles.indexOf('e') !== -1;
|
||||
// const doW = handles.indexOf('w') !== -1;
|
||||
// handles = doE ? (doW ? 'e,w' : 'e') : (doW ? 'w' : '');
|
||||
// }
|
||||
const autoHide = !grid.opts.alwaysShowResizeHandle;
|
||||
dEl.setupResizable({
|
||||
...grid.opts.resizable,
|
||||
...{ handles, autoHide },
|
||||
...{
|
||||
start: opts.start,
|
||||
stop: opts.stop,
|
||||
resize: opts.resize
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Enable/disable/configure dragging for grid elements.
|
||||
*
|
||||
* @param el - Grid item element(s) to configure
|
||||
* @param opts - Drag options or command ('enable', 'disable', 'destroy', 'option', or config object)
|
||||
* @param key - Option key when using 'option' command
|
||||
* @param value - Option value when using 'option' command
|
||||
* @returns this instance for chaining
|
||||
*
|
||||
* @example
|
||||
* dd.draggable(element, 'enable'); // Enable dragging
|
||||
* dd.draggable(element, {handle: '.drag-handle'}); // Configure drag handle
|
||||
*/
|
||||
draggable(el, opts, key, value) {
|
||||
this._getDDElements(el, opts).forEach(dEl => {
|
||||
if (opts === 'disable' || opts === 'enable') {
|
||||
dEl.ddDraggable && dEl.ddDraggable[opts](); // can't create DD as it requires options for setupDraggable()
|
||||
}
|
||||
else if (opts === 'destroy') {
|
||||
dEl.ddDraggable && dEl.cleanDraggable();
|
||||
}
|
||||
else if (opts === 'option') {
|
||||
dEl.setupDraggable({ [key]: value });
|
||||
}
|
||||
else {
|
||||
const grid = dEl.el.gridstackNode.grid;
|
||||
dEl.setupDraggable({
|
||||
...grid.opts.draggable,
|
||||
...{
|
||||
// containment: (grid.parentGridNode && grid.opts.dragOut === false) ? grid.el.parentElement : (grid.opts.draggable.containment || null),
|
||||
start: opts.start,
|
||||
stop: opts.stop,
|
||||
drag: opts.drag
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
dragIn(el, opts) {
|
||||
this._getDDElements(el).forEach(dEl => dEl.setupDraggable(opts));
|
||||
return this;
|
||||
}
|
||||
droppable(el, opts, key, value) {
|
||||
if (typeof opts.accept === 'function' && !opts._accept) {
|
||||
opts._accept = opts.accept;
|
||||
opts.accept = (el) => opts._accept(el);
|
||||
}
|
||||
this._getDDElements(el, opts).forEach(dEl => {
|
||||
if (opts === 'disable' || opts === 'enable') {
|
||||
dEl.ddDroppable && dEl.ddDroppable[opts]();
|
||||
}
|
||||
else if (opts === 'destroy') {
|
||||
dEl.ddDroppable && dEl.cleanDroppable();
|
||||
}
|
||||
else if (opts === 'option') {
|
||||
dEl.setupDroppable({ [key]: value });
|
||||
}
|
||||
else {
|
||||
dEl.setupDroppable(opts);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
/** true if element is droppable */
|
||||
isDroppable(el) {
|
||||
return !!(el?.ddElement?.ddDroppable && !el.ddElement.ddDroppable.disabled);
|
||||
}
|
||||
/** true if element is draggable */
|
||||
isDraggable(el) {
|
||||
return !!(el?.ddElement?.ddDraggable && !el.ddElement.ddDraggable.disabled);
|
||||
}
|
||||
/** true if element is draggable */
|
||||
isResizable(el) {
|
||||
return !!(el?.ddElement?.ddResizable && !el.ddElement.ddResizable.disabled);
|
||||
}
|
||||
on(el, name, callback) {
|
||||
this._getDDElements(el).forEach(dEl => dEl.on(name, (event) => {
|
||||
callback(event, DDManager.dragElement ? DDManager.dragElement.el : event.target, DDManager.dragElement ? DDManager.dragElement.helper : null);
|
||||
}));
|
||||
return this;
|
||||
}
|
||||
off(el, name) {
|
||||
this._getDDElements(el).forEach(dEl => dEl.off(name));
|
||||
return this;
|
||||
}
|
||||
/** @internal returns a list of DD elements, creating them on the fly by default unless option is to destroy or disable */
|
||||
_getDDElements(els, opts) {
|
||||
// don't force create if we're going to destroy it, unless it's a grid which is used as drop target for it's children
|
||||
const create = els.gridstack || opts !== 'destroy' && opts !== 'disable';
|
||||
const hosts = Utils.getElements(els);
|
||||
if (!hosts.length)
|
||||
return [];
|
||||
const list = hosts.map(e => e.ddElement || (create ? DDElement.init(e) : null)).filter(d => d); // remove nulls
|
||||
return list;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=dd-gridstack.js.map
|
||||
1
node_modules/gridstack/dist/dd-gridstack.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-gridstack.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
43
node_modules/gridstack/dist/dd-manager.d.ts
generated
vendored
Normal file
43
node_modules/gridstack/dist/dd-manager.d.ts
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* dd-manager.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDDraggable } from './dd-draggable';
|
||||
import { DDDroppable } from './dd-droppable';
|
||||
import { DDResizable } from './dd-resizable';
|
||||
/**
|
||||
* Global state manager for all Drag & Drop instances.
|
||||
*
|
||||
* This class maintains shared state across all drag & drop operations,
|
||||
* ensuring proper coordination between multiple grids and drag/drop elements.
|
||||
* All properties are static to provide global access throughout the DD system.
|
||||
*/
|
||||
export declare class DDManager {
|
||||
/**
|
||||
* Controls drag operation pausing behavior.
|
||||
* If set to true or a number (milliseconds), dragging placement and collision
|
||||
* detection will only happen after the user pauses movement.
|
||||
* This improves performance during rapid mouse movements.
|
||||
*/
|
||||
static pauseDrag: boolean | number;
|
||||
/**
|
||||
* Flag indicating if a mouse down event was already handled.
|
||||
* Prevents multiple handlers from processing the same mouse event.
|
||||
*/
|
||||
static mouseHandled: boolean;
|
||||
/**
|
||||
* Reference to the element currently being dragged.
|
||||
* Used to track the active drag operation across the system.
|
||||
*/
|
||||
static dragElement: DDDraggable;
|
||||
/**
|
||||
* Reference to the drop target element currently under the cursor.
|
||||
* Used to handle drop operations and hover effects.
|
||||
*/
|
||||
static dropElement: DDDroppable;
|
||||
/**
|
||||
* Reference to the element currently being resized.
|
||||
* Helps ignore nested grid resize handles during resize operations.
|
||||
*/
|
||||
static overResizeElement: DDResizable;
|
||||
}
|
||||
14
node_modules/gridstack/dist/dd-manager.js
generated
vendored
Normal file
14
node_modules/gridstack/dist/dd-manager.js
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* dd-manager.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
/**
|
||||
* Global state manager for all Drag & Drop instances.
|
||||
*
|
||||
* This class maintains shared state across all drag & drop operations,
|
||||
* ensuring proper coordination between multiple grids and drag/drop elements.
|
||||
* All properties are static to provide global access throughout the DD system.
|
||||
*/
|
||||
export class DDManager {
|
||||
}
|
||||
//# sourceMappingURL=dd-manager.js.map
|
||||
1
node_modules/gridstack/dist/dd-manager.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-manager.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"dd-manager.js","sourceRoot":"","sources":["../src/dd-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH;;;;;;GAMG;AACH,MAAM,OAAO,SAAS;CAiCrB","sourcesContent":["/**\n * dd-manager.ts 12.4.2\n * Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license\n */\n\nimport { DDDraggable } from './dd-draggable';\nimport { DDDroppable } from './dd-droppable';\nimport { DDResizable } from './dd-resizable';\n\n/**\n * Global state manager for all Drag & Drop instances.\n *\n * This class maintains shared state across all drag & drop operations,\n * ensuring proper coordination between multiple grids and drag/drop elements.\n * All properties are static to provide global access throughout the DD system.\n */\nexport class DDManager {\n /**\n * Controls drag operation pausing behavior.\n * If set to true or a number (milliseconds), dragging placement and collision\n * detection will only happen after the user pauses movement.\n * This improves performance during rapid mouse movements.\n */\n public static pauseDrag: boolean | number;\n\n /**\n * Flag indicating if a mouse down event was already handled.\n * Prevents multiple handlers from processing the same mouse event.\n */\n public static mouseHandled: boolean;\n\n /**\n * Reference to the element currently being dragged.\n * Used to track the active drag operation across the system.\n */\n public static dragElement: DDDraggable;\n\n /**\n * Reference to the drop target element currently under the cursor.\n * Used to handle drop operations and hover effects.\n */\n public static dropElement: DDDroppable;\n\n /**\n * Reference to the element currently being resized.\n * Helps ignore nested grid resize handles during resize operations.\n */\n public static overResizeElement: DDResizable;\n\n}\n"]}
|
||||
19
node_modules/gridstack/dist/dd-resizable-handle.d.ts
generated
vendored
Normal file
19
node_modules/gridstack/dist/dd-resizable-handle.d.ts
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* dd-resizable-handle.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { GridItemHTMLElement } from './gridstack';
|
||||
export interface DDResizableHandleOpt {
|
||||
element?: string | HTMLElement;
|
||||
start?: (event: MouseEvent) => void;
|
||||
move?: (event: MouseEvent) => void;
|
||||
stop?: (event: MouseEvent) => void;
|
||||
}
|
||||
export declare class DDResizableHandle {
|
||||
protected host: GridItemHTMLElement;
|
||||
protected dir: string;
|
||||
protected option: DDResizableHandleOpt;
|
||||
constructor(host: GridItemHTMLElement, dir: string, option: DDResizableHandleOpt);
|
||||
/** call this when resize handle needs to be removed and cleaned up */
|
||||
destroy(): DDResizableHandle;
|
||||
}
|
||||
128
node_modules/gridstack/dist/dd-resizable-handle.js
generated
vendored
Normal file
128
node_modules/gridstack/dist/dd-resizable-handle.js
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* dd-resizable-handle.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { isTouch, pointerdown, touchend, touchmove, touchstart } from './dd-touch';
|
||||
class DDResizableHandle {
|
||||
constructor(host, dir, option) {
|
||||
this.host = host;
|
||||
this.dir = dir;
|
||||
this.option = option;
|
||||
/** @internal true after we've moved enough pixels to start a resize */
|
||||
this.moving = false;
|
||||
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
|
||||
this._mouseDown = this._mouseDown.bind(this);
|
||||
this._mouseMove = this._mouseMove.bind(this);
|
||||
this._mouseUp = this._mouseUp.bind(this);
|
||||
this._keyEvent = this._keyEvent.bind(this);
|
||||
this._init();
|
||||
}
|
||||
/** @internal */
|
||||
_init() {
|
||||
if (this.option.element) {
|
||||
try {
|
||||
this.el = this.option.element instanceof HTMLElement
|
||||
? this.option.element
|
||||
: this.host.querySelector(this.option.element);
|
||||
}
|
||||
catch (error) {
|
||||
this.option.element = undefined; // make sure destroy handles it correctly
|
||||
console.error("Query for resizeable handle failed, falling back", error);
|
||||
}
|
||||
}
|
||||
if (!this.el) {
|
||||
this.el = document.createElement('div');
|
||||
this.host.appendChild(this.el);
|
||||
}
|
||||
this.el.classList.add('ui-resizable-handle');
|
||||
this.el.classList.add(`${DDResizableHandle.prefix}${this.dir}`);
|
||||
this.el.style.zIndex = '100';
|
||||
this.el.style.userSelect = 'none';
|
||||
this.el.addEventListener('mousedown', this._mouseDown);
|
||||
if (isTouch) {
|
||||
this.el.addEventListener('touchstart', touchstart);
|
||||
this.el.addEventListener('pointerdown', pointerdown);
|
||||
// this.el.style.touchAction = 'none'; // not needed unlike pointerdown doc comment
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** call this when resize handle needs to be removed and cleaned up */
|
||||
destroy() {
|
||||
if (this.moving)
|
||||
this._mouseUp(this.mouseDownEvent);
|
||||
this.el.removeEventListener('mousedown', this._mouseDown);
|
||||
if (isTouch) {
|
||||
this.el.removeEventListener('touchstart', touchstart);
|
||||
this.el.removeEventListener('pointerdown', pointerdown);
|
||||
}
|
||||
if (!this.option.element) {
|
||||
this.host.removeChild(this.el);
|
||||
}
|
||||
delete this.el;
|
||||
delete this.host;
|
||||
return this;
|
||||
}
|
||||
/** @internal called on mouse down on us: capture move on the entire document (mouse might not stay on us) until we release the mouse */
|
||||
_mouseDown(e) {
|
||||
this.mouseDownEvent = e;
|
||||
document.addEventListener('mousemove', this._mouseMove, { capture: true, passive: true }); // capture, not bubble
|
||||
document.addEventListener('mouseup', this._mouseUp, true);
|
||||
if (isTouch) {
|
||||
this.el.addEventListener('touchmove', touchmove);
|
||||
this.el.addEventListener('touchend', touchend);
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
/** @internal */
|
||||
_mouseMove(e) {
|
||||
const s = this.mouseDownEvent;
|
||||
if (this.moving) {
|
||||
this._triggerEvent('move', e);
|
||||
}
|
||||
else if (Math.abs(e.x - s.x) + Math.abs(e.y - s.y) > 2) {
|
||||
// don't start unless we've moved at least 3 pixels
|
||||
this.moving = true;
|
||||
this._triggerEvent('start', this.mouseDownEvent);
|
||||
this._triggerEvent('move', e);
|
||||
// now track keyboard events to cancel
|
||||
document.addEventListener('keydown', this._keyEvent);
|
||||
}
|
||||
e.stopPropagation();
|
||||
// e.preventDefault(); passive = true
|
||||
}
|
||||
/** @internal */
|
||||
_mouseUp(e) {
|
||||
if (this.moving) {
|
||||
this._triggerEvent('stop', e);
|
||||
document.removeEventListener('keydown', this._keyEvent);
|
||||
}
|
||||
document.removeEventListener('mousemove', this._mouseMove, true);
|
||||
document.removeEventListener('mouseup', this._mouseUp, true);
|
||||
if (isTouch) {
|
||||
this.el.removeEventListener('touchmove', touchmove);
|
||||
this.el.removeEventListener('touchend', touchend);
|
||||
}
|
||||
delete this.moving;
|
||||
delete this.mouseDownEvent;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
/** @internal call when keys are being pressed - use Esc to cancel */
|
||||
_keyEvent(e) {
|
||||
if (e.key === 'Escape') {
|
||||
this.host.gridstackNode?.grid?.engine.restoreInitial();
|
||||
this._mouseUp(this.mouseDownEvent);
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_triggerEvent(name, event) {
|
||||
if (this.option[name])
|
||||
this.option[name](event);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
DDResizableHandle.prefix = 'ui-resizable-';
|
||||
export { DDResizableHandle };
|
||||
//# sourceMappingURL=dd-resizable-handle.js.map
|
||||
1
node_modules/gridstack/dist/dd-resizable-handle.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-resizable-handle.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
28
node_modules/gridstack/dist/dd-resizable.d.ts
generated
vendored
Normal file
28
node_modules/gridstack/dist/dd-resizable.d.ts
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* dd-resizable.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
|
||||
import { DDResizeOpt, DDUIData, GridItemHTMLElement } from './types';
|
||||
export interface DDResizableOpt extends DDResizeOpt {
|
||||
maxHeight?: number;
|
||||
maxHeightMoveUp?: number;
|
||||
maxWidth?: number;
|
||||
maxWidthMoveLeft?: number;
|
||||
minHeight?: number;
|
||||
minWidth?: number;
|
||||
start?: (event: Event, ui: DDUIData) => void;
|
||||
stop?: (event: Event) => void;
|
||||
resize?: (event: Event, ui: DDUIData) => void;
|
||||
}
|
||||
export declare class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt<DDResizableOpt> {
|
||||
el: GridItemHTMLElement;
|
||||
option: DDResizableOpt;
|
||||
constructor(el: GridItemHTMLElement, option?: DDResizableOpt);
|
||||
on(event: 'resizestart' | 'resize' | 'resizestop', callback: (event: DragEvent) => void): void;
|
||||
off(event: 'resizestart' | 'resize' | 'resizestop'): void;
|
||||
enable(): void;
|
||||
disable(): void;
|
||||
destroy(): void;
|
||||
updateOption(opts: DDResizableOpt): DDResizable;
|
||||
}
|
||||
299
node_modules/gridstack/dist/dd-resizable.js
generated
vendored
Normal file
299
node_modules/gridstack/dist/dd-resizable.js
generated
vendored
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* dd-resizable.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDResizableHandle } from './dd-resizable-handle';
|
||||
import { DDBaseImplement } from './dd-base-impl';
|
||||
import { Utils } from './utils';
|
||||
import { DDManager } from './dd-manager';
|
||||
class DDResizable extends DDBaseImplement {
|
||||
// have to be public else complains for HTMLElementExtendOpt ?
|
||||
constructor(el, option = {}) {
|
||||
super();
|
||||
this.el = el;
|
||||
this.option = option;
|
||||
/** @internal */
|
||||
this.rectScale = { x: 1, y: 1 };
|
||||
/** @internal */
|
||||
this._ui = () => {
|
||||
const containmentEl = this.el.parentElement;
|
||||
const containmentRect = containmentEl.getBoundingClientRect();
|
||||
const newRect = {
|
||||
width: this.originalRect.width,
|
||||
height: this.originalRect.height + this.scrolled,
|
||||
left: this.originalRect.left,
|
||||
top: this.originalRect.top - this.scrolled
|
||||
};
|
||||
const rect = this.temporalRect || newRect;
|
||||
return {
|
||||
position: {
|
||||
left: (rect.left - containmentRect.left) * this.rectScale.x,
|
||||
top: (rect.top - containmentRect.top) * this.rectScale.y
|
||||
},
|
||||
size: {
|
||||
width: rect.width * this.rectScale.x,
|
||||
height: rect.height * this.rectScale.y
|
||||
}
|
||||
/* Gridstack ONLY needs position set above... keep around in case.
|
||||
element: [this.el], // The object representing the element to be resized
|
||||
helper: [], // TODO: not support yet - The object representing the helper that's being resized
|
||||
originalElement: [this.el],// we don't wrap here, so simplify as this.el //The object representing the original element before it is wrapped
|
||||
originalPosition: { // The position represented as { left, top } before the resizable is resized
|
||||
left: this.originalRect.left - containmentRect.left,
|
||||
top: this.originalRect.top - containmentRect.top
|
||||
},
|
||||
originalSize: { // The size represented as { width, height } before the resizable is resized
|
||||
width: this.originalRect.width,
|
||||
height: this.originalRect.height
|
||||
}
|
||||
*/
|
||||
};
|
||||
};
|
||||
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
|
||||
this._mouseOver = this._mouseOver.bind(this);
|
||||
this._mouseOut = this._mouseOut.bind(this);
|
||||
this.enable();
|
||||
this._setupAutoHide(this.option.autoHide);
|
||||
this._setupHandlers();
|
||||
}
|
||||
on(event, callback) {
|
||||
super.on(event, callback);
|
||||
}
|
||||
off(event) {
|
||||
super.off(event);
|
||||
}
|
||||
enable() {
|
||||
super.enable();
|
||||
this.el.classList.remove('ui-resizable-disabled');
|
||||
this._setupAutoHide(this.option.autoHide);
|
||||
}
|
||||
disable() {
|
||||
super.disable();
|
||||
this.el.classList.add('ui-resizable-disabled');
|
||||
this._setupAutoHide(false);
|
||||
}
|
||||
destroy() {
|
||||
this._removeHandlers();
|
||||
this._setupAutoHide(false);
|
||||
delete this.el;
|
||||
super.destroy();
|
||||
}
|
||||
updateOption(opts) {
|
||||
const updateHandles = (opts.handles && opts.handles !== this.option.handles);
|
||||
const updateAutoHide = (opts.autoHide && opts.autoHide !== this.option.autoHide);
|
||||
Object.keys(opts).forEach(key => this.option[key] = opts[key]);
|
||||
if (updateHandles) {
|
||||
this._removeHandlers();
|
||||
this._setupHandlers();
|
||||
}
|
||||
if (updateAutoHide) {
|
||||
this._setupAutoHide(this.option.autoHide);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** @internal turns auto hide on/off */
|
||||
_setupAutoHide(auto) {
|
||||
if (auto) {
|
||||
this.el.classList.add('ui-resizable-autohide');
|
||||
// use mouseover and not mouseenter to get better performance and track for nested cases
|
||||
this.el.addEventListener('mouseover', this._mouseOver);
|
||||
this.el.addEventListener('mouseout', this._mouseOut);
|
||||
}
|
||||
else {
|
||||
this.el.classList.remove('ui-resizable-autohide');
|
||||
this.el.removeEventListener('mouseover', this._mouseOver);
|
||||
this.el.removeEventListener('mouseout', this._mouseOut);
|
||||
if (DDManager.overResizeElement === this) {
|
||||
delete DDManager.overResizeElement;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_mouseOver(e) {
|
||||
// console.log(`${count++} pre-enter ${(this.el as GridItemHTMLElement).gridstackNode._id}`)
|
||||
// already over a child, ignore. Ideally we just call e.stopPropagation() but see https://github.com/gridstack/gridstack.js/issues/2018
|
||||
if (DDManager.overResizeElement || DDManager.dragElement)
|
||||
return;
|
||||
DDManager.overResizeElement = this;
|
||||
// console.log(`${count++} enter ${(this.el as GridItemHTMLElement).gridstackNode._id}`)
|
||||
this.el.classList.remove('ui-resizable-autohide');
|
||||
}
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_mouseOut(e) {
|
||||
// console.log(`${count++} pre-leave ${(this.el as GridItemHTMLElement).gridstackNode._id}`)
|
||||
if (DDManager.overResizeElement !== this)
|
||||
return;
|
||||
delete DDManager.overResizeElement;
|
||||
// console.log(`${count++} leave ${(this.el as GridItemHTMLElement).gridstackNode._id}`)
|
||||
this.el.classList.add('ui-resizable-autohide');
|
||||
}
|
||||
/** @internal */
|
||||
_setupHandlers() {
|
||||
this.handlers = this.option.handles.split(',')
|
||||
.map(dir => dir.trim())
|
||||
.map(dir => new DDResizableHandle(this.el, dir, {
|
||||
element: this.option.element,
|
||||
start: (event) => this._resizeStart(event),
|
||||
stop: (event) => this._resizeStop(event),
|
||||
move: (event) => this._resizing(event, dir)
|
||||
}));
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_resizeStart(event) {
|
||||
this.sizeToContent = Utils.shouldSizeToContent(this.el.gridstackNode, true); // strick true only and not number
|
||||
this.originalRect = this.el.getBoundingClientRect();
|
||||
this.scrollEl = Utils.getScrollElement(this.el);
|
||||
this.scrollY = this.scrollEl.scrollTop;
|
||||
this.scrolled = 0;
|
||||
this.startEvent = event;
|
||||
this._setupHelper();
|
||||
this._applyChange();
|
||||
const ev = Utils.initEvent(event, { type: 'resizestart', target: this.el });
|
||||
if (this.option.start) {
|
||||
this.option.start(ev, this._ui());
|
||||
}
|
||||
this.el.classList.add('ui-resizable-resizing');
|
||||
this.triggerEvent('resizestart', ev);
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_resizing(event, dir) {
|
||||
this.scrolled = this.scrollEl.scrollTop - this.scrollY;
|
||||
this.temporalRect = this._getChange(event, dir);
|
||||
this._applyChange();
|
||||
const ev = Utils.initEvent(event, { type: 'resize', target: this.el });
|
||||
if (this.option.resize) {
|
||||
this.option.resize(ev, this._ui());
|
||||
}
|
||||
this.triggerEvent('resize', ev);
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_resizeStop(event) {
|
||||
const ev = Utils.initEvent(event, { type: 'resizestop', target: this.el });
|
||||
// Remove style attr now, so the stop handler can rebuild style attrs
|
||||
this._cleanHelper();
|
||||
if (this.option.stop) {
|
||||
this.option.stop(ev); // Note: ui() not used by gridstack so don't pass
|
||||
}
|
||||
this.el.classList.remove('ui-resizable-resizing');
|
||||
this.triggerEvent('resizestop', ev);
|
||||
delete this.startEvent;
|
||||
delete this.originalRect;
|
||||
delete this.temporalRect;
|
||||
delete this.scrollY;
|
||||
delete this.scrolled;
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_setupHelper() {
|
||||
this.elOriginStyleVal = DDResizable._originStyleProp.map(prop => this.el.style[prop]);
|
||||
this.parentOriginStylePosition = this.el.parentElement.style.position;
|
||||
const parent = this.el.parentElement;
|
||||
const dragTransform = Utils.getValuesFromTransformedElement(parent);
|
||||
this.rectScale = {
|
||||
x: dragTransform.xScale,
|
||||
y: dragTransform.yScale
|
||||
};
|
||||
if (getComputedStyle(this.el.parentElement).position.match(/static/)) {
|
||||
this.el.parentElement.style.position = 'relative';
|
||||
}
|
||||
this.el.style.position = 'absolute';
|
||||
this.el.style.opacity = '0.8';
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_cleanHelper() {
|
||||
DDResizable._originStyleProp.forEach((prop, i) => {
|
||||
this.el.style[prop] = this.elOriginStyleVal[i] || null;
|
||||
});
|
||||
this.el.parentElement.style.position = this.parentOriginStylePosition || null;
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_getChange(event, dir) {
|
||||
const oEvent = this.startEvent;
|
||||
const newRect = {
|
||||
width: this.originalRect.width,
|
||||
height: this.originalRect.height + this.scrolled,
|
||||
left: this.originalRect.left,
|
||||
top: this.originalRect.top - this.scrolled
|
||||
};
|
||||
const offsetX = event.clientX - oEvent.clientX;
|
||||
const offsetY = this.sizeToContent ? 0 : event.clientY - oEvent.clientY; // prevent vert resize
|
||||
let moveLeft;
|
||||
let moveUp;
|
||||
if (dir.indexOf('e') > -1) {
|
||||
newRect.width += offsetX;
|
||||
}
|
||||
else if (dir.indexOf('w') > -1) {
|
||||
newRect.width -= offsetX;
|
||||
newRect.left += offsetX;
|
||||
moveLeft = true;
|
||||
}
|
||||
if (dir.indexOf('s') > -1) {
|
||||
newRect.height += offsetY;
|
||||
}
|
||||
else if (dir.indexOf('n') > -1) {
|
||||
newRect.height -= offsetY;
|
||||
newRect.top += offsetY;
|
||||
moveUp = true;
|
||||
}
|
||||
const constrain = this._constrainSize(newRect.width, newRect.height, moveLeft, moveUp);
|
||||
if (Math.round(newRect.width) !== Math.round(constrain.width)) { // round to ignore slight round-off errors
|
||||
if (dir.indexOf('w') > -1) {
|
||||
newRect.left += newRect.width - constrain.width;
|
||||
}
|
||||
newRect.width = constrain.width;
|
||||
}
|
||||
if (Math.round(newRect.height) !== Math.round(constrain.height)) {
|
||||
if (dir.indexOf('n') > -1) {
|
||||
newRect.top += newRect.height - constrain.height;
|
||||
}
|
||||
newRect.height = constrain.height;
|
||||
}
|
||||
return newRect;
|
||||
}
|
||||
/** @internal constrain the size to the set min/max values */
|
||||
_constrainSize(oWidth, oHeight, moveLeft, moveUp) {
|
||||
const o = this.option;
|
||||
const maxWidth = (moveLeft ? o.maxWidthMoveLeft : o.maxWidth) || Number.MAX_SAFE_INTEGER;
|
||||
const minWidth = o.minWidth / this.rectScale.x || oWidth;
|
||||
const maxHeight = (moveUp ? o.maxHeightMoveUp : o.maxHeight) || Number.MAX_SAFE_INTEGER;
|
||||
const minHeight = o.minHeight / this.rectScale.y || oHeight;
|
||||
const width = Math.min(maxWidth, Math.max(minWidth, oWidth));
|
||||
const height = Math.min(maxHeight, Math.max(minHeight, oHeight));
|
||||
return { width, height };
|
||||
}
|
||||
/** @internal */
|
||||
_applyChange() {
|
||||
let containmentRect = { left: 0, top: 0, width: 0, height: 0 };
|
||||
if (this.el.style.position === 'absolute') {
|
||||
const containmentEl = this.el.parentElement;
|
||||
const { left, top } = containmentEl.getBoundingClientRect();
|
||||
containmentRect = { left, top, width: 0, height: 0 };
|
||||
}
|
||||
if (!this.temporalRect)
|
||||
return this;
|
||||
Object.keys(this.temporalRect).forEach(key => {
|
||||
const value = this.temporalRect[key];
|
||||
const scaleReciprocal = key === 'width' || key === 'left' ? this.rectScale.x : key === 'height' || key === 'top' ? this.rectScale.y : 1;
|
||||
this.el.style[key] = (value - containmentRect[key]) * scaleReciprocal + 'px';
|
||||
});
|
||||
return this;
|
||||
}
|
||||
/** @internal */
|
||||
_removeHandlers() {
|
||||
this.handlers.forEach(handle => handle.destroy());
|
||||
delete this.handlers;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
DDResizable._originStyleProp = ['width', 'height', 'position', 'left', 'top', 'opacity', 'zIndex'];
|
||||
export { DDResizable };
|
||||
//# sourceMappingURL=dd-resizable.js.map
|
||||
1
node_modules/gridstack/dist/dd-resizable.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-resizable.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
38
node_modules/gridstack/dist/dd-touch.d.ts
generated
vendored
Normal file
38
node_modules/gridstack/dist/dd-touch.d.ts
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* touch.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
/**
|
||||
* Detect touch support - Windows Surface devices and other touch devices
|
||||
* should we use this instead ? (what we had for always showing resize handles)
|
||||
* /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
*/
|
||||
export declare const isTouch: boolean;
|
||||
export declare class DDTouch {
|
||||
/** set to true while we are handling touch dragging, to prevent accepting browser real mouse events (trusted:true) vs our simulated ones (trusted:false) */
|
||||
static touchHandled: boolean;
|
||||
static pointerLeaveTimeout: number;
|
||||
}
|
||||
/**
|
||||
* Handle the touchstart events
|
||||
* @param {Object} e The widget element's touchstart event
|
||||
*/
|
||||
export declare function touchstart(e: TouchEvent): void;
|
||||
/**
|
||||
* Handle the touchmove events
|
||||
* @param {Object} e The document's touchmove event
|
||||
*/
|
||||
export declare function touchmove(e: TouchEvent): void;
|
||||
/**
|
||||
* Handle the touchend events
|
||||
* @param {Object} e The document's touchend event
|
||||
*/
|
||||
export declare function touchend(e: TouchEvent): void;
|
||||
/**
|
||||
* Note we don't get touchenter/touchleave (which are deprecated)
|
||||
* see https://stackoverflow.com/questions/27908339/js-touch-equivalent-for-mouseenter
|
||||
* so instead of PointerEvent to still get enter/leave and send the matching mouse event.
|
||||
*/
|
||||
export declare function pointerdown(e: PointerEvent): void;
|
||||
export declare function pointerenter(e: PointerEvent): void;
|
||||
export declare function pointerleave(e: PointerEvent): void;
|
||||
145
node_modules/gridstack/dist/dd-touch.js
generated
vendored
Normal file
145
node_modules/gridstack/dist/dd-touch.js
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* touch.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { DDManager } from './dd-manager';
|
||||
import { Utils } from './utils';
|
||||
/**
|
||||
* Detect touch support - Windows Surface devices and other touch devices
|
||||
* should we use this instead ? (what we had for always showing resize handles)
|
||||
* /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
*/
|
||||
export const isTouch = typeof window !== 'undefined' && typeof document !== 'undefined' &&
|
||||
('ontouchstart' in document
|
||||
|| 'ontouchstart' in window
|
||||
// || !!window.TouchEvent // true on Windows 10 Chrome desktop so don't use this
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|| (window.DocumentTouch && document instanceof window.DocumentTouch)
|
||||
|| navigator.maxTouchPoints > 0
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|| navigator.msMaxTouchPoints > 0);
|
||||
// interface TouchCoord {x: number, y: number};
|
||||
export class DDTouch {
|
||||
}
|
||||
/**
|
||||
* Get the x,y position of a touch event
|
||||
*/
|
||||
// function getTouchCoords(e: TouchEvent): TouchCoord {
|
||||
// return {
|
||||
// x: e.changedTouches[0].pageX,
|
||||
// y: e.changedTouches[0].pageY
|
||||
// };
|
||||
// }
|
||||
/**
|
||||
* Simulate a mouse event based on a corresponding touch event
|
||||
* @param {Object} e A touch event
|
||||
* @param {String} simulatedType The corresponding mouse event
|
||||
*/
|
||||
function simulateMouseEvent(e, simulatedType) {
|
||||
// Ignore multi-touch events
|
||||
if (e.touches.length > 1)
|
||||
return;
|
||||
// Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors
|
||||
if (e.cancelable)
|
||||
e.preventDefault();
|
||||
// Dispatch the simulated event to the target element
|
||||
Utils.simulateMouseEvent(e.changedTouches[0], simulatedType);
|
||||
}
|
||||
/**
|
||||
* Simulate a mouse event based on a corresponding Pointer event
|
||||
* @param {Object} e A pointer event
|
||||
* @param {String} simulatedType The corresponding mouse event
|
||||
*/
|
||||
function simulatePointerMouseEvent(e, simulatedType) {
|
||||
// Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors
|
||||
if (e.cancelable)
|
||||
e.preventDefault();
|
||||
// Dispatch the simulated event to the target element
|
||||
Utils.simulateMouseEvent(e, simulatedType);
|
||||
}
|
||||
/**
|
||||
* Handle the touchstart events
|
||||
* @param {Object} e The widget element's touchstart event
|
||||
*/
|
||||
export function touchstart(e) {
|
||||
// Ignore the event if another widget is already being handled
|
||||
if (DDTouch.touchHandled)
|
||||
return;
|
||||
DDTouch.touchHandled = true;
|
||||
// Simulate the mouse events
|
||||
// simulateMouseEvent(e, 'mouseover');
|
||||
// simulateMouseEvent(e, 'mousemove');
|
||||
simulateMouseEvent(e, 'mousedown');
|
||||
}
|
||||
/**
|
||||
* Handle the touchmove events
|
||||
* @param {Object} e The document's touchmove event
|
||||
*/
|
||||
export function touchmove(e) {
|
||||
// Ignore event if not handled by us
|
||||
if (!DDTouch.touchHandled)
|
||||
return;
|
||||
simulateMouseEvent(e, 'mousemove');
|
||||
}
|
||||
/**
|
||||
* Handle the touchend events
|
||||
* @param {Object} e The document's touchend event
|
||||
*/
|
||||
export function touchend(e) {
|
||||
// Ignore event if not handled
|
||||
if (!DDTouch.touchHandled)
|
||||
return;
|
||||
// cancel delayed leave event when we release on ourself which happens BEFORE we get this!
|
||||
if (DDTouch.pointerLeaveTimeout) {
|
||||
window.clearTimeout(DDTouch.pointerLeaveTimeout);
|
||||
delete DDTouch.pointerLeaveTimeout;
|
||||
}
|
||||
const wasDragging = !!DDManager.dragElement;
|
||||
// Simulate the mouseup event
|
||||
simulateMouseEvent(e, 'mouseup');
|
||||
// simulateMouseEvent(event, 'mouseout');
|
||||
// If the touch interaction did not move, it should trigger a click
|
||||
if (!wasDragging) {
|
||||
simulateMouseEvent(e, 'click');
|
||||
}
|
||||
// Unset the flag to allow other widgets to inherit the touch event
|
||||
DDTouch.touchHandled = false;
|
||||
}
|
||||
/**
|
||||
* Note we don't get touchenter/touchleave (which are deprecated)
|
||||
* see https://stackoverflow.com/questions/27908339/js-touch-equivalent-for-mouseenter
|
||||
* so instead of PointerEvent to still get enter/leave and send the matching mouse event.
|
||||
*/
|
||||
export function pointerdown(e) {
|
||||
// console.log("pointer down")
|
||||
if (e.pointerType === 'mouse')
|
||||
return;
|
||||
e.target.releasePointerCapture(e.pointerId); // <- Important!
|
||||
}
|
||||
export function pointerenter(e) {
|
||||
// ignore the initial one we get on pointerdown on ourself
|
||||
if (!DDManager.dragElement) {
|
||||
// console.log('pointerenter ignored');
|
||||
return;
|
||||
}
|
||||
// console.log('pointerenter');
|
||||
if (e.pointerType === 'mouse')
|
||||
return;
|
||||
simulatePointerMouseEvent(e, 'mouseenter');
|
||||
}
|
||||
export function pointerleave(e) {
|
||||
// ignore the leave on ourself we get before releasing the mouse over ourself
|
||||
// by delaying sending the event and having the up event cancel us
|
||||
if (!DDManager.dragElement) {
|
||||
// console.log('pointerleave ignored');
|
||||
return;
|
||||
}
|
||||
if (e.pointerType === 'mouse')
|
||||
return;
|
||||
DDTouch.pointerLeaveTimeout = window.setTimeout(() => {
|
||||
delete DDTouch.pointerLeaveTimeout;
|
||||
// console.log('pointerleave delayed');
|
||||
simulatePointerMouseEvent(e, 'mouseleave');
|
||||
}, 10);
|
||||
}
|
||||
//# sourceMappingURL=dd-touch.js.map
|
||||
1
node_modules/gridstack/dist/dd-touch.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/dd-touch.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
node_modules/gridstack/dist/gridstack-all.js
generated
vendored
Normal file
3
node_modules/gridstack/dist/gridstack-all.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
7
node_modules/gridstack/dist/gridstack-all.js.LICENSE.txt
generated
vendored
Normal file
7
node_modules/gridstack/dist/gridstack-all.js.LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* GridStack 12.4.2
|
||||
* https://gridstackjs.com/
|
||||
*
|
||||
* Copyright (c) 2021-2025 Alain Dumesny
|
||||
* see root license https://github.com/gridstack/gridstack.js/tree/master/LICENSE
|
||||
*/
|
||||
1
node_modules/gridstack/dist/gridstack-all.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/gridstack-all.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
321
node_modules/gridstack/dist/gridstack-engine.d.ts
generated
vendored
Normal file
321
node_modules/gridstack/dist/gridstack-engine.d.ts
generated
vendored
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* gridstack-engine.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { GridStackNode, GridStackPosition, GridStackMoveOpts, SaveFcn, CompactOptions } from './types';
|
||||
/** callback to update the DOM attributes since this class is generic (no HTML or other info) for items that changed - see _notify() */
|
||||
type OnChangeCB = (nodes: GridStackNode[]) => void;
|
||||
/** options used during creation - similar to GridStackOptions */
|
||||
export interface GridStackEngineOptions {
|
||||
column?: number;
|
||||
maxRow?: number;
|
||||
float?: boolean;
|
||||
nodes?: GridStackNode[];
|
||||
onChange?: OnChangeCB;
|
||||
}
|
||||
/**
|
||||
* Defines the GridStack engine that handles all grid layout calculations and node positioning.
|
||||
* This is the core engine that performs grid manipulation without any DOM operations.
|
||||
*
|
||||
* The engine manages:
|
||||
* - Node positioning and collision detection
|
||||
* - Layout algorithms (compact, float, etc.)
|
||||
* - Grid resizing and column changes
|
||||
* - Widget movement and resizing logic
|
||||
*
|
||||
* NOTE: Values should not be modified directly - use the main GridStack API instead
|
||||
* to ensure proper DOM updates and event triggers.
|
||||
*/
|
||||
export declare class GridStackEngine {
|
||||
column: number;
|
||||
maxRow: number;
|
||||
nodes: GridStackNode[];
|
||||
addedNodes: GridStackNode[];
|
||||
removedNodes: GridStackNode[];
|
||||
batchMode: boolean;
|
||||
defaultColumn: number;
|
||||
/** true when grid.load() already cached the layout and can skip out of bound caching info */
|
||||
skipCacheUpdate?: boolean;
|
||||
constructor(opts?: GridStackEngineOptions);
|
||||
/**
|
||||
* Enable/disable batch mode for multiple operations to optimize performance.
|
||||
* When enabled, layout updates are deferred until batch mode is disabled.
|
||||
*
|
||||
* @param flag true to enable batch mode, false to disable and apply changes
|
||||
* @param doPack if true (default), pack/compact nodes when disabling batch mode
|
||||
* @returns the engine instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Start batch mode for multiple operations
|
||||
* engine.batchUpdate(true);
|
||||
* engine.addNode(node1);
|
||||
* engine.addNode(node2);
|
||||
* engine.batchUpdate(false); // Apply all changes at once
|
||||
*/
|
||||
batchUpdate(flag?: boolean, doPack?: boolean): GridStackEngine;
|
||||
protected _useEntireRowArea(node: GridStackNode, nn: GridStackPosition): boolean;
|
||||
/**
|
||||
* Return the first node that intercepts/collides with the given node or area.
|
||||
* Used for collision detection during drag and drop operations.
|
||||
*
|
||||
* @param skip the node to skip in collision detection (usually the node being moved)
|
||||
* @param area the area to check for collisions (defaults to skip node's area)
|
||||
* @param skip2 optional second node to skip in collision detection
|
||||
* @returns the first colliding node, or undefined if no collision
|
||||
*
|
||||
* @example
|
||||
* const colliding = engine.collide(draggedNode, {x: 2, y: 1, w: 2, h: 1});
|
||||
* if (colliding) {
|
||||
* console.log('Would collide with:', colliding.id);
|
||||
* }
|
||||
*/
|
||||
collide(skip: GridStackNode, area?: GridStackNode, skip2?: GridStackNode): GridStackNode | undefined;
|
||||
/**
|
||||
* Return all nodes that intercept/collide with the given node or area.
|
||||
* Similar to collide() but returns all colliding nodes instead of just the first.
|
||||
*
|
||||
* @param skip the node to skip in collision detection
|
||||
* @param area the area to check for collisions (defaults to skip node's area)
|
||||
* @param skip2 optional second node to skip in collision detection
|
||||
* @returns array of all colliding nodes
|
||||
*
|
||||
* @example
|
||||
* const allCollisions = engine.collideAll(draggedNode);
|
||||
* console.log('Colliding with', allCollisions.length, 'nodes');
|
||||
*/
|
||||
collideAll(skip: GridStackNode, area?: GridStackNode, skip2?: GridStackNode): GridStackNode[];
|
||||
/** does a pixel coverage collision based on where we started, returning the node that has the most coverage that is >50% mid line */
|
||||
protected directionCollideCoverage(node: GridStackNode, o: GridStackMoveOpts, collides: GridStackNode[]): GridStackNode | undefined;
|
||||
/**
|
||||
* Attempt to swap the positions of two nodes if they meet swapping criteria.
|
||||
* Nodes can swap if they are the same size or in the same column/row, not locked, and touching.
|
||||
*
|
||||
* @param a first node to swap
|
||||
* @param b second node to swap
|
||||
* @returns true if swap was successful, false if not possible, undefined if not applicable
|
||||
*
|
||||
* @example
|
||||
* const swapped = engine.swap(nodeA, nodeB);
|
||||
* if (swapped) {
|
||||
* console.log('Nodes swapped successfully');
|
||||
* }
|
||||
*/
|
||||
swap(a: GridStackNode, b: GridStackNode): boolean | undefined;
|
||||
/**
|
||||
* Check if the specified rectangular area is empty (no nodes occupy any part of it).
|
||||
*
|
||||
* @param x the x coordinate (column) of the area to check
|
||||
* @param y the y coordinate (row) of the area to check
|
||||
* @param w the width in columns of the area to check
|
||||
* @param h the height in rows of the area to check
|
||||
* @returns true if the area is completely empty, false if any node overlaps
|
||||
*
|
||||
* @example
|
||||
* if (engine.isAreaEmpty(2, 1, 3, 2)) {
|
||||
* console.log('Area is available for placement');
|
||||
* }
|
||||
*/
|
||||
isAreaEmpty(x: number, y: number, w: number, h: number): boolean;
|
||||
/**
|
||||
* Re-layout grid items to reclaim any empty space.
|
||||
* This optimizes the grid layout by moving items to fill gaps.
|
||||
*
|
||||
* @param layout layout algorithm to use:
|
||||
* - 'compact' (default): find truly empty spaces, may reorder items
|
||||
* - 'list': keep the sort order exactly the same, move items up sequentially
|
||||
* @param doSort if true (default), sort nodes by position before compacting
|
||||
* @returns the engine instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Compact to fill empty spaces
|
||||
* engine.compact();
|
||||
*
|
||||
* // Compact preserving item order
|
||||
* engine.compact('list');
|
||||
*/
|
||||
compact(layout?: CompactOptions, doSort?: boolean): GridStackEngine;
|
||||
/**
|
||||
* Enable/disable floating widgets (default: `false`).
|
||||
* When floating is enabled, widgets can move up to fill empty spaces.
|
||||
* See [example](http://gridstackjs.com/demo/float.html)
|
||||
*
|
||||
* @param val true to enable floating, false to disable
|
||||
*
|
||||
* @example
|
||||
* engine.float = true; // Enable floating
|
||||
* engine.float = false; // Disable floating (default)
|
||||
*/
|
||||
set float(val: boolean);
|
||||
/**
|
||||
* Get the current floating mode setting.
|
||||
*
|
||||
* @returns true if floating is enabled, false otherwise
|
||||
*
|
||||
* @example
|
||||
* const isFloating = engine.float;
|
||||
* console.log('Floating enabled:', isFloating);
|
||||
*/
|
||||
get float(): boolean;
|
||||
/**
|
||||
* Sort the nodes array from first to last, or reverse.
|
||||
* This is called during collision/placement operations to enforce a specific order.
|
||||
*
|
||||
* @param dir sort direction: 1 for ascending (first to last), -1 for descending (last to first)
|
||||
* @returns the engine instance for chaining
|
||||
*
|
||||
* @example
|
||||
* engine.sortNodes(); // Sort ascending (default)
|
||||
* engine.sortNodes(-1); // Sort descending
|
||||
*/
|
||||
sortNodes(dir?: 1 | -1): GridStackEngine;
|
||||
/**
|
||||
* Prepare and validate a node's coordinates and values for the current grid.
|
||||
* This ensures the node has valid position, size, and properties before being added to the grid.
|
||||
*
|
||||
* @param node the node to prepare and validate
|
||||
* @param resizing if true, resize the node down if it's out of bounds; if false, move it to fit
|
||||
* @returns the prepared node with valid coordinates
|
||||
*
|
||||
* @example
|
||||
* const node = { w: 3, h: 2, content: 'Hello' };
|
||||
* const prepared = engine.prepareNode(node);
|
||||
* console.log('Node prepared at:', prepared.x, prepared.y);
|
||||
*/
|
||||
prepareNode(node: GridStackNode, resizing?: boolean): GridStackNode;
|
||||
/**
|
||||
* Part 2 of preparing a node to fit inside the grid - validates and fixes coordinates and dimensions.
|
||||
* This ensures the node fits within grid boundaries and respects min/max constraints.
|
||||
*
|
||||
* @param node the node to validate and fix
|
||||
* @param resizing if true, resize the node to fit; if false, move the node to fit
|
||||
* @returns the engine instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Fix a node that might be out of bounds
|
||||
* engine.nodeBoundFix(node, true); // Resize to fit
|
||||
* engine.nodeBoundFix(node, false); // Move to fit
|
||||
*/
|
||||
nodeBoundFix(node: GridStackNode, resizing?: boolean): GridStackEngine;
|
||||
/**
|
||||
* Returns a list of nodes that have been modified from their original values.
|
||||
* This is used to track which nodes need DOM updates.
|
||||
*
|
||||
* @param verify if true, performs additional verification by comparing current vs original positions
|
||||
* @returns array of nodes that have been modified
|
||||
*
|
||||
* @example
|
||||
* const changed = engine.getDirtyNodes();
|
||||
* console.log('Modified nodes:', changed.length);
|
||||
*
|
||||
* // Get verified dirty nodes
|
||||
* const verified = engine.getDirtyNodes(true);
|
||||
*/
|
||||
getDirtyNodes(verify?: boolean): GridStackNode[];
|
||||
/**
|
||||
* Find the first available empty spot for the given node dimensions.
|
||||
* Updates the node's x,y attributes with the found position.
|
||||
*
|
||||
* @param node the node to find a position for (w,h must be set)
|
||||
* @param nodeList optional list of nodes to check against (defaults to engine nodes)
|
||||
* @param column optional column count (defaults to engine column count)
|
||||
* @param after optional node to start search after (maintains order)
|
||||
* @returns true if an empty position was found and node was updated
|
||||
*
|
||||
* @example
|
||||
* const node = { w: 2, h: 1 };
|
||||
* if (engine.findEmptyPosition(node)) {
|
||||
* console.log('Found position at:', node.x, node.y);
|
||||
* }
|
||||
*/
|
||||
findEmptyPosition(node: GridStackNode, nodeList?: GridStackNode[], column?: number, after?: GridStackNode): boolean;
|
||||
/**
|
||||
* Add the given node to the grid, handling collision detection and re-packing.
|
||||
* This is the main method for adding new widgets to the engine.
|
||||
*
|
||||
* @param node the node to add to the grid
|
||||
* @param triggerAddEvent if true, adds node to addedNodes list for event triggering
|
||||
* @param after optional node to place this node after (for ordering)
|
||||
* @returns the added node (or existing node if duplicate)
|
||||
*
|
||||
* @example
|
||||
* const node = { x: 0, y: 0, w: 2, h: 1, content: 'Hello' };
|
||||
* const added = engine.addNode(node, true);
|
||||
*/
|
||||
addNode(node: GridStackNode, triggerAddEvent?: boolean, after?: GridStackNode): GridStackNode;
|
||||
/**
|
||||
* Remove the given node from the grid.
|
||||
*
|
||||
* @param node the node to remove
|
||||
* @param removeDOM if true (default), marks node for DOM removal
|
||||
* @param triggerEvent if true, adds node to removedNodes list for event triggering
|
||||
* @returns the engine instance for chaining
|
||||
*
|
||||
* @example
|
||||
* engine.removeNode(node, true, true);
|
||||
*/
|
||||
removeNode(node: GridStackNode, removeDOM?: boolean, triggerEvent?: boolean): GridStackEngine;
|
||||
/**
|
||||
* Remove all nodes from the grid.
|
||||
*
|
||||
* @param removeDOM if true (default), marks all nodes for DOM removal
|
||||
* @param triggerEvent if true (default), triggers removal events
|
||||
* @returns the engine instance for chaining
|
||||
*
|
||||
* @example
|
||||
* engine.removeAll(); // Remove all nodes
|
||||
*/
|
||||
removeAll(removeDOM?: boolean, triggerEvent?: boolean): GridStackEngine;
|
||||
/**
|
||||
* Check if a node can be moved to a new position, considering layout constraints.
|
||||
* This is a safer version of moveNode() that validates the move first.
|
||||
*
|
||||
* For complex cases (like maxRow constraints), it simulates the move in a clone first,
|
||||
* then applies the changes only if they meet all specifications.
|
||||
*
|
||||
* @param node the node to move
|
||||
* @param o move options including target position
|
||||
* @returns true if the node was successfully moved
|
||||
*
|
||||
* @example
|
||||
* const canMove = engine.moveNodeCheck(node, { x: 2, y: 1 });
|
||||
* if (canMove) {
|
||||
* console.log('Node moved successfully');
|
||||
* }
|
||||
*/
|
||||
moveNodeCheck(node: GridStackNode, o: GridStackMoveOpts): boolean;
|
||||
/** return true if can fit in grid height constrain only (always true if no maxRow) */
|
||||
willItFit(node: GridStackNode): boolean;
|
||||
/** true if x,y or w,h are different after clamping to min/max */
|
||||
changedPosConstrain(node: GridStackNode, p: GridStackPosition): boolean;
|
||||
/** return true if the passed in node was actually moved (checks for no-op and locked) */
|
||||
moveNode(node: GridStackNode, o: GridStackMoveOpts): boolean;
|
||||
getRow(): number;
|
||||
beginUpdate(node: GridStackNode): GridStackEngine;
|
||||
endUpdate(): GridStackEngine;
|
||||
/** saves a copy of the largest column layout (eg 12 even when rendering 1 column) so we don't loose orig layout, unless explicity column
|
||||
* count to use is given. returning a list of widgets for serialization
|
||||
* @param saveElement if true (default), the element will be saved to GridStackWidget.el field, else it will be removed.
|
||||
* @param saveCB callback for each node -> widget, so application can insert additional data to be saved into the widget data structure.
|
||||
* @param column if provided, the grid will be saved for the given column count (IFF we have matching internal saved layout, or current layout).
|
||||
* Note: nested grids will ALWAYS save the container w to match overall layouts (parent + child) to be consistent.
|
||||
*/
|
||||
save(saveElement?: boolean, saveCB?: SaveFcn, column?: number): GridStackNode[];
|
||||
/**
|
||||
* call to cache the given layout internally to the given location so we can restore back when column changes size
|
||||
* @param nodes list of nodes
|
||||
* @param column corresponding column index to save it under
|
||||
* @param clear if true, will force other caches to be removed (default false)
|
||||
*/
|
||||
cacheLayout(nodes: GridStackNode[], column: number, clear?: boolean): GridStackEngine;
|
||||
/**
|
||||
* call to cache the given node layout internally to the given location so we can restore back when column changes size
|
||||
* @param node single node to cache
|
||||
* @param column corresponding column index to save it under
|
||||
*/
|
||||
cacheOneLayout(n: GridStackNode, column: number): GridStackEngine;
|
||||
protected findCacheLayout(n: GridStackNode, column: number): number | undefined;
|
||||
removeNodeFromLayoutCache(n: GridStackNode): void;
|
||||
/** called to remove all internal values but the _id */
|
||||
cleanupNode(node: GridStackNode): GridStackEngine;
|
||||
}
|
||||
export {};
|
||||
1278
node_modules/gridstack/dist/gridstack-engine.js
generated
vendored
Normal file
1278
node_modules/gridstack/dist/gridstack-engine.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/gridstack/dist/gridstack-engine.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/gridstack-engine.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
174
node_modules/gridstack/dist/gridstack.css
generated
vendored
Normal file
174
node_modules/gridstack/dist/gridstack.css
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* gridstack SASS styles 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
.grid-stack {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-stack-rtl {
|
||||
direction: ltr;
|
||||
}
|
||||
.grid-stack-rtl > .grid-stack-item {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.grid-stack-placeholder > .placeholder-content {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
.grid-stack > .grid-stack-item {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--gs-column-width);
|
||||
height: var(--gs-cell-height);
|
||||
}
|
||||
.grid-stack > .grid-stack-item > .grid-stack-item-content {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.grid-stack > .grid-stack-item.size-to-content:not(.size-to-content-max) > .grid-stack-item-content {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.grid-stack > .grid-stack-item > .grid-stack-item-content,
|
||||
.grid-stack > .grid-stack-placeholder > .placeholder-content {
|
||||
top: var(--gs-item-margin-top);
|
||||
right: var(--gs-item-margin-right);
|
||||
bottom: var(--gs-item-margin-bottom);
|
||||
left: var(--gs-item-margin-left);
|
||||
}
|
||||
|
||||
.grid-stack-item > .ui-resizable-handle {
|
||||
position: absolute;
|
||||
font-size: 0.1px;
|
||||
display: block;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.grid-stack-item.ui-resizable-disabled > .ui-resizable-handle, .grid-stack-item.ui-resizable-autohide > .ui-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-ne,
|
||||
.grid-stack-item > .ui-resizable-nw,
|
||||
.grid-stack-item > .ui-resizable-se,
|
||||
.grid-stack-item > .ui-resizable-sw {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%23666" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 20 20"><path d="m10 3 2 2H8l2-2v14l-2-2h4l-2 2"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-ne {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-sw {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-nw {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-se {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-nw {
|
||||
cursor: nw-resize;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: var(--gs-item-margin-top);
|
||||
left: var(--gs-item-margin-left);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-n {
|
||||
cursor: n-resize;
|
||||
height: 10px;
|
||||
top: var(--gs-item-margin-top);
|
||||
left: 25px;
|
||||
right: 25px;
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-ne {
|
||||
cursor: ne-resize;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: var(--gs-item-margin-top);
|
||||
right: var(--gs-item-margin-right);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-e {
|
||||
cursor: e-resize;
|
||||
width: 10px;
|
||||
top: 15px;
|
||||
bottom: 15px;
|
||||
right: var(--gs-item-margin-right);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-se {
|
||||
cursor: se-resize;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: var(--gs-item-margin-bottom);
|
||||
right: var(--gs-item-margin-right);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-s {
|
||||
cursor: s-resize;
|
||||
height: 10px;
|
||||
left: 25px;
|
||||
bottom: var(--gs-item-margin-bottom);
|
||||
right: 25px;
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-sw {
|
||||
cursor: sw-resize;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: var(--gs-item-margin-bottom);
|
||||
left: var(--gs-item-margin-left);
|
||||
}
|
||||
.grid-stack-item > .ui-resizable-w {
|
||||
cursor: w-resize;
|
||||
width: 10px;
|
||||
top: 15px;
|
||||
bottom: 15px;
|
||||
left: var(--gs-item-margin-left);
|
||||
}
|
||||
.grid-stack-item.ui-draggable-dragging > .ui-resizable-handle {
|
||||
display: none !important;
|
||||
}
|
||||
.grid-stack-item.ui-draggable-dragging {
|
||||
will-change: left, top;
|
||||
}
|
||||
.grid-stack-item.ui-resizable-resizing {
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.ui-draggable-dragging,
|
||||
.ui-resizable-resizing {
|
||||
z-index: 10000;
|
||||
}
|
||||
.ui-draggable-dragging > .grid-stack-item-content,
|
||||
.ui-resizable-resizing > .grid-stack-item-content {
|
||||
box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.grid-stack-animate,
|
||||
.grid-stack-animate .grid-stack-item {
|
||||
transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
|
||||
}
|
||||
|
||||
.grid-stack-animate .grid-stack-item.ui-draggable-dragging,
|
||||
.grid-stack-animate .grid-stack-item.ui-resizable-resizing,
|
||||
.grid-stack-animate .grid-stack-item.grid-stack-placeholder {
|
||||
transition: left 0s, top 0s, height 0s, width 0s;
|
||||
}
|
||||
|
||||
.grid-stack > .grid-stack-item[gs-y="0"] {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.grid-stack > .grid-stack-item[gs-x="0"] {
|
||||
left: 0%;
|
||||
}
|
||||
801
node_modules/gridstack/dist/gridstack.d.ts
generated
vendored
Normal file
801
node_modules/gridstack/dist/gridstack.d.ts
generated
vendored
Normal file
@@ -0,0 +1,801 @@
|
||||
/*!
|
||||
* GridStack 12.4.2
|
||||
* https://gridstackjs.com/
|
||||
*
|
||||
* Copyright (c) 2021-2025 Alain Dumesny
|
||||
* see root license https://github.com/gridstack/gridstack.js/tree/master/LICENSE
|
||||
*/
|
||||
import { GridStackEngine } from './gridstack-engine';
|
||||
import { Utils } from './utils';
|
||||
import { ColumnOptions, GridItemHTMLElement, GridStackElement, GridStackEventHandlerCallback, GridStackNode, GridStackWidget, numberOrString, DDDragOpt, GridStackOptions, GridStackEventHandler, GridStackNodesHandler, AddRemoveFcn, SaveFcn, CompactOptions, ResizeToContentFcn, GridStackDroppedHandler, GridStackElementHandler, Position, RenderFcn } from './types';
|
||||
import { DDGridStack } from './dd-gridstack';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './gridstack-engine';
|
||||
export * from './dd-gridstack';
|
||||
export * from './dd-manager';
|
||||
export * from './dd-element';
|
||||
export * from './dd-draggable';
|
||||
export * from './dd-droppable';
|
||||
export * from './dd-resizable';
|
||||
export * from './dd-resizable-handle';
|
||||
export * from './dd-base-impl';
|
||||
export interface GridHTMLElement extends HTMLElement {
|
||||
gridstack?: GridStack;
|
||||
}
|
||||
/** list of possible events, or space separated list of them */
|
||||
export type GridStackEvent = 'added' | 'change' | 'disable' | 'drag' | 'dragstart' | 'dragstop' | 'dropped' | 'enable' | 'removed' | 'resize' | 'resizestart' | 'resizestop' | 'resizecontent';
|
||||
/** Defines the coordinates of an object */
|
||||
export interface MousePosition {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
/** Defines the position of a cell inside the grid*/
|
||||
export interface CellPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
/**
|
||||
* Main gridstack class - you will need to call `GridStack.init()` first to initialize your grid.
|
||||
* Note: your grid elements MUST have the following classes for the CSS layout to work:
|
||||
* @example
|
||||
* <div class="grid-stack">
|
||||
* <div class="grid-stack-item">
|
||||
* <div class="grid-stack-item-content">Item 1</div>
|
||||
* </div>
|
||||
* </div>
|
||||
*/
|
||||
export declare class GridStack {
|
||||
el: GridHTMLElement;
|
||||
opts: GridStackOptions;
|
||||
/**
|
||||
* initializing the HTML element, or selector string, into a grid will return the grid. Calling it again will
|
||||
* simply return the existing instance (ignore any passed options). There is also an initAll() version that support
|
||||
* multiple grids initialization at once. Or you can use addGrid() to create the entire grid from JSON.
|
||||
* @param options grid options (optional)
|
||||
* @param elOrString element or CSS selector (first one used) to convert to a grid (default to '.grid-stack' class selector)
|
||||
*
|
||||
* @example
|
||||
* const grid = GridStack.init();
|
||||
*
|
||||
* Note: the HTMLElement (of type GridHTMLElement) will store a `gridstack: GridStack` value that can be retrieve later
|
||||
* const grid = document.querySelector('.grid-stack').gridstack;
|
||||
*/
|
||||
static init(options?: GridStackOptions, elOrString?: GridStackElement): GridStack;
|
||||
/**
|
||||
* Will initialize a list of elements (given a selector) and return an array of grids.
|
||||
* @param options grid options (optional)
|
||||
* @param selector elements selector to convert to grids (default to '.grid-stack' class selector)
|
||||
*
|
||||
* @example
|
||||
* const grids = GridStack.initAll();
|
||||
* grids.forEach(...)
|
||||
*/
|
||||
static initAll(options?: GridStackOptions, selector?: string): GridStack[];
|
||||
/**
|
||||
* call to create a grid with the given options, including loading any children from JSON structure. This will call GridStack.init(), then
|
||||
* grid.load() on any passed children (recursively). Great alternative to calling init() if you want entire grid to come from
|
||||
* JSON serialized data, including options.
|
||||
* @param parent HTML element parent to the grid
|
||||
* @param opt grids options used to initialize the grid, and list of children
|
||||
*/
|
||||
static addGrid(parent: HTMLElement, opt?: GridStackOptions): GridStack;
|
||||
/** call this method to register your engine instead of the default one.
|
||||
* See instead `GridStackOptions.engineClass` if you only need to
|
||||
* replace just one instance.
|
||||
*/
|
||||
static registerEngine(engineClass: typeof GridStackEngine): void;
|
||||
/**
|
||||
* callback method use when new items|grids needs to be created or deleted, instead of the default
|
||||
* item: <div class="grid-stack-item"><div class="grid-stack-item-content">w.content</div></div>
|
||||
* grid: <div class="grid-stack">grid content...</div>
|
||||
* add = true: the returned DOM element will then be converted to a GridItemHTMLElement using makeWidget()|GridStack:init().
|
||||
* add = false: the item will be removed from DOM (if not already done)
|
||||
* grid = true|false for grid vs grid-items
|
||||
*/
|
||||
static addRemoveCB?: AddRemoveFcn;
|
||||
/**
|
||||
* callback during saving to application can inject extra data for each widget, on top of the grid layout properties
|
||||
*/
|
||||
static saveCB?: SaveFcn;
|
||||
/**
|
||||
* callback to create the content of widgets so the app can control how to store and restore it
|
||||
* By default this lib will do 'el.textContent = w.content' forcing text only support for avoiding potential XSS issues.
|
||||
*/
|
||||
static renderCB?: RenderFcn;
|
||||
/** called after a widget has been updated (eg: load() into an existing list of children) so application can do extra work */
|
||||
static updateCB?: (w: GridStackNode) => void;
|
||||
/** callback to use for resizeToContent instead of the built in one */
|
||||
static resizeToContentCB?: ResizeToContentFcn;
|
||||
/** parent class for sizing content. defaults to '.grid-stack-item-content' */
|
||||
static resizeToContentParent: string;
|
||||
/** scoping so users can call GridStack.Utils.sort() for example */
|
||||
static Utils: typeof Utils;
|
||||
/** scoping so users can call new GridStack.Engine(12) for example */
|
||||
static Engine: typeof GridStackEngine;
|
||||
/** engine used to implement non DOM grid functionality */
|
||||
engine: GridStackEngine;
|
||||
/** point to a parent grid item if we're nested (inside a grid-item in between 2 Grids) */
|
||||
parentGridNode?: GridStackNode;
|
||||
/** time to wait for animation (if enabled) to be done so content sizing can happen */
|
||||
animationDelay: number;
|
||||
protected static engineClass: typeof GridStackEngine;
|
||||
protected resizeObserver: ResizeObserver;
|
||||
protected responseLayout: ColumnOptions;
|
||||
private _skipInitialResize;
|
||||
/**
|
||||
* Construct a grid item from the given element and options
|
||||
* @param el the HTML element tied to this grid after it's been initialized
|
||||
* @param opts grid options - public for classes to access, but use methods to modify!
|
||||
*/
|
||||
constructor(el: GridHTMLElement, opts?: GridStackOptions);
|
||||
private _updateColumnVar;
|
||||
/**
|
||||
* add a new widget and returns it.
|
||||
*
|
||||
* Widget will be always placed even if result height is more than actual grid height.
|
||||
* You need to use `willItFit()` before calling addWidget for additional check.
|
||||
* See also `makeWidget(el)` for DOM element.
|
||||
*
|
||||
* @example
|
||||
* const grid = GridStack.init();
|
||||
* grid.addWidget({w: 3, content: 'hello'});
|
||||
*
|
||||
* @param w GridStackWidget definition. used MakeWidget(el) if you have dom element instead.
|
||||
*/
|
||||
addWidget(w: GridStackWidget): GridItemHTMLElement;
|
||||
/**
|
||||
* Create the default grid item divs and content (possibly lazy loaded) by using GridStack.renderCB().
|
||||
*
|
||||
* @param n GridStackNode definition containing widget configuration
|
||||
* @returns the created HTML element with proper grid item structure
|
||||
*
|
||||
* @example
|
||||
* const element = grid.createWidgetDivs({ w: 2, h: 1, content: 'Hello World' });
|
||||
*/
|
||||
createWidgetDivs(n: GridStackNode): HTMLElement;
|
||||
/**
|
||||
* Convert an existing gridItem element into a sub-grid with the given (optional) options, else inherit them
|
||||
* from the parent's subGrid options.
|
||||
* @param el gridItem element to convert
|
||||
* @param ops (optional) sub-grid options, else default to node, then parent settings, else defaults
|
||||
* @param nodeToAdd (optional) node to add to the newly created sub grid (used when dragging over existing regular item)
|
||||
* @param saveContent if true (default) the html inside .grid-stack-content will be saved to child widget
|
||||
* @returns newly created grid
|
||||
*/
|
||||
makeSubGrid(el: GridItemHTMLElement, ops?: GridStackOptions, nodeToAdd?: GridStackNode, saveContent?: boolean): GridStack;
|
||||
/**
|
||||
* called when an item was converted into a nested grid to accommodate a dragged over item, but then item leaves - return back
|
||||
* to the original grid-item. Also called to remove empty sub-grids when last item is dragged out (since re-creating is simple)
|
||||
*/
|
||||
removeAsSubGrid(nodeThatRemoved?: GridStackNode): void;
|
||||
/**
|
||||
* saves the current layout returning a list of widgets for serialization which might include any nested grids.
|
||||
* @param saveContent if true (default) the latest html inside .grid-stack-content will be saved to GridStackWidget.content field, else it will
|
||||
* be removed.
|
||||
* @param saveGridOpt if true (default false), save the grid options itself, so you can call the new GridStack.addGrid()
|
||||
* to recreate everything from scratch. GridStackOptions.children would then contain the widget list instead.
|
||||
* @param saveCB callback for each node -> widget, so application can insert additional data to be saved into the widget data structure.
|
||||
* @param column if provided, the grid will be saved for the given column size (IFF we have matching internal saved layout, or current layout).
|
||||
* Otherwise it will use the largest possible layout (say 12 even if rendering at 1 column) so we can restore to all layouts.
|
||||
* NOTE: if you want to save to currently display layout, pass this.getColumn() as column.
|
||||
* NOTE2: nested grids will ALWAYS save to the container size to be in sync with parent.
|
||||
* @returns list of widgets or full grid option, including .children list of widgets
|
||||
*/
|
||||
save(saveContent?: boolean, saveGridOpt?: boolean, saveCB?: SaveFcn, column?: number): GridStackWidget[] | GridStackOptions;
|
||||
/**
|
||||
* Load widgets from a list. This will call update() on each (matching by id) or add/remove widgets that are not there.
|
||||
* Used to restore a grid layout for a saved layout list (see `save()`).
|
||||
*
|
||||
* @param items list of widgets definition to update/create
|
||||
* @param addRemove boolean (default true) or callback method can be passed to control if and how missing widgets can be added/removed, giving
|
||||
* the user control of insertion.
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with saved layout
|
||||
* const savedLayout = grid.save(); // Save current layout
|
||||
* // ... later restore it
|
||||
* grid.load(savedLayout);
|
||||
*
|
||||
* // Load with custom add/remove callback
|
||||
* grid.load(layout, (items, grid, add) => {
|
||||
* if (add) {
|
||||
* // Custom logic for adding new widgets
|
||||
* items.forEach(item => {
|
||||
* const el = document.createElement('div');
|
||||
* el.innerHTML = item.content || '';
|
||||
* grid.addWidget(el, item);
|
||||
* });
|
||||
* } else {
|
||||
* // Custom logic for removing widgets
|
||||
* items.forEach(item => grid.removeWidget(item.el));
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Load without adding/removing missing widgets
|
||||
* grid.load(layout, false);
|
||||
*
|
||||
* @see {@link http://gridstackjs.com/demo/serialization.html} for complete example
|
||||
*/
|
||||
load(items: GridStackWidget[], addRemove?: boolean | AddRemoveFcn): GridStack;
|
||||
/**
|
||||
* use before calling a bunch of `addWidget()` to prevent un-necessary relayouts in between (more efficient)
|
||||
* and get a single event callback. You will see no changes until `batchUpdate(false)` is called.
|
||||
*/
|
||||
batchUpdate(flag?: boolean): GridStack;
|
||||
/**
|
||||
* Gets the current cell height in pixels. This takes into account the unit type and converts to pixels if necessary.
|
||||
*
|
||||
* @param forcePixel if true, forces conversion to pixels even when cellHeight is specified in other units
|
||||
* @returns the cell height in pixels
|
||||
*
|
||||
* @example
|
||||
* const height = grid.getCellHeight();
|
||||
* console.log('Cell height:', height, 'px');
|
||||
*
|
||||
* // Force pixel conversion
|
||||
* const pixelHeight = grid.getCellHeight(true);
|
||||
*/
|
||||
getCellHeight(forcePixel?: boolean): number;
|
||||
/**
|
||||
* Update current cell height - see `GridStackOptions.cellHeight` for format by updating eh Browser CSS variable.
|
||||
*
|
||||
* @param val the cell height. Options:
|
||||
* - `undefined`: cells content will be made square (match width minus margin)
|
||||
* - `0`: the CSS will be generated by the application instead
|
||||
* - number: height in pixels
|
||||
* - string: height with units (e.g., '70px', '5rem', '2em')
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* grid.cellHeight(100); // 100px height
|
||||
* grid.cellHeight('70px'); // explicit pixel height
|
||||
* grid.cellHeight('5rem'); // relative to root font size
|
||||
* grid.cellHeight(grid.cellWidth() * 1.2); // aspect ratio
|
||||
* grid.cellHeight('auto'); // auto-size based on content
|
||||
*/
|
||||
cellHeight(val?: numberOrString): GridStack;
|
||||
/** Gets current cell width. */
|
||||
/**
|
||||
* Gets the current cell width in pixels. This is calculated based on the grid container width divided by the number of columns.
|
||||
*
|
||||
* @returns the cell width in pixels
|
||||
*
|
||||
* @example
|
||||
* const width = grid.cellWidth();
|
||||
* console.log('Cell width:', width, 'px');
|
||||
*
|
||||
* // Use cell width to calculate widget dimensions
|
||||
* const widgetWidth = width * 3; // For a 3-column wide widget
|
||||
*/
|
||||
cellWidth(): number;
|
||||
/** return our expected width (or parent) , and optionally of window for dynamic column check */
|
||||
protected _widthOrContainer(forBreakpoint?: boolean): number;
|
||||
/** checks for dynamic column count for our current size, returning true if changed */
|
||||
protected checkDynamicColumn(): boolean;
|
||||
/**
|
||||
* Re-layout grid items to reclaim any empty space. This is useful after removing widgets
|
||||
* or when you want to optimize the layout.
|
||||
*
|
||||
* @param layout layout type. Options:
|
||||
* - 'compact' (default): might re-order items to fill any empty space
|
||||
* - 'list': keep the widget left->right order the same, even if that means leaving an empty slot if things don't fit
|
||||
* @param doSort re-sort items first based on x,y position. Set to false to do your own sorting ahead (default: true)
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Compact layout after removing widgets
|
||||
* grid.removeWidget('.widget-to-remove');
|
||||
* grid.compact();
|
||||
*
|
||||
* // Use list layout (preserve order)
|
||||
* grid.compact('list');
|
||||
*
|
||||
* // Compact without sorting first
|
||||
* grid.compact('compact', false);
|
||||
*/
|
||||
compact(layout?: CompactOptions, doSort?: boolean): GridStack;
|
||||
/**
|
||||
* Set the number of columns in the grid. Will update existing widgets to conform to new number of columns,
|
||||
* as well as cache the original layout so you can revert back to previous positions without loss.
|
||||
*
|
||||
* Requires `gridstack-extra.css` or `gridstack-extra.min.css` for [2-11] columns,
|
||||
* else you will need to generate correct CSS.
|
||||
* See: https://github.com/gridstack/gridstack.js#change-grid-columns
|
||||
*
|
||||
* @param column Integer > 0 (default 12)
|
||||
* @param layout specify the type of re-layout that will happen. Options:
|
||||
* - 'moveScale' (default): scale widget positions and sizes
|
||||
* - 'move': keep widget sizes, only move positions
|
||||
* - 'scale': keep widget positions, only scale sizes
|
||||
* - 'none': don't change widget positions or sizes
|
||||
* Note: items will never be outside of the current column boundaries.
|
||||
* Ignored for `column=1` as we always want to vertically stack.
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Change to 6 columns with default scaling
|
||||
* grid.column(6);
|
||||
*
|
||||
* // Change to 4 columns, only move positions
|
||||
* grid.column(4, 'move');
|
||||
*
|
||||
* // Single column layout (vertical stack)
|
||||
* grid.column(1);
|
||||
*/
|
||||
column(column: number, layout?: ColumnOptions): GridStack;
|
||||
/**
|
||||
* Get the number of columns in the grid (default 12).
|
||||
*
|
||||
* @returns the current number of columns in the grid
|
||||
*
|
||||
* @example
|
||||
* const columnCount = grid.getColumn(); // returns 12 by default
|
||||
*/
|
||||
getColumn(): number;
|
||||
/**
|
||||
* Returns an array of grid HTML elements (no placeholder) - used to iterate through our children in DOM order.
|
||||
* This method excludes placeholder elements and returns only actual grid items.
|
||||
*
|
||||
* @returns array of GridItemHTMLElement instances representing all grid items
|
||||
*
|
||||
* @example
|
||||
* const items = grid.getGridItems();
|
||||
* items.forEach(item => {
|
||||
* console.log('Item ID:', item.gridstackNode.id);
|
||||
* });
|
||||
*/
|
||||
getGridItems(): GridItemHTMLElement[];
|
||||
/**
|
||||
* Returns true if change callbacks should be ignored due to column change, sizeToContent, loading, etc.
|
||||
* This is useful for callers who want to implement dirty flag functionality.
|
||||
*
|
||||
* @returns true if change callbacks are currently being ignored
|
||||
*
|
||||
* @example
|
||||
* if (!grid.isIgnoreChangeCB()) {
|
||||
* // Process the change event
|
||||
* console.log('Grid layout changed');
|
||||
* }
|
||||
*/
|
||||
isIgnoreChangeCB(): boolean;
|
||||
/**
|
||||
* Destroys a grid instance. DO NOT CALL any methods or access any vars after this as it will free up members.
|
||||
* @param removeDOM if `false` grid and items HTML elements will not be removed from the DOM (Optional. Default `true`).
|
||||
*/
|
||||
destroy(removeDOM?: boolean): GridStack;
|
||||
/**
|
||||
* Enable/disable floating widgets (default: `false`). When enabled, widgets can float up to fill empty spaces.
|
||||
* See [example](http://gridstackjs.com/demo/float.html)
|
||||
*
|
||||
* @param val true to enable floating, false to disable
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* grid.float(true); // Enable floating
|
||||
* grid.float(false); // Disable floating (default)
|
||||
*/
|
||||
float(val: boolean): GridStack;
|
||||
/**
|
||||
* Get the current float mode setting.
|
||||
*
|
||||
* @returns true if floating is enabled, false otherwise
|
||||
*
|
||||
* @example
|
||||
* const isFloating = grid.getFloat();
|
||||
* console.log('Floating enabled:', isFloating);
|
||||
*/
|
||||
getFloat(): boolean;
|
||||
/**
|
||||
* Get the position of the cell under a pixel on screen.
|
||||
* @param position the position of the pixel to resolve in
|
||||
* absolute coordinates, as an object with top and left properties
|
||||
* @param useDocRelative if true, value will be based on document position vs parent position (Optional. Default false).
|
||||
* Useful when grid is within `position: relative` element
|
||||
*
|
||||
* Returns an object with properties `x` and `y` i.e. the column and row in the grid.
|
||||
*/
|
||||
getCellFromPixel(position: MousePosition, useDocRelative?: boolean): CellPosition;
|
||||
/**
|
||||
* Returns the current number of rows, which will be at least `minRow` if set.
|
||||
* The row count is based on the highest positioned widget in the grid.
|
||||
*
|
||||
* @returns the current number of rows in the grid
|
||||
*
|
||||
* @example
|
||||
* const rowCount = grid.getRow();
|
||||
* console.log('Grid has', rowCount, 'rows');
|
||||
*/
|
||||
getRow(): number;
|
||||
/**
|
||||
* Checks if the specified rectangular area is empty (no widgets occupy any part of it).
|
||||
*
|
||||
* @param x the x coordinate (column) of the area to check
|
||||
* @param y the y coordinate (row) of the area to check
|
||||
* @param w the width in columns of the area to check
|
||||
* @param h the height in rows of the area to check
|
||||
* @returns true if the area is completely empty, false if any widget overlaps
|
||||
*
|
||||
* @example
|
||||
* // Check if a 2x2 area at position (1,1) is empty
|
||||
* if (grid.isAreaEmpty(1, 1, 2, 2)) {
|
||||
* console.log('Area is available for placement');
|
||||
* }
|
||||
*/
|
||||
isAreaEmpty(x: number, y: number, w: number, h: number): boolean;
|
||||
/**
|
||||
* If you add elements to your grid by hand (or have some framework creating DOM), you have to tell gridstack afterwards to make them widgets.
|
||||
* If you want gridstack to add the elements for you, use `addWidget()` instead.
|
||||
* Makes the given element a widget and returns it.
|
||||
*
|
||||
* @param els widget or single selector to convert.
|
||||
* @param options widget definition to use instead of reading attributes or using default sizing values
|
||||
* @returns the converted GridItemHTMLElement
|
||||
*
|
||||
* @example
|
||||
* const grid = GridStack.init();
|
||||
*
|
||||
* // Create HTML content manually, possibly looking like:
|
||||
* // <div id="item-1" gs-x="0" gs-y="0" gs-w="3" gs-h="2"></div>
|
||||
* grid.el.innerHTML = '<div id="item-1" gs-w="3"></div><div id="item-2"></div>';
|
||||
*
|
||||
* // Convert existing elements to widgets
|
||||
* grid.makeWidget('#item-1'); // Uses gs-* attributes from DOM
|
||||
* grid.makeWidget('#item-2', {w: 2, h: 1, content: 'Hello World'});
|
||||
*
|
||||
* // Or pass DOM element directly
|
||||
* const element = document.getElementById('item-3');
|
||||
* grid.makeWidget(element, {x: 0, y: 1, w: 4, h: 2});
|
||||
*/
|
||||
makeWidget(els: GridStackElement, options?: GridStackWidget): GridItemHTMLElement;
|
||||
/**
|
||||
* Register event handler for grid events. You can call this on a single event name, or space separated list.
|
||||
*
|
||||
* Supported events:
|
||||
* - `added`: Called when widgets are being added to a grid
|
||||
* - `change`: Occurs when widgets change their position/size due to constraints or direct changes
|
||||
* - `disable`: Called when grid becomes disabled
|
||||
* - `dragstart`: Called when grid item starts being dragged
|
||||
* - `drag`: Called while grid item is being dragged (for each new row/column value)
|
||||
* - `dragstop`: Called after user is done moving the item, with updated DOM attributes
|
||||
* - `dropped`: Called when an item has been dropped and accepted over a grid
|
||||
* - `enable`: Called when grid becomes enabled
|
||||
* - `removed`: Called when items are being removed from the grid
|
||||
* - `resizestart`: Called before user starts resizing an item
|
||||
* - `resize`: Called while grid item is being resized (for each new row/column value)
|
||||
* - `resizestop`: Called after user is done resizing the item, with updated DOM attributes
|
||||
*
|
||||
* @param name event name(s) to listen for (space separated for multiple)
|
||||
* @param callback function to call when event occurs
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Listen to multiple events at once
|
||||
* grid.on('added removed change', (event, items) => {
|
||||
* items.forEach(item => console.log('Item changed:', item));
|
||||
* });
|
||||
*
|
||||
* // Listen to individual events
|
||||
* grid.on('added', (event, items) => {
|
||||
* items.forEach(item => console.log('Added item:', item));
|
||||
* });
|
||||
*/
|
||||
on(name: 'dropped', callback: GridStackDroppedHandler): GridStack;
|
||||
on(name: 'enable' | 'disable', callback: GridStackEventHandler): GridStack;
|
||||
on(name: 'change' | 'added' | 'removed' | 'resizecontent', callback: GridStackNodesHandler): GridStack;
|
||||
on(name: 'resizestart' | 'resize' | 'resizestop' | 'dragstart' | 'drag' | 'dragstop', callback: GridStackElementHandler): GridStack;
|
||||
on(name: string, callback: GridStackEventHandlerCallback): GridStack;
|
||||
/**
|
||||
* unsubscribe from the 'on' event GridStackEvent
|
||||
* @param name of the event (see possible values) or list of names space separated
|
||||
*/
|
||||
off(name: GridStackEvent | string): GridStack;
|
||||
/**
|
||||
* Remove all event handlers from the grid. This is useful for cleanup when destroying a grid.
|
||||
*
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* grid.offAll(); // Remove all event listeners
|
||||
*/
|
||||
offAll(): GridStack;
|
||||
/**
|
||||
* Removes widget from the grid.
|
||||
* @param el widget or selector to modify
|
||||
* @param removeDOM if `false` DOM element won't be removed from the tree (Default? true).
|
||||
* @param triggerEvent if `false` (quiet mode) element will not be added to removed list and no 'removed' callbacks will be called (Default? true).
|
||||
*/
|
||||
removeWidget(els: GridStackElement, removeDOM?: boolean, triggerEvent?: boolean): GridStack;
|
||||
/**
|
||||
* Removes all widgets from the grid.
|
||||
* @param removeDOM if `false` DOM elements won't be removed from the tree (Default? `true`).
|
||||
* @param triggerEvent if `false` (quiet mode) element will not be added to removed list and no 'removed' callbacks will be called (Default? true).
|
||||
*/
|
||||
removeAll(removeDOM?: boolean, triggerEvent?: boolean): GridStack;
|
||||
/**
|
||||
* Toggle the grid animation state. Toggles the `grid-stack-animate` class.
|
||||
* @param doAnimate if true the grid will animate.
|
||||
* @param delay if true setting will be set on next event loop.
|
||||
*/
|
||||
setAnimation(doAnimate?: boolean, delay?: boolean): GridStack;
|
||||
/**
|
||||
* Toggle the grid static state, which permanently removes/add Drag&Drop support, unlike disable()/enable() that just turns it off/on.
|
||||
* Also toggle the grid-stack-static class.
|
||||
* @param val if true the grid become static.
|
||||
* @param updateClass true (default) if css class gets updated
|
||||
* @param recurse true (default) if sub-grids also get updated
|
||||
*/
|
||||
setStatic(val: boolean, updateClass?: boolean, recurse?: boolean): GridStack;
|
||||
/**
|
||||
* Updates the passed in options on the grid (similar to update(widget) for for the grid options).
|
||||
* @param options PARTIAL grid options to update - only items specified will be updated.
|
||||
* NOTE: not all options updating are currently supported (lot of code, unlikely to change)
|
||||
*/
|
||||
updateOptions(o: GridStackOptions): GridStack;
|
||||
/**
|
||||
* Updates widget position/size and other info. This is used to change widget properties after creation.
|
||||
* Can update position, size, content, and other widget properties.
|
||||
*
|
||||
* Note: If you need to call this on all nodes, use load() instead which will update what changed.
|
||||
* Setting the same x,y for multiple items will be indeterministic and likely unwanted.
|
||||
*
|
||||
* @param els widget element(s) or selector to modify
|
||||
* @param opt new widget options (x,y,w,h, etc.). Only those set will be updated.
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Update widget size and position
|
||||
* grid.update('.my-widget', { x: 2, y: 1, w: 3, h: 2 });
|
||||
*
|
||||
* // Update widget content
|
||||
* grid.update(widget, { content: '<p>New content</p>' });
|
||||
*
|
||||
* // Update multiple properties
|
||||
* grid.update('#my-widget', {
|
||||
* w: 4,
|
||||
* h: 3,
|
||||
* noResize: true,
|
||||
* locked: true
|
||||
* });
|
||||
*/
|
||||
update(els: GridStackElement, opt: GridStackWidget): GridStack;
|
||||
private moveNode;
|
||||
/**
|
||||
* Updates widget height to match the content height to avoid vertical scrollbars or dead space.
|
||||
* This automatically adjusts the widget height based on its content size.
|
||||
*
|
||||
* Note: This assumes only 1 child under resizeToContentParent='.grid-stack-item-content'
|
||||
* (sized to gridItem minus padding) that represents the entire content size.
|
||||
*
|
||||
* @param el the grid item element to resize
|
||||
*
|
||||
* @example
|
||||
* // Resize a widget to fit its content
|
||||
* const widget = document.querySelector('.grid-stack-item');
|
||||
* grid.resizeToContent(widget);
|
||||
*
|
||||
* // This is commonly used with dynamic content:
|
||||
* widget.querySelector('.content').innerHTML = 'New longer content...';
|
||||
* grid.resizeToContent(widget);
|
||||
*/
|
||||
resizeToContent(el: GridItemHTMLElement): void;
|
||||
/** call the user resize (so they can do extra work) else our build in version */
|
||||
private resizeToContentCBCheck;
|
||||
/**
|
||||
* Rotate widgets by swapping their width and height. This is typically called when the user presses 'r' during dragging.
|
||||
* The rotation swaps the w/h dimensions and adjusts min/max constraints accordingly.
|
||||
*
|
||||
* @param els widget element(s) or selector to rotate
|
||||
* @param relative optional pixel coordinate relative to upper/left corner to rotate around (keeps that cell under cursor)
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Rotate a specific widget
|
||||
* grid.rotate('.my-widget');
|
||||
*
|
||||
* // Rotate with relative positioning during drag
|
||||
* grid.rotate(widget, { left: 50, top: 30 });
|
||||
*/
|
||||
rotate(els: GridStackElement, relative?: Position): GridStack;
|
||||
/**
|
||||
* Updates the margins which will set all 4 sides at once - see `GridStackOptions.margin` for format options.
|
||||
* Supports CSS string format of 1, 2, or 4 values or a single number.
|
||||
*
|
||||
* @param value margin value - can be:
|
||||
* - Single number: `10` (applies to all sides)
|
||||
* - Two values: `'10px 20px'` (top/bottom, left/right)
|
||||
* - Four values: `'10px 20px 5px 15px'` (top, right, bottom, left)
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* grid.margin(10); // 10px all sides
|
||||
* grid.margin('10px 20px'); // 10px top/bottom, 20px left/right
|
||||
* grid.margin('5px 10px 15px 20px'); // Different for each side
|
||||
*/
|
||||
margin(value: numberOrString): GridStack;
|
||||
/**
|
||||
* Returns the current margin value as a number (undefined if the 4 sides don't match).
|
||||
* This only returns a number if all sides have the same margin value.
|
||||
*
|
||||
* @returns the margin value in pixels, or undefined if sides have different values
|
||||
*
|
||||
* @example
|
||||
* const margin = grid.getMargin();
|
||||
* if (margin !== undefined) {
|
||||
* console.log('Uniform margin:', margin, 'px');
|
||||
* } else {
|
||||
* console.log('Margins are different on different sides');
|
||||
* }
|
||||
*/
|
||||
getMargin(): number;
|
||||
/**
|
||||
* Returns true if the height of the grid will be less than the vertical
|
||||
* constraint. Always returns true if grid doesn't have height constraint.
|
||||
* @param node contains x,y,w,h,auto-position options
|
||||
*
|
||||
* @example
|
||||
* if (grid.willItFit(newWidget)) {
|
||||
* grid.addWidget(newWidget);
|
||||
* } else {
|
||||
* alert('Not enough free space to place the widget');
|
||||
* }
|
||||
*/
|
||||
willItFit(node: GridStackWidget): boolean;
|
||||
/**
|
||||
* called when we are being resized - check if the one Column Mode needs to be turned on/off
|
||||
* and remember the prev columns we used, or get our count from parent, as well as check for cellHeight==='auto' (square)
|
||||
* or `sizeToContent` gridItem options.
|
||||
*/
|
||||
onResize(clientWidth?: number): GridStack;
|
||||
/** resizes content for given node (or all) if shouldSizeToContent() is true */
|
||||
private resizeToContentCheck;
|
||||
/** add or remove the grid element size event handler */
|
||||
protected _updateResizeEvent(forceRemove?: boolean): GridStack;
|
||||
/**
|
||||
* Get the global drag & drop implementation instance.
|
||||
* This provides access to the underlying drag & drop functionality.
|
||||
*
|
||||
* @returns the DDGridStack instance used for drag & drop operations
|
||||
*
|
||||
* @example
|
||||
* const dd = GridStack.getDD();
|
||||
* // Access drag & drop functionality
|
||||
*/
|
||||
static getDD(): DDGridStack;
|
||||
/**
|
||||
* call to setup dragging in from the outside (say toolbar), by specifying the class selection and options.
|
||||
* Called during GridStack.init() as options, but can also be called directly (last param are used) in case the toolbar
|
||||
* is dynamically create and needs to be set later.
|
||||
* @param dragIn string selector (ex: '.sidebar-item') or list of dom elements
|
||||
* @param dragInOptions options - see DDDragOpt. (default: {handle: '.grid-stack-item-content', appendTo: 'body'}
|
||||
* @param widgets GridStackWidget def to assign to each element which defines what to create on drop
|
||||
* @param root optional root which defaults to document (for shadow dom pass the parent HTMLDocument)
|
||||
*/
|
||||
static setupDragIn(dragIn?: string | HTMLElement[], dragInOptions?: DDDragOpt, widgets?: GridStackWidget[], root?: HTMLElement | Document): void;
|
||||
/**
|
||||
* Enables/Disables dragging by the user for specific grid elements.
|
||||
* For all items and future items, use enableMove() instead. No-op for static grids.
|
||||
*
|
||||
* Note: If you want to prevent an item from moving due to being pushed around by another
|
||||
* during collision, use the 'locked' property instead.
|
||||
*
|
||||
* @param els widget element(s) or selector to modify
|
||||
* @param val if true widget will be draggable, assuming the parent grid isn't noMove or static
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Make specific widgets draggable
|
||||
* grid.movable('.my-widget', true);
|
||||
*
|
||||
* // Disable dragging for specific widgets
|
||||
* grid.movable('#fixed-widget', false);
|
||||
*/
|
||||
movable(els: GridStackElement, val: boolean): GridStack;
|
||||
/**
|
||||
* Enables/Disables user resizing for specific grid elements.
|
||||
* For all items and future items, use enableResize() instead. No-op for static grids.
|
||||
*
|
||||
* @param els widget element(s) or selector to modify
|
||||
* @param val if true widget will be resizable, assuming the parent grid isn't noResize or static
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Make specific widgets resizable
|
||||
* grid.resizable('.my-widget', true);
|
||||
*
|
||||
* // Disable resizing for specific widgets
|
||||
* grid.resizable('#fixed-size-widget', false);
|
||||
*/
|
||||
resizable(els: GridStackElement, val: boolean): GridStack;
|
||||
/**
|
||||
* Temporarily disables widgets moving/resizing.
|
||||
* If you want a more permanent way (which freezes up resources) use `setStatic(true)` instead.
|
||||
*
|
||||
* Note: This is a no-op for static grids.
|
||||
*
|
||||
* This is a shortcut for:
|
||||
* ```typescript
|
||||
* grid.enableMove(false);
|
||||
* grid.enableResize(false);
|
||||
* ```
|
||||
*
|
||||
* @param recurse if true (default), sub-grids also get updated
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Disable all interactions
|
||||
* grid.disable();
|
||||
*
|
||||
* // Disable only this grid, not sub-grids
|
||||
* grid.disable(false);
|
||||
*/
|
||||
disable(recurse?: boolean): GridStack;
|
||||
/**
|
||||
* Re-enables widgets moving/resizing - see disable().
|
||||
* Note: This is a no-op for static grids.
|
||||
*
|
||||
* This is a shortcut for:
|
||||
* ```typescript
|
||||
* grid.enableMove(true);
|
||||
* grid.enableResize(true);
|
||||
* ```
|
||||
*
|
||||
* @param recurse if true (default), sub-grids also get updated
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Re-enable all interactions
|
||||
* grid.enable();
|
||||
*
|
||||
* // Enable only this grid, not sub-grids
|
||||
* grid.enable(false);
|
||||
*/
|
||||
enable(recurse?: boolean): GridStack;
|
||||
/**
|
||||
* Enables/disables widget moving for all widgets. No-op for static grids.
|
||||
* Note: locally defined items (with noMove property) still override this setting.
|
||||
*
|
||||
* @param doEnable if true widgets will be movable, if false moving is disabled
|
||||
* @param recurse if true (default), sub-grids also get updated
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Enable moving for all widgets
|
||||
* grid.enableMove(true);
|
||||
*
|
||||
* // Disable moving for all widgets
|
||||
* grid.enableMove(false);
|
||||
*
|
||||
* // Enable only this grid, not sub-grids
|
||||
* grid.enableMove(true, false);
|
||||
*/
|
||||
enableMove(doEnable: boolean, recurse?: boolean): GridStack;
|
||||
/**
|
||||
* Enables/disables widget resizing for all widgets. No-op for static grids.
|
||||
* Note: locally defined items (with noResize property) still override this setting.
|
||||
*
|
||||
* @param doEnable if true widgets will be resizable, if false resizing is disabled
|
||||
* @param recurse if true (default), sub-grids also get updated
|
||||
* @returns the grid instance for chaining
|
||||
*
|
||||
* @example
|
||||
* // Enable resizing for all widgets
|
||||
* grid.enableResize(true);
|
||||
*
|
||||
* // Disable resizing for all widgets
|
||||
* grid.enableResize(false);
|
||||
*
|
||||
* // Enable only this grid, not sub-grids
|
||||
* grid.enableResize(true, false);
|
||||
*/
|
||||
enableResize(doEnable: boolean, recurse?: boolean): GridStack;
|
||||
/**
|
||||
* prepares the element for drag&drop - this is normally called by makeWidget() unless are are delay loading
|
||||
* @param el GridItemHTMLElement of the widget
|
||||
* @param [force=false]
|
||||
* */
|
||||
prepareDragDrop(el: GridItemHTMLElement, force?: boolean): GridStack;
|
||||
/** call given event callback on our main top-most grid (if we're nested) */
|
||||
protected triggerEvent(event: Event, target: GridItemHTMLElement): void;
|
||||
}
|
||||
2863
node_modules/gridstack/dist/gridstack.js
generated
vendored
Normal file
2863
node_modules/gridstack/dist/gridstack.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/gridstack/dist/gridstack.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/gridstack.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
node_modules/gridstack/dist/gridstack.min.css
generated
vendored
Normal file
1
node_modules/gridstack/dist/gridstack.min.css
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.grid-stack{position:relative}.grid-stack-rtl{direction:ltr}.grid-stack-rtl>.grid-stack-item{direction:rtl}.grid-stack-placeholder>.placeholder-content{background-color:rgba(0,0,0,.1);margin:0;position:absolute;width:auto;z-index:0!important}.grid-stack>.grid-stack-item{position:absolute;padding:0;top:0;left:0;width:var(--gs-column-width);height:var(--gs-cell-height)}.grid-stack>.grid-stack-item>.grid-stack-item-content{margin:0;position:absolute;width:auto;overflow-x:hidden;overflow-y:auto}.grid-stack>.grid-stack-item.size-to-content:not(.size-to-content-max)>.grid-stack-item-content{overflow-y:hidden}.grid-stack>.grid-stack-item>.grid-stack-item-content,.grid-stack>.grid-stack-placeholder>.placeholder-content{top:var(--gs-item-margin-top);right:var(--gs-item-margin-right);bottom:var(--gs-item-margin-bottom);left:var(--gs-item-margin-left)}.grid-stack-item>.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.grid-stack-item.ui-resizable-autohide>.ui-resizable-handle,.grid-stack-item.ui-resizable-disabled>.ui-resizable-handle{display:none}.grid-stack-item>.ui-resizable-ne,.grid-stack-item>.ui-resizable-nw,.grid-stack-item>.ui-resizable-se,.grid-stack-item>.ui-resizable-sw{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%23666" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 20 20"><path d="m10 3 2 2H8l2-2v14l-2-2h4l-2 2"/></svg>');background-repeat:no-repeat;background-position:center}.grid-stack-item>.ui-resizable-ne{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-sw{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-nw{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-se{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-nw{cursor:nw-resize;width:20px;height:20px;top:var(--gs-item-margin-top);left:var(--gs-item-margin-left)}.grid-stack-item>.ui-resizable-n{cursor:n-resize;height:10px;top:var(--gs-item-margin-top);left:25px;right:25px}.grid-stack-item>.ui-resizable-ne{cursor:ne-resize;width:20px;height:20px;top:var(--gs-item-margin-top);right:var(--gs-item-margin-right)}.grid-stack-item>.ui-resizable-e{cursor:e-resize;width:10px;top:15px;bottom:15px;right:var(--gs-item-margin-right)}.grid-stack-item>.ui-resizable-se{cursor:se-resize;width:20px;height:20px;bottom:var(--gs-item-margin-bottom);right:var(--gs-item-margin-right)}.grid-stack-item>.ui-resizable-s{cursor:s-resize;height:10px;left:25px;bottom:var(--gs-item-margin-bottom);right:25px}.grid-stack-item>.ui-resizable-sw{cursor:sw-resize;width:20px;height:20px;bottom:var(--gs-item-margin-bottom);left:var(--gs-item-margin-left)}.grid-stack-item>.ui-resizable-w{cursor:w-resize;width:10px;top:15px;bottom:15px;left:var(--gs-item-margin-left)}.grid-stack-item.ui-draggable-dragging>.ui-resizable-handle{display:none!important}.grid-stack-item.ui-draggable-dragging{will-change:left,top}.grid-stack-item.ui-resizable-resizing{will-change:width,height}.ui-draggable-dragging,.ui-resizable-resizing{z-index:10000}.ui-draggable-dragging>.grid-stack-item-content,.ui-resizable-resizing>.grid-stack-item-content{box-shadow:1px 4px 6px rgba(0,0,0,.2);opacity:.8}.grid-stack-animate,.grid-stack-animate .grid-stack-item{transition:left .3s,top .3s,height .3s,width .3s}.grid-stack-animate .grid-stack-item.grid-stack-placeholder,.grid-stack-animate .grid-stack-item.ui-draggable-dragging,.grid-stack-animate .grid-stack-item.ui-resizable-resizing{transition:left 0s,top 0s,height 0s,width 0s}.grid-stack>.grid-stack-item[gs-y="0"]{top:0}.grid-stack>.grid-stack-item[gs-x="0"]{left:0}
|
||||
432
node_modules/gridstack/dist/types.d.ts
generated
vendored
Normal file
432
node_modules/gridstack/dist/types.d.ts
generated
vendored
Normal file
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* types.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { GridStack } from './gridstack';
|
||||
import { GridStackEngine } from './gridstack-engine';
|
||||
/**
|
||||
* Default values for grid options - used during initialization and when saving out grid configuration.
|
||||
* These values are applied when options are not explicitly provided.
|
||||
*/
|
||||
export declare const gridDefaults: GridStackOptions;
|
||||
/**
|
||||
* Different layout options when changing the number of columns.
|
||||
*
|
||||
* These options control how widgets are repositioned when the grid column count changes.
|
||||
* Note: The new list may be partially filled if there's a cached layout for that size.
|
||||
*
|
||||
* Options:
|
||||
* - `'list'`: Treat items as a sorted list, keeping them sequentially without resizing (unless too big)
|
||||
* - `'compact'`: Similar to list, but uses compact() method to fill empty slots by reordering
|
||||
* - `'moveScale'`: Scale and move items by the ratio of newColumnCount / oldColumnCount
|
||||
* - `'move'`: Only move items, keep their sizes
|
||||
* - `'scale'`: Only scale items, keep their positions
|
||||
* - `'none'`: Leave items unchanged unless they don't fit in the new column count
|
||||
* - Custom function: Provide your own layout logic
|
||||
*/
|
||||
export type ColumnOptions = 'list' | 'compact' | 'moveScale' | 'move' | 'scale' | 'none' | ((column: number, oldColumn: number, nodes: GridStackNode[], oldNodes: GridStackNode[]) => void);
|
||||
/**
|
||||
* Options for the compact() method to reclaim empty space.
|
||||
* - `'list'`: Keep items in order, move them up sequentially
|
||||
* - `'compact'`: Find truly empty spaces, may reorder items for optimal fit
|
||||
*/
|
||||
export type CompactOptions = 'list' | 'compact';
|
||||
/**
|
||||
* Type representing values that can be either numbers or strings (e.g., dimensions with units).
|
||||
* Used for properties like width, height, margins that accept both numeric and string values.
|
||||
*/
|
||||
export type numberOrString = number | string;
|
||||
/**
|
||||
* Extended HTMLElement interface for grid items.
|
||||
* All grid item DOM elements implement this interface to provide access to their grid data.
|
||||
*/
|
||||
export interface GridItemHTMLElement extends HTMLElement {
|
||||
/** Pointer to the associated grid node instance containing position, size, and other widget data */
|
||||
gridstackNode?: GridStackNode;
|
||||
}
|
||||
/**
|
||||
* Type representing various ways to specify grid elements.
|
||||
* Can be a CSS selector string, GridItemHTMLElement (HTML element with GS variables when loaded).
|
||||
*/
|
||||
export type GridStackElement = string | GridItemHTMLElement;
|
||||
/**
|
||||
* Event handler function types for the .on() method.
|
||||
* Different handlers receive different parameters based on the event type.
|
||||
*/
|
||||
/** General event handler that receives only the event */
|
||||
export type GridStackEventHandler = (event: Event) => void;
|
||||
/** Element-specific event handler that receives event and affected element */
|
||||
export type GridStackElementHandler = (event: Event, el: GridItemHTMLElement) => void;
|
||||
/** Node-based event handler that receives event and array of affected nodes */
|
||||
export type GridStackNodesHandler = (event: Event, nodes: GridStackNode[]) => void;
|
||||
/** Drop event handler that receives previous and new node states */
|
||||
export type GridStackDroppedHandler = (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => void;
|
||||
/** Union type of all possible event handler types */
|
||||
export type GridStackEventHandlerCallback = GridStackEventHandler | GridStackElementHandler | GridStackNodesHandler | GridStackDroppedHandler;
|
||||
/**
|
||||
* Optional callback function called during load() operations.
|
||||
* Allows custom handling of widget addition/removal for framework integration.
|
||||
*
|
||||
* @param parent - The parent HTML element
|
||||
* @param w - The widget definition
|
||||
* @param add - True if adding, false if removing
|
||||
* @param grid - True if this is a grid operation
|
||||
* @returns The created/modified HTML element, or undefined
|
||||
*/
|
||||
export type AddRemoveFcn = (parent: HTMLElement, w: GridStackWidget, add: boolean, grid: boolean) => HTMLElement | undefined;
|
||||
/**
|
||||
* Optional callback function called during save() operations.
|
||||
* Allows adding custom data to the saved widget structure.
|
||||
*
|
||||
* @param node - The internal grid node
|
||||
* @param w - The widget structure being saved (can be modified)
|
||||
*/
|
||||
export type SaveFcn = (node: GridStackNode, w: GridStackWidget) => void;
|
||||
/**
|
||||
* Optional callback function for custom widget content rendering.
|
||||
* Called during load()/addWidget() to create custom content beyond plain text.
|
||||
*
|
||||
* @param el - The widget's content container element
|
||||
* @param w - The widget definition with content and other properties
|
||||
*/
|
||||
export type RenderFcn = (el: HTMLElement, w: GridStackWidget) => void;
|
||||
/**
|
||||
* Optional callback function for custom resize-to-content behavior.
|
||||
* Called when a widget needs to resize to fit its content.
|
||||
*
|
||||
* @param el - The grid item element to resize
|
||||
*/
|
||||
export type ResizeToContentFcn = (el: GridItemHTMLElement) => void;
|
||||
/**
|
||||
* Configuration for responsive grid behavior.
|
||||
*
|
||||
* Defines how the grid responds to different screen sizes by changing column counts.
|
||||
* NOTE: Make sure to include the appropriate CSS (gridstack-extra.css) to support responsive behavior.
|
||||
*/
|
||||
export interface Responsive {
|
||||
/** wanted width to maintain (+-50%) to dynamically pick a column count. NOTE: make sure to have correct extra CSS to support this. */
|
||||
columnWidth?: number;
|
||||
/** maximum number of columns allowed (default: 12). NOTE: make sure to have correct extra CSS to support this. */
|
||||
columnMax?: number;
|
||||
/** explicit width:column breakpoints instead of automatic 'columnWidth'. NOTE: make sure to have correct extra CSS to support this. */
|
||||
breakpoints?: Breakpoint[];
|
||||
/** specify if breakpoints are for window size or grid size (default:false = grid) */
|
||||
breakpointForWindow?: boolean;
|
||||
/** global re-layout mode when changing columns */
|
||||
layout?: ColumnOptions;
|
||||
}
|
||||
/**
|
||||
* Defines a responsive breakpoint for automatic column count changes.
|
||||
* Used with the responsive.breakpoints option.
|
||||
*/
|
||||
export interface Breakpoint {
|
||||
/** Maximum width (in pixels) for this breakpoint to be active */
|
||||
w?: number;
|
||||
/** Number of columns to use when this breakpoint is active */
|
||||
c: number;
|
||||
/** Layout mode for this specific breakpoint (overrides global responsive.layout) */
|
||||
layout?: ColumnOptions;
|
||||
}
|
||||
/**
|
||||
* Defines the options for a Grid
|
||||
*/
|
||||
export interface GridStackOptions {
|
||||
/**
|
||||
* Accept widgets dragged from other grids or from outside (default: `false`). Can be:
|
||||
* - `true`: will accept HTML elements having 'grid-stack-item' as class attribute
|
||||
* - `false`: will not accept any external widgets
|
||||
* - string: explicit class name to accept instead of default
|
||||
* - function: callback called before an item will be accepted when entering a grid
|
||||
*
|
||||
* @example
|
||||
* // Accept all grid items
|
||||
* acceptWidgets: true
|
||||
*
|
||||
* // Accept only items with specific class
|
||||
* acceptWidgets: 'my-draggable-item'
|
||||
*
|
||||
* // Custom validation function
|
||||
* acceptWidgets: (el) => {
|
||||
* return el.getAttribute('data-accept') === 'true';
|
||||
* }
|
||||
*
|
||||
* @see {@link http://gridstack.github.io/gridstack.js/demo/two.html} for complete example
|
||||
*/
|
||||
acceptWidgets?: boolean | string | ((element: Element) => boolean);
|
||||
/** possible values (default: `mobile`) - does not apply to non-resizable widgets
|
||||
* `false` the resizing handles are only shown while hovering over a widget
|
||||
* `true` the resizing handles are always shown
|
||||
* 'mobile' if running on a mobile device, default to `true` (since there is no hovering per say), else `false`.
|
||||
See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html) */
|
||||
alwaysShowResizeHandle?: true | false | 'mobile';
|
||||
/** turns animation on (default?: true) */
|
||||
animate?: boolean;
|
||||
/** if false gridstack will not initialize existing items (default?: true) */
|
||||
auto?: boolean;
|
||||
/**
|
||||
* One cell height (default: 'auto'). Can be:
|
||||
* - an integer (px): fixed pixel height
|
||||
* - a string (ex: '100px', '10em', '10rem'): CSS length value
|
||||
* - 0: library will not generate styles for rows (define your own CSS)
|
||||
* - 'auto': height calculated for square cells (width / column) and updated live on window resize
|
||||
* - 'initial': similar to 'auto' but stays fixed size during window resizing
|
||||
*
|
||||
* Note: % values don't work correctly - see demo/cell-height.html
|
||||
*
|
||||
* @example
|
||||
* // Fixed 100px height
|
||||
* cellHeight: 100
|
||||
*
|
||||
* // CSS units
|
||||
* cellHeight: '5rem'
|
||||
* cellHeight: '100px'
|
||||
*
|
||||
* // Auto-sizing for square cells
|
||||
* cellHeight: 'auto'
|
||||
*
|
||||
* // No CSS generation (custom styles)
|
||||
* cellHeight: 0
|
||||
*/
|
||||
cellHeight?: numberOrString;
|
||||
/** throttle time delay (in ms) used when cellHeight='auto' to improve performance vs usability (default?: 100).
|
||||
* A value of 0 will make it instant at a cost of re-creating the CSS file at ever window resize event!
|
||||
* */
|
||||
cellHeightThrottle?: number;
|
||||
/** (internal) unit for cellHeight (default? 'px') which is set when a string cellHeight with a unit is passed (ex: '10rem') */
|
||||
cellHeightUnit?: string;
|
||||
/** list of children item to create when calling load() or addGrid() */
|
||||
children?: GridStackWidget[];
|
||||
/** number of columns (default?: 12). Note: IF you change this, CSS also have to change. See https://github.com/gridstack/gridstack.js#change-grid-columns.
|
||||
* Note: for nested grids, it is recommended to use 'auto' which will always match the container grid-item current width (in column) to keep inside and outside
|
||||
* items always the same. flag is NOT supported for regular non-nested grids.
|
||||
*/
|
||||
column?: number | 'auto';
|
||||
/** responsive column layout for width:column behavior */
|
||||
columnOpts?: Responsive;
|
||||
/** additional class on top of '.grid-stack' (which is required for our CSS) to differentiate this instance.
|
||||
Note: only used by addGrid(), else your element should have the needed class */
|
||||
class?: string;
|
||||
/** disallows dragging of widgets (default?: false) */
|
||||
disableDrag?: boolean;
|
||||
/** disallows resizing of widgets (default?: false). */
|
||||
disableResize?: boolean;
|
||||
/** allows to override UI draggable options. (default?: { handle?: '.grid-stack-item-content', appendTo?: 'body' }) */
|
||||
draggable?: DDDragOpt;
|
||||
/** let user drag nested grid items out of a parent or not (default true - not supported yet) */
|
||||
/** the type of engine to create (so you can subclass) default to GridStackEngine */
|
||||
engineClass?: typeof GridStackEngine;
|
||||
/** enable floating widgets (default?: false) See example (http://gridstack.github.io/gridstack.js/demo/float.html) */
|
||||
float?: boolean;
|
||||
/** draggable handle selector (default?: '.grid-stack-item-content') */
|
||||
handle?: string;
|
||||
/** draggable handle class (e.g. 'grid-stack-item-content'). If set 'handle' is ignored (default?: null) */
|
||||
handleClass?: string;
|
||||
/** additional widget class (default?: 'grid-stack-item') */
|
||||
itemClass?: string;
|
||||
/** re-layout mode when we're a subgrid and we are being resized. default to 'list' */
|
||||
layout?: ColumnOptions;
|
||||
/** true when widgets are only created when they scroll into view (visible) */
|
||||
lazyLoad?: boolean;
|
||||
/**
|
||||
* gap between grid item and content (default?: 10). This will set all 4 sides and support the CSS formats below
|
||||
* an integer (px)
|
||||
* a string with possible units (ex: '2em', '20px', '2rem')
|
||||
* string with space separated values (ex: '5px 10px 0 20px' for all 4 sides, or '5em 10em' for top/bottom and left/right pairs like CSS).
|
||||
* Note: all sides must have same units (last one wins, default px)
|
||||
*/
|
||||
margin?: numberOrString;
|
||||
/** OLD way to optionally set each side - use margin: '5px 10px 0 20px' instead. Used internally to store each side. */
|
||||
marginTop?: numberOrString;
|
||||
marginRight?: numberOrString;
|
||||
marginBottom?: numberOrString;
|
||||
marginLeft?: numberOrString;
|
||||
/** (internal) unit for margin (default? 'px') set when `margin` is set as string with unit (ex: 2rem') */
|
||||
marginUnit?: string;
|
||||
/** maximum rows amount. Default? is 0 which means no maximum rows */
|
||||
maxRow?: number;
|
||||
/** minimum rows amount which is handy to prevent grid from collapsing when empty. Default is `0`.
|
||||
* When no set the `min-height` CSS attribute on the grid div (in pixels) can be used, which will round to the closest row.
|
||||
*/
|
||||
minRow?: number;
|
||||
/** If you are using a nonce-based Content Security Policy, pass your nonce here and
|
||||
* GridStack will add it to the `<style>` elements it creates. */
|
||||
nonce?: string;
|
||||
/** class for placeholder (default?: 'grid-stack-placeholder') */
|
||||
placeholderClass?: string;
|
||||
/** placeholder default content (default?: '') */
|
||||
placeholderText?: string;
|
||||
/** allows to override UI resizable options. default is { handles: 'se', autoHide: true on desktop, false on mobile } */
|
||||
resizable?: DDResizeOpt;
|
||||
/**
|
||||
* if true widgets could be removed by dragging outside of the grid. It could also be a selector string (ex: ".trash"),
|
||||
* in this case widgets will be removed by dropping them there (default?: false)
|
||||
* See example (http://gridstack.github.io/gridstack.js/demo/two.html)
|
||||
*/
|
||||
removable?: boolean | string;
|
||||
/** allows to override UI removable options. (default?: { accept: '.grid-stack-item' }) */
|
||||
removableOptions?: DDRemoveOpt;
|
||||
/** fix grid number of rows. This is a shortcut of writing `minRow:N, maxRow:N`. (default `0` no constrain) */
|
||||
row?: number;
|
||||
/**
|
||||
* if true turns grid to RTL. Possible values are true, false, 'auto' (default?: 'auto')
|
||||
* See [example](http://gridstack.github.io/gridstack.js/demo/right-to-left(rtl).html)
|
||||
*/
|
||||
rtl?: boolean | 'auto';
|
||||
/** set to true if all grid items (by default, but item can also override) height should be based on content size instead of WidgetItem.h to avoid v-scrollbars.
|
||||
* Note: this is still row based, not pixels, so it will use ceil(getBoundingClientRect().height / getCellHeight())
|
||||
*/
|
||||
sizeToContent?: boolean;
|
||||
/**
|
||||
* makes grid static (default?: false). If `true` widgets are not movable/resizable.
|
||||
* You don't even need draggable/resizable. A CSS class
|
||||
* 'grid-stack-static' is also added to the element.
|
||||
*/
|
||||
staticGrid?: boolean;
|
||||
/**
|
||||
* @deprecated Not used anymore, styles are now implemented with local CSS variables
|
||||
*/
|
||||
styleInHead?: boolean;
|
||||
/** list of differences in options for automatically created sub-grids under us (inside our grid-items) */
|
||||
subGridOpts?: GridStackOptions;
|
||||
/** enable/disable the creation of sub-grids on the fly by dragging items completely
|
||||
* over others (nest) vs partially (push). Forces `DDDragOpt.pause=true` to accomplish that. */
|
||||
subGridDynamic?: boolean;
|
||||
}
|
||||
/** options used during GridStackEngine.moveNode() */
|
||||
export interface GridStackMoveOpts extends GridStackPosition {
|
||||
/** node to skip collision */
|
||||
skip?: GridStackNode;
|
||||
/** do we pack (default true) */
|
||||
pack?: boolean;
|
||||
/** true if we are calling this recursively to prevent simple swap or coverage collision - default false*/
|
||||
nested?: boolean;
|
||||
/** vars to calculate other cells coordinates */
|
||||
cellWidth?: number;
|
||||
cellHeight?: number;
|
||||
marginTop?: number;
|
||||
marginBottom?: number;
|
||||
marginLeft?: number;
|
||||
marginRight?: number;
|
||||
/** position in pixels of the currently dragged items (for overlap check) */
|
||||
rect?: GridStackPosition;
|
||||
/** true if we're live resizing */
|
||||
resizing?: boolean;
|
||||
/** best node (most coverage) we collied with */
|
||||
collide?: GridStackNode;
|
||||
/** for collision check even if we don't move */
|
||||
forceCollide?: boolean;
|
||||
}
|
||||
export interface GridStackPosition {
|
||||
/** widget position x (default?: 0) */
|
||||
x?: number;
|
||||
/** widget position y (default?: 0) */
|
||||
y?: number;
|
||||
/** widget dimension width (default?: 1) */
|
||||
w?: number;
|
||||
/** widget dimension height (default?: 1) */
|
||||
h?: number;
|
||||
}
|
||||
/**
|
||||
* GridStack Widget creation options
|
||||
*/
|
||||
export interface GridStackWidget extends GridStackPosition {
|
||||
/** if true then x, y parameters will be ignored and widget will be places on the first available position (default?: false) */
|
||||
autoPosition?: boolean;
|
||||
/** minimum width allowed during resize/creation (default?: undefined = un-constrained) */
|
||||
minW?: number;
|
||||
/** maximum width allowed during resize/creation (default?: undefined = un-constrained) */
|
||||
maxW?: number;
|
||||
/** minimum height allowed during resize/creation (default?: undefined = un-constrained) */
|
||||
minH?: number;
|
||||
/** maximum height allowed during resize/creation (default?: undefined = un-constrained) */
|
||||
maxH?: number;
|
||||
/** prevent direct resizing by the user (default?: undefined = un-constrained) */
|
||||
noResize?: boolean;
|
||||
/** prevents direct moving by the user (default?: undefined = un-constrained) */
|
||||
noMove?: boolean;
|
||||
/** prevents being pushed by other widgets or api (default?: undefined = un-constrained), which is different from `noMove` (user action only) */
|
||||
locked?: boolean;
|
||||
/** value for `gs-id` stored on the widget (default?: undefined) */
|
||||
id?: string;
|
||||
/** html to append inside as content */
|
||||
content?: string;
|
||||
/** true when widgets are only created when they scroll into view (visible) */
|
||||
lazyLoad?: boolean;
|
||||
/** local (vs grid) override - see GridStackOptions.
|
||||
* Note: This also allow you to set a maximum h value (but user changeable during normal resizing) to prevent unlimited content from taking too much space (get scrollbar) */
|
||||
sizeToContent?: boolean | number;
|
||||
/** local override of GridStack.resizeToContentParent that specify the class to use for the parent (actual) vs child (wanted) height */
|
||||
resizeToContentParent?: string;
|
||||
/** optional nested grid options and list of children, which then turns into actual instance at runtime to get options from */
|
||||
subGridOpts?: GridStackOptions;
|
||||
}
|
||||
/** Drag&Drop resize options */
|
||||
export interface DDResizeOpt {
|
||||
/** do resize handle hide by default until mouse over. default: true on desktop, false on mobile */
|
||||
autoHide?: boolean;
|
||||
/**
|
||||
* sides where you can resize from (ex: 'e, se, s, sw, w') - default 'se' (south-east)
|
||||
* Note: it is not recommended to resize from the top sides as weird side effect may occur.
|
||||
*/
|
||||
handles?: string;
|
||||
/**
|
||||
* Custom element or query inside the widget node that is used instead of the
|
||||
* generated resize handle.
|
||||
*/
|
||||
element?: string | HTMLElement;
|
||||
}
|
||||
/** Drag&Drop remove options */
|
||||
export interface DDRemoveOpt {
|
||||
/** class that can be removed (default?: opts.itemClass) */
|
||||
accept?: string;
|
||||
/** class that cannot be removed (default: 'grid-stack-non-removable') */
|
||||
decline?: string;
|
||||
}
|
||||
/** Drag&Drop dragging options */
|
||||
export interface DDDragOpt {
|
||||
/** class selector of items that can be dragged. default to '.grid-stack-item-content' */
|
||||
handle?: string;
|
||||
/** default to 'body' */
|
||||
appendTo?: string;
|
||||
/** if set (true | msec), dragging placement (collision) will only happen after a pause by the user. Note: this is Global */
|
||||
pause?: boolean | number;
|
||||
/** default to `true` */
|
||||
scroll?: boolean;
|
||||
/** prevents dragging from starting on specified elements, listed as comma separated selectors (eg: '.no-drag'). default built in is 'input,textarea,button,select,option' */
|
||||
cancel?: string;
|
||||
/** helper function when dropping: 'clone' or your own method */
|
||||
helper?: 'clone' | ((el: HTMLElement) => HTMLElement);
|
||||
/** callbacks */
|
||||
start?: (event: Event, ui: DDUIData) => void;
|
||||
stop?: (event: Event) => void;
|
||||
drag?: (event: Event, ui: DDUIData) => void;
|
||||
}
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export interface Position {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
export interface Rect extends Size, Position {
|
||||
}
|
||||
/** data that is passed during drag and resizing callbacks */
|
||||
export interface DDUIData {
|
||||
position?: Position;
|
||||
size?: Size;
|
||||
draggable?: HTMLElement;
|
||||
}
|
||||
/**
|
||||
* internal runtime descriptions describing the widgets in the grid
|
||||
*/
|
||||
export interface GridStackNode extends GridStackWidget {
|
||||
/** pointer back to HTML element */
|
||||
el?: GridItemHTMLElement;
|
||||
/** pointer back to parent Grid instance */
|
||||
grid?: GridStack;
|
||||
/** actual sub-grid instance */
|
||||
subGrid?: GridStack;
|
||||
/** allow delay creation when visible */
|
||||
visibleObservable?: IntersectionObserver;
|
||||
}
|
||||
38
node_modules/gridstack/dist/types.js
generated
vendored
Normal file
38
node_modules/gridstack/dist/types.js
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* types.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
/**
|
||||
* Default values for grid options - used during initialization and when saving out grid configuration.
|
||||
* These values are applied when options are not explicitly provided.
|
||||
*/
|
||||
export const gridDefaults = {
|
||||
alwaysShowResizeHandle: 'mobile',
|
||||
animate: true,
|
||||
auto: true,
|
||||
cellHeight: 'auto',
|
||||
cellHeightThrottle: 100,
|
||||
cellHeightUnit: 'px',
|
||||
column: 12,
|
||||
draggable: { handle: '.grid-stack-item-content', appendTo: 'body', scroll: true },
|
||||
handle: '.grid-stack-item-content',
|
||||
itemClass: 'grid-stack-item',
|
||||
margin: 10,
|
||||
marginUnit: 'px',
|
||||
maxRow: 0,
|
||||
minRow: 0,
|
||||
placeholderClass: 'grid-stack-placeholder',
|
||||
placeholderText: '',
|
||||
removableOptions: { accept: 'grid-stack-item', decline: 'grid-stack-non-removable' },
|
||||
resizable: { handles: 'se' },
|
||||
rtl: 'auto',
|
||||
// **** same as not being set ****
|
||||
// disableDrag: false,
|
||||
// disableResize: false,
|
||||
// float: false,
|
||||
// handleClass: null,
|
||||
// removable: false,
|
||||
// staticGrid: false,
|
||||
//removable
|
||||
};
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
node_modules/gridstack/dist/types.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/types.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
283
node_modules/gridstack/dist/utils.d.ts
generated
vendored
Normal file
283
node_modules/gridstack/dist/utils.d.ts
generated
vendored
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* utils.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
import { GridStackElement, GridStackNode, numberOrString, GridStackPosition, GridStackWidget } from './types';
|
||||
export interface HeightData {
|
||||
h: number;
|
||||
unit: string;
|
||||
}
|
||||
export interface DragTransform {
|
||||
xScale: number;
|
||||
yScale: number;
|
||||
xOffset: number;
|
||||
yOffset: number;
|
||||
}
|
||||
/**
|
||||
* Collection of utility methods used throughout GridStack.
|
||||
* These are general-purpose helper functions for DOM manipulation,
|
||||
* positioning calculations, object operations, and more.
|
||||
*/
|
||||
export declare class Utils {
|
||||
/**
|
||||
* Convert a potential selector into an actual list of HTML elements.
|
||||
* Supports CSS selectors, element references, and special ID handling.
|
||||
*
|
||||
* @param els selector string, HTMLElement, or array of elements
|
||||
* @param root optional root element to search within (defaults to document, useful for shadow DOM)
|
||||
* @returns array of HTML elements matching the selector
|
||||
*
|
||||
* @example
|
||||
* const elements = Utils.getElements('.grid-item');
|
||||
* const byId = Utils.getElements('#myWidget');
|
||||
* const fromShadow = Utils.getElements('.item', shadowRoot);
|
||||
*/
|
||||
static getElements(els: GridStackElement, root?: HTMLElement | Document): HTMLElement[];
|
||||
/**
|
||||
* Convert a potential selector into a single HTML element.
|
||||
* Similar to getElements() but returns only the first match.
|
||||
*
|
||||
* @param els selector string or HTMLElement
|
||||
* @param root optional root element to search within (defaults to document)
|
||||
* @returns the first HTML element matching the selector, or null if not found
|
||||
*
|
||||
* @example
|
||||
* const element = Utils.getElement('#myWidget');
|
||||
* const first = Utils.getElement('.grid-item');
|
||||
*/
|
||||
static getElement(els: GridStackElement, root?: HTMLElement | Document): HTMLElement;
|
||||
/**
|
||||
* Check if a widget should be lazy loaded based on node or grid settings.
|
||||
*
|
||||
* @param n the grid node to check
|
||||
* @returns true if the item should be lazy loaded
|
||||
*
|
||||
* @example
|
||||
* if (Utils.lazyLoad(node)) {
|
||||
* // Set up intersection observer for lazy loading
|
||||
* }
|
||||
*/
|
||||
static lazyLoad(n: GridStackNode): boolean;
|
||||
/**
|
||||
* Create a div element with the specified CSS classes.
|
||||
*
|
||||
* @param classes array of CSS class names to add
|
||||
* @param parent optional parent element to append the div to
|
||||
* @returns the created div element
|
||||
*
|
||||
* @example
|
||||
* const div = Utils.createDiv(['grid-item', 'draggable']);
|
||||
* const nested = Utils.createDiv(['content'], parentDiv);
|
||||
*/
|
||||
static createDiv(classes: string[], parent?: HTMLElement): HTMLElement;
|
||||
/**
|
||||
* Check if a widget should resize to fit its content.
|
||||
*
|
||||
* @param n the grid node to check (can be undefined)
|
||||
* @param strict if true, only returns true for explicit sizeToContent:true (not numbers)
|
||||
* @returns true if the widget should resize to content
|
||||
*
|
||||
* @example
|
||||
* if (Utils.shouldSizeToContent(node)) {
|
||||
* // Trigger content-based resizing
|
||||
* }
|
||||
*/
|
||||
static shouldSizeToContent(n: GridStackNode | undefined, strict?: boolean): boolean;
|
||||
/**
|
||||
* Check if two grid positions overlap/intersect.
|
||||
*
|
||||
* @param a first position with x, y, w, h properties
|
||||
* @param b second position with x, y, w, h properties
|
||||
* @returns true if the positions overlap
|
||||
*
|
||||
* @example
|
||||
* const overlaps = Utils.isIntercepted(
|
||||
* {x: 0, y: 0, w: 2, h: 1},
|
||||
* {x: 1, y: 0, w: 2, h: 1}
|
||||
* ); // true - they overlap
|
||||
*/
|
||||
static isIntercepted(a: GridStackPosition, b: GridStackPosition): boolean;
|
||||
/**
|
||||
* Check if two grid positions are touching (edges or corners).
|
||||
*
|
||||
* @param a first position
|
||||
* @param b second position
|
||||
* @returns true if the positions are touching
|
||||
*
|
||||
* @example
|
||||
* const touching = Utils.isTouching(
|
||||
* {x: 0, y: 0, w: 2, h: 1},
|
||||
* {x: 2, y: 0, w: 1, h: 1}
|
||||
* ); // true - they share an edge
|
||||
*/
|
||||
static isTouching(a: GridStackPosition, b: GridStackPosition): boolean;
|
||||
/**
|
||||
* Calculate the overlapping area between two grid positions.
|
||||
*
|
||||
* @param a first position
|
||||
* @param b second position
|
||||
* @returns the area of overlap (0 if no overlap)
|
||||
*
|
||||
* @example
|
||||
* const overlap = Utils.areaIntercept(
|
||||
* {x: 0, y: 0, w: 3, h: 2},
|
||||
* {x: 1, y: 0, w: 3, h: 2}
|
||||
* ); // returns 4 (2x2 overlap)
|
||||
*/
|
||||
static areaIntercept(a: GridStackPosition, b: GridStackPosition): number;
|
||||
/**
|
||||
* Calculate the total area of a grid position.
|
||||
*
|
||||
* @param a position with width and height
|
||||
* @returns the total area (width * height)
|
||||
*
|
||||
* @example
|
||||
* const area = Utils.area({x: 0, y: 0, w: 3, h: 2}); // returns 6
|
||||
*/
|
||||
static area(a: GridStackPosition): number;
|
||||
/**
|
||||
* Sort an array of grid nodes by position (y first, then x).
|
||||
*
|
||||
* @param nodes array of nodes to sort
|
||||
* @param dir sort direction: 1 for ascending (top-left first), -1 for descending
|
||||
* @returns the sorted array (modifies original)
|
||||
*
|
||||
* @example
|
||||
* const sorted = Utils.sort(nodes); // Sort top-left to bottom-right
|
||||
* const reverse = Utils.sort(nodes, -1); // Sort bottom-right to top-left
|
||||
*/
|
||||
static sort(nodes: GridStackNode[], dir?: 1 | -1): GridStackNode[];
|
||||
/**
|
||||
* Find a grid node by its ID.
|
||||
*
|
||||
* @param nodes array of nodes to search
|
||||
* @param id the ID to search for
|
||||
* @returns the node with matching ID, or undefined if not found
|
||||
*
|
||||
* @example
|
||||
* const node = Utils.find(nodes, 'widget-1');
|
||||
* if (node) console.log('Found node at:', node.x, node.y);
|
||||
*/
|
||||
static find(nodes: GridStackNode[], id: string): GridStackNode | undefined;
|
||||
/**
|
||||
* Convert various value types to boolean.
|
||||
* Handles strings like 'false', 'no', '0' as false.
|
||||
*
|
||||
* @param v value to convert
|
||||
* @returns boolean representation
|
||||
*
|
||||
* @example
|
||||
* Utils.toBool('true'); // true
|
||||
* Utils.toBool('false'); // false
|
||||
* Utils.toBool('no'); // false
|
||||
* Utils.toBool('1'); // true
|
||||
*/
|
||||
static toBool(v: unknown): boolean;
|
||||
/**
|
||||
* Convert a string value to a number, handling null and empty strings.
|
||||
*
|
||||
* @param value string or null value to convert
|
||||
* @returns number value, or undefined for null/empty strings
|
||||
*
|
||||
* @example
|
||||
* Utils.toNumber('42'); // 42
|
||||
* Utils.toNumber(''); // undefined
|
||||
* Utils.toNumber(null); // undefined
|
||||
*/
|
||||
static toNumber(value: null | string): number;
|
||||
/**
|
||||
* Parse a height value with units into numeric value and unit string.
|
||||
* Supports px, em, rem, vh, vw, %, cm, mm units.
|
||||
*
|
||||
* @param val height value as number or string with units
|
||||
* @returns object with h (height) and unit properties
|
||||
*
|
||||
* @example
|
||||
* Utils.parseHeight('100px'); // {h: 100, unit: 'px'}
|
||||
* Utils.parseHeight('2rem'); // {h: 2, unit: 'rem'}
|
||||
* Utils.parseHeight(50); // {h: 50, unit: 'px'}
|
||||
*/
|
||||
static parseHeight(val: numberOrString): HeightData;
|
||||
/**
|
||||
* Copy unset fields from source objects to target object (shallow merge with defaults).
|
||||
* Similar to Object.assign but only sets undefined/null fields.
|
||||
*
|
||||
* @param target the object to copy defaults into
|
||||
* @param sources one or more source objects to copy defaults from
|
||||
* @returns the modified target object
|
||||
*
|
||||
* @example
|
||||
* const config = { width: 100 };
|
||||
* Utils.defaults(config, { width: 200, height: 50 });
|
||||
* // config is now { width: 100, height: 50 }
|
||||
*/
|
||||
static defaults(target: any, ...sources: any[]): {};
|
||||
/**
|
||||
* Compare two objects for equality (shallow comparison).
|
||||
* Checks if objects have the same fields and values at one level deep.
|
||||
*
|
||||
* @param a first object to compare
|
||||
* @param b second object to compare
|
||||
* @returns true if objects have the same values
|
||||
*
|
||||
* @example
|
||||
* Utils.same({x: 1, y: 2}, {x: 1, y: 2}); // true
|
||||
* Utils.same({x: 1}, {x: 1, y: 2}); // false
|
||||
*/
|
||||
static same(a: unknown, b: unknown): boolean;
|
||||
/**
|
||||
* Copy position and size properties from one widget to another.
|
||||
* Copies x, y, w, h and optionally min/max constraints.
|
||||
*
|
||||
* @param a target widget to copy to
|
||||
* @param b source widget to copy from
|
||||
* @param doMinMax if true, also copy min/max width/height constraints
|
||||
* @returns the target widget (a)
|
||||
*
|
||||
* @example
|
||||
* Utils.copyPos(widget1, widget2); // Copy position/size
|
||||
* Utils.copyPos(widget1, widget2, true); // Also copy constraints
|
||||
*/
|
||||
static copyPos(a: GridStackWidget, b: GridStackWidget, doMinMax?: boolean): GridStackWidget;
|
||||
/** true if a and b has same size & position */
|
||||
static samePos(a: GridStackPosition, b: GridStackPosition): boolean;
|
||||
/** given a node, makes sure it's min/max are valid */
|
||||
static sanitizeMinMax(node: GridStackNode): void;
|
||||
/** removes field from the first object if same as the second objects (like diffing) and internal '_' for saving */
|
||||
static removeInternalAndSame(a: unknown, b: unknown): void;
|
||||
/** removes internal fields '_' and default values for saving */
|
||||
static removeInternalForSave(n: GridStackNode, removeEl?: boolean): void;
|
||||
/** return the closest parent (or itself) matching the given class */
|
||||
/** delay calling the given function for given delay, preventing new calls from happening while waiting */
|
||||
static throttle(func: () => void, delay: number): () => void;
|
||||
static removePositioningStyles(el: HTMLElement): void;
|
||||
/** single level clone, returning a new object with same top fields. This will share sub objects and arrays */
|
||||
static clone<T>(obj: T): T;
|
||||
/**
|
||||
* Recursive clone version that returns a full copy, checking for nested objects and arrays ONLY.
|
||||
* Note: this will use as-is any key starting with double __ (and not copy inside) some lib have circular dependencies.
|
||||
*/
|
||||
static cloneDeep<T>(obj: T): T;
|
||||
/** deep clone the given HTML node, removing teh unique id field */
|
||||
static cloneNode(el: HTMLElement): HTMLElement;
|
||||
static appendTo(el: HTMLElement, parent: string | HTMLElement): void;
|
||||
static addElStyles(el: HTMLElement, styles: {
|
||||
[prop: string]: string | string[];
|
||||
}): void;
|
||||
static initEvent<T>(e: DragEvent | MouseEvent, info: {
|
||||
type: string;
|
||||
target?: EventTarget;
|
||||
}): T;
|
||||
/** copies the MouseEvent (or convert Touch) properties and sends it as another event to the given target */
|
||||
static simulateMouseEvent(e: MouseEvent | Touch, simulatedType: string, target?: EventTarget): void;
|
||||
/**
|
||||
* defines an element that is used to get the offset and scale from grid transforms
|
||||
* returns the scale and offsets from said element
|
||||
*/
|
||||
static getValuesFromTransformedElement(parent: HTMLElement): DragTransform;
|
||||
/** swap the given object 2 field values */
|
||||
static swap(o: unknown, a: string, b: string): void;
|
||||
/** returns true if event is inside the given element rectangle */
|
||||
/** true if the item can be rotated (checking for prop, not space available) */
|
||||
static canBeRotated(n: GridStackNode): boolean;
|
||||
}
|
||||
796
node_modules/gridstack/dist/utils.js
generated
vendored
Normal file
796
node_modules/gridstack/dist/utils.js
generated
vendored
Normal file
@@ -0,0 +1,796 @@
|
||||
/**
|
||||
* utils.ts 12.4.2
|
||||
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
|
||||
*/
|
||||
// /**
|
||||
// * Checks for obsolete method names and provides deprecation warnings.
|
||||
// * Creates a wrapper function that logs a deprecation warning when called.
|
||||
// *
|
||||
// * @param self the object context to apply the function to
|
||||
// * @param f the new function to call
|
||||
// * @param oldName the deprecated method name
|
||||
// * @param newName the new method name to use instead
|
||||
// * @param rev the version when the deprecation was introduced
|
||||
// * @returns a wrapper function that warns about deprecation
|
||||
// */
|
||||
// eslint-disable-next-line
|
||||
// export function obsolete(self, f, oldName: string, newName: string, rev: string): (...args: any[]) => any {
|
||||
// const wrapper = (...args) => {
|
||||
// console.warn('gridstack.js: Function `' + oldName + '` is deprecated in ' + rev + ' and has been replaced ' +
|
||||
// 'with `' + newName + '`. It will be **removed** in a future release');
|
||||
// return f.apply(self, args);
|
||||
// }
|
||||
// wrapper.prototype = f.prototype;
|
||||
// return wrapper;
|
||||
// }
|
||||
// /**
|
||||
// * Checks for obsolete grid options and migrates them to new names.
|
||||
// * Automatically copies old option values to new option names and shows deprecation warnings.
|
||||
// *
|
||||
// * @param opts the options object to check and migrate
|
||||
// * @param oldName the deprecated option name
|
||||
// * @param newName the new option name to use instead
|
||||
// * @param rev the version when the deprecation was introduced
|
||||
// */
|
||||
// export function obsoleteOpts(opts: GridStackOptions, oldName: string, newName: string, rev: string): void {
|
||||
// if (opts[oldName] !== undefined) {
|
||||
// opts[newName] = opts[oldName];
|
||||
// console.warn('gridstack.js: Option `' + oldName + '` is deprecated in ' + rev + ' and has been replaced with `' +
|
||||
// newName + '`. It will be **removed** in a future release');
|
||||
// }
|
||||
// }
|
||||
// /**
|
||||
// * Checks for obsolete grid options that have been completely removed.
|
||||
// * Shows deprecation warnings for options that are no longer supported.
|
||||
// *
|
||||
// * @param opts the options object to check
|
||||
// * @param oldName the removed option name
|
||||
// * @param rev the version when the option was removed
|
||||
// * @param info additional information about the removal
|
||||
// */
|
||||
// export function obsoleteOptsDel(opts: GridStackOptions, oldName: string, rev: string, info: string): void {
|
||||
// if (opts[oldName] !== undefined) {
|
||||
// console.warn('gridstack.js: Option `' + oldName + '` is deprecated in ' + rev + info);
|
||||
// }
|
||||
// }
|
||||
// /**
|
||||
// * Checks for obsolete HTML element attributes and migrates them.
|
||||
// * Automatically copies old attribute values to new attribute names and shows deprecation warnings.
|
||||
// *
|
||||
// * @param el the HTML element to check and migrate
|
||||
// * @param oldName the deprecated attribute name
|
||||
// * @param newName the new attribute name to use instead
|
||||
// * @param rev the version when the deprecation was introduced
|
||||
// */
|
||||
// export function obsoleteAttr(el: HTMLElement, oldName: string, newName: string, rev: string): void {
|
||||
// const oldAttr = el.getAttribute(oldName);
|
||||
// if (oldAttr !== null) {
|
||||
// el.setAttribute(newName, oldAttr);
|
||||
// console.warn('gridstack.js: attribute `' + oldName + '`=' + oldAttr + ' is deprecated on this object in ' + rev + ' and has been replaced with `' +
|
||||
// newName + '`. It will be **removed** in a future release');
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* Collection of utility methods used throughout GridStack.
|
||||
* These are general-purpose helper functions for DOM manipulation,
|
||||
* positioning calculations, object operations, and more.
|
||||
*/
|
||||
export class Utils {
|
||||
/**
|
||||
* Convert a potential selector into an actual list of HTML elements.
|
||||
* Supports CSS selectors, element references, and special ID handling.
|
||||
*
|
||||
* @param els selector string, HTMLElement, or array of elements
|
||||
* @param root optional root element to search within (defaults to document, useful for shadow DOM)
|
||||
* @returns array of HTML elements matching the selector
|
||||
*
|
||||
* @example
|
||||
* const elements = Utils.getElements('.grid-item');
|
||||
* const byId = Utils.getElements('#myWidget');
|
||||
* const fromShadow = Utils.getElements('.item', shadowRoot);
|
||||
*/
|
||||
static getElements(els, root = document) {
|
||||
if (typeof els === 'string') {
|
||||
const doc = ('getElementById' in root) ? root : undefined;
|
||||
// Note: very common for people use to id='1,2,3' which is only legal as HTML5 id, but not CSS selectors
|
||||
// so if we start with a number, assume it's an id and just return that one item...
|
||||
// see https://github.com/gridstack/gridstack.js/issues/2234#issuecomment-1523796562
|
||||
if (doc && !isNaN(+els[0])) { // start with digit
|
||||
const el = doc.getElementById(els);
|
||||
return el ? [el] : [];
|
||||
}
|
||||
let list = root.querySelectorAll(els);
|
||||
if (!list.length && els[0] !== '.' && els[0] !== '#') {
|
||||
// see if mean to be a class
|
||||
list = root.querySelectorAll('.' + els);
|
||||
// else if mean to be an id
|
||||
if (!list.length)
|
||||
list = root.querySelectorAll('#' + els);
|
||||
// else see if gs-id attribute
|
||||
if (!list.length) {
|
||||
const el = root.querySelector(`[gs-id="${els}"]`);
|
||||
return el ? [el] : [];
|
||||
}
|
||||
}
|
||||
return Array.from(list);
|
||||
}
|
||||
return [els];
|
||||
}
|
||||
/**
|
||||
* Convert a potential selector into a single HTML element.
|
||||
* Similar to getElements() but returns only the first match.
|
||||
*
|
||||
* @param els selector string or HTMLElement
|
||||
* @param root optional root element to search within (defaults to document)
|
||||
* @returns the first HTML element matching the selector, or null if not found
|
||||
*
|
||||
* @example
|
||||
* const element = Utils.getElement('#myWidget');
|
||||
* const first = Utils.getElement('.grid-item');
|
||||
*/
|
||||
static getElement(els, root = document) {
|
||||
if (typeof els === 'string') {
|
||||
const doc = ('getElementById' in root) ? root : undefined;
|
||||
if (!els.length)
|
||||
return null;
|
||||
if (doc && els[0] === '#') {
|
||||
return doc.getElementById(els.substring(1));
|
||||
}
|
||||
if (els[0] === '#' || els[0] === '.' || els[0] === '[') {
|
||||
return root.querySelector(els);
|
||||
}
|
||||
// if we start with a digit, assume it's an id (error calling querySelector('#1')) as class are not valid CSS
|
||||
if (doc && !isNaN(+els[0])) { // start with digit
|
||||
return doc.getElementById(els);
|
||||
}
|
||||
// finally try string, then id, then class
|
||||
let el = root.querySelector(els);
|
||||
if (doc && !el) {
|
||||
el = doc.getElementById(els);
|
||||
}
|
||||
if (!el) {
|
||||
el = root.querySelector('.' + els);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
return els;
|
||||
}
|
||||
/**
|
||||
* Check if a widget should be lazy loaded based on node or grid settings.
|
||||
*
|
||||
* @param n the grid node to check
|
||||
* @returns true if the item should be lazy loaded
|
||||
*
|
||||
* @example
|
||||
* if (Utils.lazyLoad(node)) {
|
||||
* // Set up intersection observer for lazy loading
|
||||
* }
|
||||
*/
|
||||
static lazyLoad(n) {
|
||||
return n.lazyLoad || n.grid?.opts?.lazyLoad && n.lazyLoad !== false;
|
||||
}
|
||||
/**
|
||||
* Create a div element with the specified CSS classes.
|
||||
*
|
||||
* @param classes array of CSS class names to add
|
||||
* @param parent optional parent element to append the div to
|
||||
* @returns the created div element
|
||||
*
|
||||
* @example
|
||||
* const div = Utils.createDiv(['grid-item', 'draggable']);
|
||||
* const nested = Utils.createDiv(['content'], parentDiv);
|
||||
*/
|
||||
static createDiv(classes, parent) {
|
||||
const el = document.createElement('div');
|
||||
classes.forEach(c => { if (c)
|
||||
el.classList.add(c); });
|
||||
parent?.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
/**
|
||||
* Check if a widget should resize to fit its content.
|
||||
*
|
||||
* @param n the grid node to check (can be undefined)
|
||||
* @param strict if true, only returns true for explicit sizeToContent:true (not numbers)
|
||||
* @returns true if the widget should resize to content
|
||||
*
|
||||
* @example
|
||||
* if (Utils.shouldSizeToContent(node)) {
|
||||
* // Trigger content-based resizing
|
||||
* }
|
||||
*/
|
||||
static shouldSizeToContent(n, strict = false) {
|
||||
return n?.grid && (strict ?
|
||||
(n.sizeToContent === true || (n.grid.opts.sizeToContent === true && n.sizeToContent === undefined)) :
|
||||
(!!n.sizeToContent || (n.grid.opts.sizeToContent && n.sizeToContent !== false)));
|
||||
}
|
||||
/**
|
||||
* Check if two grid positions overlap/intersect.
|
||||
*
|
||||
* @param a first position with x, y, w, h properties
|
||||
* @param b second position with x, y, w, h properties
|
||||
* @returns true if the positions overlap
|
||||
*
|
||||
* @example
|
||||
* const overlaps = Utils.isIntercepted(
|
||||
* {x: 0, y: 0, w: 2, h: 1},
|
||||
* {x: 1, y: 0, w: 2, h: 1}
|
||||
* ); // true - they overlap
|
||||
*/
|
||||
static isIntercepted(a, b) {
|
||||
return !(a.y >= b.y + b.h || a.y + a.h <= b.y || a.x + a.w <= b.x || a.x >= b.x + b.w);
|
||||
}
|
||||
/**
|
||||
* Check if two grid positions are touching (edges or corners).
|
||||
*
|
||||
* @param a first position
|
||||
* @param b second position
|
||||
* @returns true if the positions are touching
|
||||
*
|
||||
* @example
|
||||
* const touching = Utils.isTouching(
|
||||
* {x: 0, y: 0, w: 2, h: 1},
|
||||
* {x: 2, y: 0, w: 1, h: 1}
|
||||
* ); // true - they share an edge
|
||||
*/
|
||||
static isTouching(a, b) {
|
||||
return Utils.isIntercepted(a, { x: b.x - 0.5, y: b.y - 0.5, w: b.w + 1, h: b.h + 1 });
|
||||
}
|
||||
/**
|
||||
* Calculate the overlapping area between two grid positions.
|
||||
*
|
||||
* @param a first position
|
||||
* @param b second position
|
||||
* @returns the area of overlap (0 if no overlap)
|
||||
*
|
||||
* @example
|
||||
* const overlap = Utils.areaIntercept(
|
||||
* {x: 0, y: 0, w: 3, h: 2},
|
||||
* {x: 1, y: 0, w: 3, h: 2}
|
||||
* ); // returns 4 (2x2 overlap)
|
||||
*/
|
||||
static areaIntercept(a, b) {
|
||||
const x0 = (a.x > b.x) ? a.x : b.x;
|
||||
const x1 = (a.x + a.w < b.x + b.w) ? a.x + a.w : b.x + b.w;
|
||||
if (x1 <= x0)
|
||||
return 0; // no overlap
|
||||
const y0 = (a.y > b.y) ? a.y : b.y;
|
||||
const y1 = (a.y + a.h < b.y + b.h) ? a.y + a.h : b.y + b.h;
|
||||
if (y1 <= y0)
|
||||
return 0; // no overlap
|
||||
return (x1 - x0) * (y1 - y0);
|
||||
}
|
||||
/**
|
||||
* Calculate the total area of a grid position.
|
||||
*
|
||||
* @param a position with width and height
|
||||
* @returns the total area (width * height)
|
||||
*
|
||||
* @example
|
||||
* const area = Utils.area({x: 0, y: 0, w: 3, h: 2}); // returns 6
|
||||
*/
|
||||
static area(a) {
|
||||
return a.w * a.h;
|
||||
}
|
||||
/**
|
||||
* Sort an array of grid nodes by position (y first, then x).
|
||||
*
|
||||
* @param nodes array of nodes to sort
|
||||
* @param dir sort direction: 1 for ascending (top-left first), -1 for descending
|
||||
* @returns the sorted array (modifies original)
|
||||
*
|
||||
* @example
|
||||
* const sorted = Utils.sort(nodes); // Sort top-left to bottom-right
|
||||
* const reverse = Utils.sort(nodes, -1); // Sort bottom-right to top-left
|
||||
*/
|
||||
static sort(nodes, dir = 1) {
|
||||
const und = 10000;
|
||||
return nodes.sort((a, b) => {
|
||||
const diffY = dir * ((a.y ?? und) - (b.y ?? und));
|
||||
if (diffY === 0)
|
||||
return dir * ((a.x ?? und) - (b.x ?? und));
|
||||
return diffY;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Find a grid node by its ID.
|
||||
*
|
||||
* @param nodes array of nodes to search
|
||||
* @param id the ID to search for
|
||||
* @returns the node with matching ID, or undefined if not found
|
||||
*
|
||||
* @example
|
||||
* const node = Utils.find(nodes, 'widget-1');
|
||||
* if (node) console.log('Found node at:', node.x, node.y);
|
||||
*/
|
||||
static find(nodes, id) {
|
||||
return id ? nodes.find(n => n.id === id) : undefined;
|
||||
}
|
||||
/**
|
||||
* Convert various value types to boolean.
|
||||
* Handles strings like 'false', 'no', '0' as false.
|
||||
*
|
||||
* @param v value to convert
|
||||
* @returns boolean representation
|
||||
*
|
||||
* @example
|
||||
* Utils.toBool('true'); // true
|
||||
* Utils.toBool('false'); // false
|
||||
* Utils.toBool('no'); // false
|
||||
* Utils.toBool('1'); // true
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static toBool(v) {
|
||||
if (typeof v === 'boolean') {
|
||||
return v;
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
v = v.toLowerCase();
|
||||
return !(v === '' || v === 'no' || v === 'false' || v === '0');
|
||||
}
|
||||
return Boolean(v);
|
||||
}
|
||||
/**
|
||||
* Convert a string value to a number, handling null and empty strings.
|
||||
*
|
||||
* @param value string or null value to convert
|
||||
* @returns number value, or undefined for null/empty strings
|
||||
*
|
||||
* @example
|
||||
* Utils.toNumber('42'); // 42
|
||||
* Utils.toNumber(''); // undefined
|
||||
* Utils.toNumber(null); // undefined
|
||||
*/
|
||||
static toNumber(value) {
|
||||
return (value === null || value.length === 0) ? undefined : Number(value);
|
||||
}
|
||||
/**
|
||||
* Parse a height value with units into numeric value and unit string.
|
||||
* Supports px, em, rem, vh, vw, %, cm, mm units.
|
||||
*
|
||||
* @param val height value as number or string with units
|
||||
* @returns object with h (height) and unit properties
|
||||
*
|
||||
* @example
|
||||
* Utils.parseHeight('100px'); // {h: 100, unit: 'px'}
|
||||
* Utils.parseHeight('2rem'); // {h: 2, unit: 'rem'}
|
||||
* Utils.parseHeight(50); // {h: 50, unit: 'px'}
|
||||
*/
|
||||
static parseHeight(val) {
|
||||
let h;
|
||||
let unit = 'px';
|
||||
if (typeof val === 'string') {
|
||||
if (val === 'auto' || val === '')
|
||||
h = 0;
|
||||
else {
|
||||
const match = val.match(/^(-[0-9]+\.[0-9]+|[0-9]*\.[0-9]+|-[0-9]+|[0-9]+)(px|em|rem|vh|vw|%|cm|mm)?$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid height val = ${val}`);
|
||||
}
|
||||
unit = match[2] || 'px';
|
||||
h = parseFloat(match[1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
h = val;
|
||||
}
|
||||
return { h, unit };
|
||||
}
|
||||
/**
|
||||
* Copy unset fields from source objects to target object (shallow merge with defaults).
|
||||
* Similar to Object.assign but only sets undefined/null fields.
|
||||
*
|
||||
* @param target the object to copy defaults into
|
||||
* @param sources one or more source objects to copy defaults from
|
||||
* @returns the modified target object
|
||||
*
|
||||
* @example
|
||||
* const config = { width: 100 };
|
||||
* Utils.defaults(config, { width: 200, height: 50 });
|
||||
* // config is now { width: 100, height: 50 }
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
static defaults(target, ...sources) {
|
||||
sources.forEach(source => {
|
||||
for (const key in source) {
|
||||
if (!source.hasOwnProperty(key))
|
||||
return;
|
||||
if (target[key] === null || target[key] === undefined) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
else if (typeof source[key] === 'object' && typeof target[key] === 'object') {
|
||||
// property is an object, recursively add it's field over... #1373
|
||||
Utils.defaults(target[key], source[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
/**
|
||||
* Compare two objects for equality (shallow comparison).
|
||||
* Checks if objects have the same fields and values at one level deep.
|
||||
*
|
||||
* @param a first object to compare
|
||||
* @param b second object to compare
|
||||
* @returns true if objects have the same values
|
||||
*
|
||||
* @example
|
||||
* Utils.same({x: 1, y: 2}, {x: 1, y: 2}); // true
|
||||
* Utils.same({x: 1}, {x: 1, y: 2}); // false
|
||||
*/
|
||||
static same(a, b) {
|
||||
if (typeof a !== 'object')
|
||||
return a == b;
|
||||
if (typeof a !== typeof b)
|
||||
return false;
|
||||
// else we have object, check just 1 level deep for being same things...
|
||||
if (Object.keys(a).length !== Object.keys(b).length)
|
||||
return false;
|
||||
for (const key in a) {
|
||||
if (a[key] !== b[key])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Copy position and size properties from one widget to another.
|
||||
* Copies x, y, w, h and optionally min/max constraints.
|
||||
*
|
||||
* @param a target widget to copy to
|
||||
* @param b source widget to copy from
|
||||
* @param doMinMax if true, also copy min/max width/height constraints
|
||||
* @returns the target widget (a)
|
||||
*
|
||||
* @example
|
||||
* Utils.copyPos(widget1, widget2); // Copy position/size
|
||||
* Utils.copyPos(widget1, widget2, true); // Also copy constraints
|
||||
*/
|
||||
static copyPos(a, b, doMinMax = false) {
|
||||
if (b.x !== undefined)
|
||||
a.x = b.x;
|
||||
if (b.y !== undefined)
|
||||
a.y = b.y;
|
||||
if (b.w !== undefined)
|
||||
a.w = b.w;
|
||||
if (b.h !== undefined)
|
||||
a.h = b.h;
|
||||
if (doMinMax) {
|
||||
if (b.minW)
|
||||
a.minW = b.minW;
|
||||
if (b.minH)
|
||||
a.minH = b.minH;
|
||||
if (b.maxW)
|
||||
a.maxW = b.maxW;
|
||||
if (b.maxH)
|
||||
a.maxH = b.maxH;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
/** true if a and b has same size & position */
|
||||
static samePos(a, b) {
|
||||
return a && b && a.x === b.x && a.y === b.y && (a.w || 1) === (b.w || 1) && (a.h || 1) === (b.h || 1);
|
||||
}
|
||||
/** given a node, makes sure it's min/max are valid */
|
||||
static sanitizeMinMax(node) {
|
||||
// remove 0, undefine, null
|
||||
if (!node.minW) {
|
||||
delete node.minW;
|
||||
}
|
||||
if (!node.minH) {
|
||||
delete node.minH;
|
||||
}
|
||||
if (!node.maxW) {
|
||||
delete node.maxW;
|
||||
}
|
||||
if (!node.maxH) {
|
||||
delete node.maxH;
|
||||
}
|
||||
}
|
||||
/** removes field from the first object if same as the second objects (like diffing) and internal '_' for saving */
|
||||
static removeInternalAndSame(a, b) {
|
||||
if (typeof a !== 'object' || typeof b !== 'object')
|
||||
return;
|
||||
// skip arrays as we don't know how to hydrate them (unlike object spread operator)
|
||||
if (Array.isArray(a) || Array.isArray(b))
|
||||
return;
|
||||
for (let key in a) {
|
||||
const aVal = a[key];
|
||||
const bVal = b[key];
|
||||
if (key[0] === '_' || aVal === bVal) {
|
||||
delete a[key];
|
||||
}
|
||||
else if (aVal && typeof aVal === 'object' && bVal !== undefined) {
|
||||
Utils.removeInternalAndSame(aVal, bVal);
|
||||
if (!Object.keys(aVal).length) {
|
||||
delete a[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** removes internal fields '_' and default values for saving */
|
||||
static removeInternalForSave(n, removeEl = true) {
|
||||
for (let key in n) {
|
||||
if (key[0] === '_' || n[key] === null || n[key] === undefined)
|
||||
delete n[key];
|
||||
}
|
||||
delete n.grid;
|
||||
if (removeEl)
|
||||
delete n.el;
|
||||
// delete default values (will be re-created on read)
|
||||
if (!n.autoPosition)
|
||||
delete n.autoPosition;
|
||||
if (!n.noResize)
|
||||
delete n.noResize;
|
||||
if (!n.noMove)
|
||||
delete n.noMove;
|
||||
if (!n.locked)
|
||||
delete n.locked;
|
||||
if (n.w === 1 || n.w === n.minW)
|
||||
delete n.w;
|
||||
if (n.h === 1 || n.h === n.minH)
|
||||
delete n.h;
|
||||
}
|
||||
/** return the closest parent (or itself) matching the given class */
|
||||
// static closestUpByClass(el: HTMLElement, name: string): HTMLElement {
|
||||
// while (el) {
|
||||
// if (el.classList.contains(name)) return el;
|
||||
// el = el.parentElement
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
/** delay calling the given function for given delay, preventing new calls from happening while waiting */
|
||||
static throttle(func, delay) {
|
||||
let isWaiting = false;
|
||||
return (...args) => {
|
||||
if (!isWaiting) {
|
||||
isWaiting = true;
|
||||
setTimeout(() => { func(...args); isWaiting = false; }, delay);
|
||||
}
|
||||
};
|
||||
}
|
||||
static removePositioningStyles(el) {
|
||||
const style = el.style;
|
||||
if (style.position) {
|
||||
style.removeProperty('position');
|
||||
}
|
||||
if (style.left) {
|
||||
style.removeProperty('left');
|
||||
}
|
||||
if (style.top) {
|
||||
style.removeProperty('top');
|
||||
}
|
||||
if (style.width) {
|
||||
style.removeProperty('width');
|
||||
}
|
||||
if (style.height) {
|
||||
style.removeProperty('height');
|
||||
}
|
||||
}
|
||||
/** @internal returns the passed element if scrollable, else the closest parent that will, up to the entire document scrolling element */
|
||||
static getScrollElement(el) {
|
||||
if (!el)
|
||||
return document.scrollingElement || document.documentElement; // IE support
|
||||
const style = getComputedStyle(el);
|
||||
const overflowRegex = /(auto|scroll)/;
|
||||
if (overflowRegex.test(style.overflow + style.overflowY)) {
|
||||
return el;
|
||||
}
|
||||
else {
|
||||
return Utils.getScrollElement(el.parentElement);
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
static updateScrollPosition(el, position, distance) {
|
||||
const scrollEl = Utils.getScrollElement(el);
|
||||
if (!scrollEl)
|
||||
return;
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const scrollRect = scrollEl.getBoundingClientRect();
|
||||
const innerHeightOrClientHeight = (window.innerHeight || document.documentElement.clientHeight);
|
||||
const offsetDiffDown = elRect.bottom - Math.min(scrollRect.bottom, innerHeightOrClientHeight);
|
||||
const offsetDiffUp = elRect.top - Math.max(scrollRect.top, 0);
|
||||
const prevScroll = scrollEl.scrollTop;
|
||||
if (offsetDiffUp < 0 && distance < 0) {
|
||||
// scroll up
|
||||
if (el.offsetHeight > scrollRect.height) {
|
||||
scrollEl.scrollTop += distance;
|
||||
}
|
||||
else {
|
||||
scrollEl.scrollTop += Math.abs(offsetDiffUp) > Math.abs(distance) ? distance : offsetDiffUp;
|
||||
}
|
||||
}
|
||||
else if (offsetDiffDown > 0 && distance > 0) {
|
||||
// scroll down
|
||||
if (el.offsetHeight > scrollRect.height) {
|
||||
scrollEl.scrollTop += distance;
|
||||
}
|
||||
else {
|
||||
scrollEl.scrollTop += offsetDiffDown > distance ? distance : offsetDiffDown;
|
||||
}
|
||||
}
|
||||
position.top += scrollEl.scrollTop - prevScroll;
|
||||
}
|
||||
/**
|
||||
* @internal Function used to scroll the page.
|
||||
*
|
||||
* @param event `MouseEvent` that triggers the resize
|
||||
* @param el `HTMLElement` that's being resized
|
||||
* @param distance Distance from the V edges to start scrolling
|
||||
*/
|
||||
static updateScrollResize(event, el, distance) {
|
||||
const scrollEl = Utils.getScrollElement(el);
|
||||
const height = scrollEl.clientHeight;
|
||||
// #1727 event.clientY is relative to viewport, so must compare this against position of scrollEl getBoundingClientRect().top
|
||||
// #1745 Special situation if scrollEl is document 'html': here browser spec states that
|
||||
// clientHeight is height of viewport, but getBoundingClientRect() is rectangle of html element;
|
||||
// this discrepancy arises because in reality scrollbar is attached to viewport, not html element itself.
|
||||
const offsetTop = (scrollEl === Utils.getScrollElement()) ? 0 : scrollEl.getBoundingClientRect().top;
|
||||
const pointerPosY = event.clientY - offsetTop;
|
||||
const top = pointerPosY < distance;
|
||||
const bottom = pointerPosY > height - distance;
|
||||
if (top) {
|
||||
// This also can be done with a timeout to keep scrolling while the mouse is
|
||||
// in the scrolling zone. (will have smoother behavior)
|
||||
scrollEl.scrollBy({ behavior: 'smooth', top: pointerPosY - distance });
|
||||
}
|
||||
else if (bottom) {
|
||||
scrollEl.scrollBy({ behavior: 'smooth', top: distance - (height - pointerPosY) });
|
||||
}
|
||||
}
|
||||
/** single level clone, returning a new object with same top fields. This will share sub objects and arrays */
|
||||
static clone(obj) {
|
||||
if (obj === null || obj === undefined || typeof (obj) !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
// return Object.assign({}, obj);
|
||||
if (obj instanceof Array) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return [...obj];
|
||||
}
|
||||
return { ...obj };
|
||||
}
|
||||
/**
|
||||
* Recursive clone version that returns a full copy, checking for nested objects and arrays ONLY.
|
||||
* Note: this will use as-is any key starting with double __ (and not copy inside) some lib have circular dependencies.
|
||||
*/
|
||||
static cloneDeep(obj) {
|
||||
// list of fields we will skip during cloneDeep (nested objects, other internal)
|
||||
const skipFields = ['parentGrid', 'el', 'grid', 'subGrid', 'engine'];
|
||||
// return JSON.parse(JSON.stringify(obj)); // doesn't work with date format ?
|
||||
const ret = Utils.clone(obj);
|
||||
for (const key in ret) {
|
||||
// NOTE: we don't support function/circular dependencies so skip those properties for now...
|
||||
if (ret.hasOwnProperty(key) && typeof (ret[key]) === 'object' && key.substring(0, 2) !== '__' && !skipFields.find(k => k === key)) {
|
||||
ret[key] = Utils.cloneDeep(obj[key]);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
/** deep clone the given HTML node, removing teh unique id field */
|
||||
static cloneNode(el) {
|
||||
const node = el.cloneNode(true);
|
||||
node.removeAttribute('id');
|
||||
return node;
|
||||
}
|
||||
static appendTo(el, parent) {
|
||||
let parentNode;
|
||||
if (typeof parent === 'string') {
|
||||
parentNode = Utils.getElement(parent);
|
||||
}
|
||||
else {
|
||||
parentNode = parent;
|
||||
}
|
||||
if (parentNode) {
|
||||
parentNode.appendChild(el);
|
||||
}
|
||||
}
|
||||
// public static setPositionRelative(el: HTMLElement): void {
|
||||
// if (!(/^(?:r|a|f)/).test(getComputedStyle(el).position)) {
|
||||
// el.style.position = "relative";
|
||||
// }
|
||||
// }
|
||||
static addElStyles(el, styles) {
|
||||
if (styles instanceof Object) {
|
||||
for (const s in styles) {
|
||||
if (styles.hasOwnProperty(s)) {
|
||||
if (Array.isArray(styles[s])) {
|
||||
// support fallback value
|
||||
styles[s].forEach(val => {
|
||||
el.style[s] = val;
|
||||
});
|
||||
}
|
||||
else {
|
||||
el.style[s] = styles[s];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static initEvent(e, info) {
|
||||
const evt = { type: info.type };
|
||||
const obj = {
|
||||
button: 0,
|
||||
which: 0,
|
||||
buttons: 1,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
target: info.target ? info.target : e.target
|
||||
};
|
||||
['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].forEach(p => evt[p] = e[p]); // keys
|
||||
['pageX', 'pageY', 'clientX', 'clientY', 'screenX', 'screenY'].forEach(p => evt[p] = e[p]); // point info
|
||||
return { ...evt, ...obj };
|
||||
}
|
||||
/** copies the MouseEvent (or convert Touch) properties and sends it as another event to the given target */
|
||||
static simulateMouseEvent(e, simulatedType, target) {
|
||||
const me = e;
|
||||
const simulatedEvent = new MouseEvent(simulatedType, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
detail: 1,
|
||||
screenX: e.screenX,
|
||||
screenY: e.screenY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: me.ctrlKey ?? false,
|
||||
altKey: me.altKey ?? false,
|
||||
shiftKey: me.shiftKey ?? false,
|
||||
metaKey: me.metaKey ?? false,
|
||||
button: 0,
|
||||
relatedTarget: e.target
|
||||
});
|
||||
(target || e.target).dispatchEvent(simulatedEvent);
|
||||
}
|
||||
/**
|
||||
* defines an element that is used to get the offset and scale from grid transforms
|
||||
* returns the scale and offsets from said element
|
||||
*/
|
||||
static getValuesFromTransformedElement(parent) {
|
||||
const transformReference = document.createElement('div');
|
||||
Utils.addElStyles(transformReference, {
|
||||
opacity: '0',
|
||||
position: 'fixed',
|
||||
top: 0 + 'px',
|
||||
left: 0 + 'px',
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
zIndex: '-999999',
|
||||
});
|
||||
parent.appendChild(transformReference);
|
||||
const transformValues = transformReference.getBoundingClientRect();
|
||||
parent.removeChild(transformReference);
|
||||
transformReference.remove();
|
||||
return {
|
||||
xScale: 1 / transformValues.width,
|
||||
yScale: 1 / transformValues.height,
|
||||
xOffset: transformValues.left,
|
||||
yOffset: transformValues.top,
|
||||
};
|
||||
}
|
||||
/** swap the given object 2 field values */
|
||||
static swap(o, a, b) {
|
||||
if (!o)
|
||||
return;
|
||||
const tmp = o[a];
|
||||
o[a] = o[b];
|
||||
o[b] = tmp;
|
||||
}
|
||||
/** returns true if event is inside the given element rectangle */
|
||||
// Note: Safari Mac has null event.relatedTarget which causes #1684 so check if DragEvent is inside the coordinates instead
|
||||
// Utils.el.contains(event.relatedTarget as HTMLElement)
|
||||
// public static inside(e: MouseEvent, el: HTMLElement): boolean {
|
||||
// // srcElement, toElement, target: all set to placeholder when leaving simple grid, so we can't use that (Chrome)
|
||||
// const target: HTMLElement = e.relatedTarget || (e as any).fromElement;
|
||||
// if (!target) {
|
||||
// const { bottom, left, right, top } = el.getBoundingClientRect();
|
||||
// return (e.x < right && e.x > left && e.y < bottom && e.y > top);
|
||||
// }
|
||||
// return el.contains(target);
|
||||
// }
|
||||
/** true if the item can be rotated (checking for prop, not space available) */
|
||||
static canBeRotated(n) {
|
||||
return !(!n || n.w === n.h || n.locked || n.noResize || n.grid?.opts.disableResize || (n.minW && n.minW === n.maxW) || (n.minH && n.minH === n.maxH));
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=utils.js.map
|
||||
1
node_modules/gridstack/dist/utils.js.map
generated
vendored
Normal file
1
node_modules/gridstack/dist/utils.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6199
node_modules/gridstack/doc/API.md
generated
vendored
Normal file
6199
node_modules/gridstack/doc/API.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
114
node_modules/gridstack/package.json
generated
vendored
Normal file
114
node_modules/gridstack/package.json
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"name": "gridstack",
|
||||
"version": "12.4.2",
|
||||
"license": "MIT",
|
||||
"author": "Alain Dumesny <alaind831+github@gmail.com> (https://github.com/adumesny)",
|
||||
"contributors": [
|
||||
"Pavel Reznikov <pashka.reznikov@gmail.com>",
|
||||
"Dylan Weiss <dylan.weiss@gmail.com> (https://dylandreams.com)"
|
||||
],
|
||||
"description": "TypeScript/JS lib for dashboard layout and creation, responsive, mobile support, no external dependencies, with many wrappers (React, Angular, Vue, Ember, knockout...)",
|
||||
"main": "./dist/gridstack.js",
|
||||
"types": "./dist/gridstack.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/gridstack/gridstack.js.git"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://www.paypal.me/alaind831"
|
||||
},
|
||||
{
|
||||
"type": "venmo",
|
||||
"url": "https://www.venmo.com/adumesny"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn build:ng && grunt && webpack && tsc --project tsconfig.build.json --stripInternal && yarn doc",
|
||||
"build:ng": "cd angular && yarn build lib",
|
||||
"w": "webpack",
|
||||
"t": "rm -rf dist/* && grunt && tsc --project tsconfig.build.json --stripInternal",
|
||||
"doc": "doctoc ./README.md && doctoc ./doc/CHANGES.md && node scripts/generate-docs.js",
|
||||
"doc:main": "node scripts/generate-docs.js --main-only",
|
||||
"doc:angular": "node scripts/generate-docs.js --angular-only",
|
||||
"test": "yarn lint && vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage:ui": "vitest --ui --coverage.enabled=true",
|
||||
"test:coverage:detailed": "vitest run --config .vitestrc.coverage.ts",
|
||||
"test:coverage:html": "vitest run --coverage && open coverage/index.html",
|
||||
"test:coverage:lcov": "vitest run --coverage --coverage.reporter=lcov",
|
||||
"test:ci": "vitest run --coverage --reporter=verbose --reporter=junit --outputFile.junit=./coverage/junit-report.xml",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"lint": "tsc --project tsconfig.build.json --noEmit && eslint src/*.ts angular/projects/lib/src/**/*.ts",
|
||||
"reset": "rm -rf dist node_modules",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"keywords": [
|
||||
"Typescript",
|
||||
"gridstack.js",
|
||||
"grid",
|
||||
"gridster",
|
||||
"layout",
|
||||
"responsive",
|
||||
"dashboard",
|
||||
"resize",
|
||||
"drag&drop",
|
||||
"widgets",
|
||||
"Angular",
|
||||
"React",
|
||||
"Vue",
|
||||
"JavaScript"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/gridstack/gridstack.js/issues"
|
||||
},
|
||||
"homepage": "http://gridstackjs.com/",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"@vitest/ui": "^2.0.5",
|
||||
"connect": "^3.7.0",
|
||||
"core-js": "^3.30.1",
|
||||
"coveralls": "^3.1.1",
|
||||
"doctoc": "^2.2.1",
|
||||
"eslint": "^8.38.0",
|
||||
"grunt": "^1.6.1",
|
||||
"grunt-cli": "^1.3.2",
|
||||
"grunt-contrib-connect": "^3.0.0",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-cssmin": "^4.0.0",
|
||||
"grunt-contrib-uglify": "^5.2.2",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-eslint": "^24.0.1",
|
||||
"grunt-protractor-runner": "^5.0.0",
|
||||
"grunt-protractor-webdriver": "^0.2.5",
|
||||
"grunt-sass": "3.1.0",
|
||||
"happy-dom": "^20.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"protractor": "^7.0.0",
|
||||
"sass": "^1.62.0",
|
||||
"serve-static": "^1.15.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typedoc": "^0.28.9",
|
||||
"typedoc-plugin-markdown": "^4.8.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^2.0.5",
|
||||
"webpack": "^5.79.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"files": [
|
||||
"dist",
|
||||
"doc/API.md"
|
||||
]
|
||||
}
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"datatables.net-responsive": "^2.5.1",
|
||||
"datatables.net-scroller": "^2.4.1",
|
||||
"datatables.net-select": "^1.7.1",
|
||||
"gridstack": "^12.4.2",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-contextmenu": "^2.9.2",
|
||||
"jquery-migrate": "^3.4.1",
|
||||
@@ -223,6 +224,22 @@
|
||||
"delegate": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gridstack": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-12.4.2.tgz",
|
||||
"integrity": "sha512-aXbJrQpi3LwpYXYOr4UriPM5uc/dPcjK01SdOE5PDpx2vi8tnLhU7yBg/1i4T59UhNkG/RBfabdFUObuN+gMnw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://www.paypal.me/alaind831"
|
||||
},
|
||||
{
|
||||
"type": "venmo",
|
||||
"url": "https://www.venmo.com/adumesny"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
@@ -516,6 +533,11 @@
|
||||
"delegate": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"gridstack": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-12.4.2.tgz",
|
||||
"integrity": "sha512-aXbJrQpi3LwpYXYOr4UriPM5uc/dPcjK01SdOE5PDpx2vi8tnLhU7yBg/1i4T59UhNkG/RBfabdFUObuN+gMnw=="
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"datatables.net-responsive": "^2.5.1",
|
||||
"datatables.net-scroller": "^2.4.1",
|
||||
"datatables.net-select": "^1.7.1",
|
||||
"gridstack": "^12.4.2",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-contextmenu": "^2.9.2",
|
||||
"jquery-migrate": "^3.4.1",
|
||||
|
||||
@@ -1367,6 +1367,12 @@ try {
|
||||
DisplayWelcomePopup($oP);
|
||||
$oKPI->ComputeAndReport('Compute page');
|
||||
$oP->output();
|
||||
if (Session::$fLastStartTime !== 0.0) {
|
||||
IssueLog::Warning('Session left open for: '.(microtime(true) - Session::$fLastStartTime));
|
||||
}
|
||||
if (\PHP_SESSION_NONE !== session_status()) {
|
||||
IssueLog::Warning('Session left open by a lib');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$oErrorPage = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
if ($e instanceof SecurityException) {
|
||||
|
||||
@@ -867,53 +867,6 @@ try {
|
||||
$oObj->DisplayDashboard($oPage, $sAttCode);
|
||||
break;
|
||||
|
||||
case 'export_dashboard':
|
||||
$oPage = new DownloadPage('');
|
||||
$sDashboardId = utils::ReadParam('id', '', false, 'raw_data');
|
||||
$sDashboardFileRelative = utils::ReadParam('file', '', false, 'raw_data');
|
||||
|
||||
$sDashboardFile = RuntimeDashboard::GetDashboardFileFromRelativePath($sDashboardFileRelative);
|
||||
|
||||
$oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId);
|
||||
if (!is_null($oDashboard)) {
|
||||
$oPage->TrashUnexpectedOutput();
|
||||
$oPage->SetContentType('text/xml');
|
||||
$oPage->SetContentDisposition('attachment', 'dashboard_'.$oDashboard->GetTitle().'.xml');
|
||||
$oPage->add($oDashboard->ToXml());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'import_dashboard':
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
$sTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id');
|
||||
if (!utils::IsTransactionValid($sTransactionId, true)) {
|
||||
throw new SecurityException('ajax.render.php import_dashboard : invalid transaction_id');
|
||||
}
|
||||
$sDashboardId = utils::ReadParam('id', '', false, 'raw_data');
|
||||
$sDashboardFileRelative = utils::ReadParam('file', '', false, 'raw_data');
|
||||
|
||||
$sDashboardFile = RuntimeDashboard::GetDashboardFileFromRelativePath($sDashboardFileRelative);
|
||||
|
||||
$oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId);
|
||||
$aResult = ['error' => ''];
|
||||
if (!is_null($oDashboard)) {
|
||||
try {
|
||||
$oDoc = utils::ReadPostedDocument('dashboard_upload_file');
|
||||
$oDashboard->FromXml($oDoc->GetData());
|
||||
$oDashboard->Save();
|
||||
} catch (DOMException $e) {
|
||||
$aResult = ['error' => Dict::S('UI:Error:InvalidDashboardFile')];
|
||||
} catch (Exception $e) {
|
||||
$aResult = ['error' => $e->getMessage()];
|
||||
}
|
||||
} else {
|
||||
$aResult['error'] = 'Dashboard id="'.$sDashboardId.'" not found.';
|
||||
}
|
||||
$oPage->SetData($aResult);
|
||||
break;
|
||||
|
||||
case 'toggle_dashboard':
|
||||
$oPage->SetContentType('text/html');
|
||||
$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
|
||||
@@ -951,50 +904,6 @@ try {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'save_dashboard':
|
||||
$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'context_param');
|
||||
|
||||
$aExtraParams = utils::ReadParam('extra_params', [], false, 'raw_data');
|
||||
$sReloadURL = utils::ReadParam('reload_url', '', false, utils::ENUM_SANITIZATION_FILTER_URL);
|
||||
appUserPreferences::SetPref('display_original_dashboard_'.$sDashboardId, false);
|
||||
$sJSExtraParams = json_encode($aExtraParams);
|
||||
$aParams = [];
|
||||
$aParams['layout_class'] = utils::ReadParam('layout_class', '');
|
||||
$aParams['title'] = utils::ReadParam('title', '', false, 'raw_data');
|
||||
$aParams['auto_reload'] = utils::ReadParam('auto_reload', false);
|
||||
$aParams['auto_reload_sec'] = utils::ReadParam('auto_reload_sec', 300);
|
||||
$aParams['cells'] = utils::ReadParam('cells', [], false, 'raw_data');
|
||||
|
||||
$oDashboard = new RuntimeDashboard($sDashboardId);
|
||||
$oDashboard->FromParams($aParams);
|
||||
$bIsNew = $oDashboard->Save();
|
||||
|
||||
$sDashboardFile = addslashes(utils::ReadParam('file', '', false, 'string'));
|
||||
$sDashboardDivId = preg_replace('/[^a-zA-Z0-9_]/', '', $sDashboardId);
|
||||
$sOperation = 'reload_dashboard';
|
||||
if ($bIsNew) {
|
||||
// Trigger a reload of the current page since the dashboard just changed
|
||||
$oPage->add_script(
|
||||
<<<JS
|
||||
window.location.reload();
|
||||
JS
|
||||
);
|
||||
} else {
|
||||
$oPage->add_script(
|
||||
<<<JS
|
||||
$('.ibo-dashboard#{$sDashboardDivId}').block();
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
|
||||
{ operation: 'reload_dashboard', dashboard_id: '{$sDashboardId}', file: '{$sDashboardFile}', extra_params: {$sJSExtraParams}, reload_url: '{$sReloadURL}'},
|
||||
function(data){
|
||||
$('.ibo-dashboard#{$sDashboardDivId}').html(data);
|
||||
$('.ibo-dashboard#{$sDashboardDivId}').unblock();
|
||||
}
|
||||
);
|
||||
JS
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'revert_dashboard':
|
||||
$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
|
||||
$sReloadURL = utils::ReadParam('reload_url', '', false, utils::ENUM_SANITIZATION_FILTER_URL);
|
||||
@@ -1018,157 +927,6 @@ EOF
|
||||
);
|
||||
break;
|
||||
|
||||
case 'render_dashboard':
|
||||
$sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data');
|
||||
$aExtraParams = utils::ReadParam('extra_params', [], false, 'raw_data');
|
||||
$aParams = [];
|
||||
$aParams['layout_class'] = utils::ReadParam('layout_class', '');
|
||||
$aParams['title'] = utils::ReadParam('title', '', false, 'raw_data');
|
||||
$aParams['cells'] = utils::ReadParam('cells', [], false, 'raw_data');
|
||||
$aParams['auto_reload'] = utils::ReadParam('auto_reload', false);
|
||||
$aParams['auto_reload_sec'] = utils::ReadParam('auto_reload_sec', 300);
|
||||
$sReloadURL = utils::ReadParam('reload_url', '', false, utils::ENUM_SANITIZATION_FILTER_URL);
|
||||
$oDashboard = new RuntimeDashboard($sDashboardId);
|
||||
$oDashboard->FromParams($aParams);
|
||||
$oDashboard->SetReloadURL($sReloadURL);
|
||||
$oDashboard->Render($oPage, true /* bEditMode */, $aExtraParams);
|
||||
break;
|
||||
|
||||
case 'dashboard_editor':
|
||||
$sId = utils::ReadParam('id', '', false, 'context_param');
|
||||
$aExtraParams = utils::ReadParam('extra_params', [], false, 'raw_data');
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($sId, '', 'element_identifier');
|
||||
$sDashboardFile = utils::ReadParam('file', '', false, 'string');
|
||||
$sReloadURL = utils::ReadParam('reload_url', '', false, utils::ENUM_SANITIZATION_FILTER_URL);
|
||||
$oDashboard = RuntimeDashboard::GetDashboardToEdit($sDashboardFile, $sId);
|
||||
if (!is_null($oDashboard)) {
|
||||
if (!empty($sReloadURL)) {
|
||||
$oDashboard->SetReloadURL($sReloadURL);
|
||||
}
|
||||
$oDashboard->RenderEditor($oPage, $aExtraParams);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'new_dashlet_id':
|
||||
$sDashboardDivId = utils::ReadParam("dashboardid");
|
||||
$bIsCustomized = true; // Only called at runtime when customizing a dashboard
|
||||
$iRow = utils::ReadParam("iRow", 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
|
||||
$iCol = utils::ReadParam("iCol", 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
|
||||
$sDashletIdOrig = utils::ReadParam("dashletid", '', false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
$sFinalDashletId = Dashboard::GetDashletUniqueId($bIsCustomized, $sDashboardDivId, $iRow, $iCol, $sDashletIdOrig);
|
||||
$oPage = new AjaxPage('');
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
$oPage->add($sFinalDashletId);
|
||||
break;
|
||||
|
||||
case 'new_dashlet':
|
||||
require_once(APPROOT.'application/forms.class.inc.php');
|
||||
require_once(APPROOT.'application/dashlet.class.inc.php');
|
||||
$sDashletClass = utils::ReadParam('dashlet_class', '', false, utils::ENUM_SANITIZATION_FILTER_PHP_CLASS);
|
||||
$sDashletId = utils::ReadParam('dashlet_id', '', false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
if (is_subclass_of($sDashletClass, 'Dashlet')) {
|
||||
$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
|
||||
$offset = $oPage->start_capture();
|
||||
$oDashlet->DoRender($oPage, true /* bEditMode */, false /* bEnclosingDiv */);
|
||||
$sHtml = addslashes($oPage->end_capture($offset));
|
||||
$sHtml = str_replace("\n", '', $sHtml);
|
||||
$sHtml = str_replace("\r", '', $sHtml);
|
||||
$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
|
||||
// but is executed BEFORE all 'ready_scripts'
|
||||
$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', ['operation' => 'update_dashlet_property']);
|
||||
$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
|
||||
$sHtml = str_replace("\n", '', $sHtml);
|
||||
$sHtml = str_replace("\r", '', $sHtml);
|
||||
$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script // but is executed BEFORE all 'ready_scripts'
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_dashlet_property':
|
||||
require_once(APPROOT.'application/forms.class.inc.php');
|
||||
require_once(APPROOT.'application/dashlet.class.inc.php');
|
||||
$aExtraParams = utils::ReadParam('extra_params', [], false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$aParams = utils::ReadParam('params', [], false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); // raw_data because we need different filter depending on the options
|
||||
$sDashletClass = utils::Sanitize($aParams['attr_dashlet_class'], DashletUnknown::class, utils::ENUM_SANITIZATION_FILTER_PHP_CLASS); // Dashlet PHP class or DashletUnknown if impl isn't present in the installed extensions
|
||||
$sDashletType = utils::Sanitize($aParams['attr_dashlet_type'], '', utils::ENUM_SANITIZATION_FILTER_PHP_CLASS); // original Dashlet PHP class, could be non-existing on the iTop instance (XML definition loading)
|
||||
$sDashletId = utils::Sanitize($aParams['attr_dashlet_id'], '', utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
$aUpdatedProperties = utils::Sanitize($aParams['updated'], [], utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER); // Code of the changed properties as an array: 'attr_xxx', 'attr_xxy' etc
|
||||
$aPreviousValues = utils::Sanitize($aParams['previous_values'], [], utils::ENUM_SANITIZATION_FILTER_RAW_DATA); // hash array: 'attr_xxx' => 'old_value' - no sanitization as values will be handled in the Dashlet object
|
||||
|
||||
if (is_subclass_of($sDashletClass, 'Dashlet')) {
|
||||
/** @var \Dashlet $oDashlet */
|
||||
$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
|
||||
$oDashlet->SetDashletType($sDashletType);
|
||||
$oForm = $oDashlet->GetForm();
|
||||
$aValues = $oForm->ReadParams(); // hash array: 'xxx' => 'new_value'
|
||||
|
||||
$aCurrentValues = $aValues;
|
||||
$aUpdatedDecoded = [];
|
||||
foreach ($aUpdatedProperties as $sProp) {
|
||||
$sDecodedProp = str_replace('attr_', '', $sProp); // Remove the attr_ prefix
|
||||
// Set the previous value
|
||||
if (isset($aPreviousValues[$sProp]) && $aPreviousValues[$sProp] != '') {
|
||||
$aCurrentValues[$sDecodedProp] = $aPreviousValues[$sProp];
|
||||
} else {
|
||||
if (gettype($aCurrentValues[$sDecodedProp]) == "array") {
|
||||
$aCurrentValues[$sDecodedProp] = [];
|
||||
} else {
|
||||
$aCurrentValues[$sDecodedProp] = '';
|
||||
}
|
||||
}
|
||||
$aUpdatedDecoded[] = $sDecodedProp;
|
||||
}
|
||||
|
||||
$oDashlet->FromParams($aCurrentValues);
|
||||
$sPrevClass = get_class($oDashlet);
|
||||
$oDashlet = $oDashlet->Update($aValues, $aUpdatedDecoded);
|
||||
$sNewClass = get_class($oDashlet);
|
||||
if ($sNewClass != $sPrevClass) {
|
||||
$oPage->add_ready_script("$('#dashlet_$sDashletId').dashlet('option', {dashlet_class: '$sNewClass'});");
|
||||
}
|
||||
if ($oDashlet->IsRedrawNeeded()) {
|
||||
$oBlock = $oDashlet->DoRender($oPage, true, false, $aExtraParams);
|
||||
$sHtml = ConsoleBlockRenderer::RenderBlockTemplateInPage($oPage, $oBlock);
|
||||
$sHtml = json_encode($sHtml);
|
||||
$oPage->add_script("$('#dashlet_$sDashletId').html({$sHtml});");
|
||||
}
|
||||
if ($oDashlet->IsFormRedrawNeeded()) {
|
||||
$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', ['operation' => 'update_dashlet_property', 'extra_params' => $aExtraParams]);
|
||||
$sHtml = $oForm->RenderAsPropertySheet($oPage, true, '.itop-dashboard');
|
||||
$sHtml = json_encode($sHtml);
|
||||
$oPage->add_script("$('#dashlet_properties_$sDashletId').html({$sHtml});");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dashlet_creation_dlg':
|
||||
$sOQL = utils::ReadParam('oql', '', false, 'raw_data');
|
||||
RuntimeDashboard::GetDashletCreationDlgFromOQL($oPage, $sOQL);
|
||||
break;
|
||||
|
||||
case 'add_dashlet':
|
||||
$oForm = RuntimeDashboard::GetDashletCreationForm();
|
||||
$aValues = $oForm->ReadParams();
|
||||
|
||||
$sDashletClass = $aValues['dashlet_class'];
|
||||
$sMenuId = $aValues['menu_id'];
|
||||
|
||||
if (is_subclass_of($sDashletClass, 'Dashlet')) {
|
||||
$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0);
|
||||
$oDashlet->FromParams($aValues);
|
||||
|
||||
ApplicationMenu::LoadAdditionalMenus();
|
||||
$index = ApplicationMenu::GetMenuIndexById($sMenuId);
|
||||
$oMenu = ApplicationMenu::GetMenuNode($index);
|
||||
$oMenu->AddDashlet($oDashlet);
|
||||
// navigate to the dashboard page
|
||||
if ($aValues['open_editor']) {
|
||||
$oPage->add_ready_script("window.location.href='".addslashes(utils::GetAbsoluteUrlAppRoot().'pages/UI.php?c[menu]='.urlencode($sMenuId))."&edit=1';"); // reloads the page, doing a GET even if we arrived via a POST
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'shortcut_list_dlg':
|
||||
$sOQL = utils::ReadParam('oql', '', false, 'raw_data');
|
||||
$sTableSettings = utils::ReadParam('table_settings', '', false, 'raw_data');
|
||||
|
||||
@@ -704,6 +704,9 @@ PHP;
|
||||
$oPropertyTypesNode = $this->oFactory->GetNodes('/itop_design/meta/property_types')->item(0);
|
||||
$this->CompilePropertyTypes($oPropertyTypesNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
$oDashletNode = $this->oFactory->GetNodes('/itop_design/meta/dashlets')->item(0);
|
||||
$this->CompileDashlets($oDashletNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
// Compile the XML parameters
|
||||
/** @var \MFElement $oParametersNode */
|
||||
$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
|
||||
@@ -3592,6 +3595,16 @@ EOF;
|
||||
}
|
||||
}
|
||||
|
||||
protected function CompileDashlets(?DOMNode $oDashlets, string $sTempTargetDir, string $sFinalTargetDir): void
|
||||
{
|
||||
if ($oDashlets) {
|
||||
$oDoc = new DesignDocument();
|
||||
$oClone = $oDoc->importNode($oDashlets->cloneNode(true), true);
|
||||
$oDoc->appendChild($oClone);
|
||||
$oDoc->save($sTempTargetDir.'/core/dashlets.xml');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
|
||||
297
sources/Application/Dashboard/Controller/DashboardController.php
Normal file
297
sources/Application/Dashboard/Controller/DashboardController.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard\Controller;
|
||||
|
||||
use appUserPreferences;
|
||||
use Combodo\iTop\Application\Dashlet\DashletFactory;
|
||||
use Combodo\iTop\Application\TwigBase\Controller\Controller;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletWrapper;
|
||||
use Combodo\iTop\Application\UI\Base\Component\TurboForm\TurboFormUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\iUIBlock;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\AjaxPage;
|
||||
use Combodo\iTop\Application\WebPage\DownloadPage;
|
||||
use Combodo\iTop\Application\WebPage\JsonPage;
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\FormBlockService;
|
||||
use Combodo\iTop\PropertyType\PropertyTypeDesign;
|
||||
use Combodo\iTop\PropertyType\Serializer\XMLSerializer;
|
||||
use Combodo\iTop\Service\ServiceLocator\ServiceLocator;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use Dict;
|
||||
use DOMException;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use ModelReflectionRuntime;
|
||||
use RuntimeDashboard;
|
||||
use SecurityException;
|
||||
use UserRights;
|
||||
use utils;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public const ROUTE_NAMESPACE = 'dashboard';
|
||||
|
||||
private FormBlockService $oFormBlockService;
|
||||
private XMLSerializer $oXMLSerializer;
|
||||
|
||||
public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = [], array $aThemes = ['application/forms/itop_console_layout.html.twig', 'application/forms/wip_form_demonstrator.html.twig'])
|
||||
{
|
||||
parent::__construct($sViewPath, $sModuleName, $aAdditionalPaths, $aThemes);
|
||||
$this->oFormBlockService = MetaModel::GetService('FormBlockService');
|
||||
$this->oXMLSerializer = MetaModel::GetService('XMLSerializer');
|
||||
}
|
||||
|
||||
public function OperationGetDashlet()
|
||||
{
|
||||
// TODO 3.3 Do we want to use a readparam here or SF internal mechanism ?
|
||||
$sDashletClass = utils::ReadParam('dashlet_class', '', false, utils::ENUM_SANITIZATION_FILTER_PHP_CLASS);
|
||||
$sDashletId = utils::ReadParam('dashlet_id', '', false, utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
// TODO 3.3 Check if raw data is the right call
|
||||
$sValues = utils::ReadParam('values', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$aValues = !empty($sValues) ? json_decode($sValues, true) : [];
|
||||
|
||||
$oPage = new AjaxPage('');
|
||||
|
||||
if (is_a($sDashletClass, 'Dashlet', true)) {
|
||||
// TODO 3.3 Make a real unique id if none is provided
|
||||
$sDashletId = !empty($sDashletId) ? $sDashletId : uniqid();
|
||||
|
||||
$oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, $sDashletId);
|
||||
|
||||
if (!empty($aValues)) {
|
||||
$oDashlet->FromModelData($aValues);
|
||||
} else {
|
||||
$aValues = $oDashlet->GetModelData();
|
||||
}
|
||||
|
||||
// TODO 3.3 Removing bEditMode for dashlet rendering fixes id having an "_edit" issues, but is it the right solution ?
|
||||
$oDashletBlock = $oDashlet->DoRender($oPage, false /* bEditMode */, false /* bEnclosingDiv */);
|
||||
|
||||
if ($oDashletBlock instanceof iUIBlock) {
|
||||
// Wrap the dashlet
|
||||
// TODO 3.3 Re-normalize Dashlet's values instead of using user input
|
||||
$oDashletWrapper = new DashletWrapper($oDashletBlock, $sDashletClass, $oDashlet->GetID(), $aValues);
|
||||
$oPage->AddUiBlock($oDashletWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationGetDashletForm()
|
||||
{
|
||||
// TODO 3.3 Do we want to use a readparam here or SF internal mechanism ?
|
||||
$sDashletClass = utils::ReadParam('dashlet_class', '', false, utils::ENUM_SANITIZATION_FILTER_PHP_CLASS);
|
||||
$sValues = utils::ReadParam('values', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$aValues = !empty($sValues) ? json_decode($sValues, true) : [];
|
||||
|
||||
$oPage = new AjaxPage('');
|
||||
|
||||
$oForm = TurboFormUIBlockFactory::MakeForDashletConfiguration($sDashletClass, $aValues);
|
||||
$oButtonContainer = UIContentBlockUIBlockFactory::MakeStandard(null, ['ibo-dashlet-panel--form-container--buttons']);
|
||||
$oButtonContainer->AddSubBlock(ButtonUIBlockFactory::MakeForSecondaryAction('Cancel', 'dashboard_cancel'));
|
||||
$oButtonContainer->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction('Confirm', 'dashboard_submit', 'dashboard_submit', true));
|
||||
$oForm->AddSubBlock($oButtonContainer);
|
||||
$oPage->AddUiBlock($oForm);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationSave()
|
||||
{
|
||||
$sViewData = utils::ReadPostedParam('values', '', utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$aViewData = !empty($sViewData) ? json_decode($sViewData, true, 20) : [];
|
||||
|
||||
try {
|
||||
// Get the form block from the service (and the compiler)
|
||||
$oRequest = $this->getRequest();
|
||||
$oFormBlock = $this->oFormBlockService->GetFormBlockById('DashboardGrid', 'Dashboard');
|
||||
$oBuilder = $this->oFormFactoryBuilderService->GetFormBuilder($oFormBlock, $aViewData);
|
||||
$oForm = $oBuilder->getForm();
|
||||
$oForm->handleRequest($oRequest);
|
||||
// We are in the submit action, so we submit the form with the provided values
|
||||
$oForm->submit($aViewData);
|
||||
|
||||
// TODO 3.3 Validate the form, it requires CSRF + stripping extra fields
|
||||
// See $oForm->getErrors(true) to get all errors
|
||||
if ($oForm->isSubmitted() && (true || $oForm->isValid())) {
|
||||
// Save XML
|
||||
$aModelData = $oForm->getData();
|
||||
$oDashboard = new RuntimeDashboard($aModelData['id']);
|
||||
$oDomNode = $oDashboard->CreateEmptyDashboard();
|
||||
$this->oXMLSerializer->Serialize($aModelData, $oDomNode, 'DashboardGrid', 'Dashboard');
|
||||
$sXml = $oDomNode->ownerDocument->saveXML();
|
||||
$oDashboard->PersistDashboard($sXml);
|
||||
$sStatus = 'ok';
|
||||
$sMessage = 'Dashboard saved';
|
||||
} else {
|
||||
$sStatus = 'error';
|
||||
$aFormErrors = $oForm->getErrors(true, true);
|
||||
$sMessage = $aFormErrors->__toString();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Exception($e->getMessage(), $e);
|
||||
$sStatus = 'error';
|
||||
$sMessage = $e->getMessage();
|
||||
}
|
||||
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetData([
|
||||
'status' => $sStatus,
|
||||
'message' => $sMessage,
|
||||
]);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationExport()
|
||||
{
|
||||
$oPage = new DownloadPage('');
|
||||
$sDashboardId = utils::ReadParam('id', '', false, 'raw_data');
|
||||
$sDashboardFile = APPROOT.utils::ReadParam('file', '', false, 'raw_data');
|
||||
|
||||
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
|
||||
if (false === $sDashboardFileSanitized) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
/** @var \Combodo\iTop\Application\Dashboard\Dashboard $oDashboard */
|
||||
$oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId);
|
||||
|
||||
$sDashboardDefinition = $oDashboard->ToXML();
|
||||
|
||||
if (!is_null($oDashboard)) {
|
||||
$oPage->TrashUnexpectedOutput();
|
||||
$oPage->SetContentType('text/xml');
|
||||
$oPage->SetContentDisposition('attachment', 'dashboard_'.$oDashboard->GetTitle().'.xml');
|
||||
|
||||
$oPage->add($sDashboardDefinition);
|
||||
}
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationLoad()
|
||||
{
|
||||
$sDashboardId = utils::ReadParam('id', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$bIsCustom = utils::ReadParam('is_custom', 'false', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA) === 'true';
|
||||
$sDashboardFile = APPROOT.utils::ReadParam('file', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
|
||||
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
|
||||
if (false === $sDashboardFileSanitized) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
$sStatus = 'error';
|
||||
$sMessage = 'Unknown error';
|
||||
$aData = [];
|
||||
|
||||
try {
|
||||
if ($bIsCustom) {
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $sDashboardId, '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
if ($oUDSet->Count() > 0) {
|
||||
// Assuming there is at most one couple {user, menu}!
|
||||
$oUserDashboard = $oUDSet->Fetch();
|
||||
$sDashboardDefinition = $oUserDashboard->Get('contents');
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
|
||||
$oDashboard = new RuntimeDashboard($sDashboardId);
|
||||
$oDashboard->FromXML($sDashboardDefinition);
|
||||
|
||||
// TODO 3.3 If the dashboard definition is previous schema, we need to convert it to the new one
|
||||
|
||||
$oDoc = new PropertyTypeDesign();
|
||||
$oDoc->loadXML($sDashboardDefinition);
|
||||
|
||||
$oMainNode = $oDoc->getElementsByTagName('dashboard')->item(0);
|
||||
|
||||
$aData = $oDashboard->ToModelData();
|
||||
|
||||
// TODO 3.3 Re-render every dashlet to have their latest representation
|
||||
// Let's render every dashlet to prevent frontend from having to do it for each in individual ajax call
|
||||
// if (array_key_exists('pos_dashlets', $aData)) {
|
||||
// foreach ($aData['pos_dashlets'] as $sDashletId => $sPosValues) {
|
||||
// if(array_key_exists('dashlet', $sPosValues)) {
|
||||
// $sDashletClass = $sPosValues['dashlet']['type'];
|
||||
// $aValues = $sPosValues['dashlet']['properties'];
|
||||
//
|
||||
// $oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, $sDashletId);
|
||||
// $oDashlet->FromModelData($aValues);
|
||||
//
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
$sStatus = 'ok';
|
||||
$sMessage = 'Dashboard loaded';
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Exception($e->getMessage(), $e);
|
||||
$sStatus = 'error';
|
||||
$sMessage = $e->getMessage();
|
||||
}
|
||||
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetData([
|
||||
'status' => $sStatus,
|
||||
'message' => $sMessage,
|
||||
'data' => $aData,
|
||||
]);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationImport()
|
||||
{
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
$sTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id');
|
||||
if (!utils::IsTransactionValid($sTransactionId, true)) {
|
||||
throw new SecurityException('ajax.render.php import_dashboard : invalid transaction_id');
|
||||
}
|
||||
$sDashboardId = utils::ReadParam('id', '', false, 'raw_data');
|
||||
$sDashboardFileRelative = utils::ReadParam('file', '', false, 'raw_data');
|
||||
|
||||
$sDashboardFile = RuntimeDashboard::GetDashboardFileFromRelativePath($sDashboardFileRelative);
|
||||
|
||||
$oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId);
|
||||
$aResult = ['error' => ''];
|
||||
if (!is_null($oDashboard)) {
|
||||
try {
|
||||
$oDoc = utils::ReadPostedDocument('dashboard_upload_file');
|
||||
$oDashboard->FromXml($oDoc->GetData());
|
||||
$oDashboard->PersistDashboard($oDoc->GetData());
|
||||
} catch (DOMException $e) {
|
||||
$aResult = ['error' => Dict::S('UI:Error:InvalidDashboardFile')];
|
||||
} catch (Exception $e) {
|
||||
$aResult = ['error' => $e->getMessage()];
|
||||
}
|
||||
} else {
|
||||
$aResult['error'] = 'Dashboard id="'.$sDashboardId.'" not found.';
|
||||
}
|
||||
$oPage->SetData($aResult);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
}
|
||||
629
sources/Application/Dashboard/Dashboard.php
Normal file
629
sources/Application/Dashboard/Dashboard.php
Normal file
@@ -0,0 +1,629 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard;
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Dashboard\Layout\DashboardLayoutGrid;
|
||||
use Combodo\iTop\Application\Dashlet\Dashlet;
|
||||
use Combodo\iTop\Application\Dashlet\DashletFactory;
|
||||
use Combodo\iTop\Application\Dashlet\Service\DashletService;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\PropertyType\PropertyTypeDesign;
|
||||
use Combodo\iTop\PropertyType\Serializer\XMLSerializer;
|
||||
use DashboardLayout;
|
||||
use Dict;
|
||||
use DOMException;
|
||||
use InvalidParameterException;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
*
|
||||
* A user editable dashboard page
|
||||
*
|
||||
*/
|
||||
abstract class Dashboard
|
||||
{
|
||||
/** @var string $sTitle */
|
||||
protected $sTitle;
|
||||
/** @var bool $bAutoReload */
|
||||
protected $bAutoReload;
|
||||
/** @var float|int $iAutoReloadSec */
|
||||
protected $iAutoReloadSec;
|
||||
/** @var string $sLayoutClass */
|
||||
protected $sLayoutClass;
|
||||
/** @var array $aWidgetsData */
|
||||
protected $aWidgetsData;
|
||||
/** @var DesignElement|null $oDOMNode */
|
||||
protected $oDOMNode;
|
||||
/** @var string $sId */
|
||||
protected $sId;
|
||||
/** @var array $aCells */
|
||||
protected $aCells;
|
||||
/** @var \ModelReflection $oMetaModel */
|
||||
protected $oMetaModel;
|
||||
|
||||
/** @var array Array of dashlets with position */
|
||||
protected array $aGridDashlets = [];
|
||||
|
||||
protected $oDashletFactory;
|
||||
|
||||
private XMLSerializer $oXMLSerializer;
|
||||
private DashletService $oDashletService;
|
||||
|
||||
/**
|
||||
* Dashboard constructor.
|
||||
*
|
||||
* @param string $sId
|
||||
*/
|
||||
public function __construct($sId)
|
||||
{
|
||||
$this->sTitle = '';
|
||||
$this->sLayoutClass = 'DashboardLayoutOneCol';
|
||||
$this->bAutoReload = false;
|
||||
$this->iAutoReloadSec = MetaModel::GetConfig()->GetStandardReloadInterval();
|
||||
$this->aCells = [];
|
||||
$this->oDOMNode = null;
|
||||
$this->sId = $sId;
|
||||
$this->oDashletFactory = DashletFactory::GetInstance();
|
||||
$this->oXMLSerializer = MetaModel::GetService('XMLSerializer');
|
||||
$this->oDashletService = MetaModel::GetService('DashletService');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sXml
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function FromXml($sXml)
|
||||
{
|
||||
$this->aCells = []; // reset the content of the dashboard
|
||||
set_error_handler(['Dashboard', 'ErrorHandler']);
|
||||
$oDoc = new PropertyTypeDesign();
|
||||
$oDoc->loadXML($sXml);
|
||||
restore_error_handler();
|
||||
$this->FromDOMDocument($oDoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sXml
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function ToXML(): string
|
||||
{
|
||||
$oDomNode = $this->CreateEmptyDashboard();
|
||||
$aModelData = $this->ToModelData();
|
||||
$this->oXMLSerializer->Serialize($aModelData, $oDomNode, 'DashboardGrid', 'Dashboard');
|
||||
|
||||
return $oDomNode->ownerDocument->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DOMDocument $oDoc
|
||||
*/
|
||||
public function FromDOMDocument(DesignDocument $oDoc)
|
||||
{
|
||||
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
|
||||
|
||||
if ($this->oDOMNode->getElementsByTagName('cells')->count() === 0) {
|
||||
$this->FromDOMDocumentV2($this->oDOMNode);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0)) {
|
||||
$this->sLayoutClass = $oLayoutNode->textContent;
|
||||
} else {
|
||||
$this->sLayoutClass = 'DashboardLayoutOneCol';
|
||||
}
|
||||
|
||||
if ($oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0)) {
|
||||
$this->sTitle = $oTitleNode->textContent;
|
||||
} else {
|
||||
$this->sTitle = '';
|
||||
}
|
||||
|
||||
$this->bAutoReload = false;
|
||||
$this->iAutoReloadSec = MetaModel::GetConfig()->GetStandardReloadInterval();
|
||||
if ($oAutoReloadNode = $this->oDOMNode->getElementsByTagName('auto_reload')->item(0)) {
|
||||
if ($oAutoReloadEnabled = $oAutoReloadNode->getElementsByTagName('enabled')->item(0)) {
|
||||
$this->bAutoReload = ($oAutoReloadEnabled->textContent == 'true');
|
||||
}
|
||||
if ($oAutoReloadInterval = $oAutoReloadNode->getElementsByTagName('interval')->item(0)) {
|
||||
$this->iAutoReloadSec = max(MetaModel::GetConfig()->Get('min_reload_interval'), (int)$oAutoReloadInterval->textContent);
|
||||
}
|
||||
}
|
||||
|
||||
if ($oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0)) {
|
||||
$oCellsList = $oCellsNode->getElementsByTagName('cell');
|
||||
$aCellOrder = [];
|
||||
$iCellRank = 0;
|
||||
/** @var \DOMElement $oCellNode */
|
||||
foreach ($oCellsList as $oCellNode) {
|
||||
$oCellRank = $oCellNode->getElementsByTagName('rank')->item(0);
|
||||
if ($oCellRank) {
|
||||
$iCellRank = (float)$oCellRank->textContent;
|
||||
}
|
||||
$oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0);
|
||||
{
|
||||
$oDashletList = $oDashletsNode->getElementsByTagName('dashlet');
|
||||
$iRank = 0;
|
||||
$aDashletOrder = [];
|
||||
/** @var DesignElement $oDomNode */
|
||||
foreach ($oDashletList as $oDomNode) {
|
||||
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
|
||||
if ($oRank) {
|
||||
$iRank = (float)$oRank->textContent;
|
||||
}
|
||||
|
||||
$oNewDashlet = $this->InitDashletFromDOMNode($oDomNode);
|
||||
$aDashletOrder[] = ['rank' => $iRank, 'dashlet' => $oNewDashlet];
|
||||
}
|
||||
usort($aDashletOrder, [$this, 'SortOnRank']);
|
||||
$aDashletList = [];
|
||||
foreach ($aDashletOrder as $aItem) {
|
||||
$aDashletList[] = $aItem['dashlet'];
|
||||
}
|
||||
$aCellOrder[] = ['rank' => $iCellRank, 'dashlets' => $aDashletList];
|
||||
}
|
||||
}
|
||||
usort($aCellOrder, [$this, 'SortOnRank']);
|
||||
foreach ($aCellOrder as $aItem) {
|
||||
$this->aCells[] = $aItem['dashlets'];
|
||||
}
|
||||
} else {
|
||||
$this->aCells = [];
|
||||
}
|
||||
|
||||
switch ($this->sLayoutClass) {
|
||||
case 'DashboardLayoutTwoCols':
|
||||
$iNbCols = 2;
|
||||
break;
|
||||
case 'DashboardLayoutThreeCols':
|
||||
$iNbCols = 3;
|
||||
break;
|
||||
case 'DashboardLayoutOneCol':
|
||||
default:
|
||||
$iNbCols = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
$iCellIdx = 0;
|
||||
$iNbRows = ceil(count($this->aCells) / $iNbCols);
|
||||
|
||||
// GRID LAYOUT: Global positioning
|
||||
$iGridCurrentX = 0;
|
||||
$iGridCurrentY = 0;
|
||||
$iGridColWidth = (int)(12 / $iNbCols);
|
||||
|
||||
for ($iRows = 0; $iRows < $iNbRows; $iRows++) {
|
||||
// GRID LAYOUT: Store the maximum column Y in this row
|
||||
$iGridMaxColY = -1;
|
||||
|
||||
for ($iCols = 0; $iCols < $iNbCols; $iCols++) {
|
||||
|
||||
// GRID LAYOUT: Column positioning
|
||||
$iGridCurrentColX = 0;
|
||||
$iGridCurrentColY = 0;
|
||||
$iGridMaxHeightDashlet = -1;
|
||||
|
||||
if (array_key_exists($iCellIdx, $this->aCells)) {
|
||||
$aDashlets = $this->aCells[$iCellIdx];
|
||||
if (count($aDashlets) > 0) {
|
||||
/** @var \Dashlet $oDashlet */
|
||||
foreach ($aDashlets as $oDashlet) {
|
||||
if ($oDashlet::IsVisible()) {
|
||||
$sDashletClass = $oDashlet->GetDashletType();
|
||||
$aDashletsInfo = $this->oDashletService->GetDashletDefinition($sDashletClass);
|
||||
|
||||
// GRID LAYOUT: Set position relative to grid
|
||||
$iPositionX = $iGridCurrentX + $iGridCurrentColX;
|
||||
$iPositionY = $iGridCurrentY + $iGridCurrentColY;
|
||||
$iWidth = array_key_exists('preferred_width', $aDashletsInfo) ? $aDashletsInfo['preferred_width'] : 1;
|
||||
// GRID LAYOUT: Limit dashlet width to fit column width
|
||||
if ($iWidth > $iGridColWidth) {
|
||||
$iWidth = $iGridColWidth;
|
||||
}
|
||||
$iHeight = array_key_exists('preferred_height', $aDashletsInfo) ? $aDashletsInfo['preferred_height'] : 1;
|
||||
// GRID LAYOUT: Store max height of dashlets in this current column
|
||||
if ($iHeight > $iGridMaxHeightDashlet) {
|
||||
$iGridMaxHeightDashlet = $iHeight;
|
||||
}
|
||||
// GRID LAYOUT: Ensure that dashlet fits in the current row of the column
|
||||
if ($iGridCurrentColX + $iWidth > $iGridColWidth) {
|
||||
$iPositionX = $iGridCurrentX;
|
||||
$iPositionY++;
|
||||
}
|
||||
$aPosDashlet = [];
|
||||
$aPosDashlet['dashlet'] = $oDashlet;
|
||||
$aPosDashlet['position_x'] = $iPositionX;
|
||||
$aPosDashlet['position_y'] = $iPositionY;
|
||||
$aPosDashlet['width'] = $iWidth;
|
||||
$aPosDashlet['height'] = $iHeight;
|
||||
$this->aGridDashlets[] = $aPosDashlet;
|
||||
|
||||
// GRID LAYOUT: Update column cursor
|
||||
$iGridCurrentColX += $iWidth;
|
||||
if ($iGridCurrentColX >= $iGridColWidth) {
|
||||
$iGridCurrentColX = 0;
|
||||
$iGridCurrentColY += $iGridMaxHeightDashlet;
|
||||
$iGridMaxHeightDashlet = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$iCellIdx++;
|
||||
|
||||
// GRID LAYOUT: Store max y in this current row
|
||||
if ($iGridCurrentColY > $iGridMaxColY) {
|
||||
$iGridMaxColY = $iGridCurrentColY;
|
||||
}
|
||||
|
||||
// GRID LAYOUT: Next column
|
||||
$iGridCurrentX += $iGridColWidth;
|
||||
}
|
||||
|
||||
// GRID LAYOUT: Next Row
|
||||
$iGridCurrentY += ($iGridMaxColY + 1);
|
||||
$iGridCurrentX = 0;
|
||||
}
|
||||
|
||||
$this->aCells = [];
|
||||
$this->sLayoutClass = DashboardLayoutGrid::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DOMDocument $oDOMNode
|
||||
*/
|
||||
public function FromDOMDocumentV2(DesignElement $oDOMNode)
|
||||
{
|
||||
$aDashboardValues = $this->oXMLSerializer->Deserialize($oDOMNode, 'DashboardGrid', 'Dashboard');
|
||||
$this->FromModelData($aDashboardValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DesignElement $oDomNode
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function InitDashletFromDOMNode($oDomNode)
|
||||
{
|
||||
$sId = $oDomNode->getAttribute('id');
|
||||
|
||||
$sDashletType = $oDomNode->getAttribute('xsi:type');
|
||||
|
||||
// Test if dashlet can be instantiated, otherwise (uninstalled, broken, ...) we display a placeholder
|
||||
$sClass = static::GetDashletClassFromType($sDashletType);
|
||||
/** @var \Dashlet $oNewDashlet */
|
||||
$oNewDashlet = $this->oDashletFactory->CreateDashlet($sClass, $sId);
|
||||
$oNewDashlet->FromDOMNode($oDomNode);
|
||||
|
||||
return $oNewDashlet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aItem1
|
||||
* @param array $aItem2
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function SortOnRank($aItem1, $aItem2)
|
||||
{
|
||||
return ($aItem1['rank'] > $aItem2['rank']) ? +1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler to turn XML loading warnings into exceptions
|
||||
*
|
||||
* @param $errno
|
||||
* @param $errstr
|
||||
* @param $errfile
|
||||
* @param $errline
|
||||
*
|
||||
* @return bool
|
||||
* @throws \DOMException
|
||||
*/
|
||||
public static function ErrorHandler($errno, $errstr, $errfile, $errline)
|
||||
{
|
||||
if ($errno == E_WARNING && (substr_count($errstr, "DOMDocument::loadXML()") > 0)) {
|
||||
throw new DOMException($errstr);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DesignElement
|
||||
* @throws \DOMException
|
||||
*/
|
||||
public function CreateEmptyDashboard(): DesignElement
|
||||
{
|
||||
$oDoc = new DesignDocument();
|
||||
$oDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
|
||||
$oDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
|
||||
|
||||
/** @var DesignElement $oMainNode */
|
||||
$oMainNode = $oDoc->createElement('dashboard');
|
||||
$oMainNode->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
|
||||
$oDoc->appendChild($oMainNode);
|
||||
|
||||
return $oMainNode;
|
||||
}
|
||||
|
||||
public function PersistDashboard(string $sXml): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function GetId()
|
||||
{
|
||||
return $this->sId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sanitize ID for usages in XML/HTML attributes
|
||||
*
|
||||
* @return string
|
||||
* @since 2.7.0
|
||||
*/
|
||||
public function GetSanitizedId()
|
||||
{
|
||||
return utils::Sanitize($this->GetId(), '', 'element_identifier');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetLayout()
|
||||
{
|
||||
return $this->sLayoutClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sLayoutClass
|
||||
*/
|
||||
public function SetLayout($sLayoutClass)
|
||||
{
|
||||
$this->sLayoutClass = $sLayoutClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetTitle()
|
||||
{
|
||||
return $this->sTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sTitle
|
||||
*/
|
||||
public function SetTitle($sTitle)
|
||||
{
|
||||
$this->sTitle = $sTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function GetAutoReload()
|
||||
{
|
||||
return $this->bAutoReload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bAutoReload
|
||||
*/
|
||||
public function SetAutoReload($bAutoReload)
|
||||
{
|
||||
$this->bAutoReload = $bAutoReload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int
|
||||
*/
|
||||
public function GetAutoReloadInterval()
|
||||
{
|
||||
return $this->iAutoReloadSec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $iAutoReloadSec
|
||||
*/
|
||||
public function SetAutoReloadInterval($iAutoReloadSec)
|
||||
{
|
||||
$this->iAutoReloadSec = max(MetaModel::GetConfig()->Get('min_reload_interval'), (int)$iAutoReloadSec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param bool $bEditMode
|
||||
* @param array $aExtraParams
|
||||
* @param bool $bCanEdit
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = [], $bCanEdit = true)
|
||||
{
|
||||
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
|
||||
|
||||
/** @var \DashboardLayout $oLayout */
|
||||
$oLayout = new $this->sLayoutClass();
|
||||
|
||||
foreach ($this->aCells as $iCellIdx => $aDashlets) {
|
||||
foreach ($aDashlets as $oDashlet) {
|
||||
$aDashletCoordinates = $oLayout->GetDashletCoordinates($iCellIdx);
|
||||
$this->PrepareDashletForRendering($oDashlet, $aDashletCoordinates, $aExtraParams);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($this->aCells) > 0) {
|
||||
$aDashlets = $this->aCells;
|
||||
} else {
|
||||
$aDashlets = $this->aGridDashlets;
|
||||
}
|
||||
|
||||
$oDashboard = $oLayout->Render($oPage, $aDashlets, $bEditMode, $aExtraParams);
|
||||
$oDashboard->SetFile(utils::LocalPath($this->GetDefinitionFile()));
|
||||
|
||||
$oPage->AddUiBlock($oDashboard);
|
||||
|
||||
$bFromDashboardPage = isset($aExtraParams['from_dashboard_page']) ? isset($aExtraParams['from_dashboard_page']) : false;
|
||||
|
||||
if ($bFromDashboardPage) {
|
||||
$sTitleForHTML = utils::HtmlEntities(Dict::S($this->sTitle));
|
||||
$sHtml = "<div class=\"ibo-top-bar--toolbar-dashboard-title\" title=\"{$sTitleForHTML}\">{$sTitleForHTML}</div>";
|
||||
if ($oPage instanceof iTopWebPage) {
|
||||
$oTopBar = $oPage->GetTopBarLayout();
|
||||
$oToolbar = ToolbarUIBlockFactory::MakeStandard();
|
||||
$oTopBar->SetToolbar($oToolbar);
|
||||
|
||||
$oToolbar->AddHtml($sHtml);
|
||||
} else {
|
||||
$oPage->add_script(
|
||||
<<<JS
|
||||
$(".ibo-top-bar--toolbar-dashboard-title").html("$sTitleForHTML").attr("title", $('<div>').html("$sTitleForHTML").text());
|
||||
JS
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$oDashboard->SetTitle(Dict::S($this->sTitle));
|
||||
}
|
||||
|
||||
if (!$bEditMode) {
|
||||
$oPage->LinkScriptFromAppRoot('js/dashlet.js');
|
||||
$oPage->LinkScriptFromAppRoot('js/dashboard.js');
|
||||
}
|
||||
|
||||
return $oDashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare dashlet for rendering (eg. change its ID or another processing).
|
||||
* Meant to be overloaded.
|
||||
*
|
||||
* @param \Dashlet $oDashlet
|
||||
* @param array $aCoordinates
|
||||
* @param array $aExtraParams
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function PrepareDashletForRendering(Dashlet $oDashlet, $aCoordinates, $aExtraParams = []);
|
||||
|
||||
/**
|
||||
* @param \DesignerForm $oForm
|
||||
* @param array $aExtraParams
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function SetFormParams($oForm, $aExtraParams = []);
|
||||
|
||||
/**
|
||||
* @param string $sType
|
||||
* @param \ModelFactory|null $oFactory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function GetDashletClassFromType($sType, $oFactory = null)
|
||||
{
|
||||
if (is_subclass_of($sType, 'Dashlet')) {
|
||||
return $sType;
|
||||
}
|
||||
|
||||
return 'DashletUnknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* N°2634: we must have a unique id per dashlet!
|
||||
* To avoid collision with other dashlets with the same ID we prefix it with row/cell id
|
||||
* Collisions typically happen with extensions.
|
||||
*
|
||||
* @param boolean $bIsCustomized
|
||||
* @param string $sDashboardDivId
|
||||
* @param int $iRow
|
||||
* @param int $iCol
|
||||
* @param string $sDashletOrigId
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 2.7.0 N°2735
|
||||
*/
|
||||
public static function GetDashletUniqueId($bIsCustomized, $sDashboardDivId, $iRow, $iCol, $sDashletOrigId)
|
||||
{
|
||||
if (strpos($sDashletOrigId, '_ID_row') !== false) {
|
||||
return $sDashletOrigId;
|
||||
}
|
||||
|
||||
$sDashletId = $sDashboardDivId."_ID_row".$iRow."_col".$iCol."_".$sDashletOrigId;
|
||||
if ($bIsCustomized) {
|
||||
$sDashletId = 'CUSTOM_'.$sDashletId;
|
||||
}
|
||||
|
||||
return $sDashletId;
|
||||
}
|
||||
|
||||
public function FromModelData(mixed $aDashboardValues)
|
||||
{
|
||||
$this->sLayoutClass = DashboardLayoutGrid::class;
|
||||
$this->sTitle = $aDashboardValues['title'] ?? '';
|
||||
$iRefresh = $aDashboardValues['refresh'] ?? 0;
|
||||
$this->bAutoReload = $iRefresh > 0;
|
||||
$this->iAutoReloadSec = $iRefresh;
|
||||
|
||||
foreach ($aDashboardValues['pos_dashlets'] as $sId => $aPosDashlet) {
|
||||
$aGridDashlet = [];
|
||||
$aGridDashlet['position_x'] = $aPosDashlet['position_x'] ?? 0;
|
||||
$aGridDashlet['position_y'] = $aPosDashlet['position_y'] ?? 0;
|
||||
$aGridDashlet['width'] = $aPosDashlet['width'] ?? 2;
|
||||
$aGridDashlet['height'] = $aPosDashlet['height'] ?? 1;
|
||||
$aDashlet = $aPosDashlet['dashlet'];
|
||||
$sType = $aDashlet['type'];
|
||||
$oDashlet = DashletFactory::GetInstance()->CreateDashlet($sType, $sId);
|
||||
$oDashlet->FromModelData($aDashlet['properties']);
|
||||
$aGridDashlet['dashlet'] = $oDashlet;
|
||||
$this->aGridDashlets[] = $aGridDashlet;
|
||||
}
|
||||
}
|
||||
|
||||
public function ToModelData(): array
|
||||
{
|
||||
$aModelData = [];
|
||||
$aModelData['id'] = $this->sId;
|
||||
$aModelData['layout'] = $this->sLayoutClass;
|
||||
$aModelData['title'] = $this->sTitle;
|
||||
$aModelData['refresh'] = $this->bAutoReload ? $this->iAutoReloadSec : 0;
|
||||
|
||||
foreach ($this->aGridDashlets as $aGridDashlet) {
|
||||
$aPosDashlet = [];
|
||||
$aPosDashlet['position_x'] = $aGridDashlet['position_x'] ?? 0;
|
||||
$aPosDashlet['position_y'] = $aGridDashlet['position_y'] ?? 0;
|
||||
$aPosDashlet['width'] = $aGridDashlet['width'] ?? 2;
|
||||
$aPosDashlet['height'] = $aGridDashlet['height'] ?? 1;
|
||||
/** @var Dashlet $oDashlet */
|
||||
$oDashlet = $aGridDashlet['dashlet'];
|
||||
$aDashlet = [];
|
||||
$aDashlet['type'] = $oDashlet->GetDashletType();
|
||||
$sId = $oDashlet->GetID();
|
||||
$aDashlet['id'] = $sId;
|
||||
$aDashlet['properties'] = $oDashlet->ToModelData();
|
||||
$aPosDashlet['dashlet'] = $aDashlet;
|
||||
|
||||
$aModelData['pos_dashlets'][$sId] = $aPosDashlet;
|
||||
}
|
||||
|
||||
return $aModelData;
|
||||
}
|
||||
}
|
||||
12
sources/Application/Dashboard/DashboardException.php
Normal file
12
sources/Application/Dashboard/DashboardException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard;
|
||||
|
||||
class DashboardException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard\FormBlock;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\CollectionBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\TextFormBlock;
|
||||
|
||||
class DashboardFormBlock extends FormBlock
|
||||
{
|
||||
protected function BuildForm(): void
|
||||
{
|
||||
// Label
|
||||
$this->Add('title', TextFormBlock::class, [
|
||||
'label' => 'Title',
|
||||
]);
|
||||
|
||||
// Refresh
|
||||
$this->Add('refresh', ChoiceFormBlock::class, [
|
||||
'label' => 'Refresh',
|
||||
'choices' => [
|
||||
'Never' => 0,
|
||||
'Every 5 minutes' => 5,
|
||||
'Every 15 minutes' => 15,
|
||||
'Every 30 minutes' => 30,
|
||||
'Every hour' => 60,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->Add('dashlets_list', CollectionBlock::class, [
|
||||
'label' => 'Dashlets List',
|
||||
'block_entry_type' => DashletFormBlock::class,
|
||||
'block_entry_options' => [],
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
57
sources/Application/Dashboard/FormBlock/DashletFormBlock.php
Normal file
57
sources/Application/Dashboard/FormBlock/DashletFormBlock.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard\FormBlock;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\IntegerFormBlock;
|
||||
use Combodo\iTop\PropertyType\PropertyTypeService;
|
||||
use MetaModel;
|
||||
|
||||
class DashletFormBlock extends FormBlock
|
||||
{
|
||||
private PropertyTypeService $oPropertyTypeService;
|
||||
|
||||
public function __construct(string $sName, array $aOptions = [])
|
||||
{
|
||||
$this->oPropertyTypeService = MetaModel::GetService('PropertyTypeService');
|
||||
parent::__construct($sName, $aOptions);
|
||||
}
|
||||
|
||||
protected function BuildForm(): void
|
||||
{
|
||||
// type
|
||||
$aPropertyTypes = $this->oPropertyTypeService->ListPropertyTypesByType('Dashlet');
|
||||
$this->Add('class', ChoiceFormBlock::class, [
|
||||
'label' => 'Class',
|
||||
'choices' => array_combine($aPropertyTypes, $aPropertyTypes),
|
||||
]);
|
||||
|
||||
// column
|
||||
$this->Add('position_x', IntegerFormBlock::class, [
|
||||
'label' => 'Position X',
|
||||
]);
|
||||
|
||||
// row
|
||||
$this->Add('position_y', IntegerFormBlock::class, [
|
||||
'label' => 'Position Y',
|
||||
]);
|
||||
|
||||
// column
|
||||
$this->Add('height', IntegerFormBlock::class, [
|
||||
'label' => 'Height',
|
||||
]);
|
||||
|
||||
// row
|
||||
$this->Add('width', IntegerFormBlock::class, [
|
||||
'label' => 'Width',
|
||||
]);
|
||||
|
||||
// dashlet
|
||||
$this->Add('dashlet', DashletPropertiesFormBlock::class, [
|
||||
'label' => 'Dashlet',
|
||||
])
|
||||
->InputDependsOn(DashletPropertiesFormBlock::INPUT_DASHLET_TYPE, 'class', ChoiceFormBlock::OUTPUT_VALUE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard\FormBlock;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\PropertyType\PropertyTypeService;
|
||||
use MetaModel;
|
||||
|
||||
class DashletPropertiesFormBlock extends FormBlock
|
||||
{
|
||||
// inputs
|
||||
public const INPUT_DASHLET_TYPE = 'dashlet_type';
|
||||
private PropertyTypeService $oPropertyTypeService;
|
||||
|
||||
public function __construct(string $sName, array $aOptions = [])
|
||||
{
|
||||
parent::__construct($sName, $aOptions);
|
||||
$this->oPropertyTypeService = MetaModel::GetService('PropertyTypeService');
|
||||
}
|
||||
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddInput(self::INPUT_DASHLET_TYPE, StringIOFormat::class);
|
||||
}
|
||||
|
||||
public function GetFormType(): string
|
||||
{
|
||||
$sDashletType = strval($this->GetInputValue(self::INPUT_DASHLET_TYPE));
|
||||
$oDashlet = $this->oPropertyTypeService->GetFormBlockById($sDashletType, 'Dashlet');
|
||||
|
||||
return $oDashlet->GetFormType();
|
||||
}
|
||||
|
||||
public function GetOptions(): array
|
||||
{
|
||||
$sDashletType = strval($this->GetInputValue(self::INPUT_DASHLET_TYPE));
|
||||
$oDashlet = $this->oPropertyTypeService->GetFormBlockById($sDashletType, 'Dashlet');
|
||||
|
||||
return $oDashlet->GetOptions();
|
||||
}
|
||||
}
|
||||
61
sources/Application/Dashboard/Layout/DashboardLayoutGrid.php
Normal file
61
sources/Application/Dashboard/Layout/DashboardLayoutGrid.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\Dashboard\Layout;
|
||||
|
||||
use Combodo\iTop\Application\Dashlet\Service\DashletService;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardGrid;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout as DashboardLayoutUIBlock;
|
||||
use MetaModel;
|
||||
|
||||
class DashboardLayoutGrid extends \DashboardLayout
|
||||
{
|
||||
public function Render($oPage, $aDashlets, $bEditMode = false, array $aExtraParams = [])
|
||||
{
|
||||
/** @var DashletService $oDashletService */
|
||||
$oDashletService = MetaModel::GetService('DashletService');
|
||||
$oDashboardLayout = new DashboardLayoutUIBlock($aExtraParams['dashboard_div_id']);
|
||||
|
||||
$oDashboardGrid = new DashboardGrid();
|
||||
$oDashboardLayout->SetGrid($oDashboardGrid);
|
||||
foreach ($aDashlets as $aPosDashlet) {
|
||||
/** @var \Dashlet $oDashlet */
|
||||
if (!array_key_exists('dashlet', $aPosDashlet)) {
|
||||
continue;
|
||||
}
|
||||
$oDashlet = $aPosDashlet['dashlet'];
|
||||
if ($oDashlet) {
|
||||
$sDashletId = $oDashlet->GetID();
|
||||
$sDashletClass = $oDashlet->GetDashletType();
|
||||
$aDashletDenormalizedProperties = $oDashlet->GetModelData();
|
||||
$aDashletsInfo = $oDashletService->GetDashletDefinition($sDashletClass);
|
||||
|
||||
// Also set minimal height/width
|
||||
$iPositionX = $aPosDashlet['position_x'] ?? 0;
|
||||
$iPositionY = $aPosDashlet['position_y'] ?? 0;
|
||||
$iWidth = max($aPosDashlet['width'], array_key_exists('min_width', $aDashletsInfo) ? $aDashletsInfo['min_width'] : 1);
|
||||
$iHeight = max($aPosDashlet['height'], array_key_exists('min_height', $aDashletsInfo) ? $aDashletsInfo['min_height'] : 1);
|
||||
$oDashboardGrid->AddDashlet($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams), $sDashletId, $sDashletClass, $aDashletDenormalizedProperties, $iPositionX, $iPositionY, $iWidth, $iHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return $oDashboardLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetDashletCoordinates($iCellIdx)
|
||||
{
|
||||
$iRowNumber = $iCellIdx % 12;
|
||||
$iColumnNumber = (int)floor($iCellIdx / 12);
|
||||
|
||||
return [$iColumnNumber, $iRowNumber];
|
||||
}
|
||||
}
|
||||
785
sources/Application/Dashboard/RuntimeDashboard.php
Normal file
785
sources/Application/Dashboard/RuntimeDashboard.php
Normal file
@@ -0,0 +1,785 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\Dashlet\DashletFactory;
|
||||
use Combodo\iTop\Application\Dashlet\Service\DashletService;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableSettings;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout as DashboardLayoutUIBlock;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Service\ServiceLocator\ServiceLocator;
|
||||
|
||||
/**
|
||||
* Class RuntimeDashboard
|
||||
*/
|
||||
class RuntimeDashboard extends Dashboard
|
||||
{
|
||||
/** @var string $sDefinitionFile */
|
||||
private $sDefinitionFile = '';
|
||||
/** @var null $sReloadURL */
|
||||
private $sReloadURL = null;
|
||||
/** @var bool $bCustomized */
|
||||
protected $bCustomized;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct($sId)
|
||||
{
|
||||
parent::__construct($sId);
|
||||
$this->oMetaModel = new ModelReflectionRuntime();
|
||||
$this->oDashletFactory->SetModelReflectionRuntime($this->oMetaModel);
|
||||
$this->bCustomized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @since 2.7.0
|
||||
*/
|
||||
public function GetCustomFlag()
|
||||
{
|
||||
return $this->bCustomized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bCustomized
|
||||
*
|
||||
* @since 2.7.0
|
||||
*/
|
||||
public function SetCustomFlag($bCustomized)
|
||||
{
|
||||
$this->bCustomized = $bCustomized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function SetFormParams($oForm, $aExtraParams = [])
|
||||
{
|
||||
$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', ['operation' => 'update_dashlet_property', 'extra_params' => $aExtraParams]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sXml
|
||||
*
|
||||
* @return bool
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function PersistDashboard(string $sXml): bool
|
||||
{
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $this->sId, '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
$bIsNew = false;
|
||||
if ($oUDSet->Count() > 0) {
|
||||
// Assuming there is at most one couple {user, menu}!
|
||||
$oUserDashboard = $oUDSet->Fetch();
|
||||
$oUserDashboard->Set('contents', $sXml);
|
||||
} else {
|
||||
// No such customized dashboard for the current user, let's create a new record
|
||||
$oUserDashboard = new UserDashboard();
|
||||
$oUserDashboard->Set('user_id', UserRights::GetUserId());
|
||||
$oUserDashboard->Set('menu_code', $this->sId);
|
||||
$oUserDashboard->Set('contents', $sXml);
|
||||
$bIsNew = true;
|
||||
}
|
||||
utils::PushArchiveMode(false);
|
||||
$oUserDashboard->DBWrite();
|
||||
utils::PopArchiveMode();
|
||||
|
||||
return $bIsNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \DeleteException
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function Revert()
|
||||
{
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $this->sId, '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
if ($oUDSet->Count() > 0) {
|
||||
// Assuming there is at most one couple {user, menu}!
|
||||
$oUserDashboard = $oUDSet->Fetch();
|
||||
utils::PushArchiveMode(false);
|
||||
$oUserDashboard->DBDelete();
|
||||
utils::PopArchiveMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDashboardFile file name relative to the current module folder
|
||||
* @param string $sDashBoardId code of the dashboard either menu_id or <class>__<attcode>
|
||||
*
|
||||
* @return null|RuntimeDashboard
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function GetDashboard($sDashboardFile, $sDashBoardId)
|
||||
{
|
||||
$bCustomized = false;
|
||||
|
||||
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
|
||||
if (false === $sDashboardFileSanitized) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
if (!appUserPreferences::GetPref('display_original_dashboard_'.$sDashBoardId, false)) {
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $sDashBoardId, '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
if ($oUDSet->Count() > 0) {
|
||||
// Assuming there is at most one couple {user, menu}!
|
||||
$oUserDashboard = $oUDSet->Fetch();
|
||||
$sDashboardDefinition = $oUserDashboard->Get('contents');
|
||||
$bCustomized = true;
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
|
||||
if ($sDashboardDefinition !== false) {
|
||||
$oDashboard = new RuntimeDashboard($sDashBoardId);
|
||||
$oDashboard->FromXml($sDashboardDefinition);
|
||||
$oDashboard->SetCustomFlag($bCustomized);
|
||||
$oDashboard->SetDefinitionFile($sDashboardFileSanitized);
|
||||
} else {
|
||||
$oDashboard = null;
|
||||
}
|
||||
|
||||
return $oDashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDashboardFile file name relative to the current module folder
|
||||
* @param string $sDashBoardId code of the dashboard either menu_id or <class>__<attcode>
|
||||
*
|
||||
* @return null|RuntimeDashboard
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function GetDashboardToEdit($sDashboardFile, $sDashBoardId)
|
||||
{
|
||||
$bCustomized = false;
|
||||
|
||||
$sDashboardFileSanitized = utils::RealPath(APPROOT.$sDashboardFile, APPROOT);
|
||||
if (false === $sDashboardFileSanitized) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $sDashBoardId, '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
if ($oUDSet->Count() > 0) {
|
||||
// Assuming there is at most one couple {user, menu}!
|
||||
$oUserDashboard = $oUDSet->Fetch();
|
||||
$sDashboardDefinition = $oUserDashboard->Get('contents');
|
||||
$bCustomized = true;
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
|
||||
if ($sDashboardDefinition !== false) {
|
||||
$oDashboard = new RuntimeDashboard($sDashBoardId);
|
||||
$oDashboard->FromXml($sDashboardDefinition);
|
||||
$oDashboard->SetCustomFlag($bCustomized);
|
||||
$oDashboard->SetDefinitionFile($sDashboardFileSanitized);
|
||||
} else {
|
||||
$oDashboard = null;
|
||||
}
|
||||
|
||||
return $oDashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = [], $bCanEdit = true)
|
||||
{
|
||||
if (!isset($aExtraParams['query_params']) && isset($aExtraParams['this->class'])) {
|
||||
$oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']);
|
||||
$aRenderParams = ['query_params' => $oObj->ToArgsForQuery()];
|
||||
} else {
|
||||
$aRenderParams = $aExtraParams;
|
||||
}
|
||||
|
||||
$oDashboard = parent::Render($oPage, $bEditMode, $aRenderParams);
|
||||
|
||||
if ($this->HasCustomDashboard() && !filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$this->GetId(), false), FILTER_VALIDATE_BOOLEAN)) {
|
||||
$oDashboard->SetIsCustom(true);
|
||||
}
|
||||
|
||||
if (isset($aExtraParams['query_params']['this->object()'])) {
|
||||
/** @var \DBObject $oObj */
|
||||
$oObj = $aExtraParams['query_params']['this->object()'];
|
||||
$aAjaxParams = ['this->class' => get_class($oObj), 'this->id' => $oObj->GetKey()];
|
||||
if (isset($aExtraParams['from_dashboard_page'])) {
|
||||
$aAjaxParams['from_dashboard_page'] = $aExtraParams['from_dashboard_page'];
|
||||
}
|
||||
} else {
|
||||
$aAjaxParams = $aExtraParams;
|
||||
}
|
||||
if (!$bEditMode && !$oPage->IsPrintableVersion()) {
|
||||
$sId = $this->GetId();
|
||||
$sDivId = utils::Sanitize($sId, '', 'element_identifier');
|
||||
if ($this->GetAutoReload()) {
|
||||
$iReloadInterval = 1000 * $this->GetAutoReloadInterval();
|
||||
|
||||
$oPage->add_script(
|
||||
<<<JS
|
||||
if (typeof(AutoReloadDashboardId$sDivId) !== 'undefined')
|
||||
{
|
||||
clearInterval(AutoReloadDashboardId$sDivId);
|
||||
delete AutoReloadDashboardId$sDivId;
|
||||
}
|
||||
|
||||
AutoReloadDashboardId$sDivId = setInterval("ReloadDashboard$sDivId();", $iReloadInterval);
|
||||
|
||||
function ReloadDashboard$sDivId()
|
||||
{
|
||||
// Do not reload when a dialog box is active
|
||||
if (!($('.ui-dialog:visible').length > 0) && $('.ibo-dashboard#$sDivId').is(':visible'))
|
||||
{
|
||||
updateDashboard$sDivId();
|
||||
}
|
||||
}
|
||||
JS
|
||||
);
|
||||
} else {
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
if (typeof(AutoReloadDashboardId$sDivId) !== 'undefined')
|
||||
{
|
||||
clearInterval(AutoReloadDashboardId$sDivId);
|
||||
delete AutoReloadDashboardId$sDivId;
|
||||
}
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
if ($bCanEdit) {
|
||||
$this->RenderSelector($oPage, $oDashboard, $aAjaxParams);
|
||||
$this->RenderEditionTools($oPage, $oDashboard, $aAjaxParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param \Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout $oDashboard
|
||||
* @param bool $bFromDashboardPage
|
||||
* @param array $aAjaxParams
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
protected function RenderSelector(WebPage $oPage, DashboardLayoutUIBlock $oDashboard, $aAjaxParams = [])
|
||||
{
|
||||
if (!$this->HasCustomDashboard()) {
|
||||
return;
|
||||
}
|
||||
$sId = $this->GetId();
|
||||
$sDivId = utils::Sanitize($sId, '', 'element_identifier');
|
||||
$sExtraParams = json_encode($aAjaxParams);
|
||||
|
||||
$sSwitchToStandard = Dict::S('UI:Toggle:SwitchToStandardDashboard');
|
||||
$sSwitchToCustom = Dict::S('UI:Toggle:SwitchToCustomDashboard');
|
||||
$bStandardSelected = filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$sId, false), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$sSelectorHtml = '<div class="ibo-dashboard--selector" data-dashboard-id="'.$sDivId.'" data-tooltip-content="'.($bStandardSelected ? $sSwitchToCustom : $sSwitchToStandard).'">';
|
||||
$sSelectorHtml .= '<label class="ibo-dashboard--switch"><input type="checkbox" '.($bStandardSelected ? '' : 'checked').'><span class="ibo-dashboard--slider"></span></label></input></label>';
|
||||
$sSelectorHtml .= '</div>';
|
||||
|
||||
$bFromDashboardPage = isset($aAjaxParams['from_dashboard_page']) ? isset($aAjaxParams['from_dashboard_page']) : false;
|
||||
if ($bFromDashboardPage) {
|
||||
if ($oPage instanceof iTopWebPage) {
|
||||
$oToolbar = $oPage->GetTopBarLayout()->GetToolbar();
|
||||
$oToolbar->AddHtml($sSelectorHtml);
|
||||
}
|
||||
} else {
|
||||
$oToolbar = $oDashboard->GetToolbar();
|
||||
$oToolbar->AddHtml($sSelectorHtml);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function HasCustomDashboard()
|
||||
{
|
||||
// TODO 3.3 Make it more efficient by caching the result
|
||||
try {
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $this->GetId(), '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
|
||||
return ($oUDSet->Count() > 0);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param array $aExtraParams
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function RenderEditionTools(WebPage $oPage, DashboardLayoutUIBlock $oDashboard, $aExtraParams)
|
||||
{
|
||||
$oPage->LinkScriptFromAppRoot('node_modules/blueimp-file-upload/js/jquery.iframe-transport.js');
|
||||
$oPage->LinkScriptFromAppRoot('node_modules/blueimp-file-upload/js/jquery.fileupload.js');
|
||||
$sId = utils::Sanitize($this->GetId(), '', 'element_identifier');
|
||||
|
||||
$sMenuTogglerId = "ibo-dashboard-menu-toggler-{$sId}";
|
||||
$sActionEditId = "ibo-dashboard-menu-edit-{$sId}";
|
||||
$sPopoverMenuId = "ibo-dashboard-menu-popover-{$sId}";
|
||||
$sName = 'UI:Dashboard:Actions';
|
||||
|
||||
$bFromDashboardPage = isset($aExtraParams['from_dashboard_page']) ? isset($aExtraParams['from_dashboard_page']) : false;
|
||||
if ($bFromDashboardPage) {
|
||||
if (!($oPage instanceof iTopWebPage)) {
|
||||
// TODO 3.0 change the menu
|
||||
return;
|
||||
}
|
||||
$oToolbar = $oPage->GetTopBarLayout()->GetToolbar();
|
||||
} else {
|
||||
$oToolbar = $oDashboard->GetToolbar();
|
||||
}
|
||||
|
||||
// TODO 3.3 Check if we need different action for custom dashboard creation / edition
|
||||
$oActionEditButton = ButtonUIBlockFactory::MakeIconAction(
|
||||
'fas fa-pen',
|
||||
$this->HasCustomDashboard() ? Dict::S('UI:Dashboard:EditCustom') : Dict::S('UI:Dashboard:CreateCustom'),
|
||||
$sActionEditId,
|
||||
'',
|
||||
false,
|
||||
$sActionEditId
|
||||
)
|
||||
->AddCSSClass('ibo-top-bar--toolbar-dashboard-edit-button')
|
||||
->AddCSSClass('ibo-action-button');
|
||||
|
||||
$oToolbar->AddSubBlock($oActionEditButton);
|
||||
|
||||
$oActionButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S($sName), $sName, '', false, $sMenuTogglerId)
|
||||
->AddCSSClass('ibo-top-bar--toolbar-dashboard-menu-toggler')
|
||||
->AddCSSClass('ibo-action-button');
|
||||
|
||||
$oToolbar->AddSubBlock($oActionButton);
|
||||
$aActions = [];
|
||||
$sFile = addslashes(utils::LocalPath($this->sDefinitionFile));
|
||||
$sJSExtraParams = json_encode($aExtraParams);
|
||||
if ($this->HasCustomDashboard()) {
|
||||
// $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:EditCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
|
||||
// $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
|
||||
$oRevert = new JSPopupMenuItem(
|
||||
'UI:Dashboard:RevertConfirm',
|
||||
Dict::S('UI:Dashboard:DeleteCustom'),
|
||||
"if (confirm('".addslashes(Dict::S('UI:Dashboard:RevertConfirm'))."')) return RevertDashboard('{$this->sId}', $sJSExtraParams); else return false"
|
||||
);
|
||||
$aActions[$oRevert->GetUID()] = $oRevert->GetMenuItem();
|
||||
} else {
|
||||
// $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:CreateCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
|
||||
// $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
|
||||
}
|
||||
|
||||
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions);
|
||||
|
||||
$oActionsMenu = $oPage->GetPopoverMenu($sPopoverMenuId, $aActions)
|
||||
->SetTogglerJSSelector("#$sMenuTogglerId")
|
||||
->SetContainer(PopoverMenu::ENUM_CONTAINER_BODY);
|
||||
|
||||
$oToolbar->AddSubBlock($oActionButton)
|
||||
->AddSubBlock($oActionsMenu);
|
||||
|
||||
$sReloadURL = json_encode($this->GetReloadURL());
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
function EditDashboard(sId, sDashboardFile, aExtraParams)
|
||||
{
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'dashboard_editor', id: sId, file: sDashboardFile, extra_params: aExtraParams, reload_url: '$sReloadURL'},
|
||||
function(data)
|
||||
{
|
||||
$('body').append(data);
|
||||
}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
function RevertDashboard(sId, aExtraParams)
|
||||
{
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'revert_dashboard', dashboard_id: sId, extra_params: aExtraParams, reload_url: '$sReloadURL'},
|
||||
function(data)
|
||||
{
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
return false;
|
||||
}
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function RenderProperties($oPage, $aExtraParams = [])
|
||||
{
|
||||
parent::RenderProperties($oPage, $aExtraParams);
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$('#select_layout input').on('click', function() {
|
||||
var sLayoutClass = $(this).val();
|
||||
$('.itop-dashboard').runtimedashboard('option', {layout_class: sLayoutClass});
|
||||
} );
|
||||
$('#row_attr_dashboard_title').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: false, 'do_apply': function() {
|
||||
var sTitle = $('#attr_dashboard_title').val();
|
||||
$('.itop-dashboard').runtimedashboard('option', {title: sTitle});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$('#row_attr_auto_reload').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: true, 'do_apply': function() {
|
||||
var bAutoReload = $('#attr_auto_reload').is(':checked');
|
||||
$('.itop-dashboard').runtimedashboard('option', {auto_reload: bAutoReload});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$('#row_attr_auto_reload_sec').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: true, 'do_apply': function() {
|
||||
var iAutoReloadSec = $('#attr_auto_reload_sec').val();
|
||||
$('.itop-dashboard').runtimedashboard('option', {auto_reload_sec: iAutoReloadSec});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sOQL
|
||||
*
|
||||
* @return \DesignerForm
|
||||
* @throws \DictExceptionMissingString
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public static function GetDashletCreationForm($sOQL = null)
|
||||
{
|
||||
/** @var DashletService $oDashletService */
|
||||
$oDashletService = MetaModel::GetService('DashletService');
|
||||
$oAppContext = new ApplicationContext();
|
||||
$sContextMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
|
||||
$oForm = new DesignerForm();
|
||||
|
||||
// Get the list of all 'dashboard' menus in which we can insert a dashlet
|
||||
$aAllMenus = ApplicationMenu::ReflectionMenuNodes();
|
||||
$sRootMenuId = ApplicationMenu::GetRootMenuId($sContextMenuId);
|
||||
$aAllowedDashboards = [];
|
||||
$sDefaultDashboard = null;
|
||||
|
||||
// Store the parent menus for acces check
|
||||
$aParentMenus = [];
|
||||
foreach ($aAllMenus as $idx => $aMenu) {
|
||||
/** @var MenuNode $oMenu */
|
||||
$oMenu = $aMenu['node'];
|
||||
if (count(ApplicationMenu::GetChildren($oMenu->GetIndex())) > 0) {
|
||||
$aParentMenus[$oMenu->GetMenuId()] = $aMenu;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($aAllMenus as $idx => $aMenu) {
|
||||
$oMenu = $aMenu['node'];
|
||||
if ($oMenu instanceof DashboardMenuNode) {
|
||||
// Get the root parent for access check
|
||||
$sParentId = $aMenu['parent'];
|
||||
$aParentMenu = $aParentMenus[$sParentId];
|
||||
while (isset($aParentMenus[$aParentMenu['parent']])) {
|
||||
// grand parent exists
|
||||
$sParentId = $aParentMenu['parent'];
|
||||
$aParentMenu = $aParentMenus[$sParentId];
|
||||
}
|
||||
/** @var \MenuNode $oParentMenu */
|
||||
$oParentMenu = $aParentMenu['node'];
|
||||
if ($oMenu->IsEnabled() && $oParentMenu->IsEnabled()) {
|
||||
$sMenuLabel = $oMenu->GetTitle();
|
||||
$sParentLabel = Dict::S('Menu:'.$sParentId);
|
||||
if ($sParentLabel != $sMenuLabel) {
|
||||
$aAllowedDashboards[$oMenu->GetMenuId()] = $sParentLabel.' - '.$sMenuLabel;
|
||||
} else {
|
||||
$aAllowedDashboards[$oMenu->GetMenuId()] = $sMenuLabel;
|
||||
}
|
||||
if (empty($sDefaultDashboard) && ($sRootMenuId == ApplicationMenu::GetRootMenuId($oMenu->GetMenuId()))) {
|
||||
$sDefaultDashboard = $oMenu->GetMenuId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
asort($aAllowedDashboards);
|
||||
|
||||
$oField = new DesignerComboField('menu_id', Dict::S('UI:DashletCreation:Dashboard'), $sDefaultDashboard);
|
||||
$oField->SetAllowedValues($aAllowedDashboards);
|
||||
$oField->SetMandatory(true);
|
||||
$oForm->AddField($oField);
|
||||
|
||||
// Get the list of possible dashlets that support a creation from
|
||||
// an OQL
|
||||
$aDashlets = $oDashletService->GetAvailableDashlets('can_create_by_oql');
|
||||
|
||||
$oSelectorField = new DesignerFormSelectorField('dashlet_class', Dict::S('UI:DashletCreation:DashletType'), '');
|
||||
$oForm->AddField($oSelectorField);
|
||||
foreach ($aDashlets as $sDashletClass => $aDashletInfo) {
|
||||
$oSubForm = new DesignerForm();
|
||||
$oMetaModel = new ModelReflectionRuntime();
|
||||
/** @var \Dashlet $oDashlet */
|
||||
$oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, 0);
|
||||
$oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL);
|
||||
|
||||
$oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']);
|
||||
}
|
||||
$oField = new DesignerBooleanField('open_editor', Dict::S('UI:DashletCreation:EditNow'), true);
|
||||
$oForm->AddField($oField);
|
||||
|
||||
return $oForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param $sOQL
|
||||
*
|
||||
* @throws \DictExceptionMissingString
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public static function GetDashletCreationDlgFromOQL($oPage, $sOQL)
|
||||
{
|
||||
$oPage->add('<div id="dashlet_creation_dlg">');
|
||||
|
||||
$oForm = self::GetDashletCreationForm($sOQL);
|
||||
|
||||
$oForm->Render($oPage);
|
||||
$oPage->add('</div>');
|
||||
|
||||
$sDialogTitle = Dict::S('UI:DashletCreation:Title');
|
||||
$sOkButtonLabel = Dict::S('UI:Button:Ok');
|
||||
$sCancelButtonLabel = Dict::S('UI:Button:Cancel');
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$('#dashlet_creation_dlg').dialog({
|
||||
width: 600,
|
||||
modal: true,
|
||||
title: '$sDialogTitle',
|
||||
buttons: [
|
||||
{ text: "$sCancelButtonLabel",
|
||||
click: function() {
|
||||
$(this).dialog( "close" ); $(this).remove();
|
||||
} ,
|
||||
'class': 'ibo-button ibo-is-alternative ibo-is-neutral action cancel'
|
||||
},
|
||||
{ text: "$sOkButtonLabel",
|
||||
click: function() {
|
||||
var oForm = $(this).find('form');
|
||||
var sFormId = oForm.attr('id');
|
||||
var oParams = null;
|
||||
var aErrors = ValidateForm(sFormId, false);
|
||||
if (aErrors.length == 0)
|
||||
{
|
||||
oParams = ReadFormParams(sFormId);
|
||||
}
|
||||
oParams.operation = 'add_dashlet';
|
||||
var me = $(this);
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function(data) {
|
||||
me.dialog( "close" );
|
||||
me.remove();
|
||||
$('body').append(data);
|
||||
});
|
||||
},
|
||||
'class': 'ibo-button ibo-is-regular ibo-is-primary action' }
|
||||
],
|
||||
close: function() { $(this).remove(); }
|
||||
});
|
||||
JS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetDefinitionFile()
|
||||
{
|
||||
return $this->sDefinitionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDashboardFileRelative can also be an absolute path (compatibility with old URL)
|
||||
*
|
||||
* @return string full path to the Dashboard file
|
||||
* @throws \SecurityException if path isn't under approot
|
||||
* @uses utils::RealPath()
|
||||
* @since 2.7.8 3.0.3 3.1.0 N°4449 remove FPD
|
||||
*/
|
||||
public static function GetDashboardFileFromRelativePath($sDashboardFileRelative)
|
||||
{
|
||||
if (utils::RealPath($sDashboardFileRelative, APPROOT)) {
|
||||
// compatibility with old URL containing absolute path !
|
||||
return $sDashboardFileRelative;
|
||||
}
|
||||
|
||||
$sDashboardFile = APPROOT.$sDashboardFileRelative;
|
||||
if (false === utils::RealPath($sDashboardFile, APPROOT)) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
return $sDashboardFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDefinitionFile
|
||||
*/
|
||||
public function SetDefinitionFile($sDefinitionFile)
|
||||
{
|
||||
$this->sDefinitionFile = $sDefinitionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetReloadURL()
|
||||
{
|
||||
return $this->sReloadURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sReloadURL
|
||||
*/
|
||||
public function SetReloadURL($sReloadURL)
|
||||
{
|
||||
$this->sReloadURL = $sReloadURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function PrepareDashletForRendering(Dashlet $oDashlet, $aCoordinates, $aExtraParams = [])
|
||||
{
|
||||
$sDashletIdOrig = $oDashlet->GetID();
|
||||
$sDashboardSanitizedId = $this->GetSanitizedId();
|
||||
$sDashletIdNew = static::GetDashletUniqueId($this->GetCustomFlag(), $sDashboardSanitizedId, $aCoordinates[1], $aCoordinates[0], $sDashletIdOrig);
|
||||
$oDashlet->SetID($sDashletIdNew);
|
||||
$this->UpdateDashletUserPrefs($oDashlet, $sDashletIdOrig, $aExtraParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate dashlet specific prefs to new format
|
||||
* Before 2.7.0 we were using the same for dashboard menu or dashboard attributes, standard or custom :
|
||||
* <alias>-<class>|Dashlet<idx_dashlet>
|
||||
* Since 2.7.0 it is the following, with a "CUSTOM_" prefix if necessary :
|
||||
* * dashboard menu : <dashboard_id>_IDrow<row_idx>-col<col_idx>-<dashlet_idx>
|
||||
* * dashboard attribute : <class>__<attcode>_IDrow<row_idx>-col<col_idx>-<dashlet_idx>
|
||||
*
|
||||
* @param \Dashlet $oDashlet
|
||||
* @param string $sDashletIdOrig
|
||||
*
|
||||
* @param array $aExtraParams
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
* @since 2.7.0 N°2735
|
||||
*/
|
||||
private function UpdateDashletUserPrefs(Dashlet $oDashlet, $sDashletIdOrig, array $aExtraParams)
|
||||
{
|
||||
$bIsDashletWithListPref = ($oDashlet instanceof DashletObjectList);
|
||||
if (!$bIsDashletWithListPref) {
|
||||
return;
|
||||
}
|
||||
/** @var \DashletObjectList $oDashlet */
|
||||
|
||||
$bDashletIdInNewFormat = ($sDashletIdOrig === $oDashlet->GetID());
|
||||
if ($bDashletIdInNewFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sNewPrefKey = $this->GetDashletObjectListAppUserPreferencesPrefix($oDashlet, $aExtraParams, $oDashlet->GetID());
|
||||
$sPrefValueForNewKey = appUserPreferences::GetPref($sNewPrefKey, null);
|
||||
$bHasPrefInNewFormat = ($sPrefValueForNewKey !== null);
|
||||
if ($bHasPrefInNewFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sOldPrefKey = $this->GetDashletObjectListAppUserPreferencesPrefix($oDashlet, $aExtraParams, $sDashletIdOrig);
|
||||
$sPrefValueForOldKey = appUserPreferences::GetPref($sOldPrefKey, null);
|
||||
$bHasPrefInOldFormat = ($sPrefValueForOldKey !== null);
|
||||
if (!$bHasPrefInOldFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
appUserPreferences::SetPref($sNewPrefKey, $sPrefValueForOldKey);
|
||||
appUserPreferences::UnsetPref($sOldPrefKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DashletObjectList $oDashlet
|
||||
* @param array $aExtraParams
|
||||
* @param string $sDashletId
|
||||
*
|
||||
* @return string
|
||||
* @since 2.7.0
|
||||
*/
|
||||
private function GetDashletObjectListAppUserPreferencesPrefix(DashletObjectList $oDashlet, $aExtraParams, $sDashletId)
|
||||
{
|
||||
$sDataTableId = Dashlet::APP_USER_PREFERENCES_PREFIX.$sDashletId;
|
||||
$aClassAliases = [];
|
||||
try {
|
||||
$oFilter = $oDashlet->GetDBSearch($aExtraParams);
|
||||
$aClassAliases = $oFilter->GetSelectedClasses();
|
||||
} catch (Exception $e) {
|
||||
//on error, return default value
|
||||
return null;
|
||||
}
|
||||
|
||||
return DataTableSettings::GetAppUserPreferenceKey($aClassAliases, $sDataTableId);
|
||||
}
|
||||
}
|
||||
63
sources/Application/Dashlet/Base/DashletBadge.php
Normal file
63
sources/Application/Dashlet/Base/DashletBadge.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
// Copyright (C) 2012-2024 Combodo SAS
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// iTop is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
namespace Combodo\iTop\Application\Dashlet\Base;
|
||||
|
||||
use Combodo\iTop\Application\Dashlet\Dashlet;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer;
|
||||
use DBObjectSearch;
|
||||
use DesignerForm;
|
||||
use DesignerIconSelectionField;
|
||||
use Dict;
|
||||
use DisplayBlock;
|
||||
use utils;
|
||||
|
||||
class DashletBadge extends Dashlet
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function __construct($oModelReflection, $sId)
|
||||
{
|
||||
parent::__construct($oModelReflection, $sId);
|
||||
$this->aProperties['class'] = 'Contact';
|
||||
$this->aCSSClasses[] = 'ibo-dashlet--is-inline';
|
||||
$this->aCSSClasses[] = 'ibo-dashlet-badge';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = [])
|
||||
{
|
||||
$oDashletContainer = new DashletContainer($this->sId, ['dashlet-content']);
|
||||
|
||||
$sClass = $this->aProperties['class'];
|
||||
$oFilter = new DBObjectSearch($sClass);
|
||||
$oBlock = new DisplayBlock($oFilter, 'actions');
|
||||
$aExtraParams['context_filter'] = 1;
|
||||
$aExtraParams['withJSRefreshCallBack'] = true;
|
||||
$sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM)
|
||||
$oBlock->DisplayIntoContentBlock($oDashletContainer, $oPage, $sBlockId, $aExtraParams);
|
||||
|
||||
return $oDashletContainer;
|
||||
}
|
||||
}
|
||||
268
sources/Application/Dashlet/Base/DashletGroupBy.php
Normal file
268
sources/Application/Dashlet/Base/DashletGroupBy.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
// Copyright (C) 2012-2024 Combodo SAS
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// iTop is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
namespace Combodo\iTop\Application\Dashlet\Base;
|
||||
|
||||
use Combodo\iTop\Application\Dashlet\Dashlet;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use DBObjectSearch;
|
||||
use DesignerComboField;
|
||||
use DesignerForm;
|
||||
use DesignerFormSelectorField;
|
||||
use DesignerHiddenField;
|
||||
use DesignerIntegerField;
|
||||
use DesignerLongTextField;
|
||||
use DesignerTextField;
|
||||
use Dict;
|
||||
use DisplayBlock;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
|
||||
abstract class DashletGroupBy extends Dashlet
|
||||
{
|
||||
public function __construct($oModelReflection, $sId, string $sDashletType = null)
|
||||
{
|
||||
parent::__construct($oModelReflection, $sId, $sDashletType);
|
||||
$this->aProperties['title'] = '';
|
||||
$this->aProperties['query'] = 'SELECT Contact';
|
||||
$this->aProperties['group_by'] = 'status';
|
||||
$this->aProperties['style'] = 'table';
|
||||
$this->aProperties['aggregation_function'] = 'count';
|
||||
$this->aProperties['aggregation_attribute'] = '';
|
||||
$this->aProperties['limit'] = '';
|
||||
$this->aProperties['order_by'] = '';
|
||||
$this->aProperties['order_direction'] = '';
|
||||
}
|
||||
|
||||
protected $sGroupByLabel = null;
|
||||
protected $sGroupByExpr = null;
|
||||
protected $sGroupByAttCode = null;
|
||||
protected $sFunction = null;
|
||||
protected $sAggregationFunction = null;
|
||||
protected $sAggregationAttribute = null;
|
||||
protected $sLimit = null;
|
||||
protected $sOrderBy = null;
|
||||
protected $sOrderDirection = null;
|
||||
protected $sClass = null;
|
||||
|
||||
/**
|
||||
* Compute Grouping
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function OnUpdate()
|
||||
{
|
||||
$this->sGroupByExpr = null;
|
||||
$this->sGroupByLabel = null;
|
||||
$this->sGroupByAttCode = null;
|
||||
$this->sFunction = null;
|
||||
$this->sClass = null;
|
||||
|
||||
$sQuery = $this->aProperties['query'];
|
||||
$sGroupBy = $this->aProperties['group_by'];
|
||||
|
||||
$this->sAggregationFunction = $this->aProperties['aggregation_function'];
|
||||
$this->sAggregationAttribute = $this->aProperties['aggregation_attribute'] ?? '';
|
||||
|
||||
$this->sLimit = $this->aProperties['limit'] ?? 0;
|
||||
$this->sOrderBy = $this->aProperties['order_by'] ?? null;
|
||||
if (empty($this->sOrderBy)) {
|
||||
if ($this->aProperties['style'] == 'pie') {
|
||||
$this->sOrderBy = 'function';
|
||||
} else {
|
||||
$this->sOrderBy = 'attribute';
|
||||
}
|
||||
}
|
||||
|
||||
// First perform the query - if the OQL is not ok, it will generate an exception : no need to go further
|
||||
try {
|
||||
$oQuery = $this->oModelReflection->GetQuery($sQuery);
|
||||
$this->sClass = $oQuery->GetClass();
|
||||
$sClassAlias = $oQuery->GetClassAlias();
|
||||
} catch (Exception $e) {
|
||||
// Invalid query, let the user edit the dashlet/dashboard anyhow
|
||||
$this->sClass = null;
|
||||
$sClassAlias = '';
|
||||
}
|
||||
|
||||
// Check groupby... it can be wrong at this stage
|
||||
if (preg_match('/^(.*):(.*)$/', $sGroupBy, $aMatches)) {
|
||||
$this->sGroupByAttCode = $aMatches[1];
|
||||
$this->sFunction = $aMatches[2];
|
||||
} else {
|
||||
$this->sGroupByAttCode = $sGroupBy;
|
||||
$this->sFunction = null;
|
||||
}
|
||||
|
||||
if ((!is_null($this->sClass)) && empty($this->aProperties['order_direction'])) {
|
||||
$aAttributeTypes = $this->oModelReflection->ListAttributes($this->sClass);
|
||||
if (isset($aAttributeTypes[$this->sGroupByAttCode])) {
|
||||
$sAttributeType = $aAttributeTypes[$this->sGroupByAttCode];
|
||||
if (is_subclass_of($sAttributeType, 'AttributeDateTime') || $sAttributeType == 'AttributeDateTime') {
|
||||
$this->sOrderDirection = 'asc';
|
||||
} else {
|
||||
$this->sOrderDirection = 'desc';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->sOrderDirection = $this->aProperties['order_direction'];
|
||||
}
|
||||
|
||||
if ((!is_null($this->sClass)) && $this->oModelReflection->IsValidAttCode($this->sClass, $this->sGroupByAttCode)) {
|
||||
$sAttLabel = $this->oModelReflection->GetLabel($this->sClass, $this->sGroupByAttCode);
|
||||
if (!is_null($this->sFunction)) {
|
||||
switch ($this->sFunction) {
|
||||
case 'hour':
|
||||
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Hour', $sAttLabel);
|
||||
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%H')"; // 0 -> 23
|
||||
break;
|
||||
|
||||
case 'month':
|
||||
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Month', $sAttLabel);
|
||||
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m')"; // yyyy-mm
|
||||
break;
|
||||
|
||||
case 'day_of_week':
|
||||
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfWeek', $sAttLabel);
|
||||
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%w')";
|
||||
break;
|
||||
|
||||
case 'day_of_month':
|
||||
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfMonth', $sAttLabel);
|
||||
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m-%d')"; // mm-dd
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->sGroupByLabel = 'Unknown group by function '.$this->sFunction;
|
||||
$this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode;
|
||||
}
|
||||
} else {
|
||||
$this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode;
|
||||
$this->sGroupByLabel = $sAttLabel;
|
||||
}
|
||||
} else {
|
||||
$this->sGroupByAttCode = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \ArchivedObjectException
|
||||
*/
|
||||
public function Render($oPage, $bEditMode = false, $aExtraParams = [])
|
||||
{
|
||||
$sTitle = $this->aProperties['title'];
|
||||
$sQuery = $this->aProperties['query'];
|
||||
$sStyle = $this->aProperties['style'];
|
||||
|
||||
// First perform the query - if the OQL is not ok, it will generate an exception : no need to go further
|
||||
if (isset($aExtraParams['query_params'])) {
|
||||
$aQueryParams = $aExtraParams['query_params'];
|
||||
} elseif (isset($aExtraParams['this->class']) && isset($aExtraParams['this->id'])) {
|
||||
$oObj = MetaModel::GetObject($aExtraParams['this->class'], $aExtraParams['this->id']);
|
||||
$aQueryParams = $oObj->ToArgsForQuery();
|
||||
} else {
|
||||
$aQueryParams = [];
|
||||
}
|
||||
$oFilter = DBObjectSearch::FromOQL($sQuery, $aQueryParams);
|
||||
$oFilter->SetShowObsoleteData(utils::ShowObsoleteData());
|
||||
|
||||
$sClass = $oFilter->GetClass();
|
||||
if (!$this->oModelReflection->IsValidAttCode($sClass, $this->sGroupByAttCode)) {
|
||||
return new Html('<p>'.Dict::S('UI:DashletGroupBy:MissingGroupBy').'</p>');
|
||||
}
|
||||
|
||||
switch ($sStyle) {
|
||||
case 'bars':
|
||||
$sType = 'chart';
|
||||
$aParams = [
|
||||
'chart_type' => 'bars',
|
||||
'chart_title' => $sTitle,
|
||||
'group_by' => $this->sGroupByExpr,
|
||||
'group_by_label' => $this->sGroupByLabel,
|
||||
'aggregation_function' => $this->sAggregationFunction,
|
||||
'aggregation_attribute' => $this->sAggregationAttribute,
|
||||
'limit' => $this->sLimit,
|
||||
'order_direction' => $this->sOrderDirection,
|
||||
'order_by' => $this->sOrderBy,
|
||||
];
|
||||
$sHtmlTitle = ''; // done in the itop block
|
||||
break;
|
||||
|
||||
case 'pie':
|
||||
$sType = 'chart';
|
||||
$aParams = [
|
||||
'chart_type' => 'pie',
|
||||
'chart_title' => $sTitle,
|
||||
'group_by' => $this->sGroupByExpr,
|
||||
'group_by_label' => $this->sGroupByLabel,
|
||||
'aggregation_function' => $this->sAggregationFunction,
|
||||
'aggregation_attribute' => $this->sAggregationAttribute,
|
||||
'limit' => $this->sLimit,
|
||||
'order_direction' => $this->sOrderDirection,
|
||||
'order_by' => $this->sOrderBy,
|
||||
];
|
||||
$sHtmlTitle = ''; // done in the itop block
|
||||
break;
|
||||
|
||||
case 'table':
|
||||
default:
|
||||
$sHtmlTitle = utils::HtmlEntities(Dict::S($sTitle)); // done in the itop block
|
||||
$sType = 'count';
|
||||
$aParams = [
|
||||
'group_by' => $this->sGroupByExpr,
|
||||
'group_by_label' => $this->sGroupByLabel,
|
||||
'aggregation_function' => $this->sAggregationFunction,
|
||||
'aggregation_attribute' => $this->sAggregationAttribute,
|
||||
'limit' => $this->sLimit,
|
||||
'order_direction' => $this->sOrderDirection,
|
||||
'order_by' => $this->sOrderBy,
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
//$oPanel = \Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory::MakeStandard();
|
||||
//PanelUIBlockFactory::MakeForClass($sClass, Dict::S($sTitle));
|
||||
|
||||
$sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occurring in the same DOM)
|
||||
$oBlock = new DisplayBlock($oFilter, $sType);
|
||||
//$oBlock->DisplayIntoContentBlock($oPanel, $oPage, $sBlockId, array_merge($aExtraParams, $aParams));
|
||||
$aExtraParams["surround_with_panel"] = true;
|
||||
$aExtraParams["panel_title"] = Dict::S($sTitle);
|
||||
$aExtraParams["panel_class"] = $sClass;
|
||||
$oPanel = $oBlock->GetDisplay($oPage, $sBlockId, array_merge($aExtraParams, $aParams));
|
||||
if ($bEditMode) {
|
||||
$oPanel->AddHtml('<div class="ibo-dashlet-blocker dashlet-blocker"></div>');
|
||||
}
|
||||
|
||||
return $oPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function CanCreateFromOQL()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
36
sources/Application/Dashlet/Base/DashletGroupByBars.php
Normal file
36
sources/Application/Dashlet/Base/DashletGroupByBars.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
// Copyright (C) 2012-2024 Combodo SAS
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// iTop is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
namespace Combodo\iTop\Application\Dashlet\Base;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer;
|
||||
use Dict;
|
||||
use utils;
|
||||
|
||||
class DashletGroupByBars extends DashletGroupBy
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function __construct($oModelReflection, $sId)
|
||||
{
|
||||
parent::__construct($oModelReflection, $sId);
|
||||
$this->aProperties['style'] = 'bars';
|
||||
}
|
||||
}
|
||||
59
sources/Application/Dashlet/Base/DashletGroupByPie.php
Normal file
59
sources/Application/Dashlet/Base/DashletGroupByPie.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
// Copyright (C) 2012-2024 Combodo SAS
|
||||
//
|
||||
// This file is part of iTop.
|
||||
//
|
||||
// iTop is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// iTop is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
namespace Combodo\iTop\Application\Dashlet\Base;
|
||||
|
||||
use Combodo\iTop\Application\Helper\WebResourcesHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletContainer;
|
||||
use Dict;
|
||||
use utils;
|
||||
|
||||
class DashletGroupByPie extends DashletGroupBy
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function __construct($oModelReflection, $sId)
|
||||
{
|
||||
parent::__construct($oModelReflection, $sId);
|
||||
$this->aProperties['style'] = 'pie';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetJSFilesRelPaths(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::GetJSFilesRelPaths(),
|
||||
WebResourcesHelper::GetJSFilesRelPathsForC3JS()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetCSSFilesRelPaths(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::GetCSSFilesRelPaths(),
|
||||
WebResourcesHelper::GetCSSFilesRelPathsForC3JS()
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user