Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 144
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLSelectOrOtherField
0.00% covered (danger)
0.00%
0 / 143
0.00% covered (danger)
0.00%
0 / 9
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getInputHTML
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
90
 shouldInfuseOOUI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOOUIModules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInputOOUI
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
72
 getInputWidget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInputCodex
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
90
 loadDataFromRequest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getFieldClasses
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 MediaWiki\Html\Html;
6use MediaWiki\HTMLForm\HTMLFormField;
7use MediaWiki\Request\WebRequest;
8use XmlSelect;
9
10/**
11 * Select dropdown field, with an additional "other" textbox.
12 *
13 * HTMLComboboxField implements the same functionality using a single form field
14 * and should be used instead.
15 *
16 * @stable to extend
17 */
18class HTMLSelectOrOtherField extends HTMLTextField {
19    private const FIELD_CLASS = 'mw-htmlform-select-or-other';
20
21    /**
22     * @stable to call
23     * @inheritDoc
24     */
25    public function __construct( $params ) {
26        parent::__construct( $params );
27        $this->getOptions();
28        if ( !in_array( 'other', $this->mOptions, true ) ) {
29            $msg =
30                $params['other'] ?? wfMessage( 'htmlform-selectorother-other' )->text();
31            // Have 'other' always as first element
32            $this->mOptions = [ $msg => 'other' ] + $this->mOptions;
33        }
34    }
35
36    public function getInputHTML( $value ) {
37        $valInSelect = false;
38
39        if ( $value !== false ) {
40            $value = strval( $value );
41            $valInSelect = in_array(
42                $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
43            );
44        }
45
46        $selected = $valInSelect ? $value : 'other';
47
48        $select = new XmlSelect( $this->mName, false, $selected );
49        $select->addOptions( $this->getOptions() );
50
51        $tbAttribs = [ 'size' => $this->getSize() ];
52
53        if ( !empty( $this->mParams['disabled'] ) ) {
54            $select->setAttribute( 'disabled', 'disabled' );
55            $tbAttribs['disabled'] = 'disabled';
56        }
57
58        if ( isset( $this->mParams['tabindex'] ) ) {
59            $select->setAttribute( 'tabindex', $this->mParams['tabindex'] );
60            $tbAttribs['tabindex'] = $this->mParams['tabindex'];
61        }
62
63        $select = $select->getHTML();
64
65        if ( isset( $this->mParams['maxlength'] ) ) {
66            $tbAttribs['maxlength'] = $this->mParams['maxlength'];
67        }
68
69        if ( isset( $this->mParams['minlength'] ) ) {
70            $tbAttribs['minlength'] = $this->mParams['minlength'];
71        }
72
73        $textbox = Html::input( $this->mName . '-other', $valInSelect ? '' : $value, 'text', $tbAttribs );
74
75        $wrapperAttribs = [
76            'id' => $this->mID,
77            'class' => $this->getFieldClasses()
78        ];
79        if ( $this->mClass !== '' ) {
80            $wrapperAttribs['class'][] = $this->mClass;
81        }
82        return Html::rawElement(
83            'div',
84            $wrapperAttribs,
85            "$select<br />\n$textbox"
86        );
87    }
88
89    protected function shouldInfuseOOUI() {
90        return true;
91    }
92
93    protected function getOOUIModules() {
94        return [ 'mediawiki.widgets.SelectWithInputWidget' ];
95    }
96
97    public function getInputOOUI( $value ) {
98        $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.SelectWithInputWidget.styles' );
99
100        $valInSelect = false;
101        if ( $value !== false ) {
102            $value = strval( $value );
103            $valInSelect = in_array(
104                $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
105            );
106        }
107
108        # DropdownInput
109        $dropdownAttribs = [
110            'name' => $this->mName,
111            'options' => $this->getOptionsOOUI(),
112            'value' => $valInSelect ? $value : 'other',
113        ];
114
115        $allowedParams = [
116            'disabled',
117            'tabindex',
118        ];
119
120        $dropdownAttribs += \OOUI\Element::configFromHtmlAttributes(
121            $this->getAttributes( $allowedParams )
122        );
123
124        # TextInput
125        $textAttribs = [
126            'name' => $this->mName . '-other',
127            'size' => $this->getSize(),
128            'value' => $valInSelect ? '' : $value,
129        ];
130
131        $allowedParams = [
132            'required',
133            'autofocus',
134            'multiple',
135            'disabled',
136            'tabindex',
137            'maxlength',
138            'minlength',
139        ];
140
141        $textAttribs += \OOUI\Element::configFromHtmlAttributes(
142            $this->getAttributes( $allowedParams )
143        );
144
145        if ( $this->mPlaceholder !== '' ) {
146            $textAttribs['placeholder'] = $this->mPlaceholder;
147        }
148
149        $disabled = false;
150        if ( isset( $this->mParams[ 'disabled' ] ) && $this->mParams[ 'disabled' ] ) {
151            $disabled = true;
152        }
153
154        $inputClasses = $this->getFieldClasses();
155        if ( $this->mClass !== '' ) {
156            $inputClasses = array_merge( $inputClasses, explode( ' ', $this->mClass ) );
157        }
158        return $this->getInputWidget( [
159            'id' => $this->mID,
160            'classes' => $inputClasses,
161            'disabled' => $disabled,
162            'textinput' => $textAttribs,
163            'dropdowninput' => $dropdownAttribs,
164            'required' => $this->mParams[ 'required' ] ?? false,
165            'or' => true,
166        ] );
167    }
168
169    public function getInputWidget( $params ) {
170        return new \MediaWiki\Widget\SelectWithInputWidget( $params );
171    }
172
173    public function getInputCodex( $value, $hasErrors ) {
174        // Figure out the value of the select.
175        $valInSelect = false;
176        if ( $value !== false ) {
177            $value = strval( $value );
178            $valInSelect = in_array(
179                $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
180            );
181        }
182        $selected = $valInSelect ? $value : 'other';
183
184        // Create the <select> element.
185        $select = new XmlSelect( $this->mName, false, $selected );
186        // TODO: Add support for error class once it's implemented in the Codex CSS-only Select.
187        $select->setAttribute( 'class', 'cdx-select' );
188        $select->addOptions( $this->getOptions() );
189
190        // Set up attributes for the select and the text input.
191        $textInputAttribs = [ 'size' => $this->getSize() ];
192        $textInputAttribs['name'] = $this->mName . '-other';
193
194        if ( !empty( $this->mParams['disabled'] ) ) {
195            $select->setAttribute( 'disabled', 'disabled' );
196            $textInputAttribs['disabled'] = 'disabled';
197        }
198
199        if ( isset( $this->mParams['tabindex'] ) ) {
200            $select->setAttribute( 'tabindex', $this->mParams['tabindex'] );
201            $textInputAttribs['tabindex'] = $this->mParams['tabindex'];
202        }
203
204        if ( isset( $this->mParams['maxlength'] ) ) {
205            $textInputAttribs['maxlength'] = $this->mParams['maxlength'];
206        }
207
208        if ( isset( $this->mParams['minlength'] ) ) {
209            $textInputAttribs['minlength'] = $this->mParams['minlength'];
210        }
211
212        // Get HTML of the select and text input.
213        $select = $select->getHTML();
214        $textInput = parent::buildCodexComponent(
215            $valInSelect ? '' : $value,
216            $hasErrors,
217            'text',
218            $this->mName . '-other',
219            $textInputAttribs
220        );
221
222        // Set up the wrapper element and return the entire component.
223        $wrapperAttribs = [
224            'id' => $this->mID,
225            'class' => $this->getFieldClasses()
226        ];
227        if ( $this->mClass !== '' ) {
228            $wrapperAttribs['class'][] = $this->mClass;
229        }
230        return Html::rawElement(
231            'div',
232            $wrapperAttribs,
233            "$select\n$textInput"
234        );
235    }
236
237    /**
238     * @param WebRequest $request
239     *
240     * @return string
241     */
242    public function loadDataFromRequest( $request ) {
243        if ( $request->getCheck( $this->mName ) ) {
244            $val = $request->getText( $this->mName );
245
246            if ( $val === 'other' ) {
247                $val = $request->getText( $this->mName . '-other' );
248            }
249
250            return $val;
251        } else {
252            return $this->getDefault();
253        }
254    }
255
256    /**
257     * Returns a list of classes that should be applied to the widget itself. Unfortunately, we can't use
258     * $this->mClass or the 'cssclass' config option, because they're also added to the outer field wrapper
259     * (which includes the label). This method exists a temporary workaround until HTMLFormField will have
260     * a stable way for subclasses to specify additional classes for the widget itself.
261     * @internal Should only be used in HTMLTimezoneField
262     * @return string[]
263     */
264    protected function getFieldClasses(): array {
265        return [ self::FIELD_CLASS ];
266    }
267}
268
269/** @deprecated class alias since 1.42 */
270class_alias( HTMLSelectOrOtherField::class, 'HTMLSelectOrOtherField' );