No edit summary |
No edit summary |
||
| (9 intermediate revisions by 2 users not shown) | |||
| Line 6: | Line 6: | ||
============================================================ */ | ============================================================ */ | ||
(function() { | (function() { | ||
const saved = localStorage.getItem('flatmmo-theme') || 'dark'; | |||
document.documentElement.className += ' ' + saved + '-theme'; | document.documentElement.className += ' ' + saved + '-theme'; | ||
const searchMode = localStorage.getItem('flatmmo-search') || 'top'; | |||
document.documentElement.className += ' search-' + searchMode; | document.documentElement.className += ' search-' + searchMode; | ||
| Line 23: | Line 23: | ||
// --- Theme toggle button --- | // --- Theme toggle button --- | ||
(function() { | (function() { | ||
let currentTheme = localStorage.getItem('flatmmo-theme') || 'dark'; | |||
document.body.classList.remove('dark-theme', 'light-theme'); | document.body.classList.remove('dark-theme', 'light-theme'); | ||
document.body.classList.add(currentTheme + '-theme'); | document.body.classList.add(currentTheme + '-theme'); | ||
const btn = document.createElement('button'); | |||
btn.className = 'theme-toggle-btn'; | btn.className = 'theme-toggle-btn'; | ||
btn.title = 'Toggle light/dark mode'; | btn.title = 'Toggle light/dark mode'; | ||
| Line 34: | Line 34: | ||
btn.addEventListener('click', function() { | btn.addEventListener('click', function() { | ||
const isDark = document.body.classList.contains('dark-theme'); | |||
const newTheme = isDark ? 'light' : 'dark'; | |||
document.body.classList.remove('dark-theme', 'light-theme'); | document.body.classList.remove('dark-theme', 'light-theme'); | ||
| Line 46: | Line 46: | ||
}); | }); | ||
const target = document.querySelector('.cosmos-header__wiki-buttons') | |||
|| document.querySelector('.cosmos-actions') | || document.querySelector('.cosmos-actions') | ||
|| document.querySelector('.wds-button-group') | || document.querySelector('.wds-button-group') | ||
| Line 61: | Line 61: | ||
// --- Search bar toggle + compact header search bar --- | // --- Search bar toggle + compact header search bar --- | ||
(function() { | (function() { | ||
const searchMode = localStorage.getItem('flatmmo-search') || 'top'; | |||
document.body.classList.remove('search-top', 'search-header'); | document.body.classList.remove('search-top', 'search-header'); | ||
| Line 73: | Line 73: | ||
} | } | ||
const searchToggle = document.createElement('button'); | |||
searchToggle.className = 'search-toggle-btn'; | searchToggle.className = 'search-toggle-btn'; | ||
searchToggle.title = 'Toggle search bar position'; | searchToggle.title = 'Toggle search bar position'; | ||
| Line 79: | Line 79: | ||
searchToggle.addEventListener('click', function() { | searchToggle.addEventListener('click', function() { | ||
const isTop = document.body.classList.contains('search-top'); | |||
const newMode = isTop ? 'header' : 'top'; | |||
document.body.classList.remove('search-top', 'search-header'); | document.body.classList.remove('search-top', 'search-header'); | ||
| Line 91: | Line 91: | ||
}); | }); | ||
const searchWrap = document.createElement('div'); | |||
searchWrap.className = 'header-search-wrap'; | searchWrap.className = 'header-search-wrap'; | ||
const searchInput = document.createElement('input'); | |||
searchInput.type = 'text'; | searchInput.type = 'text'; | ||
searchInput.className = 'header-search-input'; | searchInput.className = 'header-search-input'; | ||
| Line 100: | Line 100: | ||
searchInput.setAttribute('autocomplete', 'off'); | searchInput.setAttribute('autocomplete', 'off'); | ||
const searchBtn = document.createElement('button'); | |||
searchBtn.className = 'header-search-btn'; | searchBtn.className = 'header-search-btn'; | ||
searchBtn.innerHTML = '🔍'; | searchBtn.innerHTML = '🔍'; | ||
| Line 106: | Line 106: | ||
function doSearch() { | function doSearch() { | ||
const q = searchInput.value.trim(); | |||
if (q) { | if (q) { | ||
window.location.href = mw.util.getUrl('Special:Search') + '?search=' + encodeURIComponent(q); | window.location.href = mw.util.getUrl('Special:Search') + '?search=' + encodeURIComponent(q); | ||
| Line 120: | Line 120: | ||
searchWrap.appendChild(searchBtn); | searchWrap.appendChild(searchBtn); | ||
const target = document.querySelector('.cosmos-header__wiki-buttons') | |||
|| document.querySelector('.cosmos-actions') | || document.querySelector('.cosmos-actions') | ||
|| document.querySelector('.wds-button-group') | || document.querySelector('.wds-button-group') | ||
| Line 129: | Line 129: | ||
target.appendChild(searchWrap); | target.appendChild(searchWrap); | ||
const bannerUser = document.querySelector('.cosmos-userButton-label') | |||
|| document.querySelector('#p-personal-label'); | || document.querySelector('#p-personal-label'); | ||
const userName = bannerUser ? bannerUser.textContent.trim() : ''; | |||
if (userName) { | if (userName) { | ||
const userLink = document.createElement('a'); | |||
userLink.className = 'relocated-user'; | userLink.className = 'relocated-user'; | ||
userLink.href = mw.util.getUrl('User:' + userName); | userLink.href = mw.util.getUrl('User:' + userName); | ||
userLink.title = userName; | userLink.title = userName; | ||
const userIcon = document.createElement('span'); | |||
userIcon.className = 'relocated-user-icon'; | userIcon.className = 'relocated-user-icon'; | ||
userIcon.innerHTML = '👤'; | userIcon.innerHTML = '👤'; | ||
const userText = document.createElement('span'); | |||
userText.className = 'relocated-user-name'; | userText.className = 'relocated-user-name'; | ||
userText.textContent = userName; | userText.textContent = userName; | ||
| Line 201: | Line 201: | ||
$.getScript(mw.util.getUrl("MediaWiki:Update_DB.js") + "?action=raw&ctype=text/javascript"); | $.getScript(mw.util.getUrl("MediaWiki:Update_DB.js") + "?action=raw&ctype=text/javascript"); | ||
console.log("DB Updater Loaded"); | console.log("DB Updater Loaded"); | ||
} | |||
if (document.getElementById("xpCalculator")) { | |||
$.getScript(mw.util.getUrl("MediaWiki:XpCalculator.js") + "?action=raw&ctype=text/javascript"); | |||
console.log("XP Calculator Loaded"); | |||
} | } | ||
| Line 207: | Line 212: | ||
accuracy = parseInt(accuracy); | accuracy = parseInt(accuracy); | ||
defence = parseInt(defence); | defence = parseInt(defence); | ||
if (accuracy >= defence) { | if (accuracy >= defence) { | ||
return 100; | |||
} | } | ||
const diff = defence - accuracy; | |||
const hc = Math.max(100 - (diff * 10), 5); | |||
return hc.toFixed(2); | |||
} | } | ||
if (document.querySelector('.hitChance')) { | if (document.querySelector('.hitChance')) { | ||
document.querySelectorAll(".hitChance").forEach(function(el) { | document.querySelectorAll(".hitChance").forEach(function(el) { | ||
const parts = el.innerText.split(","); | |||
let acc = parts[0]; | |||
let def = parts[1]; | |||
el.innerText = ""; | el.innerText = ""; | ||
const accSpan = document.createElement("span"); | |||
const defSpan = document.createElement("span"); | |||
const hitSpan = document.createElement("span"); | |||
const accInput = document.createElement("input"); | |||
const defInput = document.createElement("input"); | |||
const hitValueSpan = document.createElement("span"); | |||
accSpan.innerText = "Accuracy:"; | accSpan.innerText = "Accuracy:"; | ||
| Line 260: | Line 267: | ||
// --- Page Preview Popup --- | // --- Page Preview Popup --- | ||
(function() { | (function() { | ||
let popup = null; | |||
let hideTimer = null; | |||
let showTimer = null; | |||
const previewCache = {}; | |||
const isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); | |||
function createPopup() { | function createPopup() { | ||
| Line 281: | Line 289: | ||
function positionPopup(e) { | function positionPopup(e) { | ||
// On mobile, let CSS handle positioning (fixed bottom sheet) | |||
if (isMobile) { | |||
popup.style.left = ''; | |||
popup.style.top = ''; | |||
return; | |||
} | |||
let x = e.pageX + 15; | |||
let y = e.pageY + 15; | |||
const popupWidth = 340; | |||
const popupHeight = 200; | |||
if (x + popupWidth > document.documentElement.scrollLeft + window.innerWidth) { | if (x + popupWidth > document.documentElement.scrollLeft + window.innerWidth) { | ||
| Line 303: | Line 317: | ||
} | } | ||
const apiUrl = mw.util.wikiScript('api') + '?action=parse&page=' + encodeURIComponent(title) + '&prop=text&format=json&redirects=1'; | |||
$.ajax({ | $.ajax({ | ||
| Line 310: | Line 324: | ||
success: function(data) { | success: function(data) { | ||
if (data.parse && data.parse.text) { | if (data.parse && data.parse.text) { | ||
const html = data.parse.text['*']; | |||
const temp = document.createElement('div'); | |||
temp.innerHTML = html; | temp.innerHTML = html; | ||
let img = null; | |||
const infoboxImg = temp.querySelector('.flatmmo-infobox-image img, .flatmmo-infobox img'); | |||
if (infoboxImg) { | if (infoboxImg) { | ||
img = infoboxImg.src; | img = infoboxImg.src; | ||
} else { | } else { | ||
const firstImg = temp.querySelector('img'); | |||
if (firstImg && firstImg.width > 20) { | if (firstImg && firstImg.width > 20) { | ||
img = firstImg.src; | img = firstImg.src; | ||
| Line 325: | Line 339: | ||
} | } | ||
let text = ''; | |||
const paragraphs = temp.querySelectorAll('p'); | |||
for ( | for (let i = 0; i < paragraphs.length; i++) { | ||
const t = paragraphs[i].textContent.trim(); | |||
if (t.length > 10) { | if (t.length > 10) { | ||
text = t; | text = t; | ||
| Line 339: | Line 353: | ||
} | } | ||
const result = { image: img, text: text, title: title }; | |||
previewCache[title] = result; | previewCache[title] = result; | ||
callback(result); | callback(result); | ||
| Line 350: | Line 364: | ||
} | } | ||
function buildPopupHTML(data | function buildPopupHTML(data) { | ||
let html = ''; | |||
html += '<div class="flatmmo-preview-title">' + data.title + '</div>'; | html += '<div class="flatmmo-preview-title">' + data.title + '</div>'; | ||
if (data.image) { | if (data.image) { | ||
| Line 366: | Line 377: | ||
} | } | ||
return html; | return html; | ||
} | } | ||
| Line 380: | Line 382: | ||
if (!popup) createPopup(); | if (!popup) createPopup(); | ||
const href = link.getAttribute('href'); | |||
if (!href) return; | if (!href) return; | ||
const match = href.match(/\/index\.php\/(.+)/); | |||
if (!match) return; | if (!match) return; | ||
const title = decodeURIComponent(match[1]).replace(/_/g, ' '); | |||
if (title.match(/^Special:|^Talk:|^User:|^Category:|^File:|^Template:|^MediaWiki:/i)) return; | if (title.match(/^Special:|^Talk:|^User:|^Category:|^File:|^Template:|^MediaWiki:/i)) return; | ||
if (href.indexOf('action=edit') !== -1) return; | if (href.indexOf('action=edit') !== -1) return; | ||
if (link.classList.contains('new')) return; | if (link.classList.contains('new')) return; | ||
positionPopup(e); | positionPopup(e); | ||
popup.innerHTML = ' | popup.innerHTML = '<div class="flatmmo-preview-title">' + title + '</div><div class="flatmmo-preview-loading">Loading...</div>'; | ||
popup.style.display = 'block'; | popup.style.display = 'block'; | ||
fetchPreview(title, function(data) { | fetchPreview(title, function(data) { | ||
popup.innerHTML = buildPopupHTML(data | popup.innerHTML = buildPopupHTML(data); | ||
}); | }); | ||
} | } | ||
const content = document.getElementById('mw-content-text') || document.getElementById('bodyContent') || document.body; | |||
// Strip all native tooltips from content links | // Strip all native tooltips from content links | ||
$(content).find(' | $(content).find('[title]').removeAttr('title'); | ||
if (isMobile) { | if (isMobile) { | ||
// Mobile: first tap shows preview, second tap follows link | // Mobile: first tap shows preview, second tap follows link | ||
$(content).on('click', 'a:not(.new)', function(e) { | $(content).on('click', 'a:not(.new)', function(e) { | ||
const link = this; | |||
if ($(link).closest('.flatmmo-preview').length) return; | if ($(link).closest('.flatmmo-preview').length) return; | ||
const href = link.getAttribute('href'); | |||
if (!href || !href.match(/\/index\.php\//)) return; | if (!href || !href.match(/\/index\.php\//)) return; | ||
if (href.indexOf('action=edit') !== -1) return; | if (href.indexOf('action=edit') !== -1) return; | ||
| Line 444: | Line 441: | ||
// Desktop: hover behavior | // Desktop: hover behavior | ||
$(content).on('mouseover', 'a', function(e) { | $(content).on('mouseover', 'a', function(e) { | ||
const link = this; | |||
if ($(link).closest('.flatmmo-preview').length) return; | if ($(link).closest('.flatmmo-preview').length) return; | ||
| Line 468: | Line 465: | ||
console.log('Page preview popup initialized'); | console.log('Page preview popup initialized'); | ||
})(); | |||
// --- Mobile table scroll fix --- | |||
(function() { | |||
if (window.innerWidth > 768) return; | |||
const tables = document.querySelectorAll('#mw-content-text table'); | |||
for (let i = 0; i < tables.length; i++) { | |||
const table = tables[i]; | |||
if (table.parentElement.classList.contains('table-scroll-wrap')) continue; | |||
table.style.overflow = 'visible'; | |||
const wrapper = document.createElement('div'); | |||
wrapper.className = 'table-scroll-wrap'; | |||
table.parentNode.insertBefore(wrapper, table); | |||
wrapper.appendChild(table); | |||
} | |||
})(); | })(); | ||
}); // end $(document).ready | }); // end $(document).ready | ||
Latest revision as of 17:01, 21 June 2026
1 /* Any JavaScript here will be loaded for all users on every page load. */
2
3 /* ============================================================
4 THEME TOGGLE — Dark/Light mode switcher
5 Defaults to dark. Saves preference to localStorage.
6 ============================================================ */
7 (function() {
8 const saved = localStorage.getItem('flatmmo-theme') || 'dark';
9 document.documentElement.className += ' ' + saved + '-theme';
10
11 const searchMode = localStorage.getItem('flatmmo-search') || 'top';
12 document.documentElement.className += ' search-' + searchMode;
13
14 document.addEventListener('DOMContentLoaded', function() {
15 document.body.classList.add(saved + '-theme');
16 document.body.classList.add('search-' + searchMode);
17 });
18 })();
19
20 $(document).ready(function () {
21 console.log("loaded");
22
23 // --- Theme toggle button ---
24 (function() {
25 let currentTheme = localStorage.getItem('flatmmo-theme') || 'dark';
26
27 document.body.classList.remove('dark-theme', 'light-theme');
28 document.body.classList.add(currentTheme + '-theme');
29
30 const btn = document.createElement('button');
31 btn.className = 'theme-toggle-btn';
32 btn.title = 'Toggle light/dark mode';
33 btn.textContent = currentTheme === 'dark' ? '☀️' : '🌙';
34
35 btn.addEventListener('click', function() {
36 const isDark = document.body.classList.contains('dark-theme');
37 const newTheme = isDark ? 'light' : 'dark';
38
39 document.body.classList.remove('dark-theme', 'light-theme');
40 document.body.classList.add(newTheme + '-theme');
41 document.documentElement.classList.remove('dark-theme', 'light-theme');
42 document.documentElement.classList.add(newTheme + '-theme');
43
44 localStorage.setItem('flatmmo-theme', newTheme);
45 btn.textContent = newTheme === 'dark' ? '☀️' : '🌙';
46 });
47
48 const target = document.querySelector('.cosmos-header__wiki-buttons')
49 || document.querySelector('.cosmos-actions')
50 || document.querySelector('.wds-button-group')
51 || document.querySelector('#content');
52 if (target) {
53 if (target.id === 'content') {
54 target.insertBefore(btn, target.firstChild);
55 } else {
56 target.appendChild(btn);
57 }
58 }
59 })();
60
61 // --- Search bar toggle + compact header search bar ---
62 (function() {
63 const searchMode = localStorage.getItem('flatmmo-search') || 'top';
64
65 document.body.classList.remove('search-top', 'search-header');
66 document.body.classList.add('search-' + searchMode);
67
68 if (searchMode === 'header') {
69 window.scrollTo(0, 0);
70 document.addEventListener('DOMContentLoaded', function() {
71 window.scrollTo(0, 0);
72 });
73 }
74
75 const searchToggle = document.createElement('button');
76 searchToggle.className = 'search-toggle-btn';
77 searchToggle.title = 'Toggle search bar position';
78 searchToggle.textContent = searchMode === 'top' ? '🔍' : '🔎';
79
80 searchToggle.addEventListener('click', function() {
81 const isTop = document.body.classList.contains('search-top');
82 const newMode = isTop ? 'header' : 'top';
83
84 document.body.classList.remove('search-top', 'search-header');
85 document.body.classList.add('search-' + newMode);
86 document.documentElement.classList.remove('search-top', 'search-header');
87 document.documentElement.classList.add('search-' + newMode);
88
89 localStorage.setItem('flatmmo-search', newMode);
90 searchToggle.textContent = newMode === 'top' ? '🔍' : '🔎';
91 });
92
93 const searchWrap = document.createElement('div');
94 searchWrap.className = 'header-search-wrap';
95
96 const searchInput = document.createElement('input');
97 searchInput.type = 'text';
98 searchInput.className = 'header-search-input';
99 searchInput.placeholder = 'Search wiki...';
100 searchInput.setAttribute('autocomplete', 'off');
101
102 const searchBtn = document.createElement('button');
103 searchBtn.className = 'header-search-btn';
104 searchBtn.innerHTML = '🔍';
105 searchBtn.title = 'Search';
106
107 function doSearch() {
108 const q = searchInput.value.trim();
109 if (q) {
110 window.location.href = mw.util.getUrl('Special:Search') + '?search=' + encodeURIComponent(q);
111 }
112 }
113
114 searchBtn.addEventListener('click', doSearch);
115 searchInput.addEventListener('keydown', function(e) {
116 if (e.key === 'Enter') doSearch();
117 });
118
119 searchWrap.appendChild(searchInput);
120 searchWrap.appendChild(searchBtn);
121
122 const target = document.querySelector('.cosmos-header__wiki-buttons')
123 || document.querySelector('.cosmos-actions')
124 || document.querySelector('.wds-button-group')
125 || document.querySelector('#content');
126 if (target && target.id !== 'content') {
127 target.appendChild(searchToggle);
128 target.style.position = 'relative';
129 target.appendChild(searchWrap);
130
131 const bannerUser = document.querySelector('.cosmos-userButton-label')
132 || document.querySelector('#p-personal-label');
133 const userName = bannerUser ? bannerUser.textContent.trim() : '';
134 if (userName) {
135 const userLink = document.createElement('a');
136 userLink.className = 'relocated-user';
137 userLink.href = mw.util.getUrl('User:' + userName);
138 userLink.title = userName;
139
140 const userIcon = document.createElement('span');
141 userIcon.className = 'relocated-user-icon';
142 userIcon.innerHTML = '👤';
143
144 const userText = document.createElement('span');
145 userText.className = 'relocated-user-name';
146 userText.textContent = userName;
147
148 userLink.appendChild(userIcon);
149 userLink.appendChild(userText);
150 target.appendChild(userLink);
151 }
152 }
153 })();
154
155 // --- Script loaders ---
156 $.getScript(mw.util.getUrl("MediaWiki:ProfileTags.js") + "?action=raw&ctype=text/javascript");
157
158 if (document.getElementById("interactiveMap")) {
159 console.log("map loaded");
160 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap/Objects.js") + "?action=raw&ctype=text/javascript", function() {
161 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap/NPCs.js") + "?action=raw&ctype=text/javascript", function() {
162 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap/Maps.js") + "?action=raw&ctype=text/javascript", function() {
163 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap.js") + "?action=raw&ctype=text/javascript", function() {
164 });
165 });
166 });
167 });
168 }
169
170 if (document.getElementById("wardrobe")) {
171 $.getScript(mw.util.getUrl("MediaWiki:Wardrobe.js") + "?action=raw&ctype=text/javascript");
172 console.log("wardrobe loaded");
173 }
174
175 if (document.getElementById("mapEditor")) {
176 $.getScript("https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js", function() {
177 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap/Objects.js") + "?action=raw&ctype=text/javascript", function() {
178 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap/NPCs.js") + "?action=raw&ctype=text/javascript", function() {
179 $.getScript(mw.util.getUrl("MediaWiki:InteractiveMap/Maps.js") + "?action=raw&ctype=text/javascript", function() {
180 $.getScript(mw.util.getUrl("MediaWiki:MapEditor.js") + "?action=raw&ctype=text/javascript", function() {
181 });
182 });
183 });
184 });
185 });
186 }
187
188 if (document.querySelector(".code")) {
189 $.getScript("../custom/highlight.min.js", function() {
190 document.querySelectorAll(".code").forEach(function(el) { hljs.highlightElement(el); });
191 console.log('Code highlighted');
192 });
193 }
194
195 if (document.querySelector('[class*="Tracker"]') && !document.querySelector('[class*="page-MediaWiki"]')) {
196 $.getScript(mw.util.getUrl("MediaWiki:Trackers.js") + "?action=raw&ctype=text/javascript");
197 console.log("Tracker loaded");
198 }
199
200 if (document.getElementById("enemiesDB")) {
201 $.getScript(mw.util.getUrl("MediaWiki:Update_DB.js") + "?action=raw&ctype=text/javascript");
202 console.log("DB Updater Loaded");
203 }
204
205 if (document.getElementById("xpCalculator")) {
206 $.getScript(mw.util.getUrl("MediaWiki:XpCalculator.js") + "?action=raw&ctype=text/javascript");
207 console.log("XP Calculator Loaded");
208 }
209
210 // --- Hit chance calculator ---
211 function hitChance(accuracy, defence) {
212 accuracy = parseInt(accuracy);
213 defence = parseInt(defence);
214
215 if (accuracy >= defence) {
216 return 100;
217 }
218
219 const diff = defence - accuracy;
220 const hc = Math.max(100 - (diff * 10), 5);
221
222 return hc.toFixed(2);
223 }
224
225 if (document.querySelector('.hitChance')) {
226 document.querySelectorAll(".hitChance").forEach(function(el) {
227 const parts = el.innerText.split(",");
228 let acc = parts[0];
229 let def = parts[1];
230 el.innerText = "";
231 const accSpan = document.createElement("span");
232 const defSpan = document.createElement("span");
233 const hitSpan = document.createElement("span");
234 const accInput = document.createElement("input");
235 const defInput = document.createElement("input");
236 const hitValueSpan = document.createElement("span");
237
238 accSpan.innerText = "Accuracy:";
239 defSpan.innerText = "Defence:";
240 hitSpan.innerText = "Hit Chance:";
241
242 accInput.type = "number";
243 defInput.type = "number";
244 accInput.min = 0;
245 defInput.min = 0;
246 if (acc !== "0") {
247 accInput.disabled = true;
248 }
249 if (def !== "0") {
250 defInput.disabled = true;
251 }
252 def = def < 0 ? 0 : def;
253 acc = acc < 0 ? 0 : acc;
254 accInput.defaultValue = acc;
255 defInput.defaultValue = def;
256 accInput.onchange = function() {
257 hitValueSpan.innerText = hitChance(accInput.value, defInput.value) + "%";
258 };
259 defInput.onchange = function() {
260 hitValueSpan.innerText = hitChance(accInput.value, defInput.value) + "%";
261 };
262 el.append(accSpan, accInput, defSpan, defInput, hitSpan, hitValueSpan);
263 hitValueSpan.innerText = hitChance(accInput.value, defInput.value) + "%";
264 });
265 }
266
267 // --- Page Preview Popup ---
268 (function() {
269 let popup = null;
270 let hideTimer = null;
271 let showTimer = null;
272 const previewCache = {};
273 const isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
274
275 function createPopup() {
276 popup = document.createElement('div');
277 popup.className = 'flatmmo-preview';
278 document.body.appendChild(popup);
279
280 popup.addEventListener('mouseenter', function() {
281 clearTimeout(hideTimer);
282 });
283 popup.addEventListener('mouseleave', function() {
284 hideTimer = setTimeout(function() {
285 popup.style.display = 'none';
286 }, 200);
287 });
288 }
289
290 function positionPopup(e) {
291 // On mobile, let CSS handle positioning (fixed bottom sheet)
292 if (isMobile) {
293 popup.style.left = '';
294 popup.style.top = '';
295 return;
296 }
297 let x = e.pageX + 15;
298 let y = e.pageY + 15;
299 const popupWidth = 340;
300 const popupHeight = 200;
301
302 if (x + popupWidth > document.documentElement.scrollLeft + window.innerWidth) {
303 x = e.pageX - popupWidth - 15;
304 }
305 if (y + popupHeight > document.documentElement.scrollTop + window.innerHeight) {
306 y = e.pageY - popupHeight - 15;
307 }
308
309 popup.style.left = x + 'px';
310 popup.style.top = y + 'px';
311 }
312
313 function fetchPreview(title, callback) {
314 if (previewCache[title]) {
315 callback(previewCache[title]);
316 return;
317 }
318
319 const apiUrl = mw.util.wikiScript('api') + '?action=parse&page=' + encodeURIComponent(title) + '&prop=text&format=json&redirects=1';
320
321 $.ajax({
322 url: apiUrl,
323 dataType: 'json',
324 success: function(data) {
325 if (data.parse && data.parse.text) {
326 const html = data.parse.text['*'];
327 const temp = document.createElement('div');
328 temp.innerHTML = html;
329
330 let img = null;
331 const infoboxImg = temp.querySelector('.flatmmo-infobox-image img, .flatmmo-infobox img');
332 if (infoboxImg) {
333 img = infoboxImg.src;
334 } else {
335 const firstImg = temp.querySelector('img');
336 if (firstImg && firstImg.width > 20) {
337 img = firstImg.src;
338 }
339 }
340
341 let text = '';
342 const paragraphs = temp.querySelectorAll('p');
343 for (let i = 0; i < paragraphs.length; i++) {
344 const t = paragraphs[i].textContent.trim();
345 if (t.length > 10) {
346 text = t;
347 break;
348 }
349 }
350
351 if (text.length > 250) {
352 text = text.substring(0, 247) + '...';
353 }
354
355 const result = { image: img, text: text, title: title };
356 previewCache[title] = result;
357 callback(result);
358 }
359 },
360 error: function() {
361 console.log('Preview request failed for:', title);
362 }
363 });
364 }
365
366 function buildPopupHTML(data) {
367 let html = '';
368 html += '<div class="flatmmo-preview-title">' + data.title + '</div>';
369 if (data.image) {
370 html += '<div class="flatmmo-preview-image"><img src="' + data.image + '"></div>';
371 }
372 if (data.text) {
373 html += '<div class="flatmmo-preview-text">' + data.text + '</div>';
374 }
375 if (!data.image && !data.text) {
376 html += '<div class="flatmmo-preview-text" style="color:#999;">No preview available.</div>';
377 }
378 return html;
379 }
380
381 function showPreview(link, e) {
382 if (!popup) createPopup();
383
384 const href = link.getAttribute('href');
385 if (!href) return;
386
387 const match = href.match(/\/index\.php\/(.+)/);
388 if (!match) return;
389
390 const title = decodeURIComponent(match[1]).replace(/_/g, ' ');
391
392 if (title.match(/^Special:|^Talk:|^User:|^Category:|^File:|^Template:|^MediaWiki:/i)) return;
393 if (href.indexOf('action=edit') !== -1) return;
394 if (link.classList.contains('new')) return;
395
396 positionPopup(e);
397 popup.innerHTML = '<div class="flatmmo-preview-title">' + title + '</div><div class="flatmmo-preview-loading">Loading...</div>';
398 popup.style.display = 'block';
399
400 fetchPreview(title, function(data) {
401 popup.innerHTML = buildPopupHTML(data);
402 });
403 }
404
405 const content = document.getElementById('mw-content-text') || document.getElementById('bodyContent') || document.body;
406
407 // Strip all native tooltips from content links
408 $(content).find('[title]').removeAttr('title');
409
410 if (isMobile) {
411 // Mobile: first tap shows preview, second tap follows link
412 $(content).on('click', 'a:not(.new)', function(e) {
413 const link = this;
414 if ($(link).closest('.flatmmo-preview').length) return;
415
416 const href = link.getAttribute('href');
417 if (!href || !href.match(/\/index\.php\//)) return;
418 if (href.indexOf('action=edit') !== -1) return;
419
420 if (link.getAttribute('data-preview-shown') === 'true') {
421 link.removeAttribute('data-preview-shown');
422 return;
423 }
424
425 e.preventDefault();
426 link.setAttribute('data-preview-shown', 'true');
427 showPreview(link, e);
428
429 setTimeout(function() {
430 link.removeAttribute('data-preview-shown');
431 }, 3000);
432 });
433
434 // Tap outside closes popup
435 $(document).on('click', function(e) {
436 if (popup && !$(e.target).closest('.flatmmo-preview').length && !$(e.target).closest('#mw-content-text a').length) {
437 popup.style.display = 'none';
438 }
439 });
440 } else {
441 // Desktop: hover behavior
442 $(content).on('mouseover', 'a', function(e) {
443 const link = this;
444 if ($(link).closest('.flatmmo-preview').length) return;
445
446 clearTimeout(hideTimer);
447 clearTimeout(showTimer);
448
449 if (link.getAttribute('title')) {
450 link.removeAttribute('title');
451 }
452
453 showTimer = setTimeout(function() {
454 showPreview(link, e);
455 }, 300);
456 });
457
458 $(content).on('mouseout', 'a', function() {
459 clearTimeout(showTimer);
460 hideTimer = setTimeout(function() {
461 if (popup) popup.style.display = 'none';
462 }, 200);
463 });
464 }
465
466 console.log('Page preview popup initialized');
467 })();
468
469 // --- Mobile table scroll fix ---
470 (function() {
471 if (window.innerWidth > 768) return;
472 const tables = document.querySelectorAll('#mw-content-text table');
473 for (let i = 0; i < tables.length; i++) {
474 const table = tables[i];
475 if (table.parentElement.classList.contains('table-scroll-wrap')) continue;
476 table.style.overflow = 'visible';
477 const wrapper = document.createElement('div');
478 wrapper.className = 'table-scroll-wrap';
479 table.parentNode.insertBefore(wrapper, table);
480 wrapper.appendChild(table);
481 }
482 })();
483
484 }); // end $(document).ready
