OOUIfy CheckMatrix in PHP and JS
authorMoriel Schottlender <moriel@gmail.com>
Thu, 26 Jul 2018 23:14:41 +0000 (16:14 -0700)
committerBartosz Dziewoński <matma.rex@gmail.com>
Wed, 22 Aug 2018 19:31:27 +0000 (21:31 +0200)
This is to make sure that the design is similar, but also so
that the widget can be read in JS where needed and that we
can toggle the disabled state on/off through the whole widget,
that is made from a series of checkbox widgets.

Bug: T199946
Change-Id: I9943b0aa1746fdfb60c7d4c88d6d4d7ac0589a2c

autoload.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/widget/CheckMatrixWidget.php [new file with mode: 0644]
resources/Resources.php
resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js [new file with mode: 0644]

index 40b8acf..96a83c5 100644 (file)
@@ -926,6 +926,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
        'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
        'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php',
+       'MediaWiki\\Widget\\CheckMatrixWidget' => __DIR__ . '/includes/widget/CheckMatrixWidget.php',
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
        'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php',
index da68a62..a679e45 100644 (file)
@@ -129,7 +129,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
                                        $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-on';
                                }
 
-                               $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs );
+                               $checkbox = $this->getOneCheckboxHTML( $checked, $attribs + $thisAttribs );
 
                                $rowContents .= Html::rawElement(
                                        'td',
@@ -148,24 +148,35 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
                return $html;
        }
 
-       protected function getOneCheckbox( $checked, $attribs ) {
-               if ( $this->mParent instanceof OOUIHTMLForm ) {
-                       return new OOUI\CheckboxInputWidget( [
-                               'name' => "{$this->mName}[]",
-                               'selected' => $checked,
-                       ] + OOUI\Element::configFromHtmlAttributes(
-                               $attribs
-                       ) );
-               } else {
-                       $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs );
-                       if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
-                               $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
-                                       $checkbox .
-                                       Html::element( 'label', [ 'for' => $attribs['id'] ] ) .
-                                       Html::closeElement( 'div' );
-                       }
-                       return $checkbox;
+       public function getInputOOUI( $value ) {
+               $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
+
+               return new MediaWiki\Widget\CheckMatrixWidget(
+                       [
+                               'name' => $this->mName,
+                               'infusable' => true,
+                               'id' => $this->mID,
+                               'rows' => $this->mParams['rows'],
+                               'columns' => $this->mParams['columns'],
+                               'tooltips' => $this->mParams['tooltips'],
+                               'forcedOff' => isset( $this->mParams['force-options-off'] ) ?
+                                       $this->mParams['force-options-off'] : [],
+                               'forcedOn' => isset( $this->mParams['force-options-on'] ) ?
+                                       $this->mParams['force-options-on'] : [],
+                               'values' => $value
+                       ] + OOUI\Element::configFromHtmlAttributes( $attribs )
+               );
+       }
+
+       protected function getOneCheckboxHTML( $checked, $attribs ) {
+               $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs );
+               if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
+                       $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
+                               $checkbox .
+                               Html::element( 'label', [ 'for' => $attribs['id'] ] ) .
+                               Html::closeElement( 'div' );
                }
+               return $checkbox;
        }
 
        protected function isTagForcedOff( $tag ) {
@@ -262,4 +273,12 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
 
                return $res;
        }
+
+       protected function getOOUIModules() {
+               return [ 'mediawiki.widgets.CheckMatrixWidget' ];
+       }
+
+       protected function shouldInfuseOOUI() {
+               return true;
+       }
 }
diff --git a/includes/widget/CheckMatrixWidget.php b/includes/widget/CheckMatrixWidget.php
new file mode 100644 (file)
index 0000000..7783f31
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+
+namespace MediaWiki\Widget;
+
+/**
+ * Check matrix widget. Displays a matrix of checkboxes for given options
+ *
+ * @copyright 2018 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class CheckMatrixWidget extends \OOUI\Widget {
+
+       protected $name = '';
+       protected $columns = [];
+       protected $rows = [];
+       protected $tooltips = [];
+       protected $values = [];
+       protected $forcedOn = [];
+       protected $forcedOff = [];
+
+       /**
+        * CheckMatrixWidget constructor
+        *
+        * Operates similarly to MultiSelectWidget, but instead of using an array of
+        * options, uses an array of rows and an array of columns to dynamically
+        * construct a matrix of options. The tags used to identify a particular cell
+        * are of the form "columnName-rowName"
+        *
+        * @param array $config Configuration array with the following options:
+        *   - columns
+        *     - Required list of columns in the matrix.
+        *   - rows
+        *     - Required list of rows in the matrix.
+        *   - force-options-on
+        *     - Accepts array of column-row tags to be displayed as enabled but unavailable to change
+        *   - force-options-off
+        *     - Accepts array of column-row tags to be displayed as disabled but unavailable to change.
+        *   - tooltips
+        *     - Optional array mapping row label to tooltip content
+        *   - tooltip-class
+        *     - Optional CSS class used on tooltip container span. Defaults to mw-icon-question.
+        */
+       public function __construct( array $config = [] ) {
+               // Configuration initialization
+
+               parent::__construct( $config );
+
+               $this->name = isset( $config['name'] ) ?
+                       $config[ 'name' ] : null;
+               $this->id = isset( $config['id'] ) ?
+                       $config['id'] : null;
+
+               // Properties
+               $this->rows = isset( $config['rows'] ) ?
+                       $config['rows'] : [];
+               $this->columns = isset( $config['columns'] ) ?
+                       $config['columns'] : [];
+               $this->tooltips = isset( $config['tooltips'] ) ?
+                       $config['tooltips'] : [];
+
+               $this->values = isset( $config['values'] ) ?
+                       $config['values'] : [];
+
+               $this->forcedOn = isset( $config['forcedOn'] ) ?
+                       $config['forcedOn'] : [];
+               $this->forcedOff = isset( $config['forcedOff'] ) ?
+                       $config['forcedOff'] : [];
+
+               // Build the table
+               $table = new \OOUI\Tag( 'table' );
+               $tr = new \OOUI\Tag( 'tr' );
+               // Build the header
+               $tr->appendContent( $this->getCellTag( "\u{00A0}" ) );
+               foreach ( $this->columns as $columnLabel => $columnTag ) {
+                       $tr->appendContent(
+                               $this->getCellTag( $columnLabel )
+                       );
+               }
+               $table->appendContent( $tr );
+
+               // Build the options matrix
+               foreach ( $this->rows as $rowLabel => $rowTag ) {
+                       $table->appendContent(
+                               $this->getTableRow( $rowLabel, $rowTag )
+                       );
+               }
+
+               // Initialization
+               $this->addClasses( [ 'mw-widget-checkMatrixWidget' ] );
+               $this->appendContent( $table );
+       }
+
+       /**
+        * Get a formatted table row for the option, with
+        * a checkbox widget.
+        *
+        * @param  string $label Row label
+        * @param  string $tag   Row tag name
+        * @return \OOUI\Tag The resulting table row
+        */
+       private function getTableRow( $label, $tag ) {
+               $row = new \OOUI\Tag( 'tr' );
+               $tooltip = $this->getTooltip( $label );
+               $labelFieldConfig = $tooltip ? [ 'help' => $tooltip ] : [];
+               // Build label cell
+               $labelField = new \OOUI\FieldLayout(
+                       new \OOUI\Widget(), // Empty widget, since we don't have the checkboxes here
+                       [
+                               'label' => $label,
+                               'align' => 'inline',
+                       ] + $labelFieldConfig
+               );
+               $row->appendContent( $this->getCellTag( $labelField ) );
+
+               // Build checkbox column cells
+               foreach ( $this->columns as $columnTag ) {
+                       $thisTag = "$columnTag-$tag";
+
+                       // Construct a checkbox
+                       $checkbox = new \OOUI\CheckboxInputWidget( [
+                               'value' => $thisTag,
+                               'name' => $this->name ? "{$this->name}[]" : null,
+                               'id' => $this->id ? "{$this->id}-$thisTag" : null,
+                               'selected' => $this->isTagChecked( $thisTag ),
+                               'disabled' => $this->isTagDisabled( $thisTag ),
+                       ] );
+
+                       $row->appendContent( $this->getCellTag( $checkbox ) );
+               }
+               return $row;
+       }
+
+       /**
+        * Get an individual cell tag with requested content
+        *
+        * @param  string $content Content for the <td> cell
+        * @return \OOUI\Tag Resulting cell
+        */
+       private function getCellTag( $content ) {
+               $cell = new \OOUI\Tag( 'td' );
+               $cell->appendContent( $content );
+               return $cell;
+       }
+
+       /**
+        * Check whether the given tag's checkbox should
+        * be checked
+        *
+        * @param  string $tagName Tag name
+        * @return boolean Tag should be checked
+        */
+       private function isTagChecked( $tagName ) {
+               // If the tag is in the value list
+               return in_array( $tagName, (array)$this->values, true ) ||
+                       // Or if the tag is forced on
+                       in_array( $tagName, (array)$this->forcedOn, true );
+       }
+
+       /**
+        * Check whether the given tag's checkbox should
+        * be disabled
+        *
+        * @param  string $tagName Tag name
+        * @return boolean Tag should be disabled
+        */
+       private function isTagDisabled( $tagName ) {
+               return (
+                       // If the entire widget is disabled
+                       $this->isDisabled() ||
+                       // If the tag is 'forced on' or 'forced off'
+                       in_array( $tagName, (array)$this->forcedOn, true ) ||
+                       in_array( $tagName, (array)$this->forcedOff, true )
+               );
+       }
+
+       /**
+        * Get the tooltip help associated with this row
+        *
+        * @param  string $label Label name
+        * @return string Tooltip. Null if none is available.
+        */
+       private function getTooltip( $label ) {
+               return isset( $this->tooltips[ $label ] ) ?
+                       $this->tooltips[ $label ] : null;
+       }
+
+       protected function getJavaScriptClassName() {
+               return 'mw.widgets.CheckMatrixWidget';
+       }
+
+       public function getConfig( &$config ) {
+               $config += [
+                       'name' => $this->name,
+                       'id' => $this->id,
+                       'rows' => $this->rows,
+                       'columns' => $this->columns,
+                       'tooltips' => $this->tooltips,
+                       'forcedOff' => $this->forcedOff,
+                       'forcedOn' => $this->forcedOn,
+                       'values' => $this->values,
+               ];
+               return parent::getConfig( $config );
+       }
+}
index fc24035..eea8c8a 100644 (file)
@@ -2629,6 +2629,15 @@ return [
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'mediawiki.widgets.CheckMatrixWidget' => [
+               'scripts' => [
+                       'resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js',
+               ],
+               'dependencies' => [
+                       'oojs-ui-core',
+               ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        'mediawiki.widgets.CategoryMultiselectWidget' => [
                'scripts' => [
                        'resources/src/mediawiki.widgets/mw.widgets.CategoryTagItemWidget.js',
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js b/resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js
new file mode 100644 (file)
index 0000000..e13c6fa
--- /dev/null
@@ -0,0 +1,142 @@
+( function ( $, mw ) {
+       /**
+        * A JavaScript version of CheckMatrixWidget.
+        *
+        * @class
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        * @cfg {Object} columns Required object representing the column labels and associated
+        *  tags in the matrix.
+        * @cfg {Object} rows Required object representing the row labels and associated
+        *  tags in the matrix.
+        * @cfg {string[]} [forcedOn] An array of column-row tags to be displayed as
+        *  enabled but unavailable to change
+        * @cfg {string[]} [forcedOff] An array of column-row tags to be displayed as
+        *  disnabled but unavailable to change
+        * @cfg {Object} Object mapping row label to tooltip content
+        */
+       mw.widgets.CheckMatrixWidget = function MWWCheckMatrixWidget( config ) {
+               var $headRow = $( '<tr>' ),
+                       $table = $( '<table>' ),
+                       widget = this;
+               config = config || {};
+
+               // Parent constructor
+               mw.widgets.CheckMatrixWidget.parent.call( this, config );
+               this.checkboxes = {};
+               this.name = config.name;
+               this.id = config.id;
+               this.rows = config.rows || {};
+               this.columns = config.columns || {};
+               this.tooltips = config.tooltips || [];
+               this.values = config.values || [];
+               this.forcedOn = config.forcedOn || [];
+               this.forcedOff = config.forcedOff || [];
+
+               // Build header
+               $headRow.append( $( '<td>' ).html( '&#160;' ) );
+
+               // Iterate over the columns object (ignore the value)
+               $.each( this.columns, function ( columnLabel ) {
+                       $headRow.append( $( '<td>' ).text( columnLabel ) );
+               } );
+               $table.append( $headRow );
+
+               // Build table
+               $.each( this.rows, function ( rowLabel, rowTag ) {
+                       var $row = $( '<tr>' ),
+                               labelField = new OO.ui.FieldLayout(
+                                       new OO.ui.Widget(), // Empty widget, since we don't have the checkboxes here
+                                       {
+                                               label: rowLabel,
+                                               help: widget.tooltips[ rowLabel ],
+                                               align: 'inline'
+                                       }
+                               );
+
+                       // Label
+                       $row.append( $( '<td>' ).append( labelField.$element ) );
+
+                       // Columns
+                       $.each( widget.columns, function ( columnLabel, columnTag ) {
+                               var thisTag = columnTag + '-' + rowTag,
+                                       checkbox = new OO.ui.CheckboxInputWidget( {
+                                               value: thisTag,
+                                               name: widget.name ? widget.name + '[]' : undefined,
+                                               id: widget.id ? widget.id + '-' + thisTag : undefined,
+                                               selected: widget.isTagSelected( thisTag ),
+                                               disabled: widget.isTagDisabled( thisTag )
+                                       } );
+
+                               widget.checkboxes[ thisTag ] = checkbox;
+                               $row.append( $( '<td>' ).append( checkbox.$element ) );
+                       } );
+
+                       $table.append( $row );
+               } );
+
+               this.$element
+                       .addClass( 'mw-widget-checkMatrixWidget' )
+                       .append( $table );
+       };
+
+       /* Setup */
+
+       OO.inheritClass( mw.widgets.CheckMatrixWidget, OO.ui.Widget );
+
+       /* Methods */
+
+       /**
+        * Check whether the given tag is selected
+        *
+        * @param {string} tagName Tag name
+        * @return {boolean} Tag is selected
+        */
+       mw.widgets.CheckMatrixWidget.prototype.isTagSelected = function ( tagName ) {
+               return (
+                       // If tag is not forced off
+                       this.forcedOff.indexOf( tagName ) === -1 &&
+                       (
+                               // If tag is in values
+                               this.values.indexOf( tagName ) > -1 ||
+                               // If tag is forced on
+                               this.forcedOn.indexOf( tagName ) > -1
+                       )
+               );
+       };
+
+       /**
+        * Check whether the given tag is disabled
+        *
+        * @param {string} tagName Tag name
+        * @return {boolean} Tag is disabled
+        */
+       mw.widgets.CheckMatrixWidget.prototype.isTagDisabled = function ( tagName ) {
+               return (
+                       // If the entire widget is disabled
+                       this.isDisabled() ||
+                       // If tag is forced off or forced on
+                       this.forcedOff.indexOf( tagName ) > -1 ||
+                       this.forcedOn.indexOf( tagName ) > -1
+               );
+       };
+       /**
+        * @inheritdoc
+        */
+       mw.widgets.CheckMatrixWidget.prototype.setDisabled = function ( isDisabled ) {
+               var widget = this;
+
+               // Parent method
+               mw.widgets.CheckMatrixWidget.parent.prototype.setDisabled.call( this, isDisabled );
+
+               // setDisabled sometimes gets called before the widget is ready
+               if ( this.checkboxes && Object.keys( this.checkboxes ).length > 0 ) {
+                       // Propagate to all checkboxes and update their disabled state
+                       $.each( this.checkboxes, function ( name, checkbox ) {
+                               checkbox.setDisabled( widget.isTagDisabled( name ) );
+                       } );
+               }
+       };
+}( jQuery, mediaWiki ) );