PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/history-panel.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:11 UTC 6,459 bytes, 176 lines download raw bytes

1/**
2 * History-panel subsystem — the floating frosted list of labeled
3 * undo/redo entries that hangs off the topbar History button.
4 *
5 * Same docking pattern as the FX adjustment popups: drag the head to
6 * reposition, click the minimise button to dock into the modalManager
7 * chip chain, click the chip to restore. Esc closes.
8 *
9 * @param {{
10 *   undo: () => void,
11 *   redo: () => void,
12 * }} deps
13 *
14 * @returns {{
15 *   toggleHistoryPanel:        () => void,
16 *   refreshHistoryPanelIfOpen: () => void,
17 *   jumpToHistory:             (offset: number) => void,
18 * }}
19 */
20import { state } from './state.js';
21import modalManager from '../modalManager.js';
22import { HISTORY_ICON, relTime } from './layer-helpers.js';
23import { historyPanelHTML } from './build/popups.js';
24
25export function createHistoryPanel({ undo, redo }) {
26  function jumpToHistory(offset) {
27    if (offset === 0) return;
28    if (offset < 0) {
29      for (let i = 0; i < -offset; i++) undo();
30    } else {
31      for (let i = 0; i < offset; i++) redo();
32    }
33  }
34
35  function closeHistoryPanel() {
36    if (state.historyPanelEl) {
37      if (state.historyPanelEl._escHandler) {
38        document.removeEventListener('keydown', state.historyPanelEl._escHandler, true);
39      }
40      if (state.historyPanelEl._awayHandler) {
41        document.removeEventListener('pointerdown', state.historyPanelEl._awayHandler, true);
42      }
43      state.historyPanelEl.remove();
44      state.historyPanelEl = null;
45    }
46  }
47
48  function minimiseHistoryPanel() {
49    if (!state.historyPanelEl) return;
50    const panel = state.historyPanelEl;
51    const r = panel.getBoundingClientRect();
52    panel._stashLeft = r.left;
53    panel._stashTop  = r.top;
54    panel.style.display = 'none';
55    state.historyPanelEl = null;
56    const modalId = panel._modalId || 'ge-history-panel-min';
57    panel._modalId = modalId;
58    modalManager.register(modalId, {
59      label: 'History',
60      icon: HISTORY_ICON,
61      restoreFn: () => {
62        panel.style.left = panel._stashLeft + 'px';
63        panel.style.top  = panel._stashTop  + 'px';
64        panel.style.display = '';
65        state.historyPanelEl = panel;
66        refreshHistoryPanelIfOpen();
67      },
68      closeFn: () => {
69        panel.remove();
70        modalManager.unregister(modalId);
71      },
72    });
73    modalManager.minimize(modalId);
74  }
75
76  function toggleHistoryPanel() {
77    if (state.historyPanelEl) { closeHistoryPanel(); return; }
78    const panel = document.createElement('div');
79    panel.id = 'ge-history-panel';
80    panel.className = 'ge-frosted';
81    panel.innerHTML = historyPanelHTML(HISTORY_ICON);
82    document.body.appendChild(panel);
83    state.historyPanelEl = panel;
84    const btn = document.getElementById('ge-history-btn');
85    if (btn) {
86      const r = btn.getBoundingClientRect();
87      panel.style.top  = (r.bottom + 6) + 'px';
88      panel.style.left = Math.max(8, r.left) + 'px';
89    }
90    panel.querySelector('.ge-adj-min').addEventListener('click', minimiseHistoryPanel);
91    // Click anywhere outside the panel (or trigger button) closes it.
92    setTimeout(() => {
93      const onAway = (ev) => {
94        if (!state.historyPanelEl) return;
95        if (state.historyPanelEl.contains(ev.target)) return;
96        if (btn && (ev.target === btn || btn.contains(ev.target))) return;
97        closeHistoryPanel();
98        document.removeEventListener('pointerdown', onAway, true);
99      };
100      document.addEventListener('pointerdown', onAway, true);
101      panel._awayHandler = onAway;
102    }, 0);
103
104    const head = panel.querySelector('[data-history-drag]');
105    head.addEventListener('pointerdown', (e) => {
106      if (e.target.closest('button')) return;
107      e.preventDefault();
108      const startX = e.clientX, startY = e.clientY;
109      const r0 = panel.getBoundingClientRect();
110      head.setPointerCapture(e.pointerId);
111      head.style.cursor = 'grabbing';
112      const onMove = (ev) => {
113        const nx = Math.max(0, Math.min(window.innerWidth - 60, r0.left + (ev.clientX - startX)));
114        const ny = Math.max(0, Math.min(window.innerHeight - 30, r0.top  + (ev.clientY - startY)));
115        panel.style.left = nx + 'px';
116        panel.style.top  = ny + 'px';
117      };
118      const onUp = () => {
119        head.releasePointerCapture(e.pointerId);
120        head.style.cursor = '';
121        head.removeEventListener('pointermove', onMove);
122        head.removeEventListener('pointerup', onUp);
123      };
124      head.addEventListener('pointermove', onMove);
125      head.addEventListener('pointerup', onUp);
126    });
127
128    const onKey = (ev) => {
129      if (ev.key === 'Escape') {
130        ev.preventDefault();
131        ev.stopPropagation();
132        closeHistoryPanel();
133      }
134    };
135    document.addEventListener('keydown', onKey, true);
136    panel._escHandler = onKey;
137
138    refreshHistoryPanelIfOpen();
139  }
140
141  function refreshHistoryPanelIfOpen() {
142    if (!state.historyPanelEl) return;
143    const list = state.historyPanelEl.querySelector('#ge-history-list');
144    if (!list) return;
145    // Chronological order — oldest at top, latest at bottom. Past
146    // (undo) states first, then Current, then future (redo) states.
147    const rows = [];
148    for (let i = 0; i < state.undoStack.length; i++) {
149      const s = state.undoStack[i];
150      rows.push({ offset: -(state.undoStack.length - i), label: s._label || 'Edit', ts: s._ts });
151    }
152    rows.push({ offset: 0, label: 'Current', ts: Date.now(), current: true });
153    for (let i = state.redoStack.length - 1; i >= 0; i--) {
154      const s = state.redoStack[i];
155      rows.push({ offset: (state.redoStack.length - i), label: s._label || 'Edit', ts: s._ts, future: true });
156    }
157    list.innerHTML = rows.map(r => `
158    <button class="ge-history-row${r.current ? ' current' : ''}${r.future ? ' future' : ''}" data-offset="${r.offset}">
159      <span class="ge-history-row-dot"></span>
160      <span class="ge-history-row-label">${(r.label || '').replace(/[<>&]/g,'')}</span>
161      <span class="ge-history-row-time">${relTime(r.ts)}</span>
162    </button>
163  `).join('');
164    list.querySelectorAll('.ge-history-row').forEach(btn => {
165      btn.addEventListener('click', () => {
166        const off = parseInt(btn.dataset.offset, 10);
167        jumpToHistory(off);
168      });
169    });
170    // Scroll the current marker into view.
171    const cur = list.querySelector('.current');
172    if (cur) cur.scrollIntoView({ block: 'center' });
173  }
174
175  return { toggleHistoryPanel, refreshHistoryPanelIfOpen, jumpToHistory };
176}

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.