1/* 2 Name: controllable-fields.js 3 Author: AuRise Creative | https://aurisecreative.com 4 Last Modified: August 31, 2024 @ 22:32 5 6 To use this feature... 7 8 1. Add a "au-cf7-controller" class to the radio, checkbox, select, or number/range HTML elements that will be controlling others 9 - Checkbox: displays the controlled fields when checked and hides when unchecked. 10 - Radio: displays the controlled fields when checked and hides the rest. 11 - Select: displays the controlled fields when they match the value that is selected and hides the rest. 12 - Number: displays the controlled fields based on number ranges set in the controlled element's values. 13 2. Controlled fields should have a data-controller attribute on its wrapping element set to the ID attribute of its controller 14 within the same form. It can have multiple controllers, with ID using pipes (|) to separate them. 15 3. Controlled fields should have a "au-cf7-hidden-by-controller" class added to its wrapping element to hide it by default. This 16 feature simply toggles that class on/off, so you'll need CSS to actually hide it based on that class. 17 4. Controlled fields controlled by a radio button, select field, or number field, should have a `data-values` attribute set to a 18 pipe separated list of the values used to display it on the wrapping element. Values can include operations such as >, >=, 19 <, <=, !=, or *. The operation = is assumed when no comparison is found. 20 5. If it has multiple controllers, then there should be a `data-{controller ID}-values` attribute that sets the values for that 21 specific controller. 22 5. If the controlled field should be required when displayed, instead of adding the required attribute to the input/select field, 23 add the "required-when-visible" to the class attribute. 24 6. It is possible to nest controllers. 25 7. Controlled fields with multiple controllers are only hidden if all controllers evaluate to no matches. Otherwise, it will 26 remain visible. Multiple controllers are treated as an OR comparison, meaning at least one controller must be active for 27 the controlled field to display. Nest them to treat them as an AND operation. 28*/ 29window['$'] = window['$'] || jQuery.noConflict(); 30const aurise_controllable_fields_cf7 = { 31 version: '2024.04.27.10.41', 32 init: function() { 33 // Controllable fields 34 let click_controllers = '.wpcf7 form input[type=checkbox].au-cf7-controller,.wpcf7 form input[type=radio].au-cf7-controller', 35 select_controllers = '.wpcf7 form select.au-cf7-controller', 36 text_controllers = '.wpcf7 form input[type=text].au-cf7-controller,.wpcf7 form input[type=email].au-cf7-controller,.wpcf7 form input[type=hidden].au-cf7-controller,.wpcf7 form input[type=password].au-cf7-controller,.wpcf7 form input[type=search].au-cf7-controller,.wpcf7 form input[type=tel].au-cf7-controller,.wpcf7 form input[type=url].au-cf7-controller', 37 interactive_controllers = '.wpcf7 form input[type=number].au-cf7-controller,.wpcf7 form input[type=range].au-cf7-controller', 38 controllers = $(click_controllers + ',' + select_controllers + ',' + interactive_controllers + ',' + text_controllers); // All controllers 39 if (controllers.length) { 40 41 //Add controllable field listeners 42 $(click_controllers).on('click', aurise_controllable_fields_cf7.toggleHandler); 43 $(select_controllers).on('change', aurise_controllable_fields_cf7.toggleHandler); 44 $(text_controllers).on('change keyup blur', aurise_controllable_fields_cf7.toggleHandler); 45 $(interactive_controllers).on('change keyup blur mousemove mousedown mouseup', aurise_controllable_fields_cf7.toggleHandler); 46 47 // Initialize default states 48 let c1 = 1; 49 controllers.each(function() { 50 let $controller = $(this), 51 controller_name = $controller.attr('name'); 52 53 // If this controller does not have an ID attribute, add a temporary one 54 let temp_id = false; 55 if ($controller.attr('id') === undefined) { 56 temp_id = 'aurise-cf7-cf-temp-controller-id-'
56+ controller_name + '-' + c1; 57 $controller.attr('id', temp_id); 58 } 59 60 let id = $controller.attr('id'), 61 cid = aurise_controllable_fields_cf7.getControllerId(this), 62 $controlled = $('[data-controller*="' + (temp_id ? controller_name : cid) + '"]'); 63 if ($controlled.length) { 64 if (temp_id) { 65 $controlled.attr('data-controller', $controlled.attr('data-controller').replace(controller_name, id)); 66 } 67 let controlled_value = $controller.is('input[type=checkbox]') ? aurise_controllable_fields_cf7.getCheckbox($controller) : $controller.val(), 68 ariaControls = []; 69 aurise_controllable_fields_cf7.toggleControlledFields(cid, id, controlled_value); 70 // Update aria attributes 71 $controlled.each(function() { 72 let c2 = 1, 73 $thisControlled = $(this), 74 thisControlledId = $thisControlled.attr('id'), 75 thisLabel = $thisControlled.attr('aria-labelledby'); 76 if (!thisControlledId) { 77 // Generate a random ID 78 thisControlledId = 'aurise-cf7-cf-temp-controlled-id-' + cid + '-' + c1 + '-' + c2; 79 $thisControlled.attr('id', thisControlledId); 80 } 81 ariaControls.push(thisControlledId); 82 if (!thisLabel) { 83 thisLabel = id; 84 } else { 85 thisLabel += ' ' + id; 86 } 87 $thisControlled.attr({ 'role': 'group', 'aria-labelledby': thisLabel }); 88 c2++; 89 }); 90 ariaControls = ariaControls.join(' '); 91 $controller.attr({ 'aria-controls': ariaControls, 'aria-owns': ariaControls }); 92 } else { 93 console.warn('No controlled elements found for controller: ' + cid); 94 // If I am a checkbox or radio button, look for my name object instead 95 } 96 $controller.trigger('controller_init'); 97 c1++; 98 }); 99 } 100 }, 101 getControllerId: function(input) { 102 let $controller = $(input), 103 id = $controller.attr('id'), 104 id2 = $controller.attr('data-id'); 105 // If there is a data-id attribute, return that 106 if (id2) { 107 return id2; 108 } 109 // If I don't have an id attribute, return my name attribute 110 if (!id || $controller.is('[type=radio]') || $controller.is('[type=checkbox]')) { 111 return $controller.attr('name').replace('[]', ''); 112 } 113 return id; // Return my id attribute 114 }, 115 getControllerById: function(cid) { 116 // The cid could be the id attribute or data-id attribute 117 let $controller = $('[data-id="' + cid + '"].au-cf7-controller'); 118 if ($controller.length) { 119 return $controller; 120 } 121 $controller = $('#' + cid + '.au-cf7-controller'); 122 if ($controller.length) { 123 return $controller; 124 } 125 return false; 126 }, 127 getCheckbox: function(input) { 128 //Returns a true/false boolean value based on whether the checkbox is checked 129 let $input = $(input); 130 return ($input.is(':checked') || $input.prop('checked')); 131 }, 132 toggleCheckbox: function(input, passedValue) { 133 //Changes a checkbox input to be checked or unchecked based on boolean parameter (or toggles if not included) 134 //Only changes it visually - it does not change any data in any objects 135 let $input = $(input), 136 value = passedValue; 137 if (typeof(value) != 'boolean' || value === undefined) { 138 value = !aurise_controllable_fields_cf7.getCheckbox($input); 139 } 140 if (value) { 141 $input.attr('checked', 'checked'); 142 $input.prop('checked', true); 143 } else { 144 $input.removeAttr('checked'); 145 $input.prop('checked', false); 146 } 147 input.trigger('toggled_checkbox'); 148 }, 149 number: { 150 changing: false,
151 check: false 152 }, 153 toggleHandler: function(e) { 154 // Handle the number spinbox and ranged slider 155 if (typeof(e) == 'object') { 156 if (e.type == 'mousedown') { 157 // Mouse is being clicked down, turn it on 158 aurise_controllable_fields_cf7.number.changing = true; 159 aurise_controllable_fields_cf7.number.check = setInterval(function() { 160 if (aurise_controllable_fields_cf7.number.changing) { 161 aurise_controllable_fields_cf7.toggleHandler($(e.target).attr('id')); 162 } 163 }, 500); 164 } else if (e.type == 'mouseup' && aurise_controllable_fields_cf7.number.changing) { 165 // Mouse was released after changing the number, turn it all off 166 aurise_controllable_fields_cf7.number.changing = false; 167 clearInterval(aurise_controllable_fields_cf7.number.check); 168 return; 169 } else if ((e.type == 'mousemove' && !aurise_controllable_fields_cf7.number.changing) || e.type == 'mouseup') { 170 // If mouse is moving but numbers aren't changing, or mouse was released but we don't care, bail 171 return; 172 } 173 } 174 let $controller = typeof(e) == 'string' ? $('#' + e) : $(e.target), 175 id = $controller.attr('id'), 176 cid = aurise_controllable_fields_cf7.getControllerId($controller); 177 aurise_controllable_fields_cf7.toggleControlledFields(cid, id, null); 178 }, 179 toggleControlledFields: function(cid, id, forcedToggle) { 180 let $controller = $('#' + id); 181 if ($controller.length < 1) { console.warn('Controller #' + id + '" does not exist!'); return; } 182 let is_checkbox = $controller.is('[type=checkbox]'), 183 is_radio = $controller.is('[type=radio]'), 184 is_select = $controller.is('select'), 185 is_toggle = is_checkbox || is_radio, 186 toggle_value = is_toggle ? aurise_controllable_fields_cf7.getCheckbox($controller) : null, 187 is_expanded = false, 188 //is_number = $controller.is('[type=number]'), 189 $controlledObjs = $('[data-controller*="' + cid + '"]'), 190 $empty_controlled = false; 191 if ($controlledObjs.length < 1) { console.warn('No controlled elements found for controller: ' + cid); return; } 192 let controlled_value = forcedToggle === null || forcedToggle === undefined ? $controller.val() : forcedToggle, 193 count_displayed = 0; 194 195 // Loop through each controlled element; the value must match that of the input to display it 196 $controlledObjs.each(function(co, controlled) { 197 let $controlled = $(controlled), 198 // If I have multiple controllers, get the values for this specific controller 199 multiControllers = $controlled.attr('data-controller').indexOf('|') >= 0, 200 acceptedValues = multiControllers ? $controlled.attr('data-' + cid + '-values') : $controlled.attr('data-values'), 201 matches = 0, 202 displayMe = false; 203 204 //console.log(acceptedValues, toggle_value); 205 if (acceptedValues === '' || acceptedValues === undefined) { 206 $empty_controlled = $controlled; 207 208 } 209 matches += aurise_controllable_fields_cf7.compareLoop(acceptedValues, controlled_value, is_toggle, toggle_value); 210 if (matches > 0) { 211 // This controlled element's value matches what was selected, display it 212 count_displayed++; 213 $controlled.removeClass('au-cf7-hidden-by-controller'); 214 displayMe = true; 215 } else { 216 // Before hiding, we need to check if I have multiple controllers and if I do, if any of them say I should still be shown 217 if (multiControllers) { 218 matches = 0; 219 let controllers = $controlled.attr('data-controller').split('|'); 220 $.each(controllers, function(c, other_cid) { 221 // Skip checking the controller this is for, just check the other ones 222 if (other_cid !== cid) { 223 let $other_controller = aurise_controllable_fields_cf7.getControllerById(other_cid), 224 other_controlled_value = $other_
224controller.val(), 225 is_other_toggle = $other_controller.is('[type=checkbox]') || $other_controller.is('[type=radio]'), 226 otherAcceptedValues = $other_controller.attr('data-' + other_cid + '-values'); 227 matches += aurise_controllable_fields_cf7.compareLoop( 228 otherAcceptedValues, 229 other_controlled_value, 230 is_other_toggle, 231 is_other_toggle ? aurise_controllable_fields_cf7.getCheckbox($other_controller) : null 232 ); 233 } 234 }); 235 if (matches > 0) { 236 // This controlled element's value matches what was selected, display it 237 count_displayed++; 238 $controlled.removeClass('au-cf7-hidden-by-controller'); 239 displayMe = true; 240 } else { 241 // This controlled element's value DOES NOT match what was selected, hide it 242 $controlled.addClass('au-cf7-hidden-by-controller'); 243 } 244 } else { 245 // Before hiding, we need to check if the checkbox value is still checked because we could just be checking/unchecking a different box of the same group 246 if (is_toggle) { 247 if (is_checkbox) { 248 // Skip checking the controller this is for, just check the other checked checkboxes in the set 249 let controllers = $controller.closest('form').find('input[type=checkbox][name="' + $controller.attr('name') + '"]:checked:not(#' + $controller.attr('id') + ')'); 250 if (controllers.length) { 251 $.each(controllers, function(c, other_controller) { 252 let $other_controller = $(other_controller); 253 is_other_toggle = $other_controller.is('[type=checkbox]') || $other_controller.is('[type=radio]'); 254 matches += aurise_controllable_fields_cf7.compareLoop( 255 acceptedValues, 256 $other_controller.val(), 257 is_other_toggle, is_other_toggle ? aurise_controllable_fields_cf7.getCheckbox(other_controller) : null 258 ); 259 }); 260 } 261 } 262 } 263 264 if (matches > 0) { 265 // This controlled element's value matches what was selected, display it 266 count_displayed++; 267 $controlled.removeClass('au-cf7-hidden-by-controller'); 268 displayMe = true; 269 } else { 270 // This controlled element's value DOES NOT match what was selected, hide it 271 $controlled.addClass('au-cf7-hidden-by-controller'); 272 } 273 } 274 } 275 276 // Handle nested required fields: TO-DO: filter out nested-requireds? 277 let $required_fields = displayMe ? $controlled.find('.required-when-visible') : $controlled.find('[required]') 278 if ($required_fields.length > 0) { 279 if (displayMe) { 280 // Add the required attributes because they are now visible 281 $required_fields.each(function(rf, required_field) { 282 $(required_field).attr({ 'required': 'required', 'aria-required': 'true' }); 283 }); 284 } else { 285 // Remove the required attributes because they are being hidden 286 $required_fields.each(function(rf, required_field) { 287 let $thisRequiredField = $(required_field); 288 if (!$thisRequiredField.hasClass('required-when-visible')) { 289 $thisRequiredField.addClass('required-when-visible'); 290 } 291 $thisRequiredField.attr('aria-required', 'false').removeAttr('required'); 292 }); 293 } 294 } 295 296 // Hide nested controllers 297 if (!displayMe) { 298 let $nested_controllers = $controlled.find('.au-cf7-controller'); 299 if ($nested_controllers.length) { 300 $nested_controllers.each(function(nc, nested_controller) { 301 let $nested_controller = $(nested_controller); 302 aurise_controllable_fields_cf7.toggleCheckbox($nested_controller, false); 303 aurise_controllable_fields_cf7.toggleControlledFields(aurise_controllable_fields_cf7.getControllerId($nested_controller), $nested_controller.attr('id'), false); 304 }); 305 } 306 } else if (!is_expanded) { 307 is_expanded = true; 308 } 309 }); 310 311 // Update ARIA flag on controller 312 if (is_expanded) { 313 $controller.attr('aria-expanded', 'true'); 314 $controller.trigger('displayed_controlled'); 315 } else if (is_toggle && $empty_controlled !== false && !$('input[name="' + $controller.attr('name') + '"]:checked').length) { 316 // If they're all unchecked and there is a "default" to display, display it 317 $controller.attr('aria-expanded', 'true'); 318 $empty_controlled.removeClass('au-cf7-hidden-by-controller'); 319 $controller.trigger('default_controlled'); 320 } else { 321 $controller.attr('aria-expanded', 'false'); 322 $controller.trigger('hide_controlled'); 323 } 324 $controller.trigger('toggled_controlled'); 325 }, 326 compareLoop: function(acceptedValues, controlled_value, is_toggle, toggle_value) { 327 let matches = 0; 328 if (acceptedValues.indexOf('|') >= 0) { 329 acceptedValues = acceptedValues.split('|'); 330 } else { 331 acceptedValues = [acceptedValues]; 332 } 333 $.each(acceptedValues, function(i, acceptedValue) { 334 if (!is_toggle || (is_toggle && toggle_value)) { 335 // If neither a checkbox nor select field, continue to check for matches 336 // For checkboxes and radios to possibly match at all, it must also be checked 337 matches += aurise_controllable_fields_cf7.compareValues(acceptedValue, controlled_value, is_toggle); 338 } 339 }); 340 return matches; 341 }, 342 compareValues: function(value, controlled_value, is_toggle) { 343 /** 344 * value = the controller's value that it should match to be true, set by the user 345 * controlled_value = the controlled element's value that is being tested, an option 346 */ 347 if (controlled_value === true) { 348 // Treat boolean true as 1 349 controlled_value = '1'; 350 } else if (controlled_value === null || controlled_value === undefined || typeof(controlled_value) != 'string') { 351 // Treat null and undefined values as empty strings 352 controlled_value = ''; 353 } 354 controlled_value = controlled_value.trim(); 355 if (value === true) { 356 // Treat boolean true as 1 357 value = '1'; 358 } else if (value === null || value === undefined || typeof(value) != 'string') { 359 value = ''; 360 } 361 value = value.trim(); 362 363 // Global value checks 364 if (value == '' || value == 'BLANK' || value == 'EMPTY') { 365 // Match only empty/blank 366 if (controlled_value == '') { 367 return 1; 368 } 369 return 0; 370 } else if (value == 'EMPTY_OR_ZERO' || value == 'FALSEY') { 371 // Match falsey values 372 if (!controlled_value || controlled_value == 0 || controlled_value == 'false') { 373 return 1; 374 } 375 return 0; 376 } else if (value == '*') { 377 // Match anything that isn't falsy 378 if (!controlled_value || controlled_value == 0 || controlled_value == 'false') { 379 return 0; 380 } 381 return 1; 382 } 383 384 // If controlled value is a number, do numerical checks 385 if (controlled_value != '' && value != '' && !isNaN(controlled_value)) { 386 controlled_value = parseFloat(controlled_value); 387 388 // If the value isn't a number, then there's probably operations involved 389 if (isNaN(value) || value.startsWith('--')) { 390 // Check for a range, e.g. 10-20, -10 (less than 10) or 50- (greater than 50) 391 if (value.indexOf('-') > -1) { 392 if (value.startsWith('--')) { 393 // If the value is -10, then the controlled value is less than or equal to 10 394 value = value.replace('--', '<='); // Replace with the appropriate operation 395 } else if (value.endsWith('--')) { 396 // If the value is 10-, then the controlled value is greater than or equal to 10 397 value = '>=' + value.replace('--', ''); // Replace with the appropriate operation 398 } else { 399 // Do a "between" comparison 400 let values = value.split('-'); 401 if (values.length > 1) { 402 403 let min = parseFloat(values[0]), 404 max = parseFloat(values[1]); 405 if (min <= controlled_value && controlled_value <= max) { 406 return 1; 407 } 408 } 409 return 0; 410 } 411 } 412 413 // Single numeric comparisons 414 if (value.startsWith('!=')) { 415 // Match everything else except this value 416 if (controlled_value != value.replace('!=', '')) { 417 return 1; 418 } 419 return 0; 420 } else if (value.startsWith('<=')) { 421 if (controlled_value <= parseFloat(value.replace('<=', ''))) { 422 return 1; 423 } 424 return 0; 425 } else if (value.startsWith('<')) { 426 if (controlled_value < parseFloat(value.replace('<', ''))) { 427 return 1; 428 } 429 return 0; 430 } else if (value.startsWith('>=')) { 431 if (controlled_value >= parseFloat(value.replace('>=', ''))) { 432 return 1; 433 } 434 return 0; 435 } else if (value.startsWith('>')) { 436 if (controlled_value > parseFloat(value.replace('>', ''))) { 437 return 1; 438 } 439 return 0; 440 } 441 } else { 442 value = parseFloat(value); 443 } 444 } 445 return controlled_value == value ? 1 : 0; 446 } 447}; 448$(document).ready(aurise_controllable_fields_cf7.init);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.