MediaWiki:ScmcCatalog.js: различия между версиями
Страница интерфейса MediaWiki
Дополнительные действия
Defer (обсуждение | вклад) Нет описания правки |
Defer (обсуждение | вклад) Нет описания правки |
||
| Строка 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) { | |||
var level = getLevelClass(className); | |||
var hasRowClass = false; | |||
String(className || '') | |||
.split(/\s+/) | |||
.forEach(function (item) { | |||
if (item === 'scmc-catalog-row') { | |||
hasRowClass = true; | |||
} | |||
}); | |||
return (hasRowClass ? 'scmc-catalog-row ' : 'scmc-catalog-row ') + level; | |||
} | |||
function setLevelClass(className, level) { | |||
level = parseInt(level, 10); | |||
if (!level || level < 1 || level > 5) { | |||
level = 1; | |||
} | |||
return 'scmc-catalog-row scmc-level-' + level; | |||
} | } | ||
| Строка 144: | Строка 174: | ||
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')), | ||
| Строка 187: | Строка 217: | ||
function buildRowLine(data) { | function buildRowLine(data) { | ||
var className = sanitizeClassName(data.className || 'scmc-catalog-row scmc-level-1'); | var className = sanitizeClassName(data.className || 'scmc-catalog-row scmc-level-1'); | ||
var section = clean(data.section); | var section = clean(data.section); | ||
var page = clean(data.page); | var page = clean(data.page); | ||
| Строка 295: | Строка 319: | ||
} | } | ||
if (!event.target.closest('.scmc-status-menu') && !event.target.closest('.scmc-section-menu') && !event.target.closest('.scmc-level-menu')) { | if ( | ||
!event.target.closest('.scmc-status-menu') && | |||
!event.target.closest('.scmc-section-menu') && | |||
!event.target.closest('.scmc-level-menu') | |||
) { | |||
closeMenus(); | closeMenus(); | ||
} | } | ||
| Строка 362: | Строка 390: | ||
'Общее' | 'Общее' | ||
]; | ]; | ||
var map = {}; | var map = {}; | ||
| Строка 388: | Строка 417: | ||
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'); | ||
| Строка 395: | Строка 430: | ||
var position = makeButton('scmc-catalog-index', String(index + 1), 'Изменить место'); | var position = makeButton('scmc-catalog-index', String(index + 1), 'Изменить место'); | ||
var expandButton = makeButton('scmc-catalog-expand', '›', 'Показать действия'); | var expandButton = makeButton('scmc-catalog-expand', '›', 'Показать действия'); | ||
expandButton.setAttribute('aria-expanded', 'false'); | expandButton.setAttribute('aria-expanded', 'false'); | ||
| Строка 436: | Строка 472: | ||
var actions = document.createElement('div'); | var actions = document.createElement('div'); | ||
actions.className = 'scmc-catalog-actions'; | actions.className = 'scmc-catalog-actions'; | ||
var statusButton = makeButton('scmc-catalog-status', statusMap[status].emoji, statusMap[status].label); | |||
statusButton.setAttribute('data-status', status); | |||
var sectionButton = makeButton('scmc-catalog-section-btn', data.section || 'Без раздела', 'Изменить раздел'); | |||
var levelButton = makeButton( | |||
'scmc-catalog-level-btn', | |||
'Ур. ' + getLevelNumber(data.className), | |||
'Изменить уровень' | |||
); | |||
var noteButton = makeButton('scmc-catalog-btn scmc-catalog-note-btn', '✎', 'Изменить заметку'); | var noteButton = makeButton('scmc-catalog-btn scmc-catalog-note-btn', '✎', 'Изменить заметку'); | ||
| Строка 464: | Строка 511: | ||
}, 'Изменение режима сканирования страницы: ' + (currentData.title || currentData.page)); | }, 'Изменение режима сканирования страницы: ' + (currentData.title || currentData.page)); | ||
}); | }); | ||
actions.appendChild(statusButton); | actions.appendChild(statusButton); | ||
| Строка 594: | Строка 635: | ||
var wasHidden = row.classList.contains('scmc-catalog-hidden'); | var wasHidden = row.classList.contains('scmc-catalog-hidden'); | ||
var noteText; | var noteText; | ||
row.className = className; | row.className = className; | ||
row.classList.toggle('is-open', wasOpen); | row.classList.toggle('is-open', wasOpen); | ||
row.classList.toggle('scmc-catalog-hidden', wasHidden); | row.classList.toggle('scmc-catalog-hidden', wasHidden); | ||
row.setAttribute('data-section', clean(data.section)); | row.setAttribute('data-section', clean(data.section)); | ||
row.setAttribute('data-page', clean(data.page)); | row.setAttribute('data-page', clean(data.page)); | ||
| Строка 622: | Строка 660: | ||
var main = row.querySelector('.scmc-catalog-main'); | var main = row.querySelector('.scmc-catalog-main'); | ||
if (main) { | if (main) { | ||
main.innerHTML = ''; | main.innerHTML = ''; | ||
| Строка 636: | Строка 675: | ||
noteText = visibleNote(data); | noteText = visibleNote(data); | ||
if (noteText) { | if (noteText) { | ||
var note = document.createElement('div'); | var note = document.createElement('div'); | ||
| Строка 665: | Строка 705: | ||
statusButton.title = statusMap[status].label; | statusButton.title = statusMap[status].label; | ||
statusButton.setAttribute('data-status', status); | statusButton.setAttribute('data-status', status); | ||
} | |||
var expandButton = row.querySelector('.scmc-catalog-expand'); | |||
if (expandButton) { | |||
expandButton.setAttribute('aria-expanded', wasOpen ? 'true' : 'false'); | |||
expandButton.title = wasOpen ? 'Скрыть действия' : 'Показать действия'; | |||
} | } | ||
} | } | ||
| Строка 673: | Строка 719: | ||
rowElements.forEach(function (row, index) { | rowElements.forEach(function (row, index) { | ||
var indexButton = row.querySelector('.scmc-catalog-index'); | var indexButton = row.querySelector('.scmc-catalog-index'); | ||
if (indexButton) { | if (indexButton) { | ||
indexButton.textContent = String(index + 1); | indexButton.textContent = String(index + 1); | ||
| Строка 689: | Строка 736: | ||
updater(changed); | updater(changed); | ||
changed.className = sanitizeClassName(changed.className); | |||
var newLine = buildRowLine(changed); | var newLine = buildRowLine(changed); | ||
| Строка 745: | Строка 794: | ||
rowElements = Array.prototype.slice.call(list.querySelectorAll('.scmc-catalog-row')); | rowElements = Array.prototype.slice.call(list.querySelectorAll('.scmc-catalog-row')); | ||
rowsWithoutMoving = rowElements.filter(function (item) { | rowsWithoutMoving = rowElements.filter(function (item) { | ||
return item !== row; | return item !== row; | ||
| Строка 780: | Строка 830: | ||
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 = [ | ||
| Строка 787: | Строка 838: | ||
data.note, | data.note, | ||
noteText, | noteText, | ||
data.scan === 'stop' ? 'stop сканер не сканировать' : '', | data.scan === 'stop' ? 'scan off stop сканер не сканировать' : '', | ||
'уровень ' + level, | |||
'ур ' + level, | |||
status, | status, | ||
statusWords | statusWords | ||
Версия от 10:57, 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) {
var level = getLevelClass(className);
var hasRowClass = false;
String(className || '')
.split(/\s+/)
.forEach(function (item) {
if (item === 'scmc-catalog-row') {
hasRowClass = true;
}
});
return (hasRowClass ? 'scmc-catalog-row ' : 'scmc-catalog-row ') + level;
}
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 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');
var expandButton = event.target.closest('.scmc-catalog-expand');
if (expandButton && catalog.contains(expandButton)) {
event.preventDefault();
event.stopPropagation();
var expandRow = expandButton.closest('.scmc-catalog-row');
var isOpen = expandRow.classList.toggle('is-open');
expandButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
expandButton.title = isOpen ? 'Скрыть действия' : 'Показать действия';
return;
}
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), 'Изменить место');
var expandButton = makeButton('scmc-catalog-expand', '›', 'Показать действия');
expandButton.setAttribute('aria-expanded', 'false');
position.addEventListener('click', function () {
var currentIndex = rowElements.indexOf(row) + 1;
var total = rowElements.length;
var answer = prompt('На какое место переместить страницу? Сейчас: ' + currentIndex + '. Всего: ' + total + '.', String(currentIndex));
if (answer === null) return;
var target = parseInt(answer, 10);
if (!target || target < 1 || target > total) {
alert('Нужно число от 1 до ' + total + '.');
return;
}
moveRow(row, getRowData(row), target);
});
var main = document.createElement('div');
main.className = 'scmc-catalog-main';
var titleWrap = document.createElement('div');
titleWrap.className = 'scmc-catalog-title';
var link = document.createElement('a');
link.href = /^https?:\/\//i.test(data.page) ? data.page : mw.util.getUrl(data.page);
link.textContent = data.title || data.page;
titleWrap.appendChild(link);
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 statusButton = makeButton('scmc-catalog-status', statusMap[status].emoji, statusMap[status].label);
statusButton.setAttribute('data-status', status);
var sectionButton = makeButton('scmc-catalog-section-btn', data.section || 'Без раздела', 'Изменить раздел');
var levelButton = makeButton(
'scmc-catalog-level-btn',
'Ур. ' + getLevelNumber(data.className),
'Изменить уровень'
);
var noteButton = makeButton('scmc-catalog-btn scmc-catalog-note-btn', '✎', 'Изменить заметку');
noteButton.addEventListener('click', function () {
var currentData = getRowData(row);
var answer = prompt('Заметка для страницы "' + (currentData.title || currentData.page) + '". Пустое поле удалит data-note.', currentData.note || '');
if (answer === null) return;
saveRowChange(row, currentData, function (sourceData) {
sourceData.note = clean(answer);
}, 'Обновление заметки страницы: ' + (currentData.title || currentData.page));
});
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));
});
actions.appendChild(statusButton);
actions.appendChild(sectionButton);
actions.appendChild(levelButton);
actions.appendChild(noteButton);
actions.appendChild(scanToggle);
row.appendChild(position);
row.appendChild(expandButton);
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 wasOpen = row.classList.contains('is-open');
var wasHidden = row.classList.contains('scmc-catalog-hidden');
var noteText;
row.className = className;
row.classList.toggle('is-open', wasOpen);
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);
}
var expandButton = row.querySelector('.scmc-catalog-expand');
if (expandButton) {
expandButton.setAttribute('aria-expanded', wasOpen ? 'true' : 'false');
expandButton.title = wasOpen ? 'Скрыть действия' : 'Показать действия';
}
}
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);
alert(error.message || 'Не удалось сохранить изменение. Проверьте вход в аккаунт и права редактора.');
});
}
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);
alert(error.message || 'Не удалось изменить место строки.');
});
}
function setSaving(state) {
catalog.classList.toggle('scmc-status-saving', !!state);
Array.prototype.forEach.call(catalog.querySelectorAll('button'), function (button) {
button.disabled = !!state;
});
}
function update() {
var query = clean(search.value).toLowerCase();
var words = query ? query.split(/\s+/).filter(Boolean) : [];
var visible = 0;
rowElements.forEach(function (row) {
var data = getRowData(row);
var status = normalizeStatus(data.status);
var noteText = visibleNote(data);
var statusWords = statusMap[status] ? statusMap[status].words.join(' ') : '';
var level = getLevelNumber(data.className);
var text = [
data.title,
data.page,
data.section,
data.note,
noteText,
data.scan === 'stop' ? 'scan off stop сканер не сканировать' : '',
'уровень ' + 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;
}
});
})();