Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.23% covered (warning)
58.23%
46 / 79
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLAutoCompleteSelectField
58.97% covered (warning)
58.97%
46 / 78
16.67% covered (danger)
16.67%
1 / 6
108.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
12.63
 loadDataFromRequest
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 validate
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 getAttributes
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getInputHTML
72.41% covered (warning)
72.41%
21 / 29
0.00% covered (danger)
0.00%
0 / 1
10.70
 getInputOOUI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\HTMLForm\Field;
4
5use InvalidArgumentException;
6use MediaWiki\HTMLForm\HTMLFormField;
7use MediaWiki\Json\FormatJson;
8use MediaWiki\Xml\XmlSelect;
9
10/**
11 * Text field for selecting a value from a large list of possible values, with
12 * auto-completion and optionally with a select dropdown for selecting common
13 * options.
14 *
15 * HTMLComboboxField implements most of the same functionality and should be
16 * used instead, if possible.
17 *
18 * If one of 'options-messages', 'options', or 'options-message' is provided
19 * and non-empty, the select dropdown will be shown. An 'other' key will be
20 * appended using message 'htmlform-selectorother-other' if not already
21 * present.
22 *
23 * Besides the parameters recognized by HTMLTextField, the following are
24 * recognized:
25 *   options-messages - As for HTMLSelectField
26 *   options - As for HTMLSelectField
27 *   options-message - As for HTMLSelectField
28 *   autocomplete-data - Associative array mapping display text to values.
29 *   autocomplete-data-messages - Like autocomplete, but keys are message names.
30 *   require-match - Boolean, if true the value must be in the options or the
31 *     autocomplete.
32 *   other-message - Message to use instead of htmlform-selectorother-other for
33 *      the 'other' message.
34 *   other - Raw text to use for the 'other' message
35 *
36 * @stable to extend
37 */
38class HTMLAutoCompleteSelectField extends HTMLTextField {
39    /** @var string[] */
40    protected $autocompleteData = [];
41
42    /**
43     * @stable to call
44     * @inheritDoc
45     */
46    public function __construct( $params ) {
47        $params += [
48            'require-match' => false,
49        ];
50
51        parent::__construct( $params );
52
53        if ( array_key_exists( 'autocomplete-data-messages', $this->mParams ) ) {
54            foreach ( $this->mParams['autocomplete-data-messages'] as $key => $value ) {
55                $key = $this->msg( $key )->plain();
56                $this->autocompleteData[$key] = strval( $value );
57            }
58        } elseif ( array_key_exists( 'autocomplete-data', $this->mParams ) ) {
59            foreach ( $this->mParams['autocomplete-data'] as $key => $value ) {
60                $this->autocompleteData[$key] = strval( $value );
61            }
62        }
63        if ( !is_array( $this->autocompleteData ) || !$this->autocompleteData ) {
64            throw new InvalidArgumentException( 'HTMLAutoCompleteSelectField called without any autocompletions' );
65        }
66
67        $this->getOptions();
68        if ( $this->mOptions && !in_array( 'other', $this->mOptions, true ) ) {
69            if ( isset( $params['other-message'] ) ) {
70                $msg = $this->getMessage( $params['other-message'] )->text();
71            } elseif ( isset( $params['other'] ) ) {
72                $msg = $params['other'];
73            } else {
74                $msg = wfMessage( 'htmlform-selectorother-other' )->text();
75            }
76            $this->mOptions[$msg] = 'other';
77        }
78    }
79
80    public function loadDataFromRequest( $request ) {
81        if ( $request->getCheck( $this->mName ) ) {
82            $val = $request->getText( $this->mName . '-select', 'other' );
83
84            if ( $val === 'other' ) {
85                $val = $request->getText( $this->mName );
86                if ( isset( $this->autocompleteData[$val] ) ) {
87                    $val = $this->autocompleteData[$val];
88                }
89            }
90
91            return $val;
92        } else {
93            return $this->getDefault();
94        }
95    }
96
97    public function validate( $value, $alldata ) {
98        $p = parent::validate( $value, $alldata );
99
100        if ( $p !== true ) {
101            return $p;
102        }
103
104        $validOptions = HTMLFormField::flattenOptions( $this->getOptions() ?: [] );
105
106        if (
107            in_array( strval( $value ), $validOptions, true ) ||
108            in_array( strval( $value ), $this->autocompleteData, true )
109        ) {
110            return true;
111        } elseif ( $this->mParams['require-match'] ) {
112            return $this->msg( 'htmlform-select-badoption' );
113        }
114
115        return true;
116    }
117
118    // FIXME Ewww, this shouldn't be adding any attributes not requested in $list :(
119    public function getAttributes( array $list ) {
120        $attribs = [
121            'type' => 'text',
122            'data-autocomplete' => FormatJson::encode( array_keys( $this->autocompleteData ) ),
123        ] + parent::getAttributes( $list );
124
125        if ( $this->getOptions() ) {
126            $attribs['data-cond-state'] = FormatJson::encode( [
127                'hide' => [ '!==', $this->mName . '-select', 'other' ],
128            ] );
129        }
130
131        return $attribs;
132    }
133
134    public function getInputHTML( $value ) {
135        $oldClass = $this->mClass;
136        $classes = (array)$this->mClass;
137
138        $valInSelect = false;
139        $ret = '';
140
141        if ( $this->getOptions() ) {
142            if ( $value !== false ) {
143                $value = strval( $value );
144                $valInSelect = in_array(
145                    $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
146                );
147            }
148
149            $selected = $valInSelect ? $value : 'other';
150            $select = new XmlSelect( $this->mName . '-select', $this->mID . '-select', $selected );
151            $select->addOptions( $this->getOptions() );
152
153            if ( !empty( $this->mParams['disabled'] ) ) {
154                $select->setAttribute( 'disabled', 'disabled' );
155            }
156
157            if ( isset( $this->mParams['tabindex'] ) ) {
158                $select->setAttribute( 'tabindex', $this->mParams['tabindex'] );
159            }
160
161            $ret = $select->getHTML() . "<br />\n";
162
163            $classes[] = 'mw-htmlform-hide-if';
164        }
165
166        if ( $valInSelect ) {
167            $value = '';
168        } else {
169            $key = array_search( strval( $value ), $this->autocompleteData, true );
170            if ( $key !== false ) {
171                $value = $key;
172            }
173        }
174
175        $classes[] = 'mw-htmlform-autocomplete';
176        $this->mClass = implode( ' ', $classes );
177        $ret .= parent::getInputHTML( $valInSelect ? '' : $value );
178        $this->mClass = $oldClass;
179
180        return $ret;
181    }
182
183    /**
184     * Get the OOUI version of this input.
185     * @param string $value
186     * @return false
187     */
188    public function getInputOOUI( $value ) {
189        // To be implemented, for now override the function from HTMLTextField
190        return false;
191    }
192}
193
194/** @deprecated class alias since 1.42 */
195class_alias( HTMLAutoCompleteSelectField::class, 'HTMLAutoCompleteSelectField' );