Merge "Update docs/hooks.txt for ShowSearchHitTitle"
[mediawiki.git] / resources / src / mediawiki / htmlform / hide-if.js
blob157ac0621107b645cb3a8c70a320d02b396be2cf
1 /*
2  * HTMLForm enhancements:
3  * Set up 'hide-if' behaviors for form fields that have them.
4  */
5 ( function ( mw, $ ) {
7         /**
8          * Helper function for hide-if to find the nearby form field.
9          *
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
13          * "foo[bar][baz]").
14          *
15          * @ignore
16          * @private
17          * @param {jQuery} $el
18          * @param {string} name
19          * @return {jQuery|OO.ui.Widget|null}
20          */
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;
29                 }
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 );
37                                 }
38                                 return $found;
39                         }
40                 }
41                 return null;
42         }
44         /**
45          * Helper function for hide-if to return a test function and list of
46          * dependent fields for a hide-if specification.
47          *
48          * @ignore
49          * @private
50          * @param {jQuery} $el
51          * @param {Array} spec
52          * @return {Array}
53          * @return {Array} return.0 Dependent fields, array of jQuery objects or OO.ui.Widgets
54          * @return {Function} return.1 Test function
55          */
56         function hideIfParse( $el, spec ) {
57                 var op, i, l, v, field, $field, fields, func, funcs, getVal;
59                 op = spec[ 0 ];
60                 l = spec.length;
61                 switch ( op ) {
62                         case 'AND':
63                         case 'OR':
64                         case 'NAND':
65                         case 'NOR':
66                                 funcs = [];
67                                 fields = [];
68                                 for ( i = 1; i < l; i++ ) {
69                                         if ( !$.isArray( spec[ i ] ) ) {
70                                                 throw new Error( op + ' parameters must be arrays' );
71                                         }
72                                         v = hideIfParse( $el, spec[ i ] );
73                                         fields = fields.concat( v[ 0 ] );
74                                         funcs.push( v[ 1 ] );
75                                 }
77                                 l = funcs.length;
78                                 switch ( op ) {
79                                         case 'AND':
80                                                 func = function () {
81                                                         var i;
82                                                         for ( i = 0; i < l; i++ ) {
83                                                                 if ( !funcs[ i ]() ) {
84                                                                         return false;
85                                                                 }
86                                                         }
87                                                         return true;
88                                                 };
89                                                 break;
91                                         case 'OR':
92                                                 func = function () {
93                                                         var i;
94                                                         for ( i = 0; i < l; i++ ) {
95                                                                 if ( funcs[ i ]() ) {
96                                                                         return true;
97                                                                 }
98                                                         }
99                                                         return false;
100                                                 };
101                                                 break;
103                                         case 'NAND':
104                                                 func = function () {
105                                                         var i;
106                                                         for ( i = 0; i < l; i++ ) {
107                                                                 if ( !funcs[ i ]() ) {
108                                                                         return true;
109                                                                 }
110                                                         }
111                                                         return false;
112                                                 };
113                                                 break;
115                                         case 'NOR':
116                                                 func = function () {
117                                                         var i;
118                                                         for ( i = 0; i < l; i++ ) {
119                                                                 if ( funcs[ i ]() ) {
120                                                                         return false;
121                                                                 }
122                                                         }
123                                                         return true;
124                                                 };
125                                                 break;
126                                 }
128                                 return [ fields, func ];
130                         case 'NOT':
131                                 if ( l !== 2 ) {
132                                         throw new Error( 'NOT takes exactly one parameter' );
133                                 }
134                                 if ( !$.isArray( spec[ 1 ] ) ) {
135                                         throw new Error( 'NOT parameters must be arrays' );
136                                 }
137                                 v = hideIfParse( $el, spec[ 1 ] );
138                                 fields = v[ 0 ];
139                                 func = v[ 1 ];
140                                 return [ fields, function () {
141                                         return !func();
142                                 } ];
144                         case '===':
145                         case '!==':
146                                 if ( l !== 3 ) {
147                                         throw new Error( op + ' takes exactly two parameters' );
148                                 }
149                                 field = hideIfGetField( $el, spec[ 1 ] );
150                                 if ( !field ) {
151                                         return [ [], function () {
152                                                 return false;
153                                         } ];
154                                 }
155                                 v = spec[ 2 ];
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() : '';
163                                                 };
164                                         } else {
165                                                 getVal = function () {
166                                                         return field.getValue();
167                                                 };
168                                         }
169                                 } else {
170                                         $field = $( field );
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() : '';
175                                                 };
176                                         } else {
177                                                 getVal = function () {
178                                                         return $field.val();
179                                                 };
180                                         }
181                                 }
183                                 switch ( op ) {
184                                         case '===':
185                                                 func = function () {
186                                                         return getVal() === v;
187                                                 };
188                                                 break;
189                                         case '!==':
190                                                 func = function () {
191                                                         return getVal() !== v;
192                                                 };
193                                                 break;
194                                 }
196                                 return [ [ field ], func ];
198                         default:
199                                 throw new Error( 'Unrecognized operation \'' + op + '\'' );
200                 }
201         }
203         mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
204                 var
205                         $fields = $root.find( '.mw-htmlform-hide-if' ),
206                         $oouiFields = $fields.filter( '[data-ooui]' ),
207                         modules = [];
209                 if ( $oouiFields.length ) {
210                         modules.push( 'mediawiki.htmlform.ooui' );
211                         $oouiFields.each( function () {
212                                 var data, extraModules,
213                                         $el = $( this );
215                                 data = $el.data( 'mw-modules' );
216                                 if ( data ) {
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 );
220                                 }
221                         } );
222                 }
224                 mw.loader.using( modules ).done( function () {
225                         $fields.each( function () {
226                                 var v, i, fields, test, func, spec, self,
227                                         $el = $( this );
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 );
232                                         spec = self.hideIf;
233                                         // The original element has been replaced with infused one
234                                         $el = self.$element;
235                                 } else {
236                                         self = $el;
237                                         spec = $el.data( 'hideIf' );
238                                 }
240                                 if ( !spec ) {
241                                         return;
242                                 }
244                                 v = hideIfParse( $el, spec );
245                                 fields = v[ 0 ];
246                                 test = v[ 1 ];
247                                 // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget
248                                 func = function () {
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 );
265                                                         if ( shouldHide ) {
266                                                                 if ( $this.data( 'was-disabled' ) === undefined ) {
267                                                                         $this.data( 'was-disabled', $this.prop( 'disabled' ) );
268                                                                 }
269                                                                 $this.prop( 'disabled', true );
270                                                         } else {
271                                                                 $this.prop( 'disabled', $this.data( 'was-disabled' ) );
272                                                         }
273                                                 } );
274                                         } else {
275                                                 // self is a OO.ui.FieldLayout
276                                                 if ( shouldHide ) {
277                                                         if ( self.wasDisabled === undefined ) {
278                                                                 self.wasDisabled = self.fieldWidget.isDisabled();
279                                                         }
280                                                         self.fieldWidget.setDisabled( true );
281                                                 } else if ( self.wasDisabled !== undefined ) {
282                                                         self.fieldWidget.setDisabled( self.wasDisabled );
283                                                 }
284                                         }
285                                 };
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 );
289                                 }
290                                 func();
291                         } );
292                 } );
293         } );
295 }( mediaWiki, jQuery ) );