PageSourceSearch

https://www.vicbug.app/v2/routing/undo-redo.js?v=284

js vicbug.app collected 2026-09-24 13:55:10 UTC 5,120 bytes, 163 lines download raw bytes

1/**
2 * Undo/Redo — Waypoint state snapshots with history navigation.
3 */
4
5import { eventBus } from '../core/event-bus.js?v=284';
6import { store } from '../core/state.js?v=284';
7import { getWaypoints, restoreWaypoints } from './waypoints.js?v=284';
8import { calculateRoute, clearRoute } from './route-engine.js?v=284';
9
10const MAX_HISTORY = 50;
11
12let waypointsHistory = [[]]; // Start with empty snapshot
13let historyIndex = 0;
14let isUndoRedo = false; // Prevent recursive saves during restore
15
16// ─── Public API ───
17
18export function initUndoRedo() {
19  const undoBtn = document.getElementById('undo-btn');
20  const redoBtn = document.getElementById('redo-btn');
21
22  if (undoBtn) undoBtn.addEventListener('click', undo);
23  if (redoBtn) redoBtn.addEventListener('click', redo);
24
25  // Keyboard shortcuts
26  document.addEventListener('keydown', (e) => {
27    // Cmd+Z / Ctrl+Z = undo
28    if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
29      // Don't capture if focused on an input
30      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
31      e.preventDefault();
32      undo();
33    }
34    // Cmd+Shift+Z / Ctrl+Shift+Z or Cmd+Y / Ctrl+Y = redo
35    if ((e.metaKey || e.ctrlKey) && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) {
36      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
37      e.preventDefault();
38      redo();
39    }
40  });
41
42  // Save snapshot whenever waypoints change
43  eventBus.on('waypoint:changed', () => {
44    if (isUndoRedo) return; // Skip saves during undo/redo restore
45    saveSnapshot(getWaypoints());
46  });
47
48  // GPX split: save snapshot without triggering route recalculation
49  eventBus.on('waypoint:gpxSplit', () => {
50    saveSnapshot(getWaypoints());
51  });
52
53  eventBus.on('history:commit', ({ before, after } = {}) => {
54    if (before) {
55      // Freeze the pre-edit geometry so undo does not ask the router to redraw it.
56      waypointsHistory[historyIndex] = structuredClone(before);
57    }
58    saveSnapshot(after || getWaypoints());
59  });
60
61  // Route loaded (saved route open, GPX import): those paths restore waypoints
62  // without emitting waypoint:changed, so no baseline snapshot exists — the
63  // first undo after loading would step back to empty (or to a previously
64  // built route's stale history). Reset and seed with the loaded state so
65  // undo reverts edits made ON the loaded route.
66  eventBus.on('history:seed', () => {
67    waypointsHistory = [[]];
68    historyIndex = 0;
69    const current = getWaypoints();
70    if (current.length > 0) saveSnapshot(current);
71    updateButtons();
72  });
73
74  updateButtons();
75}
76
77// ─── Core ───
78
79function saveSnapshot(waypoints) {
80  // Deep copy the waypoints array (including gpxCoords for GPX-imported routes)
81  const snapshot = waypoints.map(wp => ({
82    id: wp.id,
83    lngLat: wp.lngLat ? [...wp.lngLat] : null,
84    address: wp.address || '',
85    profile: wp.profile || null,
86    gapToNext: wp.gapToNext || false,
87    gpxTimes: wp.gpxTimes ? [...wp.gpxTimes] : undefined,
88    gpxCoords: wp.gpxCoords ? wp.gpxCoords.map(c => [...c]) : undefined,
89    gpxRoadClasses: wp.gpxRoadClasses ? [...wp.gpxRoadClasses] : undefined,
90    gpxSurfaces: wp.gpxSurfaces ? [...wp.gpxSurfaces] : undefined,
91    gpxLts: wp.gpxLts ? [...wp.gpxLts] : undefined,
92    gpxBikePathAccess: wp.gpxBikePathAccess ? [...wp.gpxBikePathAccess] : undefined,
93    gpxBikeInfra: wp.gpxBikeInfra ? [...wp.gpxBikeInfra] : undefined,
94  }));
95
96  // Truncate any future states (user made a new action after undoing)
97  waypointsHistory = waypointsHistory.slice(0, historyIndex + 1);
98
99  // Push new snapshot
100  waypointsHistory.push(snapshot);
101
102  // Cap history size
103  if (waypointsHistory.length > MAX_HISTORY) {
104    waypointsHistory.shift();
105  }
106
107  historyIndex = waypointsHistory.length - 1;
108  updateButtons();
109}
110
111function undo() {
112  if (historyIndex <= 0) return;
113
114  const previousSnapshot = waypointsHistory[historyIndex - 1];
115  const hasActiveRoute = !!store.get('routeGeometry');
116
117  // If there's an active route and undo would leave 0 waypoints, warn before deleting
118  if (hasActiveRoute && previousSnapshot.length === 0) {
119    if (!confirm('This will delete the entire route. Are you sure?')) return;
120  }
121
122  historyIndex--;
123
124  isUndoRedo = true;
125  restoreWaypoints(previousSnapshot);
126  if (previousSnapshot.length < 2) {
127    clearRoute();
128  } else {
129    calculateRoute();
130  }
131  isUndoRedo = false;
132
133  updateButtons();
134}
135
136function redo() {
137  if (historyIndex >= waypointsHistory.length - 1) return;
138
139  historyIndex++;
140  const snapshot = waypointsHistory[historyIndex];
141
142  isUndoRedo = true;
143  restoreWaypoints(snapshot);
144  calculateRoute();
145  isUndoRedo = false;
146
147  updateButtons();
148}
149
150function updateButtons() {
151  const canUndo = historyIndex > 0;
152  const canRedo = historyIndex < waypointsHistory.length - 1;
153
154  const undoBtn = document.getElementById('undo-btn');
155  const redoBtn = document.getElementById('redo-btn');
156
157  if (undoBtn) {
158    undoBtn.style.display = canUndo ? 'inline-flex' : 'none';
159  }
160  if (redoBtn) {
161    redoBtn.style.display = canRedo ? 'inline-flex' : 'none';
162  }
163}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.