MediaWiki:Common.js: відмінності між версіями
Admin (обговорення | внесок) (Back-лінк: над титульним блоком окремим рядком) |
Admin (обговорення | внесок) (Аналітика: Matomo-трекер замість простого beacon (cookieless, DNT)) |
||
| (Не показано 8 проміжних версій цього користувача) | |||
| Рядок 358: | Рядок 358: | ||
}); | }); | ||
}); | }); | ||
/* === Країнопедія: віджет «Повідомити про правку» (krai-feedback) === */ | |||
mw.loader.using(["mediawiki.util"], function () { | |||
$(function () { | |||
if (mw.config.get("wgNamespaceNumber") !== 0) return; // тільки статті | |||
if (mw.config.get("wgAction") !== "view") return; // тільки читання | |||
if (document.getElementById("krai-fb-btn")) return; | |||
var ENDPOINT = "/feedback.php"; | |||
var loadedAt = Math.floor(Date.now() / 1000); // ts фіксуємо на завантаженні (>3с завжди) | |||
var overlay = null; | |||
var content = document.getElementById("mw-content-text") || document.body; | |||
var lastSelText = ""; | |||
// --- плаваюча кнопка (коли нічого не виділено) --- | |||
var btn = document.createElement("button"); | |||
btn.id = "krai-fb-btn"; | |||
btn.type = "button"; | |||
btn.className = "k-btn k-btn--quiet krai-fb-btn"; | |||
btn.textContent = "✏️ Знайшли неточність?"; | |||
document.body.appendChild(btn); | |||
btn.addEventListener("click", function () { openModal(); }); | |||
// --- мікроменю над виділеним текстом --- | |||
var tip = document.createElement("button"); | |||
tip.id = "krai-fb-tip"; | |||
tip.type = "button"; | |||
tip.className = "k-btn k-btn--quiet krai-fb-tip"; | |||
tip.textContent = "✏️ Запропонувати правку"; | |||
tip.style.display = "none"; | |||
document.body.appendChild(tip); | |||
function selectionInContent(sel) { | |||
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return false; | |||
return sel.anchorNode && content.contains(sel.anchorNode); | |||
} | |||
function hideTip() { tip.style.display = "none"; } | |||
function showTip() { | |||
var sel = window.getSelection(); | |||
var text = ((sel && sel.toString()) || "").trim(); | |||
if (!selectionInContent(sel) || text.length < 8) { hideTip(); return; } | |||
lastSelText = text.slice(0, 1000); | |||
var rect = sel.getRangeAt(0).getBoundingClientRect(); | |||
if (!rect || (!rect.width && !rect.height)) { hideTip(); return; } | |||
tip.style.display = "block"; | |||
var vw = document.documentElement.clientWidth; | |||
var top = rect.top + window.scrollY - tip.offsetHeight - 8; | |||
var left = rect.left + window.scrollX + (rect.width / 2) - (tip.offsetWidth / 2); | |||
left = Math.max(window.scrollX + 8, Math.min(left, window.scrollX + vw - tip.offsetWidth - 8)); | |||
if (top < window.scrollY + 4) top = rect.bottom + window.scrollY + 8; // немає місця зверху → знизу | |||
tip.style.top = top + "px"; | |||
tip.style.left = left + "px"; | |||
} | |||
document.addEventListener("mouseup", function () { setTimeout(showTip, 10); }); | |||
document.addEventListener("touchend", function () { setTimeout(showTip, 10); }); | |||
document.addEventListener("selectionchange", function () { | |||
var sel = window.getSelection(); | |||
if (!sel || sel.isCollapsed || !((sel.toString() || "").trim())) hideTip(); | |||
}); | |||
window.addEventListener("scroll", hideTip, true); | |||
window.addEventListener("resize", hideTip); | |||
tip.addEventListener("mousedown", function (e) { e.preventDefault(); }); // не втратити виділення | |||
tip.addEventListener("click", function (e) { | |||
e.preventDefault(); | |||
hideTip(); | |||
openModal(lastSelText); | |||
}); | |||
function buildModal() { | |||
overlay = document.createElement("div"); | |||
overlay.className = "krai-fb-overlay"; | |||
overlay.innerHTML = | |||
'<div class="k-dialog krai-fb-modal" role="dialog" aria-modal="true" aria-labelledby="krai-fb-title">' + | |||
'<button type="button" class="krai-fb-close" aria-label="Закрити">×</button>' + | |||
'<h2 id="krai-fb-title" class="krai-fb-title">Запропонувати правку</h2>' + | |||
'<p class="krai-fb-sub">Помітили неточність або застарілі дані? Напишіть — ми перевіримо й оновимо. Дякуємо 💛</p>' + | |||
'<form class="krai-fb-form k-form">' + | |||
'<div class="k-field">' + | |||
'<label class="k-label">Цитата / фрагмент <span>(необов’язково)</span></label>' + | |||
'<textarea class="k-textarea" name="fragment" rows="2" placeholder="Місце, якого стосується правка"></textarea>' + | |||
'</div>' + | |||
'<div class="k-field">' + | |||
'<label class="k-label">Що не так або що виправити?</label>' + | |||
'<textarea class="k-textarea" name="comment" rows="4" placeholder="Опишіть коротко"></textarea>' + | |||
'</div>' + | |||
'<div class="k-field">' + | |||
'<label class="k-label">Контакт <span>(необов’язково, якщо треба відповідь)</span></label>' + | |||
'<input type="text" class="k-input" name="contact" placeholder="Email або @Telegram">' + | |||
'</div>' + | |||
'<input type="text" name="website" class="krai-fb-hp" tabindex="-1" autocomplete="off" aria-hidden="true">' + | |||
'<div class="krai-fb-actions">' + | |||
'<button type="button" class="k-btn k-btn--quiet krai-fb-cancel">Скасувати</button>' + | |||
'<button type="submit" class="k-btn k-btn--primary krai-fb-submit">Надіслати</button>' + | |||
'</div>' + | |||
'<div class="krai-fb-msg" aria-live="polite"></div>' + | |||
'</form>' + | |||
'</div>'; | |||
document.body.appendChild(overlay); | |||
overlay.addEventListener("click", function (e) { if (e.target === overlay) closeModal(); }); | |||
overlay.querySelector(".krai-fb-close").addEventListener("click", closeModal); | |||
overlay.querySelector(".krai-fb-cancel").addEventListener("click", closeModal); | |||
overlay.querySelector(".krai-fb-form").addEventListener("submit", onSubmit); | |||
} | |||
function openModal(presetFragment) { | |||
if (!overlay) buildModal(); | |||
var f = overlay.querySelector('textarea[name="fragment"]'); | |||
var sel = presetFragment || ((window.getSelection && window.getSelection().toString()) || "").trim(); | |||
if (sel && f && !f.value) f.value = sel.slice(0, 1000); | |||
overlay.classList.add("is-open"); | |||
document.addEventListener("keydown", onEsc); | |||
var c = overlay.querySelector('textarea[name="comment"]'); | |||
if (c) c.focus(); | |||
} | |||
function closeModal() { | |||
if (overlay) overlay.classList.remove("is-open"); | |||
document.removeEventListener("keydown", onEsc); | |||
} | |||
function onEsc(e) { if (e.key === "Escape") closeModal(); } | |||
function onSubmit(e) { | |||
e.preventDefault(); | |||
var form = e.target; | |||
var msg = form.querySelector(".krai-fb-msg"); | |||
var comment = form.comment.value.trim(); | |||
var fragment = form.fragment.value.trim(); | |||
if (comment.length < 5 && !fragment) { | |||
msg.className = "krai-fb-msg is-err"; | |||
msg.textContent = "Напишіть, будь ласка, кілька слів про правку."; | |||
return; | |||
} | |||
var submitBtn = form.querySelector(".krai-fb-submit"); | |||
submitBtn.disabled = true; | |||
msg.className = "krai-fb-msg"; | |||
msg.textContent = "Надсилаю…"; | |||
var data = new FormData(); | |||
data.append("page", mw.config.get("wgPageName") || ""); | |||
data.append("url", location.href); | |||
data.append("kind", "fix"); | |||
data.append("fragment", fragment); | |||
data.append("comment", comment); | |||
data.append("contact", form.contact.value.trim()); | |||
data.append("website", form.website.value); | |||
data.append("ts", String(loadedAt)); | |||
fetch(ENDPOINT, { method: "POST", body: data, credentials: "same-origin" }) | |||
.then(function (r) { return r.json().catch(function () { return { ok: r.ok }; }); }) | |||
.then(function (res) { | |||
if (res && res.ok) { | |||
form.reset(); | |||
msg.className = "krai-fb-msg is-ok"; | |||
msg.textContent = "Дякуємо! Ми перевіримо й оновимо. 💛"; | |||
setTimeout(closeModal, 1800); | |||
} else { | |||
throw new Error((res && res.error) || "err"); | |||
} | |||
}) | |||
.catch(function () { | |||
msg.className = "krai-fb-msg is-err"; | |||
msg.textContent = "Не вдалося надіслати. Спробуйте ще раз трохи згодом."; | |||
}) | |||
.then(function () { submitBtn.disabled = false; }); | |||
} | |||
}); | |||
}); | |||
/* === /krai-feedback === */ | |||
/* === Країнопедія: Matomo (self-host /matomo/, cookieless, поважає DNT) === */ | |||
var _paq = window._paq = window._paq || []; | |||
_paq.push(["disableCookies"]); | |||
_paq.push(["setDoNotTrack", true]); | |||
_paq.push(["trackPageView"]); | |||
_paq.push(["enableLinkTracking"]); | |||
(function () { | |||
var u = "//nomadica.uno/matomo/"; | |||
_paq.push(["setTrackerUrl", u + "matomo.php"]); | |||
_paq.push(["setSiteId", "1"]); | |||
var d = document, g = d.createElement("script"), s = d.getElementsByTagName("script")[0]; | |||
g.async = true; g.src = u + "matomo.js"; s.parentNode.insertBefore(g, s); | |||
})(); | |||
/* === /Matomo === */ | |||
Поточна версія на 03:14, 3 серпня 2026
mw.loader.using(["mediawiki.util"], function () {
$(function () {
var pageName = mw.config.get("wgPageName") || "";
if (pageName !== "🇷🇴_Румунія") return;
var cities = [
{ title: "Бухарест", page: "Румунія/Бухарест", exists: true },
{ title: "Клуж-Напока", page: "Румунія/Клуж-Напока", exists: false },
{ title: "Тімішоара", page: "Румунія/Тімішоара", exists: false },
{ title: "Брашов", page: "Румунія/Брашов", exists: false },
{ title: "Ясси", page: "Румунія/Ясси", exists: false },
{ title: "Сібіу", page: "Румунія/Сібіу", exists: false }
];
var skin = mw.config.get("skin");
if (skin === "minerva") {
buildMinervaCities(cities);
} else {
buildDesktopCitiesPortlet(cities);
}
});
function buildDesktopCitiesPortlet(cities) {
var portlet = null;
if (document.getElementById("p-tb")) {
portlet = mw.util.addPortlet("p-cities", "Міста", "#p-tb");
}
if (!portlet) {
portlet = mw.util.addPortlet("p-cities", "Міста");
var nav = document.getElementById("p-navigation");
if (nav && nav.parentNode) {
nav.parentNode.insertBefore(portlet, nav.nextSibling);
} else {
return;
}
}
cities.forEach(function (city) {
mw.util.addPortletLink("p-cities", mw.util.getUrl(city.page), city.title);
});
}
function buildMinervaCities(cities) {
var drawer = document.getElementById("mw-mf-page-left");
if (!drawer) return;
// Прибрати дефолтний portlet якщо він був створений (раптом)
var stale = document.getElementById("p-cities");
if (stale && stale.parentNode) stale.parentNode.removeChild(stale);
var section = document.createElement("div");
section.className = "cities-drawer-section";
var heading = document.createElement("h3");
heading.className = "cities-drawer-section__heading";
heading.textContent = "Міста";
section.appendChild(heading);
var ul = document.createElement("ul");
ul.className = "toggle-list__list cities-drawer-section__list";
cities.forEach(function (city) {
var li = document.createElement("li");
li.className = "toggle-list-item cities-drawer-item";
if (!city.exists) li.classList.add("cities-drawer-item--missing");
var a = document.createElement("a");
a.className = "toggle-list-item__anchor";
a.href = city.exists
? mw.util.getUrl(city.page)
: mw.util.getUrl(city.page, { action: "edit", redlink: 1 });
var icon = document.createElement("span");
icon.className = "minerva-icon cities-drawer-item__icon";
a.appendChild(icon);
var label = document.createElement("span");
label.className = "toggle-list-item__label";
label.textContent = city.title;
a.appendChild(label);
li.appendChild(a);
ul.appendChild(li);
});
section.appendChild(ul);
drawer.appendChild(section);
}
});
// Page hero для сторінок міст (Румунія/<Місто>) — той самий повноширинний
// hero, що й у країни. Країна вмикає hero через свій body-клас; містам
// додаємо .page-has-hero, ставимо фон і чистимо заголовок до назви міста.
// Нове місто = новий рядок у cityHero.
var cityHero = {
"Румунія/Бухарест": { img: "/resources/assets/hero/bucharest.jpg", title: "Бухарест" }
};
$(function () {
var pageName = mw.config.get("wgPageName") || "";
var cfg = cityHero[pageName];
if (!cfg) return;
document.body.classList.add("page-has-hero");
document.body.style.setProperty("--page-hero-bg", 'url("' + cfg.img + '")');
document.body.style.setProperty("--page-hero-fg", "#ffffff");
var heading = document.querySelector(".mw-first-heading, .firstHeading, #firstHeading");
if (heading) {
if (cfg.title) heading.textContent = cfg.title;
// Кикер «← Країна» над заголовком міста — повернення на сторінку країни.
var parent = pageName.split("/")[0];
if (parent && parent !== pageName && !document.querySelector(".hero-kicker")) {
var href = (mw.util && mw.util.getUrl)
? mw.util.getUrl(parent)
: (mw.config.get("wgArticlePath") || "/wiki/$1").replace("$1", encodeURIComponent(parent));
var wrap = document.createElement("div");
wrap.className = "hero-titlewrap";
var kicker = document.createElement("a");
kicker.className = "hero-kicker";
kicker.href = href;
kicker.textContent = "\u2190 " + parent.replace(/_/g, " ");
heading.parentNode.insertBefore(wrap, heading);
wrap.appendChild(kicker);
wrap.appendChild(heading);
}
}
});
// «← Назад» на звичайних внутрішніх сторінках (не головна, не міста, не
// службові). Повертає в історію браузера; якщо зайшли ззовні — на головну
// (беремо href лого). Гейт по cityHero, щоб не було гонки з page-has-hero.
$(function () {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
if (mw.config.get("wgIsMainPage")) return;
if (mw.config.get("wgAction") !== "view") return;
if (cityHero[mw.config.get("wgPageName") || ""]) return;
var heading = document.querySelector(".mw-first-heading, .firstHeading, #firstHeading");
if (!heading || document.querySelector(".page-back-link")) return;
var logo = document.querySelector(".mw-logo, .mw-logo-link, #p-logo a, .branding-box a");
var fallback = (logo && logo.getAttribute("href")) || "/";
var back = document.createElement("a");
back.className = "page-back-link";
back.href = fallback;
back.textContent = "\u2190 Назад";
back.addEventListener("click", function (e) {
var ref = document.referrer || "";
if (ref.indexOf(location.origin) === 0 && history.length > 1) {
e.preventDefault();
history.back();
}
});
var container = heading.closest(".vector-page-titlebar, .mw-body-header, .pre-content") || heading;
if (container.parentNode) container.parentNode.insertBefore(back, container);
else heading.parentNode.insertBefore(back, heading);
});
// Move .vector-page-toolbar into hero block (only on country pages with hero)
$(function () {
var pageName = mw.config.get("wgPageName") || "";
if (pageName !== "🇷🇴_Румунія" && !document.body.classList.contains("page-has-hero")) return;
var hero = document.querySelector(".vector-page-titlebar");
var toolbar = document.querySelector(".vector-page-toolbar");
if (hero && toolbar && toolbar.parentNode !== hero) {
hero.appendChild(toolbar);
}
});
// Country main pages — collapse all sections by default on Minerva
// (page name starts with regional indicator emoji pair = flag, no subpage separator)
function isCountryMainPage() {
var pageName = mw.config.get("wgPageName") || "";
if (pageName.indexOf("/") !== -1) return false;
return /^[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/.test(pageName);
}
mw.hook("wikipage.content").add(function () {
if (mw.config.get("skin") !== "minerva") return;
if (!isCountryMainPage()) return;
// Run after Minerva's section toggler has expanded sections
setTimeout(function () {
var nodes = document.querySelectorAll(
".collapsible-block.open-block, .section-heading.open-block, .collapsible-heading.open-block"
);
nodes.forEach(function (el) { el.classList.remove("open-block"); });
}, 0);
});
// Minerva drawer restyling — runs on every page (Minerva only)
$(function () {
if (mw.config.get("skin") !== "minerva") return;
var drawer = document.getElementById("mw-mf-page-left");
if (!drawer) return;
var hlist = drawer.querySelector("ul.hlist");
if (!hlist) return;
var aboutLi = hlist.querySelector("li:first-child");
var aboutLink = aboutLi && aboutLi.querySelector("a");
if (!aboutLink) return;
var banner = document.createElement("a");
banner.className = "drawer-about-banner";
banner.href = aboutLink.href;
var title = document.createElement("span");
title.className = "drawer-about-banner__title";
var kicker = document.createElement("span");
kicker.className = "drawer-about-banner__kicker";
kicker.textContent = "Що таке";
title.appendChild(kicker);
var name = document.createElement("span");
name.className = "drawer-about-banner__name";
name.textContent = "Країнопедія?";
title.appendChild(name);
banner.appendChild(title);
var arrow = document.createElement("span");
arrow.className = "drawer-about-banner__arrow";
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "\u2192";
banner.appendChild(arrow);
drawer.insertBefore(banner, drawer.firstChild);
aboutLi.parentNode.removeChild(aboutLi);
});
/* ========================================
Userlogin: панель реєстрації зі статистикою спільноти
======================================== */
mw.loader.using(["mediawiki.util", "mediawiki.api"], function () {
$(function () {
var specialName = mw.config.get("wgCanonicalSpecialPageName");
if (specialName !== "Userlogin") return;
var loginCard = document.querySelector(".mw-body");
if (!loginCard) return;
if (document.querySelector(".mw-register-panel")) return;
var panel = document.createElement("aside");
panel.className = "mw-register-panel";
var title = document.createElement("div");
title.className = "mw-register-panel__title";
title.textContent = "Долучайтесь до спільноти";
var sub = document.createElement("div");
sub.className = "mw-register-panel__sub";
sub.textContent = "Українці допомагають українцям за кордоном";
var statsList = document.createElement("div");
statsList.className = "mw-createacct-benefits-list";
var stats = [
{ label: "редагувань", cls: "icon-edits" },
{ label: "сторінки", cls: "icon-pages" },
{ label: "дописувачі цього місяця", cls: "icon-contributors" }
];
var nodes = stats.map(function (s) {
var item = document.createElement("div");
item.className = "mw-number-text " + s.cls;
var h3 = document.createElement("h3");
h3.textContent = "—";
var p = document.createElement("p");
p.textContent = s.label;
item.appendChild(h3);
item.appendChild(p);
return item;
});
nodes.forEach(function (n) { statsList.appendChild(n); });
var btn = document.createElement("a");
btn.className = "mw-register-panel__cta";
btn.href = mw.util.getUrl("Special:CreateAccount");
btn.textContent = "Зареєструватись";
panel.appendChild(title);
panel.appendChild(sub);
panel.appendChild(statsList);
panel.appendChild(btn);
loginCard.parentNode.insertBefore(panel, loginCard.nextSibling);
// Підтягнути живу статистику
new mw.Api().get({
action: "query",
meta: "siteinfo",
siprop: "statistics",
format: "json"
}).then(function (data) {
var s = data && data.query && data.query.statistics;
if (!s) return;
nodes[0].querySelector("h3").textContent = String(s.edits);
nodes[1].querySelector("h3").textContent = String(s.pages);
nodes[2].querySelector("h3").textContent = String(s.activeusers);
});
});
});
/* ========================================
CreateAccount: "Дякуємо за участь!" + посилання на вхід
у кінець benefits-панелі (приватна правка нижче register-panel блоку)
======================================== */
mw.loader.using(["mediawiki.util"], function () {
$(function () {
var specialName = mw.config.get("wgCanonicalSpecialPageName");
if (specialName !== "CreateAccount") return;
var benefits = document.querySelector(".mw-createacct-benefits-container");
if (!benefits) return;
if (benefits.querySelector(".benefits-thanks")) return;
var thanks = document.createElement("div");
thanks.className = "benefits-thanks";
thanks.textContent = "Дякуємо за участь!";
var hint = document.createElement("div");
hint.className = "benefits-login-hint";
hint.appendChild(document.createTextNode("Уже маєте акаунт? "));
var link = document.createElement("a");
link.href = mw.util.getUrl("Special:UserLogin");
link.textContent = "Увійти";
hint.appendChild(link);
benefits.appendChild(thanks);
benefits.appendChild(hint);
});
});
/* Main menu (lefte sidebar) — force-pin для всіх юзерів.
Якщо у юзера збережено unpinned стан, меню рендериться всередині
dropdown-контейнера у топ-барі замість sidebar. Переносимо його назад. */
mw.loader.using(["mediawiki.util"], function () {
$(function () {
if (mw.config.get("skin") !== "vector-2022") return;
var menu = document.getElementById("vector-main-menu");
var pinned = document.getElementById("vector-main-menu-pinned-container");
if (!menu || !pinned) return;
if (pinned.contains(menu)) return;
pinned.appendChild(menu);
menu.classList.remove("vector-pinnable-element-unpinned");
menu.classList.add("vector-pinnable-element-pinned");
var header = menu.querySelector(".vector-pinnable-header");
if (header) {
header.classList.remove("vector-pinnable-header-unpinned");
header.classList.add("vector-pinnable-header-pinned");
}
});
});
/* Додаємо body-класи "usergroup-<group>" для CSS-таргетингу
видимості пунктів меню за роллю користувача. */
$(function () {
var groups = (mw.config.get("wgUserGroups") || []).concat(["loggedin"]);
if (mw.user.isAnon()) groups = ["anon"];
groups.forEach(function (g) {
document.body.classList.add("usergroup-" + g);
});
});
/* === Країнопедія: віджет «Повідомити про правку» (krai-feedback) === */
mw.loader.using(["mediawiki.util"], function () {
$(function () {
if (mw.config.get("wgNamespaceNumber") !== 0) return; // тільки статті
if (mw.config.get("wgAction") !== "view") return; // тільки читання
if (document.getElementById("krai-fb-btn")) return;
var ENDPOINT = "/feedback.php";
var loadedAt = Math.floor(Date.now() / 1000); // ts фіксуємо на завантаженні (>3с завжди)
var overlay = null;
var content = document.getElementById("mw-content-text") || document.body;
var lastSelText = "";
// --- плаваюча кнопка (коли нічого не виділено) ---
var btn = document.createElement("button");
btn.id = "krai-fb-btn";
btn.type = "button";
btn.className = "k-btn k-btn--quiet krai-fb-btn";
btn.textContent = "✏️ Знайшли неточність?";
document.body.appendChild(btn);
btn.addEventListener("click", function () { openModal(); });
// --- мікроменю над виділеним текстом ---
var tip = document.createElement("button");
tip.id = "krai-fb-tip";
tip.type = "button";
tip.className = "k-btn k-btn--quiet krai-fb-tip";
tip.textContent = "✏️ Запропонувати правку";
tip.style.display = "none";
document.body.appendChild(tip);
function selectionInContent(sel) {
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return false;
return sel.anchorNode && content.contains(sel.anchorNode);
}
function hideTip() { tip.style.display = "none"; }
function showTip() {
var sel = window.getSelection();
var text = ((sel && sel.toString()) || "").trim();
if (!selectionInContent(sel) || text.length < 8) { hideTip(); return; }
lastSelText = text.slice(0, 1000);
var rect = sel.getRangeAt(0).getBoundingClientRect();
if (!rect || (!rect.width && !rect.height)) { hideTip(); return; }
tip.style.display = "block";
var vw = document.documentElement.clientWidth;
var top = rect.top + window.scrollY - tip.offsetHeight - 8;
var left = rect.left + window.scrollX + (rect.width / 2) - (tip.offsetWidth / 2);
left = Math.max(window.scrollX + 8, Math.min(left, window.scrollX + vw - tip.offsetWidth - 8));
if (top < window.scrollY + 4) top = rect.bottom + window.scrollY + 8; // немає місця зверху → знизу
tip.style.top = top + "px";
tip.style.left = left + "px";
}
document.addEventListener("mouseup", function () { setTimeout(showTip, 10); });
document.addEventListener("touchend", function () { setTimeout(showTip, 10); });
document.addEventListener("selectionchange", function () {
var sel = window.getSelection();
if (!sel || sel.isCollapsed || !((sel.toString() || "").trim())) hideTip();
});
window.addEventListener("scroll", hideTip, true);
window.addEventListener("resize", hideTip);
tip.addEventListener("mousedown", function (e) { e.preventDefault(); }); // не втратити виділення
tip.addEventListener("click", function (e) {
e.preventDefault();
hideTip();
openModal(lastSelText);
});
function buildModal() {
overlay = document.createElement("div");
overlay.className = "krai-fb-overlay";
overlay.innerHTML =
'<div class="k-dialog krai-fb-modal" role="dialog" aria-modal="true" aria-labelledby="krai-fb-title">' +
'<button type="button" class="krai-fb-close" aria-label="Закрити">×</button>' +
'<h2 id="krai-fb-title" class="krai-fb-title">Запропонувати правку</h2>' +
'<p class="krai-fb-sub">Помітили неточність або застарілі дані? Напишіть — ми перевіримо й оновимо. Дякуємо 💛</p>' +
'<form class="krai-fb-form k-form">' +
'<div class="k-field">' +
'<label class="k-label">Цитата / фрагмент <span>(необов’язково)</span></label>' +
'<textarea class="k-textarea" name="fragment" rows="2" placeholder="Місце, якого стосується правка"></textarea>' +
'</div>' +
'<div class="k-field">' +
'<label class="k-label">Що не так або що виправити?</label>' +
'<textarea class="k-textarea" name="comment" rows="4" placeholder="Опишіть коротко"></textarea>' +
'</div>' +
'<div class="k-field">' +
'<label class="k-label">Контакт <span>(необов’язково, якщо треба відповідь)</span></label>' +
'<input type="text" class="k-input" name="contact" placeholder="Email або @Telegram">' +
'</div>' +
'<input type="text" name="website" class="krai-fb-hp" tabindex="-1" autocomplete="off" aria-hidden="true">' +
'<div class="krai-fb-actions">' +
'<button type="button" class="k-btn k-btn--quiet krai-fb-cancel">Скасувати</button>' +
'<button type="submit" class="k-btn k-btn--primary krai-fb-submit">Надіслати</button>' +
'</div>' +
'<div class="krai-fb-msg" aria-live="polite"></div>' +
'</form>' +
'</div>';
document.body.appendChild(overlay);
overlay.addEventListener("click", function (e) { if (e.target === overlay) closeModal(); });
overlay.querySelector(".krai-fb-close").addEventListener("click", closeModal);
overlay.querySelector(".krai-fb-cancel").addEventListener("click", closeModal);
overlay.querySelector(".krai-fb-form").addEventListener("submit", onSubmit);
}
function openModal(presetFragment) {
if (!overlay) buildModal();
var f = overlay.querySelector('textarea[name="fragment"]');
var sel = presetFragment || ((window.getSelection && window.getSelection().toString()) || "").trim();
if (sel && f && !f.value) f.value = sel.slice(0, 1000);
overlay.classList.add("is-open");
document.addEventListener("keydown", onEsc);
var c = overlay.querySelector('textarea[name="comment"]');
if (c) c.focus();
}
function closeModal() {
if (overlay) overlay.classList.remove("is-open");
document.removeEventListener("keydown", onEsc);
}
function onEsc(e) { if (e.key === "Escape") closeModal(); }
function onSubmit(e) {
e.preventDefault();
var form = e.target;
var msg = form.querySelector(".krai-fb-msg");
var comment = form.comment.value.trim();
var fragment = form.fragment.value.trim();
if (comment.length < 5 && !fragment) {
msg.className = "krai-fb-msg is-err";
msg.textContent = "Напишіть, будь ласка, кілька слів про правку.";
return;
}
var submitBtn = form.querySelector(".krai-fb-submit");
submitBtn.disabled = true;
msg.className = "krai-fb-msg";
msg.textContent = "Надсилаю…";
var data = new FormData();
data.append("page", mw.config.get("wgPageName") || "");
data.append("url", location.href);
data.append("kind", "fix");
data.append("fragment", fragment);
data.append("comment", comment);
data.append("contact", form.contact.value.trim());
data.append("website", form.website.value);
data.append("ts", String(loadedAt));
fetch(ENDPOINT, { method: "POST", body: data, credentials: "same-origin" })
.then(function (r) { return r.json().catch(function () { return { ok: r.ok }; }); })
.then(function (res) {
if (res && res.ok) {
form.reset();
msg.className = "krai-fb-msg is-ok";
msg.textContent = "Дякуємо! Ми перевіримо й оновимо. 💛";
setTimeout(closeModal, 1800);
} else {
throw new Error((res && res.error) || "err");
}
})
.catch(function () {
msg.className = "krai-fb-msg is-err";
msg.textContent = "Не вдалося надіслати. Спробуйте ще раз трохи згодом.";
})
.then(function () { submitBtn.disabled = false; });
}
});
});
/* === /krai-feedback === */
/* === Країнопедія: Matomo (self-host /matomo/, cookieless, поважає DNT) === */
var _paq = window._paq = window._paq || [];
_paq.push(["disableCookies"]);
_paq.push(["setDoNotTrack", true]);
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
var u = "//nomadica.uno/matomo/";
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", "1"]);
var d = document, g = d.createElement("script"), s = d.getElementsByTagName("script")[0];
g.async = true; g.src = u + "matomo.js"; s.parentNode.insertBefore(g, s);
})();
/* === /Matomo === */