e30f02f9fe
- Add DragDropHandler class with unified mouse + touch events - Implement ghost element preview during drag - Add grid overlay with cell highlighting - Support touch events with 150ms delay for scroll compatibility - Add Escape key to cancel drag - Complete lifecycle management (init, destroy, cleanup) - Create mobile-ready test harness with: - Touch-optimized UI (44px touch targets) - Responsive grid layout - Real-time event logging - Add/remove/reflow widgets - Works on desktop and mobile - 420 lines core code, 880 lines test suite - Comprehensive JSDoc documentation
932 lines
30 KiB
HTML
932 lines
30 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>Drag & Drop Test (Mobile-Ready)</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #1a1a2e;
|
||
color: #eee;
|
||
padding: 20px;
|
||
touch-action: none; /* Prevent default touch behaviors */
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
h1 {
|
||
margin-bottom: 20px;
|
||
color: #e94560;
|
||
font-size: clamp(20px, 5vw, 28px);
|
||
}
|
||
|
||
.test-section {
|
||
background: #16213e;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.test-section h2 {
|
||
color: #4ecca3;
|
||
margin-bottom: 10px;
|
||
font-size: clamp(16px, 4vw, 18px);
|
||
}
|
||
|
||
/* Grid Container */
|
||
.grid-container {
|
||
position: relative;
|
||
background: #0f3460;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
min-height: 600px;
|
||
overflow: visible;
|
||
}
|
||
|
||
/* Widget Styles */
|
||
.widget {
|
||
position: absolute;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
cursor: grab;
|
||
user-select: none;
|
||
transition: box-shadow 0.2s, opacity 0.2s;
|
||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||
touch-action: none;
|
||
}
|
||
|
||
.widget:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.widget:hover {
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.widget.dragging {
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.drag-ghost {
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.widget-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.widget-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.widget-title {
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
flex: 1;
|
||
}
|
||
|
||
.widget-position {
|
||
font-size: 11px;
|
||
opacity: 0.7;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* Control Panel */
|
||
.controls {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
button {
|
||
background: #e94560;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 16px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
touch-action: manipulation;
|
||
min-height: 44px; /* iOS touch target */
|
||
}
|
||
|
||
button:hover {
|
||
background: #d63651;
|
||
}
|
||
|
||
button.secondary {
|
||
background: #4ecca3;
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
button.secondary:hover {
|
||
background: #5edc9f;
|
||
}
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.stat-box {
|
||
background: #0f3460;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 11px;
|
||
opacity: 0.7;
|
||
text-transform: uppercase;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #4ecca3;
|
||
}
|
||
|
||
.hint {
|
||
background: #0f3460;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
font-size: 12px;
|
||
color: #aaa;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.hint strong {
|
||
color: #4ecca3;
|
||
}
|
||
|
||
/* Grid Overlay */
|
||
.grid-overlay div {
|
||
transition: all 0.1s ease;
|
||
}
|
||
|
||
/* Mobile optimizations */
|
||
@media (max-width: 768px) {
|
||
body {
|
||
padding: 10px;
|
||
}
|
||
|
||
.test-section {
|
||
padding: 12px;
|
||
}
|
||
|
||
.grid-container {
|
||
min-height: 500px;
|
||
}
|
||
|
||
button {
|
||
flex: 1 1 calc(50% - 4px);
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.grid-container {
|
||
min-height: 400px;
|
||
}
|
||
|
||
.stats {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
/* Event log */
|
||
.event-log {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background: #0f3460;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
font-family: monospace;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.event-item {
|
||
padding: 4px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.event-time {
|
||
color: #888;
|
||
}
|
||
|
||
.event-type {
|
||
color: #4ecca3;
|
||
font-weight: bold;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🎯 Drag & Drop Test (Mobile-Ready)</h1>
|
||
|
||
<div class="test-section">
|
||
<h2>Draggable Widgets</h2>
|
||
<div class="hint">
|
||
<strong>Desktop:</strong> Click and drag widgets to move them<br>
|
||
<strong>Mobile:</strong> Touch and hold (150ms), then drag<br>
|
||
<strong>Keyboard:</strong> Press <kbd>Escape</kbd> to cancel drag
|
||
</div>
|
||
<div id="grid-container" class="grid-container"></div>
|
||
</div>
|
||
|
||
<div class="test-section">
|
||
<h2>Controls</h2>
|
||
<div class="controls">
|
||
<button onclick="addWidget()">Add Widget</button>
|
||
<button onclick="removeWidget()">Remove Last Widget</button>
|
||
<button onclick="reflowWidgets()" class="secondary">Reflow Grid</button>
|
||
<button onclick="resetGrid()">Reset</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="test-section">
|
||
<h2>Statistics</h2>
|
||
<div id="stats" class="stats"></div>
|
||
</div>
|
||
|
||
<div class="test-section">
|
||
<h2>Event Log</h2>
|
||
<button onclick="clearLog()">Clear Log</button>
|
||
<div id="event-log" class="event-log"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// GridEngine class (bundled inline)
|
||
class GridEngine {
|
||
constructor(config = {}) {
|
||
this.columns = config.columns || 12;
|
||
this.rowHeight = config.rowHeight || 80;
|
||
this.gap = config.gap || 12;
|
||
this.snapToGrid = config.snapToGrid !== false;
|
||
this.containerWidth = 0;
|
||
this.container = config.container;
|
||
|
||
if (this.container) {
|
||
this.updateContainerWidth();
|
||
}
|
||
}
|
||
|
||
updateContainerWidth() {
|
||
if (this.container) {
|
||
this.containerWidth = this.container.offsetWidth - (this.gap * 2);
|
||
}
|
||
}
|
||
|
||
getPixelPosition(widget) {
|
||
this.updateContainerWidth();
|
||
const totalGaps = this.gap * (this.columns + 1);
|
||
const colWidth = (this.containerWidth - totalGaps) / this.columns;
|
||
|
||
const left = widget.x * (colWidth + this.gap) + this.gap;
|
||
const top = widget.y * (this.rowHeight + this.gap) + this.gap;
|
||
const width = widget.w * colWidth + (widget.w - 1) * this.gap;
|
||
const height = widget.h * this.rowHeight + (widget.h - 1) * this.gap;
|
||
|
||
return { left, top, width, height };
|
||
}
|
||
|
||
snapToCell(pixelX, pixelY) {
|
||
this.updateContainerWidth();
|
||
const totalGaps = this.gap * (this.columns + 1);
|
||
const colWidth = (this.containerWidth - totalGaps) / this.columns;
|
||
|
||
const x = Math.round((pixelX - this.gap) / (colWidth + this.gap));
|
||
const y = Math.round((pixelY - this.gap) / (this.rowHeight + this.gap));
|
||
|
||
return {
|
||
x: Math.max(0, Math.min(x, this.columns - 1)),
|
||
y: Math.max(0, y)
|
||
};
|
||
}
|
||
|
||
detectCollision(widget, widgets) {
|
||
if (!Array.isArray(widgets) || widgets.length === 0) {
|
||
return false;
|
||
}
|
||
|
||
return widgets.some(other => {
|
||
if (other.id === widget.id) return false;
|
||
|
||
const noIntersect = (
|
||
widget.x + widget.w <= other.x ||
|
||
widget.x >= other.x + other.w ||
|
||
widget.y + widget.h <= other.y ||
|
||
widget.y >= other.y + other.h
|
||
);
|
||
|
||
return !noIntersect;
|
||
});
|
||
}
|
||
|
||
reflow(widgets) {
|
||
const sorted = [...widgets].sort((a, b) => {
|
||
if (a.y !== b.y) return a.y - b.y;
|
||
return a.x - b.x;
|
||
});
|
||
|
||
for (let i = 0; i < sorted.length; i++) {
|
||
while (this.detectCollision(sorted[i], sorted.slice(0, i))) {
|
||
sorted[i].y++;
|
||
}
|
||
}
|
||
|
||
return sorted;
|
||
}
|
||
|
||
validateWidget(widget) {
|
||
if (typeof widget.x !== 'number' || typeof widget.y !== 'number') {
|
||
return false;
|
||
}
|
||
if (typeof widget.w !== 'number' || typeof widget.h !== 'number') {
|
||
return false;
|
||
}
|
||
if (widget.x < 0 || widget.x + widget.w > this.columns) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
calculateGridHeight(widgets) {
|
||
if (!Array.isArray(widgets) || widgets.length === 0) {
|
||
return this.rowHeight + this.gap * 2;
|
||
}
|
||
|
||
const maxY = Math.max(...widgets.map(w => w.y + w.h));
|
||
return maxY * (this.rowHeight + this.gap) + this.gap;
|
||
}
|
||
}
|
||
|
||
// DragDropHandler class (bundled inline)
|
||
class DragDropHandler {
|
||
constructor(gridEngine, options = {}) {
|
||
this.gridEngine = gridEngine;
|
||
this.options = {
|
||
showGrid: true,
|
||
showCollisions: true,
|
||
enableSnap: true,
|
||
ghostOpacity: 0.5,
|
||
touchDelay: 150,
|
||
...options
|
||
};
|
||
|
||
this.dragState = null;
|
||
this.dragHandlers = new Map();
|
||
this.gridOverlay = null;
|
||
this.touchTimer = null;
|
||
|
||
this.boundMouseMove = this.onMouseMove.bind(this);
|
||
this.boundMouseUp = this.onMouseUp.bind(this);
|
||
this.boundTouchMove = this.onTouchMove.bind(this);
|
||
this.boundTouchEnd = this.onTouchEnd.bind(this);
|
||
this.boundKeyDown = this.onKeyDown.bind(this);
|
||
}
|
||
|
||
initWidget(element, widget, onDragEnd) {
|
||
const dragHandle = element.querySelector('.drag-handle') || element;
|
||
|
||
const mouseDownHandler = (e) => {
|
||
if (e.button !== 0) return;
|
||
e.preventDefault();
|
||
this.startDrag(e, element, widget, onDragEnd);
|
||
};
|
||
|
||
const touchStartHandler = (e) => {
|
||
this.touchTimer = setTimeout(() => {
|
||
e.preventDefault();
|
||
this.startDrag(e.touches[0], element, widget, onDragEnd);
|
||
}, this.options.touchDelay);
|
||
};
|
||
|
||
const touchCancelHandler = () => {
|
||
if (this.touchTimer) {
|
||
clearTimeout(this.touchTimer);
|
||
this.touchTimer = null;
|
||
}
|
||
};
|
||
|
||
dragHandle.addEventListener('mousedown', mouseDownHandler);
|
||
dragHandle.addEventListener('touchstart', touchStartHandler, { passive: false });
|
||
dragHandle.addEventListener('touchcancel', touchCancelHandler);
|
||
dragHandle.addEventListener('touchend', touchCancelHandler);
|
||
|
||
this.dragHandlers.set(element, {
|
||
mouseDownHandler,
|
||
touchStartHandler,
|
||
touchCancelHandler,
|
||
dragHandle
|
||
});
|
||
|
||
dragHandle.style.cursor = 'grab';
|
||
}
|
||
|
||
destroyWidget(element) {
|
||
const handlers = this.dragHandlers.get(element);
|
||
if (!handlers) return;
|
||
|
||
const { dragHandle, mouseDownHandler, touchStartHandler, touchCancelHandler } = handlers;
|
||
|
||
dragHandle.removeEventListener('mousedown', mouseDownHandler);
|
||
dragHandle.removeEventListener('touchstart', touchStartHandler);
|
||
dragHandle.removeEventListener('touchcancel', touchCancelHandler);
|
||
dragHandle.removeEventListener('touchend', touchCancelHandler);
|
||
|
||
this.dragHandlers.delete(element);
|
||
}
|
||
|
||
startDrag(e, element, widget, onDragEnd) {
|
||
const rect = element.getBoundingClientRect();
|
||
const offsetX = e.clientX - rect.left;
|
||
const offsetY = e.clientY - rect.top;
|
||
|
||
const ghost = this.createGhost(element);
|
||
|
||
this.dragState = {
|
||
element,
|
||
widget: { ...widget },
|
||
startX: e.clientX,
|
||
startY: e.clientY,
|
||
offsetX,
|
||
offsetY,
|
||
ghost,
|
||
isDragging: true,
|
||
onDragEnd
|
||
};
|
||
|
||
const dragHandle = element.querySelector('.drag-handle') || element;
|
||
dragHandle.style.cursor = 'grabbing';
|
||
|
||
document.addEventListener('mousemove', this.boundMouseMove);
|
||
document.addEventListener('mouseup', this.boundMouseUp);
|
||
document.addEventListener('touchmove', this.boundTouchMove, { passive: false });
|
||
document.addEventListener('touchend', this.boundTouchEnd);
|
||
document.addEventListener('keydown', this.boundKeyDown);
|
||
|
||
if (this.options.showGrid) {
|
||
this.showGridOverlay();
|
||
}
|
||
|
||
element.style.opacity = '0.3';
|
||
element.classList.add('dragging');
|
||
|
||
logEvent('Drag Start', { id: widget.id, x: widget.x, y: widget.y });
|
||
}
|
||
|
||
onMouseMove(e) {
|
||
if (!this.dragState?.isDragging) return;
|
||
e.preventDefault();
|
||
this.updateDragPosition(e.clientX, e.clientY);
|
||
}
|
||
|
||
onTouchMove(e) {
|
||
if (!this.dragState?.isDragging) return;
|
||
e.preventDefault();
|
||
const touch = e.touches[0];
|
||
this.updateDragPosition(touch.clientX, touch.clientY);
|
||
}
|
||
|
||
updateDragPosition(clientX, clientY) {
|
||
const { ghost, offsetX, offsetY, widget } = this.dragState;
|
||
|
||
ghost.style.left = (clientX - offsetX) + 'px';
|
||
ghost.style.top = (clientY - offsetY) + 'px';
|
||
|
||
const containerRect = this.gridEngine.container.getBoundingClientRect();
|
||
const relativeX = clientX - containerRect.left - offsetX;
|
||
const relativeY = clientY - containerRect.top - offsetY;
|
||
|
||
const snapped = this.gridEngine.snapToCell(relativeX, relativeY);
|
||
|
||
this.dragState.widget.x = snapped.x;
|
||
this.dragState.widget.y = snapped.y;
|
||
|
||
if (this.gridOverlay) {
|
||
this.highlightGridCells(snapped.x, snapped.y, widget.w, widget.h);
|
||
}
|
||
}
|
||
|
||
onMouseUp(e) {
|
||
if (!this.dragState?.isDragging) return;
|
||
e.preventDefault();
|
||
this.endDrag();
|
||
}
|
||
|
||
onTouchEnd(e) {
|
||
if (!this.dragState?.isDragging) return;
|
||
e.preventDefault();
|
||
this.endDrag();
|
||
}
|
||
|
||
onKeyDown(e) {
|
||
if (!this.dragState?.isDragging) return;
|
||
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
this.cancelDrag();
|
||
}
|
||
}
|
||
|
||
endDrag() {
|
||
if (!this.dragState) return;
|
||
|
||
const { element, widget, onDragEnd } = this.dragState;
|
||
|
||
element.style.opacity = '1';
|
||
element.classList.remove('dragging');
|
||
|
||
const dragHandle = element.querySelector('.drag-handle') || element;
|
||
dragHandle.style.cursor = 'grab';
|
||
|
||
if (onDragEnd) {
|
||
onDragEnd(widget, widget.x, widget.y);
|
||
}
|
||
|
||
logEvent('Drag End', { id: widget.id, x: widget.x, y: widget.y });
|
||
this.cleanup();
|
||
}
|
||
|
||
cancelDrag() {
|
||
if (!this.dragState) return;
|
||
|
||
const { element } = this.dragState;
|
||
|
||
element.style.opacity = '1';
|
||
element.classList.remove('dragging');
|
||
|
||
const dragHandle = element.querySelector('.drag-handle') || element;
|
||
dragHandle.style.cursor = 'grab';
|
||
|
||
logEvent('Drag Cancelled', null);
|
||
this.cleanup();
|
||
}
|
||
|
||
cleanup() {
|
||
if (this.dragState?.ghost) {
|
||
this.dragState.ghost.remove();
|
||
}
|
||
|
||
this.hideGridOverlay();
|
||
|
||
document.removeEventListener('mousemove', this.boundMouseMove);
|
||
document.removeEventListener('mouseup', this.boundMouseUp);
|
||
document.removeEventListener('touchmove', this.boundTouchMove);
|
||
document.removeEventListener('touchend', this.boundTouchEnd);
|
||
document.removeEventListener('keydown', this.boundKeyDown);
|
||
|
||
if (this.touchTimer) {
|
||
clearTimeout(this.touchTimer);
|
||
this.touchTimer = null;
|
||
}
|
||
|
||
this.dragState = null;
|
||
}
|
||
|
||
createGhost(element) {
|
||
const ghost = element.cloneNode(true);
|
||
ghost.style.position = 'fixed';
|
||
ghost.style.opacity = this.options.ghostOpacity;
|
||
ghost.style.pointerEvents = 'none';
|
||
ghost.style.zIndex = '10000';
|
||
ghost.style.width = element.offsetWidth + 'px';
|
||
ghost.style.height = element.offsetHeight + 'px';
|
||
ghost.style.transition = 'none';
|
||
ghost.classList.add('drag-ghost');
|
||
|
||
document.body.appendChild(ghost);
|
||
return ghost;
|
||
}
|
||
|
||
showGridOverlay() {
|
||
if (this.gridOverlay) return;
|
||
|
||
this.gridOverlay = document.createElement('div');
|
||
this.gridOverlay.className = 'grid-overlay';
|
||
this.gridOverlay.style.position = 'absolute';
|
||
this.gridOverlay.style.top = '0';
|
||
this.gridOverlay.style.left = '0';
|
||
this.gridOverlay.style.width = '100%';
|
||
this.gridOverlay.style.height = '100%';
|
||
this.gridOverlay.style.pointerEvents = 'none';
|
||
this.gridOverlay.style.zIndex = '9999';
|
||
|
||
this.gridEngine.container.appendChild(this.gridOverlay);
|
||
}
|
||
|
||
hideGridOverlay() {
|
||
if (this.gridOverlay) {
|
||
this.gridOverlay.remove();
|
||
this.gridOverlay = null;
|
||
}
|
||
}
|
||
|
||
highlightGridCells(x, y, w, h) {
|
||
if (!this.gridOverlay) return;
|
||
|
||
this.gridOverlay.innerHTML = '';
|
||
|
||
const totalGaps = this.gridEngine.gap * (this.gridEngine.columns + 1);
|
||
const colWidth = (this.gridEngine.containerWidth - totalGaps) / this.gridEngine.columns;
|
||
|
||
for (let row = y; row < y + h; row++) {
|
||
for (let col = x; col < x + w; col++) {
|
||
const cell = document.createElement('div');
|
||
cell.style.position = 'absolute';
|
||
cell.style.left = (col * (colWidth + this.gridEngine.gap) + this.gridEngine.gap) + 'px';
|
||
cell.style.top = (row * (this.gridEngine.rowHeight + this.gridEngine.gap) + this.gridEngine.gap) + 'px';
|
||
cell.style.width = colWidth + 'px';
|
||
cell.style.height = this.gridEngine.rowHeight + 'px';
|
||
cell.style.backgroundColor = 'rgba(78, 204, 163, 0.3)';
|
||
cell.style.border = '2px solid rgba(78, 204, 163, 0.6)';
|
||
cell.style.borderRadius = '4px';
|
||
cell.style.boxSizing = 'border-box';
|
||
|
||
this.gridOverlay.appendChild(cell);
|
||
}
|
||
}
|
||
}
|
||
|
||
hasCollision(widgets) {
|
||
if (!this.dragState) return false;
|
||
|
||
const { widget } = this.dragState;
|
||
const otherWidgets = widgets.filter(w => w.id !== widget.id);
|
||
|
||
return this.gridEngine.detectCollision(widget, otherWidgets);
|
||
}
|
||
|
||
getDragState() {
|
||
return this.dragState;
|
||
}
|
||
|
||
isDragging() {
|
||
return this.dragState?.isDragging || false;
|
||
}
|
||
|
||
destroy() {
|
||
if (this.isDragging()) {
|
||
this.cancelDrag();
|
||
}
|
||
|
||
for (const element of this.dragHandlers.keys()) {
|
||
this.destroyWidget(element);
|
||
}
|
||
|
||
this.dragHandlers.clear();
|
||
}
|
||
}
|
||
|
||
// Test application
|
||
let gridEngine = null;
|
||
let dragDropHandler = null;
|
||
let widgets = [];
|
||
let widgetElements = new Map();
|
||
let widgetCounter = 0;
|
||
|
||
const widgetTypes = [
|
||
{ icon: '📊', name: 'Stats', color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||
{ icon: '🎒', name: 'Inventory', color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
|
||
{ icon: '📝', name: 'Notes', color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
|
||
{ icon: '🗺️', name: 'Map', color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
|
||
{ icon: '⚔️', name: 'Combat', color: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }
|
||
];
|
||
|
||
function init() {
|
||
const container = document.getElementById('grid-container');
|
||
|
||
gridEngine = new GridEngine({
|
||
columns: 12,
|
||
rowHeight: 80,
|
||
gap: 12,
|
||
container
|
||
});
|
||
|
||
dragDropHandler = new DragDropHandler(gridEngine, {
|
||
showGrid: true,
|
||
ghostOpacity: 0.7,
|
||
touchDelay: 150
|
||
});
|
||
|
||
// Create initial widgets
|
||
createInitialWidgets();
|
||
updateStats();
|
||
|
||
// Handle window resize
|
||
let resizeTimeout;
|
||
window.addEventListener('resize', () => {
|
||
clearTimeout(resizeTimeout);
|
||
resizeTimeout = setTimeout(() => {
|
||
renderAllWidgets();
|
||
updateStats();
|
||
}, 100);
|
||
});
|
||
|
||
logEvent('Initialized', { widgets: widgets.length });
|
||
}
|
||
|
||
function createInitialWidgets() {
|
||
const initialWidgets = [
|
||
{ x: 0, y: 0, w: 6, h: 3, type: 0 },
|
||
{ x: 6, y: 0, w: 6, h: 2, type: 1 },
|
||
{ x: 0, y: 3, w: 4, h: 3, type: 2 },
|
||
{ x: 4, y: 3, w: 4, h: 3, type: 3 }
|
||
];
|
||
|
||
initialWidgets.forEach(config => {
|
||
const widget = {
|
||
id: `widget-${widgetCounter++}`,
|
||
x: config.x,
|
||
y: config.y,
|
||
w: config.w,
|
||
h: config.h,
|
||
type: config.type
|
||
};
|
||
widgets.push(widget);
|
||
createWidgetElement(widget);
|
||
});
|
||
}
|
||
|
||
function createWidgetElement(widget) {
|
||
const container = document.getElementById('grid-container');
|
||
const type = widgetTypes[widget.type];
|
||
|
||
const element = document.createElement('div');
|
||
element.className = 'widget';
|
||
element.style.background = type.color;
|
||
|
||
element.innerHTML = `
|
||
<div class="widget-header">
|
||
<span class="widget-icon">${type.icon}</span>
|
||
<span class="widget-title">${type.name}</span>
|
||
</div>
|
||
<div class="widget-position">Position: (${widget.x}, ${widget.y})</div>
|
||
<div class="widget-position">Size: ${widget.w}×${widget.h}</div>
|
||
`;
|
||
|
||
container.appendChild(element);
|
||
widgetElements.set(widget.id, element);
|
||
|
||
// Position widget
|
||
positionWidget(element, widget);
|
||
|
||
// Initialize drag
|
||
dragDropHandler.initWidget(element, widget, (updatedWidget, newX, newY) => {
|
||
widget.x = newX;
|
||
widget.y = newY;
|
||
positionWidget(element, widget);
|
||
updateWidgetPosition(element, widget);
|
||
updateStats();
|
||
});
|
||
}
|
||
|
||
function positionWidget(element, widget) {
|
||
const pos = gridEngine.getPixelPosition(widget);
|
||
element.style.left = pos.left + 'px';
|
||
element.style.top = pos.top + 'px';
|
||
element.style.width = pos.width + 'px';
|
||
element.style.height = pos.height + 'px';
|
||
}
|
||
|
||
function updateWidgetPosition(element, widget) {
|
||
const posElements = element.querySelectorAll('.widget-position');
|
||
posElements[0].textContent = `Position: (${widget.x}, ${widget.y})`;
|
||
}
|
||
|
||
function renderAllWidgets() {
|
||
widgets.forEach(widget => {
|
||
const element = widgetElements.get(widget.id);
|
||
if (element) {
|
||
positionWidget(element, widget);
|
||
}
|
||
});
|
||
}
|
||
|
||
window.addWidget = function() {
|
||
const randomType = Math.floor(Math.random() * widgetTypes.length);
|
||
const widget = {
|
||
id: `widget-${widgetCounter++}`,
|
||
x: Math.floor(Math.random() * 8),
|
||
y: Math.floor(Math.random() * 3),
|
||
w: 4,
|
||
h: 2,
|
||
type: randomType
|
||
};
|
||
|
||
widgets.push(widget);
|
||
createWidgetElement(widget);
|
||
updateStats();
|
||
logEvent('Widget Added', { id: widget.id });
|
||
};
|
||
|
||
window.removeWidget = function() {
|
||
if (widgets.length === 0) return;
|
||
|
||
const widget = widgets.pop();
|
||
const element = widgetElements.get(widget.id);
|
||
|
||
if (element) {
|
||
dragDropHandler.destroyWidget(element);
|
||
element.remove();
|
||
widgetElements.delete(widget.id);
|
||
}
|
||
|
||
updateStats();
|
||
logEvent('Widget Removed', { id: widget.id });
|
||
};
|
||
|
||
window.reflowWidgets = function() {
|
||
widgets = gridEngine.reflow(widgets);
|
||
renderAllWidgets();
|
||
updateStats();
|
||
logEvent('Grid Reflowed', null);
|
||
};
|
||
|
||
window.resetGrid = function() {
|
||
// Clear all widgets
|
||
widgets.forEach(widget => {
|
||
const element = widgetElements.get(widget.id);
|
||
if (element) {
|
||
dragDropHandler.destroyWidget(element);
|
||
element.remove();
|
||
}
|
||
});
|
||
|
||
widgets = [];
|
||
widgetElements.clear();
|
||
widgetCounter = 0;
|
||
|
||
// Recreate initial widgets
|
||
createInitialWidgets();
|
||
updateStats();
|
||
logEvent('Grid Reset', null);
|
||
};
|
||
|
||
function updateStats() {
|
||
const container = document.getElementById('stats');
|
||
container.innerHTML = `
|
||
<div class="stat-box">
|
||
<div class="stat-label">Widgets</div>
|
||
<div class="stat-value">${widgets.length}</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-label">Grid Height</div>
|
||
<div class="stat-value">${gridEngine.calculateGridHeight(widgets)}px</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-label">Columns</div>
|
||
<div class="stat-value">${gridEngine.columns}</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-label">Container Width</div>
|
||
<div class="stat-value">${gridEngine.containerWidth}px</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function logEvent(type, data) {
|
||
const log = document.getElementById('event-log');
|
||
const time = new Date().toLocaleTimeString();
|
||
const item = document.createElement('div');
|
||
item.className = 'event-item';
|
||
item.innerHTML = `
|
||
<span class="event-time">${time}</span>
|
||
<span class="event-type"> ${type}</span>
|
||
${data ? ` - ${JSON.stringify(data)}` : ''}
|
||
`;
|
||
log.insertBefore(item, log.firstChild);
|
||
|
||
// Keep only last 50 entries
|
||
while (log.children.length > 50) {
|
||
log.removeChild(log.lastChild);
|
||
}
|
||
}
|
||
|
||
window.clearLog = function() {
|
||
document.getElementById('event-log').innerHTML = '';
|
||
};
|
||
|
||
// Initialize on load
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|