2 * HTMLForm enhancements:
3 * Set up 'hide-if' behaviors for form fields that have them.
8 * Helper function for hide-if to find the nearby form field.
10 * Find the closest match for the given name, "closest" being the minimum
11 * level of parents to go to find a form field matching the given name or
12 * ending in array keys matching the given name (e.g. "baz" matches
18 * @param {string} name
19 * @return {jQuery|OO.ui.Widget|null}
21 function hideIfGetField( $el, name ) {
22 var $found, $p, $widget,
23 suffix = name.replace( /^([^\[]+)/, '[$1]' );
25 function nameFilter() {
26 return this.name === name ||
27 ( this.name === ( 'wp' + name ) ) ||
28 this.name.slice( -suffix.length ) === suffix;
31 for ( $p = $el.parent(); $p.length > 0; $p = $p.parent() ) {
32 $found = $p.find( '[name]' ).filter( nameFilter );
33 if ( $found.length ) {
34 $widget = $found.closest( '.oo-ui-widget[data-ooui]' );
35 if ( $widget.length ) {
36 return OO.ui.Widget.static.infuse( $widget );
45 * Helper function for hide-if to return a test function and list of
46 * dependent fields for a hide-if specification.
53 * @return {Array} return.0 Dependent fields, array of jQuery objects or OO.ui.Widgets
54 * @return {Function} return.1 Test function
56 function hideIfParse( $el, spec ) {
57 var op, i, l, v, field, $field, fields, func, funcs, getVal;
68 for ( i = 1; i < l; i++ ) {
69 if ( !$.isArray( spec[ i ] ) ) {
70 throw new Error( op + ' parameters must be arrays' );
72 v = hideIfParse( $el, spec[ i ] );
73 fields = fields.concat( v[ 0 ] );
82 for ( i = 0; i < l; i++ ) {
83 if ( !funcs[ i ]() ) {
94 for ( i = 0; i < l; i++ ) {
106 for ( i = 0; i < l; i++ ) {
107 if ( !funcs[ i ]() ) {
118 for ( i = 0; i < l; i++ ) {
119 if ( funcs[ i ]() ) {
128 return [ fields, func ];
132 throw new Error( 'NOT takes exactly one parameter' );
134 if ( !$.isArray( spec[ 1 ] ) ) {
135 throw new Error( 'NOT parameters must be arrays' );
137 v = hideIfParse( $el, spec[ 1 ] );
140 return [ fields, function () {
147 throw new Error( op + ' takes exactly two parameters' );
149 field = hideIfGetField( $el, spec[ 1 ] );
151 return [ [], function () {
157 if ( !( field instanceof jQuery ) ) {
158 // field is a OO.ui.Widget
159 if ( field.supports( 'isSelected' ) ) {
160 getVal = function () {
161 var selected = field.isSelected();
162 return selected ? field.getValue() : '';
165 getVal = function () {
166 return field.getValue();
171 if ( $field.prop( 'type' ) === 'radio' || $field.prop( 'type' ) === 'checkbox' ) {
172 getVal = function () {
173 var $selected = $field.filter( ':checked' );
174 return $selected.length ? $selected.val() : '';
177 getVal = function () {
186 return getVal() === v;
191 return getVal() !== v;
196 return [ [ field ], func ];
199 throw new Error( 'Unrecognized operation \'' + op + '\'' );
203 mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
205 $fields = $root.find( '.mw-htmlform-hide-if' ),
206 $oouiFields = $fields.filter( '[data-ooui]' ),
209 if ( $oouiFields.length ) {
210 modules.push( 'mediawiki.htmlform.ooui' );
211 $oouiFields.each( function () {
212 var data, extraModules,
215 data = $el.data( 'mw-modules' );
217 // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
218 extraModules = data.split( ',' );
219 modules.push.apply( modules, extraModules );
224 mw.loader.using( modules ).done( function () {
225 $fields.each( function () {
226 var v, i, fields, test, func, spec, self,
229 if ( $el.is( '[data-ooui]' ) ) {
230 // self should be a FieldLayout that mixes in mw.htmlform.Element
231 self = OO.ui.FieldLayout.static.infuse( $el );
233 // The original element has been replaced with infused one
237 spec = $el.data( 'hideIf' );
244 v = hideIfParse( $el, spec );
247 // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget
249 var shouldHide = test();
250 self.toggle( !shouldHide );
252 // It is impossible to submit a form with hidden fields failing validation, e.g. one that
253 // is required. However, validity is not checked for disabled fields, as these are not
254 // submitted with the form. So we should also disable fields when hiding them.
255 if ( self instanceof jQuery ) {
256 // This also finds elements inside any nested fields (in case of HTMLFormFieldCloner),
257 // which is problematic. But it works because:
258 // * HTMLFormFieldCloner::createFieldsForKey() copies 'hide-if' rules to nested fields
259 // * jQuery collections like $fields are in document order, so we register event
260 // handlers for parents first
261 // * Event handlers are fired in the order they were registered, so even if the handler
262 // for parent messed up the child, the handle for child will run next and fix it
263 self.find( 'input, textarea, select' ).each( function () {
264 var $this = $( this );
266 if ( $this.data( 'was-disabled' ) === undefined ) {
267 $this.data( 'was-disabled', $this.prop( 'disabled' ) );
269 $this.prop( 'disabled', true );
271 $this.prop( 'disabled', $this.data( 'was-disabled' ) );
275 // self is a OO.ui.FieldLayout
277 if ( self.wasDisabled === undefined ) {
278 self.wasDisabled = self.fieldWidget.isDisabled();
280 self.fieldWidget.setDisabled( true );
281 } else if ( self.wasDisabled !== undefined ) {
282 self.fieldWidget.setDisabled( self.wasDisabled );
286 for ( i = 0; i < fields.length; i++ ) {
287 // The .on() method works mostly the same for jQuery objects and OO.ui.Widget
288 fields[ i ].on( 'change', func );
295 }( mediaWiki, jQuery ) );