|
|
| Line 1: |
Line 1: |
| (function () { | | (function () { |
| 'use strict';
| | if (mw.config.get('wgPageName') === 'Project:Changelog_Editor') { |
| | | mw.loader.load('/w/index.php?title=MediaWiki:ChangelogEditor.css&action=raw&ctype=text/css', 'text/css'); |
| var DATA_PAGE = 'Module:Changelog/data.json';
| | mw.loader.load('/w/index.php?title=MediaWiki:ChangelogEditor.js&action=raw&ctype=text/javascript'); |
| var PER_PAGE = 15;
| |
| var VALID_TAGS = ['New', 'Balance', 'Fix', 'QoL', 'Performance'];
| |
| | |
| var entries = [];
| |
| var originalJSON = '';
| |
| var currentPage = 0;
| |
| var editToken = null;
| |
| var editingIndex = null;
| |
| var allPageTitles = [];
| |
| var dirty = false;
| |
| | |
| var root = document.getElementById('rb-changelog-editor');
| |
| if (!root) return; | |
| | |
| function el(tag, attrs, children) {
| |
| var node = document.createElement(tag);
| |
| if (attrs) {
| |
| Object.keys(attrs).forEach(function (k) {
| |
| if (k === 'className') node.className = attrs[k];
| |
| else if (k === 'textContent') node.textContent = attrs[k];
| |
| else if (k === 'innerHTML') node.innerHTML = attrs[k];
| |
| else if (k.indexOf('on') === 0) node.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
| |
| else node.setAttribute(k, attrs[k]);
| |
| });
| |
| }
| |
| if (children) {
| |
| (Array.isArray(children) ? children : [children]).forEach(function (c) {
| |
| if (c == null) return;
| |
| node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
| |
| });
| |
| }
| |
| return node;
| |
| }
| |
| | |
| function todayISO() {
| |
| var d = new Date(); | |
| return d.getFullYear() + '-' +
| |
| String(d.getMonth() + 1).padStart(2, '0') + '-' +
| |
| String(d.getDate()).padStart(2, '0');
| |
| }
| |
| | |
| function generateId(date) {
| |
| var parts = date.split('-');
| |
| var dd = parts[2], mm = parts[1], yy = parts[0].slice(2);
| |
| var prefix = dd + mm + yy;
| |
| var maxNum = 0;
| |
| entries.forEach(function (e) {
| |
| if (e.id && e.id.indexOf(prefix) === 0) {
| |
| var num = parseInt(e.id.slice(prefix.length), 10);
| |
| if (num > maxNum) maxNum = num;
| |
| }
| |
| });
| |
| return prefix + (maxNum + 1);
| |
| }
| |
| | |
| function markDirty() {
| |
| dirty = true;
| |
| render();
| |
| }
| |
| | |
| function getUnsavedCount() {
| |
| if (!dirty) return 0;
| |
| var currentJSON = JSON.stringify(entries, null, 2);
| |
| return currentJSON !== originalJSON ? 1 : 0;
| |
| }
| |
| | |
| function computeChangeSummary() {
| |
| var orig;
| |
| try { orig = JSON.parse(originalJSON); } catch (e) { orig = []; }
| |
| var origMap = {};
| |
| orig.forEach(function (e) { origMap[e.id] = JSON.stringify(e); });
| |
| var curMap = {};
| |
| entries.forEach(function (e) { curMap[e.id] = JSON.stringify(e); });
| |
| | |
| var added = [], modified = [], deleted = [];
| |
| entries.forEach(function (e) {
| |
| if (!origMap[e.id]) added.push(e);
| |
| else if (origMap[e.id] !== curMap[e.id]) modified.push(e);
| |
| });
| |
| orig.forEach(function (e) {
| |
| if (!curMap[e.id]) deleted.push(e);
| |
| });
| |
| | |
| var origIds = orig.map(function (e) { return e.id; }).join(',');
| |
| var curIds = entries.map(function (e) { return e.id; }).join(',');
| |
| var reordered = origIds !== curIds && added.length === 0 && deleted.length === 0;
| |
| | |
| return { added: added, modified: modified, deleted: deleted, reordered: reordered };
| |
| }
| |
| | |
| function apiGet(params) {
| |
| var url = mw.util.wikiScript('api') + '?' + Object.keys(params).map(function (k) {
| |
| return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
| |
| }).join('&');
| |
| return fetch(url).then(function (r) { return r.json(); });
| |
| }
| |
| | |
| function fetchData() {
| |
| var url = mw.util.wikiScript('index') + '?title=' + encodeURIComponent(DATA_PAGE) + '&action=raw';
| |
| return fetch(url).then(function (r) {
| |
| if (!r.ok) throw new Error('Failed to fetch data: ' + r.status);
| |
| return r.text();
| |
| }).then(function (text) {
| |
| entries = JSON.parse(text);
| |
| originalJSON = JSON.stringify(entries, null, 2);
| |
| dirty = false;
| |
| });
| |
| }
| |
| | |
| function fetchEditToken() {
| |
| return apiGet({ action: 'query', meta: 'tokens', type: 'csrf', format: 'json' })
| |
| .then(function (data) {
| |
| editToken = data.query.tokens.csrftoken;
| |
| });
| |
| }
| |
| | |
| function saveData(summary) {
| |
| var content = JSON.stringify(entries, null, 2);
| |
| var params = new URLSearchParams();
| |
| params.append('action', 'edit');
| |
| params.append('title', DATA_PAGE);
| |
| params.append('text', content);
| |
| params.append('summary', summary);
| |
| params.append('contentformat', 'application/json');
| |
| params.append('contentmodel', 'json');
| |
| params.append('token', editToken); | |
| params.append('format', 'json');
| |
| | |
| return fetch(mw.util.wikiScript('api'), {
| |
| method: 'POST',
| |
| body: params,
| |
| credentials: 'same-origin'
| |
| }).then(function (r) { return r.json(); }).then(function (data) {
| |
| if (data.error) {
| |
| if (data.error.code === 'badtoken') {
| |
| return fetchEditToken().then(function () {
| |
| params.set('token', editToken);
| |
| return fetch(mw.util.wikiScript('api'), {
| |
| method: 'POST',
| |
| body: params,
| |
| credentials: 'same-origin'
| |
| }).then(function (r) { return r.json(); });
| |
| }).then(function (data2) {
| |
| if (data2.error) throw new Error(data2.error.info);
| |
| return data2;
| |
| });
| |
| }
| |
| throw new Error(data.error.info);
| |
| }
| |
| originalJSON = JSON.stringify(entries, null, 2);
| |
| dirty = false;
| |
| return data;
| |
| });
| |
| }
| |
| | |
| function searchPageTitles(query) {
| |
| if (!query || query.length < 2) return [];
| |
| var q = query.toLowerCase();
| |
| return allPageTitles.filter(function (t) {
| |
| return t.toLowerCase().indexOf(q) !== -1;
| |
| }).slice(0, 10);
| |
| }
| |
| | |
| function fetchAllPageTitles() {
| |
| var titles = [];
| |
| function fetchBatch(apcontinue) {
| |
| var params = {
| |
| action: 'query',
| |
| list: 'allpages',
| |
| aplimit: '500',
| |
| apnamespace: '0',
| |
| format: 'json'
| |
| };
| |
| if (apcontinue) params.apcontinue = apcontinue;
| |
| return apiGet(params).then(function (data) {
| |
| data.query.allpages.forEach(function (p) { titles.push(p.title); });
| |
| if (data['continue'] && data['continue'].apcontinue) {
| |
| return fetchBatch(data['continue'].apcontinue);
| |
| }
| |
| allPageTitles = titles;
| |
| });
| |
| }
| |
| return fetchBatch(null);
| |
| }
| |
| | |
| window.addEventListener('beforeunload', function (e) {
| |
| if (dirty) {
| |
| e.preventDefault();
| |
| e.returnValue = '';
| |
| }
| |
| });
| |
| | |
| function render() {
| |
| root.innerHTML = '';
| |
| root.appendChild(renderApp());
| |
| }
| |
| | |
| function renderApp() {
| |
| var wrap = el('div', { className: 'rb-ce-wrap' });
| |
| wrap.appendChild(renderHeader());
| |
| if (editingIndex !== null) {
| |
| wrap.appendChild(renderForm());
| |
| }
| |
| wrap.appendChild(renderList());
| |
| wrap.appendChild(renderPagination());
| |
| return wrap;
| |
| }
| |
| | |
| function renderHeader() {
| |
| var title = el('h2', null, 'Changelog Editor');
| |
| var unsaved = dirty ? el('span', { className: 'rb-ce-badge', textContent: 'unsaved' }) : null;
| |
| | |
| var addBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-add',
| |
| textContent: '+ Add Entry',
| |
| onClick: function () {
| |
| editingIndex = -1;
| |
| currentPage = 0;
| |
| render();
| |
| }
| |
| });
| |
| | |
| var saveBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-primary',
| |
| textContent: 'Save All',
| |
| disabled: !dirty ? 'disabled' : null,
| |
| onClick: function () { showConfirmDialog(); }
| |
| });
| |
| | |
| var actions = el('div', { className: 'rb-ce-header-actions' }, [addBtn, saveBtn, unsaved]);
| |
| return el('div', { className: 'rb-ce-header' }, [title, actions]);
| |
| }
| |
| | |
| function renderList() {
| |
| var list = el('div', { className: 'rb-ce-list' });
| |
| if (entries.length === 0) {
| |
| list.appendChild(el('div', { className: 'rb-ce-empty', textContent: 'No changelog entries yet.' }));
| |
| return list;
| |
| }
| |
| | |
| var start = currentPage * PER_PAGE;
| |
| var end = Math.min(start + PER_PAGE, entries.length);
| |
| for (var i = start; i < end; i++) {
| |
| list.appendChild(renderEntry(i));
| |
| }
| |
| return list;
| |
| }
| |
| | |
| function renderEntry(idx) {
| |
| var entry = entries[idx];
| |
| var tag = el('span', { className: 'rb-ce-tag rb-ce-tag-' + entry.tag, textContent: entry.tag });
| |
| var date = el('span', { className: 'rb-ce-entry-date', textContent: entry.date });
| |
| var meta = el('div', { className: 'rb-ce-entry-meta' }, [tag, date]);
| |
| | |
| var text = el('p', { className: 'rb-ce-entry-text', textContent: entry.text });
| |
| var idSpan = el('span', { className: 'rb-ce-entry-id', textContent: 'ID: ' + entry.id });
| |
| var body = el('div', { className: 'rb-ce-entry-body' }, [text, idSpan]);
| |
| | |
| if ((entry.pages && entry.pages.length) || (entry.exclude && entry.exclude.length)) {
| |
| var chips = el('div', { className: 'rb-ce-chips' });
| |
| if (entry.pages) {
| |
| entry.pages.forEach(function (p) {
| |
| chips.appendChild(el('span', { className: 'rb-ce-chip' }, [
| |
| el('span', { className: 'rb-ce-chip-label', textContent: '+' }),
| |
| p
| |
| ]));
| |
| });
| |
| }
| |
| if (entry.exclude) {
| |
| entry.exclude.forEach(function (p) {
| |
| chips.appendChild(el('span', { className: 'rb-ce-chip rb-ce-chip-exclude' }, [
| |
| el('span', { className: 'rb-ce-chip-label', textContent: '\u2212' }),
| |
| p
| |
| ]));
| |
| });
| |
| }
| |
| body.appendChild(chips);
| |
| }
| |
| | |
| var editBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost rb-ce-btn-sm',
| |
| textContent: 'Edit',
| |
| onClick: function () { editingIndex = idx; render(); }
| |
| });
| |
| var delBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-danger rb-ce-btn-sm',
| |
| textContent: 'Del',
| |
| onClick: function () {
| |
| if (confirm('Delete this entry?')) {
| |
| entries.splice(idx, 1);
| |
| if (editingIndex === idx) editingIndex = null;
| |
| markDirty();
| |
| }
| |
| }
| |
| });
| |
| var upBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost rb-ce-btn-sm',
| |
| textContent: '\u25B2',
| |
| disabled: idx === 0 ? 'disabled' : null,
| |
| onClick: function () {
| |
| var tmp = entries[idx - 1];
| |
| entries[idx - 1] = entries[idx];
| |
| entries[idx] = tmp;
| |
| markDirty();
| |
| }
| |
| });
| |
| var downBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost rb-ce-btn-sm',
| |
| textContent: '\u25BC',
| |
| disabled: idx === entries.length - 1 ? 'disabled' : null,
| |
| onClick: function () {
| |
| var tmp = entries[idx + 1];
| |
| entries[idx + 1] = entries[idx];
| |
| entries[idx] = tmp;
| |
| markDirty();
| |
| }
| |
| });
| |
| var actions = el('div', { className: 'rb-ce-entry-actions' }, [upBtn, downBtn, editBtn, delBtn]);
| |
| | |
| return el('div', { className: 'rb-ce-entry' }, [meta, body, actions]);
| |
| }
| |
| | |
| function renderForm() {
| |
| var isNew = editingIndex === -1;
| |
| var entry = isNew ? { id: '', date: todayISO(), tag: 'New', text: '', pages: [], exclude: [] } : JSON.parse(JSON.stringify(entries[editingIndex]));
| |
| if (!entry.pages) entry.pages = [];
| |
| if (!entry.exclude) entry.exclude = [];
| |
| var autoId = isNew ? generateId(entry.date) : entry.id;
| |
| | |
| var form = el('div', { className: 'rb-ce-form' });
| |
| | |
| var dateInput = el('input', { type: 'date', value: entry.date });
| |
| var tagSelect = el('select');
| |
| VALID_TAGS.forEach(function (t) {
| |
| var opt = el('option', { value: t, textContent: t });
| |
| if (t === entry.tag) opt.selected = true;
| |
| tagSelect.appendChild(opt);
| |
| });
| |
| var idInput = el('input', { type: 'text', value: isNew ? autoId : entry.id, placeholder: 'Auto-generated' });
| |
| | |
| dateInput.addEventListener('change', function () {
| |
| if (isNew) {
| |
| entry.date = dateInput.value;
| |
| idInput.value = generateId(dateInput.value);
| |
| }
| |
| });
| |
| | |
| var row1 = el('div', { className: 'rb-ce-form-row' }, [
| |
| el('div', { className: 'rb-ce-form-field' }, [el('label', null, 'Date'), dateInput]),
| |
| el('div', { className: 'rb-ce-form-field' }, [el('label', null, 'Tag'), tagSelect]),
| |
| el('div', { className: 'rb-ce-form-field' }, [el('label', null, 'ID'), idInput])
| |
| ]);
| |
| | |
| var textArea = el('textarea', { placeholder: 'Changelog text (wikitext supported)' });
| |
| textArea.value = entry.text;
| |
| var row2 = el('div', { className: 'rb-ce-form-row' }, [
| |
| el('div', { className: 'rb-ce-form-field', style: 'flex:1' }, [el('label', null, 'Text'), textArea])
| |
| ]);
| |
| | |
| var pagesChips = createChipInput(entry.pages);
| |
| var excludeChips = createChipInput(entry.exclude);
| |
| var row3 = el('div', { className: 'rb-ce-form-row' }, [
| |
| el('div', { className: 'rb-ce-form-field', style: 'flex:1' }, [el('label', null, 'Pages (manual additions)'), pagesChips.element]),
| |
| el('div', { className: 'rb-ce-form-field', style: 'flex:1' }, [el('label', null, 'Exclude pages'), excludeChips.element])
| |
| ]);
| |
| | |
| var saveFormBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-primary',
| |
| textContent: isNew ? 'Add Entry' : 'Update Entry',
| |
| onClick: function () {
| |
| var newEntry = {
| |
| id: idInput.value.trim() || autoId,
| |
| date: dateInput.value,
| |
| tag: tagSelect.value,
| |
| text: textArea.value
| |
| };
| |
| var pages = pagesChips.getValues();
| |
| var exclude = excludeChips.getValues();
| |
| if (pages.length > 0) newEntry.pages = pages;
| |
| if (exclude.length > 0) newEntry.exclude = exclude;
| |
| | |
| if (!newEntry.text.trim()) {
| |
| alert('Text is required.');
| |
| return;
| |
| }
| |
| | |
| if (isNew) {
| |
| entries.unshift(newEntry);
| |
| } else {
| |
| entries[editingIndex] = newEntry;
| |
| }
| |
| editingIndex = null;
| |
| markDirty();
| |
| }
| |
| });
| |
| var cancelBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost',
| |
| textContent: 'Cancel',
| |
| onClick: function () { editingIndex = null; render(); }
| |
| });
| |
| var actions = el('div', { className: 'rb-ce-form-actions' }, [saveFormBtn, cancelBtn]);
| |
| | |
| form.appendChild(row1);
| |
| form.appendChild(row2);
| |
| form.appendChild(row3);
| |
| form.appendChild(actions);
| |
| return form;
| |
| }
| |
| | |
| function createChipInput(initialValues) {
| |
| var values = (initialValues || []).slice();
| |
| var wrap = el('div', { className: 'rb-ce-chip-input-wrap' });
| |
| var input = el('input', { type: 'text', placeholder: 'Type page name...' });
| |
| var dropdown = null;
| |
| var debounceTimer = null;
| |
| var activeIdx = -1;
| |
| | |
| function renderChips() {
| |
| while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
| |
| values.forEach(function (v, i) {
| |
| var removeBtn = el('span', {
| |
| className: 'rb-ce-chip-remove',
| |
| textContent: '\u00D7',
| |
| onClick: function (e) {
| |
| e.stopPropagation();
| |
| values.splice(i, 1);
| |
| renderChips();
| |
| }
| |
| });
| |
| wrap.appendChild(el('span', { className: 'rb-ce-chip' }, [v, removeBtn]));
| |
| });
| |
| wrap.appendChild(input);
| |
| }
| |
| | |
| function addChip(val) {
| |
| var trimmed = val.trim();
| |
| if (trimmed && values.indexOf(trimmed) === -1) {
| |
| values.push(trimmed);
| |
| }
| |
| input.value = '';
| |
| hideDropdown();
| |
| renderChips();
| |
| input.focus();
| |
| }
| |
| | |
| function showDropdown(items) {
| |
| hideDropdown();
| |
| if (items.length === 0) return;
| |
| dropdown = el('div', { className: 'rb-ce-autocomplete' });
| |
| activeIdx = -1;
| |
| items.forEach(function (item, i) {
| |
| var div = el('div', {
| |
| className: 'rb-ce-autocomplete-item',
| |
| textContent: item,
| |
| onClick: function () { addChip(item); }
| |
| });
| |
| dropdown.appendChild(div);
| |
| });
| |
| wrap.appendChild(dropdown);
| |
| }
| |
| | |
| function hideDropdown() {
| |
| if (dropdown && dropdown.parentNode) {
| |
| dropdown.parentNode.removeChild(dropdown);
| |
| }
| |
| dropdown = null;
| |
| activeIdx = -1;
| |
| }
| |
| | |
| function highlightItem(idx) {
| |
| if (!dropdown) return;
| |
| var items = dropdown.querySelectorAll('.rb-ce-autocomplete-item');
| |
| items.forEach(function (it, i) {
| |
| it.classList.toggle('rb-ce-ac-active', i === idx);
| |
| });
| |
| activeIdx = idx;
| |
| }
| |
| | |
| input.addEventListener('input', function () {
| |
| clearTimeout(debounceTimer);
| |
| var q = input.value;
| |
| if (q.length < 2) { hideDropdown(); return; }
| |
| debounceTimer = setTimeout(function () {
| |
| var results = searchPageTitles(q);
| |
| showDropdown(results);
| |
| }, 300);
| |
| });
| |
| | |
| input.addEventListener('keydown', function (e) {
| |
| if (e.key === ',' || e.key === 'Enter') {
| |
| e.preventDefault();
| |
| if (activeIdx >= 0 && dropdown) {
| |
| var items = dropdown.querySelectorAll('.rb-ce-autocomplete-item');
| |
| if (items[activeIdx]) addChip(items[activeIdx].textContent);
| |
| } else if (input.value.trim()) {
| |
| addChip(input.value);
| |
| }
| |
| } else if (e.key === 'ArrowDown' && dropdown) {
| |
| e.preventDefault();
| |
| var items = dropdown.querySelectorAll('.rb-ce-autocomplete-item');
| |
| highlightItem(Math.min(activeIdx + 1, items.length - 1));
| |
| } else if (e.key === 'ArrowUp' && dropdown) {
| |
| e.preventDefault();
| |
| highlightItem(Math.max(activeIdx - 1, 0));
| |
| } else if (e.key === 'Escape') {
| |
| hideDropdown();
| |
| } else if (e.key === 'Backspace' && input.value === '' && values.length > 0) {
| |
| values.pop();
| |
| renderChips();
| |
| }
| |
| });
| |
| | |
| input.addEventListener('blur', function () {
| |
| setTimeout(hideDropdown, 200);
| |
| });
| |
| | |
| wrap.addEventListener('click', function () { input.focus(); });
| |
| | |
| renderChips();
| |
| return { element: wrap, getValues: function () { return values.slice(); } };
| |
| }
| |
| | |
| function renderPagination() {
| |
| var totalPages = Math.max(1, Math.ceil(entries.length / PER_PAGE));
| |
| var info = el('span', null, 'Page ' + (currentPage + 1) + ' of ' + totalPages + ' (' + entries.length + ' entries)');
| |
| var prevBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost rb-ce-btn-sm',
| |
| textContent: '\u25C0 Prev',
| |
| disabled: currentPage === 0 ? 'disabled' : null,
| |
| onClick: function () { currentPage--; editingIndex = null; render(); }
| |
| });
| |
| var nextBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost rb-ce-btn-sm',
| |
| textContent: 'Next \u25B6',
| |
| disabled: currentPage >= totalPages - 1 ? 'disabled' : null,
| |
| onClick: function () { currentPage++; editingIndex = null; render(); }
| |
| });
| |
| return el('div', { className: 'rb-ce-pagination' }, [prevBtn, info, nextBtn]);
| |
| }
| |
| | |
| function showConfirmDialog() {
| |
| var summary = computeChangeSummary();
| |
| var lines = [];
| |
| if (summary.added.length) lines.push(summary.added.length + ' added');
| |
| if (summary.modified.length) lines.push(summary.modified.length + ' modified');
| |
| if (summary.deleted.length) lines.push(summary.deleted.length + ' deleted');
| |
| if (summary.reordered) lines.push('entries reordered');
| |
| var changeText = lines.join(', ') || 'No detectable changes';
| |
| | |
| var overlay = el('div', { className: 'rb-ce-overlay' });
| |
| var dialog = el('div', { className: 'rb-ce-dialog' });
| |
| | |
| dialog.appendChild(el('h3', null, 'Confirm Save'));
| |
| | |
| var summaryDiv = el('div', { className: 'rb-ce-dialog-summary' });
| |
| summaryDiv.appendChild(el('p', null, 'Changes: ' + changeText));
| |
| if (summary.added.length) {
| |
| var addedList = el('ul');
| |
| summary.added.forEach(function (e) {
| |
| addedList.appendChild(el('li', null, '+ [' + e.tag + '] ' + e.text.substring(0, 80) + (e.text.length > 80 ? '...' : '')));
| |
| });
| |
| summaryDiv.appendChild(addedList);
| |
| }
| |
| if (summary.deleted.length) {
| |
| var delList = el('ul');
| |
| summary.deleted.forEach(function (e) {
| |
| delList.appendChild(el('li', null, '\u2212 [' + e.tag + '] ' + e.text.substring(0, 80) + (e.text.length > 80 ? '...' : '')));
| |
| });
| |
| summaryDiv.appendChild(delList);
| |
| }
| |
| dialog.appendChild(summaryDiv);
| |
| | |
| var defaultSummary = 'Changelog: ' + changeText;
| |
| var summaryField = el('div', { className: 'rb-ce-dialog-field' });
| |
| summaryField.appendChild(el('label', null, 'Edit summary'));
| |
| var summaryInput = el('input', { type: 'text', value: defaultSummary });
| |
| summaryField.appendChild(summaryInput);
| |
| dialog.appendChild(summaryField);
| |
| | |
| var saving = false;
| |
| var saveBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-primary',
| |
| textContent: 'Save',
| |
| onClick: function () {
| |
| if (saving) return;
| |
| saving = true;
| |
| saveBtn.disabled = true;
| |
| saveBtn.textContent = 'Saving...';
| |
| saveData(summaryInput.value || defaultSummary).then(function () {
| |
| overlay.parentNode.removeChild(overlay);
| |
| render();
| |
| }).catch(function (err) {
| |
| saving = false;
| |
| saveBtn.disabled = false;
| |
| saveBtn.textContent = 'Save';
| |
| try {
| |
| sessionStorage.setItem('rb-ce-backup', JSON.stringify(entries));
| |
| } catch (e) {}
| |
| alert('Save failed: ' + err.message + '\n\nYour changes have been backed up to session storage.');
| |
| });
| |
| }
| |
| });
| |
| var cancelBtn = el('button', {
| |
| className: 'rb-ce-btn rb-ce-btn-ghost',
| |
| textContent: 'Cancel',
| |
| onClick: function () { overlay.parentNode.removeChild(overlay); }
| |
| });
| |
| dialog.appendChild(el('div', { className: 'rb-ce-dialog-actions' }, [cancelBtn, saveBtn]));
| |
| | |
| overlay.appendChild(dialog);
| |
| overlay.addEventListener('click', function (e) {
| |
| if (e.target === overlay) overlay.parentNode.removeChild(overlay);
| |
| });
| |
| document.body.appendChild(overlay);
| |
| summaryInput.focus();
| |
| summaryInput.select();
| |
| }
| |
| | |
| function init() {
| |
| root.innerHTML = '<div class="rb-ce-status">Loading changelog data...</div>';
| |
| | |
| try {
| |
| var backup = sessionStorage.getItem('rb-ce-backup');
| |
| if (backup) {
| |
| if (confirm('A backup from a previous unsaved session was found. Restore it?')) {
| |
| entries = JSON.parse(backup);
| |
| dirty = true;
| |
| sessionStorage.removeItem('rb-ce-backup');
| |
| Promise.all([fetchEditToken(), fetchAllPageTitles()]).then(function () {
| |
| render();
| |
| });
| |
| return;
| |
| }
| |
| sessionStorage.removeItem('rb-ce-backup');
| |
| }
| |
| } catch (e) {}
| |
| | |
| Promise.all([fetchData(), fetchEditToken(), fetchAllPageTitles()])
| |
| .then(function () {
| |
| render();
| |
| })
| |
| .catch(function (err) {
| |
| root.innerHTML = '<div class="rb-ce-error">Failed to load: ' + err.message + '</div>';
| |
| });
| |
| }
| |
| | |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', init);
| |
| } else {
| |
| init();
| |
| } | | } |
| })(); | | })(); |