hide-if.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. /*
  2. * HTMLForm enhancements:
  3. * Set up 'hide-if' behaviors for form fields that have them.
  4. */
  5. ( function () {
  6. /**
  7. * Helper function for hide-if to find the nearby form field.
  8. *
  9. * Find the closest match for the given name, "closest" being the minimum
  10. * level of parents to go to find a form field matching the given name or
  11. * ending in array keys matching the given name (e.g. "baz" matches
  12. * "foo[bar][baz]").
  13. *
  14. * @ignore
  15. * @private
  16. * @param {jQuery} $el
  17. * @param {string} name
  18. * @return {jQuery|OO.ui.Widget|null}
  19. */
  20. function hideIfGetField( $el, name ) {
  21. var $found, $p, $widget,
  22. suffix = name.replace( /^([^[]+)/, '[$1]' );
  23. function nameFilter() {
  24. return this.name === name ||
  25. ( this.name === ( 'wp' + name ) ) ||
  26. this.name.slice( -suffix.length ) === suffix;
  27. }
  28. for ( $p = $el.parent(); $p.length > 0; $p = $p.parent() ) {
  29. $found = $p.find( '[name]' ).filter( nameFilter );
  30. if ( $found.length ) {
  31. $widget = $found.closest( '.oo-ui-widget[data-ooui]' );
  32. if ( $widget.length ) {
  33. return OO.ui.Widget.static.infuse( $widget );
  34. }
  35. return $found;
  36. }
  37. }
  38. return null;
  39. }
  40. /**
  41. * Helper function for hide-if to return a test function and list of
  42. * dependent fields for a hide-if specification.
  43. *
  44. * @ignore
  45. * @private
  46. * @param {jQuery} $el
  47. * @param {Array} spec
  48. * @return {Array}
  49. * @return {Array} return.0 Dependent fields, array of jQuery objects or OO.ui.Widgets
  50. * @return {Function} return.1 Test function
  51. */
  52. function hideIfParse( $el, spec ) {
  53. var op, i, l, v, field, $field, fields, func, funcs, getVal;
  54. op = spec[ 0 ];
  55. l = spec.length;
  56. switch ( op ) {
  57. case 'AND':
  58. case 'OR':
  59. case 'NAND':
  60. case 'NOR':
  61. funcs = [];
  62. fields = [];
  63. for ( i = 1; i < l; i++ ) {
  64. if ( !Array.isArray( spec[ i ] ) ) {
  65. throw new Error( op + ' parameters must be arrays' );
  66. }
  67. v = hideIfParse( $el, spec[ i ] );
  68. fields = fields.concat( v[ 0 ] );
  69. funcs.push( v[ 1 ] );
  70. }
  71. l = funcs.length;
  72. switch ( op ) {
  73. case 'AND':
  74. func = function () {
  75. var i;
  76. for ( i = 0; i < l; i++ ) {
  77. if ( !funcs[ i ]() ) {
  78. return false;
  79. }
  80. }
  81. return true;
  82. };
  83. break;
  84. case 'OR':
  85. func = function () {
  86. var i;
  87. for ( i = 0; i < l; i++ ) {
  88. if ( funcs[ i ]() ) {
  89. return true;
  90. }
  91. }
  92. return false;
  93. };
  94. break;
  95. case 'NAND':
  96. func = function () {
  97. var i;
  98. for ( i = 0; i < l; i++ ) {
  99. if ( !funcs[ i ]() ) {
  100. return true;
  101. }
  102. }
  103. return false;
  104. };
  105. break;
  106. case 'NOR':
  107. func = function () {
  108. var i;
  109. for ( i = 0; i < l; i++ ) {
  110. if ( funcs[ i ]() ) {
  111. return false;
  112. }
  113. }
  114. return true;
  115. };
  116. break;
  117. }
  118. return [ fields, func ];
  119. case 'NOT':
  120. if ( l !== 2 ) {
  121. throw new Error( 'NOT takes exactly one parameter' );
  122. }
  123. if ( !Array.isArray( spec[ 1 ] ) ) {
  124. throw new Error( 'NOT parameters must be arrays' );
  125. }
  126. v = hideIfParse( $el, spec[ 1 ] );
  127. fields = v[ 0 ];
  128. func = v[ 1 ];
  129. return [ fields, function () {
  130. return !func();
  131. } ];
  132. case '===':
  133. case '!==':
  134. if ( l !== 3 ) {
  135. throw new Error( op + ' takes exactly two parameters' );
  136. }
  137. field = hideIfGetField( $el, spec[ 1 ] );
  138. if ( !field ) {
  139. return [ [], function () {
  140. return false;
  141. } ];
  142. }
  143. v = spec[ 2 ];
  144. if ( !( field instanceof $ ) ) {
  145. // field is a OO.ui.Widget
  146. if ( field.supports( 'isSelected' ) ) {
  147. getVal = function () {
  148. var selected = field.isSelected();
  149. return selected ? field.getValue() : '';
  150. };
  151. } else {
  152. getVal = function () {
  153. return field.getValue();
  154. };
  155. }
  156. } else {
  157. $field = $( field );
  158. if ( $field.prop( 'type' ) === 'radio' || $field.prop( 'type' ) === 'checkbox' ) {
  159. getVal = function () {
  160. var $selected = $field.filter( ':checked' );
  161. return $selected.length ? $selected.val() : '';
  162. };
  163. } else {
  164. getVal = function () {
  165. return $field.val();
  166. };
  167. }
  168. }
  169. switch ( op ) {
  170. case '===':
  171. func = function () {
  172. return getVal() === v;
  173. };
  174. break;
  175. case '!==':
  176. func = function () {
  177. return getVal() !== v;
  178. };
  179. break;
  180. }
  181. return [ [ field ], func ];
  182. default:
  183. throw new Error( 'Unrecognized operation \'' + op + '\'' );
  184. }
  185. }
  186. mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
  187. var
  188. $fields = $root.find( '.mw-htmlform-hide-if' ),
  189. $oouiFields = $fields.filter( '[data-ooui]' ),
  190. modules = [];
  191. if ( $oouiFields.length ) {
  192. modules.push( 'mediawiki.htmlform.ooui' );
  193. $oouiFields.each( function () {
  194. var data, extraModules,
  195. $el = $( this );
  196. data = $el.data( 'mw-modules' );
  197. if ( data ) {
  198. // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
  199. extraModules = data.split( ',' );
  200. modules.push.apply( modules, extraModules );
  201. }
  202. } );
  203. }
  204. mw.loader.using( modules ).done( function () {
  205. $fields.each( function () {
  206. var v, i, fields, test, func, spec, self,
  207. $el = $( this );
  208. if ( $el.is( '[data-ooui]' ) ) {
  209. // self should be a FieldLayout that mixes in mw.htmlform.Element
  210. self = OO.ui.FieldLayout.static.infuse( $el );
  211. spec = self.hideIf;
  212. // The original element has been replaced with infused one
  213. $el = self.$element;
  214. } else {
  215. self = $el;
  216. spec = $el.data( 'hideIf' );
  217. }
  218. if ( !spec ) {
  219. return;
  220. }
  221. v = hideIfParse( $el, spec );
  222. fields = v[ 0 ];
  223. test = v[ 1 ];
  224. // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget
  225. func = function () {
  226. var shouldHide = test();
  227. self.toggle( !shouldHide );
  228. // It is impossible to submit a form with hidden fields failing validation, e.g. one that
  229. // is required. However, validity is not checked for disabled fields, as these are not
  230. // submitted with the form. So we should also disable fields when hiding them.
  231. if ( self instanceof $ ) {
  232. // This also finds elements inside any nested fields (in case of HTMLFormFieldCloner),
  233. // which is problematic. But it works because:
  234. // * HTMLFormFieldCloner::createFieldsForKey() copies 'hide-if' rules to nested fields
  235. // * jQuery collections like $fields are in document order, so we register event
  236. // handlers for parents first
  237. // * Event handlers are fired in the order they were registered, so even if the handler
  238. // for parent messed up the child, the handle for child will run next and fix it
  239. self.find( 'input, textarea, select' ).each( function () {
  240. var $this = $( this );
  241. if ( shouldHide ) {
  242. if ( $this.data( 'was-disabled' ) === undefined ) {
  243. $this.data( 'was-disabled', $this.prop( 'disabled' ) );
  244. }
  245. $this.prop( 'disabled', true );
  246. } else {
  247. $this.prop( 'disabled', $this.data( 'was-disabled' ) );
  248. }
  249. } );
  250. } else {
  251. // self is a OO.ui.FieldLayout
  252. if ( shouldHide ) {
  253. if ( self.wasDisabled === undefined ) {
  254. self.wasDisabled = self.fieldWidget.isDisabled();
  255. }
  256. self.fieldWidget.setDisabled( true );
  257. } else if ( self.wasDisabled !== undefined ) {
  258. self.fieldWidget.setDisabled( self.wasDisabled );
  259. }
  260. }
  261. };
  262. for ( i = 0; i < fields.length; i++ ) {
  263. // The .on() method works mostly the same for jQuery objects and OO.ui.Widget
  264. fields[ i ].on( 'change', func );
  265. }
  266. func();
  267. } );
  268. } );
  269. } );
  270. }() );