From 5a430cdc1c0224b8e95509b58ad4be7abda2674c Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Thu, 26 Jul 2018 16:14:41 -0700 Subject: [PATCH] OOUIfy CheckMatrix in PHP and JS 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 | 1 + includes/htmlform/fields/HTMLCheckMatrix.php | 55 +++-- includes/widget/CheckMatrixWidget.php | 204 ++++++++++++++++++ resources/Resources.php | 9 + .../mw.widgets.CheckMatrixWidget.js | 142 ++++++++++++ 5 files changed, 393 insertions(+), 18 deletions(-) create mode 100644 includes/widget/CheckMatrixWidget.php create mode 100644 resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js diff --git a/autoload.php b/autoload.php index 40b8acfee9..96a83c528f 100644 --- a/autoload.php +++ b/autoload.php @@ -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', diff --git a/includes/htmlform/fields/HTMLCheckMatrix.php b/includes/htmlform/fields/HTMLCheckMatrix.php index da68a626a6..a679e45951 100644 --- a/includes/htmlform/fields/HTMLCheckMatrix.php +++ b/includes/htmlform/fields/HTMLCheckMatrix.php @@ -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 index 0000000000..7783f31852 --- /dev/null +++ b/includes/widget/CheckMatrixWidget.php @@ -0,0 +1,204 @@ +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 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 ); + } +} diff --git a/resources/Resources.php b/resources/Resources.php index fc24035162..eea8c8af73 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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 index 0000000000..e13c6fa062 --- /dev/null +++ b/resources/src/mediawiki.widgets/mw.widgets.CheckMatrixWidget.js @@ -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 = $( '' ), + $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( $( '' ), + 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( $( '
' ).html( ' ' ) ); + + // Iterate over the columns object (ignore the value) + $.each( this.columns, function ( columnLabel ) { + $headRow.append( $( '' ).text( columnLabel ) ); + } ); + $table.append( $headRow ); + + // Build table + $.each( this.rows, function ( rowLabel, rowTag ) { + var $row = $( '
' ).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( $( '' ).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 ) ); -- 2.20.1