|
4 | 4 | * Replaces all occurrences of <your-unraid-ip> in text and code blocks |
5 | 5 | * with a user-provided IP address. Persists via localStorage. |
6 | 6 | * |
7 | | - * Security: User input is escaped via escapeHtml() before insertion. |
8 | | - * Original DOM content is preserved from MkDocs-rendered pages only. |
| 7 | + * The input bar is rendered via MkDocs Material template override (announce block), |
| 8 | + * so it persists across page navigations without re-injection. |
9 | 9 | * |
| 10 | + * Security: User input is escaped via escapeHtml() before insertion. |
10 | 11 | * Browser-only — no data is stored or transmitted. |
11 | 12 | */ |
12 | 13 | (function () { |
|
15 | 16 | var STORAGE_KEY = "immich-guide-unraid-ip"; |
16 | 17 | var PLACEHOLDER_RE = /<your-unraid-ip>|<your-unraid-ip>/g; |
17 | 18 |
|
18 | | - /** Escape HTML special characters to prevent XSS from user input. */ |
19 | 19 | function escapeHtml(str) { |
20 | 20 | var div = document.createElement("div"); |
21 | 21 | div.appendChild(document.createTextNode(str)); |
22 | 22 | return div.innerHTML; |
23 | 23 | } |
24 | 24 |
|
25 | | - /** Build and inject the input bar below the header. */ |
26 | | - function createInputBar() { |
27 | | - var bar = document.createElement("div"); |
28 | | - bar.className = "ip-replacer"; |
29 | | - |
30 | | - var label = document.createElement("label"); |
31 | | - label.setAttribute("for", "ip-input"); |
32 | | - label.textContent = "Your Unraid IP:"; |
33 | | - |
34 | | - var input = document.createElement("input"); |
35 | | - input.id = "ip-input"; |
36 | | - input.type = "text"; |
37 | | - input.placeholder = "192.168.1.x"; |
38 | | - input.autocomplete = "off"; |
39 | | - input.spellcheck = false; |
40 | | - |
41 | | - var resetBtn = document.createElement("button"); |
42 | | - resetBtn.id = "ip-reset"; |
43 | | - resetBtn.type = "button"; |
44 | | - resetBtn.title = "Reset to placeholder"; |
45 | | - resetBtn.textContent = "Reset"; |
46 | | - |
47 | | - var disclaimer = document.createElement("span"); |
48 | | - disclaimer.className = "ip-replacer-disclaimer"; |
49 | | - disclaimer.textContent = "Browser-only \u2014 no data is stored or transmitted."; |
50 | | - |
51 | | - bar.appendChild(label); |
52 | | - bar.appendChild(input); |
53 | | - bar.appendChild(resetBtn); |
54 | | - bar.appendChild(disclaimer); |
55 | | - |
56 | | - var header = document.querySelector(".md-header"); |
57 | | - if (header && header.nextSibling) { |
58 | | - header.parentNode.insertBefore(bar, header.nextSibling); |
59 | | - } else { |
60 | | - document.body.prepend(bar); |
61 | | - } |
62 | | - |
63 | | - return bar; |
64 | | - } |
65 | | - |
66 | | - /** |
67 | | - * Snapshot original innerHTML of elements that contain the placeholder. |
68 | | - * We store originals from the MkDocs-rendered DOM (trusted content). |
69 | | - */ |
70 | 25 | function snapshotOriginals() { |
71 | 26 | var selectors = |
72 | 27 | ".md-content p, .md-content li, .md-content td, .md-content th, " + |
73 | 28 | ".md-content code, .md-content pre, .md-content a, .md-content h1, " + |
74 | 29 | ".md-content h2, .md-content h3, .md-content h4"; |
75 | 30 | var elements = document.querySelectorAll(selectors); |
76 | 31 | var targets = []; |
77 | | - |
78 | 32 | for (var i = 0; i < elements.length; i++) { |
79 | 33 | var el = elements[i]; |
80 | 34 | PLACEHOLDER_RE.lastIndex = 0; |
81 | 35 | if (PLACEHOLDER_RE.test(el.innerHTML)) { |
82 | | - if (!el.dataset.ipOriginal) { |
83 | | - el.dataset.ipOriginal = el.innerHTML; |
84 | | - } |
| 36 | + el.dataset.ipOriginal = el.innerHTML; |
85 | 37 | targets.push(el); |
86 | 38 | } |
87 | 39 | } |
88 | 40 | return targets; |
89 | 41 | } |
90 | 42 |
|
91 | | - /** |
92 | | - * Replace placeholder with given IP in all target elements. |
93 | | - * User input is escaped to prevent XSS before being placed into a <span>. |
94 | | - * The base content comes from dataset.ipOriginal (trusted MkDocs DOM). |
95 | | - */ |
96 | 43 | function applyReplacement(targets, ip) { |
97 | 44 | var safeIp = escapeHtml(ip.trim()); |
98 | 45 | for (var i = 0; i < targets.length; i++) { |
|
111 | 58 | } |
112 | 59 | } |
113 | 60 |
|
114 | | - /** Restore all originals from trusted snapshots. */ |
115 | 61 | function restoreOriginals(targets) { |
116 | 62 | for (var i = 0; i < targets.length; i++) { |
117 | 63 | var el = targets[i]; |
|
121 | 67 | } |
122 | 68 | } |
123 | 69 |
|
124 | | - /** Initialize on page load and MkDocs instant navigation. */ |
| 70 | + function applyFromStorage() { |
| 71 | + var saved = ""; |
| 72 | + try { saved = localStorage.getItem(STORAGE_KEY) || ""; } catch (e) {} |
| 73 | + var targets = snapshotOriginals(); |
| 74 | + if (saved) applyReplacement(targets, saved); |
| 75 | + return { targets: targets, saved: saved }; |
| 76 | + } |
| 77 | + |
125 | 78 | function init() { |
126 | | - var bar = document.querySelector(".ip-replacer") || createInputBar(); |
127 | 79 | var input = document.getElementById("ip-input"); |
128 | 80 | var resetBtn = document.getElementById("ip-reset"); |
| 81 | + var infoBtn = document.getElementById("ip-info-btn"); |
| 82 | + var tooltip = document.getElementById("ip-tooltip"); |
129 | 83 |
|
130 | | - // Restore saved value |
131 | | - var saved = ""; |
132 | | - try { |
133 | | - saved = localStorage.getItem(STORAGE_KEY) || ""; |
134 | | - } catch (e) { |
135 | | - // localStorage unavailable (private browsing etc.) |
136 | | - } |
137 | | - if (input) { |
138 | | - input.value = saved; |
139 | | - } |
| 84 | + if (!input) return; |
140 | 85 |
|
141 | | - // Snapshot and apply |
142 | | - var targets = snapshotOriginals(); |
143 | | - if (saved) { |
144 | | - applyReplacement(targets, saved); |
145 | | - } |
| 86 | + // Restore saved IP into input field |
| 87 | + var state = applyFromStorage(); |
| 88 | + input.value = state.saved; |
146 | 89 |
|
147 | | - // Input handler |
148 | | - if (input) { |
149 | | - input.addEventListener("input", function () { |
150 | | - var val = input.value; |
151 | | - try { |
152 | | - localStorage.setItem(STORAGE_KEY, val); |
153 | | - } catch (e) { |
154 | | - // ignore |
155 | | - } |
156 | | - targets = snapshotOriginals(); |
157 | | - applyReplacement(targets, val); |
| 90 | + // Tooltip toggle |
| 91 | + if (infoBtn && tooltip) { |
| 92 | + infoBtn.addEventListener("click", function (e) { |
| 93 | + e.stopPropagation(); |
| 94 | + tooltip.classList.toggle("ip-replacer-tooltip--visible"); |
| 95 | + }); |
| 96 | + document.addEventListener("click", function () { |
| 97 | + tooltip.classList.remove("ip-replacer-tooltip--visible"); |
158 | 98 | }); |
159 | 99 | } |
160 | 100 |
|
| 101 | + // IP input handler |
| 102 | + input.addEventListener("input", function () { |
| 103 | + var val = input.value; |
| 104 | + try { localStorage.setItem(STORAGE_KEY, val); } catch (e) {} |
| 105 | + var targets = snapshotOriginals(); |
| 106 | + applyReplacement(targets, val); |
| 107 | + }); |
| 108 | + |
161 | 109 | // Reset handler |
162 | 110 | if (resetBtn) { |
163 | 111 | resetBtn.addEventListener("click", function () { |
164 | | - if (input) { |
165 | | - input.value = ""; |
166 | | - } |
167 | | - try { |
168 | | - localStorage.removeItem(STORAGE_KEY); |
169 | | - } catch (e) { |
170 | | - // ignore |
171 | | - } |
| 112 | + input.value = ""; |
| 113 | + try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} |
| 114 | + var targets = snapshotOriginals(); |
172 | 115 | restoreOriginals(targets); |
173 | 116 | }); |
174 | 117 | } |
175 | 118 | } |
176 | 119 |
|
177 | | - // Run on initial load |
| 120 | + // Run on initial page load |
178 | 121 | if (document.readyState === "loading") { |
179 | 122 | document.addEventListener("DOMContentLoaded", init); |
180 | 123 | } else { |
181 | 124 | init(); |
182 | 125 | } |
183 | | - |
184 | | - // Re-run on MkDocs Material instant navigation (SPA page changes) |
185 | | - document.addEventListener("DOMContentSwap", function () { |
186 | | - setTimeout(function () { |
187 | | - var saved = ""; |
188 | | - try { |
189 | | - saved = localStorage.getItem(STORAGE_KEY) || ""; |
190 | | - } catch (e) { |
191 | | - // ignore |
192 | | - } |
193 | | - var targets = snapshotOriginals(); |
194 | | - if (saved) { |
195 | | - applyReplacement(targets, saved); |
196 | | - } |
197 | | - }, 50); |
198 | | - }); |
199 | 126 | })(); |
0 commit comments