feat: Hierarchien für Farbschemata via Drag & Drop
Schematas können per Drag & Drop als Unterkategorien anderer Schematas organisiert werden. Kinder werden eingerückt mit linkem Rand angezeigt. Entnesten via Button oder Root-Dropzone. Löschen orphaned Kinder. Umbenennen aktualisiert Kind-Referenzen automatisch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
224
js/collection.js
224
js/collection.js
@@ -80,21 +80,38 @@ export function addColorToSchema(hsl) {
|
||||
export function saveSchema(name, farben, bild, originalName) {
|
||||
const data = load();
|
||||
if (originalName && originalName !== name) {
|
||||
const old = data.schemata.find(s => s.name === originalName);
|
||||
const preservedParent = old?.parent;
|
||||
// Kind-Referenzen auf neuen Namen umbiegen
|
||||
data.schemata.forEach(s => { if (s.parent === originalName) s.parent = name; });
|
||||
data.schemata = data.schemata.filter(s => s.name !== originalName);
|
||||
}
|
||||
const entry = { name, farben, bild: bild || null };
|
||||
if (preservedParent) entry.parent = preservedParent;
|
||||
data.schemata.push(entry);
|
||||
} else {
|
||||
const existing = data.schemata.find(s => s.name === name);
|
||||
if (existing) {
|
||||
existing.farben = farben;
|
||||
if (bild !== undefined) existing.bild = bild || null;
|
||||
// parent bleibt erhalten
|
||||
} else {
|
||||
data.schemata.push({ name, farben, bild: bild || null });
|
||||
}
|
||||
}
|
||||
save(data);
|
||||
renderSammlung();
|
||||
}
|
||||
|
||||
export function deleteSchema(name) {
|
||||
const data = load();
|
||||
const schema = data.schemata.find(s => s.name === name);
|
||||
const parentOfDeleted = schema?.parent || null;
|
||||
// Kinder auf die Eltern-Ebene des gelöschten Schemas verschieben
|
||||
data.schemata.forEach(s => {
|
||||
if (s.parent === name) {
|
||||
if (parentOfDeleted) { s.parent = parentOfDeleted; } else { delete s.parent; }
|
||||
}
|
||||
});
|
||||
data.schemata = data.schemata.filter(s => s.name !== name);
|
||||
save(data);
|
||||
renderSammlung();
|
||||
@@ -153,6 +170,147 @@ export function importCollection() {
|
||||
input.click();
|
||||
}
|
||||
|
||||
// --- Schema-Hierarchie ---
|
||||
|
||||
function buildTree(schemata) {
|
||||
const nodes = {};
|
||||
schemata.forEach(s => { nodes[s.name] = { ...s, children: [] }; });
|
||||
const roots = [];
|
||||
schemata.forEach(s => {
|
||||
if (s.parent && nodes[s.parent]) {
|
||||
nodes[s.parent].children.push(nodes[s.name]);
|
||||
} else {
|
||||
roots.push(nodes[s.name]);
|
||||
}
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
|
||||
function isDescendant(schemata, ancestorName, checkName) {
|
||||
for (const s of schemata) {
|
||||
if (s.parent === ancestorName) {
|
||||
if (s.name === checkName || isDescendant(schemata, s.name, checkName)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setSchemaParent(childName, parentName) {
|
||||
const data = load();
|
||||
const schema = data.schemata.find(s => s.name === childName);
|
||||
if (!schema) return;
|
||||
if (parentName) { schema.parent = parentName; } else { delete schema.parent; }
|
||||
save(data);
|
||||
renderSammlung();
|
||||
}
|
||||
|
||||
let draggedSchemaName = null;
|
||||
|
||||
function renderSchemaCard(node, container, allSchemata, depth) {
|
||||
const card = document.createElement('div');
|
||||
card.className = depth === 0 ? 'schema-card' : 'schema-card schema-card-nested';
|
||||
card.dataset.schemaName = node.name;
|
||||
card.draggable = true;
|
||||
|
||||
card.addEventListener('dragstart', (e) => {
|
||||
draggedSchemaName = node.name;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
setTimeout(() => card.classList.add('schema-dragging'), 0);
|
||||
});
|
||||
card.addEventListener('dragend', () => {
|
||||
card.classList.remove('schema-dragging');
|
||||
draggedSchemaName = null;
|
||||
document.querySelectorAll('.schema-drag-over').forEach(el => el.classList.remove('schema-drag-over'));
|
||||
});
|
||||
card.addEventListener('dragover', (e) => {
|
||||
if (!draggedSchemaName || draggedSchemaName === node.name) return;
|
||||
if (isDescendant(allSchemata, draggedSchemaName, node.name)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.schema-drag-over').forEach(el => el.classList.remove('schema-drag-over'));
|
||||
card.classList.add('schema-drag-over');
|
||||
});
|
||||
card.addEventListener('dragleave', (e) => {
|
||||
if (!card.contains(e.relatedTarget)) card.classList.remove('schema-drag-over');
|
||||
});
|
||||
card.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
card.classList.remove('schema-drag-over');
|
||||
if (!draggedSchemaName || draggedSchemaName === node.name) return;
|
||||
if (isDescendant(allSchemata, draggedSchemaName, node.name)) return;
|
||||
setSchemaParent(draggedSchemaName, node.name);
|
||||
});
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;gap:0.5rem';
|
||||
|
||||
const nameRow = document.createElement('div');
|
||||
nameRow.style.cssText = 'display:flex;align-items:center;gap:0.5rem;min-width:0';
|
||||
|
||||
const handle = document.createElement('span');
|
||||
handle.textContent = '⠿';
|
||||
handle.title = 'Ziehen um Untergruppe zu setzen';
|
||||
handle.style.cssText = 'color:#ccc;cursor:grab;font-size:1.1rem;user-select:none;flex-shrink:0';
|
||||
nameRow.appendChild(handle);
|
||||
|
||||
if (node.bild) {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'schema-thumb';
|
||||
thumb.style.backgroundImage = 'url(' + node.bild + ')';
|
||||
nameRow.appendChild(thumb);
|
||||
}
|
||||
|
||||
const nameEl = document.createElement('strong');
|
||||
nameEl.textContent = node.name;
|
||||
nameRow.appendChild(nameEl);
|
||||
|
||||
const btnGroup = document.createElement('div');
|
||||
btnGroup.style.cssText = 'display:flex;gap:0.4rem;flex-shrink:0';
|
||||
|
||||
if (depth > 0) {
|
||||
const unestBtn = document.createElement('button');
|
||||
unestBtn.className = 'action-btn';
|
||||
unestBtn.textContent = '↑ Entnesten';
|
||||
unestBtn.style.fontSize = '0.75rem';
|
||||
unestBtn.addEventListener('click', () => setSchemaParent(node.name, null));
|
||||
btnGroup.appendChild(unestBtn);
|
||||
}
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'action-btn';
|
||||
editBtn.textContent = 'Bearbeiten';
|
||||
editBtn.style.fontSize = '0.75rem';
|
||||
editBtn.addEventListener('click', () => editSchemaHandler?.(node));
|
||||
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.className = 'action-btn';
|
||||
delBtn.textContent = 'Löschen';
|
||||
delBtn.style.fontSize = '0.75rem';
|
||||
delBtn.addEventListener('click', () => deleteSchema(node.name));
|
||||
|
||||
btnGroup.appendChild(editBtn);
|
||||
btnGroup.appendChild(delBtn);
|
||||
header.appendChild(nameRow);
|
||||
header.appendChild(btnGroup);
|
||||
|
||||
const swatchesDiv = document.createElement('div');
|
||||
swatchesDiv.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap';
|
||||
node.farben.forEach(hsl => swatchesDiv.appendChild(makeSwatch(hsl)));
|
||||
|
||||
card.appendChild(header);
|
||||
card.appendChild(swatchesDiv);
|
||||
|
||||
if (node.children.length > 0) {
|
||||
const childrenDiv = document.createElement('div');
|
||||
childrenDiv.className = 'schema-children';
|
||||
node.children.forEach(child => renderSchemaCard(child, childrenDiv, allSchemata, depth + 1));
|
||||
card.appendChild(childrenDiv);
|
||||
}
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
|
||||
function makeSwatch(hsl) {
|
||||
const hex = hslToHex(hsl);
|
||||
|
||||
@@ -227,56 +385,24 @@ export function renderSammlung() {
|
||||
msg.textContent = 'Noch keine Schemata.';
|
||||
schemataContainer.appendChild(msg);
|
||||
} else {
|
||||
data.schemata.forEach(schema => {
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = 'border:1px solid #ddd;border-radius:8px;padding:1rem;margin-bottom:0.75rem;background:#fff';
|
||||
const tree = buildTree(data.schemata);
|
||||
tree.forEach(node => renderSchemaCard(node, schemataContainer, data.schemata, 0));
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem';
|
||||
|
||||
// Thumbnail + Name nebeneinander
|
||||
const nameRow = document.createElement('div');
|
||||
nameRow.style.cssText = 'display:flex;align-items:center;gap:0.6rem';
|
||||
|
||||
if (schema.bild) {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'schema-thumb';
|
||||
thumb.style.backgroundImage = 'url(' + schema.bild + ')';
|
||||
nameRow.appendChild(thumb);
|
||||
}
|
||||
|
||||
const nameEl = document.createElement('strong');
|
||||
nameEl.textContent = schema.name;
|
||||
nameRow.appendChild(nameEl);
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'action-btn';
|
||||
editBtn.textContent = 'Schema bearbeiten';
|
||||
editBtn.style.fontSize = '0.75rem';
|
||||
editBtn.addEventListener('click', () => editSchemaHandler?.(schema));
|
||||
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.className = 'action-btn';
|
||||
delBtn.textContent = 'Schema löschen';
|
||||
delBtn.style.fontSize = '0.75rem';
|
||||
delBtn.addEventListener('click', () => deleteSchema(schema.name));
|
||||
|
||||
const btnGroup = document.createElement('div');
|
||||
btnGroup.style.cssText = 'display:flex;gap:0.4rem';
|
||||
btnGroup.appendChild(editBtn);
|
||||
btnGroup.appendChild(delBtn);
|
||||
|
||||
header.appendChild(nameRow);
|
||||
header.appendChild(btnGroup);
|
||||
|
||||
const swatchesDiv = document.createElement('div');
|
||||
swatchesDiv.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap';
|
||||
schema.farben.forEach(hsl => swatchesDiv.appendChild(makeSwatch(hsl)));
|
||||
|
||||
card.appendChild(header);
|
||||
card.appendChild(swatchesDiv);
|
||||
schemataContainer.appendChild(card);
|
||||
const rootZone = document.createElement('div');
|
||||
rootZone.className = 'schema-root-dropzone';
|
||||
rootZone.textContent = 'Hierher ziehen um auf oberste Ebene zu verschieben';
|
||||
rootZone.addEventListener('dragover', (e) => {
|
||||
if (!draggedSchemaName) return;
|
||||
e.preventDefault();
|
||||
rootZone.classList.add('active');
|
||||
});
|
||||
rootZone.addEventListener('dragleave', () => rootZone.classList.remove('active'));
|
||||
rootZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
rootZone.classList.remove('active');
|
||||
if (draggedSchemaName) setSchemaParent(draggedSchemaName, null);
|
||||
});
|
||||
schemataContainer.appendChild(rootZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
style.css
49
style.css
@@ -238,6 +238,55 @@ button.action-btn:hover { background: #f0f0f0; }
|
||||
}
|
||||
.schema-bild-preview.hat-bild { border-style: solid; border-color: #ccc; }
|
||||
|
||||
/* --- Schema-Hierarchie --- */
|
||||
.schema-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.schema-card-nested {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.schema-card.schema-drag-over {
|
||||
border-color: #4a90d9;
|
||||
box-shadow: 0 0 0 3px rgba(74,144,217,0.2);
|
||||
}
|
||||
|
||||
.schema-card.schema-dragging { opacity: 0.45; }
|
||||
|
||||
.schema-children {
|
||||
margin-top: 0.75rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 3px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.schema-children .schema-card { margin-bottom: 0; }
|
||||
|
||||
.schema-root-dropzone {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: #bbb;
|
||||
margin-top: 0.5rem;
|
||||
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.schema-root-dropzone.active {
|
||||
border-color: #4a90d9;
|
||||
background: #f0f7ff;
|
||||
color: #4a90d9;
|
||||
}
|
||||
|
||||
/* Thumbnail in der Sammlung */
|
||||
.schema-thumb {
|
||||
width: 40px;
|
||||
|
||||
Reference in New Issue
Block a user