Files
rpg-companion-sillytavern/src/systems/dashboard/editMode.standalone.test.html
T
Lucas 'Paperboy' Rose-Winters c8c19ce956 fix(dashboard): make resize handles always visible in edit mode
- Change resize handles from hover-only to always visible in edit mode
- Handles now show at 60% opacity in edit mode
- Brighten to 100% opacity on hover for visual feedback
- Update UI hint to explicitly mention green dots on corners/edges
- Makes resize functionality more discoverable
- Improves UX by showing affordances clearly
2025-10-23 10:23:00 +11:00

1026 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>Edit Mode Test - Complete Dashboard System</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;
overflow-x: hidden;
}
h1 {
margin-bottom: 20px;
color: #e94560;
font-size: clamp(20px, 5vw, 28px);
}
.dashboard-container {
background: #16213e;
border-radius: 12px;
padding: 20px;
position: relative;
min-height: 600px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #0f3460;
}
.dashboard-title {
font-size: 24px;
font-weight: bold;
color: #4ecca3;
}
.edit-mode-toggle {
background: #4ecca3;
color: #1a1a2e;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.edit-mode-toggle:hover {
background: #5edc9f;
transform: translateY(-2px);
}
.edit-mode-toggle.active {
background: #e94560;
color: white;
}
.grid-container {
position: relative;
background: #0f3460;
border-radius: 8px;
padding: 12px;
min-height: 500px;
}
.grid-container.edit-mode {
border: 2px dashed rgba(78, 204, 163, 0.5);
}
.widget {
position: absolute;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
padding: 12px;
user-select: none;
transition: box-shadow 0.2s, border-color 0.2s;
border: 2px solid rgba(255, 255, 255, 0.1);
touch-action: none;
cursor: grab;
}
.edit-mode .widget {
cursor: grab;
}
.widget:active {
cursor: grabbing;
}
.widget:hover {
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.widget.dragging {
opacity: 0.3;
}
.widget.resizing {
box-shadow: 0 8px 24px rgba(78, 204, 163, 0.6);
border-color: rgba(78, 204, 163, 0.8);
}
.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-info {
font-size: 11px;
opacity: 0.7;
margin-top: 4px;
}
/* Resize handles - visible in edit mode */
.resize-handles {
opacity: 0;
transition: opacity 0.2s;
}
/* Always show handles in edit mode, brighter on hover */
.edit-mode .resize-handles {
opacity: 0.6;
}
.edit-mode .widget:hover .resize-handles,
.widget.resizing .resize-handles {
opacity: 1;
}
.resize-handle {
transition: background 0.2s, transform 0.2s;
}
.resize-handle:hover {
background: rgba(78, 204, 163, 1) !important;
transform: scale(1.3) !important;
}
/* Widget edit controls - hidden by default */
.widget-edit-controls {
opacity: 0;
transition: opacity 0.2s;
}
.edit-mode .widget:hover .widget-edit-controls {
opacity: 1;
}
.hint {
background: #0f3460;
padding: 12px;
border-radius: 5px;
font-size: 12px;
color: #aaa;
line-height: 1.6;
margin-bottom: 15px;
}
.hint strong {
color: #4ecca3;
}
.hint kbd {
background: #1a1a2e;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #4ecca3;
color: #4ecca3;
font-family: monospace;
font-size: 11px;
}
.status-bar {
display: flex;
gap: 15px;
align-items: center;
padding: 12px;
background: #0f3460;
border-radius: 6px;
margin-top: 15px;
font-size: 13px;
}
.status-item {
display: flex;
align-items: center;
gap: 6px;
}
.status-label {
opacity: 0.7;
}
.status-value {
color: #4ecca3;
font-weight: bold;
}
.mode-indicator {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.mode-indicator.view {
background: rgba(78, 204, 163, 0.2);
color: #4ecca3;
}
.mode-indicator.edit {
background: rgba(233, 69, 96, 0.2);
color: #e94560;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.dashboard-container {
padding: 15px;
}
.grid-container {
min-height: 400px;
}
}
</style>
</head>
<body>
<h1>✏️ Edit Mode Test - Complete Dashboard System</h1>
<div class="hint">
<strong>Features:</strong><br>
• Click "Edit Layout" to enter edit mode<br>
• In edit mode: <strong>green dots appear on widget corners/edges</strong> - drag them to resize<br>
• Click widget body to drag and reposition<br>
• Click widgets in the library (left side) to add them<br>
• Hover over widgets to see edit controls (settings ⚙ and delete ×)<br>
• Click "Save" to commit changes or "Cancel" to discard<br>
• Press <kbd>Escape</kbd> while dragging/resizing to cancel
</div>
<div class="dashboard-container">
<div class="dashboard-header">
<div class="dashboard-title">🎮 RPG Dashboard</div>
<button id="edit-toggle" class="edit-mode-toggle">Edit Layout</button>
</div>
<div id="grid-container" class="grid-container"></div>
<div class="status-bar">
<div class="status-item">
<span class="status-label">Mode:</span>
<span id="mode-indicator" class="mode-indicator view">VIEW</span>
</div>
<div class="status-item">
<span class="status-label">Widgets:</span>
<span id="widget-count" class="status-value">0</span>
</div>
<div class="status-item">
<span class="status-label">Grid Units:</span>
<span id="total-units" class="status-value">0</span>
</div>
</div>
</div>
<script>
// ========== BUNDLED CLASSES ==========
// GridEngine
class GridEngine {
constructor(config = {}) {
this.columns = config.columns || 12;
this.rowHeight = config.rowHeight || 80;
this.gap = config.gap || 12;
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)
};
}
}
// DragDropHandler
class DragDropHandler {
constructor(gridEngine, options = {}) {
this.gridEngine = gridEngine;
this.options = { ghostOpacity: 0.5, touchDelay: 150, ...options };
this.dragState = null;
this.dragHandlers = new Map();
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;
const mouseDownHandler = (e) => {
if (e.button !== 0) return;
// Don't drag if clicking on resize handle or widget controls
if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) {
return;
}
e.preventDefault();
this.startDrag(e, element, widget, onDragEnd);
};
const touchStartHandler = (e) => {
// Don't drag if touching resize handle or widget controls
if (e.target.closest('.resize-handle') || e.target.closest('.widget-edit-controls')) {
return;
}
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 });
}
startDrag(e, element, widget, onDragEnd) {
const rect = element.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
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';
document.body.appendChild(ghost);
this.dragState = { element, widget: { ...widget }, startX: e.clientX, startY: e.clientY, offsetX, offsetY, ghost, isDragging: true, onDragEnd };
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);
element.style.opacity = '0.3';
element.classList.add('dragging');
}
onMouseMove(e) {
if (!this.dragState?.isDragging) return;
e.preventDefault();
this.updateDragPosition(e.clientX, e.clientY);
}
onTouchMove(e) {
if (!this.dragState?.isDragging) return;
e.preventDefault();
this.updateDragPosition(e.touches[0].clientX, e.touches[0].clientY);
}
updateDragPosition(clientX, clientY) {
const { ghost, offsetX, offsetY } = 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;
}
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');
if (onDragEnd) onDragEnd(widget, widget.x, widget.y);
this.cleanup();
}
cancelDrag() {
if (!this.dragState) return;
const { element } = this.dragState;
element.style.opacity = '1';
element.classList.remove('dragging');
this.cleanup();
}
cleanup() {
if (this.dragState?.ghost) this.dragState.ghost.remove();
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;
}
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);
}
}
// ResizeHandler (functional version)
class ResizeHandler {
constructor(gridEngine, options = {}) {
this.gridEngine = gridEngine;
this.options = { minWidth: 2, minHeight: 2, maxWidth: 12, maxHeight: 10, ...options };
this.resizeHandlers = new Map();
this.resizeState = 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);
}
initWidget(element, widget, onResizeEnd, constraints = {}) {
const handles = document.createElement('div');
handles.className = 'resize-handles';
handles.style.position = 'absolute';
handles.style.inset = '0';
handles.style.pointerEvents = 'none';
const widgetConstraints = {
minW: constraints.minW || this.options.minWidth,
minH: constraints.minH || this.options.minHeight,
maxW: constraints.maxW || this.options.maxWidth,
maxH: constraints.maxH || this.options.maxHeight
};
const handleTypes = { nw: 'nwse-resize', ne: 'nesw-resize', se: 'nwse-resize', sw: 'nesw-resize', n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize' };
const handleListeners = [];
Object.entries(handleTypes).forEach(([type, cursor]) => {
const handle = document.createElement('div');
handle.className = `resize-handle resize-handle-${type}`;
handle.dataset.handle = type;
handle.style.position = 'absolute';
handle.style.width = '12px';
handle.style.height = '12px';
handle.style.background = 'rgba(78, 204, 163, 0.8)';
handle.style.border = '2px solid white';
handle.style.borderRadius = '3px';
handle.style.pointerEvents = 'auto';
handle.style.cursor = cursor;
handle.style.zIndex = '101';
if (type.includes('n')) handle.style.top = '-6px';
if (type.includes('s')) handle.style.bottom = '-6px';
if (type.includes('w')) handle.style.left = '-6px';
if (type.includes('e')) handle.style.right = '-6px';
if (type === 'n' || type === 's') {
handle.style.left = '50%';
handle.style.transform = 'translateX(-50%)';
}
if (type === 'w' || type === 'e') {
handle.style.top = '50%';
handle.style.transform = 'translateY(-50%)';
}
const mouseDownHandler = (e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
this.startResize(e, type, element, widget, onResizeEnd, widgetConstraints);
};
const touchStartHandler = (e) => {
e.preventDefault();
e.stopPropagation();
this.startResize(e.touches[0], type, element, widget, onResizeEnd, widgetConstraints);
};
handle.addEventListener('mousedown', mouseDownHandler);
handle.addEventListener('touchstart', touchStartHandler, { passive: false });
handleListeners.push({ element: handle, mouseDownHandler, touchStartHandler });
handles.appendChild(handle);
});
element.appendChild(handles);
this.resizeHandlers.set(element, { handles, handleListeners });
}
startResize(e, handleType, element, widget, onResizeEnd, constraints) {
this.resizeState = {
element,
widget: { ...widget },
handle: handleType,
startX: e.clientX,
startY: e.clientY,
startWidth: widget.w,
startHeight: widget.h,
startGridX: widget.x,
startGridY: widget.y,
onResizeEnd,
constraints
};
document.addEventListener('mousemove', this.boundMouseMove);
document.addEventListener('mouseup', this.boundMouseUp);
document.addEventListener('touchmove', this.boundTouchMove, { passive: false });
document.addEventListener('touchend', this.boundTouchEnd);
element.classList.add('resizing');
}
onMouseMove(e) {
if (!this.resizeState) return;
e.preventDefault();
this.updateResizeSize(e.clientX, e.clientY);
}
onTouchMove(e) {
if (!this.resizeState) return;
e.preventDefault();
this.updateResizeSize(e.touches[0].clientX, e.touches[0].clientY);
}
updateResizeSize(clientX, clientY) {
const { widget, handle, startX, startY, startWidth, startHeight, startGridX, startGridY, constraints, element } = this.resizeState;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
this.gridEngine.updateContainerWidth();
const totalGaps = this.gridEngine.gap * (this.gridEngine.columns + 1);
const colWidth = (this.gridEngine.containerWidth - totalGaps) / this.gridEngine.columns;
const rowHeight = this.gridEngine.rowHeight;
const deltaGridX = Math.round(deltaX / (colWidth + this.gridEngine.gap));
const deltaGridY = Math.round(deltaY / (rowHeight + this.gridEngine.gap));
let newW = startWidth, newH = startHeight, newX = startGridX, newY = startGridY;
if (handle.includes('e')) newW = startWidth + deltaGridX;
else if (handle.includes('w')) { newW = startWidth - deltaGridX; newX = startGridX + deltaGridX; }
if (handle.includes('s')) newH = startHeight + deltaGridY;
else if (handle.includes('n')) { newH = startHeight - deltaGridY; newY = startGridY + deltaGridY; }
newW = Math.max(constraints.minW, Math.min(newW, constraints.maxW));
newH = Math.max(constraints.minH, Math.min(newH, constraints.maxH));
newW = Math.min(newW, this.gridEngine.columns - newX);
if (handle.includes('w') && newW === constraints.minW) newX = startGridX + startWidth - constraints.minW;
if (handle.includes('n') && newH === constraints.minH) newY = startGridY + startHeight - constraints.minH;
this.resizeState.widget.w = newW;
this.resizeState.widget.h = newH;
this.resizeState.widget.x = newX;
this.resizeState.widget.y = newY;
const pos = this.gridEngine.getPixelPosition(this.resizeState.widget);
element.style.width = pos.width + 'px';
element.style.height = pos.height + 'px';
element.style.left = pos.left + 'px';
element.style.top = pos.top + 'px';
}
onMouseUp(e) {
if (!this.resizeState) return;
e.preventDefault();
this.endResize();
}
onTouchEnd(e) {
if (!this.resizeState) return;
e.preventDefault();
this.endResize();
}
endResize() {
if (!this.resizeState) return;
const { element, widget, onResizeEnd } = this.resizeState;
element.classList.remove('resizing');
if (onResizeEnd) onResizeEnd(widget, widget.w, widget.h, widget.x, widget.y);
this.cleanup();
}
cleanup() {
document.removeEventListener('mousemove', this.boundMouseMove);
document.removeEventListener('mouseup', this.boundMouseUp);
document.removeEventListener('touchmove', this.boundTouchMove);
document.removeEventListener('touchend', this.boundTouchEnd);
this.resizeState = null;
}
destroyWidget(element) {
const data = this.resizeHandlers.get(element);
if (!data) return;
const { handles, handleListeners } = data;
handleListeners.forEach(({ element: h, mouseDownHandler, touchStartHandler }) => {
h.removeEventListener('mousedown', mouseDownHandler);
h.removeEventListener('touchstart', touchStartHandler);
});
handles.remove();
this.resizeHandlers.delete(element);
}
}
// EditModeManager (simplified for demo)
class EditModeManager {
constructor(config) {
this.container = config.container;
this.onSave = config.onSave;
this.onCancel = config.onCancel;
this.onWidgetAdd = config.onWidgetAdd;
this.onWidgetDelete = config.onWidgetDelete;
this.isEditMode = false;
this.editControls = null;
this.widgetLibrary = null;
this.widgetControlsMap = new Map();
}
enterEditMode() {
if (this.isEditMode) return;
this.isEditMode = true;
this.createEditControls();
this.showWidgetLibrary();
this.container.classList.add('edit-mode');
console.log('Entered edit mode');
}
exitEditMode(save = false) {
if (!this.isEditMode) return;
if (save) {
if (this.onSave) this.onSave();
} else {
if (this.onCancel) this.onCancel();
}
this.isEditMode = false;
this.removeEditControls();
this.hideWidgetLibrary();
this.container.classList.remove('edit-mode');
}
toggleEditMode() {
if (this.isEditMode) {
if (confirm('Exit edit mode? Unsaved changes will be lost.')) {
this.exitEditMode(false);
}
} else {
this.enterEditMode();
}
}
createEditControls() {
this.editControls = document.createElement('div');
this.editControls.style.position = 'absolute';
this.editControls.style.top = '10px';
this.editControls.style.right = '10px';
this.editControls.style.display = 'flex';
this.editControls.style.gap = '8px';
this.editControls.style.zIndex = '10000';
const saveBtn = document.createElement('button');
saveBtn.textContent = '💾 Save';
saveBtn.style.cssText = 'background:#4ecca3;color:#1a1a2e;border:none;padding:10px 20px;border-radius:6px;cursor:pointer;font-weight:bold;';
saveBtn.onclick = () => this.exitEditMode(true);
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '✖ Cancel';
cancelBtn.style.cssText = 'background:#e94560;color:white;border:none;padding:10px 20px;border-radius:6px;cursor:pointer;font-weight:bold;';
cancelBtn.onclick = () => this.exitEditMode(false);
this.editControls.appendChild(saveBtn);
this.editControls.appendChild(cancelBtn);
this.container.appendChild(this.editControls);
}
removeEditControls() {
if (this.editControls) {
this.editControls.remove();
this.editControls = null;
}
}
showWidgetLibrary() {
this.widgetLibrary = document.createElement('div');
this.widgetLibrary.style.cssText = 'position:fixed;left:20px;top:50%;transform:translateY(-50%);background:#16213e;border-radius:8px;padding:15px;box-shadow:0 4px 12px rgba(0,0,0,0.3);z-index:10001;max-width:200px;';
const title = document.createElement('div');
title.textContent = 'Widget Library';
title.style.cssText = 'font-size:14px;font-weight:bold;margin-bottom:10px;color:#4ecca3;';
this.widgetLibrary.appendChild(title);
const widgetTypes = [
{ type: 'stats', icon: '📊', name: 'Stats' },
{ type: 'inventory', icon: '🎒', name: 'Inventory' },
{ type: 'notes', icon: '📝', name: 'Notes' },
{ type: 'map', icon: '🗺️', name: 'Map' }
];
widgetTypes.forEach(w => {
const item = document.createElement('div');
item.style.cssText = 'display:flex;align-items:center;gap:8px;padding:10px;margin-bottom:8px;background:#0f3460;border-radius:6px;cursor:pointer;transition:all 0.2s;';
item.innerHTML = `<span style="font-size:20px;">${w.icon}</span><span style="font-size:12px;">${w.name}</span>`;
item.onclick = () => {
if (this.onWidgetAdd) this.onWidgetAdd(w.type);
};
item.onmouseenter = () => item.style.background = '#1a3a5a';
item.onmouseleave = () => item.style.background = '#0f3460';
this.widgetLibrary.appendChild(item);
});
document.body.appendChild(this.widgetLibrary);
}
hideWidgetLibrary() {
if (this.widgetLibrary) {
this.widgetLibrary.remove();
this.widgetLibrary = null;
}
}
addWidgetControls(element, widgetId) {
const controls = document.createElement('div');
controls.className = 'widget-edit-controls';
controls.style.cssText = 'position:absolute;top:4px;right:4px;display:flex;gap:4px;z-index:100;opacity:0;transition:opacity 0.2s;';
const settingsBtn = document.createElement('button');
settingsBtn.textContent = '⚙';
settingsBtn.title = 'Settings';
settingsBtn.style.cssText = 'width:24px;height:24px;background:#4ecca3;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px;';
settingsBtn.onclick = (e) => {
e.stopPropagation();
alert(`Settings for widget: ${widgetId}`);
};
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '×';
deleteBtn.title = 'Delete';
deleteBtn.style.cssText = 'width:24px;height:24px;background:#e94560;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px;';
deleteBtn.onclick = (e) => {
e.stopPropagation();
if (confirm('Delete this widget?')) {
if (this.onWidgetDelete) this.onWidgetDelete(widgetId);
}
};
controls.appendChild(settingsBtn);
controls.appendChild(deleteBtn);
element.appendChild(controls);
this.widgetControlsMap.set(widgetId, controls);
}
removeWidgetControls(widgetId) {
const controls = this.widgetControlsMap.get(widgetId);
if (controls) {
controls.remove();
this.widgetControlsMap.delete(widgetId);
}
}
getIsEditMode() {
return this.isEditMode;
}
}
// ========== APPLICATION ==========
let gridEngine, dragHandler, resizeHandler, editManager;
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%)' }
];
function init() {
const container = document.getElementById('grid-container');
gridEngine = new GridEngine({ columns: 12, rowHeight: 80, gap: 12, container });
dragHandler = new DragDropHandler(gridEngine);
resizeHandler = new ResizeHandler(gridEngine);
editManager = new EditModeManager({
container,
onSave: () => {
console.log('Saved layout');
updateModeIndicator();
},
onCancel: () => {
console.log('Cancelled edit');
updateModeIndicator();
},
onWidgetAdd: (type) => {
addWidget(type);
},
onWidgetDelete: (widgetId) => {
deleteWidget(widgetId);
}
});
document.getElementById('edit-toggle').onclick = () => {
editManager.toggleEditMode();
updateModeIndicator();
};
createInitialWidgets();
updateStats();
}
function createInitialWidgets() {
const initial = [
{ 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 }
];
initial.forEach(cfg => {
const widget = { id: `widget-${widgetCounter++}`, x: cfg.x, y: cfg.y, w: cfg.w, h: cfg.h, type: cfg.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-info">Position: (${widget.x}, ${widget.y})</div>
<div class="widget-info">Size: ${widget.w}×${widget.h}</div>
`;
container.appendChild(element);
widgetElements.set(widget.id, element);
positionWidget(element, widget);
dragHandler.initWidget(element, widget, (updated, newX, newY) => {
widget.x = newX;
widget.y = newY;
positionWidget(element, widget);
updateWidgetInfo(element, widget);
updateStats();
});
resizeHandler.initWidget(element, widget, (updated, newW, newH, newX, newY) => {
widget.w = newW;
widget.h = newH;
widget.x = newX;
widget.y = newY;
positionWidget(element, widget);
updateWidgetInfo(element, widget);
updateStats();
});
editManager.addWidgetControls(element, widget.id);
}
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 updateWidgetInfo(element, widget) {
const infos = element.querySelectorAll('.widget-info');
infos[0].textContent = `Position: (${widget.x}, ${widget.y})`;
infos[1].textContent = `Size: ${widget.w}×${widget.h}`;
}
function addWidget(type) {
const typeIndex = widgetTypes.findIndex(t => t.name.toLowerCase() === type);
const widget = {
id: `widget-${widgetCounter++}`,
x: Math.floor(Math.random() * 6),
y: Math.floor(Math.random() * 2),
w: 4,
h: 2,
type: typeIndex >= 0 ? typeIndex : 0
};
widgets.push(widget);
createWidgetElement(widget);
updateStats();
}
function deleteWidget(widgetId) {
const index = widgets.findIndex(w => w.id === widgetId);
if (index === -1) return;
widgets.splice(index, 1);
const element = widgetElements.get(widgetId);
if (element) {
dragHandler.destroyWidget(element);
resizeHandler.destroyWidget(element);
editManager.removeWidgetControls(widgetId);
element.remove();
widgetElements.delete(widgetId);
}
updateStats();
}
function updateStats() {
document.getElementById('widget-count').textContent = widgets.length;
const totalUnits = widgets.reduce((sum, w) => sum + (w.w * w.h), 0);
document.getElementById('total-units').textContent = totalUnits;
}
function updateModeIndicator() {
const indicator = document.getElementById('mode-indicator');
const toggle = document.getElementById('edit-toggle');
if (editManager.getIsEditMode()) {
indicator.textContent = 'EDIT';
indicator.className = 'mode-indicator edit';
toggle.textContent = 'Exit Edit Mode';
toggle.classList.add('active');
} else {
indicator.textContent = 'VIEW';
indicator.className = 'mode-indicator view';
toggle.textContent = 'Edit Layout';
toggle.classList.remove('active');
}
}
init();
</script>
</body>
</html>