Открыть меню
Переключить меню настроек
Открыть персональное меню
Вы не представились системе
Ваш IP-адрес будет виден всем, если вы внесёте какие-либо изменения.

MediaWiki:ScmcCatalog.js

Страница интерфейса MediaWiki
Версия от 10:09, 15 июня 2026; Defer (обсуждение | вклад) (Новая страница: «(function () { if (typeof mw === 'undefined' || !window.document) return; var statusMap = { green: { emoji: '🟢', label: 'Готово', words: ['готово', 'готов'] }, yellow: { emoji: '🟡', label: 'Нужно обновить', words: ['нужно обновить', 'обновить', 'устарело'] }, red: { emoji: '🔴', label: 'Нет страницы', words: ['нет страницы', 'нет', 'п...»)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

Замечание: Возможно, после публикации вам придётся очистить кэш своего браузера, чтобы увидеть изменения.

  • Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl+F5 или Ctrl+R (⌘+R на Mac)
  • Google Chrome: Нажмите Ctrl+Shift+R (⌘+Shift+R на Mac)
  • Edge: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl+F5
  • Opera: Нажмите Ctrl+F5.
(function () {
    if (typeof mw === 'undefined' || !window.document) return;

    var statusMap = {
        green: { emoji: '🟢', label: 'Готово', words: ['готово', 'готов'] },
        yellow: { emoji: '🟡', label: 'Нужно обновить', words: ['нужно обновить', 'обновить', 'устарело'] },
        red: { emoji: '🔴', label: 'Нет страницы', words: ['нет страницы', 'нет', 'пусто'] },
        blue: { emoji: '🔵', label: 'Заморожено', words: ['заморожено', 'заморожен'] }
    };

    function ready(fn) {
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            fn();
        } else {
            document.addEventListener('DOMContentLoaded', fn);
        }
    }

    function clean(text) {
        return String(text || '').replace(/\s+/g, ' ').trim();
    }

    function key(text) {
        return clean(text).replace(/_/g, ' ').toLowerCase();
    }

    function encodeAttr(value) {
        return String(value || '')
            .replace(/&/g, '&')
            .replace(/"/g, '"')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    function decodeAttr(value) {
        var textarea = document.createElement('textarea');
        textarea.innerHTML = String(value || '');
        return textarea.value;
    }

    function attrFromTag(tag, name) {
        var re = new RegExp(name + '\\s*=\\s*(["\\\'])(.*?)\\1', 'i');
        var match = tag.match(re);
        return match ? decodeAttr(match[2]) : '';
    }

    function normalizeStatus(status) {
        return statusMap[status] ? status : 'blue';
    }

    function getLevelClass(className) {
        var match = String(className || '').match(/\bscmc-level-[1-5]\b/);
        return match ? match[0] : 'scmc-level-1';
    }

    function buildFallbackLink(page, title) {
        if (/^https?:\/\//i.test(page)) {
            return '[' + page + ' ' + title + ']';
        }

        return '[[' + page + '|' + title + ']]';
    }

    function makeButton(className, text, title) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = className;
        button.textContent = text;
        if (title) button.title = title;
        return button;
    }

    function closeMenus() {
        var statusMenu = document.querySelector('.scmc-status-menu');
        var sectionMenu = document.querySelector('.scmc-section-menu');

        if (statusMenu) statusMenu.remove();
        if (sectionMenu) sectionMenu.remove();
    }

    function placeMenu(menu, rect) {
        document.body.appendChild(menu);

        var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 12);
        var top = Math.min(rect.bottom + 8, window.innerHeight - menu.offsetHeight - 12);

        menu.style.left = Math.max(12, left) + 'px';
        menu.style.top = Math.max(12, top) + 'px';
    }

    function getSource() {
        var api = new mw.Api();
        var currentPage = mw.config.get('wgPageName');

        return api.get({
            action: 'query',
            prop: 'revisions',
            titles: currentPage,
            rvprop: 'content',
            rvslots: 'main',
            formatversion: 2
        }).then(function (data) {
            var pageData = data.query && data.query.pages ? data.query.pages[0] : null;
            var rev = pageData && pageData.revisions ? pageData.revisions[0] : null;

            if (rev && rev.slots && rev.slots.main && typeof rev.slots.main.content === 'string') {
                return rev.slots.main.content;
            }

            if (rev && typeof rev.content === 'string') {
                return rev.content;
            }

            throw new Error('Не удалось прочитать код страницы.');
        });
    }

    function saveSource(text, summary) {
        var api = new mw.Api();

        return api.postWithToken('csrf', {
            action: 'edit',
            title: mw.config.get('wgPageName'),
            text: text,
            summary: summary || 'Обновление каталога Marine Corps',
            minor: true,
            formatversion: 2
        });
    }

    function getRowsFromSource(source) {
        var result = [];
        var re = /<div\b[^>]*\bscmc-catalog-row\b[^>]*>[\s\S]*?<\/div>/gi;
        var match;

        while ((match = re.exec(source)) !== null) {
            var full = match[0];
            var open = full.match(/^<div\b[^>]*>/i);
            if (!open) continue;

            var tag = open[0];

            var data = {
                className: attrFromTag(tag, 'class') || 'scmc-catalog-row scmc-level-1',
                section: clean(attrFromTag(tag, 'data-section')),
                page: clean(attrFromTag(tag, 'data-page')),
                title: clean(attrFromTag(tag, 'data-title')),
                scan: clean(attrFromTag(tag, 'data-scan')),
                status: normalizeStatus(clean(attrFromTag(tag, 'data-status'))),
                note: clean(attrFromTag(tag, 'data-note'))
            };

            if (!data.page) continue;
            if (!data.title) data.title = data.page;

            result.push({
                start: match.index,
                end: match.index + full.length,
                full: full,
                data: data
            });
        }

        return result;
    }

    function findSourceRow(sourceRows, targetData) {
        var matches = sourceRows.filter(function (item) {
            return key(item.data.page) === key(targetData.page) &&
                key(item.data.section) === key(targetData.section) &&
                key(item.data.title) === key(targetData.title);
        });

        if (!matches.length) {
            throw new Error('Не нашёл строку: ' + (targetData.title || targetData.page));
        }

        if (matches.length > 1) {
            throw new Error('Нашлось несколько одинаковых строк: ' + (targetData.title || targetData.page) + '. Лучше поправить вручную.');
        }

        return matches[0];
    }

    function buildRowLine(data) {
        var className = clean(data.className || 'scmc-catalog-row scmc-level-1');
        var level = getLevelClass(className);

        if (className.indexOf('scmc-catalog-row') === -1) {
            className = 'scmc-catalog-row ' + level;
        }

        var section = clean(data.section);
        var page = clean(data.page);
        var title = clean(data.title || data.page);
        var scan = clean(data.scan);
        var status = normalizeStatus(data.status);
        var note = clean(data.note);

        var out = '<div class="' + encodeAttr(className) + '"';
        out += ' data-section="' + encodeAttr(section) + '"';
        out += ' data-page="' + encodeAttr(page) + '"';
        out += ' data-title="' + encodeAttr(title) + '"';

        if (scan) {
            out += ' data-scan="' + encodeAttr(scan) + '"';
        }

        out += ' data-status="' + encodeAttr(status) + '"';

        if (note) {
            out += ' data-note="' + encodeAttr(note) + '"';
        }

        out += '>' + buildFallbackLink(page, title) + '</div>';

        return out;
    }

    ready(function () {
        var catalog = document.querySelector('.scmc-catalog');
        if (!catalog) return;

        var tools = catalog.querySelector('.scmc-catalog-tools');
        var rowElements = Array.prototype.slice.call(catalog.querySelectorAll('.scmc-catalog-row'));
        if (!tools || !rowElements.length) return;

        var activeFilter = 'all';
        var search;
        var filters;
        var counter;

        buildTools(tools);
        enhanceRows(rowElements);

        search = tools.querySelector('.scmc-catalog-search');
        filters = Array.prototype.slice.call(tools.querySelectorAll('.scmc-catalog-filter'));
        counter = tools.querySelector('.scmc-catalog-counter');

        update();

        search.addEventListener('input', update);

        filters.forEach(function (button) {
            button.addEventListener('click', function () {
                activeFilter = button.getAttribute('data-filter') || 'all';

                filters.forEach(function (other) {
                    other.classList.toggle('is-active', other === button);
                });

                update();
            });
        });

        document.addEventListener('click', function (event) {
            var statusButton = event.target.closest('.scmc-catalog-status');
            var sectionButton = event.target.closest('.scmc-catalog-section-btn');

            if (statusButton && catalog.contains(statusButton)) {
                event.preventDefault();
                event.stopPropagation();
                openStatusMenu(statusButton);
                return;
            }

            if (sectionButton && catalog.contains(sectionButton)) {
                event.preventDefault();
                event.stopPropagation();
                openSectionMenu(sectionButton);
                return;
            }

            if (!event.target.closest('.scmc-status-menu') && !event.target.closest('.scmc-section-menu')) {
                closeMenus();
            }
        });

        function buildTools(target) {
            target.innerHTML = '';

            var input = document.createElement('input');
            input.className = 'scmc-catalog-search';
            input.type = 'search';
            input.placeholder = 'Поиск по названию, странице, разделу, статусу или заметке...';

            var filtersWrap = document.createElement('div');
            filtersWrap.className = 'scmc-catalog-filters';

            [
                ['all', 'Все'],
                ['green', '🟢 Готово'],
                ['yellow', '🟡 Нужно обновить'],
                ['red', '🔴 Нет страницы'],
                ['blue', '🔵 Заморожено'],
                ['problem', 'Проблемные'],
                ['stop', 'Из основного проекта'],
                ['note', 'С заметками']
            ].forEach(function (item, index) {
                var button = document.createElement('button');
                button.type = 'button';
                button.className = 'scmc-catalog-filter' + (index === 0 ? ' is-active' : '');
                button.setAttribute('data-filter', item[0]);
                button.textContent = item[1];
                filtersWrap.appendChild(button);
            });

            var count = document.createElement('div');
            count.className = 'scmc-catalog-counter';

            target.appendChild(input);
            target.appendChild(filtersWrap);
            target.appendChild(count);
        }

        function getRowData(row) {
            return {
                className: row.getAttribute('class') || 'scmc-catalog-row scmc-level-1',
                section: clean(row.getAttribute('data-section')),
                page: clean(row.getAttribute('data-page')),
                title: clean(row.getAttribute('data-title') || row.textContent),
                scan: clean(row.getAttribute('data-scan')),
                status: normalizeStatus(clean(row.getAttribute('data-status'))),
                note: clean(row.getAttribute('data-note'))
            };
        }

        function visibleNote(data) {
            var parts = [];

            if (data.scan === 'stop') {
                parts.push('Из основного проекта.');
            }

            if (data.note) {
                parts.push(data.note);
            }

            return parts.join(' ');
        }

        function getSections() {
            var map = {};

            rowElements.forEach(function (row) {
                var section = clean(row.getAttribute('data-section'));
                if (section) map[section] = true;
            });

            return Object.keys(map).sort(function (a, b) {
                return a.localeCompare(b, 'ru');
            });
        }

        function enhanceRows(rows) {
            rows.forEach(function (row, index) {
                if (row.getAttribute('data-scmc-enhanced') === 'true') return;

                var data = getRowData(row);
                var status = normalizeStatus(data.status);
                var isStop = data.scan === 'stop';

                row.setAttribute('data-status', status);
                if (isStop) row.setAttribute('data-scan', 'stop');

                row.setAttribute('data-scmc-enhanced', 'true');
                row.innerHTML = '';

                var position = makeButton('scmc-catalog-index', String(index + 1), 'Изменить место');

                position.addEventListener('click', function () {
                    var answer = prompt('На какое место переместить страницу? Сейчас: ' + (index + 1) + '. Всего: ' + rows.length + '.', String(index + 1));
                    if (answer === null) return;

                    var target = parseInt(answer, 10);
                    if (!target || target < 1 || target > rows.length) {
                        alert('Нужно число от 1 до ' + rows.length + '.');
                        return;
                    }

                    moveRow(data, target);
                });

                var main = document.createElement('div');
                main.className = 'scmc-catalog-main';

                var titleWrap = document.createElement('div');
                titleWrap.className = 'scmc-catalog-title';

                var link = document.createElement('a');
                link.href = /^https?:\/\//i.test(data.page) ? data.page : mw.util.getUrl(data.page);
                link.textContent = data.title || data.page;
                titleWrap.appendChild(link);

                if (data.page && data.page !== data.title) {
                    var pageHint = document.createElement('span');
                    pageHint.className = 'scmc-catalog-page';
                    pageHint.textContent = data.page;
                    titleWrap.appendChild(pageHint);
                }

                main.appendChild(titleWrap);

                var noteText = visibleNote(data);
                if (noteText) {
                    var note = document.createElement('div');
                    note.className = 'scmc-catalog-note';
                    note.textContent = noteText;
                    main.appendChild(note);
                }

                var actions = document.createElement('div');
                actions.className = 'scmc-catalog-actions';

                var noteButton = makeButton('scmc-catalog-btn scmc-catalog-note-btn', '✎', 'Изменить заметку');

                noteButton.addEventListener('click', function () {
                    var answer = prompt('Заметка для страницы "' + (data.title || data.page) + '". Пустое поле удалит data-note.', data.note || '');
                    if (answer === null) return;

                    saveRowChange(data, function (sourceData) {
                        sourceData.note = clean(answer);
                    }, 'Обновление заметки страницы: ' + (data.title || data.page));
                });

                var scanToggle = makeButton(
                    'scmc-catalog-btn scmc-scan-toggle',
                    '',
                    isStop ? 'Из основного проекта. Нажми, чтобы убрать stop.' : 'Нажми, чтобы добавить stop.'
                );

                scanToggle.setAttribute('data-active', isStop ? 'true' : 'false');

                scanToggle.addEventListener('click', function () {
                    saveRowChange(data, function (sourceData) {
                        sourceData.scan = sourceData.scan === 'stop' ? '' : 'stop';
                    }, 'Изменение режима сканирования страницы: ' + (data.title || data.page));
                });

                var sectionButton = makeButton('scmc-catalog-section-btn', data.section || 'Без раздела', 'Изменить раздел');

                var statusButton = makeButton('scmc-catalog-status', statusMap[status].emoji, statusMap[status].label);
                statusButton.setAttribute('data-status', status);

                actions.appendChild(noteButton);
                actions.appendChild(scanToggle);
                actions.appendChild(sectionButton);
                actions.appendChild(statusButton);

                row.appendChild(position);
                row.appendChild(main);
                row.appendChild(actions);
            });
        }

        function openStatusMenu(button) {
            closeMenus();

            var row = button.closest('.scmc-catalog-row');
            if (!row) return;

            var data = getRowData(row);
            var rect = button.getBoundingClientRect();
            var menu = document.createElement('div');
            menu.className = 'scmc-status-menu';

            Object.keys(statusMap).forEach(function (status) {
                var option = document.createElement('button');
                option.type = 'button';
                option.textContent = statusMap[status].emoji + ' ' + statusMap[status].label;

                option.addEventListener('click', function (event) {
                    event.preventDefault();
                    event.stopPropagation();
                    menu.remove();

                    if (status === data.status) return;

                    saveRowChange(data, function (sourceData) {
                        sourceData.status = status;
                    }, 'Обновление статуса страницы: ' + (data.title || data.page));
                });

                menu.appendChild(option);
            });

            placeMenu(menu, rect);
        }

        function openSectionMenu(button) {
            closeMenus();

            var row = button.closest('.scmc-catalog-row');
            if (!row) return;

            var data = getRowData(row);
            var rect = button.getBoundingClientRect();
            var menu = document.createElement('div');
            menu.className = 'scmc-section-menu';

            getSections().forEach(function (section) {
                var option = document.createElement('button');
                option.type = 'button';
                option.textContent = section;

                option.addEventListener('click', function (event) {
                    event.preventDefault();
                    event.stopPropagation();
                    menu.remove();

                    if (section === data.section) return;

                    saveRowChange(data, function (sourceData) {
                        sourceData.section = section;
                    }, 'Изменение раздела страницы: ' + (data.title || data.page));
                });

                menu.appendChild(option);
            });

            placeMenu(menu, rect);
        }

        function saveRowChange(targetData, updater, summary) {
            setSaving(true);

            getSource()
                .then(function (source) {
                    var sourceRows = getRowsFromSource(source);
                    var target = findSourceRow(sourceRows, targetData);
                    var changed = Object.assign({}, target.data);

                    updater(changed);

                    var newLine = buildRowLine(changed);
                    var newText = source.slice(0, target.start) + newLine + source.slice(target.end);

                    return saveSource(newText, summary);
                })
                .then(function () {
                    location.reload();
                })
                .catch(function (error) {
                    setSaving(false);
                    alert(error.message || 'Не удалось сохранить изменение. Проверьте вход в аккаунт и права редактора.');
                });
        }

        function moveRow(targetData, targetPosition) {
            setSaving(true);

            getSource()
                .then(function (source) {
                    var sourceRows = getRowsFromSource(source);
                    var target = findSourceRow(sourceRows, targetData);
                    var oldIndex = sourceRows.indexOf(target);
                    var newIndex = Math.max(0, Math.min(sourceRows.length - 1, targetPosition - 1));

                    if (oldIndex === newIndex) {
                        setSaving(false);
                        return null;
                    }

                    var lines = sourceRows.map(function (item) {
                        return item.full;
                    });

                    var moved = lines.splice(oldIndex, 1)[0];
                    lines.splice(newIndex, 0, moved);

                    var first = sourceRows[0];
                    var last = sourceRows[sourceRows.length - 1];
                    var newText = source.slice(0, first.start) + lines.join('\n') + source.slice(last.end);

                    return saveSource(newText, 'Изменение порядка страниц в каталоге Marine Corps');
                })
                .then(function (result) {
                    if (result !== null) location.reload();
                })
                .catch(function (error) {
                    setSaving(false);
                    alert(error.message || 'Не удалось изменить место строки.');
                });
        }

        function setSaving(state) {
            catalog.classList.toggle('scmc-status-saving', !!state);

            Array.prototype.forEach.call(catalog.querySelectorAll('button'), function (button) {
                button.disabled = !!state;
            });
        }

        function update() {
            var query = clean(search.value).toLowerCase();
            var words = query ? query.split(/\s+/).filter(Boolean) : [];
            var visible = 0;

            rowElements.forEach(function (row) {
                var data = getRowData(row);
                var status = normalizeStatus(data.status);
                var noteText = visibleNote(data);
                var statusWords = statusMap[status] ? statusMap[status].words.join(' ') : '';

                var text = [
                    data.title,
                    data.page,
                    data.section,
                    data.note,
                    noteText,
                    data.scan === 'stop' ? 'из основного проекта stop основной' : '',
                    status,
                    statusWords
                ].join(' ').toLowerCase();

                var filterOk =
                    activeFilter === 'all' ||
                    activeFilter === status ||
                    (activeFilter === 'problem' && status !== 'green') ||
                    (activeFilter === 'stop' && data.scan === 'stop') ||
                    (activeFilter === 'note' && !!data.note);

                var textOk = words.every(function (word) {
                    return text.indexOf(word) !== -1;
                });

                var show = filterOk && textOk;
                row.classList.toggle('scmc-catalog-hidden', !show);

                if (show) visible++;
            });

            counter.textContent = 'Показано: ' + visible + ' из ' + rowElements.length;
        }
    });
})();