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

MediaWiki:ScmcCatalog.jss: различия между версиями

Страница интерфейса MediaWiki
Новая страница: «(function () { if (typeof mw === 'undefined' || !window.document) return; var statusMap = { green: { emoji: '🟢', label: 'Готово', words: ['готово', 'готов'] }, yellow: { emoji: '🟡', label: 'Нужно обновить', words: ['нужно обновить', 'обновить', 'устарело'] }, red: { emoji: '🔴', label: 'Нет страницы', words: ['нет страницы', 'нет', 'п...»
 
Полностью удалено содержимое страницы
Метка: очистка
 
Строка 1: Строка 1:
(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;
        }
    });
})();

Текущая версия от 10:09, 15 июня 2026