Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 121 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
HTMLMultiSelectField | |
0.00% |
0 / 120 |
|
0.00% |
0 / 11 |
1806 | |
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 / 5 |
|
0.00% |
0 / 1 |
6 | |||
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 RuntimeException; |
11 | use Xml; |
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 | if ( isset( $this->mParams['dropdown'] ) ) { |
92 | $this->mParent->getOutput()->addModules( 'jquery.chosen' ); |
93 | } |
94 | |
95 | $value = HTMLFormField::forceToStringRecursive( $value ); |
96 | $html = $this->formatOptions( $this->getOptions(), $value ); |
97 | |
98 | return $html; |
99 | } |
100 | |
101 | /** |
102 | * @stable to override |
103 | * |
104 | * @param array $options |
105 | * @param mixed $value |
106 | * |
107 | * @return string |
108 | */ |
109 | public function formatOptions( $options, $value ) { |
110 | $html = ''; |
111 | |
112 | $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] ); |
113 | |
114 | foreach ( $options as $label => $info ) { |
115 | if ( is_array( $info ) ) { |
116 | $html .= Html::rawElement( 'h1', [], $label ) . "\n"; |
117 | $html .= $this->formatOptions( $info, $value ); |
118 | } else { |
119 | $thisAttribs = [ |
120 | 'id' => "{$this->mID}-$info", |
121 | 'value' => $info, |
122 | ]; |
123 | if ( in_array( $info, $this->mParams['disabled-options'], true ) ) { |
124 | $thisAttribs['disabled'] = 'disabled'; |
125 | } |
126 | $checked = in_array( $info, $value, true ); |
127 | |
128 | $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs, $label ); |
129 | |
130 | $html .= ' ' . Html::rawElement( |
131 | 'div', |
132 | [ 'class' => 'mw-htmlform-flatlist-item' ], |
133 | $checkbox |
134 | ); |
135 | } |
136 | } |
137 | |
138 | return $html; |
139 | } |
140 | |
141 | protected function getOneCheckbox( $checked, $attribs, $label ) { |
142 | if ( $this->mParent instanceof OOUIHTMLForm ) { |
143 | throw new RuntimeException( __METHOD__ . ' is not supported' ); |
144 | } else { |
145 | $elementFunc = [ Html::class, $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ]; |
146 | $checkbox = |
147 | Xml::check( "{$this->mName}[]", $checked, $attribs ) . |
148 | "\u{00A0}" . |
149 | call_user_func( $elementFunc, |
150 | 'label', |
151 | [ 'for' => $attribs['id'] ], |
152 | $label |
153 | ); |
154 | return $checkbox; |
155 | } |
156 | } |
157 | |
158 | /** |
159 | * Get options and make them into arrays suitable for OOUI. |
160 | * @stable to override |
161 | */ |
162 | public function getOptionsOOUI() { |
163 | // @phan-suppress-previous-line PhanPluginNeverReturnMethod |
164 | // Sections make this difficult. See getInputOOUI(). |
165 | throw new RuntimeException( __METHOD__ . ' is not supported' ); |
166 | } |
167 | |
168 | /** |
169 | * Get the OOUI version of this field. |
170 | * |
171 | * Returns OOUI\CheckboxMultiselectInputWidget for fields that only have one section, |
172 | * string otherwise. |
173 | * |
174 | * @stable to override |
175 | * @since 1.28 |
176 | * @param string[] $value |
177 | * @return string|\OOUI\CheckboxMultiselectInputWidget |
178 | * @suppress PhanParamSignatureMismatch |
179 | */ |
180 | public function getInputOOUI( $value ) { |
181 | $this->mParent->getOutput()->addModules( 'oojs-ui-widgets' ); |
182 | |
183 | // Reject nested arrays (T274955) |
184 | $value = array_filter( $value, 'is_scalar' ); |
185 | |
186 | $hasSections = false; |
187 | $optionsOouiSections = []; |
188 | $options = $this->getOptions(); |
189 | // If the options are supposed to be split into sections, each section becomes a separate |
190 | // CheckboxMultiselectInputWidget. |
191 | foreach ( $options as $label => $section ) { |
192 | if ( is_array( $section ) ) { |
193 | $optionsOouiSections[ $label ] = Html::listDropdownOptionsOoui( $section ); |
194 | unset( $options[$label] ); |
195 | $hasSections = true; |
196 | } |
197 | } |
198 | // If anything remains in the array, they are sectionless options. Put them in a separate widget |
199 | // at the beginning. |
200 | if ( $options ) { |
201 | $optionsOouiSections = array_merge( |
202 | [ '' => Html::listDropdownOptionsOoui( $options ) ], |
203 | $optionsOouiSections |
204 | ); |
205 | } |
206 | '@phan-var array[][] $optionsOouiSections'; |
207 | |
208 | $out = []; |
209 | foreach ( $optionsOouiSections as $sectionLabel => $optionsOoui ) { |
210 | $attr = []; |
211 | $attr['name'] = "{$this->mName}[]"; |
212 | |
213 | $attr['value'] = $value; |
214 | |
215 | $options = $optionsOoui; |
216 | foreach ( $options as &$option ) { |
217 | $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true ); |
218 | } |
219 | if ( $this->mOptionsLabelsNotFromMessage ) { |
220 | foreach ( $options as &$option ) { |
221 | $option['label'] = new \OOUI\HtmlSnippet( $option['label'] ); |
222 | } |
223 | } |
224 | unset( $option ); |
225 | $attr['options'] = $options; |
226 | |
227 | $attr += \OOUI\Element::configFromHtmlAttributes( |
228 | $this->getAttributes( [ 'disabled', 'tabindex' ] ) |
229 | ); |
230 | |
231 | if ( $this->mClass !== '' ) { |
232 | $attr['classes'] = [ $this->mClass ]; |
233 | } |
234 | |
235 | $widget = new \OOUI\CheckboxMultiselectInputWidget( $attr ); |
236 | if ( $sectionLabel ) { |
237 | $out[] = new \OOUI\FieldsetLayout( [ |
238 | 'items' => [ $widget ], |
239 | // @phan-suppress-next-line SecurityCheck-XSS Key is html, taint cannot track that |
240 | 'label' => new \OOUI\HtmlSnippet( $sectionLabel ), |
241 | ] ); |
242 | } else { |
243 | $out[] = $widget; |
244 | } |
245 | } |
246 | |
247 | if ( !$hasSections && $out ) { |
248 | if ( $this->mPlaceholder ) { |
249 | $out[0]->setData( ( $out[0]->getData() ?: [] ) + [ |
250 | 'placeholder' => $this->mPlaceholder, |
251 | ] ); |
252 | } |
253 | // Directly return the only OOUI\CheckboxMultiselectInputWidget. |
254 | // This allows it to be made infusable and later tweaked by JS code. |
255 | return $out[0]; |
256 | } |
257 | |
258 | return implode( '', $out ); |
259 | } |
260 | |
261 | /** |
262 | * @stable to override |
263 | * @param WebRequest $request |
264 | * |
265 | * @return string|array |
266 | */ |
267 | public function loadDataFromRequest( $request ) { |
268 | $fromRequest = $request->getArray( $this->mName, [] ); |
269 | // Fetch the value in either one of the two following case: |
270 | // - we have a valid submit attempt (form was just submitted) |
271 | // - we have a value (an URL manually built by the user, or GET form with no wpFormIdentifier) |
272 | if ( $this->isSubmitAttempt( $request ) || $fromRequest ) { |
273 | // Checkboxes are just not added to the request arrays if they're not checked, |
274 | // so it's perfectly possible for there not to be an entry at all |
275 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable getArray does not return null |
276 | return $fromRequest; |
277 | } else { |
278 | // That's ok, the user has not yet submitted the form, so show the defaults |
279 | return $this->getDefault(); |
280 | } |
281 | } |
282 | |
283 | /** |
284 | * @inheritDoc |
285 | * @stable to override |
286 | */ |
287 | public function getDefault() { |
288 | return $this->mDefault ?? []; |
289 | } |
290 | |
291 | /** |
292 | * @inheritDoc |
293 | * @stable to override |
294 | */ |
295 | public function filterDataForSubmit( $data ) { |
296 | $data = HTMLFormField::forceToStringRecursive( $data ); |
297 | $options = HTMLFormField::flattenOptions( $this->getOptions() ); |
298 | $forcedOn = array_intersect( $this->mParams['disabled-options'], $this->getDefault() ); |
299 | |
300 | $res = []; |
301 | foreach ( $options as $opt ) { |
302 | $res["$opt"] = in_array( $opt, $forcedOn, true ) || in_array( $opt, $data, true ); |
303 | } |
304 | |
305 | return $res; |
306 | } |
307 | |
308 | /** |
309 | * @inheritDoc |
310 | * @stable to override |
311 | */ |
312 | protected function needsLabel() { |
313 | return false; |
314 | } |
315 | } |
316 | |
317 | /** @deprecated class alias since 1.42 */ |
318 | class_alias( HTMLMultiSelectField::class, 'HTMLMultiSelectField' ); |