164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import React, { useRef, useState, useEffect } from 'react';
|
||
import { WidgetData } from '../types';
|
||
import { CogwheelIcon } from './CogwheelIcon';
|
||
import { useAtom, useAtomValue } from 'jotai';
|
||
import { draggableState } from '../state/draggable.state';
|
||
|
||
interface WidgetProps {
|
||
widget: WidgetData;
|
||
gridSize: { width: number; height: number };
|
||
children: React.ReactNode;
|
||
settingsContent?: React.ReactNode;
|
||
updateWidget: (id: string, data: Partial<WidgetData>) => void
|
||
}
|
||
|
||
export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, settingsContent, updateWidget }) => {
|
||
const widgetRef = useRef<HTMLDivElement>(null);
|
||
const modalRef = useRef<HTMLDivElement>(null);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||
const [widthInput, setWidthInput] = useState(widget.gridWidth);
|
||
const [heightInput, setHeightInput] = useState(widget.gridHeight);
|
||
|
||
const isDraggable = useAtomValue(draggableState)
|
||
const snapToGrid = (pos: number, grid: number) => Math.round(pos / grid);
|
||
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
if(!isDraggable || isSettingsOpen){
|
||
return
|
||
}
|
||
|
||
if (widgetRef.current && e.target === widgetRef.current) {
|
||
setIsDragging(true);
|
||
const rect = widgetRef.current.getBoundingClientRect();
|
||
setDragOffset({
|
||
x: e.clientX - rect.left,
|
||
y: e.clientY - rect.top
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleMouseMove = (e: MouseEvent) => {
|
||
if (isDragging && widgetRef.current) {
|
||
e.preventDefault();
|
||
const newGridX = snapToGrid(e.clientX - dragOffset.x, gridSize.width);
|
||
const newGridY = snapToGrid(e.clientY - dragOffset.y, gridSize.height);
|
||
|
||
widgetRef.current.style.left = `${newGridX * gridSize.width}px`;
|
||
widgetRef.current.style.top = `${newGridY * gridSize.height}px`;
|
||
}
|
||
};
|
||
|
||
const handleMouseUp = () => {
|
||
if (isDragging && widgetRef.current) {
|
||
setIsDragging(false);
|
||
const rect = widgetRef.current.getBoundingClientRect();
|
||
const newGridX = snapToGrid(rect.left, gridSize.width);
|
||
const newGridY = snapToGrid(rect.top, gridSize.height);
|
||
updateWidget(widget.id, { gridX: newGridX, gridY: newGridY });
|
||
}
|
||
};
|
||
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||
setIsSettingsOpen(false);
|
||
}
|
||
};
|
||
|
||
const stopPropagation = (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
};
|
||
|
||
const handleSizeChange = () => {
|
||
const newWidth = Math.max(1, widthInput);
|
||
const newHeight = Math.max(1, heightInput);
|
||
updateWidget(widget.id, { gridWidth: newWidth, gridHeight: newHeight });
|
||
setWidthInput(newWidth);
|
||
setHeightInput(newHeight);
|
||
setIsSettingsOpen(false);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (isDragging) {
|
||
window.addEventListener('mousemove', handleMouseMove);
|
||
window.addEventListener('mouseup', handleMouseUp);
|
||
document.body.style.userSelect = 'none';
|
||
}
|
||
return () => {
|
||
window.removeEventListener('mousemove', handleMouseMove);
|
||
window.removeEventListener('mouseup', handleMouseUp);
|
||
document.body.style.userSelect = '';
|
||
};
|
||
}, [isDragging, dragOffset, gridSize]);
|
||
|
||
useEffect(() => {
|
||
if (isSettingsOpen) {
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
}
|
||
return () => {
|
||
document.removeEventListener('mousedown', handleClickOutside);
|
||
};
|
||
}, [isSettingsOpen]);
|
||
|
||
useEffect(() => {
|
||
if (widgetRef.current && !isDragging) {
|
||
widgetRef.current.style.left = `${widget.gridX * gridSize.width}px`;
|
||
widgetRef.current.style.top = `${widget.gridY * gridSize.height}px`;
|
||
widgetRef.current.style.width = `${(widget.gridWidth) * gridSize.width}px`;
|
||
widgetRef.current.style.height = `${(widget.gridHeight) * gridSize.height}px`;
|
||
}
|
||
}, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]);
|
||
|
||
const genericSettingsContent = (
|
||
<>
|
||
<label>
|
||
Width (grid cells):
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={widthInput}
|
||
onChange={(e) => setWidthInput(parseInt(e.target.value) || 1)}
|
||
/>
|
||
</label>
|
||
<label>
|
||
Height (grid cells):
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={heightInput}
|
||
onChange={(e) => setHeightInput(parseInt(e.target.value) || 1)}
|
||
/>
|
||
</label>
|
||
<button onClick={handleSizeChange}>Apply</button>
|
||
</>
|
||
);
|
||
|
||
return (
|
||
<div
|
||
ref={widgetRef}
|
||
className="container"
|
||
onMouseDown={handleMouseDown}
|
||
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
|
||
>
|
||
<button className="close-button" onClick={() => undefined /*removeWidget(widget.id)*/} onMouseDown={stopPropagation}>×</button>
|
||
<button
|
||
className="settings-button"
|
||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||
onMouseDown={stopPropagation}
|
||
>
|
||
<CogwheelIcon size={24} />
|
||
</button>
|
||
{children}
|
||
{isSettingsOpen && (
|
||
<div className="widget-settings-modal" onMouseDown={stopPropagation}>
|
||
<div ref={modalRef} className="widget-modal-content">
|
||
<button className="close-button" onClick={() => setIsSettingsOpen(false)} onMouseDown={stopPropagation}>×</button>
|
||
{settingsContent}
|
||
{genericSettingsContent}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}; |