User:V111P/SKLChanger.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* Simple Keyboard Layout Changer v. 2013-11-17
 * http://en.wikipedia.org/wiki/User:V111P/js/Simple_Keyboard_Layout_Changer
 *
 * With this script you can transform the characters you type in input boxes and textareas
 * into different characters - for example, into characters from a different language.
 * The script supports multiple groups of characters to convert to and you can switch
 * between the different groups with a click on a link.
 * Warning: Test the undo function (Ctrl-Z) in your browser to see if you like it.
 * Undo doesn't work in Google Chrome 30 at all.
 *
 * CC0 Public Domain Dedication:
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

window.sKLChanger = (function ($) {
'use strict';

// US keyboard layout is used by default
var defaultCharsToConvert = '`-='
	+ 'qwertyuiop[]\\'
	+ 'asdfghjkl;\''
	+ 'zxcvbnm,./'
	// with shift
	+ '~!@#$%^&*()_+'
	+ 'QWERTYUIOP{}|'
	+ 'ASDFGHJKL:"'
	+ 'ZXCVBNM<>?';

var lang = mw.config.get('wgUserLanguage');
var msgs = {
	indicatorLabelWhenTurnedOff_short: (lang == 'bg' ? 'Изкл.' : 'OFF'),
	indicatorLabelWhenTurnedOff_long: (lang == 'bg'
		? 'Превключвателят е изключен'
		: 'The Keyboard Layout Changer is turned off'),
	nonsupportedBrowserMsg_short: '<s>OFF</s>',
	nonsupportedBrowserMsg_long: (lang == 'bg'
		? 'Превключвателят на клавиатурни подредби няма да работи с вашия браузър'
		: 'Simple Keyboard Layout Changer: Browser not supported')
};

var defaultIndicatorPos = {
	nearElementSelector: '#editform', //'prepend' on #editform doesn't (always) work in IE
	position: 'before' // 'before', 'after', 'prepend', 'append'
};

var inputBoxesSelector = 'textarea, input[type="text"], input[type="password"], '
	+ 'input:not([type]), input[name="search"], input#searchInput';
	// Some MediaWiki skins don't have type="text" attribute on their search input boxes.
	// :not() not supported in older browsers

var inputBoxesDeselector = 'input#wpAntispam'; // #wpAntispam is a hidden input box in MediaWiki

var showLayoutHint; // if true, the string with the characters will be shown
	// in the title attribute of the long-label indicator link

var longLabelClassName = 'sKL_longIndicator';

var keyboardLayouts = [];
var currLayoutN = -1; // none initially
var charsToConvertMap = {};
var keyboardLayoutIndicator = $([]); // create an empty jQuery object
var browser; // 'ok', 'ie', 'other'


// init() - auto called if a window.sKLChangerConfig object exists at the time of the loading of this file
function init(configObj) {
	// jQuery is passing a function as an argument on $(init)
	configObj = (typeof configObj == 'object' ? configObj : (window.sKLChangerConfig || {}));

	var inputArea = $(inputBoxesSelector).not(inputBoxesDeselector)
		.filter(':visible')[0]; // hidden elements cause an error in Firefox 6

	if (!inputArea)
		return;

	setCharsToConvert(configObj.charsToConvert || defaultCharsToConvert);
	showLayoutHint = (configObj.showLayoutHint === false ? false : true);

	// add-and-replace the passed messages to the msgs object
	$.extend(msgs, configObj.messages);
	if (typeof configObj.offName == 'string')
		msgs.indicatorLabelWhenTurnedOff_short = configObj.offName;
	if (typeof configObj.offLabel == 'string')
		msgs.indicatorLabelWhenTurnedOff_long = configObj.offLabel;

	if ((window.wikEd && wikEd.useWikEd)
		|| (inputArea && typeof inputArea.selectionStart != 'undefined')
	)
		browser = 'ok';
	else if (document.selection && document.selection.createRange)
		browser = 'ie'; // old IE & IE in Compatibility View mode
	else {
		browser = 'other'; // won't work
		msgs.indicatorLabelWhenTurnedOff_long = msgs.nonsupportedBrowserMsg_long;
	}

	removeAllLayouts();

	if (configObj.layouts)
		$.each(configObj.layouts, function (i, val) {
			addLayout(val.name, val.label, val.chars);
		});

	addIndicator(configObj.indicatorPosition);
	var defaultLayout = configObj.defaultLayout;
	if (typeof defaultLayout != 'undefined')
		turnOn(defaultLayout);
}


function addIndicator(positionObj) {
	var longIndicatorID = 'sKL_indicator2';
	var label_short = msgs.indicatorLabelWhenTurnedOff_short;
	var label_long = msgs.indicatorLabelWhenTurnedOff_long;
	if (browser == 'other') {
		label_short = msgs.nonsupportedBrowserMsg_short;
		label_long = msgs.nonsupportedBrowserMsg_long;
	}

	keyboardLayoutIndicator.remove(); // remove any indicators already on the page
	keyboardLayoutIndicator = $([]);
	$('#sKLChangerPortlet').remove();
	$('#'+longIndicatorID).remove();

	// first add the indicator in the "personal bar"
	mw.util.addPortletLink('p-personal', '#', '[' + label_short + ']',
			'sKLChangerPortlet', label_long);
	var indicatorLink = $('#sKLChangerPortlet').find('a');
	keyboardLayoutIndicator = keyboardLayoutIndicator.add(indicatorLink);

	// then add the indicator/switch near the specified element, if it exists

	var nearElementSelector = defaultIndicatorPos.nearElementSelector;
	var position = defaultIndicatorPos.position;

	if (positionObj
			&& typeof positionObj.nearElementSelector == 'string'
			&& typeof positionObj.position == 'string'
			&& $(positionObj.nearElementSelector).length != 0
	) {
		nearElementSelector = positionObj.nearElementSelector;
		position = positionObj.position;
	}

	if ($(nearElementSelector).length > 0) {
		var indicator2 = $('<a/>', {
				href: '#',
				id: longIndicatorID,
				'class': longLabelClassName,
				text: '[' + label_long + ']'
			});

		switch (position) {
			case 'before' : indicator2.insertBefore(nearElementSelector); break;
			case 'after'  : indicator2.insertAfter(nearElementSelector); break;
			case 'prepend': indicator2.prependTo(nearElementSelector); break;
			case 'append' : default: indicator2.appendTo(nearElementSelector);
		}

		keyboardLayoutIndicator = keyboardLayoutIndicator.add(indicator2);
	}

	// finally, attach the event handler to all indicators
	keyboardLayoutIndicator.bind('click', function(event) {
				event.preventDefault();
				nextLayout();
			});
}


function removeLayout(layoutToRemove) {
	if (typeof layoutToRemove == 'string') {
		$.each(keyboardLayouts, function(indexInArray, layout) {
			if (layout.name == layoutToRemove) {
				turnOff();
				keyboardLayouts.splice(indexInArray, 1);
				return false;
			}
		});
	}
	else if (typeof layoutToRemove == 'number') {
		turnOff();
		keyboardLayouts.splice(layoutToRemove, 1);
	}
}


function removeAllLayouts() {
	turnOff();
	keyboardLayouts = [];
}


function getAllLayouts() {
	return keyboardLayouts;
}


function addLayout(name, label, layoutStr, position) {
	if (!name || !label || !layoutStr)
		return;

	if (typeof position == 'undefined'
		|| position >= keyboardLayouts.length
		|| position < 0
	)
		position = keyboardLayouts.length;

	var layout = {name:	name, label: label, layoutStr: layoutStr};
	keyboardLayouts.splice(position, 0, layout);
	if (position <= currLayoutN)
		currLayoutN++; // keep current layout
}


function setCharsToConvert(charStr) {
	charStr = charStr || '';

	// each character code of the chars in charStr is converted
	// to a property name in charsToConvertMap with the value being
	// the position of the char in charStr.
	for (var i = 0; i < charStr.length; i++) {
		charsToConvertMap[charStr.charCodeAt(i)] = i;
	}
}


function error(msg) {
	if (console && console.error)
		console.error('Simple Keyboard Layout Changer: ' + msg);
}


function layoutNameToIndex(layoutName) {
	if (typeof layoutName != 'string') {
		error('layoutNameToIndex: layout-name string expected, got: ' + layoutName);
		return -1;
	}
	for (var i = 0; i < keyboardLayouts.length; i++)
		if (keyboardLayouts[i].name === layoutName)
			return i;
	error('layoutNameToIndex: Can\'t find layout with name "' + layoutName + '"');
	return -1;
}


function turnOn(layoutName) {
	if (keyboardLayouts.length == 0) {
		error('No keyboard layouts defined.');
		return;
	}

	if (typeof layoutName == 'undefined')
		currLayoutN = 0;
	else {
		currLayoutN = layoutNameToIndex(layoutName);
		if (currLayoutN == -1) {
			turnOff();
			return;
		}
	}

	$(inputBoxesSelector).on('keypress.sKLChanger', keyPressed);
	if (window.wikEd) {
		wikEd.AddEventListener(wikEd.frameDocument, 'keypress', keyPressed, true);
	}
	setIndicatorLabel();
}


function turnOff() {
	$(inputBoxesSelector).off('keypress.sKLChanger');
	currLayoutN = -1;
	setIndicatorLabel();
	if (window.wikEd) {
		wikEd.RemoveEventListener(wikEd.frameDocument, 'keypress', keyPressed, true);
	}
}


function nextLayout() {
	if (currLayoutN + 1 == keyboardLayouts.length)
		turnOff();
	else
		if (currLayoutN == -1)
			turnOn();
		else {
			currLayoutN++;
			setIndicatorLabel();
		}
}


function setIndicatorLabel() {
	setTimeout(safeSetIndicatorLabel, 1);
}


function safeSetIndicatorLabel() {
	var layoutName, layoutLabel;

	var longIndicatorTitleAttr = (currLayoutN > -1 && showLayoutHint
			? keyboardLayouts[currLayoutN].layoutStr
			: '');

	if (currLayoutN > -1) {
		layoutName = keyboardLayouts[currLayoutN].name;
		layoutLabel = keyboardLayouts[currLayoutN].label;
	}
	else {
		layoutName = msgs.indicatorLabelWhenTurnedOff_short;
		layoutLabel = msgs.indicatorLabelWhenTurnedOff_long;
	}

	keyboardLayoutIndicator.each(function(index, indicatorEl) {
		var indicator = $(indicatorEl);
		var txt, title;

		if (indicator.hasClass(longLabelClassName)) {
			txt = layoutLabel;
			title = longIndicatorTitleAttr;
		}
		else {
			txt = layoutName;
			title = layoutLabel;
		}

		setTimeout(function () {
			indicator.text('[' + txt + ']').attr('title', title);
		}, 1);
	});
}


function keyPressed(event) {
	var charCode = event.charCode || event.keyCode;
	if (event.charCode === 0)
		charCode = 0; // Firefox non-printable

	if (charCode == 0 || event.ctrlKey || event.altKey || event.metaKey)
		return true;

	switch (charCode) {
		case 0: //Non-printable
		case 13: //Enter
		case 27: //Esc (for IE)
		case 32: //Space
		return true;
	}

	var currLayoutStr = keyboardLayouts[currLayoutN].layoutStr;
	var index = charsToConvertMap[charCode];
	if (typeof index == 'undefined')
		return true;
	var charToReplaceWith = currLayoutStr.charAt(index);

	if (charToReplaceWith) {
		event.preventDefault();
		var inputBox = this;
		setTimeout(function () { // if the textarea value is changed directly, IE clears its undo history
			insertStr(charToReplaceWith, inputBox);
		}, 1);
	}
	
}


function insertStr(str, inputBox) {
	if (window.wikEd && wikEd.useWikEd && inputBox == wikEd.frameDocument) {
		wikEd.FrameExecCommand('inserthtml',
			str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
	}
	else if (browser == 'ok') {
		var startPos = inputBox.selectionStart;
		var endPos = inputBox.selectionEnd;
		var scrollTop = inputBox.scrollTop;
		var pos = startPos + str.length;
		inputBox.value = inputBox.value.substring(0, startPos)
			+ str
			+ inputBox.value.substring(endPos, inputBox.value.length);
		inputBox.focus();
		inputBox.setSelectionRange(pos, pos);	
		inputBox.scrollTop = scrollTop;
	}
	else if (browser == 'ie') {
		var range = document.selection.createRange();
		if (range.text)
			document.selection.clear();
		range.text = str;
	}
}


if (window.sKLChangerConfig) {
	if (document.readyState == 'complete' || document.readyState == 'interactive')
		init();
	else
		$(init); // but $() doesn't work after errors
}


return {
	init: init,
	removeLayout: removeLayout,
	removeAllLayouts: removeAllLayouts,
	getAllLayouts: getAllLayouts,
	addLayout: addLayout,
	setCharsToConvert: setCharsToConvert,
	turnOn: turnOn,
	turnOff: turnOff,
	nextLayout: nextLayout,
	// variables / constants:
	defaultCharsToConvert: defaultCharsToConvert
};

})(jQuery);