Jump to content

User:DreamRimmer/DR Editor.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <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>