Divec has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/264577

Change subject: WIP: VisualEditorEntry jQuery.ime class
......................................................................

WIP: VisualEditorEntry jQuery.ime class

Class to make jQuery.IME work properly with VisualEditor; conditionally loaded
if VE is present.

TODO:
* Back out fake upstream jQuery.IME changes
* Pull through real jQuery.IME changes with 
https://github.com/wikimedia/jquery.ime/pull/424 merged
* Refactor for use with ResourceLoader?

Change-Id: Iad6a6eccccdf2ec9822cf7df0f0ace7a95b6bea8
---
M UniversalLanguageSelector.php
M extension.json
M lib/jquery.ime/jquery.ime.js
M resources/js/ext.uls.ime.js
4 files changed, 500 insertions(+), 368 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/UniversalLanguageSelector 
refs/changes/77/264577/1

diff --git a/UniversalLanguageSelector.php b/UniversalLanguageSelector.php
index 995e26f..437daf8 100644
--- a/UniversalLanguageSelector.php
+++ b/UniversalLanguageSelector.php
@@ -162,7 +162,7 @@
  *
  * @since 2013.07
  */
-$wgULSNoImeSelectors = array( '#wpCaptchaWord', '.ve-ce-surface *' );
+$wgULSNoImeSelectors = array( '#wpCaptchaWord' );
 
 /**
  * Array of jQuery selectors of elements on which webfonts must not be applied.
diff --git a/extension.json b/extension.json
index f5925d3..c6dfd9b 100644
--- a/extension.json
+++ b/extension.json
@@ -80,7 +80,7 @@
                ],
                "@ULSNoImeSelectors": "Array of jQuery selectors of elements on 
which IME must not be enabled. @since 2013.07",
                "ULSNoImeSelectors": [
-                       "#wpCaptchaWord", ".ve-ce-surface *"
+                       "#wpCaptchaWord"
                ],
                "@ULSNoWebfontsSelectors": "Array of jQuery selectors of 
elements on which webfonts must not be applied. By default exclude elements 
with .autonym because that style set font as Autonym @since 2013.09",
                "ULSNoWebfontsSelectors": [
diff --git a/lib/jquery.ime/jquery.ime.js b/lib/jquery.ime/jquery.ime.js
index 8bb2d4f..178fa4c 100644
--- a/lib/jquery.ime/jquery.ime.js
+++ b/lib/jquery.ime/jquery.ime.js
@@ -1,20 +1,46 @@
-/*! jquery.ime - v0.1.0+20151121
+/*! jquery.ime - v0.1.0+20160105
 * https://github.com/wikimedia/jquery.ime
-* Copyright (c) 2015 Santhosh Thottingal; Licensed GPL, MIT */
+* Copyright (c) 2016 Santhosh Thottingal; Licensed GPL, MIT */
 ( function ( $ ) {
        'use strict';
+       var TextEntryFactory, TextEntry, FormWidgetEntry, ContentEditableEntry,
+               defaultInputMethod;
 
        // rangy is defined in the rangy library
        /*global rangy */
 
        /**
+        * Minimal functionality from OOjs: Initialize a class for OO 
inheritance
+        */
+       function initClass( fn ) {
+               fn.static = fn.static || {};
+       }
+
+       /**
+        * Minimal functionality from OOjs: Initialize a class for OO 
inheritance
+        * OOjs-like subclass initialization (minimal required functionality)
+        */
+       function inheritClass( targetFn, originFn ) {
+               targetFn.parent = originFn;
+               targetFn.prototype = $.extend( {}, originFn.prototype );
+               targetFn.prototype.constructor = originFn.constructor;
+               targetFn.static = $.extend( {}, originFn.static );
+       }
+
+       /**
         * IME Class
+        * @class
+        *
+        * @constructor
+        * @param {HTMLElement} element Element on which to listen for events
+        * @param {TextEntry} textEntry Text entry object to use to get/set text
         * @param {Function} [options.helpHandler] Called for each input method 
row in the selector
         * @param {Object} options.helpHandler.imeSelector
         * @param {String} options.helpHandler.ime Id of the input method
         */
-       function IME( element, options ) {
+       function IME( element, textEntry, options ) {
                this.$element = $( element );
+               this.textEntry = textEntry;
                // 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 );
@@ -127,7 +153,7 @@
                 */
                keypress: function ( e ) {
                        var altGr = false,
-                               c, startPos, pos, endPos, divergingPos, input, 
replacement;
+                               c, input, replacement;
 
                        if ( !this.active ) {
                                return true;
@@ -160,24 +186,10 @@
 
                        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 = this.getCaretPosition( this.$element );
-                       startPos = pos[0];
-                       endPos = pos[1];
-
-                       // Get the last few characters before the one the user 
just typed,
+                       // Append the character being typed to the preceding 
few characters,
                        // to provide context for the transliteration regexes.
-                       // We need to append c because it hasn't been added to 
$this.val() yet
-                       input = this.lastNChars(
-                               this.$element.val() || this.$element.text(),
-                               startPos,
-                               this.inputmethod.maxKeyLength
-                       );
-                       input += c;
-
-                       replacement = this.transliterate( input, this.context, 
altGr );
+                       input = this.textEntry.getTextBeforeSelection( 
this.inputmethod.maxKeyLength );
+                       replacement = this.transliterate( input + c, 
this.context, altGr );
 
                        // Update the context
                        this.context += c;
@@ -198,11 +210,7 @@
                                return true;
                        }
 
-                       // Drop a common prefix, if any
-                       divergingPos = this.firstDivergence( input, 
replacement.output );
-                       input = input.substring( divergingPos );
-                       replacement.output = replacement.output.substring( 
divergingPos );
-                       replaceText( this.$element, replacement.output, 
startPos - input.length + 1, endPos );
+                       this.textEntry.replaceTextAtSelection( input.length, 
replacement.output );
 
                        e.stopPropagation();
 
@@ -332,121 +340,200 @@
 
                        return deferred.promise();
                },
+       };
 
-               /**
-                * Returns an array [start, end] of the beginning
-                * and the end of the current selection in $element
-                * @returns {Array}
-                */
-               getCaretPosition: function ( $element ) {
-                       return getCaretPosition( $element );
-               },
+       /**
+        * TextEntry factory
+        * @class
+        *
+        * @constructor
+        */
+       TextEntryFactory = function IMETextEntryFactory() {
+               this.TextEntryClasses = [];
+       };
 
-               /**
-                * Set the caret position in the div.
-                * @param {jQuery} $element The content editable div element
-                * @param {Object} position An object with start and end 
properties.
-                * @return {Array} If the cursor could not be placed at given 
position, how
-                * many characters had to go back to place the cursor
-                */
-               setCaretPosition: function ( $element, position ) {
-                       return setCaretPosition( $element, position );
-               },
+       /* Inheritance */
 
-               /**
-                * 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
-                */
-               firstDivergence: function ( a, b ) {
-                       return firstDivergence( a, b );
-               },
+       initClass( TextEntryFactory );
 
-               /**
-                * 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
-                */
-               lastNChars: function ( str, pos, n ) {
-                       return lastNChars( str, pos, n );
+       /* Methods */
+
+       /**
+        * Register a TextEntry class, with priority over previous registrations
+        *
+        * @param {TextEntry} Class to register
+        */
+       TextEntryFactory.prototype.register = function ( TextEntryClass ) {
+               this.TextEntryClasses.unshift( TextEntryClass );
+       };
+
+       /**
+        * Wrap an editable element with the appropriate TextEntry class
+        *
+        * @param {jQuery} $element The element to wrap
+        * @return {TextEntry|undefined} A TextEntry, or undefined if no match
+        */
+       TextEntryFactory.prototype.wrap = function ( $element ) {
+               var i, len, TextEntryClass;
+               for ( i = 0, len = this.TextEntryClasses.length; i < len; i++ ) 
{
+                       TextEntryClass = this.TextEntryClasses[ i ];
+                       if ( TextEntryClass.static.canWrap( $element ) ) {
+                               return new TextEntryClass( $element );
+                       }
+               }
+               return undefined;
+       };
+
+       /* Initialization */
+
+       TextEntryFactory.static.singleton = new TextEntryFactory();
+
+       /**
+        * Generic text entry
+        * @class
+        *
+        * @abstract
+        */
+       TextEntry = function IMETextEntry() {
+       };
+
+       /* Inheritance */
+
+       initClass( TextEntry );
+
+       /* Static methods */
+
+       /**
+        * Test whether can wrap this type of element
+        *
+        * @param {jQuery} $element The element to wrap
+        * @return {boolean} Whether the element can be wrapped
+        */
+       TextEntry.static.canWrap = function () {
+               return false;
+       };
+
+       /* Abstract methods */
+
+       /**
+        * Get text immediately before the current selection start.
+        *
+        * This SHOULD return the empty string for non-collapsed selections.
+        *
+        * @param {number} maxLength Maximum number of chars (code units) to 
return
+        * @return {String} Up to maxLength of text
+        */
+       TextEntry.prototype.getTextBeforeSelection = null;
+
+       /**
+        * Replace the currently selected text and/or text before the selection
+        *
+        * @param {number} precedingCharCount Number of chars before selection 
to replace
+        * @param {String} newText Replacement text
+        */
+       TextEntry.prototype.replaceTextAtSelection = null;
+
+       /**
+        * TextEntry class for input/textarea widgets
+        * @class
+        *
+        * @constructor
+        * @param {jQuery} $element The element to wrap
+        */
+       FormWidgetEntry = function IMEFormWidgetEntry( $element ) {
+               this.$element = $element;
+       };
+
+       /* Inheritance */
+
+       inheritClass( FormWidgetEntry, TextEntry );
+
+       /* Static methods */
+
+       /**
+        * @inheritdoc TextEntry
+        */
+       FormWidgetEntry.static.canWrap = function ( $element ) {
+               return $element.is( 'input:not([type]), input[type=text], 
input[type=search], textarea' ) &&
+                       !$element.prop( 'readonly' ) &&
+                       !$element.prop( 'disabled' ) &&
+                       !$element.hasClass( 'noime' );
+       };
+
+       /* Instance methods */
+
+       /**
+        * @inheritdoc TextEntry
+        */
+       FormWidgetEntry.prototype.getTextBeforeSelection = function ( maxLength 
) {
+               var pos = this.getCaretPosition();
+               return this.$element.val().substring(
+                       Math.max( 0, pos.start - maxLength ),
+                       pos.start
+               );
+       };
+
+       /**
+        * @inheritdoc TextEntry
+        */
+       FormWidgetEntry.prototype.replaceTextAtSelection = function ( 
precedingCharCount, newText ) {
+               var selection,
+                       length,
+                       newLines,
+                       start,
+                       scrollTop,
+                       pos,
+                       element = this.$element.get( 0 );
+
+               if ( typeof element.selectionStart === 'number' && typeof 
element.selectionEnd === 'number' ) {
+                       // IE9+ and all other browsers
+                       start = element.selectionStart;
+                       scrollTop = element.scrollTop;
+
+                       // Replace the whole text of the text area:
+                       // text before + newText + text after.
+                       // 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 - 
precedingCharCount ) +
+                               newText +
+                               element.value.substring( element.selectionEnd, 
element.value.length );
+
+                       // restore scroll
+                       element.scrollTop = scrollTop;
+                       // set selection
+                       element.selectionStart = element.selectionEnd = start - 
precedingCharCount + newText.length;
+               } else {
+                       // IE8 and lower
+                       pos = this.getCaretPosition();
+                       selection = element.createTextRange();
+                       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', pos.start - 
precedingCharCount );
+                       selection.moveEnd( 'character', pos.end - length );
+
+                       selection.text = newText;
+                       selection.collapse( false );
+                       selection.select();
                }
        };
 
        /**
-        * jQuery plugin ime
-        * @param {Object} option
+        * Get the current selection offsets inside the widget
+        *
+        * @return {Object} Offsets in chars (0 means first offset *or* no 
selection in widget)
+        * @return.start {number} Selection start
+        * @return.end {number} Selection end
         */
-       $.fn.ime = function ( option ) {
-               return this.each( function () {
-                       var data,
-                               $this = $( this ),
-                               options = typeof option === 'object' && option;
-
-                       // Some exclusions: IME shouldn't be applied to 
textareas with
-                       // these properties.
-                       if ( $this.prop( 'readonly' ) ||
-                               $this.prop( 'disabled' ) ||
-                               $this.hasClass( 'noime' ) ) {
-                               return;
-                       }
-
-                       data = $this.data( 'ime' );
-
-                       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
-               helpHandler: null // Called for each ime option in the menu
-       };
-
-       /**
-        * private function for debugging
-        */
-       function debug( $obj ) {
-               if ( window.console && window.console.log ) {
-                       window.console.log( $obj );
-               }
-       }
-
-       /**
-        * Returns an array [start, end] of the beginning
-        * and the end of the current selection in $element
-        */
-       function getCaretPosition( $element ) {
-               var el = $element.get( 0 ),
+       FormWidgetEntry.prototype.getCaretPosition = function () {
+               var el = this.$element.get( 0 ),
                        start = 0,
                        end = 0,
                        normalizedValue,
@@ -455,10 +542,6 @@
                        len,
                        newLines,
                        endRange;
-
-               if ( $element.is( '[contenteditable]' ) ) {
-                       return getDivCaretPosition( el );
-               }
 
                if ( typeof el.selectionStart === 'number' && typeof 
el.selectionEnd === 'number' ) {
                        start = el.selectionStart;
@@ -499,264 +582,173 @@
                                }
                        }
                }
+               return { start: start, end: end };
+       };
 
-               return [start, end];
-       }
+       TextEntryFactory.static.singleton.register( FormWidgetEntry );
 
        /**
-        * Helper function to get an IE TextRange object for an element
+        * TextEntry class for ContentEditable
+        * @class
+        *
+        * @constructor
+        * @param {jQuery} $element The element to wrap
         */
-       function rangeForElementIE( element ) {
-               var selection;
+       ContentEditableEntry = function IMEContentEditableEntry( $element ) {
+               this.$element = $element;
+       };
 
-               if ( element.nodeName.toLowerCase() === 'input' ) {
-                       selection = element.createTextRange();
-               } else {
-                       selection = document.body.createTextRange();
-                       selection.moveToElementText( element );
+       /* Inheritance */
+
+       inheritClass( ContentEditableEntry, TextEntry );
+
+       /* Static methods */
+
+       /**
+        * @inheritdoc TextEntry
+        */
+       ContentEditableEntry.static.canWrap = function ( $element ) {
+               return $element.is( '[contenteditable]' ) && 
!$element.hasClass( 'noime' );
+       };
+
+       /* Instance methods */
+
+       /**
+        * @inheritdoc SelectionWrapper
+        */
+       ContentEditableEntry.prototype.getTextBeforeSelection = function ( 
maxLength ) {
+               var range = this.getSelectedRange();
+               if ( !range || !range.collapsed || 
range.startContainer.nodeType !== Node.TEXT_NODE ) {
+                       return '';
                }
+               return range.startContainer.nodeValue.substring(
+                       Math.max( 0, range.startOffset - maxLength ),
+                       range.startOffset
+               );
+       };
 
-               return selection;
-       }
-
-       function replaceText( $element, replacement, start, end ) {
-               var selection,
-                       length,
-                       newLines,
-                       scrollTop,
-                       range,
-                       correction,
-                       textNode,
-                       element = $element.get( 0 );
-
-               if ( $element.is( '[contenteditable]' ) ) {
-                       correction = setCaretPosition( $element, {
-                               start: start,
-                               end: end
-                       } );
-
-                       rangy.init();
-                       selection = rangy.getSelection();
-                       range = selection.getRangeAt( 0 );
-
-                       if ( correction[0] > 0 ) {
-                               replacement = selection.toString().substring( 
0, correction[0] ) + replacement;
-                       }
-
-                       textNode = document.createTextNode( replacement );
-                       range.deleteContents();
-                       range.insertNode( textNode );
-                       range.commonAncestorContainer.normalize();
-                       start = end = start + replacement.length - 
correction[0];
-                       setCaretPosition( $element, {
-                               start: start,
-                               end: end
-                       } );
-
+       /**
+        * @inheritdoc SelectionWrapper
+        */
+       ContentEditableEntry.prototype.replaceTextAtSelection = function ( 
precedingCharCount, newText ) {
+               var textNode, textOffset, newOffset, newRange,
+                       range = this.getSelectedRange();
+               if ( !range ) {
                        return;
                }
-
-               if ( typeof element.selectionStart === 'number' && typeof 
element.selectionEnd === 'number' ) {
-                       // IE9+ and all other browsers
-                       scrollTop = element.scrollTop;
-
-                       // Replace the whole text of the text area:
-                       // text before + replacement + text after.
-                       // 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;
-               } else {
-                       // IE8 and lower
-                       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();
+               if ( !range.collapsed ) {
+                       range.deleteContents();
                }
-       }
 
-       function getDivCaretPosition( element ) {
-               var charIndex = 0,
-                       start = 0,
-                       end = 0,
-                       foundStart = false,
-                       foundEnd = false,
-                       sel;
+               if ( range.startContainer.nodeType === Node.TEXT_NODE ) {
+                       // Alter this text node's content and move the cursor
+                       textNode = range.startContainer;
+                       textOffset = range.startOffset;
+                       textNode.nodeValue =
+                               textNode.nodeValue.substr( 0, textOffset - 
precedingCharCount ) +
+                               newText +
+                               textNode.nodeValue.substr( textOffset );
+                       newOffset = textOffset - precedingCharCount + 
newText.length;
+                       newRange = rangy.createRange();
+                       newRange.setStart( range.startContainer, newOffset );
+                       newRange.setEnd( range.startContainer, newOffset );
+                       rangy.getSelection().setSingleRange( newRange );
+               } else {
+                       // XXX assert precedingCharCount === 0
+                       // Insert a new text node with the new text
+                       textNode = document.createTextNode( newText );
+                       range.startContainer.insertBefore(
+                               textNode,
+                               range.startContainer.childNodes[ 
range.startOffset ]
+                       );
+                       newRange = rangy.createRange();
+                       newRange.setStart( textNode, textNode.length );
+                       newRange.setEnd( textNode, textNode.length );
+                       rangy.getSelection().setSingleRange( newRange );
+               }
+       };
 
+       /**
+        * Get the selection range inside the wrapped element, or null
+        *
+        * @return {Range|null} The selection range
+        */
+       ContentEditableEntry.prototype.getSelectedRange = function () {
+               var sel, range;
                rangy.init();
                sel = rangy.getSelection();
-
-               function traverseTextNodes( node, range ) {
-                       var i, childNodesCount;
-
-                       if ( node.nodeType === Node.TEXT_NODE ) {
-                               if ( !foundStart && node === 
range.startContainer ) {
-                                       start = charIndex + range.startOffset;
-                                       foundStart = true;
-                               }
-
-                               if ( foundStart && node === range.endContainer 
) {
-                                       end = charIndex + range.endOffset;
-                                       foundEnd = true;
-                               }
-
-                               charIndex += node.length;
-                       } else {
-                               childNodesCount = node.childNodes.length;
-
-                               for ( i = 0; i < childNodesCount; ++i ) {
-                                       traverseTextNodes( node.childNodes[i], 
range );
-                                       if ( foundEnd ) {
-                                               break;
-                                       }
-                               }
-                       }
+               if ( sel.rangeCount === 0 ) {
+                       return null;
                }
-
-               if ( sel.rangeCount ) {
-                       traverseTextNodes( element, sel.getRangeAt( 0 ) );
+               range = sel.getRangeAt( 0 );
+               if ( !this.$element[ 0 ].contains( 
range.commonAncestorContainer ) ) {
+                       return null;
                }
+               return range;
+       };
 
-               return [ start, end ];
-       }
+       TextEntryFactory.static.singleton.register( ContentEditableEntry );
 
-       function setCaretPosition( $element, position ) {
-               var currentPosition,
-                       startCorrection = 0,
-                       endCorrection = 0,
-                       element = $element[0];
-
-               setDivCaretPosition( element, position );
-               currentPosition = getDivCaretPosition( element );
-               // see Bug https://bugs.webkit.org/show_bug.cgi?id=66630
-               while ( position.start !== currentPosition[0] ) {
-                       position.start -= 1; // go back one more position.
-                       if ( position.start < 0 ) {
-                               // never go beyond 0
-                               break;
-                       }
-                       setDivCaretPosition( element, position );
-                       currentPosition = getDivCaretPosition( element );
-                       startCorrection += 1;
-               }
-
-               while ( position.end !== currentPosition[1] ) {
-                       position.end += 1; // go forward one more position.
-                       setDivCaretPosition( element, position );
-                       currentPosition = getDivCaretPosition( element );
-                       endCorrection += 1;
-                       if ( endCorrection > 10 ) {
-                               // XXX avoid rare case of infinite loop here.
-                               break;
-                       }
-               }
-
-               return [startCorrection, endCorrection];
-       }
+       /* Exports */
 
        /**
-        * Set the caret position in the div.
-        * @param {Element} element The content editable div element
-        * @param position
+        * jQuery plugin ime
+        * @param {Object} option
         */
-       function setDivCaretPosition( element, position ) {
-               var nextCharIndex,
-                       charIndex = 0,
-                       range = rangy.createRange(),
-                       foundStart = false,
-                       foundEnd = false;
+       $.fn.ime = function ( option ) {
+               return this.each( function () {
+                       var data, textEntry,
+                               $this = $( this ),
+                               options = typeof option === 'object' && option;
 
-               range.collapseToPoint( element, 0 );
-
-               function traverseTextNodes( node ) {
-                       var i, len;
-
-                       if ( node.nodeType === 3 ) {
-                               nextCharIndex = charIndex + node.length;
-
-                               if ( !foundStart && position.start >= charIndex 
&& position.start <= nextCharIndex ) {
-                                       range.setStart( node, position.start - 
charIndex );
-                                       foundStart = true;
+                       data = $this.data( 'ime' );
+                       if ( !data ) {
+                               textEntry = 
TextEntryFactory.static.singleton.wrap( $this );
+                               if ( textEntry === undefined ) {
+                                       return;
                                }
-
-                               if ( foundStart && position.end >= charIndex && 
position.end <= nextCharIndex ) {
-                                       range.setEnd( node, position.end - 
charIndex );
-                                       foundEnd = true;
-                               }
-
-                               charIndex = nextCharIndex;
-                       } else {
-                               for ( i = 0, len = node.childNodes.length; i < 
len; ++i ) {
-                                       traverseTextNodes( node.childNodes[i] );
-                                       if ( foundEnd ) {
-                                               
rangy.getSelection().setSingleRange( range );
-                                               break;
-                                       }
-                               }
+                               data = new IME( this, textEntry, options );
+                               $this.data( 'ime', data );
                        }
-               }
 
-               traverseTextNodes( element );
+                       if ( typeof option === 'string' ) {
+                               data[option]();
+                       }
+               } );
+       };
 
-       }
+       $.ime = {};
+       $.ime.inputmethods = {};
+       $.ime.sources = {};
+       $.ime.preferences = {};
+       $.ime.languages = {};
+
+       $.ime.textEntryFactory = TextEntryFactory.static.singleton;
+       $.ime.TextEntry = TextEntry;
+       $.ime.inheritClass = inheritClass;
+
+       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
+               helpHandler: null // Called for each ime option in the menu
+       };
 
        /**
-        * 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
+        * private function for debugging
         */
-       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 debug( $obj ) {
+               if ( window.console && window.console.log ) {
+                       window.console.log( $obj );
                }
        }
 
@@ -1601,6 +1593,10 @@
                        name: 'ফনেটিক',
                        source: 'rules/as/as-phonetic.js'
                },
+               'as-rodali': {
+                       name: 'ৰ\'দালি',
+                       source: 'rules/as/as-rodali.js'
+               },
                'as-transliteration': {
                        name: 'প্ৰতিৰূপান্তৰণ',
                        source: 'rules/as/as-transliteration.js'
@@ -2014,6 +2010,10 @@
                'or-transliteration': {
                        name: 'ଟ୍ରାନ୍ସଲିଟରେସନ',
                        source: 'rules/or/or-transliteration.js'
+               },
+               'or-OdiScript': {
+                       name: 'ଓଡ଼ିସ୍କ୍ରିପ୍ଟ',
+                       source: 'rules/or/or-OdiScript.js'
                },
                'or-inscript': {
                        name: 'ଇନସ୍କ୍ରିପ୍ଟ',
@@ -2435,7 +2435,7 @@
                },
                'or': {
                        autonym: 'ଓଡ଼ିଆ',
-                       inputmethods: [ 'or-phonetic', 'or-transliteration', 
'or-inscript', 'or-inscript2', 'or-lekhani' ]
+                       inputmethods: [ 'or-phonetic', 'or-transliteration', 
'or-inscript', 'or-inscript2', 'or-lekhani', 'or-OdiScript' ]
                },
                'pa': {
                        autonym: 'ਪੰਜਾਬੀ',
diff --git a/resources/js/ext.uls.ime.js b/resources/js/ext.uls.ime.js
index e80ff0d..887c6c6 100644
--- a/resources/js/ext.uls.ime.js
+++ b/resources/js/ext.uls.ime.js
@@ -165,6 +165,7 @@
        };
 
        mw.ime.init = function () {
+               var VisualEditorEntry;
                if ( !$.ime ) {
                        // jquery.ime not loaded yet.
                        return;
@@ -191,6 +192,137 @@
                $.fn.imeselector.Constructor.prototype.getAutonym = function ( 
languageCode ) {
                        return $.uls.data.getAutonym( languageCode );
                };
+
+               if ( window.ve ) {
+                       /**
+                        * TextEntry class for VisualEditor documents
+                        * @class
+                        *
+                        * @constructor
+                        * @param {jQuery} $element The element to wrap
+                        */
+                       VisualEditorEntry = function IMEVisualEditorEntry( 
$element ) {
+                               this.$element = $element;
+                               this.viewSurface = $element.data( 'view' 
).surface;
+                               this.modelSurface = this.viewSurface.getModel();
+                               this.modelDocument = 
this.modelSurface.getDocument();
+                       };
+
+                       /* Inheritance */
+
+                       OO.inheritClass( VisualEditorEntry, $.ime.TextEntry );
+
+                       /* Static methods */
+
+                       /**
+                        * @inheritdoc TextEntry
+                        */
+                       VisualEditorEntry.static.canWrap = function ( $element 
) {
+                               return $element.data( 'view' ) instanceof 
ve.ce.DocumentNode;
+                       };
+
+                       /* Instance methods */
+
+                       /**
+                        * @inheritdoc TextEntry
+                        */
+                       VisualEditorEntry.prototype.getTextBeforeSelection = 
function ( maxLength ) {
+                               var sel = this.viewSurface.nativeSelection;
+                               // Return the empty string for uncollapsed 
selection or
+                               // non-textNode focusNode. This includes some 
positions adjacent
+                               // to a textNode, e.g. 
<b><#text>foo</#text>|</b>, which are
+                               // possible to reach in Firefox by cursoring. 
The rationale is
+                               // that substitution sequence characters are 
intended to be typed
+                               // consecutively, without, say, clicking to 
move the selection
+                               // before completing the sequence.
+                               if (
+                                       !sel.isCollapsed ||
+                                       sel.rangeCount === 0 ||
+                                       sel.focusNode.nodeType !== 
Node.TEXT_NODE
+                               ) {
+                                       return '';
+                               }
+                               return sel.focusNode.data.slice(
+                                       Math.max( 0, sel.focusOffset - 
maxLength ),
+                                       sel.focusOffset
+                               );
+                       };
+
+                       /**
+                        * @inheritdoc TextEntry
+                        */
+                       VisualEditorEntry.prototype.replaceTextAtSelection = 
function ( precedingCharCount, newText ) {
+                               var sel, range, node, start, end,
+                                       modelSel = 
this.modelSurface.getSelection();
+                               if ( !( modelSel instanceof 
ve.dm.LinearSelection ) ) {
+                                       return null;
+                               }
+
+                               // 1. Trigger ve.ce.Surface.handleInsertion to 
trigger any
+                               // model-initiated content removal that may be 
necessary (e.g.
+                               // if the selection crosses branch nodes).
+                               // 2. Delete the native selection contents if 
any. It won't cause
+                               // corruption because (1) already performed any 
merging that the
+                               // browser would mess up.
+                               // 3. If we are in a text node, replace the 
node contents and
+                               // selection as appropriate. Else we're in an 
element node, so
+                               // insert newText into a new text node at the 
cursor (there
+                               // should be no preceding text to replace 
because we're not in
+                               // a text node).
+
+                               if ( !modelSel.isCollapsed() ) {
+                                       // Trigger any necessary model-based 
content removal
+                                       this.viewSurface.handleInsertion();
+                               }
+                               sel = this.viewSurface.nativeSelection;
+                               if ( sel.rangeCount === 0 ) {
+                                       ve.error( 'Selection lost (should never 
occur)' );
+                                       return;
+                               }
+                               if ( !sel.isCollapsed ) {
+                                       // handleInsertion happened, so safe to 
use deleteContents
+                                       sel.getRangeAt( 0 ).deleteContents();
+                               }
+
+                               range = document.createRange();
+                               if ( sel.focusNode.nodeType !== Node.TEXT_NODE 
) {
+                                       if ( precedingCharCount !== 0 ) {
+                                               ve.error( 'Preceding chars, but 
not in text node (should never occur)' );
+                                       }
+                                       node = document.createTextNode( newText 
);
+                                       sel.focusNode.insertBefore(
+                                               node,
+                                               sel.focusNode.childNodes[ 
sel.focusOffset ] || null
+                                       );
+                                       range.setStart( node, node.data.length 
);
+                                       range.setEnd( node, node.data.length );
+                                       sel.removeAllRanges();
+                                       sel.addRange( range );
+                                       
this.viewSurface.surfaceObserver.pollOnce();
+                                       return;
+                               }
+                               // Else cursor is in a text node...
+
+                               node = sel.focusNode;
+                               end = sel.focusOffset;
+                               start = end - precedingCharCount;
+                               if ( start < 0 ) {
+                                       ve.error( 'Request to expand beyond 
node start (should never occur)' );
+                                       start = 0;
+                               }
+                               node.data = node.data.slice( 0, start )
+                                       + newText
+                                       + node.data.slice( end, 
node.data.length );
+                               range = document.createRange();
+                               range.setStart( node, start + newText.length );
+                               range.setEnd( node, start + newText.length );
+                               sel.removeAllRanges();
+                               sel.addRange( range );
+                               this.viewSurface.surfaceObserver.pollOnce();
+                       };
+
+                       $.ime.textEntryFactory.register( VisualEditorEntry );
+               }
        };
 
        /**

-- 
To view, visit https://gerrit.wikimedia.org/r/264577
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Iad6a6eccccdf2ec9822cf7df0f0ace7a95b6bea8
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/UniversalLanguageSelector
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to