|
|
| Строка 1: |
Строка 1: |
| (function () {
| |
| if (typeof mw === 'undefined' || !window.document) return;
| |
|
| |
|
| var statusMap = {
| |
| green: { emoji: '🟢', label: 'Готово', words: ['готово', 'готов'] },
| |
| yellow: { emoji: '🟡', label: 'Нужно обновить', words: ['нужно обновить', 'обновить', 'устарело'] },
| |
| red: { emoji: '🔴', label: 'Нет страницы', words: ['нет страницы', 'нет', 'пусто'] },
| |
| blue: { emoji: '🔵', label: 'Заморожено', words: ['заморожено', 'заморожен'] }
| |
| };
| |
|
| |
| function ready(fn) {
| |
| if (document.readyState === 'complete' || document.readyState === 'interactive') {
| |
| fn();
| |
| } else {
| |
| document.addEventListener('DOMContentLoaded', fn);
| |
| }
| |
| }
| |
|
| |
| function clean(text) {
| |
| return String(text || '').replace(/\s+/g, ' ').trim();
| |
| }
| |
|
| |
| function key(text) {
| |
| return clean(text).replace(/_/g, ' ').toLowerCase();
| |
| }
| |
|
| |
| function encodeAttr(value) {
| |
| return String(value || '')
| |
| .replace(/&/g, '&')
| |
| .replace(/"/g, '"')
| |
| .replace(/</g, '<')
| |
| .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 buildFallbackLink(page, title) {
| |
| if (/^https?:\/\//i.test(page)) {
| |
| return '[' + page + ' ' + title + ']';
| |
| }
| |
|
| |
| return '[[' + page + '|' + title + ']]';
| |
| }
| |
|
| |
| function makeButton(className, text, title) {
| |
| var button = document.createElement('button');
| |
| button.type = 'button';
| |
| button.className = className;
| |
| button.textContent = text;
| |
| if (title) button.title = title;
| |
| return button;
| |
| }
| |
|
| |
| function closeMenus() {
| |
| var statusMenu = document.querySelector('.scmc-status-menu');
| |
| var sectionMenu = document.querySelector('.scmc-section-menu');
| |
|
| |
| if (statusMenu) statusMenu.remove();
| |
| if (sectionMenu) sectionMenu.remove();
| |
| }
| |
|
| |
| function placeMenu(menu, rect) {
| |
| document.body.appendChild(menu);
| |
|
| |
| var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 12);
| |
| var top = Math.min(rect.bottom + 8, window.innerHeight - menu.offsetHeight - 12);
| |
|
| |
| menu.style.left = Math.max(12, left) + 'px';
| |
| menu.style.top = Math.max(12, top) + 'px';
| |
| }
| |
|
| |
| function getSource() {
| |
| var api = new mw.Api();
| |
| var currentPage = mw.config.get('wgPageName');
| |
|
| |
| return api.get({
| |
| action: 'query',
| |
| prop: 'revisions',
| |
| titles: currentPage,
| |
| rvprop: 'content',
| |
| rvslots: 'main',
| |
| formatversion: 2
| |
| }).then(function (data) {
| |
| var pageData = data.query && data.query.pages ? data.query.pages[0] : null;
| |
| var rev = pageData && pageData.revisions ? pageData.revisions[0] : null;
| |
|
| |
| if (rev && rev.slots && rev.slots.main && typeof rev.slots.main.content === 'string') {
| |
| return rev.slots.main.content;
| |
| }
| |
|
| |
| if (rev && typeof rev.content === 'string') {
| |
| return rev.content;
| |
| }
| |
|
| |
| throw new Error('Не удалось прочитать код страницы.');
| |
| });
| |
| }
| |
|
| |
| function saveSource(text, summary) {
| |
| var api = new mw.Api();
| |
|
| |
| return api.postWithToken('csrf', {
| |
| action: 'edit',
| |
| title: mw.config.get('wgPageName'),
| |
| text: text,
| |
| summary: summary || 'Обновление каталога Marine Corps',
| |
| minor: true,
| |
| formatversion: 2
| |
| });
| |
| }
| |
|
| |
| function getRowsFromSource(source) {
| |
| var result = [];
| |
| var re = /<div\b[^>]*\bscmc-catalog-row\b[^>]*>[\s\S]*?<\/div>/gi;
| |
| var match;
| |
|
| |
| while ((match = re.exec(source)) !== null) {
| |
| var full = match[0];
| |
| var open = full.match(/^<div\b[^>]*>/i);
| |
| if (!open) continue;
| |
|
| |
| var tag = open[0];
| |
|
| |
| var data = {
| |
| className: attrFromTag(tag, 'class') || 'scmc-catalog-row scmc-level-1',
| |
| section: clean(attrFromTag(tag, 'data-section')),
| |
| page: clean(attrFromTag(tag, 'data-page')),
| |
| title: clean(attrFromTag(tag, 'data-title')),
| |
| scan: clean(attrFromTag(tag, 'data-scan')),
| |
| status: normalizeStatus(clean(attrFromTag(tag, 'data-status'))),
| |
| note: clean(attrFromTag(tag, 'data-note'))
| |
| };
| |
|
| |
| if (!data.page) continue;
| |
| if (!data.title) data.title = data.page;
| |
|
| |
| result.push({
| |
| start: match.index,
| |
| end: match.index + full.length,
| |
| full: full,
| |
| data: data
| |
| });
| |
| }
| |
|
| |
| return result;
| |
| }
| |
|
| |
| function findSourceRow(sourceRows, targetData) {
| |
| var matches = sourceRows.filter(function (item) {
| |
| return key(item.data.page) === key(targetData.page) &&
| |
| key(item.data.section) === key(targetData.section) &&
| |
| key(item.data.title) === key(targetData.title);
| |
| });
| |
|
| |
| if (!matches.length) {
| |
| throw new Error('Не нашёл строку: ' + (targetData.title || targetData.page));
| |
| }
| |
|
| |
| if (matches.length > 1) {
| |
| throw new Error('Нашлось несколько одинаковых строк: ' + (targetData.title || targetData.page) + '. Лучше поправить вручную.');
| |
| }
| |
|
| |
| return matches[0];
| |
| }
| |
|
| |
| function buildRowLine(data) {
| |
| var className = clean(data.className || 'scmc-catalog-row scmc-level-1');
| |
| var level = getLevelClass(className);
| |
|
| |
| if (className.indexOf('scmc-catalog-row') === -1) {
| |
| className = 'scmc-catalog-row ' + level;
| |
| }
| |
|
| |
| var section = clean(data.section);
| |
| var page = clean(data.page);
| |
| var title = clean(data.title || data.page);
| |
| var scan = clean(data.scan);
| |
| var status = normalizeStatus(data.status);
| |
| var note = clean(data.note);
| |
|
| |
| var out = '<div class="' + encodeAttr(className) + '"';
| |
| out += ' data-section="' + encodeAttr(section) + '"';
| |
| out += ' data-page="' + encodeAttr(page) + '"';
| |
| out += ' data-title="' + encodeAttr(title) + '"';
| |
|
| |
| if (scan) {
| |
| out += ' data-scan="' + encodeAttr(scan) + '"';
| |
| }
| |
|
| |
| out += ' data-status="' + encodeAttr(status) + '"';
| |
|
| |
| if (note) {
| |
| out += ' data-note="' + encodeAttr(note) + '"';
| |
| }
| |
|
| |
| out += '>' + buildFallbackLink(page, title) + '</div>';
| |
|
| |
| return out;
| |
| }
| |
|
| |
| ready(function () {
| |
| var catalog = document.querySelector('.scmc-catalog');
| |
| if (!catalog) return;
| |
|
| |
| var tools = catalog.querySelector('.scmc-catalog-tools');
| |
| var rowElements = Array.prototype.slice.call(catalog.querySelectorAll('.scmc-catalog-row'));
| |
| if (!tools || !rowElements.length) return;
| |
|
| |
| var activeFilter = 'all';
| |
| var search;
| |
| var filters;
| |
| var counter;
| |
|
| |
| buildTools(tools);
| |
| enhanceRows(rowElements);
| |
|
| |
| search = tools.querySelector('.scmc-catalog-search');
| |
| filters = Array.prototype.slice.call(tools.querySelectorAll('.scmc-catalog-filter'));
| |
| counter = tools.querySelector('.scmc-catalog-counter');
| |
|
| |
| update();
| |
|
| |
| search.addEventListener('input', update);
| |
|
| |
| filters.forEach(function (button) {
| |
| button.addEventListener('click', function () {
| |
| activeFilter = button.getAttribute('data-filter') || 'all';
| |
|
| |
| filters.forEach(function (other) {
| |
| other.classList.toggle('is-active', other === button);
| |
| });
| |
|
| |
| update();
| |
| });
| |
| });
| |
|
| |
| document.addEventListener('click', function (event) {
| |
| var statusButton = event.target.closest('.scmc-catalog-status');
| |
| var sectionButton = event.target.closest('.scmc-catalog-section-btn');
| |
|
| |
| if (statusButton && catalog.contains(statusButton)) {
| |
| event.preventDefault();
| |
| event.stopPropagation();
| |
| openStatusMenu(statusButton);
| |
| return;
| |
| }
| |
|
| |
| if (sectionButton && catalog.contains(sectionButton)) {
| |
| event.preventDefault();
| |
| event.stopPropagation();
| |
| openSectionMenu(sectionButton);
| |
| return;
| |
| }
| |
|
| |
| if (!event.target.closest('.scmc-status-menu') && !event.target.closest('.scmc-section-menu')) {
| |
| closeMenus();
| |
| }
| |
| });
| |
|
| |
| function buildTools(target) {
| |
| target.innerHTML = '';
| |
|
| |
| var input = document.createElement('input');
| |
| input.className = 'scmc-catalog-search';
| |
| input.type = 'search';
| |
| input.placeholder = 'Поиск по названию, странице, разделу, статусу или заметке...';
| |
|
| |
| var filtersWrap = document.createElement('div');
| |
| filtersWrap.className = 'scmc-catalog-filters';
| |
|
| |
| [
| |
| ['all', 'Все'],
| |
| ['green', '🟢 Готово'],
| |
| ['yellow', '🟡 Нужно обновить'],
| |
| ['red', '🔴 Нет страницы'],
| |
| ['blue', '🔵 Заморожено'],
| |
| ['problem', 'Проблемные'],
| |
| ['stop', 'Из основного проекта'],
| |
| ['note', 'С заметками']
| |
| ].forEach(function (item, index) {
| |
| var button = document.createElement('button');
| |
| button.type = 'button';
| |
| button.className = 'scmc-catalog-filter' + (index === 0 ? ' is-active' : '');
| |
| button.setAttribute('data-filter', item[0]);
| |
| button.textContent = item[1];
| |
| filtersWrap.appendChild(button);
| |
| });
| |
|
| |
| var count = document.createElement('div');
| |
| count.className = 'scmc-catalog-counter';
| |
|
| |
| target.appendChild(input);
| |
| target.appendChild(filtersWrap);
| |
| target.appendChild(count);
| |
| }
| |
|
| |
| function getRowData(row) {
| |
| return {
| |
| className: row.getAttribute('class') || 'scmc-catalog-row scmc-level-1',
| |
| section: clean(row.getAttribute('data-section')),
| |
| page: clean(row.getAttribute('data-page')),
| |
| title: clean(row.getAttribute('data-title') || row.textContent),
| |
| scan: clean(row.getAttribute('data-scan')),
| |
| status: normalizeStatus(clean(row.getAttribute('data-status'))),
| |
| note: clean(row.getAttribute('data-note'))
| |
| };
| |
| }
| |
|
| |
| function visibleNote(data) {
| |
| var parts = [];
| |
|
| |
| if (data.scan === 'stop') {
| |
| parts.push('Из основного проекта.');
| |
| }
| |
|
| |
| if (data.note) {
| |
| parts.push(data.note);
| |
| }
| |
|
| |
| return parts.join(' ');
| |
| }
| |
|
| |
| function getSections() {
| |
| var map = {};
| |
|
| |
| rowElements.forEach(function (row) {
| |
| var section = clean(row.getAttribute('data-section'));
| |
| if (section) map[section] = true;
| |
| });
| |
|
| |
| return Object.keys(map).sort(function (a, b) {
| |
| return a.localeCompare(b, 'ru');
| |
| });
| |
| }
| |
|
| |
| function enhanceRows(rows) {
| |
| rows.forEach(function (row, index) {
| |
| if (row.getAttribute('data-scmc-enhanced') === 'true') return;
| |
|
| |
| var data = getRowData(row);
| |
| var status = normalizeStatus(data.status);
| |
| var isStop = data.scan === 'stop';
| |
|
| |
| row.setAttribute('data-status', status);
| |
| if (isStop) row.setAttribute('data-scan', 'stop');
| |
|
| |
| row.setAttribute('data-scmc-enhanced', 'true');
| |
| row.innerHTML = '';
| |
|
| |
| var position = makeButton('scmc-catalog-index', String(index + 1), 'Изменить место');
| |
|
| |
| position.addEventListener('click', function () {
| |
| var answer = prompt('На какое место переместить страницу? Сейчас: ' + (index + 1) + '. Всего: ' + rows.length + '.', String(index + 1));
| |
| if (answer === null) return;
| |
|
| |
| var target = parseInt(answer, 10);
| |
| if (!target || target < 1 || target > rows.length) {
| |
| alert('Нужно число от 1 до ' + rows.length + '.');
| |
| return;
| |
| }
| |
|
| |
| moveRow(data, target);
| |
| });
| |
|
| |
| var main = document.createElement('div');
| |
| main.className = 'scmc-catalog-main';
| |
|
| |
| var titleWrap = document.createElement('div');
| |
| titleWrap.className = 'scmc-catalog-title';
| |
|
| |
| var link = document.createElement('a');
| |
| link.href = /^https?:\/\//i.test(data.page) ? data.page : mw.util.getUrl(data.page);
| |
| link.textContent = data.title || data.page;
| |
| titleWrap.appendChild(link);
| |
|
| |
| if (data.page && data.page !== data.title) {
| |
| var pageHint = document.createElement('span');
| |
| pageHint.className = 'scmc-catalog-page';
| |
| pageHint.textContent = data.page;
| |
| titleWrap.appendChild(pageHint);
| |
| }
| |
|
| |
| main.appendChild(titleWrap);
| |
|
| |
| var noteText = visibleNote(data);
| |
| if (noteText) {
| |
| var note = document.createElement('div');
| |
| note.className = 'scmc-catalog-note';
| |
| note.textContent = noteText;
| |
| main.appendChild(note);
| |
| }
| |
|
| |
| var actions = document.createElement('div');
| |
| actions.className = 'scmc-catalog-actions';
| |
|
| |
| var noteButton = makeButton('scmc-catalog-btn scmc-catalog-note-btn', '✎', 'Изменить заметку');
| |
|
| |
| noteButton.addEventListener('click', function () {
| |
| var answer = prompt('Заметка для страницы "' + (data.title || data.page) + '". Пустое поле удалит data-note.', data.note || '');
| |
| if (answer === null) return;
| |
|
| |
| saveRowChange(data, function (sourceData) {
| |
| sourceData.note = clean(answer);
| |
| }, 'Обновление заметки страницы: ' + (data.title || data.page));
| |
| });
| |
|
| |
| var scanToggle = makeButton(
| |
| 'scmc-catalog-btn scmc-scan-toggle',
| |
| '',
| |
| isStop ? 'Из основного проекта. Нажми, чтобы убрать stop.' : 'Нажми, чтобы добавить stop.'
| |
| );
| |
|
| |
| scanToggle.setAttribute('data-active', isStop ? 'true' : 'false');
| |
|
| |
| scanToggle.addEventListener('click', function () {
| |
| saveRowChange(data, function (sourceData) {
| |
| sourceData.scan = sourceData.scan === 'stop' ? '' : 'stop';
| |
| }, 'Изменение режима сканирования страницы: ' + (data.title || data.page));
| |
| });
| |
|
| |
| var sectionButton = makeButton('scmc-catalog-section-btn', data.section || 'Без раздела', 'Изменить раздел');
| |
|
| |
| var statusButton = makeButton('scmc-catalog-status', statusMap[status].emoji, statusMap[status].label);
| |
| statusButton.setAttribute('data-status', status);
| |
|
| |
| actions.appendChild(noteButton);
| |
| actions.appendChild(scanToggle);
| |
| actions.appendChild(sectionButton);
| |
| actions.appendChild(statusButton);
| |
|
| |
| row.appendChild(position);
| |
| row.appendChild(main);
| |
| row.appendChild(actions);
| |
| });
| |
| }
| |
|
| |
| function openStatusMenu(button) {
| |
| closeMenus();
| |
|
| |
| var row = button.closest('.scmc-catalog-row');
| |
| if (!row) return;
| |
|
| |
| var data = getRowData(row);
| |
| var rect = button.getBoundingClientRect();
| |
| var menu = document.createElement('div');
| |
| menu.className = 'scmc-status-menu';
| |
|
| |
| Object.keys(statusMap).forEach(function (status) {
| |
| var option = document.createElement('button');
| |
| option.type = 'button';
| |
| option.textContent = statusMap[status].emoji + ' ' + statusMap[status].label;
| |
|
| |
| option.addEventListener('click', function (event) {
| |
| event.preventDefault();
| |
| event.stopPropagation();
| |
| menu.remove();
| |
|
| |
| if (status === data.status) return;
| |
|
| |
| saveRowChange(data, function (sourceData) {
| |
| sourceData.status = status;
| |
| }, 'Обновление статуса страницы: ' + (data.title || data.page));
| |
| });
| |
|
| |
| menu.appendChild(option);
| |
| });
| |
|
| |
| placeMenu(menu, rect);
| |
| }
| |
|
| |
| function openSectionMenu(button) {
| |
| closeMenus();
| |
|
| |
| var row = button.closest('.scmc-catalog-row');
| |
| if (!row) return;
| |
|
| |
| var data = getRowData(row);
| |
| var rect = button.getBoundingClientRect();
| |
| var menu = document.createElement('div');
| |
| menu.className = 'scmc-section-menu';
| |
|
| |
| getSections().forEach(function (section) {
| |
| var option = document.createElement('button');
| |
| option.type = 'button';
| |
| option.textContent = section;
| |
|
| |
| option.addEventListener('click', function (event) {
| |
| event.preventDefault();
| |
| event.stopPropagation();
| |
| menu.remove();
| |
|
| |
| if (section === data.section) return;
| |
|
| |
| saveRowChange(data, function (sourceData) {
| |
| sourceData.section = section;
| |
| }, 'Изменение раздела страницы: ' + (data.title || data.page));
| |
| });
| |
|
| |
| menu.appendChild(option);
| |
| });
| |
|
| |
| placeMenu(menu, rect);
| |
| }
| |
|
| |
| function saveRowChange(targetData, updater, summary) {
| |
| setSaving(true);
| |
|
| |
| getSource()
| |
| .then(function (source) {
| |
| var sourceRows = getRowsFromSource(source);
| |
| var target = findSourceRow(sourceRows, targetData);
| |
| var changed = Object.assign({}, target.data);
| |
|
| |
| updater(changed);
| |
|
| |
| var newLine = buildRowLine(changed);
| |
| var newText = source.slice(0, target.start) + newLine + source.slice(target.end);
| |
|
| |
| return saveSource(newText, summary);
| |
| })
| |
| .then(function () {
| |
| location.reload();
| |
| })
| |
| .catch(function (error) {
| |
| setSaving(false);
| |
| alert(error.message || 'Не удалось сохранить изменение. Проверьте вход в аккаунт и права редактора.');
| |
| });
| |
| }
| |
|
| |
| function moveRow(targetData, targetPosition) {
| |
| setSaving(true);
| |
|
| |
| getSource()
| |
| .then(function (source) {
| |
| var sourceRows = getRowsFromSource(source);
| |
| var target = findSourceRow(sourceRows, targetData);
| |
| var oldIndex = sourceRows.indexOf(target);
| |
| var newIndex = Math.max(0, Math.min(sourceRows.length - 1, targetPosition - 1));
| |
|
| |
| if (oldIndex === newIndex) {
| |
| setSaving(false);
| |
| return null;
| |
| }
| |
|
| |
| var lines = sourceRows.map(function (item) {
| |
| return item.full;
| |
| });
| |
|
| |
| var moved = lines.splice(oldIndex, 1)[0];
| |
| lines.splice(newIndex, 0, moved);
| |
|
| |
| var first = sourceRows[0];
| |
| var last = sourceRows[sourceRows.length - 1];
| |
| var newText = source.slice(0, first.start) + lines.join('\n') + source.slice(last.end);
| |
|
| |
| return saveSource(newText, 'Изменение порядка страниц в каталоге Marine Corps');
| |
| })
| |
| .then(function (result) {
| |
| if (result !== null) location.reload();
| |
| })
| |
| .catch(function (error) {
| |
| setSaving(false);
| |
| alert(error.message || 'Не удалось изменить место строки.');
| |
| });
| |
| }
| |
|
| |
| function setSaving(state) {
| |
| catalog.classList.toggle('scmc-status-saving', !!state);
| |
|
| |
| Array.prototype.forEach.call(catalog.querySelectorAll('button'), function (button) {
| |
| button.disabled = !!state;
| |
| });
| |
| }
| |
|
| |
| function update() {
| |
| var query = clean(search.value).toLowerCase();
| |
| var words = query ? query.split(/\s+/).filter(Boolean) : [];
| |
| var visible = 0;
| |
|
| |
| rowElements.forEach(function (row) {
| |
| var data = getRowData(row);
| |
| var status = normalizeStatus(data.status);
| |
| var noteText = visibleNote(data);
| |
| var statusWords = statusMap[status] ? statusMap[status].words.join(' ') : '';
| |
|
| |
| var text = [
| |
| data.title,
| |
| data.page,
| |
| data.section,
| |
| data.note,
| |
| noteText,
| |
| data.scan === 'stop' ? 'из основного проекта stop основной' : '',
| |
| status,
| |
| statusWords
| |
| ].join(' ').toLowerCase();
| |
|
| |
| var filterOk =
| |
| activeFilter === 'all' ||
| |
| activeFilter === status ||
| |
| (activeFilter === 'problem' && status !== 'green') ||
| |
| (activeFilter === 'stop' && data.scan === 'stop') ||
| |
| (activeFilter === 'note' && !!data.note);
| |
|
| |
| var textOk = words.every(function (word) {
| |
| return text.indexOf(word) !== -1;
| |
| });
| |
|
| |
| var show = filterOk && textOk;
| |
| row.classList.toggle('scmc-catalog-hidden', !show);
| |
|
| |
| if (show) visible++;
| |
| });
| |
|
| |
| counter.textContent = 'Показано: ' + visible + ' из ' + rowElements.length;
| |
| }
| |
| });
| |
| })();
| |