Compare commits

...

106 Commits

Author SHA1 Message Date
Eric Espie
24110bd880 fix query selector when checkbox is missing 2026-02-06 11:39:20 +01:00
Eric Espie
c11fd6e348 Merge remote-tracking branch 'origin/develop' into feature/new-dashboard-layout 2026-02-05 17:07:17 +01:00
Eric Espie
dc89c4705e Merge remote-tracking branch 'origin/develop' into feature/new-dashboard-layout 2026-02-05 15:37:29 +01:00
Eric Espie
863dd67302 Better session control 2026-02-05 09:26:50 +01:00
Stephen Abello
f9272ac2ed Fix dashboard loading a legacy XML format returning empty data 2026-02-04 14:21:33 +01:00
Eric Espie
a1d5acadf8 N°9185 - XML dashboard format should be in the new format when user exports its dashboard 2026-02-03 11:53:49 +01:00
Eric Espie
0b17e1f9b3 XML data from dashboard 2026-02-03 11:02:39 +01:00
Stephen Abello
b2041e3b63 Fix code style 2026-02-03 09:39:17 +01:00
Stephen Abello
73e6a0af8a Allow dashboard to refresh from backend, or refresh with new data format when switching from custom to default 2026-02-03 09:15:41 +01:00
Benjamin DALSASS
2ff183a78a Update dashboard layout 2026-01-30 06:40:22 +01:00
Eric Espie
3aba8c5aa3 Error return 2026-01-29 10:50:58 +01:00
Eric Espie
d7451fe2ea Fix CI 2026-01-28 15:59:56 +01:00
Eric Espie
bb1c4f865a Fix CI 2026-01-28 15:00:22 +01:00
Eric Espie
3fa658edb5 Cleanup Dashboard and dashlet unused methods and ajax end points 2026-01-27 17:52:36 +01:00
Eric Espie
814f91bd48 minor change to comply with PSR-11 2026-01-27 16:48:11 +01:00
Eric Espie
3092e9d5d4 Documentation 2026-01-27 16:37:17 +01:00
Eric Espie
f3fddfe54e Add FormFactoryBuilderService to Service Locator 2026-01-27 13:34:47 +01:00
Eric Espie
bfa7a209d6 Enhance Service locator 2026-01-27 11:54:05 +01:00
Benjamin DALSASS
390b5c0bc3 Change dashboard editor save request method from GET to POST 2026-01-26 14:10:16 +01:00
Benjamin DALSASS
e9a5d1a65e Dashboard editor adjust title size 2026-01-23 14:49:21 +01:00
Eric Espie
07675778d1 Import feature 2026-01-23 14:32:47 +01:00
Stephen Abello
ceda1d40f8 When reloading state, ask for a re-render of each dashlet as we'll need their scripts to be re-executed (that also make the data refresh) 2026-01-23 11:05:16 +01:00
Eric Espie
1bb7ddd488 Export feature 2026-01-23 09:52:37 +01:00
Benjamin DALSASS
4abf948a03 Dashboard editor: Scroll view to new added dashlet 2026-01-23 09:43:10 +01:00
Eric Espie
f4a0a2f2ff Fix CI 2026-01-22 17:32:09 +01:00
Eric Espie
eb60dd3bb1 Fix CI 2026-01-22 16:56:07 +01:00
Eric Espie
e9b8e088fe Serialization for boolean and integer 2026-01-22 16:05:05 +01:00
Eric Espie
5fe9b1fe41 PHP CS-Fixer 2026-01-22 16:03:33 +01:00
Eric Espie
1d100f1727 Serialization for boolean and integer 2026-01-22 16:03:02 +01:00
Benjamin DALSASS
7e91e01ad8 Remove default icon to dashlet headers until default icon not available in icon list (to be fixed) 2026-01-22 14:31:31 +01:00
Benjamin DALSASS
890d1ece8d Update ChoiceImageFormBlock and add feature to ValueTypeClass.php 2026-01-22 14:30:34 +01:00
Eric Espie
2d4c5b8b2a Fix various errors 2026-01-22 13:49:17 +01:00
Eric Espie
ce263ae97b Fix group by error 2026-01-22 13:33:41 +01:00
Eric Espie
fe4b2a0d93 Comments 2026-01-22 09:53:28 +01:00
Eric Espie
140c0a960f Load dashboard using deserialization 2026-01-21 17:59:40 +01:00
Eric Espie
e909c41445 Save as XML serialized 2026-01-21 17:27:16 +01:00
Stephen Abello
945d2f32c5 Remove Dashlet when cancelling a creation form, change the dashlet panel title if we are editing a dashlet 2026-01-21 15:34:40 +01:00
Benjamin DALSASS
28b74bb489 Patch collection sharing same form block 2026-01-21 14:46:48 +01:00
Stephen Abello
8581ec0bf6 Try some display fixes in @bdalsass SCSS experiment 2026-01-21 11:43:53 +01:00
Stephen Abello
7be77949ac Fix some id issue caused by the $bEditMode being passed to dashlet rendering 2026-01-21 11:15:32 +01:00
Benjamin DALSASS
c85e0373b1 Add partial turbo event 2026-01-20 14:39:40 +01:00
Benjamin DALSASS
096dfd7dec Remove abstract filter to ValueTypeClass 2026-01-20 09:54:28 +01:00
Eric Espie
23a4dd5bb6 Fix CI 2026-01-20 09:52:47 +01:00
Benjamin DALSASS
ab5394ef83 Dashboard old data xml compatibility and dashlet layout style 2026-01-20 08:29:03 +01:00
Eric Espie
bb752debaa Fix CI 2026-01-19 18:04:53 +01:00
Eric Espie
7ffddbb633 Fix CI 2026-01-19 17:58:36 +01:00
Eric Espie
5346ccd13d PHP CS-Fixer 2026-01-19 17:28:45 +01:00
Eric Espie
2fdb09173a Split&move Dashboard file 2026-01-19 17:23:09 +01:00
Eric Espie
6e1c2e9f51 moved Dashboard 2026-01-19 17:03:49 +01:00
Eric Espie
ccf0ec626b moved Dashboard 2026-01-19 17:01:07 +01:00
Stephen Abello
cb61ae8d61 Make dashlet that needs to execute JS be able to do so in the first paint and when refreshed 2026-01-19 17:00:37 +01:00
Eric Espie
6aa8ac7ff1 Fix dashlet creation 2026-01-19 16:59:36 +01:00
Eric Espie
0b6cbc1fef Fix dashlet creation 2026-01-19 16:51:58 +01:00
Eric Espie
805e306712 Fix default dashlet 2026-01-19 16:17:38 +01:00
Eric Espie
8e16d24d85 Default values for dashlet dimensions 2026-01-19 10:39:24 +01:00
Eric Espie
344854a0d0 Dashlet extraction 2026-01-16 18:52:55 +01:00
Eric Espie
3c80c93b4f Dashlet extraction 2026-01-16 18:47:28 +01:00
Eric Espie
efc6e6730b Dashlet extraction 2026-01-16 09:57:14 +01:00
Eric Espie
34fa1dba4b Dashlet extraction 2026-01-16 09:45:36 +01:00
Eric Espie
542d33a040 Dashlet extraction 2026-01-16 09:41:26 +01:00
Eric Espie
3769bc1024 Dashlet definition for core dashlets 2026-01-16 09:34:07 +01:00
Stephen Abello
e5338f28eb Save actions submit the form, it has yet to validate the form 2026-01-15 15:45:52 +01:00
Stephen Abello
5cfe7fa6eb Allow dashboard to restore state (either saved state or from backend). Restore old state on edition cancel 2026-01-15 11:51:49 +01:00
Stephen Abello
27c16a782c Remove done todo 2026-01-15 11:09:44 +01:00
Stephen Abello
7c21178e1d Handle backend save response in dashboard js 2026-01-15 11:08:20 +01:00
Stephen Abello
3a6e148c11 Prepare steps for custom dashboard handling 2026-01-15 10:56:35 +01:00
Eric Espie
4d9e18890a fix warnings 2026-01-15 10:44:20 +01:00
Eric Espie
50ffaa2b55 Read custom dashboards with grid format 2026-01-15 10:19:33 +01:00
Eric Espie
15c8f5903b save dashboard controller 2026-01-14 16:57:19 +01:00
Stephen Abello
0bc6f5d56a Align posted dashboard data to new data denormalized format 2026-01-14 11:39:56 +01:00
Stephen Abello
d248524cc8 Align posted dashboard data to new data denormalized format 2026-01-14 11:15:15 +01:00
Stephen Abello
410dc152d7 Make cloning dashlet actually use original dashlet data to get a new similar dashlet 2026-01-14 10:01:38 +01:00
Stephen Abello
423413e3a0 Fix form interaction with buttons when screen is smaller 2026-01-14 09:42:37 +01:00
Stephen Abello
8896c576d7 Make form style + button style match mockup 2026-01-14 09:32:11 +01:00
Benjamin DALSASS
975046da17 Add ChoiceImageFormBlock 2026-01-14 08:19:03 +01:00
Benjamin DALSASS
e4a281c3ff Add ChoiceImageFormBlock 2026-01-14 07:32:29 +01:00
Eric Espie
90729f84b6 Polymorphic type 2026-01-13 18:00:34 +01:00
Stephen Abello
c075a5c145 Make dashlet edition / refresh better interact with listener and gridstack library in order to allow multiple edits without breaking 2026-01-13 16:30:00 +01:00
Benjamin DALSASS
cd6d130bcb Move dashboard blocks to itop project and keep demonstrator one 2026-01-13 11:24:56 +01:00
Stephen Abello
7ca2c56dad Send denormalized data to backend in order to persist dashboard 2026-01-13 11:18:18 +01:00
Benjamin DALSASS
dd0ac58643 Prepare property service for form block dashboard 2026-01-13 07:36:47 +01:00
Stephen Abello
df943ec8b5 Change DashletBadge preferred height 2026-01-12 16:28:18 +01:00
Stephen Abello
076a6d0495 Make dashlet position non-random. Use their preferred width/height for now 2026-01-12 16:26:52 +01:00
Stephen Abello
02e59c906b Remove dummy buttons used for development purpose 2026-01-12 16:11:43 +01:00
Stephen Abello
49f91961e7 Make dashlet rendering computing request data 2026-01-12 15:09:14 +01:00
Stephen Abello
441519d71d Rename parameter 2026-01-12 15:08:33 +01:00
Eric Espie
5c75d0ce7c Fix some value types 2026-01-12 13:50:28 +01:00
Eric Espie
2efe80265d Pass form values from dashlet 2026-01-12 12:02:45 +01:00
Eric Espie
b58a64e143 Add Icon selection formBlock 2026-01-12 11:44:44 +01:00
Stephen Abello
ff11aec7fe Make clone dashlet ask for a rendered dashlet to the backend 2026-01-12 11:10:08 +01:00
Benjamin DALSASS
f79bb9d51c Add option display to form (css needs updates)
Add options with_run_button and with_book_button to FormBlockOql
2026-01-12 08:00:17 +01:00
Stephen Abello
3c365fc103 Make add / edit in dashboard interactive, and pass on data 2026-01-09 16:31:36 +01:00
Stephen Abello
7b193dd737 Use npm for gridstack, remove unnecessary folders using our dedicated service 2026-01-09 15:10:10 +01:00
Eric Espie
1538596db8 PHP CS-Fixer 2026-01-09 11:04:38 +01:00
Eric Espie
9d4fc345bc Fix (de)normalized data for Forms 2026-01-09 10:51:32 +01:00
Eric Espie
08c9309572 Fix (de)normalized data for Forms 2026-01-08 17:41:58 +01:00
Eric Espie
d072aa05f1 Prepare (de)normalized data for Forms 2026-01-08 17:28:48 +01:00
Eric Espie
85c1f091e2 Prepare normalized data for Forms 2026-01-08 17:25:52 +01:00
Eric Espie
be6a8abdf4 N°9066 - Split XML serializer into normalizer and encoder 2026-01-08 17:23:06 +01:00
Benjamin DALSASS
60bcf0c85f Change AbstractForm BindingReceivedEvent 2026-01-08 16:02:23 +01:00
Benjamin DALSASS
4a8804b8ac Dashlet form in new dashboard layout (suite) 2026-01-08 15:30:51 +01:00
Benjamin DALSASS
b014b9f638 Dashlet form in new dashboard layout 2026-01-08 14:42:16 +01:00
Eric Espie
1c633c6173 typo 2026-01-08 13:45:11 +01:00
Stephen Abello
cdc95aca7b Dump autoloader 2026-01-08 10:18:30 +01:00
Stephen Abello
b4460999ef Dump autoloader 2026-01-08 10:17:35 +01:00
Stephen Abello
a713e1b56e N°8641 - Dashboard editor front-end first commit for Form SDK integration.
* No dashlet edition
* Dashboard are not persisted
* Unable to load a dashboard from an endpoint (refresh)
* Grid library need proper npm integration
2026-01-08 10:17:30 +01:00
210 changed files with 25063 additions and 6141 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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('&nbsp;'));
}
$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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,3 +15,4 @@
@import "wizard-container/wizard-container";
@import "object/all";
@import "activity-panel/all";
@import "dashlet-panel/all";

View File

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

View File

@@ -0,0 +1,2 @@
@import "dashlet-entry";
@import "dashlet-panel";

View 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;
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,565 @@
# gridstack.js
[![NPM version](https://img.shields.io/npm/v/gridstack.svg)](https://www.npmjs.com/package/gridstack)
[![Coverage Status](https://coveralls.io/repos/github/gridstack/gridstack.js/badge.svg?branch=develop)](https://coveralls.io/github/gridstack/gridstack.js?branch=develop)
[![downloads](https://img.shields.io/npm/dm/gridstack.svg)](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!
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/alaind831)
[![Donate](https://img.shields.io/badge/Donate-Venmo-g.svg)](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)
<!-- [![Slack Status](https://gridstackjs.com/badge.svg)](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
[![NPM version](https://img.shields.io/npm/v/gridstack.svg)](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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

26
node_modules/gridstack/dist/dd-droppable.d.ts generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

27
node_modules/gridstack/dist/dd-element.d.ts generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

82
node_modules/gridstack/dist/dd-gridstack.d.ts generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

43
node_modules/gridstack/dist/dd-manager.d.ts generated vendored Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

28
node_modules/gridstack/dist/dd-resizable.d.ts generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

38
node_modules/gridstack/dist/dd-touch.d.ts generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

3
node_modules/gridstack/dist/gridstack-all.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

321
node_modules/gridstack/dist/gridstack-engine.d.ts generated vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

283
node_modules/gridstack/dist/utils.d.ts generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

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
View 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
View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View 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;
}
}

View 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
{
}

View File

@@ -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' => [],
]);
}
}

View 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);
}
}

View File

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

View 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];
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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';
}
}

View 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