Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 119 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
HTMLMultiSelectField | |
0.00% |
0 / 118 |
|
0.00% |
0 / 11 |
1722 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
validate | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getInputHTML | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
formatOptions | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
getOneCheckbox | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getOptionsOOUI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInputOOUI | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
210 | |||
loadDataFromRequest | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getDefault | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
filterDataForSubmit | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
needsLabel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\HTMLForm\Field; |
4 | |
5 | use MediaWiki\Html\Html; |
6 | use MediaWiki\HTMLForm\HTMLFormField; |
7 | use MediaWiki\HTMLForm\HTMLNestedFilterable; |
8 | use MediaWiki\HTMLForm\OOUIHTMLForm; |
9 | use MediaWiki\Request\WebRequest; |
10 | use MediaWiki\Xml\Xml; |
11 | use RuntimeException; |
12 | |
13 | /** |
14 | * Multi-select field |
15 | * |
16 | * @stable to extend |
17 | */ |
18 | class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable { |
19 | /** @var string */ |
20 | private $mPlaceholder; |
21 | |
22 | /** |
23 | * @stable to call |
24 | * |
25 | * @param array $params |
26 | * In adition to the usual HTMLFormField parameters, this can take the following fields: |
27 | * - dropdown: If given, the options will be displayed inside a dropdown with a text field that |
28 | * can be used to filter them. This is desirable mostly for very long lists of options. |
29 | * This only works for users with JavaScript support and falls back to the list of checkboxes. |
30 | * - flatlist: If given, the options will be displayed on a single line (wrapping to following |
31 | * lines if necessary), rather than each one on a line of its own. This is desirable mostly |
32 | * for very short lists of concisely labelled options. |
33 | */ |
34 | public function __construct( $params ) { |
35 | parent::__construct( $params ); |
36 | |
37 | // If the disabled-options parameter is not provided, use an empty array |
38 | if ( !isset( $this->mParams['disabled-options'] ) ) { |
39 | $this->mParams['disabled-options'] = []; |
40 | } |
41 | |
42 | if ( isset( $params['dropdown'] ) ) { |
43 | $this->mClass .= ' mw-htmlform-dropdown'; |
44 | if ( isset( $params['placeholder'] ) ) { |
45 | $this->mPlaceholder = $params['placeholder']; |
46 | } elseif ( isset( $params['placeholder-message'] ) ) { |
47 | $this->mPlaceholder = $this->msg( $params['placeholder-message'] )->text(); |
48 | } |
49 | } |
50 | |
51 | if ( isset( $params['flatlist'] ) ) { |
52 | $this->mClass .= ' mw-htmlform-flatlist'; |
53 | } |
54 | } |
55 | |
56 | /** |
57 | * @inheritDoc |
58 | * @stable to override |
59 | */ |
60 | public function validate( $value, $alldata ) { |
61 | $p = parent::validate( $value, $alldata ); |
62 | |
63 | if ( $p !== true ) { |
64 | return $p; |
65 | } |
66 | |
67 | if ( !is_array( $value ) ) { |
68 | return false; |
69 | } |
70 | |
71 | // Reject nested arrays (T274955) |
72 | $value = array_filter( $value, 'is_scalar' ); |
73 | |
74 | # If all options are valid, array_intersect of the valid options |
75 | # and the provided options will return the provided options. |
76 | $validOptions = HTMLFormField::flattenOptions( $this->getOptions() ); |
77 | |
78 | $validValues = array_intersect( $value, $validOptions ); |
79 | if ( count( $validValues ) == count( $value ) ) { |
80 | return true; |
81 | } else { |
82 | return $this->msg( 'htmlform-select-badoption' ); |
83 | } |
84 | } |
85 | |
86 | /** |
87 | * @inheritDoc |
88 | * @stable to override |
89 | */ |
90 | public function getInputHTML( $value ) { |
91 | $value = HTMLFormField::forceToStringRecursive( $value ); |
92 | $html = $this->formatOptions( $this->getOptions(), $value ); |
93 | |
94 | return $html; |
95 | } |
96 | |
97 | /** |
98 | * @stable to override |
99 | * |
100 | * @param array $options |
101 | * @param mixed $value |
102 | * |
103 | * @return string |
104 | */ |
105 | public function formatOptions( $options, $value ) { |
106 | $html = ''; |
107 | |
108 | $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] ); |
109 | |
110 | foreach ( $options as $label => $info ) { |
111 | if ( is_array( $info ) ) { |
112 | $html .= Html::rawElement( 'h1', [], $label ) . "\n"; |
113 | $html .= $this->formatOptions( $info, $value ); |
114 | } else { |
115 | $thisAttribs = [ |
116 | 'id' => "{$this->mID}-$info", |
117 | 'value' => $info, |
118 | ]; |
119 | if ( in_array( $info, $this->mParams['disabled-options'], true ) ) { |
120 | $thisAttribs['disabled'] = 'disabled'; |
121 | } |
122 | $checked = in_array( $info, $value, true ); |
123 | |
124 | $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs, $label ); |
125 | |
126 | $html .= ' ' . Html::rawElement( |
127 | 'div', |
128 | [ 'class' => 'mw-htmlform-flatlist-item' ], |
129 | $checkbox |
130 | ); |
131 | } |
132 | } |
133 | |
134 | return $html; |
135 | } |
136 | |
137 | protected function getOneCheckbox( $checked, $attribs, $label ) { |
138 | if ( $this->mParent instanceof OOUIHTMLForm ) { |
139 | throw new RuntimeException( __METHOD__ . ' is not supported' ); |
140 | } else { |
141 | $elementFunc = [ Html::class, $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ]; |
142 | $checkbox = |
143 | Xml::check( "{$this->mName}[]", $checked, $attribs ) . |
144 | "\u{00A0}" . |
145 | call_user_func( $elementFunc, |
146 | 'label', |
147 | [ 'for' => $attribs['id'] ], |
148 | $label |
149 | ); |
150 | return $checkbox; |
151 | } |
152 | } |
153 | |
154 | /** |
155 | * Get options and make them into arrays suitable for OOUI. |
156 | * @stable to override |
157 | */ |
158 | public function getOptionsOOUI() { |
159 | // @phan-suppress-previous-line PhanPluginNeverReturnMethod |
160 | // Sections make this difficult. See getInputOOUI(). |
161 | throw new RuntimeException( __METHOD__ . ' is not supported' ); |
162 | } |
163 | |
164 | /** |
165 | * Get the OOUI version of this field. |
166 | * |
167 | * Returns OOUI\CheckboxMultiselectInputWidget for fields that only have one section, |
168 | * string otherwise. |
169 | * |
170 | * @stable to override |
171 | * @since 1.28 |
172 | * @param string[] $value |
173 | * @return string|\OOUI\CheckboxMultiselectInputWidget |
174 | * @suppress PhanParamSignatureMismatch |
175 | */ |
176 | public function getInputOOUI( $value ) { |
177 | $this->mParent->getOutput()->addModules( 'oojs-ui-widgets' ); |
178 | |
179 | // Reject nested arrays (T274955) |
180 | $value = array_filter( $value, 'is_scalar' ); |
181 | |
182 | $hasSections = false; |
183 | $optionsOouiSections = []; |
184 | $options = $this->getOptions(); |
185 | // If the options are supposed to be split into sections, each section becomes a separate |
186 | // CheckboxMultiselectInputWidget. |
187 | foreach ( $options as $label => $section ) { |
188 | if ( is_array( $section ) ) { |
189 | $optionsOouiSections[ $label ] = Html::listDropdownOptionsOoui( $section ); |
190 | unset( $options[$label] ); |
191 | $hasSections = true; |
192 | } |
193 | } |
194 | // If anything remains in the array, they are sectionless options. Put them in a separate widget |
195 | // at the beginning. |
196 | if ( $options ) { |
197 | $optionsOouiSections = array_merge( |
198 | [ '' => Html::listDropdownOptionsOoui( $options ) ], |
199 | $optionsOouiSections |
200 | ); |
201 | } |
202 | '@phan-var array[][] $optionsOouiSections'; |
203 | |
204 | $out = []; |
205 | foreach ( $optionsOouiSections as $sectionLabel => $optionsOoui ) { |
206 | $attr = []; |
207 | $attr['name'] = "{$this->mName}[]"; |
208 | |
209 | $attr['value'] = $value; |
210 | |
211 | $options = $optionsOoui; |
212 | foreach ( $options as &$option ) { |
213 | $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true ); |
214 | } |
215 | if ( $this->mOptionsLabelsNotFromMessage ) { |
216 | foreach ( $options as &$option ) { |
217 | $option['label'] = new \OOUI\HtmlSnippet( $option['label'] ); |
218 | } |
219 | } |
220 | unset( $option ); |
221 | $attr['options'] = $options; |
222 | |
223 | $attr += \OOUI\Element::configFromHtmlAttributes( |
224 | $this->getAttributes( [ 'disabled', 'tabindex' ] ) |
225 | ); |
226 | |
227 | if ( $this->mClass !== '' ) { |
228 | $attr['classes'] = [ $this->mClass ]; |
229 | } |
230 | |
231 | $widget = new \OOUI\CheckboxMultiselectInputWidget( $attr ); |
232 | if ( $sectionLabel ) { |
233 | $out[] = new \OOUI\FieldsetLayout( [ |
234 | 'items' => [ $widget ], |
235 | // @phan-suppress-next-line SecurityCheck-XSS Key is html, taint cannot track that |
236 | 'label' => new \OOUI\HtmlSnippet( $sectionLabel ), |
237 | ] ); |
238 | } else { |
239 | $out[] = $widget; |
240 | } |
241 | } |
242 | |
243 | if ( !$hasSections && $out ) { |
244 | if ( $this->mPlaceholder ) { |
245 | $out[0]->setData( ( $out[0]->getData() ?: [] ) + [ |
246 | 'placeholder' => $this->mPlaceholder, |
247 | ] ); |
248 | } |
249 | // Directly return the only OOUI\CheckboxMultiselectInputWidget. |
250 | // This allows it to be made infusable and later tweaked by JS code. |
251 | return $out[0]; |
252 | } |
253 | |
254 | return implode( '', $out ); |
255 | } |
256 | |
257 | /** |
258 | * @stable to override |
259 | * @param WebRequest $request |
260 | * |
261 | * @return string|array |
262 | */ |
263 | public function loadDataFromRequest( $request ) { |
264 | $fromRequest = $request->getArray( $this->mName, [] ); |
265 | // Fetch the value in either one of the two following case: |
266 | // - we have a valid submit attempt (form was just submitted) |
267 | // - we have a value (an URL manually built by the user, or GET form with no wpFormIdentifier) |
268 | if ( $this->isSubmitAttempt( $request ) || $fromRequest ) { |
269 | // Checkboxes are just not added to the request arrays if they're not checked, |
270 | // so it's perfectly possible for there not to be an entry at all |
271 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable getArray does not return null |
272 | return $fromRequest; |
273 | } else { |
274 | // That's ok, the user has not yet submitted the form, so show the defaults |
275 | return $this->getDefault(); |
276 | } |
277 | } |
278 | |
279 | /** |
280 | * @inheritDoc |
281 | * @stable to override |
282 | */ |
283 | public function getDefault() { |
284 | return $this->mDefault ?? []; |
285 | } |
286 | |
287 | /** |
288 | * @inheritDoc |
289 | * @stable to override |
290 | */ |
291 | public function filterDataForSubmit( $data ) { |
292 | $data = HTMLFormField::forceToStringRecursive( $data ); |
293 | $options = HTMLFormField::flattenOptions( $this->getOptions() ); |
294 | $forcedOn = array_intersect( $this->mParams['disabled-options'], $this->getDefault() ); |
295 | |
296 | $res = []; |
297 | foreach ( $options as $opt ) { |
298 | $res["$opt"] = in_array( $opt, $forcedOn, true ) || in_array( $opt, $data, true ); |
299 | } |
300 | |
301 | return $res; |
302 | } |
303 | |
304 | /** |
305 | * @inheritDoc |
306 | * @stable to override |
307 | */ |
308 | protected function needsLabel() { |
309 | return false; |
310 | } |
311 | } |
312 | |
313 | /** @deprecated class alias since 1.42 */ |
314 | class_alias( HTMLMultiSelectField::class, 'HTMLMultiSelectField' ); |