update
This commit is contained in:
31
changelog.md
31
changelog.md
@ -1,5 +1,36 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-06-28 - 1.10.10 - improve(dees-dashboardgrid)
|
||||||
|
Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js
|
||||||
|
|
||||||
|
- Improved margin system supporting uniform or individual margins (top, right, bottom, left)
|
||||||
|
- Added collision detection to prevent widget overlap during drag operations
|
||||||
|
- Implemented auto-positioning for new widgets to find first available space
|
||||||
|
- Added compact() method to eliminate gaps and compress layout vertically or horizontally
|
||||||
|
- Enhanced resize constraints with minW, maxW, minH, maxH support
|
||||||
|
- Added optional grid lines visualization for better layout understanding
|
||||||
|
- Improved resize handles with better visibility and hover states
|
||||||
|
- Added RTL (right-to-left) layout support
|
||||||
|
- Implemented cellHeightUnit option supporting 'px', 'em', 'rem', or 'auto' (square cells)
|
||||||
|
- Added configurable animation with enableAnimation property
|
||||||
|
- Enhanced demo with interactive controls for testing all features
|
||||||
|
- Better calculation of widget positions accounting for margins between cells
|
||||||
|
- Added findAvailablePosition() for intelligent widget placement
|
||||||
|
- Improved drag and resize calculations for pixel-perfect positioning
|
||||||
|
|
||||||
|
## 2025-06-28 - 1.10.9 - feat(dees-dashboardgrid)
|
||||||
|
Add new dashboard grid component with drag-and-drop and resize capabilities
|
||||||
|
|
||||||
|
- Created dees-dashboardgrid component for building flexible dashboard layouts
|
||||||
|
- Features drag-and-drop functionality for rearranging widgets
|
||||||
|
- Includes resize handles for adjusting widget dimensions
|
||||||
|
- Supports configurable grid properties (columns, cell height, gap)
|
||||||
|
- Provides widget locking and editable mode controls
|
||||||
|
- Styled with shadcn design principles
|
||||||
|
- No external dependencies - built with native browser APIs
|
||||||
|
- Emits events for widget movements and resizes
|
||||||
|
- Includes comprehensive demo with sample dashboard widgets
|
||||||
|
|
||||||
## 2025-06-27 - 1.10.8 - feat(ui-components)
|
## 2025-06-27 - 1.10.8 - feat(ui-components)
|
||||||
Update multiple components with shadcn-aligned styling and improved animations
|
Update multiple components with shadcn-aligned styling and improved animations
|
||||||
|
|
||||||
|
191
ts_web/elements/dees-dashboardgrid.demo.ts
Normal file
191
ts_web/elements/dees-dashboardgrid.demo.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import type { DeesDashboardgrid } from './dees-dashboardgrid.js';
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
|
||||||
|
export const demoFunc = () => {
|
||||||
|
return html`
|
||||||
|
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||||
|
const grid = elementArg.querySelector('#dashboardGrid') as DeesDashboardgrid;
|
||||||
|
|
||||||
|
// Set initial widgets
|
||||||
|
grid.widgets = [
|
||||||
|
{
|
||||||
|
id: 'metrics1',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 3,
|
||||||
|
h: 2,
|
||||||
|
title: 'Revenue',
|
||||||
|
icon: 'lucide:dollarSign',
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">$124,563</div>
|
||||||
|
<div style="color: #22c55e; font-size: 14px; margin-top: 8px;">↑ 12.5% from last month</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'metrics2',
|
||||||
|
x: 3,
|
||||||
|
y: 0,
|
||||||
|
w: 3,
|
||||||
|
h: 2,
|
||||||
|
title: 'Users',
|
||||||
|
icon: 'lucide:users',
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">8,234</div>
|
||||||
|
<div style="color: #3b82f6; font-size: 14px; margin-top: 8px;">↑ 5.2% from last week</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'chart1',
|
||||||
|
x: 6,
|
||||||
|
y: 0,
|
||||||
|
w: 6,
|
||||||
|
h: 4,
|
||||||
|
title: 'Analytics',
|
||||||
|
icon: 'lucide:lineChart',
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px; height: 100%; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<div style="text-align: center; color: #71717a;">
|
||||||
|
<dees-icon .icon=${'lucide:lineChart'} style="font-size: 48px; margin-bottom: 16px;"></dees-icon>
|
||||||
|
<div>Chart visualization area</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configure grid
|
||||||
|
grid.cellHeight = 80;
|
||||||
|
grid.margin = { top: 10, right: 10, bottom: 10, left: 10 };
|
||||||
|
grid.enableAnimation = true;
|
||||||
|
grid.showGridLines = false;
|
||||||
|
|
||||||
|
let widgetCounter = 4;
|
||||||
|
|
||||||
|
// Control buttons
|
||||||
|
const buttons = elementArg.querySelectorAll('dees-button');
|
||||||
|
buttons.forEach(button => {
|
||||||
|
const text = button.textContent?.trim();
|
||||||
|
|
||||||
|
if (text === 'Toggle Animation') {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
grid.enableAnimation = !grid.enableAnimation;
|
||||||
|
});
|
||||||
|
} else if (text === 'Toggle Grid Lines') {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
grid.showGridLines = !grid.showGridLines;
|
||||||
|
});
|
||||||
|
} else if (text === 'Add Widget') {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const newWidget = {
|
||||||
|
id: `widget${widgetCounter++}`,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 3,
|
||||||
|
h: 2,
|
||||||
|
autoPosition: true,
|
||||||
|
title: `Widget ${widgetCounter - 1}`,
|
||||||
|
icon: 'lucide:package',
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px; text-align: center;">
|
||||||
|
<div style="color: #71717a;">New widget content</div>
|
||||||
|
<div style="margin-top: 8px; font-size: 24px; font-weight: 600; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">${Math.floor(Math.random() * 1000)}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
grid.addWidget(newWidget, true);
|
||||||
|
});
|
||||||
|
} else if (text === 'Compact Grid') {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
grid.compact();
|
||||||
|
});
|
||||||
|
} else if (text === 'Toggle Edit Mode') {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
grid.editable = !grid.editable;
|
||||||
|
button.textContent = grid.editable ? 'Lock Grid' : 'Unlock Grid';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to grid events
|
||||||
|
grid.addEventListener('widget-move', (e: CustomEvent) => {
|
||||||
|
console.log('Widget moved:', e.detail.widget);
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.addEventListener('widget-resize', (e: CustomEvent) => {
|
||||||
|
console.log('Widget resized:', e.detail.widget);
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.demoBox {
|
||||||
|
position: relative;
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-controls dees-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 600px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<div class="demoBox">
|
||||||
|
<div class="demo-controls">
|
||||||
|
<dees-button-group label="Animation:">
|
||||||
|
<dees-button>Toggle Animation</dees-button>
|
||||||
|
</dees-button-group>
|
||||||
|
|
||||||
|
<dees-button-group label="Display:">
|
||||||
|
<dees-button>Toggle Grid Lines</dees-button>
|
||||||
|
</dees-button-group>
|
||||||
|
|
||||||
|
<dees-button-group label="Actions:">
|
||||||
|
<dees-button>Add Widget</dees-button>
|
||||||
|
<dees-button>Compact Grid</dees-button>
|
||||||
|
</dees-button-group>
|
||||||
|
|
||||||
|
<dees-button-group label="Mode:">
|
||||||
|
<dees-button>Toggle Edit Mode</dees-button>
|
||||||
|
</dees-button-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-container-wrapper">
|
||||||
|
<dees-dashboardgrid id="dashboardGrid"></dees-dashboardgrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
Drag widgets to reposition • Resize from edges and corners • Add widgets with auto-positioning
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-demowrapper>
|
||||||
|
`;
|
||||||
|
};
|
832
ts_web/elements/dees-dashboardgrid.ts
Normal file
832
ts_web/elements/dees-dashboardgrid.ts
Normal file
@ -0,0 +1,832 @@
|
|||||||
|
import * as plugins from './00plugins.js';
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
type TemplateResult,
|
||||||
|
property,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
state,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
|
import './dees-icon.js';
|
||||||
|
import { demoFunc } from './dees-dashboardgrid.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-dashboardgrid': DeesDashboardgrid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDashboardWidget {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
minW?: number;
|
||||||
|
minH?: number;
|
||||||
|
maxW?: number;
|
||||||
|
maxH?: number;
|
||||||
|
content: TemplateResult | string;
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
noMove?: boolean;
|
||||||
|
noResize?: boolean;
|
||||||
|
locked?: boolean;
|
||||||
|
autoPosition?: boolean; // Auto-position widget in first available space
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-dashboardgrid')
|
||||||
|
export class DeesDashboardgrid extends DeesElement {
|
||||||
|
// STATIC
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@property({ type: Array })
|
||||||
|
public widgets: IDashboardWidget[] = [];
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public cellHeight: number = 80;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public margin: number | { top?: number; right?: number; bottom?: number; left?: number } = 10;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public columns: number = 12;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public editable: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public enableAnimation: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public cellHeightUnit: 'px' | 'em' | 'rem' | 'auto' = 'px';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public rtl: boolean = false; // Right-to-left support
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public showGridLines: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private draggedWidget: IDashboardWidget | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private draggedElement: HTMLElement | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private dragOffsetX: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private dragOffsetY: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private dragMouseX: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private dragMouseY: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private placeholderPosition: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private resizingWidget: IDashboardWidget | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private resizeStartW: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private resizeStartH: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private resizeStartX: number = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private resizeStartY: number = 0;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget {
|
||||||
|
position: absolute;
|
||||||
|
will-change: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([enableanimation]) .grid-widget {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget.dragging {
|
||||||
|
z-index: 1000;
|
||||||
|
transition: none !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: grabbing;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget.placeholder {
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget.placeholder .widget-content {
|
||||||
|
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||||
|
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget.resizing {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-content {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: ${cssManager.bdTheme(
|
||||||
|
'0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
'0 1px 3px rgba(0, 0, 0, 0.3)'
|
||||||
|
)};
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget:hover .widget-content {
|
||||||
|
box-shadow: ${cssManager.bdTheme(
|
||||||
|
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
'0 4px 12px rgba(0, 0, 0, 0.4)'
|
||||||
|
)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget.dragging .widget-content {
|
||||||
|
box-shadow: ${cssManager.bdTheme(
|
||||||
|
'0 16px 48px rgba(0, 0, 0, 0.25)',
|
||||||
|
'0 16px 48px rgba(0, 0, 0, 0.6)'
|
||||||
|
)};
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
|
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header.locked {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header.locked:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header dees-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-body {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: auto;
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-body.has-header {
|
||||||
|
top: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize handles */
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-e {
|
||||||
|
cursor: ew-resize;
|
||||||
|
width: 12px;
|
||||||
|
right: -6px;
|
||||||
|
top: 10%;
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-s {
|
||||||
|
cursor: ns-resize;
|
||||||
|
height: 12px;
|
||||||
|
width: 80%;
|
||||||
|
bottom: -6px;
|
||||||
|
left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-se {
|
||||||
|
cursor: se-resize;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-se::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-right: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||||
|
border-bottom: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-widget:hover .resize-handle-se {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-se:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-se:hover::after {
|
||||||
|
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder */
|
||||||
|
.grid-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||||
|
opacity: 0.1;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 400px;
|
||||||
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state dees-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid lines */
|
||||||
|
.grid-lines {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-line-vertical {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-line-horizontal {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
if (this.widgets.length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
|
||||||
|
<div>No widgets configured</div>
|
||||||
|
<div style="font-size: 14px; margin-top: 8px;">Add widgets to populate the dashboard</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const maxY = Math.max(...this.widgets.map(w => w.y + w.h), 4);
|
||||||
|
const cellHeightValue = this.getCellHeight();
|
||||||
|
const gridHeight = maxY * cellHeightValue + (maxY + 1) * margins.vertical;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="grid-container" style="height: ${gridHeight}px;">
|
||||||
|
${this.showGridLines ? this.renderGridLines(gridHeight) : ''}
|
||||||
|
${this.widgets.map(widget => this.renderWidget(widget))}
|
||||||
|
${this.placeholderPosition && this.draggedWidget ? this.renderPlaceholder() : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGridLines(gridHeight: number): TemplateResult {
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const cellHeightValue = this.getCellHeight();
|
||||||
|
|
||||||
|
// Convert margin to percentage for consistent calculation
|
||||||
|
const containerWidth = this.getBoundingClientRect().width;
|
||||||
|
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
|
||||||
|
|
||||||
|
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||||
|
|
||||||
|
const verticalLines = [];
|
||||||
|
const horizontalLines = [];
|
||||||
|
|
||||||
|
// Vertical lines
|
||||||
|
for (let i = 0; i <= this.columns; i++) {
|
||||||
|
const left = i * cellWidth + i * marginHorizontalPercent;
|
||||||
|
verticalLines.push(html`
|
||||||
|
<div class="grid-line-vertical" style="left: ${left}%;"></div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal lines
|
||||||
|
const numHorizontalLines = Math.ceil(gridHeight / (cellHeightValue + margins.vertical));
|
||||||
|
for (let i = 0; i <= numHorizontalLines; i++) {
|
||||||
|
const top = i * cellHeightValue + i * margins.vertical;
|
||||||
|
horizontalLines.push(html`
|
||||||
|
<div class="grid-line-horizontal" style="top: ${top}px;"></div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="grid-lines">
|
||||||
|
${verticalLines}
|
||||||
|
${horizontalLines}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderWidget(widget: IDashboardWidget): TemplateResult {
|
||||||
|
const isDragging = this.draggedWidget?.id === widget.id;
|
||||||
|
const isResizing = this.resizingWidget?.id === widget.id;
|
||||||
|
const isLocked = widget.locked || !this.editable;
|
||||||
|
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const cellHeightValue = this.getCellHeight();
|
||||||
|
|
||||||
|
// Convert margin to percentage of container width for consistent calculation
|
||||||
|
const containerWidth = this.getBoundingClientRect().width;
|
||||||
|
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
|
||||||
|
|
||||||
|
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||||
|
|
||||||
|
const left = widget.x * cellWidth + (widget.x + 1) * marginHorizontalPercent;
|
||||||
|
const top = widget.y * cellHeightValue + (widget.y + 1) * margins.vertical;
|
||||||
|
const width = widget.w * cellWidth + (widget.w - 1) * marginHorizontalPercent;
|
||||||
|
const height = widget.h * cellHeightValue + (widget.h - 1) * margins.vertical;
|
||||||
|
|
||||||
|
// Apply transform when dragging for smooth movement
|
||||||
|
let transform = '';
|
||||||
|
if (isDragging && this.draggedElement) {
|
||||||
|
const containerRect = this.getBoundingClientRect();
|
||||||
|
const translateX = this.dragMouseX - containerRect.left - this.dragOffsetX - (left / 100 * containerRect.width);
|
||||||
|
const translateY = this.dragMouseY - containerRect.top - this.dragOffsetY - top;
|
||||||
|
transform = `transform: translate(${translateX}px, ${translateY}px);`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="grid-widget ${isDragging ? 'dragging' : ''} ${isResizing ? 'resizing' : ''}"
|
||||||
|
style="
|
||||||
|
${this.rtl ? 'right' : 'left'}: ${left}%;
|
||||||
|
top: ${top}px;
|
||||||
|
width: ${width}%;
|
||||||
|
height: ${height}px;
|
||||||
|
${transform}
|
||||||
|
"
|
||||||
|
data-widget-id="${widget.id}"
|
||||||
|
>
|
||||||
|
<div class="widget-content">
|
||||||
|
${widget.title ? html`
|
||||||
|
<div
|
||||||
|
class="widget-header ${isLocked ? 'locked' : ''}"
|
||||||
|
@mousedown=${!isLocked && !widget.noMove ? (e: MouseEvent) => this.startDrag(e, widget) : null}
|
||||||
|
>
|
||||||
|
${widget.icon ? html`<dees-icon .icon=${widget.icon}></dees-icon>` : ''}
|
||||||
|
${widget.title}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="widget-body ${widget.title ? 'has-header' : ''}">
|
||||||
|
${widget.content}
|
||||||
|
</div>
|
||||||
|
${!isLocked && !widget.noResize ? html`
|
||||||
|
<div class="resize-handle resize-handle-e" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 'e')}></div>
|
||||||
|
<div class="resize-handle resize-handle-s" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 's')}></div>
|
||||||
|
<div class="resize-handle resize-handle-se" @mousedown=${(e: MouseEvent) => this.startResize(e, widget, 'se')}></div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPlaceholder(): TemplateResult {
|
||||||
|
if (!this.placeholderPosition || !this.draggedWidget) return html``;
|
||||||
|
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const cellHeightValue = this.getCellHeight();
|
||||||
|
|
||||||
|
// Convert margin to percentage of container width for consistent calculation
|
||||||
|
const containerWidth = this.getBoundingClientRect().width;
|
||||||
|
const marginHorizontalPercent = (margins.horizontal / containerWidth) * 100;
|
||||||
|
|
||||||
|
const cellWidth = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||||
|
|
||||||
|
const left = this.placeholderPosition.x * cellWidth + (this.placeholderPosition.x + 1) * marginHorizontalPercent;
|
||||||
|
const top = this.placeholderPosition.y * cellHeightValue + (this.placeholderPosition.y + 1) * margins.vertical;
|
||||||
|
const width = this.draggedWidget.w * cellWidth + (this.draggedWidget.w - 1) * marginHorizontalPercent;
|
||||||
|
const height = this.draggedWidget.h * cellHeightValue + (this.draggedWidget.h - 1) * margins.vertical;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="grid-widget placeholder"
|
||||||
|
style="
|
||||||
|
${this.rtl ? 'right' : 'left'}: ${left}%;
|
||||||
|
top: ${top}px;
|
||||||
|
width: ${width}%;
|
||||||
|
height: ${height}px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="widget-content"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startDrag(e: MouseEvent, widget: IDashboardWidget) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.draggedWidget = widget;
|
||||||
|
this.draggedElement = (e.currentTarget as HTMLElement).closest('.grid-widget') as HTMLElement;
|
||||||
|
|
||||||
|
const rect = this.draggedElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.dragOffsetX = e.clientX - rect.left;
|
||||||
|
this.dragOffsetY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Initialize mouse position
|
||||||
|
this.dragMouseX = e.clientX;
|
||||||
|
this.dragMouseY = e.clientY;
|
||||||
|
|
||||||
|
// Initialize placeholder at current widget position
|
||||||
|
this.placeholderPosition = { x: widget.x, y: widget.y };
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', this.handleDrag);
|
||||||
|
document.addEventListener('mouseup', this.endDrag);
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDrag = (e: MouseEvent) => {
|
||||||
|
if (!this.draggedWidget || !this.draggedElement) return;
|
||||||
|
|
||||||
|
// Update mouse position for smooth dragging
|
||||||
|
this.dragMouseX = e.clientX;
|
||||||
|
this.dragMouseY = e.clientY;
|
||||||
|
|
||||||
|
const containerRect = this.getBoundingClientRect();
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const cellHeightValue = this.getCellHeight();
|
||||||
|
|
||||||
|
// Convert margin to percentage to match renderWidget calculations
|
||||||
|
const marginHorizontalPercent = (margins.horizontal / containerRect.width) * 100;
|
||||||
|
const cellWidthPercent = (100 - marginHorizontalPercent * (this.columns + 1)) / this.columns;
|
||||||
|
const cellWidthPixels = containerRect.width * cellWidthPercent / 100;
|
||||||
|
|
||||||
|
// Get mouse position relative to grid container
|
||||||
|
const mouseX = e.clientX - containerRect.left - this.dragOffsetX;
|
||||||
|
const mouseY = e.clientY - containerRect.top - this.dragOffsetY;
|
||||||
|
|
||||||
|
// Calculate which cell the mouse is over by finding the closest cell center
|
||||||
|
let gridX = 0;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
// Check distance to center of each possible column position
|
||||||
|
for (let i = 0; i < this.columns; i++) {
|
||||||
|
// Calculate position in pixels (matching renderWidget percentage formula)
|
||||||
|
const leftPercent = i * cellWidthPercent + (i + 1) * marginHorizontalPercent;
|
||||||
|
const cellLeftPixels = containerRect.width * leftPercent / 100;
|
||||||
|
const cellCenterPixels = cellLeftPixels + cellWidthPixels / 2;
|
||||||
|
const distance = Math.abs(mouseX - cellCenterPixels);
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
gridX = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Y: find closest row center
|
||||||
|
let gridY = 0;
|
||||||
|
minDistance = Infinity;
|
||||||
|
|
||||||
|
// Check reasonable number of rows
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const cellTop = i * cellHeightValue + (i + 1) * margins.vertical;
|
||||||
|
const cellCenter = cellTop + cellHeightValue / 2;
|
||||||
|
const distance = Math.abs(mouseY - cellCenter);
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
gridY = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop checking if we're too far away
|
||||||
|
if (cellTop > mouseY + cellHeightValue) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clampedX = Math.max(0, Math.min(gridX, this.columns - this.draggedWidget.w));
|
||||||
|
const clampedY = Math.max(0, gridY);
|
||||||
|
|
||||||
|
// Update placeholder position instead of widget position during drag
|
||||||
|
if (!this.placeholderPosition ||
|
||||||
|
clampedX !== this.placeholderPosition.x ||
|
||||||
|
clampedY !== this.placeholderPosition.y) {
|
||||||
|
const collision = this.checkCollision(this.draggedWidget, clampedX, clampedY);
|
||||||
|
if (!collision) {
|
||||||
|
this.placeholderPosition = { x: clampedX, y: clampedY };
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private endDrag = () => {
|
||||||
|
// Apply final position from placeholder
|
||||||
|
if (this.draggedWidget && this.placeholderPosition) {
|
||||||
|
this.draggedWidget.x = this.placeholderPosition.x;
|
||||||
|
this.draggedWidget.y = this.placeholderPosition.y;
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('widget-move', {
|
||||||
|
detail: { widget: this.draggedWidget },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear drag state
|
||||||
|
this.draggedWidget = null;
|
||||||
|
this.draggedElement = null;
|
||||||
|
this.placeholderPosition = null;
|
||||||
|
this.dragMouseX = 0;
|
||||||
|
this.dragMouseY = 0;
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', this.handleDrag);
|
||||||
|
document.removeEventListener('mouseup', this.endDrag);
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
private startResize(e: MouseEvent, widget: IDashboardWidget, handle: string) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.resizingWidget = widget;
|
||||||
|
this.resizeStartW = widget.w;
|
||||||
|
this.resizeStartH = widget.h;
|
||||||
|
this.resizeStartX = e.clientX;
|
||||||
|
this.resizeStartY = e.clientY;
|
||||||
|
|
||||||
|
const handleResize = (e: MouseEvent) => {
|
||||||
|
if (!this.resizingWidget) return;
|
||||||
|
|
||||||
|
const containerRect = this.getBoundingClientRect();
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const cellHeightValue = this.getCellHeight();
|
||||||
|
const cellWidth = (containerRect.width - margins.horizontal * (this.columns + 1)) / this.columns;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - this.resizeStartX;
|
||||||
|
const deltaY = e.clientY - this.resizeStartY;
|
||||||
|
|
||||||
|
if (handle.includes('e')) {
|
||||||
|
const newW = Math.round(this.resizeStartW + deltaX / (cellWidth + margins.horizontal));
|
||||||
|
const maxW = widget.maxW || (this.columns - this.resizingWidget.x);
|
||||||
|
this.resizingWidget.w = Math.max(widget.minW || 1, Math.min(newW, maxW));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.includes('s')) {
|
||||||
|
const newH = Math.round(this.resizeStartH + deltaY / (cellHeightValue + margins.vertical));
|
||||||
|
const maxH = widget.maxH || Infinity;
|
||||||
|
this.resizingWidget.h = Math.max(widget.minH || 1, Math.min(newH, maxH));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('widget-resize', {
|
||||||
|
detail: { widget: this.resizingWidget },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const endResize = () => {
|
||||||
|
this.resizingWidget = null;
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', endResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleResize);
|
||||||
|
document.addEventListener('mouseup', endResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public removeWidget(widgetId: string) {
|
||||||
|
this.widgets = this.widgets.filter(w => w.id !== widgetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateWidget(widgetId: string, updates: Partial<IDashboardWidget>) {
|
||||||
|
this.widgets = this.widgets.map(w =>
|
||||||
|
w.id === widgetId ? { ...w, ...updates } : w
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLayout(): Array<{ id: string; x: number; y: number; w: number; h: number }> {
|
||||||
|
return this.widgets.map(({ id, x, y, w, h }) => ({ id, x, y, w, h }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLayout(layout: Array<{ id: string; x: number; y: number; w: number; h: number }>) {
|
||||||
|
this.widgets = this.widgets.map(widget => {
|
||||||
|
const layoutItem = layout.find(l => l.id === widget.id);
|
||||||
|
return layoutItem ? { ...widget, ...layoutItem } : widget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public lockGrid() {
|
||||||
|
this.editable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unlockGrid() {
|
||||||
|
this.editable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMargins(): { horizontal: number; vertical: number; top: number; right: number; bottom: number; left: number } {
|
||||||
|
if (typeof this.margin === 'number') {
|
||||||
|
return {
|
||||||
|
horizontal: this.margin,
|
||||||
|
vertical: this.margin,
|
||||||
|
top: this.margin,
|
||||||
|
right: this.margin,
|
||||||
|
bottom: this.margin,
|
||||||
|
left: this.margin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const margins = {
|
||||||
|
top: this.margin.top ?? 10,
|
||||||
|
right: this.margin.right ?? 10,
|
||||||
|
bottom: this.margin.bottom ?? 10,
|
||||||
|
left: this.margin.left ?? 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...margins,
|
||||||
|
horizontal: (margins.left + margins.right) / 2,
|
||||||
|
vertical: (margins.top + margins.bottom) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCellHeight(): number {
|
||||||
|
if (this.cellHeightUnit === 'auto') {
|
||||||
|
// Calculate square cells based on container width
|
||||||
|
const containerWidth = this.getBoundingClientRect().width;
|
||||||
|
const margins = this.getMargins();
|
||||||
|
const cellWidth = (containerWidth - margins.horizontal * (this.columns + 1)) / this.columns;
|
||||||
|
return cellWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cellHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkCollision(widget: IDashboardWidget, newX: number, newY: number): boolean {
|
||||||
|
const widgets = this.widgets.filter(w => w.id !== widget.id);
|
||||||
|
|
||||||
|
for (const other of widgets) {
|
||||||
|
if (newX < other.x + other.w &&
|
||||||
|
newX + widget.w > other.x &&
|
||||||
|
newY < other.y + other.h &&
|
||||||
|
newY + widget.h > other.y) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addWidget(widget: IDashboardWidget, autoPosition = false) {
|
||||||
|
if (autoPosition || widget.autoPosition) {
|
||||||
|
// Find first available position
|
||||||
|
const position = this.findAvailablePosition(widget.w, widget.h);
|
||||||
|
widget.x = position.x;
|
||||||
|
widget.y = position.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.widgets = [...this.widgets, widget];
|
||||||
|
}
|
||||||
|
|
||||||
|
private findAvailablePosition(width: number, height: number): { x: number; y: number } {
|
||||||
|
// Try to find space starting from top-left
|
||||||
|
for (let y = 0; y < 100; y++) { // Reasonable limit
|
||||||
|
for (let x = 0; x <= this.columns - width; x++) {
|
||||||
|
const testWidget = { id: 'test', x, y, w: width, h: height, content: '' } as IDashboardWidget;
|
||||||
|
if (!this.checkCollision(testWidget, x, y)) {
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no space found, place at bottom
|
||||||
|
const maxY = Math.max(...this.widgets.map(w => w.y + w.h), 0);
|
||||||
|
return { x: 0, y: maxY };
|
||||||
|
}
|
||||||
|
|
||||||
|
public compact(direction: 'vertical' | 'horizontal' = 'vertical') {
|
||||||
|
const sortedWidgets = [...this.widgets].sort((a, b) => {
|
||||||
|
if (direction === 'vertical') {
|
||||||
|
if (a.y !== b.y) return a.y - b.y;
|
||||||
|
return a.x - b.x;
|
||||||
|
} else {
|
||||||
|
if (a.x !== b.x) return a.x - b.x;
|
||||||
|
return a.y - b.y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const widget of sortedWidgets) {
|
||||||
|
if (widget.locked || widget.noMove) continue;
|
||||||
|
|
||||||
|
if (direction === 'vertical') {
|
||||||
|
// Move up as far as possible
|
||||||
|
while (widget.y > 0 && !this.checkCollision(widget, widget.x, widget.y - 1)) {
|
||||||
|
widget.y--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move left as far as possible
|
||||||
|
while (widget.x > 0 && !this.checkCollision(widget, widget.x - 1, widget.y)) {
|
||||||
|
widget.x--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ export * from './dees-chips.js';
|
|||||||
export * from './dees-contextmenu.js';
|
export * from './dees-contextmenu.js';
|
||||||
export * from './dees-dataview-codebox.js';
|
export * from './dees-dataview-codebox.js';
|
||||||
export * from './dees-dataview-statusobject.js';
|
export * from './dees-dataview-statusobject.js';
|
||||||
|
export * from './dees-dashboardgrid.js';
|
||||||
export * from './dees-editor.js';
|
export * from './dees-editor.js';
|
||||||
export * from './dees-editor-markdown.js';
|
export * from './dees-editor-markdown.js';
|
||||||
export * from './dees-editor-markdownoutlet.js';
|
export * from './dees-editor-markdownoutlet.js';
|
||||||
|
Reference in New Issue
Block a user