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