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:
2026-07-29 10:42:42 +07:00
parent 0f99788d21
commit 9869566d3e
3 changed files with 16 additions and 13 deletions
+6 -7
View File
@@ -12084,12 +12084,12 @@ const App = () => {
Object.keys(drag.multiIds).forEach(function(mid) { Object.keys(drag.multiIds).forEach(function(mid) {
var info = drag.multiIds[mid]; var info = drag.multiIds[mid];
var newVal = info.start + delta; var newVal = info.start + delta;
// Use live currentTrackMap: info.trackId is stale after cross-track moves var isDraggedItem = mid === drag.itemId;
var currentTid = currentTrackMap.hasOwnProperty(mid) ? currentTrackMap[mid] : info.trackId; var currentTid = currentTrackMap.hasOwnProperty(mid) ? currentTrackMap[mid] : info.trackId;
var onCurrentTrack = currentTid === t.id; var onCurrentTrack = currentTid === t.id;
if (onCurrentTrack) { if (onCurrentTrack) {
if (!crossTracks || t.id === targetTrackId) { if (!crossTracks || (isDraggedItem && t.id === targetTrackId) || (!isDraggedItem && t.id === currentTid)) {
// Item is HERE and staying update position in-place // Update position in-place (same track or non-dragged items stay)
if (info.type === 'section') { if (info.type === 'section') {
var idx = resultSections.findIndex(function(s) { return s.id === mid; }); var idx = resultSections.findIndex(function(s) { return s.id === mid; });
if (idx >= 0) resultSections[idx] = { ...resultSections[idx], start: Math.max(0, newVal) }; if (idx >= 0) resultSections[idx] = { ...resultSections[idx], start: Math.max(0, newVal) };
@@ -12101,13 +12101,13 @@ const App = () => {
if (idx >= 0) resultClips[idx] = { ...resultClips[idx], startTime: Math.max(0, newVal) }; if (idx >= 0) resultClips[idx] = { ...resultClips[idx], startTime: Math.max(0, newVal) };
} }
} else { } else {
// Item must leave this track // Dragged item leaves this track
if (info.type === 'section') resultSections = resultSections.filter(function(s) { return s.id !== mid; }); 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 === '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 (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) { } else if (isDraggedItem && crossTracks && t.id === targetTrackId) {
// This IS the target track filter-then-upsert at new position // Only the dragged item can cross tracks insert on target
var srcItem = null; var srcItem = null;
for (var pi2 = 0; pi2 < prev.length; pi2++) { for (var pi2 = 0; pi2 < prev.length; pi2++) {
var tr2 = prev[pi2]; var tr2 = prev[pi2];
@@ -12130,7 +12130,6 @@ const App = () => {
} }
} }
} }
// else: item is on another track and we're not crossing nothing to do
}); });
return { ...t, sections: resultSections, midiItems: resultMidis, clips: resultClips }; return { ...t, sections: resultSections, midiItems: resultMidis, clips: resultClips };
} }
+4 -6
View File
@@ -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 ── 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 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? 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 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)
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{// Dragged item leaves this track
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(isDraggedItem&&crossTracks&&t.id===targetTrackId){// Only the dragged item can cross tracks — insert on target
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)});}}}});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 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 ──
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=>{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=>{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 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
+6
View File
@@ -718,3 +718,9 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. - **Ghi chú/Test (nếu có):** `npm run build` — build passes.
--- ---
### [2026-07-29 10:42] Task: Fix multi-item drag merging all items into target track
- **Tóm tắt thay đổi:** Khi cross-track drag, chỉ item đang được kéo (`drag.itemId`) được chuyển track — các item được chọn khác giữ nguyên track cũ, chỉ cập nhật vị trí ngang (delta). Fix `isDraggedItem` check trong multiIds loop.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
---