fix: multi-item drag no longer merges all items into target track
Only the dragged item (drag.itemId) crosses tracks. Other selected items stay on their original tracks and only move horizontally.
This commit is contained in:
@@ -344,12 +344,10 @@ const handleSectionItemDragStart=(trackId,itemType,itemId,clickOffset,isDuplicat
|
||||
const handleSectionItemResizeStart=(trackId,itemType,itemId,side,clickTime)=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===trackId);if(!track)return;const items=itemType==='section'?track.sections:track.midiItems;const item=(items||[]).find(it=>it.id===itemId);if(!item)return;const start=itemType==='section'?item.start:item.startTime;setResizedSectionItem({trackId,itemType,itemId,side,originalStart:start,originalDuration:item.duration});};// ── Document-level mousemove/mouseup for Section/MIDI item drag ──
|
||||
useEffect(()=>{const handleMouseMove=e=>{const drag=draggedSectionItemRef.current;if(!drag)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);const beatSec=60.0/(parseInt(bpm)||120);const secondsPerBar=beatSec*4;const marginBar=maxDurationRef.current-secondsPerBar;const rawStart=Math.max(0,Math.min(time-drag.clickOffset,marginBar));const newStart=snapTime(rawStart,snapValueRef.current,bpmRef.current);const itemPx=newStart*zoom;const keepMargin=80;if(itemPx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=itemPx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(itemPx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,itemPx-keepMargin);setCanvasRedrawCount(n=>n+1);}const targetTrackId=hoveredTrackIdRef.current||drag.trackId;updateActiveTracks(prev=>{let movedItem=null;for(let track of prev){// Search all item types for the dragged item
|
||||
const sec=(track.sections||[]).find(it=>it.id===drag.itemId);const mid=(track.midiItems||[]).find(it=>it.id===drag.itemId);if(sec||mid){movedItem=sec||mid;break;}}// Build a live map: where does each multi-drag item currently live?
|
||||
var currentTrackMap={};if(drag.multiIds){Object.keys(drag.multiIds).forEach(function(mid){var info=drag.multiIds[mid];for(var pi=0;pi<prev.length;pi++){var pt=prev[pi];var foundHere=false;if(info.type==='section')foundHere=(pt.sections||[]).some(function(s){return s.id===mid;});else if(info.type==='midiItem')foundHere=(pt.midiItems||[]).some(function(m){return m.id===mid;});else if(info.type==='clip')foundHere=(pt.clips||[]).some(function(c){return c.id===mid||'default_'+pt.id===mid;});if(foundHere){currentTrackMap[mid]=pt.id;break;}}});}return prev.map(t=>{if(drag.multiIds){var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;var resultSections=(t.sections||[]).slice();var resultMidis=(t.midiItems||[]).slice();var resultClips=(t.clips||[]).slice();var crossTracks=targetTrackId!==drag.trackId;Object.keys(drag.multiIds).forEach(function(mid){var info=drag.multiIds[mid];var newVal=info.start+delta;// Use live currentTrackMap: info.trackId is stale after cross-track moves
|
||||
var currentTid=currentTrackMap.hasOwnProperty(mid)?currentTrackMap[mid]:info.trackId;var onCurrentTrack=currentTid===t.id;if(onCurrentTrack){if(!crossTracks||t.id===targetTrackId){// Item is HERE and staying – update position in-place
|
||||
if(info.type==='section'){var idx=resultSections.findIndex(function(s){return s.id===mid;});if(idx>=0)resultSections[idx]={...resultSections[idx],start:Math.max(0,newVal)};}else if(info.type==='midiItem'){var idx=resultMidis.findIndex(function(mx){return mx.id===mid;});if(idx>=0)resultMidis[idx]={...resultMidis[idx],startTime:Math.max(0,newVal)};}else if(info.type==='clip'){var idx=resultClips.findIndex(function(cx){return cx.id===mid||'default_'+t.id===mid;});if(idx>=0)resultClips[idx]={...resultClips[idx],startTime:Math.max(0,newVal)};}}else{// Item must leave this track
|
||||
if(info.type==='section')resultSections=resultSections.filter(function(s){return s.id!==mid;});else if(info.type==='midiItem')resultMidis=resultMidis.filter(function(mx){return mx.id!==mid;});else if(info.type==='clip'){var cid3='default_'+t.id;resultClips=resultClips.filter(function(cx){return cx.id!==mid&&cx.id!==cid3;});}}}else if(crossTracks&&t.id===targetTrackId){// This IS the target track – filter-then-upsert at new position
|
||||
var srcItem=null;for(var pi2=0;pi2<prev.length;pi2++){var tr2=prev[pi2];if(info.type==='section')srcItem=(tr2.sections||[]).find(function(s){return s.id===mid;});else if(info.type==='midiItem')srcItem=(tr2.midiItems||[]).find(function(mx){return mx.id===mid;});else if(info.type==='clip'){var c2=(tr2.clips||[]).find(function(cx){return cx.id===mid||'default_'+tr2.id===mid;});if(c2)srcItem=c2;}if(srcItem)break;}if(srcItem){if(info.type==='section'){resultSections=resultSections.filter(function(s){return s.id!==mid;});resultSections.push({...srcItem,start:Math.max(0,newVal)});}else if(info.type==='midiItem'){resultMidis=resultMidis.filter(function(mx){return mx.id!==mid;});resultMidis.push({...srcItem,startTime:Math.max(0,newVal)});}else if(info.type==='clip'){var cid4='default_'+t.id;resultClips=resultClips.filter(function(cx){return cx.id!==mid&&cx.id!==cid4;});resultClips.push({...srcItem,startTime:Math.max(0,newVal)});}}}// else: item is on another track and we're not crossing → nothing to do
|
||||
});return{...t,sections:resultSections,midiItems:resultMidis,clips:resultClips};}const items=drag.itemType==='section'?t.sections||[]:t.midiItems||[];const updatedItems=items.filter(it=>it.id!==drag.itemId);if(t.id===targetTrackId){if(movedItem){updatedItems.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}else{if(drag.itemType==='section'){updatedItems.push({id:drag.itemId,name:'Section',start:newStart,duration:4*secondsPerBar,color:'#06b6d4'});}else{updatedItems.push({id:drag.itemId,name:'MIDI Item',startTime:newStart,duration:4*secondsPerBar,notes:[],color:'#a78bfa'});}}}return drag.itemType==='section'?{...t,sections:updatedItems}:{...t,midiItems:updatedItems};});});if(drag.trackId!==targetTrackId){setDraggedSectionItem(prev=>({...prev,trackId:targetTrackId}));}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;setDraggedSectionItem(null);showToast(`Đã di chuyển ${drag.itemType==='section'?'section':'MIDI item'}.`,'success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag ──
|
||||
var currentTrackMap={};if(drag.multiIds){Object.keys(drag.multiIds).forEach(function(mid){var info=drag.multiIds[mid];for(var pi=0;pi<prev.length;pi++){var pt=prev[pi];var foundHere=false;if(info.type==='section')foundHere=(pt.sections||[]).some(function(s){return s.id===mid;});else if(info.type==='midiItem')foundHere=(pt.midiItems||[]).some(function(m){return m.id===mid;});else if(info.type==='clip')foundHere=(pt.clips||[]).some(function(c){return c.id===mid||'default_'+pt.id===mid;});if(foundHere){currentTrackMap[mid]=pt.id;break;}}});}return prev.map(t=>{if(drag.multiIds){var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;var resultSections=(t.sections||[]).slice();var resultMidis=(t.midiItems||[]).slice();var resultClips=(t.clips||[]).slice();var crossTracks=targetTrackId!==drag.trackId;Object.keys(drag.multiIds).forEach(function(mid){var info=drag.multiIds[mid];var newVal=info.start+delta;var isDraggedItem=mid===drag.itemId;var currentTid=currentTrackMap.hasOwnProperty(mid)?currentTrackMap[mid]:info.trackId;var onCurrentTrack=currentTid===t.id;if(onCurrentTrack){if(!crossTracks||isDraggedItem&&t.id===targetTrackId||!isDraggedItem&&t.id===currentTid){// Update position in-place (same track or non-dragged items stay)
|
||||
if(info.type==='section'){var idx=resultSections.findIndex(function(s){return s.id===mid;});if(idx>=0)resultSections[idx]={...resultSections[idx],start:Math.max(0,newVal)};}else if(info.type==='midiItem'){var idx=resultMidis.findIndex(function(mx){return mx.id===mid;});if(idx>=0)resultMidis[idx]={...resultMidis[idx],startTime:Math.max(0,newVal)};}else if(info.type==='clip'){var idx=resultClips.findIndex(function(cx){return cx.id===mid||'default_'+t.id===mid;});if(idx>=0)resultClips[idx]={...resultClips[idx],startTime:Math.max(0,newVal)};}}else{// Dragged item leaves this track
|
||||
if(info.type==='section')resultSections=resultSections.filter(function(s){return s.id!==mid;});else if(info.type==='midiItem')resultMidis=resultMidis.filter(function(mx){return mx.id!==mid;});else if(info.type==='clip'){var cid3='default_'+t.id;resultClips=resultClips.filter(function(cx){return cx.id!==mid&&cx.id!==cid3;});}}}else if(isDraggedItem&&crossTracks&&t.id===targetTrackId){// Only the dragged item can cross tracks — insert on target
|
||||
var srcItem=null;for(var pi2=0;pi2<prev.length;pi2++){var tr2=prev[pi2];if(info.type==='section')srcItem=(tr2.sections||[]).find(function(s){return s.id===mid;});else if(info.type==='midiItem')srcItem=(tr2.midiItems||[]).find(function(mx){return mx.id===mid;});else if(info.type==='clip'){var c2=(tr2.clips||[]).find(function(cx){return cx.id===mid||'default_'+tr2.id===mid;});if(c2)srcItem=c2;}if(srcItem)break;}if(srcItem){if(info.type==='section'){resultSections=resultSections.filter(function(s){return s.id!==mid;});resultSections.push({...srcItem,start:Math.max(0,newVal)});}else if(info.type==='midiItem'){resultMidis=resultMidis.filter(function(mx){return mx.id!==mid;});resultMidis.push({...srcItem,startTime:Math.max(0,newVal)});}else if(info.type==='clip'){var cid4='default_'+t.id;resultClips=resultClips.filter(function(cx){return cx.id!==mid&&cx.id!==cid4;});resultClips.push({...srcItem,startTime:Math.max(0,newVal)});}}}});return{...t,sections:resultSections,midiItems:resultMidis,clips:resultClips};}const items=drag.itemType==='section'?t.sections||[]:t.midiItems||[];const updatedItems=items.filter(it=>it.id!==drag.itemId);if(t.id===targetTrackId){if(movedItem){updatedItems.push(drag.itemType==='section'?{...movedItem,start:newStart}:{...movedItem,startTime:newStart});}else{if(drag.itemType==='section'){updatedItems.push({id:drag.itemId,name:'Section',start:newStart,duration:4*secondsPerBar,color:'#06b6d4'});}else{updatedItems.push({id:drag.itemId,name:'MIDI Item',startTime:newStart,duration:4*secondsPerBar,notes:[],color:'#a78bfa'});}}}return drag.itemType==='section'?{...t,sections:updatedItems}:{...t,midiItems:updatedItems};});});if(drag.trackId!==targetTrackId){setDraggedSectionItem(prev=>({...prev,trackId:targetTrackId}));}};const handleMouseUp=()=>{const drag=draggedSectionItemRef.current;if(!drag)return;setDraggedSectionItem(null);showToast(`Đã di chuyển ${drag.itemType==='section'?'section':'MIDI item'}.`,'success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag ──
|
||||
useEffect(()=>{const handleMouseMove=e=>{var pd=pendingDragRef.current;if(!pd)return;var dx=e.clientX-pd.startX;if(Math.abs(dx)>5){var pdSnap=pendingDragRef.current;pendingDragRef.current=null;if(handleSectionItemDragStartRef.current)handleSectionItemDragStartRef.current(pdSnap.trackId,pdSnap.itemType,pdSnap.itemId,pdSnap.clickOffset,true,pdSnap.selectedIds);}};document.addEventListener('mousemove',handleMouseMove);var handleMouseUp=function(){pendingDragRef.current=null;};document.addEventListener('mouseup',handleMouseUp);return function(){document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);pendingDragRef.current=null;};},[]);// ── Document-level mousemove/mouseup for Section/MIDI item resize ──
|
||||
useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const snappedDuration=snapValueRef.current!=='free'?snapTime(clampedTime-resize.originalStart,snapValueRef.current,bpm):clampedTime-resize.originalStart;const newDuration=Math.max(0.1,snappedDuration);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,edgePx-keepMargin);setCanvasRedrawCount(n=>n+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ──
|
||||
useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);// Small movement (no real drag) → deselect all
|
||||
|
||||
Reference in New Issue
Block a user