1<?php
2
3namespace MediaWiki\Widget;
4
5/**
6 * Check matrix widget. Displays a matrix of checkboxes for given options
7 *
8 * @copyright 2018 MediaWiki Widgets Team and others; see AUTHORS.txt
9 * @license MIT
10 */
11class CheckMatrixWidget extends \OOUI\Widget {
12	/** @var string|null */
13	protected $name;
14	/** @var string|null */
15	protected $id;
16	/** @var array */
17	protected $columns;
18	/** @var array */
19	protected $rows;
20	/** @var array */
21	protected $tooltips;
22	/** @var array */
23	protected $values;
24	/** @var array */
25	protected $forcedOn;
26	/** @var array */
27	protected $forcedOff;
28
29	/**
30	 * Operates similarly to MultiSelectWidget, but instead of using an array of
31	 * options, uses an array of rows and an array of columns to dynamically
32	 * construct a matrix of options. The tags used to identify a particular cell
33	 * are of the form "columnName-rowName"
34	 *
35	 * @param array $config Configuration array with the following options:
36	 *   - columns
37	 *     - Required associative array mapping column labels (as HTML) to their tags.
38	 *   - rows
39	 *     - Required associative array mapping row labels (as HTML) to their tags.
40	 *   - force-options-on
41	 *     - Array of column-row tags to be displayed as enabled but unavailable to change.
42	 *   - force-options-off
43	 *     - Array of column-row tags to be displayed as disabled but unavailable to change.
44	 *   - tooltips
45	 *     - Optional associative array mapping row labels to tooltips (as text, will be escaped).
46	 */
47	public function __construct( array $config = [] ) {
48		// Configuration initialization
49
50		parent::__construct( $config );
51
52		$this->name = $config['name'] ?? null;
53		$this->id = $config['id'] ?? null;
54
55		// Properties
56		$this->rows = $config['rows'] ?? [];
57		$this->columns = $config['columns'] ?? [];
58		$this->tooltips = $config['tooltips'] ?? [];
59
60		$this->values = $config['values'] ?? [];
61
62		$this->forcedOn = $config['forcedOn'] ?? [];
63		$this->forcedOff = $config['forcedOff'] ?? [];
64
65		// Build the table
66		$table = new \OOUI\Tag( 'table' );
67		$table->addClasses( [ 'mw-htmlform-matrix mw-widget-checkMatrixWidget-matrix' ] );
68		$thead = new \OOUI\Tag( 'thead' );
69		$table->appendContent( $thead );
70		$tr = new \OOUI\Tag( 'tr' );
71
72		// Build the header
73		$tr->appendContent( $this->getCellTag( "\u{00A0}" ) );
74		foreach ( $this->columns as $columnLabel => $columnTag ) {
75			$tr->appendContent(
76				$this->getCellTag( new \OOUI\HtmlSnippet( $columnLabel ), 'th' )
77			);
78		}
79		$thead->appendContent( $tr );
80
81		// Build the options matrix
82		$tbody = new \OOUI\Tag( 'tbody' );
83		$table->appendContent( $tbody );
84		foreach ( $this->rows as $rowLabel => $rowTag ) {
85			$tbody->appendContent(
86				$this->getTableRow( $rowLabel, $rowTag )
87			);
88		}
89
90		// Initialization
91		$this->addClasses( [ 'mw-widget-checkMatrixWidget' ] );
92		$this->appendContent( $table );
93	}
94
95	/**
96	 * Get a formatted table row for the option, with
97	 * a checkbox widget.
98	 *
99	 * @param string $label Row label (as HTML)
100	 * @param string $tag Row tag name
101	 * @return \OOUI\Tag The resulting table row
102	 */
103	private function getTableRow( $label, $tag ) {
104		$row = new \OOUI\Tag( 'tr' );
105		$tooltip = $this->getTooltip( $label );
106		$labelFieldConfig = $tooltip ? [ 'help' => $tooltip ] : [];
107		// Build label cell
108		$labelField = new \OOUI\FieldLayout(
109			new \OOUI\Widget(), // Empty widget, since we don't have the checkboxes here
110			[
111				'label' => new \OOUI\HtmlSnippet( $label ),
112				'align' => 'inline',
113			] + $labelFieldConfig
114		);
115		$row->appendContent( $this->getCellTag( $labelField ) );
116
117		// Build checkbox column cells
118		foreach ( $this->columns as $columnTag ) {
119			$thisTag = "$columnTag-$tag";
120
121			// Construct a checkbox
122			$checkbox = new \OOUI\CheckboxInputWidget( [
123				'value' => $thisTag,
124				'name' => $this->name ? "{$this->name}[]" : null,
125				'id' => $this->id ? "{$this->id}-$thisTag" : null,
126				'selected' => $this->isTagChecked( $thisTag ),
127				'disabled' => $this->isTagDisabled( $thisTag ),
128			] );
129
130			$row->appendContent( $this->getCellTag( $checkbox ) );
131		}
132		return $row;
133	}
134
135	/**
136	 * Get an individual cell tag with requested content
137	 *
138	 * @param mixed $content Content for the <td> cell
139	 * @param string $tagElement
140	 * @return \OOUI\Tag Resulting cell
141	 */
142	private function getCellTag( $content, $tagElement = 'td' ) {
143		$cell = new \OOUI\Tag( $tagElement );
144		$cell->appendContent( $content );
145		return $cell;
146	}
147
148	/**
149	 * Check whether the given tag's checkbox should
150	 * be checked
151	 *
152	 * @param string $tagName Tag name
153	 * @return bool Tag should be checked
154	 */
155	private function isTagChecked( $tagName ) {
156		// If the tag is in the value list
157		return in_array( $tagName, (array)$this->values, true ) ||
158			// Or if the tag is forced on
159			in_array( $tagName, (array)$this->forcedOn, true );
160	}
161
162	/**
163	 * Check whether the given tag's checkbox should
164	 * be disabled
165	 *
166	 * @param string $tagName Tag name
167	 * @return bool Tag should be disabled
168	 */
169	private function isTagDisabled( $tagName ) {
170		return (
171			// If the entire widget is disabled
172			$this->isDisabled() ||
173			// If the tag is 'forced on' or 'forced off'
174			in_array( $tagName, (array)$this->forcedOn, true ) ||
175			in_array( $tagName, (array)$this->forcedOff, true )
176		);
177	}
178
179	/**
180	 * Get the tooltip help associated with this row
181	 *
182	 * @param string $label Label name
183	 * @return string Tooltip. Null if none is available.
184	 */
185	private function getTooltip( $label ) {
186		return $this->tooltips[ $label ] ?? null;
187	}
188
189	protected function getJavaScriptClassName() {
190		return 'mw.widgets.CheckMatrixWidget';
191	}
192
193	public function getConfig( &$config ) {
194		$config += [
195			'name' => $this->name,
196			'id' => $this->id,
197			'rows' => $this->rows,
198			'columns' => $this->columns,
199			'tooltips' => $this->tooltips,
200			'forcedOff' => $this->forcedOff,
201			'forcedOn' => $this->forcedOn,
202			'values' => $this->values,
203		];
204		return parent::getConfig( $config );
205	}
206}
207