Support 'hide-if' parameters in OOUI HTMLForm
authorBartosz Dziewoński <matma.rex@gmail.com>
Sat, 30 Jul 2016 23:19:26 +0000 (01:19 +0200)
committerJforrester <jforrester@wikimedia.org>
Mon, 22 Aug 2016 15:42:22 +0000 (15:42 +0000)
For plain HTML forms, we just put the required data in the 'data-hide-if'
attribute. For OOUI, it's not so easy - while we could just call
->setAttribute(...) on the FieldLayout, this would disappear when
infusing (since it's not part of the config), and we have no control over
when some piece of JavaScript decides to infuse the element. Even if we
managed to handle it first, infusing replaces the DOM nodes for elements
with new ones, which would "disable" our event handlers.

To solve this, I'm creating two new layouts HTMLFormFieldLayout and
HTMLFormActionFieldLayout (subclassing FieldLayout and ActionFieldLayout)
with a common trait (mixin) HTMLFormElement. This is all implemented both
in PHP and JS. Right now it only serves to carry the 'hide-if' data from
PHP to JS code, but I imagine it'll be extended in the future for other
HTMLForm features not yet present in the OOUI version (e.g. 'cloner'
fields).

The code in hide-if.js has been modified to work with jQuery objects or
with OOjs UI Widgets with minimal changes. I had to duplicate the map of
HTMLFormField classes to modules they require there (from autoinfuse.js),
which is ugly - I'm fixing this in a follow-up commit
I3da75706209cbc16b19cc3f02b355e58ca75fec9.

Bug: T141558
Change-Id: I3b06a6f75eed01d3e0bdc5dd33e1b40b7a2fc0a2

autoload.php
includes/htmlform/HTMLFormElement.php [new file with mode: 0644]
includes/htmlform/HTMLFormField.php
resources/Resources.php
resources/src/mediawiki/htmlform/autoinfuse.js
resources/src/mediawiki/htmlform/hide-if.js
resources/src/mediawiki/htmlform/htmlform.Element.js [new file with mode: 0644]

index e37a011..e7671be 100644 (file)
@@ -527,8 +527,11 @@ $wgAutoloadLocalClasses = [
        'HTMLFileCache' => __DIR__ . '/includes/cache/HTMLFileCache.php',
        'HTMLFloatField' => __DIR__ . '/includes/htmlform/fields/HTMLFloatField.php',
        'HTMLForm' => __DIR__ . '/includes/htmlform/HTMLForm.php',
+       'HTMLFormActionFieldLayout' => __DIR__ . '/includes/htmlform/HTMLFormElement.php',
+       'HTMLFormElement' => __DIR__ . '/includes/htmlform/HTMLFormElement.php',
        'HTMLFormField' => __DIR__ . '/includes/htmlform/HTMLFormField.php',
        'HTMLFormFieldCloner' => __DIR__ . '/includes/htmlform/fields/HTMLFormFieldCloner.php',
+       'HTMLFormFieldLayout' => __DIR__ . '/includes/htmlform/HTMLFormElement.php',
        'HTMLFormFieldRequiredOptionsException' => __DIR__ . '/includes/htmlform/HTMLFormFieldRequiredOptionsException.php',
        'HTMLFormFieldWithButton' => __DIR__ . '/includes/htmlform/fields/HTMLFormFieldWithButton.php',
        'HTMLHiddenField' => __DIR__ . '/includes/htmlform/fields/HTMLHiddenField.php',
diff --git a/includes/htmlform/HTMLFormElement.php b/includes/htmlform/HTMLFormElement.php
new file mode 100644 (file)
index 0000000..e553218
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * Allows custom data specific to HTMLFormField to be set for OOjs UI forms. A matching JS widget
+ * (defined in htmlform.Element.js) picks up the extra config when constructed using OO.ui.infuse().
+ *
+ * Currently only supports passing 'hide-if' data.
+ */
+trait HTMLFormElement {
+
+       protected $hideIf = null;
+
+       public function initializeHTMLFormElement( array $config = [] ) {
+               // Properties
+               $this->hideIf = isset( $config['hideIf'] ) ? $config['hideIf'] : null;
+
+               // Initialization
+               if ( $this->hideIf ) {
+                       $this->addClasses( [ 'mw-htmlform-hide-if' ] );
+               }
+               $this->registerConfigCallback( function( &$config ) {
+                       if ( $this->hideIf !== null ) {
+                               $config['hideIf'] = $this->hideIf;
+                       }
+               } );
+       }
+}
+
+class HTMLFormFieldLayout extends OOUI\FieldLayout {
+       use HTMLFormElement;
+
+       public function __construct( $fieldWidget, array $config = [] ) {
+               // Parent constructor
+               parent::__construct( $fieldWidget, $config );
+               // Traits
+               $this->initializeHTMLFormElement( $config );
+       }
+
+       protected function getJavaScriptClassName() {
+               return 'mw.htmlform.FieldLayout';
+       }
+}
+
+class HTMLFormActionFieldLayout extends OOUI\ActionFieldLayout {
+       use HTMLFormElement;
+
+       public function __construct( $fieldWidget, $buttonWidget = false, array $config = [] ) {
+               // Parent constructor
+               parent::__construct( $fieldWidget, $buttonWidget, $config );
+               // Traits
+               $this->initializeHTMLFormElement( $config );
+       }
+
+       protected function getJavaScriptClassName() {
+               return 'mw.htmlform.ActionFieldLayout';
+       }
+}
index b47bfa0..3319d3b 100644 (file)
@@ -627,7 +627,7 @@ abstract class HTMLFormField {
                ];
 
                if ( $infusable && $this->shouldInfuseOOUI() ) {
-                       $this->mParent->getOutput()->addModules( 'oojs-ui-core' );
+                       $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
                        $config['classes'][] = 'mw-htmlform-field-autoinfuse';
                }
 
@@ -637,6 +637,11 @@ abstract class HTMLFormField {
                        $config['label'] = new OOUI\HtmlSnippet( $label );
                }
 
+               if ( $this->mHideIf ) {
+                       $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+                       $config['hideIf'] = $this->mHideIf;
+               }
+
                return $this->getFieldLayoutOOUI( $inputField, $config );
        }
 
@@ -655,9 +660,9 @@ abstract class HTMLFormField {
        protected function getFieldLayoutOOUI( $inputField, $config ) {
                if ( isset( $this->mClassWithButton ) ) {
                        $buttonWidget = $this->mClassWithButton->getInputOOUI( '' );
-                       return new OOUI\ActionFieldLayout( $inputField, $buttonWidget, $config );
+                       return new HTMLFormActionFieldLayout( $inputField, $buttonWidget, $config );
                }
-               return new OOUI\FieldLayout( $inputField, $config );
+               return new HTMLFormFieldLayout( $inputField, $config );
        }
 
        /**
index 1578b42..cfaaf5f 100644 (file)
@@ -1089,6 +1089,15 @@ return [
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'mediawiki.htmlform.ooui' => [
+               'scripts' => [
+                       'resources/src/mediawiki/htmlform/htmlform.Element.js',
+               ],
+               'dependencies' => [
+                       'oojs-ui-core',
+               ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        'mediawiki.htmlform.styles' => [
                'styles' => 'resources/src/mediawiki/htmlform/styles.css',
                'position' => 'top',
index f77e367..8efbc69 100644 (file)
@@ -11,7 +11,7 @@
                if ( $oouiNodes.length ) {
                        // The modules are preloaded (added server-side in HTMLFormField, and the individual fields
                        // which need extra ones), but this module doesn't depend on them. Wait until they're loaded.
-                       modules = [ 'oojs-ui-core' ];
+                       modules = [ 'mediawiki.htmlform.ooui' ];
                        if ( $oouiNodes.filter( '.mw-htmlform-field-HTMLTitleTextField' ).length ) {
                                // FIXME: TitleInputWidget should be in its own module
                                modules.push( 'mediawiki.widgets' );
index f739746..6460ed1 100644 (file)
@@ -4,6 +4,8 @@
  */
 ( function ( mw, $ ) {
 
+       /*jshint -W024*/
+
        /**
         * Helper function for hide-if to find the nearby form field.
         *
         * @private
         * @param {jQuery} $el
         * @param {string} name
-        * @return {jQuery|null}
+        * @return {jQuery|OO.ui.Widget|null}
         */
        function hideIfGetField( $el, name ) {
-               var $found, $p,
+               var $found, $p, $widget,
                        suffix = name.replace( /^([^\[]+)/, '[$1]' );
 
                function nameFilter() {
                for ( $p = $el.parent(); $p.length > 0; $p = $p.parent() ) {
                        $found = $p.find( '[name]' ).filter( nameFilter );
                        if ( $found.length ) {
+                               $widget = $found.closest( '.oo-ui-widget[data-ooui]' );
+                               if ( $widget.length ) {
+                                       return OO.ui.Widget.static.infuse( $widget );
+                               }
                                return $found;
                        }
                }
         * @param {jQuery} $el
         * @param {Array} spec
         * @return {Array}
-        * @return {jQuery} return.0 Dependent fields
+        * @return {Array} return.0 Dependent fields, array of jQuery objects or OO.ui.Widgets
         * @return {Function} return.1 Test function
         */
        function hideIfParse( $el, spec ) {
-               var op, i, l, v, $field, $fields, fields, func, funcs, getVal;
+               var op, i, l, v, field, $field, fields, func, funcs, getVal;
 
                op = spec[ 0 ];
                l = spec.length;
                                                throw new Error( op + ' parameters must be arrays' );
                                        }
                                        v = hideIfParse( $el, spec[ i ] );
-                                       fields = fields.concat( v[ 0 ].toArray() );
+                                       fields = fields.concat( v[ 0 ] );
                                        funcs.push( v[ 1 ] );
                                }
-                               $fields = $( fields );
 
                                l = funcs.length;
                                switch ( op ) {
                                                break;
                                }
 
-                               return [ $fields, func ];
+                               return [ fields, func ];
 
                        case 'NOT':
                                if ( l !== 2 ) {
                                        throw new Error( 'NOT parameters must be arrays' );
                                }
                                v = hideIfParse( $el, spec[ 1 ] );
-                               $fields = v[ 0 ];
+                               fields = v[ 0 ];
                                func = v[ 1 ];
-                               return [ $fields, function () {
+                               return [ fields, function () {
                                        return !func();
                                } ];
 
                                if ( l !== 3 ) {
                                        throw new Error( op + ' takes exactly two parameters' );
                                }
-                               $field = hideIfGetField( $el, spec[ 1 ] );
-                               if ( !$field ) {
-                                       return [ $(), function () {
+                               field = hideIfGetField( $el, spec[ 1 ] );
+                               if ( !field ) {
+                                       return [ [], function () {
                                                return false;
                                        } ];
                                }
                                v = spec[ 2 ];
 
-                               if ( $field.first().prop( 'type' ) === 'radio' ||
-                                       $field.first().prop( 'type' ) === 'checkbox'
-                               ) {
-                                       getVal = function () {
-                                               var $selected = $field.filter( ':checked' );
-                                               return $selected.length ? $selected.val() : '';
-                                       };
+                               if ( field instanceof OO.ui.Widget ) {
+                                       if ( field.supports( 'isSelected' ) ) {
+                                               getVal = function () {
+                                                       var selected = field.isSelected();
+                                                       return selected ? field.getValue() : '';
+                                               };
+                                       } else {
+                                               getVal = function () {
+                                                       return field.getValue();
+                                               };
+                                       }
                                } else {
-                                       getVal = function () {
-                                               return $field.val();
-                                       };
+                                       $field = $( field );
+                                       if ( $field.prop( 'type' ) === 'radio' || $field.prop( 'type' ) === 'checkbox' ) {
+                                               getVal = function () {
+                                                       var $selected = $field.filter( ':checked' );
+                                                       return $selected.length ? $selected.val() : '';
+                                               };
+                                       } else {
+                                               getVal = function () {
+                                                       return $field.val();
+                                               };
+                                       }
                                }
 
                                switch ( op ) {
                                                break;
                                }
 
-                               return [ $field, func ];
+                               return [ [ field ], func ];
 
                        default:
                                throw new Error( 'Unrecognized operation \'' + op + '\'' );
 
        mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
                $root.find( '.mw-htmlform-hide-if' ).each( function () {
-                       var v, $fields, test, func,
-                               $el = $( this ),
-                               spec = $el.data( 'hideIf' );
+                       var v, i, fields, test, func, spec, self, modules,
+                               $el = $( this );
 
-                       if ( !spec ) {
-                               return;
+                       modules = [];
+                       if ( $el.is( '[data-ooui]' ) ) {
+                               modules.push( 'mediawiki.htmlform.ooui' );
+                               if ( $el.filter( '.mw-htmlform-field-HTMLTitleTextField' ).length ) {
+                                       // FIXME: TitleInputWidget should be in its own module
+                                       modules.push( 'mediawiki.widgets' );
+                               }
+                               if ( $el.filter( '.mw-htmlform-field-HTMLUserTextField' ).length ) {
+                                       modules.push( 'mediawiki.widgets.UserInputWidget' );
+                               }
+                               if (
+                                       $el.filter( '.mw-htmlform-field-HTMLSelectNamespace' ).length ||
+                                       $el.filter( '.mw-htmlform-field-HTMLSelectNamespaceWithButton' ).length
+                               ) {
+                                       // FIXME: NamespaceInputWidget should be in its own module (probably?)
+                                       modules.push( 'mediawiki.widgets' );
+                               }
                        }
 
-                       v = hideIfParse( $el, spec );
-                       $fields = v[ 0 ];
-                       test = v[ 1 ];
-                       func = function () {
-                               if ( test() ) {
-                                       $el.hide();
+                       mw.loader.using( modules ).done( function () {
+                               if ( $el.is( '[data-ooui]' ) ) {
+                                       // self should be a FieldLayout that mixes in mw.htmlform.Element
+                                       self = OO.ui.FieldLayout.static.infuse( $el );
+                                       spec = self.hideIf;
+                                       // The original element has been replaced with infused one
+                                       $el = self.$element;
                                } else {
-                                       $el.show();
+                                       self = $el;
+                                       spec = $el.data( 'hideIf' );
+                               }
+
+                               if ( !spec ) {
+                                       return;
+                               }
+
+                               v = hideIfParse( $el, spec );
+                               fields = v[ 0 ];
+                               test = v[ 1 ];
+                               // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget
+                               func = function () {
+                                       self.toggle( !test() );
+                               };
+                               for ( i = 0; i < fields.length; i++ ) {
+                                       // The .on() method works mostly the same for jQuery objects and OO.ui.Widget
+                                       fields[ i ].on( 'change', func );
                                }
-                       };
-                       $fields.on( 'change', func );
-                       func();
+                               func();
+                       } );
                } );
        } );
 
diff --git a/resources/src/mediawiki/htmlform/htmlform.Element.js b/resources/src/mediawiki/htmlform/htmlform.Element.js
new file mode 100644 (file)
index 0000000..37474f6
--- /dev/null
@@ -0,0 +1,45 @@
+( function ( mw ) {
+
+       mw.htmlform = {};
+
+       /**
+        * Allows custom data specific to HTMLFormField to be set for OOjs UI forms. This picks up the
+        * extra config from a matching PHP widget (defined in HTMLFormElement.php) when constructed using
+        * OO.ui.infuse().
+        *
+        * Currently only supports passing 'hide-if' data.
+        *
+        * @ignore
+        */
+       mw.htmlform.Element = function ( config ) {
+               // Configuration initialization
+               config = config || {};
+
+               // Properties
+               this.hideIf = config.hideIf;
+
+               // Initialization
+               if ( this.hideIf ) {
+                       this.$element.addClass( 'mw-htmlform-hide-if' );
+               }
+       };
+
+       mw.htmlform.FieldLayout = function ( config ) {
+               // Parent constructor
+               mw.htmlform.FieldLayout.parent.call( this, config );
+               // Mixin constructors
+               mw.htmlform.Element.call( this, config );
+       };
+       OO.inheritClass( mw.htmlform.FieldLayout, OO.ui.FieldLayout );
+       OO.mixinClass( mw.htmlform.FieldLayout, mw.htmlform.Element );
+
+       mw.htmlform.ActionFieldLayout = function ( config ) {
+               // Parent constructor
+               mw.htmlform.ActionFieldLayout.parent.call( this, config );
+               // Mixin constructors
+               mw.htmlform.Element.call( this, config );
+       };
+       OO.inheritClass( mw.htmlform.ActionFieldLayout, OO.ui.ActionFieldLayout );
+       OO.mixinClass( mw.htmlform.ActionFieldLayout, mw.htmlform.Element );
+
+}( mediaWiki ) );