MediaWiki:Common.js

Матеріал з Київський національний лінгвістичний університет
Перейти до навігації Перейти до пошуку

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

  • Firefox / Safari: тримайте Shift, коли натискаєте Оновити, або натисніть Ctrl-F5 чи Ctrl-Shift-R (⌘-R на Apple Mac)
  • Google Chrome: натисніть Ctrl-Shift-R (⌘-Shift-R на Apple Mac)
  • Edge: тримайте Ctrl, коли натискаєте Оновити, або натисніть Ctrl-F5.
/* ── 1. Налаштування Viewport та базові функції ── */
(function() {
    var viewport = document.querySelector('meta[name="viewport"]');
    if (viewport) {
        viewport.setAttribute('content', 'width=device-width, initial-scale=1.0');
    } else {
        var meta = document.createElement('meta');
        meta.name = 'viewport';
        meta.content = 'width=device-width, initial-scale=1.0';
        document.head.appendChild(meta);
    }
})();

/** 
 * Глобальні допоміжні функції для парсингу контенту 
 * (Винесено вгору, щоб були доступні всім частинам скрипта)
 */
function extractImageName(wikitext) {
    if (!wikitext) return null;
    var m = wikitext.match(/\|\s*image\s*=\s*([^\|\n\}]+)/i);
    if (m && m[1].trim()) return m[1].trim();
    m = wikitext.match(/\[\[(?:Файл|File|Зображення|Image):([^\|\]]+)/i);
    return m ? m[1].trim() : null;
}

function extractDescription(wikitext) {
    if (!wikitext) return '';
    var text = wikitext;

    /* Прибрати інфобокс */
    var depth = 0, end = -1;
    for (var i = 0; i < text.length; i++) {
        if (text[i] === '{' && text[i + 1] === '{') { depth++; i++; }
        else if (text[i] === '}' && text[i + 1] === '}') {
            depth--;
            if (depth === 0) { end = i + 2; break; }
            i++;
        }
    }
    if (end > 0) text = text.substring(end);

    text = text
        .replace(/\[\[(?:Файл|File|Зображення|Image):[^\]]*\]\]/gi, '')
        .replace(/\{\|[\s\S]*?\|\}/g, '')
        .replace(/\{\{[^}]*\}\}/g, '')
        .replace(/={2,6}[^=\n]+=+/g, '')
        .replace(/\[\[(?:[^\|\]]*\|)?([^\]]+)\]\]/g, '$1')
        .replace(/\[[^\s\]]+\s+([^\]]+)\]/g, '$1')
        .replace(/\[[^\]]+\]/g, '')
        .replace(/'{2,3}/g, '')
        .replace(/<[^>]+>/g, '')
        .replace(/^[\*#:;].*/gm, '');

    var lines = text.split('\n')
        .map(function (l) { return l.trim(); })
        .filter(function (l) { return l.length > 20; });

    if (!lines.length) return '';
    var result = lines[0].substring(0, 160);
    return lines[0].length > 160 ? result + '…' : result;
}

/* ── 2. Обробка кліків по картках (jQuery) ── */
$(function() {
    $(document).on('click', '.category-card', function(e) {
        var $card = $(this);
        var href = $card.attr('data-href');
        if (href && $(e.target).closest('a').length === 0) {
            window.location.href = href;
        }
    });
});

/* ── 3. Випадкові статті на головній ── */
function loadRandomArticles() {
    var list = document.getElementById('random-articles-list');
    if (!list) return;
 
    list.innerHTML = '<div class="random-articles-loading">Завантаження...</div>';
 
    var apiBase = mw.config.get('wgScriptPath') + '/api.php';
    var excludedPages = ['Головна сторінка', 'Структурні підрозділи', 'Викладачі'];
 
    fetch(apiBase + '?action=query&list=random&rnnamespace=0&rnlimit=10&format=json')
        .then(function (r) { return r.json(); })
        .then(function (data) {
            var pages = data.query.random
                .filter(function (p) { return excludedPages.indexOf(p.title) === -1; })
                .slice(0, 3);
            var titles = pages.map(function (p) { return p.title; }).join('|');
            return fetch(
                apiBase + '?action=query' +
                '&titles=' + encodeURIComponent(titles) +
                '&prop=revisions&rvprop=content&format=json'
            );
        })
        .then(function (r) { return r.json(); })
        .then(function (data) {
            if (!data.query || !data.query.pages) return [];
            var pages = Object.values(data.query.pages);
            
            var promises = pages.map(function (page) {
                var title   = page.title;
                var pageUrl = mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(title.replace(/ /g, '_')));
                var content = (page.revisions && page.revisions[0]) ? (page.revisions[0]['*'] || '') : '';
 
                var excerpt = extractDescription(content) || 'Немає опису.';
                var imgName = extractImageName(content);
 
                if (imgName) {
                    var fileTitle = /^(Файл|File|Image|Зображення):/i.test(imgName) ? imgName : 'File:' + imgName;
                    return fetch(apiBase + '?action=query&titles=' + encodeURIComponent(fileTitle) + '&prop=imageinfo&iiprop=url&iiurlwidth=120&format=json')
                        .then(function (r) { return r.json(); })
                        .then(function (imgData) {
                            var ip = Object.values(imgData.query.pages);
                            var ii = ip[0] && ip[0].imageinfo && ip[0].imageinfo[0];
                            return { title: title, excerpt: excerpt, pageUrl: pageUrl, imgSrc: ii ? (ii.thumburl || ii.url) : null };
                        })
                        .catch(function () { return { title: title, excerpt: excerpt, pageUrl: pageUrl, imgSrc: null }; });
                }
                return Promise.resolve({ title: title, excerpt: excerpt, pageUrl: pageUrl, imgSrc: null });
            });
            return Promise.all(promises);
        })
        .then(function (results) {
            list.innerHTML = '';
            results.forEach(function (item) {
                var thumbHtml = item.imgSrc
                    ? '<img class="random-article-thumb" src="' + item.imgSrc + '" alt="">'
                    : '<div class="random-article-thumb-placeholder">📄</div>';
                var card = document.createElement('a');
                card.href      = item.pageUrl;
                card.className = 'random-article-card';
                card.innerHTML = thumbHtml +
                    '<div class="random-article-info">' +
                        '<div class="random-article-title">'  + mw.html.escape(item.title)  + '</div>' +
                        '<div class="random-article-excerpt">' + mw.html.escape(item.excerpt) + '</div>' +
                    '</div>';
                list.appendChild(card);
            });
        })
        .catch(function (err) {
            if (list) list.innerHTML = '<div class="random-articles-loading">Не вдалося завантажити статті.</div>';
            console.error('[RA Error]', err);
        });
}
 
mw.hook('wikipage.content').add(function () {
    if (document.getElementById('random-articles-panel')) {
        loadRandomArticles();
        var btn = document.getElementById('random-articles-refresh');
        if (btn) btn.addEventListener('click', loadRandomArticles);
    }
});

/* ── 4. Vue-додаток для категорій ── */
mw.hook('wikipage.content').add(function () {
    var mountEl = document.getElementById('vue-category-cards');
    if (!mountEl) return;

    var categoryName  = mountEl.getAttribute('data-category')     || '';
    var filterField   = mountEl.getAttribute('data-filter-field') || 'auto';
    var pageLimit     = parseInt(mountEl.getAttribute('data-limit') || '50', 10);
    var columns       = mountEl.getAttribute('data-columns')       || '3';

    if (!categoryName) return;

    mw.loader.getScript('https://unpkg.com/vue@3/dist/vue.global.prod.js').then(function () {
        var apiBase = mw.config.get('wgScriptPath') + '/api.php';

        var app = Vue.createApp({
            data() {
                return {
                    items: [], loading: true, error: null,
                    activeFilter: 'Всі', filters: ['Всі'],
                    preview: null, previewX: 0, previewY: 0, previewTimer: null,
                    searchQuery: '', gridColumns: columns
                };
            },
            computed: {
                filteredItems() {
                    var vm = this;
                    var q = vm.searchQuery.trim().toLowerCase();
                    return vm.items.filter(function (item) {
                        var matchFilter = vm.activeFilter === 'Всі' || item.filterLabel === vm.activeFilter;
                        var matchSearch = !q || item.title.toLowerCase().includes(q) || (item.description && item.description.toLowerCase().includes(q));
                        return matchFilter && matchSearch;
                    }).sort((a,b) => a.title.localeCompare(b.title, 'uk'));
                },
                gridStyle() { return 'grid-template-columns: repeat(' + this.gridColumns + ', 1fr);'; }
            },
            mounted() { this.loadItems(); },
            methods: {
                showPreview(item, event) {
                    var vm = this; clearTransition();
                    var rect = event.currentTarget.getBoundingClientRect();
                    vm.previewTimer = setTimeout(() => {
                        vm.previewX = (window.innerWidth - rect.right > 290) ? rect.right + window.scrollX + 10 : rect.left + window.scrollX - 280;
                        vm.previewY = rect.top + window.scrollY;
                        vm.preview = item;
                    }, 250);
                },
                hidePreview() { this.previewTimer = setTimeout(() => { this.preview = null; }, 150); },
                keepPreview() { clearTimeout(this.previewTimer); },
                async loadItems() {
                    try {
                        var catResp = await fetch(apiBase + '?action=query&list=categorymembers&cmtitle=Категорія:' + encodeURIComponent(categoryName) + '&cmlimit=' + pageLimit + '&cmnamespace=0&format=json');
                        var catData = await catResp.json();
                        var members = catData.query.categorymembers || [];
                        if (!members.length) { this.loading = false; return; }

                        var titles = members.map(m => m.title).join('|');
                        var pageResp = await fetch(apiBase + '?action=query&titles=' + encodeURIComponent(titles) + '&prop=revisions&rvprop=content&format=json');
                        var pageData = await pageResp.json();
                        var pages = Object.values(pageData.query.pages);

                        var filterSet = new Set();
                        var promises = pages.map(async (page) => {
                            var content = page.revisions ? page.revisions[0]['*'] : '';
                            var description = extractDescription(content);
                            
                            // Проста логіка фільтра
                            var filterVal = '';
                            if (filterField !== 'auto') {
                                var re = new RegExp('\\|\\s*' + filterField + '\\s*=\\s*([^\\|\\n\\}]+)', 'i');
                                var m = content.match(re);
                                filterVal = m ? m[1].trim().split(/[\[\],|]/)[0] : '';
                            }

                            var imgName = extractImageName(content);
                            var imgSrc = null;
                            if (imgName) {
                                var imgResp = await fetch(apiBase + '?action=query&titles=File:' + encodeURIComponent(imgName) + '&prop=imageinfo&iiprop=url&iiurlwidth=200&format=json');
                                var imgD = await imgResp.json();
                                var ii = Object.values(imgD.query.pages)[0].imageinfo;
                                if (ii) imgSrc = ii[0].thumburl || ii[0].url;
                            }

                            if (filterVal) filterSet.add(filterVal);
                            return {
                                title: page.title,
                                filterLabel: filterVal || 'Інше',
                                description: description,
                                imgSrc: imgSrc,
                                pageUrl: mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(page.title.replace(/ /g, '_')))
                            };
                        });

                        this.items = await Promise.all(promises);
                        this.filters = ['Всі', ...Array.from(filterSet).sort()];
                    } catch (e) { this.error = 'Помилка завантаження'; }
                    this.loading = false;
                }
            },
            template: `
                <div class="vcc-app">
                    <div class="vcc-topbar">
                        <input class="vcc-search" type="text" v-model="searchQuery" placeholder="Пошук..." />
                        <span class="vcc-count" v-if="!loading">Знайдено: {{ filteredItems.length }}</span>
                    </div>
                    <div class="vcc-layout">
                        <div class="vcc-main">
                            <div v-if="loading" class="vcc-loading">Завантаження...</div>
                            <div v-else class="vcc-grid" :style="gridStyle">
                                <a v-for="item in filteredItems" :href="item.pageUrl" class="vcc-card" @mouseenter="showPreview(item, $event)" @mouseleave="hidePreview">
                                    <div class="vcc-card-img-wrap">
                                        <img v-if="item.imgSrc" :src="item.imgSrc" class="vcc-card-img" />
                                        <div v-else class="vcc-card-img-placeholder">{{ item.title[0] }}</div>
                                    </div>
                                    <div class="vcc-card-body">
                                        <div class="vcc-card-badge" v-if="item.filterLabel !== 'Інше'">{{ item.filterLabel }}</div>
                                        <div class="vcc-card-name">{{ item.title }}</div>
                                    </div>
                                </a>
                            </div>
                        </div>
                        <div class="vcc-sidebar" v-if="filters.length > 2">
                            <button v-for="f in filters" :class="['vcc-filter-btn', {active: activeFilter===f}]" @click="activeFilter=f">{{ f }}</button>
                        </div>
                    </div>
                    <teleport to="body">
                        <div v-if="preview" class="vcc-preview" :style="{top: previewY+'px', left: previewX+'px'}" @mouseenter="keepPreview" @mouseleave="hidePreview">
                            <div class="vcc-preview-name">{{ preview.title }}</div>
                            <div class="vcc-preview-desc">{{ preview.description }}</div>
                        </div>
                    </teleport>
                </div>`
        }).mount('#vue-category-cards');
    });
});

/* ── 5. Анімація статистики ── */
function animateNumber(element, target) {
    var current = 0;
    var increment = target / 50;
    var timer = setInterval(function() {
        current += increment;
        if (current >= target) {
            element.textContent = target;
            clearInterval(timer);
        } else {
            element.textContent = Math.floor(current);
        }
    }, 20);
}

mw.hook('wikipage.content').add(function() {
    if (mw.config.get('wgPageName') !== 'Головна_сторінка') return;
    var statsNumbers = document.querySelectorAll('.stats-panel-number');
    var observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                statsNumbers.forEach(el => {
                    var num = parseInt(el.textContent.replace(/\D/g,''));
                    if (num) animateNumber(el, num);
                });
                observer.disconnect();
            }
        });
    });
    var panel = document.querySelector('.stats-panel');
    if (panel) observer.observe(panel);
});

/* ── 6. Modern UI: Прогрес та Particles ── */
mw.hook('wikipage.content').add(function() {
    if (mw.config.get('wgPageName') !== 'Головна_сторінка') return;
    
    // Progress Bar
    var prog = document.createElement('div');
    prog.className = 'scroll-progress';
    document.body.appendChild(prog);
    window.addEventListener('scroll', function() {
        var scrolled = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
        prog.style.width = scrolled + '%';
    });

    // Particles
    var container = document.querySelector('.welcome-wrapper');
    if (!container) return;
    var canvas = document.createElement('canvas');
    canvas.className = 'particles-canvas';
    container.style.position = 'relative';
    container.prepend(canvas);
    var ctx = canvas.getContext('2d');
    
    function resize() {
        canvas.width = container.offsetWidth;
        canvas.height = container.offsetHeight;
    }
    window.addEventListener('resize', resize);
    resize();

    var particles = Array.from({length: 40}, () => ({
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        vx: (Math.random() - 0.5) * 0.5,
        vy: (Math.random() - 0.5) * 0.5
    }));

    function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = 'rgba(255, 215, 0, 0.4)';
        particles.forEach(p => {
            p.x += p.vx; p.y += p.vy;
            if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
            if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
            ctx.beginPath();
            ctx.arc(p.x, p.y, 2, 0, Math.PI*2);
            ctx.fill();
        });
        requestAnimationFrame(draw);
    }
    draw();
});