Benutzer:Schnark/js/watchlist++.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

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/watchlist++]] <nowiki>
/*global mediaWiki, OO*/
/*global CSS*/
(function ($, mw) {
"use strict";

var l10n, FLAGS,
	hasOwn = Object.prototype.hasOwnProperty;

//jscs:disable maximumLineLength
l10n = {
	en: {
		'colon-separator': ': ',
		'comma-separator': ', ',
		'rc-change-size-new': '$1 {{PLURAL:$1|byte|bytes}} after change',
		'pipe-separator': ' | ',
		'parentheses': '($1)',
		'pagetitle': '$1 - {{SITENAME}}',

		'watchlist++': 'Watchlist++',
		'watchlist++-mode-classical': 'Classical',
		'watchlist++-mode-watchlist++': 'Watchlist++',
		'watchlist++-flags-new-letter': 'N',
		'watchlist++-flags-minor-letter': 'M',
		'watchlist++-flags-bot-letter': 'B',
		'watchlist++-flags-anon-letter': 'A',
		'watchlist++-flags-log-letter': 'L',
		'watchlist++-flags-wikidata-letter': 'D',
		'watchlist++-flags-category-letter': 'K',
		'watchlist++-tags-letter': 'T',
		'watchlist++-flags-new-title': 'new page',
		'watchlist++-flags-minor-title': 'minor edit',
		'watchlist++-flags-bot-title': 'bot edit',
		'watchlist++-flags-anon-title': 'IP edit',
		'watchlist++-flags-log-title': 'log action',
		'watchlist++-flags-wikidata-title': 'Wikidata',
		'watchlist++-flags-category-title': 'category change',
		'watchlist++-tags-title': '{{PLURAL:$1|tag|tags}}: $2',
		'watchlist++-number-editors': 'by $1 users',
		'watchlist++-by': 'by $1',
		'watchlist++-changes': '$1 {{PLURAL:$1|change|changes}}',
		'watchlist++-change': 'change',
		'watchlist++-change-block': 'block',
		'watchlist++-change-block-unblock': 'unblock',
		'watchlist++-change-delete': 'deletion',
		'watchlist++-change-delete-restore': 'undeletion',
		'watchlist++-change-import': 'import',
		'watchlist++-change-massmessage': 'mass message',
		'watchlist++-change-move': 'move',
		'watchlist++-change-newusers': 'signup',
		'watchlist++-change-protect': 'protection',
		'watchlist++-change-protect-unprotect': 'unprotection',
		'watchlist++-change-renameuser': 'rename',
		'watchlist++-change-rights': 'user right change',
		'watchlist++-change-supress': 'supress',
		'watchlist++-change-upload': 'upload',
		'watchlist++-log-comment': '$1: $2',
		'watchlist++-log-protect': '($1)',
		'watchlist++-log-delete-revision': 'deleted revision',
		'watchlist++-log-rights-add': '+ $1',
		'watchlist++-log-rights-remove': '− $1',
		'watchlist++-log-upload-overwrite': 'reupload',
		'watchlist++-changes-read': '(+$1)',
		'watchlist++-mark-symbol': '\u279C',
		'watchlist++-read-symbol': '\u2713',
		'watchlist++-unread-symbol': '×',
		'watchlist++-read-until': 'mark as read up to this change',
		'watchlist++-read-all': 'mark as read',
		'watchlist++-unread-until': 'mark as unread from this change on',
		'watchlist++-unread-all': 'mark as unread',
		'watchlist++-rules-head': '{{PLURAL:$1|One rule|$1 rules}}',
		'watchlist++-rules-head-expand': 'Show rules',
		'watchlist++-rules-head-collapse': 'Hide rules',
		'watchlist++-rules-add': 'new rule',
		'watchlist++-rules-export-import': 'export/import',
		'watchlist++-rules-delete-all': 'restore default rules',
		'watchlist++-rules-bookmarklet': 'backup for Watchlist++',
		'watchlist++-rules-bookmarklet-title': 'Install this link as bookmarklet to get a backup of your current rules',
		'watchlist++-rules-delete-all-confirm': 'Are you sure you want to delete all rules and reset the default rules instead?',
		'watchlist++-rules-delete': 'delete',
		'watchlist++-rules-delete-confirm': 'Are you sure you want to delete the rule "$1"?',
		'watchlist++-rules-edit': 'edit',
		'watchlist++-export-import-rules': 'These are the current rules encoded as JSON. You can edit them as you like. Please note that changes will be applied only after reloading the page.',
		'watchlist++-json-error': 'Your JSON is not valid: $1',
		'watchlist++-storage-error': 'A storage error occured, changes to the rules could not be saved.',
		'watchlist++-filter': '$1 $2 <code>$3</code>',
		'watchlist++-filter-ns': 'The namespace',
		'watchlist++-filter-title': 'The title',
		'watchlist++-filter-user': 'The user',
		'watchlist++-filter-diff': 'The size change',
		'watchlist++-filter-flags': 'The value for the flags',
		'watchlist++-filter-timestamp': 'The timestamp',
		'watchlist++-filter-comment': 'The edit comment',
		'watchlist++-filter-logtype': 'The log action',
		'watchlist++-filter-tags': 'The change tag',
		'watchlist++-type-is': 'is',
		'watchlist++-type-is-not': 'is not',
		'watchlist++-type-match': 'matches',
		'watchlist++-type-match-not': 'doesn\'t match',
		'watchlist++-type-and': 'contains at least the bits of',
		'watchlist++-type-and-not': 'doesn\'t contain all the bits of',
		'watchlist++-type-or': 'contains at most the bits of',
		'watchlist++-type-or-not': 'dosn\'t contain only the bits of',
		'watchlist++-type-gt': 'is greater than',
		'watchlist++-type-gt-not': 'is less than or equal to',
		'watchlist++-type-ge': 'is greater than or equal to',
		'watchlist++-type-ge-not': 'is less than',
		'watchlist++-rule-read-mark': 'Mark all changes as $1 and with tag no. $2, for which all these rules are met:',
		'watchlist++-rule-read': 'Mark all changes as $1, for which all these rules are met:',
		'watchlist++-rule-mark': 'Mark all with tag no. $1, for which all these rules are met:',
		'watchlist++-rules-read': 'read',
		'watchlist++-rules-unread': 'unread',
		'watchlist++-rules-desc': '$1 ($2)',
		'watchlist++-hhmm': '$1:$2',
		'watchlist++-ddmm': '$1.&nbsp;$2.',
		'watchlist++-toggle-read': 'Toggle read changes',
		'watchlist++-add-older': 'Load older changes',
		'watchlist++-add-older-title': '',
		'watchlist++-add-newer': 'Load newer changes',
		'watchlist++-add-newer-title': '',
		'watchlist++-reset-notification': 'Mark all changes as read',
		'watchlist++-rules-desc-mark-own-pages': 'Highlight changes to own pages by other users',
		'watchlist++-rules-desc-hide-bots': 'Mark bot changes as read',
		'watchlist++-new-rule-title': 'Create new rule',
		'watchlist++-new-rule-text': 'Create a new rule by selecting how it should mark changes and which criterions must be met to apply it.',
		'watchlist++-edit-rule-title': 'Edit rule',
		'watchlist++-edit-rule-text': 'Edit the rule by selecting how it should mark changes and which criterions must be met to apply it.',
		'watchlist++-new-rule-desc': 'Description: ',
		'watchlist++-new-rule-mark': '(tag)',
		'watchlist++-new-rule-read': '(read/unread)',
		'watchlist++-new-rule-type': '(new criterion)',
		'watchlist++-rule-mark-1': 'red',
		'watchlist++-rule-mark-2': 'orange',
		'watchlist++-rule-mark-3': 'yellow',
		'watchlist++-rule-mark-4': 'green',
		'watchlist++-rule-mark-5': 'blue',
		'watchlist++-rule-mark-6': 'pink',
		'watchlist++-rule-mark-7': 'violet',
		'watchlist++-new-rule-help': 'Help',
		'watchlist++-new-rule-save': 'Save',
		'watchlist++-new-rule-cancel': 'Cancel',
		'watchlist++-new-rule-help-done': 'Back',
		'watchlist++-new-rule-error-no-action': 'Missing action for rule!',
		'watchlist++-new-rule-error-no-filter': 'Missing filters for rule!',
		'watchlist++-new-rule-error-no-number': 'Missing number for bitwise comparison!',
		'watchlist++-new-rule-error-no-re': 'Invalid regular expression!',
		'watchlist++-new-rule-help-text': '<ul><li>Namespace: numerical value ($1)</li><li>Title: page title (without namespace)</li><li>User: name of user</li><li>Size change: number, can be negative</li><li>Flags: numerical value from these constants: $2</li><li>Timestamp: in format yyyy-mm-ddThh:mm:ssZ</li><li>Edit comment: as source code</li><li>Log action: internal name</li><li>Change tag: internal names, separated by pipes (<code>|</code>)</li></ul>'
	},
	de: {
		'rc-change-size-new': '$1 {{PLURAL:$1|Byte|Byte}} nach der Änderung',
		'pagetitle': '$1 – {{SITENAME}}',
		'watchlist++': 'Beobachtungsliste++',
		'watchlist++-mode-classical': 'Klassisch',
		'watchlist++-mode-watchlist++': 'Beobachtungsliste++',
		'watchlist++-flags-minor-letter': 'k',
		'watchlist++-flags-category-letter': 'K',
		'watchlist++-flags-new-title': 'Neuanlage',
		'watchlist++-flags-minor-title': 'Kleine Änderung',
		'watchlist++-flags-bot-title': 'Bot-Änderung',
		'watchlist++-flags-anon-title': 'IP-Änderung',
		'watchlist++-flags-log-title': 'Logaktion',
		'watchlist++-flags-category-title': 'Kategorieänderung',
		'watchlist++-tags-title': '{{PLURAL:$1|Tag|Tags}}: $2',
		'watchlist++-number-editors': 'von $1 Benutzern',
		'watchlist++-by': 'von $1',
		'watchlist++-changes': '$1 {{PLURAL:$1|Änderung|Änderungen}}',
		'watchlist++-change': 'Änderung',
		'watchlist++-change-block': 'Sperrung',
		'watchlist++-change-block-unblock': 'Entsperrung',
		'watchlist++-change-delete': 'Löschung',
		'watchlist++-change-delete-restore': 'Wiederherstellung',
		'watchlist++-change-import': 'Import',
		'watchlist++-change-newusers': 'Kontoerstellung',
		'watchlist++-change-massmessage': 'Massennachricht',
		'watchlist++-change-move': 'Verschiebung',
		'watchlist++-change-protect': 'Seitenschutz',
		'watchlist++-change-protect-unprotect': 'Seitenfreigabe',
		'watchlist++-change-renameuser': 'Umbenennung',
		'watchlist++-change-rights': 'Benutzerrechteänderung',
		'watchlist++-change-supress': 'Verstecken',
		'watchlist++-change-upload': 'Upload',
		'watchlist++-log-delete-revision': 'Versionslöschung',
		'watchlist++-log-upload-overwrite': 'neue Dateiversion',
		'watchlist++-read-until': 'bis hier als gelesen markieren',
		'watchlist++-read-all': 'als gelesen markieren',
		'watchlist++-unread-until': 'ab hier als ungelesen markieren',
		'watchlist++-unread-all': 'als ungelesen markieren',
		'watchlist++-rules-head': '{{PLURAL:$1|Eine Regel|$1 Regeln}}',
		'watchlist++-rules-head-expand': 'Regeln zeigen',
		'watchlist++-rules-head-collapse': 'Regeln verbergen',
		'watchlist++-rules-add': 'neue Regel',
		'watchlist++-rules-export-import': 'Export/Import',
		'watchlist++-rules-delete-all': 'Standardregeln wiederherstellen',
		'watchlist++-rules-bookmarklet': 'Sicherungskopie für Beobachtungsliste++',
		'watchlist++-rules-bookmarklet-title': 'Installiere diesen Link als Bookmarklet um eine Sicherungskopie deiner aktuellen Regeln zu sichern',
		'watchlist++-rules-delete-all-confirm': 'Sollen wirklich alle Regeln gelöscht werden und stattdessen wieder die Standardregeln verwendet werden?',
		'watchlist++-rules-delete': 'löschen',
		'watchlist++-rules-delete-confirm': 'Soll die Regel „$1“ wirklich gelöscht werden?',
		'watchlist++-rules-edit': 'bearbeiten',
		'watchlist++-export-import-rules': 'Folgende Daten im JSON-Format repräsentieren die aktuellen Markierungsregeln. Du kannst sie nach Belieben ändern. Beachte, dass Änderungen erst beim nächsten Mal wirksam werden.',
		'watchlist++-json-error': 'Die JSON-Daten sind nicht korrekt: $1',
		'watchlist++-storage-error': 'Ein Speicherfehler trat auf, Änderungen an den Regeln konnten nicht gespeichert werden.',
		'watchlist++-filter-ns': 'Der Namensraum',
		'watchlist++-filter-title': 'Der Seitentitel',
		'watchlist++-filter-user': 'Der Benutzer',
		'watchlist++-filter-diff': 'Die Größenänderung',
		'watchlist++-filter-flags': 'Der Wert für die Flags',
		'watchlist++-filter-timestamp': 'Der Zeitstempel',
		'watchlist++-filter-comment': 'Der Bearbeitungskommentar',
		'watchlist++-filter-logtype': 'Die Logaktion',
		'watchlist++-filter-tags': 'Die Bearbeitungsmarkierung',
		'watchlist++-type-is': 'ist',
		'watchlist++-type-is-not': 'ist nicht',
		'watchlist++-type-match': 'passt auf',
		'watchlist++-type-match-not': 'passt nicht auf',
		'watchlist++-type-and': 'enthält mindestens die Bits von',
		'watchlist++-type-and-not': 'enthält nicht alle Bits von',
		'watchlist++-type-or': 'enthält höchstens die Bits von',
		'watchlist++-type-or-not': 'enthält nicht nur die Bits von',
		'watchlist++-type-gt': 'ist größer als',
		'watchlist++-type-gt-not': 'ist kleiner oder gleich',
		'watchlist++-type-ge': 'ist größer oder gleich',
		'watchlist++-type-ge-not': 'ist kleiner als',
		'watchlist++-rule-read-mark': 'Markiere alle Änderungen als $1 und mit Kennzeichnung Nr. $2, auf die alle folgenden Regeln zutreffen:',
		'watchlist++-rule-read': 'Markiere alle Änderungen als $1, auf die alle folgenden Regeln zutreffen:',
		'watchlist++-rule-mark': 'Markiere alle Änderungen mit Kennzeichnung Nr. $1, auf die alle folgenden Regeln zutreffen:',
		'watchlist++-rules-read': 'gelesen',
		'watchlist++-rules-unread': 'ungelesen',
		'watchlist++-toggle-read': 'Gelesene Änderungen ein-/ausblenden',
		'watchlist++-add-older': 'Ältere',
		'watchlist++-add-older-title': 'Ältere Änderungen laden',
		'watchlist++-add-newer': 'Neuere',
		'watchlist++-add-newer-title': 'Neuere Änderungen laden',
		'watchlist++-reset-notification': 'Alle Änderungen als gelesen markieren',
		'watchlist++-rules-desc-mark-own-pages': 'Änderungen an eigenen Seiten durch andere Benutzer markieren',
		'watchlist++-rules-desc-hide-bots': 'Bot-Änderungen als gelesen markieren',
		'watchlist++-new-rule-title': 'Neue Filterregel erstellen',
		'watchlist++-new-rule-text': 'Erstelle eine neue Filterregel, indem du auswählst, welche Markierungen sie vornehmen soll und welche Kriterien alle erfüllt sein müssen, damit sie angewendet wird.',
		'watchlist++-edit-rule-title': 'Filterregel bearbeiten',
		'watchlist++-edit-rule-text': 'Bearbeite die Filterregel, indem du auswählst, welche Markierungen sie vornehmen soll und welche Kriterien alle erfüllt sein müssen, damit sie angewendet wird.',
		'watchlist++-new-rule-desc': 'Beschreibung: ',
		'watchlist++-new-rule-mark': '(Markierung)',
		'watchlist++-new-rule-read': '(gelesen/ungelesen)',
		'watchlist++-new-rule-type': '(neues Kriterium)',
		'watchlist++-rule-mark-1': 'rot',
		'watchlist++-rule-mark-2': 'orange',
		'watchlist++-rule-mark-3': 'gelb',
		'watchlist++-rule-mark-4': 'grün',
		'watchlist++-rule-mark-5': 'blau',
		'watchlist++-rule-mark-6': 'rosa',
		'watchlist++-rule-mark-7': 'violett',
		'watchlist++-new-rule-help': 'Hilfe',
		'watchlist++-new-rule-save': 'Speichern',
		'watchlist++-new-rule-cancel': 'Abbrechen',
		'watchlist++-new-rule-help-done': 'Zurück',
		'watchlist++-new-rule-error-no-action': 'Fehlende Aktion für Regel!',
		'watchlist++-new-rule-error-no-filter': 'Fehlende Filterkriterien für Regel!',
		'watchlist++-new-rule-error-no-number': 'Keine Zahl bei Bit-Vergleich!',
		'watchlist++-new-rule-error-no-re': 'Ungültiger regulärer Ausdruck!',
		'watchlist++-new-rule-help-text': '<ul><li>Namensraum: numerischer Wert ($1)</li><li>Titel: Seitentitel (ohne Namensraum)</li><li>Benutzer: Benutzername</li><li>Größenänderung: Zahl, eventuell negativ</li><li>Flags: numerischer Wert aus folgenden Konstanten: $2</li><li>Zeitstempel: im Format yyyy-mm-ddThh:mm:ssZ</li><li>Bearbeitungskommentar: als Quelltext</li><li>Logaktion: interne Bezeichnung</li><li>Bearbeitungsmarkierung: interne Bezeichnungen, getrennt durch senkrechte Striche (<code>|</code>)</li></ul>'
	},
	'de-ch': {
		'watchlist++-rules-delete-confirm': 'Soll die Regel «$1» wirklich gelöscht werden?',
		'watchlist++-filter-diff': 'Die Grössenänderung',
		'watchlist++-type-gt': 'ist grösser als',
		'watchlist++-type-ge': 'ist grösser oder gleich',
		'watchlist++-new-rule-help-text': '<ul><li>Namensraum: numerischer Wert ($1)</li><li>Titel: Seitentitel (ohne Namensraum)</li><li>Benutzer: Benutzername</li><li>Grössenänderung: Zahl, eventuell negativ</li><li>Flags: numerischer Wert aus folgenden Konstanten: $2</li><li>Zeitstempel: im Format yyyy-mm-ddThh:mm:ssZ</li><li>Bearbeitungskommentar: als Quelltext</li><li>Logaktion: interne Bezeichnung</li><li>Bearbeitungsmarkierung: interne Bezeichnungen, getrennt durch senkrechte Striche (<code>|</code>)</li></ul>'
	},
	'de-formal': {
		'watchlist++-rules-bookmarklet-title': 'Installieren Sie diesen Link als Bookmarklet um eine Sicherungskopie Ihrer aktuellen Regeln zu sichern',
		'watchlist++-export-import-rules': 'Folgende Daten im JSON-Format repräsentieren die aktuellen Markierungsregeln. Sie können sie nach Belieben ändern. Beachten Sie, dass Änderungen erst beim nächsten Mal wirksam werden.',
		'watchlist++-new-rule-text': 'Erstellen Sie eine neue Filterregel, indem Sie auswählen, welche Markierungen sie vornehmen soll und welche Kriterien alle erfüllt sein müssen, damit sie angewendet wird.',
		'watchlist++-edit-rule-text': 'Bearbeiten Sie die Filterregel, indem Sie auswählen, welche Markierungen sie vornehmen soll und welche Kriterien alle erfüllt sein müssen, damit sie angewendet wird.'
	}
};
//jscs:enable maximumLineLength

FLAGS = {
	NEW: 1,
	MINOR: 2,
	BOT: 4,
	ANON: 8,
	LOG: 16,
	WIKIDATA: 32,
	CATEGORY: 64
};

function initL10N (l10n, keep) {
	var i, chain = mw.language.getFallbackLanguageChain();
	keep = $.grep(mw.messages.get(keep), function (val) {
		return val !== null;
	});
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
	mw.messages.set(keep);
}

function getTitleFromPagename (page) {
	var pos = page.indexOf(':');
	if (pos > -1) {
		if (hasOwn.call(mw.config.get('wgNamespaceIds'), page.slice(0, pos).replace(/ /g, '_').toLowerCase())) {
			return page.slice(pos + 1);
		}
	}
	return page;
}

function fixWikidataComment (comment) {
	//Wikidata provides completely broken comments,
	//and developers don't seem to care to fix it.
	//So we try to fix at least some of the worst things.
	return comment
		.replace(/<a href="\/wiki\/[^#]+#wb[^"]+" title="[^"]+">[^<]+<\/a>/g, '') //broken autosummary, remove
		.replace(
			//actually, *all* links use the wrong base,
			//but for known namespaces even these are broken, too, so it's not easy to fix them
			/<a href="\/w\/index.php\?title=(Property:P\d+|Q\d+)&amp;action=edit&amp;redlink=1" class="new" title="[^"]+">/g,
			'<a class="external" href="//www.wikidata.org/wiki/$1">'
		)
		.replace(/>wb([a-z\-]+):([^:]+):/g, function (all, one, two) {
			//tons of possible messages, so just use the codes
			return '>' + one + ' (' + two.replace(/\|+/g, mw.msg('comma-separator')) + '):';
		});
}

function getCSS1 () {
	var css = '', i, markers = [
		'',
		'#d33', //red
		'#ff6d22', //orange (from RC filters)
		'#fc3', //yellow
		'#00af89', //green
		'#36c', //blue
		'#e6d', //pink (not in any palette)
		'#a033c0' //violet (not in any palette)
	];

	css += '.td-marker { font-weight: bold; font-size: 200%; }';
	css += '.td-marker > * { visibility: hidden; }';
	for (i = 1; i < markers.length; i++) {
		css += '.mark-' + i + ' .td-marker > * { visibility: visible; color: ' + markers[i] + '; }';
	}

	css += '.changes-block.collapsed .td-collapse span::before { content: "\u25B6"; color: #36c; cursor: pointer; }';
	css += '.changes-block.expanded .td-collapse span::before { content: "\u25BC"; color: #36c; cursor: pointer; }';
	css += '.changes-single .td-collapse span::before { content: "\u25B7"; color: #a2a9b1; }';
	css += '#changes-table .changes-line.collapsed { display:none; }';

	css += '.changes-line .td-title > * { visibility: hidden; }';
	css += '.changes-line .td-title.title-change > * { visibility: visible; font-style: italic; }';

	css += '.td-flags {font-family: monospace, monospace; }';
	css += '.td-flags abbr { font-weight: bold; }';

	css += '.change-read .td-read .read { display: none; }';
	css += '.change-unread .td-read .unread { display: none; }';

	css += '#changes-table { line-height: 1.5em; }';
	css += '#changes-table table { border-spacing: 0; }';
	css += '#changes-table td { white-space: nowrap; padding: 1px 3px; }';
	css += '.td-title, .td-user, .td-comment { overflow: hidden; text-overflow: ellipsis; }';
	css += '.td-title:hover, .td-user:hover, .td-comment:hover {' +
		//background-color: #fff; wird ohnehin von übernächster Anweisung überschrieben
		'white-space: normal !important; word-wrap: break-word; overflow: visible; position: relative; z-index: 1; }';
	css += '.td-timestamp, .td-diff { text-align: right; }';
	css += '#changes-table tr:hover { background-color: #eaf3ff; }';

	css += '#rules-head { padding: 0 0.5em; }';
	css += '.collapsed ol { display: none; }';

	css += '.schnark-rule-widget { border-collapse: collapse; margin-top: 1em; }';
	css += '.schnark-rule-widget td { padding-left: 0; padding-right: 0.5em; }';

	return css;
}

function getCSS2 () {
	var css = '';
	css += '.change-unread .td-change, .change-unread .td-user { font-weight: bold; }';
	css += '#changes-table .td-change-read { font-weight: normal; display: inline !important; }';
		//inline um in fixWidth() CSS3 zu überschreiben
	return css;
}

function getCSS3 () {
	return '#changes-table .change-read, #changes-table .td-change-read { display: none; }';
}

function getCSS4 () {
	return ' ' + //Leerzeichen, um jscs nicht zu verwirren und Zeilenumbruch zu ermöglichen
	'#changes-table table {' +
		'display: grid;' +
		'grid-template-columns:' +
			'max-content ' + //marker
			'max-content ' + //collapse
			'minmax(15em,1.5fr) ' + //title
			'max-content ' + //change
			'minmax(10em,1fr) ' + //user
			'max-content ' + //flags
			'max-content ' + //diff
			'minmax(5em,1fr) ' + //comment
			'max-content ' + //timestamp
			'max-content;' + //read
		'width: 100%;' +
	'}' +

	//Subgrid wäre logischer, bietet aber letztlich keine echten Vorteile.
	//Nötig dazu wäre:
	//* obige Regel von table zu tbody verschieben
	//* tr { display: grid; grid-template-columns: subgrid; grid-column: 1/11; }
	'#changes-table thead,' +
	'#changes-table tbody,' +
	'#changes-table tfoot,' +
	'#changes-table tr {' +
		'display: contents;' +
	'}' +

	'#changes-table th,' +
	'#changes-table td {' +
		'display: block;' +
		'width: 100%;' +
		'box-sizing: border-box;' +
	'}' +

	//oben ist die Farbe am <tr>, aber dort geht sie wegen display: contents verloren
	'#changes-table tr:hover td {' +
		'background-color: #eaf3ff;' +
	'}';
}

//rules & filters
function getDefaultRules () {
	return [{
		desc: mw.msg('watchlist++-rules-desc-mark-own-pages'),
		filter: [{
			key: 'user',
			type: 'is',
			val: mw.config.get('wgUserName'),
			not: ''
		}, {
			key: 'title',
			type: 'match',
			val: '^' + mw.util.escapeRegExp(mw.config.get('wgUserName')) + '($|/)'
		}, {
			key: 'ns',
			type: 'match',
			val: '^2|3$'
		}],
		mark: 2
	}, {
		desc: mw.msg('watchlist++-rules-desc-hide-bots'),
		filter: [{
			key: 'flags',
			type: 'and',
			val: String(FLAGS.BOT)
		}],
		read: true
	}];
}

function getNotificationBugfixData () {
	var data = mw.storage.get('schnark-watchlist++-nbd');
	if (data) {
		data = JSON.parse(data);
	} else {
		data = {};
	}
	return data;
}

function setNotificationBugfixData (data) {
	mw.storage.set('schnark-watchlist++-nbd', JSON.stringify(data));
}

function getAllRules () {
	var rules = mw.storage.get('schnark-watchlist++-rules');
	if (rules) {
		rules = JSON.parse(rules);
	} else {
		rules = getDefaultRules();
	}
	return rules;
}

function setRules (rules) {
	if (!mw.storage.set('schnark-watchlist++-rules', JSON.stringify(rules))) {
		window.alert(mw.msg('watchlist++-storage-error'));
	}
}

function deleteRules () {
	if (!mw.storage.remove('schnark-watchlist++-rules')) {
		window.alert(mw.msg('watchlist++-storage-error'));
	}
}

function exportImportRules () {
	var rules = getAllRules();
	rules = JSON.stringify(rules);
	rules = window.prompt(mw.msg('watchlist++-export-import-rules'), rules);
	if (rules !== null) {
		try {
			rules = JSON.parse(rules);
		} catch (e) {
			window.alert(mw.msg('watchlist++-json-error', String(e)));
			return;
		}
		setRules(rules);
		return true;
	}
}

function addRuleDialog (onUpdate, ruleId) {
	var windowManager, ruleDialog;

	function getHelpContent () {
		var i, ns = [], flags = [];
		for (i in mw.config.get('wgFormattedNamespaces')) {
			if (!isNaN(i) && i > 0) {
				ns.push(mw.config.get('wgFormattedNamespaces')[i] + mw.msg('colon-separator') + i);
			}
		}
		for (i in FLAGS) {
			if (hasOwn.call(FLAGS, i)) {
				flags.push(FLAGS[i] + mw.msg('colon-separator') + mw.msg('watchlist++-flags-' + i.toLowerCase() + '-title'));
			}
		}
		return mw.msg('watchlist++-new-rule-help-text',
			ns.join(mw.msg('comma-separator')), flags.join(mw.msg('comma-separator')));
	}

	function RuleLineWidget (config) {
		RuleLineWidget.parent.call(this, {$element: $('<tr>')});

		this.keyInput = new OO.ui.DropdownWidget({
			$overlay: config.$overlay,
			menu: {
				items: [
					new OO.ui.MenuOptionWidget({
						data: '',
						label: mw.msg('watchlist++-new-rule-type')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'ns',
						label: mw.msg('watchlist++-filter-ns')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'title',
						label: mw.msg('watchlist++-filter-title')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'user',
						label: mw.msg('watchlist++-filter-user')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'diff',
						label: mw.msg('watchlist++-filter-diff')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'flags',
						label: mw.msg('watchlist++-filter-flags')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'timestamp',
						label: mw.msg('watchlist++-filter-timestamp')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'comment',
						label: mw.msg('watchlist++-filter-comment')
					}),
					new  OO.ui.MenuOptionWidget({
						data: 'logtype',
						label: mw.msg('watchlist++-filter-logtype')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'tags',
						label: mw.msg('watchlist++-filter-tags')
					})
				]
			}
		});
		this.keyInput.getMenu().selectItemByData(config.data ? config.data[0] : '');

		this.typeInput = new OO.ui.DropdownWidget({
			$overlay: config.$overlay,
			menu: {
				items: [
					new OO.ui.MenuOptionWidget({
						data: 'is',
						label: mw.msg('watchlist++-type-is')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'is-not',
						label: mw.msg('watchlist++-type-is-not')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'match',
						label: mw.msg('watchlist++-type-match')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'match-not',
						label: mw.msg('watchlist++-type-match-not')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'and',
						label: mw.msg('watchlist++-type-and')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'and-not',
						label: mw.msg('watchlist++-type-and-not')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'or',
						label: mw.msg('watchlist++-type-or')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'or-not',
						label: mw.msg('watchlist++-type-or-not')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'gt',
						label: mw.msg('watchlist++-type-gt')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'gt-not',
						label: mw.msg('watchlist++-type-gt-not')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'ge',
						label: mw.msg('watchlist++-type-ge')
					}),
					new OO.ui.MenuOptionWidget({
						data: 'ge-not',
						label: mw.msg('watchlist++-type-ge-not')
					})
				]
			}
		});
		this.typeInput.getMenu().selectItemByData(config.data ? config.data[1] : 'is');

		this.valInput = new OO.ui.TextInputWidget({
			value: config.data ? config.data[2] : ''
		});

		if (!config.data) {
			this.keyInput.getMenu().once('select', function () {
				this.typeInput.toggle(true);
				this.valInput.toggle(true);
				this.emit('activate');
			}.bind(this));
			this.typeInput.toggle(false);
			this.valInput.toggle(false);
		}

		this.keyInput.getMenu().connect(this, {
			select: this.emit.bind(this, 'change')
		});
		this.typeInput.getMenu().connect(this, {
			select: this.emit.bind(this, 'change')
		});
		this.valInput.connect(this, {
			change: this.emit.bind(this, 'change'),
			enter: this.emit.bind(this, 'enter')
		});

		this.keyInput.$element.css('width', '20em');
		this.typeInput.$element.css('width', '20em');

		this.$element.append([
			$('<td>').append(this.keyInput.$element),
			$('<td>').append(this.typeInput.$element),
			$('<td>').append(this.valInput.$element)
		]);
	}

	OO.inheritClass(RuleLineWidget, OO.ui.Widget);

	RuleLineWidget.prototype.getValue = function () {
		var key = this.keyInput.getMenu().findSelectedItem().getData();
		return key ? [
			key,
			this.typeInput.getMenu().findSelectedItem().getData(),
			this.valInput.getValue()
		] : false;
	};

	function RuleWidget (config) {
		var i;
		RuleWidget.parent.call(this, {$element: $('<table>').addClass('schnark-rule-widget')});

		this.$overlay = config.$overlay;
		this.lines = [];

		if (config.data) {
			for (i = 0; i < config.data.length; i++) {
				this.addLine(config.data[i]);
			}
		}
		this.addLine();
	}

	OO.inheritClass(RuleWidget, OO.ui.Widget);

	RuleWidget.prototype.addLine = function (data) {
		var line = new RuleLineWidget({$overlay: this.$overlay, data: data});
		this.lines.push(line);
		this.$element.append(line.$element);
		line.on('activate', this.onLineActivate.bind(this));
	};

	RuleWidget.prototype.onLineActivate = function () {
		this.addLine();
		this.emit('sizechange');
	};

	RuleWidget.prototype.getValue = function () {
		return this.lines.map(function (line) {
			return line.getValue();
		}).filter(function (val) {
			return !!val;
		});
	};

	function RuleDialog (config) {
		RuleDialog.parent.call(this, config);
		this.onUpdate = config.onUpdate;
		this.ruleId = config.ruleId;
	}

	OO.inheritClass(RuleDialog, OO.ui.ProcessDialog);
	RuleDialog.static.name = 'schnark-watchlist';
	RuleDialog.static.size = 'larger';
	RuleDialog.static.actions = [
		{
			action: 'done', modes: 'edit',
			label: mw.msg('watchlist++-new-rule-save'),
			flags: ['primary', 'progressive']
		}, {
			action: 'help', modes: 'edit',
			label: mw.msg('watchlist++-new-rule-help')
		}, {
			modes: 'edit',
			label: mw.msg('watchlist++-new-rule-cancel'),
			flags: ['safe', 'close']
		}, {
			action: 'back', modes: 'help',
			label: mw.msg('watchlist++-new-rule-help-done'),
			flags: ['safe', 'back']
		}
	];

	RuleDialog.prototype.initialize = function () {
		RuleDialog.parent.prototype.initialize.apply(this, arguments);
		this.panel1 = new OO.ui.PanelLayout({$: this.$, padded: true, expanded: false});
		this.createForm(this.ruleId !== undefined ? getAllRules()[this.ruleId] : false);
		this.panel2 = new OO.ui.PanelLayout({$: this.$, padded: true, expanded: false});
		this.panel2.$element.append(getHelpContent());
		this.stackLayout = new OO.ui.StackLayout({items: [this.panel1, this.panel2]});
		this.$body.append(this.stackLayout.$element);
	};

	RuleDialog.prototype.getSetupProcess = function () {
		return RuleDialog.parent.prototype.getSetupProcess.apply(this, arguments)
			.next(function () {
				this.actions.setMode('edit');
			}, this);
	};

	RuleDialog.prototype.getActionProcess = function (action) {
		var error;
		if (action === 'help') {
			return new OO.ui.Process(function () {
				this.actions.setMode('help');
				this.stackLayout.setItem(this.panel2);
				this.updateSize();
			}, this);
		} else if (action === 'back') {
			return new OO.ui.Process(function () {
				this.actions.setMode('edit');
				this.stackLayout.setItem(this.panel1);
				this.updateSize();
			}, this);
		} else if (action === 'done') {
			error = this.saveRuleOrReturnError();
			if (error) {
				return new OO.ui.Process(function () {
					return new OO.ui.Error(mw.msg('watchlist++-new-rule-error-' + error), {recoverable: false});
				});
			} else {
				return new OO.ui.Process(function () {
					this.close();
				}, this);
			}
		}
		return RuleDialog.parent.prototype.getActionProcess.apply(this, arguments);
	};

	RuleDialog.prototype.onDismissErrorButtonClick = function () {
		this.actions.setAbilities({done: true}); //well, kind of semi-recoverable
		return RuleDialog.parent.prototype.onDismissErrorButtonClick.apply(this, arguments);
	};

	RuleDialog.prototype.getBodyHeight = function () {
		return this.stackLayout.getCurrentItem().$element.outerHeight(true);
	};

	RuleDialog.prototype.createForm = function (rule) {
		var $element;

		$element = (new OO.ui.LabelWidget({
			label: mw.msg(rule ? 'watchlist++-edit-rule-text' : 'watchlist++-new-rule-text')
		})).$element;
		$element.css('margin-bottom', '1em');
		this.panel1.$element.append($element);

		this.descInput = new OO.ui.TextInputWidget({value: rule ? rule.desc : ''});
		this.markInput = new OO.ui.DropdownWidget({
			$overlay: this.$overlay,
			menu: {
				items: [
					new OO.ui.MenuOptionWidget({
						data: '',
						label: mw.msg('watchlist++-new-rule-mark')
					}),
					new OO.ui.MenuOptionWidget({
						data: '1',
						label: mw.msg('watchlist++-rule-mark-1')
					}),
					new OO.ui.MenuOptionWidget({
						data: '2',
						label: mw.msg('watchlist++-rule-mark-2')
					}),
					new OO.ui.MenuOptionWidget({
						data: '3',
						label: mw.msg('watchlist++-rule-mark-3')
					}),
					new OO.ui.MenuOptionWidget({
						data: '4',
						label: mw.msg('watchlist++-rule-mark-4')
					}),
					new OO.ui.MenuOptionWidget({
						data: '5',
						label: mw.msg('watchlist++-rule-mark-5')
					}),
					new OO.ui.MenuOptionWidget({ //out of order for historical reasons
						data: '7',
						label: mw.msg('watchlist++-rule-mark-7')
					}),
					new OO.ui.MenuOptionWidget({
						data: '6',
						label: mw.msg('watchlist++-rule-mark-6')
					})
				]
			}
		});
		this.markInput.getMenu().selectItemByData(rule && 'mark' in rule ? String(rule.mark) : '');
		this.readInput = new OO.ui.DropdownWidget({
			$overlay: this.$overlay,
			menu: {
				items: [
					new OO.ui.MenuOptionWidget({
						data: '',
						label: mw.msg('watchlist++-new-rule-read')
					}),
					new OO.ui.MenuOptionWidget({
						data: '0',
						label: mw.msg('watchlist++-rules-unread')
					}),
					new OO.ui.MenuOptionWidget({
						data: '1',
						label: mw.msg('watchlist++-rules-read')
					})
				]
			}
		});
		this.readInput.getMenu().selectItemByData(rule && 'read' in rule ? (rule.read ? '1' : '0') : '');
		this.filterInput = new RuleWidget({
			$overlay: this.$overlay,
			data: rule ? rule.filter.map(function (filter) {
				return [filter.key, filter.type + ('not' in filter ? '-not' : ''), filter.val];
			}) : false
		});

		this.filterInput.connect(this, {sizechange: 'updateSize'});

		this.markInput.$element.css('width', '10em');
		this.readInput.$element.css('width', '20em');

		$element = (new OO.ui.HorizontalLayout({items: [
			this.markInput,
			this.readInput
		]})).$element;
		$element.css('margin', '0.5em 0');

		this.panel1.$element.append([
			(new OO.ui.FieldLayout(this.descInput, {
				label: mw.msg('watchlist++-new-rule-desc'),
				align: 'top'
			})).$element,
			$element,
			this.filterInput.$element
		]);
	};

	RuleDialog.prototype.saveRuleOrReturnError = function () {
		var desc, mark, read, filters, rule, rules, i;
		desc = this.descInput.getValue() || '';
		mark = this.markInput.getMenu().findSelectedItem().getData();
		read = this.readInput.getMenu().findSelectedItem().getData();
		filters = this.filterInput.getValue().map(function (data) {
			var filter = {key: data[0], type: data[1].replace(/-not$/, ''), val: data[2]};
			if (/-not$/.test(data[1])) {
				filter.not = '';
			}
			return filter;
		});
		rule = {desc: desc, filter: filters};
		if (mark) {
			rule.mark = Number(mark);
		}
		if (read) {
			rule.read = (read === '1');
		}
		if (!mark && !read) {
			return 'no-action';
		}
		if (filters.length === 0) {
			return 'no-filter';
		}
		for (i = 0; i < filters.length; i++) {
			switch (filters[i].type) {
			case 'and':
			case 'or':
				if (isNaN(filters[i].val)) {
					return 'no-number';
				}
				break;
			case 'match':
				try {
					/*jshint nonew: false*/
					new RegExp(filters[i].val);
				} catch (e) {
					return 'no-re';
				}
			}
		}
		rules = getAllRules();
		if (this.ruleId !== undefined) {
			rules[this.ruleId] = rule;
		} else {
			rules.push(rule);
		}
		setRules(rules);
		if (this.onUpdate) {
			this.onUpdate();
		}
	};

	windowManager = new OO.ui.WindowManager();
	$('body').append(windowManager.$element);
	ruleDialog = new RuleDialog({
		onUpdate: onUpdate,
		ruleId: ruleId
	});
	windowManager.addWindows([ruleDialog]);
	windowManager.openWindow(ruleDialog, {
		title: mw.msg(ruleId === undefined ? 'watchlist++-new-rule-title' : 'watchlist++-edit-rule-title')
	});
}

function testFilter (filter, data) {
	/*jshint bitwise: false*/
	var v = data[filter.key], ret;
	switch (filter.type) {
	case 'is':
		ret = String(v) === filter.val;
		break;
	case 'match':
		try {
			ret = (new RegExp(filter.val)).test(String(v));
		} catch (e) {
			ret = false;
		}
		break;
	case 'and':
		ret = ((Number(v) & Number(filter.val)) === Number(filter.val));
		break;
	case 'or':
		ret = ((Number(v) | Number(filter.val)) === Number(filter.val));
		break;
	case 'gt':
		ret = (v > filter.val);
		break;
	case 'ge':
		ret = (v >= filter.val);
		break;
	}
	if ('not' in filter) {
		ret = !ret;
	}
	return ret;
}

function testFilters (filters, data) {
	var i;
	for (i = 0; i < filters.length; i++) {
		if (!testFilter(filters[i], data)) {
			return false;
		}
	}
	return true;
}

function applyRules (rules, data) {
	var i, read, mark, desc;
	for (i = 0; i < rules.length; i++) {
		if (testFilters(rules[i].filter, data)) {
			if ('read' in rules[i]) {
				if (read === undefined || read === true) {
					read = rules[i].read;
				}
			}
			if ('mark' in rules[i]) {
				if (mark === undefined || mark > rules[i].mark) {
					mark = rules[i].mark;
					desc = rules[i].desc;
				}
			}
		}
	}
	return {read: read, mark: mark, desc: desc};
}

function Watchlist () {
	this.changes = [];
	this.pages = [];
	this.uuids = 1;
	this.queryContinue = {
		older: false,
		newer: false
	};
}

Watchlist.compare = function (a, b) {
	var aa = a.getPrimarySortkey(), bb = b.getPrimarySortkey();
	if (aa < bb) {
		return 1;
	}
	if (aa > bb) {
		return -1;
	}
	aa = a.getSecondarySortkey();
	bb = b.getSecondarySortkey();
	if (aa < bb) {
		return 1;
	}
	if (aa > bb) {
		return -1;
	}
	return 0;
};

Watchlist.prototype.getUUID = function (type) {
	return (this.uuids++) * (type === 'page' ? -1 : 1);
};

Watchlist.prototype.getFromUUID = function (uuid) {
	var i, search = uuid < 0 ? this.pages : this.changes;
	for (i = 0; i < search.length; i++) {
		if (search[i].uuid === uuid) {
			return search[i];
		}
	}
};

Watchlist.prototype.canGetOlder = function () {
	return this.queryContinue.older !== true;
};

Watchlist.prototype.canResetAll = function () {
	return this.changes.some(function (change) {
		return !change.read;
	});
};

Watchlist.prototype.getApiQuery = function (cont) {
	var data = {
		action: 'query',
		list: 'watchlist',
		wlallrev: true,
		wllimit: 500,
		wlprop: 'ids|title|flags|user|userid|comment|parsedcomment|timestamp|sizes|notificationtimestamp|loginfo|tags',
		wltype: 'edit|new|log|external|categorize',
		format: 'json',
		formatversion: 2
	};
	if (cont && this.queryContinue[cont]) {
		if (cont === 'older' && this.queryContinue.older === true) {
			return false;
		}
		$.extend(data, this.queryContinue[cont]);
	}
	return data;
};

Watchlist.prototype.updateContinue = function (json, cont) {
	var d;
	if (cont !== 'newer') {
		if (json['continue']) {
			this.queryContinue.older = json['continue'];
		} else {
			this.queryContinue.older = true;
		}
	}
	if (cont !== 'older') {
		if (json.query && json.query.watchlist && json.query.watchlist[0]) {
			d = new Date(json.query.watchlist[0].timestamp);
			if (!isNaN(d.valueOf())) {
				d.setTime(d.getTime() + 1000);
				this.queryContinue.newer = {wlend: d.toISOString().replace(/\.0*Z$/, 'Z')};
			}
		}
	}
};

Watchlist.prototype.runApi = function (callback, cont) {
	var data = this.getApiQuery(cont);
	if (!data) {
		callback(0);
		return;
	}
	$.getJSON(mw.util.wikiScript('api'), data).then(function (json) {
		if (json) {
			this.updateContinue(json, cont);
		}
		if (json && json.query && json.query.watchlist && json.query.watchlist.length) {
			this.fromApi(json.query.watchlist);
			callback(json.query.watchlist.length);
		} else {
			callback(0);
		}
	}.bind(this));
};

Watchlist.prototype.fromApi = function (data) {
	var i, change;
	for (i = 0; i < data.length; i++) {
		change = (new Change(this.getUUID('change'))).fromApi(data[i]);
		this.changes.push(change);
		if (change.isMoveCreate()) {
			change = (new Change(this.getUUID('change'))).fromMove(change);
			this.changes.push(change);
		}
	}
	this.changes.sort(Watchlist.compare);
	this.groupChanges();
	this.applyRules();
};

Watchlist.prototype.groupChanges = function () {
	var i, name, oldName, groups = {}, moves = {};
	for (i = 0; i < this.changes.length; i++) {
		name = this.changes[i].getPagename();
		if (hasOwn.call(moves, name)) {
			name = moves[name];
		}
		if (!hasOwn.call(groups, name)) {
			groups[name] = [];
		}
		groups[name].push(this.changes[i]);
		oldName = this.changes[i].getOldPagename();
		if (oldName) {
			moves[oldName] = name;
		}
	}
	this.pages = [];
	for (name in groups) {
		if (hasOwn.call(groups, name)) {
			this.pages.push(new Page(groups[name], this));
		}
	}
	this.pages.sort(Watchlist.compare);
};

Watchlist.prototype.render = function () {
	var i, html = [];
	for (i = 0; i < this.pages.length; i++) {
		html.push(this.pages[i].render());
	}
	return '<table>' + html.join('') + '</table>';
};

Watchlist.prototype.applyRules = function () {
	var i, rules = getAllRules();
	for (i = 0; i < this.pages.length; i++) {
		this.pages[i].applyRules(rules);
	}
};

Watchlist.prototype.markAllAsRead = function () {
	var i;
	for (i = 0; i < this.pages.length; i++) {
		this.pages[i].markAllAsRead();
	}
	this.resetNotificationtimestamps();
};

Watchlist.prototype.getEmptyTsForPage = function (page) {
	//HACK verhält sich beinahe so wie '' als notificationtimestamp
	return page.changes[0].timestamp.replace(/Z$/, 'z');
};

Watchlist.prototype.resetNotificationtimestamps = function () {
	$.post(mw.util.wikiScript('api'), {
		action: 'setnotificationtimestamp',
		entirewatchlist: '',
		token: mw.user.tokens.get('csrfToken'),
		format: 'json',
		formatversion: 2
	});
	var nbData = {}, i, page;
	for (i = 0; i < this.pages.length; i++) {
		page = this.pages[i];
		if (!page.changes[0].canTrustNotification()) {
			nbData[page.getPagename()] = this.getEmptyTsForPage(page);
		}
	}
	setNotificationBugfixData(nbData);
};

Watchlist.prototype.resetNotificationtimestamp = function (title, timestamp) {
	var data = {
		action: 'setnotificationtimestamp',
		titles: title,
		token: mw.user.tokens.get('csrfToken'),
		format: 'json',
		formatversion: 2
	};
	if (timestamp) {
		data.timestamp = timestamp;
	}
	$.post(mw.util.wikiScript('api'), data);
};

Watchlist.prototype.getBugfixNotification = function (pagename) {
	return getNotificationBugfixData()[pagename];
};

Watchlist.prototype.setBugfixNotification = function (pagename, ts) {
	var nbData = getNotificationBugfixData();
	if (ts) {
		nbData[pagename] = ts;
	} else {
		delete nbData[pagename];
	}
	setNotificationBugfixData(nbData);
};

function Page (changes, watchlist) {
	var i;
	this.changes = changes;
	this.uuid = watchlist.getUUID('page');
	this.watchlist = watchlist;
	for (i = 0; i < this.changes.length; i++) {
		this.changes[i].page = this;
	}
}

Page.prototype.getPagename = function () {
	return this.changes[0].getPagename();
};

Page.prototype.getPrimarySortkey = function () {
	return this.changes[0].getPrimarySortkey();
};

Page.prototype.getSecondarySortkey = function () {
	return this.changes[0].getSecondarySortkey();
};

Page.prototype.getRelevantChanges = function () {
	var changes = [], i;
	if (this.readcount < this.changes.length) {
		for (i = 0; i < this.changes.length; i++) {
			if (!this.changes[i].read) {
				changes.push(this.changes[i]);
			}
		}
		return (new Change(this.uuid)).fromChanges(changes, this.readcount, false);
	}
	return (new Change(this.uuid)).fromChanges(this.changes, 0, true);
};

Page.prototype.render = function () {
	var html = [], i;
	if (this.changes.length === 1) {
		return this.changes[0].render({type: 'single'});
	}
	html.push(this.getRelevantChanges().render({type: 'block'}));
	for (i = 0; i < this.changes.length; i++) {
		html.push(this.changes[i].render({
			type: 'line',
			titleChange: (i > 0) && (this.changes[i].getPagename() !== this.changes[i - 1].getPagename())
		}));
	}
	return html.join('');
};

Page.prototype.getIndexesByTimestamp = function (ts) {
	var i, i0, i1;
	if (!ts) {
		i0 = -1;
	} else {
		for (i = this.changes.length - 1; i >= 0; i--) {
			if (this.changes[i].timestamp >= ts) {
				break;
			}
		}
		i0 = i;
	}
	for (i = i0 + 1; i < this.changes.length; i++) {
		if (this.changes[i].canTrustNotification()) {
			break;
		}
	}
	i1 = i;
	//i0: Index of oldes definitely unread change (-1 if no such change)
	//i1: Index of newes definitely read change (.length if no such change)
	return [i0, i1];
};

Page.prototype.getNotification = function () {
	var notification = this.changes[0].notification,
		indexes = this.getIndexesByTimestamp(notification);
	if (indexes[0] + 1 === indexes[1]) {
		return notification;
	}
	return this.watchlist.getBugfixNotification(this.getPagename()) || this.changes[indexes[1] - 1].timestamp;
};

Page.prototype.setNotification = function (ts) {
	var indexes = this.getIndexesByTimestamp(ts);
	this.watchlist.resetNotificationtimestamp(this.getPagename(), ts);
	this.watchlist.setBugfixNotification(this.getPagename(), indexes[0] + 1 !== indexes[1] ?
		(ts || this.watchlist.getEmptyTsForPage(this)) : false);
};

Page.prototype.applyRules = function (rules) {
	var i, read, notification = this.getNotification();
	this.readcount = 0;
	for (i = 0; i < this.changes.length; i++) {
		read = this.changes[i].applyRules(rules, notification);
		if (read) {
			this.readcount++;
		}
	}
};

Page.prototype.markAllAsRead = function () {
	var i;
	for (i = 0; i < this.changes.length; i++) {
		this.changes[i].read = true;
	}
};

Page.prototype.markAsReadUntil = function (change) {
	var i, ts = '';
	if (!change) {
		change = this.changes[0];
	}
	for (i = this.changes.length - 1; i >= 0; i--) {
		this.changes[i].markAsRead();
		if (this.changes[i] === change) {
			break;
		}
	}
	if (i <= 0) {
		$('#uuid-' + this.uuid).removeClass('change-unread').addClass('change-read');
	} else {
		ts = this.changes[i - 1].timestamp;
	}
	this.setNotification(ts);
};

Page.prototype.markAsUnreadFrom = function (change) {
	var i, ts;
	if (!change) {
		change = this.changes[this.changes.length - 1];
	}
	for (i = 0; i < this.changes.length; i++) {
		this.changes[i].markAsUnread();
		if (this.changes[i] === change) {
			break;
		}
	}
	$('#uuid-' + this.uuid).removeClass('change-read').addClass('change-unread');
	ts = this.changes[i].timestamp;
	this.setNotification(ts);
};

function Change (uuid) {
	this.uuid = uuid;
}

Change.prototype.fromApi = function (data) {
	/*jshint bitwise: false*/
	/*jshint camelcase: false*///API liefert old_revid etc.
	//jscs:disable requireCamelCaseOrUpperCaseIdentifiers
	var flags;
	this.ns = data.ns;
	this.title = getTitleFromPagename(data.title);
	this.pagename = data.title;

	switch (data.type) {
	case 'edit':
		flags = 0;
		this.revid = data.revid;
		this.oldrevid = data.old_revid;
		break;
	case 'external':
		flags = FLAGS.WIKIDATA;
		break;
	case 'log':
		flags = FLAGS.LOG;
		break;
	case 'new':
		flags = FLAGS.NEW;
		break;
	case 'categorize':
		flags = FLAGS.CATEGORY;
		this.revid = data.revid;
		this.oldrevid = data.old_revid;
		break;
	}

	this.user = data.user;
	if (flags & FLAGS.WIKIDATA) {
		if (mw.util.isIPAddress(this.user)) {
			flags += FLAGS.ANON;
		}
	} else {
		if (data.anon) {
			flags += FLAGS.ANON;
		}
	}

	if (data.minor) {
		flags += FLAGS.MINOR;
	}
	if (data.bot) {
		flags += FLAGS.BOT;
	}
	/*if (data['new']) {
		flags += FLAGS.NEW;
	}*/

	this.timestamp = data.timestamp;
	this.flags = flags;
	this.comment = data.comment || '';
	this.parsedcomment = data.parsedcomment || '';
	this.oldsize = data.oldlen || 0;
	this.newsize = data.newlen || 0;
	this.diff = this.newsize - this.oldsize;
	this.logtype = data.logtype || '';
	this.logaction = data.logaction || '';
	this.logparams = data.logparams || {};
	this.tags = data.tags || [];

	this.notification = data.notificationtimestamp;

	if (data.logparams && data.logtype === 'move') {
		this.oldns = this.ns;
		this.oldtitle = this.title;
		this.oldpagename = this.pagename;
		this.ns = data.logparams.target_ns;
		this.title = getTitleFromPagename(data.logparams.target_title);
		this.pagename = data.logparams.target_title;
	}

	if (this.flags & FLAGS.WIKIDATA) {
		this.parsedcomment = fixWikidataComment(this.parsedcomment);
	}
	//jscs:enable requireCamelCaseOrUpperCaseIdentifiers
	return this;
};

Change.prototype.fromMove = function (change) {
	/*jshint bitwise: false*/
	this.ns = change.oldns;
	this.title = change.oldtitle;
	this.pagename = change.oldpagename;
	this.user = change.user;
	this.timestamp = change.timestamp;
	//jscs:disable disallowImplicitTypeConversion, das ist kein ~-1, also darf ich das
	this.flags = (change.flags & ~FLAGS.LOG) | FLAGS.NEW;
	//jscs:enable disallowImplicitTypeConversion
	this.comment = change.comment;
	this.parsedcomment = change.parsedcomment;
	this.oldsize = 0;
	this.newsize = change.pagename.length + 14; //FIXME
	this.diff = this.newsize;
	this.logtype = '';
	this.logaction = '';
	this.logparams = {};
	this.tags = change.tags;
	this.notification = change.notification;
	return this;
};

Change.prototype.fromChanges = function (changes, additional, allread) {
	/*jshint bitwise: false*/
	var users = [], comments = [], isnew = false, flags, tags, mark, desc = [], firstLast;

	function getFirstLast (changes) {
		var i, lastReal, firstReal;
		if (changes.length === 1) {
			return [changes[0], changes[0]];
		}
		for (i = 0; i < changes.length; i++) {
			if (changes[i].isRealEdit()) {
				lastReal = i;
				break;
			}
		}
		if (lastReal === undefined) {
			return false;
		}
		for (i = changes.length - 1; i >= 0; i--) {
			if (changes[i].isRealEdit()) {
				firstReal = i;
				break;
			}
		}
		return [changes[firstReal], changes[lastReal]];
	}

	changes.forEach(function (change, i) {
		if (users.indexOf(change.user) === -1) {
			users.push(change.user);
		}
		if (comments.indexOf(change.parsedcomment) === -1) {
			comments.push(change.parsedcomment);
		}
		if (i === 0) {
			flags = change.flags;
			tags = change.tags;
		} else {
			flags = flags & change.flags;
			tags = tags.filter(function (tag) {
				return change.tags.indexOf(tag) > -1;
			});
		}
		isnew = isnew || !!(change.flags & FLAGS.NEW);
		mark = (mark === undefined ?
			change.mark :
			(change.mark === undefined ? mark : Math.min(mark, change.mark))
		);
		if (change.desc && desc.indexOf(change.desc) === -1) {
			desc.push(change.desc);
		}
	});
	if (isnew) {
		flags = flags | FLAGS.NEW;
	}
	firstLast = getFirstLast(changes);

	if (firstLast) {
		this.revid = firstLast[1].revid;
		this.oldrevid = firstLast[0].oldrevid;
	}

	this.ns = changes[0].ns;
	this.title = changes[0].title;
	this.pagename = changes[0].pagename;
	this.user = users;
	this.timestamp = changes[0].timestamp;
	this.flags = flags;
	this.comment = comments.length === 1 ? changes[0].comment : '';
	this.parsedcomment = comments.length === 1 ? comments[0] : '';
	this.newsize = firstLast ? firstLast[1].newsize : 0;
	this.oldsize = firstLast ? firstLast[0].oldsize : 0;
	this.diff = this.newsize - this.oldsize;
	this.logtype = '';
	this.logaction = '';
	this.logparams = {};
	this.tags = tags;
	this.count = changes.length;
	this.readcount = additional;
	this.read = allread;
	this.mark = mark;
	this.desc = desc.length ? desc.join(mw.msg('comma-separator')) : '';
	return this;
};

Change.prototype.isRealEdit = function () {
	/*jshint bitwise: false*/
	return !(this.flags & (FLAGS.CATEGORY | FLAGS.WIKIDATA | FLAGS.LOG));
};

Change.prototype.canTrustNotification = function () {
	/*jshint bitwise: false*/
	return !(this.flags & (FLAGS.CATEGORY | FLAGS.WIKIDATA));
};

Change.prototype.getOldPagename = function () {
	return this.oldpagename || false;
};

Change.prototype.isMoveCreate = function () {
	return this.getOldPagename() && this.logparams.suppressredirect !== '';
};

Change.prototype.getPagename = function () {
	return this.pagename;
};

Change.prototype.getPrimarySortkey = function () {
	return this.timestamp;
};

Change.prototype.getSecondarySortkey = function () {
	return this.pagename;
};

Change.prototype.getData = function () {
	return {
		ns: this.ns,
		title: this.title,
		user: this.user,
		timestamp: this.timestamp,
		flags: this.flags,
		comment: this.comment,
		diff: this.diff,
		logtype: this.logtype,
		tags: this.tags.join('|')
	};
};

Change.prototype.renderTags = function () {
	var l = this.tags.length;
	return l ?
		mw.html.element('abbr', {
			title: mw.msg('watchlist++-tags-title', l, this.tags.join(mw.msg('comma-separator')))
		}, mw.msg('watchlist++-tags-letter')) :
		'&nbsp;';
};

Change.prototype.renderFlag = function (and, msg, empty) {
	/*jshint bitwise: false*/
	return (this.flags & and) ?
		mw.html.element('abbr', {
			title: mw.msg('watchlist++-flags-' + msg + '-title')
		}, mw.msg('watchlist++-flags-' + msg + '-letter')) :
		(empty ? '&nbsp;' : false);
};

Change.prototype.renderFlags = function () {
	return this.renderTags() +
		(
			this.renderFlag(FLAGS.CATEGORY, 'category') ||
			this.renderFlag(FLAGS.WIKIDATA, 'wikidata') ||
			this.renderFlag(FLAGS.LOG, 'log') ||
			'&nbsp;'
		) +
		this.renderFlag(FLAGS.ANON, 'anon', true) +
		this.renderFlag(FLAGS.BOT, 'bot', true) +
		this.renderFlag(FLAGS.MINOR, 'minor', true) +
		this.renderFlag(FLAGS.NEW, 'new', true);
};

Change.prototype.renderEditor = function () {
	var editor = this.user, link;
	if (Array.isArray(editor)) {
		if (editor.length === 1) {
			editor = editor[0];
		} else {
			return mw.html.element('span',
				{title: editor.join(mw.msg('comma-separator'))},
				mw.msg('watchlist++-number-editors', editor.length));
		}
	}
	link = mw.html.element('a', {href: mw.util.getUrl('Special:Contributions/' + editor),
		target: '_blank', rel: 'noopener'}, editor);
	return '<span>' + mw.msg('watchlist++-by', link) + '</span>';
};

Change.prototype.renderTitle = function () {
	return mw.html.element('a', {href: mw.util.getUrl(this.pagename), target: '_blank', rel: 'noopener'}, this.pagename);
};

Change.prototype.renderChangeUrl = function () {
	/*jshint bitwise: false*/
	if (this.flags & FLAGS.NEW) {
		return false;
	}
	if (this.flags & FLAGS.LOG) {
		return false;
	}
	if (this.flags & FLAGS.WIKIDATA) {
		//TODO Wikidata liefert keinerlei Hinweise über die API um welche Änderung es wirklich geht
		return 'https://www.wikidata.org/wiki/Special:ItemByTitle/' + mw.config.get('wgDBname') + '/' +
			this.pagename + '?action=history';
	}
	if (!this.revid) { //Gruppe ohne richtige Bearbeitungen
		return false;
	}
	return mw.util.wikiScript() + '?' + (this.oldrevid > 0 ?
		'diff=' + this.revid + '&oldid=' + this.oldrevid : 'diff=prev&oldid=' + this.revid);
};

Change.prototype.renderChangeType = function () {
	var key = 'watchlist++-change-' + this.logtype + '-' + this.logaction;
	if (mw.messages.exists(key)) {
		return mw.msg(key);
	}
	key = 'watchlist++-change-' + this.logtype;
	if (mw.messages.exists(key)) {
		return mw.msg(key);
	}
	if (this.logtype) {
		mw.log.warn('No message for ' + this.logtype);
	}
	return mw.msg('watchlist++-change');
};

Change.prototype.renderChangeTooltip = function () {
	if (this.logtype === 'block' && this.logaction !== 'unblock') {
		return this.logparams.duration;
	}
};

Change.prototype.renderChange = function () {
	var href, title, tooltip = false, additional, html;

	if (this.count) {
		title = mw.msg('watchlist++-changes', this.count);
	} else {
		title = this.renderChangeType();
		tooltip = this.renderChangeTooltip() || false;
	}
	if (this.readcount) {
		additional = mw.html.element('span', {'class': 'td-change-read'},
			mw.msg('watchlist++-changes-read', this.readcount));
	}

	href = this.renderChangeUrl();
	if (href) {
		html = mw.html.element('a', {href: href, title: tooltip, target: '_blank', rel: 'noopener'}, title);
	} else {
		html = title;
		if (tooltip) {
			html = mw.html.element('span', {title: tooltip}, html);
		}
	}
	if (additional) {
		html += ' ' + additional;
	}
	return html;
};

Change.prototype.renderDiff = function () {
	if (!this.isRealEdit()) {
		return '';
	}
	var cssClass, sign = '';
	if (this.diff < 0) {
		cssClass = 'mw-plusminus-neg';
	} else if (this.diff > 0) {
		sign = '+';
		cssClass = 'mw-plusminus-pos';
	} else {
		cssClass = 'mw-plusminus-null';
	}
	return mw.html.element('span',
		{'class': cssClass, dir: 'ltr', title: mw.msg('rc-change-size-new', mw.language.convertNumber(this.newsize))},
		/*mw.msg('rc-change-size',*/ sign + mw.language.convertNumber(this.diff)/*)*/
	);
};

Change.prototype.renderTimestamp = function () {
	function pad (n) {
		return n < 10 ? '0' + String(n) : String(n);
	}

	var d = new Date(this.timestamp), now = new Date(), timestamp;
	if (isNaN(d.valueOf())) {
		return '';
	}

	if (now.getFullYear() === d.getFullYear() && now.getMonth() === d.getMonth() && now.getDate() === d.getDate()) {
		timestamp = mw.msg('watchlist++-hhmm', d.getHours(), pad(d.getMinutes()));
	} else {
		timestamp = mw.msg('watchlist++-ddmm', d.getDate(), d.getMonth() + 1);
	}
	return mw.html.element('a', {
		href: mw.util.getUrl(this.pagename, {action: 'history'}),
		target: '_blank', rel: 'noopener',
		title: d.toLocaleString()
	}, new mw.html.Raw(timestamp));
};

Change.prototype.renderComment = function () {
	var additional, comment;
	switch (this.logtype + '/' + this.logaction) {
	//TODO mehr?
	case 'block/block':
	case 'block/reblock':
		//TODO mehr Informationen
		break;
	case 'delete/revision':
		additional = mw.msg('watchlist++-log-delete-revision');
		break;
	case 'protect/modify':
	case 'protect/protect':
		additional = mw.msg('watchlist++-log-protect', this.logparams.description);
		break;
	case 'rights/rights':
		additional = [
			this.logparams.oldgroups.filter(function (group) {
				return this.logparams.newgroups.indexOf(group) === -1;
			}, this).join('comma-separator'),
			this.logparams.newgroups.filter(function (group) {
				return this.logparams.oldgroups.indexOf(group) === -1;
			}, this).join('comma-separator')
		];
		if (additional[0]) {
			additional[0] = mw.msg('watchlist++-log-rights-remove', additional[0]);
		}
		if (additional[1]) {
			additional[1] = mw.msg('watchlist++-log-rights-add', additional[1]);
		}
		additional = additional.filter(function (msg) {
			return !!msg;
		}).join('comma-separator');
		break;
	case 'upload/overwrite':
	case 'upload/revert':
		additional = mw.msg('watchlist++-log-upload-overwrite');
		break;
	}
	comment = '<span class="comment">' +
		this.parsedcomment.replace(/<a /g, '<a target="_blank" rel="noopener" ') +
		'</span>';
	if (additional) {
		return mw.msg('watchlist++-log-comment', additional, comment);
	}
	return comment;
};

Change.prototype.render = function (options) {
	var classes = [];
	switch (options.type) {
	case 'block':
		classes.push('changes-block');
		classes.push('collapsed');
		break;
	case 'line':
		classes.push('changes-line');
		classes.push('collapsed');
		break;
	case 'single':
		classes.push('changes-single');
	}
	if (this.read) {
		classes.push('change-read');
	} else {
		classes.push('change-unread');
	}
	if (this.mark) {
		classes.push('mark-' + this.mark);
	}
	return mw.html.element('tr', {id: 'uuid-' + this.uuid, 'class': classes.join(' ')}, new mw.html.Raw([
		mw.html.element('td', {'class': 'td-marker', title: this.desc || ''}, new mw.html.Raw(
			mw.html.element('span', {}, mw.msg('watchlist++-mark-symbol')))
		),
		mw.html.element('td', {'class': 'td-collapse'}, new mw.html.Raw('<span></span>')),
		mw.html.element('td', {'class': 'td-title' + (options.titleChange ? ' title-change' : '')},
			new mw.html.Raw(this.renderTitle())
		),
		mw.html.element('td', {'class': 'td-change'}, new mw.html.Raw(this.renderChange())),
		mw.html.element('td', {'class': 'td-user'}, new mw.html.Raw(this.renderEditor())),
		mw.html.element('td', {'class': 'td-flags'}, new mw.html.Raw(this.renderFlags())),
		mw.html.element('td', {'class': 'td-diff'}, new mw.html.Raw(this.renderDiff())),
		mw.html.element('td', {'class': 'td-comment'}, new mw.html.Raw(this.renderComment())),
		mw.html.element('td', {'class': 'td-timestamp'}, new mw.html.Raw(this.renderTimestamp())),
		mw.html.element('td', {'class': 'td-read'}, new mw.html.Raw(
			mw.html.element('a', {href: '#',
				'class': 'read',
				title: mw.msg(options.type === 'line' ? 'watchlist++-read-until' : 'watchlist++-read-all')},
				mw.msg('watchlist++-read-symbol')
			) +
			mw.html.element('a', {href: '#',
				'class': 'unread',
				title: mw.msg(options.type === 'line' ? 'watchlist++-unread-until' : 'watchlist++-unread-all')},
				mw.msg('watchlist++-unread-symbol')
			)
		))
	].join('')));
};

Change.prototype.markAsRead = function () {
	this.read = true;
	$('#uuid-' + this.uuid).removeClass('change-unread').addClass('change-read');
};

Change.prototype.markAsUnread = function () {
	this.read = false;
	$('#uuid-' + this.uuid).removeClass('change-read').addClass('change-unread');
};

Change.prototype.markAsReadUntil = function () {
	this.page.markAsReadUntil(this);
};

Change.prototype.markAsUnreadFrom = function () {
	this.page.markAsUnreadFrom(this);
};

Change.prototype.applyRules = function (rules, notification) {
	if (this.rulesApplied) {
		return this.read;
	}
	var ret = applyRules(rules, this.getData());
	if (ret.mark !== undefined) {
		this.mark = ret.mark;
		this.desc = ret.desc;
	}
	if (!notification || notification > this.timestamp) {
		this.read = true;
	} else if (ret.read !== undefined) {
		this.read = ret.read;
	} else {
		this.read = false;
	}
	this.rulesApplied = true;
	return this.read;
};

function Table (watchlist) {
	this.watchlist = watchlist;
}

Table.prototype.show = function (html) {
	this.updateOlderButton();
	this.updateResetButton();
	if (!html) {
		$('#changes-table').show();
		return;
	}
	$('#changes-table').html(html).show();
	this.fixWidth();
	this.addEventHandlers2();
};

Table.prototype.addEventHandlers1 = function () {
	var updateRules = this.showRules.bind(this);

	$('#rules-add').on('click', function (e) {
		e.preventDefault();
		addRuleDialog(updateRules);
	});
	$('#rules-export-import').on('click', function (e) {
		e.preventDefault();
		if (exportImportRules()) {
			updateRules();
		}
	});
	$('#rules-delete-all').on('click', function (e) {
		e.preventDefault();
		if (window.confirm(mw.msg('watchlist++-rules-delete-all-confirm'))) {
			deleteRules();
			updateRules();
		}
	});
	$('#rules-list').on('click', '.rules-edit', function (e) {
		e.preventDefault();
		addRuleDialog(updateRules, $('#rules-list > li').index($(this).parent('li')));
	});
	$('#rules-list').on('click', '.rules-delete', function (e) {
		var $li, i, rules;
		$li = $(this).parent('li');
		i = $('#rules-list > li').index($li);
		rules = getAllRules();
		e.preventDefault();
		if (window.confirm(mw.msg('watchlist++-rules-delete-confirm', rules[i].desc))) {
			rules.splice(i, 1);
			setRules(rules);
			updateRules();
		}
	});

	this.headButton.on('click', function () {
		if ($('#rules-container.collapsed').length) {
			this.headButton.setIcon('collapse');
			this.headButton.setTitle(mw.msg('watchlist++-rules-head-collapse'));
		} else {
			this.headButton.setIcon('expand');
			this.headButton.setTitle(mw.msg('watchlist++-rules-head-expand'));
		}
		$('#rules-container').toggleClass('collapsed expanded');
	}.bind(this));
	this.toggleReadButton.on('click', function () {
		this.s1.disabled = !this.s1.disabled;
		this.s2.disabled = !this.s2.disabled;
	}.bind(this));
	this.addOlderButton.on('click', function () {
		this.showWatchlistContinue('older');
	}.bind(this));
	this.addNewerButton.on('click', function () {
		this.showWatchlistContinue('newer');
	}.bind(this));
	this.resetNotificationButton.on('click', function () {
		$('#changes-table tr').removeClass('change-unread').addClass('change-read');
		this.watchlist.markAllAsRead();
		this.updateResetButton();
	}.bind(this));
};

Table.prototype.addEventHandlers2 = function () {
	var watchlist = this.watchlist, updateButton;
	function read (el) {
		watchlist.getFromUUID(
			Number($(el).closest('tr').attr('id').replace(/uuid-/, ''))
		).markAsReadUntil();
	}
	function unread (el) {
		watchlist.getFromUUID(
			Number($(el).closest('tr').attr('id').replace(/uuid-/, ''))
		).markAsUnreadFrom();
	}
	updateButton = this.updateResetButton.bind(this);

	$('.td-title a, .td-change a').on('click', function () {
		read(this);
		updateButton();
	});
	$('.td-read .read').on('click', function (e) {
		read(this);
		updateButton();
		e.preventDefault();
	});
	$('.td-read .unread').on('click', function (e) {
		unread(this);
		updateButton();
		e.preventDefault();
	});
	$('.changes-block .td-collapse').on('click', function () {
		$(this).parent('tr').nextUntil('.changes-block, .changes-single').addBack().toggleClass('collapsed expanded');
		updateButton();
	});
};

Table.prototype.fixWidth = function () {
	if (isCompatibleGrid()) {
		return;
	}

	function calculateMaxWidth ($sel) {
		var maxWidth = 0;
		$sel.each(function () {
			var width = $(this).width();
			if (width > maxWidth) {
				maxWidth = width;
			}
		});
		return maxWidth;
	}

	var padding = 25, s = mw.util.addCSS('#changes-table tr { display: table-row !important; }' + getCSS2()),
		$cols = {
			marker: $('.td-marker'),
			collapse: $('.td-collapse'),
			title: $('.td-title'),
			change: $('.td-change'),
			user: $('.td-user'),
			flags: $('.td-flags'),
			diff: $('.td-diff'),
			comment: $('.td-comment'),
			timestamp: $('.td-timestamp'),
			read: $('.td-read')
		}, maxWidths = {
			marker: calculateMaxWidth($cols.marker),
			collapse: calculateMaxWidth($cols.collapse),
			title: calculateMaxWidth($cols.title),
			change: calculateMaxWidth($cols.change),
			user: calculateMaxWidth($cols.user),
			flags: calculateMaxWidth($cols.flags),
			diff: calculateMaxWidth($cols.diff),
			comment: calculateMaxWidth($cols.comment),
			timestamp: calculateMaxWidth($cols.timestamp),
			read: calculateMaxWidth($cols.read)
		}, minWidths = {
			marker: maxWidths.marker,
			collapse: maxWidths.collapse,
			title: Math.min(maxWidths.title, maxWidths.change * 1.75),
			change: maxWidths.change,
			user: Math.min(maxWidths.user, maxWidths.change * 1.25),
			flags: maxWidths.flags,
			diff: maxWidths.diff,
			comment: Math.min(maxWidths.comment, maxWidths.change * 1.25),
			timestamp: maxWidths.timestamp,
			read: maxWidths.read
		}, availWidth = $('#changes-table').width() - padding, minWidth = 0, maxWidth = 0, i;
	$(s.ownerNode).remove();
	for (i in $cols) {
		if (hasOwn.call($cols, i)) {
			minWidth += minWidths[i];
			maxWidth += maxWidths[i];
		}
	}
	$('#changes-table table').width(availWidth/* + padding*/);
	if (maxWidth <= availWidth) {
		minWidths = maxWidths;
	} else if (minWidth >= availWidth) {
		$('#changes-table table').width(minWidth/* + padding*/);
	} else if (maxWidth > minWidth) {
		i = (availWidth - minWidth) / (maxWidth - minWidth);
		minWidths.title += i * (maxWidths.title - minWidths.title);
		minWidths.user += i * (maxWidths.user - minWidths.user);
		minWidths.comment += i * (maxWidths.comment - minWidths.comment);
	}
	for (i in $cols) {
		if (hasOwn.call($cols, i)) {
			$cols[i].width(minWidths[i]);
		}
	}
	$('#changes-table table').css('table-layout', 'fixed');
};

Table.prototype.showRules = function () {
	var rules = getAllRules(), i, bookmarklet = 'javascript', rulesHtml = [];

	function formatFilter (filter) {
		return mw.msg('watchlist++-filter',
			mw.msg('watchlist++-filter-' + filter.key),
			mw.msg('watchlist++-type-' + filter.type + ('not' in filter ? '-not' : '')),
			filter.val
		);
	}

	function formatRule (rule, link) {
		var filters = [], i, html;
		for (i = 0; i < rule.filter.length; i++) {
			filters.push('<li>' + formatFilter(rule.filter[i]) + '</li>');
		}
		html = link ? mw.msg('watchlist++-rules-desc', mw.html.escape(rule.desc), link) : mw.html.escape(rule.desc);
		html += '<br>';
		if ('read' in rule && 'mark' in rule) {
			html += mw.msg('watchlist++-rule-read-mark', rule.read ? mw.msg('watchlist++-rules-read') :
				mw.msg('watchlist++-rules-unread'), rule.mark);
		} else if ('read' in rule) {
			html += mw.msg('watchlist++-rule-read', rule.read ? mw.msg('watchlist++-rules-read') :
				mw.msg('watchlist++-rules-unread'));
		} else {
			html += mw.msg('watchlist++-rule-mark', rule.mark);
		}
		return html + '<ul>' + filters.join('') + '</ul>';
	}

	this.headButton.setLabel(mw.msg('watchlist++-rules-head', rules.length));

	bookmarklet += ':';
	bookmarklet += 'mw.libs.restoreWatchlistRules(' + JSON.stringify(rules) + ')';
	$('#rules-bookmarklet').attr('href', bookmarklet);

	for (i = 0; i < rules.length; i++) {
		rulesHtml.push('<li>' + formatRule(
			rules[i],
			mw.html.element('a', {href: '#', 'class': 'rules-edit'}, mw.msg('watchlist++-rules-edit')) +
			mw.msg('pipe-separator') +
			mw.html.element('a', {href: '#', 'class': 'rules-delete'}, mw.msg('watchlist++-rules-delete'))
		) + '</li>');
	}
	$('#rules-list').html(rulesHtml.join(''));
};

Table.prototype.buildInterface = function ($container, title) {
	var html;

	function Spinner () {
		OO.ui.Element.apply(this, arguments);
		OO.ui.mixin.PendingElement.apply(this, arguments);
	}

	OO.inheritClass(Spinner, OO.ui.Element);
	OO.mixinClass(Spinner, OO.ui.mixin.PendingElement);

	Spinner.prototype.pushPending = function () {
		OO.ui.mixin.PendingElement.prototype.pushPending.apply(this, arguments);
		this.$element.css('height', this.isPending() ? '1em' : '');
	};

	Spinner.prototype.popPending = function () {
		OO.ui.mixin.PendingElement.prototype.popPending.apply(this, arguments);
		this.$element.css('height', this.isPending() ? '1em' : '');
	};

	if (title) {
		$('#firstHeading').text(mw.msg('watchlist++'));
	}

	mw.util.addCSS(getCSS1() + (isCompatibleGrid() ? getCSS4() : ''));
	this.s1 = mw.util.addCSS(getCSS2());
	this.s2 = mw.util.addCSS(getCSS3());
	this.s1.disabled = true;

	html = mw.html.element('div', {id: 'rules-container', 'class': 'collapsed'}, new mw.html.Raw(
		mw.html.element('h4', {id: 'rules-head'}, '') +
		mw.msg('parentheses', [
			mw.html.element('a', {href: '#', id: 'rules-add'}, mw.msg('watchlist++-rules-add')),
			mw.html.element('a', {href: '#', id: 'rules-export-import'}, mw.msg('watchlist++-rules-export-import')),
			mw.html.element('a', {id: 'rules-bookmarklet', title: mw.msg('watchlist++-rules-bookmarklet-title')},
				mw.msg('watchlist++-rules-bookmarklet')),
			mw.html.element('a', {href: '#', id: 'rules-delete-all'}, mw.msg('watchlist++-rules-delete-all'))
		].join(mw.msg('pipe-separator'))) +
		'<ol id="rules-list"></ol>'
	));
	html += mw.html.element('div', {id: 'button-container'}, '');
	html += mw.html.element('div', {id: 'spinner-container'}, '');
	html += mw.html.element('div', {id: 'changes-table'}, '');
	$container.html(html);

	this.headButton = new OO.ui.ButtonWidget({
		icon: 'expand',
		title: mw.msg('watchlist++-rules-head-expand'),
		framed: false
	});
	$('#rules-head').append(this.headButton.$element);

	this.toggleReadButton = new OO.ui.ButtonWidget({
		label: mw.msg('watchlist++-toggle-read')
	});
	this.addOlderButton = new OO.ui.ButtonWidget({
		label: mw.msg('watchlist++-add-older'),
		title: mw.msg('watchlist++-add-older-title')
	});
	this.addNewerButton = new OO.ui.ButtonWidget({
		label: mw.msg('watchlist++-add-newer'),
		title: mw.msg('watchlist++-add-newer-title')
	});
	this.resetNotificationButton = new OO.ui.ButtonWidget({
		label: mw.msg('watchlist++-reset-notification')
	});

	$('#button-container').append(new OO.ui.HorizontalLayout({
		items: [
			this.toggleReadButton,
			new OO.ui.ButtonGroupWidget({
				items: [this.addOlderButton, this.addNewerButton]
			}),
			this.resetNotificationButton
		]
	}).$element);

	this.spinner = new Spinner();
	$('#spinner-container').append(this.spinner.$element);

	this.showRules();
	this.addEventHandlers1();
};

Table.prototype.updateOlderButton = function () {
	this.addOlderButton.setDisabled(!this.watchlist.canGetOlder());
};

Table.prototype.updateResetButton = function () {
	this.resetNotificationButton.setDisabled(!this.watchlist.canResetAll());
};

Table.prototype.showWatchlistInitial = function () {
	this.spinner.pushPending();
	this.watchlist.runApi(function () {
		$(function () {
			this.show(this.watchlist.render());
			this.spinner.popPending();
		}.bind(this));
	}.bind(this));
};

Table.prototype.showWatchlistContinue = function (cont) {
	this.spinner.pushPending();
	this.watchlist.runApi(function (c) {
		if (c) {
			this.show(this.watchlist.render());
		} else {
			this.show();
		}
		this.spinner.popPending();
	}.bind(this), cont);
};

function addWatchlistLink () {
	if (!$('.rcfilters-head').length) {
		$('.mw-watchlist-toollinks a').last().after(
			mw.msg('pipe-separator') +
				mw.html.element('a', {href: mw.util.getUrl('Special:Watchlist++')}, mw.msg('watchlist++'))
		);
	} else {
		mw.util.addPortletLink('p-cactions', mw.util.getUrl('Special:Watchlist++'), mw.msg('watchlist++'));
	}
}

function buildWrapper () {
	var mode = mw.user.options.get('userjs-schnark-watchlistPP-mode'),
		$containerClassical, $containerWatchlistPP;
	if (['classical', 'watchlist++'].indexOf(mode) === -1) {
		mode = 'classical';
	}
	$containerClassical = $('#mw-content-text');
	$containerWatchlistPP = $('<div>');
	$containerClassical.after($containerWatchlistPP);
	if (mode === 'classical') {
		$containerWatchlistPP.addClass('oo-ui-element-hidden');
	} else {
		$containerClassical.addClass('oo-ui-element-hidden');
	}
	buildSwitcher({classical: $containerClassical, 'watchlist++': $containerWatchlistPP}, mode);
	return $containerWatchlistPP;
}

function buildSwitcher (containers, mode) {
	var $currentContainer = containers[mode];
	mw.loader.using(['oojs-ui-widgets', 'mediawiki.api']).then(function () {
		var switcher;
		switcher = new OO.ui.ButtonSelectWidget({
			items: [
				new OO.ui.ButtonOptionWidget({data: 'classical', label: mw.msg('watchlist++-mode-classical')}),
				new OO.ui.ButtonOptionWidget({data: 'watchlist++', label: mw.msg('watchlist++-mode-watchlist++')})
			]
		});
		switcher.selectItemByData(mode);
		switcher.on('select', function (item) {
			var mode = item.getData();
			$currentContainer.addClass('oo-ui-element-hidden');
			$currentContainer = containers[mode];
			$currentContainer.removeClass('oo-ui-element-hidden');
			(new mw.Api()).saveOption('userjs-schnark-watchlistPP-mode', mode);
		});
		$('.mw-indicators').eq(0).empty().append(switcher.$element);
	});
}

function buildWatchlist ($container, title) {
	mw.loader.using([
		'mediawiki.util', 'mediawiki.storage', 'user.options',
		'mediawiki.jqueryMsg', 'mediawiki.special.changeslist',
		'oojs-ui.styles.icons-movement', 'oojs-ui-widgets', 'oojs-ui-windows'
	]).then(function () {
		var watchlist = new Watchlist(), table = new Table(watchlist);
		if (title) {
			document.title = mw.msg('pagetitle', mw.msg('watchlist++'));
		}
		table.buildInterface($container, title);
		table.showWatchlistInitial();
	});
}

function isCompatibleGrid () {
	return window.CSS && CSS.supports &&
		CSS.supports('display', 'grid') &&
		CSS.supports('display', 'contents');
}

if (
	mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' ||
	(mw.config.get('wgNamespaceNumber') === -1 && mw.config.get('wgTitle') === 'Watchlist++')
) {
	$.when(
		mw.loader.using(['user.options', 'mediawiki.util', 'mediawiki.language', 'oojs-ui-core']),
		$.ready
	).then(function () {
		initL10N(l10n, ['colon-separator', 'comma-separator',
			'rc-change-size-new', 'pipe-separator', 'parentheses', 'pagetitle']);
		if (mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist') {
			if (mw.user.options.get('userjs-schnark-watchlistPP-integrate')) {
				buildWatchlist(buildWrapper(), false);
			} else {
				addWatchlistLink();
			}
		} else {
			buildWatchlist($('#mw-content-text'), true);
		}
	});
}

mw.libs.restoreWatchlistRules = setRules;

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