Add drag-and-drop for modifiers at all levels
- Modifiers can be dragged and dropped to reorder within same parent - Modifiers can be dropped on other modifiers (same or different parent) - Modifiers can be dropped on items to add as top-level modifier - Default action is COPY (drag creates a copy) - Hold Ctrl+Alt while dragging to MOVE instead of copy - Deep cloning preserves nested options with new IDs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
12b47c3e41
commit
fb329727d2
1 changed files with 319 additions and 5 deletions
|
|
@ -2122,6 +2122,209 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Find modifier/option and its parent array
|
||||||
|
findModifierWithParent(modifierId) {
|
||||||
|
for (const cat of this.menu.categories) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
// Check if it's a top-level modifier
|
||||||
|
const idx = item.modifiers.findIndex(m => m.id === modifierId);
|
||||||
|
if (idx !== -1) {
|
||||||
|
return { array: item.modifiers, index: idx, parentId: item.id, isItem: true };
|
||||||
|
}
|
||||||
|
// Check nested
|
||||||
|
const nested = this.findNestedModifierWithParent(item.modifiers, modifierId);
|
||||||
|
if (nested) return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
findNestedModifierWithParent(modifiers, modifierId) {
|
||||||
|
for (const mod of modifiers) {
|
||||||
|
if (mod.options && mod.options.length > 0) {
|
||||||
|
const idx = mod.options.findIndex(o => o.id === modifierId);
|
||||||
|
if (idx !== -1) {
|
||||||
|
return { array: mod.options, index: idx, parentId: mod.id, isItem: false };
|
||||||
|
}
|
||||||
|
const nested = this.findNestedModifierWithParent(mod.options, modifierId);
|
||||||
|
if (nested) return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get the parent array for a parentId (item or modifier)
|
||||||
|
getParentModifiersArray(parentId) {
|
||||||
|
for (const cat of this.menu.categories) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
if (item.id === parentId) {
|
||||||
|
return item.modifiers;
|
||||||
|
}
|
||||||
|
const found = this.findOptionRecursive(item.modifiers, parentId);
|
||||||
|
if (found) {
|
||||||
|
if (!found.options) found.options = [];
|
||||||
|
return found.options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deep clone a modifier with new IDs
|
||||||
|
cloneModifierDeep(mod) {
|
||||||
|
const clone = JSON.parse(JSON.stringify(mod));
|
||||||
|
clone.id = this.generateId();
|
||||||
|
clone.dbId = null; // Will get new dbId on save
|
||||||
|
if (clone.options && clone.options.length > 0) {
|
||||||
|
clone.options = clone.options.map(opt => this.cloneModifierDeep(opt));
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reorder modifier within same parent
|
||||||
|
reorderModifier(draggedId, targetId, parentId, position) {
|
||||||
|
this.saveState();
|
||||||
|
const parentArray = this.getParentModifiersArray(parentId);
|
||||||
|
if (!parentArray) return;
|
||||||
|
|
||||||
|
const draggedIdx = parentArray.findIndex(m => m.id === draggedId);
|
||||||
|
const targetIdx = parentArray.findIndex(m => m.id === targetId);
|
||||||
|
if (draggedIdx === -1 || targetIdx === -1) return;
|
||||||
|
|
||||||
|
// Remove dragged
|
||||||
|
const [dragged] = parentArray.splice(draggedIdx, 1);
|
||||||
|
|
||||||
|
// Find new target index (may have shifted)
|
||||||
|
let newTargetIdx = parentArray.findIndex(m => m.id === targetId);
|
||||||
|
if (position === 'after') newTargetIdx++;
|
||||||
|
|
||||||
|
// Insert at new position
|
||||||
|
parentArray.splice(newTargetIdx, 0, dragged);
|
||||||
|
|
||||||
|
// Update sort orders
|
||||||
|
parentArray.forEach((m, i) => m.sortOrder = i);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.toast('Modifier reordered', 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Move modifier from one parent to another
|
||||||
|
moveModifier(draggedId, fromParentId, targetModId, toParentId, position) {
|
||||||
|
this.saveState();
|
||||||
|
const fromArray = this.getParentModifiersArray(fromParentId);
|
||||||
|
const toArray = this.getParentModifiersArray(toParentId);
|
||||||
|
if (!fromArray || !toArray) return;
|
||||||
|
|
||||||
|
const draggedIdx = fromArray.findIndex(m => m.id === draggedId);
|
||||||
|
if (draggedIdx === -1) return;
|
||||||
|
|
||||||
|
// Remove from source
|
||||||
|
const [dragged] = fromArray.splice(draggedIdx, 1);
|
||||||
|
|
||||||
|
// Find target position in destination
|
||||||
|
const targetIdx = toArray.findIndex(m => m.id === targetModId);
|
||||||
|
let insertIdx = targetIdx === -1 ? toArray.length : targetIdx;
|
||||||
|
if (position === 'after') insertIdx++;
|
||||||
|
|
||||||
|
// Insert at new position
|
||||||
|
toArray.splice(insertIdx, 0, dragged);
|
||||||
|
|
||||||
|
// Update sort orders in both arrays
|
||||||
|
fromArray.forEach((m, i) => m.sortOrder = i);
|
||||||
|
toArray.forEach((m, i) => m.sortOrder = i);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.toast('Modifier moved', 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Copy modifier to position relative to another modifier
|
||||||
|
copyModifier(draggedId, fromParentId, targetModId, toParentId, position) {
|
||||||
|
this.saveState();
|
||||||
|
const fromArray = this.getParentModifiersArray(fromParentId);
|
||||||
|
const toArray = this.getParentModifiersArray(toParentId);
|
||||||
|
if (!fromArray || !toArray) return;
|
||||||
|
|
||||||
|
const dragged = fromArray.find(m => m.id === draggedId);
|
||||||
|
if (!dragged) return;
|
||||||
|
|
||||||
|
// Clone with new IDs
|
||||||
|
const clone = this.cloneModifierDeep(dragged);
|
||||||
|
|
||||||
|
// Find target position
|
||||||
|
const targetIdx = toArray.findIndex(m => m.id === targetModId);
|
||||||
|
let insertIdx = targetIdx === -1 ? toArray.length : targetIdx;
|
||||||
|
if (position === 'after') insertIdx++;
|
||||||
|
|
||||||
|
// Insert clone
|
||||||
|
toArray.splice(insertIdx, 0, clone);
|
||||||
|
|
||||||
|
// Update sort orders
|
||||||
|
toArray.forEach((m, i) => m.sortOrder = i);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.toast('Modifier copied', 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Move modifier to an item (as top-level modifier)
|
||||||
|
moveModifierToItem(draggedId, fromParentId, toItemId) {
|
||||||
|
this.saveState();
|
||||||
|
const fromArray = this.getParentModifiersArray(fromParentId);
|
||||||
|
if (!fromArray) return;
|
||||||
|
|
||||||
|
// Find target item
|
||||||
|
let targetItem = null;
|
||||||
|
for (const cat of this.menu.categories) {
|
||||||
|
targetItem = cat.items.find(i => i.id === toItemId);
|
||||||
|
if (targetItem) break;
|
||||||
|
}
|
||||||
|
if (!targetItem) return;
|
||||||
|
|
||||||
|
const draggedIdx = fromArray.findIndex(m => m.id === draggedId);
|
||||||
|
if (draggedIdx === -1) return;
|
||||||
|
|
||||||
|
// Remove from source
|
||||||
|
const [dragged] = fromArray.splice(draggedIdx, 1);
|
||||||
|
|
||||||
|
// Add to target item's modifiers
|
||||||
|
dragged.sortOrder = targetItem.modifiers.length;
|
||||||
|
targetItem.modifiers.push(dragged);
|
||||||
|
|
||||||
|
// Update sort orders
|
||||||
|
fromArray.forEach((m, i) => m.sortOrder = i);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.toast('Modifier moved to item', 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Copy modifier to an item (as top-level modifier)
|
||||||
|
copyModifierToItem(draggedId, fromParentId, toItemId) {
|
||||||
|
this.saveState();
|
||||||
|
const fromArray = this.getParentModifiersArray(fromParentId);
|
||||||
|
if (!fromArray) return;
|
||||||
|
|
||||||
|
// Find target item
|
||||||
|
let targetItem = null;
|
||||||
|
for (const cat of this.menu.categories) {
|
||||||
|
targetItem = cat.items.find(i => i.id === toItemId);
|
||||||
|
if (targetItem) break;
|
||||||
|
}
|
||||||
|
if (!targetItem) return;
|
||||||
|
|
||||||
|
const dragged = fromArray.find(m => m.id === draggedId);
|
||||||
|
if (!dragged) return;
|
||||||
|
|
||||||
|
// Clone with new IDs
|
||||||
|
const clone = this.cloneModifierDeep(dragged);
|
||||||
|
clone.sortOrder = targetItem.modifiers.length;
|
||||||
|
|
||||||
|
// Add to target item
|
||||||
|
targetItem.modifiers.push(clone);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.toast('Modifier copied to item', 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update category
|
||||||
// Update category
|
// Update category
|
||||||
updateCategory(categoryId, field, value) {
|
updateCategory(categoryId, field, value) {
|
||||||
this.saveState();
|
this.saveState();
|
||||||
|
|
@ -2756,7 +2959,9 @@
|
||||||
const hasOptions = mod.options && mod.options.length > 0;
|
const hasOptions = mod.options && mod.options.length > 0;
|
||||||
const modExpanded = this.expandedModifierIds.has(mod.id);
|
const modExpanded = this.expandedModifierIds.has(mod.id);
|
||||||
return `
|
return `
|
||||||
<div class="item-card modifier depth-${depth} ${modExpanded ? 'expanded' : ''}" data-modifier-id="${mod.id}" data-parent-item-id="${parentItemId}" data-depth="${depth}"
|
<div class="item-card modifier depth-${depth} ${modExpanded ? 'expanded' : ''}"
|
||||||
|
data-modifier-id="${mod.id}" data-parent-id="${parentItemId}" data-depth="${depth}"
|
||||||
|
draggable="true"
|
||||||
style="margin-left: ${indent}px;"
|
style="margin-left: ${indent}px;"
|
||||||
onclick="event.stopPropagation(); MenuBuilder.selectOption('${parentItemId}', '${mod.id}', ${depth})">
|
onclick="event.stopPropagation(); MenuBuilder.selectOption('${parentItemId}', '${mod.id}', ${depth})">
|
||||||
${hasOptions ? `
|
${hasOptions ? `
|
||||||
|
|
@ -2765,11 +2970,14 @@
|
||||||
<path d="M9 18l6-6-6-6"/>
|
<path d="M9 18l6-6-6-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
` : `
|
` : ''}
|
||||||
<div class="drag-handle" style="visibility: hidden;">
|
<div class="drag-handle">
|
||||||
<svg width="12" height="12"></svg>
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/>
|
||||||
|
<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>
|
||||||
|
<circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
`}
|
|
||||||
<div class="item-image" style="width: ${iconSize}px; height: ${iconSize}px; font-size: ${iconSize/2}px;">${icon}</div>
|
<div class="item-image" style="width: ${iconSize}px; height: ${iconSize}px; font-size: ${iconSize/2}px;">${icon}</div>
|
||||||
<div class="item-info" onclick="event.stopPropagation(); ${hasOptions ? `MenuBuilder.toggleModifier('${mod.id}')` : `MenuBuilder.selectOption('${parentItemId}', '${mod.id}', ${depth})`}" style="cursor: pointer;">
|
<div class="item-info" onclick="event.stopPropagation(); ${hasOptions ? `MenuBuilder.toggleModifier('${mod.id}')` : `MenuBuilder.selectOption('${parentItemId}', '${mod.id}', ${depth})`}" style="cursor: pointer;">
|
||||||
<div class="item-name">${this.escapeHtml(mod.name)}</div>
|
<div class="item-name">${this.escapeHtml(mod.name)}</div>
|
||||||
|
|
@ -2794,6 +3002,7 @@
|
||||||
}).join('');
|
}).join('');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Render menu structure
|
||||||
// Render menu structure
|
// Render menu structure
|
||||||
render() {
|
render() {
|
||||||
const container = document.getElementById('menuStructure');
|
const container = document.getElementById('menuStructure');
|
||||||
|
|
@ -3035,6 +3244,111 @@
|
||||||
}
|
}
|
||||||
card.classList.remove('drop-before', 'drop-after');
|
card.classList.remove('drop-before', 'drop-after');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modifier drag handlers
|
||||||
|
document.querySelectorAll('.item-card.modifier').forEach(card => {
|
||||||
|
card.addEventListener('dragstart', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.dataTransfer.setData('modifierId', card.dataset.modifierId);
|
||||||
|
e.dataTransfer.setData('parentId', card.dataset.parentId);
|
||||||
|
e.dataTransfer.setData('depth', card.dataset.depth);
|
||||||
|
e.dataTransfer.setData('source', 'modifier');
|
||||||
|
// Default is copy, hold Ctrl+Alt for move
|
||||||
|
e.dataTransfer.effectAllowed = (e.ctrlKey && e.altKey) ? 'move' : 'copy';
|
||||||
|
card.classList.add('dragging');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('dragend', () => {
|
||||||
|
card.classList.remove('dragging');
|
||||||
|
document.querySelectorAll('.drop-before, .drop-after, .drag-over').forEach(el => {
|
||||||
|
el.classList.remove('drop-before', 'drop-after', 'drag-over');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const isDraggingModifier = e.dataTransfer.types.includes('modifierid');
|
||||||
|
if (isDraggingModifier && !card.classList.contains('dragging')) {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const midpoint = rect.top + rect.height / 2;
|
||||||
|
card.classList.remove('drop-before', 'drop-after');
|
||||||
|
if (e.clientY < midpoint) {
|
||||||
|
card.classList.add('drop-before');
|
||||||
|
} else {
|
||||||
|
card.classList.add('drop-after');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('dragleave', (e) => {
|
||||||
|
card.classList.remove('drop-before', 'drop-after');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const draggedModId = e.dataTransfer.getData('modifierId');
|
||||||
|
const draggedParentId = e.dataTransfer.getData('parentId');
|
||||||
|
const targetModId = card.dataset.modifierId;
|
||||||
|
const targetParentId = card.dataset.parentId;
|
||||||
|
const isMove = e.dataTransfer.effectAllowed === 'move';
|
||||||
|
|
||||||
|
if (draggedModId && targetModId && draggedModId !== targetModId) {
|
||||||
|
const position = card.classList.contains('drop-before') ? 'before' : 'after';
|
||||||
|
|
||||||
|
if (isMove && draggedParentId === targetParentId) {
|
||||||
|
// Same parent + move = reorder
|
||||||
|
this.reorderModifier(draggedModId, targetModId, targetParentId, position);
|
||||||
|
} else if (isMove) {
|
||||||
|
// Different parent + move = move modifier
|
||||||
|
this.moveModifier(draggedModId, draggedParentId, targetModId, targetParentId, position);
|
||||||
|
} else {
|
||||||
|
// Copy (default)
|
||||||
|
this.copyModifier(draggedModId, draggedParentId, targetModId, targetParentId, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
card.classList.remove('drop-before', 'drop-after');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow dropping modifiers onto items (to add as top-level modifier)
|
||||||
|
document.querySelectorAll('.item-card:not(.modifier)').forEach(card => {
|
||||||
|
card.addEventListener('dragover', (e) => {
|
||||||
|
const isDraggingModifier = e.dataTransfer.types.includes('modifierid');
|
||||||
|
if (isDraggingModifier) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
card.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('dragleave', (e) => {
|
||||||
|
if (e.dataTransfer.types.includes('modifierid')) {
|
||||||
|
card.classList.remove('drag-over');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('drop', (e) => {
|
||||||
|
const isDraggingModifier = e.dataTransfer.types.includes('modifierid');
|
||||||
|
if (isDraggingModifier) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const draggedModId = e.dataTransfer.getData('modifierId');
|
||||||
|
const draggedParentId = e.dataTransfer.getData('parentId');
|
||||||
|
const targetItemId = card.dataset.itemId;
|
||||||
|
const isMove = e.dataTransfer.effectAllowed === 'move';
|
||||||
|
|
||||||
|
if (draggedModId && targetItemId) {
|
||||||
|
if (isMove) {
|
||||||
|
this.moveModifierToItem(draggedModId, draggedParentId, targetItemId);
|
||||||
|
} else {
|
||||||
|
this.copyModifierToItem(draggedModId, draggedParentId, targetItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
card.classList.remove('drag-over');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue