User:Jon Harald Søby/diffedit.js

From Meta, a Wikimedia project coordination wiki

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*!
 * diffedit.js – script that lets you edit pages directly from the diff view
 *
 * @author Jon Harald Søby
 * @version 1.2.0 (2023-10-13)
 * @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: 'পাতাটির নতুনতর সংশোধনের পার্থক্য লোড করতে ক্লিক করুন'
			},
			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'
			},
			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 gold', 'transition': 'outline 500ms cubic-bezier(.5,2,.5,-1)' } );
			} else {
				$( 'input[name=wpSummary] ').css( { 'outline': '5px solid 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 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' } );
		} );
	} );
} )();