HTMLCheckMatrix.php 8.8 KB

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