MediaWiki:Gadget-im.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/*
* Interslavic input method
* Based on:
* - https://de.wikipedia.org/wiki/Benutzer:Jowereit/typografie.js
* - https://de.wikipedia.org/wiki/Benutzer:Schnark/js/veAutocorrect.js
*/
( function() {
// Main form selectors
var mainFormFields = [
'#wpTextbox1',
'#wpText',
'#wpUploadDescription'
]
// Ignored selectors
var ignoredFields = [
'#wpUserEmail',
'#wpEmail',
'#mw-input-wpemailaddress',
'#wpCaptchaWord'
];
// Re-used variables
var isAnonymous = mw.config.get( 'wgUserName' ) === null;
var api;
var defaultStorageKey = '__im';
var imEnabled = true;
var mainTextbox;
var veCommandCount = 0;
var imVeFuncs = {};
// Menu configuration
var imMenu = {
title: 'Metoda vpisa',
description: 'Koristajte skračeńja dlja vpisa razširenoj latinice i kirilice.',
help: 'Pomoč',
helpLink: '/wiki/Project:Pomoč/Metoda_vpisa',
enable: 'Vključiti',
disable: 'Odključiti'
}
// Polyfill for Element.prototype.matches
if ( !Element.prototype.matches ) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}
/**
* Restore the unused letters in code editor.
*
* @param self HTML element.
* @param e Event information.
* @param isVE Whether function is called from VisualEditor.
*/
function imShiftBackspace( self, e, isVE ) {
// Visual Editor
if ( isVE ) return;
// WikiEditor
if ( !e.shiftKey ) return;
var lastEntry = self.lastEntry( 1 );
// Look into imReplaceUnused
if ( lastEntry === 'Ј' || lastEntry === 'ј' ) {
self.insert( ( lastEntry === 'Ј' ? 'Й': 'й' ), 1 );
}
// Look into imReplaceCyrlI
if ( lastEntry === 'И' || lastEntry === 'и' ) {
self.insert( ( lastEntry === 'И' ? 'І': 'і' ), 1 );
}
}
/**
* Print letters with hachek on typing x.
*
* @param self HTML element.
* @param e Event information.
* @param isVE Whether function is called from VisualEditor.
*/
function imReplaceHachek( self, e, isVE ) {
// Extended Latin to Basic
var from = {
'č': 'c',
'ě': 'e',
'ĺ': 'l',
'ń': 'n',
'š': 's',
'ž': 'z'
}
// Basic Latin to Extended
var to = {
'c': 'č',
'e': 'ě',
'l': 'ĺ',
'n': 'ń',
's': 'š',
'z': 'ž'
}
// VisualEditor
if ( isVE ) {
for ( var key in to ) {
ve_replaceLetter( key, to[key], 'x', 2 );
}
return;
};
// WikiEditor
replaceMultipleLetters( self, e, from, to, 'x' );
}
/**
* Print Є on Serbian/Macedonian keyboard.
*
* @param self HTML element.
* @param e Event information.
* @param isVE Whether function is called from VisualEditor.
*/
function imReplaceDzheToE( self, e, isVE ) {
// Extended Cyrillic to Basic
var from = {
'є': 'е'
}
// Basic Cyrillic to Extended
var to = {
'е': 'є'
}
// VisualEditor
if ( isVE ) {
for ( var key in to ) {
ve_replaceLetter( key, to[key], 'џ', 2 );
}
return;
};
// WikiEditor
replaceMultipleLetters( self, e, from, to, 'џ' );
}
/**
* Print Ы on Ukrainian keyboard and И otherwise.
*
* @param self HTML element.
* @param e Event information.
* @param isVE Whether function is called from VisualEditor.
*/
function imReplaceCyrlI( self, e, isVE ) {
// Replacements
var to = {
'І': 'И',
'і': 'и'
}
// VisualEditor
if ( isVE ) {
// І to И
for ( var key in to ) {
ve_replace( key, to[key], key.length );
}
// Ь І to Ы
ve_replaceLetter( 'ь', 'ы', 'і', 2 );
return;
};
// Has to be written before any changes
var lastEntry = self.lastEntry( 1 ).toLowerCase();
// WikiEditor: Ь І to Ы
replaceMultipleLetters( self, e, { 'ы': 'ь' }, { 'ь': 'ы' }, 'і' );
// WikiEditor: І to И
var lastEntry = self.lastEntry( 1 ).toLowerCase();
if ( [ 'ь', 'і', 'ы' ].indexOf( lastEntry ) > -1 ) {
return;
}
if ( to[ e.key ] ) {
replaceLetter( self, e, e.key, to[ e.key ] );
return;
}
console.log('ext.gadget.im: key ' + e.key + ' has not returned a function.');
}
/**
* Print soft letters instead of ь.
*
* @param self HTML element.
* @param e Event information.
* @param isVE Whether function is called from VisualEditor.
*/
function imReplaceSoftSign( self, e, isVE ) {
// Extended Cyrillic to Basic
var from = {
'є': 'е',
'љ': 'л',
'њ': 'н'
}
// Basic Cyrillic to Extended
var to = {
'е': 'є',
'л': 'љ',
'н': 'њ'
}
// VisualEditor
if ( isVE ) {
for ( var key in to ) {
ve_replaceLetter( key, to[ key ], 'ь', 2 );
}
return;
};
// WikiEditor
replaceMultipleLetters( self, e, from, to, 'ь' );
}
/**
* Replace unused letters with appropriate ones.
*
* @param self HTML element.
* @param e Event information.
* @param isVE Whether function is called from VisualEditor.
*/
function imReplaceUnused( self, e, isVE ) {
// Replacements
var to = {
'Й': 'Ј',
'й': 'ј',
'Э': 'Е',
'э': 'е'
}
// VisualEditor
if ( isVE ) {
for ( var key in to ) {
ve_replace( key, to[ key ], key.length );
}
return;
};
if ( to[ e.key ] ) {
replaceLetter( self, e, e.key, to[ e.key ] );
return;
}
console.log('ext.gadget.im: key ' + e.key + ' has not returned a function.');
}
/*
* Key combos and Visual Editor functions
*/
var keyCombos = {};
keyCombos['Backspace'] = imShiftBackspace;
keyCombos['X'] = imReplaceHachek;
keyCombos['x'] = imReplaceHachek;
imVeFuncs['x'] = imReplaceHachek;
keyCombos['І'] = imReplaceCyrlI;
keyCombos['і'] = imReplaceCyrlI;
imVeFuncs['і'] = imReplaceCyrlI;
keyCombos['Й'] = imReplaceUnused;
keyCombos['й'] = imReplaceUnused;
imVeFuncs['й'] = imReplaceUnused;
keyCombos['Э'] = imReplaceUnused;
keyCombos['э'] = imReplaceUnused;
imVeFuncs['э'] = imReplaceUnused;
keyCombos['Џ'] = imReplaceDzheToE;
keyCombos['џ'] = imReplaceDzheToE;
imVeFuncs['џ'] = imReplaceDzheToE;
keyCombos['Ь'] = imReplaceSoftSign;
keyCombos['ь'] = imReplaceSoftSign;
imVeFuncs['ь'] = imReplaceSoftSign;
/**
* Return some text before the caret.
*
* @param target HTML element.
* @param length Number of characters.
* @returns String before the caret.
*/
function lastEntry( target, length ) {
var currSelection = $( target ).textSelection( 'getCaretPosition', { startAndEnd: true } );
var selStart = currSelection[ 0 ];
var selEnd = currSelection[ 1 ];
// Calculate caret position differently if there is a selection
var caretPos = ( selStart === selEnd ? selStart : selEnd );
var start = caretPos - length;
return $( target ).textSelection( 'getContents' )
.substring( start > 0 ? start : 0, caretPos );
}
/**
* Select some characters and insert text in their position.
*
* @param target HTML element.
* @param text Inserted text.
* @param strip Number of replaced characters.
*/
function insert( target, text, strip ) {
var currSelection = $( target ).textSelection( 'getCaretPosition', { startAndEnd: true } );
var selStart = currSelection[ 0 ];
var selEnd = currSelection[ 1 ];
// Do not touch existing selection
if ( selStart === selEnd ) {
$( target ).textSelection( 'setSelection', {
start: selStart - strip,
end: selStart
} );
}
// Keep undo working if possible
if ( !document.execCommand( 'insertText', false, text ) ) {
$( target ).textSelection( 'replaceSelection', text );
var newPos = $( target ).textSelection( 'getCaretPosition', { startAndEnd: true } );
$( target ).textSelection( 'setSelection', {
start: newPos[ 1 ]
} );
}
}
/**
* Replace a single letter.
*
* @param self HTML element.
* @param e Event information.
* @param from Object with replaced text.
* @param to Object with text after replacement.
*/
function replaceLetter( self, e, from, to ) {
if ( e.ctrlKey || e.metaKey ) return;
var length = ( from.length > 1 ? Math.abs( 1 - from.length ) : 0 );
var lastEntry = self.lastEntry( length );
var fromSubstring = from.substring( 0, from.length - 1 );
if ( length > 0 && lastEntry.toLowerCase() !== fromSubstring.toLowerCase() ) {
return;
}
var result = to.charAt( 0 ) + to.slice( 1 );
if ( ( from.length > 1 && lastEntry.toUpperCase() === lastEntry )
|| from.toUpperCase() === from ) {
result = to.charAt( 0 ).toUpperCase() + to.slice(1);
}
self.insert( result, length );
e.preventDefault();
}
/**
* Replace multiple different letters with other ones.
*
* @param self HTML element
* @param e Event information
* @param from Object with replaced text
* @param to Object with text after replacement
* @param symbol Symbol that is getting replaced
*/
function replaceMultipleLetters( self, e, from, to, symbol ) {
if ( e.ctrlKey || e.metaKey ) return;
var obj = null;
var lastEntry = self.lastEntry( 1 );
var isUpperCase = ( lastEntry.toUpperCase() === lastEntry );
if ( isUpperCase ) {
lastEntry = lastEntry.toLowerCase();
}
var trail = '';
if ( Object.keys( to ).indexOf( lastEntry ) > -1 ) {
obj = to;
} else if ( Object.keys( from ).indexOf( lastEntry ) > -1 ) {
obj = from;
trail = symbol;
}
if ( obj !== null ) {
var result = ( isUpperCase ? obj[ lastEntry ].toUpperCase() : obj[ lastEntry ] ) + trail;
self.insert( result, 1 );
e.preventDefault();
}
}
/**
* Register/unregister commands in Visual Editor.
*
* @param from Object with replaced text.
* @param to Object with text after replacement.
* @param strip Number of stripped characters.
*/
function ve_replace( from, to, strip ) {
if ( !( 've' in window ) || !ve.ui ) return;
// Random name with prefix
var name = 'im-' + veCommandCount;
// Disable the command if required
if ( !imEnabled ) {
var doneAction = false;
if ( ve.ui.sequenceRegistry ) {
ve.ui.sequenceRegistry.unregister( name );
doneAction = true;
}
if ( ve.ui.wikitextSequenceRegistry ) {
ve.ui.wikitextSequenceRegistry.unregister( name );
doneAction = true;
}
if ( doneAction ) veCommandCount++;
return;
};
if ( ve.ui.commandRegistry ) {
ve.ui.commandRegistry.register(
// 1st true: annotate, 2nd true: collapse to end
new ve.ui.Command(name, 'content', 'insert', { args: [ to, true, true ] } )
);
}
// Create and register a sequence
var seq = new ve.ui.Sequence( name, name, from, strip );
if ( ve.ui.sequenceRegistry ) {
ve.ui.sequenceRegistry.register( seq );
}
if ( ve.ui.wikitextSequenceRegistry ) {
ve.ui.wikitextSequenceRegistry.register( seq );
}
veCommandCount++;
}
/**
* Replace a letter in Visual Editor back and forth.
*
* @param from Object with replaced text.
* @param to Object with text after replacement.
* @param symbol Replaced symbol.
* @param strip Number of stripped characters.
*/
function ve_replaceLetter( from, to, symbol, strip ) {
var reSymbol = ( symbol ? '[' + symbol.toUpperCase() + symbol + ']$' : '$' );
symbol = ( symbol ? symbol : '' );
var ucCombo = new RegExp( from.toUpperCase() + reSymbol );
var ucComboRevert = new RegExp( to.toUpperCase() + reSymbol );
// dj to ď
ve_replace( from + symbol, to, strip );
ve_replace( ucCombo, to.toUpperCase(), strip );
// ďj to dj
ve_replace( to + symbol, from + symbol, strip );
ve_replace( ucComboRevert, from.toUpperCase() + symbol, strip );
}
/**
* Autocorrect the input from any text field.
*
* @param e Event information.
*/
function autocorrect( e ) {
if ( !imEnabled ) return true;
if ( !e.key ) return true;
// If there is no function for this key, stop future actions
if ( Object.keys( keyCombos ).indexOf( e.key ) === -1 ) {
return true;
}
// Add CodeMirror support hack here
var element = this;
if ( element.classList.contains( 'CodeMirror-code' ) && $( mainTextbox ).length ) {
element = mainTextbox;
}
var currSelection = $( element ).textSelection( 'getCaretPosition', { startAndEnd: true } );
var selStart = currSelection[ 0 ];
var selEnd = currSelection[ 1 ];
keyCombos[ e.key ]( element, e, false, selStart, selEnd );
}
/**
* Toggle VisualEditor autocorrect.
*
* @param status Current input method status.
*/
function ve_autocorrect( status ) {
if ( !status ) status = imEnabled;
if ( status === false && veCommandCount === 0 ) return;
veCommandCount = 0;
for ( var func in imVeFuncs ) {
imVeFuncs[ func ]( null, null, true );
}
}
/**
* Render the menu.
*/
function renderMenu() {
if ( $( '#p-im' ).length ) return;
// Custom setup on Minerva
if ( mw.config.get( 'skin' ) === 'minerva' ) {
var $portlet = mw.util.addPortletLink(
'pt-preferences',
imMenu.helpLink,
imMenu.title
);
var $portletLink = $( $portlet ).find( 'a' );
getStatus().then( function( status ) {
imEnabled = status;
$portletLink.find( 'span:not(.mw-ui-icon)' )
.text( imMenu.title + ': ' + ( imEnabled ? imMenu.disable : imMenu.enable ) );
} );
$portletLink.on( 'click', function( e ) {
e.preventDefault();
setStatus();
$portletLink.find( 'span:not(.mw-ui-icon)' )
.text( imMenu.title + ': ' + ( imEnabled ? imMenu.disable : imMenu.enable ) );
} );
return;
}
// Default setup
var $panel = $( '#p-tb' ).clone().attr( 'id', 'p-im' ).attr( 'aria-labelledby', 'p-im-label' );
$panel.find( '#p-tb-label' ).attr( 'id', 'p-im-label' ).text( imMenu.title );
var $body = $panel.find( ':last-child' ).eq( 0 );
$body.empty();
var $toggle = $( '<label>' ).attr( 'for', 'im-toggle' ).text( ' ' + imMenu.enable + ' [Ctrl+M]' );
var $checkbox = $( '<input>' )
.attr( 'type', 'checkbox' )
.attr( 'id', 'im-toggle' )
.prependTo( $toggle );
$checkbox.change( function () {
setStatus( this.checked );
if ( $( mainTextbox ).length ) {
$( mainTextbox ).focus();
}
} );
getStatus().then( function( status ) {
$checkbox.attr( 'checked', imEnabled = status );
} );
var $help = $( '<a>' ).attr( 'href', imMenu.helpLink ).text( imMenu.help );
$body
.append( $( '<p>' ).html( $toggle ) )
.append( $( '<p>' ).text( imMenu.description ) )
.append( $( '<p>' ).html( $help ) );
$( '#p-tb' ).after( $panel );
}
/**
* Read user option / local storage for gadget status.
*
* @return {string} Value from option / local storage or true.
*/
function getStatus() {
var value;
return mw.loader.using( 'mediawiki.storage' ).then( function() {
value = mw.storage.get( 'ext-gadget-im' );
return value !== null ? false : true;
} );
}
/**
* Set user option / cookie for gadget status.
*
* @param target HTML element.
*/
function setStatus( status ) {
if ( !status ) status = !imEnabled;
var isDefault = status === true;
mw.loader.using( 'mediawiki.storage' ).done( function() {
var action = ( isDefault ? 'remove' : 'set' );
var stored = mw.storage[ action ]( 'ext-gadget-im', status );
if ( stored ) {
var $toggle = $( '#im-toggle' );
$toggle.prop( 'checked', status );
imEnabled = status;
ve_autocorrect( status );
}
} );
}
/**
* Add input method events to an element.
*
* @param target HTML element.
*/
function addEvents( target ) {
if ( !target || target[ defaultStorageKey ] === true ) return;
if (
target.matches( ignoredFields.join() )
|| ( target.tagName !== 'TEXTAREA' && target.tagName !== 'INPUT' && !target.isContentEditable )
|| target.type === 'submit'
) return;
target.onkeydown = autocorrect;
target.lastEntry = function( length ) {
return lastEntry( this, length );
}
target.insert = function( text, strip ) {
return insert( this, text, strip );
}
target[ defaultStorageKey ] = true;
}
/**
* Toggle the script from keyboard on Ctrl+M
*
* @param e Event information.
*/
function toggleOnCtrlM( e ) {
if ( !e.ctrlKey ) return true;
if ( e.altKey || e.shiftKey ) return true;
if ( e.which !== 77 && e.which !== 109 ) return true;
e.preventDefault();
setStatus();
return false;
}
/**
* Start the script.
*/
function init() {
// Add events to all existing fields
var fields = document.querySelectorAll( 'input, textarea' );
for ( var i = 0; i < fields.length; i++ ) {
var el = fields[ i ];
addEvents( el );
if ( !mainTextbox && mainFormFields.indexOf( '#' + el.id ) > -1 ) {
mainTextbox = el;
}
}
renderMenu();
document.addEventListener( 'keydown', toggleOnCtrlM );
// Add Visual Editor support
if ( mw.config.get( 'wgIsProbablyEditable' ) ) {
var veDeps = [ 'ext.visualEditor.desktopArticleTarget.init' ];
mw.loader.using( veDeps, function () {
mw.libs.ve.addPlugin( ve_autocorrect );
} );
// Add DiscussionTools support
if ( mw.config.get( 'wgDiscussionToolsFeaturesEnabled' ) ) {
mw.loader.using( [ 'ext.discussionTools.ReplyWidgetVisual' ] ).then( function() {
mw.loader.using( veDeps, ve_autocorrect );
} );
}
// Add veaction (edge cases in VE) / Flow support
var isFlowBoard = mw.config.get( 'wgPageContentModel' ) === 'flow-board';
if ( isFlowBoard || location.href.search( 'veaction' ) > 0 ) {
veDeps.push( 'ext.visualEditor.core' );
if ( isFlowBoard || mw.libs && mw.libs.ve && mw.libs.ve.isWikitextAvailable ) {
veDeps.push( 'ext.visualEditor.mwwikitext' );
}
mw.loader.using( veDeps, ve_autocorrect );
}
}
// Watch all input on the page
document.addEventListener( 'keydown', function( e ) {
addEvents( e.target );
} );
}
mw.loader.using( 'jquery.textSelection', init );
}() );