From 9869566d3e0da8ca37528bf2f47418c4dcf85453 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 29 Jul 2026 10:42:42 +0700 Subject: [PATCH] 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. --- app/static/js/app.jsx | 13 ++++++------- app/static/js/app.precompiled.js | 10 ++++------ wiki.md | 6 ++++++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 272a990..e0843fd 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -12084,12 +12084,12 @@ const App = () => { 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 isDraggedItem = mid === drag.itemId; 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 (!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) }; @@ -12101,13 +12101,13 @@ const App = () => { if (idx >= 0) resultClips[idx] = { ...resultClips[idx], startTime: Math.max(0, newVal) }; } } else { - // Item must leave this track + // 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 (crossTracks && t.id === targetTrackId) { - // This IS the target track – filter-then-upsert at new position + } 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]; @@ -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 }; } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 1ae0f67..4650155 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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(itemPxn+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{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;pi2it.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{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;pi2it.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(edgePxn+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 diff --git a/wiki.md b/wiki.md index 24761e1..0db201e 100644 --- a/wiki.md +++ b/wiki.md @@ -718,3 +718,9 @@ - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **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. +---