Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 157
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 / 156
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 / 10
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        if ( array_key_exists( 'other', $params ) ) {
33            // Do nothing
34        } elseif ( array_key_exists( 'other-message', $params ) ) {
35            $params['other'] = $this->getMessage( $params['other-message'] )->plain();
36        } else {
37            $params['other'] = $this->msg( 'htmlform-selectorother-other' )->plain();
38        }
39
40        parent::__construct( $params );
41
42        if ( $this->getOptions() === null ) {
43            throw new InvalidArgumentException( 'HTMLSelectAndOtherField called without any options' );
44        }
45        if ( !in_array( 'other', $this->mOptions, true ) ) {
46            // Have 'other' always as first element
47            $this->mOptions = [ $params['other'] => 'other' ] + $this->mOptions;
48        }
49        $this->mFlatOptions = self::flattenOptions( $this->getOptions() );
50    }
51
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    protected function getOOUIModules() {
92        return [ 'mediawiki.widgets.SelectWithInputWidget' ];
93    }
94
95    public function getInputOOUI( $value ) {
96        $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.SelectWithInputWidget.styles' );
97
98        # TextInput
99        $textAttribs = [
100            'name' => $this->mName . '-other',
101            'value' => $value[2],
102        ];
103
104        $allowedParams = [
105            'required',
106            'autofocus',
107            'multiple',
108            'disabled',
109            'tabindex',
110            'maxlength',
111        ];
112
113        $textAttribs += \OOUI\Element::configFromHtmlAttributes(
114            $this->getAttributes( $allowedParams )
115        );
116
117        # DropdownInput
118        $dropdownInputAttribs = [
119            'name' => $this->mName,
120            'options' => $this->getOptionsOOUI(),
121            'value' => $value[1],
122        ];
123
124        $allowedParams = [
125            'tabindex',
126            'disabled',
127        ];
128
129        $dropdownInputAttribs += \OOUI\Element::configFromHtmlAttributes(
130            $this->getAttributes( $allowedParams )
131        );
132
133        $disabled = false;
134        if ( isset( $this->mParams[ 'disabled' ] ) && $this->mParams[ 'disabled' ] ) {
135            $disabled = true;
136        }
137
138        $inputClasses = [ self::FIELD_CLASS ];
139        if ( $this->mClass !== '' ) {
140            $inputClasses = array_merge( $inputClasses, explode( ' ', $this->mClass ) );
141        }
142        return $this->getInputWidget( [
143            'id' => $this->mID,
144            'disabled' => $disabled,
145            'textinput' => $textAttribs,
146            'dropdowninput' => $dropdownInputAttribs,
147            'or' => false,
148            'required' => $this->mParams[ 'required' ] ?? false,
149            'classes' => $inputClasses,
150            'data' => [
151                'maxlengthUnit' => $this->mParams['maxlength-unit'] ?? 'bytes'
152            ],
153        ] );
154    }
155
156    /**
157     * @stable to override
158     * @param array $params
159     * @return \MediaWiki\Widget\SelectWithInputWidget
160     */
161    public function getInputWidget( $params ) {
162        return new SelectWithInputWidget( $params );
163    }
164
165    public function getInputCodex( $value, $hasErrors ) {
166        $select = parent::getInputCodex( $value[1], $hasErrors );
167
168        // Set up attributes for the text input.
169        $textInputAttribs = [
170            'size' => $this->getSize(),
171            'name' => $this->mName . '-other'
172        ];
173
174        if ( isset( $this->mParams['maxlength-unit'] ) ) {
175            $textInputAttribs['data-mw-maxlength-unit'] = $this->mParams['maxlength-unit'];
176        }
177
178        $allowedParams = [
179            'required',
180            'autofocus',
181            'multiple',
182            'disabled',
183            'tabindex',
184            'maxlength', // gets dynamic with javascript, see mediawiki.htmlform.js
185            'maxlength-unit', // 'bytes' or 'codepoints', see mediawiki.htmlform.js
186        ];
187
188        $textInputAttribs += $this->getAttributes( $allowedParams );
189
190        // Get text input HTML.
191        $textInput = HTMLTextField::buildCodexComponent(
192            $value[2],
193            $hasErrors,
194            'text',
195            $this->mName . '-other',
196            $textInputAttribs
197        );
198
199        // Set up the wrapper element and return the entire component.
200        $wrapperAttribs = [
201            'id' => $this->mID,
202            'class' => [ self::FIELD_CLASS ]
203        ];
204        if ( $this->mClass !== '' ) {
205            $wrapperAttribs['class'][] = $this->mClass;
206        }
207        return Html::rawElement(
208            'div',
209            $wrapperAttribs,
210            "$select<br />\n$textInput"
211        );
212    }
213
214    /**
215     * @inheritDoc
216     */
217    public function getDefault() {
218        $default = parent::getDefault();
219
220        // Default values of empty form
221        $final = '';
222        $list = 'other';
223        $text = '';
224
225        if ( $default !== null ) {
226            $final = $default;
227            // Assume the default is a text value, with the 'other' option selected.
228            // Then check if that assumption is correct, and update $list and $text if not.
229            $text = $final;
230            foreach ( $this->mFlatOptions as $option ) {
231                $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text();
232                if ( str_starts_with( $final, $match ) ) {
233                    $list = $option;
234                    $text = substr( $final, strlen( $match ) );
235                    break;
236                }
237            }
238        }
239
240        return [ $final, $list, $text ];
241    }
242
243    /**
244     * @param WebRequest $request
245     *
246     * @return array ["<overall message>","<select value>","<text field value>"]
247     */
248    public function loadDataFromRequest( $request ) {
249        if ( $request->getCheck( $this->mName ) ) {
250            $list = $request->getText( $this->mName );
251            $text = $request->getText( $this->mName . '-other' );
252
253            // Should be built the same as in mediawiki.htmlform.js
254            if ( $list == 'other' ) {
255                $final = $text;
256            } elseif ( !in_array( $list, $this->mFlatOptions, true ) ) {
257                # User has spoofed the select form to give an option which wasn't
258                # in the original offer.  Sulk...
259                $final = $text;
260            } elseif ( $text == '' ) {
261                $final = $list;
262            } else {
263                $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text;
264            }
265            return [ $final, $list, $text ];
266        }
267        return $this->getDefault();
268    }
269
270    public function getSize() {
271        return $this->mParams['size'] ?? 45;
272    }
273
274    public function validate( $value, $alldata ) {
275        # HTMLSelectField forces $value to be one of the options in the select
276        # field, which is not useful here.  But we do want the validation further up
277        # the chain
278        $p = parent::validate( $value[1], $alldata );
279
280        if ( $p !== true ) {
281            return $p;
282        }
283
284        if ( isset( $this->mParams['required'] )
285            && $this->mParams['required'] !== false
286            && $value[0] === ''
287        ) {
288            return $this->msg( 'htmlform-required' );
289        }
290
291        return true;
292    }
293}
294
295/** @deprecated class alias since 1.42 */
296class_alias( HTMLSelectAndOtherField::class, 'HTMLSelectAndOtherField' );