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
