Project

General

Profile

Feature #43462 » ai_generated_brain_dump.txt

Jimmy Westberg, 2025-11-11 08:46

 
1
Generated brain dump:
2

    
3
h1. Redmine History Timeline Navigator — Bracke Forest (v1.8.1)
4

    
5
This little script adds a floating “History Timeline” panel on Redmine issue pages.  
6
It lets you skim through all history notes, jump smoothly to any note, and filter by author, date or comment number.  
7
Super handy when you’re scrolling through long threads of updates.
8

    
9
h3. What’s new in 1.8.1
10

    
11
* No highlight flash on first load (just marks internally)
12
* Preview now skips duplicate or missing titles
13
* Pills section hides horizontal overflow and adds ellipsis
14
* Removed the old “bounce” animation – pills just scroll smoothly now
15

    
16
h3. How to test it
17

    
18
# Open any Redmine issue and go to the *History* tab (you need visible notes).
19
# Open your browser console and paste the JavaScript code below.
20
# You’ll see a “Quick Jump” button near the bottom center – click it to open the timeline.
21
# Try moving around with:
22
** n / j → next note  
23
** p / k → previous note  
24
** g → jump to comment number  
25
** Or use the buttons, filters, and slider at the top.
26

    
27
When you jump, the script scrolls to that note, briefly highlights it, and updates the counter.  
28
Hover over a pill and you’ll get a live preview (with image thumbnail if one exists).
29

    
30
h3. A few notes
31

    
32
* Works best with Redmine’s default markup (journals + notes).
33
* Custom themes that change the DOM might need selector tweaks in the constants section.
34
* You can toggle “Only visible notes” and “Only long notes” to reduce clutter.
35

    
36
If you want to remove it, just run this in the console:
37
    window.__BF_HistoryTimeline?.destroy()
38

    
39
h3. Keyboard shortcuts
40

    
41
* n / j → next note
42
* p / k → previous note
43
* g → go to a specific comment number
44

    
45
---
46

    
47
h2. JavaScript Code
48

    
49
    // Redmine History Timeline Navigator — Bracke Forest (v1.8.1)
50
    // - No visual highlight on first init (internal mark only)
51
    // - Preview de-dup (no title if missing/duplicated)
52
    // - bf-pills: overflow-x:hidden + ellipsis
53
    // - Removed "bounce"; pills now scroll into view without animation
54

    
55
    (function () {
56
      const CONFIG = {
57
        PANEL_WIDTH: 280,
58
        LEFT_OFFSET: 6,
59
        BOTTOM_OFFSET: 60,
60
        NOTE_TARGET_PAD: 80,
61
        LONG_THRESHOLD: 500,
62
        PREVIEW_MAX_CHARS: 220,
63
        PREVIEW_IMG_MAX_W: 160,
64
        PREVIEW_IMG_MAX_H: 100,
65
        HIGHLIGHT_RING: '0 0 0 2px rgba(37,99,235,.85)',
66
        HIGHLIGHT_FADE_MS: 700
67
      };
68

    
69
      const HISTORY_SEL = '#tab-content-history';
70
      const NOTE_SEL = '.note[id^="note-"]';
71
      const JOURNAL_SEL = '.journal';
72
      const PANEL_ID = 'bf-history-timeline-panel';
73
      const STYLE_ID = PANEL_ID + '-style';
74
      const LAUNCH_ID = 'bf-history-launcher';
75
      const HOVERZONE_ID = 'bf-history-hoverzone';
76
      const PREVIEW_ID = 'bf-history-preview';
77
      const HINT_ID = 'bf-history-hint';
78
      const NOTE_HL_CLASS = 'bf-note-highlight';
79

    
80
      if (window.__BF_HistoryTimeline?.destroy) {
81
        try { window.__BF_HistoryTimeline.destroy(); } catch(e){}
82
      }
83

    
84
      function ready(fn){
85
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
86
          setTimeout(fn, 0);
87
        } else {
88
          document.addEventListener('DOMContentLoaded', fn, { once: true });
89
        }
90
      }
91

    
92
      ready(bootstrap);
93

    
94
      let retryTimer = null;
95
      function bootstrap() {
96
        const tryInit = () => {
97
          const historyRoot = document.querySelector(HISTORY_SEL);
98
          if (!historyRoot) return false;
99
          init(historyRoot);
100
          return true;
101
        };
102
        if (!tryInit()) {
103
          let tries = 0;
104
          retryTimer = setInterval(() => {
105
            tries++;
106
            if (tryInit() || tries > 50) { clearInterval(retryTimer); retryTimer = null; }
107
          }, 200);
108
        }
109
      }
110

    
111
      function init(historyRoot){
112
        [PANEL_ID, STYLE_ID, LAUNCH_ID, HOVERZONE_ID, PREVIEW_ID, HINT_ID].forEach(id => document.getElementById(id)?.remove());
113
        document.querySelectorAll('.' + NOTE_HL_CLASS).forEach(el => el.classList.remove(NOTE_HL_CLASS));
114
        const esc = (s) => (s || '').replace(/[&<>"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
115
        const by = (sel, root=document) => root.querySelector(sel);
116

    
117
        function jumpTo(el) {
118
          if (!el) return;
119
          const journal = el.closest(JOURNAL_SEL);
120
          if (journal && journal.style && journal.style.display === 'none') journal.style.display = '';
121
          const rect = el.getBoundingClientRect();
122
          const y = window.scrollY + rect.top - CONFIG.NOTE_TARGET_PAD;
123
          window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
124
          highlight(el);
125
          setActiveIndex(indexOf(el));
126
        }
127

    
128
        // (rest of script continues unchanged)
129
      }
130
    })()
(2-2/10)