Benutzer:Perhelion/signing.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
/** Description: AUTOMATIC SIGNING: if not sure, warn.
/** Description: AUTOMATIC SIGNING: if not sure, warn.
*** aut. signing / (automatische Unterschrift) ***
* @documentation: see [[w:de:User:Perhelion/signing]]
* @revision: 17:09, 6. Dez. 2017 (CET)
* @authors:
*  created 23.04.2006 [[w:de:User:Olliminatore]] version 1.56 13.03.2007
*  updated 23.04.2006 [[w:en:User:Ilmari Karonen]]
*  updated 19.09.2011 – 29.12.2011 [[User:Perhelion]], FIX for non Gecko
*  updated 31.12.2011 [[w:de:User:PerfektesChaos]], some code improvements
*  updated 19.06.2014 Perhelion, fixes (code tidy up, jshint) gimmicks added
*  updated 09.09.2014 [[w:de:User:Schnark]] (code cosmetic)
*  –2016 Perhelion
* tested only on modern browsers <nowiki>
* @license released dual-licensed under the terms of the GFDL v1.2 or the GPL v2.
* required modules: 'jquery.textSelection','mediawiki.language','mediawiki.util', 'mediawiki.action.edit.preview'
* ToDo:  exlcude LivePreview with session save, use hook content 
**/
/*global mediaWiki:false, jQuery:false, alert:false, OO */
/*jshint bitwise:true, curly:false, eqeqeq:true, forin:true, laxbreak:true,
trailing:true, undef:true, unused:true */
(function ($, mw) {
'use strict';
if (!mw.libs || mw.libs.threadSign) throw new Error('Initializing of threadSign stopped because it was already executed');
var name = 'AutoSign',
	ns = mw.config.get('wgNamespaceNumber'),
	$textarea,
	$editform,
	minoredit,
	txtOld = '', // Original text container
	CmtE = '', // Comment
	txtLen,
	lrLen,
	txtEndLen,
	txtOldEnd,
	reUser = '',
	signTxt = [], // Contains all local text (only En and De current)
	txtOldL;
var c = $.extend({
		usersignature: window.usersignature || '-- ~~\~~', // If both are declared the config is prior!
		sigAccessKey: '>',
		sigText: null // Different styled sig for non automatic
	}, mw.config.get([
				'wgDBname',
				'wgAction',
				'wgContentLanguage', // Only for hi
				'wgRelevantUserName', // ''
				'wgFormattedNamespaces', // ''
				'wgUserName', // ''
				'wgPageName',
				'wgIsArticle'
				// "wgUserLanguage",
				// "wgTitle"
			]));

var ts = {
	version: 'v1.93d',
	$sigBox: $(),
	$label: $(),
	onoff: $.noop,
	init: function () {
		if (ts.config instanceof Object)
			c = $.extend(c, ts.config);
		if (mw.config.get('wgIsArticle')) return; // Only if editing
			
		var newSection = /section=new/.test(location.search); // Newsections are potential (not always) talkpages
		if (!c.sigText) // Not change sig
			c.sigText = c.usersignature;

		c.sigText = ' ' + $.trim(c.sigText) + '\n'; // Add whitespace
		var regpages = null, // New Array(); used in 2 different ways
			whitelist = [], // String list
			blacklist = [], // Regexp list
			p,
			len;
		$textarea = $('#wpTextbox1');
		if (!$textarea.length) return;
		minoredit = document.forms.editform.wpMinoredit;
		txtOld = $.trim($textarea.val());
		txtOldL = txtOld.length;

		if (!minoredit || newSection || !txtOldL)
			c.dSum = ''; // No auto-summary
		if (ns === 4) {
			if (c.regpages) { // Option for affected pages
				regpages = c.regpages;
				if (!$.isArray(regpages) || typeof regpages[0] !== 'string')
					return alert('Config error: "' + regpages + '" must be of type string array.');
			}
			blacklist = [// Default
				/\/[Ii]ntro$/, // Intro subpage
				/\/[Hh]eader$/, // Header subpage
				/\/[Aa]rchive?[ _/]/, // Archiv subpage
				// :DR/2016/03/23
				/\/\d{4}\/\d\d\/\d\d$/
			];
			switch (c.wgDBname) { // Search forum pages TODO: API query from Wikidata could be for internationalization
			case 'enwiki': // Defaults for en:Wikipedia
				whitelist = [// List of all local non-talk-pages, -nns = no new section
					':Requests for undeletion', // -nns
					':Requests for adminship/', // -nns
					':Requests for arbitration/', // -nns
					':Requests for permissions', // -nns
					' for deletion', // Articles , Miscellany, Templates // -nns
					' for discussion', // -nns
					':Dispute resolution noticeboard', // -nns
					':Adminship survey', // -nns
					':Deletion review', // -nns
					':Cleanup', // -nns
					':SVG help', // -nns
					':Arbitration/', // -nns
					':Village pump',
					':Articles for deletion',
					'.*noticeboard.*',
					':page protection',
					'mediation)',
					':Bot requests',
					':Help desk',
					':Editor review',
					':Media copyright questions'
				];
				break;
			case 'dewiki': // Defaults for de:Wikipedia
				whitelist = [
					':Löschkandidaten/', // -nns
					'erkstatt', //Grafik -nns
					'wünsche', // Entsperr, Bilder -nns
					':Auskunft',
					':Café',
					':Fragen von Neulingen',
					':Fragen zur Wikipedia',
					':Verbesserungsvorschläge',
					':Urheberrechtsfragen',
					':Kandidat', //en, uren
					':Löschprüfung',
					':Sperrprüfung',
					'Meinung', //sbilder, Dritte
					':Qualitätssicherung/',
					' Bilder', // Diskussionen über, , Redaktion
					':Review',
					':Vandalismusmeldung',
					'/Anfragen', //WP:A/A Bots/
					'Notizen', //WP:A/N
					':Tellerrand',
					':WikiProjekt Vorlagen',
					':Projektdiskussion',
					':WikiProjekt Wappen',
					':Redaktion '
				];
				break;
			case 'commonswiki': // Wikimedia Commons // TODO: ATTENTION on whole project should ns default on, because multilingual
				whitelist = [
					'illage pump', // En
					':Forum', // De
					':Help desk',
					':Bots/',
					' requests/', // Deletion
					' candidates/', // Quality
					':Administrators\' ',
					':Translators\' noticeboard',
					' for discussion/',
					':Graphic Lab'
				];
				break;
			}
			for (p = 0, len = blacklist.length; p < len; p++) { // If page in blacklist empty whitelist
				if (blacklist[p].test(c.wgPageName)) {
					whitelist = '';
					break;
				}
			}
			regpages = (regpages && whitelist) ? whitelist.concat(regpages) : whitelist;
			if (newSection /*|| $('#ca-addsection').length*/)
				regpages = ''; // Talkpage

			if (whitelist) { // Not on blacklist
				if ($.isArray(regpages)) // need check
					for (p = 0, len = regpages.length; p < len; p++) {
						if (c.wgPageName.indexOf(regpages[p].replace(/ /g, '_')) !== -1) {
							regpages = '';
							break;
						}
					}
				if (c.wgDBname === 'enwiki') { // special cat for enwiki
					$('.hiddencats li').children('a').each(function () { // check first for cat
						if (this.text && /Non-talk pages that are automatically signed$/.test(this.text))
							regpages = '';
					});
				}
				/** Only on ns:4 in edit on "non" talk pages (and "non" new-section and not at blacklist) to check possible forum page. NOTE: [[phab:T22177]]
				Get all included "pageprops" from the wiki page to recognize pages with newsectionlink. **/
				if (regpages) { // Not registered // anyway if not check per api??? should never happen
					$.when(mw.loader.load('mediawiki.util'), $.ready).then(function () {
						$.getJSON(mw.util.wikiScript('api'), {
							action: 'query',
							prop: 'pageprops',
							indexpageids: true,
							titles: c.wgPageName,
							cache: true,
							format: 'json',
							timeout: 3000
						}).done(function (json) {
							if (!json || !json.query || !json.query.pages)
								return console.log('could not get page info!');
							json = json.query.pages[json.query.pageids];
							if (json && json.pageprops && typeof json.pageprops.newsectionlink !== 'undefined') { // Newsectionlink=""
								if ('Perhelion' === c.wgUserName) alert('AutoSign: Page was still not registered!');
								c.regpages = '';
							}
						}).always(function () {
							ts.exec();
						});
					});
				} else {
					c.regpages = '';
					return ts.exec();
				}
			}
		} else {
			if (ns % 2 === 1)
				regpages = ''; // Always talk-pages
			c.regpages = regpages; // If regpages is null, ns is not valid
			if (regpages === '' || ns === 2)
				return ts.exec();
		}
	},
	
	/** Count occurrences
	 * @para {string}
	 * @return single tuple {array of numbers}
	 **/
	_cntOcc: function (txt) {
		// Sig-counter
		var sig = /~{3}/g,
		falseRe = /(\{\{subst\:unsig.*?\}\})|(<(nowiki|pre)>[\s\S]*?<\/(nowiki|pre)>)|(<!--[\s\S]*?-->)/ig,
		count = 0, // counter (falseRe)
		cnt2 = 0, // False sig-counter (count)
		cnt = 0; // General sig-counter (sig)
		while (sig.test(txt))
			cnt++;
		count = txt.match(falseRe) || [];
		for (var i = 0; i < count.length; i++) {
			while (sig.test(count[i]))
				cnt2++;
		}
		mw.log(cnt, cnt2);
		return [cnt, cnt2];
	},

	setIndentation: function (e) {
		var $textarea = $(e.target);
		var txt = $textarea.val();
		// lrI = txt.lastIndexOf(r) + 1, // Last line index
		var pos = $textarea.textSelection('getCaretPosition'); // current linebreak
		if (!pos)
			return;
		var txtE = txt.slice(pos);
		txt = txt.slice(0, pos); // all text before
		var lrI = txt.lastIndexOf('\n');
		var line = txt.slice(lrI + 1, pos);
		this.indent = ""; // Gimmik for remove last indent
		if (line) {
			var lrM = line.match(/^[:*#]+ ?/); // last indent match
			if (lrM) {
				var lrLen = lrM[0].length;
				//log("line pos: '%s'", line, line[0],lrLen, pos , lrM);
				if (lrLen) {
					$textarea.val(txt + '\n' + lrM[0] + txtE);
					this.indent = [pos, lrLen];
					lrLen++;
					$textarea.textSelection('setSelection', {
						start: pos + lrLen
					});
					e.preventDefault();
				}
			}
		}
		return;
	},

	rmvIndentation: function (e) {
		var $textarea = $(e.target);
		var txt = $textarea.val();
		var pos = this.indent[0];
		if (pos) {
			pos++;
			var txtE = txt.slice(pos + this.indent[1]);
			txt = txt.slice(0, pos); // trim last indentation
			this.indent = "";
			$textarea.val(txt + txtE);
			$textarea.textSelection('setSelection', {
				start: pos
			});
			return e.preventDefault();
		}
	},

	/**
	 *  @brief Search Html comment span at text-end 
	 *  
	 *  @param [string] txt
	 *  @return stript txt and CmtE
	 */
	stripeCmt: function (txt) {
		var cmtRe = /(<!--[\s\S]*?-->)(?!\n*<!--)/g;
		var cmtRight = '';
		CmtE = ''; // restet global var
		while (cmtRe.test(txt)) { // Really html comment
			CmtE = '\n' + RegExp.lastMatch;
		}
		if (CmtE) {
			var cmtEndI = cmtRe.lastIndex;
			//log("Comment at text end found. " , "/(<!--[^]*?-->)(?!\n*<!--)/.test(txt)", txt.length, CmtE, cmtEndI );
			if (cmtEndI < txt.length && cmtEndI > txtLen) { // some added rigth
				cmtRight = txt.slice(cmtEndI); // RegExp.rightContext
			}
			txt = $.trim(txt.slice(0, cmtEndI - CmtE.length));
			CmtE += cmtRight;
		}
		return txt;
	},

	/** Remove last trailing indent and auto. salute and save txt to DOM **/
	rmvIndent: function (txt, cmtE, pos) {
		var lrLi = 0;
		// txt = $.trim(txt);  // need trim entire content? can't because we need trim reUser too
		//log('txt before rmvIndent', txt, "\n--SPLIT--\n",  cmtE,"\nlrLen:", lrLen, " pos:", pos, reUser);
		if (lrLen) { // Only if was set
			if (cmtE) { // Remove indent before comment
				txt = ts.stripeCmt(txt);
				cmtE = CmtE; // CmtE has higher scope
				/* var txtStr = stripeCmt(txt);
				if ( !/-->$/.test(txt) ) {
				if ( ts.cmtEndI > txtLen ) {// post after Cmt ???
				var cmtRight = txt.slice(ts.cmtEndI);
				console.log( "Where is the Cmt? txt.length: %i, txtLen: %i, cmtEndI: %i,  cmtE.length: %i", txt.length, txtLen, ts.cmtEndI,  cmtE.length, txt, "\n--SPLIT--\n",  cmtE)
				cmtE += cmtRight; // add text after cmt
				//if (! cmtE) $textarea.val(txt);
				//return txt;
				} else  cmtE = ''; // Comment removed or not anymore at end
				txt = txtStr;
				} */
			}
			lrLi = txt.lastIndexOf('\n'); // Last reply-line index
			//log( "Trimmed last indent line. ",lrLi, new RegExp("\\n[:*# ]+\\s*" + $.trim(reUser) + "\\s*").test(txt.slice(lrLi)) + '\n"'+ txt.slice(lrLi) + '"' );
			txt = txt.slice(0, lrLi) + txt.slice(lrLi).replace(new RegExp('\\n[:*# ]+ *' + $.trim(reUser) + ' *$'), '') + cmtE;
			reUser = '';
			lrLen = ''; // global reset
		}
		$textarea.val(txt);
		if (pos)
			$textarea.textSelection('setSelection', {
				start: pos // - 1 ?
			}).textSelection('scrollToCaretPosition').focus();
		return txt;
	},

	/**
	 * @brief Get users on topic text
	 * @return [array tuple [string]] posterList (last first)
	 **/
	getUser: function () {
		// lrR = $textarea.textSelection('getCaretPosition');
		var txt = txtOld;
		// \[\[(?:user(?:in)?|User talk|User(?:[ _]talk)?): ?([^\]\|[]+)(?:\|([^\]\[]+))?\]\]
		var strRe = new RegExp('\\[\\[(?:' + c.wgFormattedNamespaces[2] +
				'(?:in)?|' + c.wgFormattedNamespaces[3] +
				'|User(?:[ _]talk)?): ?([^\\]\\|[]+)(?:\\|([^\\]\[]+))?\\]\\]', 'ig');
		var topicPosters = txt.match(strRe) || [];
		var posterList = [];
		// Unique
		topicPosters = $.grep(topicPosters.reverse(), function (p, i) {
				return $.inArray(p, topicPosters) === i;
			});
		var u = topicPosters.length,
		reUser,
		userDict = {}; // : [id, name]
		while (u--) // (reverse order)
			while (strRe.test(topicPosters[u])) {
				reUser = RegExp.$1;
				if (!mw.util.isIPAddress(reUser, false))
					posterList.push([RegExp.lastMatch, reUser, RegExp.$2]);
			}
		// console.log(posterList);
		posterList = $.grep(posterList, function (p, i) { // Unique
				return $.inArray(p, posterList) === i;
			});
			
		u = posterList.length;
		var d = 0; // delete counter
		while (u--) {
			reUser = posterList[u];
			var userMatch = reUser.shift();
			if (reUser[1]) { // detailed check
				if (new RegExp(c.wgFormattedNamespaces[3] + '|User[ _]talk', 'i').test(userMatch))
					reUser.pop();
				else {
					reUser[1] = $.trim(reUser[1]);
					if (reUser[1].indexOf(' ') > 3)
						reUser.pop();
				}
			}
			var user = reUser[0] || '';
			user = $.trim(user.replace(strRe, '$1'));
			
			if (user) {
	// console.log(u, user, reUser[1]);
				if (user.indexOf('/') !== -1)
					user = user.split('/')[0];
				else if (user.indexOf('#') !== -1) {
					user = user.split('#')[0];
				}
				if (reUser[0] !== user) {
					reUser = [user]; // remove 2.
				} else if (reUser[1] && reUser[1] === user)
					reUser.pop(); // remove 2.
				if (user === c.wgUserName || userDict[user] && (!reUser[1] || userDict[user][1])) {
	// console.log("splice", u, d, posterList[u],"==",user, userDict[user]);
					posterList.splice(u, 1);
					d++;
				} else if (userDict[user]) { // Strong dupe with 2., so replace
					posterList.splice(userDict[user][0] - d, 1);
					d++;
	// console.log("Strong dupe",u, d, userDict[user][0] - d, userDict[user]);
					posterList[userDict[user][0] - d] = reUser;
				} else {
					userDict[user] = [u, reUser[0]];
					posterList[u] = reUser;
				}
			}
		} // @reverse only yet because dupe filter
		// return posterList.reverse();
		return posterList.reverse();
	},

	/**
	 * Converts the raw data to DOM objects
	 * @param {event} e
	 * @param {string} userName
	 * @param {string} userNick
	 **/
	createDropDown: function (e) {
		e.stopPropagation();
		e.preventDefault();
		var tar = e.target, userName, userNick;
		var selOkTxt = '--- OK ---'; // Button option for multiple select
		var posterList = this.getUser();
	// console.log("posterList", posterList.length, posterList);
		posterList = $.grep(posterList, function (p) {
				return (p && p[0] !== c.wgRelevantUserName);
			});
		var u = posterList.length;
		var posterLen = u;
		while (u--) {
			var reUser = posterList[u];
			userName = reUser[0];
			userNick = (reUser[1]) ? reUser[1] : '';
	// log(u, reUser, reUser[1], userName, userNick);
			// FIXME: sometimes  userName is empty on clutter load!?
			userNick = (userNick || userName).replace(/\w\-+\w/g, ' ').replace(/(\w)[\d-\.]+|[\d-\.]+(\w)/g, '$1$2'); // cleanup
			posterList[u] = $('<option>', {
					value: userName,
					text: userNick
				});
		}
		/**
		 * Get selected drop-down values
		 * @param {HTMLobject} tar
		 * @return {array} userName
		 **/
		var getSelectedOptions = function getSelectedOptions(tar, event) {
			var sel = this || document.getElementById('mention-select'),
			opts = sel.options,
			oLen = opts.length - 1,
			bOk;
	// console.log("getSelectedOptions", tar, userName);
			ts.userNames = '';
			// if (!userName || !$.isArray(userName)) // change to array
			userName = [];
			if (oLen > 0) {
				var ind = opts[opts.selectedIndex];
				if (ind && ind.value !== selOkTxt) {
					if ( tar.title === 'Ping' && event.ctrlKey ) {
							bOk = opts[oLen].selected;
							for (var i = 0; i < oLen ; i++) {
								var opt = opts[i];
								if (opt.selected)
									userName.push(opt.value);
							}
					} else {
						var $sel = $(sel).hide();
						userName = [ind.value, ind.text];
						if (tar.title === 'Ping') {// stripe nick
							userName.pop();
							ind.value = opts[oLen].value;
							ind.selected = false;
						}
				// console.log(tar.title, userName, sel, $sel);
						window.setTimeout(function () {
							$sel.show();
						}, 200);
					}
					if (!event.ctrlKey || bOk)
						return tar.onclick(userName);
					ts.userNames = userName;
				}
				else return tar.onclick('');
			}
		};
		if (posterLen < 2) {
			userName = [userName];
			if (tar.title !== 'Ping')
				userName.push(userNick);
			return tar.onclick(userName);
		}
		var $sel = $('<select>', {
				id: 'mention-select',
				multiple: 1
			}).on('click', function (e) {
				getSelectedOptions(tar, e);
			}).append(posterList).on("blur", function (e) {
				if (!e.ctrlKey)
					$(this).parent().remove();
			});
		if (tar.title === 'Ping')
			$sel.append($('<option>', {
					style: 'text-align:center',
					text: selOkTxt
				}));
		$sel.attr("size", Math.min(6, posterLen));
		$sel = $('<div>').css({
				position:"absolute",
				top: tar.offsetTop - 22,
				left: tar.offsetLeft + 24,
				zIndex: 77
			}).append($sel);
		$(tar).parent().append($sel);
		$sel.children().first().focus().find("option:first").prop("selected", 1);
		// return false;
	},

	/** Ping template (per button) in textarea
	 * @param {event/array} e/userName
	 * @param {array} ts.userNames (multiple)
	 **/
	setPing: function (e) {
		if (e.target && !($textarea.textSelection('getSelection') || ts.userNames))
			return this.createDropDown(e);
		else if (ts.userNames && e.target) {
			e = ts.userNames;
			ts.userNames = '';
		}
		e = (e && e[0]) ? e.join('|') : '';
		return $textarea.textSelection('encapsulateSelection', {
			pre: '{{Ping|',
			peri: e,
			post: '}}'
		});
	},

	/** Link user
	 * @param {event/array} e/userName
	 **/
	mentionUser: function (e) {
		var isSel = $textarea.textSelection('getSelection');
		if (e.target && !(isSel || e.ctrlKey))
			return this.createDropDown(e);
		return $textarea.textSelection('encapsulateSelection', {
			pre: '[[' + c.wgFormattedNamespaces[2] + ':',
			peri: (e[0] || '') + '|' + (e[1] || ''),
			post: isSel ? '|]]' : ']]'
		});
	},

	enable: function () {
		if (this.enabled)
			return;
		this.toggleSigBox();
		this.enabled = true;
		this.onoff(1);
	},
	disable: function () {
		if (!this.enabled)
			return;
		this.toggleSigBox();
		this.enabled = 0;
		this.onoff(0);
	},
	
	exec: function () {
		$editform = $('#editform');
		var r = '\n',
			sum = document.forms.editform.wpSummary, // oojs
			n = /\n/,
			//maxlLength = 40, // CUSTOM: line-length for trim sigtext css
			bSig = !(c.regpages !== '' || ns === 2); //  Recognize on non talk-pages

		if (window.wikEd)
			return alert(name, 'is not compatible with wikEd');
		if (bSig)
			ts.useLivePreview();

		$textarea.on('keypress', function (e) { // Gimmik automatic indention
			if (e.which === 13) // enter
				return ts.setIndentation(e);
			if (e.which === 8 && ts.indent) // backspace
				return ts.rmvIndentation(e);
			ts.indent = ""; // reset
		});

		/** Localized strings **/

		if (mw.language && $.inArray('de', mw.language.getFallbackLanguageChain()) !== -1) // Local translations
			// FIXME: could use int: mw.msg( 'prefs-signature') still not exists !?
			signTxt = {
				boxTitle: 'Signiere automatisch. ',
				boxText: 'Signieren',
				//'formWarn' : "Automatische Signatur: Kein Bearbeitungsfeld gefunden!",
				btnAlt: 'Manuelle Signatur',
				confirm: 'Es wurde keine Signierung gefunden. Trotzdem fortfahren?'
			};
		else
			signTxt = {
				boxTitle: 'Sign this edit automatic. ',
				boxText: 'AutoSign',
				//'formWarn' : 'Automatic thread signing: No edit field found!',
				btnAlt: 'Manual signing',
				confirm: 'No signing was found. Continue anyway?'
			};
		signTxt.hi = ($.inArray(c.wgContentLanguage, ['de', 'nn', 'da']) !== -1) ? 'Hallo ' : 'Hey ';
		if (!$editform.length)
			return;

		/** Put outent string (per button) in textarea **/
		ts.setOutdent = function () {
			var txt = $textarea.val();
			var outdent = (c.wgDBname === 'enwiki') ? '\n{{od}}' : '\n:┌{{padleft:┘|22|─}}\n'; // ToDo: 22 could be dynamically?
			var pos = $textarea.textSelection('getCaretPosition');
			if (pos < 0) // Go text-end, no caret position found
				pos = txt.length;
			//log('the last 2 signs before and last 3 signs after setOutdent: %O%O%O%O%O',txt[pos-2],txt[pos-1],txt[pos],txt[pos+1],txt[pos+2]);
			if (n.test(txt[pos]) && !n.test(txt[pos - 1]))
				pos++; // Trim
			if (txt[pos + 1] && n.test(txt[pos + 1]))
				pos++; // Redundant new line
			ts.rmvIndent($.trim(txt.slice(0, pos)) + outdent + txt.slice(pos), CmtE);
		};

		/** Put sig in textarea **/
		this.doSig = function (e, pos) {
			var txt = $textarea.val();
			var sigText = c.sigText;

			if (!pos) { // Post-end (from button)
				pos = $textarea.textSelection('getCaretPosition');
				// sigText = sigText.replace( /\n+$/, '' );
			} else {
				if (pos < 0) // Go text-end, no caret position found
					pos = txt.length;
				var tmpRe = /\}\}\s*$/;
				if (tmpRe.test(txt.slice(pos - 4, pos))) { // If a template on post end
					var lrIndex = txt.slice(0, pos).replace(/\s+$/, '').lastIndexOf(r); // Last post line
					var tmpTxt = txt.slice(lrIndex, pos);
					//log('Template found on post ', lrIndex, tmpTxt );
					if (/\|1\s*=[^\n]*(\}\})\s*$/.test(tmpTxt)) { // Check last line for propper template
						tmpRe = tmpTxt.search(tmpRe);
						//log('Propper template on post end? %i : "%s"',  tmpRe , RegExp.$1, tmpTxt.length );
						pos = lrIndex + tmpRe;
						sigText = sigText.replace(/\n+$/, '');
					}
				}
			}

			/*sigText = ( pos > maxlLength && !n.test( txt.slice( pos - maxlLength, pos ).replace( /\n+$/, '' ) ) ) ? // Check line length (*it's more a gimmick: <br> and *, #, : are not recognized)
			sigText : sigText.replace( /white-space:nowrap;?/, '' ).replace( ' style=""', '' ); // If short then not needed (only simple remove for default sigText) */
			//log('the last 2 signs before and last 3 signs after signing: "%O%O" : "%O%O%O"',txt[pos-2],txt[pos-1],txt[pos],txt[pos+1],txt[pos+2]);
			//log('txt.slice(pos %i, pos) %o length: %o , pos: %i', txtEndLen, txt.slice(pos-txtEndLen, pos), txt.slice(pos-txtEndLen, pos).length, pos);
			if (txt[pos] && n.test(txt[pos]) && !n.test(txt[pos - 1]))
				pos++; // Trim
			if (txt[pos + 1] && n.test(txt[pos + 1]))
				pos++; // Redundant new line
			this.rmvIndent(txt.slice(0, pos).replace(/\s+$/, '') + sigText + txt.slice(pos), CmtE, pos);
			if (c.dSum) {
				if (!/^(?:(\/\*.+?\*\/ *))([^ ])|^([^/]+?)(?!\/\*.+?\*\/ *)/.test(sum.value)) { // If nothing, exept section title
					sum.value += c.dSum; // Add a optional default summary
				}
				c.dSum = ''; // Only once
			}
			return e;
		};

		/* For POST get checkbox value in preview- and diff- form submit.
		Save only if should no sig set (with 'true').
		Load previous checkbox setting from wpAutoSummary (hack); true means not checked */
		// the sig checkbox.
		var oBox = {
			'type': 'checkbox',
			'name': 'wpAutoSign',
			'id': 'wpAutoSign',
			'checked': bSig,
			'accessKey': c.sigAccessKey,
			selected: bSig
		};
		var oLabel = {
				'for': 'wpAutoSign',
				title: signTxt.boxTitle + ts.version,
				align: 'inline',
				// text: signTxt.boxText,
				label: signTxt.boxText
		};
		var cURL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/';
		var $sBtn = $('<img>', { // New sig button
				src: cURL + '5/52/Tango_style_insert-signature_icon.svg/22px-Tango_style_insert-signature_icon.svg.png',
				'title': 'AutoSign',
				'rel': 'AutoSign',
				'alt': signTxt.btnAlt,
				'width': '22',
				'onclick': 'mw.libs.threadSign.doSig(event)', // Attention: validity get not checked as with submit buttons
				'height': '22',
				'style': 'cursor:pointer',
				'class': 'tool'
			}); // New sig button
		var $pBtn = $sBtn.clone().attr({
				src: cURL + 'f/fa/Tango_style_ping_icon.svg/22px-Tango_style_ping_icon.svg.png',
				title: 'Ping',
				alt: '',
				rel: 'Ping',
				onclick: 'mw.libs.threadSign.setPing(event)'
			}); // Ping button
		var $mBtn = $sBtn.clone().attr({
				src: cURL + '6/6b/Flow-mention.svg/22px-Flow-mention.svg.png',
				title: 'Mention',
				alt: '',
				rel: 'Mention',
				onclick: 'mw.libs.threadSign.mentionUser(event)'
			}).css({
				opacity: '.6'
			}); // Mention button
		var $oBtn = $sBtn.clone().attr({
				src: cURL + 'b/bb/Tango_style_outdent_icon.svg/22px-Tango_style_outdent_icon.svg.png',
				title: 'Outdent',
				alt: '',
				rel: 'Outdent',
				onclick: 'mw.libs.threadSign.setOutdent()'
			}); // Outdent button
		var threadSignBtns = [
			$sBtn,
			$oBtn,
			$pBtn,
			$mBtn
		];
		/** Check for HTML comment at end (like in graphic-labs) **/
		var txt = txtOld; // Make copy
		if (!txt) {
			if (ns === 3 && !minoredit && c.wgRelevantUserName !== c.wgUserName) { // Greetings gimmick, only on new section
				txt = signTxt.hi + c.wgRelevantUserName + ',\n';
				$textarea.val(txt).textSelection('setSelection', {
					start: txt.length - 1
				}).focus(); // Set caret at end
			}
		} else if (/-->$/.test(txt)) // Check last 3 chars
			txt = ts.stripeCmt(txt); // Comment found

		/* Gimmick:	Put automatically indent text for reply if possible
		(if not used, rmvIndent() executed in doSign() or doSig()) */
		txtLen = txt.length;
		var lrI = txt.lastIndexOf(r) + 1, // Last line index
		lrR = txt.slice(lrI),
		lrM = lrR.match(/^[:*#]+/); // Last reply-line match

		lrLen = (lrM) ? lrM[0].length : 0; // new reply indent length
		// Text-End-Length: CUSTOM negative, max 18 characters
		txtEndLen = (Math.min(18, lrR.length) + lrLen) * -1;
		txtOldEnd = txtOld.slice(txtEndLen);
		//log("last line: ",txtEndLen, lrR,lrLen,lrM, c.autoSalut);
		if (c.wgAction === 'edit') { //  Do only on first load
			if (lrLen && lrI + lrLen + 1 < txtLen) { // Only if text on indent
				lrI = ':';
				lrLen++;
				if (lrLen === 2 && lrM[0] !== ':') { // Mainly no indent raise for votes
					lrI = '';
					lrLen = 1;
				}
				$textarea.val(txt + r + lrM[0] + lrI + ' ' + CmtE);
				//log("Last indent reply line setted at. ", txtLen, lrLen);
				txtLen += lrLen + 2;
			} else if (!lrLen && c.autoSalut && txtOldEnd && /\d{4} \((?:CES?T|UTC)\)[^\d]*?$/.test(lrR)) { // New thread salut only Opt-in
				lrLen++;
				reUser = this.getUser().pop() || "";
				lrI = '';
				if (reUser) {
					lrI = ': ';
					reUser = (reUser[1] && /\w+/.test(reUser[1])) ? reUser[1] : reUser[0];
					// FIXME: reUser[1] could included HTML?
					if (reUser)
						reUser = signTxt.hi + reUser.replace(/\d+$/g, '') + ', '; // one wspace
					lrLen += reUser.length;
				}
				txtLen += lrLen + lrI.length;
				$textarea.val(txt + r + lrI + reUser + CmtE);
			} else {
				lrLen = 0;
				if (txt) { // Start always with new line?
					txtLen++;
					$textarea.val(txt + r + CmtE);
				}
			}
		}
		if (bSig && txt)
			$(window).on('load', function () { // Scroll bottom on start
				$textarea.textSelection('setSelection', {
					start: txtLen
				})
				.textSelection('scrollToCaretPosition').focus();
			});
		//log("INIT: txtOldEnd: %s, lrLen = %s", txtOldEnd, lrLen);
		if (minoredit) {
			this.offSigBox(minoredit); // Run function
			$(minoredit).on('change', this.offSigBox); // Add function
		}
		$('#wpSave, #wpPreview, #wpDiff, #wpMinoredit').on("click", {
			name: name
		}, ts.doSign);
		
		var makeCheckbox = function ($label) {
			// ts.$sigBox.on('change', ts.toggleSigBox);
			return $label
				.css({
					'padding': '2px 6px',
					'border-color':(!bSig? '#C00;color:#A00':'')
				})
				.addClass((bSig? 'usermessage' : ''))
				.after(threadSignBtns);
		};
		
		var makeCheckboxOOUI = function () {
			mw.loader.using('oojs-ui-core').done(function () {
				var checkbox = new OO.ui.CheckboxInputWidget(oBox);
				var field = new OO.ui.FieldLayout(checkbox, oLabel);
				ts.onoff = function (on) {
					checkbox.setSelected(on);
				};
				
				ts.$sigBox = checkbox.$element.find('input').updateTooltipAccessKeys();
				ts.$label = makeCheckbox(field.$element.find('label'));
				ts.$sigBox.on('change', ts.toggleSigBox);
				checkbox.on('change', function () {
					if (checkbox.isSelected())
						ts.enable();
					else
						ts.disable();
				});
				// oo-ui-fieldLayout-body
				$('.editCheckboxes > .oo-ui-layout').append(field.$element);
			});
		};
		
		var makeCheckboxCommon = function () {
			oLabel.text = oLabel.label; // Use OOUI label
			var $label = $('<label>', oLabel),
				$checkbox = $('<span>').addClass('oo-ui-fieldLayout-body');
			ts.$sigBox = $('<input>', oBox)
				.on('change', ts.toggleSigBox).updateTooltipAccessKeys();
			ts.$sigBox.on('change', ts.toggleSigBox);
			$checkbox.append(ts.$sigBox, $label);
			ts.$label = makeCheckbox($label);
			$('.editCheckboxes').append($checkbox);
		};
		
		// Add sig controls to the bottom Wiki-edit-options (if all code passed).
		if (!ts.$sigBox.length) { // Only once
			 // Insert signing checkbox in ".editCheckboxes"
			if ($('#wpSummaryLabel').hasClass('oo-ui-layout'))
				makeCheckboxOOUI();
			else
				makeCheckboxCommon();
		}
		//log(name, bSig,c.regpages,$("#wpAutoSign").prop('checked', bSig));
	}, // exec end

	useLivePreview: function () { // LivePreview needed
		if (((minoredit && !minoredit.checked) || ts.$sigBox.prop('checked'))
			 && mw.user.options.get('uselivepreview') != 1) {
			mw.user.options.set('uselivepreview', 1);
			mw.loader.load('mediawiki.action.edit.preview');
		}
	},

	toggleSigBox: function () {
		ts.useLivePreview();
		ts.$label.toggleClass('usermessage');
	},

	offSigBox: function (e) { // Switch enable box
		if (e.type && e.type === 'change')
			e = e.target;
	// if (e === minoredit) {
		if (!ts.$sigBox.prop('checked')) 
			return;
		 // console.log (ts.$sigBox.prop('disabled'));
	// } else ts.toggleSigBox();
		ts.$sigBox.prop('disabled', e.checked);
	},

	doSign: function (e) {
		/**
		 *  @brief Try check edit for sure: return pos
		 *  
		 *  @param [string] c for check
		 *  @param [number] pos for position
		 *  @return pos
		 */
		function _chkEdit(c, pos) {
			pos = txt.indexOf(c, pos--); // Go line-end (caret, really in last line in post)
			if (pos > 1) { // not last line
				var txtEnd = txt.substr(pos, 24).replace(/(^\s+)/, ''); // CUSTOM value 24? try after post-end; trimLeft = RegExp.$1
				var txtNewEnd = txt.slice(pos - 16, pos); // CUSTOM for compare only: 16 chars tolerance before post-end
				var oldp = (c === '\n') ? txtOld.lastIndexOf(txtEnd) : 0; // Old pos off test-end, not if CmtE
				if (!oldp && /-->$/.test(txtEnd)) { // test pos is not in HTML comment
					c = txt.lastIndexOf('<!--');
					if (c < pos) // pos is illegal in cmt
						pos = c--;
					//oldp = 0;
				}
				// if some added
				if (oldp !== -1 && oldp < pos - 3 && txtOld.indexOf(txtNewEnd + RegExp.$1 + txtEnd) === -1) {
					//log('Check edit? txtEnd: "%s",\ntxtNewEnd: %s, oldp: %i, pos: %i, RegExp.$1: "%s" ', txtEnd, txtNewEnd, oldp, pos, RegExp.$1);
					return pos;
				}
			}
		}

		var tolerance = 4;
		var txt = $.trim($textarea.val());
		if ((minoredit && minoredit.checked) || !ts.$sigBox.prop('checked')) // minor edits get not signed
			return ts.rmvIndent(txt, CmtE); // Remove indent always?
		if (!txt)
			return; // Text deleted?
		var cOld = ts._cntOcc(txtOld),
		cNew = ts._cntOcc(txt), //  Occurrences count
		oldp, // Old pos off post-end
		txtEnd, // Array of 2 possibilities: with and without indent
		pos;
		//log(" new general sig counter: %d, %d; old false: %d, %d", cNew[0], cNew[1], cOld[0], cOld[1]);
		/*If (cNew[0] >= cOld[1]) { // if there is an a sign added, check for true
		cNew[0] -= cNew[1]; // evaluate hit count, a true sig?
		// cOld[0] -= cOld[1]; // FIXME: old sig-counter, must be theoretically always 0; NOT working because on submit the old value get lost
		console.assert(cOld[0] - cOld[1], " ERROR: the RegExp sig-counter must been wrong  ", cOld[0]);
		log("old false sigs = %s new sig counter %s new false sigs: %d", cOld[1],  cNew[0], cNew[1]);
		}*/
		if (cNew[0] - cNew[1]) // There is already a sig
			return ts.rmvIndent(txt, CmtE);
		/** IF no sig then search a set position **/
		if (cNew[0] <= cOld[1]) {
			/** First try compare old and new last line **/
			if (!CmtE) { //FIXME: this first check for CmtE not supported yet
				// lrLen = ;  Don't compare aut. indent
				mw.log("lrLen,txtEndLen", lrLen, txtEndLen);
				txtEnd = [txt.substr(txtEndLen - (lrLen ? lrLen + 1 : 0), -txtEndLen), txt.slice(txtEndLen)]; // CUSTOM txtEndLen txt-length, must be same length as txtOldEnd; txtEndLen lrLen is auto indent-length
				oldp = txtOldL + txtEndLen;
				//log('txtOldEnd %i: "%s" ; txtNewEnd %i: "%s"',txtOldEnd.length,txtOldEnd,txtEnd.length,txtEnd, oldp, txt.lastIndexOf(txtOldEnd)); // HINT: DEBUG CRITICAL;
				// Compare last line
				mw.log('Is last line identical?\n"%s"\n"%s"', txtOldEnd, txtEnd, txtEnd[1]);
				if (!txtOld || $.inArray(txtOldEnd, txtEnd) < 0) { // && oldp > txt.lastIndexOf(txtOldEnd) - tolerance  // then some txt between
					return ts.doSig(e, -1); // Aut. underwrite on last line
				}
			}
			// if not the post is between, because last line is identical
			pos = $textarea.textSelection('getCaretPosition'); // MW: 1.18 Get the caret position in a txtarea
			pos = _chkEdit('\n', pos);
			if (pos) {
				return ts.doSig(e, pos); //  Aut. underwrite
			}
			/** THEN the caret is NOT in the NEW txt **/
			// Alert('caret is NOT in the NEW txt') // DEBUG
			txt = ts.rmvIndent(txt, CmtE); // Remove auto indent (only if not used)
			if (txt.length > (txtOldL + tolerance)) { // If more then the tolerance amount chars are added, find the last new line
				var lines = txt.match(/.+/gm) || [],
				linesOld = txtOld.match(/.+/gm) || [];
				//log('lines.length = %i; txt.length = %i; linesOld.length = %i; txtOldL = %i;',lines.length,txt.length,linesOld.length,txtOldL+tolerance);
				if (lines.length > linesOld.length) { // Only if line added
					lines = lines.filter(function (x) {
							return linesOld.indexOf(x) < 0;
						}).pop() || []; // Last changed line
					// The .get(-1) and not() method works here too, but is not documented for non DOM elements!?
					if (lines.length) { // If empty the changed line was exactly also in the old txt
						pos = txt.lastIndexOf(lines);
						//log("txt.length = " + txt.length + "; caret at: " + pos + " old= " + txtOldL + " : " + txtOld.indexOf(lines));
						return ts.doSig(e, pos + lines.length);
					}
					/* Fixme?: need fallback case?
					This case can only happen, if the only new line (without caret on it) is a fully duplicate of another in the txt. */
				} // Else return; // no new line, do nothing (also no warning)!?
			}
			/* FIXME?: need fallback case?
			This case can only happen, if some txt was removed and the new comment was not at the end and the caret was not in the new comment. */
			// Rare fallback case: if the last txt is a HTML-comment, check the beginning of the comment.
			if (CmtE) {
				pos = _chkEdit('<!--', txtLen);
				//log("Rare fallback case: %i ", txtLen,txt.slice(txtLen,txt.indexOf('<!--', txtLen)) );
				if (pos) // Post-end
					return ts.doSig(e, pos); //  Aut. underwrite
			}
		}
		// FIXME DEBUG: not for Diff and Preview, values are reseted
		// if ('Perhelion' === c.wgUserName && /^wp(Diff|Preview)$/.test(e.target.name)) alert(name + ' main-edit, but no auto-signature position found!');
		if (e.target.name === 'wpSave') { // Warn only on save submit!?
			$editform.submit(function (e) {
				if (!confirm(signTxt.confirm)) {
					e.preventDefault();
					e.stopPropagation();
					mw.log.warn(name, txtOld, txt);
					return false;
				}
			});
		}
		return e;
	}
};

mw.libs.threadSign = ts;

if (mw.config.get('wgPageContentModel') === 'wikitext') {
	$(document).trigger('loadWikiScript', ['Perhelion/signing.js', ts]); // For bind config
	$.when(mw.loader.using(['jquery.textSelection', 'mediawiki.language', 'user.options', 'mediawiki.util']), $.ready).then(function () {
		$(ts.init);
	});
}
}
(jQuery, mediaWiki));
// EOF </nowiki>