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
|
||||
updateCategory(categoryId, field, value) {
|
||||
this.saveState();
|
||||
|
|
@ -2756,7 +2959,9 @@
|
|||
const hasOptions = mod.options && mod.options.length > 0;
|
||||
const modExpanded = this.expandedModifierIds.has(mod.id);
|
||||
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;"
|
||||
onclick="event.stopPropagation(); MenuBuilder.selectOption('${parentItemId}', '${mod.id}', ${depth})">
|
||||
${hasOptions ? `
|
||||
|
|
@ -2765,11 +2970,14 @@
|
|||
<path d="M9 18l6-6-6-6"/>
|
||||
</svg>
|
||||
</div>
|
||||
` : `
|
||||
<div class="drag-handle" style="visibility: hidden;">
|
||||
<svg width="12" height="12"></svg>
|
||||
` : ''}
|
||||
<div class="drag-handle">
|
||||
<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 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-name">${this.escapeHtml(mod.name)}</div>
|
||||
|
|
@ -2794,6 +3002,7 @@
|
|||
}).join('');
|
||||
},
|
||||
|
||||
// Render menu structure
|
||||
// Render menu structure
|
||||
render() {
|
||||
const container = document.getElementById('menuStructure');
|
||||
|
|
@ -3034,6 +3243,111 @@
|
|||
this.reorderItemInCategory(draggedItemId, targetItemId, position);
|
||||
}
|
||||
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