Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//Dokumentation unter [[Benutzer:Schnark/js/diff]] <nowiki>

/*global mediaWiki, OO, ve*/
/*global MouseEvent*/
(function ($, mw) {
"use strict";

//colors:
//base, lighten30%, lighten40%
function makeRevSliderStyle (oldColors, newColors) {
	function fade (color, alpha) {
		return color.replace(/^#(..)(..)(..)$/, function (all, r, g, b) {
			return 'rgba(' + parseInt(r, 16) + ',' + parseInt(g, 16) + ',' + parseInt(b, 16) + ',' + alpha + ')';
		});
	}

	//jscs:disable maximumLineLength
	return '\n' +
		'.revslider-schnark .mw-revslider-pointer-line .mw-revslider-upper-color {' +
			'border-color: ' + newColors[0] + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer-line .mw-revslider-lower-color {' +
			'border-color: ' + oldColors[0] + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-revision-new .mw-revslider-revision-border-box {' +
			'border-bottom-color: ' + newColors[0] + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-revision-old .mw-revslider-revision-border-box {' +
			'border-bottom-color: ' + oldColors[0] + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-revision-hovered.mw-revslider-revision-wrapper-up .mw-revslider-pointer-ghost  {' +
			'background-color: ' + newColors[1] + ';' +
			'border-color: ' + newColors[0] + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-revision-hovered.mw-revslider-revision-wrapper-down .mw-revslider-pointer-ghost  {' +
			'background-color: ' + oldColors[1] + ';' +
			'border-color: ' + oldColors[0] + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-revision-wrapper-hovered .mw-revslider-revision-hovered.mw-revslider-revision-wrapper-up {' +
			'background-color: ' + fade(newColors[0], 0.3) + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-revision-wrapper-hovered .mw-revslider-revision-hovered.mw-revslider-revision-wrapper-down {' +
			'background-color: ' + fade(oldColors[0], 0.3) + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer.mw-revslider-pointer-newer {' +
			'border-color: ' + newColors[0] + ';' +
			'background-color: ' + newColors[0] + ';' +
			'background-image: linear-gradient(' + newColors[1] + ' 0, ' + newColors[0] + ' 100%);' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer.mw-revslider-pointer-newer:hover {' +
			'background-image: linear-gradient(' + newColors[2] + ' 0, ' + newColors[0] + ' 100%);' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer.mw-revslider-pointer-older {' +
			'border-color: ' + oldColors[0] + ';' +
			'background-color: ' + oldColors[0] + ';' +
			'background-image: linear-gradient(' + oldColors[1] + ' 0, ' + oldColors[0] + ' 100%);' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer.mw-revslider-pointer-older:hover {' +
			'background-image: linear-gradient(' + oldColors[2] + ' 0, ' + oldColors[0] + ' 100%);' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer-container-newer .mw-revslider-slider-line {' +
			'border-bottom-color: ' + fade(newColors[0], 0.5) + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer-container-newer:hover .mw-revslider-slider-line {' +
			'border-bottom-color: ' + fade(newColors[0], 0.8) + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer-container-older .mw-revslider-slider-line {' +
			'border-top-color: ' + fade(oldColors[0], 0.5) + ';' +
		'}' +
		'.revslider-schnark .mw-revslider-pointer-container-older:hover .mw-revslider-slider-line {' +
			'border-top-color: ' + fade(oldColors[0], 0.8) + ';' +
		'}';
	//jscs:enable maximumLineLength
}

var diffEngineDeferred,
	configPromise,
	cachedRev = {},
	origUri, mobileUri,
	currentMode,
	cachedInterfaceElements,
	optionsPrefix = 'userjs-schnark-diff-',
	version = '6.28',
	l10n = {
		en: {
			'schnark-diff-switch-wikitext': 'Classical',
			'schnark-diff-switch-schnark': 'Improved',
			'schnark-diff-switch-config': 'Configuration for improved diff',
			'schnark-diff-edit-diff-button': 'Δ',
			'schnark-diff-edit-diff-button-tooltip': 'Improved diff',
			'schnark-diff-config': 'Configuration for improved diff (v$1/$2)',
			'schnark-diff-config-color-scheme': 'Color scheme: ',
			'schnark-diff-config-color-scheme-classic': 'Classical (red/green)',
			'schnark-diff-config-color-scheme-modern': 'Modern (yellow/blue)',
			'schnark-diff-config-color-scheme-wiked': 'wikEd (yellow/blue, moves in gray)',
			'schnark-diff-config-color-scheme-ve': 'VisualDiff (light red/green)',
			'schnark-diff-config-moves': 'Show moved blocks: ',
			'schnark-diff-config-moves-normal': 'normal',
			'schnark-diff-config-moves-nested': 'nested',
			'schnark-diff-config-moves-none': 'no',
			'schnark-diff-config-moves-simple': 'simple',
			'schnark-diff-config-char-diff': 'Number of rounds for diff on character level: ',
			'schnark-diff-config-word-diff-qual': 'Quality of diffs on character level (0–100): ',
			'schnark-diff-config-recursion': 'Number of recursions: ',
			'schnark-diff-config-too-short': 'Length of a short word: ',
			'schnark-diff-config-small-region': 'Length of a small region: ',
			'schnark-diff-config-min-moved-length': 'Minimal length for a moved block: ',
			'schnark-diff-config-save': 'Save settings',
			'schnark-diff-config-reset': 'Use defaults'
		},
		'en-gb': {
			'schnark-diff-config-color-scheme': 'Colour scheme: ',
			'schnark-diff-config-color-scheme-wiked': 'wikEd (yellow/blue, moves in grey)'
		},
		de: {
			'schnark-diff-switch-wikitext': 'Klassisch',
			'schnark-diff-switch-schnark': 'Verbessert',
			'schnark-diff-switch-config': 'Konfiguration für verbesserten Diff',
			'schnark-diff-edit-diff-button-tooltip': 'Verbesserter Diff',
			'schnark-diff-config': 'Konfiguration für verbesserten Diff (v$1/$2)',
			'schnark-diff-config-color-scheme': 'Farbschema: ',
			'schnark-diff-config-color-scheme-classic': 'Klassisch (rot/grün)',
			'schnark-diff-config-color-scheme-modern': 'Modern (gelb/blau)',
			'schnark-diff-config-color-scheme-wiked': 'wikEd (gelb/blau, Verschiebungen grau)',
			'schnark-diff-config-color-scheme-ve': 'VisualDiff (hellrot/grün)',
			'schnark-diff-config-moves': 'Anzeige von Verschiebungen: ',
			'schnark-diff-config-moves-nested': 'verschachtelt',
			'schnark-diff-config-moves-none': 'gar nicht',
			'schnark-diff-config-moves-simple': 'einfach',
			'schnark-diff-config-char-diff': 'Anzahl der Durchgänge für Diff auf Zeichenebene: ',
			'schnark-diff-config-word-diff-qual': 'Qualität des Diffs auf Zeichenebene (0–100): ',
			'schnark-diff-config-recursion': 'Anzahl der Rekursionen: ',
			'schnark-diff-config-too-short': 'Länge eines kurzen Worts: ',
			'schnark-diff-config-small-region': 'Größe eines kleinen Bereichs: ',
			'schnark-diff-config-min-moved-length': 'Minimale Länge eines verschobenen Blocks: ',
			'schnark-diff-config-save': 'Einstellungen speichern',
			'schnark-diff-config-reset': 'Standardwerte verwenden'
		},
		'de-ch': {
			'schnark-diff-config-small-region': 'Grösse eines kleinen Bereichs: '
		}
	},
	colorSchemes = {
		classic: {
			additional: '.deletion-like, .insertion-like {' +
				'color: inherit; background-color: transparent; border-top: thick solid;' +
				'}' + '.deletion-like {' +
				'border-color: #903;' +
				'}' + '.insertion-like {' +
				'border-color: #093;' +
				'}' + makeRevSliderStyle(['#990033', '#ee0066', '#ff0055'], ['#009933', '#00ee66', '#00ff55'])
		},
		modern: {
			ins: 'text-decoration: none; color: black; background-color: #a3d3ff;',
			del: 'text-decoration: none; color: black; background-color: #ffe49c;',
			movedIns: 'text-decoration: none; color: black; background-color: #bdedff;',
			movedDel: 'text-decoration: none; color: black; background-color: #fffeb6;',
			blocks: [['#000', '#ffc0e0'], ['#000', '#a0ffa0'], ['#000', '#ffd0a0'], ['#000', '#ffa0a0'],
				['#000', '#a0a0ff'], ['#000', '#ffbbbb'], ['#000', '#c0ffa0'], ['#000', '#d8ffa0'], ['#000', '#b0a0d0']]
		},
		wiked: {
			ins: 'text-decoration: none; color: black; background-color: #a3d3ff;',
			del: 'text-decoration: none; color: black; background-color: #ffe49c;',
			movedIns: 'text-decoration: none; color: black; background-color: #bdedff;',
			movedDel: 'text-decoration: none; color: black; background-color: #fffeb6;',
			movedFrom: 'color: #000; text-decoration: none; font-weight: bold; background-color: #ffe49c;',
			movedTo: '#e8e8e8',
			movedHover: 'color: #fff !important; background-color: #777 !important;',
			blocks: [],
			additional: '.enhanced-diff-moved-hover .enhanced-diff-equal {color: #fff; background-color: #777;}'
		},
		ve: {
			ins: 'text-decoration: none; color: black; background-color: #7fd7c4;',
			del: 'text-decoration: none; color: black; background-color: #e88e89;',
			movedIns: 'text-decoration: none; color: black; background-color: #c8ccd1;',
			movedDel: 'text-decoration: line-through; color: black; background-color: #c8ccd1;',
			movedFrom: 'color: #000; text-decoration: none; font-weight: bold; background-color: #c8ccd1;',
			movedTo: '#b6d4fb',
			blocks: [],
			omit: 'color: #72777d;',
			additional: makeRevSliderStyle(['#d73c34', '#f0b7b4', '#f9e0de'], ['#39b79c', '#a6e3d6', '#cdefe8'])
		}
	}, currentColorScheme,
	hasOwn = Object.prototype.hasOwnProperty;

function initL10N (l10n) {
	var i, chain = mw.language.getFallbackLanguageChain();
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
}

function formatVersion (v) {
	if (typeof v === 'number' && Math.floor(v) === v) {
		return String(v) + '.0';
	} else {
		return String(v);
	}
}

function enableColorScheme (n, schnarkDiff) {
	var backup;
	if (n === currentColorScheme) {
		return;
	}
	if (currentColorScheme) {
		colorSchemes[currentColorScheme].disabled = true;
	}
	currentColorScheme = n;
	if ($.isPlainObject(colorSchemes[n])) {
		backup = schnarkDiff.style.get();
		schnarkDiff.style.set(colorSchemes[n]);
		colorSchemes[n] = mw.util.addCSS(schnarkDiff.getCSS());
		schnarkDiff.style.set(backup);
	} else {
		colorSchemes[n].disabled = false;
	}
}

function buildConfig (defaultConfig, currentConfig, libVersion) {
	var fieldset, colorScheme, moves, charDiff, wordDiffQual, recursion, tooShort,
		smallRegion, minMovedLength, buttonSave, buttonReset;

	function set (config) {
		colorScheme.getMenu().selectItemByData(config.colorScheme);
		moves.getMenu().selectItemByData(config.nested ? -1 : config.indicateMoves);
		charDiff.setValue(config.charDiff);
		wordDiffQual.setValue(Math.round(config.wordDiffQual * 100));
		recursion.setValue(config.recursion);
		tooShort.setValue(config.tooShort);
		smallRegion.setValue(config.smallRegion);
		minMovedLength.setValue(config.minMovedLength);
	}

	function get () {
		return {
			colorScheme: colorScheme.getMenu().findSelectedItem().getData(),
			indicateMoves: Math.abs(moves.getMenu().findSelectedItem().getData()),
			nested: moves.getMenu().findSelectedItem().getData() < 0,
			charDiff: Number(charDiff.getValue()),
			wordDiffQual: wordDiffQual.getValue() / 100,
			recursion: Number(recursion.getValue()),
			tooShort: Number(tooShort.getValue()),
			smallRegion: Number(smallRegion.getValue()),
			minMovedLength: Number(minMovedLength.getValue())
		};
	}

	colorScheme = new OO.ui.DropdownWidget({
		menu: {
			items: [
				new OO.ui.MenuOptionWidget({
					data: 'classic',
					label: mw.msg('schnark-diff-config-color-scheme-classic')
				}),
				new OO.ui.MenuOptionWidget({
					data: 'modern',
					label: mw.msg('schnark-diff-config-color-scheme-modern')
				}),
				new OO.ui.MenuOptionWidget({
					data: 'wiked',
					label: mw.msg('schnark-diff-config-color-scheme-wiked')
				}),
				new OO.ui.MenuOptionWidget({
					data: 've',
					label: mw.msg('schnark-diff-config-color-scheme-ve')
				})
			]
		},
		$overlay: true
	});
	moves = new OO.ui.DropdownWidget({
		menu: {
			items: [
				new OO.ui.MenuOptionWidget({
					data: 1,
					label: mw.msg('schnark-diff-config-moves-normal')
				}),
				new OO.ui.MenuOptionWidget({
					data: -1,
					label: mw.msg('schnark-diff-config-moves-nested')
				}),
				new OO.ui.MenuOptionWidget({
					data: 0,
					label: mw.msg('schnark-diff-config-moves-none')
				}),
				new OO.ui.MenuOptionWidget({
					data: 2,
					label: mw.msg('schnark-diff-config-moves-simple')
				})
			]
		},
		$overlay: true
	});
	charDiff = new OO.ui.NumberInputWidget({
		min: 0,
		allowInteger: true
	});
	wordDiffQual = new OO.ui.NumberInputWidget({
		min: 0,
		max: 100
	});
	recursion = new OO.ui.NumberInputWidget({
		min: 0,
		allowInteger: true
	});
	tooShort = new OO.ui.NumberInputWidget({
		min: 0,
		allowInteger: true
	});
	smallRegion = new OO.ui.NumberInputWidget({
		min: 0,
		allowInteger: true
	});
	minMovedLength = new OO.ui.NumberInputWidget({
		min: 0,
		allowInteger: true
	});
	buttonSave = new OO.ui.ButtonWidget({
		label: mw.msg('schnark-diff-config-save')
	});
	buttonReset = new OO.ui.ButtonWidget({
		label: mw.msg('schnark-diff-config-reset')
	});

	buttonSave.on('click', saveConfig);
	buttonReset.on('click', function () {
		set(defaultConfig);
	});

	fieldset = new OO.ui.FieldsetLayout({
		label: mw.msg('schnark-diff-config', formatVersion(version), formatVersion(libVersion))
	});
	fieldset.addItems([
		new OO.ui.FieldLayout(colorScheme, {
			label: mw.msg('schnark-diff-config-color-scheme')
		}),
		new OO.ui.FieldLayout(moves, {
			label: mw.msg('schnark-diff-config-moves')
		}),
		new OO.ui.FieldLayout(charDiff, {
			label: mw.msg('schnark-diff-config-char-diff')
		}),
		new OO.ui.FieldLayout(wordDiffQual, {
			label: mw.msg('schnark-diff-config-word-diff-qual')
		}),
		new OO.ui.FieldLayout(recursion, {
			label: mw.msg('schnark-diff-config-recursion')
		}),
		new OO.ui.FieldLayout(tooShort, {
			label: mw.msg('schnark-diff-config-too-short')
		}),
		new OO.ui.FieldLayout(smallRegion, {
			label: mw.msg('schnark-diff-config-small-region')
		}),
		new OO.ui.FieldLayout(minMovedLength, {
			label: mw.msg('schnark-diff-config-min-moved-length')
		}),
		new OO.ui.FieldLayout(new OO.ui.Widget({
			content: [
				new OO.ui.HorizontalLayout({
					items: [
						buttonSave,
						buttonReset
					]
				})
			]
		}), {
			align: 'top',
			label: null
		})
	]);

	set(currentConfig);

	return {
		$div: $('<div>').append(fieldset.$element),
		get: get,
		defaultConfig: defaultConfig,
		libVersion: libVersion
	};
}

function applyLocalConfig (defaultConfig) {
	var key, val, config = {};
	for (key in defaultConfig) {
		if (hasOwn.call(defaultConfig, key)) {
			val = mw.user.options.get(optionsPrefix + key);
			if (val === null) {
				val = defaultConfig[key];
			} else {
				switch (typeof defaultConfig[key]) {
				case 'number':
					val = Number(val);
					if (isNaN(val)) {
						val = defaultConfig[key];
					}
					break;
				case 'boolean':
					val = val === 'true';
					break;
				}
			}
			config[key] = val;
		}
	}
	return config;
}

function getDiffEngine () {
	var load = true;
	function callback (schnarkDiff) {
		load = false;
		mw.hook('userjs.load-script.diff-core').remove(callback);
		schnarkDiff.config.set(
			$('body').is('.rtl') ? {
				movedLeft: '\u25b6',
				movedRight: '\u25c0'
			} : {
				movedLeft: '\u25c0',
				movedRight: '\u25b6'
			}
		);
		diffEngineDeferred.resolve(schnarkDiff);
	}
	if (!diffEngineDeferred) {
		diffEngineDeferred = $.Deferred();
		mw.hook('userjs.load-script.diff-core').add(callback);
		if (load) {
			//</nowiki>[[Benutzer:Schnark/js/diff.js/core.js]]<nowiki>
			mw.loader.load('https://de.wikipedia.org/w/index.php?title=Benutzer:Schnark/js/diff.js/core.js' +
				'&action=raw&ctype=text/javascript');
		}
	}
	return diffEngineDeferred.promise();
}

function getConfigPromise (forceNew) {
	if (!configPromise) {
		configPromise = getDiffEngine().then(function (schnarkDiff) {
			var defaultConfig = schnarkDiff.config.get(['charDiff', 'wordDiffQual', 'recursion',
				'tooShort', 'smallRegion', 'minMovedLength']);
			defaultConfig.indicateMoves = 1;
			defaultConfig.nested = true;
			defaultConfig.colorScheme = 'classic';
			return buildConfig(defaultConfig, applyLocalConfig(defaultConfig), schnarkDiff.version);
		});
	} else if (forceNew) {
		configPromise = configPromise.then(function (config) {
			return buildConfig(config.defaultConfig, config.get(), config.libVersion);
		});
	}
	return configPromise;
}

function getDefaultConfig () {
	return getConfigPromise().then(function (data) {
		return data.defaultConfig;
	});
}

function getConfig () {
	return getConfigPromise().then(function (data) {
		return data.get();
	});
}

function getConfigPanel (forceNew) {
	return getConfigPromise(forceNew).then(function (data) {
		return data.$div;
	});
}

function getRevision (id, section) {
	var param;
	switch (id) {
	case 'edit':
	case 'conflict':
		return mw.loader.using('jquery.textSelection').then( //strange jscs issue
			function () {
				return $(id === 'edit' ? '#wpTextbox1' : '#wpTextbox2').textSelection('getContents');
			}
		);
	case 've':
		return ve.init.target.serialize(
			ve.init.target.getSurface().getDom()
		).then(
			function (data) {
				return data.content;
			}
		);
	case 0:
		return $.Deferred().resolve('').promise();
	default:
		if (section === 'new') {
			return $.Deferred().resolve('').promise();
		}
		if (!cachedRev[id]) {
			param = {
				action: 'query',
				prop: 'revisions',
				revids: id,
				rvprop: 'content',
				rvslots: '*',
				format: 'json',
				formatversion: 2
			};
			if (section) {
				param.rvsection = section;
			}
			cachedRev[id] = $.getJSON(mw.util.wikiScript('api'), param).then(function (json) {
				try {
					return json.query.pages[0].revisions[0].slots ||
						json.query.pages[0].revisions[0].content;
				} catch (e) {
					return '';
				}
			}, function () {
				return '';
			});
		}
		return cachedRev[id];
	}
}

function generateDiff (schnarkDiff, oldText, newText, nested) {
	return schnarkDiff.htmlDiff(
		(oldText || '').replace(/\s+$/, ''),
		(newText || '').replace(/\s+$/, ''),
		nested
	);
}

function generateDiffs (schnarkDiff, oldContent, newContent, nested) {
	var oldStr = typeof oldContent === 'string',
		newStr = typeof newContent === 'string',
		diffs = [];
	if (oldStr || newStr) {
		if (!oldStr) {
			oldContent = oldContent.main.content;
		}
		if (!newStr) {
			newContent = newContent.main.content;
		}
		return generateDiff(schnarkDiff, oldContent, newContent, nested);
	}
	diffs.push(generateDiff(schnarkDiff, oldContent.main.content, newContent.main.content, nested));
	$.each(newContent, function (name, slot) {
		var oldText;
		if (name !== 'main') {
			oldText = oldContent[name];
			oldText = oldText ? oldText.content : '';
			if (oldText !== slot.content) {
				diffs.push(
					mw.html.element('h3', {}, name) +
					generateDiff(schnarkDiff, oldText, slot.content, nested)
				);
			}
		}
	});
	$.each(oldContent, function (name, slot) {
		if (name !== 'main' && !newContent[name]) {
			diffs.push(
				mw.html.element('h3', {}, name) +
				generateDiff(schnarkDiff, slot.content, '', nested)
			);
		}
	});
	if (diffs.length > 1 && oldContent.main.content === newContent.main.content) {
		diffs[0] = '';
	}
	return diffs.join('');
}

function getDiff (data) {
	return $.when(
		getDiffEngine(),
		getRevision(data.oldId, data.section),
		getRevision(data.newId, data.section),
		getConfig()
	).then(function (results) {
		if (!Array.isArray(results)) { //change to the Promise.all way
			results = arguments;
		}
		var schnarkDiff = results[0],
			oldContent = results[1],
			newContent = results[2],
			config = results[3],
			defaultConfig = schnarkDiff.config.get(),
			nested = config.nested,
			diffHtml, $div;
		enableColorScheme(config.colorScheme, schnarkDiff);
		delete config.nested;
		delete config.colorScheme;
		schnarkDiff.config.set(config);
		diffHtml = generateDiffs(schnarkDiff, oldContent, newContent, nested);
		$div = $('<div>').addClass('schnark-diff').html(diffHtml);
		schnarkDiff.addEvents($div);
		schnarkDiff.config.set(defaultConfig);
		return $div;
	});
}

function saveConfig () {
	return $.when(
		getDefaultConfig(),
		getConfig()
	).then(function (data) {
		if (!Array.isArray(data)) { //change to the Promise.all way
			data = arguments;
		}
		var defaultConfig = data[0], currentConfig = applyLocalConfig(defaultConfig), config = data[1], options = {}, key;
		for (key in config) {
			if (hasOwn.call(config, key)) {
				if (config[key] === currentConfig[key]) {
					continue;
				}
				if (config[key] === defaultConfig[key]) {
					options[optionsPrefix + key] = null;
				} else {
					options[optionsPrefix + key] = String(config[key]);
				}
			}
		}
		mw.user.options.set(options);
		return mw.loader.using('mediawiki.api').then(function () {
			return (new mw.Api()).saveOptions(options);
		});
	});
}

function loadModules (diffPage) {
	var deps = ['mediawiki.Uri', 'oojs-ui-core', 'oojs-ui-widgets',
		'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-editing-advanced'];
	if (diffPage) {
		deps.push('ext.visualEditor.diffPage.init');
	}
	return mw.loader.using(deps);
}

function createVisualDiffButton () {
	return new OO.ui.ButtonOptionWidget({data: 'visual', icon: 'eye', label: mw.msg('visualeditor-savedialog-review-visual')});
}

function createTraditionalDiffButton () {
	return new OO.ui.ButtonOptionWidget({data: 'source', icon: 'wikiText', label: mw.msg('schnark-diff-switch-wikitext')});
}

function createSchnarkDiffButton () {
	return new OO.ui.ButtonOptionWidget({data: 'schnark', icon: 'markup', label: mw.msg('schnark-diff-switch-schnark')});
}

function createSchnarkDiffConfigButton () {
	return new OO.ui.ButtonOptionWidget({data: 'config', icon: 'settings', title: mw.msg('schnark-diff-switch-config')});
}

function createSwitcher (wikitext, visual) {
	var buttons = [];
	if (visual) {
		buttons.push(createVisualDiffButton());
	}
	buttons.push(createTraditionalDiffButton());
	if (!wikitext) {
		buttons[buttons.length - 1].setDisabled(true);
	}
	buttons.push(createSchnarkDiffButton());
	buttons.push(createSchnarkDiffConfigButton());
	return new OO.ui.ButtonSelectWidget({
		items: buttons,
		classes: ['schnark-diff-review-button-select']
	});
}

function makeEditDiffButton (showNew) {
	var diffButton = OO.ui.infuse($('#wpDiffWidget')),
		enhancedDiffButton = new OO.ui.ButtonWidget({
			label: mw.msg('schnark-diff-edit-diff-button'),
			title: mw.msg('schnark-diff-edit-diff-button-tooltip')
		}),
		group = new OO.ui.ButtonGroupWidget();
	enhancedDiffButton.on('click', showNew);
	enhancedDiffButton.$element.css('margin-top', '0.5em');
	diffButton.$element.after(group.$element);
	group.addItems([diffButton, enhancedDiffButton]);
}

function makeMouseupEvent () {
	var evt;
	try {
		return new MouseEvent('mouseup');
	} catch (e) {
		//old browser
		evt = document.createEvent('MouseEvents');
		evt.initMouseEvent('mouseup', true, true, window,
			0, 0, 0, 0, 0, false, false, false, false, 0, null);
		return evt;
	}
}

function getInterfaceElements () {
	if (!cachedInterfaceElements) {
		cachedInterfaceElements = {};
		cachedInterfaceElements.$schnarkDiffContainer = $('<div>');
		cachedInterfaceElements.$schnarkDiff = $('<div>');
		cachedInterfaceElements.schnarkProgress = new OO.ui.ProgressBarWidget();
		cachedInterfaceElements.$schnarkDiffContainer.append(
			cachedInterfaceElements.schnarkProgress.$element.css({ //.ve-init-mw-diffPage-loading
				clear: 'both',
				margin: '2em auto'
			}).addClass('oo-ui-element-hidden'),
			cachedInterfaceElements.$schnarkDiff
		);
		cachedInterfaceElements.$configContainer = $('<div>');
		cachedInterfaceElements.$config = $('<div>');
		cachedInterfaceElements.configProgress = new OO.ui.ProgressBarWidget();
		cachedInterfaceElements.$configContainer.append(
			cachedInterfaceElements.configProgress.$element.css({ //.ve-init-mw-diffPage-loading
				clear: 'both',
				margin: '2em auto'
			}).addClass('oo-ui-element-hidden'),
			cachedInterfaceElements.$config
		);
	}
	return cachedInterfaceElements;
}

function createInterface ($diff, isEdit) {
	var oldId, newId, section,
		reviewModeButtonSelect, $buttonSelectContainer,
		$revSlider = $('.mw-revslider-container'),
		$diffHeader, $diffBody,
		interfaceElements,
		hasVisual, hasDiff, $switcherContainer,
		uri = new mw.Uri();
	hasDiff = !!$diff;
	if (isEdit) {
		oldId = $('#wpTextbox2').length === 1 ? 'conflict' : mw.config.get('wgCurRevisionId');
		newId = 'edit';
		section = $('input[name="wpSection"]').val();
		hasVisual = false;
	} else {
		if (
			$diff.parent().find(
				'#mw-rev-suppressed-no-diff, #mw-rev-deleted-no-diff,' +
				'#mw-rev-suppressed-unhide-diff, #mw-rev-deleted-unhide-diff'
			).length
		) { //no real diff shown
			return;
		}
		oldId = mw.config.get('wgDiffOldId');
		newId = mw.config.get('wgDiffNewId');
		$switcherContainer = $('.ve-init-mw-diffPage-diffMode').hide();
		hasVisual = !!$switcherContainer.length;
	}

	interfaceElements = getInterfaceElements();
	$('.schnark-diff-review-button-select').remove();

	reviewModeButtonSelect = createSwitcher(hasDiff, hasVisual);
	$buttonSelectContainer = $('<div>').css({ //.ve-init-mw-diffPage-diffMode
		textAlign: $('body').is('.rtl') ? 'left' : 'right',
		margin: '1em 0'
	}).append(reviewModeButtonSelect.$element);

	if (hasDiff) {
		$diffHeader = $diff
			.find('tr.diff-title').add(
				$diff.find('td.diff-multi, td.diff-notice').parent()
			);
		$diffBody = $diff.find('tr').not($diffHeader);
		$diff.before($buttonSelectContainer);
		$diff.after(interfaceElements.$schnarkDiffContainer, interfaceElements.$configContainer);
	} else {
		$('#editform').before($buttonSelectContainer,
			interfaceElements.$schnarkDiffContainer, interfaceElements.$configContainer);
	}

	function selectVisual () {
		$switcherContainer.find('[role="button"]').eq(0)
			.trigger($.Event('mousedown', {which: 1}))[0].dispatchEvent(makeMouseupEvent());
	}
	function unselectVisual () {
		$switcherContainer.find('[role="button"]').eq(1)
			.trigger($.Event('mousedown', {which: 1}))[0].dispatchEvent(makeMouseupEvent());
	}
	function hiddenSwitcherIsVisual () {
		return $switcherContainer.find('.oo-ui-optionWidget-selected').index() === 0;
	}

	function storeDiffMode () {
		mw.user.options.set(optionsPrefix + 'diffmode', currentMode);
		mw.loader.using('mediawiki.api').then(function () {
			new mw.Api().saveOption(optionsPrefix + 'diffmode', currentMode);
		});
		if (!isEdit && history.replaceState) {
			uri.query.diffmode = currentMode;
			history.replaceState('', document.title, uri);
		}
	}

	reviewModeButtonSelect.on(isEdit ? 'choose' : 'select', function (item) {
	//choose is fired even if nothing is changed,
	//this is what we want to refresh our diff
		currentMode = item.getData();
		switch (currentMode) {
		case 'schnark':
			if (hasVisual && hiddenSwitcherIsVisual()) {
				unselectVisual();
			}
			if (hasDiff) {
				$diffBody.addClass('oo-ui-element-hidden');
				$diffHeader.find('#mw-diff-otitle1').addClass('deletion-like');
				$diffHeader.find('#mw-diff-ntitle1').addClass('insertion-like');
				$diffHeader.removeClass('oo-ui-element-hidden');
			}
			interfaceElements.$schnarkDiffContainer.removeClass('oo-ui-element-hidden');
			interfaceElements.$configContainer.addClass('oo-ui-element-hidden');
			$revSlider.addClass('revslider-schnark');

			if (interfaceElements.$schnarkDiff.data('diff-ids') !== oldId + '|' + newId) {
				interfaceElements.$schnarkDiff.empty();
				interfaceElements.schnarkProgress.$element.removeClass('oo-ui-element-hidden');
				getDiff({
					oldId: oldId,
					newId: newId,
					section: section
				}).then(function ($d) {
					mw.hook('userjs.schnark-diff').fire($d);
					interfaceElements.schnarkProgress.$element.addClass('oo-ui-element-hidden');
					interfaceElements.$schnarkDiff.append($d);
					interfaceElements.$schnarkDiff.data('diff-ids', oldId + '|' + newId);
				});
			} else {
				//Even if ids didn't change, the diff might, since
				//a) it depends on the current config, and
				//b) for edits the text might have changed, but
				//in both cases the new diff will be generated fast,
				//so don't show the progressbar, just replace the diff.
				getDiff({
					oldId: oldId,
					newId: newId,
					section: section
				}).then(function ($d) {
					mw.hook('userjs.schnark-diff').fire($d);
					interfaceElements.$schnarkDiff.empty().append($d);
				});
			}
			storeDiffMode();
			break;
		case 'config':
			if (hasVisual && hiddenSwitcherIsVisual()) {
				unselectVisual();
			}
			if (hasDiff) {
				$diffBody.addClass('oo-ui-element-hidden');
				$diffHeader.addClass('oo-ui-element-hidden');
			}
			interfaceElements.$schnarkDiffContainer.addClass('oo-ui-element-hidden');
			interfaceElements.$configContainer.removeClass('oo-ui-element-hidden');

			if (!interfaceElements.$config.hasClass('is-ready')) {
				interfaceElements.$config.empty();
				interfaceElements.configProgress.$element.removeClass('oo-ui-element-hidden');
				getConfigPanel(true).then(function ($p) { //we need a new copy every time, see below
					interfaceElements.configProgress.$element.addClass('oo-ui-element-hidden');
					interfaceElements.$config.addClass('is-ready').append($p);
				});
			} else {
				//Even if we built the config before, it could have been removed and re-attached.
				//In this case, jQuery also removes event-handlers, so we have to replace the now
				//unfunctional config panel with a fresh one. This will happen fast, so do it
				//without progressbar.
				getConfigPanel(true).then(function ($p) {
					interfaceElements.$config.empty().append($p);
				});
			}
			break;
		default:
			if (currentMode === 'source') {
				$diffBody.removeClass('oo-ui-element-hidden');
			}
			$diffHeader.find('#mw-diff-otitle1').removeClass('deletion-like');
			$diffHeader.find('#mw-diff-ntitle1').removeClass('insertion-like');
			$diffHeader.removeClass('oo-ui-element-hidden');
			interfaceElements.$schnarkDiffContainer.addClass('oo-ui-element-hidden');
			interfaceElements.$configContainer.addClass('oo-ui-element-hidden');
			$revSlider.removeClass('revslider-schnark');
			if (hasVisual && hiddenSwitcherIsVisual() && currentMode === 'source') {
				unselectVisual();
			} else if (currentMode === 'visual') {
				selectVisual();
			}
			storeDiffMode();
		}
	});

	if (!currentMode) {
		if (!hasDiff) {
			currentMode = 'schnark';
		} else {
			currentMode =
				(mobileUri || origUri || uri).query.diffmode ||
				mw.user.options.get(optionsPrefix + 'diffmode') ||
				'source';
			if (currentMode === 'visual' && !hasVisual) {
				currentMode = 'source';
			}
		}
	}
	if (isEdit) {
		reviewModeButtonSelect.chooseItem(reviewModeButtonSelect.findItemFromData(currentMode));
	} else {
		reviewModeButtonSelect.selectItemByData(currentMode);
	}

	return reviewModeButtonSelect;
}

function initVe () {
	function SaveDialogWithDiff () {
		SaveDialogWithDiff.parent.apply(this, arguments);
	}

	//I have several scripts that extend the SaveDialog, so let's get the current one
	OO.inheritClass(SaveDialogWithDiff, ve.ui.windowFactory.lookup('mwSave'));

	SaveDialogWithDiff.prototype.setDiffAndReview = function () {
		SaveDialogWithDiff.parent.prototype.setDiffAndReview.apply(this, arguments);
		this.$reviewSchnarkDiff.append(new OO.ui.ProgressBarWidget().$element);
		this.$reviewDiffConfig.append(new OO.ui.ProgressBarWidget().$element);
	};

	SaveDialogWithDiff.prototype.clearDiff = function () {
		SaveDialogWithDiff.parent.prototype.clearDiff.apply(this, arguments);
		this.$reviewSchnarkDiff.empty();
		this.$reviewDiffConfig.children().detach();
	};

	SaveDialogWithDiff.prototype.initialize = function () {
		SaveDialogWithDiff.parent.prototype.initialize.apply(this, arguments);
		this.reviewModeButtonSelect.addItems([
			createSchnarkDiffButton(),
			createSchnarkDiffConfigButton()
		]);
		this.reviewModeButtonSelect.findItemFromData('source').setLabel(mw.msg('schnark-diff-switch-wikitext'));
		this.$reviewVisualDiff.addClass('oo-ui-element-hidden'); //hiding is delayed, so sometimes 2 loading bars are visible,
		this.$reviewWikitextDiff.addClass('oo-ui-element-hidden'); //but for some reason this doesn't happen without my script
		this.$reviewSchnarkDiff = $('<div>').addClass('ve-ui-mwSaveDialog-viewer oo-ui-element-hidden');
		this.$reviewDiffConfig = $('<div>').addClass('ve-ui-mwSaveDialog-viewer oo-ui-element-hidden');
		this.reviewPanel.$element.find('.ve-ui-mwSaveDialog-actions').before(this.$reviewSchnarkDiff, this.$reviewDiffConfig);
	};

	SaveDialogWithDiff.prototype.updateReviewMode = function () {
		var mode = this.reviewModeButtonSelect.findSelectedItem().getData(), surfaceMode;
		if (!this.hasDiff) {
			return;
		}
		this.$reviewSchnarkDiff.toggleClass('oo-ui-element-hidden', mode !== 'schnark');
		this.$reviewDiffConfig.toggleClass('oo-ui-element-hidden', mode !== 'config');
		if (mode === 'schnark' || mode === 'config') {
			this.$reviewVisualDiff.toggleClass('oo-ui-element-hidden', true);
			this.$reviewWikitextDiff.toggleClass('oo-ui-element-hidden', true);
			if (mode === 'schnark') {
				surfaceMode = ve.init.target.getSurface().getMode();
				ve.userConfig('visualeditor-diffmode-' + surfaceMode, 'schnark');
				getDiff({
					oldId: mw.config.get('wgCurRevisionId'),
					newId: 've',
					section: surfaceMode === 'source' && ve.init.target.section
				}).then(function ($div) {
					mw.hook('userjs.schnark-diff').fire($div);
					this.$reviewSchnarkDiff.empty().append($div);
					this.updateSize();
				}.bind(this));
			} else {
				getConfigPanel().then(function ($div) {
					this.$reviewDiffConfig.children().replaceWith($div);
					this.updateSize();
				}.bind(this));
			}
			try {
				if (!this.report) {
					this.report = this.getActions().get({actions: 'report'})[0];
				}
				this.report.toggle(false);
			} catch (e) {
				mw.log.warn('Code for "report" broke, probably it can be removed.');
			}
			this.updateSize();
		} else {
			SaveDialogWithDiff.parent.prototype.updateReviewMode.apply(this, arguments);
		}
	};

	ve.ui.windowFactory.register(SaveDialogWithDiff);
}

function initOwe () {
	var switcher;
	makeEditDiffButton(function () {
		if (switcher) {
			switcher.chooseItem(switcher.findItemFromData('schnark'));
		} else {
			switcher = createInterface(false, true);
		}
		switcher.scrollElementIntoView();
	});
	mw.hook('wikipage.diff').add(function ($diff) {
		switcher = createInterface($diff, true);
	});
}

function initDiff () {
	mw.loader.using('mediawiki.Uri').then(function () {
		var $mobileLink = $('#footer-places-mobileview a, #mobileview a');
		//try to get Uri before VisualDiff changes it
		origUri = new mw.Uri();
		if ($mobileLink.length && $mobileLink.prop('href')) {
			//dirty hack, but the mobile link has the parameters we want
			//and doesn't change
			mobileUri = new mw.Uri($mobileLink.prop('href'));
		}
	});
	mw.hook('wikipage.diff').add(function ($diff) {
		loadModules(!!$('.ve-init-mw-diffPage-diffMode').length).then(function () {
			createInterface($diff);
		});
	});
}

function init () {
	function initVeOnce () {
		mw.hook('ve.activationComplete').remove(initVeOnce);
		initVe();
	}

	initL10N(l10n);
	mw.hook('ve.activationComplete').add(initVeOnce);
	if (['edit', 'submit'].indexOf(mw.config.get('wgAction')) > -1) {
		loadModules().then(initOwe);
	} else {
		initDiff();
	}
}

$.when(mw.loader.using(['mediawiki.util', 'mediawiki.language', 'user.options']), $.ready).then(init);

})(jQuery, mediaWiki);
//</nowiki>