MediaWiki:Common.js

Версія від 19:55, 2 серпня 2026, створена Admin (обговорення | внесок) (feedback: кнопка «Знайшли неточність?», мікроменю «Запропонувати правку»)

Увага: Після публікування слід очистити кеш браузера, щоб побачити зміни.

  • Firefox / Safari: тримайте Shift, коли натискаєте Оновити, або натисніть Ctrl-F5 чи Ctrl-Shift-R (⌘-R на Apple Mac)
  • Google Chrome: натисніть Ctrl-Shift-R (⌘-Shift-R на Apple Mac)
  • Internet Explorer / Edge: тримайте Ctrl, коли натискаєте Оновити, або натисніть Ctrl-F5
  • Opera: натисніть Ctrl-F5
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">' +
            '<label class="krai-fb-label">Цитата / фрагмент <span>(необов’язково)</span>' +
              '<textarea class="k-textarea" name="fragment" rows="2" placeholder="Місце, якого стосується правка"></textarea>' +
            '</label>' +
            '<label class="krai-fb-label">Що не так або що виправити?' +
              '<textarea class="k-textarea" name="comment" rows="4" placeholder="Опишіть коротко"></textarea>' +
            '</label>' +
            '<label class="krai-fb-label">Контакт <span>(необов’язково, якщо треба відповідь)</span>' +
              '<input type="text" class="k-input" name="contact" placeholder="Email або @Telegram"></label>' +
            '<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 === */