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

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

Страница интерфейса MediaWiki
Новая страница: «(function () { if (typeof mw === 'undefined' || !window.document) return; var statusMap = { green: { emoji: '🟢', label: 'Готово', words: ['готово', 'готов'] }, yellow: { emoji: '🟡', label: 'Нужно обновить', words: ['нужно обновить', 'обновить', 'устарело'] }, red: { emoji: '🔴', label: 'Нет страницы', words: ['нет страницы', 'нет', 'п...»
 
Нет описания правки
 
(не показано 6 промежуточных версий этой же участницы)
Строка 52: Строка 52:
         var match = String(className || '').match(/\bscmc-level-[1-5]\b/);
         var match = String(className || '').match(/\bscmc-level-[1-5]\b/);
         return match ? match[0] : 'scmc-level-1';
         return match ? match[0] : 'scmc-level-1';
    }
    function getLevelNumber(className) {
        var match = String(className || '').match(/\bscmc-level-([1-5])\b/);
        return match ? parseInt(match[1], 10) : 1;
    }
    function sanitizeClassName(className) {
        return 'scmc-catalog-row ' + getLevelClass(className);
    }
    function setLevelClass(className, level) {
        level = parseInt(level, 10);
        if (!level || level < 1 || level > 5) {
            level = 1;
        }
        return 'scmc-catalog-row scmc-level-' + level;
     }
     }


Строка 69: Строка 88:
         if (title) button.title = title;
         if (title) button.title = title;
         return button;
         return button;
    }
    function getErrorMessage(error, fallback) {
        if (typeof error === 'string') {
            if (error === 'permissiondenied') return 'Нет доступа для сохранения изменений. Проверь вход в аккаунт и права редактора.';
            if (error === 'badtoken') return 'Ошибка токена. Обнови страницу через Ctrl + F5 и попробуй снова.';
            return error;
        }
        if (error && error.error && error.error.info) return error.error.info;
        if (error && error.info) return error.info;
        if (error && error.message) return error.message;
        return fallback || 'Произошла неизвестная ошибка.';
    }
    function closeCatalogDialog(backdrop) {
        if (backdrop && backdrop.parentNode) {
            backdrop.parentNode.removeChild(backdrop);
        }
        document.body.classList.remove('scmc-dialog-open');
    }
    function makeCatalogDialog(title) {
        var backdrop = document.createElement('div');
        backdrop.className = 'scmc-dialog-backdrop';
        var dialog = document.createElement('div');
        dialog.className = 'scmc-dialog';
        var head = document.createElement('div');
        head.className = 'scmc-dialog-head';
        var titleEl = document.createElement('div');
        titleEl.className = 'scmc-dialog-title';
        titleEl.textContent = title || 'Сообщение';
        var closeBtn = document.createElement('button');
        closeBtn.type = 'button';
        closeBtn.className = 'scmc-dialog-close';
        closeBtn.textContent = '×';
        head.appendChild(titleEl);
        head.appendChild(closeBtn);
        var body = document.createElement('div');
        body.className = 'scmc-dialog-body';
        var actions = document.createElement('div');
        actions.className = 'scmc-dialog-actions';
        dialog.appendChild(head);
        dialog.appendChild(body);
        dialog.appendChild(actions);
        backdrop.appendChild(dialog);
        document.body.appendChild(backdrop);
        document.body.classList.add('scmc-dialog-open');
        return {
            backdrop: backdrop,
            dialog: dialog,
            body: body,
            actions: actions,
            closeBtn: closeBtn
        };
    }
    function showCatalogMessage(title, message, type) {
        var ui = makeCatalogDialog(title || 'Сообщение');
        if (type) {
            ui.dialog.setAttribute('data-type', type);
        }
        var text = document.createElement('div');
        text.className = 'scmc-dialog-message';
        text.textContent = message || '';
        var ok = document.createElement('button');
        ok.type = 'button';
        ok.className = 'scmc-dialog-btn scmc-dialog-btn-primary';
        ok.textContent = 'Ок';
        ui.body.appendChild(text);
        ui.actions.appendChild(ok);
        function close() {
            closeCatalogDialog(ui.backdrop);
        }
        ui.closeBtn.addEventListener('click', close);
        ok.addEventListener('click', close);
        ui.backdrop.addEventListener('click', function (event) {
            if (event.target === ui.backdrop) close();
        });
        document.addEventListener('keydown', function esc(event) {
            if (event.key === 'Escape') {
                document.removeEventListener('keydown', esc);
                close();
            }
        });
        setTimeout(function () {
            ok.focus();
        }, 0);
    }
    function askCatalogText(options) {
        return new Promise(function (resolve) {
            options = options || {};
            var ui = makeCatalogDialog(options.title || 'Изменить');
            var label = document.createElement('div');
            label.className = 'scmc-dialog-label';
            label.textContent = options.label || '';
            var input;
            if (options.multiline) {
                input = document.createElement('textarea');
                input.rows = 4;
            } else {
                input = document.createElement('input');
                input.type = 'text';
            }
            input.className = 'scmc-dialog-input';
            input.value = options.value || '';
            var error = document.createElement('div');
            error.className = 'scmc-dialog-error';
            error.textContent = '';
            var cancel = document.createElement('button');
            cancel.type = 'button';
            cancel.className = 'scmc-dialog-btn';
            cancel.textContent = 'Отмена';
            var save = document.createElement('button');
            save.type = 'button';
            save.className = 'scmc-dialog-btn scmc-dialog-btn-primary';
            save.textContent = options.saveText || 'Сохранить';
            ui.body.appendChild(label);
            ui.body.appendChild(input);
            ui.body.appendChild(error);
            ui.actions.appendChild(cancel);
            ui.actions.appendChild(save);
            function close(value) {
                closeCatalogDialog(ui.backdrop);
                resolve(value);
            }
            function submit() {
                var value = input.value;
                if (typeof options.validate === 'function') {
                    var validation = options.validate(value);
                    if (validation) {
                        error.textContent = validation;
                        input.classList.add('is-error');
                        return;
                    }
                }
                close(value);
            }
            ui.closeBtn.addEventListener('click', function () {
                close(null);
            });
            cancel.addEventListener('click', function () {
                close(null);
            });
            save.addEventListener('click', submit);
            input.addEventListener('input', function () {
                error.textContent = '';
                input.classList.remove('is-error');
            });
            input.addEventListener('keydown', function (event) {
                if (event.key === 'Enter' && !options.multiline) {
                    event.preventDefault();
                    submit();
                }
                if (event.key === 'Escape') {
                    event.preventDefault();
                    close(null);
                }
            });
            ui.backdrop.addEventListener('click', function (event) {
                if (event.target === ui.backdrop) close(null);
            });
            setTimeout(function () {
                input.focus();
                input.select();
            }, 0);
        });
     }
     }


Строка 74: Строка 304:
         var statusMenu = document.querySelector('.scmc-status-menu');
         var statusMenu = document.querySelector('.scmc-status-menu');
         var sectionMenu = document.querySelector('.scmc-section-menu');
         var sectionMenu = document.querySelector('.scmc-section-menu');
        var levelMenu = document.querySelector('.scmc-level-menu');


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


Строка 142: Строка 374:


             var data = {
             var data = {
                 className: attrFromTag(tag, 'class') || 'scmc-catalog-row scmc-level-1',
                 className: sanitizeClassName(attrFromTag(tag, 'class') || 'scmc-catalog-row scmc-level-1'),
                 section: clean(attrFromTag(tag, 'data-section')),
                 section: clean(attrFromTag(tag, 'data-section')),
                 page: clean(attrFromTag(tag, 'data-page')),
                 page: clean(attrFromTag(tag, 'data-page')),
Строка 184: Строка 416:


     function buildRowLine(data) {
     function buildRowLine(data) {
         var className = clean(data.className || 'scmc-catalog-row scmc-level-1');
         var className = sanitizeClassName(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 section = clean(data.section);
         var page = clean(data.page);
         var page = clean(data.page);
Строка 257: Строка 483:
             var statusButton = event.target.closest('.scmc-catalog-status');
             var statusButton = event.target.closest('.scmc-catalog-status');
             var sectionButton = event.target.closest('.scmc-catalog-section-btn');
             var sectionButton = event.target.closest('.scmc-catalog-section-btn');
            var levelButton = event.target.closest('.scmc-catalog-level-btn');


             if (statusButton && catalog.contains(statusButton)) {
             if (statusButton && catalog.contains(statusButton)) {
Строка 272: Строка 499:
             }
             }


             if (!event.target.closest('.scmc-status-menu') && !event.target.closest('.scmc-section-menu')) {
             if (levelButton && catalog.contains(levelButton)) {
                event.preventDefault();
                event.stopPropagation();
                openLevelMenu(levelButton);
                return;
            }
 
            if (
                !event.target.closest('.scmc-status-menu') &&
                !event.target.closest('.scmc-section-menu') &&
                !event.target.closest('.scmc-level-menu')
            ) {
                 closeMenus();
                 closeMenus();
             }
             }
Строка 295: Строка 533:
                 ['blue', '🔵 Заморожено'],
                 ['blue', '🔵 Заморожено'],
                 ['problem', 'Проблемные'],
                 ['problem', 'Проблемные'],
                 ['stop', 'Из основного проекта'],
                 ['stop', 'Scan OFF'],
                 ['note', 'С заметками']
                 ['note', 'С заметками']
             ].forEach(function (item, index) {
             ].forEach(function (item, index) {
Строка 316: Строка 554:
         function getRowData(row) {
         function getRowData(row) {
             return {
             return {
                 className: row.getAttribute('class') || 'scmc-catalog-row scmc-level-1',
                 className: sanitizeClassName(row.getAttribute('class') || 'scmc-catalog-row scmc-level-1'),
                 section: clean(row.getAttribute('data-section')),
                 section: clean(row.getAttribute('data-section')),
                 page: clean(row.getAttribute('data-page')),
                 page: clean(row.getAttribute('data-page')),
Строка 327: Строка 565:


         function visibleNote(data) {
         function visibleNote(data) {
             var parts = [];
             return clean(data.note);
 
            if (data.scan === 'stop') {
                parts.push('Из основного проекта.');
            }
 
            if (data.note) {
                parts.push(data.note);
            }
 
            return parts.join(' ');
         }
         }


         function getSections() {
         function getSections() {
            var order = [
                'Морпехи',
                'Ксеноморфы',
                'Правила и процедуры',
                'Справочник',
                'Лор и материалы',
                'Общее'
            ];
             var map = {};
             var map = {};


Строка 348: Строка 585:
             });
             });


             return Object.keys(map).sort(function (a, b) {
             Object.keys(map).forEach(function (section) {
                 return a.localeCompare(b, 'ru');
                if (order.indexOf(section) === -1) {
                    order.push(section);
                 }
            });
 
            return order.filter(function (section) {
                return map[section];
             });
             });
         }
         }
Строка 361: Строка 604:
                 var isStop = data.scan === 'stop';
                 var isStop = data.scan === 'stop';


                row.className = sanitizeClassName(data.className);
                 row.setAttribute('data-status', status);
                 row.setAttribute('data-status', status);
                 if (isStop) row.setAttribute('data-scan', 'stop');
 
                 if (isStop) {
                    row.setAttribute('data-scan', 'stop');
                } else {
                    row.removeAttribute('data-scan');
                }


                 row.setAttribute('data-scmc-enhanced', 'true');
                 row.setAttribute('data-scmc-enhanced', 'true');
Строка 370: Строка 619:


                 position.addEventListener('click', function () {
                 position.addEventListener('click', function () {
                     var answer = prompt('На какое место переместить страницу? Сейчас: ' + (index + 1) + '. Всего: ' + rows.length + '.', String(index + 1));
                     var currentIndex = rowElements.indexOf(row) + 1;
                    if (answer === null) return;
                    var total = rowElements.length;
 
                    askCatalogText({
                        title: 'Изменить место страницы',
                        label: 'Введите новое место. Сейчас: ' + currentIndex + '. Всего: ' + total + '.',
                        value: String(currentIndex),
                        saveText: 'Переместить',
                        validate: function (value) {
                            var raw = clean(value);
 
                            if (!/^\d+$/.test(raw)) {
                                return 'Нужно указать число от 1 до ' + total + '.';
                            }
 
                            var target = parseInt(raw, 10);
 
                            if (target < 1 || target > total) {
                                return 'Нужно число от 1 до ' + total + '.';
                            }


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


                    moveRow(data, target);
                        moveRow(row, getRowData(row), parseInt(answer, 10));
                    });
                 });
                 });


Строка 392: Строка 659:
                 link.textContent = data.title || data.page;
                 link.textContent = data.title || data.page;
                 titleWrap.appendChild(link);
                 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);
                 main.appendChild(titleWrap);
Строка 416: Строка 676:


                 noteButton.addEventListener('click', function () {
                 noteButton.addEventListener('click', function () {
                     var answer = prompt('Заметка для страницы "' + (data.title || data.page) + '". Пустое поле удалит data-note.', data.note || '');
                     var currentData = getRowData(row);
                    if (answer === null) return;
 
                    askCatalogText({
                        title: 'Заметка к странице',
                        label: 'Страница: ' + (currentData.title || currentData.page) + '. Пустое поле удалит заметку.',
                        value: currentData.note || '',
                        multiline: true,
                        saveText: 'Сохранить'
                    }).then(function (answer) {
                        if (answer === null) return;


                    saveRowChange(data, function (sourceData) {
                        saveRowChange(row, currentData, function (sourceData) {
                        sourceData.note = clean(answer);
                            sourceData.note = clean(answer);
                    }, 'Обновление заметки страницы: ' + (data.title || data.page));
                        }, 'Обновление заметки страницы: ' + (currentData.title || currentData.page));
                    });
                 });
                 });
                var levelButton = makeButton(
                    'scmc-catalog-level-btn',
                    'Ур. ' + getLevelNumber(data.className),
                    'Изменить уровень'
                );
                var sectionButton = makeButton('scmc-catalog-section-btn', data.section || 'Без раздела', 'Изменить раздел');


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


Строка 433: Строка 710:


                 scanToggle.addEventListener('click', function () {
                 scanToggle.addEventListener('click', function () {
                     saveRowChange(data, function (sourceData) {
                    var currentData = getRowData(row);
 
                     saveRowChange(row, currentData, function (sourceData) {
                         sourceData.scan = sourceData.scan === 'stop' ? '' : 'stop';
                         sourceData.scan = sourceData.scan === 'stop' ? '' : 'stop';
                     }, 'Изменение режима сканирования страницы: ' + (data.title || data.page));
                     }, 'Изменение режима сканирования страницы: ' + (currentData.title || currentData.page));
                 });
                 });
                var sectionButton = makeButton('scmc-catalog-section-btn', data.section || 'Без раздела', 'Изменить раздел');


                 var statusButton = makeButton('scmc-catalog-status', statusMap[status].emoji, statusMap[status].label);
                 var statusButton = makeButton('scmc-catalog-status', statusMap[status].emoji, statusMap[status].label);
Строка 444: Строка 721:


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


Строка 477: Строка 755:
                     if (status === data.status) return;
                     if (status === data.status) return;


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


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


         function saveRowChange(targetData, updater, summary) {
        function openLevelMenu(button) {
            closeMenus();
 
            var row = button.closest('.scmc-catalog-row');
            if (!row) return;
 
            var data = getRowData(row);
            var currentLevel = getLevelNumber(data.className);
            var rect = button.getBoundingClientRect();
            var menu = document.createElement('div');
            menu.className = 'scmc-level-menu';
 
            [1, 2, 3, 4, 5].forEach(function (level) {
                var option = document.createElement('button');
                option.type = 'button';
                option.textContent = (level === currentLevel ? '✓ ' : '') + 'Уровень ' + level;
 
                option.addEventListener('click', function (event) {
                    event.preventDefault();
                    event.stopPropagation();
                    menu.remove();
 
                    if (level === currentLevel) return;
 
                    saveRowChange(row, data, function (sourceData) {
                        sourceData.className = setLevelClass(sourceData.className, level);
                    }, 'Изменение уровня страницы: ' + (data.title || data.page));
                });
 
                menu.appendChild(option);
            });
 
            placeMenu(menu, rect);
        }
 
        function applyLocalRowUpdate(row, data) {
            var className = sanitizeClassName(data.className || row.getAttribute('class') || 'scmc-catalog-row scmc-level-1');
            var status = normalizeStatus(data.status);
            var scan = clean(data.scan);
            var wasHidden = row.classList.contains('scmc-catalog-hidden');
            var noteText;
 
            row.className = className;
            row.classList.toggle('scmc-catalog-hidden', wasHidden);
 
            row.setAttribute('data-section', clean(data.section));
            row.setAttribute('data-page', clean(data.page));
            row.setAttribute('data-title', clean(data.title || data.page));
            row.setAttribute('data-status', status);
 
            if (scan) {
                row.setAttribute('data-scan', scan);
            } else {
                row.removeAttribute('data-scan');
            }
 
            if (clean(data.note)) {
                row.setAttribute('data-note', clean(data.note));
            } else {
                row.removeAttribute('data-note');
            }
 
            row.setAttribute('data-scmc-enhanced', 'true');
 
            var main = row.querySelector('.scmc-catalog-main');
 
            if (main) {
                main.innerHTML = '';
 
                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);
 
                main.appendChild(titleWrap);
 
                noteText = visibleNote(data);
 
                if (noteText) {
                    var note = document.createElement('div');
                    note.className = 'scmc-catalog-note';
                    note.textContent = noteText;
                    main.appendChild(note);
                }
            }
 
            var scanToggle = row.querySelector('.scmc-scan-toggle');
            if (scanToggle) {
                scanToggle.setAttribute('data-active', scan === 'stop' ? 'true' : 'false');
                scanToggle.title = scan === 'stop' ? 'Scan OFF включён. Нажми, чтобы снова сканировать.' : 'Нажми, чтобы включить Scan OFF.';
            }
 
            var sectionButton = row.querySelector('.scmc-catalog-section-btn');
            if (sectionButton) {
                sectionButton.textContent = data.section || 'Без раздела';
            }
 
            var levelButton = row.querySelector('.scmc-catalog-level-btn');
            if (levelButton) {
                levelButton.textContent = 'Ур. ' + getLevelNumber(data.className);
            }
 
            var statusButton = row.querySelector('.scmc-catalog-status');
            if (statusButton) {
                statusButton.textContent = statusMap[status].emoji;
                statusButton.title = statusMap[status].label;
                statusButton.setAttribute('data-status', status);
            }
        }
 
        function refreshIndexes() {
            rowElements = Array.prototype.slice.call(catalog.querySelectorAll('.scmc-catalog-row'));
 
            rowElements.forEach(function (row, index) {
                var indexButton = row.querySelector('.scmc-catalog-index');
 
                if (indexButton) {
                    indexButton.textContent = String(index + 1);
                }
            });
        }
 
         function saveRowChange(row, targetData, updater, summary) {
             setSaving(true);
             setSaving(true);


Строка 532: Строка 935:


                     updater(changed);
                     updater(changed);
                    changed.className = sanitizeClassName(changed.className);


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


                     return saveSource(newText, summary);
                     return saveSource(newText, summary).then(function () {
                        return changed;
                    });
                 })
                 })
                 .then(function () {
                 .then(function (changed) {
                     location.reload();
                     applyLocalRowUpdate(row, changed);
                    refreshIndexes();
                    update();
                    setSaving(false);
                 })
                 })
                 .catch(function (error) {
                 .catch(function (error) {
                     setSaving(false);
                     setSaving(false);
                     alert(error.message || 'Не удалось сохранить изменение. Проверьте вход в аккаунт и права редактора.');
                     showCatalogMessage(
                        'Ошибка сохранения',
                        getErrorMessage(error, 'Не удалось сохранить изменение. Проверь вход в аккаунт и права редактора.'),
                        'error'
                    );
                 });
                 });
         }
         }


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


Строка 573: Строка 987:
                     var newText = source.slice(0, first.start) + lines.join('\n') + source.slice(last.end);
                     var newText = source.slice(0, first.start) + lines.join('\n') + source.slice(last.end);


                     return saveSource(newText, 'Изменение порядка страниц в каталоге Marine Corps');
                     return saveSource(newText, 'Изменение порядка страниц в каталоге Marine Corps').then(function () {
                        return newIndex;
                    });
                 })
                 })
                 .then(function (result) {
                 .then(function (newIndex) {
                     if (result !== null) location.reload();
                     if (newIndex !== null) {
                        var list = catalog.querySelector('.scmc-catalog-list');
                        var rowsWithoutMoving;
 
                        rowElements = Array.prototype.slice.call(list.querySelectorAll('.scmc-catalog-row'));
 
                        rowsWithoutMoving = rowElements.filter(function (item) {
                            return item !== row;
                        });
 
                        list.insertBefore(row, rowsWithoutMoving[newIndex] || null);
                        refreshIndexes();
                        update();
                    }
 
                    setSaving(false);
                 })
                 })
                 .catch(function (error) {
                 .catch(function (error) {
                     setSaving(false);
                     setSaving(false);
                     alert(error.message || 'Не удалось изменить место строки.');
                     showCatalogMessage(
                        'Ошибка перемещения',
                        getErrorMessage(error, 'Не удалось изменить место строки. Проверь вход в аккаунт и права редактора.'),
                        'error'
                    );
                 });
                 });
         }
         }
Строка 602: Строка 1037:
                 var noteText = visibleNote(data);
                 var noteText = visibleNote(data);
                 var statusWords = statusMap[status] ? statusMap[status].words.join(' ') : '';
                 var statusWords = statusMap[status] ? statusMap[status].words.join(' ') : '';
                var level = getLevelNumber(data.className);


                 var text = [
                 var text = [
Строка 609: Строка 1045:
                     data.note,
                     data.note,
                     noteText,
                     noteText,
                     data.scan === 'stop' ? 'из основного проекта stop основной' : '',
                     'уровень ' + level,
                    'ур ' + level,
                     status,
                     status,
                     statusWords
                     statusWords
Строка 633: Строка 1070:
             counter.textContent = 'Показано: ' + visible + ' из ' + rowElements.length;
             counter.textContent = 'Показано: ' + visible + ' из ' + rowElements.length;
         }
         }
    });
        ready(function () {
        var guide = document.querySelector('.scmc-renata-guide');
        if (!guide) return;
        var toggle = guide.querySelector('.scmc-renata-guide-toggle');
        if (!toggle) return;
        function switchGuide() {
            var isOpen = guide.getAttribute('data-open') === 'true';
            guide.setAttribute('data-open', isOpen ? 'false' : 'true');
        }
        toggle.addEventListener('click', switchGuide);
        toggle.addEventListener('keydown', function (event) {
            if (event.key === 'Enter' || event.key === ' ') {
                event.preventDefault();
                switchGuide();
            }
        });
     });
     });
})();
})();

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

(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, '&amp;')
            .replace(/"/g, '&quot;')
            .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 getLevelNumber(className) {
        var match = String(className || '').match(/\bscmc-level-([1-5])\b/);
        return match ? parseInt(match[1], 10) : 1;
    }

    function sanitizeClassName(className) {
        return 'scmc-catalog-row ' + getLevelClass(className);
    }

    function setLevelClass(className, level) {
        level = parseInt(level, 10);

        if (!level || level < 1 || level > 5) {
            level = 1;
        }

        return 'scmc-catalog-row scmc-level-' + level;
    }

    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 getErrorMessage(error, fallback) {
        if (typeof error === 'string') {
            if (error === 'permissiondenied') return 'Нет доступа для сохранения изменений. Проверь вход в аккаунт и права редактора.';
            if (error === 'badtoken') return 'Ошибка токена. Обнови страницу через Ctrl + F5 и попробуй снова.';
            return error;
        }

        if (error && error.error && error.error.info) return error.error.info;
        if (error && error.info) return error.info;
        if (error && error.message) return error.message;

        return fallback || 'Произошла неизвестная ошибка.';
    }

    function closeCatalogDialog(backdrop) {
        if (backdrop && backdrop.parentNode) {
            backdrop.parentNode.removeChild(backdrop);
        }

        document.body.classList.remove('scmc-dialog-open');
    }

    function makeCatalogDialog(title) {
        var backdrop = document.createElement('div');
        backdrop.className = 'scmc-dialog-backdrop';

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

        var head = document.createElement('div');
        head.className = 'scmc-dialog-head';

        var titleEl = document.createElement('div');
        titleEl.className = 'scmc-dialog-title';
        titleEl.textContent = title || 'Сообщение';

        var closeBtn = document.createElement('button');
        closeBtn.type = 'button';
        closeBtn.className = 'scmc-dialog-close';
        closeBtn.textContent = '×';

        head.appendChild(titleEl);
        head.appendChild(closeBtn);

        var body = document.createElement('div');
        body.className = 'scmc-dialog-body';

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

        dialog.appendChild(head);
        dialog.appendChild(body);
        dialog.appendChild(actions);
        backdrop.appendChild(dialog);

        document.body.appendChild(backdrop);
        document.body.classList.add('scmc-dialog-open');

        return {
            backdrop: backdrop,
            dialog: dialog,
            body: body,
            actions: actions,
            closeBtn: closeBtn
        };
    }

    function showCatalogMessage(title, message, type) {
        var ui = makeCatalogDialog(title || 'Сообщение');

        if (type) {
            ui.dialog.setAttribute('data-type', type);
        }

        var text = document.createElement('div');
        text.className = 'scmc-dialog-message';
        text.textContent = message || '';

        var ok = document.createElement('button');
        ok.type = 'button';
        ok.className = 'scmc-dialog-btn scmc-dialog-btn-primary';
        ok.textContent = 'Ок';

        ui.body.appendChild(text);
        ui.actions.appendChild(ok);

        function close() {
            closeCatalogDialog(ui.backdrop);
        }

        ui.closeBtn.addEventListener('click', close);
        ok.addEventListener('click', close);

        ui.backdrop.addEventListener('click', function (event) {
            if (event.target === ui.backdrop) close();
        });

        document.addEventListener('keydown', function esc(event) {
            if (event.key === 'Escape') {
                document.removeEventListener('keydown', esc);
                close();
            }
        });

        setTimeout(function () {
            ok.focus();
        }, 0);
    }

    function askCatalogText(options) {
        return new Promise(function (resolve) {
            options = options || {};

            var ui = makeCatalogDialog(options.title || 'Изменить');

            var label = document.createElement('div');
            label.className = 'scmc-dialog-label';
            label.textContent = options.label || '';

            var input;

            if (options.multiline) {
                input = document.createElement('textarea');
                input.rows = 4;
            } else {
                input = document.createElement('input');
                input.type = 'text';
            }

            input.className = 'scmc-dialog-input';
            input.value = options.value || '';

            var error = document.createElement('div');
            error.className = 'scmc-dialog-error';
            error.textContent = '';

            var cancel = document.createElement('button');
            cancel.type = 'button';
            cancel.className = 'scmc-dialog-btn';
            cancel.textContent = 'Отмена';

            var save = document.createElement('button');
            save.type = 'button';
            save.className = 'scmc-dialog-btn scmc-dialog-btn-primary';
            save.textContent = options.saveText || 'Сохранить';

            ui.body.appendChild(label);
            ui.body.appendChild(input);
            ui.body.appendChild(error);
            ui.actions.appendChild(cancel);
            ui.actions.appendChild(save);

            function close(value) {
                closeCatalogDialog(ui.backdrop);
                resolve(value);
            }

            function submit() {
                var value = input.value;

                if (typeof options.validate === 'function') {
                    var validation = options.validate(value);

                    if (validation) {
                        error.textContent = validation;
                        input.classList.add('is-error');
                        return;
                    }
                }

                close(value);
            }

            ui.closeBtn.addEventListener('click', function () {
                close(null);
            });

            cancel.addEventListener('click', function () {
                close(null);
            });

            save.addEventListener('click', submit);

            input.addEventListener('input', function () {
                error.textContent = '';
                input.classList.remove('is-error');
            });

            input.addEventListener('keydown', function (event) {
                if (event.key === 'Enter' && !options.multiline) {
                    event.preventDefault();
                    submit();
                }

                if (event.key === 'Escape') {
                    event.preventDefault();
                    close(null);
                }
            });

            ui.backdrop.addEventListener('click', function (event) {
                if (event.target === ui.backdrop) close(null);
            });

            setTimeout(function () {
                input.focus();
                input.select();
            }, 0);
        });
    }

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

        if (statusMenu) statusMenu.remove();
        if (sectionMenu) sectionMenu.remove();
        if (levelMenu) levelMenu.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: sanitizeClassName(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 = sanitizeClassName(data.className || 'scmc-catalog-row scmc-level-1');
        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');
            var levelButton = event.target.closest('.scmc-catalog-level-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 (levelButton && catalog.contains(levelButton)) {
                event.preventDefault();
                event.stopPropagation();
                openLevelMenu(levelButton);
                return;
            }

            if (
                !event.target.closest('.scmc-status-menu') &&
                !event.target.closest('.scmc-section-menu') &&
                !event.target.closest('.scmc-level-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', 'Scan OFF'],
                ['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: sanitizeClassName(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) {
            return clean(data.note);
        }

        function getSections() {
            var order = [
                'Морпехи',
                'Ксеноморфы',
                'Правила и процедуры',
                'Справочник',
                'Лор и материалы',
                'Общее'
            ];

            var map = {};

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

            Object.keys(map).forEach(function (section) {
                if (order.indexOf(section) === -1) {
                    order.push(section);
                }
            });

            return order.filter(function (section) {
                return map[section];
            });
        }

        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.className = sanitizeClassName(data.className);
                row.setAttribute('data-status', status);

                if (isStop) {
                    row.setAttribute('data-scan', 'stop');
                } else {
                    row.removeAttribute('data-scan');
                }

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

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

                position.addEventListener('click', function () {
                    var currentIndex = rowElements.indexOf(row) + 1;
                    var total = rowElements.length;

                    askCatalogText({
                        title: 'Изменить место страницы',
                        label: 'Введите новое место. Сейчас: ' + currentIndex + '. Всего: ' + total + '.',
                        value: String(currentIndex),
                        saveText: 'Переместить',
                        validate: function (value) {
                            var raw = clean(value);

                            if (!/^\d+$/.test(raw)) {
                                return 'Нужно указать число от 1 до ' + total + '.';
                            }

                            var target = parseInt(raw, 10);

                            if (target < 1 || target > total) {
                                return 'Нужно число от 1 до ' + total + '.';
                            }

                            return '';
                        }
                    }).then(function (answer) {
                        if (answer === null) return;

                        moveRow(row, getRowData(row), parseInt(answer, 10));
                    });
                });

                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);

                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 currentData = getRowData(row);

                    askCatalogText({
                        title: 'Заметка к странице',
                        label: 'Страница: ' + (currentData.title || currentData.page) + '. Пустое поле удалит заметку.',
                        value: currentData.note || '',
                        multiline: true,
                        saveText: 'Сохранить'
                    }).then(function (answer) {
                        if (answer === null) return;

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

                var levelButton = makeButton(
                    'scmc-catalog-level-btn',
                    'Ур. ' + getLevelNumber(data.className),
                    'Изменить уровень'
                );

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

                var scanToggle = makeButton(
                    'scmc-catalog-btn scmc-scan-toggle',
                    '',
                    isStop ? 'Scan OFF включён. Нажми, чтобы снова сканировать.' : 'Нажми, чтобы включить Scan OFF.'
                );

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

                scanToggle.addEventListener('click', function () {
                    var currentData = getRowData(row);

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

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

                actions.appendChild(noteButton);
                actions.appendChild(levelButton);
                actions.appendChild(sectionButton);
                actions.appendChild(scanToggle);
                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(row, 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(row, data, function (sourceData) {
                        sourceData.section = section;
                    }, 'Изменение раздела страницы: ' + (data.title || data.page));
                });

                menu.appendChild(option);
            });

            placeMenu(menu, rect);
        }

        function openLevelMenu(button) {
            closeMenus();

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

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

            [1, 2, 3, 4, 5].forEach(function (level) {
                var option = document.createElement('button');
                option.type = 'button';
                option.textContent = (level === currentLevel ? '✓ ' : '') + 'Уровень ' + level;

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

                    if (level === currentLevel) return;

                    saveRowChange(row, data, function (sourceData) {
                        sourceData.className = setLevelClass(sourceData.className, level);
                    }, 'Изменение уровня страницы: ' + (data.title || data.page));
                });

                menu.appendChild(option);
            });

            placeMenu(menu, rect);
        }

        function applyLocalRowUpdate(row, data) {
            var className = sanitizeClassName(data.className || row.getAttribute('class') || 'scmc-catalog-row scmc-level-1');
            var status = normalizeStatus(data.status);
            var scan = clean(data.scan);
            var wasHidden = row.classList.contains('scmc-catalog-hidden');
            var noteText;

            row.className = className;
            row.classList.toggle('scmc-catalog-hidden', wasHidden);

            row.setAttribute('data-section', clean(data.section));
            row.setAttribute('data-page', clean(data.page));
            row.setAttribute('data-title', clean(data.title || data.page));
            row.setAttribute('data-status', status);

            if (scan) {
                row.setAttribute('data-scan', scan);
            } else {
                row.removeAttribute('data-scan');
            }

            if (clean(data.note)) {
                row.setAttribute('data-note', clean(data.note));
            } else {
                row.removeAttribute('data-note');
            }

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

            var main = row.querySelector('.scmc-catalog-main');

            if (main) {
                main.innerHTML = '';

                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);

                main.appendChild(titleWrap);

                noteText = visibleNote(data);

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

            var scanToggle = row.querySelector('.scmc-scan-toggle');
            if (scanToggle) {
                scanToggle.setAttribute('data-active', scan === 'stop' ? 'true' : 'false');
                scanToggle.title = scan === 'stop' ? 'Scan OFF включён. Нажми, чтобы снова сканировать.' : 'Нажми, чтобы включить Scan OFF.';
            }

            var sectionButton = row.querySelector('.scmc-catalog-section-btn');
            if (sectionButton) {
                sectionButton.textContent = data.section || 'Без раздела';
            }

            var levelButton = row.querySelector('.scmc-catalog-level-btn');
            if (levelButton) {
                levelButton.textContent = 'Ур. ' + getLevelNumber(data.className);
            }

            var statusButton = row.querySelector('.scmc-catalog-status');
            if (statusButton) {
                statusButton.textContent = statusMap[status].emoji;
                statusButton.title = statusMap[status].label;
                statusButton.setAttribute('data-status', status);
            }
        }

        function refreshIndexes() {
            rowElements = Array.prototype.slice.call(catalog.querySelectorAll('.scmc-catalog-row'));

            rowElements.forEach(function (row, index) {
                var indexButton = row.querySelector('.scmc-catalog-index');

                if (indexButton) {
                    indexButton.textContent = String(index + 1);
                }
            });
        }

        function saveRowChange(row, 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);

                    changed.className = sanitizeClassName(changed.className);

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

                    return saveSource(newText, summary).then(function () {
                        return changed;
                    });
                })
                .then(function (changed) {
                    applyLocalRowUpdate(row, changed);
                    refreshIndexes();
                    update();
                    setSaving(false);
                })
                .catch(function (error) {
                    setSaving(false);
                    showCatalogMessage(
                        'Ошибка сохранения',
                        getErrorMessage(error, 'Не удалось сохранить изменение. Проверь вход в аккаунт и права редактора.'),
                        'error'
                    );
                });
        }

        function moveRow(row, 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 () {
                        return newIndex;
                    });
                })
                .then(function (newIndex) {
                    if (newIndex !== null) {
                        var list = catalog.querySelector('.scmc-catalog-list');
                        var rowsWithoutMoving;

                        rowElements = Array.prototype.slice.call(list.querySelectorAll('.scmc-catalog-row'));

                        rowsWithoutMoving = rowElements.filter(function (item) {
                            return item !== row;
                        });

                        list.insertBefore(row, rowsWithoutMoving[newIndex] || null);
                        refreshIndexes();
                        update();
                    }

                    setSaving(false);
                })
                .catch(function (error) {
                    setSaving(false);
                    showCatalogMessage(
                        'Ошибка перемещения',
                        getErrorMessage(error, 'Не удалось изменить место строки. Проверь вход в аккаунт и права редактора.'),
                        'error'
                    );
                });
        }

        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 level = getLevelNumber(data.className);

                var text = [
                    data.title,
                    data.page,
                    data.section,
                    data.note,
                    noteText,
                    'уровень ' + level,
                    'ур ' + level,
                    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;
        }
    });
        ready(function () {
        var guide = document.querySelector('.scmc-renata-guide');
        if (!guide) return;

        var toggle = guide.querySelector('.scmc-renata-guide-toggle');
        if (!toggle) return;

        function switchGuide() {
            var isOpen = guide.getAttribute('data-open') === 'true';
            guide.setAttribute('data-open', isOpen ? 'false' : 'true');
        }

        toggle.addEventListener('click', switchGuide);

        toggle.addEventListener('keydown', function (event) {
            if (event.key === 'Enter' || event.key === ' ') {
                event.preventDefault();
                switchGuide();
            }
        });
    });
})();