diff --git a/js/collection.js b/js/collection.js index c98df95..57eb983 100644 --- a/js/collection.js +++ b/js/collection.js @@ -80,14 +80,23 @@ 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 existing = data.schemata.find(s => s.name === name); - if (existing) { - existing.farben = farben; - if (bild !== undefined) existing.bild = bild || null; + const entry = { name, farben, bild: bild || null }; + if (preservedParent) entry.parent = preservedParent; + data.schemata.push(entry); } else { - data.schemata.push({ name, farben, bild: bild || null }); + 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(); @@ -95,6 +104,14 @@ export function saveSchema(name, farben, bild, originalName) { 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); } } } diff --git a/style.css b/style.css index f15f614..e2f3354 100644 --- a/style.css +++ b/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;