User:Jon Harald Søby/diffedit.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*!
* diffedit.js – script that lets you edit pages directly from the diff view
*
* @author Jon Harald Søby
* @version 1.2.1 (2024-09-30)
* @licence CC-by-SA 4.0
*
* For documentation, see [[User:Jon Harald Søby/diffedit]]
*/
function initDiffedit( $, mw, OO ) {
'use strict';
var messages,
linetopCache = '',
tabIndex = 1,
pageId = mw.config.get( 'wgArticleId' ),
oldRevisionId = mw.config.get( 'wgDiffOldId' ),
newRevisionId = mw.config.get( 'wgDiffNewId' ),
currentRevisionId = mw.config.get( 'wgCurRevisionId' ),
contentModel = mw.config.get( 'wgPageContentModel' ),
allowedContentModels = [ 'wikitext', 'text', 'sanitized-css', 'json', 'javascript', 'css', 'Scribunto' ],
api = new mw.Api();
/* The 'messages' function is from [[d:MediaWiki:Gadget-Merge.js]],
* see that page's history for credits
*/
messages = function() {
var translations = {
en: {
editTitle: 'Edit this diff',
editTitleNewerRevs: 'This diff can\'t be edited, since there are newer revisions of the page.',
noPermission: 'You do not have permission to edit this page',
refresh: 'Show latest',
refreshTitle: 'Click to load the diff against the newest revision of the page'
},
ar: {
editTitle: 'عدّل هذا الفرق',
editTitleNewerRevs: 'لا يمكن تعديل هذا الفرق، نظرًا لوجود مراجعات أحدث للصفحة.',
noPermission: 'ليس لديك صلاحية تعديل هذه الصفحة',
refresh: 'عرض الأحدث',
refreshTitle: 'انقر لتحميل الفرق مقابل أحدث مراجعة للصفحة'
},
bn: {
editTitle: 'এই পার্থক্যটি সম্পাদনা করুন',
editTitleNewerRevs: 'পাতাটিতে নতুনতর সম্পাদনা থাকায়, এই পার্থক্যটি সম্পাদনা করা যাবে না।',
noPermission: 'আপনার এই পাতাটি সম্পাদনা করার অনুমতি নেই',
refresh: 'সর্বশেষটি দেখান',
refreshTitle: 'পাতাটির নতুনতর সংশোধনের পার্থক্য লোড করতে ক্লিক করুন'
},
ckb: {
editTitle: 'ئەم جیاوازییە دەستکاری بکە',
editTitleNewerRevs: 'ئەم جیاوازییە ناتوانرێت دەستکاری بکرێ، چونکە پەڕەکە پێداچوونەوەی نوێتری ھەیە.',
noPermission: 'دەسەڵاتت نییە دەستکاریی ئەم پەڕەیە بکەیت.',
refresh: 'کۆتا دانە پیشان بدە',
refreshTitle: 'کرتە بکە تا جیاوازییەکە لەگەڵ نوێترین پێداچوونەوەی پەڕەکە بار بکەیت'
},
de: {
editTitle: 'Diesen Versionsunterschied bearbeiten',
editTitleNewerRevs: 'Dieser Versionsunterschied kann nicht bearbeitet werden, da es neuere Versionen der Seite gibt.',
noPermission: 'Du hast keine Berechtigung, diese Seite zu bearbeiten.',
refresh: 'Letzte anzeigen',
refreshTitle: 'Klicke, um den Versionsunterschied zur neuesten Version der Seite zu laden'
},
es: {
editTitle: 'Editar esta diferencia',
editTitleNewerRevs: 'Esta diferencia no puede editarse ya que hay revisiones más recientes de la página.',
noPermission: 'No tienes permiso para editar esta página',
refresh: 'Mostrar la última revisión',
refreshTitle: 'Pulsa para cargar la diferencia con la revisión más reciente de la página'
},
fa: {
editTitle: 'ویرایش این تفاوت',
editTitleNewerRevs: 'این تفاوت را نمیتوان ویرایش کرد؛ زیرا نسخههای جدیدتری از صفحه موجود هستند.',
noPermission: 'شما اختیارات لازم برای ویرایش این صفحه را ندارید',
refresh: 'نمایش جدیدترین',
refreshTitle: 'برای بارگیری تفاوت با جدیدترین نسخهٔ صفحه کلیک کنید'
},
fr: {
editTitle: 'Modifier ce diff',
editTitleNewerRevs: 'Ce diff ne peut pas être édité, car il existe des révisions plus récentes de la page.',
noPermission: 'Vous n\'avez pas le droit de modifier cette page',
refresh: 'Montrer le dernier',
refreshTitle: 'Cliquez pour charger la comparaison avec la dernière révision de la page.'
},
he: {
editTitle: 'עריכת ההשוואה הזאת',
editTitleNewerRevs: 'ההשוואה הזאת לא ניתנת לעריכה, מכיוון שקיימות גרסאות חדשות יותר של הדף.',
noPermission: 'אין לך הרשאות לעריכת הדף הזה',
refresh: 'להראות את המאוחר ביותר',
refreshTitle: 'ללחוץ לקבלת ההשוואה עם הגרסה האחרונה של הדף'
},
hr: {
editTitle: 'Uredi prikazanu razliku inačica',
editTitleNewerRevs: 'Prikazana razlika ne može se uređivati zato što postoje novije inačice stranice.',
noPermission: 'Nije Vam dopušteno uređivati ovu stranicu',
refresh: 'Posljednja inačica',
refreshTitle: 'Učitaj razliku s najnovijom inačicom stranice'
},
ja: {
editTitle: 'この差分を編集',
editTitleNewerRevs: 'ページに新しい版があるため、この差分は編集できません。',
noPermission: 'このページを編集する権限がありません',
refresh: '最新版を表示',
refreshTitle: 'クリックして、ページの最新版との差分を読み込みます'
},
ka: {
editTitle: 'ამ განსხვავების რედაქტირება',
editTitleNewerRevs: 'ამ განსხვავების რედაქტირება შეუძლებელია, რადგან არსებობს გვერდის უფრო ახალი ვერსია.',
noPermission: 'თქვენ არ გაქვთ ამ გვერდის რედაქტირების უფლება',
refresh: 'უკანასკნელის ხილვა',
refreshTitle: 'დააწკაპეთ, რათა იხილოთ განსხვავება გვერდის უახლეს ვერსიასთან'
},
ko: {
editTitle: '이 diff 수정',
editTitleNewerRevs: '페이지의 최신 버전이 있으므로 이 diff를 수정할 수 없습니다.',
noPermission: '이 페이지를 편집할 수 있는 권한이 없습니다.',
refresh: '최신 보기',
refreshTitle: '페이지의 최신 개정판에 대한 diff를 로드하려면 클릭하십시오.'
},
nb: {
editTitle: 'Rediger denne diffen',
editTitleNewerRevs: 'Denne diffen kan ikke redigeres, siden det finnes nyere revisjoner av siden.',
noPermission: 'Du har ikke tillatelse til å redigere denne sida',
refresh: 'Vis nyeste',
refreshTitle: 'Klikk for å laste diffen mot den nyeste revisjonen av siden'
},
nl: {
editTitle: 'Dit verschil bewerken',
editTitleNewerRevs: 'Dit verschil kan niet worden bewerkt, omdat er een nieuwere revisie van de pagina is.',
noPermission: 'U mag deze pagina niet bewerken',
refresh: 'Toon de laatste',
refreshTitle: 'Klik om het verschil te laden met de nieuwste revisie van de pagina'
},
nn: {
editTitle: 'Endre denne diffen',
editTitleNewerRevs: 'Du kan ikkje endre denne diffen, av di det finst nyare revisjonar av sida.',
noPermission: 'Du har ikkje løyve til å endra denne sida',
refresh: 'Syn nyaste',
refreshTitle: 'Klikk for å lasta diffen mot den nyaste versjonen av sida'
},
pl: {
editTitle: 'Edytuj to porównanie',
editTitleNewerRevs: 'Nie możesz edytować porównania, bo istnieją nowsze wersje tej strony.',
noPermission: 'Nie masz uprawnień do edycji tej strony',
refresh: 'Pokaż najnowszą',
refreshTitle: 'Kliknij, aby załadować porównanie z najnowszą wersją tej strony'
},
sv: {
editTitle: 'Redigera denna diff',
editTitleNewerRevs: 'Denna diff kan inte redigeras då det finns nyare sidversioner.',
noPermission: 'Du har inte behörighet att redigera den här sidan',
refresh: 'Visa senaste',
refreshTitle: 'Klicka för att ladda in skillnaden mot den senaste sidversionen'
},
th: {
editTitle: 'แก้ไขความแตกต่างนี้',
editTitleNewerRevs: 'แก้ไขความแตกต่างนี้ไม่ได้ เนื่องจากมีการแก้ไขหน้าเว็บที่ใหม่กว่า',
noPermission: 'คุณไม่ได้รับอนุญาตให้แก้ไขหน้านี้',
refresh: 'แสดงการแก้ไขล่าสุด',
refreshTitle: 'คลิกเพื่อโหลดความแตกต่างกับรุ่นใหม่ล่าสุดของหน้า'
},
tl: {
editTitle: 'I-edit ang diff na ito',
editTitleNewerRevs: 'Hindi ma-edit ang diff na ito, dahil may mga mas bagong rebisyon ng page.',
noPermission: 'Wala kang permission mag-edit ang page.',
refresh: 'Ipakita ang pinakbago',
refreshTitle: 'I-click upang i-load ang diff laban sa pinakabagong rebisyon ng pahina'
},
vi: {
editTitle: 'Sửa đổi từ trang Khác này',
editTitleNewerRevs: 'Không thể sửa đổi vì có các phiên bản mới hơn.',
noPermission: 'Bạn không có quyền sửa trang này',
refresh: 'Xem sửa đổi mới nhất',
refreshTitle: 'Nhấp để xem khác biệt với phiên bản hiện tại của trang'
},
zh: {
editTitle: '編輯此差異',
editTitleNewerRevs: '由於有更新的版本,這個差異無法被修改。',
noPermission: '您沒有權限修改這個頁面。',
refresh: '顯示最新版本',
refreshTitle: '點擊以載入頁面與最新修改的差異'
},
},
chain = mw.language.getFallbackLanguageChain(),
len = chain.length,
ret = {},
i = len - 1;
while ( i >= 0 ) {
if ( translations.hasOwnProperty( chain[ i ] ) ) {
$.extend( ret, translations[ chain[ i ] ] );
}
i = i - 1;
}
return ret;
}();
function enumerateLines() {
var currentLine = 0,
numberRegex = '0-9',
localNumbers = mw.language.getDigitTransformTable();
if ( ( localNumbers instanceof Array && localNumbers.length ) || Object.keys( localNumbers ).length ) {
for ( const i of Array( 10 ).keys() ) {
numberRegex = numberRegex.concat( localNumbers[ i ] );
}
}
$( 'table.diff tbody tr td:last-of-type' ).each( function() {
if ( $( this ).hasClass( 'diff-lineno' ) ) {
var lineNo = $( this ).text();
lineNo = mw.language.convertNumber( lineNo.replace( new RegExp( '[^' + numberRegex + ']', 'g' ), '' ), true );
currentLine = lineNo;
} else if ( $( this ).hasClass( 'diff-addedline' ) || $( this ).hasClass( 'diff-context' ) ) {
$( this ).addClass( 'diff-editable' ).attr( 'data-mw-diff-line', currentLine );
$( this ).attr( 'data-mw-diff-tabindex', tabIndex );
currentLine++;
tabIndex++;
}
});
}
function addEditButton( titleText ) {
var editTitle = titleText ? titleText : messages.editTitle,
editIcon = titleText ? 'editLock' : 'edit';
var editButton = new OO.ui.ButtonWidget( {
label: mw.message( 'edit' ).text(),
icon: editIcon,
title: editTitle,
flags: [ 'primary', 'progressive' ],
disabled: !!titleText
});
linetopCache = $( '.diff-linetop' ).html();
$( '.diffedit-editbutton' ).append( editButton.$element );
if ( !titleText ) {
editButton.on( 'click', function() {
toggleEditMode( 'enable' );
addEditLine();
});
}
}
function addRefreshButton() {
var refreshButton = new OO.ui.ButtonWidget( {
label: messages.refresh,
icon: 'reload',
title: messages.refreshTitle,
flags: [ 'progressive' ],
invisibleLabel: true,
framed: false
});
$( '.diffedit-editbutton' ).prepend( refreshButton.$element );
refreshButton.on( 'click', function() {
window.location.href = mw.config.get( 'wgServer' ) + mw.util.getUrl( mw.config.get( 'wgPageName' ), { 'diff': 'cur', 'oldid': oldRevisionId } );
});
}
function addEditLine() {
var editSummary = new OO.ui.TextInputWidget( {
icon: 'textSummary',
accessKey: mw.message( 'accesskey-summary' ).text(),
name: 'wpSummary',
tabIndex: tabIndex + 1,
placeholder: mw.message( 'revisionslider-label-comment' ).text(),
title: mw.message( 'tooltip-summary' ).text(),
classes: [ 'diffedit-editsummary' ]
}),
publishButton = new OO.ui.ButtonWidget( {
label: mw.message( 'publishchanges' ).text(),
title: mw.message( 'tooltip-publish' ).text(),
accessKey: mw.message( 'accesskey-publish' ).text(),
tabIndex: tabIndex + 2,
flags: [ 'primary', 'progressive' ],
classes: [ 'diffedit-publishbutton' ]
}),
cancelButton = new OO.ui.ButtonWidget( {
label: mw.message( 'cancel' ).text(),
icon: 'cancel',
invisibleLabel: true,
tabIndex: tabIndex + 3,
flags: [ 'destructive' ],
framed: false
}),
editLine = new OO.ui.FieldLayout( new OO.ui.Widget( {
content: [
new OO.ui.HorizontalLayout( {
items: [ editSummary, publishButton, cancelButton ]
})]
}));
publishButton.on( 'click', function() {
processEdit();
});
publishButton.$element.hover( function() {
if ( $( 'input[name=wpSummary]' ).val().length === 0 ) {
$( 'input[name=wpSummary]' ).css( { 'outline': '5px solid var(--color-warning)', 'transition': 'outline 500ms cubic-bezier(.5,2,.5,-1)' } );
} else {
$( 'input[name=wpSummary] ').css( { 'outline': '5px solid var(--border-color-transparent)' } );
}
});
editSummary.$element.keyup( function( e ) {
if ( e.key === 'Enter' ) {
processEdit();
}
});
cancelButton.on( 'click', function() {
toggleEditMode( 'disable' );
});
editLine.$field.css( 'float', 'none' );
$( '.diff-linetop' ).html( editLine.$element );
$( 'input[name=wpSummary]' ).on( 'keyup keydown change', function() {
$( this ).css( { 'outline': '5px solid var(--border-color-transparent)', 'transition': 'outline 500ms ease-out' } );
});
}
function toggleEditMode( state ) {
if ( state === 'enable' ) {
$( 'table.diff' ).addClass( 'diff-editmode' );
$( '.diff-editable' ).each( function() {
$( this ).attr( { 'contenteditable': 'true', 'tabindex': $( this ).attr( 'data-mw-diff-tabindex' ) } ).css( { 'word-wrap': 'break-word', 'white-space': 'pre-wrap' } );
});
$( '.diff-editable div' ).text();
$( '.diff-editable' ).first().attr( 'accesskey', ',' ).focus();
} else {
$( 'table.diff' ).removeClass( 'diff-editmode' );
$( '.diff-linetop' ).html( linetopCache );
addEditButton();
$( '.diff-editable' ).each( function() {
$( this ).attr( 'contenteditable', 'false').removeAttr( 'tabindex' ).css( { 'word-wrap': 'break-word', 'white-space': 'pre-wrap' } );
});
}
}
function processEdit() {
$( '.diffedit-publishbutton' ).addClass( 'oo-ui-pendingElement-pending' ).removeClass( 'oo-ui-flaggedElement-primary' );
var currentContent = '',
contentFromApi = api.get( {
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
pageids: pageId,
rvstartid: currentRevisionId,
rvendid: currentRevisionId
} );
contentFromApi.done( function( data ) {
var mainSlot = data.query.pages[pageId].revisions[0].slots.main,
contentModel = mainSlot.contentmodel;
currentContent = mainSlot[ '*' ];
currentContent = currentContent.split( '\n' );
var newContent = currentContent;
$( '.diff-editable' ).each( function() {
var thisline = [],
lineNo = $( this ).attr( 'data-mw-diff-line' );
if ( $( this ).contents().length === 0 ) {
thisline.push( '' );
} else if ( $( this ).contents()[0].nodeName === '#text' ) {
thisline.push( $( this ).contents()[0].textContent );
} else {
$( this ).contents().each( function() {
thisline.push( this.textContent );
});
}
newContent[lineNo-1] = thisline.join( '\n' );
});
api.postWithEditToken( {
action: 'edit',
pageid: pageId,
baserevid: newRevisionId,
nocreate: true,
text: newContent.join( '\n' ),
minor: true,
summary: $( 'input[name=wpSummary]' ).val() + ' ' + mw.message( 'parentheses', '[[m:Special:MyLanguage/User:Jon Harald Søby/diffedit|diffedit]]' ).text()
} ).done( function( data ) {
window.location.href = mw.config.get( 'wgServer' ) + mw.util.getUrl( mw.config.get( 'wgPageName' ), { 'diff': 'cur' } );
}).fail( function( err ) {
console.log(err);
alert( 'Error: ' + err );
});
}).fail( function( err ) {
alert( 'Failed: ' + err );
return;
});
}
$( '.diff-lineno:first' ).next().addClass( 'diff-linetop' ).append( $( '<div />' ).addClass( 'diffedit-editbutton' ) );
if ( !mw.config.get( 'wgIsProbablyEditable' ) ) {
addEditButton( messages.noPermission );
} else if ( $( '.mw-diff-slot-header' ).length ) {
addEditButton( mw.message( 'editpage-invalidcontentmodel-text', 'mixed content' ).text() );
} else if ( !allowedContentModels.includes( contentModel ) ) {
addEditButton( mw.message( 'editpage-invalidcontentmodel-text', contentModel ).text() );
} else if ( newRevisionId !== currentRevisionId ) {
addEditButton( messages.editTitleNewerRevs );
addRefreshButton();
} else {
enumerateLines();
addEditButton();
$( '.diff-linetop, .diff-editable' ).keyup( function( e ) {
if ( e.key === 'Escape' ) {
toggleEditMode( 'disable' );
} else if ( e.ctrlKey && e.key === 'Enter' ) {
processEdit();
}
});
}
}
( function() {
if ( !( mw.config.get( 'wgDiffNewId' ) ) ) {
return;
}
mw.loader.using( [
'mediawiki.api',
'mediawiki.jqueryMsg',
'mediawiki.language',
'oojs-ui-core',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-layout'
] ).then( function() {
mw.loader.load( 'https://meta.wikimedia.org/w/index.php?title=User:Jon_Harald_Søby/diffedit.css&action=raw&ctype=text/css', 'text/css' );
new mw.Api().loadMessagesIfMissing( [
'edit', // Edit
'publishchanges', // Publish
'tooltip-publish', // Publish these changes
'cancel', // Cancel
'accesskey-publish', // s
'accesskey-summary', // b
'editpage-invalidcontentmodel-text', // Invalid content model $1
'revisionslider-label-comment', // Edit summary
'tooltip-summary', // Summary tooltip
'parentheses' // ($1)
] ).done( function( data ) {
initDiffedit( jQuery, mediaWiki, OO );
} ).fail( function( err ) {
mw.notify( err, { title: 'diffedit error', type: 'error' } );
} );
} );
} )();