mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(planner): eliminate drag-and-drop jank in trip planner
- Suppress trek-stagger animation on the day list while a drag is active so nth-child delays (0–320 ms) no longer re-fire on every hover change - Replace sibling drop-indicator <div> injections with borderTop/borderBottom on the target row to prevent nth-child index shifts during drag - Dedup setDragOverDayId calls in onDragOver handlers so setState is only invoked when the active day actually changes - Move initTransportPositions out of getMergedItems (render path) into a useEffect to stop mid-drag setState cascades
This commit is contained in:
+3
-1
@@ -58,4 +58,6 @@ coverage
|
|||||||
*.tgz
|
*.tgz
|
||||||
|
|
||||||
.scannerwork
|
.scannerwork
|
||||||
test-data
|
test-data
|
||||||
|
|
||||||
|
.run
|
||||||
@@ -336,6 +336,10 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
return () => document.removeEventListener('dragend', cleanup)
|
return () => document.removeEventListener('dragend', cleanup)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Initialize missing transport positions outside of render to avoid setState-during-render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(() => { days.forEach(day => initTransportPositions(day.id)) }, [days, reservations])
|
||||||
|
|
||||||
const toggleDay = (dayId, e) => {
|
const toggleDay = (dayId, e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setExpandedDays(prev => {
|
setExpandedDays(prev => {
|
||||||
@@ -490,11 +494,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
const dn = (dayNotes[String(dayId)] || []).slice().sort((a, b) => a.sort_order - b.sort_order)
|
const dn = (dayNotes[String(dayId)] || []).slice().sort((a, b) => a.sort_order - b.sort_order)
|
||||||
const transport = getTransportForDay(dayId)
|
const transport = getTransportForDay(dayId)
|
||||||
|
|
||||||
// Initialize positions for transports that don't have one yet
|
|
||||||
if (transport.some(r => r.day_plan_position == null)) {
|
|
||||||
initTransportPositions(dayId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// All places keep their order_index — untimed can be freely moved, timed auto-sort when time is set
|
// All places keep their order_index — untimed can be freely moved, timed auto-sort when time is set
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
...da.map(a => ({ type: 'place' as const, sortKey: a.order_index, data: a })),
|
...da.map(a => ({ type: 'place' as const, sortKey: a.order_index, data: a })),
|
||||||
@@ -1117,7 +1116,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tagesliste */}
|
{/* Tagesliste */}
|
||||||
<div className="scroll-container trek-stagger" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
<div className={`scroll-container${draggingId ? '' : ' trek-stagger'}`} style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
||||||
{days.map((day, index) => {
|
{days.map((day, index) => {
|
||||||
const isSelected = selectedDayId === day.id
|
const isSelected = selectedDayId === day.id
|
||||||
const isExpanded = expandedDays.has(day.id)
|
const isExpanded = expandedDays.has(day.id)
|
||||||
@@ -1135,7 +1134,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
{/* Tages-Header — akzeptiert Drops aus der PlacesSidebar */}
|
{/* Tages-Header — akzeptiert Drops aus der PlacesSidebar */}
|
||||||
<div
|
<div
|
||||||
onClick={() => { onSelectDay(day.id); if (onDayDetail) onDayDetail(day) }}
|
onClick={() => { onSelectDay(day.id); if (onDayDetail) onDayDetail(day) }}
|
||||||
onDragOver={e => { e.preventDefault(); setDragOverDayId(day.id) }}
|
onDragOver={e => { e.preventDefault(); if (dragOverDayId !== day.id) setDragOverDayId(day.id) }}
|
||||||
onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverDayId(null) }}
|
onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverDayId(null) }}
|
||||||
onDrop={e => handleDropOnDay(e, day.id)}
|
onDrop={e => handleDropOnDay(e, day.id)}
|
||||||
style={{
|
style={{
|
||||||
@@ -1349,7 +1348,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
>
|
>
|
||||||
{merged.length === 0 && !dayNoteUi ? (
|
{merged.length === 0 && !dayNoteUi ? (
|
||||||
<div
|
<div
|
||||||
onDragOver={e => { e.preventDefault(); setDragOverDayId(day.id) }}
|
onDragOver={e => { e.preventDefault(); if (dragOverDayId !== day.id) setDragOverDayId(day.id) }}
|
||||||
onDrop={e => handleDropOnDay(e, day.id)}
|
onDrop={e => handleDropOnDay(e, day.id)}
|
||||||
style={{ padding: '16px', textAlign: 'center', borderRadius: 8,
|
style={{ padding: '16px', textAlign: 'center', borderRadius: 8,
|
||||||
background: dragOverDayId === day.id ? 'rgba(17,24,39,0.05)' : 'transparent',
|
background: dragOverDayId === day.id ? 'rgba(17,24,39,0.05)' : 'transparent',
|
||||||
@@ -1409,7 +1408,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`place-${assignment.id}`}>
|
<React.Fragment key={`place-${assignment.id}`}>
|
||||||
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
|
|
||||||
<div
|
<div
|
||||||
draggable={canEditDays}
|
draggable={canEditDays}
|
||||||
onDragStart={e => {
|
onDragStart={e => {
|
||||||
@@ -1499,6 +1497,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
borderLeft: lockedIds.has(assignment.id)
|
borderLeft: lockedIds.has(assignment.id)
|
||||||
? '3px solid #dc2626'
|
? '3px solid #dc2626'
|
||||||
: '3px solid transparent',
|
: '3px solid transparent',
|
||||||
|
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
|
||||||
transition: 'background 0.15s, border-color 0.15s',
|
transition: 'background 0.15s, border-color 0.15s',
|
||||||
opacity: isDraggingThis ? 0.4 : 1,
|
opacity: isDraggingThis ? 0.4 : 1,
|
||||||
}}
|
}}
|
||||||
@@ -1722,7 +1721,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`transport-${res.id}-${day.id}`}>
|
<React.Fragment key={`transport-${res.id}-${day.id}`}>
|
||||||
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
|
|
||||||
<div
|
<div
|
||||||
onClick={() => canEditDays && onEditTransport?.(res)}
|
onClick={() => canEditDays && onEditTransport?.(res)}
|
||||||
onDragOver={e => {
|
onDragOver={e => {
|
||||||
@@ -1771,6 +1769,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
margin: '1px 8px',
|
margin: '1px 8px',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
border: `1px solid ${color}33`,
|
border: `1px solid ${color}33`,
|
||||||
|
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
|
||||||
|
borderBottom: showDropLineAfter ? '2px solid var(--text-primary)' : undefined,
|
||||||
background: `${color}08`,
|
background: `${color}08`,
|
||||||
cursor: canEditDays && onEditTransport ? 'pointer' : 'default', userSelect: 'none',
|
cursor: canEditDays && onEditTransport ? 'pointer' : 'default', userSelect: 'none',
|
||||||
transition: 'background 0.1s',
|
transition: 'background 0.1s',
|
||||||
@@ -1844,7 +1844,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
{showDropLineAfter && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1855,7 +1854,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
const noteIdx = idx
|
const noteIdx = idx
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`note-${note.id}`}>
|
<React.Fragment key={`note-${note.id}`}>
|
||||||
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
|
|
||||||
<div
|
<div
|
||||||
draggable={canEditDays}
|
draggable={canEditDays}
|
||||||
onDragStart={e => { if (!canEditDays) { e.preventDefault(); return } e.dataTransfer.setData('noteId', String(note.id)); e.dataTransfer.setData('fromDayId', String(day.id)); e.dataTransfer.effectAllowed = 'move'; dragDataRef.current = { noteId: String(note.id), fromDayId: String(day.id) }; setDraggingId(`note-${note.id}`) }}
|
onDragStart={e => { if (!canEditDays) { e.preventDefault(); return } e.dataTransfer.setData('noteId', String(note.id)); e.dataTransfer.setData('fromDayId', String(day.id)); e.dataTransfer.effectAllowed = 'move'; dragDataRef.current = { noteId: String(note.id), fromDayId: String(day.id) }; setDraggingId(`note-${note.id}`) }}
|
||||||
@@ -1911,6 +1909,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
margin: '1px 8px',
|
margin: '1px 8px',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
border: '1px solid var(--border-faint)',
|
border: '1px solid var(--border-faint)',
|
||||||
|
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
|
||||||
background: 'var(--bg-hover)',
|
background: 'var(--bg-hover)',
|
||||||
opacity: draggingId === `note-${note.id}` ? 0.4 : 1,
|
opacity: draggingId === `note-${note.id}` ? 0.4 : 1,
|
||||||
transition: 'background 0.1s', cursor: 'grab', userSelect: 'none',
|
transition: 'background 0.1s', cursor: 'grab', userSelect: 'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user