Benutzer:Schnark/js/diff.js

//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>

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.