|
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 => ({'&':'&','<':'<','>':'>','"':'"'}[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
|
})()
|