Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLSelectAndOtherField
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 10
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getInputHTML
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 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 / 46
0.00% covered (danger)
0.00%
0 / 1
20
 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 / 35
0.00% covered (danger)
0.00%
0 / 1
12
 getDefault
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 loadDataFromRequest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace MediaWiki\HTMLForm\Field;
4
5use InvalidArgumentException;
6use MediaWiki\Html\Html;
7use MediaWiki\Request\WebRequest;
8use MediaWiki\Widget\SelectWithInputWidget;
9
10/**
11 * Double field with a dropdown list constructed from a system message in the format
12 *     * Optgroup header
13 *     ** <option value>
14 *     * New Optgroup header
15 * Plus a text field underneath for an additional reason.  The 'value' of the field is
16 * "<select>: <extra reason>", or "<extra reason>" if nothing has been selected in the
17 * select dropdown.
18 *
19 * @stable to extend
20 * @todo FIXME: If made 'required', only the text field should be compulsory.
21 */
22class HTMLSelectAndOtherField extends HTMLSelectField {
23    private const FIELD_CLASS = 'mw-htmlform-select-and-other-field';
24    /** @var string[] */
25    private $mFlatOptions;
26
27    /**
28     * @stable to call
29     * @inheritDoc
30     */
31    public function __construct( $params ) {
32        parent::__construct( $params );
33
34        if ( $this->getOptions() === null ) {
35            throw new InvalidArgumentException( 'HTMLSelectAndOtherField called without any options' );
36        }
37        if ( !in_array( 'other', $this->mOptions, true ) ) {
38            if ( array_key_exists( 'other', $params ) ) {
39                $msg = $params['other'];
40            } elseif ( array_key_exists( 'other-message', $params ) ) {
41                $msg = $this->getMessage( $params['other-message'] )->text();
42            } else {
43                $msg = $this->msg( 'htmlform-selectorother-other' )->text();
44            }
45            // Have 'other' always as first element
46            $this->mOptions = [ $msg => 'other' ] + $this->mOptions;
47        }
48        $this->mFlatOptions = self::flattenOptions( $this->getOptions() );
49    }
50
51    /** @inheritDoc */
52    public function getInputHTML( $value ) {
53        $select = parent::getInputHTML( $value[1] );
54
55        $textAttribs = [
56            'size' => $this->getSize(),
57        ];
58
59        if ( isset( $this->mParams['maxlength-unit'] ) ) {
60            $textAttribs['data-mw-maxlength-unit'] = $this->mParams['maxlength-unit'];
61        }
62
63        $allowedParams = [
64            'required',
65            'autofocus',
66            'multiple',
67            'disabled',
68            'tabindex',
69            'maxlength', // gets dynamic with javascript, see mediawiki.htmlform.js
70            'maxlength-unit', // 'bytes' or 'codepoints', see mediawiki.htmlform.js
71        ];
72
73        $textAttribs += $this->getAttributes( $allowedParams );
74
75        $textbox = Html::input( $this->mName . '-other', $value[2], 'text', $textAttribs );
76
77        $wrapperAttribs = [
78            'id' => $this->mID,
79            'class' => self::FIELD_CLASS
80        ];
81        if ( $this->mClass !== '' ) {
82            $wrapperAttribs['class'] .= ' ' . $this->mClass;
83        }
84        return Html::rawElement(
85            'div',
86            $wrapperAttribs,
87            "$select<br />\n$textbox"
88        );
89    }
90
91    /** @inheritDoc */
92    protected function getOOUIModules() {
93        return [ 'mediawiki.widgets.SelectWithInputWidget', 'mediawiki.widgets.visibleLengthLimit' ];
94    }
95
96    /** @inheritDoc */
97    public function getInputOOUI( $value ) {
98        $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.SelectWithInputWidget.styles' );
99
100        # TextInput
101        $textAttribs = [
102            'name' => $this->mName . '-other',
103            'value' => $value[2],
104        ];
105
106        $allowedParams = [
107            'required',
108            'autofocus',
109            'multiple',
110            'disabled',
111            'tabindex',
112            'maxlength',
113        ];
114
115        $textAttribs += \OOUI\Element::configFromHtmlAttributes(
116            $this->getAttributes( $allowedParams )
117        );
118
119        # DropdownInput
120        $dropdownInputAttribs = [
121            'name' => $this->mName,
122            'options' => $this->getOptionsOOUI(),
123            'value' => $value[1],
124        ];
125
126        $allowedParams = [
127            'tabindex',
128            'disabled',
129        ];
130
131        $dropdownInputAttribs += \OOUI\Element::configFromHtmlAttributes(
132            $this->getAttributes( $allowedParams )
133        );
134
135        $disabled = false;
136        if ( isset( $this->mParams[ 'disabled' ] ) && $this->mParams[ 'disabled' ] ) {
137            $disabled = true;
138        }
139
140        $inputClasses = [ self::FIELD_CLASS ];
141        if ( $this->mClass !== '' ) {
142            $inputClasses = array_merge( $inputClasses, explode( ' ', $this->mClass ) );
143        }
144        return $this->getInputWidget( [
145            'id' => $this->mID,
146            'disabled' => $disabled,
147            'textinput' => $textAttribs,
148            'dropdowninput' => $dropdownInputAttribs,
149            'or' => false,
150            'required' => $this->mParams[ 'required' ] ?? false,
151            'classes' => $inputClasses,
152            'data' => [
153                'maxlengthUnit' => $this->mParams['maxlength-unit'] ?? 'bytes'
154            ],
155        ] );
156    }
157
158    /**
159     * @stable to override
160     * @param array $params
161     * @return \MediaWiki\Widget\SelectWithInputWidget
162     */
163    public function getInputWidget( $params ) {
164        return new SelectWithInputWidget( $params );
165    }
166
167    /** @inheritDoc */
168    public function getInputCodex( $value, $hasErrors ) {
169        $select = parent::getInputCodex( $value[1], $hasErrors );
170
171        // Set up attributes for the text input.
172        $textInputAttribs = [
173            'size' => $this->getSize(),
174            'name' => $this->mName . '-other'
175        ];
176
177        if ( isset( $this->mParams['maxlength-unit'] ) ) {
178            $textInputAttribs['data-mw-maxlength-unit'] = $this->mParams['maxlength-unit'];
179        }
180
181        $allowedParams = [
182            'required',
183            'autofocus',
184            'multiple',
185            'disabled',
186            'tabindex',
187            'maxlength', // gets dynamic with javascript, see mediawiki.htmlform.js
188            'maxlength-unit', // 'bytes' or 'codepoints', see mediawiki.htmlform.js
189        ];
190
191        $textInputAttribs += $this->getAttributes( $allowedParams );
192
193        // Get text input HTML.
194        $textInput = HTMLTextField::buildCodexComponent(
195            $value[2],
196            $hasErrors,
197            'text',
198            $this->mName . '-other',
199            $textInputAttribs
200        );
201
202        // Set up the wrapper element and return the entire component.
203        $wrapperAttribs = [
204            'id' => $this->mID,
205            'class' => [ self::FIELD_CLASS ]
206        ];
207        if ( $this->mClass !== '' ) {
208            $wrapperAttribs['class'][] = $this->mClass;
209        }
210        return Html::rawElement(
211            'div',
212            $wrapperAttribs,
213            "$select<br />\n$textInput"
214        );
215    }
216
217    /**
218     * @inheritDoc
219     */
220    public function getDefault() {
221        $default = parent::getDefault();
222
223        // Default values of empty form
224        $final = '';
225        $list = 'other';
226        $text = '';
227
228        if ( $default !== null ) {
229            $final = $default;
230            // Assume the default is a text value, with the 'other' option selected.
231            // Then check if that assumption is correct, and update $list and $text if not.
232            $text = $final;
233            foreach ( $this->mFlatOptions as $option ) {
234                $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text();
235                if ( str_starts_with( $final, $match ) ) {
236                    $list = $option;
237                    $text = substr( $final, strlen( $match ) );
238                    break;
239                }
240            }
241        }
242
243        return [ $final, $list, $text ];
244    }
245
246    /**
247     * @param WebRequest $request
248     *
249     * @return array ["<overall message>","<select value>","<text field value>"]
250     */
251    public function loadDataFromRequest( $request ) {
252        if ( $request->getCheck( $this->mName ) ) {
253            $list = $request->getText( $this->mName );
254            $text = $request->getText( $this->mName . '-other' );
255
256            // Should be built the same as in mediawiki.htmlform.js
257            if ( $list == 'other' ) {
258                $final = $text;
259            } elseif ( !in_array( $list, $this->mFlatOptions, true ) ) {
260                # User has spoofed the select form to give an option which wasn't
261                # in the original offer.  Sulk...
262                $final = $text;
263            } elseif ( $text == '' ) {
264                $final = $list;
265            } else {
266                $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text;
267            }
268            return [ $final, $list, $text ];
269        }
270        return $this->getDefault();
271    }
272
273    /**
274     * @stable to override
275     * @return int
276     */
277    public function getSize() {
278        return $this->mParams['size'] ?? 45;
279    }
280
281    /** @inheritDoc */
282    public function validate( $value, $alldata ) {
283        # HTMLSelectField forces $value to be one of the options in the select
284        # field, which is not useful here.  But we do want the validation further up
285        # the chain
286        $p = parent::validate( $value[1], $alldata );
287
288        if ( $p !== true ) {
289            return $p;
290        }
291
292        if ( isset( $this->mParams['required'] )
293            && $this->mParams['required'] !== false
294            && $value[0] === ''
295        ) {
296            return $this->msg( 'htmlform-required' );
297        }
298
299        return true;
300    }
301}
302
303/** @deprecated class alias since 1.42 */
304class_alias( HTMLSelectAndOtherField::class, 'HTMLSelectAndOtherField' );