Add HTMLAutoCompleteSelectField
authorBrad Jorsch <bjorsch@wikimedia.org>
Fri, 27 Jun 2014 20:16:59 +0000 (16:16 -0400)
committerBrad Jorsch <bjorsch@wikimedia.org>
Tue, 26 Aug 2014 19:14:23 +0000 (15:14 -0400)
This is much like the one OAuth has in
Special:OAuthConsumerRegistration/propose, except it stores the
autocompletion options in a data property rather than a global and uses
jquery.suggestions rather than jquery.ui.autocomplete.

Change-Id: I42473cea75f3706cc0125167f9191275ca6cb3b0

RELEASE-NOTES-1.24
includes/AutoLoader.php
includes/htmlform/HTMLAutoCompleteSelectField.php [new file with mode: 0644]
includes/htmlform/HTMLForm.php
resources/src/mediawiki/mediawiki.htmlform.js

index 51b6c5d..e7fba76 100644 (file)
@@ -173,6 +173,7 @@ production.
   the user will now see "Animals#Dog" in their browser instead of "Dog#Dog".
 * API token handling has been rewritten. Any API module using tokens will need
   to be updated.
+* Added HTMLAutoCompleteSelectField.
 
 === Bug fixes in 1.24 ===
 * (bug 50572) MediaWiki:Blockip should support gender
index a3a1558..2fb4aa7 100644 (file)
@@ -81,6 +81,7 @@ $wgAutoloadLocalClasses = array(
        'Html' => 'includes/Html.php',
        'HtmlFormatter' => 'includes/HtmlFormatter.php',
        'HTMLApiField' => 'includes/htmlform/HTMLApiField.php',
+       'HTMLAutoCompleteSelectField' => 'includes/htmlform/HTMLAutoCompleteSelectField.php',
        'HTMLButtonField' => 'includes/htmlform/HTMLButtonField.php',
        'HTMLCheckField' => 'includes/htmlform/HTMLCheckField.php',
        'HTMLCheckMatrix' => 'includes/htmlform/HTMLCheckMatrix.php',
diff --git a/includes/htmlform/HTMLAutoCompleteSelectField.php b/includes/htmlform/HTMLAutoCompleteSelectField.php
new file mode 100644 (file)
index 0000000..4ea5e99
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * Text field for selecting a value from a large list of possible values, with
+ * auto-completion and optionally with a select dropdown for selecting common
+ * options.
+ *
+ * If one of 'options-messages', 'options', or 'options-message' is provided
+ * and non-empty, the select dropdown will be shown. An 'other' key will be
+ * appended using message 'htmlform-selectorother-other' if not already
+ * present.
+ *
+ * Besides the parameters recognized by HTMLTextField, the following are
+ * recognized:
+ *   options-messages - As for HTMLSelectField
+ *   options - As for HTMLSelectField
+ *   options-message - As for HTMLSelectField
+ *   autocomplete - Associative array mapping display text to values.
+ *   autocomplete-messages - Like autocomplete, but keys are message names.
+ *   require-match - Boolean, if true the value must be in the options or the
+ *     autocomplete.
+ *   other-message - Message to use instead of htmlform-selectorother-other for
+ *      the 'other' message.
+ *   other - Raw text to use for the 'other' message
+ *
+ */
+class HTMLAutoCompleteSelectField extends HTMLTextField {
+       protected $autocomplete = array();
+
+       function __construct( $params ) {
+               $params += array(
+                       'require-match' => false,
+               );
+
+               parent::__construct( $params );
+
+               if ( array_key_exists( 'autocomplete-messages', $this->mParams ) ) {
+                       foreach ( $this->mParams['autocomplete-messages'] as $key => $value ) {
+                               $key = $this->msg( $key )->plain();
+                               $this->autocomplete[$key] = strval( $value );
+                       }
+               } elseif ( array_key_exists( 'autocomplete', $this->mParams ) ) {
+                       foreach ( $this->mParams['autocomplete'] as $key => $value ) {
+                               $this->autocomplete[$key] = strval( $value );
+                       }
+               }
+               if ( !is_array( $this->autocomplete ) || !$this->autocomplete ) {
+                       throw new MWException( 'HTMLAutoCompleteSelectField called without any autocompletions' );
+               }
+
+               $this->getOptions();
+               if ( $this->mOptions && !in_array( 'other', $this->mOptions, true ) ) {
+                       if ( isset( $params['other-message'] ) ) {
+                               $msg = wfMessage( $params['other-message'] )->text();
+                       } elseif ( isset( $params['other'] ) ) {
+                               $msg = $params['other'];
+                       } else {
+                               $msg = wfMessage( 'htmlform-selectorother-other' )->text();
+                       }
+                       $this->mOptions[$msg] = 'other';
+               }
+       }
+
+       function loadDataFromRequest( $request ) {
+               if ( $request->getCheck( $this->mName ) ) {
+                       $val = $request->getText( $this->mName . '-select', 'other' );
+
+                       if ( $val === 'other' ) {
+                               $val = $request->getText( $this->mName );
+                               if ( isset( $this->autocomplete[$val] ) ) {
+                                       $val = $this->autocomplete[$val];
+                               }
+                       }
+
+                       return $val;
+               } else {
+                       return $this->getDefault();
+               }
+       }
+
+       function validate( $value, $alldata ) {
+               $p = parent::validate( $value, $alldata );
+
+               if ( $p !== true ) {
+                       return $p;
+               }
+
+               $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
+
+               if ( in_array( strval( $value ), $validOptions, true ) ) {
+                       return true;
+               } elseif ( in_array( strval( $value ), $this->autocomplete, true ) ) {
+                       return true;
+               } elseif ( $this->mParams['require-match'] ) {
+                       return $this->msg( 'htmlform-select-badoption' )->parse();
+               }
+
+               return true;
+       }
+
+       function getAttributes( array $list ) {
+               $attribs = array(
+                       'type' => 'text',
+                       'data-autocomplete' => FormatJson::encode( array_keys( $this->autocomplete ) ),
+               ) + parent::getAttributes( $list );
+
+               if ( $this->getOptions() ) {
+                       $attribs['data-hide-if'] = FormatJson::encode(
+                               array( '!==',  $this->mName . '-select', 'other' )
+                       );
+               }
+
+               return $attribs;
+       }
+
+       function getInputHTML( $value ) {
+               $oldClass = $this->mClass;
+               $this->mClass = (array)$this->mClass;
+
+               $valInSelect = false;
+               $ret = '';
+
+               if ( $this->getOptions() ) {
+                       if ( $value !== false ) {
+                               $value = strval( $value );
+                               $valInSelect = in_array(
+                                       $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
+                               );
+                       }
+
+                       $selected = $valInSelect ? $value : 'other';
+                       $select = new XmlSelect( $this->mName . '-select', $this->mID . '-select', $selected );
+                       $select->addOptions( $this->getOptions() );
+                       $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
+
+                       if ( !empty( $this->mParams['disabled'] ) ) {
+                               $select->setAttribute( 'disabled', 'disabled' );
+                       }
+
+                       if ( isset( $this->mParams['tabindex'] ) ) {
+                               $select->setAttribute( 'tabindex', $this->mParams['tabindex'] );
+                       }
+
+                       $ret = $select->getHTML() . "<br />\n";
+
+                       $this->mClass[] = 'mw-htmlform-hide-if';
+               }
+
+               if ( $valInSelect ) {
+                       $value = '';
+               } else {
+                       $key = array_search( strval( $value ), $this->autocomplete, true );
+                       if ( $key !== false ) {
+                               $value = $key;
+                       }
+               }
+
+               $this->mClass[] = 'mw-htmlform-autocomplete';
+               $ret .= parent::getInputHTML( $valInSelect ? '' : $value );
+               $this->mClass = $oldClass;
+
+               return $ret;
+       }
+
+}
index 3a455ab..50f7037 100644 (file)
@@ -120,6 +120,7 @@ class HTMLForm extends ContextSource {
                'edittools' => 'HTMLEditTools',
                'checkmatrix' => 'HTMLCheckMatrix',
                'cloner' => 'HTMLFormFieldCloner',
+               'autocompleteselect' => 'HTMLAutoCompleteSelectField',
                // HTMLTextField will output the correct type="" attribute automagically.
                // There are about four zillion other HTML5 input types, like range, but
                // we don't use those at the moment, so no point in adding all of them.
index 5027f7a..594800e 100644 (file)
        } );
 
        function enhance( $root ) {
+               var $matrixTooltips, $autocomplete;
 
                /**
                 * @ignore
                        } );
                }
 
-               var $matrixTooltips = $root.find( '.mw-htmlform-matrix .mw-htmlform-tooltip' );
+               $matrixTooltips = $root.find( '.mw-htmlform-matrix .mw-htmlform-tooltip' );
                if ( $matrixTooltips.length ) {
                        mw.loader.using( 'jquery.tipsy', function () {
                                $matrixTooltips.tipsy( { gravity: 's' } );
                        } );
                }
 
+               // Set up autocomplete fields
+               $autocomplete = $root.find( '.mw-htmlform-autocomplete' );
+               if ( $autocomplete.length ) {
+                       mw.loader.using( 'jquery.suggestions', function () {
+                               $autocomplete.suggestions( {
+                                       fetch: function ( val ) {
+                                               var $el = $( this );
+                                               $el.suggestions( 'suggestions',
+                                                       $.grep( $el.data( 'autocomplete' ), function ( v ) {
+                                                               return v.indexOf( val ) === 0;
+                                                       } )
+                                               );
+                                       }
+                               } );
+                       } );
+               }
+
                // Add/remove cloner clones without having to resubmit the form
                $root.find( '.mw-htmlform-cloner-delete-button' ).click( function ( ev ) {
                        ev.preventDefault();