From 89107070d14187915e760f8543579ec4d784620f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20Dziewo=C5=84ski?= Date: Sun, 31 Jul 2016 01:19:26 +0200 Subject: [PATCH] Support 'hide-if' parameters in OOUI HTMLForm 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 | 3 + includes/htmlform/HTMLFormElement.php | 57 ++++++++ includes/htmlform/HTMLFormField.php | 11 +- resources/Resources.php | 9 ++ .../src/mediawiki/htmlform/autoinfuse.js | 2 +- resources/src/mediawiki/htmlform/hide-if.js | 124 ++++++++++++------ .../mediawiki/htmlform/htmlform.Element.js | 45 +++++++ 7 files changed, 209 insertions(+), 42 deletions(-) create mode 100644 includes/htmlform/HTMLFormElement.php create mode 100644 resources/src/mediawiki/htmlform/htmlform.Element.js diff --git a/autoload.php b/autoload.php index e37a011093..e7671be3dc 100644 --- a/autoload.php +++ b/autoload.php @@ -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 index 0000000000..e553218fa5 --- /dev/null +++ b/includes/htmlform/HTMLFormElement.php @@ -0,0 +1,57 @@ +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'; + } +} diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index b47bfa06ca..3319d3b86a 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -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 ); } /** diff --git a/resources/Resources.php b/resources/Resources.php index 1578b4288f..cfaaf5f1bd 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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', diff --git a/resources/src/mediawiki/htmlform/autoinfuse.js b/resources/src/mediawiki/htmlform/autoinfuse.js index f77e36720b..8efbc69fb8 100644 --- a/resources/src/mediawiki/htmlform/autoinfuse.js +++ b/resources/src/mediawiki/htmlform/autoinfuse.js @@ -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' ); diff --git a/resources/src/mediawiki/htmlform/hide-if.js b/resources/src/mediawiki/htmlform/hide-if.js index f739746f83..6460ed1497 100644 --- a/resources/src/mediawiki/htmlform/hide-if.js +++ b/resources/src/mediawiki/htmlform/hide-if.js @@ -4,6 +4,8 @@ */ ( function ( mw, $ ) { + /*jshint -W024*/ + /** * Helper function for hide-if to find the nearby form field. * @@ -16,10 +18,10 @@ * @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() { @@ -31,6 +33,10 @@ 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; } } @@ -46,11 +52,11 @@ * @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; @@ -66,10 +72,9 @@ 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 ) { @@ -122,7 +127,7 @@ break; } - return [ $fields, func ]; + return [ fields, func ]; case 'NOT': if ( l !== 2 ) { @@ -132,9 +137,9 @@ 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(); } ]; @@ -143,25 +148,37 @@ 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 ) { @@ -177,7 +194,7 @@ break; } - return [ $field, func ]; + return [ [ field ], func ]; default: throw new Error( 'Unrecognized operation \'' + op + '\'' ); @@ -186,26 +203,57 @@ 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 index 0000000000..37474f6833 --- /dev/null +++ b/resources/src/mediawiki/htmlform/htmlform.Element.js @@ -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 ) ); -- 2.20.1