// <nowiki>
mw.loader.using(
['mediawiki.diff', 'mediawiki.diff.styles', 'oojs-ui-core'],
() => {
if (
!['view', 'edit', 'history'].includes(mw.config.get('wgAction')) ||
mw.config.get('wgNamespaceNumber') < 0
) {
return;
}
if (mw.config.get('wgAction') === 'edit') {
return;
}
const DR = {};
DR.pagename = mw.config.get('wgPageName');
DR.contentmodel = null;
DR.contentmodels = [
'wikitext',
'text',
'sanitized-css',
'json',
'javascript',
'css',
'Scribunto'
];
DR.isEditorOpen = false;
DR.currentRevision = null;
DR.scrollToElement = function(elementId, offset = 0) {
const element = document.getElementById(elementId);
if (element) {
const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementPosition + offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
};
function getRevisionFromURL() {
const params = new URLSearchParams(window.___location.search);
return params.get('oldid') || params.get('diff');
}
function initEditor(section, sectionName) {
if (DR.isEditorOpen) {
mw.notify('DR Editor is already open.', { type: 'warn' });
return;
}
DR.isEditorOpen = true;
const revisionId = getRevisionFromURL();
const queryParams = {
action: 'query',
titles: DR.pagename,
prop: 'revisions',
rvprop: ['content', 'contentmodel', 'timestamp', 'user'],
rvlimit: revisionId && revisionId !== 'cur' ? 2 : 1,
format: 'json',
formatversion: 2
};
if (revisionId && revisionId !== 'cur') {
queryParams.rvstartid = revisionId;
}
if (typeof section !== 'undefined' && section !== null) {
queryParams.rvsection = section;
}
new mw.Api()
.get(queryParams)
.done(response => {
let content = '';
let contentmodel = 'wikitext';
let isOldRevision = false;
let revisionInfo = null;
if (response.query.pages[0].revisions) {
const revisions = response.query.pages[0].revisions;
content = revisions[0].content;
contentmodel = revisions[0].contentmodel;
if (revisionId && revisionId !== 'cur' && revisions.length > 0) {
isOldRevision = revisions.length > 1 || revisions[0].revid != response.query.pages[0].lastrevid;
revisionInfo = {
timestamp: revisions[0].timestamp,
user: revisions[0].user,
revid: revisions[0].revid
};
}
} else if (response.query.pages[0].missing) {
content = '';
} else {
mw.notify('Failed to load page content.', { type: 'error' });
DR.isEditorOpen = false;
return;
}
DR.content = content;
DR.contentmodel = contentmodel;
DR.currentSection = section;
DR.currentSectionName = sectionName;
if (!DR.contentmodels.includes(DR.contentmodel)) {
mw.notify('Page content model is not a simple text-based one.', {
title: 'Unallowed content model',
type: 'error',
autoHide: true,
autoHideSeconds: 5,
tag: 'DR-notification'
});
DR.isEditorOpen = false;
return;
}
$('#mw-content-text').hide();
DR.textarea = new OO.ui.MultilineTextInputWidget({
value: DR.content,
type: 'text',
id: 'DR-textarea-div',
inputId: 'DR-textarea'
});
DR.predefinedSummaries = [
{ data: '', label: 'Select a summary...' },
{ data: 'copyedit', label: 'Copyedit (minor fixes)' },
{ data: 'grammar', label: 'Grammar and spelling' },
{ data: 'wikify', label: 'Wikify (add links)' },
{ data: 'format', label: 'Formatting' },
{ data: 'cleanup', label: 'General cleanup' },
{ data: 'expand', label: 'Expand content' },
{ data: 'update', label: 'Update information' },
{ data: 'citation needed', label: 'Add citations' },
{ data: 'remove vandalism', label: 'Remove vandalism' },
{ data: 'revert', label: 'Revert changes' }
];
DR.summaryInput = new OO.ui.TextInputWidget({
placeholder: 'Edit summary',
id: 'DR-summary',
inputId: 'DR-summary-input'
});
DR.summaryDropdown = new OO.ui.DropdownWidget({
menu: {
items: DR.predefinedSummaries.map(item =>
new OO.ui.MenuOptionWidget({
data: item.data,
label: item.label
})
)
},
id: 'DR-summary-dropdown'
});
DR.summaryDropdown.getMenu().on('select', function(item) {
if (item && item.getData()) {
const currentValue = DR.summaryInput.getValue();
const newValue = currentValue ? `${currentValue}; ${item.getData()}` : item.getData();
DR.summaryInput.setValue(newValue);
}
});
DR.summaryField = new OO.ui.FieldLayout(DR.summaryInput, {
label: 'Edit summary:',
align: 'top'
});
DR.summaryDropdownField = new OO.ui.FieldLayout(DR.summaryDropdown, {
label: 'Quick summaries:',
align: 'top'
});
DR.minorCheckbox = new OO.ui.CheckboxInputWidget({
selected: false,
id: 'DR-minor'
});
DR.minorField = new OO.ui.FieldLayout(DR.minorCheckbox, {
label: 'Mark edit as minor',
align: 'inline'
});
DR.saveButton = new OO.ui.ButtonWidget({
label: 'Save',
flags: ['primary', 'progressive'],
classes: 'DR-buttons',
id: 'DR-save'
});
DR.previewButton = new OO.ui.ButtonWidget({
label: 'Preview',
classes: 'DR-buttons',
id: 'DR-preview'
});
DR.reviewButton = new OO.ui.ButtonWidget({
label: 'Review Changes',
classes: 'DR-buttons',
id: 'DR-review'
});
DR.cancel = new OO.ui.ButtonWidget({
label: 'Cancel',
flags: ['destructive'],
classes: 'DR-buttons',
id: 'DR-cancel'
});
const createEnhancedTitle = () => {
const $titleContainer = $('<div>')
.css({
'background': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '3px',
'padding': '10px 14px',
'margin-bottom': '12px',
'border-left': '4px solid #36c'
});
const $mainTitle = $('<span>')
.css({
'font-size': '16px',
'font-weight': 'bold',
'color': '#36c'
})
.text('DreamRimmer\'s editor');
const $separator = $('<span>')
.css({
'color': '#72777d',
'margin': '0 8px'
})
.text('•');
const $pageName = $('<span>')
.css({
'font-size': '15px',
'font-weight': '500',
'color': '#202122'
})
.text(DR.pagename.replace(/_/g, ' '));
$titleContainer.append($mainTitle, $separator, $pageName);
if (DR.currentSectionName) {
const $sectionTag = $('<span>')
.css({
'background-color': '#ffffff',
'padding': '6px 10px',
'border-radius': '3px',
'font-size': '12px',
'font-weight': 'bold',
'margin-left': '10px',
'border': '1px solid #36c'
})
.text(`Section: ${DR.currentSectionName}`);
$titleContainer.append($sectionTag);
}
if (revisionId && revisionId !== 'cur') {
const $revisionTag = $('<span>')
.css({
'background-color': '#fef6e7',
'color': '#ac6600',
'padding': '6px 10px',
'border-radius': '3px',
'font-size': '12px',
'font-weight': 'bold',
'margin-left': '8px',
'border': '1px solid #fc3'
})
.text(`Revision: ${revisionId}`);
$titleContainer.append($revisionTag);
}
return $titleContainer;
};
const enhancedTitle = createEnhancedTitle();
let warningDiv = '';
if (isOldRevision && revisionInfo) {
const date = new Date(revisionInfo.timestamp).toLocaleString('en-GB', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
day: 'numeric',
month: 'long',
year: 'numeric'
}).replace(/(\d{2}):(\d{2}), (\d+) (\w+) (\d{4})/, '$1:$2, $3 $4 $5');
const userLink = `<a href="/wiki/User:${revisionInfo.user}">${revisionInfo.user}</a>`;
const talkLink = `<a href="/wiki/User_talk:${revisionInfo.user}">talk</a>`;
const contribsLink = `<a href="/wiki/Special:Contributions/${revisionInfo.user}">contribs</a>`;
const userWithLinks = `${userLink} (${talkLink} | ${contribsLink})`;
warningDiv = $('<div>')
.addClass('DR-old-revision-warning')
.css({
'background-color': '#fef6e7',
'border': '1px solid #fc3',
'padding': '10px',
'margin-bottom': '10px',
'border-radius': '3px',
'color': '#333'
})
.html(`<strong>Warning:</strong> You are editing an out-of-date revision of this page. If you publish it, any changes made since this revision will be lost. This revision was made on <strong>${date}</strong> by ${userWithLinks}.`);
}
let infoDiv = '';
if (response.query.pages[0].missing) {
infoDiv = $('<div>')
.addClass('DR-new-page-info')
.css({
'background-color': '#d5fdf4',
'border': '1px solid #00af89',
'padding': '10px',
'margin-bottom': '10px',
'border-radius': '3px',
'color': '#333'
})
.html(`<strong>Info:</strong> You are creating a new page. This page does not exist yet and will be created when you save your changes.`);
}
let sectionInfoDiv = '';
if (DR.currentSectionName) {
sectionInfoDiv = $('<div>')
.addClass('DR-section-info')
.css({
'background-color': '#eaf3ff',
'border': '1px solid #36c',
'padding': '10px',
'margin-bottom': '10px',
'border-radius': '3px',
'color': '#333'
})
.html(`<strong>Info:</strong> You are editing section "<strong>${DR.currentSectionName}</strong>". Your changes will only affect this section of the page.`);
}
const $editorContainer = $('<div>')
.attr('id', 'DR-main')
.append(
enhancedTitle,
warningDiv,
infoDiv,
sectionInfoDiv,
$('<div>')
.attr('id', 'DR-output')
.css({
border: '1px solid #A2A9B1',
padding: '5px',
'margin-bottom': '10px',
display: 'none'
}),
DR.textarea.$element,
DR.summaryField.$element,
DR.summaryDropdownField.$element,
DR.minorField.$element,
$('<div>')
.attr('id', 'DR-buttons')
.css({
display: 'flex',
padding: '5px',
'justify-content': 'space-between',
'margin-top': '4px'
})
);
$('#mw-content-text').after($editorContainer);
setTimeout(() => {
DR.scrollToElement('DR-main', -50);
}, 100);
$('#DR-buttons').prepend(
$('<div>').append(
DR.saveButton.$element,
DR.previewButton.$element,
DR.reviewButton.$element,
DR.cancel.$element
)
);
$('#DR-textarea-div').css({ margin: 0, 'max-width': '100%' });
$('#DR-textarea').css({
'min-height': '300px',
'min-width': '100%',
resize: 'vertical',
'font-size': 'small',
'font-family': 'monospace, monospace'
});
$('#DR-summary').css({
'margin-top': '3px',
'max-width': '100%',
width: '100%'
});
$('#DR-summary-dropdown').css({
'margin-top': '3px',
'max-width': '100%',
width: '100%'
});
DR.saveButton.$element.click(() => {
const newContent = $('#DR-textarea').val();
let summary = DR.summaryInput.getValue() || '';
if (DR.currentSectionName) {
summary = `/* ${DR.currentSectionName} */ ${summary}`;
}
summary += ' ([[:en:User:DreamRimmer/DR Editor|DR]])';
const editParams = {
action: 'edit',
title: DR.pagename,
text: newContent,
summary: summary
};
if (typeof DR.currentSection !== 'undefined' && DR.currentSection !== null) {
editParams.section = DR.currentSection;
}
if (DR.minorCheckbox.isSelected()) {
editParams.minor = true;
}
new mw.Api()
.postWithEditToken(editParams)
.done(response => {
if (response.error && response.error.code === 'editconflict') {
const dialog = new OO.ui.MessageDialog();
const windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
windowManager.addWindows([dialog]);
dialog.open({
title: 'Edit Conflict',
message:
response.error.info ||
'An edit conflict occurred. Please resolve it manually.'
});
} else if (response.edit && response.edit.result === 'Success') {
mw.notify('Page saved successfully!', {
title: 'Saved',
type: 'success',
autoHide: true,
autoHideSeconds: 5,
tag: 'DR-notification'
});
___location.reload();
} else {
mw.notify('Error saving page.', {
title: 'Error',
type: 'error',
autoHide: true,
autoHideSeconds: 5,
tag: 'DR-notification'
});
}
})
.fail(() => {
mw.notify('Error saving page.', {
title: 'Error',
type: 'error',
autoHide: true,
autoHideSeconds: 5,
tag: 'DR-notification'
});
});
});
DR.previewButton.$element.click(() => {
$('#DR-output')
.show()
.html(
'<img src="https://upload.wikimedia.org/wikipedia/commons/5/51/Ajax-loader4.gif" width="30" height="30" alt="Loading...">'
);
setTimeout(() => {
DR.scrollToElement('DR-output', -20);
}, 100);
const previewContent = $('#DR-textarea').val();
new mw.Api()
.post({
action: 'parse',
text: previewContent,
title: DR.pagename,
contentmodel: DR.contentmodel,
pst: true,
format: 'json'
})
.done(response => {
$('#DR-output').html(response.parse.text['*']);
setTimeout(() => {
DR.scrollToElement('DR-output', -20);
}, 200);
})
.fail(() => {
$('#DR-output').html(
'<div style="color: red;">Error generating preview.</div>'
);
});
});
DR.reviewButton.$element.click(() => {
$('#DR-output')
.show()
.html(
'<img src="https://upload.wikimedia.org/wikipedia/commons/5/51/Ajax-loader4.gif" width="30" height="30" alt="Loading...">'
);
setTimeout(() => {
DR.scrollToElement('DR-output', -20);
}, 100);
const compareParams = {
action: 'compare',
fromtitle: DR.pagename,
toslots: 'main',
'totext-main': $('#DR-textarea').val(),
format: 'json',
formatversion: 2
};
if (typeof DR.currentSection !== 'undefined' && DR.currentSection !== null) {
compareParams.fromsection = DR.currentSection;
}
if (revisionId && revisionId !== 'cur') {
compareParams.fromrev = revisionId;
}
$.ajax({
url: mw.config.get('wgScriptPath') + '/api.php',
data: compareParams,
type: 'POST',
dataType: 'json',
success: response => {
const diffHtml =
response.compare.body === ''
? '<div>(No changes)</div>'
: '<table class="diff diff-editfont-monospace" style="margin: auto; font-size: small;">' +
'<colgroup>' +
'<col class="diff-marker">' +
'<col class="diff-content">' +
'<col class="diff-marker">' +
'<col class="diff-content">' +
'</colgroup>' +
'<tbody>' +
response.compare.body +
'</tbody>' +
'</table>';
$('#DR-output').html(diffHtml);
mw.hook('wikipage.diff').fire($('#DR-output'));
setTimeout(() => {
DR.scrollToElement('DR-output', -20);
}, 200);
},
error: () => {
$('#DR-output').html(
'<div style="color: red;">Error generating diff.</div>'
);
}
});
});
DR.cancel.$element.click(() => {
$('#mw-content-text').show();
$('#DR-main').remove();
DR.isEditorOpen = false;
});
})
.fail(error => {
mw.notify('API error: ' + error, { type: 'error' });
DR.isEditorOpen = false;
});
}
function checkForEditAction() {
if (mw.config.get('wgAction') === 'edit' && DR.isEditorOpen) {
$('#mw-content-text').show();
$('#DR-main').remove();
DR.isEditorOpen = false;
}
}
$(window).on('beforeunload', () => {
if (DR.isEditorOpen) {
DR.isEditorOpen = false;
}
});
$(window).on('popstate', checkForEditAction);
$(document).ready(() => {
checkForEditAction();
const topBtn = $('<li>')
.attr('id', 'DR-Edit-TopBtn')
.append(
$('<span>').append(
$('<a>')
.attr('href', '#')
.text('DR Editor')
).data({ number: -1, target: DR.pagename })
);
if (mw.config.get('skin') === 'vector-2022') {
topBtn.addClass('vector-tab-noicon mw-list-item');
topBtn.empty().append(
$('<a>')
.attr('href', '#')
.append($('<span>').text('DR Editor'))
.data({ number: -1, target: DR.pagename })
);
} else if (mw.config.get('skin') === 'minerva') {
$(topBtn).css({ 'align-items': 'center', display: 'flex' });
$(topBtn).find('span').addClass('page-actions-menu__list-item');
$(topBtn)
.find('a')
.addClass(
'mw-ui-icon mw-ui-icon-element mw-ui-icon-wikimedia-edit-base20 mw-ui-icon-with-label-desktop'
)
.css('vertical-align', 'middle');
}
if (mw.config.get('skin') === 'minerva') {
$(topBtn).css({ 'align-items': 'center', display: 'flex' });
$(topBtn).find('span').addClass('page-actions-menu__list-item');
$(topBtn)
.find('a')
.addClass(
'mw-ui-icon mw-ui-icon-element mw-ui-icon-wikimedia-edit-base20 mw-ui-icon-with-label-desktop'
)
.css('vertical-align', 'middle');
}
if ($('#ca-edit').length > 0 || mw.config.get('wgArticleId') === 0) {
if ($('#DR-Edit-TopBtn').length === 0) {
if (mw.config.get('skin') === 'minerva') {
if ($('#ca-edit').length > 0) {
$('#ca-edit').parent().after(topBtn);
} else {
$('#page-actions ul').append(topBtn);
}
} else {
if ($('#ca-edit').length > 0) {
$('#ca-edit').after(topBtn);
} else {
$('#ca-talk').after(topBtn);
}
}
}
$('#DR-Edit-TopBtn').click(() => {
initEditor();
});
}
$(".mw-editsection > .mw-editsection-bracket:contains(']')").each(function(){
if ($(this).siblings('.DR-section-edit').length === 0) {
const $editLink = $(this).siblings('a[href*="action=edit"]').first();
if ($editLink.length > 0) {
const href = $editLink.attr('href');
const urlParams = new URLSearchParams(href.split('?')[1]);
const section = urlParams.get('section');
const sectionName = $(this).closest('.mw-editsection').parent().find(':header').first().text().trim() || 'Section ' + section;
const btn = $('<a/>')
.addClass('DR-section-edit')
.text('DR')
.attr('href', '#')
.on('click', function(e) {
e.preventDefault();
initEditor(section, sectionName);
});
$(this).before(', ').before(btn);
}
}
});
});
if (mw.config.get('skin') === 'minerva') {
$('.mw-editsection a[href*="action=edit"][href*="section="]').each(function() {
const $editButton = $(this);
const href = $editButton.attr('href');
if (href && !$editButton.siblings('.DR-minerva-section-edit').length) {
const urlParams = new URLSearchParams(href.split('?')[1]);
const section = urlParams.get('section');
const $heading = $editButton.closest('.mw-heading');
const $header = $heading.find('h1, h2, h3, h4, h5, h6').first();
const sectionName = $header.length ? $header.text().trim() : 'Section ' + section;
const $drButton = $('<a>')
.addClass('DR-minerva-section-edit cdx-button cdx-button--size-large cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet')
.css({
'margin-left': '8px'
})
.attr('href', '#')
.attr('title', `Edit section with DR Editor: ${sectionName}`)
.text('DR')
.on('click', function(e) {
e.preventDefault();
initEditor(section, sectionName);
});
$editButton.after($drButton);
}
});
}
$(async function () {
let section = null;
const dependencies = [
'jquery.textSelection',
'oojs-ui-core',
'oojs-ui.styles.icons-editing-core'
];
await mw.loader.using(dependencies);
mw.loader.addStyleTag(
'.diff > tbody > tr{position:relative} .diffundo{position:absolute;inset-inline-end:0;bottom:0} tr:not(:hover) > td > .diffundo:not(:focus-within){opacity:0} .diffundo-undone{text-decoration:line-through;opacity:0.5}'
);
const idxMap = new WeakMap();
let offset = 0;
let rev;
const handler = button => {
const $row = button.$element.closest('tr');
const numRow = $row.prevAll().toArray().find(row => idxMap.has(row));
if (!numRow) {
mw.notify("Couldn't get the line number.", {
tag: 'diffundo',
type: 'error'
});
return;
}
const isUndone = $row.hasClass('diffundo-undone');
const $toReplace = $row.children(isUndone ? '.diff-deletedline' : '.diff-addedline');
const $toRestore = $row.children(isUndone ? '.diff-addedline' : '.diff-deletedline');
const isInsert = !$toReplace.length;
const isRemove = !$toRestore.length;
const $midLines = $row.prevUntil(numRow).map(function () {
return this.querySelector(
this.classList.contains('diffundo-undone')
? ':scope > .diff-deletedline'
: ':scope > .diff-context, :scope > .diff-addedline'
);
});
const lineIdx = idxMap.get(numRow) + $midLines.length;
const $textarea = $('#DR-textarea');
const lines = $textarea.textSelection('getContents').split('\n');
let canUndo;
if (isInsert) {
canUndo =
!$midLines.length ||
lines[lineIdx - 1] === $midLines[0].textContent;
} else {
canUndo = lines[lineIdx] === $toReplace.text();
}
if (!canUndo) {
mw.notify('The line has been modified since the diff.', {
tag: 'diffundo',
type: 'warn'
});
return;
}
const coords = [window.scrollX, window.scrollY];
let [start, end] = $textarea.textSelection('getCaretPosition', { startAndEnd: true });
const beforeLen = lines.slice(0, lineIdx).join('').length + lineIdx;
if (isRemove) {
const toReplaceLen = lines[lineIdx].length;
lines.splice(lineIdx, 1);
[start, end] = [start, end].map(idx => {
if (idx > beforeLen + toReplaceLen) {
return idx - toReplaceLen - 1;
} else if (idx > beforeLen) {
return beforeLen;
}
return idx;
});
$row.nextAll().each(function () {
if (idxMap.has(this)) {
idxMap.set(this, idxMap.get(this) - 1);
}
});
} else if (isInsert) {
const text = $toRestore.text();
lines.splice(lineIdx, 0, text);
[start, end] = [start, end].map(idx => {
if (idx > beforeLen) {
return idx + text.length + 1;
}
return idx;
});
$row.nextAll().each(function () {
if (idxMap.has(this)) {
idxMap.set(this, idxMap.get(this) + 1);
}
});
} else {
const toReplaceLen = lines[lineIdx].length;
const text = $toRestore.text();
lines.splice(lineIdx, 1, text);
[start, end] = [start, end].map(idx => {
if (idx > beforeLen + toReplaceLen) {
return idx - (toReplaceLen - text.length);
} else if (idx > beforeLen) {
return beforeLen;
}
return idx;
});
}
$textarea.textSelection('setContents', lines.join('\n'));
$textarea
.textSelection('setSelection', { start, end })
.textSelection('scrollToCaretPosition');
$row.toggleClass('diffundo-undone', !isUndone);
window.scrollTo(...coords);
setTimeout(() => {
button.focus();
});
};
const updateOffset = async () => {
if (rev) {
const { query } = await new mw.Api().get({
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'info',
formatversion: 2
});
if (query.pages[0].lastrevid === rev) return;
}
const { parse } = await new mw.Api().get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'revid|sections|wikitext',
formatversion: 2
});
const charOffset = section
? parse.sections.find(s => s.index === section)?.byteoffset
: 0;
if (section && (charOffset === undefined || charOffset === null)) {
mw.notify("Couldn't get the section offset.", {
tag: 'diffundo',
type: 'error'
});
return false;
}
offset = charOffset
? [...parse.wikitext].slice(0, charOffset - 1).join('').split('\n')
.length
: 0;
rev = parse.revid;
};
mw.hook('wikipage.diff').add(async $diff => {
if (!$('#DR-main').length) {
return;
}
const $lineNums = $diff.find('.diff-lineno:last-child');
if (
!$lineNums.length ||
(section &&
((await updateOffset()) === false || !$diff[0].isConnected))
) {
return;
}
$lineNums.each(function () {
const num = this.textContent.replace(/\D/g, '');
if (!num) return;
idxMap.set(this.parentElement, num - 1 - offset);
});
$diff.find('.diff-addedline, .diff-empty.diff-side-added').append(() => {
const button = new OO.ui.ButtonWidget({
classes: ['diffundo'],
framed: false,
icon: 'undo',
title: 'Undo this line'
});
return button.on('click', handler, [button]).$element;
});
});
});
}
);
// </nowiki>