/*
* HTMLForm enhancements:
* Set up 'hide-if' behaviors for form fields that have them.
*/
( function ( mw, $ ) {
/**
* Helper function for hide-if to find the nearby form field.
*
* Find the closest match for the given name, "closest" being the minimum
* level of parents to go to find a form field matching the given name or
* ending in array keys matching the given name (e.g. "baz" matches
* "foo[bar][baz]").
*
* @ignore
* @private
* @param {jQuery} $el
* @param {string} name
* @return {jQuery|OO.ui.Widget|null}
*/
function hideIfGetField( $el, name ) {
var $found, $p, $widget,
suffix = name.replace( /^([^[]+)/, '[$1]' );
function nameFilter() {
return this.name === name ||
( this.name === ( 'wp' + name ) ) ||
this.name.slice( -suffix.length ) === suffix;
}
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;
}
}
return null;
}
/**
* Helper function for hide-if to return a test function and list of
* dependent fields for a hide-if specification.
*
* @ignore
* @private
* @param {jQuery} $el
* @param {Array} spec
* @return {Array}
* @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, $field, fields, func, funcs, getVal;
op = spec[ 0 ];
l = spec.length;
switch ( op ) {
case 'AND':
case 'OR':
case 'NAND':
case 'NOR':
funcs = [];
fields = [];
for ( i = 1; i < l; i++ ) {
if ( !Array.isArray( spec[ i ] ) ) {
throw new Error( op + ' parameters must be arrays' );
}
v = hideIfParse( $el, spec[ i ] );
fields = fields.concat( v[ 0 ] );
funcs.push( v[ 1 ] );
}
l = funcs.length;
switch ( op ) {
case 'AND':
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
if ( !funcs[ i ]() ) {
return false;
}
}
return true;
};
break;
case 'OR':
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
if ( funcs[ i ]() ) {
return true;
}
}
return false;
};
break;
case 'NAND':
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
if ( !funcs[ i ]() ) {
return true;
}
}
return false;
};
break;
case 'NOR':
func = function () {
var i;
for ( i = 0; i < l; i++ ) {
if ( funcs[ i ]() ) {
return false;
}
}
return true;
};
break;
}
return [ fields, func ];
case 'NOT':
if ( l !== 2 ) {
throw new Error( 'NOT takes exactly one parameter' );
}
if ( !Array.isArray( spec[ 1 ] ) ) {
throw new Error( 'NOT parameters must be arrays' );
}
v = hideIfParse( $el, spec[ 1 ] );
fields = v[ 0 ];
func = v[ 1 ];
return [ fields, function () {
return !func();
} ];
case '===':
case '!==':
if ( l !== 3 ) {
throw new Error( op + ' takes exactly two parameters' );
}
field = hideIfGetField( $el, spec[ 1 ] );
if ( !field ) {
return [ [], function () {
return false;
} ];
}
v = spec[ 2 ];
if ( !( field instanceof jQuery ) ) {
// field is a OO.ui.Widget
if ( field.supports( 'isSelected' ) ) {
getVal = function () {
var selected = field.isSelected();
return selected ? field.getValue() : '';
};
} else {
getVal = function () {
return field.getValue();
};
}
} else {
$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 ) {
case '===':
func = function () {
return getVal() === v;
};
break;
case '!==':
func = function () {
return getVal() !== v;
};
break;
}
return [ [ field ], func ];
default:
throw new Error( 'Unrecognized operation \'' + op + '\'' );
}
}
mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
var
$fields = $root.find( '.mw-htmlform-hide-if' ),
$oouiFields = $fields.filter( '[data-ooui]' ),
modules = [];
if ( $oouiFields.length ) {
modules.push( 'mediawiki.htmlform.ooui' );
$oouiFields.each( function () {
var data, extraModules,
$el = $( this );
data = $el.data( 'mw-modules' );
if ( data ) {
// We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
extraModules = data.split( ',' );
modules.push.apply( modules, extraModules );
}
} );
}
mw.loader.using( modules ).done( function () {
$fields.each( function () {
var v, i, fields, test, func, spec, self,
$el = $( this );
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 {
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 () {
var shouldHide = test();
self.toggle( !shouldHide );
// It is impossible to submit a form with hidden fields failing validation, e.g. one that
// is required. However, validity is not checked for disabled fields, as these are not
// submitted with the form. So we should also disable fields when hiding them.
if ( self instanceof jQuery ) {
// This also finds elements inside any nested fields (in case of HTMLFormFieldCloner),
// which is problematic. But it works because:
// * HTMLFormFieldCloner::createFieldsForKey() copies 'hide-if' rules to nested fields
// * jQuery collections like $fields are in document order, so we register event
// handlers for parents first
// * Event handlers are fired in the order they were registered, so even if the handler
// for parent messed up the child, the handle for child will run next and fix it
self.find( 'input, textarea, select' ).each( function () {
var $this = $( this );
if ( shouldHide ) {
if ( $this.data( 'was-disabled' ) === undefined ) {
$this.data( 'was-disabled', $this.prop( 'disabled' ) );
}
$this.prop( 'disabled', true );
} else {
$this.prop( 'disabled', $this.data( 'was-disabled' ) );
}
} );
} else {
// self is a OO.ui.FieldLayout
if ( shouldHide ) {
if ( self.wasDisabled === undefined ) {
self.wasDisabled = self.fieldWidget.isDisabled();
}
self.fieldWidget.setDisabled( true );
} else if ( self.wasDisabled !== undefined ) {
self.fieldWidget.setDisabled( self.wasDisabled );
}
}
};
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 );
}
func();
} );
} );
} );
}( mediaWiki, jQuery ) );