MediaWiki:ScmcCatalog.js: различия между версиями
Страница интерфейса MediaWiki
Дополнительные действия
Defer (обсуждение | вклад) Нет описания правки |
Defer (обсуждение | вклад) Нет описания правки |
||
| (не показаны 2 промежуточные версии этой же участницы) | |||
| Строка 88: | Строка 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); | |||
}); | |||
} | } | ||
| Строка 410: | Строка 621: | ||
var currentIndex = rowElements.indexOf(row) + 1; | var currentIndex = rowElements.indexOf(row) + 1; | ||
var total = rowElements.length; | var total = rowElements.length; | ||
var target = parseInt( | 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)); | |||
}); | |||
}); | }); | ||
| Строка 450: | Строка 677: | ||
noteButton.addEventListener('click', function () { | noteButton.addEventListener('click', function () { | ||
var currentData = getRowData(row); | var currentData = getRowData(row); | ||
saveRowChange(row, currentData, function (sourceData) { | 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)); | |||
}); | |||
}); | }); | ||
| Строка 718: | Строка 953: | ||
.catch(function (error) { | .catch(function (error) { | ||
setSaving(false); | setSaving(false); | ||
showCatalogMessage( | |||
'Ошибка сохранения', | |||
getErrorMessage(error, 'Не удалось сохранить изменение. Проверь вход в аккаунт и права редактора.'), | |||
'error' | |||
); | |||
}); | }); | ||
} | } | ||
| Строка 772: | Строка 1011: | ||
.catch(function (error) { | .catch(function (error) { | ||
setSaving(false); | setSaving(false); | ||
showCatalogMessage( | |||
'Ошибка перемещения', | |||
getErrorMessage(error, 'Не удалось изменить место строки. Проверь вход в аккаунт и права редактора.'), | |||
'error' | |||
); | |||
}); | }); | ||
} | } | ||
| Строка 802: | Строка 1045: | ||
data.note, | data.note, | ||
noteText, | noteText, | ||
'уровень ' + level, | 'уровень ' + level, | ||
'ур ' + level, | 'ур ' + level, | ||
| Строка 828: | Строка 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, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
}
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();
}
});
});
})();