( function ( $ ) { 'use strict'; function IME ( element, options ) { this.$element = $( element ); // This needs to be delayed here since extending language list happens at DOM ready $.ime.defaults.languages = arrayKeys( $.ime.languages ); this.options = $.extend( {}, $.ime.defaults, options ); this.active = false; this.inputmethod = null; this.context = ''; this.selector = this.$element.imeselector( this.options ); this.listen(); } IME.prototype = { constructor: IME, listen: function () { this.$element.on( 'keypress.ime', $.proxy( this.keypress, this ) ); }, /** * Transliterate a given string input based on context and input method definition. * If there are no matching rules defined, returns the original string. * * @param input * @param context * @param altGr bool whether altGr key is pressed or not * @returns String transliterated string */ transliterate: function ( input, context, altGr ) { var patterns, regex, rule, replacement, i; if ( altGr ) { patterns = this.inputmethod.patterns_x || []; } else { patterns = this.inputmethod.patterns; } if ( $.isFunction( patterns ) ) { return patterns.call( this, input, context ); } for ( i = 0; i < patterns.length; i++ ) { rule = patterns[i]; regex = new RegExp( rule[0] + '$' ); // Last item in the rules. // It can also be a function, because the replace // method can have a function as the second argument. replacement = rule.slice( -1 )[0]; // Input string match test if ( regex.test( input ) ) { // Context test required? if ( rule.length === 3 ) { if ( new RegExp( rule[1] + '$' ).test( context ) ) { return input.replace( regex, replacement ); } } else { // No context test required. Just replace. return input.replace( regex, replacement ); } } } // No matches, return the input return input; }, keypress: function ( e ) { var altGr = false, c, startPos, pos, endPos, divergingPos, input, replacement; if ( !this.active ) { return true; } if ( !this.inputmethod ) { return true; } // handle backspace if ( e.which === 8 ) { // Blank the context this.context = ''; return true; } if ( e.altKey || e.altGraphKey ) { altGr = true; } // Don't process ASCII control characters (except linefeed), // as well as anything involving // Alt (except for extended keymaps), Ctrl and Meta if ( ( e.which < 32 && e.which !== 13 && !altGr ) || e.ctrlKey || e.metaKey ) { // Blank the context this.context = ''; return true; } c = String.fromCharCode( e.which ); // Get the current caret position. The user may have selected text to overwrite, // so get both the start and end position of the selection. If there is no selection, // startPos and endPos will be equal. pos = getCaretPosition( this.$element ); startPos = pos[0]; endPos = pos[1]; // Get the last few characters before the one the user just typed, // to provide context for the transliteration regexes. // We need to append c because it hasn't been added to $this.val() yet input = lastNChars( this.$element.val() || this.$element.text(), startPos, this.inputmethod.maxKeyLength ) + c; replacement = this.transliterate( input, this.context, altGr ); // Update the context this.context += c; if ( this.context.length > this.inputmethod.contextLength ) { // The buffer is longer than needed, truncate it at the front this.context = this.context.substring( this.context.length - this.inputmethod.contextLength ); } // it is a noop if ( replacement === input ) { return true; } // Drop a common prefix, if any divergingPos = firstDivergence( input, replacement ); input = input.substring( divergingPos ); replacement = replacement.substring( divergingPos ); replaceText( this.$element, replacement, startPos - input.length + 1, endPos ); e.stopPropagation(); return false; }, isActive: function () { return this.active; }, disable: function () { this.active = false; $.ime.preferences.setIM( 'system' ); }, enable: function () { this.active = true; }, toggle: function () { this.active = !this.active; }, getIM: function () { return this.inputmethod; }, setIM: function ( inputmethodId ) { this.inputmethod = $.ime.inputmethods[inputmethodId]; $.ime.preferences.setIM( inputmethodId ); }, setLanguage: function( languageCode ) { $.ime.preferences.setLanguage( languageCode ); }, load: function ( name, callback ) { var ime = this, dependency; if ( $.ime.inputmethods[name] ) { if ( callback ) { callback.call( ime ); } return true; } dependency = $.ime.sources[name].depends; if ( dependency ) { this.load( dependency ) ; } $.ajax( { url: ime.options.imePath + $.ime.sources[name].source, dataType: 'script' } ).done( function () { debug( name + ' loaded' ); if ( callback ) { callback.call( ime ); } } ).fail( function ( jqxhr, settings, exception ) { debug( 'Error in loading inputmethod ' + name + ' Exception: ' + exception ); } ); } }; $.fn.ime = function ( option ) { return this.each( function () { var $this = $( this ), data = $this.data( 'ime' ), options = typeof option === 'object' && option; if ( !data ) { data = new IME( this, options ); $this.data( 'ime', data ); } if ( typeof option === 'string' ) { data[option](); } } ); }; $.ime = {}; $.ime.inputmethods = {}; $.ime.sources = {}; $.ime.preferences = {}; $.ime.languages = {}; var defaultInputMethod = { contextLength: 0, maxKeyLength: 1 }; $.ime.register = function ( inputMethod ) { $.ime.inputmethods[inputMethod.id] = $.extend( {}, defaultInputMethod, inputMethod ); }; // default options $.ime.defaults = { imePath: '../', // Relative/Absolute path for the rules folder of jquery.ime languages: [] // Languages to be used- by default all languages }; // private function for debugging function debug ( $obj ) { if ( window.console && window.console.log ) { window.console.log( $obj ); } } /** * */ function getCaretPosition( $element ) { var el = $element.get( 0 ), start = 0, end = 0, normalizedValue, range, textInputRange, len, endRange; if ( typeof el.selectionStart === 'number' && typeof el.selectionEnd === 'number' ) { start = el.selectionStart; end = el.selectionEnd; } else { // IE range = document.selection.createRange(); if ( range && range.parentElement() === el ) { len = el.value.length; normalizedValue = el.value.replace( /\r\n/g, '\n' ); // Create a working TextRange that lives only in the input textInputRange = el.createTextRange(); textInputRange.moveToBookmark( range.getBookmark() ); // Check if the start and end of the selection are at the very end // of the input, since moveStart/moveEnd doesn't return what we want // in those cases endRange = el.createTextRange(); endRange.collapse( false ); if ( textInputRange.compareEndPoints( 'StartToEnd', endRange ) > -1 ) { start = end = len; } else { start = -textInputRange.moveStart( 'character', -len ); start += normalizedValue.slice( 0, start ).split( '\n' ).length - 1; if ( textInputRange.compareEndPoints( 'EndToEnd', endRange ) > -1 ) { end = len; } else { end = -textInputRange.moveEnd( 'character', -len ); end += normalizedValue.slice( 0, end ).split( '\n' ).length - 1; } } } } return [ start, end ]; } /** * Helper function to get an IE TextRange object for an element */ function rangeForElementIE( e ) { if ( e.nodeName.toLowerCase() === 'input' ) { return e.createTextRange(); } else { var sel = document.body.createTextRange(); sel.moveToElementText( e ); return sel; } } /** * */ function replaceText( $element, replacement, start, end ) { var element = $element.get( 0 ), selection, length, newLines, scrollTop; if ( document.body.createTextRange ) { // IE selection = rangeForElementIE(element); length = element.value.length; // IE doesn't count \n when computing the offset, so we won't either newLines = element.value.match( /\n/g ); if ( newLines ) { length = length - newLines.length; } selection.moveStart( 'character', start ); selection.moveEnd( 'character', end - length ); selection.text = replacement; selection.collapse( false ); selection.select(); } else { // All other browsers scrollTop = element.scrollTop; // This could be made better if range selection worked on browsers. // But for complex scripts, browsers place cursor in unexpected places // and it's not possible to fix cursor programmatically. // Ref Bug https://bugs.webkit.org/show_bug.cgi?id=66630 element.value = element.value.substring( 0, start ) + replacement + element.value.substring( end, element.value.length ); // restore scroll element.scrollTop = scrollTop; // set selection element.selectionStart = element.selectionEnd = start + replacement.length; } } /** * Find the point at which a and b diverge, i.e. the first position * at which they don't have matching characters. * * @param a String * @param b String * @return Position at which a and b diverge, or -1 if a === b */ function firstDivergence ( a, b ) { var minLength, i; minLength = a.length < b.length ? a.length : b.length; for ( i = 0; i < minLength; i++ ) { if ( a.charCodeAt( i ) !== b.charCodeAt( i ) ) { return i; } } return -1; } /** * Get the n characters in str that immediately precede pos * Example: lastNChars( 'foobarbaz', 5, 2 ) === 'ba' * * @param str String to search in * @param pos Position in str * @param n Number of characters to go back from pos * @return Substring of str, at most n characters long, immediately preceding pos */ function lastNChars ( str, pos, n ) { if ( n === 0 ) { return ''; } else if ( pos <= n ) { return str.substr( 0, pos ); } else { return str.substr( pos - n, n ); } } function arrayKeys ( obj ) { var rv = []; $.each( obj, function ( key ) { rv.push( key ); } ); return rv; } }( jQuery ) ); ( function ( $ ) { 'use strict'; $.extend( $.ime.preferences, { registry: { language : 'en', previousLanguages: [], // array of previous languages imes: { 'en': 'system' } }, setLanguage: function ( language ) { this.registry.language = language; if ( !this.registry.previousLanguages ) { this.registry.previousLanguages = []; } //Add to the previous languages, but avoid duplicates. if ( $.inArray( language, this.registry.previousLanguages ) === -1 ) { this.registry.previousLanguages.push( language ); } }, getLanguage: function () { return this.registry.language; }, getPreviousLanguages: function () { return this.registry.previousLanguages; }, // Set the given IM as the last used for the language setIM: function ( inputMethod ) { if( !this.registry.imes ){ this.registry.imes= {}; } this.registry.imes[this.getLanguage()] = inputMethod; }, // Return the last used or the default IM for language getIM: function ( language ) { if( !this.registry.imes ){ this.registry.imes= {}; } return this.registry.imes[language] || $.ime.languages[language].inputmethods[0]; }, save: function () { // save registry in cookies or localstorage }, load: function () { // load registry from cookies or localstorage } } ); }( jQuery ) ); ( function ( $ ) { 'use strict'; function IMESelector ( element, options ) { this.$element = $( element ); this.options = $.extend( {}, IMESelector.defaults, options ); this.active = false; this.$imeSetting = null; this.$menu = null; this.inputmethod = null; this.init(); this.listen(); } IMESelector.prototype = { constructor: IMESelector, init: function () { this.prepareSelectorMenu(); this.position(); this.$imeSetting.hide(); }, prepareSelectorMenu: function() { // TODO: In this approach there is a menu for each editable area. // With correct event mapping we can probably reduce it to one menu. this.$imeSetting = $( selectorTemplate ); this.$menu = $( '