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) {
|
export function saveSchema(name, farben, bild, originalName) {
|
||||||
const data = load();
|
const data = load();
|
||||||
if (originalName && originalName !== name) {
|
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);
|
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);
|
const existing = data.schemata.find(s => s.name === name);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.farben = farben;
|
existing.farben = farben;
|
||||||
if (bild !== undefined) existing.bild = bild || null;
|
if (bild !== undefined) existing.bild = bild || null;
|
||||||
|
// parent bleibt erhalten
|
||||||
} else {
|
} else {
|
||||||
data.schemata.push({ name, farben, bild: bild || null });
|
data.schemata.push({ name, farben, bild: bild || null });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
save(data);
|
save(data);
|
||||||
renderSammlung();
|
renderSammlung();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteSchema(name) {
|
export function deleteSchema(name) {
|
||||||
const data = load();
|
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);
|
data.schemata = data.schemata.filter(s => s.name !== name);
|
||||||
save(data);
|
save(data);
|
||||||
renderSammlung();
|
renderSammlung();
|
||||||
@@ -153,6 +170,147 @@ export function importCollection() {
|
|||||||
input.click();
|
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) {
|
function makeSwatch(hsl) {
|
||||||
const hex = hslToHex(hsl);
|
const hex = hslToHex(hsl);
|
||||||
|
|
||||||
@@ -227,56 +385,24 @@ export function renderSammlung() {
|
|||||||
msg.textContent = 'Noch keine Schemata.';
|
msg.textContent = 'Noch keine Schemata.';
|
||||||
schemataContainer.appendChild(msg);
|
schemataContainer.appendChild(msg);
|
||||||
} else {
|
} else {
|
||||||
data.schemata.forEach(schema => {
|
const tree = buildTree(data.schemata);
|
||||||
const card = document.createElement('div');
|
tree.forEach(node => renderSchemaCard(node, schemataContainer, data.schemata, 0));
|
||||||
card.style.cssText = 'border:1px solid #ddd;border-radius:8px;padding:1rem;margin-bottom:0.75rem;background:#fff';
|
|
||||||
|
|
||||||
const header = document.createElement('div');
|
const rootZone = document.createElement('div');
|
||||||
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem';
|
rootZone.className = 'schema-root-dropzone';
|
||||||
|
rootZone.textContent = 'Hierher ziehen um auf oberste Ebene zu verschieben';
|
||||||
// Thumbnail + Name nebeneinander
|
rootZone.addEventListener('dragover', (e) => {
|
||||||
const nameRow = document.createElement('div');
|
if (!draggedSchemaName) return;
|
||||||
nameRow.style.cssText = 'display:flex;align-items:center;gap:0.6rem';
|
e.preventDefault();
|
||||||
|
rootZone.classList.add('active');
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
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-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 */
|
/* Thumbnail in der Sammlung */
|
||||||
.schema-thumb {
|
.schema-thumb {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
|||||||
Reference in New Issue
Block a user