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