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