Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
26.92% covered (danger)
26.92%
35 / 130
38.46% covered (danger)
38.46%
5 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLCheckMatrix
27.13% covered (danger)
27.13%
35 / 129
38.46% covered (danger)
38.46%
5 / 13
481.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validate
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getInputHTML
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
72
 getInputOOUI
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getOneCheckboxHTML
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isTagForcedOff
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isTagForcedOn
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getTableRow
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 loadDataFromRequest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDefault
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 filterDataForSubmit
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getOOUIModules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shouldInfuseOOUI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\HTMLForm\Field;
4
5use FormatJson;
6use MediaWiki\Html\Html;
7use MediaWiki\HTMLForm\HTMLFormField;
8use MediaWiki\HTMLForm\HTMLFormFieldRequiredOptionsException;
9use MediaWiki\HTMLForm\HTMLNestedFilterable;
10use MediaWiki\Request\WebRequest;
11use Xml;
12
13/**
14 * A checkbox matrix
15 * Operates similarly to HTMLMultiSelectField, but instead of using an array of
16 * options, uses an array of rows and an array of columns to dynamically
17 * construct a matrix of options. The tags used to identify a particular cell
18 * are of the form "columnName-rowName"
19 *
20 * Options:
21 *   - columns
22 *     - Required associative array mapping column labels (as HTML) to their tags.
23 *   - rows
24 *     - Required associative array mapping row labels (as HTML) to their tags.
25 *   - force-options-on
26 *     - Array of column-row tags to be displayed as enabled but unavailable to change.
27 *   - force-options-off
28 *     - Array of column-row tags to be displayed as disabled but unavailable to change.
29 *   - tooltips
30 *     - Optional associative array mapping row labels to tooltips (as text, will be escaped).
31 *   - tooltips-html
32 *     - Optional associative array mapping row labels to tooltips (as HTML).
33 *       Only used by OOUI form fields. Takes precedence when supported, so to support both
34 *       OOUI and non-OOUI forms, you can set both.
35 *   - tooltip-class
36 *     - Optional CSS class used on tooltip container span. Defaults to mw-icon-question.
37 *       Not used by OOUI form fields.
38 *
39 * @stable to extend
40 */
41class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
42    private static $requiredParams = [
43        // Required by underlying HTMLFormField
44        'fieldname',
45        // Required by HTMLCheckMatrix
46        'rows',
47        'columns'
48    ];
49
50    /**
51     * @stable to call
52     * @inheritDoc
53     */
54    public function __construct( $params ) {
55        $missing = array_diff( self::$requiredParams, array_keys( $params ) );
56        if ( $missing ) {
57            throw new HTMLFormFieldRequiredOptionsException( $this, $missing );
58        }
59        parent::__construct( $params );
60    }
61
62    public function validate( $value, $alldata ) {
63        $rows = $this->mParams['rows'];
64        $columns = $this->mParams['columns'];
65
66        // Make sure user-defined validation callback is run
67        $p = parent::validate( $value, $alldata );
68        if ( $p !== true ) {
69            return $p;
70        }
71
72        // Make sure submitted value is an array
73        if ( !is_array( $value ) ) {
74            return false;
75        }
76
77        // If all options are valid, array_intersect of the valid options
78        // and the provided options will return the provided options.
79        $validOptions = [];
80        foreach ( $rows as $rowTag ) {
81            foreach ( $columns as $columnTag ) {
82                $validOptions[] = $columnTag . '-' . $rowTag;
83            }
84        }
85        $validValues = array_intersect( $value, $validOptions );
86        if ( count( $validValues ) == count( $value ) ) {
87            return true;
88        } else {
89            return $this->msg( 'htmlform-select-badoption' );
90        }
91    }
92
93    /**
94     * Build a table containing a matrix of checkbox options.
95     * The value of each option is a combination of the row tag and column tag.
96     * mParams['rows'] is an array with row labels as keys and row tags as values.
97     * mParams['columns'] is an array with column labels as keys and column tags as values.
98     *
99     * @param array $value Array of the options that should be checked
100     *
101     * @return string
102     */
103    public function getInputHTML( $value ) {
104        $html = '';
105        $tableContents = '';
106        $rows = $this->mParams['rows'];
107        $columns = $this->mParams['columns'];
108
109        $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
110
111        // Build the column headers
112        $headerContents = Html::rawElement( 'td', [], "\u{00A0}" );
113        foreach ( $columns as $columnLabel => $columnTag ) {
114            $headerContents .= Html::rawElement( 'th', [], $columnLabel );
115        }
116        $thead = Html::rawElement( 'tr', [], "\n$headerContents\n" );
117        $tableContents .= Html::rawElement( 'thead', [], "\n$thead\n" );
118
119        $tooltipClass = 'mw-icon-question';
120        if ( isset( $this->mParams['tooltip-class'] ) ) {
121            $tooltipClass = $this->mParams['tooltip-class'];
122        }
123
124        // Build the options matrix
125        foreach ( $rows as $rowLabel => $rowTag ) {
126            // Append tooltip if configured
127            if ( isset( $this->mParams['tooltips'][$rowLabel] ) ) {
128                $tooltipAttribs = [
129                    'class' => "mw-htmlform-tooltip $tooltipClass",
130                    'title' => $this->mParams['tooltips'][$rowLabel],
131                    'aria-label' => $this->mParams['tooltips'][$rowLabel]
132                ];
133                $rowLabel .= ' ' . Html::element( 'span', $tooltipAttribs, '' );
134            }
135            $rowContents = Html::rawElement( 'td', [], $rowLabel );
136            foreach ( $columns as $columnTag ) {
137                $thisTag = "$columnTag-$rowTag";
138                // Construct the checkbox
139                $thisAttribs = [
140                    'id' => "{$this->mID}-$thisTag",
141                    'value' => $thisTag,
142                ];
143                $checked = in_array( $thisTag, (array)$value, true );
144                if ( $this->isTagForcedOff( $thisTag ) ) {
145                    $checked = false;
146                    $thisAttribs['disabled'] = 1;
147                    $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-off';
148                } elseif ( $this->isTagForcedOn( $thisTag ) ) {
149                    $checked = true;
150                    $thisAttribs['disabled'] = 1;
151                    $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-on';
152                }
153
154                $checkbox = $this->getOneCheckboxHTML( $checked, $attribs + $thisAttribs );
155
156                $rowContents .= Html::rawElement(
157                    'td',
158                    [],
159                    $checkbox
160                );
161            }
162            $tableContents .= Html::rawElement( 'tr', [], "\n$rowContents\n" );
163        }
164
165        // Put it all in a table
166        $html .= Html::rawElement( 'table',
167                [ 'class' => 'mw-htmlform-matrix' ],
168                Html::rawElement( 'tbody', [], "\n$tableContents\n" ) ) . "\n";
169
170        return $html;
171    }
172
173    public function getInputOOUI( $value ) {
174        $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
175
176        return new \MediaWiki\Widget\CheckMatrixWidget(
177            [
178                'name' => $this->mName,
179                'infusable' => true,
180                'id' => $this->mID,
181                'rows' => $this->mParams['rows'],
182                'columns' => $this->mParams['columns'],
183                'tooltips' => $this->mParams['tooltips'] ?? [],
184                'tooltips-html' => $this->mParams['tooltips-html'] ?? [],
185                'forcedOff' => $this->mParams['force-options-off'] ?? [],
186                'forcedOn' => $this->mParams['force-options-on'] ?? [],
187                'values' => $value,
188            ] + \OOUI\Element::configFromHtmlAttributes( $attribs )
189        );
190    }
191
192    protected function getOneCheckboxHTML( $checked, $attribs ) {
193        return Xml::check( "{$this->mName}[]", $checked, $attribs );
194    }
195
196    protected function isTagForcedOff( $tag ) {
197        return isset( $this->mParams['force-options-off'] )
198            && in_array( $tag, $this->mParams['force-options-off'] );
199    }
200
201    protected function isTagForcedOn( $tag ) {
202        return isset( $this->mParams['force-options-on'] )
203            && in_array( $tag, $this->mParams['force-options-on'] );
204    }
205
206    /**
207     * Get the complete table row for the input, including help text,
208     * labels, and whatever.
209     * We override this function since the label should always be on a separate
210     * line above the options in the case of a checkbox matrix, i.e. it's always
211     * a "vertical-label".
212     *
213     * @param string|array $value The value to set the input to
214     *
215     * @return string Complete HTML table row
216     */
217    public function getTableRow( $value ) {
218        [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
219        $inputHtml = $this->getInputHTML( $value );
220        $fieldType = $this->getClassName();
221        $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
222        $cellAttributes = [ 'colspan' => 2 ];
223
224        $moreClass = '';
225        $moreAttributes = [];
226        if ( $this->mCondState ) {
227            $moreAttributes['data-cond-state'] = FormatJson::encode( $this->mCondState );
228            $moreClass = implode( ' ', $this->mCondStateClass );
229        }
230
231        $label = $this->getLabelHtml( $cellAttributes );
232
233        $field = Html::rawElement(
234            'td',
235            [ 'class' => 'mw-input' ] + $cellAttributes,
236            $inputHtml . "\n$errors"
237        );
238
239        $html = Html::rawElement( 'tr',
240            [ 'class' => "mw-htmlform-vertical-label $moreClass" ] + $moreAttributes,
241            $label );
242        $html .= Html::rawElement( 'tr',
243            [ 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $moreClass" ] +
244                $moreAttributes,
245            $field );
246
247        return $html . $helptext;
248    }
249
250    /**
251     * @param WebRequest $request
252     *
253     * @return array
254     */
255    public function loadDataFromRequest( $request ) {
256        if ( $this->isSubmitAttempt( $request ) ) {
257            // Checkboxes are just not added to the request arrays if they're not checked,
258            // so it's perfectly possible for there not to be an entry at all
259            return $request->getArray( $this->mName, [] );
260        } else {
261            // That's ok, the user has not yet submitted the form, so show the defaults
262            return $this->getDefault();
263        }
264    }
265
266    public function getDefault() {
267        return $this->mDefault ?? [];
268    }
269
270    public function filterDataForSubmit( $data ) {
271        $columns = HTMLFormField::flattenOptions( $this->mParams['columns'] );
272        $rows = HTMLFormField::flattenOptions( $this->mParams['rows'] );
273        $res = [];
274        foreach ( $columns as $column ) {
275            foreach ( $rows as $row ) {
276                // Make sure option hasn't been forced
277                $thisTag = "$column-$row";
278                if ( $this->isTagForcedOff( $thisTag ) ) {
279                    $res[$thisTag] = false;
280                } elseif ( $this->isTagForcedOn( $thisTag ) ) {
281                    $res[$thisTag] = true;
282                } else {
283                    $res[$thisTag] = in_array( $thisTag, $data );
284                }
285            }
286        }
287
288        return $res;
289    }
290
291    protected function getOOUIModules() {
292        return [ 'mediawiki.widgets.CheckMatrixWidget' ];
293    }
294
295    protected function shouldInfuseOOUI() {
296        return true;
297    }
298}
299
300/** @deprecated class alias since 1.42 */
301class_alias( HTMLCheckMatrix::class, 'HTMLCheckMatrix' );