diff options
Diffstat (limited to 'www/wiki/resources/src/mediawiki/htmlform/hide-if.js')
-rw-r--r-- | www/wiki/resources/src/mediawiki/htmlform/hide-if.js | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki/htmlform/hide-if.js b/www/wiki/resources/src/mediawiki/htmlform/hide-if.js new file mode 100644 index 00000000..6d3c9fd1 --- /dev/null +++ b/www/wiki/resources/src/mediawiki/htmlform/hide-if.js @@ -0,0 +1,295 @@ +/* + * 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 ) ); |