fix: sửa lỗi mouse với ctrl+click+drag

This commit is contained in:
2026-07-28 17:29:50 +07:00
parent cff3950670
commit 6604f85e0d
3 changed files with 259 additions and 31 deletions
+127 -31
View File
@@ -1084,11 +1084,25 @@ const WaveformLane = ({
e.preventDefault();
e.stopPropagation();
if (e.ctrlKey) {
// Ctrl+Click: copy on drag, do NOT toggle selection
if (onSetPendingDrag) onSetPendingDrag(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e, selectedItemIds ? new Set(selectedItemIds) : new Set());
// Ctrl+Click: toggle selection (add/remove from group) + set pending copy-drag
var preToggle = selectedItemIds ? new Set(selectedItemIds) : new Set();
if (selectedItemIds && selectedItemIds.has(hitItem.id)) {
if (onDeselectItem) onDeselectItem(hitItem.id);
} else {
if (onAddToSelection) onAddToSelection(hitItem.id);
}
if (onSetPendingDrag) onSetPendingDrag(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e, preToggle);
} else {
// No modifier: start drag immediately (single or multi move)
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, false);
// Click: select this item (clear others if not already selected), then start drag
if (!selectedItemIds || !selectedItemIds.has(hitItem.id)) {
if (onClearSelection) onClearSelection();
if (onAddToSelection) onAddToSelection(hitItem.id);
}
// Drag all currently selected items (or just this one)
var dragIds = (selectedItemIds && selectedItemIds.has(hitItem.id) && selectedItemIds.size > 1)
? selectedItemIds
: new Set([hitItem.id]);
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, false, dragIds);
}
return;
}
@@ -1161,11 +1175,12 @@ const WaveformLane = ({
return;
}
// Ctrl+Click on empty space: deselect all, no sweep select
// Ctrl+Click on empty space: deselect all on click, marquee on drag
if (e.ctrlKey && !clickedClip && !hitItem) {
e.preventDefault();
e.stopPropagation();
if (onClearSelection) onClearSelection();
// Start a pending sweep: mouseup with no drag deselect all; drag marquee
if (onSweepSelectStart) onSweepSelectStart(track.id, time, e.clientY);
return;
}
onPlayheadSet(time);
@@ -6955,6 +6970,8 @@ const App = () => {
const isSweepingRef = useRef(false);
const sweepStartRef = useRef(0);
const sweepTrackIdRef = useRef(null);
const sweepStartYRef = useRef(0);
const sweepEndYRef = useRef(0);
const sweepSelectRef = useRef(null);
const pendingDragRef = useRef(null); // { trackId, itemType, itemId, clickOffset, startX, startY }
const handleSectionItemDragStartRef = useRef(null);
@@ -11735,18 +11752,30 @@ const App = () => {
});
};
const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e, preToggleSnapshot) => {
pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY, selectedIds: preToggleSnapshot || new Set(selectedItemIds) };
// Compute selectedIds AFTER the toggle that just happened:
// - If itemId was in snapshot (was selected) toggle removed it delete
// - If itemId was not in snapshot (was not selected) toggle added it add
var ids = preToggleSnapshot ? new Set(preToggleSnapshot) : new Set(selectedItemIds);
if (preToggleSnapshot && preToggleSnapshot.has(itemId)) {
ids.delete(itemId);
} else {
ids.add(itemId);
}
pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY, selectedIds: ids };
};
// Sweep Select
const handleSweepSelectStart = (trackId, startTime) => {
const handleSweepSelectStart = (trackId, startTime, startY) => {
isSweepingRef.current = true;
sweepStartRef.current = startTime;
sweepTrackIdRef.current = trackId;
sweepStartYRef.current = startY || 0;
sweepEndYRef.current = startY || 0;
var init = { startTime, endTime: startTime };
sweepSelectRef.current = init;
setSweepSelect(init);
setSelectedItemIds(new Set());
// Do NOT clear selection here wait until mouseup.
// Small movement deselect all; large movement marquee toggle.
};
// Section / MIDI Item Drag Start
@@ -11809,7 +11838,8 @@ const App = () => {
return { ...t, sections: updatedSections, midiItems: updatedMidi, clips: updatedClips };
});
});
setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds: newOriginals });
var newItemId = Object.keys(newOriginals)[0] || itemId;
setDraggedSectionItem({ trackId, itemType, itemId: newItemId, clickOffset, multiIds: newOriginals });
} else {
var items = itemType === 'section' ? (track.sections || []) : (track.midiItems || []);
var item = items.find(function(it) { return it.id === itemId; });
@@ -11873,35 +11903,86 @@ const App = () => {
updateActiveTracks(prev => {
let movedItem = null;
for (let track of prev) {
const items = drag.itemType === 'section' ? (track.sections || []) : (track.midiItems || []);
const found = items.find(it => it.id === drag.itemId);
if (found) {
movedItem = found;
break;
}
// 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 midSections = (t.sections || []).slice();
var midMidis = (t.midiItems || []).slice();
var midClips = (t.clips || []).slice();
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;
if (info.type === 'section') {
var idx = midSections.findIndex(function(s) { return s.id === mid; });
if (idx >= 0) midSections[idx] = { ...midSections[idx], start: Math.max(0, newVal) };
} else if (info.type === 'midiItem') {
var idx = midMidis.findIndex(function(m) { return m.id === mid; });
if (idx >= 0) midMidis[idx] = { ...midMidis[idx], startTime: Math.max(0, newVal) };
} else if (info.type === 'clip') {
var idx = midClips.findIndex(function(c) { return c.id === mid; });
if (idx >= 0) midClips[idx] = { ...midClips[idx], startTime: Math.max(0, newVal) };
// 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: midSections, midiItems: midMidis, clips: midClips };
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);
@@ -12051,6 +12132,13 @@ const App = () => {
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
if (Math.abs(end - start) < 0.02) {
setSelectedItemIds(new Set());
setSweepSelect(null);
return;
}
// Real drag toggle items that overlap the marquee
const curTracks = activeTracksRef.current || [];
const found = new Set();
curTracks.forEach(t => {
@@ -12072,7 +12160,15 @@ const App = () => {
}
});
});
setSelectedItemIds(found);
// Toggle: add unselected, remove already-selected
setSelectedItemIds(prev => {
const next = new Set(prev);
found.forEach(id => {
if (next.has(id)) next.delete(id);
else next.add(id);
});
return next;
});
setSweepSelect(null);
sweepTrackIdRef.current = null;
}
@@ -16871,7 +16967,7 @@ const App = () => {
e.preventDefault();
if (e.dataTransfer.files[0]) loadFileOnTrack(track.id, e.dataTransfer.files[0]);
},
onMouseEnter: () => setHoveredTrackId(track.id)
onMouseEnter: () => { setHoveredTrackId(track.id); hoveredTrackIdRef.current = track.id; }
}, /*#__PURE__*/React.createElement(WaveformLane, {
track: track,
zoom: zoom,