Daniel Werner has uploaded a new change for review.

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


Change subject: Moves jQuery.ui.suggester from Wikibase into ValueView extension
......................................................................

Moves jQuery.ui.suggester from Wikibase into ValueView extension

See Ib39a72c73ead15b8fe4ba6d02cbe5eff902da39d for related commit in Wikibase.

Change-Id: I0e7eeee9d94c1dd68fb6af7ff63ef30062595ea8
---
M ValueView/ValueView.resources.mw.php
M ValueView/ValueView.tests.qunit.php
A ValueView/resources/jquery.ui/jquery.ui.suggester.css
A ValueView/resources/jquery.ui/jquery.ui.suggester.js
A ValueView/tests/qunit/jquery.ui/jquery.ui.suggester.tests.js
5 files changed, 982 insertions(+), 4 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DataValues 
refs/changes/49/57449/1

diff --git a/ValueView/ValueView.resources.mw.php 
b/ValueView/ValueView.resources.mw.php
index 55227f3..6fce860 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -27,7 +27,7 @@
  * @since 0.1
  *
  * @file
- * @ingroup DataValues
+ * @ingroup ValueView
  *
  * @licence GNU GPL v2+
  * @author Daniel Werner < [email protected] >
@@ -70,6 +70,18 @@
                                'jquery.eachchange'
                        )
                ),
+
+               'jquery.ui.suggester' => $moduleTemplate + array(
+                       'scripts' => array(
+                               'jquery.ui/jquery.ui.suggester.js'
+                       ),
+                       'styles' => array(
+                               'jquery.ui/jquery.ui.suggester.css'
+                       ),
+                       'dependencies' => array(
+                               'jquery.ui.autocomplete'
+                       )
+               ),
        );
 
        // return jQuery.valueview's native resources plus those required by 
the MW extension:
diff --git a/ValueView/ValueView.tests.qunit.php 
b/ValueView/ValueView.tests.qunit.php
index 24ff89f..7f438f8 100644
--- a/ValueView/ValueView.tests.qunit.php
+++ b/ValueView/ValueView.tests.qunit.php
@@ -22,7 +22,7 @@
  * @since 0.1
  *
  * @file
- * @ingroup DataTypes
+ * @ingroup ValueView
  *
  * @licence GNU GPL v2+
  * @author Daniel Werner < [email protected] >
@@ -43,7 +43,7 @@
                                'jquery.eachchange',
                        ),
                ),
-       
+
                'jquery.inputAutoExpand.tests' => array(
                        'scripts' => array(
                                "$bp/jquery/jquery.inputAutoExpand.tests.js",
@@ -51,7 +51,16 @@
                        'dependencies' => array(
                                'jquery.inputAutoExpand',
                        ),
-               )
+               ),
+
+               'jquery.ui.suggester.tests' => array(
+                       'scripts' => array(
+                               "$bp/jquery.ui/jquery.ui.suggester.tests.js",
+                       ),
+                       'dependencies' => array(
+                               'jquery.ui.suggester',
+                       ),
+               ),
        );
 
 } );
diff --git a/ValueView/resources/jquery.ui/jquery.ui.suggester.css 
b/ValueView/resources/jquery.ui/jquery.ui.suggester.css
new file mode 100644
index 0000000..98cc6f7
--- /dev/null
+++ b/ValueView/resources/jquery.ui/jquery.ui.suggester.css
@@ -0,0 +1,35 @@
+/**
+ * Default/Vector styles for jquery.ui.suggester
+ *
+ * @license GNU GPL v2+
+ * @author H. Snater <[email protected]>
+ */
+
+.ui-suggester-list {
+       border-color: #C9C9C9;
+       background: white;
+}
+
+.ui-suggester-list .ui-state-hover {
+       border: none;
+       background: #4C59A6;
+       color: white;
+}
+
+.ui-suggester-list .ui-state-hover a,
+.ui-suggester-list .ui-state-hover a:hover {
+       color: white;
+}
+
+.ui-suggester-list {
+       padding: 0;
+}
+
+.ui-suggester-list .ui-menu-item a {
+       padding: 0 0.2em;
+}
+
+.ui-suggester-list .ui-menu-item a.ui-state-hover,
+.ui-suggester-list .ui-menu-item a.ui-state-active {
+       margin: 0;
+}
diff --git a/ValueView/resources/jquery.ui/jquery.ui.suggester.js 
b/ValueView/resources/jquery.ui/jquery.ui.suggester.js
new file mode 100644
index 0000000..db1ca3e
--- /dev/null
+++ b/ValueView/resources/jquery.ui/jquery.ui.suggester.js
@@ -0,0 +1,622 @@
+/**
+ * Suggester widget enhancing jquery.ui.autocomplete
+ *
+ * @licence GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ *
+ * jquery.ui.suggester adds a few enhancements to jquery.ui.autocomplete, e.g. 
adding a scrollbar
+ * when a certain number of items is listed in the suggestion list, 
highlighting matching characters
+ * in the suggestions and dealing with language direction.
+ * Specifying 'ajax.url' and 'ajax.params' parameters will trigger using a 
custom function to
+ * handle the server response (_request()). Alternatively, an array may be 
passed as
+ * source or a completely custom function - both is covered by native 
jquery.ui.autocomplete
+ * functionality.
+ * See jquery.ui.autocomplete for further documentation - just listing 
additional options here.
+ *
+ * @example $( 'input' ).suggester( { source: ['a', 'b', 'c'] } );
+ * @desc Creates a simple auto-completion input element passing an array as 
result set.
+ *
+ * @example $( 'input' ).suggester( {
+ *   ajax: {
+ *     url: <url>,
+ *     params: { <additional parameters> }
+ *   }
+ * } );
+ * @desc Creates an auto-completion input element fetching suggestions via 
AJAX.
+ *
+ * @option maxItems {Number|null} (optional) If the number of suggestions is 
higher than maxItems,
+ *         the suggestion list will be made scrollable. Setting maxItems to 
null will automatically
+ *         resize the suggestion list's height.
+ *         Default value: 10
+ *
+ * @option ajax.url {String} (optional) URL to fetch suggestions from (if 
these shall be queried
+ *         via AJAX)
+ *         Default value: null
+ *
+ * @option ajax.params {Object} (optional) Additional AJAX parameters (if 
suggestions shall be
+ *         retrieved via AJAX)
+ *         Default value: {}
+ *
+ * @option ajax.timeout {Number} (optional) AJAX timeout in milliseconds.
+ *         Default value: 8000
+ *
+ * @option adaptLetterCase {String|Boolean} (optional) Defines whether to 
adjust the letter case
+ *         according to the suggestion list's first value whenever the 
suggestion list is filled.
+ *         Possible values: false, 'first', 'all'
+ *         Default value: false
+ *
+ * @option replace {Array} (optional) Array containing a regular expression 
and a replacement
+ *         pattern (e.g. [/^File:/, '']) that is applied to each result 
returned by the API.
+ *         Default value: null (no replacing)
+ *
+ * @option customListItem {Object|Boolean} (optional) A custom item appended 
to the suggestion list.
+ *         Default value: false (no custom list item)
+ *         Example:
+ *           {
+ *             content: 'custom item label',
+ *             action: function( event, suggester ) {
+ *               console.log( suggester.element.val() );
+ *               suggester.close();
+ *             }
+ *           }
+ * @option customListItem.content {jQuery|String} The content of the 
additional list item. The
+ *         content will be wrapped in a link node inside a list node 
(<li><a>content</a></li>).
+ *         For custom styling, the css class 'ui-suggester-custom' is assigned 
to the <li/> node.
+ * @option customListItem.action {Function} The action to perform when 
selecting the additional
+ *         list item.
+ *         Parameters: (1) {jQuery.Event} Event that has triggered the custom 
action
+ *                     (2) {$.ui.suggester} Reference to the suggester widget
+ * @option customListItem.cssClass {String} (optional) Additional css 
class(es) to assign to the custom
+ *         item's <li/> node.
+ *
+ * @event response Triggered when the API call returned successful.
+ *        (1) {jQuery.Event}
+ *        (2) {Array} List of retrieved items.
+ *
+ * @event error Triggered when the API call was not successful.
+ *        (1) {jQuery.Event}
+ *        (2) {String} Error text status.
+ *        (3) {Object} Detailed error information.
+ *
+ * @dependency jquery.ui.autocomplete
+ */
+( function( $ ) {
+       'use strict';
+
+       $.widget( 'ui.suggester', $.ui.autocomplete, {
+
+               /**
+                * Additional options
+                * @type {Object}
+                */
+               options: {
+                       maxItems: 10,
+                       ajax: {
+                               url: null,
+                               params: {},
+                               timeout: 8000
+                       },
+                       adaptLetterCase: false,
+                       replace: null,
+                       customListItem: false
+               },
+
+               /**
+                * Caching the last pressed key's code
+                * @type {Number}
+                */
+               _lastKeyDown: null,
+
+               /**
+                * @see ui.autocomplete._create
+                */
+               _create: function() {
+                       var self = this;
+
+                       if ( this.options.source === null ) {
+                               this.options.source = this._request;
+                       }
+
+                       $.ui.autocomplete.prototype._create.call( this );
+
+                       if ( $.isArray( this.options.source ) ) {
+                               this.source = this._filterArray;
+                       }
+
+                       // Replace input value with active suggestion list item 
while hovering the list with the
+                       // mouse cursor.
+                       var fnNativeMenuFocus = this.menu.option( 'focus' );
+                       this.menu.option( 'focus', function( event, ui ) {
+                               fnNativeMenuFocus( event, ui );
+                               if ( /^mouse/.test( event.originalEvent.type ) 
) {
+                                       self.element.val( ui.item.data( 
'item.autocomplete' ).value );
+                               }
+                       } );
+
+                       /**
+                        * @see ui.menu.refresh
+                        */
+                       this.menu.refresh = function() {
+                               self._trigger( 'refreshmenu' );
+                               $.ui.menu.prototype.refresh.call( this );
+                       };
+
+                       this.element
+                       .addClass( 'ui-suggester-input' )
+                       .on( this.widgetName + 'open.' + this.widgetName, 
function( event ) {
+                               self._updateDirection();
+                               self._highlightMatchingCharacters();
+                       } )
+                       .on( this.widgetName + 'refreshmenu.' + 
this.widgetName, function( event ) {
+                               if ( self.options.customListItem ) {
+                                       self._renderCustomListItem( 
self.options.customListItem );
+                               }
+                       } )
+                       .on( 'keydown.' + this.widgetName, function( event ) {
+                               if ( event.keyCode === $.ui.keyCode.ENTER ) {
+                                       if ( self.menu.active ) {
+                                               var item = 
self.menu.active.data( 'item.autocomplete' );
+                                               if ( item && item.isCustom && 
item.customAction ) {
+                                                       // Custom actions are 
supposed to be suggester-specific. If, for some
+                                                       // reason, they should 
interact with external components, the action(s)
+                                                       // may trigger custom 
events.
+                                                       item.customAction( 
event, self );
+                                                       return;
+                                               }
+                                       }
+                               }
+                               self._lastKeyDown = event.keyCode;
+                       } );
+
+                       this.menu.element.addClass( 'ui-suggester-list' );
+
+                       // Extend menu's selected method to be able to trigger 
custom item's action.
+                       var fnNativeMenuSelected = this.menu.option( 'selected' 
);
+                       this.menu.option( 'selected', function( event, ui ) {
+                               var item = ui.item.data( 'item.autocomplete' );
+                               if ( !item.isCustom ) {
+                                       fnNativeMenuSelected( event, ui );
+                               } else if ( $.isFunction( item.customAction ) ) 
{
+                                       item.customAction( event, self );
+                               }
+                       } );
+
+                       // since results list does not reposition automatically 
on resize, just close it
+                       // (one resize event handler is enough for all widgets)
+                       $( window )
+                       .off( '.' + this.widgetName )
+                       .on( 'resize.' + this.widgetName, function( event ) {
+                               if ( event.originalEvent === undefined && $( 
'.ui-suggester-input' ).length > 0 ) {
+                                       $( '.ui-suggester-input' ).data( 
self.widgetName ).close( {} );
+                               }
+                       } );
+               },
+
+               /**
+                * @see ui.autocomplete.destroy
+                */
+               destroy: function() {
+                       // about to remove the last suggester instance on the 
page
+                       if ( $( '.ui-suggester-input' ).length === 1 ) {
+                               $( window ).off( '.' + this.widgetName );
+                       }
+                       this.element.off( '.' + this.widgetName );
+                       this.element.removeClass( 'ui-suggester-input' );
+                       $.ui.autocomplete.prototype.destroy.call( this );
+               },
+
+               /**
+                * Disables the suggester.
+                */
+               disable: function() {
+                       this.close();
+                       this.element.prop( 'disabled', true ).addClass( 
'ui-state-disabled' );
+               },
+
+               /**
+                * Enables the suggester.
+                */
+               enable: function() {
+                       this.element.prop( 'disabled', false ).addClass( 
'ui-state-disabled' );
+               },
+
+               /**
+                * Filters an array passed as suggestion source.
+                *
+                * @param {Object} request
+                * @param {Function} response
+                */
+               _filterArray: function( request, response ) {
+                       var resultSet = $.ui.autocomplete.filter( 
this.options.source, request.term );
+
+                       if ( resultSet.length && this.options.adaptLetterCase ) 
{
+                               this.term = this._adaptLetterCase( this.term, 
resultSet[0] );
+                               this.element.val( this.term );
+                       }
+
+                       response( resultSet );
+               },
+
+               /**
+                * Performs the AJAX request.
+                *
+                * @param request {Object} Contains request parameters
+                * @param suggest {Function} Callback putting results into 
auto-complete menu
+                */
+               _request: function( request, suggest ) {
+                       $.ajax( {
+                               url: this.options.ajax.url,
+                               dataType: 'jsonp',
+                               data:  $.extend( {}, this.options.ajax.params, 
{ 'search': request.term } ),
+                               timeout: this.options.ajax.timeout,
+                               success: $.proxy( this._success, this ),
+                               error: $.proxy( function( jqXHR, textStatus, 
errorThrown ) {
+                                       suggest();
+                                       this.element.focus();
+                                       this._trigger( 'error', $.Event(), 
[textStatus, errorThrown] );
+                               }, this )
+                       } );
+               },
+
+               /**
+                * @see jquery.ui.autocomplete.__response
+                */
+               __response: function( content ) {
+                       $.ui.autocomplete.prototype.__response.call( this, 
content );
+                       // There is no content but the menu should be visible 
if there is a custom list item:
+                       if ( !this.options.disabled && ( !content || 
!content.length ) && this.customListItem ) {
+                               this._suggest( [] );
+                               this._trigger( 'open' );
+                       }
+               },
+
+               /**
+                * Handles the response when the API call returns successfully.
+                *
+                * @param {Object} response
+                */
+               _success: function( response ) {
+                       var suggest = this._response();
+                       if ( response[0] === this.element.val() ) {
+
+                               var self = this;
+                               if ( this.options.replace !== null ) {
+                                       $.each( response[1], function( i, value 
) {
+                                               response[1][i] = value.replace( 
self.options.replace[0], self.options.replace[1] );
+                                       } );
+                               }
+
+                               // auto-complete input box text (because of the 
API call lag, this is
+                               // avoided when hitting backspace, since the 
value would be reset too slow)
+                               if ( this._lastKeyDown !== 8 && 
response[1].length > 0 ) {
+                                       this.autocompleteString(
+                                               response[0],
+                                               response[1][0]
+                                       );
+                               }
+
+                               suggest( response[1] ); // pass array of 
returned values to callback
+
+                               this._trigger( 'response', $.Event(), 
[response[1]] );
+                       } else {
+                               // suggest nothing when the response does not 
match with the current input value
+                               // informing autocomplete that there is one 
less pending request
+                               suggest();
+                       }
+               },
+
+               /**
+                * @see ui.autocomplete._suggest
+                */
+               _suggest: function( items ) {
+                       $.ui.autocomplete.prototype._suggest.call( this, items 
);
+                       // In $.ui.autocomplete, _resizeMenu() is called before 
positioning the menu. However,
+                       // resizing the menu width has to be performed after 
positioning since the width shall
+                       // be constrained by the browser viewport width.
+                       this._scaleMenu();
+               },
+
+               /**
+                * Scales the menu's height to the height of maximum list items 
and takes care of the menu
+                * width not reaching out of the browser viewport.
+                */
+               _scaleMenu: function() {
+                       this._resetMenuStyle();
+                       var $menu = this.menu.element;
+
+                       if ( this.options.maxItems ) {
+                               if ( $menu.children().length > 
this.options.maxItems ) {
+                                       var fixedHeight = 0;
+                                       for ( var i = 0; i < 
this.options.maxItems; i++ ) {
+                                               fixedHeight += $( 
$menu.children()[i] ).height();
+                                       }
+                                       $menu.width( $menu.width() + 
this._getScrollbarWidth() );
+                                       $menu.height( fixedHeight );
+                                       $menu.css( 'overflowY', 'scroll' );
+                               }
+                       }
+
+                       $menu.css(
+                               'minWidth',
+                               this.element.outerWidth( true ) - ( 
$menu.outerWidth( true ) - $menu.width() ) + 'px'
+                       );
+
+                       $menu.width( $menu.outerWidth( true ) );
+
+                       // menu reaches out of the browser viewport
+                       if ( $menu.offset().left + $menu.outerWidth( true ) > 
$( window ).width() ) {
+                               // force maximum menu width
+                               $menu.width(
+                                       $( window ).width()
+                                               - $menu.offset().left
+                                               - ( $menu.outerWidth( true ) - 
$menu.width() )
+                                               - 20 // safe space
+                               );
+                       }
+               },
+
+               /**
+                * Renders a custom list item and appends it to the suggestion 
list.
+                * @see ui.autocomplete._renderItem
+                *
+                * @param {Object} customListItem Custom list item definition 
(see option description)
+                * @return {jQuery} The new list item
+                */
+               _renderCustomListItem: function( customListItem ) {
+                       var content = customListItem.content,
+                               $li = $( '<li/>' )
+                                       .addClass( 'ui-suggester-custom' )
+                                       .data( 'item.autocomplete', {
+                                               isCustom: true,
+                                               customAction: 
customListItem.action,
+                                               // internal autocomplete logic 
needs a value (e.g. for activating)
+                                               value: this.term
+                                       } ),
+                               $a = $( '<a/>' ).appendTo( $li );
+
+                       if ( customListItem.cssClass ) {
+                               $li.addClass( customListItem.cssClass );
+                       }
+
+                       if ( typeof content === 'string' ) {
+                               $a.text( content );
+                       } else if ( content instanceof $ ) {
+                               $a.append( content );
+                       } else {
+                               throw new Error( 'suggester: Custom list item 
is invalid.' );
+                       }
+
+                       if ( this.menu.element.children( '.ui-suggester-custom' 
).length > 0 ) {
+                               // TODO: This is entity selector "more" button 
specific. There should be a method
+                               // to specify a position where to add the 
custom list item.
+                               return this.menu.element.children( 
'.ui-suggester-custom' ).first().before( $li );
+                       } else {
+                               return $li.appendTo( this.menu.element );
+                       }
+               },
+
+               /**
+                * Sets (updates) or gets the custom list item.
+                *
+                * @param {Object} [customListItem] Custom list item (omit to 
get the current custom list
+                *        item in the form of a jQuery node). For the object 
structure of this parameter see
+                *        the customListItem option description.
+                * @return {jQuery|String|Boolean} The custom list item's 
content or false if none is
+                *         defined
+                */
+               customListItem: function( customListItem ) {
+                       if ( customListItem === undefined ) {
+                               if ( !this.options.customListItem ) {
+                                       return false;
+                               }
+                               var $a = this.menu.element.children( 
'.ui-suggester-custom a' );
+                               if ( typeof this.options.customListItem === 
'string' ) {
+                                       return $a.text();
+                               } else {
+                                       return $a.children();
+                               }
+                       } else {
+                               this.options.customListItem = customListItem;
+                               this.menu.refresh();
+                               return this.customListItem();
+                       }
+               },
+
+               /**
+                * Resets the menu css styles.
+                */
+               _resetMenuStyle: function() {
+                       this.menu.element
+                       .css( 'minWidth', 'auto' )
+                       .width( 'auto' )
+                       .height( 'auto' )
+                       .css( 'overflowY', 'ellipsis' );
+               },
+
+               /**
+                * Calculates the menu height (including all menu items - even 
those out of the viewport).
+                *
+                * @return {Number} menu height
+                */
+               _getMenuHeight: function() {
+                       this._resetMenuStyle();
+                       var height = 0;
+                       this.menu.element.children( 'li' ).each( function( i ) {
+                               height += $( this ).height();
+                       } );
+                       return height;
+               },
+
+               /**
+                * Calculates the width of the browser's scrollbar.
+                *
+                * @returns {Number} scrollbar width
+                */
+               _getScrollbarWidth: function() {
+                       var $inner = $( '<p/>', { style: 
'width:100px;height:100px' } ),
+                               $outer = $( '<div/>', {
+                                       style: 
'position:absolute;top:-1000px;left:-1000px;visibility:hidden;'
+                                               + 
'width:50px;height:50px;overflow:hidden;'
+                               } ).append( $inner ).appendTo( $( 'body' ) ),
+                               majorWidth = $outer[0].clientWidth;
+
+                       $outer.css( 'overflow', 'scroll' );
+                       var minorWidth = $outer[0].clientWidth;
+                       $outer.remove();
+                       return ( majorWidth - minorWidth );
+               },
+
+               /**
+                * Makes autocomplete results list stretch from the right side 
of the input box in rtl.
+                */
+               _updateDirection: function() {
+                       if (
+                               this.element.attr( 'dir' ) === 'rtl' ||
+                                       (
+                                               this.element.attr( 'dir' ) === 
undefined
+                                               && document.documentElement.dir 
=== 'rtl'
+                                       )
+                               ) {
+                               this.options.position.my = 'right top';
+                               this.options.position.at = 'right bottom';
+                               this.menu.element.position( $.extend( {
+                                       of: this.element
+                               }, this.options.position ) );
+
+                               // to display rtl and ltr correctly
+                               // sometimes a rtl wiki can have ltr page 
names, etc. (try ".gov")
+                               this.menu.element.children().attr( {
+                                       'dir': 'auto'
+                               } );
+                       }
+               },
+
+               /**
+                * Highlights matching characters in the result list.
+                */
+               _highlightMatchingCharacters: function() {
+                       var term = ( this.term ) ? this.term : '',
+                               escapedTerm = $.ui.autocomplete.escapeRegex( 
term ),
+                               regExp = new RegExp(
+                                       '((?:(?!' + escapedTerm +').)*?)(' + 
escapedTerm + ')(.*)', ''
+                               );
+
+                       this.menu.element.children( '.ui-menu-item' ).each( 
function() {
+                               if ( !$( this ).data( 'item.autocomplete' 
).isCustom ) {
+                                       var $itemLink = $( this ).find( 'a' );
+
+                                       // only replace if suggestions actually 
starts with the current input
+                                       if ( $itemLink.text().indexOf( term ) 
=== 0 ) {
+                                               var matches = 
$itemLink.text().match( regExp );
+
+                                               $itemLink
+                                               .text( matches[1] )
+                                               .append( $( '<b/>' ).text( 
matches[2] ) )
+                                               .append( 
document.createTextNode( matches[3] ) );
+                                       }
+                               }
+                       } );
+               },
+
+               /**
+                * Adjusts the letter case of a source string to the letter 
case in a destination string
+                * according to the adaptLetterCase option.
+                *
+                * @param {String} source
+                * @param {String} destination
+                * @return {String} Altered source string
+                */
+               _adaptLetterCase: function( source, destination ) {
+                       if ( this.options.adaptLetterCase === 'all' ) {
+                               return destination.substr( 0, source.length );
+                       } else if ( this.options.adaptLetterCase === 'first' ) {
+                               return destination.substr( 0, 1 ) + 
source.substr( 1 );
+                       } else {
+                               return source;
+                       }
+               },
+
+               /**
+                * Sets/gets the plain input box value.
+                *
+                * @param {String} [value] Value to be set
+                * @return {String} value Current/new value
+                */
+               value: function( value ) {
+                       if ( value !== undefined ) {
+                               this.element.val( value );
+                       }
+                       return this.element.val();
+               },
+
+               /**
+                * Resets/updates the menu position.
+                */
+               repositionMenu: function() {
+                       this.menu.element.position( $.extend( {
+                               of: this.element
+                       }, this.options.position ) );
+               },
+
+               /**
+                * Completes the input box with the remaining characters of a 
given string. The characters
+                * of the remaining part are text-highlighted, so the will be 
overwritten if typing
+                * characters is continue. Tabbing or clicking outside of the 
input box will leave the
+                * completed string in the input box.
+                *
+                * @param incomplete {String}
+                * @param complete {String}
+                * @return {Number} number of characters added (and 
highlighted) at the end of the
+                *         incomplete string
+                */
+               autocompleteString: function( incomplete, complete ) {
+                       if(
+                               // if nothing to complete, just return and 
don't move the cursor
+                               // (can be annoying in this situation)
+                               incomplete === complete
+                               // The following statement is a work-around for 
a technically unexpected search
+                               // behaviour: e.g. in English Wikipedia 
opensearch for "Allegro [...]" returns
+                               // "Allegro" as first result instead of 
"Allegro (music)", so auto-completion should
+                               // probably be prevented here since it would 
always reset the input box's value to
+                               // "Allegro"
+                               || complete.toLowerCase().indexOf( 
this.element.val().toLowerCase() ) === -1
+                       ) {
+                               return 0;
+                       }
+
+                       // set value to complete value...
+                       if ( this.options.adaptLetterCase ) {
+                               this.term = this._adaptLetterCase( incomplete, 
complete );
+                               if ( complete.indexOf( this.term ) === 0 ) {
+                                       this.element.val( complete );
+                               }
+                       } else if ( incomplete === complete.substr( 0, 
incomplete.length ) ) {
+                               this.element.val( incomplete + complete.substr( 
incomplete.length ) );
+                       }
+
+                       // ... and select the suggested, not manually typed 
part of the value
+                       var start = incomplete.length,
+                               end = complete.length,
+                               node = this.element[0];
+
+                       // highlighting takes some browser specific 
implementation
+                       if( node.createTextRange ) { // opera < 10.5 and IE
+                               var selRange = node.createTextRange();
+                               selRange.collapse( true );
+                               selRange.moveStart( 'character', start);
+                               selRange.moveEnd( 'character', end);
+                               selRange.select();
+                       } else if( node.setSelectionRange ) { // major modern 
browsers
+                               // make a 'backward' selection so pressing 
arrow left won't put the cursor near the
+                               // selections end but rather at the typing 
position
+                               node.setSelectionRange( start, end, 'backward' 
);
+                       } else if( node.selectionStart ) {
+                               node.selectionStart = start;
+                               node.selectionEnd = end;
+                       }
+                       return ( end - start );
+               }
+
+       } );
+
+} )( jQuery );
diff --git a/ValueView/tests/qunit/jquery.ui/jquery.ui.suggester.tests.js 
b/ValueView/tests/qunit/jquery.ui/jquery.ui.suggester.tests.js
new file mode 100644
index 0000000..f8f4885
--- /dev/null
+++ b/ValueView/tests/qunit/jquery.ui/jquery.ui.suggester.tests.js
@@ -0,0 +1,300 @@
+/**
+ * QUnit tests for suggester jQuery widget
+ *
+ * @since 0.1
+ * @file
+ * @ingroup ValueView
+ *
+ * @licence GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ */
+
+( function( $, QUnit ) {
+       'use strict';
+
+       /**
+        * Factory for creating a jquery.ui.suggester widget suitable for 
testing.
+        *
+        * @param {Object} [options]
+        *        default: { source: [ 'a', 'ab', 'abc', 'd' ], maxItems: 4 }
+        */
+       var newTestSuggester = function( options ) {
+               options = options || {
+                       maxItems: 4 // will be used in test 'automatic height 
adjustment'
+               };
+               if ( !options.source ) {
+                       options.source = [
+                               'a',
+                               'ab',
+                               'abc',
+                               'd',
+                               'EFG'
+                       ];
+               }
+
+               // element needs to be in the DOM for setting text selection 
range
+               var $input = $( '<input/>' ).addClass( 
'test_suggester').appendTo( 'body' ).suggester( options );
+
+               /**
+                * Shorthand function to reopen the menu by searching for a 
string that will produce at
+                * least one suggestion.
+                *
+                * @param {String} [search]
+                *        default: 'a'
+                */
+               $input.data( 'suggester' ).reopenMenu = function( search ) {
+                       search = search || 'a';
+                       this.close();
+                       this.search( search );
+               };
+
+               return $input;
+       };
+
+       QUnit.module( 'jquery.ui.suggester', QUnit.newMwEnvironment( {
+               teardown: function() {
+                       $( '.test_suggester' ).remove();
+               }
+       } ) );
+
+       QUnit.test( 'basic tests', function( assert ) {
+               var $input = newTestSuggester();
+               var suggester = $input.data( 'suggester' );
+
+               $input.on( 'suggesterresponse', function( event, items ) {
+                       assert.deepEqual(
+                               items,
+                               ['ab', 'abc'],
+                               'Fired response event passing result set.'
+                       );
+               } );
+               $input.val( 'a' );
+               suggester._success( ['a', ['ab', 'abc']] );
+               $input.off( 'suggesterresponse' );
+
+               assert.equal(
+                       $input.val(),
+                       'ab',
+                       'Replaced input element\'s value with first result 
(remaining part of the string is highlighted).'
+               );
+
+               suggester.close();
+
+               $input.val( 'a' );
+               suggester.search( 'a' ); // trigger opening menu
+
+               assert.equal(
+                       suggester.menu.element.find( 'a' ).children( 'b' 
).length,
+                       3,
+                       'Highlighted matching characters within the 
suggestions.'
+               );
+
+               var fullHeight = suggester.menu.element.height(); // height of 
all found items
+               suggester.options.maxItems = 2;
+               suggester.search( 'a' );
+
+               assert.ok(
+                       fullHeight > suggester.menu.element.height(),
+                       'Suggestion menu gets resized.'
+               );
+
+               assert.ok(
+                       suggester._getScrollbarWidth() > 0,
+                       'Detected scrollbar width.'
+               );
+
+               // Firefox will throw an error when the input element is not 
part of the DOM while trying to
+               // set the selection range which is part of the following 
assertion
+               $( 'body' ).append( $input );
+               assert.equal(
+                       suggester.autocompleteString( $input.val(), 'ab' ),
+                       1,
+                       'Auto-completed text.'
+               );
+
+               suggester.destroy();
+               $input.remove();
+
+       } );
+
+       QUnit.test( 'Adapt letter case', function( assert ) {
+               var $input = newTestSuggester();
+               var suggester = $input.data( 'suggester' );
+
+               assert.equal(
+                       suggester._adaptLetterCase( 'abc', 'AbC' ),
+                       'abc',
+                       "adaptLetterCase: Did not adapt any letter case."
+               );
+
+               $input.val( 'ef' );
+               suggester.search( 'EF' ); // simulate case-insensitive search
+
+               assert.equal(
+                       $input.val(),
+                       'ef',
+                       "Did not adjusted input value's letter case according 
to suggestion list's first result set."
+               );
+
+               suggester.destroy();
+               $input.remove();
+
+               $input = newTestSuggester( { adaptLetterCase: 'all' } );
+               suggester = $input.data( 'suggester' );
+
+               assert.equal(
+                       suggester._adaptLetterCase( 'abc', 'AbC' ),
+                       'AbC',
+                       "adjustLetterCase: Adapted the case of all letters."
+               );
+
+               $input.val( 'ef' );
+               suggester.search( 'EF' );
+
+               assert.equal(
+                       $input.val(),
+                       'EF',
+                       "Adjusted input value's letter case according to 
suggestion list's first result set."
+               );
+
+               suggester.destroy();
+               $input.remove();
+
+               $input = newTestSuggester( { adaptLetterCase: 'first' } );
+               suggester = $input.data( 'suggester' );
+
+               assert.equal(
+                       suggester._adaptLetterCase( 'abc', 'AbC' ),
+                       'Abc',
+                       "adaptLetterCase: Adapted the case of the first letter."
+               );
+
+               $input.val( 'ef' );
+               suggester.search( 'EF' );
+
+               assert.equal(
+                       $input.val(),
+                       'Ef',
+                       "Capitalized input value's letters according to 
suggestion list's first result set."
+               );
+
+               suggester.destroy();
+               $input.remove();
+       } );
+
+       QUnit.test( 'automatic height adjustment', function( assert ) {
+               var $input = newTestSuggester();
+               var suggester = $input.data( 'suggester' );
+
+               var additionalResults = [
+                       'a1',
+                       'a2',
+                       'a3'
+               ];
+
+               suggester.search( 'a' );
+
+               var initHeight = suggester.menu.element.height();
+               suggester.options.source.push( additionalResults[0] );
+               suggester.reopenMenu();
+
+               // testing (MAX_ITEMS - 1)++
+               assert.ok(
+                       suggester.menu.element.height() > initHeight,
+                       'height changed after adding another item to result set'
+               );
+
+               // adding one more item (MAX_ITEMS + 1) first, since there 
might be side effects adding the scrollbar
+               suggester.options.source.push( additionalResults[1] );
+               suggester.reopenMenu();
+               initHeight = suggester.menu.element.height();
+
+               suggester.options.source.push( additionalResults[2] );
+               suggester.reopenMenu();
+
+               // testing (MAX_ITEMS + 1)++
+               assert.equal(
+                       suggester.menu.element.height(),
+                       initHeight,
+                       'height unchanged after adding more than maximum items'
+               );
+
+               suggester.destroy();
+               $input.remove();
+
+       } );
+
+       QUnit.test( 'Custom list item', function( assert ) {
+               var $input = newTestSuggester( {
+                               customListItem: { content: 'plain text' }
+                       } ),
+                       suggester = $input.data( 'suggester' ),
+                       check = false;
+
+               suggester.search( 'd' );
+
+               assert.equal(
+                       suggester.menu.element.children( '.ui-menu-item' 
).length,
+                       2,
+                       'Appended custom list item.'
+               );
+
+               assert.equal(
+                       suggester.menu.element.children( '.ui-menu-item' 
).last().children( 'a' ).text(),
+                       'plain text',
+                       'Set plain text on custom list item.'
+               );
+
+               suggester.customListItem( {
+                       content: $( '<span/>' ).text( 'jQuery node' ),
+                       action: function( suggester ) {
+                               check = true;
+                       }
+               } );
+
+               suggester.search( 'a' );
+
+               assert.equal(
+                       suggester.menu.element.find( '.ui-menu-item a span' 
).length,
+                       1,
+                       'Attached jQuery node as custom list item.'
+               );
+
+               suggester.menu.element.find( '.ui-menu-item a' 
).last().mouseenter();
+               suggester.menu.element.find( '.ui-menu-item a' ).last().click();
+
+               assert.ok(
+                       check,
+                       'Issued custom list item action via click event.'
+               );
+
+               check = false;
+
+               suggester.search( 'a' );
+
+               assert.equal(
+                       suggester.menu.element.find( '.ui-menu-item a span' 
).length,
+                       1,
+                       'Attached jQuery node as custom list item.'
+               );
+
+               var keydownEvent = $.Event( 'keydown' );
+
+               // move up to select custom list item which is the last one in 
the list
+               keydownEvent.keyCode = $.ui.keyCode.UP;
+               suggester.element.trigger( keydownEvent );
+
+               // simulate hitting enter key
+               keydownEvent.keyCode = $.ui.keyCode.ENTER;
+               suggester.element.trigger( keydownEvent );
+
+               assert.ok(
+                       check,
+                       'Issued custom list item action via keydown event.'
+               );
+
+               suggester.destroy();
+               $input.remove();
+       } );
+
+}( jQuery, QUnit ) );

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I0e7eeee9d94c1dd68fb6af7ff63ef30062595ea8
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Daniel Werner <[email protected]>

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

Reply via email to