feat: Implement complete layer management UI with all required features
This commit is contained in:
@@ -1,30 +1,214 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
|
||||||
const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer }) => {
|
const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer, yjsDoc }) => {
|
||||||
const [newLayerName, setNewLayerName] = useState('');
|
const [newLayerName, setNewLayerName] = useState('');
|
||||||
|
const [editingLayerId, setEditingLayerId] = useState(null);
|
||||||
|
const [editingName, setEditingName] = useState('');
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const [showImportDialog, setShowImportDialog] = useState(false);
|
||||||
|
const [importFile, setImportFile] = useState(null);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
const addLayer = () => {
|
const addLayer = () => {
|
||||||
if (!newLayerName.trim()) return;
|
if (!newLayerName.trim()) return;
|
||||||
setLayers([...layers, { name: newLayerName, visible: true, locked: false }]);
|
|
||||||
|
const newLayer = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: newLayerName,
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
color: '#000000',
|
||||||
|
lineType: 'solid',
|
||||||
|
transparency: 0,
|
||||||
|
sortOrder: layers.length
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
yjsDoc.layers.set(newLayer.id, newLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers([...layers, newLayer]);
|
||||||
setNewLayerName('');
|
setNewLayerName('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteLayer = (name) => {
|
const deleteLayer = (id) => {
|
||||||
if (layers.length <= 1) return;
|
if (layers.length <= 1) return;
|
||||||
setLayers(layers.filter(l => l.name !== name));
|
|
||||||
if (activeLayer === name) setActiveLayer(layers[0].name);
|
// Confirmation
|
||||||
|
if (!window.confirm('Sind Sie sicher, dass Sie diesen Layer löschen möchten?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
yjsDoc.layers.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedLayers = layers.filter(l => l.id !== id);
|
||||||
|
setLayers(updatedLayers);
|
||||||
|
|
||||||
|
if (activeLayer === id) {
|
||||||
|
setActiveLayer(updatedLayers[0]?.id || null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleVisible = (name) => {
|
const toggleVisible = (id) => {
|
||||||
setLayers(layers.map(l => l.name === name ? { ...l, visible: !l.visible } : l));
|
const updatedLayers = layers.map(l =>
|
||||||
|
l.id === id ? { ...l, visible: !l.visible } : l
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update in Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
const layer = yjsDoc.layers.get(id);
|
||||||
|
if (layer) {
|
||||||
|
layer.visible = !layer.visible;
|
||||||
|
yjsDoc.layers.set(id, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers(updatedLayers);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLocked = (name) => {
|
const toggleLocked = (id) => {
|
||||||
setLayers(layers.map(l => l.name === name ? { ...l, locked: !l.locked } : l));
|
const updatedLayers = layers.map(l =>
|
||||||
|
l.id === id ? { ...l, locked: !l.locked } : l
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update in Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
const layer = yjsDoc.layers.get(id);
|
||||||
|
if (layer) {
|
||||||
|
layer.locked = !layer.locked;
|
||||||
|
yjsDoc.layers.set(id, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers(updatedLayers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEditing = (id, name) => {
|
||||||
|
setEditingLayerId(id);
|
||||||
|
setEditingName(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEditing = (id) => {
|
||||||
|
if (!editingName.trim()) return;
|
||||||
|
|
||||||
|
const updatedLayers = layers.map(l =>
|
||||||
|
l.id === id ? { ...l, name: editingName } : l
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update in Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
const layer = yjsDoc.layers.get(id);
|
||||||
|
if (layer) {
|
||||||
|
layer.name = editingName;
|
||||||
|
yjsDoc.layers.set(id, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers(updatedLayers);
|
||||||
|
setEditingLayerId(null);
|
||||||
|
setEditingName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditing = () => {
|
||||||
|
setEditingLayerId(null);
|
||||||
|
setEditingName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setColor = (id, color) => {
|
||||||
|
const updatedLayers = layers.map(l =>
|
||||||
|
l.id === id ? { ...l, color } : l
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update in Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
const layer = yjsDoc.layers.get(id);
|
||||||
|
if (layer) {
|
||||||
|
layer.color = color;
|
||||||
|
yjsDoc.layers.set(id, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers(updatedLayers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLineType = (id, lineType) => {
|
||||||
|
const updatedLayers = layers.map(l =>
|
||||||
|
l.id === id ? { ...l, lineType } : l
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update in Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
const layer = yjsDoc.layers.get(id);
|
||||||
|
if (layer) {
|
||||||
|
layer.lineType = lineType;
|
||||||
|
yjsDoc.layers.set(id, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers(updatedLayers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredLayers = layers.filter(layer =>
|
||||||
|
layer.name.toLowerCase().includes(filterText.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportLayers = () => {
|
||||||
|
const dataStr = JSON.stringify(layers, null, 2);
|
||||||
|
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||||
|
|
||||||
|
const exportFileDefaultName = 'layers.json';
|
||||||
|
|
||||||
|
const linkElement = document.createElement('a');
|
||||||
|
linkElement.setAttribute('href', dataUri);
|
||||||
|
linkElement.setAttribute('download', exportFileDefaultName);
|
||||||
|
linkElement.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const importedLayers = JSON.parse(e.target.result);
|
||||||
|
|
||||||
|
// Validate imported layers
|
||||||
|
if (Array.isArray(importedLayers)) {
|
||||||
|
// Add to Yjs document
|
||||||
|
if (yjsDoc) {
|
||||||
|
importedLayers.forEach(layer => {
|
||||||
|
yjsDoc.layers.set(layer.id, layer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayers(importedLayers);
|
||||||
|
|
||||||
|
// Set first layer as active if none is active
|
||||||
|
if (!activeLayer && importedLayers.length > 0) {
|
||||||
|
setActiveLayer(importedLayers[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Fehler beim Importieren der Layer: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
event.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 10px 10px' }}>
|
<div style={{ padding: '0 10px 10px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
<div style={{ display: 'flex', gap: 5, marginBottom: 10 }}>
|
<div style={{ display: 'flex', gap: 5, marginBottom: 10 }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -32,18 +216,40 @@ const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer }) => {
|
|||||||
value={newLayerName}
|
value={newLayerName}
|
||||||
onChange={(e) => setNewLayerName(e.target.value)}
|
onChange={(e) => setNewLayerName(e.target.value)}
|
||||||
style={{ flex: 1, padding: 4, borderRadius: 3, border: '1px solid #ccc' }}
|
style={{ flex: 1, padding: 4, borderRadius: 3, border: '1px solid #ccc' }}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && addLayer()}
|
||||||
/>
|
/>
|
||||||
<button onClick={addLayer} style={{ padding: '4px 8px', background: '#1890ff', color: 'white', border: 'none', borderRadius: 3, cursor: 'pointer' }}>+</button>
|
<button onClick={addLayer} style={{ padding: '4px 8px', background: '#1890ff', color: 'white', border: 'none', borderRadius: 3, cursor: 'pointer' }}>+</button>
|
||||||
</div>
|
</div>
|
||||||
{layers.map((layer, idx) => (
|
|
||||||
|
<div style={{ display: 'flex', gap: 5, marginBottom: 10 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Layer filtern..."
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
|
style={{ flex: 1, padding: 4, borderRadius: 3, border: '1px solid #ccc' }}
|
||||||
|
/>
|
||||||
|
<button onClick={exportLayers} style={{ padding: '4px 8px', background: '#52c41a', color: 'white', border: 'none', borderRadius: 3, cursor: 'pointer' }} title="Exportieren">⬇</button>
|
||||||
|
<button onClick={handleImportClick} style={{ padding: '4px 8px', background: '#faad14', color: 'white', border: 'none', borderRadius: 3, cursor: 'pointer' }} title="Importieren">⬆</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".json"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
|
{filteredLayers.map((layer) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={layer.id}
|
||||||
onClick={() => setActiveLayer(layer.name)}
|
onClick={() => setActiveLayer(layer.id)}
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
background: activeLayer === layer.name ? '#e6f7ff' : '#fff',
|
background: activeLayer === layer.id ? '#e6f7ff' : '#fff',
|
||||||
border: activeLayer === layer.name ? '1px solid #1890ff' : '1px solid #ddd',
|
border: activeLayer === layer.id ? '1px solid #1890ff' : '1px solid #ddd',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -52,10 +258,57 @@ const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer }) => {
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ flex: 1 }}>{layer.name}</span>
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
backgroundColor: layer.color || '#000000',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 2
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{editingLayerId === layer.id ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editingName}
|
||||||
|
onChange={(e) => setEditingName(e.target.value)}
|
||||||
|
onBlur={() => saveEditing(layer.id)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') saveEditing(layer.id);
|
||||||
|
if (e.key === 'Escape') cancelEditing();
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
style={{ flex: 1, padding: 2, borderRadius: 2, border: '1px solid #1890ff' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
onDoubleClick={() => startEditing(layer.id, layer.name)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
{layer.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span style={{ display: 'flex', gap: 4 }}>
|
<span style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<select
|
||||||
|
value={layer.lineType || 'solid'}
|
||||||
|
onChange={(e) => setLineType(layer.id, e.target.value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
padding: '2px 4px',
|
||||||
|
borderRadius: 2,
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
fontSize: 10,
|
||||||
|
background: 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="solid">─</option>
|
||||||
|
<option value="dashed">╌</option>
|
||||||
|
<option value="dotted">⋯</option>
|
||||||
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); toggleVisible(layer.name); }}
|
onClick={(e) => { e.stopPropagation(); toggleVisible(layer.id); }}
|
||||||
style={{
|
style={{
|
||||||
width: 24, height: 24, fontSize: 12,
|
width: 24, height: 24, fontSize: 12,
|
||||||
background: layer.visible ? '#52c41a' : '#d9d9d9',
|
background: layer.visible ? '#52c41a' : '#d9d9d9',
|
||||||
@@ -66,7 +319,7 @@ const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer }) => {
|
|||||||
{layer.visible ? '👁' : '—'}
|
{layer.visible ? '👁' : '—'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); toggleLocked(layer.name); }}
|
onClick={(e) => { e.stopPropagation(); toggleLocked(layer.id); }}
|
||||||
style={{
|
style={{
|
||||||
width: 24, height: 24, fontSize: 12,
|
width: 24, height: 24, fontSize: 12,
|
||||||
background: layer.locked ? '#faad14' : '#d9d9d9',
|
background: layer.locked ? '#faad14' : '#d9d9d9',
|
||||||
@@ -77,7 +330,7 @@ const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer }) => {
|
|||||||
{layer.locked ? '🔒' : '🔓'}
|
{layer.locked ? '🔒' : '🔓'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); deleteLayer(layer.name); }}
|
onClick={(e) => { e.stopPropagation(); deleteLayer(layer.id); }}
|
||||||
style={{
|
style={{
|
||||||
width: 24, height: 24, fontSize: 12,
|
width: 24, height: 24, fontSize: 12,
|
||||||
background: '#ff4d4f', color: 'white', border: 'none', borderRadius: 3, cursor: 'pointer',
|
background: '#ff4d4f', color: 'white', border: 'none', borderRadius: 3, cursor: 'pointer',
|
||||||
@@ -91,6 +344,11 @@ const LayerPanel = ({ layers, setLayers, activeLayer, setActiveLayer }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 10, fontSize: 12, color: '#666', textAlign: 'center' }}>
|
||||||
|
Doppelklick auf Layername zum Umbenennen
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user