User:DreamRimmer/DR Editor.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | This user script seems to have a documentation page at User:DreamRimmer/DR Editor. |
// <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>