Trevor Parscal has uploaded a new change for review.

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


Change subject: ve.ui.LookupWidget
......................................................................

ve.ui.LookupWidget

Refactoring of externally sourced suggestions for text inputs.

*.php
* Added links to new file

ve.ui.InputLabelWidget.js
* Changed to focus input element, not wrapper div

ve.ui.InputWidget.js
* Fixed incorrect documentation

ve.ui.LookupInputWidget.js
* New mixing that abstracts placing a menu of options below a text input and 
filling it with data from an external source

ve.ui.MenuWidget.js
* Fixed to get reference to input element, no wrapper div

ve.ui.MWLinkTargetInputWidget.js
* Moved pending and lookup functionality to mixing
* Implemented menu population using only matching pages, rather than a 
combination of that and page existence checks (fewer API calls)

ve.ui.TextInputMenuWidget.js
* Added configurable container to render underneath, rather than assuming 
this.input.$
* Added auto-position-on-window-resize functionality
* Fixed frame position correction to ensure that it only is used when the 
overlay is in a different frame from the container to position underneath

ve.ui.TextInputWidget.js
* Added placeholder text feature

Change-Id: If5ed1b64fd15982807691ce8bb0362970633108a
---
M VisualEditor.php
M demos/ve/index.php
M modules/ve/test/index.php
M modules/ve/ui/widgets/ve.ui.InputLabelWidget.js
M modules/ve/ui/widgets/ve.ui.InputWidget.js
A modules/ve/ui/widgets/ve.ui.LookupInputWidget.js
M modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js
M modules/ve/ui/widgets/ve.ui.MenuWidget.js
M modules/ve/ui/widgets/ve.ui.TextInputMenuWidget.js
M modules/ve/ui/widgets/ve.ui.TextInputWidget.js
10 files changed, 398 insertions(+), 278 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/70/61470/1

diff --git a/VisualEditor.php b/VisualEditor.php
index 744a721..f7a5f16 100644
--- a/VisualEditor.php
+++ b/VisualEditor.php
@@ -377,6 +377,7 @@
                        've/ui/widgets/ve.ui.MenuSectionItemWidget.js',
                        've/ui/widgets/ve.ui.MenuWidget.js',
                        've/ui/widgets/ve.ui.PendingInputWidget.js',
+                       've/ui/widgets/ve.ui.LookupInputWidget.js',
                        've/ui/widgets/ve.ui.TextInputMenuWidget.js',
                        've/ui/widgets/ve.ui.LinkTargetInputWidget.js',
                        've/ui/widgets/ve.ui.MWLinkTargetInputWidget.js',
diff --git a/demos/ve/index.php b/demos/ve/index.php
index 43218f4..9207468 100644
--- a/demos/ve/index.php
+++ b/demos/ve/index.php
@@ -262,6 +262,7 @@
                <script 
src="../../modules/ve/ui/widgets/ve.ui.MenuSectionItemWidget.js"></script>
                <script 
src="../../modules/ve/ui/widgets/ve.ui.MenuWidget.js"></script>
                <script 
src="../../modules/ve/ui/widgets/ve.ui.PendingInputWidget.js"></script>
+               <script 
src="../../modules/ve/ui/widgets/ve.ui.LookupInputWidget.js"></script>
                <script 
src="../../modules/ve/ui/widgets/ve.ui.TextInputMenuWidget.js"></script>
                <script 
src="../../modules/ve/ui/widgets/ve.ui.LinkTargetInputWidget.js"></script>
                <script 
src="../../modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js"></script>
diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php
index 6fdc970..3ce3a99 100644
--- a/modules/ve/test/index.php
+++ b/modules/ve/test/index.php
@@ -204,6 +204,8 @@
                <script 
src="../../ve/ui/widgets/ve.ui.MenuItemWidget.js"></script>
                <script 
src="../../ve/ui/widgets/ve.ui.MenuSectionItemWidget.js"></script>
                <script src="../../ve/ui/widgets/ve.ui.MenuWidget.js"></script>
+               <script 
src="../../ve/ui/widgets/ve.ui.PendingWidget.js"></script>
+               <script 
src="../../ve/ui/widgets/ve.ui.LookupInputWidget.js"></script>
                <script 
src="../../ve/ui/widgets/ve.ui.PendingInputWidget.js"></script>
                <script 
src="../../ve/ui/widgets/ve.ui.TextInputMenuWidget.js"></script>
                <script 
src="../../ve/ui/widgets/ve.ui.LinkTargetInputWidget.js"></script>
diff --git a/modules/ve/ui/widgets/ve.ui.InputLabelWidget.js 
b/modules/ve/ui/widgets/ve.ui.InputLabelWidget.js
index 31107ad..0393317 100644
--- a/modules/ve/ui/widgets/ve.ui.InputLabelWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.InputLabelWidget.js
@@ -58,7 +58,7 @@
  */
 ve.ui.InputLabelWidget.prototype.onClick = function () {
        if ( !this.disabled && this.input ) {
-               this.input.$.focus();
+               this.input.$input.focus();
        }
        return false;
 };
diff --git a/modules/ve/ui/widgets/ve.ui.InputWidget.js 
b/modules/ve/ui/widgets/ve.ui.InputWidget.js
index 85d949e..e6ca910 100644
--- a/modules/ve/ui/widgets/ve.ui.InputWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.InputWidget.js
@@ -52,7 +52,6 @@
 /**
  * @event change
  * @param value
- * @param origin
  */
 
 /**
diff --git a/modules/ve/ui/widgets/ve.ui.LookupInputWidget.js 
b/modules/ve/ui/widgets/ve.ui.LookupInputWidget.js
new file mode 100644
index 0000000..9804e0c
--- /dev/null
+++ b/modules/ve/ui/widgets/ve.ui.LookupInputWidget.js
@@ -0,0 +1,228 @@
+/*!
+ * VisualEditor UserInterface LookupInputWidget class.
+ *
+ * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * Lookup input widget.
+ *
+ * Mixin that adds a menu showing suggested values to a text input. Subclasses 
must handle `select`
+ * events on #lookupMenu to make use of selections.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {ve.ui.TextInputWidget} input Input widget
+ * @param {Object} [config] Config options
+ * @cfg {jQuery} [$overlay=this.$$( 'body' )] Element to append menu to
+ */
+ve.ui.LookupInputWidget = function VeUiLookupInputWidget( input, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.lookupInput = input;
+       this.$overlay = config.$overlay || this.$$( 'body' );
+       this.lookupMenu = new ve.ui.TextInputMenuWidget( this, {
+               '$$': ve.ui.get$$( this.$overlay ),
+               'input': this.lookupInput,
+               '$container': config.$container
+       } );
+       this.lookupCache = {};
+       this.lookupQuery = null;
+       this.lookupRequest = null;
+
+       // Events
+       this.$overlay.append( this.lookupMenu.$ );
+
+       this.lookupInput.$input.on( {
+               'focus': ve.bind( this.onLookupInputFocus, this ),
+               'blur': ve.bind( this.onLookupInputBlur, this ),
+               'mousedown': ve.bind( this.onLookupInputMouseDown, this )
+       } );
+       this.lookupInput.addListenerMethod( this, 'change', 
'onLookupInputChange' );
+
+       // Initialization
+       this.$.addClass( 've-ui-lookupWidget' );
+       this.lookupMenu.$.addClass( 've-ui-lookupWidget-menu' );
+};
+
+/* Methods */
+
+/**
+ * Handle input focus event.
+ *
+ * @method
+ * @param {jQuery.Event} e Input focus event
+ */
+ve.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Handle input blur event.
+ *
+ * @method
+ * @param {jQuery.Event} e Input blur event
+ */
+ve.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
+       this.lookupMenu.hide();
+};
+
+/**
+ * Handle input mouse down event.
+ *
+ * @method
+ * @param {jQuery.Event} e Input mouse down event
+ */
+ve.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Handle input change event.
+ *
+ * @method
+ * @param {string} value New input value
+ */
+ve.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Open the menu.
+ *
+ * @method
+ * @chainable
+ */
+ve.ui.LookupInputWidget.prototype.openLookupMenu = function () {
+       var value = this.lookupInput.getValue();
+
+       if ( value.length && $.trim( value ) !== '' ) {
+               this.populateLookupMenu();
+               if ( !this.lookupMenu.isVisible() ) {
+                       this.lookupMenu.show();
+               }
+       } else {
+               this.lookupMenu.hide();
+       }
+
+       return this;
+};
+
+/**
+ * Populate lookup menu with current information.
+ *
+ * Method should be extended in subclasses to clear the menu and add items.
+ *
+ * @method
+ * @chainable
+ */
+ve.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
+       var items = this.getLookupMenuItems();
+
+       this.lookupMenu.clearItems();
+
+       if ( items.length ) {
+               this.lookupMenu.addItems( items );
+               this.initializeLookupMenuSelection();
+       }
+
+       return this;
+};
+
+/**
+ * Set selection in the lookup menu with current information.
+ *
+ * @method
+ * @chainable
+ */
+ve.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
+       if ( !this.lookupMenu.getSelectedItem() ) {
+               this.lookupMenu.selectItem( 
this.lookupMenu.getClosestSelectableItem( 0 ), true );
+       }
+       this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
+};
+
+/**
+ * Get lookup menu items for the current query.
+ *
+ * @method
+ * @returns {ve.ui.MenuItemWidget[]} Menu items
+ */
+ve.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
+       var value = this.lookupInput.getValue();
+
+       if ( value && value !== this.lookupQuery ) {
+               // Abort current request if query has changed
+               if ( this.lookupRequest ) {
+                       this.lookupRequest.abort();
+                       this.lookupQuery = null;
+                       this.lookupRequest = null;
+               }
+               if ( value in this.lookupCache ) {
+                       return this.getLookupMenuItemsFromData( 
this.lookupCache[value] );
+               } else {
+                       this.lookupQuery = value;
+                       this.lookupRequest = this.getLookupRequest()
+                               .always( ve.bind( function () {
+                                       this.lookupQuery = null;
+                                       this.lookupRequest = null;
+                               }, this ) )
+                               .done( ve.bind( function ( data ) {
+                                       this.lookupCache[value] = 
this.getLookupCacheItemFromData( data );
+                                       this.openLookupMenu();
+                               }, this ) );
+                       // Support pending input widgets
+                       if ( ve.isMixedIn( this.lookupInput, 
ve.ui.PendingInputWidget ) ) {
+                               this.pushPending();
+                               this.lookupRequest.always( ve.bind( function () 
{
+                                       this.popPending();
+                               }, this ) );
+                       }
+               }
+       }
+       return [];
+};
+
+/**
+ * Get a new request object of the current lookup query value.
+ *
+ * @method
+ * @abstract
+ * @returns {jQuery.Deferred} Deferred object
+ */
+ve.ui.LookupInputWidget.prototype.getLookupRequest = function () {
+       // Stub, implemented in subclass
+       return null;
+};
+
+/**
+ * Handle successful lookup request.
+ *
+ * Overriding methods should call #populateLookupMenu when results are and 
cache results for future
+ * lookups in #lookupCache as an array of #ve.ui.MenuItemWidget objects.
+ *
+ * @method
+ * @abstract
+ * @param {mixed} data Response from server
+ */
+ve.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
+       // Stub, implemented in subclass
+};
+
+/**
+ * Get a list of menu item widgets from the data stored by the lookup 
request's done handler.
+ *
+ * @method
+ * @abstract
+ * @param {mixed} data Cached result data, usually an array
+ * @returns {ve.ui.MenuItemWidget[]} Menu items
+ */
+ve.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
+       // Stub, implemented in subclass
+       return [];
+};
diff --git a/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js 
b/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js
index b404f7e..c347e12 100644
--- a/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js
@@ -12,6 +12,8 @@
  *
  * @class
  * @extends ve.ui.LinkTargetInputWidget
+ * @mixins ve.ui.PendingInputWidget
+ * @mixins ve.ui.LookupInputWidget
  *
  * @constructor
  * @param {Object} [config] Config options
@@ -24,32 +26,16 @@
        // Parent constructor
        ve.ui.LinkTargetInputWidget.call( this, config );
 
-       // Properties
-       this.$overlay = config.$overlay || this.$$( 'body' );
-       this.menu = new ve.ui.TextInputMenuWidget(
-               this, { '$$': ve.ui.get$$( this.$overlay ), 'input': this }
-       );
-       this.annotation = null;
-       this.existingPages = {};
-       this.matchingPages = {};
-       this.existingPagesQuery = null;
-       this.existingPagesRequest = null;
-       this.matchingPagesQuery = null;
-       this.matchingPagesRequest = null;
-       this.previousMatches = null;
+       // Mixin constructors
+       ve.ui.PendingInputWidget.call( this );
+       ve.ui.LookupInputWidget.call( this, this, config );
 
        // Events
-       this.$overlay.append( this.menu.$ );
-       this.$input.on( {
-               'click': ve.bind( this.onClick, this ),
-               'focus': ve.bind( this.onFocus, this ),
-               'blur': ve.bind( this.onBlur, this )
-       } );
-       this.menu.on( 'select', ve.bind( this.onMenuItemSelect, this ) );
-       this.addListenerMethods( this, {'change': 'onChange'} );
+       this.lookupMenu.addListenerMethod( this, 'select', 
'onLookupMenuItemSelect' );
+
        // Initialization
        this.$.addClass( 've-ui-mwLinkTargetInputWidget' );
-       this.menu.$.addClass( 've-ui-mwLinkTargetInputWidget-menu' );
+       this.lookupMenu.$.addClass( 've-ui-mwLinkTargetInputWidget-menu' );
 };
 
 /* Inheritance */
@@ -57,64 +43,117 @@
 ve.inheritClass( ve.ui.MWLinkTargetInputWidget, ve.ui.LinkTargetInputWidget );
 
 ve.mixinClass( ve.ui.MWLinkTargetInputWidget, ve.ui.PendingInputWidget );
+ve.mixinClass( ve.ui.MWLinkTargetInputWidget, ve.ui.LookupInputWidget );
 
 /* Methods */
 
 /**
- * Handles click events.
+ * Handle menu item select event.
  *
  * @method
- * @param {jQuery.Event} e Mouse click event
+ * @param {ve.ui.MenuItemWidget} item Selected item
  */
-ve.ui.MWLinkTargetInputWidget.prototype.onClick = function () {
-       if ( !this.disabled ) {
-               this.openMenu();
-       }
-};
-
-/**
- * Handles focus events.
- *
- * @method
- * @param {jQuery.Event} e Input focus event
- */
-ve.ui.MWLinkTargetInputWidget.prototype.onFocus = function () {
-       if ( !this.disabled ) {
-               this.openMenu();
-       }
-};
-
-/**
- * Handles blur events.
- *
- * @method
- * @param {jQuery.Event} e Input blur
- */
-ve.ui.MWLinkTargetInputWidget.prototype.onBlur = function () {
-       this.menu.hide();
-};
-
-/**
- * Handles change events.
- *
- * @method
- * @param {ve.ui.MenuItemWidget} item Menu item
- */
-ve.ui.MWLinkTargetInputWidget.prototype.onMenuItemSelect = function ( item ) {
+ve.ui.MWLinkTargetInputWidget.prototype.onLookupMenuItemSelect = function ( 
item ) {
        if ( item ) {
                this.setAnnotation( item.getData() );
        }
 };
 
 /**
- * Opens the suggestion menu on input change.
- *
+ * Gets a new request object of the current lookup query value.
  *
  * @method
- * @param {string} value New value
+ * @returns {jQuery.Deferred} Deferred object with success and fail handlers 
already attached
  */
-ve.ui.MWLinkTargetInputWidget.prototype.onChange = function () {
-       this.openMenu();
+ve.ui.MWLinkTargetInputWidget.prototype.getLookupRequest = function () {
+       return $.ajax( {
+               'url': mw.util.wikiScript( 'api' ),
+               'data': {
+                       'format': 'json',
+                       'action': 'opensearch',
+                       'search': this.value,
+                       'namespace': 0,
+                       'suggest': ''
+               },
+               'dataType': 'json'
+       } );
+};
+
+/**
+ * Get lookup cache item from server response data.
+ *
+ * @method
+ * @param {mixed} data Response from server
+ */
+ve.ui.MWLinkTargetInputWidget.prototype.getLookupCacheItemFromData = function 
( data ) {
+       return ve.isArray( data ) && data.length ? data[1] : [];
+};
+
+/**
+ * Get list of menu items from a server response.
+ *
+ * @param {Object} data Query result
+ * @returns {ve.ui.MenuItemWidget[]} Menu items
+ */
+ve.ui.MWLinkTargetInputWidget.prototype.getLookupMenuItemsFromData = function 
( data ) {
+       var i, len,
+               menu$$ = this.lookupMenu.$$,
+               items = [],
+               matchingPages = data,
+               pageExists = this.value in matchingPages;
+
+       // External link
+       if ( ve.init.platform.getExternalLinkUrlProtocolsRegExp().test( 
this.value ) ) {
+               items.push( new ve.ui.MenuSectionItemWidget(
+                       'externalLink',
+                       { '$$': menu$$, 'label': ve.msg( 
'visualeditor-linkinspector-suggest-external-link' ) }
+               ) );
+               items.push( new ve.ui.MenuItemWidget(
+                       this.getExternalLinkAnnotationFromUrl( this.value ),
+                       { '$$': menu$$, 'rel': 'externalLink', 'label': 
this.value }
+               ) );
+       }
+
+       // Internal link
+       if ( !pageExists && ( !matchingPages || matchingPages.indexOf( 
this.value ) === -1 ) ) {
+               items.push( new ve.ui.MenuSectionItemWidget(
+                       'newPage',
+                       { '$$': menu$$, 'label': ve.msg( 
'visualeditor-linkinspector-suggest-new-page' ) }
+               ) );
+               items.push( new ve.ui.MenuItemWidget(
+                       this.getInternalLinkAnnotationFromTitle( this.value ),
+                       { '$$': menu$$, 'rel': 'newPage', 'label': this.value }
+               ) );
+       }
+
+       // Matching pages
+       if ( matchingPages && matchingPages.length ) {
+               items.push( new ve.ui.MenuSectionItemWidget(
+                       'matchingPages',
+                       { '$$': menu$$, 'label': ve.msg( 
'visualeditor-linkinspector-suggest-matching-page' ) }
+               ) );
+               for ( i = 0, len = matchingPages.length; i < len; i++ ) {
+                       items.push( new ve.ui.MenuItemWidget(
+                               this.getInternalLinkAnnotationFromTitle( 
matchingPages[i] ),
+                               { '$$': menu$$, 'rel': 'matchingPage', 'label': 
matchingPages[i] }
+                       ) );
+               }
+       }
+
+       return items;
+};
+
+/**
+ * Set selection in the lookup menu with current information.
+ *
+ * @method
+ * @chainable
+ */
+ve.ui.MWLinkTargetInputWidget.prototype.initializeLookupMenuSelection = 
function () {
+       // Attempt to maintain selection on current annotation
+       this.lookupMenu.selectItem( this.lookupMenu.getItemFromData( 
this.annotation ), true );
+       // Parent method
+       ve.ui.LookupInputWidget.prototype.initializeLookupMenuSelection.call( 
this );
 };
 
 /**
@@ -128,93 +167,6 @@
 ve.ui.MWLinkTargetInputWidget.prototype.setValue = function ( value ) {
        // Keep annotation in sync with value, call parent method.
        ve.ui.TextInputWidget.prototype.setValue.call( this, value );
-};
-
-/**
- * Opens the menu.
- *
- * @method
- * @chainable
- */
-ve.ui.MWLinkTargetInputWidget.prototype.openMenu = function () {
-       this.populateMenu();
-       this.queryPageExistence();
-       this.queryMatchingPages();
-       if ( this.value.length && $.trim( this.value ) !== '' && 
!this.menu.isVisible() ) {
-               this.menu.show();
-       }
-       return this;
-};
-
-/**
- * Populates the menu.
- *
- * @method
- * @chainable
- */
-ve.ui.MWLinkTargetInputWidget.prototype.populateMenu = function () {
-       var i, len,
-               menu$$ = this.menu.$$,
-               items = [],
-               pageExists = this.existingPages[this.value],
-               matchingPages = this.matchingPages[this.value];
-
-       // Reset
-       this.menu.clearItems();
-
-       // Hide on empty target
-       if ( !this.value.length ) {
-               this.menu.hide();
-               return this;
-       }
-
-       // External link
-       if ( ve.init.platform.getExternalLinkUrlProtocolsRegExp().test( 
this.value ) ) {
-               items.push( new ve.ui.MenuSectionItemWidget(
-                       'externalLink', { '$$': menu$$, 'label': ve.msg( 
'visualeditor-linkinspector-suggest-external-link' ) }
-               ) );
-               items.push( new ve.ui.MenuItemWidget(
-                       this.getExternalLinkAnnotationFromUrl( this.value ),
-                       { '$$': menu$$, 'rel': 'externalLink', 'label': 
this.value }
-               ) );
-       }
-
-       // Internal link
-       if ( !pageExists && ( !matchingPages || matchingPages.indexOf( 
this.value ) === -1 ) ) {
-               items.push( new ve.ui.MenuSectionItemWidget(
-                       'newPage', { '$$': menu$$, 'label': ve.msg( 
'visualeditor-linkinspector-suggest-new-page' ) }
-               ) );
-               items.push( new ve.ui.MenuItemWidget(
-                       this.getInternalLinkAnnotationFromTitle( this.value ),
-                       { '$$': menu$$, 'rel': 'newPage', 'label': this.value }
-               ) );
-       }
-
-       // Matching pages
-       if ( matchingPages && matchingPages.length ) {
-               items.push( new ve.ui.MenuSectionItemWidget(
-                       'matchingPages', { '$$': menu$$, 'label': ve.msg( 
'visualeditor-linkinspector-suggest-matching-page' ) }
-               ) );
-               for ( i = 0, len = matchingPages.length; i < len; i++ ) {
-                       items.push( new ve.ui.MenuItemWidget(
-                               this.getInternalLinkAnnotationFromTitle( 
matchingPages[i] ),
-                               { '$$': menu$$, 'rel': 'matchingPage', 'label': 
matchingPages[i] }
-                       ) );
-               }
-               this.previousMatches = matchingPages;
-       }
-
-       // Add items
-       this.menu.addItems( items );
-
-       // Auto-select
-       this.menu.selectItem( this.menu.getItemFromData( this.annotation ), 
true );
-       if ( !this.menu.getSelectedItem() ) {
-               this.menu.selectItem( this.menu.getClosestSelectableItem( 0 ), 
true );
-       }
-       this.menu.highlightItem( this.menu.getSelectedItem() );
-
-       return this;
 };
 
 /**
@@ -273,115 +225,4 @@
                return annotation.getAttribute( 'title' );
        }
        return '';
-};
-
-/**
- * Checks page existence for the current value.
- *
- * {ve.ui.MWLinkTargetInputWidget.populateMenu} will be called immediately if 
the page existence has
- * been cached, or as soon as the API returns a result.
- *
- * @method
- * @chainable
- */
-ve.ui.MWLinkTargetInputWidget.prototype.queryPageExistence = function () {
-       if ( this.existingPagesQuery === this.value ) {
-               // Ignore duplicate requests
-               return;
-       }
-       if ( this.existingPagesRequest ) {
-               this.existingPagesRequest.abort();
-               this.existingPagesQuery = null;
-               this.existingPagesRequest = null;
-       }
-       if ( this.value in this.existingPages ) {
-               this.populateMenu();
-       } else {
-               this.pushPending();
-               this.existingPagesQuery = this.value;
-               this.existingPagesRequest = $.ajax( {
-                       'url': mw.util.wikiScript( 'api' ),
-                       'data': {
-                               'format': 'json',
-                               'action': 'query',
-                               'indexpageids': '',
-                               'titles': this.value,
-                               'converttitles': ''
-                       },
-                       'dataType': 'json',
-                       'success': ve.bind( function ( data ) {
-                               this.existingPagesQuery = null;
-                               this.existingPagesRequest = null;
-                               var page,
-                                       exists = false;
-                               if ( data.query ) {
-                                       page = 
data.query.pages[data.query.pageids[0]];
-                                       exists = ( page.missing === undefined 
&& page.invalid === undefined );
-                                       // Cache result for normalized title
-                                       this.existingPages[page.title] = exists;
-                               }
-                               // Cache result for original input
-                               this.existingPages[this.value] = exists;
-                               this.populateMenu();
-                       }, this ),
-                       'complete': ve.bind( function () {
-                               this.popPending();
-                       }, this )
-               } );
-       }
-       return this;
-};
-
-/**
- * Checks matching pages for the current value.
- *
- * {ve.ui.MWLinkTargetInputWidget.populateMenu} will be called immediately if 
matching pages have
- * been cached, or as soon as the API returns a result.
- *
- * @method
- * @chainable
- */
-ve.ui.MWLinkTargetInputWidget.prototype.queryMatchingPages = function () {
-       if ( this.matchingPagesQuery === this.value ) {
-               // Ignore duplicate requests
-               return;
-       }
-       if ( this.matchingPagesRequest ) {
-               this.matchingPagesRequest.abort();
-               this.matchingPagesQuery = null;
-               this.matchingPagesRequest = null;
-       }
-       if ( this.value in this.matchingPages ) {
-               this.populateMenu();
-       } else {
-               this.pushPending();
-               this.matchingPagesQuery = this.value;
-               this.matchingPagesRequest = $.ajax( {
-                       'url': mw.util.wikiScript( 'api' ),
-                       'data': {
-                               'format': 'json',
-                               'action': 'opensearch',
-                               'search': this.value,
-                               'namespace': 0,
-                               'suggest': ''
-                       },
-                       'dataType': 'json',
-                       'success': ve.bind( function ( data ) {
-                               this.matchingPagesQuery = null;
-                               this.matchingPagesRequest = null;
-                               if ( ve.isArray( data ) && data.length ) {
-                                       // Cache the matches to the query
-                                       this.matchingPages[this.value] = 
data[1];
-                                       this.populateMenu();
-                               } else {
-                                       // Don't repeat queries that resulted 
in invalid responses
-                                       this.matchingPages[this.value] = [];
-                               }
-                       }, this ),
-                       'complete': ve.bind( function () {
-                               this.popPending();
-                       }, this )
-               } );
-       }
-       return this;
 };
diff --git a/modules/ve/ui/widgets/ve.ui.MenuWidget.js 
b/modules/ve/ui/widgets/ve.ui.MenuWidget.js
index 2be85d7..25332e8 100644
--- a/modules/ve/ui/widgets/ve.ui.MenuWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.MenuWidget.js
@@ -24,7 +24,7 @@
 
        // Properties
        this.newItems = [];
-       this.$input = config.input ? config.input.$ : this.$$( '<input>' );
+       this.$input = config.input ? config.input.$input : this.$$( '<input>' );
        this.$previousFocus = null;
        this.isolated = !config.input;
        this.visible = false;
diff --git a/modules/ve/ui/widgets/ve.ui.TextInputMenuWidget.js 
b/modules/ve/ui/widgets/ve.ui.TextInputMenuWidget.js
index 62ce685..2ee7a91 100644
--- a/modules/ve/ui/widgets/ve.ui.TextInputMenuWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.TextInputMenuWidget.js
@@ -14,6 +14,7 @@
  * @constructor
  * @param {ve.ui.TextInputWidget} input Text input widget to provide menu for
  * @param {Object} [config] Config options
+ * @cfg {jQuery} [$container=input.$] Element to render menu under
  */
 ve.ui.TextInputMenuWidget = function VeUiTextInputMenuWidget( input, config ) {
        // Parent constructor
@@ -21,6 +22,8 @@
 
        // Properties
        this.input = input;
+       this.$container = config.$container || this.input.$;
+       this.onWindowResizeHandler = ve.bind( this.onWindowResize, this );
 
        // Initialization
        this.$.addClass( 've-ui-textInputMenuWidget' );
@@ -30,6 +33,18 @@
 
 ve.inheritClass( ve.ui.TextInputMenuWidget, ve.ui.MenuWidget );
 
+/* Methods */
+
+/**
+ * Handle window resize event.
+ *
+ * @method
+ * @param {jQuery.Event} e Window resize event
+ */
+ve.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
+       this.position();
+};
+
 /**
  * Shows the menu.
  *
@@ -37,21 +52,50 @@
  * @chainable
  */
 ve.ui.TextInputMenuWidget.prototype.show = function () {
-       var dim, offset,
-               $input = this.input.$input;
-
-       // Call parent method
+       // Parent method
        ve.ui.MenuWidget.prototype.show.call( this );
 
-       // Position under input
-       dim = $input.offset();
-       dim.top += $input.outerHeight( true );
-       if ( this.input.$$.frame ) {
-               offset = this.input.$$.frame.$.offset();
-               dim.left += offset.left;
-               dim.top += offset.top;
-       }
-       this.$.css( dim );
+       this.position();
+       $( window ).on( 'resize', this.onWindowResizeHandler );
+       return this;
+};
 
+/**
+ * Hides the menu.
+ *
+ * @method
+ * @chainable
+ */
+ve.ui.TextInputMenuWidget.prototype.hide = function () {
+       // Parent method
+       ve.ui.MenuWidget.prototype.hide.call( this );
+
+       $( window ).off( 'resize', this.onWindowResizeHandler );
+       return this;
+};
+
+/**
+ * Positions the menu.
+ *
+ * @method
+ * @chainable
+ */
+ve.ui.TextInputMenuWidget.prototype.position = function () {
+       var frameOffset,
+               $container = this.$container,
+               dimensions = $container.offset();
+
+       // Position under input
+       dimensions.top += $container.outerHeight( true );
+       dimensions.width = $container.outerWidth( true );
+
+       // Compensate for frame position if in a differnt frame
+       if ( this.input.$$.frame && this.input.$$.context !== 
this.$[0].ownerDocument ) {
+               frameOffset = this.input.$$.frame.$.offset();
+               dimensions.left += frameOffset.left;
+               dimensions.top += frameOffset.top;
+       }
+
+       this.$.css( dimensions );
        return this;
 };
diff --git a/modules/ve/ui/widgets/ve.ui.TextInputWidget.js 
b/modules/ve/ui/widgets/ve.ui.TextInputWidget.js
index 7169e16..c1f7692 100644
--- a/modules/ve/ui/widgets/ve.ui.TextInputWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.TextInputWidget.js
@@ -13,6 +13,7 @@
  *
  * @constructor
  * @param {Object} [config] Config options
+ * @cfg {string} [placeholder] Placeholder text
  */
 ve.ui.TextInputWidget = function VeUiTextInputWidget( config ) {
        // Parent constructor
@@ -20,6 +21,9 @@
 
        // Initialization
        this.$.addClass( 've-ui-textInputWidget' );
+       if ( config.placeholder ) {
+               this.$input.attr( 'placeholder', config.placeholder );
+       }
 };
 
 /* Inheritance */

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: If5ed1b64fd15982807691ce8bb0362970633108a
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Trevor Parscal <[email protected]>

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

Reply via email to